[ERC1271] コントラクトで署名を検証する仕組みを理解しよう！

# はじめに

初めまして。
**CryptoGames**というブロックチェーンゲーム企業でエンジニアをしている **cardene（かるでね）** です！
スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。

https://cryptogames.co.jp/

代表的なゲームは**クリプトスペルズ**というブロックチェーンゲームです。

https://cryptospells.jp/

今回は、アカウントがコントラクトである場合に、署名を検証する標準機能を提案している規格である**ERC1271**についてまとめていきます！

:::note info
以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。
:::

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

他にも様々なERCについてまとめています。

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

# 概要

提案されているのは、ブロックチェーン上のコントラクトが自分自身の署名を検証するための新しい方法です。
これまで、外部所有アカウント（EOA）は秘密鍵を使用してメッセージに署名することができましたが、コントラクトにはその機能がありませんでした。
新しい方法では、コントラクトが`isValidSignature(hash, signature)`という関数を持ち、これを使って特定の署名が本当にそのコントラクトから来たものかを確認できるようになります。

:::note info
この関数は、コントラクトが持つ署名が有効かどうかを検証するために使用されます。
コントラクトが自分自身の署名を「**これは本物です**」と証明する方法です。
これにより、コントラクト間のやり取りが安全になり、信頼性が高まります。
この提案はブロックチェーン技術とスマートコントラクトの可能性を広げ、より高度な機能の実現に寄与することになります。
:::

# 動機

この提案は、特に資産の移動や取引などの目的で、署名されたメッセージを使用したいコントラクトのためのものです。
現在、外部所有アカウント（**EOA**）は自分の秘密鍵でメッセージに署名できますが、コントラクトにはその機能がありません。
この提案では、コントラクトが代わりに署名を検証できる標準的な方法を提供しようとしています。

例えば、オフチェーンのオーダーブックを持つ分散型取引所では、ユーザー（**EOA**）が資産の購入や販売の意志を示すために注文に署名します。
そして、これらの署名は取引所のスマートコントラクトによってトレードの完了の許可として使われます。
しかし、コントラクトは自分自身の秘密鍵を持っていないため、このような署名を行うことができません。
そのため、コントラクトが署名を確認し、自身を代表するかどうかを判断できる標準的な機能が必要です。

この提案の目的は、コントラクト間のやり取りを、特に資産の移動や取引を、より安全かつ円滑に行えるようにすることです。
これにより、ブロックチェーンの機能をより広範囲に活用し、より複雑な取引や操作を可能にすることが期待されます。

# 仕様

```javascript
pragma solidity ^0.5.0;

contract ERC1271 {

  // bytes4(keccak256("isValidSignature(bytes32,bytes)")
  bytes4 constant internal MAGICVALUE = 0x1626ba7e;

  /**
   * @dev Should return whether the signature provided is valid for the provided hash
   * @param _hash      Hash of the data to be signed
   * @param _signature Signature byte array associated with _hash
   *
   * MUST return the bytes4 magic value 0x1626ba7e when function passes.
   * MUST NOT modify state (using STATICCALL for solc < 0.5, view modifier for solc > 0.5)
   * MUST allow external calls
   */ 
  function isValidSignature(
    bytes32 _hash, 
    bytes memory _signature)
    public
    view 
    returns (bytes4 magicValue);
}
```

`isValidSignature`という関数は、コントラクトが署名を検証するために使うものです。
この関数はさまざまな条件や状況に応じて署名が有効かどうかを判断します。
例えば、その時の時間やコントラクトの状態に基づく検証、署名者が外部所有アカウント（**EOA**）の場合のその人の権限レベルに基づく検証、あるいは署名方式（**ECDSA**、**マルチシグネチャ**、**BLS**など）に基づく検証などがあります。

この`isValidSignature`関数は、メッセージに署名をしたいコントラクト（例えば、スマートコントラクトウォレット、DAO、マルチシグネチャウォレットなど）によって実装されるべきです。
そして、コントラクトの署名をサポートしたいアプリケーションは、署名者がコントラクトである場合にこの関数を使用します。

:::note info
`isValidSignature`はコントラクトが署名を効率的かつ正確に検証するための方法を提供します。
これにより、コントラクトがさまざまな状況に応じて自分の署名を確認し、承認することが可能になります。
これは、特に資産の移動や重要な取引を扱う場面で重要な機能です。
:::

# 補足

この提案では、`isValidSignature`という関数が、署名が正しいかどうかを判断するのに適切な名前であるとされています。
この関数の役割は、承認された署名者が特定のデータに対して適切な署名を行った場合、その署名がコントラクトによって「**有効**」と認められることを確認することです。
つまり、署名されたメッセージは、署名者がスマートウォレットを代表して特定の行動を取ることが承認されている場合にのみ有効になります。

この関数は、署名されたハッシュと署名を簡単に区別するために二つの引数を持ちます。
コントラクトは通常とは異なるハッシュ関数（例えば、**EIP712** のような）を使うことがありますので、簡単のために未ハッシュ化のメッセージの代わりに`bytes32`ハッシュが使われます。

:::note info
**ERC712**については以下を参考にしてください。
:::

https://qiita.com/cardene/items/f54bc85cb66de5a54257

さらに、`isValidSignature()`関数は状態を変更できないように設計されるべきです。
これは、**GasToken**の発行や他の攻撃を防ぐためです。
この設計は、関数の実装を簡素化し、標準化を向上させ、オフチェーンでのコントラクトのクエリを可能にする目的があります。

また、署名の検証をより厳密かつ簡単にするために、`boolean`ではない特定の戻り値が期待されています。
これにより、署名が有効かどうかをより明確に判断することができます。

# 後方互換性

ここで提案されている方法はEOAによる署名ではなく、コントラクトに基づく署名に特化しているため、このEIPは署名検証に関するこれまでの規格と互換性があります。

# 参考実装

```javascript
/**
* @notice Verifies that the signer is the owner of the signing contract.
*/
function isValidSignature(
    bytes32 _hash,
    bytes calldata _signature
) external override view returns (bytes4) {
    // Validate signatures
    if (recoverSigner(_hash, _signature) == owner) {
        return 0x1626ba7e;
    } else {
        return 0xffffffff;
    }
}

/**
* @notice Recover the signer of hash, assuming it's an EOA account
* @dev Only for EthSign signatures
* @param _hash       Hash of message that was signed
* @param _signature  Signature encoded as (bytes32 r, bytes32 s, uint8 v)
*/
function recoverSigner(
    bytes32 _hash,
    bytes memory _signature
) internal pure returns (address signer) {
    require(_signature.length == 65, "SignatureValidator#recoverSigner: invalid signature length");

    // Variables are not scoped in Solidity.
    uint8 v = uint8(_signature[64]);
    bytes32 r = _signature.readBytes32(0);
    bytes32 s = _signature.readBytes32(32);
    
    // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature
    // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines
    // the valid range for s in (281): 0 < s < secp256k1n ÷ 2 + 1, and for v in (282): v ∈ {27, 28}. Most
    // signatures from current libraries generate a unique signature with an s-value in the lower half order.
    //
    // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value
    // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or
    // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept
    // these malleable signatures as well.
    //
    // Source OpenZeppelin
    // https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/cryptography/ECDSA.sol
    
    if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) {
        revert("SignatureValidator#recoverSigner: invalid signature 's' value");
    }
    
    if (v != 27 && v != 28) {
        revert("SignatureValidator#recoverSigner: invalid signature 'v' value");
    }
    
    // Recover ECDSA signer
    signer = ecrecover(_hash, v, r, s);
    
    // Prevent signer from being 0x0
    require(
        signer != address(0x0),
        "SignatureValidator#recoverSigner: INVALID_SIGNER"
    );
    
    return signer;
}
```

---

### `isValidSignature`

```javascript
function isValidSignature(
    bytes32 _hash,
    bytes calldata _signature
) external override view returns (bytes4) {
    // Validate signatures
    if (recoverSigner(_hash, _signature) == owner) {
        return 0x1626ba7e;
    } else {
        return 0xffffffff;
    }
}
```

**概要**

署名者が署名コントラクトの所有者であることを検証する関数。

**詳細**

この関数は、与えられたハッシュと署名がコントラクトの所有者によって行われたものかを検証します。
署名が所有者によるものであれば、特定の値 (`0x1626ba7e`) を返し、そうでない場合は別の値 (`0xffffffff`) を返します。

**引数**

- `_hash`
    - 検証するメッセージのハッシュ。
- `_signature`
    - 検証する署名。

**戻り値**

- `bytes4`
    - 署名が有効かどうかを示すバイト値。

---

### `recoverSigner`

```javascript
function recoverSigner(
    bytes32 _hash,
    bytes memory _signature
) internal pure returns (address signer) {
    require(_signature.length == 65, "SignatureValidator#recoverSigner: invalid signature length");

    // Variables are not scoped in Solidity.
    uint8 v = uint8(_signature[64]);
    bytes32 r = _signature.readBytes32(0);
    bytes32 s = _signature.readBytes32(32);
    
    // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature
    // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines
    // the valid range for s in (281): 0 < s < secp256k1n ÷ 2 + 1, and for v in (282): v ∈ {27, 28}. Most
    // signatures from current libraries generate a unique signature with an s-value in the lower half order.
    //
    // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value
    // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or
    // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept
    // these malleable signatures as well.
    //
    // Source OpenZeppelin
    // https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/cryptography/ECDSA.sol
    
    if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) {
        revert("SignatureValidator#recoverSigner: invalid signature 's' value");
    }
    
    if (v != 27 && v != 28) {
        revert("SignatureValidator#recoverSigner: invalid signature 'v' value");
    }
    
    // Recover ECDSA signer
    signer = ecrecover(_hash, v, r, s);
    
    // Prevent signer from being 0x0
    require(
        signer != address(0x0),
        "SignatureValidator#recoverSigner: INVALID_SIGNER"
    );
    
    return signer;
}
```

**概要**

署名されたハッシュの署名者を回復する関数。

**詳細**

この関数は、与えられたハッシュに対する署名者のアドレスを回復します。
署名は `(bytes32 r, bytes32 s, uint8 v)` の形式でエンコードされている必要があり、署名の長さは65バイトでなければなりません。
署名の有効性を保証するために、特定の条件（`s`値と`v`値の範囲）を満たす必要があります。

**引数**

- `_hash`
    - 署名されたメッセージのハッシュ。
- `_signature`
    - 署名データ。
    - `(bytes32 r, bytes32 s, uint8 v)`の形式である必要があります。

**戻り値**

- `address`
    - 署名者のアドレス。

---

以下は外部署名コントラクトで`isValidSignature()`関数をコールするコントラクトの実装例です。

```javascript
function callERC1271isValidSignature(
    address _addr,
    bytes32 _hash,
    bytes calldata _signature
) external view {
    bytes4 result = IERC1271Wallet(_addr).isValidSignature(_hash, _signature);
    require(result == 0x1626ba7e, "INVALID_SIGNATURE");
}
```

# セキュリティ考慮事項

`isValidSignature`関数は、ブロックチェーン上のコントラクトで署名が正しいかどうかを確認する時に使用されます。
この関数を実行する際には、どれだけのガス（ブロックチェーン上の計算資源）を消費するかに制限がないため、場合によっては多くのガスが必要になることがあります。
そのため、他のコントラクトからこの関数を呼び出す際には、使用するガスの量を固定的に設定（ハードコード）しないことが重要です。
固定的にガスの量を設定すると、ガス不足によって特定の署名が正しく検証されない可能性があるからです。

さらに、`isValidSignature`関数を実装する各コントラクトは、提供された署名が本当に有効であるかを確認する責任を持ちます。
もし、署名が不正確である場合、そのコントラクトや関連する取引に重大な影響が生じる恐れがあります。このため、署名の検証過程は非常に慎重に行われる必要があり、正確性が求められます。

# 引用

Francisco Giordano (@frangio), Matt Condon (@shrugs), Philippe Castonguay (@PhABC), Amir Bandeali (@abandeali1), Jorge Izquierdo (@izqui), Bertrand Masius (@catageek), "ERC-1271: Standard Signature Validation Method for Contracts," Ethereum Improvement Proposals, no. 1271, July 2018. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-1271.

# 最後に

今回は「アカウントがコントラクトである場合に、署名を検証する標準機能を提案している規格である**ERC1271**」についてまとめてきました！
いかがだったでしょうか？

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

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

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

https://chaldene.net/

https://zenn.dev/heku

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58


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