go-ethereum evm b11r 区块构建指南:如何将 ommer 叔块组装进规范区块

go-ethereum evm b11r 区块构建指南:如何将 ommer 叔块组装进规范区块

【免费下载链接】go-ethereum Go implementation of the Ethereum protocol 【免费下载链接】go-ethereum 项目地址: https://gitcode.com/gh_mirrors/go/go-ethereum

本文以 go-ethereum 仓库中 cmd/evm/testdata/22/readme.md 这份测试用例说明为骨架,深入讲解 evm b11r(block builder)工具如何将叔块(ommer / uncle)头组装进一个规范区块。读者将掌握:b11r 的完整命令行参数、通过 stdin 与 jq 管道动态构造 ommer 输入的实战技巧、输出 RLP 与哈希的解读方法,以及其底层实现(block.go)与自动化测试(t8n_test.go)的验证链路。

一、背景:b11r 与 ommer 在 go-ethereum 测试体系中的位置

go-ethereum 的 EVM 工具(cmd/evm/README.md)提供三个面向 EVM 层测试的子命令:

  • t8n:无状态状态转换工具(state transition),负责执行交易并生成后置状态;
  • t9n:交易静态校验工具(transaction validation);
  • b11r:区块组装工具(block builder),用于把区块头、交易列表与 ommer 列表拼装成一个完整的、可选密封(seal)的区块 RLP。

b11r 是第三个,其核心价值在于:测试或研究场景中往往需要"精确控制"一个区块的每个字段(难度、时间戳、叔块列表等),而不是依赖全节点出块流程。ommer(早期文档中也叫 uncle,叔块)是以太坊 PoW 时代的重要概念:当多个矿工几乎同时挖出同一高度的区块时,未能进入主链但被后续区块引用的区块头即称为 ommer,引用它的区块可获得额外奖励。

仓库中 cmd/evm/testdata 目录按编号组织了大量测试夹具(fixture),其中 testdata/22 专门验证"带 ommers 的区块构建"这一场景,其测试用例在 t8n_test.go 中以 { // block with ommers ... expOut: "exp.json" } 的形式注册。

二、测试数据解剖:testdata/22 的五个文件

先看 testdata/22 目录下有哪些输入与期望文件:

cmd/evm/testdata/22/
├── exp-clique.json   # Clique 密封后的期望输出
├── exp.json          # 期望输出(普通构建)
├── header.json       # 区块头输入
├── ommers.json       # ommer 区块 RLP 列表
├── readme.md         # 本用例说明文档
└── txs.rlp           # 交易列表 RLP

1. 区块头输入 header.json

{
    "parentHash": "0xd6d785d33cbecf30f30d07e00e226af58f72efdf385d46bc3e6326c23b11e34e",
    "stateRoot": "0x325aea6db48e9d737cddf59034843e99f05bec269453be83c9b9a981a232cc2e",
    "logsBloom": "0x0000...0000",
    "difficulty": "0x1000",
    "number": "0xc3be",
    "gasLimit": "0x50785",
    "gasUsed": "0x0",
    "timestamp": "0x55c5277e",
    "mixHash": "0x5865e417635a26db6d1d39ac70d1abf373e5398b3c6fd506acd038fa1334eedf"
}

注意该文件只提供了必填与部分可选字段(parentHash、stateRoot、difficulty、number、gasLimit、gasUsed、timestamp、mixHash),并没有提供 sha3Uncles、miner、transactionsRoot、receiptsRoot、nonce 等字段。这正是 b11r 的"组装"语义所在——缺省字段将由工具用默认值填充(详见第五节源码分析)。

2. 交易列表 txs.rlp

该文件内容仅为 "c0",即 RLP 编码的空列表(0xc0 是 RLP 中空列表的编码),表示本区块不包含任何交易。这也解释了最终输出的 gasUsed: 0x0 与空的状态根。

3. ommer 列表 ommers.json

这是一个 JSON 字符串数组,包含两个 RLP 编码的 ommer 区块:

["0xf901fdf901f8a0d6d785...c0c0", "0xf901fdf901f8a0d6d785...c0c0"]

每个元素都完整编码了一个区块(而非仅仅一个区块头),其中 f901f8... 部分是 ommer 的区块头,末尾的 c0c0 是空交易列表与空 ommer 列表。从 RLP 中可以看到这两个 ommer 的 parentHash 与 header.json 一致、ommerHash 字段为 0x1dcc4de8...(即 EmptyUncleHash,说明它们自身没有叔块)、coinbase 为零地址。该文件也正是 readme 命令中"先构建、再复用"所期望的最终形态(见第三节)。

4. 期望输出 exp.json 与 exp-clique.json

exp.json 保存了普通构建的期望结果(区块 RLP 与区块哈希),与 readme 中展示的输出完全一致;exp-clique.json 则是对同一组输入应用 Clique 密封后的期望结果,其 extraData 中携带了 32 字节 vanity 与 65 字节签名、nonce 变为 0xff...ff(对应 Clique 投票语义中的否决票场景),可用作对照实验(见第六节)。

三、核心命令逐步拆解:如何把两个 ommer 组装进区块

readme.md 的核心内容是一条"一步到位的管道命令",其思路非常巧妙:先用两次独立的 b11r 调用各自生成一个"完整的 ommer 区块"的 RLP,再用第三次 b11r 调用通过 stdin 把它们作为 ommer 列表喂给区块头。下面完整保留原命令并逐步拆解:

$ echo "{ \"ommers\": [`go run . b11r --input.header=testdata/22/header.json --input.txs=testdata/22/txs.rlp --output.block=stdout | jq '.[\"rlp\"]'`,`go run . b11r --input.header=testdata/22/header.json --input.txs=testdata/22/txs.rlp --output.block=stdout | jq '.[\"rlp\"]'`]}" | go run . b11r --input.header=testdata/22/header.json --input.txs=testdata/22/txs.rlp --input.ommers=stdin --output.block=stdout

步骤一:两次 b11r 调用生成 ommer 区块 RLP

命令中的前两个 go run . b11r ... --output.block=stdout 各自执行一次 b11r:以 header.json 作为区块头、txs.rlp(空交易)作为交易列表,输出一个未密封、无 ommer 的区块的 JSON,其格式为 {"rlp": "0x...", "hash": "0x..."}(见第五节 dispatchBlock 源码)。由于两次输入完全相同,两次输出的 rlp 字段也完全相同——这个 RLP 恰好就是一个"可以作为 ommer 引用的完整区块"。

步骤二:jq 提取并拼装 JSON

管道中的 jq '.["rlp"]' 从每次 b11r 的输出 JSON 中提取 rlp 字段。jq 输出的字符串带双引号(如 "0xf901fd..."),因此 echo 拼接出的文本天然就是合法的 JSON:

{ "ommers": ["0xf901fd...", "0xf901fd..."] }

这个 JSON 被通过管道送入第三次 b11r 调用的标准输入。

步骤三:第三次调用,--input.ommers=stdin 的语义

第三次调用的关键参数是 --input.ommers=stdin。结合 block.go 的 readInput 实现:

if headerStr == stdinSelector || ommersStr == stdinSelector || txsStr == stdinSelector || cliqueStr == stdinSelector {
    decoder := json.NewDecoder(os.Stdin)
    if err := decoder.Decode(inputData); err != nil {
        return nil, NewError(ErrorJson, fmt.Errorf("failed unmarshalling stdin: %v", err))
    }
}

只要 header、ommers、txs、clique 任一输入被指定为 stdin,b11r 就会把整个输入对象(结构 bbInput,包含 header、ommers、txs、withdrawals、clique 字段)作为一个 JSON 从标准输入一次性解码。随后在 readInput 中,凡未指定为 stdin 的输入(这里是 header 与 txs)仍会从各自文件读取并覆盖对应字段:

if headerStr != stdinSelector {
    var env header
    if err := readFile(headerStr, "header", &env); err != nil { ... }
    inputData.Header = &env
}
...
if txsStr != stdinSelector {
    var txs string
    if err := readFile(txsStr, "txs", &txs); err != nil { ... }
    inputData.TxRlp = txs
}

因此"--input.ommers=stdin + 文件形式提供 header/txs"是 b11r 官方推荐的混合输入方式:区块头与交易来自文件,ommer 列表来自标准输入。

步骤四:输出结果

readme 中完整记录了命令输出(与 exp.json 完全一致):

{
  "rlp": "0xf905f5f901f8a0d6d785d33cbecf30f30d07e00e226af58f72efdf385d46bc3e6326c23b11e34ea06eb9f0c3cd68c9e97134e6725d12b1f1d8f0644458da6870a37ff84c908fb1e7940000000000000000000000000000000000000000a0325aea6db48e9d737cddf59034843e99f05bec269453be83c9b9a981a232cc2ea056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421b9010000...0000",
  "hash": "0xd9a81c8fcd57a7f2a0d2c375eff6ad192c30c3729a271303f0a9a7e1b357e755"
}

(受篇幅所限,此处对超长 RLP 做了省略标注;实际输出与 exp.json 一致,均为完整十六进制串。)

四、输出解读:RLP 结构与哈希

1. RLP 结构

输出 rlp 字段是一个完整的区块 RLP,其顺序为 [header, txs, uncles],对照输出可逐段解析:

十六进制片段含义
f905f5区块列表头(负载长度 0x05f5 字节)
f901f8 起的一段区块头 header(含 parentHash=d6d785...、ommerHash=6eb9f0c3...、coinbase 零地址、stateRoot=325aea...、transactionsRoot/receiptsRoot 均为 0x56e81f...(空树哈希)、空 logsBloom、difficulty=0x1000、number=0xc3be、gasLimit=0x50785、gasUsed=0x0、timestamp=0x55c5277e、空 extraData、mixHash=5865e4...、nonce=0x0000...0000)
c0交易列表(空,对应 txs.rlp 的内容)
f903f6 起的一段ommer 列表,内含两个 f901f8... 的 ommer 区块头

注意最终区块头中的 ommerHash 为 0x6eb9f0c3...——这是对两个 ommer 头执行 CalcUncleHash 得到的哈希,而不是 header.json 提供的(该文件没有 sha3Uncles 字段)。这正是 ToBlock 中的逻辑:

// Fill optional values.
if i.Header.OmmerHash != nil {
    header.UncleHash = *i.Header.OmmerHash
} else if len(i.Ommers) != 0 {
    // Calculate the ommer hash if none is provided and there are ommers to hash
    header.UncleHash = types.CalcUncleHash(i.Ommers)
}

2. 区块哈希

hash: 0xd9a81c8fcd57a7f2a0d2c375eff6ad192c30c3729a271303f0a9a7e1b357e755 是对上述 RLP 求 Keccak-256 得到的区块哈希,它与 exp.json 完全一致,说明 readme 中的命令可以稳定复现期望输出。

五、b11r 命令行参数全解

cmd/evm/README.md 对 b11r 给出了严格的参数规范,结合源码可以整理如下:

--input.header value        区块头来源:`stdin` 或文件名(默认 "header.json")
--input.ommers value        ommer 区块 RLP 列表来源:`stdin` 或文件名
--input.txs value           交易列表(RLP 形式)来源:`stdin` 或文件名(默认 "txs.rlp")
--input.withdrawals value   提款(withdrawals)列表来源:`stdin` 或文件名
--output.basedir value      输出文件目录(不存在会自动创建)
--output.block value        区块输出位置(默认 "block.json"):
                            <file>  → 写入文件
                            `stdout` → 输出到标准输出
                            `stderr` → 输出到标准错误
--seal.clique value         Clique 密封数据:`stdin` 或文件名
--seal.ethash               以 ethash 密封区块(默认 false)
--seal.ethash.dir value     ethash DAG 路径(不存在则生成新 DAG)
--seal.ethash.mode value    ethash PoW 验证类型与强度(默认 "normal")
--verbosity value           日志详细级别(默认 3)

输入对象的 JSON 结构

  • header:共识区块头对象,必填字段为 stateRoot、number、gasLimit、timestamp,可选字段包括 parentHash、sha3Uncles、miner、transactionsRoot、receiptsRoot、logsBloom、difficulty、gasUsed、extraData、mixHash、nonce、baseFeePerGas,以及后合并时代(post-Merge)的 withdrawalsRoot、blobGasUsed、excessBlobGas、parentBeaconBlockRoot、requestsHash、blockAccessListHash、slotNumber 等可选字段(完整定义见 block.go);
  • ommers:RLP 编码的 ommer 区块十六进制字符串列表,即 type Ommers []string;
  • txs:RLP 编码的交易十六进制字符串列表(一个字符串),即 type Txs []string;
  • clique:Clique 密封所需数据,结构为 { "secretKey": ..., "voted": ..., "authorize": ..., "vanity": ... },其中 secretKey 为必填;
  • output:输出对象含两个字段——rlp(区块 RLP 字节,hex 形式)与 hash(区块哈希)。

stdin 输入的整体解码规则

需要特别强调的是:如前所述,只要任意一个输入参数为 stdin,b11r 就会尝试从标准输入解码整个 bbInput JSON 对象(字段名与上述对象一致:header、ommers、txs、withdrawals、clique)。其余非 stdin 输入仍从文件读取。这种"混合输入"模式正是本测试用例的核心用法。

六、源码级原理:b11r 的执行链路

b11r 子命令的入口为 BuildBlock,完整调用链如下:

BuildBlock
  ├─ createBasedir(ctx)         # 创建输出目录
  ├─ readInput(ctx)             # 读取 header / ommers / txs / withdrawals / clique
  │    ├─ stdin 整体解码(任一输入为 stdin 时)
  │    ├─ 文件读取(header、ommers、txs、clique、withdrawals 各自 readFile)
  │    ├─ rlp.DecodeBytes(txs.rlp) → []*types.Transaction
  │    └─ rlp.DecodeBytes(ommer.rlp) → extblock{Header, Txs, Ommers},仅取 .Header
  ├─ inputData.ToBlock()        # 组装 *types.Block
  ├─ inputData.SealBlock(block) # 可选 Clique 密封
  └─ dispatchBlock(ctx, baseDir, block)  # 输出 {rlp, hash} 到 stdout/stderr/文件

几个值得注意的实现细节:

  1. ommer 以"完整区块"形式解码,但只取区块头(block.go):每个 ommer 字符串按 extblock 结构(Header + Txs + Ommers)RLP 解码,随后仅把 ommer.Header 追加进 ommer 列表。这正是本用例中"用两次 b11r 构建完整区块、再当 ommer 喂回去"能够成立的原因——ommers.json 里存的就是完整区块 RLP。
  2. 缺省字段自动补齐(ToBlock):UncleHash、TxHash、ReceiptHash 默认取 types.EmptyUncleHash / types.EmptyTxsHash / types.EmptyReceiptsHash,Difficulty 默认 common.Big0,coinbase 默认零地址;若提供了 OmmerHash 且存在 ommer 列表,则优先用显式值,否则 CalcUncleHash 自动计算。
  3. Clique 密封的冲突检测(sealClique):若 --seal.clique 与显式提供的 extraData、coinbase、nonce 冲突(例如投票会改写 coinbase/nonce),直接返回配置错误,避免"静默构建出与预期不符的区块";密封过程使用 clique.SealHash + crypto.Sign 生成 65 字节签名,拼接到 32 字节 vanity 之后(extraData 总长 32+65=97 字节)。
  4. 输出格式(dispatchBlock):最终输出为 {"rlp": "0x...", "hash": "0x..."} 的缩进 JSON,可自由定向到 stdout / stderr / 文件。

七、用自动化测试验证:TestB11r

testdata/22 并非孤立的示例,而是 t8n_test.go 中 TestB11r 测试套件的一员。其对应用例为:

{ // block with ommers
    base: "./testdata/22",
    input: b11rInput{
        inEnv:       "header.json",
        inOmmersRlp: "ommers.json",
        inTxsRlp:    "txs.rlp",
    },
    expOut: "exp.json",
},

测试通过 cmdtest 以子进程方式真实执行 evm-test b11r ... --output.block stdout,并将 stdout 结果与 exp.json 做 JSON 语义比较(cmpJson)。这意味着任何人都可以本地复现验证:

cd cmd/evm
go test -run TestB11r -v

也可以脱离测试框架直接观察构建过程:

cd cmd/evm
go run . b11r --input.header=testdata/22/header.json --input.ommers=testdata/22/ommers.json --input.txs=testdata/22/txs.rlp --output.block=stdout

此时无需手工构造 stdin JSON——因为 ommer 已经以文件形式存在于 ommers.json 中,这与 readme 展示的"用 b11r 自己生成 ommer 再喂给 b11r"是同一效果的两种输入方式。

八、实战扩展与注意事项

1. 相关测试目录对照

在 cmd/evm/testdata 中,与 b11r 相关的夹具还有:

  • testdata/20:未密封区块构建(ommer 为空列表 []);
  • testdata/21:同一组输入分别做 ethash 密封(期望 exp.json)与 Clique 密封(期望 exp-clique.json,并配套 clique.json 密封数据),是理解 --seal.* 参数的最佳样例;
  • testdata/22:本文主题——带 ommers 的区块构建;
  • testdata/27:带 withdrawals 的区块构建(新增 --input.withdrawals 输入)。

它们共同覆盖了 b11r 的全部核心能力:组装、密封、叔块、提款。

2. 前提条件与运行位置

  • 命令中的 go run . b11r 隐含在 cmd/evm 目录下执行(go run . 以当前目录为 Go 包),testdata/22/... 等相对路径也以该目录为基准;
  • 需要本机具备 Go 工具链,且 jq 已安装(readme 命令依赖 jq 提取 rlp 字段);
  • 如果只想验证最终结果,也可以直接用 ommers.json 文件输入而跳过前两次 b11r 调用与 jq 管道。

3. 已知限制

从 cmd/evm/README.md 与测试设计可以确认:b11r 不校验 ommer 的合法性规则——例如两个 ommer 可以在同一高度、或超出叔块距离限制(8 个区块),甚至允许"负叔块奖励"。它只负责机械地把输入的 ommer 头按给定顺序组装进区块并计算哈希,这正是它作为"区块构建工具"而非"区块验证器"的定位。同一逻辑在 t8n 的 ommers 奖励处理中亦有体现(详见 cmd/evm/README.md 的 "About Ommers" 一节)。

小结

通过 cmd/evm/testdata/22/readme.md 这一个测试用例,我们完整走通了 go-ethereum evm b11r 的三大核心能力:从文件/标准输入读取三类输入(header、txs、ommers)、按 RLP 规则组装规范区块并自动补齐缺省字段、通过 --output.block 定向输出 RLP 与哈希。其"两次构建、一次组装"的管道技巧展示了 b11r 作为"区块工厂"的可组合性:任何 b11r 输出的区块 RLP 都可以成为下一次构建的 ommer 输入。配合 t8n_test.go 的自动化测试与 block.go 的源码,开发者可以在此基础上自由构造任意形态的测试区块,用于状态转换、共识引擎与 EVM 层的研究与验证。

【免费下载链接】go-ethereum Go implementation of the Ethereum protocol 【免费下载链接】go-ethereum 项目地址: https://gitcode.com/gh_mirrors/go/go-ethereum

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值