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