はじめに
初めまして。
CryptoGamesというブロックチェーンゲーム企業でエンジニアをしている cardene(かるでね) です!
スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。
代表的なゲームはクリプトスペルズというブロックチェーンゲームです。
今回は、型付けされた構造化データのハッシュ化と署名の仕組みを提案している規格であるEIP712についてまとめていきます!
以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。
他にも様々なERCについてまとめています。
概要
この標準は、データを構造化された型に基づいて処理し、それをハッシュ化および署名するためのものです。
通常のバイト列だけでなく、データの型や構造を考慮して安全に操作できるようにすることを目指しています。
以下のような特徴を持ちます。
- 正確なエンコード関数の理論的な枠組み
- エンコード関数の正確性を確保するための理論的なフレームワークが提供されています。
- Solidity structsに類似し、互換性のある構造化データの仕様
- Solidityの
structs
に似た構造データを定義し、それを安全に処理できるようにします。
- Solidityの
- 安全なハッシュアルゴリズム
- これらの構造体のインスタンスを安全にハッシュ化するためのアルゴリズムを提供します。
- 署名可能なメッセージのセットにインスタンスを安全に含める仕組み
- これらの構造体のインスタンスを署名可能なメッセージの一部として安全に扱う方法が提供されます。
- ドメイン分離のための拡張メカニズム
- 異なるドメイン間でデータを区別するための拡張機能が含まれています。
- 新しいRPC呼び出し「
eth_signTypedData
」- この新しいRPC呼び出しを使用して、構造化データに署名できます。
- EVMでの最適化されたハッシュアルゴリズムの実装
- EVM内で効率的に動作するハッシュアルゴリズムの最適な実装が提供されます。
リエントランシー保護は含まれていないため、コントラクト間での操作の再実行を防ぐ機能は提供されていません。
動機
データの署名に関して、バイト列だけを考慮する場合、一般的な方法がすでに存在しており問題は解決されています。
しかし、現実世界では、複雑な意味を持つメッセージが重要です。
構造化データをハッシュ化することは容易ではなく、誤った方法で行うとシステムのセキュリティ特性が損なわれる可能性があります。
そのため、「自前で暗号技術を実装しない」という教訓が適用されます。
代わりに、監査済みで信頼性のある標準的な方法を使用すべきです。
このEIPは、そのような標準的な方法を提供しようとするものです。
また、このEIPは、オンチェーンでの使用を向上させることを目的として、オフチェーンメッセージの署名の使いやすさを改善しようとします。
オフチェーンメッセージの署名は、ガスを節約し、ブロックチェーン上のトランザクション数を減らす利点があり、そのために採用が増えています。
現在、署名されたメッセージは、ユーザーに対してメッセージの構造要素に関する文脈が不足しており、不透明な16進数文字列として表示されています。
このEIPは、その改善を目指しています。
引用: https://eips.ethereum.org/EIPS/eip-712
この提案では、データとその構造をエンコードする方法が紹介されており、ユーザーが署名の時にデータを確認できるようになります。
このスキームは、ユーザーが署名する前に、どのようなデータが含まれているかを理解できるようにするための方法を提供します。
これにより、ユーザーは署名対象のデータが正確であることを確認しやすくなり、セキュリティを向上させることができます。
引用: https://eips.ethereum.org/EIPS/eip-712
仕様
新しい署名可能なメッセージのセットは、トランザクション(𝕋)やバイト列(𝔹⁸ⁿ)に加えて、構造化データ(𝕊)も含むように拡張されました。
この新しいセットは、𝕋 ∪ 𝔹⁸ⁿ ∪ 𝕊
となります。
𝕋 ∪ 𝔹⁸ⁿ ∪ 𝕊
トランザクション(𝕋)
これはブロックチェーン上でのコントラクトのトランザクションを表します。
例えば、ユーザーが特定のコントラクトに対してトークンの転送を要求するトランザクションが含まれます。
トランザクションはRLPエンコードされ、バイト列として扱われます。
バイト列(𝔹⁸ⁿ)
バイト列はバイナリデータのシーケンスで、例えば16進数文字列で表されます。
これは単純なデータであり、通常は署名などの操作に関連するデータを表します。
例えば、16進数文字列0xabcdef123456
が含まれます。
構造化データ(𝕊)
構造化データは、データとその構造が含まれる形式です。
これにはトランザクションやバイト列よりも複雑なデータが含まれます。
例えば、特定のコントラクトの設定やルールのセットを表す構造化データが含まれます。
このデータは特定の方法でエンコードされ、署名に使用されます。
例えば、あるユーザーが特定のERC20トークンの送金トランザクション(𝕋)を署名する際、トランザクションの詳細情報(構造化データ𝕊)が含まれることがあります。
このセットに含まれるメッセージは、トランザクション、バイト列、および構造化データをカバーし、それぞれ異なる方法でエンコードされ、署名に使用される可能性があります。
これらのメッセージは、ハッシュ化および署名に適したバイト列にエンコードされます。
具体的なエンコード方法は以下の通りです:
トランザクションの場合
encode(transaction : 𝕋) = RLP_encode(transaction)
トランザクションはRLPエンコードされます。
バイト列の場合
encode(message : 𝔹⁸ⁿ) = "\x19Ethereum Signed Message:\n" ‖ len(message) ‖ message
バイト列は\x19Ethereum Signed Message:\n
とバイト列の長さを表すデータで構成されます。
ドメインセパレータとメッセージの場合
encode(domainSeparator : 𝔹²⁵⁶, message : 𝕊) = "\x19\x01" ‖ domainSeparator ‖ hashStruct(message)
ドメインセパレータとメッセージは特定の方法でエンコードされ、そのハッシュが組み合わせられます。
このエンコーディングは、各ケースが確定的です。
また、エンコーディングは単射です。
つまり、これらの3つのケースは常に最初のバイトで異なります(RLP_encode(transaction)
は\x19
で始まりません)。
確定的
「確定的」とは、あるルールに従って常に同じ結果が得られることを意味します。
例えば、2
と2
を足すと常に4
になります。
同様に、このエンコーディング方法も、特定のデータ(トランザクション、バイト列、構造化データ)をエンコードする時には、常に同じ方法でエンコードされます。
単射
「単射」とは、異なるものが同じものに変換されないことを意味します。
例えば、同じ料理を2つ注文しても、それらは別々の料理です。
このエンコーディングも同様で、異なるデータ(トランザクション、バイト列、構造化データ)は、それぞれ異なるエンコードに変換され、同じものには変換されません。
最初のバイトで異なる
このエンコーディングは、エンコードされたデータの最初のバイトが異なることに基づいています。
例えば、トランザクションのエンコードは\x19
から始まり、バイト列のエンコードは\x19Ethereum Signed Message:\n
から始まります。
つまり、異なるデータをエンコードする場合、最初のバイトが異なるため、それらは容易に区別できます。
このエンコーディング方法はデータを一貫性のある方法で変換し、異なるデータは異なるエンコードに変換され、最初のバイトで区別されるということです。
このエンコーディングは、EIP191に準拠しています。
バージョンバイトは0x01
で固定されており、バージョン固有のデータは32
バイトのドメインセパレータdomainSeparator
であり、署名対象のデータは32
バイトのhashStruct(message)
です。
構造化データ𝕊
構造化データのセットを定義するために、使用可能な型を最初に定義します。
これらの型は、ABIv2と密接に関連しており、その定義を説明するために、Solidityの表記法を使用しています。
この標準は、**Ethereum Virtual Machine(EVM)**に特有のものでありながら、高水準のプログラミング言語にも適用できるように設計されています。
以下に、具体例を示します。
struct Mail {
address from;
address to;
string contents;
}
-
struct型の定義
- 構造データ(
struct
)は名前を持ち、0個以上のメンバー変数を含む型です。 - メンバー変数には、メンバー型と名前が指定されます。
- 構造データ(
「メンバー型」は、構造化データ内の個々の要素またはフィールドが持つデータ型を指します。
構造化データは通常、異なるメンバー型を持つ複数のメンバー(フィールド)から構成されます。
メンバー型は、そのデータがどのような情報を表現するかを示します。
例えば、以下の構造体(struct)の場合:
struct Person {
string name;
uint age;
}
ここで、nameというメンバーは文字列型(string
)のメンバー型を持ち、ageというメンバーは符号なし整数型(uint
)のメンバー型を持っています。
つまり、nameはテキストデータを表現し、ageは整数データを表現します。
メンバー型は構造化データの要素がどのような種類のデータを格納できるかを定義し、それに基づいてメンバーのデータを扱います。
-
メンバー型
- メンバー型は、アトミック型、ダイナミック型、またはリファレンス型のいずれかです。
-
アトミック型
- アトミック型は、基本的な型で、例えば整数、文字列、真偽値、アドレスなどです。
- これらはSolidityの型と類似しており、整数の大きさやアドレスの形式などが含まれます。
-
ダイナミック型
- ダイナミック型は、
bytes
とstring
という特別な型で、可変サイズのデータを表現します。 - アトミック型と同じように型を指定しますが、エンコーディング方法が異なります。
- ダイナミック型は、
-
リファレンス型
- リファレンス型には、配列(
arrays
)と構造体(structs
)が含まれます。 - 配列は固定サイズまたは可変サイズで、型と要素数を指定して表現します。
- 構造体は他の構造体を参照するための型で、再帰的な構造体もサポートされています。
- リファレンス型には、配列(
-
構造化データ𝕊
- 構造化データ𝕊には、すべての構造体型のインスタンスが含まれます。
- つまり、このセットには、構造体で定義されたデータの実際の例が含まれます。
この定義は、構造化データを取り扱うための基本的な型や規則を説明しており、構造体、アトミック型、ダイナミック型、リファレンス型などが含まれています。
そして、それらの型のインスタンスが構造化データ𝕊に含まれるということです。
hashStructの定義
hashStruct
関数は、構造化データ(𝕊)をハッシュ化するための関数です。
具体的な計算は、typeHash
とencodeData(s)
を結合し、それにkeccak256
ハッシュ関数を適用してハッシュを生成します。
typeHash
は特定の構造体型に対して一定の定数であり、実行時に計算する必要はありません。
encodeTypeの定義
構造体の型は、名前(struct
の名前)とそのメンバー(フィールド)を示す文字列としてエンコードされます。
各メンバーは、型と名前の組み合わせで表現されます。
例えば、Mail
構造体の場合、Mail(address from,address to,string contents)
とエンコードされます。
もし構造体型が他の構造体型を参照している場合(さらにそれらが他の構造体型を参照している場合も含む)、参照される構造体型は名前でソートされ、エンコーディングに追加されます。
encodeDataの定義
構造体のインスタンスのエンコーディングは、そのメンバー値のエンコードを連結したものです。
メンバー値のエンコードは、型で指定された順序で32
バイトのバイト列として表現されます。
アトミックな値は特定の方法でエンコードされます。
例えば、真偽値はuint256
の値0
または1
にエンコードされ、アドレスはuint160
にエンコードされます。
整数値は256
ビットに符号拡張され、ビッグエンディアンの順序でエンコードされます。
bytes1
からbytes31
までの値は、配列として扱われ、先頭(インデックス0
)から末尾(インデックスlength - 1
)までの順序でゼロパディングされ、bytes32
にエンコードされます。
これはABI v1およびv2のエンコーディングと対応しています。
bytes
とstring
のダイナミックな値は、その内容のkeccak256
ハッシュとしてエンコードされます。
配列の値は、その内容のencodeData
の連結のkeccak256
ハッシュとしてエンコードされます。
つまり、SomeType[5]
のエンコーディングは、SomeType
のメンバーを5つ含む構造体のエンコーディングと同じです。
構造体の値は再帰的にhashStruct(value)
としてエンコードされます。
ただし、循環参照の場合は未定義です。
これらの定義により、構造化データ(𝕊)のエンコーディングとハッシュ化が実行されます。
domainSeparatorの定義
domainSeparator = hashStruct(eip712Domain)
-
domainSeparator
(ドメインセパレータ)は、**EIP712Domain(EIP712ドメイン)**と呼ばれる構造体のハッシュです。 -
EIP712Domain
は、以下のフィールドのいずれかを持つことができる構造体型です。- プロトコルデザイナーは、署名ドメインに適切なフィールドを含める必要があります。
- 未使用のフィールドは構造体型から省略されます。
-
string name
- 署名ドメインのユーザーが読み取れる名前、つまりDAppやプロトコルの名前。
-
string version
- 署名ドメインの現在のメジャーバージョン。
- 異なるバージョンの署名は互換性がないため、ユーザーエージェントは現在アクティブなチェーンに一致しない場合に署名を拒否するべきです。
-
uint256 chainId
- EIP155チェーンID。ユーザーエージェントは、現在のアクティブなチェーンに一致しない場合に署名を拒否するべきです。
-
address verifyingContract
- 署名を検証するコントラクトのアドレス。
- ユーザーエージェントはコントラクト固有のフィッシング防止を行うかもしれません。
-
bytes32 salt
- プロトコルの区別用のソルト。
- これは最後の手段のドメインセパレータとして使用できます。
この標準の将来の拡張では、新しいフィールドとそれに関連するユーザーエージェントの動作制約を追加できます。
ユーザーエージェントは提供された情報を使用してユーザーに情報提供/警告を行うか、署名を拒否するかもしれません。
DAppの実装者は、プライベートフィールドを追加しないでください。
新しいフィールドはEIPプロセスを通じて提案する必要があります。
EIP712Domain
のフィールドは上記の順序で配置され、不在のフィールドはスキップされます。
将来のフィールドの追加はアルファベット順になり、上記のフィールドの後に配置されます。
ユーザーエージェントは、EIPT712Domain
型に指定された順序に従ってフィールドを受け入れるべきです。
これにより、ドメインセパレータが署名ドメインを一意に識別し、署名の検証に使用されることが保証されます。
eth_signTypedData JSON RPCの仕様
eth_signTypedData
メソッドは、EthereumのJSON-RPCに追加されたメソッドで、eth_sign
と似たような機能を提供します。
eth_signTypedDataメソッドの仕様
このメソッドは、特定のEthereum署名を計算します。
具体的には、次のような署名を計算します。
sign(keccak256("\x19\x01" ‖ domainSeparator ‖ hashStruct(message)))
。
ここで、domainSeparator
とmessage
は上記で定義されたものです。
注意
署名を行うアカウントはアンロックされている必要があります。
パラメータ
-
Address
- 20バイト。
- メッセージに署名するアカウントのアドレス。
-
TypedData
(型付き構造データ)- 署名するための構造化データ。
TypedData
は、タイプ情報、ドメインセパレータのパラメータ、およびメッセージオブジェクトを含むJSONオブジェクトです。
以下はTypedDataパラメータのJSONスキーマの定義です。
Explain
{
type: 'object',
properties: {
types: {
type: 'object',
properties: {
EIP712Domain: {type: 'array'},
},
additionalProperties: {
type: 'array',
items: {
type: 'object',
properties: {
name: {type: 'string'},
type: {type: 'string'}
},
required: ['name', 'type']
}
},
required: ['EIP712Domain']
},
primaryType: {type: 'string'},
domain: {type: 'object'},
message: {type: 'object'}
},
required: ['types', 'primaryType', 'domain', 'message']
}
このメソッドを使用することで、特定のアカウントが指定された構造データに対して署名を生成できます。
戻り値
DATA
署名(Signature
)。
eth_sign
と同様、0x
で始まる16
進数で表現された129
バイトの配列です。
この配列は、イエローペーパーの付録Fで定義されているr
、s
、v
パラメータをビッグエンディアン形式でエンコードしています。
バイト0
から64
までがr
パラメータ、バイト64
から128
までがs
パラメータで、最後のバイトがv
パラメータです。
なお、v
パラメータにはEIP155で指定されたチェーンIDも含まれています。
例
リクエスト
curl -X POST --data '{"jsonrpc":"2.0","method":"eth_signTypedData","params":["0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", {"types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"Person":[{"name":"name","type":"string"},{"name":"wallet","type":"address"}],"Mail":[{"name":"from","type":"Person"},{"name":"to","type":"Person"},{"name":"contents","type":"string"}]},"primaryType":"Mail","domain":{"name":"Ether Mail","version":"1","chainId":1,"verifyingContract":"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"},"message":{"from":{"name":"Cow","wallet":"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"},"to":{"name":"Bob","wallet":"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"},"contents":"Hello, Bob!"}}],"id":1}'
結果
{
"id":1,
"jsonrpc": "2.0",
"result": "0x4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b915621c"
}
上記の例では、特定のアカウント(アドレス:0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826
)が指定された構造データに対して署名を行います。
結果として、署名が16進数の形式で提供されます。
personal_signTypedData
eth_signTypedData
で計算された署名を検証するためにSolidityのecrecover
を使用する方法について、以下の手順で詳しく説明します。
この方法に関する具体的な例はExample.js
ファイルにあり、それはRopstenおよびRinkebyというテストネットワークにデプロイされたコントラクトを使用します。
手順:
- ユーザーは
eth_signTypedData
を使用して署名されたデータを取得します。- これは通常、ユーザーが提供したメッセージや型付きデータに対する署名です。
- ユーザーが提供したデータと署名をSolidityのスマートコントラクトに送信します。
- このスマートコントラクトは署名の検証を行います。
- スマートコントラクト内で
ecrecover
関数を使用して、署名から公開鍵を抽出します。-
ecrecover
は、署名(r
、s
、v
)、メッセージのハッシュ、および署名アドレスを受け取り、それに基づいて公開鍵を計算します。
-
- スマートコントラクト内で、取得した公開鍵を使用してメッセージが正当であることを確認します。
- 具体的には、メッセージのハッシュを再計算し、署名アドレスと計算された公開鍵を使用して、署名者のアドレスを再検証します。
- メッセージのハッシュが正当であり、署名者のアドレスが署名に一致する場合、署名は有効であるとみなされます。
- これにより、
eth_signTypedData
で署名されたデータが信頼性のあるものであることが確認されます。
- これにより、
この手順を通じて、eth_signTypedData
で署名されたデータの検証がスマートコントラクト内で行われ、そのデータの信頼性が確保されます。
Web3 APIの仕様
Web3.jsバージョン1で追加されたWeb3 APIについて説明します。
web3.eth.signTypedDataメソッド
web3.eth.signTypedData(typedData, address [, callback])
-
web3.eth.signTypedData
メソッドは、特定のアカウントを使用して型付きデータに署名するためのメソッドです。 - このアカウントはアンロックされている必要があります。
パラメータ
-
Object
(オブジェクト)- ドメインセパレータと署名するための型付きデータ。
- eth_signTypedData JSON-RPC呼び出しで指定されたJSONスキーマに従って構造化されます。
-
String
またはNumber
(文字列または数値)- データに署名するためのアドレス。
- または、web3.eth.accounts.wallet内のローカルウォレットのアドレスまたはインデックスです。
-
Function
(オプション)- オプションのコールバック関数。
- エラーオブジェクトを第1パラメータとして、結果を第2パラメータとして返します。
戻り値
-
Promiseが返され、
String
(文字列)を返します。 - これは
eth_signTypedData
によって返される署名です。
例
web3.eth.signTypedData(typedData, "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826")
.then(console.log);
> "0x4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b915621c"
web3.eth.personal.signTypedDataメソッド
-
web3.eth.personal.signTypedData
メソッドは、web3.eth.signTypedData
と同様に動作しますが、追加のパスワードパラメータを受け取ります。 - これは
web3.eth.personal.sign
に類似しています。
これらのメソッドを使用することで、特定のアカウントが型付きデータに署名し、その署名を取得できます。
補足
エンコード関数の拡張
- エンコード関数は、新しいデータ型をサポートするために新しいケースが追加されました。
- エンコードの最初のバイトは、これらのケースを区別する役割を果たします。
- そのため、直ちにドメインセパレータや
typeHash
を使用することは安全ではありません。 -
typeHash
をエンコードトランザクションの接頭辞として誤って構築する可能性があります。
ドメインセパレータの役割
- ドメインセパレータは、本質的に同じ構造の衝突を防ぐ役割を果たします。
- 2つのDAppが同じ構造を提案した場合でも、それらが互換性がない場合、ドメインセパレータを導入することで、署名の衝突を回避できます。
複数の異なるシグネチャユースケースへの対応
- ドメインセパレータを使用することで、同じ構造内で異なるシグネチャユースケースをサポートできます。
- たとえば、
from
とto
の両方から署名が必要な場合、異なるドメインセパレータを提供して、これらの署名を区別できます。
代替案1: ターゲットコントラクトアドレスをドメインセパレータとして使用
- この代替案では、同じ型を持つコントラクトの衝突を防ぐために、ターゲットコントラクトアドレスをドメインセパレータとして使用します。
- ただし、複数の異なるシグネチャユースケースへの対応には適していません。
- 標準では、実装者に対して適切な場面でターゲットコントラクトアドレスを使用することを提案しています。
hashStruct関数の役割
-
hashStruct
関数は、異なるデータ型を区別するためにtypeHash
で始まります。 - 異なるデータ型に異なるプレフィックスを与えることで、
encodeData
関数は特定のデータ型内で単射である必要があります。 - つまり、
encodeData(a)
がencodeData(b)
と等しい場合でも、a
とb
のデータ型(typeOf(a)
がtypeOf(b)
でない限り)が異なる場合にのみ許容されます。
typeHash
typeHash
は、Solidity内でコンパイル時定数に変換される設計となっています。
例えば以下のように定義されます。
bytes32 constant MAIL_TYPEHASH = keccak256(
"Mail(address from,address to,string contents)");
typeHash
にはいくつかの代替案が検討されましたが、それらは特定の理由から却下されました。
代替案2:ABIv2関数シグネチャを使用
-
bytes4
は衝突耐性が不足しています。 - 関数シグネチャとは異なり、より長いハッシュを使用してもランタイムコストがほとんど発生しません。
代替案3:256ビットのABIv2関数シグネチャ
- これは型情報を含みますが、関数以外のセマンティクスを含みません。
- 実際、EIP20の
transfer(address,uint256)
とEIP721のtransfer(address,uint256)
のように、実用的な衝突を引き起こす可能性があります。 - 前者の
uint256
は金額を指し、後者のuint256
は一意のIDを指します。 - 一般的に、ABIv2は互換性を重視する一方、ハッシング標準は互換性を好みません。
代替案4:256ビットのABIv2シグネチャにパラメータ名と構造体名を追加
- これは提案された解決策よりも長くなります。
- また、文字列の長さは入力の長さに対して指数関数的に増加する可能性があります。
- また、再帰的な
struct
型を許可しません。
代替案5:natspecドキュメンテーションを含む
- これにより、スキーマハッシュにさらに多くのセマンティクス情報が含まれ、衝突の可能性がさらに減少します。
- ただし、ドキュメンテーションの拡張や修正は一般的な前提に反する形で破壊的な変更となります。
- また、スキーマハッシュメカニズムを非常に冗長にします。
これらの代替案を検討した結果、typeHash
が選択されました。
typeHash
は、型の識別子としての役割を果たし、Solidity内でのコンパイル時定数として安全かつ効率的に使用できるからです。
encodeDataの補足
encodeData
は、Solidity内でhashStruct
を簡単に実装できるように設計されています。
以下にその理由を説明します。
hashStructの実装
まず、encodeData
はSolidity内でhashStruct
関数を実装するために使用されます。
hashStruct
関数は、Mail
構造体のメモリ内データからハッシュを計算するための関数です。
具体的なコードは以下のようになります。
function hashStruct(Mail memory mail) pure returns (bytes32 hash) {
return keccak256(abi.encode(
MAIL_TYPEHASH,
mail.from,
mail.to,
keccak256(mail.contents)
));
}
このコードでは、encodeData
を使用して、MAIL_TYPEHASH
とメモリ内のデータをエンコードし、それをkeccak256
関数を用いてハッシュ化しています。
これにより、構造体の内容から一意のハッシュが生成されます。
効率的なEVM内での実装
encodeData
はまた、EVM内での効率的なインプレース実装を可能にします。
以下のコード例では、アセンブリ言語を使用してhashStruct
を実装しています。
これにより、メモリ内のデータを変更せずにハッシュを計算できます。
function hashStruct(Mail memory mail) pure returns (bytes32 hash) {
// Compute sub-hashes
bytes32 typeHash = MAIL_TYPEHASH;
bytes32 contentsHash = keccak256(mail.contents);
assembly {
// Back up select memory
let temp1 := mload(sub(mail, 32))
let temp2 := mload(add(mail, 128))
// Write typeHash and sub-hashes
mstore(sub(mail, 32), typeHash)
mstore(add(mail, 64), contentsHash)
// Compute hash
hash := keccak256(sub(mail, 32), 128)
// Restore memory
mstore(sub(mail, 32), temp1)
mstore(add(mail, 64), temp2)
}
}
この実装では、メモリの配置に関する特定の仮定がなされており、特にメモリ内のデータがアドレス32
以下に配置されず、メンバーが順番に格納され、すべての値が32
バイトの境界に整列していると仮定されています。
また、動的および参照型は32
バイトのポインタとして格納されていると仮定されています。
代替案6: 緊密なパッキング
この代替案では、keccak256
関数を複数の引数で呼び出す時のデフォルトの挙動である緊密なパッキングを使用します。
これにより、ハッシュ化するバイト数が最小限に抑えられますが、EVM内で複雑なパッキング命令が必要であり、インプレース計算は許可されていません。
厳密なパッキングはバイト数を節約しますが、EVM内での処理が複雑になります。
代替案7: ABIv2エンコーディング
特に今後のabi.encode
との組み合わせにより、abi.encode
をencodeData
関数として使用することが簡単になります。
しかし、ABIv2標準自体が決定論的なセキュリティ基準を満たさないため、同じデータの複数の有効なABIv2エンコーディングが存在します。
ABIv2はインプレース計算を許可しません。
インプレース計算(In-place computation)
コンピュータプログラムやアルゴリズムの実行中に、追加のメモリ領域を使用せずに既存のメモリ領域内で計算を行う手法です。
つまり、計算に使用されるデータ構造や変数は、そのままのメモリ配置で更新および変更され、新しいメモリ領域を確保することなく処理が行われます。
インプレース計算は、メモリ使用量を最小限に抑え、プログラムの実行効率を向上させるために使用されます。
特に大規模なデータセットやメモリが限られている環境で有用です。
しかし、注意深く管理しないと、メモリ内のデータが破壊されたり、計算結果が予期しないものになったりする可能性があるため、注意が必要です。
また、ソートアルゴリズムやリスト操作、行列操作などの多くのアルゴリズムやデータ操作でも使用されます。
インプレースソート(例: クイックソート、ヒープソート)、インプレースリスト反転、インプレース行列変換などがその例です。
代替案8: hashStructからtypeHashを省略し、代わりにドメインセパレータと組み合わせる
この代替案では、hashStruct
からtypeHash
を省略し、代わりにドメインセパレータと組み合わせます。
これにより、効率は向上しますが、Solidityのkeccak256
ハッシュ関数のセマンティクスが単射的でなくなります。
つまり、同じデータに対して異なるハッシュが生成される可能性があります。
代替案9: 円環データ構造のサポート
現行の標準は、木構造のデータに最適化されており、円環状のデータ構造に対しては未定義です。
円環状のデータをサポートするためには、現在のノードへのパスを含むスタックを維持し、サイクルが検出された場合にスタックオフセットを代替する必要があります。
これは非常に複雑で実装が難しく、メンバー値のハッシュを使用して構造体のハッシュを構築する場合、ハッシュのパスに依存するため、組み合わせ性を壊します。
円環状データのハッシュを定義するために、標準を互換性のある方法で拡張することは可能です。
直線的な非循環グラフに対する最適化
直線的な非循環グラフに対する単純な実装は最適ではありません。
メンバーを再帰的に訪問する際に同じノードを二度訪問することがあります。
この問題を解決するためにはメモ化を使用して最適化できます。
これらの代替案は、Solidityでのハッシュ計算に関する異なるアプローチを提供していますが、それぞれに特定の制約や課題が存在します。
選択肢を検討し、適切なアプローチを選択する必要があります。
ドメインセパレータ(domainSeparator)の補足
異なるドメイン(領域または分野)には異なるセキュリティ要件や検証方法が必要である可能性があるため、柔軟なアプローチを取ることです。
これにより、各DApp(分散型アプリケーション)が自身のセキュリティ要件に合わせてカスタマイズできます。
DAppはEIP712Domain
構造体型とその具体的なインスタンス(eip712Domain
)を定義し、これをユーザーエージェントに提供します。
ユーザーエージェントは、受け取ったEIP712Domain
に含まれるフィールドに基づいて、異なる検証手法やセキュリティ対策を適用できます。
このアプローチの利点は、異なるDAppが異なるセキュリティ要件を持つ場合でも、EIP712スキームを使用できることです。
各DAppは自身のEIP712Domain
を定義し、必要なセキュリティ情報や設定を含めることができます。
ユーザーエージェントは、受け取ったEIP712Domain
に基づいて、特定のDAppに合わせた検証手法を実行できます。
この柔軟なアプローチにより、異なるドメインで異なるセキュリティニーズを満たすことができ、各DAppが自身の要件に合わせてセキュリティを強化できます。
同じEIP712スキームを使用しながら、カスタマイズされたセキュリティ対策を実施することで、システム全体のセキュリティが向上します。
後方互換性
RPC呼び出し、web3メソッド、SomeStruct.typeHash
パラメータはまだ具体的に定義されていない状態です。
これらの要素を具体的に定義することは、既存の分散型アプリケーション(DApp)の動作に影響を与えるべきではありません。
また、Solidityの式「keccak256(someInstance)
」は、SomeStruct
型のインスタンス「someInstance
」に対して有効な構文です。
現在、この式は「someInstance
」のメモリアドレスのkeccak256
ハッシュとして評価されています。
しかし、この動作は潜在的に危険であると考えるべきです。
一部の場面では正しく機能するように見えるかもしれませんが、他の場面では決定論性や単射性の問題を引き起こす可能性があります。
現在この動作に依存しているDAppは、危険に対して警戒すべきです。
これらの未定義の要素を具体的に定義し、Solidityの新しい動作を明確に定めることは、既存のDAppの動作に影響を及ぼさないように行われるべきです。
また、現在の危険な動作に依存しているDAppは、修正が必要であると警告されています。
テスト
コントラクトの例はExample.solに、JavaScriptによる署名の実装例はExample.jsにあります。
セキュリティ考慮事項
リプレイ攻撃(Replay Attacks)
この規格はメッセージの署名と署名の検証に関するものであり、実際のアプリケーションでは署名されたメッセージはアクションの承認に使用されます。
例えば、トークンの送金などが該当します。
実装者は、同じ署名されたメッセージが2回送信された場合に、アプリケーションが適切に振る舞うことを確認する必要があります。
具体的な対策方法はアプリケーションごとに異なり、この規格の範囲外ですが、繰り返しメッセージを拒否したり、認証済みのアクションを同じ効果を持つように設計することが考えられます。
フロントランニング攻撃(Frontrunning Attacks)
署名をブロックチェーンにブロードキャストするメカニズムは、アプリケーションごとに異なり、この規格の範囲外です。
署名がコントラクトで使用される時には、フロントランニング攻撃から保護するセキュリティ対策をアプリケーションが実施する必要があります。
この攻撃では、攻撃者が署名を傍受し、本来の使用前にコントラクトに提出します。
アプリケーションは、攻撃者が最初に署名を提出した場合に、正しく振る舞うべきです。
これは、攻撃を拒否するか、署名者の意図した効果を再現することで実現できます。
このセキュリティに関する考慮事項は、署名と署名の検証に関する規格の範囲外であり、アプリケーション固有のセキュリティ対策を実装する必要があることを示唆しています。
引用
Remco Bloemen (@Recmo), Leonid Logvinov (@LogvinovLeon), Jacob Evans (@dekz), "EIP-712: Typed structured data hashing and signing," Ethereum Improvement Proposals, no. 712, September 2017. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-712.
最後に
今回は「型付けされた構造化データのハッシュ化と署名の仕組みを提案している規格であるERC712」についてまとめてきました!
いかがだったでしょうか?
質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!
他の媒体でも情報発信しているのでぜひ他も見ていってください!