y-nakajo.hatenablog.com
今回は上記の以前の記事で触れた、Ethereumのdebug機構について調べたことをまとめていきます。
Ethereumでは任意のtxをdebugするための機能が、またSolidityではbyte codeとソースファイルをmappingするための情報が提供されています。
今回はこれらの情報について説明します。
[JSON-RPC] debug_traceTransaction
Ethereumではほとんどのnode実装がdebugのための情報を提供するためにdebug_traceTransactionというJSON-RPCを実装しています。
debug_traceTransactionは引数に指定したtransaction hashのtransactionを実行した時の状態をtraceし、その結果としてstep単位での実行されたopcode、program counter、stackの状態、memoryの状態、storageの状態を返します。
stack, memory, storageの情報は膨大になる可能性があるので、それぞれoptionでdisableにすることもできます。
より詳細な仕様については以下を参照してください。
github.com
注意点
debug_traceTransactionはnodeがフルノードであり、指定されたtransactionが実行された時のstateDBの状態を持っている場合にtrace情報を取得できます。つまり以下のような場合にはtrace情報を取得できません。
- fast modeで同期した時に切り捨てらてた情報に該当するtransactionを指定した時
- stateDBに影響を与えないtransaction (constantな情報を取得するだけのtransactionが該当)
現状はsendTransactionで発行されたtransaction(の一部)のみに対してdebug_traceTransactionが発行可能です。
debug_traceTransactionのgo-ethereum上での実装コードはこちら
go-ethereum/api_tracer.go at master · ethereum/go-ethereum · GitHub
[EVM] program_counter
program_counterとはContractをEVMが実行する時の実行コードの位置を指し示すものです。JUMP命令などは引数としてJUMP先のprogram counterを指定します。
program counterは基本的に、bytecodeを1byteずつの配列にした時のindex値になります。ただし、PUSH系命令の場合はその後にdataが続くため注意が必要です。program counterはdata部分も含めて連番で振られるためdata部分に該当するindex値は無効なprogram counterとしなければなりません。
より詳しい説明は以下の公式ドキュメントを参照ください。
Ethereum-Development-Tutorial#state-machineEthereum-Development-Tutorial#state-machine
[Solidity] source mapping
debuggerを作るだけであれば、上述したget_traceTransactionで取得した情報のみで十分です。
ですが、エラーが発生した時にソースコードの該当箇所を表示する といったtoolを作りたい場合はopcodeとsourceコードのmapping情報が必要になります。
ここではSolidity compilerが生成してくれるsource mappingについて説明します。
仕様については以下をご参照ください。
Solidity Documents#source-mappings
source mappingの種類
.solファイルをcompileすることで生成されるjsonファイルに含まれるsourceMapは以下の2種類があります。
- sourceMap: Contract Creationの時に実行されるbytecodeに対応するsourceMap (Contractをdeployした時に実行されるコード)
- deployedSourceMap: ContractのMethod呼び出しの時に実行されるbytecodeに対応するsourceMap
ContractをDeployする時と、deploy後のContractのmethodを呼び出す時で実行されるbytecodeが大きく違うため、それぞれのsourceMapが用意されています。(bytecodeも同じように2種類用意されています。)
sourceMapのフォーマット
sourceMap以下のフォーマットを1itemとし、実行されるopcodeの個数分生成されます。
start:length:fileIndex[:jumpSIgnature]
- start: opcodeが生成された元となるソースコードのファイルないのstart位置
- length: opcodeが生成された元となるソースコードの長さ
- fileIndex: opcodeが生成された元となるソースコードが記述されているfileのindex
- jumpSignature: jump命令だった場合のsignature(o: JUMP, i: JUMPI, -: JUMPDEST)
s:l:f[:j]を1itemとして、実行されるopcodeの個数分 ';'を区切り文字として連結されます。
また、情報量を削減するため以前のopcodeに対するmapping情報が同じ場合は省略されます。
1:9:1;1:9:1;1:9:1;2:7:1;
つまり上記のようなsourceMapsの場合は次のように省略されます。
1:9:1;;;2:7;
sourceMapとopcodeの突きあわせ方法
sourceMapと実際のopcodeを対応させるためにはbytecodeを変換させる必要があります。
理由としては、sourceMapはprogram counterを持っておらずまた、sourceMapのsize = bytecode sizeではないためです。
sourceMapのsizeはbytecode size - data size - 43と一致します。
data sizeとはopcodeがPUSH1 ~ PUSH32の時にその後に続くdataの総byte数になります。
43byteはContract Metadataです。詳細は以下を参照ください。
Solidity Documents#encoding-of-the-metadata-hash-in-the-bytecode
以下のロジックで実行命令のみを持つ配列を生成することではじめてsourceMapとopcodeを1対1でmappingすることができます。
- bytecodeの先頭から1byteずつopcodeをチェックする
- PUSH以外のopcodeが見つかったら配列に追加
- PUSH1 ~ PUSH32のPUSH系のopcodeが見つかったら、PUSHコードだけ配列に追加する。その後、データバイト数だけbytecode indexをずらす
- 1-3をSTOPが見つかるまで繰り返す
なお、truffle-code-utilsを使うことで上記処理をやってくれます。
file indexの求め方
次にfile indexから該当のソースファイルを見つける必要があります。file indexは以下のルールに従ってつけられています。
- compileで生成されたJSONファイルのsource.idの順番にソースファイルを並べる
- 上から順に0から番号をふる
です。素直にfile idでつけてくれていたら探索が簡単なのですがなぜかidではなく上記ルールに従ってソースファイルをソートした時のindex番号が振られています。
なお、開発途中でcompileを繰り返したり、手動で個別に1ファイルずつコンパイルするとfile indexがおかしな値になります。その場合は、build/contract/*以下を全て削除してから solc contracts/*もしくはtruffle compileをすればfile indexが振り直されます。