YubiHSM2签名以太坊(ETH)交易无效问题排查求助
YubiHSM2 以太坊交易签名问题排查与解决
一、公钥格式选择:X962非压缩点是正确选项
以太坊地址生成逻辑要求使用非压缩格式的EC公钥(X962 UncompressedPoint):
- 你当前的代码是正确的:取X962非压缩点公钥,去掉开头的
0x04标识位,对剩余字节做Keccak-256哈希,取最后20位作为地址。 - 注释中的DER编码(SubjectPublicKeyInfo格式)是公钥的通用存储/传输格式,包含ASN.1结构头,无法直接用于以太坊地址计算,所以不需要切换。
二、交易签名无效的核心原因及修复方案
你遇到的invalid sender、公钥不匹配等问题,根源是签名流程的几个关键错误,逐一修复如下:
1. 错误的签名哈希处理
以太坊要求签名的是未签名交易的Keccak-256哈希,但你的代码中对已经哈希过的transaction_hash又做了一次SHA3-256哈希,导致签名对象错误:
# 错误写法:重复哈希 signature = key.sign_ecdsa(transaction_hash, hashes.SHA3_256()) # 正确写法:直接签原始交易哈希(Web3已经做了Keccak-256哈希),不需要额外哈希算法 signature = key.sign_ecdsa(transaction_hash)
注意:YubiHSM的
sign_ecdsa方法如果传入哈希算法参数,会对输入数据先做对应哈希再签名;而serializable_transaction.hash()已经返回了Keccak-256的结果,重复哈希会导致签名对象完全错误。
2. 手动设置v值不符合EIP-155规范
EIP-155之后,以太坊的v值由链ID推导,不再是固定的27/28:
- 公式:
v = chainId * 2 + 35 + recovery_id,其中recovery_id是签名恢复ID(0或1) - 你的链ID是4,所以合法v值应为
4*2+35=43或4*2+36=44,而非27/28
正确做法:从签名中推导recovery_id,再计算v值
# 解码DER签名得到r和s r, s = ecdsa.util.sigdecode_der(signature, ecdsa.SECP256k1.generator.order()) # 检查s是否为低阶(以太坊要求s <= 曲线阶/2) curve_order = ecdsa.SECP256k1.generator.order() if s > curve_order // 2: s = curve_order - s # 尝试恢复公钥,获取recovery_id(0或1) from ecdsa import VerifyingKey, SECP256k1 recovery_id = None vk_candidates = VerifyingKey.from_public_key_recovery(transaction_hash, (r, s), SECP256k1) for i, vk in enumerate(vk_candidates): if vk.to_string() == unindexPub: recovery_id = i break if recovery_id is None: raise ValueError("签名无法匹配本地公钥") # 计算符合EIP-155的v值 v = transaction['chainId'] * 2 + 35 + recovery_id
3. 签名的s值需满足低阶要求
以太坊规范要求签名的s值必须是曲线阶的一半以内,若YubiHSM返回的s是高阶值,必须用曲线阶减去s得到低阶值,否则会被节点判定为无效签名。
4. 修复后的完整签名流程代码
transaction = { 'to': Web3.toChecksumAddress('0x785AB1daE1b0Ee3f2412aCF55e4153A9517b07e1'), 'gas': 21000, 'gasPrice': Web3.toWei(5, 'gwei'), 'value': 1, 'nonce': web3.eth.getTransactionCount(address), # 不要手动设置nonce,用链上查询值 'chainId': 4, } serializable_transaction = serializable_unsigned_transaction_from_dict(transaction) transaction_hash = serializable_transaction.hash() # 直接签交易哈希,不额外哈希 signature = key.sign_ecdsa(transaction_hash) r, s = ecdsa.util.sigdecode_der(signature, ecdsa.SECP256k1.generator.order()) # 处理s值为低阶 curve_order = ecdsa.SECP256k1.generator.order() if s > curve_order // 2: s = curve_order - s # 恢复公钥并获取recovery_id from ecdsa import VerifyingKey, SECP256k1 recovery_id = None vk_candidates = VerifyingKey.from_public_key_recovery(transaction_hash, (r, s), SECP256k1) for i, vk in enumerate(vk_candidates): if vk.to_string() == unindexPub: recovery_id = i break if recovery_id is None: raise ValueError("签名无法匹配本地公钥") # 计算v值 v = transaction['chainId'] * 2 + 35 + recovery_id # 编码并发送交易 encoded_transaction = encode_transaction(serializable_transaction, vrs=(v, r, s)) tx_hash = web3.eth.sendRawTransaction(encoded_transaction) print(f"交易发送成功,哈希:{tx_hash.hex()}")
额外注意:nonce值不要手动设置
你代码中手动设置nonce=1,如果链上该地址的nonce已经不是1,会导致insufficient gas或交易失败,正确做法是用web3.eth.getTransactionCount(address)获取当前nonce。
内容的提问来源于stack exchange,提问作者user3554230
相关产品推荐
相关产品推荐

