こんにちは、えたろうです。
「少しだけ易しいMastering Bitcoin」を一通り読んだ方は、Bitcoinの仕組みはだいたい理解できたかと思います。
Bitcoinでブロックチェーンの基本を理解した後、次のステップはやはりEthereumの理解ですよね。
- 「Bitcoinの仕組みはだいたい分かった!ブロックチェーンすげぇ!」という方
- 「Ethereumでスマートコントラクトが実現する云々」「Ethereum上で Dappsを作る云々」というのはよく聞くけど、実際Ethereumってどう動いてるの?という方
を対象にして懇切丁寧に書いて行きます。
第4回では「Ethereumのトランザクション・ブロックの構造とトランザクション実行の流れを深く理解する」ことを目標にします。
第1回で見ていった「Ethereumのざっくりとした仕組み」を深掘りしていく感じです。
トランザクションの構造
「【第1回】Bitcoinを通じて理解するEthereum Ethereumの仕組み概観 ⑵トランザクション」
で、以下のことを学んだと思います。
- アカウントの状態の変化は全てトランザクションから引き起こされる
- トランザクションには ①Message Call ②Contract Creation の2つがある
- 2種類のトランザクションが2種類のアカウントと相まって、何パターンもの"状態の変化"を引き起こす
この章では、トランザクションについてそのデータ構造から見ていきます。
①Message Call の構造
Message Call トランザクションは上記のようなデータ構造を持ちます。
blockHash と blockNumberはこのトランザクションがどのブロックに入れられているかを表します。
またtransaction indexは、このトランザクションがブロックに含まれるトランザクションの中で何番目のトランザクションかを表します。
これらがnullの場合は、まだブロックに取り込まれていないことになります。
gas と gasPriceは、「【第2回】Bitcoinを通じて理解するEthereum トランザクション手数料Gasの概念」で学んだ通りです。
nonceは、このトランザクションの作成者が何番目に送信したトランザクションかを表し、アカウントのnonceから入れられます。
これによって「あるノードが連続で生成されたトランザクションを逆の順番で受け取ってしまった」ような場合にどちらのトランザクションが先かを判別するのに使われます。
r,s,v はECDSAデジタル署名であり、これがあることで「トランザクション作成者の特定」
「トランザクションが途中で改竄されていないことの検証」に使われます。
詳しくは、「ECDSAデジタル署名の仕組み」で解説しています。
Message Callトランザクションの場合、inputに入るのは、送信先に送るデータです。
特にContractアカウントに対してMessage Callトランザクションを送る場合に、そのコントラクトの中で実行する関数の指定と、その関数に渡す引数を入れるのに使われます。(EOAアカウントに対して何らかのinputを送ることも構造上不可能ではありません)
具体的には、関数プロトタイプをKeccak-256でハッシュ化したものの最初の4バイトでコントラクトの中でどの関数を呼び出そうとしているのかを指定します。
②Contract Creation の構造
Message Call トランザクションは上記のようなデータ構造を持ちます。
Contract 生成トランザクションの場合、inputに入るのは、initや初期化コードと呼ばれるプログラムコードです。
コントラクトアカウント = プログラムを新たに作成するわけですから、「どんなプログラムを作成するのか」といった初期化コードをここに入れておく必要があります。
また、Contract生成トランザクションは、これから新たにアカウントを作成するトランザクションですので、送り先アドレス to は空になります。
(正確にはtoの中身は空ではなく、zero addressと呼ばれる0x0になります。このzero addressはコントラクト作成のための送信先として用意されているものです)
※Ethereumのリプレイプロテクション EIP-155
EthereumにおけるリプレイプロテクションはEIP-155で規定されています。
具体的には上述してきたTransactionの構造の中に、chain識別子というデータカラムを追加(実際にはchain識別子と0と0の3つを追加)します。
これによってECDSA署名を生成する際に使用されるTransaction Hashがchain識別子ごとに変わることになり、別のchain識別子が振り当てられる別のチェーンで同じトランザクションデータをbroadcastしても、署名が一致せず、そのトランザクションは受け入れられません。
このようにして、リプレイアタックを防いでいます。
Chain識別子は以下の通りです。
第1章で上記の画像を使って学んだ通り、外部アカウントからコントラクトアカウントに対してMessage Callを送り、プログラムコードを実行させた場合には、そのコントラクトアカウントから他のコントラクトアカウントに対してMessage Callを送ることも可能であり、そのトランザクションのことをInternal トランザクションというのでした。
この内部トランザクションは、基本的にはMessage Callトランザクションと同じ構造を持ちますが、 gas が空の値になっている、すなわち gas Limitを持ちません。
内部トランザクションはあくまで最初に外部アカウントからコントラクトアカウントに送られたMessage Callに紐づくものであり、内部トランザクションで使用されたGasもそのMessage CallのGas Limitを消費していることになるからです。
また、もしある内部トランザクションを実行している最中に最初のMessage Callトランザクションの Gasが足りなくなってしまった場合は、その内部トランザクションは実行されなかったことになりますが、それ以前のMessage Callトランザクションや内部トランザクションの実行はなかったことにはなりません。
レシートの構造
第1回でも少し触れた「トランザクションが実行された結果として結果として出力されるReceipt」の構造を見ていきます。
トランザクションが実行されると、その実行結果をまとめたデータであるTransaction receiptが生成されます。
①Message Call のレシートの構造
Message Callトランザクションを実行した際に生成されるレシートは上記のようなデータ構造を持っています。
contractAddressには、コントラクトアカウントを作成した場合のそのアカウントのアドレスが入りますが、Message Callではアカウントは生成しないので空になります。
cumulativeGasUsedには、このトランザクションに関連する処理全体で使われたGasの量が入ります。
Contractアカウントに対してMessage Callを行なった場合には、そのContractアカウントから他のContractアカウントを実行するInternal(内部)トランザクションを送ることも可能だと第1回で学びましたが、そのような連鎖するトランザクションで使用したGasも合算されます。
一方で、gasUsedには、このトランザクション単体で使用したGasの量が入ります。
logsには、このトランザクションを実行した際のログが入り、これはDappsなどを開発する際には非常に重要です。
また、Gas切れでトランザクションの実行が途中で終了してしまった場合にも、「どこで終了したのか」などのログがここに残ります。
logsBloomには、ブロック内の全てのトランザクションの実行ログがBloom Filterとして記録されています。
rootは、このトランザクションを実行した際に起きた、"アカウント状態"の変更を反映した上での世界の"状態"を格納しています。
これについては次章のブロックの構造で解説します。
②Contract生成 のレシートの構造
Contract 生成トランザクションを実行した際に生成されるレシートは上記のようなデータ構造を持っています。
Message Callトランザクションのレシートとの違いは、
- contractAddressが入っている点
- contract生成トランザクションからは、内部トランザクションが連鎖して実行されることはないため、cumulativeGasUsedがgasUsedと等しくなる点
- to が空になる点
があります。
ブロックの構造
Ethereumのブロックの構造は上記のようになっています。
ブロックには
①ブロックヘッダ
②そのブロックに含まれるトランザクションのリスト
③そのブロックのUncleブロックのブロックヘッダのリスト
の大きく分けて3つの要素が含まれています。
※注意書き
3つ目の「そのブロックのUncleブロックのブロックヘッダのリスト」は、
他の日本人解説者の方の解説と異なっています。
osuke(zoom)さんの「【図解】ブロックチェーンレベルでのイーサリアムの理解」
coffetimesさんの「Ethereumはどのように動いているのか」
では共に、3つ目のブロック要素を「ommerのための他のブロックヘッダーの情報」と解説されていますが、これは恐らくお二方とも参考にされているPreethi Kasireddyさんの「How does Ethereum work, anyway?」のブロック構造の解説部分が
のようになっているからだと思います。
画像のfor は恐らくof の間違いであるのですが、forの場合を直訳すると「現在のブロックのommerのための他のブロックヘッダーのリスト」となるのが原因かなと思っています。
では、ブロックの中で核となるブロックヘッダの構造をみていきます。
ブロックヘッダに含まれる要素は以下の15個の要素です。
parentHash : 親ブロックのブロックヘッダのハッシュ(KECCAK-256ハッシュ形式)
sha3Uncles(ommersHash) : 現在のブロックのuncleブロックのブロックヘッダリストのハッシュ値 (図でいうと緑で囲まれた③をハッシュ化したもの)(KECCAK-256ハッシュ形式)
difficulty : このブロックを生成する難易度。以前のブロックの難易度とタイムスタンプから計算される。genesisブロックでは131,072だった。
Bitcoinと同じようにブロック生成時間を一定に保つために調整される。
extraData : このブロックに関連する任意のデータを入れ込める。32byteまで。
gasLimit : ブロック全体のgasLimit制限。このブロックに含めるトランザクションのgasLimitの合計がこのブロック全体のgasLimit制限以下でなければならない。
gasUsed : このブロック内の全てのトランザクションに使われたガスの合計値
hash : このブロックを表すハッシュ
logsBloom : トランザクションが実行されたのちに生成されるレシートにlogsBloomがありましたが、これは「ブロック内の全てのトランザクションから出力されるログがBloom Filter に記録されたもの」です。ここでのログには「実行されたトランザクションの内容、実行したアカウント」などの情報が含まれています。
Bloom Filter 形式で保存することによって「特定のログがこのブロック内に存在するか」がが簡単に判断できるようになります。
miner (beneficiary) : このブロックを生成し、マイニング報酬を受け取るアカウントのアドレス(20byte)
mixHash : nonce と合わさることでPoWのための十分な計算がされたことの証明になる256bitのハッシュ
nonce : mixHash と合わさることでPoWのための十分な計算がされたことの証明になる64bitのハッシュ
number : このブロックのブロック番号 (genesis ブロックは0)
size : このブロックのサイズ(単位はbyte)
timestamp : このブロックが生成された時間(UNIXタイムスタンプ)
totalDifficulty : このブロック以前のブロックのdifficultyの総和
transactions : このブロックに含まれるtransactionのハッシュの配列 (これがもしかしたら黄色の枠で囲まれた②かも)
stateRoot : このブロックの全トランザクションが実行された後の、全てのアカウント状態をマークルパトリシアツリーで要約したRoot値のハッシュ値(KECCAK-256ハッシュ形式)
transactionsRoot : このブロックの全トランザクションをマークルパトリシアツリーで要約したRoot値のハッシュ値(KECCAK-256ハッシュ形式)
receiptsRoot : このブロックの全レシートをマークルパトリシアツリーで要約したRoot値のハッシュ値(KECCAK-256ハッシュ形式)
このようにEthereumのブロックヘッダはBitcoinと比較しても、多くの情報が入っています。
この図のイメージと各要素の詳しい構造を踏まえて、Ethereumブロックチェーンの全体構造を示したのが以下の図になります。
ブロックヘッダの構造で見た通り、State Rootが全てのアカウントの状態(state)を要約したものです。
各アカウントはNonce, Balance, Codehash, Storage rootという4つの値を持っており、Storage Rootはそのアカウントが持つデータの要約でした。
またTransactionsRootがこのブロックに含まれる全てのトランザクションの要約値、
ReceiptsRootがこのブロックに含まれるトランザクションの実行結果である全てのレシートの要約値です。
ここでポイントなのは、ブロックに含まれているのはこれらの要約値のみであり、実際の「全てのアカウント状態のデータ」「全てのレシートのデータ」はブロックチェーンの外で管理されているということです。
これはつまり、各フルノードがそれぞれのローカルストレージにこれらの値を保持していてこれらの実データはネットワークに伝搬されることはないということです。
しかし、これらのデータのマークルパトリシア木のルートハッシュがブロックに含まれているため、実データが改ざんされて作られたRoot値の場合には、すぐにその変更に全てのノードが気づくことができます。このように要約値で実データの検証はすることができるようにした上で、ネットワークに伝搬されるデータのサイズを小さくしているのです。
また、Uncleブロックのヘッダリストを含むことによって、このブロックの生成時に、Uncleブロックにも報酬を与えられるようになっています。
トランザクションの実行
第1回の「Ethereumの仕組み概観 ⑶世界の"状態"が記録されていく一連の流れ」でざっくりと見ていった、トランザクション実行の一連の流れについて詳しく書いていきます。
トランザクションは「外部アカウント(EOA)」からしか作成できないのでした。
また、トランザクションには「Message Call」と「Contract生成」の2種類があり、「外部アカウント」と「コントラクトアカウント」のどちらに送るかによってトランザクションの場合も変わってくることも学びました。
ここではBobが新たなトランザクションを作成すると仮定して、それぞれの場合のトランザクションがどのように実行されて行くのか、その順序を以下で見ていきます。
①
Bob(ユーザー・外部アカウント、多くの場合はウォレットから操作)が2種類のうちどちらかの新規トランザクションを作成します。
②
Bobが、作成したトランザクションをEthereumネットワーク(主に登録されているマイナーノード)に伝搬します。
③
新規トランザクションを受け取った各ノードが独立してトランザクションの検証を行います。
この時のトランザクション検証の基準は以下の通りです。
(1)
トランザクションがEthereumで使われる形式であるRLPフォーマットという形式になっているか
(2)
トランザクションのgasLimitが「トランザクションを実行するのに必要なGasの量(intrinsic gas)」よりも高く設定されているか
※intrinsic gasとは
事前に決められたトランザクション実行のためのGasコスト : 21,000gas
トランザクションのinputに入れるdata, codeのための手数料 : 最低68gasが必要で、送るdata, codeが1byte増えるごとに4gas増える
コントラクト生成のための手数料:コントラクト生成トランザクションの場合は32,000gas
以下の画像は「How does Ethereum work, anyway?」から引用
(3)
送信者のアカウントのBalance(Ether残高)は、必要な額[gasLimit × gasPrice + value(送金するetherの量)]を上回っているか
(4)
デジタル署名(r,s,v)が有効であるか (詳細はECDSAデジタル署名の仕組みで解説しています)
この検証で「このトランザクションは間違いなくこの作成者によって作られたものだ」ということを確認しています。
(5)
トランザクションに入っているnonceが、トランザクション作成者(Bob)のnonceと一致するか
④
③の検証を終えたトランザクションは一時的に各ノードのPool(Mempool)に入れられます。
このPoolは、Pending pool(未実行トランザクションプール)とQueued pool(待機トランザクションプール)という2つに分かれており、③の検証でnonceが一致したものは通常のPending poolに入れられ、nonceが一致しなかったものはQueued poolに入れられます。
ある外部アカウントがトランザクションを短い時間に連続して作成・伝搬した場合には、マイナーノードに順番通りにトランザクションが届かないことがあります。
この場合、nonceの値が一致しないため、一旦、Queued poolへ送られ、マイナーノードにまだ届いていない'nonceが一致するトランザクション'が承認された時に初めて、Pending poolに移動されます。
⑤
Bitcoinと同様に同時に行われているマイニング競争に勝利し、ブロック生成権を得たマイナーがブロックを作成します。
⑦
未実行トランザクションを実行し、それによって変化した全てのアカウント状態、トランザクション、ログ等を反映させます。
このトランザクション実行について詳しく見ていきます。
トランザクションが実行される際には、事前に ”substate”というデータが新たに作られ、実行の間、保持されます。
「実行中に決定するけれども、実行が終わった後直ぐに必要になる情報を記録しておくための箱」がsubstateです。
substateに含まれる要素は以下の通りです。
Self-destruct set:トランザクション実行後に破棄されるアカウントがもしあれば、そのアカウントのデータ
Log series:トランザクションがどこまで実行されたかを記録・保持するチェックポイント
Refund balance:トランザクション後に返金としてトランザクション作成者に返される額。通常のトランザクションで行われるState Treeへのデータの書き込みには処理が伴い、手数料がかかりますが、逆にState Treeからデータを削除する処理をした場合は、「State Treeを縮小している」ことになるので、その分の報酬が返ってくるというものです。
トランザクション実行の初めは0ですが、コントラクトがストレージ内のデータを何か消すたびに増えていきます。
実際の実行の手順についてトランザクションの種類ごとに見ていきます。
(1) Message Callトランザクションの実行
Message Callトランザクションには第1回で紹介したように上記の画像の場合があるのでした。
このトランザクション実行の手順は以下の通りです。
(a) State Treeにあるトランザクション作成者の外部アカウントのNonceを1増やす
(b) トランザクション実行に使うかもしれないGas上限(トランザクションのgasLimit × gasPrice + value + intrinsic gas)を作成者の外部アカウント残高からGasとして徴収
(c) トランザクションにある処理を「外部アカウント同士の残高の変更」「コントラクトアカウントのプログラム実行」などを実行する。
この時、input にdataが入っていた場合には、それがコントラクトアカウントに渡されることもある。
(d) トランザクション実行後、残っているGasとsubstateのRefund balanceの額を合計したものを作成者アカウントに返金
(e)マイナーにGas分のEtherが送られ、self-destructに入っているアカウントは消されます。また、トランザクションで使ったGasはブロックのgasUsedに追加され、ブロックのログ関係のデータも更新されます。
(2) Contract生成トランザクションの実行
Contract生成トランザクション実行の手順は、Message Callトランザクションの(c)の部分が
以下の(a') ~ (f')になったものです。
(a')トランザクション作成者の外部アカウントのアドレスとnoneから、コントラクトのアドレスを生成
(b')新規のコントラクトアカウントなので、Nonceは0を入れる
(c') トランザクションの送金額(value)に入っているetherをコントラクトアカウントのBalanceに入れる
(d') アカウントのStorageを空に設定する
(e') 空の文字列のハッシュをコントラクトのcodeHashとして設定
(f') トランザクションのinput に入れられている初期化コード(init)を実行してコントラクトアカウントを作成
初期化コードの実行では、アカウントのStorageをアップデートする、他のコントラクトアカウントを作る(Contract creationトランザクション)、Message callトランザクションを行うなど、このトランザクションのinitを書いた人の意向によりあらゆる処理が実行され得ます。
また、もちろん初期化コードの実行にはGasを使うため、トランザクションに残っているGasが足りない場合は、out of gas exceptionを返し、処理が終了します。
もしgas切れエラーで終了すると、このコントラクト生成トランザクションは事になり、使われたガスは戻ってきません。(トランザクション内で送ろうとしていたEther (value)は、戻ってきます)
ただし、プログラム処理上でアクセス権限がないために処理が止まってしまう場合などにエラーを投げるようにできるRevert code を使用した場合は、エラー時に残っていたGas分もトランザクション作成者に返されます。
正常に初期化コードの実行が成功すると、コントラクト生成に必要なGas(コントラクトコードのサイズに伴って大きくなるStorage cost)が支払われ、余ったGasはトランザクション作成者に返されます。
もしこれらを支払う十分なガスがない場合は、またout of gas exceptionエラーとなり、処理が中止されます。
ここまでがマイナーによって行われるトランザクションの実行の詳しい手順です。
第4回では「Ethereumのトランザクション・ブロックの構造とトランザクション実行の流れ」を詳しく見ていきました。
トランザクションが実行され、状態の変更が反映されたブロックは、以降
⑧
ブロックを完成させたマイナーがそのブロックを他のノードに伝搬する。
⑨⑩
送られてきたブロックを各ノードが独立して検証し、
正当であれば自身のチェーンに追加する。
と続きますが、この部分の詳細に関しては、次回で解説していきます。
続き↓
0コメント