bitcoin 源碼解析 - 交易 Transaction(三) - Script

bitcoin 源碼解析 - 交易 Transaction(三) - Script

以前的章節已經比較粗略的解釋了在Transaction體系當中的總體運做原理。接下來的章節會對這個體系進行分解,比較詳細描述細節的構成。編程

本章將要詳細分析bitcoin交易中的交易腳本-script究竟是什麼東西。安全

回顧和概要

在前面的文章中提到,在bitcoin的體系中,一個交易是被髮布到比特幣的總體系統中的,而可以操控以前交易的的TxOut(被鎖住的coin),是須要可以操控這個TxOut的人提供"鑰匙"來控制。就像前文描述的,coin在整個系統中是像流水同樣的在體系中進行流通,而coin在其中在分叉點的時候會有一個像 「鎖」 的東西把coin鎖在這個節點上。而根據這個鎖產生了一個新的交易,繼續流通被這個鎖所鎖住的coin,是須要提供一個"鑰匙"的。數據結構

因此這裏的比喻:「鎖」和「鑰匙」就是比特幣交易中的交易腳本Scriptide

其中函數

「鎖」 對應着 scriptPubKey區塊鏈

「鑰匙」對應着 scriptSigui

可是單純的把Script理解爲「鎖」和「鑰匙」實在是太淺薄了。只能完成這點事情的並不能體現Script 的強大,也沒法對後人創立「智能合約」有所啓發。this

因此在我看來,比特幣的Script其實是:加密

scriptPubKey 是上一個交易(out)提出的一個 「問題」spa

scriptSig 是我想使用上一個交易中錢,那麼我就對你提出的這個問題提供個人「答案」

由於公私鑰的關係,因此若是scriptPubKey 提出的問題是公鑰相關的問題,那麼很明顯,只有持有私鑰的人才能回答這個問題,因此就簡化爲剛纔的所說的「鎖」和「鑰匙」的關係。

而另外一方面,如何確認提供的「答案」就是能回答「問題」的呢?這就說明Script是須要被執行驗證的,並且這個驗證的過程只須要txin提供的scriptSig 和驗證者本身從本身的記錄中找到的txout的scriptPubKey ,而這個驗證者就是廣大的礦工們。

整個系統精妙的地方就在於,scriptPubKey是驗證者(礦工)各自獨立持有的東西,其安全性由本身所保證的,而想要完成交易的人只須要提供scriptSig給廣大驗證者就行,不須要一些多餘的上下文(能夠理解爲上下文由驗證者本身持有,雖然你們都互不信任,可是對於最廣大的人來講,這個上下文都是相同的)。

另外一個方面不太被大多數人所注意到的是:

實際上剛纔的模型簡化爲了「問題」和「答案」,可是這個「問題」可不是很容易提供的。

這個「問題」應該知足2個方面的要求:

  1. 問題的答案必須是十分明確的,惟一的,不能是個模糊的要求(這點在代碼中就是「代碼就是法律」的體現吧(笑),或許這就是智能合約沒法完成真正人們所向往的替代全部合同執行的緣由,由於合同雖然簽定了,可是其中的內容其實不少是有討價劃價,鑽空子的空間的)
  2. 答案必須容易的被驗證而不須要其餘上下文環境。(這點就是這個問題提出的困難的地方,也就是這個問題要麼正向很難,逆向很容易,要麼驗證須要提供其餘的附加的上下文環境。)

而公私密鑰的模式實際上是完美的符合了這2方面的要求的。

那麼有沒有其餘的問題呢?那是固然有的,好比我提出了一個數學問題,這個問題的解是惟一的而且能夠很容易的驗證個人回答對不對

那麼我就能夠建立一筆交易,而這筆交易的txin就提供這個問題的答案,只要個人這個tx優先被礦工打包進入區塊中,併成爲最長鏈,那麼這個問題下的錢就歸我了。

這個場景就是符合正向很難,逆向容易的場景。

接下來就解釋 比特幣系統中的 CScript 究竟是怎麼運做的。

CScript

在比特幣源碼當中,對於CScript 單獨列出了 script.c/script.h 來實現這塊體系(對比把tx,block等全部實現所有放在main.c/.h來講),可見得中本聰在一開始設計這套體系的時候就把這塊的內容看的至關的重要。事實上這套體系也確實很複雜,可是也是得益於這套體系,才能取得如今的地位,若是沒有這個設計,比特幣的實用性會被大幅度減弱。

class CScript : public vector<unsigned char> { // 把各類類型的數據序列化到 vector 中 CScript& operator<<(char b) { return (push_int64(b)); } CScript& operator<<(short b) { return (push_int64(b)); } CScript& operator<<(int b) { return (push_int64(b)); } CScript& operator<<(long b) { return (push_int64(b)); } CScript& operator<<(int64 b) { return (push_int64(b)); } CScript& operator<<(unsigned char b) { return (push_uint64(b)); } CScript& operator<<(unsigned int b) { return (push_uint64(b)); } CScript& operator<<(unsigned short b) { return (push_uint64(b)); } CScript& operator<<(unsigned long b) { return (push_uint64(b)); } CScript& operator<<(uint64 b) { return (push_uint64(b)); } CScript& operator<<(opcodetype opcode) CScript& operator<<(const uint160& b) CScript& operator<<(const uint256& b) CScript& operator<<(const CBigNum& b) CScript& operator<<(const vector<unsigned char>& b) { // } bool GetOp(const_iterator& pc, opcodetype& opcodeRet, vector<unsigned char>& vchRet) const { // .... } void FindAndDelete(const CScript& b) { // ... } }; 

從這個類中能夠看到,其實CScript其實就是vector<char> ,沒什麼特別的,重要的不是它是什麼,重要的是它的內容是什麼,會起什麼做用。

能夠看出其實這類的做用,是像提供了一個容器,這個容器能夠存儲其餘類型的數據(基本類型,uint64,uint256,uint160...),換句話說,這是提供了一個容器來接受各類數據類型的序列化。可是除了基本屬性以外,對於Script,定義了一個特別的東西,就是opcodetype,也就是操做符。而類中的GetOp()方法顯然就是從vector<char>這樣的「流」式數據中把操做符從其中識別出來的方法。

因此從這裏能夠一窺Script的真實做用,它是由一系列操做符和數據組合而成的,由操做符持有邏輯(動做),由數據持有"狀態"的結構體,由於它最終是被傳輸和存儲的,因此使用vector<char>做爲容器,將操做符和數據「序列化」到了這個容器中。

Script的操做符

對於CScript中持有的關於「操做符」相關的是opcodetype。

這個操做符實際上就是一個枚舉類型,若是把Script看成語言相關的概念,那麼實際上opcode就是對應相似彙編中的指令。因此指令的行爲是由人制定的,那麼指令的表示實際上就是一個代號。下面這個枚舉類型就是源碼中的opcodetype,作了一些刪減。

enum opcodetype { // push value 這部分的指令至關於表示這個指令後面的數據是怎麼樣的組織性質, OP_0=0, OP_FALSE=OP_0, OP_PUSHDATA1=76, // 0x4c 爲何是這個值其實我不太清楚,不過能夠確定的是,這個值是76那麼 OP_1 就是81 也就是0x51 OP_PUSHDATA2, OP_PUSHDATA4, OP_1NEGATE, OP_RESERVED, // 80 OP_1, // 81 也就是 0x51,可是爲何要求這個值是81不太清楚,可是感受很特別 OP_TRUE=OP_1, // 81 OP_2, OP_3, //... 一直到Op_16 // control // 如下是控制流指令,好比 if 這類的指令,就是做爲控制流存在的了 OP_NOP, OP_VER, OP_IF, OP_NOTIF, OP_VERIF, OP_VERNOTIF, OP_ELSE, OP_ENDIF, OP_VERIFY, OP_RETURN, // stack ops // 如下是對於棧的操做,這裏能夠理解爲,棧用來保存了數據當前所處於的狀態, // 這些指令至關於控制棧當前的狀態,能夠比做在編程中對當前操做對象的把控?。下文會對總體流程進行講解 OP_TOALTSTACK, OP_FROMALTSTACK, OP_2DROP, OP_2DUP, OP_3DUP, OP_2OVER, // ... // splice ops // 這些也是對數據的一些處理操做,可是這些是對棧中數據自己的內容進行操做 OP_CAT, OP_SUBSTR, OP_LEFT, OP_RIGHT, OP_SIZE, // bit logic // 這個和上者同樣,不過是位操做 OP_INVERT, OP_AND, OP_OR, OP_XOR, // ... // numeric // 這個和上者同樣,不過是數字邏輯操做 OP_1ADD, OP_1SUB, OP_2MUL, OP_2DIV, OP_NEGATE, OP_ABS, OP_NOT, OP_0NOTEQUAL, OP_ADD, OP_SUB, OP_MUL, OP_DIV, //... // crypto // 這個和上者同樣,可是操做的是和hash加密等相關的內容,能夠理解爲對bitcoin系統的特有的DSL OP_RIPEMD160, OP_SHA1, OP_SHA256, OP_HASH160, OP_HASH256, OP_CODESEPARATOR, OP_CHECKSIG, // 這個是用的最多的,就是來斷定簽名是否符合的指令 OP_CHECKSIGVERIFY, OP_CHECKMULTISIG, OP_CHECKMULTISIGVERIFY, // multi-byte opcodes OP_SINGLEBYTE_END = 0xF0, OP_DOUBLEBYTE_BEGIN = 0xF000, // template matching params // 下面這兩個表明bitcoin特別的數據結構,公鑰(地址) OP_PUBKEY, OP_PUBKEYHASH, OP_INVALIDOPCODE = 0xFFFF, }; 

能夠看到這些指令其實都是很清晰的,只不過這些指令運行的方式有點接近彙編指令的運做方式,(c語言的棧)接下來會舉例如何運行Script。

Script 運行方式

這裏有一篇比較好的文章介紹了它的運行:

理解比特幣腳本

這裏我詳細介紹一下:

首先明確總體腳本的運行時基於棧運行的,而剛纔上一章介紹的指令就是操做棧中元素的方式。

  • OP_DUP:複製棧頂元素

這裏借用一下剛纔那個連接裏面的圖。

 

在源碼中呢,執行Script的函數是EvalScript()

而總體的運行流程就是 (script.cpp)

  1. 傳入腳本Script(這個腳本是把 scriptPubKey 和 scriptSig) 拼接在一塊兒的一個總的Script
bool VerifySignature(const CTransaction& txFrom, const CTransaction& txTo, unsigned int nIn, int nHashType) { // ... // 注意這裏把 txin 的 scriptSig 和 txout 的 scriptPubKey 拼接在一塊兒 return EvalScript(txin.scriptSig + CScript(OP_CODESEPARATOR) + txout.scriptPubKey, txTo, nIn, nHashType); } 

2. 建立一個 stack(棧),這個stack就是前文一直提到的棧。可是這個棧所穿了就是一個vector,就是數據結構裏的那個東西

bool EvalScript(const CScript& script, const CTransaction& txTo, unsigned int nIn, int nHashType, vector<vector<unsigned char> >* pvStackRet) { CAutoBN_CTX pctx; CScript::const_iterator pc = script.begin(); CScript::const_iterator pend = script.end(); CScript::const_iterator pbegincodehash = script.begin(); vector<bool> vfExec; // 這個是暫時記錄 棧中執行if判斷結果的地方 vector<valtype> stack; // 棧就是這個,而valtype是一個定義 typedef vector<unsigned char> valtype; // ... } 

3. 整個的執行過程就是,首先執行了 scriptSig,那麼這個scriptSig就會在棧中留下一系列的狀態和數據,而這些狀態和數據是爲了配對scriptSig中的狀態和數據(也就是爲了配對問題的答案)。讀取(並執行,雖然對於scriptSig應該大部分都是提供數據,不會帶有執行過程)scriptSig後,那麼就開始讀取scriptPubKey,沒讀取scriptPubKey中的一個操做符,就執行一次。能夠把其看成解釋形語言的形式,讀取一條執行一條。

例如以源碼中的最基礎交易模板爲例:

bool Solver(const CScript& scriptPubKey, vector<pair<opcodetype, valtype> >& vSolutionRet) // script.cpp { // Templates static vector<CScript> vTemplates; if (vTemplates.empty()) { // Standard tx, sender provides pubkey, receiver adds signature vTemplates.push_back(CScript() << OP_PUBKEY << OP_CHECKSIG); // Short account number tx, sender provides hash of pubkey, receiver provides signature and pubkey vTemplates.push_back(CScript() << OP_DUP << OP_HASH160 << OP_PUBKEYHASH << OP_EQUALVERIFY << OP_CHECKSIG); } // .... } // 咱們以bitcoin提供的 vTemplates 中的第二個爲例: // 如下是出現相關代碼的地方: void CSendDialog::OnButtonSend(wxCommandEvent& event){ //ui.cpp //... if (fBitcoinAddress) { // Send to bitcoin address CScript scriptPubKey; scriptPubKey << OP_DUP << OP_HASH160 << hash160 << OP_EQUALVERIFY << OP_CHECKSIG; // 這裏對應的就是第二個模板 hash160是收款方地址 //... } // 生成對於這個腳本配對的 scriptSig 位於 Solver 內 bool Solver(const CScript& scriptPubKey, uint256 hash, int nHashType, CScript& scriptSigRet) { else if (item.first == OP_PUBKEYHASH) // 這裏對應的是第二個模板,注意 OP_PUBKEYHASH { // Sign and give pubkey // ... if (hash != 0) { vector<unsigned char> vchSig; if (!CKey::Sign(mapKeys[vchPubKey], hash, vchSig)) return false; vchSig.push_back((unsigned char)nHashType); scriptSigRet << vchSig << vchPubKey; // 除了 sig 外 還要把 pubkey 也添加進入scriptsig中 // 這裏就是生成答案的地方 } // ... 

因此對於整個執行過程就是這樣的:

首先對於

vector<valtype> stack;

來講,從 scriptSig 中壓棧 vchSig 和 vchPubkey。那麼棧中就擁有了 vchSig,vchPubkey。那麼接下來的執行過程以下:

總體的過程就是這樣的。就至關於一我的提供的答案,而後驗證者拿出這份答案對應的問題,而後看一眼問題,檢查一下問題的結果,而後在看問題,再執行,依次執行下去的過程。

因此若是問題不是公私鑰配對解密,而是其餘的問題,好比建立一個

pubkey<< 100 << 200 << OP_1ADD << OP_EQUALVERIFY 

的問題,那麼對應這個問題的答案就顯然是

sig << 300 

就是這樣的過程。

其餘

以上詳細的介紹了整個腳本的運做流程。如今指明一些細節:

  1. 數據類的序列化(<<操做符)進入腳本都會被 OP_PUSHDATA1,OP_PUSHDATA2,OP_PUSHDATA4 操做符所標明,指明這是一個數據
  2. 在序列化數據的時候,注意數字 1-16 和 -1 會被認爲是操做符OP_1-OP_16和OP_1NEGATE。我目前尚不清楚爲什麼須要這樣設計,或許是保留字段?
class CScript : public vector<unsigned char> { protected: CScript& push_int64(int64 n) { if (n == -1 || (n >= 1 && n <= 16))//注意這裏! { push_back(n + (OP_1 - 1)); // 對1-16產生了OP_1的偏移(OP_1=81) } else { CBigNum bn(n); *this << bn.getvch(); } return (*this); } string ToString() const { //... while (GetOp(it, opcode, vch)) { if (!str.empty()) str += " "; if (opcode <= OP_PUSHDATA4) str += ValueString(vch); else str += GetOpName(opcode); // 1-16, -1 最後會進入這個分支 } return str; } } 

總結

Script是比特幣系統中異常強大的地方,真是這種運做模式開啓了以後的智能合約的風潮。

其把簡單的認證一個交易的歸屬問題的流程從簡單的認證擴展到腳本的運行,粗略來看是把一個簡單的東西變得複雜了,其實是極大的擴展了「交易」的含義。使得交易能夠含有「邏輯」,而不只僅是「狀態」

中本聰把 「交易過程」 開創性的演化爲了 「問題-答案」 的過程,從新定義了什麼是「交易」

另外一方面正如許多人所說的,在整個指令集中沒有出現循環指令,因此這個指令集不是一個圖靈完備的語言,它只能按照腳本的編寫順序執行。至於爲何會這樣設計?有人猜想說是中本聰認爲腳本的執行不該該出現循環,不然要是有人寫了死循環惡意破壞會形成很大麻煩,有人認爲在這種模式下圖靈完備是沒有必要的,有人認爲對於「交易合同」來講這些已經足夠了,有人認爲是中本聰沒有考慮好這個問題。無論怎麼說,交易的腳本絕對是使比特幣成爲強大功能系統中不可缺乏的一環。

因此後來的以太坊正是完成了中本聰最後沒有完成的這個東西,成爲了擁有「智能合約」能力的區塊鏈,向去中心化理想國邁進新的一步。

相關文章
相關標籤/搜索