<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
本文根據https://github.com/liuchengxu/blockchain-tutorial的內容,用python實現的,但根據個人的理解進行了一些修改,大量參照了原文的內容。文章末尾有"本節完整原始碼實現地址"。
在這個系列文章的一開始,我們就提到了,區塊鏈是一個分散式資料庫。不過在之前的文章中,我們選擇性地跳過了“分散式”這個部分,而是將注意力都放到了“資料庫”部分。到目前為止,我們幾乎已經實現了一個區塊鏈資料庫的所有元素。今天,我們將會分析之前跳過的一些機制。而在下一篇文章中,我們將會開始討論區塊鏈的分散式特性。
在上一篇文章中,我們略過的一個小細節是挖礦獎勵。現在,我們已經可以來完善這個細節了。
挖礦獎勵,實際上就是一筆 coinbase 交易。當一個挖礦節點開始挖出一個新塊時,它會將交易從佇列中取出,並在前面附加一筆 coinbase 交易。coinbase 交易只有一個輸出,裡面包含了礦工的公鑰雜湊。
實現獎勵,非常簡單,更新 send 即可:
def add_block(self, transactions): """ add a block to block_chain """ last_block = self.get_last_block() prev_hash = last_block.get_header_hash() height = last_block.block_header.height + 1 block_header = BlockHeader('', height, prev_hash) # reward to wallets[0] wallets = Wallets() keys = list(wallets.wallets.keys()) w = wallets[keys[0]] coin_base_tx = self.coin_base_tx(w.address) transactions.insert(0, coin_base_tx) block = Block(block_header, transactions) block.mine(self) block.set_header_hash() self.db.create(block.block_header.hash, block.serialize()) last_hash = block.block_header.hash self.set_last_hash(last_hash) utxo = UTXOSet() utxo.update(block)
獎勵給當前錢包的第一個地址。
在 Part 3: 持久化和命令列介面 中,我們研究了 Bitcoin Core 是如何在一個資料庫中儲存塊的,並且瞭解到區塊被儲存在 blocks 資料庫,交易輸出被儲存在 chainstate 資料庫。會回顧一下 chainstate 的機構:
c + 32 位元組的交易雜湊 -> 該筆交易的未花費交易輸出記錄
B + 32 位元組的塊雜湊 -> 未花費交易輸出的塊雜湊
在之前那篇文章中,雖然我們已經實現了交易,但是並沒有使用 chainstate 來儲存交易的輸出。所以,接下來我們繼續完成這部分。
chainstate 不儲存交易。它所儲存的是 UTXO 集,也就是未花費交易輸出的集合。除此以外,它還儲存了“資料庫表示的未花費交易輸出的塊雜湊”,不過我們會暫時略過塊雜湊這一點,因為我們還沒有用到塊高度(但是我們會在接下來的文章中繼續改進)。
那麼,我們為什麼需要 UTXO 集呢?
來思考一下我們早先實現的 Blockchain._find_unspent_transactions 方法:
def _find_unspent_transactions(self, address): """ Find all unspent transactions """ spent_txos = {} unspent_txs = {} last_block = self.get_last_block() last_height = last_block.block_header.height # Reverse for height in range(last_height, -1, -1): block = self.get_block_by_height(height) for tx in block.transactions: txid = tx.txid # all outputs for vout_index, vout in enumerate(tx.vouts): txos = spent_txos.get(txid, []) # vout_index is spent if vout_index in txos: continue if vout.can_unlock_output_with(address): old_vouts = unspent_txs.get(tx, []) old_vouts.append(vout) unspent_txs[tx] = old_vouts if not tx.is_coinbase(): for vin in tx.vins: if vin.can_be_unlocked_with(address): txid_vouts = spent_txos.get(txid, []) txid_vouts.append(vin.vout) spent_txos[vin.txid] = txid_vouts return unspent_txs
這個函數找到有未花費輸出的交易。由於交易被儲存在區塊中,所以它會對區塊鏈裡面的每一個區塊進行迭代,檢查裡面的每一筆交易。截止 2017 年 9 月 18 日,在位元幣中已經有 485,860 個塊,整個資料庫所需磁碟空間超過 140 Gb。這意味著一個人如果想要驗證交易,必須要執行一個全節點。此外,驗證交易將會需要在許多塊上進行迭代。
整個問題的解決方案是有一個僅有未花費輸出的索引,這就是 UTXO 集要做的事情:這是一個從所有區塊鏈交易中構建(對區塊進行迭代,但是隻須做一次)而來的快取,然後用它來計算餘額和驗證新的交易。截止 2017 年 9 月,UTXO 集大概有 2.7 Gb。
好了,讓我們來想一下實現 UTXO 集的話需要作出哪些改變。目前,找到交易用到了以下一些方法:
可以看到,所有方法都對資料庫中的所有塊進行迭代。但是目前我們還沒有改進所有方法,因為 UTXO 集沒法儲存所有交易,只會儲存那些有未花費輸出的交易。因此,它無法用於 Blockchain.FindTransaction。
所以,我們想要以下方法:
因此,從現在開始,兩個最常用的函數將會使用 cache!來開始寫程式碼吧。
class UTXOSet(Singleton): FLAG = 'UTXO' def __init__(self, db_url='http://127.0.0.1:5984'): self.db = DB(db_url)
這裡使用一個FLAG來區分普通區塊和UTXO。
def reindex(self, bc): key = self.FLAG + "l" last_block = bc.get_last_block() if key not in self.db: utxos = bc.find_UTXO() for txid, index_vouts in utxos.items(): key = self.FLAG + txid # outs = [] for index_vout in index_vouts: vout = index_vout[1] index = index_vout[0] vout_dict = vout.serialize() vout_dict.update({"index": index}) tmp_key = key + "-"+str(index) try: self.db.create(tmp_key, vout_dict) except ResourceConflict as e: print(e) if not last_block: return self.set_last_height(last_block.block_header.height) else: utxo_last_height = self.get_last_height() last_block_height = last_block.block_header.height for i in range(utxo_last_height, last_block_height): block = bc.get_block_by_height(i) self.update(block)
這個方法首先判斷是否已經構建過UTXO集,如果沒有構建過就從頭開始構建UTXO集,如果已經構建過了,就把當前UTXO的區塊至最新的區塊進行更新。
Blockchain.FindUTXO 幾乎跟 Blockchain.FindUnspentTransactions 一模一樣,但是現在它返回了一個 TransactionID -> TransactionOutputs 的 map。
現在,UTXO 集可以用於傳送幣:
def find_spendable_outputs(self, address, amount): utxos = self.find_utxo(address) accumulated = 0 spendable_utxos = [] for ftxo in utxos: output = ftxo.txoutput accumulated += output.value spendable_utxos.append(ftxo) if accumulated >= amount: break return accumulated, spendable_utxos
或者檢查餘額:
def find_utxo(self, address): query = { "selector": { "_id": { "$regex": "^UTXO" }, "pub_key_hash": address } } docs = self.db.find(query) utxos = [] for doc in docs: index = doc.get("index", None) if index is None: continue doc_id = doc.id txid_index_str = doc_id.replace(self.FLAG, "") _flag_index = txid_index_str.find("-") txid = txid_index_str[:_flag_index] ftxo = FullTXOutput(txid, TXOutput.deserialize(doc), index) utxos.append(ftxo) return utxos
有了 UTXO 集,也就意味著我們的資料(交易)現在已經被分開儲存:實際交易被儲存在區塊鏈中,未花費輸出被儲存在 UTXO 集中。這樣一來,我們就需要一個良好的同步機制,因為我們想要 UTXO 集時刻處於最新狀態,並且儲存最新交易的輸出。但是我們不想每生成一個新塊,就重新生成索引,因為這正是我們要極力避免的頻繁區塊鏈掃描。因此,我們需要一個機制來更新 UTXO 集:
def update(self, block): for tx in block.transactions: txid = tx.txid key = self.FLAG + txid # add uxto for vout_index, vout in enumerate(tx.vouts): vout_dict = vout.serialize() vout_dict.update({"index": vout_index}) tmp_key = key + "-" +str(vout_index) try: self.db.create(tmp_key, vout_dict) except ResourceConflict as e: print(e) # vins delete used utxo for vin in tx.vins: vin_txid = vin.txid key = self.FLAG + vin_txid + "-" +str(vin.vout) doc = self.db.get(key) if not doc: continue try: self.db.delete(doc) except ResourceNotFound as e: print(e) self.set_last_height(block.block_header.height)
雖然這個方法看起來有點複雜,但是它所要做的事情非常直觀。當挖出一個新塊時,應該更新 UTXO 集。更新意味著移除已花費輸出,並從新挖出來的交易中加入未花費輸出。如果一筆交易的輸出被移除,並且不再包含任何輸出,那麼這筆交易也應該被移除。相當簡單!
# 建立創世塊 $python3 main.py <wallet.Wallet object at 0x0000010AED8276A0> <wallet.Wallet object at 0x0000010AED827940> 19RUj6zvbrAXNnEtkuric5pYQJTkZy57nc 17AEyyKbeoEbfMa3jS8Uji6tVG37DrTJN9 Mining a new block Found nonce == 53ash_hex == 0adfd71d90955ad9219871d8abe03ae83ef9f1f13f9a141ef6ca0ce2d16c93af ('conflict', 'Document update conflict.') Block(_block_header=BlockHeader(timestamp='1551246051.6814992', hash_merkle_root='1f6cf2e68e8ab0dda1cc1550f85b4df85b83db3cc3af262b26a5a306121725be', prev_block_hash='', hash='ef20a87f2edc8589e813be60d534e736f51c45a3ec94e1918c18bce057afc89d', nonce=None, height=0)) Block(_block_header=BlockHeader(timestamp='1551246052.0582814', hash_merkle_root='3cf2c8514fdaac0cb2b6502f72cf267bcf9966042be28ee48eff61e4695a90f2', prev_block_hash='ef20a87f2edc8589e813be60d534e736f51c45a3ec94e1918c18bce057afc89d', hash='b0bdedf26575722a7efdf94db7dfa60c1c4dfe1483529ff04dd553d6828de718', nonce=53, height=1)) # 轉賬 $python3 cli.py send --from 19RUj6zvbrAXNnEtkuric5pYQJTkZy57nc --to 17AEyyKbeoEbfMa3jS8Uji6tVG37DrTJN9 --amount 10 Mining a new block Found nonce == 20ash_hex == 07e91245d4e66b66279224980b0325c37d2f2e54a75402bdcd8fe55346cb3dcb send 10 from 19RUj6zvbrAXNnEtkuric5pYQJTkZy57nc to 17AEyyKbeoEbfMa3jS8Uji6tVG37DrTJN9 # 查詢餘額 $python3 cli.py balance 19RUj6zvbrAXNnEtkuric5pYQJTkZy57nc 19RUj6zvbrAXNnEtkuric5pYQJTkZy57nc balance is 1980
一切工作正常, 19RUj6zvbrAXNnEtkuric5pYQJTkZy57nc收到了創世塊和轉賬的獎勵2000個,轉賬了兩次一共使用了20個,剩餘1980個。
在這篇文章中,我還想要再討論一個優化機制。
上如上面所提到的,完整的位元幣資料庫(也就是區塊鏈)需要超過 140 Gb 的磁碟空間。因為位元幣的去中心化特性,網路中的每個節點必須是獨立,自給自足的,也就是每個節點必須儲存一個區塊鏈的完整副本。隨著越來越多的人使用位元幣,這條規則變得越來越難以遵守:因為不太可能每個人都去執行一個全節點。並且,由於節點是網路中的完全參與者,它們負有相關責任:節點必須驗證交易和區塊。另外,要想與其他節點互動和下載新塊,也有一定的網路流量需求。
在中本聰的 位元幣原始論文 中,對這個問題也有一個解決方案:簡易支付驗證(Simplified Payment Verification, SPV)。SPV 是一個位元幣輕節點,它不需要下載整個區塊鏈,也不需要驗證區塊和交易。相反,它會在區塊鏈查詢交易(為了驗證支付),並且需要連線到一個全節點來檢索必要的資料。這個機制允許在僅執行一個全節點的情況下有多個輕錢包。
為了實現 SPV,需要有一個方式來檢查是否一個區塊包含了某筆交易,而無須下載整個區塊。這就是 Merkle 樹所要完成的事情。
位元幣用 Merkle 樹來獲取交易雜湊,雜湊被儲存在區塊頭中,並會用於工作量證明系統。到目前為止,我們只是將一個塊裡面的每筆交易雜湊連線了起來,將在上面應用了 SHA-256 演演算法。雖然這是一個用於獲取區塊交易唯一表示的一個不錯的途徑,但是它沒有利用到 Merkle 樹。
來看一下 Merkle 樹:
每個塊都會有一個 Merkle 樹,它從葉子節點(樹的底部)開始,一個葉子節點就是一個交易雜湊(位元幣使用雙 SHA256 雜湊)。葉子節點的數量如果不是雙數,就只取單個資料的hash。
從下往上,兩兩成對,連線兩個節點雜湊,將組合雜湊作為新的雜湊。新的雜湊就成為新的樹節點。重複該過程,直到僅有一個節點,也就是樹根。根雜湊然後就會當做是整個塊交易的唯一標示,將它儲存到區塊頭,然後用於工作量證明。
Merkle 樹的好處就是一個節點可以在不下載整個塊的情況下,驗證是否包含某筆交易。並且這些只需要一個交易雜湊,一個 Merkle 樹根雜湊和一個 Merkle 路徑。
這部分的描述和https://github.com/liuchengxu/blockchain-tutorial/blob/master/content/part-6/transactions-2.md描述有所不同,
因為存在葉子節點為雙數,但是第二層為單數的情況,會導致原版程式碼出現索引越界的情況。這部分的描述參考http://shouce.jb51.net/blockchain_guide/crypto/merkle_trie.html
實現程式碼如下:
class MerkleNode(object): def __init__(self, left_node, right_node, data): self.left = left_node self.right = right_node if not self.left and not self.right: self.data = sum256_hex(data) else: data = self.left.data + self.right.data self.data = sum256_hex(data) class MerkleTree(object): def __init__(self, datas): nodes = [] for data_item in datas: node = MerkleNode(None, None, data_item) nodes.append(node) for _ in range(len(datas)//2): new_level = [] for j in range(0, len(nodes), 2): if j + 1 >= len(nodes): node = MerkleNode(nodes[j], "", None) else: node = MerkleNode(nodes[j], nodes[j+1], None) new_level.append(node) nodes = new_level self.root_node = nodes[0] @property def root_hash(self): return self.root_node.data
如果最後只有單個節點,那麼就將另一個資料置空,只計算一個資料的雜湊。
if j + 1 >= len(nodes): node = MerkleNode(nodes[j], "", None)
根節點的data域就是雜湊。
@property def root_hash(self): return self.root_node.data
還有一件事情,我想要再談一談。
大家應該還記得,在位元幣中有一個 *指令碼(Script)*程式語言,它用於鎖定交易輸出;交易輸入提供瞭解鎖輸出的資料。這個語言非常簡單,用這個語言寫的程式碼其實就是一系列資料和操作符而已。比如如下範例:
5 2 OP_ADD 7 OP_EQUAL
5, 2, 和 7 是資料,OP_ADD 和 OP_EQUAL 是操作符。指令碼程式碼從左到右執行:將資料依次放入棧內,當遇到操作符時,就從棧內取出資料,並將操作符作用於資料,然後將結果作為棧頂元素。指令碼的棧,實際上就是一個先進後出的記憶體儲存:棧裡的第一個元素最後一個取出,後面的每一個元素都會放到前一個元素之上。
讓我們來對上面的指令碼分部執行:
步驟 | 棧 | 指令碼 | 說明 |
---|---|---|---|
1 | 空 | 5 2 OP_ADD 7 OP_EQUAL | 一開始棧為空 |
2 | 5 | 2 OP_ADD 7 OP_EQUAL | 從指令碼裡面取出 5 放入棧上 |
3 | 5 2 | OP_ADD 7 OP_EQUAL | 從指令碼裡面取出 2 放入棧上 |
4 | 7 | 7 OP_EQUAL | 遇到操作符 OP_ADD, 從棧裡取出兩個運算元 5 和 2,相加後將結果放回棧上 |
5 | 7 7 | OP_EQUAL | 從指令碼裡面取出 7 放到棧上 |
6 | true | 空 | 遇到操作符 OP_EQUAL,從棧裡取出兩個運算元並比較,將比較的結果放回棧內,指令碼執行完畢,為空 |
OP_ADD 從棧內取兩個元素,將這兩個元素進行相加,然後將結果重新放回棧內。OP_EQUAL 從棧內取兩個元素,然後對這兩個元素進行比較:如果它們相等,就在棧上放一個 true,否則放一個 false。指令碼執行的結果就是棧頂元素:在我們的案例中,如果是 true,那麼表明指令碼執行成功。
現在來看一下在位元幣中,是如何用指令碼執行支付的:
<signature> <pubKey> OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG
這個指令碼叫做 Pay to Public Key Hash(P2PKH),這是位元幣最常用的一個指令碼。它所做的事情就是向一個公鑰雜湊支付,也就是說,用某一個公鑰鎖定一些幣。這是位元幣支付的核心:沒有賬戶,沒有資金轉移;只有一個指令碼檢查提供的簽名和公鑰是否正確。
這個指令碼實際儲存為兩個部分:
第一個部分,<signature> <pubkey>,儲存在輸入的 ScriptSig 欄位。
第二部分,OP_DUP OP_HASH160 <pubkeyHash> OP_EQUALVERYFY OP_CHECKSIG 儲存在輸出的 ScriptPubKey 裡面。
因此,輸出定了解鎖的邏輯,輸入提供解鎖輸出的“鑰匙”。然我們來執行一下這個指令碼:
步驟 | 棧 | 指令碼 |
---|---|---|
1 | 空 | <signature> <pubKey> OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG |
2 | <signature> | <pubKey> OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG |
3 | <signature> <pubkey> | OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG |
4 | <signature> <pubKey> <pubKey> | OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG |
5 | <signature> <pubKey> <pubKeyHash> | <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG |
6 | <signature> <pubKey> <pubKeyHash> <pubKeyHash> | OP_EQUALVERIFY OP_CHECKSIG |
7 | <signature> <pubKey> | OP_CHECKSIG |
8 | true 或 false | 空 |
OP_DUP 對棧頂元素進行復制。OP_HASH160 取棧頂元素,然後用 RIPEMD160 對它進行雜湊,再將結果送回到棧上。OP_EQUALVERIFY 將棧頂的兩個元素進行比較,如果它們不相等,終止指令碼。OP_CHECKSIG 通過對交易進行雜湊,並使用 <signature> 和 pubKey 來驗證一筆交易的簽名。最後的操作符有點複雜:它生成了一個修剪後的交易副本,對它進行雜湊(因為它是一個被簽名後的交易雜湊),然後使用提供的 <signature> 和 pubKey 檢查簽名是否正確。
有了一個這樣的指令碼語言,實際上也可以讓位元幣成為一個智慧合約平臺:除了將一個單一的公鑰轉移資金,這個語言還使得一些其他的支付方案成為可能。
參考:
[1] transactions2
[2] 本節完整實現原始碼
這就是今天的全部內容了!我們已經實現了一個基於區塊鏈的加密貨幣的幾乎所有關鍵特性。我們已經有了區塊鏈,地址,挖礦和交易。我們還缺少網路讓所有的節點聯合起來,更多關於python區塊鏈挖礦獎勵交易的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45