交易(transaction)是比特幣的核心所在,而區塊鏈的惟一目的,也正是爲了可以安全可靠地存儲交易。在區塊鏈中,交易一旦被建立,就沒有任何人可以再去修改或是刪除它。在今天的文章中,咱們會實現交易的通用機制。web
若是之前開發過 web 應用,在支付的實現環節,你可能會在數據庫中建立這樣兩張表:算法
account(帳戶)會存儲用戶信息,裏面包括了我的信息和餘額。transaction(交易)會存儲資金轉移信息,也就是資金從一個帳戶轉移到另外一個帳戶這樣的內容。在比特幣中,支付是另一種徹底不一樣的方式:數據庫
鑑於區塊鏈是一個公開開放的數據庫,因此咱們並不想要存儲錢包全部者的敏感信息(因此具備必定的匿名性)。資金不是經過帳戶來收集,交易也不是從一個地址將錢轉移到另外一個地址,也沒有一個字段或者屬性來保存帳戶餘額。交易就是區塊鏈要表達的全部內容。那麼,交易裏面到底有什麼內容呢?安全
一筆交易由一些輸入(input)和輸出(output)組合而來:函數
typedef struct txoutput { int value; string scriptPubKey; }TXOutput; typedef struct txinput { string txid; int vout; string scriptSig; }TXInput; typedef struct transaction { string id; vector<TXInput> vin; vector<TXOutput> vout; }Transaction;
對於每一筆新的交易,它的輸入會引用(reference)以前一筆交易的輸出(這裏有個例外,也就是咱們待會兒要談到的 coinbase 交易)。所謂引用以前的一個輸出,也就是將以前的一個輸出包含在另外一筆交易的輸入當中。交易的輸出,也就是幣實際存儲的地方。下面的圖示闡釋了交易之間的互相關聯:post
注意:區塊鏈
貫穿本文,咱們將會使用像「錢(money)」,「幣(coin)」,「花費(spend)」,「發送(send)」,「帳戶(account)」 等等這樣的詞。可是在比特幣中,實際並不存在這樣的概念。交易僅僅是經過一個腳本(script)來鎖定(lock)一些價值(value),而這些價值只能夠被鎖定它們的人解鎖(unlock)。ui
讓咱們先從輸出(output)開始:spa
typedef struct txoutput { int value; string scriptPubKey; }TXOutput;
實際上,正是輸出裏面存儲了「幣」(注意,也就是上面的 Value
字段)。而這裏的存儲,指的是用一個數學難題對輸出進行鎖定,這個難題被存儲在 ScriptPubKey
裏面。在內部,比特幣使用了一個叫作 Script 的腳本語言,用它來定義鎖定和解鎖輸出的邏輯。雖然這個語言至關的原始(這是爲了不潛在的黑客攻擊和濫用而有意爲之),並不複雜,可是咱們並不會在這裏討論它的細節。你能夠在這裏 找到詳細解釋。.net
在比特幣中,
value
字段存儲的是 satoshi 的數量,而不是>有 BTC 的數量。一個 satoshi 等於一百萬分之一的 >BTC(0.00000001 BTC),這也是比特幣裏面最小的貨幣單位>(就像是 1 分的硬幣)。
因爲尚未實現地址(address),因此目前咱們會避免涉及邏輯相關的完整腳本。ScriptPubKey
將會存儲一個任意的字符串(用戶定義的錢包地址)。
順便說一下,有了一個這樣的腳本語言,也意味着比特幣其實也能夠做爲一個智能合約平臺。
關於輸出,很是重要的一點是:它們是不可再分的(invisible),這也就是說,你沒法僅引用它的其中某一部分。要麼不用,若是要用,必須一次性用完。當一個新的交易中引用了某個輸出,那麼這個輸出必須被所有花費。若是它的值比須要的值大,那麼就會產生一個找零,找零會返還給發送方。這跟現實世界的場景十分相似,當你想要支付的時候,若是一個東西值 1 美圓,而你給了一個 5 美圓的紙幣,那麼你會獲得一個 4 美圓的找零。
這裏是輸入:
typedef struct txinput { string txid; int vout; string scriptSig; }TXInput;
正如以前所提到的,一個輸入引用了以前一筆交易的一個輸出:Txid
存儲的是這筆交易的 ID,Vout
存儲的是該輸出在這筆交易中全部輸出的索引(由於一筆交易可能有多個輸出,須要有信息指明是具體的哪個)。ScriptSig
是一個腳本,提供了可做用於一個輸出的 ScriptPubKey
的數據。若是 ScriptSig
提供的數據是正確的,那麼輸出就會被解鎖,而後被解鎖的值就能夠被用於產生新的輸出;若是數據不正確,輸出就沒法被引用在輸入中,或者說,也就是沒法使用這個輸出。這種機制,保證了用戶沒法花費屬於其餘人的幣。
再次強調,因爲咱們尚未實現地址,因此 ScriptSig
將僅僅存儲一個任意用戶定義的錢包地址。咱們會在下一篇文章中實現公鑰(public key)和簽名(signature)。
來簡要總結一下。輸出,就是 「幣」 存儲的地方。每一個輸出都會帶有一個解鎖腳本,這個腳本定義瞭解鎖該輸出的邏輯。每筆新的交易,必須至少有一個輸入和輸出。一個輸入引用了以前一筆交易的輸出,並提供了數據(也就是 ScriptSig
字段),該數據會被用在輸出的解鎖腳本中解鎖輸出,解鎖完成後便可使用它的值去產生新的輸出。
也就是說,每一筆輸入都是以前一筆交易的輸出,那麼從一筆交易開始不斷往前追溯,它涉及的輸入和輸出究竟是誰先存在呢?換個說法,這是個雞和蛋誰先誰後的問題,是先有蛋仍是先有雞呢?
在比特幣中,是先有蛋,而後纔有雞。輸入引用輸出的邏輯,是經典的「蛋仍是雞」問題:輸入先產生輸出,而後輸出使得輸入成爲可能。在比特幣中,最早有輸出,而後纔有輸入。換而言之,第一筆交易只有輸出,沒有輸入。
當礦工挖出一個新的塊時,它會向新的塊中添加一個 coinbase 交易。coinbase 交易是一種特殊的交易,它不須要引用以前一筆交易的輸出。它「憑空」產生了幣(也就是產生了新幣),這也是礦工得到挖出新塊的獎勵,能夠理解爲「發行新幣」。
在區塊鏈的最初,也就是第一個塊,叫作創世塊。正是這個創世塊,產生了區塊鏈最開始的輸出。對於創世塊,不須要引用以前交易的輸出。由於在創世塊以前根本不存在交易,也就沒有不存在有交易輸出。
來建立一個 coinbase 交易:
Transaction* NewCoinbaseTX(string to, string data) { if (data == "") data = "Reward to " + to; TXInput txin = TXInput{ "",-1,data }; TXOutput txout = TXOutput{ subsidy ,to }; Transaction* tx = new Transaction(); tx->id = ""; tx->vin.push_back(txin); tx->vout.push_back(txout); SetID(tx); return tx; }
coinbase 交易只有一個輸出,沒有輸入。在咱們的實現中,它的 Txid
爲空,Vout
等於 -1。而且,在目前的視線中,coinbase 交易也沒有在 ScriptSig
中存儲一個腳本,而只是存儲了一個任意的字符串。
在比特幣中,第一筆 coinbase 交易包含了以下信息:「The Times 03/Jan/2009 Chancellor on brink of second bailout for banks」。可點擊這裏查看.
subsidy
是獎勵的數額。在比特幣中,實際並無存儲這個數字,而是基於區塊總數進行計算而得:區塊總數除以 210000 就是 subsidy
。挖出創世塊的獎勵是 50 BTC,每挖出 210000
個塊後,獎勵減半。在咱們的實現中,這個獎勵值將會是一個常量(至少目前是)。
從如今開始,每一個塊必須存儲至少一筆交易。若是沒有交易,也就不可能挖出新的塊。這意味着咱們應該移除 Block
的 Data
字段,取而代之的是存儲交易:
struct block { time_t timeStamp; //string data; vector<struct transaction*> transactions; string prevBlockHash; string hash; int64_t nonce; };
NewBlock
和 NewGenesisBlock
也必須作出相應改變:
struct block* NewBlock(vector<struct transaction*> transactions, string prevBlockHash) { struct block* block = new Block{ time(NULL),transactions,prevBlockHash,"",0 }; ProofOfWork* pow = NewProofOfWork(block); string hash = ""; int64_t nonce = Run(hash, pow); if (pow != NULL) { delete pow; pow = NULL; } block->hash = hash; block->nonce = nonce; return block; } struct block* NewGenesisBlock(struct transaction* coinbase) { vector<struct transaction*> vec; vec.push_back(coinbase); return NewBlock(vec,""); }
接下來修改建立新鏈的函數:
struct blockchain* CreateBlockchain(string address) { string tip; const string genesisCoinbaseData = "The Times 03/Jan/2009 Chancellor on brink of second bailout for banks"; struct transaction* cbtx = NewCoinbaseTX(address, genesisCoinbaseData); struct block* genesis = NewGenesisBlock(cbtx); g_db[genesis->hash] = genesis; g_db["l"] = genesis; tip = genesis->hash; struct blockchain* bc = new Blockchain{ tip, &g_db }; return bc; }
如今,這個函數會接受一個地址做爲參數,這個地址會用來接收挖出創世塊的獎勵。
工做量證實算法必需要將存儲在區塊裏面的交易考慮進去,以此保證區塊鏈交易存儲的一致性和可靠性。因此,咱們必須修改 ProofOfWork.prepareData
方法:
string prepareData(int64_t nonce, ProofOfWork* pow) { stringstream ss; ss << pow->block->prevBlockHash << HashTransactions(pow->block) << pow->block->timeStamp << nonce; return ss.str(); }
不像以前使用 pow.block.Data
,如今咱們使用 pow.block.HashTransactions()
:
string HashTransactions(struct block* b) { vector<string> txHashes; for (int i = 0; i < b->transactions.size(); i++) { txHashes.push_back(b->transactions[i]->id); } stringstream ss; for (int i = 0; i < txHashes.size(); i++) { ss << txHashes[i]; } string hash = sha256(ss.str()); return hash; }
咱們使用哈希提供數據的惟一表示,這個以前也遇到過。咱們想要經過僅僅一個哈希,就能夠識別一個塊裏面的全部交易。爲此,咱們得到每筆交易的哈希,將它們關聯起來,而後得到一個鏈接後的組合哈希。
比特幣使用了一個更加複雜的技術:它將一個塊裏面包含的全部交易表示爲一個 Merkle tree ,而後在工做量證實系統中使用樹的根哈希(root hash)。這個方法可以讓咱們快速檢索一個塊裏面是否包含了某筆交易,即只需 root hash 而無需下載全部交易便可完成判斷。
來檢查一下到目前爲止是否正確:
很好!咱們已經得到了第一筆挖礦獎勵,可是,咱們要如何查看餘額呢?
咱們須要找到全部的未花費交易輸出(unspent transactions outputs, UTXO)。未花費(unspent) 指的是這個輸出尚未被包含在任何交易的輸入中,或者說沒有被任何輸入引用。在上面的圖示中,未花費的輸出是:
固然了,當咱們檢查餘額時,咱們並不須要知道整個區塊鏈上全部的 UTXO,只須要關注那些咱們可以解鎖的那些 UTXO(目前咱們尚未實現密鑰,因此咱們將會使用用戶定義的地址來代替)。首先,讓咱們定義在輸入和輸出上的鎖定和解鎖方法:
bool CanUnlockOutputWith(string unlockingData, TXInput* in) { return in->scriptSig == unlockingData; } bool CanBeUnlockedWith(string unlockingData, TXOutput* out) { return out->scriptPubKey == unlockingData; }
在這裏,咱們只是將 script 字段與 unlockingData
進行了比較。在後續文章咱們基於私鑰實現了地址之後,會對這部分進行改進。
下一步,找到包含未花費輸出的交易,這一步至關困難:
vector<struct transaction> FindUnspentTransactions(string address, struct blockchain* bc) { vector<struct transaction> unspentTXs; map<string, vector<int>> spentTXOs; struct blockchainiterator* bci = Iterator(bc); while (1) { struct block* block = Next(bci); for (int i = 0; i < block->transactions.size(); i++) { string txID = block->transactions[i]->id; for (int j = 0; j < block->transactions[i]->vout.size(); j++) { if (spentTXOs[txID].size() != 0) { for (int k = 0; k < spentTXOs[txID].size(); k++) { if (spentTXOs[txID][k] == j) { goto CONFLAG; } }//for (int k = 0; k < spentTXOs[txID].size(); k++) { }//if (spentTXOs[txID].size() != 0) { if ( CanBeUnlockedWith(address, &(block->transactions[i]->vout[j])) ) { unspentTXs.push_back(*(block->transactions[i])); } CONFLAG: int tmp = 0; }//for (int j = 0; j < block->transactions[i]->vout.size(); i++) { if (false == IsCoinbase(*(block->transactions[i])) ) { for (int k = 0; k < block->transactions[i]->vin.size(); k++) { if (CanUnlockOutputWith(address, &(block->transactions[i]->vin[k]))) { string inTxID = block->transactions[i]->vin[k].txid; spentTXOs[inTxID].push_back(block->transactions[i]->vin[k].vout); } } } }//for (int i = 0; i < block->transactions.size(); i++) { if (block->prevBlockHash == "") break; } return unspentTXs; }
因爲交易被存儲在區塊裏,因此咱們不得不檢查區塊鏈裏的每一筆交易。從輸出開始:
if ( CanBeUnlockedWith(address, &(block->transactions[i]->vout[j])) ) { unspentTXs.push_back(*(block->transactions[i])); }
若是一個輸出被一個地址鎖定,而且這個地址剛好是咱們要找的未花費交易輸出的地址,那麼這個輸出就是咱們想要的。不過在獲取它以前,咱們須要檢查該輸出是否已經被包含在一個輸入中,也就是檢查它是否已經被花費了:
if (spentTXOs[txID].size() != 0) { for (int k = 0; k < spentTXOs[txID].size(); k++) { if (spentTXOs[txID][k] == j) { goto CONFLAG; } }//for (int k = 0; k < spentTXOs[txID].size(); k++) { }//if (spentTXOs[txID].size() != 0) {
咱們跳過那些已經被包含在其餘輸入中的輸出(被包含在輸入中,也就是說明這個輸出已經被花費,沒法再用了)。檢查完輸出之後,咱們將全部可以解鎖給定地址鎖定的輸出的輸入彙集起來(這並不適用於 coinbase 交易,由於它們不解鎖輸出):
if (false == IsCoinbase(*(block->transactions[i])) ) { for (int k = 0; k < block->transactions[i]->vin.size(); k++) { if (CanUnlockOutputWith(address, &(block->transactions[i]->vin[k]))) { string inTxID = block->transactions[i]->vin[k].txid; spentTXOs[inTxID].push_back(block->transactions[i]->vin[k].vout); } } }
這個函數返回了一個交易列表,裏面包含了未花費輸出。爲了計算餘額,咱們還須要一個函數將這些交易做爲輸入,而後僅返回一個輸出:
vector<TXOutput> FindUTXO(string address, struct blockchain* bc) { vector<TXOutput> UTXOs; vector<struct transaction> unspentTransactions = FindUnspentTransactions(address, bc); for (auto& tx : unspentTransactions) { for (auto& out : tx.vout) { if (CanBeUnlockedWith(address, &out)) { UTXOs.push_back(out); } } } return UTXOs; }
就是這麼多了!如今咱們來實現 getbalance
命令:
void getBalance(string address) { Blockchain* bc = NewBlockchain(address); int balance = 0; vector<txoutput> UTXOs = FindUTXO(address, bc); for (auto& out : UTXOs) { balance += out.value; } std::cout << "Balance of " << address << " : " << balance << std::endl; }
帳戶餘額就是由帳戶地址鎖定的全部未花費交易輸出的總和。
在挖出創世塊之後,來檢查一下咱們的餘額:
這就是咱們的第一筆錢!
如今,咱們想要給其餘人發送一些幣。爲此,咱們須要建立一筆新的交易,將它放到一個塊裏,而後挖出這個塊。以前咱們只實現了 coinbase 交易(這是一種特殊的交易),如今咱們須要一種通用的交易:
Transaction* NewUTXOTransaction(string from, string to, int amount, Blockchain* bc) { vector<TXInput> inputs; vector<TXOutput> outputs; map<string, vector<int>> validOutputs; int acc = FindSpendableOutputs(from,amount,bc, validOutputs); if (acc < amount){ std::cerr << "ERROR: Not enough funds" << std::endl; return NULL; } map<string, vector<int>>::iterator it = validOutputs.begin(); for (; it != validOutputs.end(); it++) { for (auto& out : it->second) { TXInput input = TXInput{ it->first,out,from }; inputs.push_back(input); } } TXOutput output = TXOutput{ amount, to }; outputs.push_back(output); if (acc > amount){ TXOutput newout = TXOutput{ acc - amount, from }; outputs.push_back(newout); } Transaction* tx = new Transaction{ "", inputs, outputs }; SetID(tx); return tx; }
在建立新的輸出前,咱們首先必須找到全部的未花費輸出,而且確保它們存儲了足夠的值(value),這就是 FindSpendableOutputs
方法作的事情。隨後,對於每一個找到的輸出,會建立一個引用該輸出的輸入。接下來,咱們建立兩個輸出:
一個由接收者地址鎖定。這是給實際給其餘地址轉移的幣。
一個由發送者地址鎖定。這是一個找零。只有當未花費輸出超過新交易所需時產生。記住:輸出是不可再分的。
FindSpendableOutputs
方法基於以前定義的 FindUnspentTransactions
方法:
int FindSpendableOutputs(string address, int amount, struct blockchain* bc, map<string, vector<int>>& unspentOutputs) { unspentOutputs.clear(); vector<struct transaction> unspentTXs = FindUnspentTransactions(address, bc); int accumulated = 0; for (auto& tx : unspentTXs) { string txID = tx.id; for (int outIdx = 0; outIdx < tx.vout.size(); outIdx++) { if (CanBeUnlockedWith(address, &tx.vout[outIdx]) && accumulated < amount) { accumulated += tx.vout[outIdx].value; unspentOutputs[txID].push_back(outIdx); if (accumulated >= amount){ break; } } } } return accumulated; }
這個方法對全部的未花費交易進行迭代,並對它的值進行累加。當累加值大於或等於咱們想要傳送的值時,它就會中止並返回累加值,同時返回的還有經過交易 ID 進行分組的輸出索引。咱們並不想要取出超出須要花費的錢。
如今,咱們能夠修改 Blockchain.MineBlock
方法:
void MineBlock(vector<struct transaction*> transactions, struct blockchain* bc) { string lastHash; struct block* p = g_db["l"]; if (p == NULL) return; lastHash = p->hash; struct block* newBlock = NewBlock(transactions, lastHash); (*(bc->db))[newBlock->hash] = newBlock; (*(bc->db))["l"] = newBlock; bc->tip = newBlock->hash; }
最後,讓咱們來實現 send
方法:
void send(string from,string to,int amount) { Blockchain* bc = NewBlockchain(from); Transaction* tx = NewUTXOTransaction(from, to, amount, bc); vector<Transaction*> tmp; tmp.push_back(tx); MineBlock(tmp, bc); printChain(bc); getBalance(from); getBalance(to); std::cout << "sucess!!" << std::endl; }
發送幣意味着建立新的交易,並經過挖出新塊的方式將交易打包到區塊鏈中。不過,比特幣並非一連串馬上完成這些事情(不過咱們的實現是這麼作的)。相反,它會將全部新的交易放到一個內存池中(mempool),而後當一個礦工準備挖出一個新塊時,它就從內存池中取出全部的交易,建立一個候選塊。只有當包含這些交易的塊被挖出來,並添加到區塊鏈之後,裏面的交易纔開始確認。
讓咱們來檢查一下發送幣是否能工做(正確與錯誤狀況):
雖然不容易,可是如今終於實現交易了!不過,咱們依然缺乏了一些像比特幣那樣的一些關鍵特性:
地址(address)。咱們尚未基於私鑰(private key)的真實地址。
獎勵(reward)。如今挖礦是確定沒法盈利的!
UTXO 集。獲取餘額須要掃描整個區塊鏈,而當區塊很是多的時候,這麼作就會花費很長時間。而且,若是咱們想要驗證後續交易,也須要花費很長時間。而 UTXO 集就是爲了解決這些問題,加快交易相關的操做。
內存池(mempool)。在交易被打包到塊以前,這些交易被存儲在內存池裏面。在咱們目前的實現中,一個塊僅僅包含一筆交易,這是至關低效的。
工程代碼見羣下載 文件名爲part4.zip
參考博文:
https://blog.csdn.net/simple_the_best/article/details/78236282
https://jeiwan.cc/posts/building-blockchain-in-go-part-4/