本篇教程是面向各能力层级的 Python 开发者介绍区块链(blockchain)。你将通过从零实现一个公链(public blockchain)和构建一个简单的应用来确切理解区块链到底是什么。
你将会使用 Flask 轻量级框架为区块链的不同功能创建端点,然后在多个机器上运行代码来创建一个去中心化网络(decentralized network)。同时你还将构建一个能与区块链相交互的简单的用户界面并存储任何种信息,好比点对点支付、聊天或是电子商务。
Python 是一种易懂的编程语言,这也是我在这选择它的原因。一个完整的纯 Python 编写的简单应用代码,你都可以在 Github 上看到。
首先要知道Python 编程的基础;
了解 REST-APIs;
熟悉 Flask(非强制的,熟悉最好)。
背景
2008 年一本名为 Bitcoin: A Peer-to-Peer Electronic Cash System 的白皮书由一个叫做 Satoshi Nakamoto 的个人(或是团体)发布。其中结合了多种的加密技术和点对点网络,将支付变得无需中心权威(就像银行)参与,名为比特币的加密货币也诞生了。除了比特币,其中也同样介绍了一个用于存储数据的分布式系统(现在最为熟知的就是区块链),它的适用性要远比支付与加密货币大得多。
从那时起,区块链几乎引起了各个行业的兴趣。区块链现在也是数字加密货币像比特币、分布式计算技术像以太坊(Ethereum)还有一些开源框架像 Hyperledger Fabric 及架构在其上的 IBM 区块链平台的底层技术。
什么是区块链 blockchain?
区块链是一种存储数字数据(digital data)的方式。数据可以是任何东西,对于比特币来说,数据就是交易(用户间比特币的转移记录),数据也可以是文件;没什么。数据以区块的形式被存储,并且被以加密哈希的方式连接(或链接)起来 —— 因此得名区块链。
所有的魔力都在于数据的存储与添加到区块链上的方式。区块链本质上是有序数据的链表,也有下面几个约束:区块一旦添加便无法修改,换句话说区块只能被添加;
数据的添加有着明确的规则;
其架构是分布式的。
这些约束的强制实施又会产生下面的好处 :数据的不可变性和持久性;
无单点控制或故障;
数据添加顺序的可验证审计追踪(A verifiable audit trail of the order in which data was added)。
那么那些约束又是如何实现这些特性的呢?你将在实现区块链的过程中学到更多,让我们开始吧。
关于这个应用
让我们先来简单定义一下这个迷你应用的范围。我们的目标是构建一个允许用户发布共享信息的应用,因此,内容将会被存储在区块链中同时也是持久存在的。用户可以通过一个简单的 web 界面来交互 。将交易(transaction)存储在区块中;
为区块添加数字指纹;
链接区块;
实现工作量证明的算法;
将区块添加到链;
创建界面;
建立共识与去中心化;
编译应用;
运行应用。
我们将使用自下而上的方法来实现,来从定义区块链中的数据存储结构开始吧。一个 post 就是由我们应用中的任何用户发布的一个信息,post 将包含下面的三个必要元素:内容 content
发布者 author
时间戳 timestamp
1. 将交易(transaction)存储在区块中
我们将以 JSON 这种广泛使用的格式来将数据存储在我们的区块链中,下面是一个 post 存储在区块链中的链子:
{
"author": "some_author_name",
"content": "Some thoughts that author wants to share",
"timestamp": "The time at which the content was created"
}
通用术语 ”数据(data)“ 在网上经常被术语 “事务(transaction)” 所替代,所以为了避免困惑并保持一致性,我们将开始在事例程序中使用术语事务来指代数据。
事务被打包在区块当中,一个区块可以包含一个或多个事务。包含事务的区块会频繁的生成并被添加到区块链上,因为有着多个区块所以每个块都有着唯一的 ID:
class Block:
def __init__(self, index, transactions, timestamp):
"""Constructor for the `Block` class.:param index: Unique ID of the block.:param transactions: List of transactions.:param timestamp: Time of generation of the block."""
self.index = index
self.transactions = transactions
self.timestamp = timestamp
2. 为区块添加数字指纹
我们想要防止存储在区块中的数据遭到各种篡改,而探测是其第一步。你可以使用加密哈希函数 来探测区块中的数据是否遭到了篡改。
哈希函数会从输入的任意长度数据中生产出固定大小的数据(哈希),这通常用于标识输入。理想的哈希函数特征是 :易于计算;
结果确定,这意味着同一个数据总是产生同一哈希结果;
均匀随机,这意味着即使一个比特的改变都会显著改变原始数据的哈希。
其结果是:几乎不可能从给定的哈希猜测出输入数据(唯一的方法是尝试全部的可能输入组合);
如果你同时知道输入与哈希,那你可以轻易地通过哈希函数来验证其哈希。
这为不对称所做出的努力 —— 要求轻易地从输入数据中获得哈希与几乎不可能从哈希中得出输入数据,正是区块链想要获得的特性。
哈希函数有很多种,这里 Python 的例子中使用的是 SHA-256 哈希函数 :
>>> from hashlib import sha256
>>> data = b"Some variable length data"
>>> sha256(data).hexdigest()
'b919fbbcae38e2bdaebb6c04ed4098e5c70563d2dc51e085f784c058ff208516'
>>> sha256(data).hexdigest() # no matter how many times you run it, the result is going to be the same 256 character string
'b919fbbcae38e2bdaebb6c04ed4098e5c70563d2dc51e085f784c058ff208516'
>>> data = b"Some variable length data2" # Added one character at the end.
'9fcaab521baf8e83f07512a7de7a0f567f6eef2688e8b9490694ada0a3ddeec8'
# Note that the hash has changed entirely!
我们将区块的哈希存储在我们的 Block 对象内部的字段中,这就像是其中包含的数据的数字指纹(或签名)。
from hashlib import sha256
import json
def compute_hash(block):
"""Returns the hash of the block instance by first converting itinto JSON string."""
block_string = json.dumps(self.__dict__, sort_keys=True)
return sha256(block_string.encode()).hexdigest()
注意 :在多数加密货币中,甚至是区块中独立的事务都经过了哈希处理并被存储为哈系树的形式(也称为 merkle tree),其树根通常是区块的哈希。这不是区块链的必要功能,为保持简单而被我们忽略了。
3. 链接区块
我们已经建立了区块,区块链应该是区块的集合。我们可以通过 Python 的列表来存储所有的区块,但这还是不够的,因为如果有人故意用新块替换了集合中的旧区块该怎么办?对于上述的实现方式来说,创建一个修改过事务的区块并计算其哈希,然后替换掉一个旧区块并不是件难事。
我们需要一种任何对已有区块的改变都会使整条链失效的方法,比特币的方法是通过某区块与其紧之前区块的哈希来链接彼此,这样便在连续的区块中创建了一种依赖。这里说到的链接,是要在当前区块的 previous_hash 字段中包含其紧之前区块的哈希。
好了,如果每个区块都通过在 previous_hash 字段中包含前一个区块的哈希来链接,那第一个块该怎么办?第一个区块被称为创世区块(genesis block)它可以手工或通过独特的逻辑来生成。我们来为 Block 类加上 previous_hash 字段并且实现 Blockchain 类的初始结构。
from hashlib import sha256
import json
import time
class Block:
def__init__(self, index, transactions, timestamp, previous_hash):
"""Constructor for the `Block` class.:param index: Unique ID of the block.:param transactions: List of transactions.:param timestamp: Time of generation of the block.:param previous_hash: Hash of the previous block in the chain which this block is part of."""
self.index = index
self.transactions = transactions
self.timestamp = timestamp
self.previous_hash = previous_hash # Adding the previous hash field
def compute_hash(self):
"""Returns the hash of the block instance by first converting itinto JSON string."""
block_string = json.dumps(self.__dict__, sort_keys=True)
# The string equivalent also considers the previous_hash field now
return sha256(block_string.encode()).hexdigest()
class Blockchain:
def __init__(self):
"""Constructor for the `Blockchain` class."""
self.chain = []
self.create_genesis_block()
def create_genesis_block(self):
"""A function to generate genesis block and appends it tothe chain. The block has index 0, previous_hash as 0, anda valid hash."""
genesis_block = Block(0, [], time.time(), "0")
genesis_block.hash = genesis_block.compute_hash()
self.chain.append(genesis_block)
@property
def last_block(self):
"""A quick pythonic way to retrieve the most recent block in the chain. Note thatthe chain will always consist of at least one block (i.e., genesis block)"""
return self.chain[-1]
现在,如果之前任何区块中的内容有改变:先前区块的哈希将会发生改变;
这将会导致其后一块中的 previous_hash 字段不匹配;
由于对输入数据的哈希计算中包含着 previous_hash 字段,对下一区块计算的哈希又会随之改变。
最终,被替换过区块后的整条链都将失效,而唯一的修复办法是重新计算整条链。
4. 实现工作量证明的算法
还有一个问题,如果我们修改了先前区块,之后全部区块的哈希都可以被轻易的重新计算出来,以便生成一个不同的有效链。为避免其发生,我们可以利用之前讨论过的哈希函数的不对称性来使计算哈希这个任务变得困难与随机。我们可以这样做:不去接受区块的任何哈希,而是为它添加一些限制。限制是:其哈希应该由 ”n 个前导 0“ 开始,n 可以是任意正整数。
我们知道除非我们改变区块的数据,否则哈希是不会变得,当然我们也不想改变已经存在的数据。那我们该怎么办?很简单!我们来添加一些可以改变的虚假数据。在我们的 block 类中添加一个新的字段 nonce。nonce 是一个我们可以不断修改的数字,直至得到满足约束的哈希。约束条件的满足起到了对某些算力已经执行了的证明作用,这种技巧其实是比特币中使用的 Hashcash 算法的简化版。约束中被指定的零的个数决定了我们工作量证明算法的难度(零的个数越多就越难计算。)
还有,由于不对称性,工作量证明是很难计算的可一旦你算出了 nonce 便会很容易验证(你只需要再运行一次哈希函数即可。):
class Blockchain:
# difficulty of PoW algorithm
difficulty = 2
"""Previous code contd.."""
def proof_of_work(self, block):
"""Function that tries different values of the nonce to get a hashthat satisfies our difficulty criteria."""
block.nonce = 0
computed_hash = block.compute_hash()
while not computed_hash.startswith('0' * Blockchain.difficulty):
block.nonce += 1
computed_hash = block.compute_hash()
return computed_hash
注意,其实是没有快速计算 nonce 的逻辑,仅仅只有强力计算,唯一明确的提升是你可以使用专门被设计来计算哈希函数的有较少 CPU 指令的硬件芯片。
5. 将区块添加到链
为了将区块添加到链中,我们将首先验证如下:数据未经篡改(工作量证明是正确的);
事务的顺序被保留(待添加区块的 previous_hash 字段指向链中最新区块的哈希)。
下面是将区块添加到链的代码:
class Blockchain:
"""Previous code contd.."""
def add_block(self, block, proof):
"""A function that adds the block to the chain after verification.Verification includes:* Checking if the proof is valid.* The previous_hash referred in the block and the hash ofa latest block in the chain match."""
previous_hash = self.last_block.hash
if previous_hash != block.previous_hash:
return False
if not Blockchain.is_valid_proof(block, proof):
return False
block.hash = proof
self.chain.append(block)
return True
def is_valid_proof(self, block, block_hash):
"""Check if block_hash is valid hash of block and satisfiesthe difficulty criteria."""
return (block_hash.startswith('0' * Blockchain.difficulty) and
block_hash == block.compute_hash())
挖矿
事务最开始会存储为未经确认的事务池,将未经确认的事务放入区块中和计算工作量的过程被熟知为挖矿(mining)。一旦有 nonce 满足了我们的约束,我们便可说是有一个区块被挖出了并且它可以被加到区块链上了。
在大多数的加密货币中(包括比特币),矿工都可以获得一定量的加密货币的奖赏,以作为他们花费算力计算工作量证明的报酬。我们的挖矿函数看起来是这样的:
class Blockchain:
def __init__(self):
self.unconfirmed_transactions = []
# data yet to get into blockchain
self.chain = []
self.create_genesis_block()
"""Previous code contd..."""
def add_new_transaction(self, transaction):
self.unconfirmed_transactions.append(transaction)
def mine(self):
"""This function serves as an interface to add the pendingtransactions to the blockchain by adding them to the blockand figuring out proof of work."""
if not self.unconfirmed_transactions:
return False
last_block = self.last_block
new_block = Block(index=last_block.index + 1,
transactions=self.unconfirmed_transactions,
timestamp=time.time(),
previous_hash=last_block.hash)
proof = self.proof_of_work(new_block)
self.add_block(new_block, proof)
self.unconfirmed_transactions = []
return new_block.index
6. 创建界面
现在我们该来为我们的区块链节点创建界面以与我们的应用相交互,我们将用一个 Python 流行的框架 Flask 创建 REST API 来实现对区块链节点的交互与调用各种函数。如果你之前曾了解过任何一种 web 框架,那么看懂下面的代码对你来说都不是难事。
from flask import Flask, request
import requests
# Initialize flask application
app = Flask(__name__)
# Initialize a blockchain object.
blockchain = Blockchain()
我们需要一个端点来为应用提交事务,这也被我们的应用程序向区块链添加新数据(post)所 需要:
# Flask's way of declaring end-points
@app.route('/new_transaction', methods=['POST'])
def new_transaction():
tx_data = request.get_json()
required_fields = ["author", "content"]
for field in required_fields:
if not tx_data.get(field):
return "Invalid transaction data", 404
tx_data["timestamp"] = time.time()
blockchain.add_new_transaction(tx_data)
return "Success", 201
下面的端点用于返回链中节点的副本,我们的应用也是需要此端点来显示所有要查询的数据:
@app.route('/chain', methods=['GET'])
def get_chain():
chain_data = []
for block in blockchain.chain:
chain_data.append(block.__dict__)
return json.dumps({"length": len(chain_data),
"chain": chain_data})
下面的端点用于请求节点来挖掘未确认的事务(如果有的话),我们将用它初始化一个命令用来在我们的应用中进行挖矿:
@app.route('/mine', methods=['GET'])
def mine_unconfirmed_transactions():
result = blockchain.mine()
if not result:
return "No transactions to mine"
return "Block #{} is mined.".format(result)
@app.route('/pending_tx')
def get_pending_tx():
return json.dumps(blockchain.unconfirmed_transactions)
这些 REST 风格的端点将在我们的区块链上用来创建事务与挖矿。
7. 建立共识与去中心化
目前为止,我们所创建的区块链都是运行在单个计算机上的。即使我们链接起来了区块并且都应用了工作量证明约束,但我们还是不能信任单个节点(这里指的就是单个计算机)。我们需要数据是分布式的,我们需要多个节点来维护区块链。所以从单节点到点对点网络(peer-to-peer network)转换的第一步是,创建一种能使新加入的节点意识到网络中其他节点的机制。
# Contains the host addresses of other participating members of the network
peers = set()
# Endpoint to add new peers to the network
@app.route('/register_node', methods=['POST'])
def register_new_peers():
# The host address to the peer node
node_address = request.get_json()["node_address"]
if not node_address:
return "Invalid data", 400
# Add the node to the peer list
peers.add(node_address)
# Return the blockchain to the newly registered node so that it can sync
return get_chain()
@app.route('/register_with', methods=['POST'])
def register_with_existing_node():
"""Internally calls the `register_node` endpoint toregister current node with the remote node specified in therequest, and sync the blockchain as well with the remote node."""
node_address = request.get_json()["node_address"]
if not node_address:
return "Invalid data", 400
data = {"node_address": request.host_url}
headers = {'Content-Type': "application/json"}
# Make a request to register with remote node and obtain information
response = requests.post(node_address + "/register_node",
data=json.dumps(data), headers=headers)
if response.status_code == 200:
global blockchain
global peers
# update chain and the peers
chain_dump = response.json()['chain']
blockchain = create_chain_from_dump(chain_dump)
peers.update(response.json()['peers'])
return "Registration successful", 200
else:
# if something goes wrong, pass it on to the API response
return response.content, response.status_code
def create_chain_from_dump(chain_dump):
blockchain = Blockchain()
for idx, block_data in enumerate(chain_dump):
block = Block(block_data["index"],
block_data["transactions"],
block_data["timestamp"],
block_data["previous_hash"])
proof = block_data['hash']
if idx > 0:
added = blockchain.add_block(block, proof)
if not added:
raise Exception("The chain dump is tampered!!")
else: # the block is a genesis block, no verification needed
blockchain.chain.append(block)
return blockchain
一个新的节点加入到网络之中将会调用 register_with_existing_node() 方法(通过 /register_with 端点)来注册成为网络中的现存节点。这将有助于以下几点:要求远程节点将新的点(peer)添加到已知列表中;
用远程节点上的区块链初始化新节点上的区块链;
如果节点脱离网络,重新连接后便会重新同步。
然而多节点当中还有一个问题,由于一些蓄意操作或是意外(如网络延迟)将导致某些节点对链的拷贝是不同的。这种情况下众多节点需要就选用链的某版本达成一致,以维持整个系统的完整性。换句话说,我们需要实现共识(consensus)。
当网络中的参与节点对链表现出不同时,一种简单的共识算法是让节点都以最长的合法链为准。这个方法背后的基本原理是,将最长合法链看作是耗费掉大部分工作量的(记得工作量证明是很难计算的):
class Blockchain
"""previous code continued..."""
def check_chain_validity(cls, chain):
"""A helper method to check if the entire blockchain is valid."""
result = True
previous_hash = "0"
# Iterate through every block
for block in chain:
block_hash = block.hash
# remove the hash field to recompute the hash again
# using `compute_hash` method.
delattr(block, "hash")
if not cls.is_valid_proof(block, block.hash) or \
previous_hash != block.previous_hash:
result = False
break
block.hash, previous_hash = block_hash, block_hash
return result
def consensus():
"""Our simple consensus algorithm. If a longer valid chain isfound, our chain is replaced with it."""
global blockchain
longest_chain = None
current_len = len(blockchain.chain)
for node in peers:
response = requests.get('{}/chain'.format(node))
length = response.json()['length']
chain = response.json()['chain']
if length > current_len and blockchain.check_chain_validity(chain):
# Longer valid chain found!
current_len = length
longest_chain = chain
if longest_chain:
blockchain = longest_chain
return True
return False
接下来,我们要开发出当一个节点挖到一个区块时能在网络中进行宣告的方法,以便让其他节点能更新他们的区块链并且转向挖掘新的事务。其他区块将能简单地进行验证工作量证明,并且将挖到的区块添加到其各自的链上(要记得一旦 nonce 被发现了验证就是很简单的事了):
# endpoint to add a block mined by someone else to
# the node's chain. The node first verifies the block
# and then adds it to the chain.
@app.route('/add_block', methods=['POST'])
def verify_and_add_block():
block_data = request.get_json()
block = Block(block_data["index"],
block_data["transactions"],
block_data["timestamp"],
block_data["previous_hash"])
proof = block_data['hash']
added = blockchain.add_block(block, proof)
if not added:
return "The block was discarded by the node", 400
return "Block added to the chain", 201
def announce_new_block(block):
"""A function to announce to the network once a block has been mined.Other blocks can simply verify the proof of work and add it to theirrespective chains."""
for peer in peers:
url = "{}add_block".format(peer)
requests.post(url, data=json.dumps(block.__dict__, sort_keys=True))
announce_new_block() 方法应该在每一个区块被挖掘出后被调用,以便于节点能把它添加到链上。
@app.route('/mine', methods=['GET'])
def mine_unconfirmed_transactions():
result = blockchain.mine()
if not result:
return "No transactions to mine"
else:
# Making sure we have the longest chain before announcing to the network
chain_length = len(blockchain.chain)
consensus()
if chain_length == len(blockchain.chain):
# announce the recently mined block to the network
announce_new_block(blockchain.last_block)
return "Block #{} is mined.".format(blockchain.last_block.index
8. 编译应用
现在是时候处理我们应用程序的界面了,我们使用了 Jinja2 模板来渲染网页还有一些 CSS 都让它看起来更票了一些。
我们的应用需要连接到网路中的一个节点来获取数据与提交数据,链接多个节点也是可以的。
# Node in the blockchain network that our application will communicate with
# to fetch and add data.
CONNECTED_NODE_ADDRESS = "http://127.0.0.1:8000"
posts = []
fetch_posts() 函数将从节点的 /chain 端点获取数据、解析数据并存储在本地。The fetch_posts function gets the data from the node’s /chain endpoint, parses the data, and stores it locally.
def fetch_posts():
"""Function to fetch the chain from a blockchain node, parse thedata, and store it locally."""
get_chain_address = "{}/chain".format(CONNECTED_NODE_ADDRESS)
response = requests.get(get_chain_address)
if response.status_code == 200:
content = []
chain = json.loads(response.content)
for block in chain["chain"]:
for tx in block["transactions"]:
tx["index"] = block["index"]
tx["hash"] = block["previous_hash"]
content.append(tx)
global posts
posts = sorted(content,
key=lambda k: k['timestamp'],
reverse=True)
我们的应用有一个 HTML 表格来处理用户输入,并且之后会向连接的节点发出一个 POST 请求,来向未验证的事务池中添加一个事务。事务接着会在网络中被挖掘,最终一旦我们刷新页面便会获取到数据:
@app.route('/submit', methods=['POST'])
def submit_textarea():
"""Endpoint to create a new transaction via our application"""
post_content = request.form["content"]
author = request.form["author"]
post_object = {
'author': author,
'content': post_content,
}
# Submit a transaction
new_tx_address = "{}/new_transaction".format(CONNECTED_NODE_ADDRESS)
requests.post(new_tx_address,
json=post_object,
headers={'Content-type': 'application/json'})
# Return to the homepage
return redirect('/')
9. 运行应用
克隆下来:
git clone https://github.com/satwikkansal/python_blockchain_app.git
安装依赖:
cd python_blockchain_app
pip install -r requirements.txt
启动一个区块链节点服务:
export FLASK_APP=node_server.py
flask run --port 8000
我们的一个区块链节点的实例已经启用来了,并且运行在 8000 端口上。
在另一个终端会话上运行我们的应用:
python run_app.py
10. 多节点运行
你若想通过分离多个自定义节点进行操作,就要用 register_with/ 端点来在现存的对等网络中注册一个新节点。
这或许是一个你想尝试的简单方案:
# already running
# flask run --port 8000 &
# spinning up new nodes
flask run --port 8001 &
flask run --port 8002 &
你可以使用下面的 curl 请求来在已经运行的 8000 上注册 8001 和 8002 的节点:
curl -X POST \ http://127.0.0.1:8001/register_with \ -H 'Content-Type: application/json' \ -d '{"node_address": "http://127.0.0.1:8000"}'
curl -X POST \ http://127.0.0.1:8002/register_with \ -H 'Content-Type: application/json' \ -d '{"node_address": "http://127.0.0.1:8000"}'
这将会让 8000 端口上的节点了解到 8001 和 8002 端口上的节点,反之亦然。新节点也会跟链中的现存节点做同步,以便他们能积极参与到挖矿中。
要变更与我们应用前台同步的节点(默认的是 8000 端口),只需要改变 views.py 文件中的 CONNECTED_NODE_ADDRESS 项即可。
一旦你都做了上面的步骤,你就可以运行应用(python run_app.py)并创建事务了(在 web 界面进行 post),接着当你挖到事务时网络中的所有节点都将更新链。每个节点的链也能通过 curl 调用 /chain 端点进行检查。
curl -X GET http://localhost:8001/chain
curl -X GET http://localhost:8002/chain
11. 事务验证
你可能已经发现了应用中的一个不足:任何人都可以用任何名字发布任何内容。还有,在向区块链网络提交事务的时候发布易受干扰。一种解决办法是为用户创建公钥私钥加密算法,每个新用户都需要一个公钥和一个私钥才能在我们的应用内进行发布。密钥是用来创建和验证数字前面的,以下是其工作原理:每一个事务的提交都会被用户私钥签名,该签名会作为用户信息的一部分附加到事务的数据当中;
在个验证阶段,当挖掘事务时我们可以验证所说的拥有者是否与事务数据中指定的拥有者相同,以及信息是否被篡改过。这可以通过发布者的签名以及他的公钥完成。
结论
这篇教程覆盖到了公有区块链的基础只是,如果你一路跟了下来,你应该有能力从零实现一个区块链并且构建一个允许用户在其上分享信息的简单应用了。这个实现不像是其他共有区块链像是比特币或以太坊那样精致 —— 但如果你能对你的每个需求问对问题的话,你终将成功的。你得知道在设计一个能满足你需求的区块链中最关键的一事,是融会贯通计算机科学当中的现有知识,就这一件。