You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.25 06:36:32