FHEVM Listener Block Computer 交易类型编码规范:从 EIP-2718 到 Polygon PIP-74 的多链区块根校验实战解析
【免费下载链接】fhevmFHEVM, a full-stack framework for integrating Fully Homomorphic Encryption (FHE) with blockchain applications项目地址: https://gitcode.com/GitHub_Trending/fh/fhevm
本文以 fhevm 仓库中 Block Computer — Transaction Type Encoding Specification 为核心,系统讲解 listener 组件中
EvmBlockComputer如何通过逐笔 RLP 重编码交易来复算区块的transactionsRoot,覆盖标准 Ethereum 类型 0–4、Optimism/Base 类型 126、Arbitrum 类型 100–106 以及 Polygon Bor 类型 127(PIP-74 state sync)的完整编码格式、字段映射与源码实现,并给出跳过机制、收据编码与测试验证的完整方案。读完本文,你将掌握跨链区块完整性校验中"未知交易类型"这一核心难题的工程化解法,并能直接在 evm_block_computer.rs 中定位每一处关键实现。
1. 背景与问题:Block Computer 为什么要逐类型重编码交易
1.1 区块完整性验证的三重根校验
fhevm 的 listener 组件负责监听 EVM 兼容链(Ethereum、Optimism、Arbitrum、Base、Polygon 等)上的区块与事件。为了保证从 RPC 拉取的数据没有被篡改,EvmBlockComputer 对每个区块执行三项独立校验:
- 交易根(
transactionsRoot)校验:将区块内每一笔交易按规范 RLP 编码后,通过ordered_trie_root构造交易 trie 根,与区块头中记录的transactions_root比对; - 收据根(
receiptsRoot)校验:对每笔交易的收据同样 RLP 编码后构造 trie 根,与区块头receipts_root比对; - 区块哈希(
blockHash)校验:将 RPC 返回的 header 各字段重建为 alloy 的 consensusHeader,调用hash_slow()与 RPC 返回的hash比对。
其中,交易根校验要求精确重现每一笔交易的字节级编码,这正是整个系统的难点所在——因为交易编码格式随链、随 EIP、随硬分叉而不同。
1.2 当前实现的问题
从 evm_block_computer.rs 的safe_encode_transaction可以看出,实现只覆盖了三类情况:
- 标准 Ethereum 类型 0–4:由 alloy 原生处理(
AnyTxEnvelope::Ethereum+encoded_2718()); - Optimism/Base 存款交易类型 126(0x7E):自定义
encode_deposit_transaction; - Arbitrum 内部交易类型 106(0x6A):自定义
encode_arbitrum_internal_transaction。
其余任何未知类型都会落入_分支,返回BlockVerificationError::UnsupportedTransactionType { tx_type, index }。正如规范文档所指出,截至 Polygon 主网区块 85,523,136,类型 127(0x7F,即 PIP-74 引入的 state sync 交易)就会触发该失败,从而阻塞 cursor 的正常处理。源码中的_分支注释也印证了这一点(evm_block_computer.rs):
// TODO: Arbitrum specific encoding for transactions. // 100 => encode_arbitrum_deposit_transaction(unknown, index), // 101 => encode_arbitrum_unsigned_transaction(unknown, from, index), // 102 => encode_arbitrum_contract_transaction(unknown, index), // 104 => encode_arbitrum_retry_transaction(unknown, from, index), // 105 => encode_arbitrum_submit_retryable_transaction(unknown, index), // TODO: Specific polygon 127 transaction. // 127 => encore transaction 127 type from polygon, source: raw json.BlockVerificationError的完整定义位于 evm_block_computer.rs,其中包括UnsupportedTransactionType、TransactionEncodingFailed、TransactionFieldMissing等与交易编码直接相关的错误变体,以及三类根校验失配错误。
2. 完整交易类型清单
2.1 标准 Ethereum 类型 0–4(无需处理)
类型 0–4 由 alloy 通过AnyTxEnvelope::Ethereum → encoded_2718()原生处理,规范文档给出的编码如下:
| 类型 | EIP | 名称 | 编码 |
|---|---|---|---|
| 0 | — | Legacy | RLP([nonce, gasPrice, gasLimit, to, value, data, v, r, s]) |
| 1 | 2930 | Access list | 0x01 + RLP([chainId, nonce, gasPrice, gasLimit, to, value, data, accessList, signatureYParity, signatureR, signatureS]) |
| 2 | 1559 | Dynamic fee | 0x02 + RLP([chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, to, value, data, accessList, signatureYParity, signatureR, signatureS]) |
| 3 | 4844 | Blob | 0x03 + RLP([chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, to, value, data, accessList, maxFeePerBlobGas, blobVersionedHashes, signatureYParity, signatureR, signatureS]) |
| 4 | 7702 | Set code | 0x04 + RLP([chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, to, value, data, accessList, authorizationList, signatureYParity, signatureR, signatureS]) |
这几类统称为 EIP-2718 的 "Typed Transaction Envelope" 体系:type_byte || rlp(payload)。类型 1–4 均在类型字节后紧跟一个 RLP 列表。
2.2 Optimism / Base 类型 126(0x7E,已实现)
| 类型 | 名称 | 编码 |
|---|---|---|
| 126 (0x7E) | Deposit transaction | 0x7E + RLP([sourceHash, from, to, mint, value, gas, isSystemTx, data]) |
来源为 OP Stack 规范。一个关键细节是:from来自 RPC 恢复出的签名者(recovered signer),而不是OtherFields,因为存款交易没有签名。这一点在源码 encode_deposit_transaction 的注释与实现中均有体现:
fn encode_deposit_transaction( unknown: &UnknownTxEnvelope, from: Address, index: usize, ) -> Result<Vec<u8>, BlockVerificationError> { let fields = &unknown.inner.fields; let source_hash: B256 = extract_field(fields, "sourceHash", index)?; // Note: `from` is passed as parameter since it comes from the RPC response's signer field let to: Option<Address> = extract_optional_address(fields, "to"); let mint: U256 = extract_field(fields, "mint", index).unwrap_or_default(); let value: U256 = extract_field(fields, "value", index)?; let gas: u64 = extract_u64(fields, "gas", index)?; let is_system_tx: bool = extract_field(fields, "isSystemTx", index).unwrap_or(false); let data: Bytes = extract_field(fields, "input", index)?; // Note: "input" not "data" let mut payload = Vec::new(); source_hash.encode(&mut payload); from.encode(&mut payload); encode_optional_address(to, &mut payload); mint.encode(&mut payload); value.encode(&mut payload); gas.encode(&mut payload); is_system_tx.encode(&mut payload); data.encode(&mut payload); let mut result = vec![0x7e]; let header = alloy::rlp::Header { list: true, payload_length: payload.len(), }; header.encode(&mut result); result.extend_from_slice(&payload); Ok(result) }注意其中的两个易错点:交易数据字段在 JSON RPC 中名为"input"而非"data";mint与isSystemTx使用了unwrap_or_default()做容错(部分 RPC 可能不返回这两个字段)。
2.3 Arbitrum 类型 100–106
Arbitrum 的修改版 Geth 会把所有自定义字段以 JSON 形式序列化,因此以下类型都可以从OtherFields中编码出来,字段提取由 extract_field、extract_u64 与 extract_optional_address 三个辅助函数完成。其中extract_u64同时兼容十六进制字符串("0x...")与 JSON 数字两种 RPC 返回格式;extract_optional_address在字段缺失时返回None,RLP 编码为空的 bytes(对应合约创建场景)。
Type 100(0x64)— ArbitrumDepositTx
L1→L2 ETH 存款,经 bridge 产生,凡处理了 bridge 存款的区块都会出现。编码格式:
0x64 + RLP([chainId, l1RequestId, from, to, value])| 字段 | JSON key | Rust 类型 | 备注 |
|---|---|---|---|
| chainId | "chainId" | u64 | 十六进制字符串 |
| l1RequestId | "requestId" | B256 | |
| from | "from" | Address | 使用恢复出的签名者 |
| to | "to" | Address | 恒存在(非可选) |
| value | "value" | U256 |
fn encode_arbitrum_deposit_transaction( unknown: &UnknownTxEnvelope, from: Address, index: usize, ) -> Result<Vec<u8>, BlockVerificationError> { let fields = &unknown.inner.fields; let chain_id: u64 = extract_u64(fields, "chainId", index)?; let l1_request_id: B256 = extract_field(fields, "requestId", index)?; let to: Address = extract_field(fields, "to", index)?; let value: U256 = extract_field(fields, "value", index)?; let mut payload = Vec::new(); chain_id.encode(&mut payload); l1_request_id.encode(&mut payload); from.encode(&mut payload); to.encode(&mut payload); value.encode(&mut payload); encode_typed_tx(0x64, payload) }Type 101(0x65)— ArbitrumUnsignedTx
L1 用户经 bridge 调用 L2 合约,无签名(unsigned)。编码格式:
0x65 + RLP([chainId, from, nonce, gasFeeCap, gas, to, value, data])| 字段 | JSON key | Rust 类型 | 备注 |
|---|---|---|---|
| chainId | "chainId" | u64 | |
| from | "from" | Address | 使用恢复出的签名者 |
| nonce | "nonce" | u64 | |
| gasFeeCap | "maxFeePerGas" | U256 | Arbitrum 在 JSON 中将 gasFeeCap 映射为 maxFeePerGas |
| gas | "gas" | u64 | |
| to | "to" | Option<Address> | 合约创建时可为 None |
| value | "value" | U256 | |
| data | "input" | Bytes | 注意是 "input" 而非 "data" |
fn encode_arbitrum_unsigned_transaction( unknown: &UnknownTxEnvelope, from: Address, index: usize, ) -> Result<Vec<u8>, BlockVerificationError> { let fields = &unknown.inner.fields; let chain_id: u64 = extract_u64(fields, "chainId", index)?; let nonce: u64 = extract_u64(fields, "nonce", index)?; let gas_fee_cap: U256 = extract_field(fields, "maxFeePerGas", index)?; let gas: u64 = extract_u64(fields, "gas", index)?; let to: Option<Address> = extract_optional_address(fields, "to"); let value: U256 = extract_field(fields, "value", index)?; let data: Bytes = extract_field(fields, "input", index)?; let mut payload = Vec::new(); chain_id.encode(&mut payload); from.encode(&mut payload); nonce.encode(&mut payload); gas_fee_cap.encode(&mut payload); gas.encode(&mut payload); encode_optional_address(to, &mut payload); value.encode(&mut payload); data.encode(&mut payload); encode_typed_tx(0x65, payload) }Type 102(0x66)— ArbitrumContractTx
L1 合约调用 L2 合约,与 ArbitrumUnsignedTx 类似,但用requestId替代nonce。编码格式:
0x66 + RLP([chainId, requestId, from, gasFeeCap, gas, to, value, data])| 字段 | JSON key | Rust 类型 | 备注 |
|---|---|---|---|
| chainId | "chainId" | u64 | |
| requestId | "requestId" | B256 | |
| from | "from" | Address | 使用恢复出的签名者 |
| gasFeeCap | "maxFeePerGas" | U256 | |
| gas | "gas" | u64 | |
| to | "to" | Option<Address> | |
| value | "value" | U256 | |
| data | "input" | Bytes |
fn encode_arbitrum_contract_transaction( unknown: &UnknownTxEnvelope, from: Address, index: usize, ) -> Result<Vec<u8>, BlockVerificationError> { let fields = &unknown.inner.fields; let chain_id: u64 = extract_u64(fields, "chainId", index)?; let request_id: B256 = extract_field(fields, "requestId", index)?; let gas_fee_cap: U256 = extract_field(fields, "maxFeePerGas", index)?; let gas: u64 = extract_u64(fields, "gas", index)?; let to: Option<Address> = extract_optional_address(fields, "to"); let value: U256 = extract_field(fields, "value", index)?; let data: Bytes = extract_field(fields, "input", index)?; let mut payload = Vec::new(); chain_id.encode(&mut payload); request_id.encode(&mut payload); from.encode(&mut payload); gas_fee_cap.encode(&mut payload); gas.encode(&mut payload); encode_optional_address(to, &mut payload); value.encode(&mut payload); data.encode(&mut payload); encode_typed_tx(0x66, payload) }Type 104(0x68)— ArbitrumRetryTx
重试执行失败的 L1→L2 retryable ticket。编码格式:
0x68 + RLP([chainId, nonce, from, gasFeeCap, gas, to, value, data, ticketId, refundTo, maxRefund, submissionFeeRefund])| 字段 | JSON key | Rust 类型 | 备注 |
|---|---|---|---|
| chainId | "chainId" | u64 | |
| nonce | "nonce" | u64 | |
| from | "from" | Address | 使用恢复出的签名者 |
| gasFeeCap | "maxFeePerGas" | U256 | |
| gas | "gas" | u64 | |
| to | "to" | Option<Address> | |
| value | "value" | U256 | |
| data | "input" | Bytes | |
| ticketId | "ticketId" | B256 | Arbitrum 特有字段 |
| refundTo | "refundTo" | Address | Arbitrum 特有字段 |
| maxRefund | "maxRefund" | U256 | Arbitrum 特有字段 |
| submissionFeeRefund | "submissionFeeRefund" | U256 | Arbitrum 特有字段 |
fn encode_arbitrum_retry_transaction( unknown: &UnknownTxEnvelope, from: Address, index: usize, ) -> Result<Vec<u8>, BlockVerificationError> { let fields = &unknown.inner.fields; let chain_id: u64 = extract_u64(fields, "chainId", index)?; let nonce: u64 = extract_u64(fields, "nonce", index)?; let gas_fee_cap: U256 = extract_field(fields, "maxFeePerGas", index)?; let gas: u64 = extract_u64(fields, "gas", index)?; let to: Option<Address> = extract_optional_address(fields, "to"); let value: U256 = extract_field(fields, "value", index)?; let data: Bytes = extract_field(fields, "input", index)?; let ticket_id: B256 = extract_field(fields, "ticketId", index)?; let refund_to: Address = extract_field(fields, "refundTo", index)?; let max_refund: U256 = extract_field(fields, "maxRefund", index)?; let submission_fee_refund: U256 = extract_field(fields, "submissionFeeRefund", index)?; let mut payload = Vec::new(); chain_id.encode(&mut payload); nonce.encode(&mut payload); from.encode(&mut payload); gas_fee_cap.encode(&mut payload); gas.encode(&mut payload); encode_optional_address(to, &mut payload); value.encode(&mut payload); data.encode(&mut payload); ticket_id.encode(&mut payload); refund_to.encode(&mut payload); max_refund.encode(&mut payload); submission_fee_refund.encode(&mut payload); encode_typed_tx(0x68, payload) }Type 105(0x69)— ArbitrumSubmitRetryableTx
创建 retryable ticket,并附带 L1→L2 费用托管。编码格式:
0x69 + RLP([chainId, requestId, from, l1BaseFee, depositValue, gasFeeCap, gas, retryTo, retryValue, beneficiary, maxSubmissionFee, feeRefundAddr, retryData])| 字段 | JSON key | Rust 类型 | 备注 |
|---|---|---|---|
| chainId | "chainId" | u64 | |
| requestId | "requestId" | B256 | |
| from | "from" | Address | 使用恢复出的签名者 |
| l1BaseFee | "l1BaseFee" | U256 | Arbitrum 特有字段 |
| depositValue | "depositValue" | U256 | Arbitrum 特有字段 |
| gasFeeCap | "maxFeePerGas" | U256 | |
| gas | "gas" | u64 | |
| retryTo | "retryTo" | Option<Address> | L2 上的目标地址 |
| retryValue | "retryValue" | U256 | Arbitrum 特有字段 |
| beneficiary | "beneficiary" | Address | Arbitrum 特有字段 |
| maxSubmissionFee | "maxSubmissionFee" | U256 | Arbitrum 特有字段 |
| feeRefundAddr | "feeRefundAddr" | Address | Arbitrum 特有字段 |
| retryData | "retryData" | Bytes | Arbitrum 特有字段 |
fn encode_arbitrum_submit_retryable_transaction( unknown: &UnknownTxEnvelope, from: Address, index: usize, ) -> Result<Vec<u8>, BlockVerificationError> { let fields = &unknown.inner.fields; let chain_id: u64 = extract_u64(fields, "chainId", index)?; let request_id: B256 = extract_field(fields, "requestId", index)?; let l1_base_fee: U256 = extract_field(fields, "l1BaseFee", index)?; let deposit_value: U256 = extract_field(fields, "depositValue", index)?; let gas_fee_cap: U256 = extract_field(fields, "maxFeePerGas", index)?; let gas: u64 = extract_u64(fields, "gas", index)?; let retry_to: Option<Address> = extract_optional_address(fields, "retryTo"); let retry_value: U256 = extract_field(fields, "retryValue", index)?; let beneficiary: Address = extract_field(fields, "beneficiary", index)?; let max_submission_fee: U256 = extract_field(fields, "maxSubmissionFee", index)?; let fee_refund_addr: Address = extract_field(fields, "feeRefundAddr", index)?; let retry_data: Bytes = extract_field(fields, "retryData", index)?; let mut payload = Vec::new(); chain_id.encode(&mut payload); request_id.encode(&mut payload); from.encode(&mut payload); l1_base_fee.encode(&mut payload); deposit_value.encode(&mut payload); gas_fee_cap.encode(&mut payload); gas.encode(&mut payload); encode_optional_address(retry_to, &mut payload); retry_value.encode(&mut payload); beneficiary.encode(&mut payload); max_submission_fee.encode(&mut payload); fee_refund_addr.encode(&mut payload); retry_data.encode(&mut payload); encode_typed_tx(0x69, payload) }Type 106(0x6A)— ArbitrumInternalTx(已实现)
0x6A + RLP([chainId, data])这是当前仓库中已落地实现的 Arbitrum 类型,对应 encode_arbitrum_internal_transaction,字段提取为chainId(u64)与input(Bytes),随后按0x6a类型字节 + RLP 列表组装。
2.4 Polygon Bor 类型 127(0x7F)— PIP-74 State Sync
类型 127 由Madhugiri 硬分叉(2024 年 12 月,激活区块 80,084,800)引入,对应 PIP-74 提案"在区块体中规范化纳入 StateSync 交易"。编码格式:
| 类型 | 名称 | 编码 |
|---|---|---|
| 127 (0x7F) | State sync (PIP-74) | 0x7F + RLP([\[encStateSyncData, ...]]) |
其中每个encStateSyncData由四部分构成:
| 字段 | 类型 | 描述 |
|---|---|---|
| ID | uint64 | State sync 事件 ID |
| Contract | address | L2 上的接收合约 |
| Data | bytes | ABI 编码的 payload |
| TxHash | bytes32 | L1 交易哈希 |
该类型无法从 JSON RPC 编码——内部 payload 不在交易字段中。RPC 返回的是一个归一化视图:from=0x0, to=0x0, gas=0, input=0x, v/r/s=0,真实的 state sync 数据只能通过eth_getRawTransactionByHash获取。
State sync 交易出现在sprint 区块上(Polygon 每 16 个区块一个 sprint,即区块号可被 16 整除时)。规范文档给出了区块 85,523,136 中 index 375 交易的真实原始字节样例:
0x7f <- type byte f9025f <- RLP list header (607 bytes) f9013d <- first encStateSyncData 83 30364e <- ID: 3159630 94 a6fa...c0aa <- Contract: 0xa6fa4fb5f76172d178d61b04b0ecd319c5d1c0aa b90100 87a7811f... <- Data: 256 bytes a0 378a5f6c...1e1e <- TxHash: 0x378a5f6c... f9011c <- second encStateSyncData 83 30364f <- ID: 3159631 94 8397...8a28a <- Contract: 0x8397259c983751daf40400790063935a11afa28a b8e0 0000... <- Data: 224 bytes a0 458eea10...a51f <- TxHash: 0x458eea10...支持类型 127 的三种方案
方案 A — 跳过交易根校验(当前采用)
遇到类型 0x7F 时,跳过整个区块的交易根校验。区块哈希校验 + 收据根校验仍然执行。其中,区块哈希校验本身足以证明区块头的真实性(包括头部中存储的transactionsRoot)。代价是:恶意 RPC 可能在不被发现的情况下为含 state-sync 的区块提供被篡改的交易——这在信任 RPC 端点的前提下可以接受。
方案 B — 通过eth_getRawTransactionByHash获取原始字节
在SemEvmRpcProvider中新增get_raw_transaction_by_hash方法;fetcher 在调用校验前扫描未知类型并获取原始字节,通过HashMap<B256, Vec<u8>>传给 block computer。需要改动三处:
sem_evm_rpc_provider.rs:新增get_raw_transaction_by_hash;evm_block_fetcher.rs:新增fetch_raw_unknown_txs,并将build_fetched_block改为 async;evm_block_computer.rs:verify_block/verify_transaction_root/safe_encode_transaction增加raw_txs参数。
代价是每个 state-sync 区块多一次 RPC 调用,且需要改动 fetcher 架构。
方案 C — 从收据日志重建
收据日志包含 state sync 事件 ID(位于0x0000...1001合约事件 topics 中)和合约地址,但不包含完整 payload 数据与 L1 交易哈希,因此不可行。
2.5 其他链情况汇总
| 链 | 自定义类型? | 说明 |
|---|---|---|
| BNB/BSC | 无 | 仅标准类型 0–2 |
| Avalanche C-Chain | 无 | 仅标准类型 0–2(自定义区块头在verify_block_hash中处理) |
| Monad | 无 | 标准类型 0–4 |
| Base | 仅类型 126 | 与 Optimism 相同(OP Stack) |
| zkSync | 类型 113 (0x71) | EIP-712 zkSync 交易,除非新增 zkSync 支持否则无关 |
| Polygon zkEVM | 无 | 仅标准类型 |
其中 Avalanche 的情况值得注意:虽然交易类型是标准的,但其自定义区块头需要在verify_block_hash中特殊处理,当前实现先尝试标准哈希,失败后再尝试 Avalanche 变体(对应BlockVerificationError::BlockHashVerificationExhausted,见 evm_block_computer.rs)。
3. 共享 RLP 助手 encode_typed_tx
为减少各编码器之间的重复代码,规范建议抽取一个共享的encode_typed_tx:
/// Build a typed transaction envelope: type_byte + RLP list wrapping the payload. fn encode_typed_tx(type_byte: u8, payload: Vec<u8>) -> Result<Vec<u8>, BlockVerificationError> { let mut result = vec![type_byte]; let header = alloy::rlp::Header { list: true, payload_length: payload.len(), }; header.encode(&mut result); result.extend_from_slice(&payload); Ok(result) }同时把已有的encode_deposit_transaction(0x7E)与encode_arbitrum_internal_transaction(0x6A)也重构到该助手之上。从当前源码看,这两个函数仍各自内联了vec![0x7e]/vec![0x6a]+Header的组装逻辑(evm_block_computer.rs 与 evm_block_computer.rs),与文档中的重构建议一致,属于待落地的优化项。
4. 跳过机制设计:让未知类型不再阻塞
4.1 现状:allow_skipping 参数与调用链
当前仓库中,跳过机制已经部分落地。verify_block接受allow_skipping: bool参数(evm_block_computer.rs):开启时三项校验任一失败只打 ERROR 日志并继续,同时递增对应指标;关闭时任一失败立即返回错误(stalling=true)。
规范文档指出一个关键缺口:当前verify_block确实捕获了verify_transaction_root的错误并继续,但错误仍会从safe_encode_transaction一路传播出来。文档给出的目标流程为:
safe_encode_transaction └── type 127 or _ → Err(UnsupportedTransactionType { tx_type, index }) verify_transaction_root └── on UnsupportedTransactionType: error!(tx_type, index, block_number, "Skipping tx root verification: ...") return Ok(()) // skip, don't propagate verify_block (unchanged) └── verify_transaction_root → Ok (skipped) or Ok (verified) └── verify_receipt_root → must pass └── verify_block_hash → must pass即:把"跳过"的判定点从verify_block下移到verify_transaction_root,针对UnsupportedTransactionType这一特定错误返回Ok(()),而其他错误(如根失配)仍然传播,从而避免"因一个未知类型而丢弃整个区块的根校验"或"把真正的根失配也一并吞掉"两种极端。
规范还建议使用 ERROR 日志保证可见性,并新增listener_block_verification_skipped_total计数器(labels:chain_id,reason)用于告警。
4.2 配置入口:compute_block_allow_skipping
该跳过行为在配置层面对应compute_block_allow_skipping,定义于 config.rs,默认值为true(见default_compute_block_allow_skipping)。注释明确指出其用途正是"Polygon type 0x7F transaction root 等场景下跳过校验并以 ERROR 日志代替硬失败"。相关示例配置可见 config-polygon-compute.yaml、config-avalanche.yaml 等:
compute_block: true compute_block_allow_skipping: true该配置经由 evm_listener.rs 传入 fetcher 的with_verify_block_allow_skipping,最终进入EvmBlockFetcher::FetchConfig.verify_block_allow_skipping(evm_block_fetcher.rs),在抓取区块后调用EvmBlockComputer::verify_block(evm_block_fetcher.rs)。整条链路为:
config.compute_block_allow_skipping → EvmListener::build_fetcher (evm_listener.rs) → FetchConfig.verify_block_allow_skipping (evm_block_fetcher.rs) → EvmBlockComputer::verify_block(..., allow_skipping, chain_id) (evm_block_computer.rs)4.3 可观测性:metric 与仪表盘
当前实现中实际使用的指标为listener_compute_transaction_failure_total、listener_compute_receipt_failure_total、listener_compute_block_failure_total,均带chain_id与stalling两个 label。stalling=false表示被宽容跳过的数据质量问题,stalling=true表示硬停机(不变量被破坏)。dashboard.md 的告警约定是:stalling=true的 compute failure必须恒为零,一旦出现即为不变量违规,应立即调查;而stalling=false在 L2 上允许存在且与链相关,若持续增长则需排查 RPC 数据质量,很可能是不受支持的 L2 交易类型。
5. 自定义类型的收据编码:无需改动
收据编码对所有非 0x7E 类型统一使用AnyReceiptEnvelope::encoded_2718(),产出type_byte || rlp(status, cumulativeGasUsed, bloom, logs)。这适用于:
- Arbitrum 类型 100–106:标准收据格式,只是类型字节不同;
- Polygon 类型 127:标准收据格式(status=1、gasUsed=0、标准 logs)。
因此上述所有类型都不需要修改收据编码。唯一需要特殊处理的是 Optimism 特有的存款收据(类型 0x7E,含depositNonce/depositReceiptVersion两个 Canyon 升级字段)——该逻辑已实现于 encode_deposit_receipt 与 encode_receipt:
// Check for L2 deposit receipt (type 126) if receipt_type == 0x7e { let deposit_nonce: Option<u64> = receipt .other .get("depositNonce") .and_then(|v| v.as_str()) .and_then(|s| u64::from_str_radix(s.trim_start_matches("0x"), 16).ok()); // ... if deposit_nonce.is_some() || deposit_receipt_version.is_some() { return encode_deposit_receipt(&consensus_receipt, deposit_nonce, deposit_receipt_version); } }encode_receipt会先判断收据类型是否为 0x7E,若存在depositNonce/depositReceiptVersion则走 L2 专用编码,否则回落到标准encoded_2718()。
6. 测试与验证策略
6.1 Polygon state sync 区块
选定的测试区块为 Polygon 主网85,523,136(0x518FAC0),共 376 笔交易,index 375 是类型 0x7F。sprint 区块每 16 个区块出现一次:
TX hash: 0x4f8e7a02f12c3573bf9a7c83ac19f77f0537d614a1f05f68b39278cc02d652e5 RPC: https://polygon-bor-rpc.publicnode.com测试需验证三点:
verify_block不报错(交易根被跳过,收据根 + 区块哈希通过);verify_receipt_root独立通过;verify_block_hash独立通过。
6.2 Arbitrum
在report_block_verification测试中启用 "Arbitrum One"(目前注释在 evm_block_computer_tests.rs),使用https://arb1.arbitrum.io/rpc。多数区块包含类型 106(internal),偶尔出现类型 100、104、105。
该测试框架的整体设计(evm_block_computer_tests.rs)也值得借鉴:它对每个目标链抓取最新区块,以三层回退策略抓取收据(先eth_getBlockReceipts,再批量eth_getTransactionReceipt,最后逐笔调用),然后分别独立报告交易根 / 收据根 / 区块哈希三项校验结果,便于精准定位是哪一层失败。
6.3 字段映射验证
对每个新增的 Arbitrum 编码器,从 Arbiscan 挑选一笔该类型的真实交易,进行双重比对:
- 通过
eth_getBlockByNumber(..., true)抓取——核对 JSON 字段名与规范一致; - 通过
eth_getRawTransactionByHash抓取——将我们的编码结果与原始字节比对。
只有编码结果与原始字节完全一致,才能证明字段顺序、类型宽度与 RLP 头都正确。
7. 参考与延伸阅读
- PIP-74:Canonical Inclusion of StateSync Transactions in Block Bodies(Polygon 提案,Madhugiri 硬分叉引入,Bor v2.5.0 实现);
- Arbitrum Nitro
arb_types.go:Arbitrum 修改版 Geth 中全部自定义交易类型的权威定义(类型 100–106 的字段来源); - Arbitrum inside Nitro文档:L1→L2 消息流、retryable ticket 与各交易类型的业务语义;
- EIP-2718 Typed Transaction Envelope:标准类型 0–4 统一采用
type_byte || rlp(payload)的编码基础。
仓库内可直接延伸阅读的配套文档与实现:
- 本规范文档:spec-block-computer-tx-types.md;
- 核心实现:evm_block_computer.rs;
- RPC provider(
SemEvmRpcProvider,信号量限流的 RPC 客户端):sem_evm_rpc_provider.rs; - 区块抓取器(含 5 种抓取策略与校验入口):evm_block_fetcher.rs;
- 校验测试:evm_block_computer_tests.rs;
- 配置项
compute_block_allow_skipping:config.rs; - 校验失败指标与告警约定:dashboard.md。
综上,Block Computer 的交易类型编码是跨链区块完整性校验中最具挑战性的部分:标准链依赖 alloy 原生支持,L2 链则需要精确还原各自修改版客户端的私有交易格式。理解本文的编码表与字段映射,你就掌握了为 listener 新增任意 EVM 兼容链支持时最核心的"字节级还原"能力。
【免费下载链接】fhevmFHEVM, a full-stack framework for integrating Fully Homomorphic Encryption (FHE) with blockchain applications项目地址: https://gitcode.com/GitHub_Trending/fh/fhevm
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考