Engine API 是连接 OP-Stack 共识客户端(op-node)与执行引擎(op-geth 或 op-reth)的认证 JSON-RPC 接口,在 Base 上运行。它不是公开的 eth RPC;它运行在本地秘密端口上,受 JWT 认证保护。三个核心方法族是 engine_newPayloadV{n}(验证并导入 payload)、engine_forkchoiceUpdatedV{n}(设置 head/safe/finalized,并可选地启动区块构建)以及 engine_getPayloadV{n}(获取已构建的 payload)。理解 payloadStatus 值(VALID、INVALID、SYNCING、ACCEPTED)和 latestValidHash 对于诊断同步与派生问题至关重要。本文提供可运行示例、用于自行测量的结果表以及故障排查手册。
Base 上的 OP-Stack 双客户端架构
Base 是一个 OP-Stack rollup。其节点软件分为两个协同工作的进程:称为 op-node 的共识/派生层,以及执行引擎,如 op-geth、op-reth 或 op-erigon。op-node 从 L1 数据(提交到以太坊的排序器批次和状态根)派生规范链,而执行引擎维护 EVM 状态、执行交易并对外提供公开的 eth JSON-RPC 方法。
这两个进程通过 Engine API 通信,这是一个独立的 JSON-RPC 接口,运行在本地秘密端口(通常为 8551)上,并受 JWT 认证保护。它有意与公开的 eth RPC 端口(通常为 8545)区分开来,后者是钱包、索引器和 dApp 连接的对象。Engine API 是运维/集群控制平面,而非公共数据平面。
Base OP Stack 节点同步状态与 Engine API 一文介绍了如何从 op-node 侧读取同步状态。本文更深入:它记录了 engine_* 方法合约本身、payloadStatus 语义,以及 op-node 驱动执行引擎所遵循的时序。
- op-node:共识/派生层,从 L1 派生链,驱动执行引擎。
- 执行引擎(op-geth/op-reth/op-erigon):EVM 状态、交易执行、公开 eth RPC。
- Engine API:本地端口(如 8551)上的认证 JSON-RPC,受 JWT 保护。
- 公开 eth RPC:无需认证,服务于 dApp 和钱包;绝不能暴露 engine_* 方法。
为什么 engine_* 方法绝不能公开暴露
Engine API 可以更改规范头、导入任意 payload 并启动区块构建。如果暴露在公共接口上,攻击者可能迫使节点接受无效 payload、重组链或停止区块生产。JWT 要求是硬性安全边界,而非便利措施。
在 Base 上,op-node 和执行引擎通常运行在同一主机或私有网络上。引擎端口应绑定到 localhost 或私有接口,并通过防火墙保护。JWT 密钥是 op-node 与执行引擎之间共享的 32 字节十六进制字符串,通过文件路径传递(例如 --authrpc.jwtsecret)。
对于只需要观察链的集成者,op-node 的公开 admin RPC(opt_ 和 admin_ 命名空间)是应优先使用的只读接口。它暴露同步状态、rollup 配置和派生信息,而不会改变执行引擎。
- Engine API 可以更改规范头并导入 payload——应将其视为控制平面。
- 将引擎端口绑定到 localhost 或私有接口;切勿暴露到互联网。
- 使用 op-node 与执行引擎之间共享的 JWT 认证(--authrpc.jwtsecret)。
- 对于观察,优先使用 op-node admin RPC(opt_/admin_)而非 engine_* 方法。
三个核心 Engine API 方法族及其版本控制
Engine API 定义在以太坊 execution-apis 规范中。驱动 OP-Stack 执行引擎涉及三个方法族:engine_newPayloadV{n}、engine_forkchoiceUpdatedV{n} 和 engine_getPayloadV{n}。版本后缀(V1、V2、V3)与分叉绑定;调用者必须使用与链当前分叉及执行客户端支持集匹配的版本。
engine_newPayloadV{n}(payload, expectedBlobVersionedHashes, parentBeaconBlockRoot) 验证并导入执行 payload。它返回一个 payloadStatus 对象,包含 status、latestValidHash 和 validationError。在 OP-Stack 链上,parentBeaconBlockRoot 参数通常为 null,因为没有信标链;确切的参数集因版本和客户端而异。
engine_forkchoiceUpdatedV{n}(forkchoiceState, payloadAttributes) 设置 head、safe 和 finalized 区块哈希。当存在 payloadAttributes 时,它还会启动区块构建并返回 payloadId。engine_getPayloadV{n}(payloadId) 获取已构建的 payload,调用者随后通过下一次 newPayload 调用将其插入。
版本后缀必须与链当前分叉匹配。这是因分叉和客户端版本而异的文档化行为;请查阅执行客户端的发布说明和execution-apis 规范以获取确切映射。
- engine_newPayloadV{n}:验证并导入执行 payload;返回 payloadStatus。
- engine_forkchoiceUpdatedV{n}:设置 head/safe/finalized;带 payloadAttributes 时启动构建并返回 payloadId。
- engine_getPayloadV{n}:通过 payloadId 获取已构建的 payload,以便通过 newPayload 插入。
- 版本后缀(V1/V2/V3)与分叉绑定;必须匹配链分叉和客户端支持。
解读 payloadStatus:VALID、INVALID、SYNCING、ACCEPTED
每次 engine_newPayloadV{n} 调用都会返回一个 payloadStatus 对象。根据以太坊 execution-apis Engine API 规范,status 字段为 VALID、INVALID、SYNCING 或 ACCEPTED 之一,该对象还携带 latestValidHash 和 validationError。VALID 表示 payload 已通过验证并导入。INVALID 表示 payload 验证失败;latestValidHash 指出最后一个有效祖先,以便调用者回滚到它,validationError 携带原因。SYNCING 表示执行引擎缺少父块,尚无法验证。ACCEPTED 表示 payload 已被接受但未完全验证(通常因为父块未知且引擎处于乐观状态)。
对于 INVALID 结果,latestValidHash 是关键字段。op-node 使用它将分叉选择重置到最后一个有效祖先,丢弃无效分支。如果 latestValidHash 为 null 或零,则引擎无法确定有效祖先,这通常表明存在更深层的状态或配置问题。
validationError 是一个人类可读的字符串,通常包含确切原因:区块哈希错误、状态根无效、gas 限制不匹配或时间戳错误。记录此字段对于排查派生失败至关重要。
- VALID:payload 已通过验证并导入。
- INVALID:payload 验证失败;latestValidHash 指出最后一个有效祖先;validationError 携带原因。
- SYNCING:执行引擎缺少父块;尚无法验证。
- ACCEPTED:payload 已被接受但未完全验证(乐观)。
- INVALID 时 latestValidHash 为 null/零表明存在更深层问题。
op-node 时序:forkchoiceUpdated、getPayload、newPayload
当 op-node 构建新区块时(作为排序器或在派生期间),它遵循特定顺序。首先,它调用 engine_forkchoiceUpdatedV{n},传入当前分叉选择状态和描述要构建区块的 payloadAttributes。执行引擎开始构建并返回 payloadId。其次,op-node 调用 engine_getPayloadV{n}(payloadId) 获取已构建的 payload。第三,op-node 调用 engine_newPayloadV{n} 传入该 payload 以验证并导入。最后,op-node 再次调用 engine_forkchoiceUpdatedV{n},这次传入新的 head 哈希且不带 payloadAttributes,使该区块成为规范区块。
在构建产生 payload 之前调用 engine_getPayloadV{n} 会返回 unknown-payload 错误。payloadId 仅在带 payloadAttributes 的 forkchoiceUpdated 调用启动构建后才有效。此顺序记录在 OP Stack Rollup Node 规范中。
在 Base 上,safe 和 finalized 头的派生方式与 L1 不同。safe 头对应于 op-node 已处理的 L1 派生链,而 finalized 头对应于 L1 最终性。这种与 L1 派生的交互在 Base OP Stack 最终性、safe 与 finalized 区块和 Base OP Stack L1 派生与时间戳中有所介绍。
- 步骤 1:带 payloadAttributes 的 forkchoiceUpdated → payloadId。
- 步骤 2:getPayload(payloadId) → 已构建的 payload。
- 步骤 3:newPayload(payload) → payloadStatus。
- 步骤 4:带新 head、不带 payloadAttributes 的 forkchoiceUpdated → 规范区块。
- 在构建之前调用 getPayload 会返回 unknown-payload 错误。
可运行示例:通过认证端口调用 engine_forkchoiceUpdatedV3
以下 Node.js 示例发出不带 payloadAttributes 的 engine_forkchoiceUpdatedV3 并打印结果状态。它使用 JWT 密钥连接到认证引擎端口(默认 8551)。此示例仅用于说明 schema;请针对您运营的、具有匹配 JWT 认证令牌的节点运行它。
JWT 密钥是一个 32 字节的十六进制字符串。示例从文件路径读取它,生成签名令牌,并发送 JSON-RPC 请求。请将分叉选择状态哈希替换为您自己节点当前 head、safe 和 finalized 区块的值。
const fs = require('fs');
const jwt = require('jsonwebtoken');
const fetch = require('node-fetch');
const JWT_SECRET = fs.readFileSync('/path/to/jwt.hex', 'utf8').trim();
const ENGINE_URL = 'http://127.0.0.1:8551';
function makeToken() {
const payload = { iat: Math.floor(Date.now() / 1000) };
return jwt.sign(payload, Buffer.from(JWT_SECRET, 'hex'), { algorithm: 'HS256' });
}
async function forkchoiceUpdatedV3() {
const token = makeToken();
const body = {
jsonrpc: '2.0',
id: 1,
method: 'engine_forkchoiceUpdatedV3',
params: [
{
headBlockHash: '0x...', // replace with your current head
safeBlockHash: '0x...', // replace with your safe head
finalizedBlockHash: '0x...' // replace with your finalized head
},
null // no payloadAttributes: do not start building
]
};
const res = await fetch(ENGINE_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(body)
});
const json = await res.json();
console.log(JSON.stringify(json, null, 2));
// Expect: { result: { payloadStatus: { status: 'VALID', latestValidHash: '0x...', validationError: null }, payloadId: null } }
}
forkchoiceUpdatedV3().catch(console.error);可运行示例:engine_newPayloadV3 的 JSON 结构与解读
以下示例展示了 engine_newPayloadV3 调用的 JSON 结构,以及如何解读 VALID 与 INVALID 的 payloadStatus。payload 对象包含执行 payload 字段:parentHash、feeRecipient、stateRoot、receiptsRoot、logsBloom、prevRandao、blockNumber、gasLimit、gasUsed、timestamp、extraData、baseFeePerGas、blockHash 和 transactions。第二个参数是 expectedBlobVersionedHashes(数组,在 OP-Stack 上通常为空),第三个是 parentBeaconBlockRoot(在 OP-Stack 上为 null)。
发送请求后,检查 payloadStatus。如果 status 为 VALID,则 payload 已导入。如果 status 为 INVALID,请读取 latestValidHash 以找到最后一个有效祖先,并读取 validationError 了解原因。此示例仅用于说明;请针对您运营的节点运行它。
const fs = require('fs');
const jwt = require('jsonwebtoken');
const fetch = require('node-fetch');
const JWT_SECRET = fs.readFileSync('/path/to/jwt.hex', 'utf8').trim();
const ENGINE_URL = 'http://127.0.0.1:8551';
function makeToken() {
const payload = { iat: Math.floor(Date.now() / 1000) };
return jwt.sign(payload, Buffer.from(JWT_SECRET, 'hex'), { algorithm: 'HS256' });
}
async function newPayloadV3() {
const token = makeToken();
const body = {
jsonrpc: '2.0',
id: 1,
method: 'engine_newPayloadV3',
params: [
{
parentHash: '0x...',
feeRecipient: '0x...',
stateRoot: '0x...',
receiptsRoot: '0x...',
logsBloom: '0x...',
prevRandao: '0x...',
blockNumber: '0x...',
gasLimit: '0x...',
gasUsed: '0x...',
timestamp: '0x...',
extraData: '0x...',
baseFeePerGas: '0x...',
blockHash: '0x...',
transactions: []
},
[], // expectedBlobVersionedHashes
null // parentBeaconBlockRoot (null on OP-Stack)
]
};
const res = await fetch(ENGINE_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(body)
});
const json = await res.json();
console.log(JSON.stringify(json, null, 2));
// If VALID: payload imported.
// If INVALID: inspect latestValidHash and validationError.
}
newPayloadV3().catch(console.error);结果表:在您自己的节点上测量 Engine API 行为
由于 Engine API 行为因分叉、客户端版本和节点配置而异,最可靠的方法是在您自己的节点上进行测量。使用下表记录观察结果。按节点、按方法版本、按运行填写。不要依赖第三方数字;您节点的行为是您部署的基准事实。
针对您的认证端口运行每个 engine_* 方法,并记录返回的状态、INVALID 时的任何 latestValidHash,以及观察到的 head 与 safe 与 finalized。在分叉升级后重复此操作,以确认方法版本仍然匹配。
- Engine 方法版本(例如 V3):记录使用的确切后缀。
- 返回状态(VALID/INVALID/SYNCING/ACCEPTED):记录 payloadStatus.status。
- 任何 INVALID 时的 latestValidHash:记录哈希或 null。
- 观察到的 head 与 safe 与 finalized:记录您节点的三个哈希。
- validationError:如果存在,记录该字符串。
- 客户端版本与分叉:记录 op-geth/op-reth 版本和链分叉。
故障排查:常见 Engine API 失败及其原因
大多数 Engine API 失败可归为几类:认证、版本不匹配、时序错误和 payload 验证失败。认证失败返回 HTTP 401 或关于未授权的 JSON-RPC 错误;检查 op-node 与执行引擎之间的 JWT 密钥文件是否匹配,以及令牌是否过期。
当方法后缀与链当前分叉不匹配时,会发生版本不匹配错误。例如,在需要 V3 的链上调用 engine_newPayloadV2 会返回 method-not-found 或 invalid-params 错误。请查阅执行客户端的发布说明和execution-apis 规范以获取正确版本。
时序错误包括在构建产生 payload 之前调用 engine_getPayloadV{n},这会返回 unknown-payload 错误。确保先调用了带 payloadAttributes 的 forkchoiceUpdated。Payload 验证失败返回 INVALID 并附带 validationError;读取 latestValidHash 以回滚到最后一个有效祖先。
对于同步相关问题,Base OP Stack 节点同步状态与 Engine API 一文介绍了如何读取 unsafe/safe/finalized 头。关于监控,请参阅监控 RPC 端点。
- 401/未授权:JWT 密钥不匹配或令牌过期。
- 方法未找到 / 参数无效:版本后缀与分叉不匹配。
- 未知 payload:在构建产生 payload 之前调用了 getPayload。
- INVALID 并附带 validationError:读取 latestValidHash 和 validationError 以确定原因。
- SYNCING:执行引擎缺少父块;等待同步或检查 L1 派生。
Engine API 的局限性与权衡
Engine API 是认证且版本绑定的。它不是公共索引接口;它属于您运行的节点。JWT 要求意味着您不能简单地将公共 RPC 客户端指向引擎端口。版本后缀必须与链当前分叉匹配,而此映射是因分叉和客户端版本而异的文档化行为。
调用 engine_* 方法是运维/集群操作,而非公共索引操作。对于观察,op-node 的公开 admin RPC(opt_/admin_ 命名空间)是集成者应优先使用的只读接口。它暴露同步状态和派生信息,而不会改变执行引擎。
在 Base 上,safe 和 finalized 头的派生方式与 L1 不同,这影响 forkchoiceUpdated 对它们的解释。这种与 L1 派生的交互是运维人员的关键考虑因素。关于网络特定细节,请参阅 Base。
- 认证:引擎端口上的 JWT;不用于公开暴露。
- 版本绑定:方法后缀必须匹配链分叉;因分叉和客户端而异。
- 运维操作:不是公共索引接口;在您自己的节点上运行。
- 观察:优先使用 op-node admin RPC(opt_/admin_)进行只读访问。
- Base 上的 safe/finalized 派生与 L1 不同;影响 forkchoice 语义。
后续步骤:将 Engine API 意识融入您的 Base 运维
如果您运营 Base 节点,请确保您的 op-node 和执行引擎配置了匹配的 JWT 密钥,并且引擎端口绑定到 localhost 或私有接口。使用结果表为您的节点 Engine API 行为建立基线,并在分叉升级后重新测量。
对于需要在不运行节点的情况下观察 Base 的集成者,Base RPC 端点(RPC Assistant)提供了托管的公共 RPC 接口。关于基础设施规划,请参阅 RPC 定价和 API 服务。OnFinality Learn 中心汇集了关于 OP-Stack 运维、最终性和派生的相关指南。
请记住:Engine API 是控制平面。用它来驱动您自己的节点,而不是服务公共流量。对于公共观察,请使用 eth RPC 或 op-node admin RPC。
- 配置匹配的 JWT 密钥并私下绑定引擎端口。
- 使用结果表为 Engine API 行为建立基线;分叉后重新测量。
- 使用托管 RPC 进行公共观察;请参阅 Base RPC 端点(RPC Assistant)。
- 在 OnFinality Learn 中心探索更多指南。