之前在github上看到这个小项目, 作者用不到300行python代码实现了一个迷你区块链, 通过查看代码和运行效果, 基本可以对区块链中一些核心概念有个大体了解。本篇对代码进行一个梳理解读, 我把源码做了微小修改以丰富运行结果输出, 并删除了源码中繁琐的注释减少代码长度(本来这篇文章就是解释代码的:) 有兴趣的同学可以玩耍一下
blockchain 类
首先我们需要按照区块链技术的一些基础概念搭个架子出来例如: 未上链交易池, 整个区块链账本, 相连节点, 交易打包成新区块, 共识算法(这里是pow) 等
一步步来, 首先当然是构造函数, 先初始化三个属性:
相连节点用set存储可以避免重复添加的情况
self.new_block() 是创建整个区块链的创世块, 这个后边可以看到解释, 先忽略
1 2 3 4 5 6 7
| class Blockchain: def __init__(self): self.current_transactions = [] self.chain = [] self.nodes = set() self.new_block(previous_hash='1', proof=100, valid_hash='')
|
新建一笔交易
这个很简单, 就是向current_transactions
这个列表里面添加一个字典信息代表一笔未打包的交易, 返回值是应该添加进下一个区块的索引
1 2 3 4 5 6 7 8 9
| def new_transaction(self, sender, recipient, amount): self.current_transactions.append({ 'sender': sender, 'recipient': recipient, 'amount': amount, })
return self.last_block['index'] + 1
|
打包交易进入区块
在这个代码中, 打包进入区块是个简单的添加动作, 其实在这之前是需要先挖矿成功的, 你可能对挖矿部分很感兴趣, 其实实现起来也很简单, 可能是作者为了逻辑更清晰简单, 把挖矿逻辑和打包分开了, 这部分后边可以看到
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| def new_block(self, proof, previous_hash, valid_hash):
block = { 'index': len(self.chain) + 1, 'timestamp': time(), 'transactions': self.current_transactions, 'proof': proof, 'previous_hash': previous_hash or self.hash(self.chain[-1]), 'valid_hash': valid_hash, } self.current_transactions = []
self.chain.append(block) return block
|
可以看到一个区块有以下属性:
- index: 索引号
- timestamp: 时间戳
- transactions: 打包的交易
- proof: 工作量证明难度
- previous_hash: 前一个区块的哈希值
- valid_hash: 本区块一个有效的哈希值
proof和比特币里面的幸运数的概念一样, 不断地尝试proof去算出一个valid_hash, 虽然valid_hash可以有很多个有效值, 但我们只需要找到一个符合共识算法的有效值就可以了, 因为pow类算法重在验证做了多少工作, 而不是找一个全局唯一值
POW
hash 这个函数选用python标准库中hashlib.sha256实现, json.dumps() 时一定要按key排序, 因为dict默认是无序的, 这样每次哈希后会得到不一样的哈希值, 这当然不是我们想要的
proof_of_work中proof从0开始不断加1, 调用valid_proof进行猜测, 可以看到, 这个简单pow实现是通过对上一区块幸运数last_proof, 本区块幸运数proof, 上一区块哈希值last_hash进行sha256得到的16进制结果判断高5位是否都是0来计算一个合法的区块哈希值
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
| @property def last_block(self): return self.chain[-1]
@staticmethod def hash(block): block_string = json.dumps(block, sort_keys=True).encode() return hashlib.sha256(block_string).hexdigest()
def proof_of_work(self, last_block):
last_proof = last_block['proof'] last_hash = self.hash(last_block)
proof = 0 while self.valid_proof(last_proof, proof, last_hash)[0] is False: proof += 1 valid_hash = self.valid_proof(last_proof, proof, last_hash)[1]
return proof, valid_hash
@staticmethod def valid_proof(last_proof, proof, last_hash):
guess = f'{last_proof}{proof}{last_hash}'.encode() guess_hash = hashlib.sha256(guess).hexdigest() return guess_hash[:5] == "00000", guess_hash
|
至此一个单节点的区块链就差不多了, 我们用flask先包装几个http的api, flask是python里面一个很轻量的web框架, 没用过的同学可以玩玩
用uuid4生成节点的唯一id, 这里代表地址的概念
可以看到在proof_of_work调用成功后, 会新建一笔交易作为系统给节点的奖励, 这就是我们在比特币概念中所熟知的挖矿了, sender="0"
代表系统本身, recipient=node_identifier
代表接收者是节点自己, amount=1
是奖励值
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
| app = Flask(__name__) node_identifier = str(uuid4()).replace('-', '') blockchain = Blockchain()
@app.route('/mine', methods=['GET']) def mine(): last_block = blockchain.last_block proof, valid_hash = blockchain.proof_of_work(last_block)
blockchain.new_transaction( sender="0", recipient=node_identifier, amount=1, )
previous_hash = blockchain.hash(last_block) block = blockchain.new_block(proof, previous_hash, valid_hash)
response = { 'message': "New Block Forged", 'index': block['index'], 'transactions': block['transactions'], 'proof': block['proof'], 'previous_hash': block['previous_hash'], 'valid_hash': valid_hash } return jsonify(response), 200
|
封装添加新交易的API, 把交易添加到该节点的交易池中
1 2 3 4 5 6 7 8 9 10 11
| @app.route('/transactions/new', methods=['POST']) def new_transaction(): values = request.get_json() required = ['sender', 'recipient', 'amount'] if not all(k in values for k in required): return 'Missing values', 400
index = blockchain.new_transaction(values['sender'], values['recipient'], values['amount'])
response = {'message': f'Transaction will be added to Block {index}'} return jsonify(response), 201
|
查看整个区块链上数据
1 2 3 4 5 6 7
| @app.route('/chain', methods=['GET']) def full_chain(): response = { 'chain': blockchain.chain, 'length': len(blockchain.chain), } return jsonify(response), 200
|
运行信息
1 2 3 4 5 6 7 8 9
| if __name__ == '__main__': from argparse import ArgumentParser
parser = ArgumentParser() parser.add_argument('-p', '--port', default=5000, type=int, help='port to listen on') args = parser.parse_args() port = args.port
app.run(host='0.0.0.0', port=port)
|
把节点启动:
1 2
| ~$ pipenv run python blockchain.py * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
|
尝试请求一下我们的接口, 会看到只有一个创世区块
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| ~$ curl http://127.0.0.1:5000/chain { "chain": [ { "index": 1, "previous_hash": "1", "proof": 100, "timestamp": 1523280849.9193473, "transactions": [], "valid_hash": "" } ], "length": 1 }
|
接下来我们添加一笔交易:
此时交易进入了节点的未打包交易池中, 而且返回该笔交易即将添加进入到第二个区块中
1 2 3 4 5 6 7 8
| ~$ curl -X POST -H "Content-Type: application/json" -d '{ "sender": "fmls0x01", "recipient": "yangliu", "amount": 32 }' "http://localhost:5000/transactions/new"
{ "message": "Transaction will be added to Block 2" }
|
现在让此节点进行一次挖矿:
可以看到通过挖矿产生了一个新区块, 打包的交易中第一个是我们上一步所产生的交易, 第二个就是挖矿所产生的奖励了, 比特币的挖矿原理是一样的, 是系统给挖矿节点地址发送了一些比特币, 不过流程更复杂罢了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| ~$ curl http://127.0.0.1:5000/mine { "index": 2, "message": "New Block Forged", "previous_hash": "5cb31cb7f3a90ad7914f04b884e4536a745621c882907e38f6158fb213129f48", "proof": 109259, "transactions": [ { "amount": 32, "recipient": "yangliu", "sender": "fmls0x01" }, { "amount": 1, "recipient": "69265c55b7c9495684ec83ecfeac2b92", "sender": "0" } ], "valid_hash": "000004d6f3ddabb371fd7087154a39e850397a4bb6f7dbf7d606490300e3f842" }
|
到这一步其实还是单机区块链, 下面我们看看作者又封装的一些简单的API和方法, 来实现简单的多节点通信和共识
节点注册
针对当前节点, 有其他节点连接过来, 通过一些协议互相确认, 就是一个节点注册过程, 下面的代码把地址加入到nodes集合中, 对端节点就算是已经注册进来了
1 2 3 4 5 6 7 8 9
| def register_node(self, address):
parsed_url = urlparse(address) if parsed_url.netloc: self.nodes.add(parsed_url.netloc) elif parsed_url.path: self.nodes.add(parsed_url.path) else: raise ValueError('Invalid URL')
|
对应的API实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @app.route('/nodes/register', methods=['POST']) def register_nodes(): values = request.get_json()
nodes = values.get('nodes') if nodes is None: return "Error: Please supply a valid list of nodes", 400
for node in nodes: blockchain.register_node(node)
response = { 'message': 'New nodes have been added', 'total_nodes': list(blockchain.nodes), } return jsonify(response), 201
|
请求效果:
1 2 3 4 5 6 7 8 9 10
| ~$ curl -H "Content-Type:application/json" http://127.0.0.1:5000/nodes/register -d ' {"nodes":["http://127.0.0.1:5001"]}'
{ "message": "New nodes have been added", "total_nodes": [ "127.0.0.1:5001" ] }
|
解决共识问题
现在我们已经两个节点了, 并且节点2 已经注册到节点1, 这时如果节点2成功打包了几个区块, 那么节点1如何同节点2同步呢?
这段代码通过简单的长度判断和验证逻辑, 做到了各个节点间的共识
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| def resolve_conflicts(self):
neighbours = self.nodes new_chain = None
max_length = len(self.chain) for node in neighbours: response = requests.get(f'http://{node}/chain')
if response.status_code == 200: length = response.json()['length'] chain = response.json()['chain'] if length > max_length and self.valid_chain(chain): max_length = length new_chain = chain
if new_chain: self.chain = new_chain return True
return False
|
对应API
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @app.route('/nodes/resolve', methods=['GET']) def consensus(): replaced = blockchain.resolve_conflicts()
if replaced: response = { 'message': 'Our chain was replaced', 'new_chain': blockchain.chain } else: response = { 'message': 'Our chain is authoritative', 'chain': blockchain.chain }
return jsonify(response), 200
|
我们来一起看下效果:
上图中节点2进行一次挖矿后一共两个区块, 节点一进行同步后用节点2的链替换掉自己的链, 这样两个节点链上数据就一致了
当然比特币这种成熟系统这些动作都是自动做的, 这里都是我们手动请求触发, 不过这样对于学习了解这些概念也更清晰, 虽然代码中的各种实现都很简陋, 但贵在简单, 不到300行代码就实现了这个可运行的小系统, 对初学者的学习价值还是很高的
汤哥由于工作繁忙, 更新频率不定, 请大家谅解, 你们的关注是我的动力 :)
如果觉得有所收获, 不要说话, 请用币砸我 :)
狗狗币地址:
DDTVkcxGWBbtMp6ArPYmXRvRTryVRQVn3U
ERC20代币地址:
0x807ed647c8572C368976497EA4cb3eA812533b53