はじめに

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

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

今回は、マルチシグコントラクトなどで使用される、署名済みのデータの扱い方に関する新たな仕組みを提案している規格であるERC191についてまとめていきます!

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

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

概要

このERCは、Ethereumコントラクトにおける署名済みデータの扱い方に関する仕様を提案しています。

動機

マルチシグコントラクトは、複数の人が合意して行動を承認するためのシステムです。 これには予め署名されたトランザクション、つまりあらかじめ承認された行動の証としてのデータが使われます。 このデータには、署名の証明となる数字(r、s、v)が含まれています。 問題は、これらのデータがどう解釈されるかがはっきりしていないため、安全性に穴が生じています。

マルチシグコントラクト

複数の人が共同で承認することにより、特定のアクション(例えば、資金の送金)を実行するためのコントラクトです。 これは特にセキュリティを重視する場合や、意思決定に複数の関係者を巻き込みたい場合に有効です。 基本的には、特定のアクションを実行する前に必要な署名(承認)の数を設定します。

具体例1: 企業の財務管理 企業が大きな金額を動かす時に、単一の人物に全ての権限を持たせるのはリスクが高いです。 そこで、取締役会のメンバー3人中2人の承認が必要なマルチシグコントラクトを設定することができます。 例えば、CEO、CFO、および会計部門の責任者がキー保持者であり、これらのうち2人がトランザクションに署名することで初めて資金の移動が承認されるシステムです。

具体例2: 家族の共同貯金 家族が共同で貯金を管理しているとします。 夫と妻の2人で1/2のマルチシグコントラクトを設定し、どちらか一方の承認があれば資金を引き出せるようにします。 これにより、どちらかが不在でも緊急時に資金にアクセスできる柔軟性を持ちつつ、一方が無断で大きな金額を動かすリスクを防ぎます。

具体例3: 投資クラブ 投資クラブでは、メンバーが共同で投資判断を行います。 5人のメンバーがいて、大きな投資決定をする際には最低でも3人の同意が必要な3/5のマルチシグコントラクトを設定することができます。 これにより、個々のメンバーの暴走を防ぎつつ、合理的な投資決定を促進します。

マルチシグコントラクトは、単に資金を安全に保つだけでなく、共同での意思決定プロセスを形式化し、透明性を高める手段としても機能します。しかし、利用する際には、関係者全員がプロセスを理解し、適切に設定されていることが重要です。

具体的には、2つの大きな問題があります。 まず、標準的なEthereumトランザクションがそのまま予め署名されたトランザクションとして使われてしまうことができます。 これは、トランザクションのデータ(RLPdataと呼ばれる部分)が、予め署名されたトランザクションとして有効だとみなされてしまうからです。

次に、予め署名されたトランザクションが特定のコントラクトに結びつけられていないため、同じ署名が異なるコントラクトに悪用されるリスクがあります。 例えば、ユーザーAとBが2/3のマルチシグコントラクトXに対して署名したトランザクションを、攻撃者が別のコントラクトYに向けて再利用することが可能です。

このように、現在のマルチシグコントラクトはセキュリティ上の問題を抱えており、これらの問題を解決するための改善が求められています。

マルチシグ(複数署名)ウォレット

マルチシグコントラクトと同じ仕組みである、マルチシグウォレットについても合わせて紹介します。 1つのアクション(例えば資金の送金)を実行するために複数の人の承認が必要なデジタルウォレットです。 これは一種のセキュリティシステムで、単一のキーだけでなく、複数のキー(署名)がトランザクションを承認する必要があります。

たとえば、あなたが企業の財務を管理していて、大きな取引をする前にはいくつかの重要な人物(CEO、CFO、会計担当者など)の承認が必要だとします。 マルチシグウォレットはこのプロセスをデジタル化し、各人がトランザクションに対してデジタル署名を行うことで承認を与えることができます。

3/5のマルチシグウォレット このウォレットは5つの異なるキー(署名権限を持つ人々)を持ち、トランザクションを実行するためには少なくとも3人の承認が必要です。 会社で使用される場合、5人の役員のうち3人が取引に同意する必要があります。 これにより、一人が不正を行うリスクを減らし、責任と権限を分散させることができます。

2/2の家族用マルチシグウォレット 家族で共有する貯金用のウォレットを想像してみてください。 このウォレットには2つのキーがあり、両方の承認がないと資金は動かせません。これは、家族の大切な貯金を守るための一つの方法として機能します。

マルチシグウォレットは、その柔軟性とセキュリティから、企業の財務管理、家族の共有財布、投資クラブの資金管理など、さまざまなシナリオで利用されています。 しかし、使い方が複雑であったり、セットアップに時間がかかることもあるため、導入する際には慎重な計画と理解が必要です。

仕様

署名データの安全性を高めるために、特別な形式の提案があります。 これは、署名データの先頭に0x19というバイトを置くことで、そのデータがEthereumの通常のトランザクションとして誤って扱われないようにするものです。

text
0x19 <1 byte version> <version specific data> <data to sign>.

具体的には、署名データは次のように構成されます:0x19、1バイトのバージョン番号、そのバージョンに特有のデータ、そして最後に署名対象となる本来のデータです。

For a single byte whose value is in the [0x00, 0x7f] range, that byte is its own RLP encoding.

値が[0x00, 0x7f]の範囲にある単一バイトの場合、そのバイトは自身のRLPエンコーディングになります。

この0x19というバイトは重要で、これがあることでデータが有効なRLP(データ構造の形式)ではないという保証が得られます。 RLPでは、[0x00, 0x7f]の範囲のバイトはそれ自体が完全なデータを表します。 しかし、0x19が先頭にあることで、署名データ全体がただの1バイトのRLPデータとして認識されることはなく、意図しない解釈から保護されます。

RLP

RLP(Recursive Length Prefix)は、Ethereumにおいてデータをエンコードするための方法です。 主にトランザクション、ブロック、およびその他の構造のデータを送信や保管のためにシリアライズ(一連のバイトに変換)する時に使用されます。 RLPの目的は、任意のネストされたバイナリデータの配列を1つのバイナリ文字列にエンコードすることです。

RLPの基本原則:

  • 単一バイトデータ
    • 値が[0x00, 0x7f]の範囲内の単一バイトは、そのバイト自体がそのRLPエンコーディングです。
    • つまり、単純な小さなデータは非常に効率的にエンコードされます。
  • 文字列エンコーディング
    • それより長い文字列(バイト列)は、文字列の長さを示すプレフィックスとその後に続く生のバイトデータでエンコードされます。
    • このプレフィックスは文字列の長さに基づいて変わります。
  • リストエンコーディング
    • 複数の項目を含むリストも、まずリスト全体の長さを示すプレフィックスで始まり、それに続いてすべての項目が連結された形でエンコードされます。

RLPの使用例

  • トランザクション
    • Ethereumのトランザクションは、送信者、受取人、送金額、その他のパラメータなど、複数の異なるフィールドを含みます。
    • これらのフィールドをネットワーク上で効率的に送信するために、RLPはこれらを一つの連続したバイト列にエンコードします。
  • ブロック
    • ブロックもまた、トランザクションのリスト、タイムスタンプ、難易度、マイニング報酬など、多くの異なる情報を含みます。
    • RLPはこれら全てを統一された形式でエンコードし、ネットワーク上での送信やストレージに適した形にします。

RLPは、その単純さと汎用性からEthereumにおけるデータエンコーディングの基本的な方法として採用されています。 しかし、新しいユーザーにとっては少し複雑に感じられるかもしれませんが、Ethereumの内部動作を理解する上で非常に重要な要素です。

さらに、0x19はpersonal_sign関数で使用されるプレフィックス"\x19Ethereum Signed Message:\n"と整合性があります。 これにより、将来、0x45('E'に対応)というバージョンを使って、異なる種類の署名メッセージを扱う拡張性も考慮されています。

この提案により、署名されたデータが誤ってEthereumトランザクションとして解釈されるリスクが減り、より安全かつ明確な署名プロセスが実現されることが期待されています。

バージョン・バイトのレジストリ

バージョンバイトは、署名データの形式や用途を識別するために使用されるバイトです 。EIP(Ethereum Improvement Proposal)はそれぞれのフォーマットや規約を定義しています。

バージョンバイト EIP 説明
0x00 191 意図されたバリデーターとのデータ。
0x01 712 構造化されたデータ。
0x45 191 personal_sign メッセージ。

0x00(EIP-191)

これは「意図されたバリデーターとのデータ」に使われます。 例えば、マルチシグコントラクトで何らかの処理を実行する時、このバリデーターアドレスはマルチシグコントラクト自体のアドレスを指します。 署名されるデータは何でも良い、つまり任意のデータが可能です。

0x01(EIP-712)

このバイトは「構造化されたデータ」に対応しており、より複雑なデータ形式を扱う時に用いられます。 EIP712はこのデータ形式を定義しており、より詳細な情報や構造を持つデータの署名に適しています。

EIP712については以下の記事を参考にしてください。

0x45(EIP-191)

これは「personal_signメッセージ」用です。 ここでのバージョン固有のデータは、"Ethereum Signed Message:\n"とメッセージの長さを含んでいます。 このバージョンもまた、任意のデータに対して使用可能です。

NB: The E in Ethereum Signed Message refers to the version byte 0x45. The character E is 0x45 in hexadecimal which makes the remainder, thereum Signed Message:\n + len(message), the version-specific data.

NB: 'Ethereum Signed Message'における「E」はバージョンバイト0x45を指します。文字'E'は16進数で0x45に該当し、これにより残りの'thereum Signed Message:\n + メッセージの長さ'がバージョン固有のデータとなります。

'Ethereum Signed Message'の「E」はバージョンバイト0x45を指しており、16進数での'E'が0x45であることから、残りの'thereum Signed Message:\n + メッセージの長さ'がバージョン固有のデータとなります。

これらのバージョンバイトは、Ethereumにおけるデータの署名と検証をより正確で安全に行うためのもので、異なるシナリオや要件に応じて適切なフォーマットを選択できるようになっています。

例

以下のスニペットはSolidity 0.8.0で書かれています。

Version 0x00
javascript
function signatureBasedExecution(address target, uint256 nonce, bytes memory payload, uint8 v, bytes32 r, bytes32 s) public payable {
        
    // Arguments when calculating hash to validate
    // 1: byte(0x19) - the initial 0x19 byte
    // 2: byte(0) - the version byte
    // 3: address(this) - the validator address
    // 4-6 : Application specific data

    bytes32 hash = keccak256(abi.encodePacked(byte(0x19), byte(0), address(this), msg.value, nonce, payload));

    // recovering the signer from the hash and the signature
    addressRecovered = ecrecover(hash, v, r, s);
   
    // logic of the wallet
    // if (addressRecovered == owner) executeOnTarget(target, payload);
}

パラメーター:

  • address target
    • アクションの対象となるアドレス。
  • uint256 nonce
    • トランザクションの連番で、リプレイ攻撃を防ぐために使用されます。
  • bytes memory payload
    • 実行されるアクションのデータ。
  • uint8 v, bytes32 r, bytes32 s
    • 署名のためのECDSAパラメーター。

ハッシュの計算:

javascript
`keccak256(abi.encodePacked(byte(0x19), byte(0), address(this), msg.value, nonce, payload))
  • このハッシュは、署名を検証するために使用されます。
  • 0x19は署名されたデータが特定のフォーマットであることを示すマジックバイト、0はバージョンバイト、address(this)はコントラクト自身のアドレス(バリデーターアドレス)、msg.valueはトランザクションで送られるETHの量、nonceはトランザクションの連番、payloadは実行データです。

署名の回復:

javascript
ecrecover(hash, v, r, s)
  • この関数はハッシュと署名からアドレスを復元します。
  • 正しい署名が提供された場合、この関数は署名者のアドレスを返します。

ロジックの実装:

  • コメントされている部分は、実際のロジックを示しています。
  • 通常は、復元されたアドレスが期待されるアドレス(例えば、オーナー)と一致するかをチェックし、一致した場合にのみtargetアドレスに対してpayloadデータで何らかのアクション(例えば関数呼び出しやETHの送信)を実行します。

この関数は、マルチシグウォレットや他の認証が必要な場面での実行の認可に使用できます。 ただし、実際にはセキュリティを確保するために、正しいアドレスのチェック、nonceの管理、適切なエラーハンドリングなどの追加的なチェックが必要です。 また、payable修飾子により、この関数はETHを受け取ることができます。

引用

Martin Holst Swende (@holiman), Nick Johnson arachnid@notdot.net, "ERC-191: Signed Data Standard," Ethereum Improvement Proposals, no. 191, January 2016. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-191.

最後に

今回は「マルチシグコントラクトなどで使用される、署名済みのデータの扱い方に関する新たな仕組みを提案している規格であるERC191」についてまとめてきました! いかがだったでしょうか?

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

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