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


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

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

本章將要詳細分析bitcoin交易中的交易腳本-script到底是什么東西。

回顧和概要

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

所以這里的比喻:“鎖”和“鑰匙”就是比特幣交易中的交易腳本Script

其中

“鎖” 對應着 scriptPubKey

“鑰匙”對應着 scriptSig

但是單純的把Script理解為“鎖”和“鑰匙”實在是太淺薄了。只能完成這點事情的並不能體現Script 的強大,也無法對后人創立“智能合約”有所啟發。

所以在我看來,比特幣的Script實際上是:

scriptPubKey 是上一個交易(out)提出的一個 “問題”

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是比特幣系統中異常強大的地方,真是這種運作模式開啟了之后的智能合約的風潮。

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

中本聰把 “交易過程” 開創性的演化為了 “問題-答案” 的過程,重新定義了什么是“交易”

另一方面正如許多人所說的,在整個指令集中沒有出現循環指令,所以這個指令集不是一個圖靈完備的語言,它只能按照腳本的編寫順序執行。至於為什么會這樣設計?有人猜測說是中本聰認為腳本的執行不應該出現循環,否則要是有人寫了死循環惡意破壞會造成很大麻煩,有人認為在這種模式下圖靈完備是沒有必要的,有人認為對於“交易合同”來說這些已經足夠了,有人認為是中本聰沒有考慮好這個問題。不管怎么說,交易的腳本絕對是使比特幣成為強大功能系統中不可缺少的一環。

所以后來的以太坊正是完成了中本聰最后沒有完成的這個東西,成為了擁有“智能合約”能力的區塊鏈,向去中心化理想國邁進新的一步。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM