从比特币脚本引擎到以太坊虚拟机
這個系列是目標受眾是區塊鏈開發者和有其他開發經驗的CS專業學生
面對媒體對區塊鏈相關技術的解讀和吹捧,許多人一時不知所措。投資人、大公司都在FOMO(fear of missing out)的心理驅動下,爭相宣布all in區塊鏈。各路大咖坐而論道,談論區塊鏈技術的社會、政治、經濟乃至哲學上的意義。人類對未知和不懂的東西有種天然的不安全感,作為一名開發人員,我認為克服焦慮(以及帶來的投機心理)最好的方法是盡可能增加對底層的原理及實現的認知。
從技術角度來看,目前不論是比特幣、以太坊,抑或是尚未正式上線的EOS、IPFS,都帶有很強的實驗性質,存在各種局限,而這種局限不可避免影響上層應用的開發。區塊鏈應用也大多涉及金融、信用等重要領域,所以深入理解底層原理是對區塊鏈開發者的一個基本要求,而不僅僅是跟著教程10分鐘部署一段智能合約,特別是早期各種技術未成熟的情況下,生搬硬套稍不留心就會造成極大的損失。
本系列的第一篇文章,主要是以比特幣為代表的加密貨幣架構(區塊鏈1.0),和以以太坊為代表的可編程分布式信用基礎設施(區塊鏈2.0)的核心差異之一——是否支持圖靈完備的語言,來看看區塊鏈技術架構的演進。
比特幣和以太坊的淵源:對幣圈和鏈圈的人來說,Vitalik Buterin(1994年出生)是無可爭議的大神。很多人可能不知道,V神作為早期比特幣社區的活躍成員,一開始提議bitcoin需要開發通用的腳本語言來支持豐富功能的應用開發,但沒有獲得比特幣開發團隊的支持。于是重起爐灶,2013年發起以太坊項目,有了今天的繁榮的加密token、收藏品游戲、DAO。接下來,我們就先看看,V神不滿的比特幣腳本系統到底是什么樣的?
Part I:比特幣腳本引擎
交易
交易是在區塊鏈世界里面有很廣泛的含義,在加密貨幣應用中可以狹義理解為比特幣額度在不同地址間的轉移,即轉賬。轉賬是個歷史悠久的行為,但轉賬技術一直在革新。
理解比特幣轉賬模型尤其重要,因為比特幣腳本引擎建立在該模型之上。
兩種轉賬方式
1、簡化下的傳統中心式轉賬:alice(A賬戶)轉賬到bob(B賬戶)x元,銀行需要原子化的操作balance[A]-=x,balance[B]+=x,當然隱含條件是alice完成了對A賬戶的認證。
2、一種解釋比特幣交易原理的說法:
網絡中每個節點維護獨立的數據庫,記錄著每個地址的余額,如果Alice(addressA的擁有者)想向Bob(addressB的擁有者)轉賬x元,她會在網絡中廣播出去"addressA gives X units to addressB",帶上pubkeyA,用privatekeyA簽名。每個節點收到后,校驗成功后,在各自數據庫中執行原子化操作balance[addressA]-=x,balance[addressB]+=x。(注:實際地址由pubkey生成,這里為簡化省略)。
上面1在現實中占據主流,有成熟的擴展方案,但中心化不可避免帶來成本、平臺作惡等問題;
2的描述來自于b-money, an anonymous, distributed electronic cash system(這篇文章非常之重要,深刻影響了中本聰對比特幣的設計),但在當時無法實踐,因為重度依賴于一個同步、不受干擾的網絡環境,否則保持一致性難度很大。而且這種分布式數據庫提交問題(Byzantine Problem),現有的一致性算法paxos、raft(non-byzantine)包括pbft(byzantine)擴展性都無法支撐比特幣上萬的節點數。
比特幣交易模型的設計
關于比特幣交易模型最早來自于中本聰的
Bitcoin: A Peer-to-Peer Electronic Cash System。中本聰實際提出了兩種chain,大家現在一直說的區塊鏈(chain of blocks)是顯式的數據組織方式,另一個隱式的是交易鏈(chain of transactions)才是比特幣價值流動的鏈條。
如圖,最早的交易描述模型:
如果Alice(addressA的擁有者)想向Bob(addressB的擁有者)轉賬x元,她同樣需要把這個交易簽名后在網絡中廣播出去。不同的是,addressA的余額,并非存儲在各個節點的數據庫里,而是在別人給addressA轉賬的未花費交易輸出中,即UTXO(unspent transaction output)。我們查詢addressA的余額,實際得到的是所有收款地址是adressA的UTXOs的額度的求和。廣播內容類似"addressA(combining UTXO1...UTXO3) gives X units to addressB",帶上pubkeyA,用privatekeyA簽名。
交易在網絡中被確認后,Bob就會多了一個可用UTXO。如果他想花費這筆錢,需要證明自己擁有addressB對應的privatekeyB,那么Bob也用私鑰簽名。這樣交易就成了一串簽名的鏈條。
顯然這里有三個問題:
1.如果任意的交易的input都需要某個之前交易的輸出,那么最初比特幣從哪里來?
所以在比特交易中,有種叫做coinbase的交易,就是我們所周知的挖礦獎勵。比特幣的產生就通過挖礦算法生成,這里的input來自于系統獎勵。實際上還會校驗coinbase是否是"mature"的,即該塊是否經過足夠的確認。在比特幣中如果最終沒有歸入最長鏈,那么會作為orphan塊被棄,獎勵也作廢。
2.判斷一個交易輸出是否是UTXO需要回溯整個區塊鏈嗎?
不需要,因為交易按照merkel tree的結構組織,決定了從整個區塊鏈數據庫中查詢一個交易會非常低效。UTXOs專門存儲在leveldb的數據庫chainstate中,并且緩存在內存中。每當一個新的block生成,就會更新UTXOs集;當某個節點發生鏈重建現象,會回滾該過程。這里需要注意的是,UTXOs集不是待確認交易池(TxMemPool),而是所有待確認交易的input來源;UTXOs理論上也可以通過--reindex從整個區塊鏈中重建。
3.Alice的賬戶余額來自于四個UTXO,分別是0.05,0.2,0.2,0.3,現在需要轉帳0.6給Bob,怎么辦?
理論上Alice可以三次轉,但實際上很不明智,既要多付手續費,體驗也差,所以交易Input可以包括多個UTXO,如何選擇UTXO組合有專門的分析;多個Input之和不一定恰好等于轉賬金額Output1,還需要一個找零錢refund(Output2),當然還會有手續費fee(Output3),所以交易會包括多個Output。當然對于用戶來說,只需要設定轉賬地址、額度、手續費,組合UTXO、找零等是透明的。
注:以太坊摒棄了UTXOs模型,采用類似于bmoney的賬戶范式。具體原因等到介紹以太坊虛擬機設計中再分析。
做了這么多鋪墊,終于可以進入比特幣的腳本設計了。
Script opcodes
比特幣交易由一套腳本引擎(Script)處理。這里引用bitcoin-core源碼interpreter.cpp里的一段注釋:
/*** Script is a stack machine (like Forth) that evaluates a predicate* returning a bool indicating valid or not. There are no loops.*/Script是一種類Forth、基于棧式模型、無狀態的、非圖靈完備的語言。
opcodes分為常量、流程控制、棧操作、算術運算、位運算、密碼學運算、保留字等若干類,還包括3個內部使用的偽指令。下面舉幾個在后面的腳本中會出現的指令,全部的指令可參考官方文檔和源碼。
- OP_0 ... OP_16: 將字面量值壓入棧中。
- OP_DUP: 將棧頂元素復制一個,壓入棧中。
- OP_ADD: 彈出棧頂元素和次棧頂元素,相加后壓入棧中。
- OP_EQUAL: 彈出棧頂元素和次棧頂元素,比較是否相等,相等則將1壓入棧中,否則壓入0。
- OP_SHA256: 彈出棧頂元素,進行sha-256加密運算,結果壓入棧中。
- OP_HASH160: 彈出棧頂元素,先進行sha-256加密運算,再進行ripemd160摘要運算,結果壓入棧中。值得注意的是,這是基于公鑰生成address的過程的一部分。
- OP_CHECKSIG: 彈出棧頂元素和次棧頂元素,這里分別是sig和pubkey;內部有個VerifySignature函數,驗證簽名和公鑰是否匹配。
- OP_CHECKMULSIG:棧內壓入m個簽名,n個公鑰,逐一校驗m個簽名是否對應n個公鑰的某個子集。
Pay-to-PubkeyHash(P2PKH)
上面Alice轉載給Bob的例子,就是一個典型的P2PKH。中本聰在論文中只是給出了交易模型,下面看看更具體的實現。
如上圖,Alice在轉賬給Bob前,Bob需要提供一個自己的收款地址,但實際P2PKH中使用的是Public Key Hash。這里簡單補充下key生成過程,如下圖,私鑰單向生成公鑰,公鑰通過OP_HASH160指令生成160位的PKH(公鑰哈希),PKH可以轉成更可讀用戶使用的地址,但編碼、校驗過程等是雙向的。所以提供地址等價于提供PKH。
下圖,Alice轉賬給Bob的錢鎖定在TX1 Output中,通過一個Pubkey Script。Bob如果嘗試花掉這筆錢,他需要解鎖這個PubkeyScript,通過證明自己是TX1 Output中Public Key Hash的私鑰擁有者,提供一個Signature Script。
下面就是這兩個腳本。
鎖定腳本: scriptPubKey: OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG 解鎖腳本: scriptSig: <sig> <pubKey>上面包括在<>之間的為要壓入棧中的數,push指令缺省。實際執行時,會將scriptSig和scriptPubkey連接起來,按照從左往右順序運行腳本。
驗證過程: <sig> <pubKey> OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG棧上的情況如下面兩圖所示。有匯編基礎的同學,對棧式計算機模型的運作原理會很熟悉。
generation 獎勵礦工
獎勵礦工可以看作一種簡化的P2PKH,區別在于交易的輸入來自coinbase而不是某個UTXO。
Pay-to-Script-Hash(P2SH)
P2PKH設計比較簡單,接受者Bob直接提供收款地址。實際的價值流通過程中,會涉及很多條件。為了滿足更復雜的功能,BIP12中提出加入OP_EVAL指令(在程序語言設計中,eval意味著語言具備了元編程能力),并在之后由BIP16提出了更完善的交易標準P2SH。
收款方Bob需要先設計一個RedeemScript——提款腳本,再生成該腳本的Hash,提供給Alice。
Bob如果想花費該筆UTXO,則需要提供簽名和RedeemScript,校驗成功后執行RedeemScript的內容,滿足條件后則成功解鎖。
下面的redeemScript結合具體場景設計。后面結合智能合約的應用給出相應的例子。
鎖定腳本: Pubkey script: OP_HASH160 <Hash160(redeemScript)> OP_EQUAL 解鎖腳本: Signature script: <sig> [sig] [sig...] <redeemScript>在P2SH交易中,由于Bob提供的是一段腳本的Hash,那么Alice實際上不知道這筆交易的細節,交易的具體內容需要Bob來設計。這就是所謂的"moving the responsibility for supplying the conditions to redeem a transaction from the sender of the funds to the redeemer. They allow the sender to fund an arbitrary transaction, no matter how complicated, using a 20-byte hash"。這在設計上也不是說沒有爭議的,但是在比特幣的技術框架下,是一種以最小的改動支持更多的特性的路徑。
比特幣的智能合約
雖然一提起智能合約,人們更多會想起來以太坊。但正如前面提到的,技術發展是一脈相承。早在1997年Nick Szabó在開創性的論文 Formalizing and Securing Relationships on Public Networks中提出了智能合約的概念。比特幣的腳本系統支持有限的智能合約的開發,主要通過P2SH交易實現的。
MultiSig 多重簽名
BIP11提出了M-of-N多重簽名交易。一個交易的解鎖條件是預定指定的N個pubkey中的M個簽名認證(M<=N)。P2PKH可以看作1-of-1的簽名。多重簽名在增加安全、托管交易等場景下十分有用。所以比特幣中專門實現了OP_CHECKMULTISIG的指令。可以通過下面的腳本設計來實現。
鎖定腳本: Pubkey script: <m> <A pubkey> [B pubkey] [C pubkey...] <n> OP_CHECKMULTISIG 解鎖腳本: Signature script: OP_0 <A sig> [B sig] [C sig...]如果使用P2SH交易,也可以設計成如下腳本。
鎖定腳本: Pubkey script: OP_HASH160 <Hash160(redeemScript)> OP_EQUAL 解鎖腳本: Signature script: OP_0 <A sig> <C sig> <redeemScript> 其中: Redeem script: <OP_2> <A pubkey> <B pubkey> <C pubkey> <OP_3> OP_CHECKMULTISIGGavin Andresen寫了一個2-of-3的多重簽名交易的使用例子,十分詳細,我就不搬運了。
more...
Part II:以太坊虛擬機
區塊鏈范式
Gavin Wood在黃皮書中將區塊鏈系統抽象為基于交易的狀態機:
公式(1)中S是系統內部的狀態集合,f是交易狀態轉移函數,T是交易信息,初始狀態即Gensis狀態;
公式(2)中F是區塊層面狀態轉移函數,B是區塊信息;
公式(3)定義B是一系列交易的區塊,每個區塊都包括多個transaction;
公式(4)G是區塊定稿函數,在以太坊中包括uncle塊校驗、獎勵礦工、POW校驗等。
這個數學模型不僅是以太坊的基礎,也是目前大多數基于共識的去中心化交易系統的基礎。
以太坊相對比特幣的提升,本質體現在這個范式中的f和S。它的核心理念——具備圖靈完備和不受限制的內部交易存儲空間的區塊鏈。分別對應:
- 功能強大的函數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會加密編碼成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個元素的非葉節點,形如\
- leaf結點 2個元素的葉結點,形如\,encodedPath是地址加密編碼后的長整型數字串的一部分
- extension結點 2個元素的非葉結點,形如\,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準確來說是一個準圖靈機,文法上它能夠執行任意操作,但為了防止網絡濫用、以及避免由于圖靈完整性帶來的安全問題,以太坊中所有操作都進行了經濟學上的限制,也就是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
https://zhuanlan.zhihu.com/p/34456971
總結
以上是生活随笔為你收集整理的从比特币脚本引擎到以太坊虚拟机的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【译】zkSNARKs in a nut
- 下一篇: RAFT 寻找一种易于理解的一致性算法(