
EIP-1559 余额检查缺陷与 evm t8n 回归测试go-ethereum testdata/12 深度解析【免费下载链接】go-ethereumGo implementation of the Ethereum protocol项目地址: https://gitcode.com/gh_mirrors/go/go-ethereum本篇技术指南以 go-ethereum 仓库中的 testdata/12 测试说明 为核心完整还原 2021 年 Ropsten 测试网上发生的一起 EIP-1559 共识问题节点在按max_fee_per_gas × gas_limit做余额预检时遗漏了转账金额value导致本应被拒绝的交易被错误地打包进区块。读者读完本文后将掌握该缺陷的复现命令、evm t8n状态转换测试工具的输入输出格式以及余额检查在 core/state_transition.go 中的底层实现逻辑并能在本地亲手运行这套回归测试。一、缺陷背景Ropsten 上的 EIP-1559 共识漏洞伦敦升级London forkEIP-1559引入了一套全新的费用市场机制每笔交易需要声明maxFeePerGas费用上限与maxPriorityFeePerGas矿工小费上限同时区块头中携带由协议动态调整的baseFee。交易实际支付的 gas 单价为effectiveGasPrice min(maxFeePerGas, baseFee maxPriorityFeePerGas)问题出在余额预检环节。以太坊在真正执行交易前必须先确认发送方账户余额足以支付“最坏情况”的扣款即gasLimit × gasPrice value。在 Ropsten 上go-ethereum 的某个实现缺陷导致执行max_fee_per_gas * gas_limit检查时没有把 value转账金额计入总需求。于是一笔余额恰好只够支付 gas 却不够支付“gas 转账金额”的交易被错误地放行进入区块造成共识不一致——这正是 testdata/12 这个回归测试所要钉死的场景。二、测试夹具拆解三个 JSON 文件该测试位于 cmd/evm/testdata/12/由三个输入文件构成它们分别描述交易的初始世界状态、待执行的交易集合与区块环境。2.1 alloc.json初始账户状态alloc.json 只定义了一个账户即交易发送方{ 0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b : { balance : 84000000, code : 0x, nonce : 0x00, storage : { 0x00 : 0x00 } } }关键参数balance84000000 wei十六进制为0x501bd00这是整场“博弈”的焦点——恰好等于gasLimit × maxFeePerGas却少于加上 value 后的总需求code0x纯 EOA 账户无合约代码nonce0x00账户尚未发出过交易。2.2 txs.json一笔 EIP-1559 动态费用交易txs.json 包含一笔type: 0x2EIP-1559 DynamicFeeTx交易[ { input : 0x, gas : 0x5208, nonce : 0x0, to : 0x1111111111111111111111111111111111111111, value : 0x20, v : 0x0, r : 0x0, s : 0x0, secretKey : 0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8, chainId : 0x1, type : 0x2, maxFeePerGas : 0xfa0, maxPriorityFeePerGas : 0x20, accessList : [] } ]各字段的十进制换算与含义字段十六进制十进制说明gas0x520821000恰好等于一笔简单转账的内在 gasintrinsic gasvalue0x2032 wei转账金额正是被旧实现遗漏的部分maxFeePerGas0xfa04000 wei愿意支付的 gas 单价上限maxPriorityFeePerGas0x2032 wei愿意支付给矿工的小费上限chainId0x11主网链 IDsecretKey——测试用私钥t8n工具用它本地推导出发送方地址0xa94f5374...并对交易签名注意v/r/s均为零t8n模式下交易签名由工具内部根据secretKey自动补齐无需外部预签名。2.3 env.json区块执行环境env.json 描述交易将要被打包进的那个区块的上下文{ currentCoinbase : 0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba, currentDifficulty : 0x020000, currentNumber : 0x01, currentTimestamp : 0x03e8, previousHash : 0xfda4419b3660e99f37e536dae1ab081c180136bb38c837a93e93d9aab58553b2, currentGasLimit : 0x0f4240, currentBaseFee : 0x20 }currentBaseFee0x2032 wei配合currentNumber: 0x01与previousHash让t8n工具确认区块处于伦敦升级之后的 EIP-1559 环境currentGasLimit0x0f42401000000足以容纳这笔 21000 gas 的交易。2.4 三份文件的协作关系t8nTransition 的缩写工具的工作模式是以alloc.json为世界初始状态以env.json为区块上下文逐一执行txs.json中的交易最终输出执行后的新世界状态与执行结果。这三个文件共同构成一个最小化的“单个区块”模拟环境非常适合构造和回归验证此类边界条件。三、复现步骤用 evm t8n 跑出拒绝结果首先在仓库根目录构建evm工具其入口与全部用法见 cmd/evm/main.go 与 cmd/evm/README.mdgo build ./cmd/evm然后执行原文档给出的复现命令在cmd/evm目录下dir./testdata/12 ./evm t8n --state.forkLondon --input.alloc$dir/alloc.json --input.txs$dir/txs.json --input.env$dir/env.json --output.allocstdout --output.resultstdout命令参数含义参数作用t8n进入状态转换transition子命令模式--state.forkLondon指定执行所依据的硬分叉规则此处启用 EIP-1559--input.alloc世界初始状态文件--input.txs待执行交易列表文件--input.env区块环境文件--output.allocstdout将执行后的账户状态输出到标准输出--output.resultstdout将执行结果输出到标准输出四、修复后的正确行为交易被拒绝在原文档记录的历史输出中该输出来自缺陷修复后的版本evm t8n给出了完整执行日志与 JSON 结果。执行日志部分INFO [03-09|10:43:12.649] rejected tx index0 hashccc996..d83435 from0xa94f5374Fce5edBC8E2a8697C15331677e6EbF0B errorinsufficient funds for gas * price value: address 0xa94f5374Fce5edBC8E2a8697C15331677e6EbF0B have 84000000 want 84000032 INFO [03-09|10:43:12.650] Trie dumping started roote05f81..6597a5 INFO [03-09|10:43:12.650] Trie dumping complete accounts1 elapsed46.393µs日志中的error字段直接给出了拒绝原因insufficient funds for gas * price value: have 84000000 want 84000032。这里的两个数字可以精确还原校验逻辑需要金额 gasLimit × maxFeePerGas value 21000 × 4000 32 84000032 账户余额 84000000 84000000 84000032 → 余额不足交易拒绝缺口恰好是value的 32 wei——这正是旧实现遗漏的部分。标准输出的 JSON 结果完整如下{ alloc: { 0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b: { balance: 0x501bd00 } }, result: { stateRoot: 0xe05f81f8244a76503ceec6f88abfcd03047a612a1001217f37d30984536597a5, txRoot: 0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421, receiptsRoot: 0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421, logsHash: 0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347, logsBloom: 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000, receipts: [], rejected: [ { index: 0, error: insufficient funds for gas * price value: address 0xa94f5374Fce5edBC8E2a8697C15331677e6EbF0B have 84000000 want 84000032 } ], currentDifficulty: 0x20000, gasUsed: 0x0, currentBaseFee: 0x20 } }对输出结果的关键解读result.rejected数组记录了被拒绝交易的下标index: 0即 txs.json 中的第一笔与拒绝原因因为交易被拒绝receipts为空、gasUsed为0x0区块没有消耗任何 gasalloc中发送方余额保持0x501bd0084000000不变状态未被修改stateRoot/txRoot/receiptsRoot与空区块的预期根一致txRoot与receiptsRoot相同均为空列表的 RLP 根从世界状态的角度确认“什么都没发生”。五、源码级原理buyGas 中的最坏情况余额检查要理解缺陷的本质需要进入余额预检的实现——core/state_transition.go 中的buyGas()函数。其注释对该约束的描述是The balance requirement is the worst-case ETH the tx may need to lock up:msg.GasLimit × max(msg.GasPrice, msg.GasFeeCap) msg.Value, plusblobGas × msg.BlobGasFeeCapunder Cancun.即余额预检必须覆盖交易可能锁定的最坏情况金额 gasLimit × max(实际价格, 费用上限) valueCancun 之后还需加上 blob 相关费用。核心代码逻辑约在 core/state_transition.go#L435-L490func (st *stateTransition) buyGas() error { mgval : new(uint256.Int).SetUint64(st.msg.GasLimit) _, overflow : mgval.MulOverflow(mgval, st.msg.GasPrice) // ... 溢出检查 ... balanceCheck : new(uint256.Int).Set(mgval) if st.msg.GasFeeCap ! nil { // 用 GasFeeCap而非实际 GasPrice重新计算 worst-case balanceCheck.SetUint64(st.msg.GasLimit) if _, overflow : balanceCheck.MulOverflow(balanceCheck, st.msg.GasFeeCap); overflow { // ... 溢出检查 ... } } if st.msg.Value ! nil { // 将转账金额加入余额需求 —— 旧实现遗漏的就是这一步 if _, overflow : balanceCheck.AddOverflow(balanceCheck, st.msg.Value); overflow { // ... 溢出检查 ... } } // ... Cancun 下的 blobGas 检查 ... if have, want : st.state.GetBalance(st.msg.From), balanceCheck; have.Cmp(want) 0 { return fmt.Errorf(%w: address %v have %v want %v, ErrInsufficientFunds, st.msg.From.Hex(), have, want) } // 扣除 gas 费用 st.state.SubBalance(st.msg.From, mgval, tracing.BalanceDecreaseGasBuy) return nil }这段实现揭示了几个关键设计点为什么用 GasFeeCap 而不是有效价格实际扣款按effectiveGasPrice min(maxFeePerGas, baseFee maxPriorityFeePerGas)计算但余额检查必须采用最坏情况max(GasPrice, GasFeeCap)否则交易在后续执行中可能因价格波动而“扣款超出余额”。在本文的测试数据中maxFeePerGas 4000远大于baseFee maxPriorityFeePerGas 64因此balanceCheck使用的是 4000 而非 64这正是“worst-case”语义的体现。value 必须显式加入即使balanceCheck已按gasLimit × gasFeeCap计算转账金额仍会进一步消耗余额必须单独叠加。修复前该步缺失导致缺口恰好为value此处 32 wei。溢出防护MulOverflow/AddOverflow系列方法确保即使恶意构造超大maxFeePerGas或value也不会产生 uint256 回绕而是以required balance exceeds 256 bits的错误拒绝。最终余额不足时函数返回包装了 core/error.go#L69-L71 中ErrInsufficientFunds的错误——该错误常量定义即为insufficient funds for gas * price value与测试日志、JSON 输出中的错误文案完全对应// ErrInsufficientFunds is returned if the total cost of executing a transaction // is higher than the balance of the users account. ErrInsufficientFunds errors.New(insufficient funds for gas * price value)六、在仓库中运行回归测试testdata/12 不仅支持手工复现也纳入了自动化的回归测试套件。cmd/evm/t8n_test.go 驱动evm t8n逐目录执行cmd/evm/testdata/下的所有用例包括本文的 12 号用例并将输出与预期结果比对。在仓库根目录运行go test ./cmd/evm/ -run TestT8n -v该测试通过internal/reexec机制以子进程方式重新执行evm主程序见 t8n_test.go 中的TestMain并注入--output.basedir等参数管理输出目录从而保证每次运行的结果可复现。只要buyGas()中价值叠加逻辑被回归破坏该用例就会立即失败防止 Ropsten 共识事故以任何形式复发。七、从缺陷到共识安全的启示回顾 testdata/12 的完整链路可以得到三点工程启示共识校验必须考虑最坏情况凡是涉及“能否进入区块”的检查都不能只看名义价格而要按交易可能锁定的最大金额gasLimit × feeCap value以及未来 blob 交易中的blobGas × blobGasFeeCap计算否则就会出现“逻辑上余额够、实际扣款不够”的共识分叉回归测试是共识缺陷的保险丝testdata/12 用三个精炼的 JSON 文件 一条命令把一笔“差 32 wei”的边界交易固化成了永久回归用例。gasCap × gasLimit恰好等于余额、加上 value 后超出的构造方式正是缺陷的最小化复现t8n 工具的价值无需启动完整节点即可精确模拟单区块状态转换输出中同时呈现状态根、交易根、回执根与被拒交易明细是开发与审计 EIP 行为的高效工具。以该测试为范本任何新的费用机制或余额校验调整例如 Cancun 的 blob 费用检查同样在buyGas()中实现都应当配套此类边界用例确保“不该打包的交易永不打包”。【免费下载链接】go-ethereumGo implementation of the Ethereum protocol项目地址: https://gitcode.com/gh_mirrors/go/go-ethereum创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考