EIP-7932 二次签名算法(Secondary Signature Algorithms):Ethereum 统一签名算法注册表与 sigrecover 预编译详解
【免费下载链接】EIPsThe Ethereum Improvement Proposal repository项目地址: https://gitcode.com/GitHub_Trending/ei/EIPs
导读
EIP-7932(Secondary Signature Algorithms)为 Ethereum 引入了一套统一的多签名算法注册表(algorithm registry)与标准化接口,使得除 secp256k1 之外的后量子(Post-Quantum,PQ)签名算法也能被用于账户地址推导与签名验证。本文以 EIP-7932 规范为主体,结合仓库中 assets/eip-7932/ 下的参考实现与测试用例,系统讲解注册表设计、算法类型规范、pubkey_to_address地址推导规则、sigrecover预编译的调用格式与 gas 模型,并给出新算法接入的完整模板,帮助读者理解如何在 EVM 层面扩展签名算法生态。
背景:为什么需要“二次签名算法”
随着量子计算的发展,一批后量子(PQ)签名算法被设计出来,但它们普遍存在明显短板:密钥体积大(超过 1KiB)、签名体积大、验证耗时长,导致其计算与存储成本都远高于当前 Ethereum 使用的 secp256k1 椭圆曲线。若直接为每种算法单独引入一套地址推导和验证逻辑,会造成大量重复且难以维护的协议代码。
EIP-7932 的解决方案是:
- 创建一个统一注册表与标准化接口,用于引入新的签名算法并基于它们推导账户地址;
- 在
SIGRECOVER_PRECOMPILE_ADDRESS地址引入一个预编译合约(precompile),负责解码这些新引入的算法。
由此,各种算法(包括未来的 PQ 算法)都可以通过单一接口被 Ethereum 使用,而无需为每种算法重写地址推导与验证框架。该提案属于 Standards Track / Core 类别,当前状态为 Draft,创建于 2025-04-12。
规范总览与关键参数
规范中的关键字(MUST、SHOULD、MAY 等)按 RFC 2119 与 RFC 8174 解释。除非显式注明,整数编码一律使用大端序(big-endian)。
| 常量 | 值 | | - | - | |SIGRECOVER_PRECOMPILE_ADDRESS|Bytes20(0x12)| |SIGRECOVER_PRECOMPILE_BASE_GAS|3000|
预编译地址被安排在0x12,静态基础 gas 为3000。注意该地址与 EIP-8051(ML-DSA 预编译) 的VERIFY_MLDSA地址0x12存在关联,后者在其规范中声明requires: 7932,体现了 EIP-7932 作为签名算法基础设施的地位。
算法规范(Algorithm Specification):一个统一的 Trait 接口
新算法(除本 EIP 规定的 secp256k1 外)必须通过独立的 EIP 来规定。每种算法类型必须指定以下字段与函数:
trait Algorithm { // The algorithm type byte ALG_TYPE: uint8, // The size of signatures. Signatures MUST be padded // to this size to be valid. Note that this does include // the ALG_TYPE byte prefix SIZE: uint32 // Get the gas cost of signing this data. This // SHOULD include a reasonable minimum and MUST // be relative to secp256k1, i.e. 0 gas is secp256k1. fn gas_cost(signing_data: Bytes) -> Uint64; // Check whether the signature is valid. For some // algorithms, this may be a no-op. This function // will always be called before `verify`. fn validate(signature: Bytes) -> None | Error; // Take the signature and signing_data and return the // public key of the signer. fn verify(signature: Bytes, signing_data: Bytes) -> Bytes | Error; // Given a public key and corresponding signature, merge them into // a valid signature info container. This function MUST NOT check that // the provided `public_key` matches the signature. fn merge_detached_signature(detached_signature: Bytes, public_key: Bytes) -> Bytes | Error; }字段语义要点:
ALG_TYPE:单字节算法类型标识,是注册表的索引键;SIZE:签名(含ALG_TYPE前缀字节)必须被填充到该固定大小才有效;gas_cost:签名数据的 gas 成本,相对 secp256k1 计算(即 secp256k1 为 0 gas 基准);validate:签名有效性预检,总是在verify之前被调用;verify:给定签名与签名数据,返回签名者公钥;merge_detached_signature:将分离签名与公钥合并为合法的签名信息容器,不得校验公钥与签名是否匹配。
规范要求,每种算法的 EIP 必须包含:针对该算法的安全分析、证明 gas 成本的基准测试(benchmarks),并且必须处理算法可能引入的可延展性(malleability)问题。完整的算法接入模板见 assets/eip-7932/template-eip.md.txt。
从公钥推导地址:pubkey_to_address
从公钥推导地址必须使用如下函数:
def pubkey_to_address(public_key: Bytes, algorithm_id: uint8) -> ExecutionAddress: if algorithm_id == 0x00: # Compatibility shim to ensure backwards compatibility return ExecutionAddress(keccak(public_key[1:])[12:]) if len(public_key) == 63: # Prevent collisions with legacy secp256k1 return ExecutionAddress(keccak(algorithm_id || 0x00 || public_key)[12:]) # with `||` being binary concatenation return ExecutionAddress(keccak(algorithm_id || public_key)[12:])规则解读:
- 算法 ID 为
0x00(secp256k1):走兼容分支,直接对公钥去掉首字节(前缀 0x04)后取 keccak 哈希的后 20 字节,与现有 Ethereum 地址推导逻辑完全一致; - 公钥长度恰为 63 字节:为防止与 legacy secp256k1 地址碰撞,在哈希前插入
algorithm_id || 0x00两个字节; - 其他情况:直接拼接
algorithm_id || public_key后取 keccak 后 20 字节。
参考实现位于 assets/eip-7932/algorithm_registry/helpers.py,其中ExecutionAddress被实现为ByteVector[20],并借助eth_hash.auto.keccak计算哈希。
算法注册表(Algorithm Registry)
注册表结构定义如下:
class AlgorithmEntry(): ALG_TYPE: uint8, SIZE: uint32, gas_cost: Callable[[Bytes], uint64], merge_detached_signature: Callable[[Bytes, Bytes], Bytes], validate: Callable[[Bytes], None | Error], verify: Callable[[Bytes, Bytes], Bytes | Error] algorithm_registry: Dict[uint8, AlgorithmEntry]- 该 EIP 使用
algorithm_registry对象来标记已包含在某个硬分叉中的算法; - 在 EIP 定稿后,可以创建一个常驻(living)EIP 来追踪各分叉当前活跃的算法;
- 算法类型
0x7F被保留为无效/缺失,所有算法类型必须小于 127。
在参考实现 assets/eip-7932/algorithm_registry/registry.py 中,AlgorithmEntry被实现为 Python 类,algorithm_registry初始为空字典{},随后通过algorithm_registry[Secp256k1.ALG_TYPE] = Secp256k1注册 secp256k1 算法。
辅助函数(Helper functions)
规范定义了三个辅助函数:
def calculate_penalty(algorithm: uint8, signing_data: Bytes) -> uint: assert algorithm in algorithm_registry algorithm = algorithm_registry[algorithm] return algorithm.gas_cost(signing_data) def validate_signature(signature: Bytes): assert len(signature) > 0 assert signature[0] in algorithm_registry algorithm = algorithm_registry[signature[0]] return algorithm.validate(signature) # This function cannot be called without prior calling `validate_signature(signature)` def verify_signature(signing_data: Bytes, signature: Bytes) -> Bytes: algorithm = algorithm_registry[signature[0]] return algorithm.verify(signature, signing_data)calculate_penalty:查询算法的动态 gas 成本(签名数据相关);validate_signature:以签名的首字节作为算法类型索引,先断言签名非空且算法已注册,再调用算法自身的validate;verify_signature:注意其调用前置条件——必须先调用validate_signature(signature)才能调用本函数,从而保证索引有效。
这些辅助函数在 assets/eip-7932/algorithm_registry/helpers.py 中有对应实现,可直接对照阅读。
首个内置算法:secp256k1
EIP-7932 内置了 secp256k1 作为基准算法:
ALG_TYPE = 0x00 SIZE = 66 SECP256K1_SIGNATURE_SIZE = SIZE - 1 def secp256k1_unpack(signature: ByteVector[SECP256K1_SIGNATURE_SIZE]) -> tuple[uint256, uint256, uint8]: r = uint256.from_bytes(signature[0:32], 'big') s = uint256.from_bytes(signature[32:64], 'big') y_parity = signature[64] return (r, s, y_parity) def secp256k1_validate(signature: ByteVector[SECP256K1_SIGNATURE_SIZE]): SECP256K1N = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141 r, s, y_parity = secp256k1_unpack(signature) assert 0 < r < SECP256K1N assert 0 < s <= SECP256K1N // 2 assert y_parity in (0, 1) def gas_cost(signing_data: Bytes) -> Uint64: # This is an adaptation from the KECCAK256 opcode if len(signing_data) == 32: return Uint64(0) else: minimum_word_size = (len(signing_data) + 31) // 32 return Uint64(30 + (6 * minimum_word_size)) def validate(signature: Bytes) -> None | Error: secp256k1_validate(signature[1:]) def verify(signature: Bytes, signing_data: Bytes) -> Bytes | Error: # Another compatibility shim to ensure passing a 32 byte hash still works. if len(signing_data) != 32: signing_data = keccak256(signing_data) ecdsa = ECDSA() recover_sig = ecdsa.ecdsa_recoverable_deserialize(signature[1:65], signature[65]) public_key = PublicKey(ecdsa.ecdsa_recover(signing_data, recover_sig, raw=True)) uncompressed = public_key.serialize(compressed=False) return uncompressed def merge_detached_signature(detached_signature: bytes, _public_key: bytes) -> bytes: # Secp256k1 uses recoverable signatures, this is a no-op. return detached_signature关键设计:
ALG_TYPE = 0x00,SIZE = 66(1 字节类型前缀 + 65 字节可恢复签名:32 字节 r + 32 字节 s + 1 字节 y_parity);validate做范围检查:0 < r < SECP256K1N且0 < s <= SECP256K1N // 2(低 s 值要求,天然抑制签名可延展性),y_parity只能是 0 或 1;gas_cost是对 KECCAK256 操作码 gas 模型的改编:签名数据恰好 32 字节时成本为 0(因为无需额外哈希),否则按30 + 6 * ceil(len/32)计费;verify兼容性垫片:若签名数据不是 32 字节,先对其做 keccak256 再恢复公钥;返回的是未压缩公钥(65 字节,含 0x04 前缀);merge_detached_signature对 secp256k1 是 no-op,因为其原生使用可恢复签名。
仓库中的完整实现见 assets/eip-7932/algorithm_registry/registry.py,与规范伪代码一一对应。
sigrecover预编译
EIP-7932 在SIGRECOVER_PRECOMPILE_ADDRESS(0x12)引入新预编译,其行为约束:
- 执行前必须收取
SIGRECOVER_PRECOMPILE_BASE_GAS(3000)静态 gas; - 成功时输出签名者的 20 字节地址,左填充至 32 字节返回;
- 失败时不得返回任何数据。
伪代码定义:
def sigrecover_precompile(input: Bytes) -> Bytes: assert len(input) >= 1 assert input[0] in algorithm_registry size = algorithm_registry[input[0]].SIZE assert len(input) > size signature = input[:size] signing_data = input[size:] charge_gas(calculate_penalty(input[0], signing_data)) # Run validate/verify function validate_signature(signature) pubkey = verify_signature(signing_data, signature) # Return address left-padded to 32 bytes return (b"\x00" * 12) + pubkey_to_address(pubkey, input[0])输入/输出格式
输入编码:algorithm_type_byte || signature(padded to SIZE) || signing_data,即:
- 首字节是算法类型;
- 紧随其后是固定
SIZE字节的签名(必须填充到该大小); - 剩余部分全部作为签名数据(signing_data)。
输出:32 字节,即0x00 × 12 + signer_address(20 bytes)。
参考实现与 gas 明细
仓库中的参考实现位于 assets/eip-7932/precompile.py,为便于测试被改造为同时返回 gas:
INVALID = b"" SIGRECOVER_BASE_GAS = 3000 def sigrecover_precompile(input: bytes) -> tuple[bytes, int]: gas = SIGRECOVER_BASE_GAS try: assert len(input) >= 1 assert input[0] in registry.algorithm_registry size = registry.algorithm_registry[input[0]].SIZE assert len(input) > size signature = input[:size] signing_data = input[size:] gas += helpers.calculate_penalty(input[0], signing_data) # Run validate/verify function helpers.validate_signature(signature) pubkey = helpers.verify_signature(signing_data, signature) # Return address left-padded to 32 bytes and gas return ((b"\x00" * 12) + helpers.pubkey_to_address(pubkey, input[0]), gas) except AssertionError as _: return (INVALID, gas)总 gas = 3000(基础静态 gas)+ calculate_penalty 返回的动态部分。对于 secp256k1:当签名数据为 32 字节时总 gas 为 3000;当签名数据非 32 字节时,总 gas 为3000 + 30 + 6 * ceil(len/32)。所有失败路径(断言失败)均返回空数据b"",体现了规范“失败时不返回任何数据”的要求。
测试用例:验证预编译行为的金标准
测试文件 assets/eip-7932/precompile_test_cases.py 以"input data" -> ("output", "gas_charged")的形式覆盖了关键场景:
| 输入 | 期望输出 | 期望 gas | | - | - | - | |b""(无数据) |INVALID(空) | 3000 | |b"\x7F"(保留的无效算法) |INVALID| 3000 | |b"\x7F" + 65×0x01 + 32×0x00(无效算法 + 数据) |INVALID| 3000 | |b"\x00"(secp256k1 无数据) |INVALID| 3000 | |b"\x00" + 64×0xFE + 32×0x00(数据过短) |INVALID| 3036 | |b"\x00" + 67×0xFE + 32×0x00(签名超长) |INVALID| 3042 | |b"\x00" + 65×0xFE + 32×0x00(无效签名) |INVALID| 3000 | |b"\x00" + zero_sig + 32×0x00(有效签名) |signer_address| 3000 | |b"\x00" + 65×0xFE + 30×0x00(非 32 字节数据 + 无效签名) |INVALID| 3036 | |b"\x00" + deadbeef_sig + deadbeef(非 32 字节数据 + 有效签名) |signer_address| 3036 |
测试使用固定私钥1f7627096fa44f0b850f5d9a859d271723ee856e526b947d0d4b011168bdcac1,对应的期望地址为d3eF791e8a9c9BD26787D262e66e673FE8E7262A。从测试数据可以精确验证:
- 无效算法
0x7F在任何输入下都失败并只收 3000 基础 gas; - 数据长度为 30 字节时,secp256k1 的 penalty 为
30 + 6 * 1 = 36,总 gas 为 3036; - 数据长度为 32 字节时 penalty 为 0,总 gas 恒为 3000;
- 非 32 字节的签名数据(如
deadbeef)会被先 keccak 哈希再验签,但 gas 仍按原始数据长度计费。
该测试文件可以独立运行(python precompile_test_cases.py),输出Test cases pass表示全部断言通过,是理解预编译行为最直接的入口。
如何新增一种算法:模板 EIP 实战
新算法必须通过独立 EIP 引入,模板见 assets/eip-7932/template-eip.md.txt。模板以“Generic algorithm”为例展示了必须填写的 5 个要素:
ALG_TYPE = 0xFA SIZE = 128 def gas_cost(signing_data: Bytes) -> Uint64: return Uint64(128 + len(signing_data)) def validate(signature: Bytes) -> None | Error: # ... # Simple cryptography here # ... return None def verify(signature: Bytes, signing_data: Bytes) -> Bytes | Error: # ... # Complicated cryptography here # ... return public_key def merge_detached_signature(detached_signature: bytes, public_key: bytes) -> bytes: # ... # Either concatenation or a no-op here # ... return detached_signature + public_key模板 EIP 头部需要声明requires: 7932(见 assets/eip-7932/template-eip.md.txt),并在 Specification 中声明“This EIP defines a new EIP-7932 algorithmic type”。
仓库中已有两个真实示例可对照学习:
- EIP-8030:P256 算法支持——为 P256(secp256r1)定义
ALG_TYPE = 0x01、SIZE = 129,其 gas 模型以 EIP-7951 预编译成本减去 3000(secp256k1 基准)为起点,是“gas 相对 secp256k1”要求的直接体现; - EIP-8051:ML-DSA 预编译——引入后量子 ML-DSA 验签预编译(地址
0x12/0x13),并声明requires: 7932,是后量子算法接入本框架的代表案例。
设计权衡(Rationale)
与 ERC-4337 的互操作
EIP-7932 的早期草案曾与 ERC-4337 存在竞争关系,但当前版本通过 sigrecover 预编译对 ERC-4337 提供支持:任何 ERC-4337 实现都可以为给定私钥复用同一套签名验证逻辑与地址推导逻辑,且与具体推导地址的算法无关。
为何用预编译而非原生 EVM 代码
使用预编译可以让非 EVM 进程(例如交易层的签名验证)在不进入 EVM 的情况下直接访问注册表,从而在协议更底层的地方完成签名校验。
不透明(Opaque)的signature类型
每种算法都有独特属性(如是否支持签名恢复、密钥大小等),因此签名对象需要容纳每种签名及潜在恢复信息的全部排列组合。一个由算法定义大小的字节数组(bytearray)即可实现这一目标——这正是SIZE字段存在的意义。
向后兼容性
EIP-7932不修改任何现有逻辑,不引入任何向后兼容问题:secp256k1 的地址推导保持原样(0x00兼容分支),预编译0x12为新增地址,注册表只增不改。
安全注意事项
允许单一账户通过更多方式推导地址,可能在整体上降低该账户的安全性;不过,攻击者需要穷举所有算法所需的处理能力提升在一定程度上缓解了该风险。即便如此,添加新算法仍需充分讨论,确保网络安全性不受损害。正因如此,规范强制要求每种新算法必须附带安全分析、gas 基准测试,并处理签名可延展性问题——这些要求也正是后量子算法大规模落地前必须跨越的门槛。
延伸阅读
- 规范原文:EIPS/eip-7932.md
- 参考实现:
- 注册表与 secp256k1:assets/eip-7932/algorithm_registry/registry.py
- 地址推导与辅助函数:assets/eip-7932/algorithm_registry/helpers.py
- 预编译实现:assets/eip-7932/precompile.py
- 测试用例:assets/eip-7932/precompile_test_cases.py
- 新算法接入模板:assets/eip-7932/template-eip.md.txt
- 依赖与配套提案:EIP-8030(P256 算法类型)、EIP-8051(ML-DSA 预编译)
【免费下载链接】EIPsThe Ethereum Improvement Proposal repository项目地址: https://gitcode.com/GitHub_Trending/ei/EIPs
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考