




版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
第python区块链简易版交易完善挖矿奖励示例目录说明引言奖励UTXO集Merkle树P2PKH总结
说明
本文根据/liuchengxu/blockchain-tutorial的内容,用python实现的,但根据个人的理解进行了一些修改,大量引用了原文的内容。文章末尾有本节完整源码实现地址。
引言
在这个系列文章的一开始,我们就提到了,区块链是一个分布式数据库。不过在之前的文章中,我们选择性地跳过了分布式这个部分,而是将注意力都放到了数据库部分。到目前为止,我们几乎已经实现了一个区块链数据库的所有元素。今天,我们将会分析之前跳过的一些机制。而在下一篇文章中,我们将会开始讨论区块链的分布式特性。
奖励
在上一篇文章中,我们略过的一个小细节是挖矿奖励。现在,我们已经可以来完善这个细节了。
挖矿奖励,实际上就是一笔coinbase交易。当一个挖矿节点开始挖出一个新块时,它会将交易从队列中取出,并在前面附加一笔coinbase交易。coinbase交易只有一个输出,里面包含了矿工的公钥哈希。
实现奖励,非常简单,更新send即可:
defadd_block(self,transactions):
addablocktoblock_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)
#rewardtowallets[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)
奖励给当前钱包的第一个地址。
UTXO集
在Part3:持久化和命令行接口中,我们研究了BitcoinCore是如何在一个数据库中存储块的,并且了解到区块被存储在blocks数据库,交易输出被存储在chainstate数据库。会回顾一下chainstate的机构:
c+32字节的交易哈希-该笔交易的未花费交易输出记录
B+32字节的块哈希-未花费交易输出的块哈希
在之前那篇文章中,虽然我们已经实现了交易,但是并没有使用chainstate来存储交易的输出。所以,接下来我们继续完成这部分。
chainstate不存储交易。它所存储的是UTXO集,也就是未花费交易输出的集合。除此以外,它还存储了数据库表示的未花费交易输出的块哈希,不过我们会暂时略过块哈希这一点,因为我们还没有用到块高度(但是我们会在接下来的文章中继续改进)。
那么,我们为什么需要UTXO集呢?
来思考一下我们早先实现的Blockchain._find_unspent_transactions方法:
def_find_unspent_transactions(self,address):
Findallunspenttransactions
spent_txos={}
unspent_txs={}
last_block=self.get_last_block()
last_height=last_block.block_header.height
#Reverse
forheightinrange(last_height,-1,-1):
block=self.get_block_by_height(height)
fortxinblock.transactions:
txid=tx.txid
#alloutputs
forvout_index,voutinenumerate(tx.vouts):
txos=spent_txos.get(txid,[])
#vout_indexisspent
ifvout_indexintxos:
continue
ifvout.can_unlock_output_with(address):
old_vouts=unspent_txs.get(tx,[])
old_vouts.append(vout)
unspent_txs[tx]=old_vouts
ifnottx.is_coinbase():
forvinintx.vins:
ifvin.can_be_unlocked_with(address):
txid_vouts=spent_txos.get(txid,[])
txid_vouts.append(vin.vout)
spent_txos[vin.txid]=txid_vouts
returnunspent_txs
这个函数找到有未花费输出的交易。由于交易被保存在区块中,所以它会对区块链里面的每一个区块进行迭代,检查里面的每一笔交易。截止2017年9月18日,在比特币中已经有485,860个块,整个数据库所需磁盘空间超过140Gb。这意味着一个人如果想要验证交易,必须要运行一个全节点。此外,验证交易将会需要在许多块上进行迭代。
整个问题的解决方案是有一个仅有未花费输出的索引,这就是UTXO集要做的事情:这是一个从所有区块链交易中构建(对区块进行迭代,但是只须做一次)而来的缓存,然后用它来计算余额和验证新的交易。截止2017年9月,UTXO集大概有2.7Gb。
好了,让我们来想一下实现UTXO集的话需要作出哪些改变。目前,找到交易用到了以下一些方法:
Blockchain._find_unspent_transactions-找到有未花费输出交易的主要函数。也是在这个函数里面会对所有区块进行迭代。Blockchain._find_spendable_outputs-这个函数用于当一个新的交易创建的时候。如果找到有所需数量的输出。使用Blockchain._find_unspent_transactions.Blockchain.find_UTXO-找到一个公钥哈希的未花费输出,然后用来获取余额。使用Blockchain._find_unspent_transactions.Blockchain.FindTransation-根据ID在区块链中找到一笔交易。它会在所有块上进行迭代直到找到它。
可以看到,所有方法都对数据库中的所有块进行迭代。但是目前我们还没有改进所有方法,因为UTXO集没法存储所有交易,只会存储那些有未花费输出的交易。因此,它无法用于Blockchain.FindTransaction。
所以,我们想要以下方法:
Blockchain.find_UTXO-通过对区块进行迭代找到所有未花费输出。UTXOSet.reindex-使用UTXO找到未花费输出,然后在数据库中进行存储。这里就是缓存的地方。UTXOSet._find_spendable_outputs-类似Blockchain._find_spendable_outputs,但是使用UTXO集。UTXOSet.find_UTXO-类似Blockchain.find_UTXO,但是使用UTXO集。Blockchain.find_transaction跟之前一样。
因此,从现在开始,两个最常用的函数将会使用cache!来开始写代码吧。
classUTXOSet(Singleton):
FLAG='UTXO'
def__init__(self,db_url=':5984'):
self.db=DB(db_url)
这里使用一个FLAG来区分普通区块和UTXO。
defreindex(self,bc):
key=self.FLAG+"l"
last_block=bc.get_last_block()
ifkeynotinself.db:
utxos=bc.find_UTXO()
fortxid,index_voutsinutxos.items():
key=self.FLAG+txid
#outs=[]
forindex_voutinindex_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)
exceptResourceConflictase:
print(e)
ifnotlast_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
foriinrange(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集可以用于发送币:
deffind_spendable_outputs(self,address,amount):
utxos=self.find_utxo(address)
accumulated=0
spendable_utxos=[]
forftxoinutxos:
output=ftxo.txoutput
accumulated+=output.value
spendable_utxos.append(ftxo)
ifaccumulated=amount:
break
returnaccumulated,spendable_utxos
或者检查余额:
deffind_utxo(self,address):
query={
"selector":{
"_id":{
"$regex":"^UTXO"
"pub_key_hash":address
docs=self.db.find(query)
utxos=[]
fordocindocs:
index=doc.get("index",None)
ifindexisNone:
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)
returnutxos
有了UTXO集,也就意味着我们的数据(交易)现在已经被分开存储:实际交易被存储在区块链中,未花费输出被存储在UTXO集中。这样一来,我们就需要一个良好的同步机制,因为我们想要UTXO集时刻处于最新状态,并且存储最新交易的输出。但是我们不想每生成一个新块,就重新生成索引,因为这正是我们要极力避免的频繁区块链扫描。因此,我们需要一个机制来更新UTXO集:
defupdate(self,block):
fortxinblock.transactions:
txid=tx.txid
key=self.FLAG+txid
#adduxto
forvout_index,voutinenumerate(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)
exceptResourceConflictase:
print(e)
#vinsdeleteusedutxo
forvinintx.vins:
vin_txid=vin.txid
key=self.FLAG+vin_txid+"-"+str(vin.vout)
doc=self.db.get(key)
ifnotdoc:
continue
try:
self.db.delete(doc)
exceptResourceNotFoundase:
print(e)
self.set_last_height(block.block_header.height)
虽然这个方法看起来有点复杂,但是它所要做的事情非常直观。当挖出一个新块时,应该更新UTXO集。更新意味着移除已花费输出,并从新挖出来的交易中加入未花费输出。如果一笔交易的输出被移除,并且不再包含任何输出,那么这笔交易也应该被移除。相当简单!
#创建创世块
$python3main.py
wallet.Walletobjectat0x0000010AED8276A0wallet.Walletobjectat0x0000010AED827940
19RUj6zvbrAXNnEtkuric5pYQJTkZy57nc17AEyyKbeoEbfMa3jS8Uji6tVG37DrTJN9
Mininganewblock
Foundnonce==53ash_hex==0adfd71d90955ad9219871d8abe03ae83ef9f1f13f9a141ef6ca0ce2d16c93af
('conflict','Documentupdateconflict.')
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))
$python3cli.pysend--from19RUj6zvbrAXNnEtkuric5pYQJTkZy57nc--to17AEyyKbeoEbfMa3jS8Uji6tVG37DrTJN9--amount10
Mininganewblock
Foundnonce==20ash_hex==07e91245d4e66b66279224980b0325c37d2f2e54a75402bdcd8fe55346cb3dcb
send10from19RUj6zvbrAXNnEtkuric5pYQJTkZy57ncto17AEyyKbeoEbfMa3jS8Uji6tVG37DrTJN9
#查询余额
$python3cli.pybalance19RUj6zvbrAXNnEtkuric5pYQJTkZy57nc
19RUj6zvbrAXNnEtkuric5pYQJTkZy57ncbalanceis1980
一切工作正常,19RUj6zvbrAXNnEtkuric5pYQJTkZy57nc收到了创世块和转账的奖励2000个,转账了两次一共使用了20个,剩余1980个。
Merkle树
在这篇文章中,我还想要再讨论一个优化机制。
上如上面所提到的,完整的比特币数据库(也就是区块链)需要超过140Gb的磁盘空间。因为比特币的去中心化特性,网络中的每个节点必须是独立,自给自足的,也就是每个节点必须存储一个区块链的完整副本。随着越来越多的人使用比特币,这条规则变得越来越难以遵守:因为不太可能每个人都去运行一个全节点。并且,由于节点是网络中的完全参与者,它们负有相关责任:节点必须验证交易和区块。另外,要想与其他节点交互和下载新块,也有一定的网络流量需求。
在中本聪的比特币原始论文中,对这个问题也有一个解决方案:简易支付验证(SimplifiedPaymentVerification,SPV)。SPV是一个比特币轻节点,它不需要下载整个区块链,也不需要验证区块和交易。相反,它会在区块链查找交易(为了验证支付),并且需要连接到一个全节点来检索必要的数据。这个机制允许在仅运行一个全节点的情况下有多个轻钱包。
为了实现SPV,需要有一个方式来检查是否一个区块包含了某笔交易,而无须下载整个区块。这就是Merkle树所要完成的事情。
比特币用Merkle树来获取交易哈希,哈希被保存在区块头中,并会用于工作量证明系统。到目前为止,我们只是将一个块里面的每笔交易哈希连接了起来,将在上面应用了SHA-256算法。虽然这是一个用于获取区块交易唯一表示的一个不错的途径,但是它没有利用到Merkle树。
来看一下Merkle树:
每个块都会有一个Merkle树,它从叶子节点(树的底部)开始,一个叶子节点就是一个交易哈希(比特币使用双SHA256哈希)。叶子节点的数量如果不是双数,就只取单个数据的hash。
从下往上,两两成对,连接两个节点哈希,将组合哈希作为新的哈希。新的哈希就成为新的树节点。重复该过程,直到仅有一个节点,也就是树根。根哈希然后就会当做是整个块交易的唯一标示,将它保存到区块头,然后用于工作量证明。
Merkle树的好处就是一个节点可以在不下载整个块的情况下,验证是否包含某笔交易。并且这些只需要一个交易哈希,一个Merkle树根哈希和一个Merkle路径。
这部分的描述和/liuchengxu/blockchain-tutorial/blob/master/content/part-6/transactions-2.md描述有所不同,
因为存在叶子节点为双数,但是第二层为单数的情况,会导致原版代码出现索引越界的情况。这部分的描述参考/blockchain_guide/crypto/merkle_trie.html
实现代码如下:
classMerkleNode(object):
def__init__(self,left_node,right_node,data):
self.left=left_node
self.right=right_node
ifnotself.leftandnotself.right:
self.data=sum256_hex(data)
else:
data=self.left.data+self.right.data
self.data=sum256_hex(data)
classMerkleTree(object):
def__init__(self,datas):
nodes=[]
fordata_itemindatas:
node=MerkleNode(None,None,data_item)
nodes.append(node)
for_inrange(len(datas)//2):
new_level=[]
forjinrange(0,len(nodes),2):
ifj+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
defroot_hash(self):
returnself.root_node.data
如果最后只有单个节点,那么就将另一个数据置空,只计算一个数据的哈希。
ifj+1=len(nodes):
node=MerkleNode(nodes[j],"",None)
根节点的data域就是哈希。
@property
defroot_hash(self):
returnself.root_node.data
P2PKH
还有一件事情,我想要再谈一谈。
大家应该还记得,在比特币中有一个*脚本(Script)*编程语言,它用于锁定交易输出;交易输入提供了解锁输出的数据。这个语言非常简单,用这个语言写的代码其实就是一系列数据和操作符而已。比如如下示例:
52OP_ADD7OP_EQUAL
5,2,和7是数据,OP_ADD和OP_EQUAL是操作符。脚本代码从左到右执行:将数据依次放入栈内,当遇到操作符时,就从栈内取出数据,并将操作符作用于数据,然后将结果作为栈顶元素。脚本的栈,实际上就是一个先进后出的内存存储:栈里的第一个元素最后一个取出,后面的每一个元素都会放到前一个元素之上。
让我们来对上面的脚本分部执行:
步骤栈脚本说明1空52OP_ADD7OP_EQUAL一开始栈为空252OP_ADD7OP_EQUAL从脚本里面取出5放入栈上352OP_ADD7OP_EQUAL从脚本里面取出2放入栈上477OP_EQUAL遇到操作符OP_ADD,从栈里取出两个操作数5和2,相加后将结果放回栈上577OP_EQUAL从脚本里面取出7放到栈上6true空遇到操作符OP_EQUAL,从栈里取出两个操作数并比较,将比较的结果放回栈内,脚本执行完毕,为空
OP_ADD从栈内取两个元素,将这两个元素进行相加,然后将结果重新放回栈内。OP_EQUAL从栈内取两个元素,然后对这两个元素进行比较:如果它们相等,就在栈上放一个true,否则放一个false。脚本执行的结果就是栈顶元素:在我们的案例中,如果是true,那么表明脚本执行成功。
现在来看一下在比特币中,是如何用脚本执行支付的:
signaturepubKeyOP_DUPOP_HASH160pubKeyHashOP_EQUALVERIFYOP_CHECKSIG
这个脚本叫做PaytoPublicKeyHash(P2PKH),这是比特币最常用的一个脚本。它所做的事情就是
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 战略投资与风险评估方式试题及答案
- 法学概论考试中的选择题技巧与试题及答案
- 行政法学的历史与发展及试题
- 软件设计师备考常见问题将解答试题及答案
- 加强公司财务内控的工作计划
- 随州市随县事业单位2025年统一公开招聘笔试历年典型考题及考点剖析附带答案详解
- 硬件接口设计基础知识试题及答案
- 行政管理考试知识体系建立:试题及答案
- 分布式系统的设计与实现能力测试试题及答案
- 重要信息处理软件试题及答案参考
- 畜牧养殖大型沼气项目可行性研究报告
- 陈志海-发热伴血小板减少综合征
- 2024年武汉长江科创科技发展有限公司招聘笔试参考题库附带答案详解
- 《土石坝沥青混凝土面板和心墙设计规范》
- 世纪大道石灰固化土QC成果
- 人工打桩施工计划书
- 传奇辅助脚本
- 宗教场所消防安全培训课件
- 2024年广东湛江交通投资集团招聘笔试参考题库含答案解析
- 中华人民共和国人民武装警察法释义
- 华为经营管理-华为供应链管理(6版)
评论
0/150
提交评论