用灯塔断言保护Solana交易

本文介绍了Lighthouse Assertion Protocol在Solana交易中的应用,旨在保护用户和开发者免受恶意交易的影响。通过添加断言指令,Lighthouse可以确保交易在特定链上状态不符合预期时失败,从而防止用户遭遇如钱包抢劫等恶意活动。文章详细讲解了断言的工作原理、实现方式及其在DeFi和NFT市场中的实际应用。

概述

保护自己和用户免受恶意交易的侵害是在 Web3 中构建时的首要任务。Lighthouse,“断言协议”,是 Solana 交易的开源安全增强工具。通过在交易流程中添加 assertion instructions,Lighthouse 确保如果某些链上状态(如代币余额或预言机价格)与你定义的期望不符,则交易将失败。这可以保护用户避免像钱包耗尽和其他恶意活动等诈骗,这些活动可能会意外地以意想不到的方式操纵账户状态。

在本指南中,你将学习 Lighthouse 断言如何作为交易保护机制工作,能够如何增强安全性,以及如何将它们集成到你的 Solana 工作流中。让我们开始吧!

你将做什么

  • 了解断言交易保护机制如何防止恶意或意外的状态变化
  • 学习 Lighthouse 的核心概念:assertion instructionstransaction flow
  • 将 Lighthouse 指令集成到你的 Solana 交易中
  • 探索 DeFi、NFT 市场或一般 dApp 安全性的潜在现实应用场景

你将需要什么

  • 具备 Solana 开发Solana Web3.js 2.0 的工作知识(Lighthouse 断言适用于任何 Solana 客户端,但本指南以 Web3.js v2.0 为示例)
  • 设置好了的 Solana 开发环境(例如,Node.jsTypeScript 和你喜欢的 IDE)
依赖项 版本
node 23.3.0
@solana/web3.js ^2.0.0
@solana-program/system ^0.6.2
@solana-program/memo ^0.6.1
lighthouse-sdk ^2.0.1
typescript ^5.7.3

理解 Lighthouse 断言协议

在我们开始编码之前,让我们解析一下 Lighthouse 的断言背后的核心概念以及它们为何是 Solana 生态系统中的重要发展。

交易级别的断言

Lighthouse 允许开发者在交易结束时附加 assertion instructions,这些指令有效地作为交易保护,防止交易中的不必要/意外活动。这些断言可以在运行时检查特定条件,例如:

  1. 代币账户余额 - “确保该用户的代币账户在交换后仍有至少 90 个代币。”
  2. 预言机价格 - “在继续之前验证预言机价格是否在我期望的范围内。”
  3. 其他链上条件 - “检查在此交易开始和结束之间,某个账户数据未发生更改。”

如果任何这些条件失败,则整个交易将被回滚——这可以防止部分或恶意执行。

Lighthouse 的工作原理

交易保护机制通过定义一组必须满足的条件来工作,以便交易成功。这些条件被作为 assertion instructions 添加到交易中。断言指令包括一个断言类型和一组定义如何检查条件的参数。当交易被执行时,Lighthouse 程序将评估这些断言。如果所有断言都通过,则交易按预期完成。如果任何一个断言失败,交易将被原子中止,确保没有部分执行发生。

断言将附加到交易指令的末尾:

Lighthouse 断言示例

我们马上将通过一个示例来演示,但从程序的角度,代码可能如下所示:

// ...准备交易消息
return pipe(
  createTransactionMessage({ version: 0 }),
  (msg) => setTransactionMessageFeePayerSigner(signer, msg),
  (msg) => setTransactionMessageLifetimeUsingBlockhash(blockhash, msg),
  (msg) => appendTransactionMessageInstruction(drainerInstruction, msg),
  (msg) => appendTransactionMessageInstruction(
    getAssertAccountInfoInstruction({
      targetAccount: signer,                            // 保护的账户
      assertion: accountInfoAssertion("Lamports", {     // 保护的类型
        value: initialLamports - BASE_TRANSACTION_FEE,  // 预期的值参数
        operator: IntegerOperator.GreaterThanOrEqual,   // 针对预期账户值进行评估的操作
      }),
    }),
    msg,
  )
);

断言类型

断言类型 描述
账户信息 检查 lamports、可执行性、所有者、租赁周期、可写性、签名状态
账户增量 通过比较两个不同账户的状态检查账户数据/信息
账户数据 检查任意账户数据是否满足特定条件
代币铸币 检查有关代币铸币的信息(例如,授权者、供应量、冻结状态等)
代币账户 检查有关代币账户的信息(如余额、委托人、所有者、铸币等)
股权账户 检查有关股权账户的信息(例如,股权授权人、锁定状态、股权等)
可升级加载器账户 检查有关可升级加载器账户的信息(程序数据、升级授权人等)
Merkle 树 spl-account-compression verify_leaf 指令的封装

关于每个断言及其实现的附加详细信息可以在 Lighthouse 文档 中找到。

示例用例

下面是一些真实场景,其中 Lighthouse 交易保护机制可以提高安全性和用户信任:

用例 示例
钱包耗尽诈骗 确保最终的 SOL 或代币余额符合预期。
预言机价格完整性 一个DeFi协议要求价格信息不偏离某个特定阈值。
验证器黑名单 利用 sysvar 槽断言和 getLeaderSchedule 来避免当其领导者是已知的坏行为者时的槽(查看我们关于 Solana MEV 的指南)

关键考虑因素

在集成交易保护机制时,请记住以下几点:

  • 交易大小:断言会增加指令,从而增加整体交易大小。请确保你保持在 Solana 的 1,232 字节限制内。
  • 文档:Lighthouse 是一个相对较新的协议。请确保查看 GitHub 存储库/ 文档 以获取最新的详细信息。
  • 测试:断言可能会导致交易失败——请彻底测试边缘情况,以避免意外的拒绝。
  • 性能:断言会增加少量计算开销——虽然这些开销相对较小,但增加的计算可能会对你的交易落地率产生负面影响,并增加优先费用的成本。请查看我们的 交易优化指南,并确保了解对你的应用程序的影响。
  • 对于大型/复杂操作,Lighthouse 断言可以与 Jito Bundles 一起使用。
  • 对于针对某些历史数据快照的账户数据断言,Lighthouse 提供了 Memory accounts 以存储信息,并对此进行 AssertDelta\* 检查。

演示

让我们通过演示在各种条件下发送的期望交易来展示交易保护机制。我们的目标是让用户签署并执行使用 Solana 的 memo 程序记录消息的简单交易。我们将循环执行在四种条件下的交易:

  1. memo 交易按原样发送,无需断言
  2. memo 交易被替换为具有断言的 SOL 耗尽指令,确保用户的 SOL 余额没有减少
  3. memo 交易发送时有断言,确保用户的 SOL 余额没有减少
  4. memo 交易被替换为 SOL 耗尽指令,没有任何断言

在这个示例中,我们期望看到当指令被包含在交易中时,断言保护会抵御恶意指令,而当未包含断言时,这一恶意指令会执行。

让我们使用简单的 TypeScript 脚本逐步集成它们。

项目设置

创建并初始化一个新的项目文件夹。然后安装所需的依赖项,如 @solana/web3.js(或者你选择的 Solana 库)和可能的 Lighthouse 客户端(如果可用)或其占位符。

mkdir lighthouse-transaction-guards && cd lighthouse-transaction-guards
npm init -y
npm install @solana/web3.js@2 @solana-program/memo @solana-program/system lighthouse-sdk dotenv

如果你打算以 .ts 编写代码,你可能还需要 TypeScript 的开发依赖项:

npm install --save-dev typescript ts-node @types/node

初始化你的 tsconfig:

tsc --init --target ES2020

在你的 package.json 中添加一个启动脚本:

{
  "scripts": {
    "start": "ts-node index.ts"
  }
}

创建你的脚本文件和一个 .env 文件以存储环境变量:

echo > index.ts && echo > .env

前往 .env 文件中设置你的环境变量:

SOLANA_ENDPOINT=https://example.solana.quiknode.pro/012345
SOLANA_WS_ENDPOINT=wss://example.solana.quiknode.pro/012345
WALLET_SECRET=[0,0,0....YOUR_SECRET_KEY....0,0,0]

确保从你的 QuickNode 仪表板中获取你的 Solana Devnet 端点(HTTPS 和 WSS)并替换上述占位符。如果你还没有,可以在 这里 免费创建一个。或者,如果你更喜欢本地运行,可以查看我们关于本地 Solana 开发的指南,这里

确保将你的钱包秘密作为数字数组添加。如果你没有,可以使用 Solana CLI 中的 solana-keygen new 命令在终端生成一个 Solana 密钥。

确保你的钱包中有一些开发网络 SOL 以便运行测试。你可以在 QuickNode Faucet 上获取一些。

导入

在名为 index.ts 的文件中,导入你的 Solana 和 Lighthouse 依赖项:

import {
    appendTransactionMessageInstruction,
    createSolanaRpc,
    createTransactionMessage,
    setTransactionMessageLifetimeUsingBlockhash,
    SolanaRpcApi,
    pipe,
    Address,
    generateKeyPairSigner,
    Blockhash,
    lamports,
    IInstruction,
    createKeyPairSignerFromBytes,
    sendAndConfirmTransactionFactory,
    Lamports,
    signTransactionMessageWithSigners,
    getSignatureFromTransaction,
    createSolanaRpcSubscriptions,
    RpcSubscriptions,
    SolanaRpcSubscriptionsApi,
    KeyPairSigner,
    setTransactionMessageFeePayerSigner,
    isSolanaError,
    type Rpc,
} from "@solana/web3.js";
import { getTransferSolInstruction } from "@solana-program/system";
import { getAddMemoInstruction } from "@solana-program/memo";
import {
    accountInfoAssertion,
    getAssertAccountInfoInstruction,
    IntegerOperator,
    isLighthouseError,
    LIGHTHOUSE_ERROR__ASSERTION_FAILED
} from "lighthouse-sdk";
import { config } from "dotenv";
config();

const BASE_TRANSACTION_FEE = lamports(5000n);

如果你使用了 Solana Web3.js 2.0,你会知道有许多类型保护以确保我们在开发中捕获错误,而不是在将交易发送到验证者网络之后捕获。因此我们在这里使用了许多类型和帮助函数来构建和发送我们的交易(包括来自 Lighthouse SDK 的一些函数,这些函数与 Web3.js2 完全兼容)。

我们还定义了 BASE_TRANSACTION_FEE,这将帮助我们检查交易后的 SOL 余额是否符合预期。

助手函数

环境验证

添加以下函数以确保我们的环境变量已设置并可用:

function getEnvVars(): {
    walletSecret: string;
    endpoint: string;
    wsEndpoint: string;
} {
    const walletSecret = process.env.WALLET_SECRET;
    const endpoint = process.env.SOLANA_ENDPOINT;
    const wsEndpoint = process.env.SOLANA_WS_ENDPOINT;
    if (!walletSecret) {
        throw new Error("WALLET_SECRET is required");
    }
    if (!endpoint) {
        throw new Error("RPC_ENDPOINT is required");
    }
    if (!wsEndpoint) {
        throw new Error("WS_ENDPOINT is required");
    }
    return { walletSecret, endpoint, wsEndpoint };
}

RPC 连接

让我们创建一个辅助函数以建立与网络的连接。将以下代码添加到你的脚本中:

function createRpcConnection(
    endpoint: string,
    wsEndpoint: string,
): {
    rpc: Rpc<SolanaRpcApi>;
    rpcSubscriptions: RpcSubscriptions<SolanaRpcSubscriptionsApi>;
} {
    try {
        const rpc = createSolanaRpc(endpoint);
        const rpcSubscriptions = createSolanaRpcSubscriptions(wsEndpoint);
        return { rpc, rpcSubscriptions };
    } catch (error) {
        throw new Error("Failed to create Solana RPC connection");
    }
}

创建交易

生成交易消息

由于我们将运行多个交易,在各种情况下,让我们创建一个辅助函数,以根据预期条件生成交易消息。将以下代码添加到你的代码中:

interface GenerateMessageParams {
  authority: Address;
  attack: boolean;
  blockhash: Readonly<{
    blockhash: Blockhash;
    lastValidBlockHeight: bigint;
  }>;
  intendedInstruction: IInstruction;
  attackInstruction: IInstruction;
  includeGuard: boolean;
  initialLamports: Lamports;
  signer: KeyPairSigner;
}

function generateTransactionMessage({
  authority,
  blockhash,
  attack,
  attackInstruction,
  intendedInstruction,
  includeGuard,
  initialLamports,
  signer,
}: GenerateMessageParams) {
  const targetInstruction = attack ? attackInstruction : intendedInstruction;
  return pipe(
    createTransactionMessage({ version: 0 }),
    (msg) => setTransactionMessageFeePayerSigner(signer, msg),
    (msg) => setTransactionMessageLifetimeUsingBlockhash(blockhash, msg),
    (msg) => appendTransactionMessageInstruction(targetInstruction, msg),
    (msg) =>
      includeGuard
        ? appendTransactionMessageInstruction(
            getAssertAccountInfoInstruction({
              targetAccount: authority,
              assertion: accountInfoAssertion("Lamports", {
                value: initialLamports - BASE_TRANSACTION_FEE,
                operator: IntegerOperator.GreaterThanOrEqual,
              }),
            }),
            msg,
          )
        : msg,
  );
}

此函数使用标准的 pipe 组合来创建我们的消息,并在参数中包含几个开关。我们包含一个 attack 布尔值,用于决定是使用“攻击”指令还是“预期”指令,并且我们包含一个 includeGuard 布尔值,用于决定是否将 Lighthouse 指令保护附加到消息中。

在这里,我们可以看到添加 Lighthouse 断言指令是多么简单:

断言指令

    getAssertAccountInfoInstruction({
      targetAccount: authority,
      assertion: accountInfoAssertion("Lamports", {
        value: initialLamports - BASE_TRANSACTION_FEE,
        operator: IntegerOperator.GreaterThanOrEqual,
      })

我们定义了要保护的目标账户,在这种情况下是我们的付款人 authority。我们指明我们希望运行一个 AccountInfo 断言,明确查看 lamports 字段。我们确定我们希望我们的 lamports 的值大于或等于我们的初始 lamports(减去交易费用)。就这么简单!有关更多断言和操作,请查看 Lighthouse 文档

交易执行

现在,让我们把所有这些部分组合在一起。将以下 execute 函数添加到代码中:

async function execute(attack: boolean, includeGuard: boolean) {
  // 1. 设置环境和签名者
  const { walletSecret, endpoint, wsEndpoint } = getEnvVars();
  const keypairBytes = new Uint8Array(JSON.parse(walletSecret));
  const signer = await createKeyPairSignerFromBytes(keypairBytes);
  const attacker = await generateKeyPairSigner();

  // 2. 建立 RPC 连接
  const { rpc, rpcSubscriptions } = createRpcConnection(endpoint, wsEndpoint);
  const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({
    rpc,
    rpcSubscriptions,
  });

  // 3. 创建场景指令
  const { value: initialLamports } = await rpc
    .getBalance(signer.address)
    .send();

  const attackInstruction = getTransferSolInstruction({
    source: signer,
    destination: attacker.address,
    amount: initialLamports - BASE_TRANSACTION_FEE,
  });
  const intendedInstruction = getAddMemoInstruction({
    memo: "Just a safe instruction.",
  });

  // 4. 获取最新的区块哈希
  const { value: blockhash } = await rpc.getLatestBlockhash().send();

  // 5. 生成交易消息
  const transactionMessage = generateTransactionMessage({
    authority: signer.address,
    blockhash,
    attackInstruction,
    intendedInstruction,
    attack,
    includeGuard,
    initialLamports,
    signer,
  });

  // 6. 签名交易
  const signedTransaction =
    await signTransactionMessageWithSigners(transactionMessage);
  const signature = getSignatureFromTransaction(signedTransaction);

  // 7. 发送并确认交易,处理成功或失败
  console.log("-------------------------------------------------");
  console.log(
    `模拟详情: ${attack ? "攻击" : "预期"} 指令, ${
      includeGuard ? "带" : "不带"
    } 保护.`,
  );
  try {
    await sendAndConfirmTransaction(signedTransaction, {
      commitment: "confirmed",
    });
    if (!attack) {
      console.log("✅ - 交易成功(预期):");
      console.log("   - 签名: ", signature);
    } else if (attack && !includeGuard) {
      console.log("😭 - 攻击者在没有保护的情况下成功(预期)。");
    } else {
      console.log("❌ - 攻击者在有保护的情况下成功(意外)。");
    }
  } catch (error) {
    if (
      isSolanaError(error) &&
      isLighthouseError(error.cause, transactionMessage) &&
      LIGHTHOUSE_ERROR__ASSERTION_FAILED === error.cause.context.code
    ) {
      console.log("🛡️  - 攻击者指令被阻止!(预期)");
    } else {
      console.log("❌ - 意外错误:", error);
    }
  } finally {
    console.log("-------------------------------------------------\n");
  }
}

这里有点多,所以让我们分解一下:

  1. 首先,我们设置环境,验证/获取我们的环境变量,并定义我们的钱包(我们将使用本地钱包作为付款人/权威,并生成一个随机密钥对作为攻击者)
  2. 接下来,我们设置我们的 RPC 连接,并定义我们的 sendAndConfirmTransaction 函数,利用 Solana 工厂函数。
  3. 然后创建场景指令(攻击与预期的 memo 指令)。
  4. 获取最新的区块哈希。
  5. 使用我们在前一步中定义的 generateTransactionMessage 函数创建交易消息。
  6. 签名交易。
  7. 将交易发送到网络并记录结果。注意,我们包括了一些不同的日志,根据输入参数帮助我们确保结果符合预期。

运行代码

最后,将以下代码添加到脚本底部以运行我们之前讨论的四个场景:

async function main() {
  console.log("运行场景...");
  await execute(false, true);   // 攻击: false, 保护: true
  await execute(false, false);  // 攻击: false, 保护: false
  await execute(true, true);    // 攻击: true,  保护: true
  await execute(true, false);   // 攻击: true,  保护: false
  console.log("场景完成。");
}
main().catch(console.error);

就是这样!你现在应该能够运行代码。在你的终端中运行:

npm run start

你应该会看到类似如下的输出:

运行场景...
模拟详情: 预期指令, 带保护.
✅ - 交易成功(预期):
   - 签名:  33EE...AUHj
-------------------------------------------------

模拟详情: 预期指令, 不带保护.
✅ - 交易成功(预期):
   - 签名:  2u5J...CGGz
-------------------------------------------------

模拟详情: 攻击指令, 带保护.
🛡️  - 攻击者指令被阻止!(预期)
-------------------------------------------------

模拟详情: 攻击指令, 不带保护.
😭 - 攻击者在未保护的情况下成功(预期)。
-------------------------------------------------

场景完成。

太棒了!在这里取得了很棒的成就。根据我们的输出,断言正如我们所期望的那样,第一 2 笔交易能够按预期进行。我们的第三笔交易(攻击)被阻止,因为断言确保最终的 lamports 没有下降——由于攻击者指令试图消耗钱包,断言为真,因此该指令失败。最后,我们的第四个交易确认,如果我们没有这个保护,钱包确实被耗尽 😭。

总结

交易保护机制为用户提供了一种优雅的方式,以保护最终用户免受针对 Solana 的恶意交易。通过将 Lighthouse 断言指令附加到你的交易中,你为用户提供了额外的安全性和信心。

无论你正在构建 DeFi 平台、钱包还是任何其他基于 Solana 的应用,Lighthouse 都有助于确保你签署的内容是真正你所获得的。我们期待看到开发人员如何将这些保护机制融入以保障 Solana 生态系统的安全。

如果你遇到问题或想分享你正在做的事情,请在 DiscordTwitter 上与我们联系。

我们 ❤️ 反馈!

让我们知道 如果你有任何反馈或新的主题请求。我们期待着听到你的声音。

资源

  • 原文链接: quicknode.com/guides/sol...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
QuickNode
QuickNode
江湖只有他的大名,没有他的介绍。