以太坊原始碼分析(18)以太坊交易執行分析

尹成發表於2018-05-13
#以太坊交易執行分析

在這裡,將其整體串起來,從state_processor.Process函式開始,歸納一下其所作的處理。

##1 Process
Process 根據以太坊規則執行交易資訊來對statedb進行狀態改變,以及獎勵挖礦者或者是其他的叔父節點。
Process返回執行過程中累計的收據和日誌,並返回過程中使用的Gas。 如果由於Gas不足而導致任何交易執行失敗,將返回錯誤。
**處理邏輯:**
~~~
1. 定義及初始化收據、耗費的gas、區塊頭、日誌、gas池等變數;
2. 如果是DAO事件硬分叉相關的處理,則呼叫misc.ApplyDAOHardFork(statedb)執行處理;
3. 對區塊中的每一個交易,進行迭代處理;處理邏輯:
a. 對當前交易做預處理,設定交易的hash、索引、區塊hash,供EVM釋出新的狀態日誌使用;
b. 執行ApplyTransaction,獲取收據;
c. 若上一步出錯,中斷整個Process,返回錯誤;
d. 若正常,累積記錄收據及日誌。迴圈進入下一個交易的處理。
4. 呼叫共識模組做Finalize處理;
5. 返回所有的收據、日誌、總共使用的gas。
~~~

##2 ApplyTransaction(1.3.b )
ApplyTransaction嘗試將交易應用於給定的狀態資料庫,並使用輸入引數作為其環境。
它返回交易的收據,使用的Gas和錯誤,如果交易失敗,表明塊是無效的。
**處理邏輯:**
~~~
1. 將types.Transaction結構變數轉為core.Message物件;這過程中會對傳送者做簽名驗證,並獲得傳送者的地址快取起來;
2. 建立新的上下文(Context),此上下文將在EVM 環境(EVM environment)中使用;上下文中包含msg,區塊頭、區塊指標、作者(挖礦者、獲益者);
3. 建立新的EVM environment,其中包括了交易相關的所有資訊以及呼叫機制;
4. ApplyMessage, 將交易應用於當前的狀態中,也就是執行狀態轉換,新的狀態包含在環境物件中;得到執行結果以及花費的gas;
5. 判斷是否分叉情況( `config.IsByzantium(header.Number)` ),如果不是,獲取當前的statedb的狀態樹根雜湊;
6. 建立一個收據, 用來儲存中間狀態的root, 以及交易使用的gas;
7. 如果是建立合約的交易,那麼我們把建立地址儲存到收據裡面;
8. 拿到所有的日誌並建立日誌的布隆過濾器;返回。
~~~

##3 ApplyMessage(2.4)
ApplyMessage將交易應用於當前的狀態中,程式碼裡就是建立了一個StateTransition然後呼叫其TransitionDb()方法。
ApplyMessage返回由任何EVM執行(如果發生)返回的位元組(但這個返回值在ApplyTransaction中被忽略了),
使用的Gas(包括Gas退款),如果失敗則返回錯誤。 一個錯誤總是表示一個核心錯誤,
意味著這個訊息對於這個特定的狀態將總是失敗,並且永遠不會在一個塊中被接受。

##4 StateTransition.TransitionDb()
~~~
1. 預檢查,出錯則函式返回;
a. 檢查交易的Nonce值是否合規;
b. buyGas:根據傳送者定的gaslimit和GasPrice,從傳送者餘額中扣除以太幣;從區塊gas池中減掉本次gas;並對執行環境做好更新;
2. 支付固定費用 intrinsic gas;
3. 如果是合約建立, 那麼呼叫evm的Create方法建立新的合約,使用交易的data作為新合約的部署程式碼;
4. 否則不是合約建立,增加傳送者的Nonce值,然後呼叫evm.Call執行交易;
5. 計算並執行退款,將退回的gas對應的以太幣退回給交易傳送者。
~~~

###4.3 evm.Create建立新的合約
~~~
1. 檢查執行深度,若超過params.CallCreateDepth(即1024)就出錯返回;剛開始的執行深度為0,肯定繼續往下執行;
2. 檢查是否可執行轉賬,即檢查賬戶餘額是否≥要轉賬的數額;
3. 傳送者Nonce加1;
4. 建立合約地址並獲取hash,若該合約地址已存在,或不合法(空),則出錯返回;
5. 儲存statedb快照,然後根據合約地址建立賬戶;
6. 執行轉賬evm.Transfer(在statedb中,將value所代表的以太幣從傳送者賬戶轉到新合約賬戶);
7. 根據傳送者、前面建立的合約賬戶,轉賬的錢,已用的gas建立並初始化合約;將交易的data作為合約的程式碼;
8. 執行前一步建立的合約
9. 判斷執行結果是否有錯誤。如果合約成功執行並且沒有錯誤返回,則計算儲存返回資料所需的GAS。 如果由於沒有足夠的GAS而導致返回值不能被儲存則設定錯誤,並通過下面的錯誤檢查條件來處理。
10. 若EVM返回錯誤或上述儲存返回值出現錯誤,則回滾到快照的狀態,並且消耗完剩下的所有gas。
~~~

###4.4 evm.Call執行交易
Call方法, 無論我們轉賬或者是執行合約程式碼都會呼叫到這裡, 同時合約裡面的call指令也會執行到這裡。
Call方法和evm.Create的邏輯類似,但少了一些步驟。
~~~
1. 檢查是否允許遞迴執行以及執行深度,若深度超過params.CallCreateDepth(即1024)就出錯返回;
2. 檢查是否可執行轉賬,即檢查賬戶餘額是否≥要轉賬的數額;
3. 儲存statedb快照,建立接收者賬戶;
4. 如果接收者在statedb中尚不存在,則執行precompiles預編譯,與編譯結果為nil時出錯返回;無錯誤則在statedb中建立接收者賬戶;
5. 執行轉賬;
6. 根據傳送者、接收者,轉賬的錢,已用的gas建立並初始化合約;將交易的data作為合約的程式碼;
7. 執行前一步建立的合約
8. 若EVM返回錯誤,則回滾到快照的狀態,並且消耗完剩下的所有gas。
~~~

虛擬機器中合約的執行另行分析。

### eth原始碼交易傳送接收,校驗儲存分析:

```
建立合約指的是將合約部署到區塊鏈上,這也是通過傳送交易來實現。在建立合約的交易中,to欄位要留空不填,在data欄位中指定合約的二進位制程式碼,
from欄位是交易的傳送者也是合約的建立者。

執行合約的交易

呼叫合約中的方法,需要將交易的to欄位指定為要呼叫的合約的地址,通過data欄位指定要呼叫的方法以及向該方法傳遞的引數。

所有對賬戶的變動操作都會先提交到stateDB裡面,這個類似一個行為資料庫,或者是快取,最終執行需要提交到底層的資料庫當中,底層資料庫是levelDB(K,V資料庫)

core/interface.go定義了stateDB的介面

ProtocolManager主要成員包括:
peertSet{}型別成員用來快取相鄰個體列表,peer{}表示網路中的一個遠端個體。
通過各種通道(chan)和事件訂閱(subscription)的方式,接收和傳送包括交易和區塊在內的資料更新。當然在應用中,訂閱也往往利用通道來實現事件通知。
ProtocolManager用到的這些通道的另一端,可能是其他的個體peer,也可能是系統內單例的資料來源比如txPool,或者是事件訂閱的管理者比如event.Mux。
Fetcher型別成員累積所有其他個體傳送來的有關新資料的宣佈訊息,並在自身對照後,安排相應的獲取請求。
Downloader型別成員負責所有向相鄰個體主動發起的同步流程。

func(pm *ProtocolManager) Start()

以上這四段相對獨立的業務流程的邏輯分別是:
1.廣播新出現的交易物件。txBroadcastLoop()會在txCh通道的收端持續等待,一旦接收到有關新交易的事件,會立即呼叫BroadcastTx()函式廣播給那些尚無該交易物件的相鄰個體。
2.廣播新挖掘出的區塊。minedBroadcastLoop()持續等待本個體的新挖掘出區塊事件,然後立即廣播給需要的相鄰個體。當不再訂閱新挖掘區塊事件時,這個函式才會結束等待並返回。很有意思的是,在收到新挖掘出區塊事件後,minedBroadcastLoop()會連續呼叫兩次BroadcastBlock(),兩次呼叫僅僅一個bool型引數@propagate不一樣,當該引數為true時,會將整個新區塊依次發給相鄰區塊中的一小部分;而當其為false時,僅僅將新區塊的Hash值和Number傳送給所有相鄰列表。
3.定時與相鄰個體進行區塊全鏈的強制同步。syncer()首先啟動fetcher成員,然後進入一個無限迴圈,每次迴圈中都會向相鄰peer列表中“最優”的那個peer作一次區塊全鏈同步。發起上述同步的理由分兩種:如果有新登記(加入)的相鄰個體,則在整個peer列表數目大於5時,發起之;如果沒有新peer到達,則以10s為間隔定時的發起之。這裡所謂"最優"指的是peer中所維護區塊鏈的TotalDifficulty(td)最高,由於Td是全鏈中從創世塊到最新頭塊的Difficulty值總和,所以Td值最高就意味著它的區塊鏈是最新的,跟這樣的peer作區塊全鏈同步,顯然改動量是最小的,此即"最優"。
4.將新出現的交易物件均勻的同步給相鄰個體。txsyncLoop()主體也是一個無限迴圈,它的邏輯稍微複雜一些:首先有一個資料型別txsync{p, txs},包含peer和tx列表;通道txsyncCh用來接收txsync{}物件;txsyncLoop()每次迴圈時,如果從通道txsyncCh中收到新資料,則將它存入一個本地map[]結構,k為peer.ID,v為txsync{},並將這組tx物件傳送給這個peer;每次向peer傳送tx物件的上限數目100*1024,如果txsync{}物件中有剩餘tx,則該txsync{}物件繼續存入map[]並更新tx數目;如果本次迴圈沒有新到達txsync{},則從map[]結構中隨機找出一個txsync物件,將其中的tx組傳送給相應的peer,重複以上迴圈。

以上四段流程就是ProtocolManager向相鄰peer主動發起的通訊過程。儘管上述各函式細節從文字閱讀起來容易模糊,不過最重要的內容還是值得留意下的:本個體(peer)向其他peer主動發起的通訊中,按照資料型別可分兩類:交易tx和區塊block;而按照通訊方式劃分,亦可分為廣播新的單個資料和同步一組同型別資料,這樣簡單的兩兩配對,便可組成上述四段流程。

在上文的介紹中,出現了多處有關p2p通訊協議的結構型別,比如eth.peer,p2p.Peer,Server等等。這裡不妨對這些p2p通訊協議族的結構一併作個總解。以太坊中用到的p2p通訊協議族的結構型別,大致可分為三層:

第一層處於pkg eth中,可以直接被eth.Ethereum,eth.ProtocolManager等頂層管理模組使用,在型別宣告上也明顯考慮了eth.Ethereum的使用特點。典型的有eth.peer{}, eth.peerSet{},其中peerSet是peer的集合型別,而eth.peer代表了遠端通訊物件和其所有通訊操作,它封裝更底層的p2p.Peer物件以及讀寫通道等。
第二層屬於pkg p2p,可認為是泛化的p2p通訊結構,比較典型的結構型別包括代表遠端通訊物件的p2p.Peer{}, 封裝自更底層連線物件的conn{},通訊用通道物件protoRW{}, 以及啟動監聽、處理新加入連線或斷開連線的Server{}。這一層中,各種資料型別的界限比較清晰,儘量不出現揉雜的情況,這也是泛化結構的需求。值得關注的是p2p.Protocol{},它應該是針對上層應用特意開闢的型別,主要作用包括容納應用程式所要求的回撥函式等,並通過p2p.Server{}在新連線建立後,將其傳遞給通訊物件peer。從這個型別所起的作用來看,命名為Protocol還是比較貼切的,儘管不應將其與TCP/IP協議等既有概念混淆。
第三層處於golang自帶的網路程式碼包中,也可分為兩部分:第一部分pkg net,包括代表網路連線的<Conn>介面,代表網路地址的<Addr>以及它們的實現類;第二部分pkg syscall,包括更底層的網路相關係統呼叫類等,可視為封裝了網路層(IP)和傳輸層(TCP)協議的系統實現。

```



```
Receiptroot我們剛剛在區塊頭有看到,那他具體包含的是什麼呢?它是一個交易的結果,主要包括了poststate,交易所花費的gas,bloom和logs

blockchain無結構化查詢需求,僅hash查詢,key/value資料庫最方便,底層用levelDB儲存,效能好

stateDB用來儲存世界狀態
Core/state/statedb.go

注意:1. StateDB完整記錄Transaction的執行情況; 2. StateDB的重點是StateObjects; 3. StateDB中的 stateObjects,Account的Address為 key,記錄其Balance、nonce、code、codeHash ,以及tire中的 {string:Hash}等資訊;

所有的結構湊明朗了,那具體的驗證過程是怎麼樣的呢
Core/state_processor.go
Core/state_transition.go
Core/block_validator.go

StateProcessor 1. 呼叫StateTransition,驗證(執行)Transaction; 2. 計算Gas、Recipt、Uncle Reward

StateTransition
1. 驗證(執行)Transaction;
3. 扣除transaction.data.payload計算資料所需要消耗的gas;
4. 在vm中執行code(生成contract or 執行contract);vm執 行過程中,其gas會被自動消耗。如果gas不足,vm會自 選退出;
5. 將多餘的gas退回到sender.balance中;
6. 將消耗的gas換成balance加到當前env.Coinbase()中;

BlockValidator
1. 驗證UsedGas
2. 驗證Bloom
3. 驗證receiptSha
4. 驗證stateDB.IntermediateRoot

/core/vm/evm.go
交易的轉帳操作由Context物件中的TransferFunc型別函式來實現,類似的函式型別,還有CanTransferFunc, 和GetHashFunc。
core/vm/contract.go
合約是evm用來執行指令的結構體

入口:/cmd/geth/main.go/main

```
#EVM分析

>EVM不能被重用,非執行緒安全

Context結構體:為EVM提供輔助資訊。一旦提供,不應更改。
~~~
// Context 為EVM提供輔助資訊。一旦提供,不應更改。
type Context struct {
    // CanTransfer 返回 賬戶是否擁有足夠的以太幣以執行轉賬 CanTransfer CanTransferFunc
    // Transfer 轉賬函式,將以太幣從一個賬戶轉到另一個賬戶
    Transfer TransferFunc
    // GetHash 返回n對應的雜湊
    GetHash GetHashFunc

    // Message information
    Origin common.Address // Provides information for ORIGIN
    GasPrice *big.Int // Provides information for GASPRICE

    // Block information
    Coinbase common.Address // Provides information for COINBASE
    GasLimit uint64 // Provides information for GASLIMIT
    BlockNumber *big.Int // Provides information for NUMBER
    Time *big.Int // Provides information for TIME
    Difficulty *big.Int // Provides information for DIFFICULTY
}
~~~

> state_processor.Process開始執行交易處理,就是在那裡為入口進入到evm的執行的,具體見[core-state-process-analysis.md](core-state-process-analysis.md)


##EVM的實現
以太坊的EVM整個完全是自己實現的,能夠直接執行Solidity位元組碼,沒有使用任何第三方執行時。
執行過程是同步的,沒有啟用go協程。

1. evm最終是呼叫Interpreter執行位元組碼;
2. Interpreter.go實現執行處理;解析出操作碼後,通過JumpTable獲取操作碼對應的函式執行,並維護pc計數器、處理返回值等;
3. jump_table.go定義了操作碼的跳轉對映;
4. instructions.go實現每一個操作碼的具體的處理;
5. opcodes.go中定義了操作碼常量


對於EVM的測試,以太坊將測試程式碼放在了core\vm\runtime目錄下,提供了供測試用的執行時及測試用例。
測試用例的示例如:
~~~
func TestExecute(t *testing.T) {
    ret, _, err := Execute([]byte{
        byte(vm.PUSH1), 10,
        byte(vm.PUSH1), 0,
        byte(vm.MSTORE),
        byte(vm.PUSH1), 32,
        byte(vm.PUSH1), 0,
        byte(vm.RETURN),
    }, nil, nil)
    if err != nil {
        t.Fatal("didn't expect error", err)
    }

    num := new(big.Int).SetBytes(ret)
    if num.Cmp(big.NewInt(10)) != 0 {
        t.Error("Expected 10, got", num)
    }
}
~~~




網址:http://www.qukuailianxueyuan.io/



欲領取造幣技術與全套虛擬機器資料

區塊鏈技術交流QQ群:756146052  備註:CSDN

尹成學院微信:備註:CSDN


相關文章