一.练习目标:
构建包含A、B、C、D、E五个节点在内的区块链网络,在区块链网络中基于Pow算法共识机制实现A-E五个节点记账。编写相应的HTTP接口实现对记账信息的查询和区块记录的查询。
二. 任务内容
(1)构建包括区块(Block)、区块链(Blockchain)、网络(Network)、节点(Peer)模型
(2)实现PoW算法及目标值转换功能
(3)使用Flask-APScheduler启动A-E五个节点实现以“间隔”的方式进行PoW共识记账。
(4)基于Flask服务端架构,实现包括节点和区块数据查询接山和区块数据查询接口
三.具体实现
1.创建项目的实体模型
创建consensus_app项目,在项目中创建models.py文件模块,在其中创建区块(Block).区块链(Blockchain)、网络(Netvork)、模型(peer)。具体代码如下:
(1).创建区块(Block)模型
import hashlib
from datetime import datetime
import networkx as nx
import matplotlib.pyplot as plt
#1.定义区块难度
DIFFICULT_BITS = 0x1e11ffff
class Block(object):
def __init__(self,index,prev_hash,data,timestamp,bits):
"""
区块的初始化方法,在创建一个区块需传入包括索引号等相关信息
:param index: 区块索引号
:param prev_hash: 前一区块的哈希值
:param data: 区块中需保存的记录
:param timestamp: 区块生成的时间戳
:param bits: 区块需传入的比特值(预留)
"""
self.index = index
self.prev_hash = prev_hash
self.data = data
self.timestamp = timestamp
self.difficult_bits= bits
self.nonce = 0
# 计算新区块的默克根
self.merkle_root = self.calc_merkle_root()
# 计算区块的哈希值
self.block_hash = self.calc_block_hash()
def to_json(self):
"""
将区块内容以JSON的形式输出
:return:
"""
return {
"index": self.index,
"prev_hash": self.prev_hash,
"merkle_root": self.merkle_root,
"data": self.data,
"timestamp": self.timestamp.strftime('%Y/%m/%d %H:%M:%S'),
'bits': hex(self.difficult_bits)[2:].rjust(8, "0"),
'nonce': hex(self.nonce)[2:].rjust(8, "0"),
'block_hash': self.block_hash
}
def calc_merkle_root(self):
"""
计算默克树的根(Merkle Root)
:return:
"""
calc_txs = [tx for tx in self.data]
if len(calc_txs) == 1:
return calc_txs[0]
while len(calc_txs) > 1:
if len(calc_txs) % 2 == 1:
calc_txs.append(calc_txs[-1])
sub_hash_roots = []
for i in range(0, len(calc_txs), 2):
join_str = "".join(calc_txs[i:i+2])
sub_hash_roots.append(hashlib.sha256(join_str.encode()).hexdigest())
calc_txs = sub_hash_roots
return calc_txs[0]
def calc_block_hash(self):
"""
生成区块对应的哈希值
:return:
"""
blockheader = str(self.index) + str(self.prev_hash) \
+ str(self.data) + str(self.timestamp) + \
hex(self.difficult_bits)[2:] + str(self.nonce)
h = hashlib.sha256(blockheader.encode()).hexdigest()
self.block_hash = h
return h
需要注意的是,数据参数data中保存的区块数据不再使用交易,而是仅使用简单字符串数据,降低代码复杂度。另外,在区块中加入了“随机数"nonce和“区块难度"ificlt bits,分别为:
1. nonce:主要用于实现PoW算法:
2. dficult _bits: 不由外界传入,由上一区块参数传入。
(2).创建区块链(Blockchain)模型
from datetime import datetime
#1.定义区块难度
DIFFICULT_BITS = 0x1e11ffff
#区块链对象
class Blockchain(object):
def __init__(self):
self.chain = []
#用于记录不同节点按index顺序获取区块记账权的信息
self.peer_block = {}
self.create_genesis_block()
def add_block(self, block):
self.chain.append(block)
def add_peer_block(self,name,block_index):
"""
向peer_block中添加peer->block_index记录
:param name:节点名称
:param block_index区块索引值
"""
if name in self.peer_block:
self.peer_block[name].append(block_index)
else:
self.peer_block[name]= [block_index]
def query_peer_block(self,name):
"""
查询peer_block内容
:param name:节点名称
"""
return self.peer_block[name]
def query_block_info(self, index=0):
"""
通过索引值查询区块链chain中的区块信息
"""
int_index = int(index)
block_json = self.chain[int_index].to_json()
return block_json
def create_genesis_block(self):
"""
创建创世区块,在这里定义区块难度,这将被之后所有的区块引用
"""
genesis_block = Block(0,
"0" * 64,
["hello world"],
datetime.now(),
DIFFICULT_BITS)
self.add_block(genesis_block)
需要注意的是,在create_genesis_block 函数实现了初始区块(genesis_block)的创建,在其中加入了DIFFICULT_BITS表示区块难度,这将被之后新创建的区块沿用。
(3).创建节点
# 模拟的节点
class Peer:
def __init__(self,name):
self.name = name
self.last_block = 1
其中last_block参数勇于表示记录当前节点记录的区块链最新区块索引值
(4).创建网络(Network)模型
#区块链网络
class Network(object):
def __init__(self, name):
"""
初始化区块链网络
:paeam name:
"""
self.peers = [] #网络存在节点
self.name = name #网络名称
self.G = nx.Graph() #网络中的定义的network网络拓扑
def add_peer(self, peer):
"""
在网络中新增节点
"""
self.peers.append(peer)
self.G.add_node(peer.name)
def add_edge(self, s_peer, e_peer):
"""
在网络中新增节点间的边
"""
e = (s_peer, e_peer)
self.G.add_edge(*e)
def del_peer(self, peer_name):
"""
删除指定名称的peer节点
"""
for i, peer in enumerate(self.peers):
if peer_name == peer.name:
del self.peers[i]
self.G.remove_node(peer_name)
def draw_network(self):
"""
绘制网络
"""
pos = nx.spring_layout(self.G, iterations=100)
nx.draw(self.G, pos, with_labels=True)
plt.show()
2.创建项目实体
在consensus _app 项目中创建entity.py文件模块,在其中加入peer0 至peer4节点的实体以及区块链blockchain的实体内容
import models
# 用于创建节点和网络实体
peer0 = models.Peer('peer0')
peer1 = models.Peer('peer1')
peer2 = models.Peer('peer2')
peer3 = models.Peer('peer3')
peer4 = models.Peer('peer4')
peer_list = [peer0, peer1, peer2, peer3, peer4]
#创建区块链
blockchain = models.Blockchain()
3.创建Pow算法实现模块
在consensus_app项目中创建pow.py文件模块,在其中加入目标值生成函数和pow算法函数
#3.定义目标值的生成函数
def generate_target(difficult_bits):
"""
基于区块难度生成对应的目标值
:param difficult_bits:区块难度
:return:
"""
#取区块难度(16进制)的前2位作为指数
exponent = int(difficult_bits / 16 ** 6)
#取区块难度(16进制)的前6位作为系数
coefficient = int(difficult_bits % 16 ** 6)
print(f'exponent is {hex(exponent)}')
print(f'coefficient is {hex(coefficient)}')
#按照共识计算目标值
target = coefficient * 2 ** (8 * (exponent - 0x03))
print(f'target is {target}')
#将目标值转换成16进制表示的值
target_hex = hex(target)[2:].zfill(64)
print(f'target_hex is {target_hex}')
return target
#4.定义pow算法
def pow_alg(block):
#获取指定区块的难度
difficult_bits = block.difficult_bits
#生成目标值
target = generate_target(difficult_bits)
#通过循环的方式反复生成区块头的哈希值并与目标值比较
#设置计算次数为2的32次方
#每次循环将区块中的“随机值”累加1,并生成新的哈希值与目标值比较
for n in range(2 ** 32):
block.nonce = block.nonce + n
block.calc_block_hash()
# print(f'block_hash hex is {hex(int(block.block_hash, 16))}')
if int(block.block_hash, 16) < target:
print(f'{"*" * 20}完成计算!{"*" * 20}')
print(f'总共计算了:{block.nonce}次')
print(f'target hex值为{hex(target)[2:].zfill(64)}')
print(f'区块哈希值为:{hex(int(block.block_hash, 16))[2:].zfill(64)}')
return block
4.生成services处理模块
在consensus_app项目中创建services.py 文件模块,在其中加入加入包括创建网络(generate_network)和PoW算法的执行函数(exe_ pow)
import models
import entity
import pow
from datetime import datetime
def generate_network(network_name, peer_list):
"""
生成区块链网络,初始化Network实体,以及在Network实体中加入节点(node)和边(node)
:param network_name:网络名称
:param peer_list:已生成的节点列表
:return:生成的区块链网络实体对象
"""
g_network = models.Network(network_name)
for index, peer in enumerate(peer_list):
g_network.add_peer(peer)
if index == len(peer_list)-1:
#如果是最后一个节点,那就让最后一个节点和第一个节点首位相连
g_network.add_edge(peer.name, peer_list[0].name)
else:
#否则,建立本节点与下一个节点的边
g_network.add_edge(peer.name, peer_list[index+1].name)
return g_network
def exe_pow(data,peer_name):
"""
供节点循环执行pow算法
:param data:区块数据
;param peer_name:节点名称
"""
#1.首先获得区块链中最新区块
last_block = entity.blockchain.chain[-1]
#2.获取last_block区块的索引值
index = last_block.index + 1
#3.生成新的区块
g_block = models.Block(last_block.index + 1, last_block.prev_hash,data,datetime.now(),last_block.difficult_bits)
#4.将区块扔进pow算法中进行计算,在得到区块头后返回区块信息
c_block = pow.pow_alg(g_block)
#判断区块链内的内容,如果当前区块链中没有新数据产生,则将产生的区块加入区块链中。
if len (entity.blockchain.chain) <= index:
entity.blockchain.add_block(c_block)
entity.blockchain.add_peer_block(peer_name,index)
else:
#如果计算的区块索引已存在,则说明其他节点已抢先计算完成,则计算失败不能将结果保存至区块链
print(f'区块索引<{index}>,已存在!{peer_name}节点计算失败')
5. 创建程序执行模块
在consensus_app 项目中创建app.py文件执行模块,在其中定义区块链网络(network),创建Flask执行进程app以及定时任务执行scheduler,并设置定时执行间隔为20秒。在模块中加入HTTP相应接口。
接口名 | 接口描述 | 输入参数 | 返回值 |
peer_block _query | 获取所有节点及其记录区块的索引值 | 键值对形式。所有节 键值对形式。所有节 |
|
block_query | 查询指定区块索引值(高度) | id: 区块索引值 | 区块详细内容 |
from flask import Flask, request
from flask_apscheduler import APScheduler
import services
from datetime import datetime
import entity
app = Flask(__name__)
network = services.generate_network('test',entity.peer_list)
#创建定时任务
scheduler = APScheduler()
class Config(object):
SCHEDULER_API_ENABLED = True
app.config.from_object(Config())
scheduler.init_app(app)
#设置统一的间隔执行任务时间
interval_seconds = 20
#创建节点0(peer0),进行pow计算任务
@scheduler.task('interval',
id='peer0-calc',
seconds=interval_seconds,
misfire_grace_time=900)
def send_message():
#设置统一的区块数据
data = f'{entity.peer0.name}-{datetime.now().strftime("%Y-%m-%d %H:%M:%S")} '
services.exe_pow(data=data,peer_name=entity.peer0.name)
#创建节点1(peer1),进行pow计算任务
@scheduler.task('interval',
id='peer1-calc',
seconds=interval_seconds,
misfire_grace_time=900)
def send_message():
data = f'{entity.peer1.name}-{datetime.now().strftime("%Y-%m-%d %H:%M:%S")} '
services.exe_pow(data=data,peer_name=entity.peer1.name)
#创建节点2(peer2),进行pow计算任务
@scheduler.task('interval',
id='peer2-calc',
seconds=interval_seconds,
misfire_grace_time=900)
def send_message():
data = f'{entity.peer2.name}-{datetime.now().strftime("%Y-%m-%d %H:%M:%S")} '
services.exe_pow(data=data,peer_name=entity.peer2.name)
#创建节点3(peer4),进行pow计算任务
@scheduler.task('interval',
id='peer3-calc',
seconds=interval_seconds,
misfire_grace_time=900)
def send_message():
data = f'{entity.peer3.name}-{datetime.now().strftime("%Y-%m-%d %H:%M:%S")} '
services.exe_pow(data=data,peer_name=entity.peer3.name)
#创建节点4(peer4),进行pow计算任务
@scheduler.task('interval',
id='peer2-calc',
seconds=interval_seconds,
misfire_grace_time=900)
def send_message():
data = f'{entity.peer4.name}-{datetime.now().strftime("%Y-%m-%d %H:%M:%S")} '
services.exe_pow(data=data,peer_name=entity.peer4.name)
@app.route('/peer_block_query',methods = ['GET'])
def peer_block_query():
"""
获取所有节点及记录区块的索引值
"""
return entity.blockchain.peer_block
@app.route('/block_query',methods = ['GET'])
def block_query():
"""
查询指定区块索引值(高度)
"""
index = request.args['id']
return entity.blockchain.query_block_info(index)
if __name__ == '__main__':
scheduler.start()
app.run()
6.控制台输出结果
7.验证项目启动正确性
使用Postman以GET的方式访问http://127.0.0.1:5000/peer_block _query,将会获取节点与区块记录区块的信息
在区块链中以“键值对”的形式存储了节点与区块索引的信息,以此方式就可以判定不同区块的记录节点。
接着可以通过索引的方式查询不同区块的详细内容,使用Postman 以GET的形式访问http://127.0.0.1:5000/block _query?id=3 可以访问区块索引值(高度)为3的区块内容