利用 Lighthouse 断言防护保护 Solana 交易

本文介绍了如何利用 Lighthouse 断言协议保护 Solana 交易免受恶意操作。

概述

在 Web3 中构建时,保护自己和用户免受恶意交易侵害是首要任务。Lighthouse,即“断言协议”,是一个开源的 Solana 交易安全增强工具。通过在交易流程中添加断言指令,Lighthouse 确保在特定链上状态(如 Token 余额或预言机价格数据)与预期不符时,交易会失败。这可以保护用户免受钱包耗尽等诈骗,以及交易可能以意外方式操纵账户状态等恶意活动的侵害。

在本指南中,你将了解 Lighthouse 断言如何作为交易守卫工作,它们能做什么来增强安全性,以及如何将它们集成到你的 Solana 工作流中。让我们开始吧!

你将做什么

  • 理解断言交易守卫如何防止恶意或意外的状态变化
  • 学习核心 Lighthouse 概念:断言指令交易流程
  • 将 Lighthouse 指令集成到你的 Solana 交易中
  • 探索 DeFi、NFT 市场或通用 dApp 安全的潜在真实用例

你需要什么

  • 具备 Solana 开发Solana Kit 的工作知识(Lighthouse 断言适用于任何 Solana 客户端,但本指南使用 Solana Kit 作为示例)
  • 设置好的 Solana 开发环境(例如,Node.jsTypeScript、你喜欢的 IDE)
依赖 版本
node 23.3.0
@solana/kit ^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 允许开发者在交易末尾附加断言指令,这些指令有效地作为交易守卫,防止交易中出现不需要或意外的活动。这些断言可以在运行时检查特定条件,例如:

  1. Token 账户余额 — “确保该用户的 Token 账户在交换后仍有至少 90 个 Token。”
  2. 预言机价格 — “验证预言机价格在预期范围内后再继续。”
  3. 其他链上条件 — “检查某个账户数据在交易开始和结束之间没有改变。”

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

Lighthouse 如何工作

交易守卫通过定义一组必须满足的条件来工作,以使交易成功。这些条件作为断言指令添加到交易中。断言指令包括一个断言类型和一组参数,这些参数定义了如何检查条件。当交易执行时,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,
  )
);

断言类型

断言类型 描述
Account Info 检查 lamports、可执行性、所有者、租约纪元、可写性、签名者状态
Account Delta 通过比较两个不同账户的状态来检查账户数据/信息
Account Data 检查任意账户数据是否符合特定条件
Token Mint 检查 Token 铸币的信息(例如,权限、供应量、冻结状态等)
Token Account 检查 Token 账户的信息(例如,余额、委托人、所有者、铸币等)
Stake Account 检查质押账户的信息(例如,质押权限、锁定、质押等)
Upgradeable Loader Account 检查可升级加载器账户的信息(例如,程序数据、升级权限等)
Merkle Tree 围绕 spl-account-compression 的 verify_leaf 指令的封装

每个断言的更多细节及其实现可以在 Lighthouse 文档 中找到。

示例用例

以下是一些 Lighthouse 交易守卫可以提高安全性和用户信任的真实场景:

用例 示例
钱包耗尽诈骗 确保最终的 SOL 或 Token 余额与预期匹配。
预言机价格完整性 DeFi 协议要求价格馈送不能偏离超过某个阈值。
验证器黑名单 利用 sysvar 槽位断言和 getLeaderSchedule 以避免领导者是已知恶意行为者的槽位(查看我们关于 Solana MEV 的指南)

关键考虑因素

在集成交易守卫时,请记住以下几点:

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

演示

让我们通过在不同条件下演示一个期望的交易来展示交易守卫。我们的目标是让用户签署并执行一个简单的交易,使用 Solana 的 memo 程序记录一条消息。我们将在四种条件下运行该交易:

  1. 备忘录交易原样发送,没有断言
  2. 备忘录交易被替换为 SOL 耗尽指令,并带有一个断言,即用户的 SOL 余额没有减少
  3. 备忘录交易发送时带有一个断言,即用户的 SOL 余额没有减少
  4. 备忘录交易被替换为 SOL 耗尽指令,没有断言

在这个例子中,我们期望在交易中包含断言守卫时,它能抵御恶意指令;而当断言守卫未包含时,恶意指令能成功执行。

让我们进入一个逐步的方法,将它们集成到一个简单的 TypeScript 脚本中。

项目设置

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

mkdir lighthouse-transaction-guards && cd lighthouse-transaction-guards
npm init -y
npm install @solana/kit @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-keygen new 命令配合 Solana CLI 生成一个。

确保你的钱包中有一些 devnet 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/kit";
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 Kit,你会知道有很多类型守卫可以确保我们在开发阶段就捕获错误,而不是发送交易到验证器网络之后。这就是我们在这里所做的——有很多类型和辅助函数,我们将用它们来构建和发送交易(包括一些来自 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 指令守卫。IInstruction 接口定义了一个针对特定程序的指令(src)。

在这里,我们可以看到添加 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. 获取最新 blockhash
  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 连接,并使用 Solana 工厂函数定义 sendAndConfirmTransaction 函数。
  3. 然后,我们创建场景指令(攻击和预期的 memo 指令)。
  4. 获取最新的 blockhash
  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
-------------------------------------------------

模拟详情:攻击指令,带守卫。
🛡️  - 攻击指令被阻止!(预期)
-------------------------------------------------

模拟详情:攻击指令,不带守卫。
😭 - 攻击者在没有守卫的情况下成功(预期)。
-------------------------------------------------

场景完成。

太棒了!工作得很好。根据我们的输出,断言完全按照预期执行。前两笔交易成功执行。第三笔交易(攻击)被阻止,因为断言确保最终的 lamports 没有减少——由于攻击指令试图耗尽钱包,断言为真,指令失败。最后,第四笔交易确认,如果没有这个保障措施,我们的钱包确实会被耗尽 😭。

总结

交易守卫提供了一种优雅的方式来保护用户免受 Solana 上恶意交易的侵害。通过在你的交易中附加 Lighthouse 断言指令,你可以为用户提供额外的安全层和信心。

无论你在构建 DeFi 平台、钱包还是其他任何基于 Solana 的应用程序,Lighthouse 都能帮助你确保签署的就是你得到的。我们很高兴看到开发者如何集成这些守卫来保护 Solana 生态系统。

如果你遇到问题,或者只是想分享你正在做的事情,请通过 DiscordTwitter 联系我们。

资源

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

0 条评论

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