Logo
新用户订阅 RPC,首月享 6.5 折优惠查看优惠
OnFinality Learn
性能与优化阅读约 14 分钟

通过 RPC 发送 Solana Jito Bundle:小费、原子性与失败诊断

深入解析 Jito bundle 的运作机制、小费指令、slot 定位,以及一套可复现的方法来诊断 bundle 为何成功或未能上链。

TL;DR

Jito bundle 是一组按顺序排列、已完整签名的 Solana 交易,提交给 block engine 而非广播到公共集群,并带有原子性保证:所有成员交易要么在目标 slot 中连续上链,要么全部不上链。小费是 bundle 成员交易中的一笔普通 SOL 转账指令,而非协议层面的费用字段,其金额决定了 bundle 在 block engine 拍卖中的经济吸引力。由于 bundle 面向特定 slot,从发现机会到提交之间的时间预算受 slot 时间限制,因此延迟是可测量的。失败诊断需要区分三类情况:结构性无效的提交(签名错误、blockhash 过期、账户锁冲突)、有效但未被包含的 bundle(小费在拍卖中落败、错过 slot),以及已上链或存在冲突的成员交易。本文提供一个可运行的 Node.js 脚本,组装包含小费的两笔交易 bundle,通过 block engine 路径提交,并使用 getSignatureStatuses 验证每个成员交易的状态和 slot。

Bundle 机制与原子性保证

Jito bundle 是一组按顺序排列、已完整签名的 Solana 交易,提交给 block engine 而非广播到公共集群。只有当每个成员交易都在目标 slot 中连续上链时,block engine 才会包含该 bundle,这意味着只要有一个成员失败,整个提交都会被丢弃。这种原子性保证是 bundle 与标准交易发送方式的根本区别——标准发送中每笔交易相互独立,可能落在不同 slot,也可能完全不上链。

block engine 从 searcher 接收 bundle,并评估其能否被包含在下一个可用 slot 中。与公共集群基于 gossip 的交易传播不同,block engine 作为特权路径运行,可直接访问出块者。这种架构使赢得拍卖的 bundle 能以更低延迟被包含,但也意味着 bundle 的包含取决于第三方拍卖,而非读者自己的 RPC 端点。关于标准交易发送,请参阅 Solana RPC 超时与重试策略

bundle 中的每个成员交易在提交前都必须完整签名。block engine 不会代替 searcher 签名。这意味着 searcher 必须在本地构建、签名并序列化所有交易,然后再将 bundle 发送到 block engine 端点。随后 bundle 要么被接受进入目标 slot,要么被拒绝;不存在部分包含。

  • Bundle = 按顺序排列、已完整签名的交易数组,整个数组具有原子性。
  • block engine 的包含要求所有成员在目标 slot 中连续上链。
  • 只要有一个成员失败,整个 bundle 提交都会被丢弃。
  • 提交目标是 block engine,而非公共集群的 gossip 网络。

小费指令与经济吸引力

小费是 bundle 成员交易中的一笔转账指令,而非费用字段。Solana 没有 bundle 参与的协议级优先权拍卖;小费就是向某个小费账户的一笔普通 SOL 转账,bundle 的经济吸引力就是这笔转账的金额。这一区别对诊断至关重要:“小费太低”和“bundle 被丢弃”是两种不同的诊断。小费低意味着 bundle 结构有效且提交成功,但在拍卖中输给了出价更高的 bundle。bundle 被丢弃则意味着提交本身失败,或从未有资格进入目标 slot。

小费账户集合由 Jito 文档记录,并会随时间变化。读者必须从权威来源读取当前列表,而不是硬编码地址,因为过时的地址会产生一个结构有效但在经济上不可见的 bundle。block engine 不会将向已废弃小费账户的转账识别为有效小费,因此该 bundle 不会进入拍卖考量。在构建 bundle 之前,务必从 Jito 文档或 API 获取当前的小费账户。

小费指令通常放在 bundle 最后一笔交易的最后一个指令位置,不过具体位置是约定而非协议要求。block engine 会扫描 bundle,寻找向已识别小费账户的转账,并用该金额将 bundle 与面向同一 slot 的其他 bundle 进行排名。关于交易费用和计算预算如何与 bundle 成员交互,请参阅 Solana 承诺级别与交易确认

  • 小费 = bundle 成员交易中的普通 SOL 转账指令,而非费用字段。
  • bundle 的经济吸引力 = 小费转账的金额。
  • 小费账户有文档记录且会变化;请从权威来源读取当前列表。
  • 过时的小费地址会产生结构有效但在经济上不可见的 bundle。

Slot 定位与延迟测量

bundle 面向特定 slot,因此延迟是可测量的。从发现机会到提交 bundle 之间的时间预算受 slot 时间限制。如果 bundle 在目标 slot 的出块窗口关闭后才到达,无论小费多高都不会被包含。这意味着读者必须测量自己的提交到 slot 的余量,而不能假设。该余量是 bundle 提交时间与目标 slot 出块时间之间的差值。

要测量这一余量,请在发送 bundle 前立即记录时间戳,并记录目标 slot 编号。slot 过去后,向 block engine 或集群查询 bundle 状态。如果 bundle 未被包含,将提交时间戳与 slot 的预估出块时间进行比较。该测量结果取决于读者的网络路径、地理位置和端点配置。关于延迟测量的更全面讨论,请参阅 Solana RPC 延迟:测量与优化

slot 定位规则也意味着 bundle 提交是与时间的赛跑。能持续成功上链的 searcher 通常会优化到 block engine 的网络路径以及本地交易构建流水线。读者应将提交到 slot 的余量视为可调参数,并在真实条件下测量,而不是依赖理论最小值。

  • bundle 面向特定 slot;到达太晚就不会被包含。
  • 提交到 slot 的余量是提交时间与目标 slot 出块时间之间的间隔。
  • 测量你自己的余量;它取决于网络路径、位置和端点。
  • 优化交易构建和网络路径以缩小余量。

失败类别:无效、未包含与冲突

失败诊断需要严格区分三类情况。结构性无效的提交会以发送调用的错误响应形式出现。这一类包括签名错误、blockhash 过期、账户锁冲突或小费指令格式错误。block engine 会立即拒绝该 bundle,且不会向集群提交任何交易。错误响应通常会包含一个原因代码或消息,指明具体的结构性问题。

有效但未被包含的 bundle 是第二类。此时发送调用成功,但 bundle 没有出现在链上。这是因为小费在拍卖中落败或错过了 slot。bundle 结构有效且在经济上可见,但另一个 bundle 出价更高或到达更早。这一类需要检查 bundle 是否被包含在目标 slot 或任何后续 slot 中。如果未被包含,很可能是小费太低或提交到达太晚。

已上链或存在冲突的 bundle 是第三类。某个成员交易可能已通过其他路径执行,例如标准发送或另一个 bundle。这会改变哪些签名仍能在链上找到。如果某个成员已经上链,bundle 的原子性保证就被打破,其余成员可能被拒绝或被单独包含。读者必须逐一检查每个成员的签名状态,以确定实际发生了什么。关于适用于每个成员的 blockhash 过期规则,请参阅 Solana durable nonce 与 blockhash 过期

  • 结构性无效:签名错误、blockhash 过期、账户锁冲突、小费指令格式错误。
  • 有效但未被包含:发送成功但 bundle 未出现;小费在拍卖中落败或错过 slot。
  • 已上链或存在冲突:某个成员已通过其他路径执行。
  • 逐一检查每个成员的签名状态,以确定实际结果。

用于 Bundle 组装与验证的可运行 Node.js 脚本

以下 Node.js 脚本组装一个包含小费指令的两笔交易 bundle,通过 block engine 路径提交,然后使用 getSignatureStatuses 获取每个成员的状态并检查确认 slot,以验证实际发生了什么。脚本会打印一张表,包含提交的签名、状态、slot,以及所有成员是否落在同一 slot。该脚本是模板;读者必须将占位的小费账户替换为来自 Jito 文档 的当前地址,并配置自己的密钥对和 RPC 端点。

脚本使用 @solana/web3.js 构建和签名交易,使用 axios 向 block engine 发送 HTTP 请求。bundle 以包含 base64 编码交易的 JSON 载荷提交。block engine 端点和认证方式因提供商而异;读者应查阅当前的 Jito bundle 文档 以获取确切的请求格式。验证步骤使用标准 Solana JSON-RPC 的 getSignatureStatuses 方法,该方法记录在 Solana JSON-RPC 参考 中。

const { Connection, Keypair, Transaction, SystemProgram, sendAndConfirmTransaction, PublicKey } = require('@solana/web3.js');
const axios = require('axios');

// Configuration — replace with your own values
const RPC_ENDPOINT = 'https://your-solana-rpc-endpoint';
const BLOCK_ENGINE_URL = 'https://your-block-engine-endpoint/api/v1/bundles';
const TIP_ACCOUNT = new PublicKey('REPLACE_WITH_CURRENT_TIP_ACCOUNT');
const TIP_LAMPORTS = 10000; // 0.00001 SOL — adjust based on current auction

async function main() {
  const connection = new Connection(RPC_ENDPOINT, 'confirmed');
  const payer = Keypair.generate(); // Replace with your funded keypair

  // Fetch a recent blockhash
  const { blockhash } = await connection.getLatestBlockhash('confirmed');

  // Transaction 1: a simple transfer (replace with your actual transaction)
  const tx1 = new Transaction({ recentBlockhash: blockhash, feePayer: payer.publicKey });
  tx1.add(SystemProgram.transfer({
    fromPubkey: payer.publicKey,
    toPubkey: Keypair.generate().publicKey,
    lamports: 1000,
  }));
  tx1.sign(payer);

  // Transaction 2: tip transfer to Jito tip account
  const tx2 = new Transaction({ recentBlockhash: blockhash, feePayer: payer.publicKey });
  tx2.add(SystemProgram.transfer({
    fromPubkey: payer.publicKey,
    toPubkey: TIP_ACCOUNT,
    lamports: TIP_LAMPORTS,
  }));
  tx2.sign(payer);

  // Serialize transactions to base64
  const serializedTx1 = tx1.serialize().toString('base64');
  const serializedTx2 = tx2.serialize().toString('base64');

  // Submit bundle to block engine
  const bundlePayload = {
    jsonrpc: '2.0',
    id: 1,
    method: 'sendBundle',
    params: [[serializedTx1, serializedTx2]],
  };

  let bundleResult;
  try {
    const response = await axios.post(BLOCK_ENGINE_URL, bundlePayload, {
      headers: { 'Content-Type': 'application/json' },
    });
    bundleResult = response.data;
    console.log('Bundle submission response:', JSON.stringify(bundleResult, null, 2));
  } catch (err) {
    console.error('Bundle submission failed:', err.response ? err.response.data : err.message);
    return;
  }

  // Wait for the target slot to pass
  await new Promise((resolve) => setTimeout(resolve, 2000));

  // Verify each member's status using getSignatureStatuses
  const signatures = [tx1.signature.toString(), tx2.signature.toString()];
  const statusResponse = await connection.getSignatureStatuses(signatures, {
    searchTransactionHistory: true,
  });

  // Print results table
  console.log('\n--- Bundle Verification Results ---');
  console.log('Signature | Status | Slot | Same Slot?');
  const slots = [];
  statusResponse.value.forEach((status, index) => {
    const sig = signatures[index];
    const confirmationStatus = status ? status.confirmationStatus : 'not found';
    const slot = status ? status.slot : 'N/A';
    slots.push(slot);
    console.log(`${sig.slice(0, 8)}... | ${confirmationStatus} | ${slot} |`);
  });
  const allSameSlot = slots.every((s) => s === slots[0] && s !== 'N/A');
  console.log(`All members landed in same slot: ${allSameSlot}`);
}

main().catch(console.error);

用于端点特定测量的结果表

由于 bundle 的包含和延迟取决于读者的网络路径、端点和地理位置,本文不提供基准数字。相反,读者应使用结果表测量自己的提交到 slot 余量和 bundle 包含率。在真实条件下多次运行上述脚本,记录提交时间戳、目标 slot、bundle 是否被包含、上链的 slot 以及小费金额。该表将成为调整小费大小和提交时机的基础。

下面展示了一个示例结果表结构。请用你自己的测量数据填充。目标是识别模式:更高的小费是否与包含相关?更短的提交到 slot 余量是否提高包含率?是否存在特定 slot 或一天中的特定时段更容易被包含?这些模式特定于你的设置,无法从第三方基准中推广。

测量时,确保使用一致的 RPC 端点和 block engine 端点。任一变化都可能影响结果。关于端点选择和配置,请参阅 Solana RPC 端点(RPC Assistant)

  • 提交时间戳 | 目标 slot | 是否包含? | 上链 slot | 小费(lamports) | 提交到 slot 余量(毫秒)
  • 至少运行 20 次试验以建立基线。
  • 改变小费大小并测量包含率。
  • 改变提交时机并测量提交到 slot 余量。
  • 为每次试验记录端点和 block engine URL。

与标准 Solana 约束的交互

bundle 中的每个成员交易仍然需要最近的 blockhash,并会按与任何标准 Solana 交易相同的时间表过期。bundle 层不会延长 blockhash 的生命周期。如果某个成员的 blockhash 在 bundle 被包含之前过期,bundle 将变为结构性无效并被拒绝。这意味着读者仍必须管理 blockhash 的新鲜度,要么及时提交,要么使用 durable nonce 延长成员的生命周期。

每个成员仍然消耗费用和计算预算。bundle 不会绕过 Solana 的费用市场或计算限制。每笔交易必须有足够的计算单元并支付基础费用。小费是在这些成本之上的额外转账。searcher 在计算盈利能力时,应计入每个 bundle 的总成本,包括基础费用、计算单元成本和小费。

durable nonce 仍可用于延长成员的生命周期,使 bundle 能够提前准备并在稍后提交而不会因 blockhash 过期而失效。这与 bundle 层是组合关系,而非替代关系。读者应将 bundle 视为叠加在标准可靠性机制之上的额外提交路径,而不是替代品。关于 durable nonce 的详细说明,请参阅 Solana durable nonce 与 blockhash 过期

  • 每个 bundle 成员都需要最近的 blockhash,并按标准时间表过期。
  • 每个成员都消耗费用和计算预算;小费是额外的。
  • durable nonce 可以延长成员的生命周期,并与 bundle 组合使用。
  • bundle 增加了一条提交路径;它们不会取代标准可靠性机制。

局限性与权衡

bundle 的包含取决于第三方 block engine 的拍卖,而非读者的端点。这意味着即使构造完美且小费有竞争力的 bundle,如果另一个出价更高的 bundle 面向同一 slot,也可能不被包含。读者无法仅通过端点选择或配置来保证包含。block engine 的拍卖动态不透明,且可能随时间变化。

本文无法承诺小费的盈利能力。小费是成本,回报取决于 bundle 所捕获机会的价值。searcher 必须根据具体机会和当前拍卖条件计算自己的盈利能力。昨天足够的小费今天可能不够。不存在能保证包含的固定小费金额。

小费账户列表和 API 接口可能随时变化,恕不另行通知。Jito 可能增加或移除小费账户、更改 block engine 端点格式或修改 bundle 提交 API。读者在生产环境中依赖任何行为之前,都应对照当前文档进行验证。本文描述的是撰写之日已记录的行为,但提供商特定行为各不相同且可能变化。关于支持验证中使用的标准 Solana RPC 方法的基础设施,请参阅 Solana RPC 端点(RPC Assistant)API 服务

  • bundle 的包含取决于第三方拍卖,而非你的端点。
  • 无法承诺小费的盈利能力;请根据机会价值自行计算。
  • 小费账户列表和 API 接口可能随时变化,恕不另行通知。
  • 在生产使用前,对照当前文档验证每一项行为。

排查常见 Bundle 失败

当 bundle 失败时,第一步是确定属于哪一类失败。如果发送调用返回错误,则 bundle 结构性无效。检查错误消息以确定具体原因:签名错误意味着交易未正确签名;blockhash 过期意味着 blockhash 在提交前已失效;账户锁冲突意味着另一笔交易锁定了 bundle 中的某个账户;小费指令格式错误意味着向小费账户的转账构造不正确。修复结构性问题后重新提交。

如果发送调用成功但 bundle 没有出现在链上,则 bundle 有效但未被包含。这是最常见的失败类别。对照当前拍卖条件检查小费金额。如果小费偏低,提高后重新提交。检查提交到 slot 的余量;如果 bundle 到达太晚,优化网络路径或更早提交。使用结果表判断限制因素是小费大小还是时机。

如果某个成员交易已通过其他路径上链,bundle 的原子性保证就被打破。使用 getSignatureStatuses 逐一检查每个成员的签名状态。如果一个成员上链而其他没有,说明 bundle 被部分执行,即原子性保证被违反。这可能发生在成员被单独提交,或 block engine 只包含了 bundle 的一部分时。在这种情况下,其余成员可能需要作为新 bundle 或标准交易重新提交。关于标准发送的重试策略,请参阅 Solana RPC 超时与重试策略

  • 发送调用报错 → 结构性无效:检查签名、blockhash、账户锁、小费指令。
  • 发送调用成功但未被包含 → 有效但未包含:检查小费大小和提交到 slot 余量。
  • 成员已上链 → 冲突:用 getSignatureStatuses 逐一检查每个签名。
  • 使用结果表区分小费大小问题和时机问题。

生产环境 Bundle 发送的后续步骤

要从实验走向生产,首先建立可靠的测量流水线。使用一致的配置运行上述脚本,并将结果记录在表中。利用该表调整小费大小和提交时机。建立基线后,引入变化以测试稳健性:不同的 slot、不同的时段、不同的网络条件。目标是了解你的包含率及其影响因素。

接下来,如果你的用例需要提前准备 bundle,请集成 durable nonce。这将 bundle 构建与提交时机解耦,并降低 blockhash 过期的风险。详细指南请参阅 Solana durable nonce 与 blockhash 过期。同时请查阅 Solana 承诺级别与交易确认,确保验证逻辑使用适当的承诺级别。

最后,考虑你的基础设施。bundle 提交受益于到 block engine 的低延迟网络路径。如果你使用标准 RPC 端点进行验证,请确保它支持带 searchTransactionHistory 的 getSignatureStatuses。关于端点选项和定价,请参阅 Solana RPC 端点(RPC Assistant)RPC 定价OnFinality Learn 中心。关于网络特定细节,请参阅 Solana 网络页面

  • 建立测量流水线并将结果记录在表中。
  • 根据你自己的包含率调整小费大小和提交时机。
  • 集成 durable nonce 以提前准备 bundle。
  • 使用适当的承诺级别通过 getSignatureStatuses 进行验证。
  • 优化到 block engine 的网络路径,以降低提交到 slot 的余量。

永远不用担心基础设施

OnFinality 消除了 DevOps 的繁重工作,让您能够更聪明、更快地构建。

开始