上一章節中,咱們要給一筆交易記帳的話,必須本身手動進行一次挖礦,纔會把交易記錄加到一個區塊裏面去。 這一章節中,咱們將會引入未決交易中繼的機制。有了這個機制以後,咱們要進行一筆交易的時候,就不須要本身動手挖礦,而是將本身的交易發送到咱們的區塊鏈網絡中去(即中繼傳遞的概念),由其餘節點在挖礦以後,將咱們的交易記錄加到他們挖出的新的區塊中去。其中這些交易就被稱之爲「未決交易」。一個典型的例子就是,當一個用戶想要發起一筆交易(把必定數量的幣發送到指定的地址),他會把這筆交易廣播到整個網絡,並但願其餘礦工把該筆交易放到區塊中去。node
對於一個加密貨幣系統來講,這個功能異常的重要。由於這將意味着咱們不再須要由於要進行一筆交易而本身進行挖礦以對交易記錄進行保存。這將大大提升效率。 畢竟,如比特幣同樣,隨時着時間轉移,挖一個礦是愈來愈難。 若是咱們這些家裏沒礦(礦機)的用戶想跟別人交易一些比特幣,還要本身挖個礦,那這個交易就不知道何年何月才能達成了。git
爲了達到廣播到其餘節點並進行同步的目的,和第一章節進行區塊廣播和同步同樣,咱們須要對「未決交易」也要進行廣播。 也就是說,咱們的區塊鏈系統如今會包含如下的廣播和同步:github
本章節的完整代碼請查看這裏typescript
既然咱們的交易如今不是在建立後當即由本身進行挖礦記錄的,那麼咱們就須要將這些交易記錄下來,才能廣播給其餘礦工。咱們將保存未決交易的地方叫作「交易池」,在比特幣系統中也叫作mempool。shell
交易池其實就是咱們節點中的一個存儲了全部「未決交易」的數據結構,爲了實現簡單,咱們這裏將其設計成一個由「交易」數據組成的一個列表(若是不記得交易的結構長什麼樣的,請翻看上一章節記錄):npm
let transactionPool: Transaction[] = [];
固然,爲了和此前交易必須本身動手挖礦的那個/mindTransaction入口區分開,咱們提供了另一個接口僅做交易用 POST /sendTransaction 。 調用這個方法以後,咱們的節點將基於此前的錢包機制在咱們的本地交易池中建立一筆新的交易。此後咱們都會將這個接口做爲進行一筆交易優先使用的交易接口。json
app.post('/sendTransaction', (req, res) => { ... })
建立一筆交易的流程和第四章描述的流程相似,稍微有點區別的是,當交易建立後,咱們會將該交易歸入到咱們的交易池,而再也不是馬上進行挖礦記錄。網絡
整個「未決交易】的想法就是將這些交易傳播到整個區塊鏈網上,讓一些節點最終將其「挖」到區塊鏈中去。爲了達成這個目標,咱們首先要爲這些未決交易的傳播創建一些簡單的網絡規則:數據結構
相似區塊鏈的廣播和同步,咱們須要創建兩個新的消息類型來爲咱們未決交易池的廣播同步進行服務:QUERY_TRANSACTION_POOL 和 RESPONSE_TRANSACTION_POOL 。最終咱們的消息類型數據結構將以下所示:app
enum MessageType { QUERY_LATEST = 0, QUERY_ALL = 1, RESPONSE_BLOCKCHAIN = 2, QUERY_TRANSACTION_POOL = 3, RESPONSE_TRANSACTION_POOL = 4 }
交易池廣播請求數據最終會經過如下方式進行建立:
const responseTransactionPoolMsg = (): Message => ({ 'type': MessageType.RESPONSE_TRANSACTION_POOL, 'data': JSON.stringify(getTransactionPool()) }); const queryTransactionPoolMsg = (): Message => ({ 'type': MessageType.QUERY_TRANSACTION_POOL, 'data': null });
當節點收到RESPONSE_TRANSACTION_POOL這個廣播後,咱們須要有相應的代碼邏輯來因應請求。不管什麼時候咱們收到廣播過來的未決交易池,咱們都會嘗試將其加入到咱們的本地交易池中去。固然,咱們須要對裏面的交易作相應的校驗。 只有那些咱們此前沒有見過的交易(不在咱們本地保存的那份交易池清單中),以及有效的交易,咱們纔會將其歸入到咱們的交易池中。緊跟着,如上所述,咱們就會將咱們的交易池給廣播出去,以達到全網同步的效果:
case MessageType.RESPONSE_TRANSACTION_POOL: const receivedTransactions: Transaction[] = JSONToObject<Transaction[]>(message.data); receivedTransactions.forEach((transaction: Transaction) => { try { handleReceivedTransaction(transaction); //if no error is thrown, transaction was indeed added to the pool //let's broadcast transaction pool broadCastTransactionPool(); } catch (e) { //unconfirmed transaction not valid (we probably already have it in our pool) } });
由於廣播過來的未決交易數據是不可預知的,咱們必須對這些號稱是未決交易的數據進行有效性驗證。咱們此前實現的對交易的有效性檢查依然會用上, 好比檢查數據格式必須正確,交易的inputs,outputs和簽名必須一致(即此前章節描述的validateTxIn函數,簡單來講就是對每一個交易中的每一個input,用交易指向的來源output中的地址做爲公鑰,對input的簽名進行解密,並檢查和交易id是否同樣,以驗證input中引用的交易來源確實是屬於用戶全部。由於只有該公鑰是該用戶的才能正確解密本次交易的簽名, 這就證實了來源交易的擁有者確實就是本次交易的發起者了)。
const validateTxIn = (txIn: TxIn, transaction: Transaction, aUnspentTxOuts: UnspentTxOut[]): boolean => { const referencedUTxOut: UnspentTxOut = aUnspentTxOuts.find((uTxO) => uTxO.txOutId === txIn.txOutId && uTxO.txOutIndex === txIn.txOutIndex); if (referencedUTxOut == null) { console.log('referenced txOut not found: ' + JSON.stringify(txIn)); return false; } const address = referencedUTxOut.address; const key = ec.keyFromPublic(address, 'hex'); const validSignature: boolean = key.verify(transaction.id, txIn.signature); if (!validSignature) { console.log('invalid txIn signature: %s txId: %s address: %s', txIn.signature, transaction.id, referencedUTxOut.address); return false; } return true; };
除了以上已有規則外,咱們還須要增長一條新規:若是一筆交易中的任何一個用來引用交易貨幣來源的input在當前的未決交易池中已經存在了,那麼該交易將會被視爲無效交易,不會歸入到交易池中去。
const isValidTxForPool = (tx: Transaction, aTtransactionPool: Transaction[]): boolean => { const txPoolIns: TxIn[] = getTxPoolIns(aTtransactionPool); const containsTxIn = (txIns: TxIn[], txIn: TxIn) => { return _.find(txPoolIns, (txPoolIn => { return txIn.txOutIndex === txPoolIn.txOutIndex && txIn.txOutId === txPoolIn.txOutId; })) }; for (const txIn of tx.txIns) { if (containsTxIn(txPoolIns, txIn)) { console.log('txIn already found in the txPool'); return false; } } return true; };
這裏沒有顯式定義將未決交易從交易池中移除的操做,當前作法是,每次網絡中產生一個新區塊時,將會同時去更新交易池。
接下來咱們要實現相應的邏輯,讓一個節點將未決交易池記錄到其新挖到的一個區塊中去(正常狀況下,幫忙記帳的節點應該會收到獎勵的,可是咱們這裏並沒這樣實現)。整個邏輯很簡單:當一個節點挖到一個區塊時,隨即會將挖礦獲得的原始交易和咱們的交易池一併打包放到新挖出來的區塊中去。
const generateNextBlock = () => { const coinbaseTx: Transaction = getCoinbaseTransaction(getPublicFromWallet(), getLatestBlock().index + 1); const blockData: Transaction[] = [coinbaseTx].concat(getTransactionPool()); return generateRawNextBlock(blockData); };
由於該交易池在此前接受到RESPONSE_TRANSACTION_POOL廣播時已經作了驗證,因此咱們這裏不須要進行任何有效性驗證的工做。
一旦挖到一個新區塊,咱們的未決交易就有可能會隨新區塊一塊兒被記帳到區塊鏈中,該未決交易也就變成已決交易。也就是說,每一個新區塊攜帶的交易均可能會致使咱們的未決交易池再也不有效。 好比如下狀況:
這些都會致使咱們的交易池失效。交易池是失效了,可是,如咱們前兩章節闡述的,咱們還有一份一直保持更新的「未消費交易outpus」清單,因此咱們只需用該清單爲基石,將交易池中全部的input不存在於「未消費交易outupts」中的項移除掉就好了。代碼邏輯來在以下:
const updateTransactionPool = (unspentTxOuts: UnspentTxOut[]) => { const invalidTxs = []; for (const tx of transactionPool) { for (const txIn of tx.txIns) { if (!hasTxIn(txIn, unspentTxOuts)) { invalidTxs.push(tx); break; } } } if (invalidTxs.length > 0) { console.log('removing the following transactions from txPool: %s', JSON.stringify(invalidTxs)); transactionPool = _.without(transactionPool, ...invalidTxs) } };
npm run node1 npm run node2
經過postman分別發送請求到3001和3002兩個端口的/address接入點去獲取兩個錢包的地址, 好比節點1: GET http://localhost:3001/address
{ "address": "04d4d57026bd7b0d951b8d6c72ed9118004cd0929d10f94d7c41b24dbe9d84fa3bb389f2525c05a46bd8d1203b4b3c0e3499f30e5a55f84c573fcccd94c83bc13a" }
節點2進行一次挖礦,得到50個幣。 POST如下數據到http://localhost:3002/mineBlock 接口:
{ "data": "Some data from node2" }
返回以下:
{ "index": 1, "previousHash": "91a73664bc84c0baa1fc75ea6e4aa6d1d20c5df664c724e3159aefc2e1186627", "timestamp": 1561004915, "data": [ { "txIns": [ { "signature": "", "txOutId": "", "txOutIndex": 1 } ], "txOuts": [ { "address": "04bdc45ca144a5e5c8d0b03443f9aedfc8260d4665ac3fd41bd9eb2f06e4dc8228be1e14a4938837cada904cc81fc5747930f480d4d7888b5e82aac6f0581be0df", "amount": 50 } ], "id": "46dc195f7d44c2c1687fbd67a9a40263fd508067f89d8c07ee0e14826394b1d5" } ], "hash": "ca7eff56caf46b90cd7230a7c5684e1c201da48b7293aa282efebca2fabf0792", "difficulty": 0, "nonce": 0 }
由於全局未消費交易outputs是全網同步的,因此不管哪一個節點去查看,結果都是同樣的。 這裏以節點1爲例, Get如下接口: http://localhost:3001/unspentTransactionOutputs
返回以下:
[ { "txOutId": "e655f6a5f26dc9b4cac6e46f52336428287759cf81ef5ff10854f69d68f43fa3", "txOutIndex": 0, "address": "04bfcab8722991ae774db48f934ca79cfb7dd991229153b9f732ba5334aafcd8e7266e47076996b55a14bf9913ee3145ce0cfc1372ada8ada74bd287450313534a", "amount": 50 }, { "txOutId": "46dc195f7d44c2c1687fbd67a9a40263fd508067f89d8c07ee0e14826394b1d5", "txOutIndex": 0, "address": "04bdc45ca144a5e5c8d0b03443f9aedfc8260d4665ac3fd41bd9eb2f06e4dc8228be1e14a4938837cada904cc81fc5747930f480d4d7888b5e82aac6f0581be0df", "amount": 50 } ]
能夠看到節點2挖礦激勵所得的id爲「46dc195f7d44c2c1687fbd67a9a40263fd508067f89d8c07ee0e14826394b1d5」已經被放到未消費交易outputs中,另一條交易是系統啓動時自動建立原始交易產生的,不用管。
另外爲了方便咱們體驗,系統還一共了另一個接口來讓咱們查看對應節點所擁有的未消費outputs。好比咱們能夠發送 Get 到接口http://localhost:3002/myUnspentTransactionOutputs 去獲取節點2的未消費outputs列表。 返回以下
[ { "txOutId": "46dc195f7d44c2c1687fbd67a9a40263fd508067f89d8c07ee0e14826394b1d5", "txOutIndex": 0, "address": "04bdc45ca144a5e5c8d0b03443f9aedfc8260d4665ac3fd41bd9eb2f06e4dc8228be1e14a4938837cada904cc81fc5747930f480d4d7888b5e82aac6f0581be0df", "amount": 50 } ]
節點2給節點1的錢包發送30個幣。 POST如下交易數據到http://localhost:3002/sendTransaction 接口.
{ "address": "04d4d57026bd7b0d951b8d6c72ed9118004cd0929d10f94d7c41b24dbe9d84fa3bb389f2525c05a46bd8d1203b4b3c0e3499f30e5a55f84c573fcccd94c83bc13a", "amount": 30 }
返回以下:
{ "txIns": [ { "txOutId": "46dc195f7d44c2c1687fbd67a9a40263fd508067f89d8c07ee0e14826394b1d5", "txOutIndex": 0, "signature": "30450220130a2d3283337bb3721b992538ba19bbe6561376d0ddc9572595c0af8fb487d502210095d7cfe26717204329e22a386c274ec45d09190f1089fc354ad726ba375d917b" } ], "txOuts": [ { "address": "04d4d57026bd7b0d951b8d6c72ed9118004cd0929d10f94d7c41b24dbe9d84fa3bb389f2525c05a46bd8d1203b4b3c0e3499f30e5a55f84c573fcccd94c83bc13a", "amount": 30 }, { "address": "04bdc45ca144a5e5c8d0b03443f9aedfc8260d4665ac3fd41bd9eb2f06e4dc8228be1e14a4938837cada904cc81fc5747930f480d4d7888b5e82aac6f0581be0df", "amount": 20 } ], "id": "a4089fb337943eb80f381f107bdc1583c9b282e6cf735751d976ebd2125c5183" }
此時查看區塊鏈和未消費outputs,咱們能夠看到這筆交易其實尚未被加入到區塊鏈中,也沒有被消費掉的。
這時若是任何一個節點進行挖礦,就會把節點2發起的交易給記錄到新增的區塊中去。 好比節點1進行挖礦, POST 如下數據到 http://localhost:3001/mineBlock
{ "data": "Some data from node1" }
從返回記錄中能夠看到,新增的區塊交易記錄中,除了有挖礦激勵獲得的50個幣的記錄,上面節點2發起的交易記錄也被記帳到區塊中了
{ "index": 2, "previousHash": "ca7eff56caf46b90cd7230a7c5684e1c201da48b7293aa282efebca2fabf0792", "timestamp": 1561007564, "data": [ { "txIns": [ { "signature": "", "txOutId": "", "txOutIndex": 2 } ], "txOuts": [ { "address": "04d4d57026bd7b0d951b8d6c72ed9118004cd0929d10f94d7c41b24dbe9d84fa3bb389f2525c05a46bd8d1203b4b3c0e3499f30e5a55f84c573fcccd94c83bc13a", "amount": 50 } ], "id": "2e668dcd87fb900f346bab921a8349bc0e39d560c5a07a727cc9484aff471303" }, { "txIns": [ { "txOutId": "46dc195f7d44c2c1687fbd67a9a40263fd508067f89d8c07ee0e14826394b1d5", "txOutIndex": 0, "signature": "30450220130a2d3283337bb3721b992538ba19bbe6561376d0ddc9572595c0af8fb487d502210095d7cfe26717204329e22a386c274ec45d09190f1089fc354ad726ba375d917b" } ], "txOuts": [ { "address": "04d4d57026bd7b0d951b8d6c72ed9118004cd0929d10f94d7c41b24dbe9d84fa3bb389f2525c05a46bd8d1203b4b3c0e3499f30e5a55f84c573fcccd94c83bc13a", "amount": 30 }, { "address": "04bdc45ca144a5e5c8d0b03443f9aedfc8260d4665ac3fd41bd9eb2f06e4dc8228be1e14a4938837cada904cc81fc5747930f480d4d7888b5e82aac6f0581be0df", "amount": 20 } ], "id": "a4089fb337943eb80f381f107bdc1583c9b282e6cf735751d976ebd2125c5183" } ], "hash": "c245d2d2f9f1ec050b085fd2be39b3cd51e5af6bb53158297276d9b0b17c2b61", "difficulty": 0, "nonce": 0 }
這時咱們再查看未決交易池的話,就會發現該交易已經被更新移除掉了
[]
這時若是再查看節點2的未決交易outputs,會發現本身就剩餘20個幣。
[ { "txOutId": "a4089fb337943eb80f381f107bdc1583c9b282e6cf735751d976ebd2125c5183", "txOutIndex": 1, "address": "04bdc45ca144a5e5c8d0b03443f9aedfc8260d4665ac3fd41bd9eb2f06e4dc8228be1e14a4938837cada904cc81fc5747930f480d4d7888b5e82aac6f0581be0df", "amount": 20 } ]
經過「未決交易池」機制的引入,咱們如今再也不須要本身挖礦才能進行一筆交易,其餘節點也能幫咱們進行記帳。但,如前所述,對於幫助咱們記帳的節點並不會得到任何激勵,由於咱們的系統中沒有去實現交易手續費的功能。
下一個章節咱們將會實現一個UI界面來方便你們使用錢包和對區塊鏈進行操做。
本文由天地會珠海分舵編譯,轉載需受權,喜歡點個贊,吐槽請評論,如能給Github上的項目給個星,將不勝感激.