本文へスキップ
ecdsa.com
Python

web3.py (eth-account)

Ethereum Foundation の web3.py です。署名は eth-account が担い、v = 27/28 の 65 バイトの r‖s‖v 署名、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 は Ethereum ノード用の Python クライアントです。メッセージとトランザクションの署名は eth-account パッケージ(web3 8.0.0 では 0.14.0)に任されており、このパッケージは曲線の演算に eth-keys を使います。署名を行うコードの多くは、サンプルと同様に eth_account を直接インポートします。

Account.sign_message(encode_defunct(text=...)) は personal_sign を実装しています。10 進数のバイト長を含む EIP-191 のプレフィックス、keccak-256、RFC 6979 の nonce(独立した参照実装の署名と照合済み)、下半分に収まる s を使い、末尾が v = 27 または 28 の 65 バイトを返します。忘れられがちなのが encode_defunct です。unsafe_sign_hash で生のバイト列に署名するとプレフィックスが付かず、まさにそれが原因で、その署名はほかの場所でログインの証明として使えなくなります。

Account.recover_message() は、s を n − s に置き換えた high-S 署名からも、v = 0/1 の署名からも署名者を復元しました。つまり ethers より多くの形式を受け入れるため、署名を保存したり重複排除したりする場合は、自分で s を正規化してください。64 バイトの EIP-2098 コンパクト形式は Unexpected recoverable signature length: Expected 65, but got 64 bytes で拒否されました。復元の前に展開してください。

型付きデータには、JSON を自分でハッシュするのではなく encode_typed_data(EIP-712)を使ってください。ウォレットに何への署名が求められているかは EIP-712 インスペクターで確認できます。スマートコントラクトウォレットでは、recover_message の代わりにコントラクトへの ERC-1271 呼び出しが必要です。ログインメッセージは SIWE 検証ツールを使うと最も簡単にデバッグできます。

サンプルはログインメッセージに署名し、復元したアドレスを比較します。署名をバイト単位で比較する処理は、すべてこの比較に置き換えるべきです。

事実とその根拠となるチェック

以下の内容はドキュメントからの転記ではありません。2026年10月20日に、チェック content/libraries/verify/web3py/run.sh がコンテナイメージ python:3.14.8-slim-trixie 内で web3.py (eth-account) 8.0.0 を実行しました。このページの内容はその出力と一致していなければならず、各事実には該当する出力行を示しています。

  • ECDSA 署名のエンコーディング

    rsv-65

    sign_message() は r、s、v からなる 65 バイトを返します。

    チェックによる記録
    fact ecdsa-signature-encoding = rsv-65
  • リカバリ ID v

    27-28

    v は 27 または 28 です。

    チェックによる記録
    fact recovery-v = 27-28
  • メッセージのプレフィックス

    eip-191

    ダイジェストは、\x19Ethereum Signed Message:\n、10 進数で表した長さ、メッセージを連結して独立に計算した keccak-256 と一致します。

    チェックによる記録
    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

ライブラリ