本文介绍了如何利用 Lighthouse 断言协议保护 Solana 交易免受恶意操作。
在 Web3 中构建时,保护自己和用户免受恶意交易侵害是首要任务。Lighthouse,即“断言协议”,是一个开源的 Solana 交易安全增强工具。通过在交易流程中添加断言指令,Lighthouse 确保在特定链上状态(如 Token 余额或预言机价格数据)与预期不符时,交易会失败。这可以保护用户免受钱包耗尽等诈骗,以及交易可能以意外方式操纵账户状态等恶意活动的侵害。
在本指南中,你将了解 Lighthouse 断言如何作为交易守卫工作,它们能做什么来增强安全性,以及如何将它们集成到你的 Solana 工作流中。让我们开始吧!
| 依赖 | 版本 |
|---|---|
| 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 断言背后的核心概念,以及为什么它们对 Solana 生态系统来说是重要的发展。
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 的 memo 程序记录一条消息。我们将在四种条件下运行该交易:
在这个例子中,我们期望在交易中包含断言守卫时,它能抵御恶意指令;而当断言守卫未包含时,恶意指令能成功执行。
让我们进入一个逐步的方法,将它们集成到一个简单的 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 };
}
让我们创建一个辅助函数来建立到网络的连接。将以下内容添加到你的脚本:
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");
}
}
这里内容有点多,让我们分解一下:
sendAndConfirmTransaction 函数。generateTransactionMessage 函数创建交易消息最后,将以下代码添加到脚本底部,以运行我们之前讨论的四个场景:
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 生态系统。
如果你遇到问题,或者只是想分享你正在做的事情,请通过 Discord 或 Twitter 联系我们。
- 原文链接: quicknode.com/guides/sol...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!
作者暂未设置收款二维码