這個系列是目標受衆是區塊鏈開發者和有其餘開發經驗的CS專業學生php
面對媒體對區塊鏈相關技術的解讀和吹捧,許多人一時不知所措。投資人、大公司都在FOMO(fear of missing out)的心理驅動下,爭相宣佈all in區塊鏈。各路大咖坐而論道,談論區塊鏈技術的社會、政治、經濟乃至哲學上的意義。人類對未知和不懂的東西有種自然的不安全感,做爲一名開發人員,我認爲克服焦慮(以及帶來的投機心理)最好的方法是儘量增長對底層的原理及實現的認知。html
從技術角度來看,目前不管是比特幣、以太坊,抑或是還沒有正式上線的EOS、IPFS,都帶有很強的實驗性質,存在各類侷限,而這種侷限不可避免影響上層應用的開發。區塊鏈應用也大多涉及金融、信用等重要領域,因此深刻理解底層原理是對區塊鏈開發者的一個基本要求,而不只僅是跟着教程10分鐘部署一段智能合約,特別是早期各類技術未成熟的狀況下,生搬硬套稍不留心就會形成極大的損失。git
本系列的第一篇文章,主要是以比特幣爲表明的加密貨幣架構(區塊鏈1.0),和以以太坊爲表明的可編程分佈式信用基礎設施(區塊鏈2.0)的核心差別之一——是否支持圖靈完備的語言,來看看區塊鏈技術架構的演進。github
比特幣和以太坊的淵源:對幣圈和鏈圈的人來講,Vitalik Buterin(1994年出生)是無可爭議的大神。不少人可能不知道,V神做爲早期比特幣社區的活躍成員,一開始提議bitcoin須要開發通用的腳本語言來支持豐富功能的應用開發,但沒有得到比特幣開發團隊的支持。因而重起爐竈,2013年發起以太坊項目,有了今天的繁榮的加密token、收藏品遊戲、DAO。接下來,咱們就先看看,V神不滿的比特幣腳本系統究竟是什麼樣的?算法
交易是在區塊鏈世界裏面有很普遍的含義,在加密貨幣應用中能夠狹義理解爲比特幣額度在不一樣地址間的轉移,即轉帳。轉帳是個歷史悠久的行爲,但轉帳技術一直在革新。 理解比特幣轉帳模型尤爲重要,由於比特幣腳本引擎創建在該模型之上。數據庫
一、簡化下的傳統中心式轉帳:alice(A帳戶)轉帳到bob(B帳戶)x元,銀行須要原子化的操做balance[A]-=x,balance[B]+=x
,固然隱含條件是alice完成了對A帳戶的認證。編程
二、一種解釋比特幣交易原理的說法: 網絡中每一個節點維護獨立的數據庫,記錄着每一個地址的餘額,若是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,怎麼辦?
注:以太坊摒棄了UTXOs模型,採用相似於bmoney的帳戶範式。具體緣由等到介紹以太坊虛擬機設計中再分析。
作了這麼多鋪墊,終於能夠進入比特幣的腳本設計了。
比特幣交易由一套腳本引擎(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個內部使用的僞指令。下面舉幾個在後面的腳本中會出現的指令,所有的指令可參考官方文檔和源碼。
上面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
複製代碼
棧上的狀況以下面兩圖所示。有彙編基礎的同窗,對棧式計算機模型的運做原理會很熟悉。
獎勵礦工能夠看做一種簡化的P2PKH,區別在於交易的輸入來自coinbase而不是某個UTXO。
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交易實現的。
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_CHECKMULTISIG
複製代碼
Gavin Andresen寫了一個2-of-3的多重簽名交易的使用例子,十分詳細,我就不搬運了。
Gavin Wood在黃皮書中將區塊鏈系統抽象爲基於交易的狀態機:
公式(1)中S是系統內部的狀態集合,f是交易狀態轉移函數,T是交易信息,初始狀態即Gensis狀態;
公式(2)中F是區塊層面狀態轉移函數,B是區塊信息;
公式(3)定義B是一系列交易的區塊,每一個區塊都包括多個transaction;
公式(4)G是區塊定稿函數,在以太坊中包括uncle塊校驗、獎勵礦工、POW校驗等。
這個數學模型不只是以太坊的基礎,也是目前大多數基於共識的去中心化交易系統的基礎。
以太坊相對比特幣的提高,本質體如今這個範式中的f和S。它的核心理念——具有圖靈完備和不受限制的內部交易存儲空間的區塊鏈。分別對應:
在數據存儲方面,比特幣經過UTXO模型計算地址餘額,不鼓勵用戶存入其餘數據;經過P2SH腳本機制,理論上能夠設計各類智能合約,但受限於腳本語言的表達能力,難以支持複雜的合約開發。這種設計對於加密貨幣來講是合理的。
以太坊爲了支持記錄任意的信息、執行任意函數,須要從新設計數據結構。
以太坊中重度使用Merkle Patricia Trie組織、存儲數據,下面咱們會看到,這個新的數據結構是經過對哈希樹和前綴樹的組合創新來達到目的。
約定:下面使用MPT來代替Merkle Patricia Trie。
又稱hash tree:樹的每一個葉子結點是某個數據塊的哈希值,而每一個非葉子結點是孩子結點的哈希值。如圖所示,這棵樹不存儲Data blocks自己。在P2P網絡環境中,惡意網絡節點若是修改了這顆樹上的數據,將沒法經過校驗(Merkle Proof),從而保證了數據的完整、有效性。這依賴於單向哈希加密的性質。這種性質讓它普遍應用在分佈式系統的數據校驗中,好比IPFS、Git等。
中本聰也巧妙利用該性質,設計了比特幣的SPV(簡化支付驗證)功能。以下圖,用戶不須要運行完整的結點,只須要下載最長鏈的區塊頭數據,而後獲取待驗證交易對應區塊的merkle樹作校驗。又叫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表的好處在於不會出現衝突;但若是不作優化,查詢步驟太長。
爲了提升效率,以太坊對樹上結點數據類型進行了專門的設計。包括如下四類結點
使用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相關分支上的數據,而其餘分支不須要複製!固然以太坊主網上新區塊包含的交易大概爲幾十到幾百不等,那麼涉及的修改也會更多。關於這種結構性能上的討論參考這篇 文章。 查詢最新的帳戶狀態的入口應該是最新被確認的區塊的狀態樹。對以太坊的帳戶模型須要專門作個介紹。
比特幣使用UTXO模型計算餘額,沒法知足記錄任意狀態的需求。以太坊設計了Account模型,它會存儲包括:
[nonce, balance, storageRoot, codeHash]
其中nonce是交易計數器,balance是餘額信息,storageRoot對應另一個MPT,經過它可以在數據庫中檢索到合約的變量信息,codeHash是代碼hash值,建立後不可更改。
在UTXO模型下,交易本質上是(經過簽名的數據)對input的解鎖和對output的鎖定。在Account模型下,交易分爲兩種:
兩種類型的交易都包括如下字段:
[nonce,gasPrice,gasLimit,to,value,[v,r,s]]
合約建立還須要:
消息調用還須要:
交易既能夠由外部帳戶發起,也能夠由合約發起。好比第5228886區塊包含170個交易和7個內部合約交易。
以太坊的區塊了加入更多的數據項,相對比特幣要複雜不少,但其實本質上區別不大。好比加入了叔鏈哈希,優化激勵措施,這是爲了支持挖礦協議;區塊自己還會有大量的有效性驗證、序列化。這些內容不在本文主題範圍,不深刻討論。
參見上面這張圖的右半部分,一窺以太坊區塊如何組織數據,能看到MPT樹的大量使用;左半部分涉及到EVM,將會是接下來的重點。以太坊虛擬機(Ethereum Virtual Machine)是執行以太坊的狀態轉移函數的運行環境。 有個簡單的問題,以太坊是否能夠不專門開發一款底層VM,而是複用Java、Lisp、Lua等呢?理論上是徹底能夠的,Corda項目就徹底基於JVM平臺開發。可是更多的區塊鏈項目會選擇專門開發底層設施,包括比特幣的腳本引擎。以太坊官方給出的解釋:
EVM也是基於棧式計算機模型,但除了stack外還涉及memory和storage:
EVM準確來講是一個準圖靈機,文法上它可以執行任意操做,但爲了防止網絡濫用、以及避免因爲圖靈完整性帶來的安全問題,以太坊中全部操做都進行了經濟學上的限制,也就是gas機制,有三種狀況:
下圖展現了EVM執行的內部流程,從EVM code中取指令,全部的操做在Stack上進行,Memory做爲臨時的變量存儲,storage是帳戶狀態。執行受到gas avail限制。
如今結合EVM咱們再來看看以前介紹的交易的執行細節。正如區塊鏈範式定義的,T是以太坊狀態轉移函數,也是以太坊最複雜的部分。全部的交易在執行前,都須要先通過內部的有效性驗證:
下圖是消息調用的過程,每一個交易可能會造成很深的調用棧,交易內部由不一樣的合約之間的調用。調用經過CALL指令,參數和返回值經過memory傳遞。
EVM在合約執行時會發生若干種錯誤:
EVM的錯誤處理有個簡單的原則,叫作revert-state-and-consume-all-gas,即狀態恢復到交易執行前的checkpoint,但消耗的gas不會再退還。虛擬機把錯誤全看做是代碼出錯,不做特定的錯誤處理。
關於EVM分析的工具能夠參考Ethereum Virtual Machine (EVM) Awesome List
完整的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