# [ERC8111] ECDSA署名を64バイトに圧縮するBound Signaturesの仕組みを理解しよう！

> ERC8111とは、ECDSA署名のvパラメータを27に固定することで、署名を96バイトから64バイトに圧縮する仕組みのことです。

- URL: https://cardene.dev/articles/0f0930075300bc05d254
- 公開日: 2026-03-13
- 更新日: 2026-03-13
- トピック: Blockchain
- タグ: ethereum, solidity, smartcontract, ブロックチェーン

## はじめに

『DApps開発入門』という本や色々記事を書いているかるでねです。

https://amzn.asia/d/gxvJ0Pw

今回は、ECDSA署名のy-parityパラメータ（`v`）を固定することで署名を96バイトから64バイトに圧縮し、Signature Malleabilityも排除する仕組みを提案している**ERC8111**についてまとめていきます！

:::message
以下にまとめられているものを解説しながらまとめていきます。
:::

https://eips.ethereum.org/EIPS/eip-8111

他にも様々なEIP・BIP・SLIP・CAIP・ENSIP・RFC・ACPについてまとめています。

https://cardene.notion.site/ERC-EIP-2a03fa3ea33d43baa9ed82288f98d4a9?pvs=4

## 概要

**ERC8111**とは、ECDSA署名の`v`パラメータを`27`に固定することで、署名を96バイトから64バイトに圧縮する仕組みのことです。

Ethereumで使われるECDSA署名は`v`、`r`、`s`の3つのパラメータで構成され、Solidity ABIエンコーディングでは96バイトを占めます。
しかし`v`の値は`27`か`28`の2通りしかなく、この1ビットの情報のために32バイトの領域が消費されていました。
**ERC8111**は`v`を常に`27`に固定し、必要に応じて`s`の値を変換することで、`r`と`s`の64バイトだけで署名を表現できるようにします。
さらにこの方式では、同じメッセージに対して複数の有効な署名が存在するSignature Malleability（署名展性）の問題も同時に排除されます。

#### ECDSA署名の基礎

ECDSA（Elliptic Curve Digital Signature Algorithm）は、楕円曲線暗号を用いたデジタル署名アルゴリズムです。
Ethereumではsecp256k1という楕円曲線を使用しており、署名は以下の3つのパラメータで構成されます。

:::message
**secp256k1** は、Bitcoinの設計で採用されたのを機に広く普及した楕円曲線です。「sec」は標準化団体Standards for Efficient Cryptography、「p」は素体（prime field）、「256」はビット長、「k1」はKoblitz曲線の第1番という意味です。Ethereumもこの曲線を採用しており、秘密鍵・公開鍵・署名はすべてこの曲線上の数学に基づいています。
:::

| パラメータ | サイズ | 説明 |
|-----------|--------|------|
| `r` | 32バイト | 楕円曲線上の点のx座標。署名生成時にランダムに選ばれた点から計算される。 |
| `s` | 32バイト | 署名のチャレンジ解。秘密鍵とメッセージのハッシュから計算される。 |
| `v` | 32バイト（ABI） | y-parity（y座標の偶奇）を示す値。`27`または`28`。楕円曲線の対称性から、1つのx座標に対して2つのy座標が存在するため、どちらの解であるかを示す。 |

:::message
**y-parity（y座標の偶奇）** について補足します。楕円曲線上の点は (x, y) の座標で表されますが、同じ x 座標に対して y 座標が2つ存在します（曲線がx軸について対称なため）。`v` はそのどちらの y 座標を使うかを示す1ビットの情報で、偶数なら `27`、奇数なら `28` となります。署名を検証する側はこの情報を使って公開鍵（アドレス）を一意に復元します。
:::

Solidity ABIエンコーディングでは各パラメータが256ビット（32バイト）にアラインされるため、合計96バイトになります。
しかし、`v`の値は実質的に1ビットの情報しか持っていません。

#### Signature Malleability（署名展性）

署名展性とは、ある署名から別の有効な署名を計算できてしまう性質のことです。
ECDSAでは、署名`(v, r, s)`が有効な場合、`(v', r, s')`も有効な署名になります。
ここで`v'`は`v`を反転した値（`27`なら`28`、`28`なら`27`）、`s'`はsecp256k1の位数`N`から`s`を引いた値です。

この性質が問題になるのは、署名のハッシュを使ってトランザクションの一意性を判定する場面です。
例えば、マルチシグウォレットで「この署名はすでに使用済みか」をチェックする時、攻撃者が署名を変換して別の有効な署名を作成すると、同じ承認を二重に使用される可能性があります。

:::message alert
**具体的な攻撃シナリオ**

コントラクトが「署名のハッシュ → 使用済みフラグ」をmappingで管理していると仮定します。攻撃者が元の署名 `(v=27, r, s)` のハッシュを使用済みにされた後、数学的に等価な署名 `(v=28, r, N-s)` を使って同じ承認を再送できます。両者はハッシュが異なるため、使用済みチェックをすり抜けてしまいます。これがSignature Malleabilityによる二重使用攻撃です。
:::

## 動機

#### 署名サイズとガスコストの問題

Ethereumのスマートコントラクトでは、ECDSA署名はcalldata（コントラクト関数を呼び出す時に一緒に送られるバイトデータ。引数の実体が格納される領域）として渡されます。
calldataの各バイトにはガスコストがかかるため、署名のサイズは直接コストに影響します。
96バイトの署名を64バイトに圧縮できれば、署名を受け取るすべてのコントラクトでガスコストを削減できます。

#### 既存の圧縮方式の課題

署名を64バイトに圧縮する方式としては、すでに**ERC2098**が存在します。
**ERC2098**は`s`の最上位ビット（MSB）にy-parityの値を格納するアプローチを取っています。
**EIP2**によりcanonical signature（正規化された署名。`s` の値がsecp256k1の位数 `N` の半分以下に制限されたもの）では`s`の最上位ビットが常に0になるため、このビットをy-parityの格納に流用できます。

しかし**ERC2098**には課題があります。
圧縮された署名をスマートコントラクトで使う時、`ecrecover`プリコンパイルに渡す前に、最上位ビットから`v`を取り出し、`s`から最上位ビットをマスクする「アンパック処理」が必要です。
このアンパック処理にはガスが余分にかかります。

:::message
**ecrecover プリコンパイル** とは、Ethereumに組み込まれた特殊な低レベル関数です。通常のSolidityコードより高速かつ安価に実行できるようEVMに直接実装されており、アドレス `0x0000...0001` に配置されています。`ecrecover(digest, v, r, s)` を呼び出すと、署名を生成した秘密鍵に対応するアドレスを返します。Solidityの `ecrecover()` はこのプリコンパイルを呼び出す組み込み関数です。
:::

以下の図は、**ERC2098**と**ERC8111**の圧縮方式の違いを示しています。

![ERC2098 vs ERC8111 署名圧縮方式の比較](/images/qiita-0c8dcb92-bb42-4395-bd21-0d9bb7a9d488.webp)

#### Bound Signaturesの解決策

**ERC8111**のBound Signaturesは異なるアプローチを取ります。
`v`を常に`27`に固定し、元の署名が`v = 28`の場合は`s`をsecp256k1の位数`N`から引いた値に変換します。
この方式では、圧縮された署名がそのまま`ecrecover(digest, 27, r, s)`の入力として使えるため、アンパック処理が不要です。
そのため**ERC2098**と比べてガス効率が良くなります。

## 仕様

#### コントラクト側のルール

Bound Signaturesを受け付けるスマートコントラクトは、`v`として`27`のみを使用します。
それ以外の値は受け付けてはいけません。

以下はBound Signatureの検証コードです。

```javascript
address signer = ecrecover(digest, 27, r, s);
require(signer != address(0));
```

`ecrecover`は署名が無効な場合に`address(0)`を返すため、この検証でゼロアドレスでないことを確認します。

この設計の重要な点は、`v`を引数として受け取らないことです。
従来のECDSA署名検証では`ecrecover(digest, v, r, s)`のように`v`も受け取りますが、Bound Signaturesでは`v`が常に`27`であるため、引数から省略できます。
これにより、calldataから32バイト分を削減できます。

`v`として`27`と`28`の両方を許可すると、署名展性が再び発生してしまいます。
例えば、`v = 27, s = X`の署名が有効な場合、`v = 28, s = N - X`も有効な署名になります。
両方を受け付けると、1つのメッセージに対して2つの有効な署名が存在することになるため、`27`のみに限定する必要があります。

#### `bind`関数

ECDSA署名をBound Signatureに変換する`bind`関数は以下のように実装されます。

```javascript
const SECP256K1_N: bigint = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n

function bind(sig: Signature, v: 27 | 28 = 27): Signature {
    if (sig.v === v) {
        return sig
    }
    const s = SECP256K1_N - sig.s
    return new Signature(sig.r, s, v)
}
```

提案はTypeScriptで記述されており、`v`の型は`27 | 28`の2値のみに制限されています。
デフォルト値は`27`ですが、引数で`28`を指定すれば`v = 28`への変換も可能な汎用的な設計になっています。

この関数の動作は以下の通りです。
元の署名の`v`がすでに目標の値（デフォルトでは`27`）であれば、そのまま返します。
`v`が異なる場合は、`s`の値をsecp256k1曲線の位数`N`から引いた値に変換し、目標の`v`を設定して返します。

`SECP256K1_N`はsecp256k1楕円曲線の位数（order）で、`s`の値の取りうる範囲を定義する定数です。
楕円曲線の数学的性質により、`s`を`N - s`に変換すると、`v`（y-parity）が反転します。
そのため`v = 28`の署名を`v = 27`に変換できます。

:::message
**位数（order）** とは、楕円曲線上の点を何回足し合わせると単位元（無限遠点）に戻るかを表す数です。secp256k1の位数 `N` は約2の256乗の大きな素数で、`s` の値は必ず `1 以上 N-1 以下` の範囲に収まります。`s` と `N-s` は数学的に対称な関係にあり、一方から他方に変換しても署名の検証可能性（どの公開鍵で署名されたか）は変わりませんが、y-parityだけが反転します。この性質を利用して `v` の変換を実現しています。
:::


以下のmermaid図はbind関数の処理フローを示しています。

```mermaid
flowchart TD
    A["署名(v, r, s)を受け取る"] --> B{"v == 27 ?"}
    B -- はい --> C["そのまま返す\n(v=27, r, s)"]
    B -- いいえ --> D["s' = N - s に変換"]
    D --> E["変換後の署名を返す\n(v=27, r, s')"]
```

元の署名のvが27ならそのまま使えます。
28の場合はsを反転させることで、ecrecoverがv=27で正しい署名者を復元できる形に変換します。

以下の図は、Bound Signature生成と検証の全体フローを示しています。

![Bound Signature 生成と検証のフロー](/images/qiita-64b7fe3d-e498-4449-9531-b6236b77f45e.webp)

#### 署名検証のフロー

以下のシーケンス図は、Bound Signatureを使ったオンチェーン検証の流れを示しています。

```mermaid
sequenceDiagram
    participant Signer as 署名者(オフチェーン)
    participant Client as クライアント
    participant Contract as スマートコントラクト
    participant Precompile as ecrecover

    Signer->>Client: ECDSA署名(v, r, s)を生成
    Client->>Client: bind(sig)でv=27に変換
    Client->>Contract: (r, s)のみ送信（64バイト）
    Contract->>Precompile: ecrecover(digest, 27, r, s)
    Precompile-->>Contract: 署名者のアドレスを返却
    Contract->>Contract: アドレスの検証
```

署名者が通常のECDSA署名を生成し、クライアント側で`bind`関数を使ってv=27に変換します。
コントラクトには`r`と`s`の64バイトだけを送信し、コントラクトは固定値の`27`と合わせて`ecrecover`を呼び出します。

## 補足

#### ERC2098との比較

**ERC8111**と**ERC2098**はどちらもECDSA署名を64バイトに圧縮する方式ですが、アプローチが異なります。

| 項目 | **ERC2098** | **ERC8111** |
|------|-------------|-------------|
| 圧縮方式 | `s`のMSBにy-parityを格納 | `v`を`27`に固定し、`s`を調整 |
| 署名サイズ | 64バイト | 64バイト |
| `ecrecover`との互換性 | アンパック処理が必要 | そのまま入力可能 |
| コントラクト側の処理 | ビットシフトとマスク操作 | 不要（v=27を直接指定） |
| ガス効率 | アンパック分のガスが追加 | より効率的 |
| Signature Malleability | 排除される | 排除される |

**ERC2098**では、コントラクトが圧縮署名を受け取った後、`yParityAndS`から最上位ビットを取り出して`v`を復元し、残りの255ビットから`s`を復元するアンパック処理が必要です。
一方、**ERC8111**のBound Signaturesは`ecrecover`プリコンパイルにそのまま入力できるため、追加のガスコストが発生しません。

#### なぜ`27`を選んだか

`v`の固定値として`27`が選ばれた理由は、y-parityを「偽」（falsy）にするためです。
Ethereumでは`v = 27`がy-parityの0（偶数）に、`v = 28`がy-parityの1（奇数）に対応します。
0は多くのプログラミング言語でfalsyな値であるため、直感的な選択といえます。

#### 後方互換性

Bound Signaturesは`ecrecover`プリコンパイルと互換性があります。
`v`パラメータに`27`を指定すれば、従来の`ecrecover`でそのまま検証できます。

ただし、トランザクション署名には使用できないという制限があります。
**EIP2**により、Ethereumのトランザクション署名では「low `s`」が要求されます。
具体的には、`s`の値がsecp256k1の位数`N`の半分以下でなければなりません。
Bound Signaturesでは`s = N - original_s`のように変換するため、`s`が`N`の半分を超える「high `s`」になる可能性があります。
そのため、トランザクション署名ではなく、コントラクト内のカスタム署名検証にのみ使用できます。

具体的な用途としては、メタトランザクション（ガス代を別アドレスが肩代わりするため、ユーザーがETHを持たなくてもコントラクトを操作できる仕組み）、マルチシグウォレット、オフチェーン承認（permit。トークンのapproveをオンチェーントランザクションなしに署名だけで行う仕組み）、**EIP712**（構造化データに型情報を付与して署名する標準規格）の型付きデータ署名などがあります。

## テストケース

提案では以下の3パターンのテストベクターが提供されています。
すべて`v = 27`で`ecrecover`を実行すると、正しい署名者アドレスが復元されます。

| 署名者 | ダイジェスト | `r` | `s` |
|--------|------------|-----|-----|
| `0x4a6f6B9fF1fc974096f9063a45Fd12bD5B928AD1` | `0xb0922c37cafd247fe3ada4eb1d1e3735b7d2837437c1178e9af120d535214270` | `0xdb7f75635124c807ec1f8b03e34cd76b633dc3a189e3c85fc5aee7e7d71df38c` | `0x5f1e6c6edf21cacfc2acba2815b253b9048b894eec5aaf70343389bb596c48bc` |
| `0x4a6f6B9fF1fc974096f9063a45Fd12bD5B928AD1` | `0xd92ff06caae7253883627416a425414d79e9003b91d6208add30e73735ef13c3` | `0xaa40efd534ac7f96b85babd7df9228fa131e8523115ca1ebc025698c37f3867d` | `0xa4b8d3c650fe62e46a563aed681bfdd44d50452fa7712bd139f2bfba3aed59c9` |
| `0x6B93E3bB9C0780C0f9042346Ffc379530a5882c1` | `0xfa75eba87f076cf22489da7c53a651bb3869473f78d09d4814afb7ab2d54ed45` | `0xaf4a877600ab6d14ebac626830cf1063d624487932b3cc73a7cd98ae7fbf337f` | `0xbc41d29acfcd3a1e7b5cb2dde1b85fe8882739312639b5f16d476a87584c040f` |

これらのテストベクターは、Bound Signatureが`ecrecover`で正しく署名者を復元できることを検証するためのものです。
Bound Signaturesでは`s`の値がsecp256k1の位数`N`の半分を超える場合（high `s`）がありえますが、`ecrecover`プリコンパイル自体はhigh `s`でも問題なく動作します。
ただし、前述の通りトランザクション署名では**EIP2**によりhigh `s`は無効とされるため、この使い分けに注意が必要です。

## 参考実装

以下はBound Signaturesの検証を行うSolidityライブラリの参考実装です。

```javascript:contracts/BoundSignatures.sol
pragma solidity ^0.8.30;

library BoundSignatures {
    function recover(bytes32 digest, bytes32 r, bytes32 s) internal pure returns (address signer) {
        signer = ecrecover(digest, 27, r, s);
        require(signer != address(0));
    }
}
```

この`recover`関数は`v`パラメータを受け取らず、内部で固定値`27`を使用します。
従来の署名検証関数と比べて、引数が1つ少なくなっています。

`ecrecover`がゼロアドレスを返した場合は署名が無効であることを意味するため、`require`で検証しています。
このライブラリを使うことで、Bound Signatureの検証を安全かつ簡潔に行えます。

#### 使用例

このライブラリを使ってメッセージの署名者を検証するコントラクトの例を示します。

```javascript:contracts/Example.sol
pragma solidity ^0.8.30;

import "./BoundSignatures.sol";

contract SignatureVerifier {
    using BoundSignatures for bytes32;

    mapping(bytes32 => bool) public usedSignatures;

    function verify(bytes32 digest, bytes32 r, bytes32 s) external view returns (address) {
        return digest.recover(r, s);
    }

    function executeWithSignature(
        bytes32 digest,
        bytes32 r,
        bytes32 s,
        address expectedSigner
    ) external {
        bytes32 sigHash = keccak256(abi.encodePacked(r, s));
        require(!usedSignatures[sigHash], "Signature already used");

        address signer = digest.recover(r, s);
        require(signer == expectedSigner, "Invalid signer");

        usedSignatures[sigHash] = true;
    }
}
```

`executeWithSignature`関数では、署名のハッシュを使って二重使用を防止しています。
Bound Signaturesでは1つのメッセージに対して有効な署名が1つだけなので、`r`と`s`のハッシュだけで一意に特定できます。
Signature Malleabilityが排除されているため、攻撃者が署名を変形させて二重使用チェックを回避することはできません。

## セキュリティ

#### 署名の復元可能性

Bound Signaturesは復元可能な署名です。
`ecrecover`を使って署名者の公開鍵（アドレス）を復元できるため、従来のECDSA署名と同じセキュリティ特性を持ちます。

#### Signature Malleabilityの排除

Bound Signaturesでは`v`が`27`に固定されているため、署名展性が排除されています。
従来のECDSA署名では`(27, r, s)`と`(28, r, N - s)`の2つの有効な署名が存在しましたが、`v = 27`のみを受け付けることで、各ダイジェストに対して有効な署名は1つだけになります。
これにより、署名のハッシュをキーとしてリプレイ攻撃（一度使用した署名を再利用して同じ操作を繰り返す攻撃）を防止するパターンが安全に使えます。

## 引用

https://eips.ethereum.org/EIPS/eip-8111

https://ethereum-magicians.org/t/erc-8111-bound-signatures/27308

https://eips.ethereum.org/EIPS/eip-2098

https://eips.ethereum.org/EIPS/eip-2

## 最後に

今回は「**ERC8111**によるECDSA署名の64バイト圧縮とSignature Malleabilityの排除」についてまとめてきました。
いかがだったでしょうか？

質問などがある方は以下のTwitterのDMなどからお気軽に質問してください！

[Twitter @cardene777](https://twitter.com/cardene777)

他の媒体でも情報発信しているのでぜひ他も見ていってください！

https://zenn.dev/heku

https://cardene.substack.com/

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58

https://cardene.notion.site/ERC-EIP-2a03fa3ea33d43baa9ed82288f98d4a9?pvs=4
