🔑

RLPエンコードしてトランザクションハッシュを取得してみる

2022/11/13に公開約7,000字

最近、RLPを使った実装をする機会があったので、実際に挙動を確認しながらRLPについて解説していきます。

RLP(Recursive Length Prefix)とは

RLPとは入れ子構造のバイナリーデータ、もしくはバイト配列を符号化する方法です。
RLPエンコードは、シリアライズされたオブジェクトを符号化しEthereumのノードに保存するためにさまざまな場所で使われています。

RLPエンコードが使われている一番身近な例を挙げると、Etherscanなどのexploerを見る際に一度は目にしたであろうトランザクションハッシュです。トランザクションハッシュはトランザクションの中身をRLPエンコードし、エンコードされたデータをハッシュ化した文字列になります。

本記事ではRLPの解説をした後に実際のトランザクションデータをRLPエンコードし、それをhash化してトランザクションハッシュを算出してみようと思います。

コードはこちらにあります。
https://github.com/0xywzx/event/tree/main/20221112_rlp

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ずつ、つまり83646f67見ていけば中身を確認することができます。

最初の2byteはPrefixであり83になります。この値は0x80に文字列の長さを足したものです。80は10進数に変換すると128であり、この値に3をたすと131になり、131を再び16進数に変換すると83になります。

次の2byteは'64'で、dを表しています。
文字列は各バイトでhex_encodeされ2バイトの16進数で表現されます。
実際に下記サイトでdをhex_encodeすると64という値が取得できます。

同じように次の6foを、その次の67gを表しています。

0xb8 ~ 0xBf

文字列が55byteより長い場合、"0xb7(183)"に文字列の長さのbyte表現を足したものと、文字列の長さのbyte表現がprefixになり、その後に文字列が続きます。
実際にencodeした結果がこちらになります。

data: abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz 
-> encoded: b84e6162636465666768696a6b6c6d6e6f707172737475767778797a6162636465666768696a6b6c6d6e6f707172737475767778797a6162636465666768696a6b6c6d6e6f707172737475767778797a

Prefixはb8になります。b8b7の10進数である1831を足した、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がリストの長さになります。
010x17f0x7f83646f67dogeを表しています。
ここで気をつけるポイントは、リストの中身も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から適当に撮ってきたトランサクションになります。)
https://goerli.etherscan.io/tx/0x2a2a493613533004e3d5e6aa33a280e766f65730ff10656a5213259f47d244dd

## 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が全体の長さを表現しています。

その次の05chainId: '0x05'を表現しており、1byteの文字列なのでprefix自体がそのままデータとなっています。
次の44nonce: '0x44'を表現しており、こちらも1byteなのでprefix自体がそのままデータてとなっています。
次の840x80より大きいため、表の2番目のパターンに当てはまります。従って、84(132)から80(128)を引いた4がデータの長さになり9502f900が該当するデータになります。
よって、849502f900maxPriorityFeePerGas: '0x9502f900'を表現していることがわかります。
このようにPrefixをつけることによって、encodeされたデータを中身を順番に確認することができます。

ハッシュ化

RLPエンコードされたデータからトランザクションハッシュを得るためには、エンコードされたデータにトランザクションタプである0x02を足したデータを、Kecack256でハッシュ化する必要があります。
https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1559.md

const txhashFronTx = web3.utils.soliditySha3(
	"0x02" + encodedTx
);
console.log("txhash:", txhashFronTx);

> 0x2a2a493613533004e3d5e6aa33a280e766f65730ff10656a5213259f47d244dd

出力結果である0x2a2a493613533004e3d5e6aa33a280e766f65730ff10656a5213259f47d244ddは、先に示したトランザクションハッシュと同じであることがわかります。

おわりに

このように、EthereumではRLPエンコードを使ってトランサクションデータを整形していることがわかりました。
Ethereumではデータを保存しやすいようにデータをRLPエンコードしているのですが、次回の記事ではどのように保存しているのかに触れていきたいと思います。

参考文献

Discussion

ログインするとコメントできます