1.智能合約的源起
區塊鏈技術自比特幣誕生之日起便受到了廣泛的關注。最初,區塊鏈僅僅作為記錄用戶交易的底層賬本,不支持用戶訂制其它功能。
比特幣為了實現交易,即用戶間轉賬的功能,設計了一套基于棧的簡單腳本語言。這套語言不支持循環,不具備圖靈完備性,僅限于比特幣客戶端內部使用,且只圍繞交易這一項功能,一般被稱之為“Bitcoin Script”。
如果大家去查看比特幣區塊鏈中的每一筆交易記錄,那么會發現交易內容其實是一串字節碼,這串字節碼就是Bitcoin Script。比特幣對Bitcoin Script書寫的交易代碼的格式進行了限制,這種做法保證了交易的合法性與資金的安全性,但犧牲了整個系統的可編程性與靈活性。
一般情況下,一套語言能實現成千上萬種功能,如果設計一套語言只是實現了一個功能,未免有些可惜。或許Vitalik Buterin正是發現了區塊鏈中腳本語言的可能性,于是他在以太坊中把語言請到了舞臺中央,供用戶創建與調用,這也成為了以太坊最具魅力的特性——智能合約。
與比特幣的Bitcoin Script對應,以坊中腳本語言名字叫EVM語言(Ethereum Virtual Machine Code)。EVM語言也是基于棧的語言,但它是圖靈完備的語言,且以太坊設計了專門的虛擬機EVM來為其提供運行環境,這和Bitcoin Script有明顯的區別。
2.智能合約的使用以交易為接口
為了明確系統的功能,以太坊擴充了交易的概念。在比特幣中,交易一般指用戶之間的轉賬操作,在以太坊中,交易除了轉賬,還包括創建或者調用智能合約。因此可以說EVM語言也是為了交易而存在,但它服務的交易的內容廣泛得多。
我們先補充一些必要的概念。以太坊中賬戶分為外部賬戶與合約賬戶,外部賬戶就是用戶使用的賬戶,其中包括了用戶的私鑰和錢包等重要信息;合約賬戶用來存放一個智能合約,通常是由外部賬戶創建的。
用戶發送到以太坊區塊鏈上的每一筆交易中都包含幾個關鍵字段:“from”表示交易發起者,“to”表示交易接收者,“value”表示交易金額,“data”表示附帶的信息。
上文提及的三種操作的交易格式如下:
1) 普通轉賬操作:“from A, to B, value C”表示從外部賬戶A向外部賬戶B轉賬,轉賬金額為C;
2) 智能合約創建操作:“from A, to (空), value C, data D”表示外部賬戶A創建一個智能合約,向該合約賬戶里轉賬C, 合約的代碼為D;
3) 智能合約調用操作:“from A, to E, data F”表示外部賬戶A調用合約賬戶E的智能合約,本次調用傳入的參數為F。
3一筆交易的處理流程
下面我們來分析一筆交易在以太坊區塊鏈中是如何被處理與執行的。
這部分在以太坊的源碼中十分清晰,因此我們跟隨源碼里的函數調用流程來進行說明。以太坊Go版本源碼地址:https://github.com/ethereum/go-ethereum。
首先先定位,我們可以從core/blockchain.go中找到執行的core/state_processor.go中Process()方法,在Process()方法中可以找到如下一行代碼:
receipt, _,err:= ApplyTransaction(p.config, p.bc, nil, gp, statedb, header, tx, usedGas, cfg)
根據這個函數名字我們知道已經找到了執行交易的入口。
3.1 創建EVM虛擬機
瀏覽在core/state_processor.go中的ApplyTransaction()方法,可發現如下三行關鍵代碼:
context := NewEVMContext(msg, header, bc, author)
vmenv := vm.NewEVM(context, statedb, config, cfg)
_, gas, failed, err := ApplyMessage(vmenv, msg, gp)
第一行是創建新的EVM的執行上下文環境,第二行是創建新的EVM,第三行是用新創建的EVM來處理交易消息。由此可知,每一筆交易在執行之前以太坊都會創建一個EVM虛擬機來負責該交易的執行。
繼續瀏覽core/state_transition.go中的ApplyMessage()方法,發現該方法只有一行代碼:
return NewStateTransition(evm, msg, gp).TransitionDb()
繼續看core/state_transition.go中的TransitionDb()方法,發現方法中有一個重要的分支:
if contractCreation {
ret, _, st.gas, vmerr = evm.Create(sender, st.data, st.gas, st.value)
} else {
st.state.SetNonce(msg.From(), st.state.GetNonce(sender.Address())+1)
ret, st.gas, vmerr = evm.Call(sender, st.to(), st.data, st.gas, st.value)
}
這段代碼的意思是先判斷是不是創建合約的操作,如果是,則調用Create()方法,如果不是,則調用Call()方法。由此可知,交易的三種操作中,創建合約使用的方法是Create(),而調用合約與轉賬使用的方法是Call()。
接下來我們分別看一下這兩個方法。
3.2 智能合約的創建
先看core/vm/evm.go中的Create()方法。該方法只有兩行代碼:
contractAddr=crypto.CreateAddress(caller.Address(),evm.StateDB.GetNonce(caller.Address()))
return evm.create(caller, &codeAndHash{code: code}, gas, value, contractAddr)
第一行是根據外部賬戶的地址和nonce值計算將要創建的合約賬戶的地址,這個nonce參數用來記錄該外部賬戶已創建的合約的數目。
第二行是將創建的合約賬戶地址和其它參數一起傳給同文件中的create()方法。
繼續看create()方法可看到如下三行代碼:
nonce := evm.StateDB.GetNonce(caller.Address())
evm.StateDB.SetNonce(caller.Address(), nonce+1)
contractHash := evm.StateDB.GetCodeHash(contractAddr)
第一行是獲取想創建合約的外部賬戶的nonce值。
第二行是將該nonce的值加一后寫回去,第三行是計算合約地址的哈希值確保不會發生地址沖突。
在計算出合約地址后,可看到下面一行代碼:
evm.StateDB.CreateAccount(contractAddr)
這行代碼是根據合約地址創建出了合約賬戶。合約賬戶創建完后,可看到下面一行代碼:
evm.Transfer(evm.StateDB, caller.Address(), contractAddr, value)
創建者從外部賬戶向合約賬戶轉賬,金額為value。至此,合約賬戶創建工作完成了。
接下來需要創建合約對象并把合約代碼跑起來:
contract:=NewContract(caller, AccountRef(contractAddr), value, gas) contract.SetCallCode(&contractAddr, crypto.Keccak256Hash(code), code)
第一行是創建合約對象,第二行是將用戶定義的智能合約代碼綁定到該合約對象上。
合約對象創建完后,用下面一行代碼運行該合約:
ret, err = run(evm, contract, nil)
可能有人會疑惑:創建完合約為什么要運行一遍?
這主要有兩方面的原因:其一,系統需保證合約代碼是能正確運行的,這樣在以后的調用中才不會出錯;其二,系統需要通過運行才能計算出消耗的gas數量,進而完成對外部賬戶的創建合約操作的扣費。其實在create()方法中還有檢查棧深度、創建快照、出錯后回滾、gas計算等代碼,因它們不涉及到本文的主要內容,故略過。
3.3 轉賬與智能合約的調用
然后我們看core/vm/evm.go中的Call()方法,該方法負責合約調用和轉賬兩種交易操作。
Call()方法中不需要創建新的地址,只需要:
to = AccountRef(addr)
該行代碼獲取交易接收方的地址。之后可看到下面一行代碼:
evm.Transfer(evm.StateDB, caller.Address(), to.Address(), value)
交易發起者向交易接收方轉賬value金額。接下來的代碼和Create()相像:
contract := NewContract(caller, to, value, gas)
contract.SetCallCode(&addr,evm.StateDB.GetCodeHash(addr),evm.StateDB.GetCode(addr))
第一行創建合約對象,第二行將接收者地址上的智能合約代碼綁定到合約對象上。如果是轉賬操作,接收者地址上沒有代碼,因此綁定的代碼是空,之后的運行會很快結束。綁定完成后,使用run()方法運行該合約完成調用:
ret, err = run(evm, contract, input)
在Call()方法中同樣還有檢查棧深度、創建快照、出錯會滾、gas計算等代碼,留給感興趣的讀者自行閱讀。
Create()與Call()中最后執行都調用了core/vm/evm.go中的run()方法,而run()方法中可發現:
return interpreter.Run(contract, input, readOnly)
接下來定位到core/vm/interpreter.go中的Run()方法。該方法是EVM中解釋器的運行流程,核心邏輯為循環取出合約的代碼,查表解析出具體的操作碼,再查表計算出需要消耗的gas數目,然后調用操作碼相應的處理函數執行。
核心代碼如下:
for atomic.LoadInt32(&in.evm.abort) == 0 {
…
op = contract.GetOp(pc)
operation := in.cfg.JumpTable[op]
…
cost, err = operation.gasCost(in.gasTable, in.evm, contract, stack, mem, memorySize)
…
res, err := operation.execute(&pc, in, contract, mem, stack)
…
}
至此,一筆交易的運行就結束了。
4. 結語
綜上所述,我們能了解到以太坊設計的三種交易背后能給予用戶極大的自由度,也充分發揮了EVM語言及其虛擬機的功能。
用戶通過Solidity等語言編寫智能合約,然后編譯成EVM語言,再打包成交易的格式發送到區塊鏈上運行。以太坊得益于這種模式帶來的可編程的特性,引領區塊鏈技術進入了2.0時代。
然而,現在智能合約由于EVM的棧深度與gas消耗的限制,多數都是簡單且袖珍的程序。即使現在這樣的程序已經足夠滿足需求,但未來必將面臨更多更加復雜化的交易場景。
如何去應對這些場景,需要廣大開發者們繼續努力。
評論
查看更多