Pular para o conteúdo
ecdsa.com
Python

web3.py (eth-account)

O web3.py, da Ethereum Foundation, que delega a assinatura ao eth-account: assinaturas r‖s‖v de 65 bytes com v = 27/28, EIP-191 e RFC 6979 — e, como o viem e ao contrário do ethers, recuperação a partir de assinaturas high-S.
Licença
MIT
Versão verificada
8.0.0 ·
Código-fonte
github.com/ethereum/web3.py
Documentação
web3py.readthedocs.io/en/stable/

Visão geral da compatibilidade

ECDSAEthereum e Bitcoin

Curvas ECDSA
secp256k1
EdDSA
none

O web3.py é o cliente Python para nós Ethereum; a assinatura de mensagens e de transações fica a cargo do pacote eth-account (0.14.0 com web3 8.0.0), que por sua vez usa o eth-keys para a aritmética da curva. A maior parte do código de assinatura importa eth_account diretamente, como faz o exemplo.

Account.sign_message(encode_defunct(text=...)) implementa o personal_sign: o prefixo EIP-191 com o comprimento em bytes em decimal, keccak-256, um nonce RFC 6979 (conferido com um signatário de referência independente) e s na metade inferior, retornados como 65 bytes terminados em v = 27 ou 28. O encode_defunct é a parte que as pessoas esquecem: assinar bytes brutos com unsafe_sign_hash pula o prefixo, e é exatamente isso que torna uma assinatura inútil como prova de login em outros lugares.

Account.recover_message() recuperou o signatário a partir da assinatura high-S espelhada e também a partir de v = 0/1, então aceita mais formas do que o ethers — normalize o s você mesmo se as assinaturas forem armazenadas ou deduplicadas. A forma compacta EIP-2098 de 64 bytes foi rejeitada com Unexpected recoverable signature length: Expected 65, but got 64 bytes; expanda-a antes da recuperação.

Para dados tipados, use encode_typed_data (EIP-712) em vez de calcular você mesmo o hash do JSON, e confira no inspetor de EIP-712 o que a carteira está sendo solicitada a assinar. Carteiras de contrato inteligente exigem uma chamada ERC-1271 ao contrato em vez de recover_message; mensagens de login são mais fáceis de depurar no verificador de SIWE.

O exemplo assina uma mensagem de login e compara o endereço recuperado — a comparação que deveria substituir qualquer verificação byte a byte de assinaturas.

Os fatos e as verificações por trás deles

Nada abaixo foi copiado da documentação. A verificação content/libraries/verify/web3py/run.sh executou web3.py (eth-account) 8.0.0 na imagem de contêiner python:3.14.8-slim-trixie em 20 de outubro de 2026; a página precisa corresponder ao que a verificação imprimiu, e cada fato lista essas linhas.

  • Codificação da assinatura ECDSA

    rsv-65

    sign_message() retorna 65 bytes: r, s e v.

    Registrado pela verificação
    fact ecdsa-signature-encoding = rsv-65
  • ID de recuperação v

    27-28

    v é 27 ou 28.

    Registrado pela verificação
    fact recovery-v = 27-28
  • Prefixo da mensagem

    eip-191

    O digest é igual ao keccak-256 de \x19Ethereum Signed Message:\n, do comprimento em decimal e da mensagem, calculado de forma independente.

    Registrado pela verificação
    fact message-prefix = eip-191
  • Hash padrão

    keccak256

    As assinaturas de mensagens usam keccak-256.

    Registrado pela verificação
    fact ecdsa-default-hash = keccak256
  • Entrada do hash na chamada de assinatura

    hashes-message-or-digest

    sign_message() calcula o hash com o prefixo EIP-191; unsafe_sign_hash() assina um digest de 32 bytes sem alterá-lo e produziu a mesma assinatura para esse digest.

    Registrado pela verificação
    fact ecdsa-prehash = hashes-message-or-digest
    evidence ecdsa-prehash: unsafe_sign_hash over the EIP-191 digest equals sign_message: True
  • Nonce do ECDSA

    rfc6979

    RFC 6979: a assinatura coincidiu com a de uma implementação de referência independente, com s normalizado.

    Registrado pela verificação
    fact ecdsa-nonce = rfc6979
  • Low-S ao assinar

    low-s

    Nenhum valor high-S em 64 assinaturas de mensagens.

    Registrado pela verificação
    fact ecdsa-low-s-sign = low-s
  • High-S ao verificar

    accepts-high-s

    recover_message() recuperou o signatário a partir de (r, n − s) com o v invertido.

    Registrado pela verificação
    fact ecdsa-low-s-verify = accepts-high-s
    evidence ecdsa-low-s-verify: recover_message on (r, n - s, flipped v): the signer
  • Formatos de assinatura aceitos

    v-27-28v-0-1

    Assinaturas de 65 bytes com v = 27/28 ou v = 0/1 permitiram recuperar o signatário; a forma EIP-2098 de 64 bytes gerou ValidationError: Unexpected recoverable signature length: Expected 65, but got 64 bytes.

    Registrado pela verificação
    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

    Apenas secp256k1.

    Registrado pela verificação
    fact ecdsa-curves = secp256k1
  • EdDSA

    none

    Sem EdDSA.

    Registrado pela verificação
    fact eddsa = none
  • Algoritmos pós-quânticos

    none

    Sem algoritmos pós-quânticos.

    Registrado pela verificação
    fact pqc = none

Uso

Assine uma mensagem de login com a semântica do personal_sign e recupere o endereço do signatário.
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)

O executor de trechos de código roda cada exemplo literalmente na imagem de contêiner python:3.14.8-slim-trixie, e a execução falha se a saída não corresponder.

Configuração
pip install --quiet --root-user-action=ignore web3==8.0.0 eth-account==0.14.0
Comando
python usage.py
A saída deve corresponder a (expressão regular)
signature bytes: 65 v: 2[78]\nsigner matches: True

Bibliotecas