RLPエンコードしてトランザクションハッシュを取得してみる
最近、RLPを使った実装をする機会があったので、実際に挙動を確認しながらRLPについて解説していきます。
RLP(Recursive Length Prefix)とは
RLPとは入れ子構造のバイナリーデータ、もしくはバイト配列を符号化する方法です。
RLPエンコードは、シリアライズされたオブジェクトを符号化しEthereumのノードに保存するためにさまざまな場所で使われています。
RLPエンコードが使われている一番身近な例を挙げると、Etherscanなどのexploerを見る際に一度は目にしたであろうトランザクションハッシュです。トランザクションハッシュはトランザクションの中身をRLPエンコードし、エンコードされたデータをハッシュ化した文字列になります。
本記事ではRLPの解説をした後に実際のトランザクションデータをRLPエンコードし、それをhash化してトランザクションハッシュを算出してみようと思います。
コードはこちらにあります。
Length Prefixの早見表
RLPエンコードには5つのパターンがあり、細かく分けるとエンコードする対象が「バイト配列」の場合と「バイト配列のリスト」である場合で分けられます。
5つのパターンは下記にまとめられます。
| Prefix | 説明 | 例 |
|---|---|---|
| 0x00 ~ 0x7f | 1byteの値は、Prefix自体がデータとなる | |
| 0x80 ~ 0xb7 | 文字列が55byte未満の場合、"0x80(128)"と文字列の長さを足したものがPrefix、その後に文字列が続く | |
| 0xb8 ~ 0xBf | 文字列が55byteより長い場合、"0xb7(183)"に文字列の長さのbyte表現を足したものと、文字列の長さのbyte表現がprefixになり、その後に文字列が続く | |
| -- | -- | -- |
| 0xc0 ~ 0xf7 | 56byte未満のリストの場合、"0xc0(192)"にリストの長さを足したものがPrefixになり、その後にデータが続く | |
| 0xf8 ~ 0xff | 56byte以上のリストの場合、"0xf7(247)"にリストの長さのbyte表現を足したものとリストの長さをbyte表現したものがPrefixになり、その後にデータが続く |
Length Prefixの詳細
ここから5つのパターンを実際のデータを当てはめながら見ていきます。
0x00 ~ 0x7f
1byteの値は、Prefix自体がデータになります。
7fは16進数であり、10進数では127になります。つまり127までのデータは、Prefixがそのままデータを表します。
実際にencodeした結果がこちらになります。
data: 0x1 -> encoded: 01
data: 0x10 -> encoded: 10
data: 0x7f -> encoded: 7f
0x80 ~ 0xb7
文字列が55byte未満の場合、"0x80(128)"と文字列の長さを足したものがPrefix、その後に文字列が続く形になります。
実際にencodeした結果がこちらになります。
data: 0x80 -> encoded: 8180
data: dog -> encoded: 83646f67
data: doge -> encoded: 84646f6765
data: dogdogedogdoge -> encoded: 8e646f67646f6765646f67646f6765
data: dog -> encoded: 83646f67の結果を細かく見ていきましょう。
encodeされたデータは2byteずつ、つまり83、64、6f、67見ていけば中身を確認することができます。
最初の2byteはPrefixであり83になります。この値は0x80に文字列の長さ3を足したものです。80は10進数に変換すると128であり、この値に3をたすと131になり、131を再び16進数に変換すると83になります。
次の2byteは'64'で、dを表しています。
文字列は各バイトでhex_encodeされ2バイトの16進数で表現されます。
実際に下記サイトでdをhex_encodeすると64という値が取得できます。
- hex_encodeツール
同じように次の6fはoを、その次の67はgを表しています。
0xb8 ~ 0xBf
文字列が55byteより長い場合、"0xb7(183)"に文字列の長さのbyte表現を足したものと、文字列の長さのbyte表現がprefixになり、その後に文字列が続きます。
実際にencodeした結果がこちらになります。
data: abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz
-> encoded: b84e6162636465666768696a6b6c6d6e6f707172737475767778797a6162636465666768696a6b6c6d6e6f707172737475767778797a6162636465666768696a6b6c6d6e6f707172737475767778797a
Prefixはb8になります。b8はb7の10進数である183に1を足した、184の16進数です。
従って、encodeされたデータの長さは2番目の値4eで表現されています。4eを10進数にすると78になり、文字列の長さは78であることがわかります。
0xc0 ~ 0xf7
56byte未満のリストの場合、"0xc0(192)"にリストの長さを足したものがPrefixになり、その後にデータが続きます。
実際にencodeした結果がこちらになります。
data: [ '0x1', '0x7f', 'dog' ] -> encoded: c6017f83646f67
Prefixはc6つまり198であり、198から192(c0)を引いた8がリストの長さになります。
01が0x1、7fが0x7f、83646f67がdogeを表しています。
ここで気をつけるポイントは、リストの中身もRLPエンコードして表現されている点です。
0xf8 ~ 0xff
56byte以上のリストの場合、"0xf7(247)"にリストの長さのbyte表現を足したものとリストの長さをbyte表現したものがPrefixになり、その後にデータが続きます。
data: ['0x1', '0x7f', 'doge', 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'
]
-> encoded: f857017f84646f6765b84e6162636465666768696a6b6c6d6e6f707172737475767778797a6162636465666768696a6b6c6d6e6f707172737475767778797a6162636465666768696a6b6c6d6e6f707172737475767778797a
Prefixのf8(248)からf7(247)を引くと1になるため、次の57がリストの長さを表します。
Transaction Hashを求めてみる
RLPエンコードの5つの方法について説明しました。
ここからは実際に、このRLPエンコードがどのように使われてるのかを見ていきます。
このトランザクションの中身をRLPエンコードし、実際のこのトランザクションのハッシュ、0x2a2a493613533004e3d5e6aa33a280e766f65730ff10656a5213259f47d244ddを求めます。
(Etherscanから適当にとってきたトランサクションになります。)
## RLPエンコード
このトランザクションの中身を取得し、RLPエンコード用に加工すると下記のようになります。
{
chainId: '0x05',
nonce: '0x44',
maxPriorityFeePerGas: '0x9502f900',
maxFeePerGas: '0x9502f918',
gasLimit: '0x5208',
to: '0x6D29Fc79Eab50b1aB0C8550EC2952896aBCf0472',
value: '0x11c37937e08000',
data: '0x',
accessList: [],
v: '0x',
r: '0x0dc0954df847954f465ea8b7535b96a608d10bf15e8a259629a15262bbd0f7df',
s: '0x04e4f15c6f88b61958fea9e248552e202399e5ceee788501109730b41a2b63dd'
},
このトランザクションデータをRLPエンコードします。
const rawTx = {
chainId: '0x05',
nonce: '0x04',
maxPriorityFeePerGas: '0x59682f00',
maxFeePerGas: '0x59682f12',
gasLimit: '0xbe12',
to: '0xf5de760f2e916647fd766B4AD9E85ff943cE3A2b',
value: '0x',
data: '0x095ea7b3000000000000000000000000b31913136db41a06c316b8d19b86bca36a42a126000000000000000000000000000000000000000000000000000000000013be0d',
accessList: [],
v: '0x01',
r: '0x35e3d3c8d16c485dd361acef0576f4667efc9f2ce023327f677ae2669099ed91',
s: '0x2f5d90de7a9e49b9782787c768c6f1141b38c19d69925291c04f2a0ee8e94f60'
},
const encodedTx = Buffer.from(rlp.encode(Object.values(rawTx))).toString('hex');
console.log("encoded tx", encodedTx);
> f8af05048459682f008459682f1282be1294f5de760f2e916647fd766b4ad9e85ff943ce3a2b80b844095ea7b3000000000000000000000000b31913136db41a06c316b8d19b86bca36a42a126000000000000000000000000000000000000000000000000000000000013be0dc001a035e3d3c8d16c485dd361acef0576f4667efc9f2ce023327f677ae2669099ed91a02f5d90de7a9e49b9782787c768c6f1141b38c19d69925291c04f2a0ee8e94f60
エンコードされたデータの詳細を見ていきましょう。
Prefixがf8(246)であるため、f8(248)からf7(247)を引くと1になります。従って、prefixの次の値である71が全体の長さを表現しています。
その次の05はchainId: '0x05'を表現しており、1byteの文字列なのでprefix自体がそのままデータとなっています。
次の44はnonce: '0x44'を表現しており、こちらも1byteなのでprefix自体がそのままデータてとなっています。
次の84は0x80より大きいため、表の2番目のパターンに当てはまります。従って、84(132)から80(128)を引いた4がデータの長さになり9502f900が該当するデータになります。
よって、849502f900はmaxPriorityFeePerGas: '0x9502f900'を表現していることがわかります。
このようにPrefixをつけることによって、encodeされたデータを中身を順番に確認することができます。
ハッシュ化
RLPエンコードされたデータからトランザクションハッシュを得るためには、エンコードされたデータにトランザクションタプである0x02を足したデータを、Kecack256でハッシュ化する必要があります。
const txhashFronTx = web3.utils.soliditySha3(
"0x02" + encodedTx
);
console.log("txhash:", txhashFronTx);
> 0x2a2a493613533004e3d5e6aa33a280e766f65730ff10656a5213259f47d244dd
出力結果である0x2a2a493613533004e3d5e6aa33a280e766f65730ff10656a5213259f47d244ddは、先に示したトランザクションハッシュと同じであることがわかります。
おわりに
このように、EthereumではRLPエンコードを使ってトランサクションデータを整形していることがわかりました。
Ethereumではデータを保存しやすいようにデータをRLPエンコードしているのですが、次回の記事ではどのように保存しているのかに触れていきたいと思います。
参考文献
- RECURSIVE-LENGTH PREFIX (RLP) SERIALIZATION
- EIP1559
- Ethereum Yellow Paper: a formal specification of Ethereum, a programmable blockchain
- RustでEthereumのRLP
- [Japanese] RLP
- RLPとMerkle Patricia Tree(Trie)
- HEX_ENCODE
Discussion