钱包签了一条合成订单授权,并经 HTTPS RPC 送出一笔测试交易。签名都来自同一钱包私钥,为什么交易会因为账户 nonce 用过而失败,而订单授权还可能被再次兑现?这两个对象在不同的地方使用不同的消费状态:交易由执行层节点处理,链下类型化授权由部署它的应用或合约处理。EIP-712 帮助双方就类型与域达成一致,但原规范明确说它不包含重放保护。

一把私钥签两种不同的字节

选定 EIP-1559 的 0x02 类型交易,签名输入不是界面上的“转 100 wei”几个汉字。它包括 chain ID、此账户在这条链的交易 nonce、费率上限、gas limit、目的地址、数量、data 和 access list 的指定 RLP 编码,前面还有类型字节。节点先按此交易类型核对签名,再看账户余额、nonce、费用、gas 和执行规则;EIP-1559 的链 ID 必须绑定到目标链,不应仅因为椭圆曲线验签成立就忽略它。

1
2
3
4
5
6
7
8
钱包私钥
├─ 交易 type 0x02:keccak256(0x02 || RLP(chainId, accountNonce,
│ maxPriorityFee, maxFee, gas, to, value, data, accessList))
│ → 签名 (yParity, r, s) → 测试执行客户端:恢复发起者+查账户状态
│
└─ EIP-712 Order:keccak256(0x19 0x01 || domainSeparator
|| hashStruct(Order))
→ 签名 → 应用:恢复授权者+验域、金额、期限和应用 nonce 是否已用

上面的 EIP-712 domainSeparator 由应用选定的 name、version、chainId、verifyingContract 等组成;Order 结构可包含 recipient、amount、应用 nonce、deadline。类型和值要按规范的结构编码,不能自己 JSON.stringify 后随意哈希冒充标准签名。交易 nonce 与 Order nonce 即使都是整数,也各有各的持有人、读取位置和消费规则:前者与执行层账户当前交易序列有关;后者必须由具体应用或链上合约在正确域内管理。签名只证明这些字节受到相应私钥授权,不告诉验证者 nonce 已消费没有。

本地开发链的两种结论

实验使用隔离进程中的 PyEVMBackend(固定 PragueVM,从区块 0 开始,chain ID 131277322940537),不连公网、不用真钱、私钥只在内存。先给临时钱包合成测试余额,再用真实 EIP-1559 type-2 签名交易送入进程内执行客户端。收到的收据 status=1、测试接收方余额增加 100 wei,账户 nonce 从 0 变为 1;再次送完全相同原始交易因旧 nonce 被拒。可从此说明“本次本地执行”和“签名有效”分别是什么,不可推断以太坊主网已经确认或任何商店已经发货。

但是这版后端暴露了一个不能掩盖的失败:钱包针对 chainId + 1 重新签了另一笔 nonce 合法的 type-2 交易,测试后端仍接纳并执行。官方 EIP-1559 参考实现写明归一化前应核对交易签名与 chain ID;该实验的结果只能写作 wrong_chain_id_transaction_rejected_by_backend=false,链 ID 拒绝验收未通过。脚本也展示若提交方自行解析签名原始交易并与目标链 ID 比较,本地预检查会识别不同值,但这不使已失败的后端验证变成“通过”。不能拿这个实现组合证明跨链防重放,需要换一个冻结且能拒绝错误 chain ID 的执行客户端单独验证。此观察不说明所有 Ethereum 客户端都会接受错误链。

对于 EIP-712 授权,同一脚本使用成熟 eth-account 做真实类型化签名和地址恢复。本地应用策略预先固定域、用户地址与测试时钟,正确订单首次通过;原封不动重复发签名失败;保持旧签名却换 chain ID 或验证合约,会得到不同恢复地址并被域检查拒;使用正确私钥重新签一份 amount 不同但应用 nonce 相同的新授权,仍被已消费状态拒;过期授权也被拒。这一套消费策略是本地应用模型,尚没有部署或调用链上 EIP-712 合约,不能宣称某个合约已保证这些性质。

先按仓库内 examples/cryptography/README.md 完成环境准备;以下命令从仓库根目录执行。

1
2
python examples/cryptography/28_ethereum_authorization.py
python -m unittest discover -s examples/cryptography -p 'test_28*.py' -v

2026-10-06 UTC,两条命令退出码 0、1 项测试通过;“测试通过”表示程序准确报告一项预期机制未通过,而不是错误链 ID 验证过关。脚本公开的是上述布尔观测,不输出私钥、签名或无价值交易的任何可用于实网重放的资产。本地默认自动出块;收据所在区块是单实例测试历史,尚未检验重组、共识投票或外部终局性。结课 39 另有回环 HTTPS RPC 实验,不能借本篇的进程内测试冒充通信路径验收。

为什么不能把 RPC 回执当成现实履约

即便在一个正确的执行客户端,验签成功、账户 nonce 与余额检查通过、EVM 执行成功、交易被一个区块包含和达到目标确认深度仍是不同命题。对于 EIP-712 离线签名,签名恢复地址不等于某应用确实扣款;必须核对应用定义的授权作用、调用方、domain、nonce 消费和实际执行。钱包经 HTTPS 连接 RPC 时,还要验证 RPC 的 TLS 服务身份、观察所信任的链头或节点来源。HTTPS 保护这一跳,不能替链规定哪笔交易属于共同历史;相反,交易签名可经不同节点转发,但不会加密 HTTP 中的私人备注或保证订单交付。

两道带答案的练习

画图题: 在“钱包、HTTPS RPC、执行客户端、应用授权验证器”四个框里,分别标出交易 type-2 的 account nonce 和 EIP-712 Order 的应用 nonce、chain ID、verifyingContract 如何进入签名字节,又分别由谁消费。若旧 EIP-712 签名在合约 A 已被用过,合约 B 是否必然知道?

可核对答案: 钱包签 type-2 时包含链 ID 与账户当前交易 nonce,执行客户端应按目标链规则检查和消耗账户 nonce;钱包签 Order 时域包含链 ID、验证合约,具体授权结构含应用 nonce/期限,实际应用或合约在自己的域里原子消费。A 的本地消费表不会自动同步到 B;如果域合约地址不同,同一签名不能作为 B 同域授权使用,前提是 B 确实验证目标域而非忽略它。HTTPS RPC 只保护传输和服务身份。

实验变更题: 去掉 28_ethereum_authorization.py 的 message["nonce"] in consumed,再用同一已签 Order 调两次;另将错误 chain ID 那笔交易再发到此冻结测试后端。分别应如何记录结果?能否把第二项改写为“链 ID 防重放已通过”?

可核对答案: 删除应用消费检查,同一个有效且未过期的 EIP-712 签名可能被重复接受,eip712_same_signature_replay_rejected_by_application 不成立;目前这个冻结 py-evm 后端会把已正确以错误 chain ID 新签的交易也纳入本地执行,wrong_chain_id_transaction_included_by_backend=true。第二项是节点级身份隔离验收的反例,不得因 EIP-712 模型能拒错域就称执行客户端也通过了相同边界。

资料与导航