如何將 EVM 跟踪映射到合約源?
換句話說,如何獲得失敗交易的“經典堆棧跟踪”?
例如,我們有一個跟踪 (不要看“Missing opcode 0xfd” - 它是恢復指令)和合約可靠性源。如何找出引發異常的原始碼的哪一行?
我設法組裝了契約(使用 solc –asm),但沒有 PC(程序計數器)提示,所以我找不到對應於 PC=557 的行。此外,我認為在編譯期間執行了優化,但儘管該程序集在某種程度上仍然是可讀的。
我正在使用 solc 0.4.16+commit.d7661dd9.Linux.g++。
提前致謝。
我去了這個兔子洞,最後得到了一個可以工作的概念證明。我不能推薦這次旅行。許多級別存在阻抗不匹配,需要進行大量格式轉換。最後,我的實現仍然沒有處理跨合約呼叫。(似乎沒有辦法確定特定程序計數器屬於哪個合約地址,除非解釋呼叫指令)。
我的實現太髒了,無法分享,但主要步驟是:
1)您需要
solc
生成一個執行時源圖。它不能直接輸出這個,但它可以將它作為“組合 json 輸出”的一部分輸出。為此,執行solc --combined-json bin-runtime,srcmap-runtime
.const srcmaps = JSON.parse(fs.readFileSync("./Contract.json")); const srcmap = srcmaps.contracts["./contracts/Contract.sol:Contract"]["srcmap-runtime"]; const source = fs.readFileSync("./contracts/Contract.sol").toString(); const bin = Buffer.from( srcmaps.contracts["./contracts/Contract.sol:Contract"]["bin-runtime"], "hex" );
2)sourcemap格式被壓縮,需要自己寫一個decoder。格式的規範在solidity 文件中。您現在可以將指令索引映射到源偏移量。
3)我們不想要源文件中的字節偏移量,而是行號和列號。為此,您需要解析源文件並創建從字節偏移到行/列對的映射。我決定暫時忽略這個並使用
get-line-from-pos
npm 包。第 2 步和第 3 步一起是:
const parsed = srcmap .split(";") .map(l => l.split(":")) .map(([s, l, f, j]) => ({ s: s === "" ? undefined : s, l, f, j })) .reduce( ([last, ...list], { s, l, f, j }) => [ { s: parseInt(s || last.s, 10), l: parseInt(l || last.l, 10), f: parseInt(f || last.f, 10), j: j || last.j }, last, ...list ], [{}] ) .reverse() .slice(1) .map( ({ s, l, f, j }) => `${srcmaps.sourceList[f]}:${getLineFromPos(source, s)}` );
4)源映射在指令號中,但我們需要字節碼地址。為了解決這個問題,我們需要建構一個從字節碼偏移到指令號(或其他方式)的映射。我發現自己解析執行時二進製文件最容易。
PUSH_n
所有指令都是1 字節長,除了長指令n+1
。const isPush = inst => inst >= 0x60 && inst < 0x7f; const pushDataLength = inst => inst - 0x5f; const instructionLength = inst => (isPush(inst) ? 1 + pushDataLength(inst) : 1); const byteToInstIndex = bin => { const result = []; let byteIndex = 0; let instIndex = 0; while (byteIndex < bin.length) { const length = instructionLength(bin[byteIndex]); for (let i = 0; i < length; i += 1) { result.push(instIndex); } byteIndex += length; instIndex += 1; } return result; };
然後您需要獲取給定事務的回溯:
const promisify = func => async (...args) => new Promise((accept, reject) => func(...args, (error, result) => (error ? reject(error) : accept(result))) ); const rpcCommand = method => async (...params) => (await promisify(web3.currentProvider.sendAsync)({ jsonrpc: "2.0", method, params, id: Date.now() })).result; const traceTransaction = rpcCommand("debug_traceTransaction");
一旦你擁有了所有這些,你就可以得到一個類似於經典 stracktrace 的東西:
const trace = await traceTransaction(result.tx); trace.structLogs.forEach(({op, pc, gasCost}) => console.log( `${pc}\t${op}\t${gasCost}\t${byteToInstr[pc]}\t${parsed[ byteToInstr[pc] ]}` ) );
我希望盡快把它清理乾淨並把它變成一個圖書館。處理痕跡並將它們映射回solidity的能力有很多用途。
如果你能掌握合約的原始碼,你可以使用Hardhat來獲取 Solidity 堆棧跟踪。Hardhat Network 是一個調試優先的 EVM 實現,專為智能合約的低級開發而建構。
無恥外掛:從我的使用安全帽的 Solidity 模板開始:https://github.com/paulrberg/solidity-template:
旁注:參見 Nomic Labs 的公告。