跳到主要内容
ecdsa.com
Python

web3.py (eth-account)

以太坊基金会的 web3.py,签名由 eth-account 完成:65 字节的 r‖s‖v 签名(v = 27/28),EIP-191 和 RFC 6979——而且与 viem 一样、与 ethers 不同,它能从 high-S 签名中恢复出签名者。
许可证
MIT
检查的版本
8.0.0 ·
源代码
github.com/ethereum/web3.py
文档
web3py.readthedocs.io/en/stable/

兼容性概览

ECDSA以太坊与比特币

ECDSA 曲线
secp256k1
EdDSA
none

web3.py 是以太坊节点的 Python 客户端;消息和交易的签名交给 eth-account 包完成(0.14.0,搭配 web3 8.0.0),而它又依靠 eth-keys 完成曲线运算。大多数签名代码会像示例那样直接导入 eth_account。

Account.sign_message(encode_defunct(text=...)) 实现了 personal_sign:带十进制字节长度的 EIP-191 前缀、keccak-256、RFC 6979 nonce(已与一个独立的参考签名实现核对)以及位于低半区的 s,返回 65 字节,末尾为 v = 27 或 28。encode_defunct 是人们容易忘记的一环:用 unsafe_sign_hash 对原始字节签名会跳过前缀,而这恰恰会让签名无法在别处用作登录证明。

Account.recover_message() 既能从镜像得到的 high-S 签名中恢复出签名者,也能从 v = 0/1 的签名中恢复,因此它接受的形式比 ethers 更多——如果要存储签名或对签名去重,需要自行对 s 做规范化。64 字节的 EIP-2098 紧凑形式被拒绝,报错 Unexpected recoverable signature length: Expected 65, but got 64 bytes;恢复之前要先把它展开。

对类型化数据应使用 encode_typed_data(EIP-712),而不是自己对 JSON 计算哈希;可以用 EIP-712 查看器检查钱包被要求签署的内容。智能合约钱包需要通过 ERC-1271 调用合约,而不是使用 recover_message;登录消息在 SIWE 验证工具中最容易调试。

示例对一条登录消息签名,并比较恢复出的地址——任何逐字节比较签名的检查都应当换成这种比较。

事实及其依据的检查

以下内容均非抄自文档。检查脚本 content/libraries/verify/web3py/run.sh 于2026年10月20日在容器镜像 python:3.14.8-slim-trixie 中运行了 web3.py (eth-account) 8.0.0;本页内容必须与它的输出一致,每条事实都列出了对应的输出行。

  • ECDSA 签名编码

    rsv-65

    sign_message() 返回 65 字节:r、s 和 v。

    检查记录
    fact ecdsa-signature-encoding = rsv-65
  • 恢复 ID v

    27-28

    v 为 27 或 28。

    检查记录
    fact recovery-v = 27-28
  • 消息前缀

    eip-191

    摘要与独立计算出的 keccak-256 一致,计算对象是 \x19Ethereum Signed Message:\n、十进制长度和消息本身。

    检查记录
    fact message-prefix = eip-191
  • 默认哈希

    keccak256

    消息签名使用 keccak-256。

    检查记录
    fact ecdsa-default-hash = keccak256
  • 签名调用对哪些内容做哈希

    hashes-message-or-digest

    sign_message() 对加上 EIP-191 前缀的消息计算哈希;unsafe_sign_hash() 对 32 字节的摘要原样签名,对该摘要生成的签名与前者完全相同。

    检查记录
    fact ecdsa-prehash = hashes-message-or-digest
    evidence ecdsa-prehash: unsafe_sign_hash over the EIP-191 digest equals sign_message: True
  • ECDSA 的 nonce

    rfc6979

    RFC 6979:签名与一个独立参考实现的输出(s 经过规范化)一致。

    检查记录
    fact ecdsa-nonce = rfc6979
  • 签名时的 low-S

    low-s

    64 个消息签名中没有 high-S 值。

    检查记录
    fact ecdsa-low-s-sign = low-s
  • 验证时的 high-S

    accepts-high-s

    recover_message() 从翻转 v 后的 (r, n − s) 中恢复出了签名者。

    检查记录
    fact ecdsa-low-s-verify = accepts-high-s
    evidence ecdsa-low-s-verify: recover_message on (r, n - s, flipped v): the signer
  • 接受的签名形式

    v-27-28v-0-1

    v = 27/28 或 v = 0/1 的 65 字节签名都能恢复出签名者;64 字节的 EIP-2098 形式抛出 ValidationError: Unexpected recoverable signature length: Expected 65, but got 64 bytes。

    检查记录
    fact signature-input-shapes = v-27-28, v-0-1
    evidence signature-input-shapes: v-27-28: recovers the signer
    evidence signature-input-shapes: v-0-1: recovers the signer
    evidence signature-input-shapes: eip-2098: ValidationError: Unexpected recoverable signature length: Expected 65, but got 64 bytes
  • ECDSA 曲线

    secp256k1

    仅支持 secp256k1。

    检查记录
    fact ecdsa-curves = secp256k1
  • EdDSA

    none

    不支持 EdDSA。

    检查记录
    fact eddsa = none
  • 后量子算法

    none

    不支持后量子算法。

    检查记录
    fact pqc = none

用法

按 personal_sign 语义对登录消息签名,并恢复出签名者的地址。
from eth_account import Account
from eth_account.messages import encode_defunct

account = Account.create()
message = encode_defunct(text="Sign in to example.com\nNonce: 8f2c1a")

# personal_sign: EIP-191 prefix + keccak-256, RFC 6979, low-S, v = 27 or 28.
signed = account.sign_message(message)

# Recover and compare addresses; checksummed strings compare exactly.
signer = Account.recover_message(message, signature=signed.signature)
print("signature bytes:", len(signed.signature), "v:", signed.v)
print("signer matches:", signer == account.address)

代码片段测试工具会在容器镜像 python:3.14.8-slim-trixie 中原样运行每个示例,只要输出不匹配,运行就会失败。

环境准备
pip install --quiet --root-user-action=ignore web3==8.0.0 eth-account==0.14.0
命令
python usage.py
输出必须匹配(正则表达式)
signature bytes: 65 v: 2[78]\nsigner matches: True

库