Yang's Kernel

如果你不能解决一个问题,那么有一个更小的问题你也不能解决,找出那个问题!

0%

用python阐述区块链技术概念

之前在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,
}
# Reset the current list of transactions
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