以太坊 Bundle 是一个有序数组,包含完全签名的原始交易以及可选的目标区块,提交给区块构建者而非公共内存池。包含性在数组层面是原子的:要么所有交易按顺序连续进入目标区块,要么全部不进入。eth_sendBundle 不属于以太坊 execution-apis 规范;它是构建者特有的方法,因此将其指向标准全节点或通用 RPC 提供商会返回方法未找到错误,而不是提交失败。Bundle 未被包含的三种不同原因是:格式错误(签名、nonce 或 gas 无效)、有效但无利可图(模拟成功但更好的出价胜出)以及端点错误(调用从未到达构建者)。本文涵盖请求结构、提交前模拟工作流、可运行的 Node.js 示例,以及可针对自己的端点运行的诊断方法。
Bundle 机制:原子有序交易数组
Bundle 是一个有序数组,包含完全签名的原始交易以及可选的目标区块,提交给区块构建者而非公共内存池。其核心特性是数组层面的原子性:要么 Bundle 中的每笔交易按顺序连续进入目标区块,要么全部不进入。这正是 Bundle 对回跑和套利模式有用的原因——盈利序列只有在所有环节按顺序执行时才成立。
同样的特性意味着单个格式错误的成员会毒害整个提交。如果一笔交易签名错误、nonce 过期或 gas 不足,构建者无法原子地包含该数组,整个 Bundle 会被丢弃。这与普通交易有结构性差异:普通交易独立评估,可以被替换或丢弃而不影响其他任何东西。
由于 Bundle 成员是预先签名的,替换成员意味着重新签名整个 Bundle。如果 Bundle 的第一笔交易已被另一路径包含,剩余成员要么原子失败,要么在单独提交时在 nonce 上竞争。因此 nonce 管理是首要关注点;关于该问题的读取侧,请参阅使用 eth_getTransactionCount 进行 EVM nonce 管理。
- Bundle = 有序的已签名原始交易数组 + 可选目标区块。
- 原子性:所有成员按顺序连续进入目标区块,或全部不进入。
- 一个格式错误的成员会使整个 Bundle 失效。
- 成员预先签名意味着替换需要重新签名整个 Bundle。
为什么 Bundle 端点不是普通 RPC 端点
eth_sendBundle 根本不属于以太坊 execution-apis 规范。它是构建者特有的方法,由 Flashbots 在 docs.flashbots.net 上与 eth_callBundle 和 eth_cancelBundle 一起记录。将 Bundle 调用指向标准全节点或通用 RPC 提供商会返回方法未找到错误,而不是提交失败。
可用性和确切的响应结构因提供商而异。一些构建者只暴露 eth_sendBundle;其他构建者增加了模拟或取消方法。将方法集视为已记录/因提供商而异,并在围绕其构建提交管道之前,针对你实际打算使用的端点进行验证。
对于普通的读写流量,标准以太坊端点仍然是正确的工具。OnFinality 的以太坊网络页面和 RPC 端点指南描述了常规 JSON-RPC 接口,eth_sendRawTransaction、eth_getTransactionCount 和区块读取都属于该接口。
- eth_sendBundle 是构建者特有的,不属于 execution-apis。
- 错误的端点返回方法未找到,而不是提交失败。
- 方法可用性和响应结构已记录/因提供商而异。
请求字段及其各自约束
Flashbots 记录的 Bundle 请求结构包含三个字段。已签名交易数组是有序的原始交易列表。目标区块号或哈希约束资格:Bundle 仅对其所指向的区块有效,因此过期的目标会静默地永不包含。可选的 reverting-transaction-hashes 字段允许搜索者声明哪些成员可以回滚而不使 Bundle 失效。
每笔原始交易内部的交易字段遵循以太坊 execution-apis 规范,包括决定 Bundle 必须出价的有效优先费用的 EIP-1559 费用字段。EIP-1559 定义了 maxFeePerGas、maxPriorityFeePerGas 和基础费用销毁,它们共同决定了构建者从 Bundle 中实际获得什么。
一个常见错误是将目标区块视为建议性的。它不是。如果你为区块 N 构建 Bundle 并在 N 已产出后提交,无论它本来多么有利可图,该 Bundle 都不再有效。重新定位目标意味着重建并重新签名。
- 已签名交易数组:有序、完全签名的原始交易。
- 目标区块:编号或哈希;资格仅限于该区块。
- reverting-transaction-hashes:允许回滚而不使 Bundle 失效的成员。
- 费用字段遵循 EIP-1559,并决定有效优先费用出价。
提交前使用 eth_callBundle 模拟
eth_callBundle 针对给定区块的当前状态预览 Bundle,返回实际 gas 使用量和 coinbase 转账。这是在为一个区块消耗之前了解 Bundle 是否会回滚的唯一方法。返回 JSON-RPC 错误的模拟指向格式错误的 Bundle;成功但显示低 coinbase 转账的模拟指向有效但无利可图的 Bundle。
针对你打算定位的同一区块运行模拟。针对不同区块状态进行模拟可能产生不反映构建者将看到的结果。如果模拟成功,记录 gas 和 coinbase 值,以便与提交后实际发生的情况进行比较。
模拟不是包含的保证。它告诉你 Bundle 格式良好且针对你模拟的状态可执行;它不告诉你是否有竞争的搜索者会为同一机会出价更高。
- eth_callBundle 返回实际 gas 和 coinbase 转账。
- 针对你打算定位的同一区块进行模拟。
- 模拟成功是包含的必要条件,但不是充分条件。
可运行的 Node.js 示例:构建、模拟、提交、验证
下面的脚本构建一个 Bundle 请求,调用 eth_callBundle 进行模拟,使用 eth_sendBundle 提交,然后通过获取目标区块并检查 Bundle 的交易哈希是否按预期顺序出现来验证包含。它使用现代 Node.js 中可用的全局 fetch API,并假设你已经签名了原始交易。
将端点 URL 替换为你打算使用的构建者端点。该脚本不假设任何特定提供商;如果端点不是构建者端点,它会暴露方法未找到错误,这本身就是一个有用的诊断。
const ENDPOINT = process.env.BUNDLE_RPC_URL; // builder endpoint
const TARGET_BLOCK = process.env.TARGET_BLOCK; // e.g. "0x112a880"
const RAW_TXS = JSON.parse(process.env.RAW_TXS); // array of 0x-prefixed signed raw txs
async function rpc(method, params) {
const res = await fetch(ENDPOINT, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params })
});
const json = await res.json();
if (json.error) throw new Error(method + " error: " + JSON.stringify(json.error));
return json.result;
}
async function main() {
const bundle = { txs: RAW_TXS, blockNumber: TARGET_BLOCK };
// 1. Simulate against the target block
const sim = await rpc("eth_callBundle", [bundle, TARGET_BLOCK]);
console.log("simulation:", JSON.stringify(sim, null, 2));
// 2. Submit
const submitted = await rpc("eth_sendBundle", [bundle]);
console.log("submitted bundle hash:", submitted.bundleHash);
// 3. Verify inclusion in the target block
const block = await rpc("eth_getBlockByNumber", [TARGET_BLOCK, false]);
if (!block) {
console.log("target block not yet produced");
return;
}
const included = block.transactions;
const expected = RAW_TXS.map((raw) => {
// derive hash from raw tx using your signing library, e.g. ethers.Transaction.from(raw).hash
return raw;
});
console.log("block tx count:", included.length);
console.log("bundle members present:", expected.every((h) => included.includes(h)));
}
main().catch((e) => { console.error(e.message); process.exit(1); });结果表:针对自己的端点进行测量
由于 Bundle 端点及其方法集因提供商而异,唯一可靠的描述方式是自行测量。为你打算使用的每个端点填写下表。不要假设来自其他提供商的数值可以迁移。
针对每个端点运行上面的脚本并记录结果。结果模式告诉你该端点是否根本就是构建者端点、是否支持模拟,以及你的 Bundle 是否到达包含阶段。
- 端点 URL:你测试的构建者端点。
- eth_callBundle 支持:是 / 否 / 方法未找到。
- eth_sendBundle 支持:是 / 否 / 方法未找到。
- 模拟结果:成功并带有 gas 和 coinbase 值,或错误。
- 提交响应:返回 Bundle 哈希,或错误。
- 在目标区块中观察到包含:是 / 否 / 区块尚未产出。
三类失败保持分离
格式错误的 Bundle 是无效的:签名、nonce 或 gas 错误使 Bundle 不可用,这表现为来自 eth_callBundle 的 JSON-RPC 错误。修复方法是重新签名或重建有问题的成员并重新模拟。这一类是确定性的且可复现的。
有效但无利可图的 Bundle 模拟成功,但构建者找到了更好的出价。这表现为提交返回了 Bundle 哈希,然后根本没有出现在目标区块中。没有错误可捕获;Bundle 是正确的,但在经济上输了。修复方法是改进出价或机会,而不是调试请求。
端点错误失败发生在调用从未到达构建者时,表现为方法未找到或意外的响应结构。这一类会产生 eth_sendBundle 的经典空结果:一个格式正确的 Bundle 发送到未实现该方法的端点。在解释任何响应之前验证端点。
- 格式错误:来自 eth_callBundle 的 JSON-RPC 错误;通过重新签名修复。
- 有效但无利可图:返回 Bundle 哈希,无包含;通过改进出价修复。
- 端点错误:方法未找到或意外结构;通过使用构建者端点修复。
Nonce 与替换的交互
由于 Bundle 成员是预先签名的,替换成员意味着重新签名整个 Bundle。如果 Bundle 的第一笔交易已被另一路径包含,剩余成员要么原子失败,要么在单独提交时在 nonce 上竞争。这是生产中最容易出问题的交互。
nonce 管理的读取侧在使用 eth_getTransactionCount 进行 EVM nonce 管理中介绍,普通交易的替换语义在eth_sendRawTransaction 替换与 underpriced 错误中介绍。Bundle 不继承这些替换规则;它们是重建的,不是替换的。
如果你需要在重建之前检查已经待处理的内容,以太坊交易池与 txpool 命名空间页面描述了检查接口。包含之后,eth_getBlockReceipts:一次调用批量获取收据是在一个请求中确认每个成员状态的便捷方式。
- 替换 Bundle 成员需要重新签名整个 Bundle。
- 部分包含的 Bundle 会使剩余成员原子失败或在 nonce 上竞争。
- 与普通交易不同,Bundle 是重建的,不是替换的。
故障排查:诊断 eth_sendBundle 空结果
eth_sendBundle 返回空结果是公共论坛中最常见的症状,它对应三类失败之一。按顺序排查:首先确认端点实现了该方法,然后确认 Bundle 可以模拟,最后确认目标区块仍然是最新的。
如果 eth_callBundle 返回 JSON-RPC 错误,则 Bundle 格式错误。如果它成功但提交返回了 Bundle 哈希且没有后续包含,则 Bundle 有效但在经济上输了。如果提交本身返回方法未找到或意外结构,则端点不是构建者端点。
过期的目标区块是一种静默失败:Bundle 格式良好且可能模拟成功,但它对其所指向的区块无效。在将未包含解释为经济损失之前,始终对照当前链头重新检查目标区块号。
- 在解释任何响应之前,确认端点实现了 eth_sendBundle。
- 运行 eth_callBundle;JSON-RPC 错误意味着格式错误。
- 返回 Bundle 哈希但无包含意味着有效但无利可图。
- 方法未找到意味着端点错误。
- 过期的目标区块是静默的、非经济性的失败。
局限性与权衡
Bundle 包含是构建者的商业决策,而非协议保证。本文中的任何内容都不是关于任何提供商包含率的声明。模拟成功的 Bundle 仍可能永远不会被包含,并且没有链上机制强制构建者接受它。
定位未最终确定的区块意味着你自己的重组和确认策略仍然适用。包含在后来被重组的区块中的 Bundle 不是已结算的结果。在你的确认阈值达到之前,将包含视为临时的。
构建者端点及其方法集会无通知地变化。今天记录的方法可能被重命名、移除或补充。构建你的集成,使方法未找到或意外响应结构被显式处理,而不是被假设掉。
- 包含是构建者的商业决策,而非协议保证。
- 本文不对任何提供商的包含率作出声明。
- 未最终确定的目标区块意味着你的重组策略仍然适用。
- 构建者端点和方法集会无通知地变化。
下一步:构建 Bundle 管道
首先使用上面的结果表测量你选择的端点。确认 eth_callBundle 和 eth_sendBundle 都受支持,并记录你观察到的响应结构。这在你构建任何东西之前为你提供了基线。
然后按失败类别分离日志。记录格式错误的 Bundle 及其模拟错误,记录有效但无利可图的 Bundle 及其模拟的 coinbase 值,并单独记录端点错误失败。这使三类在生产中可区分。
对于周边基础设施,OnFinality Learn 中心收集了相邻的交易、nonce 和收据页面,API 服务和 RPC 定价页面描述了你在任何构建者端点之外仍然需要的常规端点接口。
- 在构建之前测量你的端点。
- 按失败类别记录日志:格式错误、无利可图、端点错误。
- 在任何构建者端点之外保留一个标准以太坊端点。