概述
在前面對 PolkaVM 和 Revive 的文章中,我們介紹了很多技術細節,開發工具。還對比 EVM,知道了 PolkaVM 的優勢。很多同學還是對 Polkadot SDK 為什么可以運行 EVM 兼容的智能合約,以及交易處理的整個流程不太清楚。這篇文章將會帶著大家梳理一下整個過程,在熟悉它之后,對于今后 Debug 代碼,找到出錯原因都會有很大好處。
整個處理的流程主要有下面幾個模塊組成,我們來逐一分析
RPC Server 接收交易,并轉發到節點
Runtime 定義交易的格式擴展
Revive 做交易的轉換,從 Eth 交易轉成 Polkadot SDK 的 extrinsic
最終的 bytecode 在 PolkaVM 中的執行
RPC Server
和普通的 Extrinsic 不同,也有別于之前在 Polkadot SDK 上出現的Frontier,Ink 的支持。在提交 Solidity 的交易的時候,我們會啟動一個單獨的RPC Server來接收交易。因此我們在配置本地的測試環境或者 Passet Hub 的時候,都需要這個 URL。
那么這個 Server 是怎么工作的呢?它的代碼也在 Revive Pallet下,package 名字是 pallet-revive-eth-rpc。它主要有三個組件,一個 subxt 的客戶端,一個 RPC 服務器,還有一個用來緩存數據的 sqlite 內存數據庫。
subxt 主要是和 node 來交互,比如向區塊鏈提交交易,查詢數據等。 RPC 服務器來實現以太坊的 Web3 服務接口,大部分函數定義都在 EthRpc 這個 Trait 可以找到。其他還有 debug api 和 health api。
當 server 收到請求,都會轉換成對區塊鏈的請求,通過 subxt 的客戶端發出,收到結果后返回給調用者。
這里分別舉二個例子,一個是查詢余額,一個提交交易。
這里的接口定義是 web3 的一致的,通過區塊號或者 Hash 來查詢余額。下面我們來看下它的實現。
這里 client 就是 subxt 的客戶端,它把傳進來的區塊號,哈希或者 Tag,統一轉換成區塊哈希,進行查詢,并返回結果。這里不用擔心 Decimal 的問題,在 Node 一側,當要查詢的是一個Eth格式的地址時,就已經做好了轉換。
下面是對發送交易的實現,它會組裝一個 Pallet 是 Revive,Extrinsic 是 eth_transact 的交易,然后發給 Node。這個 Extrinsic 非常的特殊,我們接下來會在分析 Revive 代碼的時候看下它是怎么處理的。
Sqlite 主要來緩存一些交易的數據,還有事件日志等。
Runtime
在 Runtime 中,需要包含 Revive Pallet 才能處理Eth的交易,還有對Runtime API的實現。一個最重要的代碼實現是對 UncheckedExtrinsic 的重新定義,對于不需要支持 Eth 的交易,普通的 sp_runtime::generic::UncheckedExtrinsic 就可以了。為了支持對 Eth 的交易,需要使用 Revive 里面 ?UncheckedExtrinsic 作為包含中 Block 中的類型。
這樣區塊鏈在處理交易池里面的 Eth 交易就可以識別他們,在驗證交易的時候把它轉換成普通的 Extrinsic。
Revive Pallet
Revive 當然是處理交易的核心,有些設計還比較的巧妙,我們先來看它如何轉換Eth的交易。對于一個提交的 Eth 交易,下面這個函數用來對它進行轉換,成為普通的 Extrinsic。
首先嘗試解碼 payload 成一個含有 Eth 簽名的交易,并得到發送交易的以太坊地址。隨后做一個基本的檢查,比 如nonce,gas,chain ID 等等。
下一步就是要轉換成 Call 了, 這里首先有一個特殊的地址 ?RUNTIME_PALLETS_ADDR,它被用來直接調用 Runtime 里面的任何交易。它把輸入的數據直接解碼成 Call 并調用,除此之外的都會轉換成 Revive 的 Call 方法,調用某個智能合約里面的一個方法。如果沒有調用的合約地址,那么只能是一個部署合約的方法,會把它嘗試轉換成一個 eth_instantiate_with_code 的方法。
從這里我們可以看出,從 Eth 來的交易,沒有只 upload_code 的方法,當然這個也不在以太坊接口的定義里面。
在下面就是對于 gas_price, gas_fee 的處理,還會把多給的 gas_fee 作 為tip。
最終返回一個 CheckedExtrinsic,它也在這里有了使用 Polkadot 帳號的簽名,它來自發送交易者映射到 Polkadot 地址格式的帳號。
這里有一個非常有意思,和難理解的就是 eth_transact 方法,它除了給出一個錯誤之外沒有任何其他邏輯。那么它的作用是什么呢?首先,它提供了一個接口給外部調用,比如我們在 RPC Server 里面使用 subxt 來調用它。其次,我們使用它就可以把提交來的交易解碼成這個 Call,得到它的 payload 部分,在用 payload 解碼成封裝的 Eth 交易。
但是在從 UncheckedExtrinsic 到 CheckedExtrinsic 的轉換中,它就被處理了,轉換成了其他的方法。call 或者 eth_instantiate_with_code,因此在正常情況下不會有 dispatch 到這個方法里面的交易。這就是為什么它只有報錯處理。
當然在 Revive 里面還有很多處理邏輯,包括我們前面文章提到的precompile,這里不再重復介紹。
交易在 VM 的執行
在 Revive 中處理交易中最核心的是一個 Stack 的數據結構,從這個名字來看,這里依然要模擬的還是 EVM 基于棧的處理模型。它的代碼如下:
除去基本信息比如區塊號,時間戳,還有就是對于計算和存貯的計量。通過 frames 來記錄每次對合約更深一層的調用,和函數的逐層調用一樣。
transient_storage 是一個在 EVM 中非常特殊的存貯,它在整個的交易中都是有效的,因此它的存貯也在 frame 之外,單獨存放。
在進行完一系列繁瑣的環境準備之后,代碼最終在 PolkaVM 的開始執行。下面是一個非常重要的數據結構 PreparedCall,我們通過它來調用 PolkaVM 去執行合約編譯后的 RISC-V 代碼,或者說 PolkaVM(它和標準的 RISC-V 有著細小的差別). Module 主要包含的是編譯好的程序代碼, RawInstance 是一個 PolkaVM 的實例,用來運行虛擬機。Runtime 則是區塊鏈節點提供給它的執行環境。
總結
在這個文章中,我們介紹了交易從發送到 RPC Server,到最終在 PolkaVM 里面運行的整個過程。希望能幫助大家在調試代碼的時候有所幫助,能夠根據錯誤信息,找到對應的模塊,解決問題。