目录
说明
本文根据的内容,用python实现的,但根据个人的理解进行了一些修改,大量引用了原文的内容。文章末尾有"本节完整源码实现地址"。
引言
到目前为止,我们已经构建了一个有工作量证明机制的区块链。有了工作量证明,挖矿也就有了着落。虽然目前距离一个有着完整功能的区块链越来越近了,但是它仍然缺少了一些重要的特性。在今天的内容中,我们会将区块链持久化到一个数据库中,然后会提供一个简单的命令行接口,用来完成一些与区块链的交互操作。本质上,区块链是一个分布式数据库,不过,我们暂时先忽略 “分布式” 这个部分,仅专注于 “存储” 这一点。
选择数据库
目前,我们的区块链实现里面并没有用到数据库,而是在每次运行程序时,简单地将区块链存储在内存中。那么一旦程序退出,所有的内容就都消失了。我们没有办法再次使用这条链,也没有办法与其他人共享,所以我们需要把它存储到磁盘上。
那么,我们要用哪个数据库呢?实际上,任何一个数据库都可以。在 中,并没有提到要使用哪一个具体的数据库,它完全取决于开发者如何选择。 ,最初由中本聪发布,现在是比特币的一个参考实现,它使用的是 。而我们将要使用的是…
couchdb
因为它:
- 简单易用
- 有一个web的ui界面,方便我们查看
- 丰富的查询支持
- 良好的python支持
couchdb的安装
直接安装,参考
docker版couchdb安装,使用docker-compose安装couchdb
1
2
3
4
5
6
7
|
# couchdb.yaml version: '2' services: couchdb:
image: hyperledger/fabric-couchdb
ports:
- 5984:5984
|
执行docker-compose -f couchdb.yaml up -d即可安装。
使用http://ip:5984/_utils即可访问couchdb的后台管理系统。
数据库结构
在开始实现持久化的逻辑之前,我们首先需要决定到底要如何在数据库中进行存储。为此,我们可以参考 bitcoin core 的做法:
简单来说,bitcoin core 使用两个 “bucket” 来存储数据:
- 其中一个 bucket 是 blocks,它存储了描述一条链中所有块的元数据
- 另一个 bucket 是 chainstate,存储了一条链的状态,也就是当前所有的未花费的交易输出,和一些元数据
此外,出于性能的考虑,bitcoin core 将每个区块(block)存储为磁盘上的不同文件。如此一来,就不需要仅仅为了读取一个单一的块而将所有(或者部分)的块都加载到内存中。而我们直接使用couchdb。
在 blocks 中,key -> value 为:
key | value |
---|---|
b 32 字节的 block hash | block index record |
f 4 字节的 file number | file information record |
l 4 字节的 file number | the last block file number used |
r 1 字节的 boolean | 是否正在 reindex |
f 1 字节的 flag name length flag name string | 1 byte boolean: various flags that can be on or off |
t 32 字节的 transaction hash | transaction index record |
在 chainstate,key -> value 为:
key | value |
---|---|
c 32 字节的 transaction hash | unspent transaction output record for that transaction |
b | 32 字节的 block hash: the block hash up to which the database represents the unspent transaction outputs |
详情可见 。
因为目前还没有交易,所以我们只需要 blocks bucket。另外,正如上面提到的,我们会将整个数据库存储为单个文件,而不是将区块存储在不同的文件中。所以,我们也不会需要文件编号(file number)相关的东西。最终,我们会用到的键值对有:
- 32 字节的 block-hash(转换为16进制字符串) -> block 结构
- l -> 链中最后一个块的 hash(转换为16进制字符串)
这就是实现持久化机制所有需要了解的内容了。
序列化
为了方便我们查看,这里我们不直接使用二进制数据,而将其转换为16进制字符串。所以我们需要对区块内容进行序列化。
让我们来实现 block 的 serialize 方法:
1
2
3
4
5
6
7
|
# class block def serialize( self ):
return {
"magic_no" : self ._magic_no,
"block_header" : self ._block_header.serialize(),
"transactions" : self ._transactions
}
|
直接返回我们需要的数据构成的字典即可,而block_header则需要进一步序列化。它的序列化同样也只需要返回具体的数据字典即可,如下:
1
2
3
|
# class blockheader def serialize( self ):
return self .__dict__
|
反序列化则是把信息转换为区块对象。
1
2
3
4
5
6
7
|
# class block @classmethod
def deserialize( cls , data):
block_header_dict = data[ 'block_header' ]
block_header = blockheader.deserialize(block_header_dict)
transactions = data[ "transactions" ]
return cls (block_header, transactions)
|
首先反序列化块,然后构造成一个对象,反序列化header:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
# class blockheader @classmethod
def deserialize( cls , data):
timestamp = data.get( 'timestamp' , '')
prev_block_hash = data.get( 'pre_block_hash' , '')
# hash = data.get('hash', '')
hash_merkle_root = data.get( 'hash_merkle_root' , '')
height = data.get( 'height' , '')
nonce = data.get( 'nonce' , '')
block_header = cls (hash_merkle_root, height, prev_block_hash)
block_header.timestamp = timestamp
block_header.nonce = nonce
return block_header
|
持久化
持久化要做的事情就是把区块数据写入到数据库中,则我们要做的事情有:
- 检查数据库是否已经有了一个区块链
- 如果没有则创建一个,创建创世块并将l指向这个块的哈希
- 添加一个区块,将l指向新添加的区块哈希
创建创世块如下:
1
2
3
4
5
6
7
|
# class blockchain: def new_genesis_block( self ):
if 'l' not in self .db:
genesis_block = block.new_genesis_block( 'genesis_block' )
genesis_block.set_header_hash()
self .db.create(genesis_block.block_header. hash , genesis_block.serialize())
self .set_last_hash(genesis_block.block_header. hash )
|
添加一个区块如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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)
block = block(block_header, transactions)
block.mine()
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)
|
对couchdb的操作的简单封装如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
class db(singleton):
def __init__( self , db_server_url, db_name = 'block_chain' ):
self ._db_server_url = db_server_url
self ._server = couchdb.server( self ._db_server_url)
self ._db_name = db_name
self ._db = none
@property
def db( self ):
if not self ._db:
try :
self ._db = self ._server[ self ._db_name]
except couchdb.resourcenotfound:
self ._db = self ._server.create( self ._db_name)
return self ._db
def create( self , id , data):
self .db[ id ] = data
return id
def __getattr__( self , name):
return getattr ( self .db, name)
def __contains__( self , name):
return self .db.__contains__(name)
def __getitem__( self , key):
return self .db[key]
def __setitem__( self , key, value):
self .db[key] = value
|
区块链迭代器
由于我们现在使用了数据库存储,不再是数组,那么我们便失去了迭代打印区块链的特性,我们需要重写__getitem__以获得该特性,实现如下:
1
2
3
4
5
6
7
8
|
# class blockchain(object): def __getitem__( self , index):
last_block = self .get_last_block()
height = last_block.block_header.height
if index < = height:
return self .get_block_by_height(index)
else :
raise indexerror( 'index is out of range' )
|
1
2
3
4
5
6
7
8
9
10
11
12
|
# class blockchain(object): def get_block_by_height( self , height):
"""
get a block by height
"""
query = { "selector" : { "block_header" : { "height" : height}}}
docs = self .db.find(query)
block = block( none , none )
for doc in docs:
block.deserialize(doc)
break
return block
|
根据区块高度获取对应的区块,此处是利用了couchdb的mongo_query的富查询来实现。
cli
到目前为止,我们的实现还没有提供一个与程序交互的接口。是时候加上交互了:
这里我们使用argparse来解析参数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
def new_parser():
parser = argparse.argumentparser()
sub_parser = parser.add_subparsers( help = 'commands' )
# a print command
print_parser = sub_parser.add_parser(
'print' , help = 'print all the blocks of the blockchain' )
print_parser.add_argument( '--print' , dest = 'print' , action = 'store_true' )
# a add command
add_parser = sub_parser.add_parser(
'addblock' , help = 'print all the blocks of the blockchain' )
add_parser.add_argument(
'--data' , type = str , dest = 'add_data' , help = 'block data' )
return parser
def print_chain(bc):
for block in bc:
print (block)
def add_block(bc, data):
bc.add_block(data)
print ( "success!" )
def main():
parser = new_parser()
args = parser.parse_args()
bc = blockchain()
if hasattr (args, 'print' ):
print_chain(bc)
if hasattr (args, 'add_data' ):
add_block(bc, args.add_data)
if __name__ = = "__main__" :
main()
|
测试一下
1
2
3
4
5
6
7
8
9
10
11
|
# 创世块创建 $python3 main.py mining a new block found nonce = = 19ash_hex = = 047f213bcb01f1ffbcdfafad57ffeead0e86924cf439594020da47ff2508291c
|
1
2
3
4
5
|
$python3 cli.py addblock - - data datas
mining a new block found nonce = = 6ash_hex = = 0864df4bfbb2fd115eeacfe9ff4d5813754198ba261c469000c29b74a1b391c5
|
一切正常工作。
感谢分享
感谢分享
感谢分享
感谢分享
感谢分享
感谢分享
感谢分享
感谢分享
感谢分享