前言
很高興能再次與您見面,再次感謝您一直以來對這系列文章的支持。今天我們會開始深入探討到EVM(以太坊虛擬機)上的一點點基礎知識。
若您是一個常常在etherscan.io上追蹤交易的朋友,一定對上面提到的各項資訊並不陌生。但我們觀察一筆交易時會發現,etherscan.io又額外提供了Internal Txns這項資訊。內部交易?這是什麼意思?它有被記錄在區塊鏈上嗎?今天的議題就會來看看它的真面目是什麼。
etherscan.io上的Internal Txns
Internal Txns
首先,Internal Txns並不是一筆真正的交易紀錄,它並不會被記錄在區塊鏈上。所以您無法用一般的RPC方法去向節點詢問到這項資訊。
事實上Internal Txns指的是一筆交易在EVM當中執行時,合約之間互相呼叫的紀錄。例如這筆交易發送給了A合約,但A合約中使用了B合約中的function,B合約又使用了C合約中的function,這樣您在etherscan.io中應會看到類似於下面的這種紀錄存在:
call: from A → to B
call: from B → to C
但這種紀錄不會被記錄在區塊鏈上,etherscan.io又要如何得知這種EVM的內部呼叫呢?
回放一筆交易 (Replay)
區塊鏈上記錄了所有的交易紀錄,因此我們也可以回放任何一筆交易來驗證其正確性。在以太坊節點的JSON-RPC標準中,有個名為
debug_traceTransaction的API,它實現了交易回放與記錄的功能。除了回放交易外,它還會將EVM中執行的每一個步驟和運算結果都記錄下來,非常詳盡。目前Geth和Ganache都有實現該API的功能,因此我們也可以用Ganache來試看看。
ganache --fork.network mainnet \
--chain.networkId 1 \
--accounts 10 \
--wallet.seed 1337 \
--port 8545
- 另外我們也一樣透過Metamask使用Uniswap來交易1000 USDT,執行步驟和我們先前的文章相同
深入了解區塊鏈(一) - 如何使用Ganache來模擬一個真實的以太坊
- 交易成功後,請將該筆交易的交易ID從Metamask記錄下來
- 我們可以透過該ID向Ganache呼叫debug_traceTransaction方法,由於web3.py不支援該方法的呼叫,因此我們需要自行透過requests套件來呼叫
import requests
#將您剛剛複製的交易ID取代下面的tx_token字串
tx_token = '您的交易ID'
payload = {
"jsonrpc": "2.0",
"id": 1,
"method": "debug_traceTransaction",
"params": [tx_token]
}
r = requests.post('http://127.0.0.1:8545', headers={'Content-Type': 'application/json'}, json=payload)
#可將內容存為文字檔來觀察,不建議直接把這內容印出到Notebook(內容很長)
with open('txn_trace.log', 'w') as fw:
fw.write(r.text)
- 若將txn_trace.log文字檔打開,並以JSON格式pretty-print後,可以看到類似的以下內容
這些內容記錄了這筆交易於EVM中所執行的每個指令(OP),以及各種儲存體(stack/memory/storage)的變化狀態,本篇暫先不詳盡說明這些細節,未來有機會再和各位介紹。
- 這次我們想要關注的重點是合約間的互相呼叫(Internal Txns),因此我們需要關注的幾個OP為:
- CALL
- STATICCALL
- CALLCODE
- DELEGATECALL
可以發現到經過一個CALL呼叫後,depth變成了2,而且所有的儲存體狀態都消失了。這代表著現在已經跳到了另一個合約的作用域當中。細節就待未來的篇章再行說明。
另外可以發現到進行CALL這個呼叫時,Stack的第二個參數(由下往上數)就是跳轉的目標合約地址。CALL這個OP的參數意義可以
參考這裡。
- 透過這些發現,我們可以寫一段簡單的Python Code來將所有的內部呼叫印出來
#列出合約地址間呼叫的關聯性
logs = r.json()['result']['structLogs']
history = [UNI_ROUTER_ADDR]
storage = {}
for idx in range(1, len(logs)):
if logs[idx-1]['depth'] < logs[idx]['depth']:
op = logs[idx-1]['op']
if op in ['CALL', 'STATICCALL', 'CALLCODE', 'DELEGATECALL']:
addr = logs[idx-1]['stack'][-2][-40:]
history.append('({}) 0x{}'.format(op, addr))
storage[addr] = logs[idx-1]['storage']
print(' -> '.join(history))
elif logs[idx-1]['depth'] > logs[idx]['depth']:
history.pop()
印出來的結果如下:
經由Uniswap Router交換Token時會呼叫到的相關合約
Uniswap Router會先到WETH合約將ETH封裝成ERC20標準的WETH,之後會呼叫Uniswap的USDT池中將USDT轉給您,並將您的WETH轉到USDT池中
後記
從本篇已開始從底層來認識這些交易,也稍微摸到了EVM的一點點基礎,未來若有機會再來和各位探討一下更為底層的技術。再次感謝您閱讀完這篇文章,期待下一次再見。
作者Steve目前任職於趨勢科技區塊鏈安全研究小組,專注於區塊鏈合約安全與與相關技術,如果喜歡我的文章,或是想獲得更多區塊鏈詐騙事件懶人包,歡迎關注
我的帳號!
另外,我已經加入由
趨勢科技防詐達人所成立的方格子專題-《區塊鏈生存守則》,在那裡我會跟其他優質的創作者一起帶大家深入瞭解區塊鏈,並隨時向大家更新區塊鏈資安事件