Saltar al contenido
ecdsa.com
Python

web3.py (eth-account)

web3.py, de la Ethereum Foundation, que delega la firma en eth-account: firmas r‖s‖v de 65 bytes con v = 27/28, EIP-191 y RFC 6979, y, como viem pero a diferencia de ethers, recuperación a partir de firmas high-S.
Licencia
MIT
Versión comprobada
8.0.0 ·
Código fuente
github.com/ethereum/web3.py
Documentación
web3py.readthedocs.io/en/stable/

Compatibilidad de un vistazo

ECDSAEthereum y Bitcoin

Curvas ECDSA
secp256k1
EdDSA
none

web3.py es el cliente de Python para nodos de Ethereum; la firma de mensajes y de transacciones se delega en el paquete eth-account (0.14.0 con web3 8.0.0), que a su vez usa eth-keys para la aritmética de la curva. La mayor parte del código de firma importa eth_account directamente, como hace el ejemplo.

Account.sign_message(encode_defunct(text=...)) implementa personal_sign: el prefijo EIP-191 con la longitud en bytes en decimal, keccak-256, un nonce RFC 6979 (contrastado con un firmante de referencia independiente) y s en la mitad inferior, con el resultado devuelto como 65 bytes que terminan en v = 27 o 28. encode_defunct es la pieza que se suele olvidar: firmar bytes en bruto con unsafe_sign_hash omite el prefijo, y eso es precisamente lo que hace que una firma no sirva como prueba de inicio de sesión en otros sistemas.

Account.recover_message() recuperó al firmante tanto a partir de la firma high-S reflejada como de v = 0/1, así que acepta más formas que ethers: normaliza s por tu cuenta si las firmas se almacenan o se deduplican. La forma compacta EIP-2098 de 64 bytes se rechazó con Unexpected recoverable signature length: Expected 65, but got 64 bytes; expándela antes de la recuperación.

Para los datos tipados, usa encode_typed_data (EIP-712) en lugar de calcular tú mismo el hash del JSON, y comprueba con el inspector de EIP-712 qué se le pide firmar a una billetera. Las billeteras de contrato inteligente necesitan una llamada ERC-1271 al contrato en lugar de recover_message; los mensajes de inicio de sesión se depuran más fácilmente en el verificador de SIWE.

El ejemplo firma un mensaje de inicio de sesión y compara la dirección recuperada, que es la comparación que debería sustituir a cualquier comprobación byte a byte de las firmas.

Datos y las comprobaciones que los respaldan

Nada de lo que sigue está copiado de la documentación. La comprobación content/libraries/verify/web3py/run.sh ejecutó web3.py (eth-account) 8.0.0 en la imagen de contenedor python:3.14.8-slim-trixie el 20 de octubre de 2026; la página debe coincidir con lo que imprimió, y cada dato cita esas líneas.

  • Codificación de la firma ECDSA

    rsv-65

    sign_message() devuelve 65 bytes: r, s y v.

    Registrado por la comprobación
    fact ecdsa-signature-encoding = rsv-65
  • ID de recuperación v

    27-28

    v es 27 o 28.

    Registrado por la comprobación
    fact recovery-v = 27-28
  • Prefijo del mensaje

    eip-191

    El resumen es igual al keccak-256 de \x19Ethereum Signed Message:\n, la longitud en decimal y el mensaje, calculado de forma independiente.

    Registrado por la comprobación
    fact message-prefix = eip-191
  • Hash predeterminado

    keccak256

    Las firmas de mensajes usan keccak-256.

    Registrado por la comprobación
    fact ecdsa-default-hash = keccak256
  • De qué calcula el hash la llamada de firma

    hashes-message-or-digest

    sign_message() calcula el hash con el prefijo EIP-191; unsafe_sign_hash() firma un resumen de 32 bytes sin modificarlo y produjo la misma firma para ese resumen.

    Registrado por la comprobación
    fact ecdsa-prehash = hashes-message-or-digest
    evidence ecdsa-prehash: unsafe_sign_hash over the EIP-191 digest equals sign_message: True
  • Nonce de ECDSA

    rfc6979

    RFC 6979: la firma coincidió con la de una implementación de referencia independiente con s normalizado.

    Registrado por la comprobación
    fact ecdsa-nonce = rfc6979
  • Low-S al firmar

    low-s

    Ningún valor high-S entre 64 firmas de mensajes.

    Registrado por la comprobación
    fact ecdsa-low-s-sign = low-s
  • High-S al verificar

    accepts-high-s

    recover_message() recuperó al firmante a partir de (r, n − s) con v invertido.

    Registrado por la comprobación
    fact ecdsa-low-s-verify = accepts-high-s
    evidence ecdsa-low-s-verify: recover_message on (r, n - s, flipped v): the signer
  • Formas de firma aceptadas

    v-27-28v-0-1

    Las firmas de 65 bytes con v = 27/28 o v = 0/1 permitieron recuperar al firmante; la forma EIP-2098 de 64 bytes lanzó ValidationError: Unexpected recoverable signature length: Expected 65, but got 64 bytes.

    Registrado por la comprobación
    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
  • Curvas ECDSA

    secp256k1

    Solo secp256k1.

    Registrado por la comprobación
    fact ecdsa-curves = secp256k1
  • EdDSA

    none

    Sin EdDSA.

    Registrado por la comprobación
    fact eddsa = none
  • Algoritmos poscuánticos

    none

    Sin algoritmos poscuánticos.

    Registrado por la comprobación
    fact pqc = none

Uso

Firma un mensaje de inicio de sesión con la semántica de personal_sign y recupera la dirección del firmante.
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)

El arnés de pruebas de fragmentos de código ejecuta cada ejemplo tal cual en la imagen de contenedor python:3.14.8-slim-trixie, y la ejecución falla si la salida no coincide.

Configuración
pip install --quiet --root-user-action=ignore web3==8.0.0 eth-account==0.14.0
Comando
python usage.py
La salida debe coincidir (expresión regular)
signature bytes: 65 v: 2[78]\nsigner matches: True

Bibliotecas