從比特幣腳本引擎到以太坊虛擬機(下篇)

這個系列是目標受衆是區塊鏈開發者和有其餘開發經驗的CS專業學生html

在本文的上篇中,咱們從交易模型入手,針對鎖定腳本和解鎖腳本的工做原理,探討了比特幣腳本引擎的設計邏輯,而且對P2PKH和P2SH兩種重要的機制進行了分析。相對於比特幣,以太坊致力於搭建一個圖靈完備的可編程的區塊鏈平臺。爲了這個目標,它作了那些不一樣於比特幣的設計呢?git

以太坊虛擬機

區塊鏈範式

Gavin Wood在黃皮書中將區塊鏈系統抽象爲基於交易的狀態機:github

公式(1)中S是系統內部的狀態集合,f是交易狀態轉移函數,T是交易信息,初始狀態即Gensis狀態;
公式(2)中F是區塊層面狀態轉移函數,B是區塊信息;
公式(3)定義B是一系列交易的區塊,每一個區塊都包括多個transaction;
公式(4)G是區塊定稿函數,在以太坊中包括uncle塊校驗、獎勵礦工、POW校驗等。數據庫

這個數學模型不只是以太坊的基礎,也是目前大多數基於共識的去中心化交易系統的基礎。
以太坊相對比特幣的提高,本質體如今這個範式中的fS。它的核心理念——具有圖靈完備和不受限制的內部交易存儲空間的區塊鏈。分別對應:編程

  • 功能強大的函數f,可以執行任何計算,比特幣不支持loop;
  • 狀態S記錄任意類型的數據(包括代碼),而比特幣的UTXO模型只能計算出地址的可花費額度。

數據結構

在數據存儲方面,比特幣經過UTXO模型計算地址餘額,不鼓勵用戶存入其餘數據;經過P2SH腳本機制,理論上能夠設計各類智能合約,但受限於腳本語言的表達能力,難以支持複雜的合約開發。這種設計對於加密貨幣來講是合理的。後端

以太坊爲了支持記錄任意的信息、執行任意函數,須要從新設計數據結構。設計模式

Merkle Patricia Trie

以太坊中重度使用Merkle Patricia Trie組織、存儲數據,下面咱們會看到,這個新的數據結構是經過對哈希樹和前綴樹的組合創新來達到目的。
約定:下面使用MPT來代替Merkle Patricia Trie。數組

Merkle Tree

又稱hash tree:樹的每一個葉子結點是某個數據塊的哈希值,而每一個非葉子結點是孩子結點的哈希值。如圖所示,這棵樹不存儲Data blocks自己。在P2P網絡環境中,惡意網絡節點若是修改了這顆樹上的數據,將沒法經過校驗(Merkle Proof),從而保證了數據的完整、有效性。這依賴於單向哈希加密的性質。這種性質讓它普遍應用在分佈式系統的數據校驗中,好比IPFS、Git等。 安全

中本聰也巧妙利用該性質,設計了比特幣的SPV(簡化支付驗證)功能。以下圖,用戶不須要運行完整的結點,只須要下載最長鏈的區塊頭數據,而後獲取待驗證交易對應區塊的merkle樹作校驗。

Patricia Trie

又叫Radix Trie,是前綴樹的空間優化變種:若是樹上某個節點是其父節點的惟一子結點,則這兩個結點能夠合併起來。它在這裏的應用是對長整型數據的映射,由某個20bytes的以太坊地址映射到其帳戶,形如<Address,Account>,Address會加密編碼成16進制的數字——在Patricia Trie上,表現爲非葉結點連成的路徑。網絡

好比,在Patricia Trie上存儲<"dog","Snoopy">,"dog"會被編碼爲"64 6f 67",先找到根節點,則查詢路線爲root->6->4->6->15->6->7->value,value也就是一個指向"Snoopy"的hash。這種方式相對hash表的好處在於不會出現衝突;但若是不作優化,查詢步驟太長。

改良點

爲了提升效率,以太坊對樹上結點數據類型進行了專門的設計。包括如下四類結點

  • null結點 表明空字符串
  • branch結點 17個元素的非葉節點,形如<i0,i1...i15,v>
  • leaf結點 2個元素的葉結點,形如<encodedPath,value>,encodedPath是地址加密編碼後的長整型數字串的一部分
  • extension結點 2個元素的非葉結點,形如<encodedPath,k>,extension的做用是把沒有分叉的路徑上結點合併起來,節省空間資源
    如圖,是一個簡化的狀態樹(狀態樹後文很快會詳細解釋,這裏不妨礙做示意圖),右上角就是<地址,餘額>的映射。prefix項的做用是輔助編碼,能夠忽略。4個帳戶的地址,按照MPT組織起來。其中全部的extension節點只是優化做用,均可以用多個branch結點替代。

使用MPT須要有後端數據庫(以太坊中使用levelDB)維護每一個結點間的鏈接關係,這個數據庫叫作狀態數據庫。使用MPT的好處包括:(1)這個結構的根節點是加密的且依賴於全部的內部數據,它的哈希能夠用於安全性校驗,這是merkle樹的性質,但和merkle樹不存儲數據塊自己不一樣的是,MPT樹結點存儲了地址數據,這是Patricia樹的性質(2)容許任何一個以前狀態(根部哈希已知的條件下)經過簡單地改變根部哈希值而被召回。

狀態

上面在解釋MPT時已經介紹了狀態樹的概念。以太坊中的世界狀態(World State)的概念,經過MPT映射存儲去中心化交易系統記錄的任意狀態。這對應了區塊鏈範式中的S,是以太坊設計的一個核心概念。

如圖,一個簡化的區塊中有三個root hash,對應三棵MPT。其中state root就是狀態樹的根哈希,它是地址(160bit)到帳戶數據(Account,序列化存儲在levelDB)中。每次有效的交易都會致使狀態變化,好比圖中簡單示意了Account175的balance從27變爲45,而全部其餘的帳戶多沒有發生交易,那麼block175224只須要新建Account175相關分支上的數據,而其餘分支不須要複製!固然以太坊主網上新區塊包含的交易大概爲幾十到幾百不等,那麼涉及的修改也會更多。關於這種結構性能上的討論參考這篇 文章查詢最新的帳戶狀態的入口應該是最新被確認的區塊的狀態樹。

對以太坊的帳戶模型須要專門作個介紹。

Account

比特幣使用UTXO模型計算餘額,沒法知足記錄任意狀態的需求。以太坊設計了Account模型,它會存儲包括:
[nonce, balance, storageRoot, codeHash]
其中nonce是交易計數器,balance是餘額信息,storageRoot對應另一個MPT,經過它可以在數據庫中檢索到合約的變量信息,codeHash是代碼hash值,建立後不可更改

帳戶分爲兩種

  • 外部帳戶(externally owned accounts)
    外部帳戶由私鑰控制,對應Account模型裏,storageRoot、codeHash並不存在,也就是不會存儲、執行代碼。若是隻有外部帳戶,那麼以太坊只能支持轉帳功能。
  • 合約帳戶(contract accounts)
    合約帳戶能夠經過外部帳戶發起交易建立,也能夠是由另外一個合約帳戶建立。合約帳戶在收到消息調用時,會加載代碼,經過EVM執行相應的邏輯,修改內部存儲的狀態。

交易

在UTXO模型下,交易本質上是(經過簽名的數據)對input的解鎖和對output的鎖定。在Account模型下,交易分爲兩種:

  • 建立合約,經過代碼建立新的合約
  • 消息調用,能夠轉帳也能夠觸發合約的某個函數

兩種類型的交易都包括如下字段:
[nonce,gasPrice,gasLimit,to,value,[v,r,s]]

  • nonce: 帳戶發出交易數量
  • gasPrice,gasLimit: 用於限制交易執行時間,防止程序死循環
  • to:交易的接受者
  • value:轉帳額度,若是是建立合約,就是捐贈給合約的額度
  • v,r,s:交易簽名相關數據,能夠用來肯定交易發送者

合約建立還須要:

  • init:一段不限大小的字節數組表示的EVM代碼,僅在合約建立時運行一次;init執行後返回body代碼片斷,以後的合約調用都會運行body代碼內容。
    合約帳戶的地址由sender和nonce共同決定,因此任意兩次成功的合約部署獲得的地址都是不一樣的。從上圖能看出,代碼和狀態的存儲是分開的。實際上編譯後的字節碼會存儲在一個virtual ROM中,且不可修改。

消息調用還須要:

  • data:一段不限大小的字節數組,表示消息調用時的輸入
    消息調用會修改帳戶的狀態,多是EOA帳戶也多是合約帳戶。

交易既能夠由外部帳戶發起,也能夠由合約發起。好比第5228886區塊包含170個交易和7個內部合約交易。

區塊

以太坊的區塊了加入更多的數據項,相對比特幣要複雜不少,但其實本質上區別不大。好比加入了叔鏈哈希,優化激勵措施,這是爲了支持挖礦協議;區塊自己還會有大量的有效性驗證、序列化。這些內容不在本文主題範圍,不深刻討論。

參見上面這張圖的右半部分,一窺以太坊區塊如何組織數據,能看到MPT樹的大量使用;左半部分涉及到EVM,將會是接下來的重點。

EVM設計與執行

以太坊虛擬機(Ethereum Virtual Machine)是執行以太坊的狀態轉移函數的運行環境。 有個簡單的問題,以太坊是否能夠不專門開發一款底層VM,而是複用Java、Lisp、Lua等呢?理論上是徹底能夠的,Corda項目就徹底基於JVM平臺開發。可是更多的區塊鏈項目會選擇專門開發底層設施,包括比特幣的腳本引擎。以太坊官方給出的解釋:

  • 以太坊的VM規格更簡單,而別的通用VM有不少沒必要要的複雜性
  • 允許定製開發,好比32bytes的字
  • 避免其餘VM帶來的外部依賴問題,可能致使安裝困難
  • 採用其餘VM,須要作徹底的安全性審查,權衡下不必定能省多少事

內存模型

EVM也是基於棧式計算機模型,但除了stack外還涉及memory和storage:

  • stack 棧上元素大小爲32bytes,這和通常的4bytes,8bytes不一樣,主要是針對以太坊運算對象多爲20bytes的地址和32bytes的密碼學變量;棧的大小不超過1024;棧的調用深度不超過1024,主要防止出現內存溢出。
  • memory 雖然運算都在棧上進行,但臨時變量能夠存在memory裏,memory大小不作限制
  • storage 狀態變量都放在storage裏,不像stack和memory上的量隨着EVM實例銷燬消失,storage裏面的數據修改後都會持久化
    如圖,是一個EVM架構的示意圖,這種設計對以太坊應用開發有着深遠的影響,包括設計模式和安全考量。 一個經典的問題是合約的升級
    合約部署後編譯成字節碼存儲在virtual rom中,代碼是不可修改的,這對不少DAPP來講是嚴重的制約。一種思路是,將代碼分佈在不一樣的合約中,合約間調用經過存儲在storage中的地址來進行,這樣實現了實際上的合約升級操做。

執行模型

EVM準確來講是一個準圖靈機,文法上它可以執行任意操做,但爲了防止網絡濫用、以及避免因爲圖靈完整性帶來的安全問題,以太坊中全部操做都進行了經濟學上的限制,也就是gas機制,有三種狀況:

  • 通常操做消耗費用,好比SLOAD,SSTORE等
  • 子消息調用或者合約建立而消耗燃料,這是執行CREATE、CALL、CALLCODE費用中的一部分
  • 內存使用消耗費用,與所須要的32bytes的字數量成正比

下圖展現了EVM執行的內部流程,從EVM code中取指令,全部的操做在Stack上進行,Memory做爲臨時的變量存儲,storage是帳戶狀態。執行受到gas avail限制。

如今結合EVM咱們再來看看以前介紹的交易的執行細節。正如區塊鏈範式定義的,T是以太坊狀態轉移函數,也是以太坊最複雜的部分。全部的交易在執行前,都須要先通過內部的有效性驗證:

  • 交易是RLP格式數據,沒有多餘的後綴字節;
  • 交易的簽名是有效的;
  • 交易的隨機數是有效的;
  • 燃料上限不小於實際交易過程當中用的燃料;
  • 發送者帳戶的餘額至少大於費用v0,須要提早支付;

下圖是消息調用的過程,每一個交易可能會造成很深的調用棧,交易內部由不一樣的合約之間的調用。調用經過CALL指令,參數和返回值經過memory傳遞。

錯誤處理

EVM在合約執行時會發生若干種錯誤:

  • 燃料不足
  • 無效指令
  • 缺乏棧數據
  • 指令JUMP JUMPI的目標地址無效
  • 新棧大小大於1024
  • 棧調用深度超過1024

EVM的錯誤處理有個簡單的原則,叫作revert-state-and-consume-all-gas,即狀態恢復到交易執行前的checkpoint,但消耗的gas不會再退還。虛擬機把錯誤全看做是代碼出錯,不做特定的錯誤處理。

EVM分析工具

關於EVM分析的工具能夠參考Ethereum Virtual Machine (EVM) Awesome List

類EVM的圖靈完備虛擬機(WIP)

完整的EVM規格是很複雜的,但具有必定的彙編基礎和簡化模型的能力,實現一個類EVM的虛擬機是能夠嘗試的挑戰。等有空我再把本身的實現放上來吧。有興趣的同窗能夠本身動手試試。

參考

1.A Next-Generation Smart Contract and Decentralized Application Platform
2.ETHEREUM: A SECURE DECENTRALISED GENERALISED TRANSACTION LEDGER
3.Design Rationale
4.Stack Exchange: Ethereum block architecture
5.Go Ethereum
6.evm-illustrated
7.Diving Into The Ethereum VM

相關文章
相關標籤/搜索