BSC 客户端源码解析:用 evm t8n 计算 Ethash 难度——GrayGlacier(EIP-5133)测试用例全解
【免费下载链接】bscA BNB Smart Chain client based on the go-ethereum fork项目地址: https://gitcode.com/GitHub_Trending/bs/bsc
本篇技术指南围绕 bsc 仓库中 cmd/evm/testdata/19/readme.md 这一官方测试用例展开,讲解如何利用evm t8n(transition)子命令,在调用方不提供难度值时自动按 Ethash 规则计算区块难度,并重点演示GrayGlacier(EIP-5133)分叉下的计算行为。读完本文,你将掌握 t8n 输入/输出 JSON 的完整字段语义、难度计算底层的调用链,以及如何通过切换分叉名复现 London / ArrowGlacier / GrayGlacier 三种难度结果的差异。
一、测试用例概览:目录结构与文件清单
用例位于 cmd/evm/testdata/19/,共包含 7 个文件,职责如下:
| 文件 | 作用 |
|---|---|
| readme.md | 用例说明文档,给出完整运行命令与输出样例 |
| env.json | 环境(区块上下文)输入,描述待构建区块与父区块信息 |
| alloc.json | 初始账户状态输入 |
| txs.json | 交易集合输入,本用例为空数组 |
| exp_london.json | London 分叉下的期望结果 |
| exp_arrowglacier.json | ArrowGlacier(EIP-4345)分叉下的期望结果 |
| exp_grayglacier.json | GrayGlacier(EIP-5133)分叉下的期望结果 |
该用例的核心验证点是:在--input.env中不提供currentDifficulty时,evm t8n会基于父区块的parentDifficulty与父区块时间戳,按指定分叉的 Ethash 难度算法自动推导当前区块难度。这正是 readme.md 开头所述 "This test shows how theevm t8ncan be used to calculate the (ethash) difficulty, if none is provided by the caller" 的含义。
二、输入文件逐字段解读
2.1 env.json:区块环境上下文
env.json 的完整内容如下:
{ "currentCoinbase": "0xc94f5374fce5edbc8e2a8697c15331677e6ebf0b", "currentGasLimit": "0x750a163df65e8a", "currentBaseFee": "0x500", "currentNumber": "13000000", "currentTimestamp": "100015", "parentTimestamp": "99999", "parentDifficulty": "0x2000000000000" }各字段语义(对应 t8n 工具内部stEnv结构,见 cmd/evm/internal/t8ntool/execution.go):
| 字段 | 值 | 说明 |
|---|---|---|
currentCoinbase | 0xc94f...f0b | 当前区块的矿工/出块地址(区块奖励接收方) |
currentGasLimit | 0x750a163df65e8a | 当前区块 Gas 上限 |
currentBaseFee | 0x500 | 当前区块基础费(1280 wei,London 之后必填) |
currentNumber | 13000000 | 当前区块高度,十进制表示 |
currentTimestamp | 100015 | 当前区块时间戳 |
parentTimestamp | 99999 | 父区块时间戳,用于难度的时间衰减因子计算 |
parentDifficulty | 0x2000000000000 | 父区块难度,这是难度推导的关键输入 |
注意两点:
- 没有
currentDifficulty字段——这正是本用例的主题:让工具自行计算; - 没有
currentRandom(prevRandao)——因为本用例仍处于 PoW(Ethash)阶段,尚未到 Merge。
2.2 alloc.json:初始账户状态
alloc.json 预置了两个账户:
{ "a94f5374fce5edbc8e2a8697c15331677e6ebf0b": { "balance": "0x5ffd4878be161d74", "code": "0x", "nonce": "0xac", "storage": {} }, "0x8a8eafb1cf62bfbeb1741769dae1a9dd47996192": { "balance": "0xfeedbead", "nonce": "0x00" } }第一个账户带有空code与空storage,第二个账户仅声明balance与nonce。由于本用例不执行任何交易,这两个账户最终会以原样状态参与状态根(stateRoot)的计算——这也解释了为什么三个分叉期望文件中的stateRoot完全相同(见第四节)。
2.3 txs.json:空交易集合
txs.json 内容仅为[],表示区块内没有交易。因此输出中gasUsed = 0x0、receipts = [],txRoot与receiptsRoot都是空树的 RLP 根0x56e81f...。
三、核心命令:如何用 evm t8n 计算难度
在 cmd/evm/testdata/19/readme.md 中给出的完整命令(在cmd/evm目录下运行已构建的evm二进制):
./evm t8n --input.alloc=./testdata/19/alloc.json --input.txs=./testdata/19/txs.json --input.env=./testdata/19/env.json --output.result=stdout --state.fork=GrayGlacier命令分解如下:
| 参数 | 含义 |
|---|---|
t8n | transition子命令的别名,在 cmd/evm/main.go 中注册,用途为 "Executes a full state transition"(执行完整状态转换) |
--input.alloc | 初始状态 JSON 路径 |
--input.txs | 交易列表 JSON 路径 |
--input.env | 区块环境 JSON 路径 |
--output.result=stdout | 结果输出到标准输出而非写入文件 |
--state.fork=GrayGlacier | 指定状态转换所依据的分叉规则(即--state.fork标志对应的ForknameFlag) |
官方文档给出的完整运行日志与输出为:
[user@work evm]$ ./evm t8n --input.alloc=./testdata/19/alloc.json --input.txs=./testdata/19/txs.json --input.env=./testdata/19/env.json --output.result=stdout --state.fork=GrayGlacier INFO [03-09|10:45:26.777] Trie dumping started root=6f0588..7f4bdc INFO [03-09|10:45:26.777] Trie dumping complete accounts=2 elapsed="176.471µs" INFO [03-09|10:45:26.777] Wrote file file=alloc.json { "result": { "stateRoot": "0x6f058887ca01549716789c380ede95aecc510e6d1fdc4dbf67d053c7c07f4bdc", "txRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", "receiptsRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", "logsHash": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "receipts": [], "currentDifficulty": "0x2000000004000", "gasUsed": "0x0", "currentBaseFee": "0x500" } }日志中的三行分别对应:Trie 状态树转储开始/完成(accounts=2与 alloc.json 中的两个账户一致),以及将处理后的状态写出为alloc.json快照文件(默认输出目录为当前目录,可通过--output.basedir调整)。
四、运行结果解读:currentDifficulty 从何而来
输出result对象中的关键字段(对应ExecutionResult结构,见 cmd/evm/internal/t8ntool/execution.go 与 gen_execresult.go):
| 字段 | 值 | 解读 |
|---|---|---|
stateRoot | 0x6f0588...7f4bdc | 最终状态根,与 alloc.json 两个账户的初始状态一致(无交易改动状态) |
txRoot | 0x56e81f... | 空交易树根 |
receiptsRoot | 0x56e81f... | 空收据树根(无交易则无收据) |
logsHash/logsBloom | 空值形态 | 无日志 |
receipts | [] | 无收据 |
currentDifficulty | 0x2000000004000 | 本用例的核心结果:工具自行计算的当前区块难度 |
gasUsed | 0x0 | 无交易,Gas 用量为零 |
currentBaseFee | 0x500 | 与输入的currentBaseFee一致(1280 wei) |
对比父区块难度parentDifficulty = 0x2000000000000与计算结果currentDifficulty = 0x2000000004000:二者仅相差0x4000(即 16384)。这个微小的增量正是 Ethash 难度炸弹(difficulty bomb)在 GrayGlacier 规则下的残留贡献——由于 EIP-5133 将难度炸弹再次推迟约 100 天,在高度 13,000,000 处炸弹指数已被压低,对难度的抬升几乎可以忽略。
五、跨分叉对比:London / ArrowGlacier / GrayGlacier
本用例目录下预置了三个分叉的期望结果文件,输入完全一致(同一 env.json、alloc.json、空的 txs.json),仅--state.fork不同:
| 期望文件 | 分叉 | currentDifficulty | 相对父难度的增量 |
|---|---|---|---|
| exp_london.json | London | 0x2000000200000 | 较大 |
| exp_arrowglacier.json | ArrowGlacier | 0x2000080000000 | 最大 |
| exp_grayglacier.json | GrayGlacier | 0x2000000004000 | 最小(仅0x4000) |
三个文件中stateRoot、txRoot、receiptsRoot、logsHash、logsBloom、receipts、gasUsed、currentBaseFee完全一致,唯一差异是currentDifficulty。这说明:状态转换与交易执行结果与分叉选择无关(毕竟没有交易),而难度计算高度依赖分叉规则。另外,期望文件中的result比 readme 日志输出多了一个"requests": null字段,这是更新版本 t8n 工具的扩展字段。
难度差异的根源在于各分叉对难度炸弹的**延迟量(bomb delay)**不同。Ethash 难度公式在父难度基础上叠加时间调整项与炸弹项2^((fakeNumber / 100000) - 2),其中fakeNumber = number - bombDelay,bombDelay 越大,同一高度下炸弹贡献越小。对应关系为:
- London(EIP-3554):在高度 12,965,000 激活,延迟约 9,700,000 个区块;
- ArrowGlacier(EIP-4345):在高度 13,773,000 激活,额外延迟约 10,700,000 个区块;
- GrayGlacier(EIP-5133,即 readme 中标注的 "Eip 5133"):在高度 15,050,000 激活,在 ArrowGlacier 基础上再延迟 700,000 个区块(合计约 11,400,000,推迟约 100 天)。
主网上的激活高度可直接在 params/config.go 中查到:
LondonBlock: big.NewInt(12_965_000), ArrowGlacierBlock: big.NewInt(13_773_000), GrayGlacierBlock: big.NewInt(15_050_000),由于本用例的currentNumber = 13000000恰好介于 London 与 ArrowGlacier 激活高度之间,处于"难度炸弹重新抬头"的时间窗口,因此三个分叉在同一高度下的难度差异被清晰放大——这正是该用例被设计出来的目的:直观展示不同炸弹延迟常数对难度数值的影响。
六、底层原理:calcDifficulty 的实现细节
t8n 的难度计算实现在 cmd/evm/internal/t8ntool/execution.go 的calcDifficulty函数中,其代码注释明确写着:"Note: this method only works for ethash engine."(该方法仅适用于 Ethash 引擎)。核心逻辑如下:
func calcDifficulty(config *params.ChainConfig, number, currentTime, parentTime uint64, parentDifficulty *big.Int, parentUncleHash common.Hash) *big.Int { uncleHash := parentUncleHash if uncleHash == (common.Hash{}) { uncleHash = types.EmptyUncleHash } parent := &types.Header{ ParentHash: common.Hash{}, UncleHash: uncleHash, Difficulty: parentDifficulty, Number: new(big.Int).SetUint64(number - 1), Time: parentTime, } // ...调用 ethash.CalcDifficulty(config, currentTime, parent) 完成实际计算 }几个值得注意的实现要点:
- 合成父区块头:工具并不需要真实父区块,而是根据输入的
parentDifficulty、parentTimestamp以及currentNumber - 1合成一个最小化的父区块头,再交给 Ethash 引擎计算。 - 未提供 unclehash 的兜底:本用例的 env.json 中没有提供父区块的
parentUncleHash,此时函数将其替换为types.EmptyUncleHash。readme 中特别强调 "no provided unclehash for the parent block",指的就是这一分支。叔叔哈希会影响难度公式中的调整项(有叔叔时难度调整因子不同),因此这里显式回退到"无叔叔"的默认值,保证结果可复现。 - 分叉规则选择:最终调用 Ethash 引擎的
CalcDifficulty时,会依据--state.fork构建出的ChainConfig,判断当前区块(父高度 + 1)处于哪个分叉,从而选择对应的难度函数(London / ArrowGlacier / GrayGlacier),并套用各自不同的炸弹延迟常数。
这也解释了为什么"调用方不提供currentDifficulty也能算出难度":Ethash 难度本质上是parentDifficulty、时间戳差、区块高度与分叉常数的纯函数,t8n 只需补齐这些输入即可。
七、复现与验证:把输出和期望文件对齐
要验证本用例,可按以下步骤操作:
- 在仓库根目录构建
evm工具:go build -o evm ./cmd/evm - 进入 cmd/evm 目录,执行 readme 中的命令:
./evm t8n --input.alloc=./testdata/19/alloc.json --input.txs=./testdata/19/txs.json --input.env=./testdata/19/env.json --output.result=stdout --state.fork=GrayGlacier - 将输出的
result.currentDifficulty与 exp_grayglacier.json 中的0x2000000004000对比,并核对stateRoot、txRoot、receiptsRoot等字段完全一致。
做交叉验证时,只需替换--state.fork的值:
# London 分叉 ./evm t8n --input.alloc=./testdata/19/alloc.json --input.txs=./testdata/19/txs.json --input.env=./testdata/19/env.json --output.result=stdout --state.fork=London # ArrowGlacier 分叉 ./evm t8n --input.alloc=./testdata/19/alloc.json --input.txs=./testdata/19/txs.json --input.env=./testdata/19/env.json --output.result=stdout --state.fork=ArrowGlacier分别得到0x2000000200000与0x2000080000000,即与 exp_london.json、exp_arrowglacier.json 一致。三个分叉、三种难度、同一组状态根,一测便知。
八、实战扩展:t8n 的其他可用参数
除了本用例用到的参数外,t8n(transition)子命令在 cmd/evm/main.go 中注册了更完整的标志集,可用于更复杂的测试场景:
| 标志 | 说明 |
|---|---|
--output.basedir | 输出文件的基础目录,默认当前目录 |
--output.alloc | 将处理后的状态(alloc)写到指定文件(默认写alloc.json) |
--output.body | 输出区块体(RLP 十六进制) |
--state.chainid | 指定链 ID,影响交易签名校验与 EIP-155 行为 |
--state.reward | 指定区块奖励金额,可设为0关闭 PoW 奖励 |
--trace/--trace.tracer/--trace.tracerconfig | 启用交易级 trace,并选择内置 tracer(如callTracer、4byteTracer)或自定义 JS tracer |
--trace.nostack/--trace.nomemory/--trace.noreturndata | 控制 trace 输出内容粒度 |
借助这些参数,可以把 testdata/19 的骨架改造成自己的难度/状态转换验证脚本:例如在 env.json 中调整parentDifficulty、parentTimestamp与currentTimestamp的差值,观察时间调整因子对难度的作用;或在 txs.json 中填入真实交易,验证包含执行负载时的完整状态转换流程。
九、小结
通过 cmd/evm/testdata/19/readme.md 这个精炼的测试用例,我们可以完整掌握:
evm t8n的输入输出 JSON 契约(env / alloc / txs → result);- 当调用方省略
currentDifficulty时,工具如何基于parentDifficulty与父时间戳自动推导 Ethash 难度; - London、ArrowGlacier、GrayGlacier 三个分叉在相同高度下难度数值的差异,及其背后的难度炸弹延迟常数(EIP-3554 / EIP-4345 / EIP-5133);
- 底层 calcDifficulty 的实现细节:合成父区块头、
EmptyUncleHash兜底、"仅适用于 Ethash 引擎"的边界。
对于从事 BSC 主网同步、PoW 阶段区块回溯验证或自定义状态转换测试的开发者而言,这个用例既是 t8n 工具链的入门样板,也是一份可复现、可扩展的难度计算参考基准。
【免费下载链接】bscA BNB Smart Chain client based on the go-ethereum fork项目地址: https://gitcode.com/GitHub_Trending/bs/bsc
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考