Перейти к содержимому
ecdsa.com
Python

web3.py (eth-account)

Библиотека web3.py от Ethereum Foundation, в которой подписи создаёт 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/

Совместимость вкратце

ECDSAEthereum и Bitcoin

EdDSA
none

web3.py — Python-клиент для узлов Ethereum; подпись сообщений и транзакций делегирована пакету 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, nonce по RFC 6979 (совпал с независимой эталонной реализацией подписи) и 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. Для кошельков на смарт-контрактах вместо recover_message нужен вызов контракта по ERC-1271; сообщения для входа удобнее всего отлаживать с помощью средства проверки SIWE.

Пример подписывает сообщение для входа и сравнивает восстановленный адрес — именно таким сравнением следует заменять любую побайтовую проверку подписей.

Факты и проверки, на которых они основаны

Ничего из приведённого ниже не скопировано из документации. 20 октября 2026 г. проверка content/libraries/verify/web3py/run.sh запустила web3.py (eth-account) 8.0.0 в образе контейнера python:3.14.8-slim-trixie; содержимое страницы должно совпадать с выводом проверки, а у каждого факта приведены соответствующие строки вывода.

  • Кодировка подписи ECDSA

    rsv-65

    sign_message() возвращает 65 байт: r, s и v.

    Что записала проверка
    fact ecdsa-signature-encoding = rsv-65
  • Идентификатор восстановления 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
  • Nonce в ECDSA

    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() восстановил подписанта по (r, n − s) с противоположным значением v.

    Что записала проверка
    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

    65-байтовые подписи с v = 27/28 или v = 0/1 позволили восстановить подписанта; 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

Библиотеки