Home > 应用 > 程序员们,只需三步,教你搭建一个区块链程序
应用

程序员们,只需三步,教你搭建一个区块链程序

区块链,大家或许都不陌生,或多或少都对它有一些了解。不过,这些了解可能都是支离破碎的。当问及其中一些概念是如何实现的,你可能就「蒙圈」了。那想了解其中的实现细节怎么办呢?

这篇文章可以手把手的教你搭建一个区块链程序,让你从技术层面,详细了解区块链的实现细节。我相信你把这篇文章的代码跑一遍,你会有一种「哦!原来是这么实现的」想法。下面,我们就来试试吧。

作者 | Daniel van Flymen

译者 | Aholiab、科科

你会点开这篇文章,说明你跟我一样,眼看加密货币价格的暴涨,很想弄清楚区块链到底是什么?背后有哪些技术。

不过,要完全理解区块链不是件容易的事,起码对我而言。为了搞懂区块链,我看过大量的视频、研究过各种教程和为数不多的几个案例,整个过程虐心之极。

所以,我决定在实践中学习。在实践中学习的一大好处是它能逼着你去理解区块链最底层的原理,并且容易让人坚持下去。如果你也想试试这个方法的话,建议你好好读完这篇文章,跟着步骤一步步地去操作。这样,你不仅可以亲自开发出一个功能完备的区块链,同时也搞清楚了区块链的机制到底是什么?

准备工作

在开始之前,我们需要做些准备工作,搞清楚一些问题。

什么是区块链?区块链是由不可变的、有顺序记录的区块组成。他们可以包含交易数据、文件数据或者其他你想要记录的数据。不过最重要的是这些区块通过哈希表链接在一起。

什么是散列?散列函数是一个输入值函数,从该输入创建一个确定输入值的输出值。更多解释可以点击下边这个链接:

https://learncryptography.com/hash-functions/what-are-hash-functions

这篇文章适合谁,首先Python程序员,你只要能轻松地读写一些基本的Python代码就可以了;第二是HTTP程序员,因为我们接下来讲到的区块链,是构建在HTTP上面的,这需要你起码要了解HTTP请求的工作原理。

我需要做什么?首先要确保安装了Python 3.6以上的环境和Flask,此外还需要安装一个碉堡的Requests库。版本信息如下:

 pip install Flask==0.12.2 requests==2.18.4

哦对了,你还需要一个HTTP客户端,比如 Postman 或者 cURL

在哪里下载完整代码,请猛戳:

https://github.com/dvf/blockchain。

下面跟着我一步一步来操作吧。

第一步:创建区块链

打开你常用的编辑器,我个人比较喜欢PyCharm。创建一个新的文件,命名为 blockchain.py。整个项目,我们都只会用到这一个文件。有不清楚的地方,可以参考源代码。

表示一个区块链

我们将创建一个 Blockchain 类,它的构造函数里创建了一个初始为空的列表(用于存储我们的区块链),和一个存储交易的列表。下边是这个类的代码:

class Blockchain(object):
 def __init__(self):
 self.chain = []
 self.current_transactions = []
 def new_block(self):
 # Creates a new Block and adds it to the chain
 pass
 def new_transaction(self):
 # Adds a new transaction to the list of transactions
 pass
 @staticmethod
 def hash(block):
 # Hashes a Block
 pass
 @property
 def last_block(self):
 # Returns the last Block in the chain
 pass

Blockchain参数的作用是管理区块链,也用于存储交易信息和添加区块的方式。

区块到底长什么样?

每一个区块包含一个索引、一个时间戳、一个交易列表、一个证明(之后更多)和前一个区块的哈希值。

以下是一个区块的例子:

block = {
 'index': 1,
 'timestamp': 1506057125.900785,
 'transactions': [
 {
 'sender': "8527147fe1f5426f9dd545de4b27ee00",
 'recipient': "a77f5cdfa2934df3954a5c7c7da5df1f",
 'amount': 5,
 }
 ],
 'proof': 324984774000,
 'previous_hash': "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
}

代码左滑可查看未显示部分(下同)

到这里,区块链的原理就很容易理解了:每一个区块包含它自己本身的一些变量,以及前一个区块的哈希值。这一点非常重要,因为哈希值保证了区块链不可篡改的特性。如果一个区块受到攻击哈希值变了,那么后面的所有区块的哈希值都会为之改变。

你可能想,我还是不太理解。没关系,先接着往下看。

在区块上添加交易

那么,我们怎么在区块上添加交易呢?可以使用new_transaction()参数。使用方法简单、直接,如下面代码所示:

class Blockchain(object):
 ...
 def new_transaction(self, sender, recipient, amount):
 """
 Creates a new transaction to go into the next mined Block
 :param sender: <str> Address of the Sender
 :param recipient: <str> Address of the Recipient
 :param amount: <int> Amount
 :return: <int> The index of the Block that will hold this transaction
 """
 self.current_transactions.append({
 'sender': sender,
 'recipient': recipient,
 'amount': amount,
 })
 return self.last_block['index'] + 1

在 new_transaction() 添加交易信息到列表中后,它会返回下一个将被开采区块的索引号,交易信息将被打包到这个区块上。这对稍后提交交易的用户有用。

创建新区块

在区块链创建完成后,我们需要创建一个创世区块(也就是区块链上的第一个区块)。当然,创世区块也需要被证明,这需要通过PoW的挖矿机制。后边我们会更多的介绍挖矿,这儿就不做过多的介绍了。

除了在构造函数中创建创始区块,我们还需要用new_block()、new_transaction() 和 hash()参数对其进行完善。代码如下:

import hashlib
import json
from time import time
class Blockchain(object):
 def __init__(self):
 self.current_transactions = []
 self.chain = []
 # Create the genesis block
 self.new_block(previous_hash=1, proof=100)
 def new_block(self, proof, previous_hash=None):
 """
 Create a new Block in the Blockchain
 :param proof: <int> The proof given by the Proof of Work algorithm
 :param previous_hash: (Optional) <str> Hash of previous Block
 :return: <dict> New Block
 """
 block = {
 'index': len(self.chain) + 1,
 'timestamp': time(),
 'transactions': self.current_transactions,
 'proof': proof,
 'previous_hash': previous_hash or self.hash(self.chain[-1]),
 }
 # Reset the current list of transactions
 self.current_transactions = []
 self.chain.append(block)
 return block
 def new_transaction(self, sender, recipient, amount):
 """
 Creates a new transaction to go into the next mined Block
 :param sender: <str> Address of the Sender
 :param recipient: <str> Address of the Recipient
 :param amount: <int> Amount
 :return: <int> The index of the Block that will hold this transaction
 """
 self.current_transactions.append({
 'sender': sender,
 'recipient': recipient,
 'amount': amount,
 })
 return self.last_block['index'] + 1
 @property
 def last_block(self):
 return self.chain[-1]
 @staticmethod
 def hash(block):
 """
 Creates a SHA-256 hash of a Block
 :param block: <dict> Block
 :return: <str>
 """
 # We must make sure that the Dictionary is Ordered, or we'll have inconsistent hashes
 block_string = json.dumps(block, sort_keys=True).encode()
 return hashlib.sha256(block_string).hexdigest()

为了方便大家理解,我在上面代码中加了一些注释。到这里,我们就已经对区块链属性有一个全面的了解了。不过我们还是要知道,区块链是怎么创建、怎么开发,以及跟矿工有什么关系。

关于工作量证明(PoW)

工作证明算法(PoW)的作用,是对区块链上创建或开发新的区块的证明。其背后的核心是:找到一串解决某个数学问题的数字,这个数字必须符合两个条件:第一,难找;第二,很容易被验证(而且是很容易被任何人验证)。

我们来举个非常简单的例子来帮助大家理解。

我们来看一下这个例子,某个整数 x 乘以另外一个数 y ,得到的结果的哈希值必须是以 0 结尾。可以简单表示为:hash(x * y) = ac23dc…0。所以,我们的目标是找到满足这个条件的一个 y 值。为了方便理解,我们暂定x=5。下面我们就用Python来做这样一个运算:

from hashlib import sha256
x = 5
y = 0 # We don't know what y should be yet...
while sha256(f'{x*y}'.encode()).hexdigest()[-1] != "0":
 y += 1
print(f'The solution is y = {y}')

最终,计算结果是 y=21。因此,生成的以 0 结尾的哈希值是:

hash(5 * 21) = 1253e9373e...5e3600155e860

在比特币中,PoW算法被称为Hashcash,原理跟上面例子差不多。矿工们为了能创建一个新区块,铆足劲儿做着上面的数学题(只有胜出者才能添加区块)。一般而言,证明的难度取决于字符串中搜索的字符数量,先找到正确数字的旷工就能够在每笔交易中获得比特币作为奖励。

系统能够很容易验证他们的解决方案。

实现基本的工作量证明

下面在我们刚刚创建好的区块链上,来实现一个相似的工作量证明算法。规则与上边那个简单的例子相似:

找到一个数字 p ,它和前边一个区块的解决数字进行散列,生成前4位为 0 的哈希值。

下面是具体的Python代码实现:

import hashlib
import json
from time import time
from uuid import uuid4
class Blockchain(object):
 ...
 def proof_of_work(self, last_proof):
 """
 Simple Proof of Work Algorithm:
 - Find a number p' such that hash(pp') contains leading 4 zeroes, where p is the previous p'
 - p is the previous proof, and p' is the new proof
 :param last_proof: <int>
 :return: <int>
 """
 proof = 0
 while self.valid_proof(last_proof, proof) is False:
 proof += 1
 return proof
 @staticmethod
 def valid_proof(last_proof, proof):
 """
 Validates the Proof: Does hash(last_proof, proof) contain 4 leading zeroes?
 :param last_proof: <int> Previous Proof
 :param proof: <int> Current Proof
 :return: <bool> True if correct, False if not.
 """
 guess = f'{last_proof}{proof}'.encode()
 guess_hash = hashlib.sha256(guess).hexdigest()
 return guess_hash[:4] == "0000"

我们可以通过修改哈希值前 0 的数量,来调整算法的难度,一般来说,4位已经是足够了。每在哈希值前多加一个0,计算所花费的时间将呈指数倍增加。

到这儿,我们的类基本写好了。下面,我们准备通过 HTTP 请求与其交互。

第二步:创建 API

我们打算使用 Python 的 Flask 框架,它是一个轻型框架,可以很容易实现端点到Python函数的映射。这样,我们就可以使用 HTTP 请求通过网页访问我们的区块链了。

我们用以下三个方法创建:

  • /transactions/new 为一个区块创建一个新的交易;
  • /mine 告诉我们的服务器开采一个新的区块;
  • /chain 返回完整的 Blockchain 类。

搭建 Flask 框架

我们的服务器会在区块链网络中形成单个节点。下面来创建一些样板代码:

import hashlib
import json
from textwrap import dedent
from time import time
from uuid import uuid4
from flask import Flask
class Blockchain(object):
 ...
# Instantiate our Node
app = Flask(__name__)
# Generate a globally unique address for this node
node_identifier = str(uuid4()).replace('-', '')
# Instantiate the Blockchain
blockchain = Blockchain()
@app.route('/mine', methods=['GET'])
def mine():
 return "We'll mine a new Block"
@app.route('/transactions/new', methods=['POST'])
def new_transaction():
 return "We'll add a new transaction"
@app.route('/chain', methods=['GET'])
def full_chain():
 response = {
 'chain': blockchain.chain,
 'length': len(blockchain.chain),
 }
 return jsonify(response), 200
if __name__ == '__main__':
 app.run(host='0.0.0.0', port=5000)

我们来简单的解释一下上边的代码:

  • 第15行: 实例化我们的节点;加载 Flask 框架。
  • 第18行:为我们的节点创建一个随机名称。
  • 第21行:实例化 Blockchain 类。
  • 第24-26行:创建 /mine 端点,这是一个GET请求。
  • 第28-30行:创建 /transactions/new 端点,这是一个 POST 请求,我们将用它来发送数据。
  • 第32-38行:创建 /chain 端点,它是用来返回整个 Blockchain 类。
  • 第40-41行:设置服务器运行端口为 5000。

交易节点

交易的请求是什么形式呢?下面我们看看用户发送到服务器的一段请求代码:

{
 "sender": "my address",
 "recipient": "someone else's address",
 "amount": 5
}

因为我们已经写好了将交易打包到区块上的代码,剩下的部分就简单了。只需要调用这个方法,从而实现添加交易的功能。下面是具体代码实现:

import hashlib
import json
from textwrap import dedent
from time import time
from uuid import uuid4
from flask import Flask, jsonify, request
...
@app.route('/transactions/new', methods=['POST'])
def new_transaction():
 values = request.get_json()
 # Check that the required fields are in the POST'ed data
 required = ['sender', 'recipient', 'amount']
 if not all(k in values for k in required):
 return 'Missing values', 400
 # Create a new Transaction
 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. 通过将新块添加到链中来伪造新块。
import hashlib
import json
from time import time
from uuid import uuid4
from flask import Flask, jsonify, request
...
@app.route('/mine', methods=['GET'])
def mine():
 # We run the proof of work algorithm to get the next proof...
 last_block = blockchain.last_block
 last_proof = last_block['proof']
 proof = blockchain.proof_of_work(last_proof)
 # We must receive a reward for finding the proof.
 # The sender is "0" to signify that this node has mined a new coin.
 blockchain.new_transaction(
 sender="0",
 recipient=node_identifier,
 amount=1,
 )
 # Forge the new Block by adding it to the chain
 previous_hash = blockchain.hash(last_block)
 block = blockchain.new_block(proof, previous_hash)
 response = {
 'message': "New Block Forged",
 'index': block['index'],
 'transactions': block['transactions'],
 'proof': block['proof'],
 'previous_hash': block['previous_hash'],
 }
 return jsonify(response), 200

这儿要注意一下,开采区块的接收者是我们节点的地址。在这里完成的大部分工作只是与Blockchain类中的方法进行交互。下面可以开始与我们的区块链交互啦。

第三步:实现与 Blockchain 类交互

你可以使用普通的 cURL 或者 Postman 通过网络和刚才生成的 API 进行交互。

启动服务器:

$ python blockchain.py
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

通过向以下地址发送请求,我们可以尝试一下挖矿。

http://localhost:5000/mine
程序员们,只需三步,教你搭建一个区块链程序

使用 Postman 发送 GET 请求

下面我们通过向下面链接发送post请求,来创建一个新的交易:

http://localhost:5000/transactions/new 。

请求中要包含我们的交易结构。

程序员们,只需三步,教你搭建一个区块链程序

使用 Postman 发送一个 POST 请求

如果你用的是cURL,则可以通过下面代码来实现。

$ curl -X POST -H "Content-Type: application/json" -d '{
 "sender": "d4ee26eee15148ee92c6cd394edd974e",
 "recipient": "someone-other-address",
 "amount": 5
}' "http://localhost:5000/transactions/new"

完成上面步骤之后,需要重启下服务器。这时候,我挖出了2个区块,获得了3个币的奖励。这里,我们还可以像以下地址发送请求,来对整条链进行检查。

{
 "chain": [
 {
 "index": 1,
 "previous_hash": 1,
 "proof": 100,
 "timestamp": 1506280650.770839,
 "transactions": []
 },
 {
 "index": 2,
 "previous_hash": "c099bc...bfb7",
 "proof": 35293,
 "timestamp": 1506280664.717925,
 "transactions": [
 {
 "amount": 1,
 "recipient": "8bbcb347e0634905b0cac7955bae152b",
 "sender": "0"
 }
 ]
 },
 {
 "index": 3,
 "previous_hash": "eff91a...10f2",
 "proof": 35089,
 "timestamp": 1506280666.1086972,
 "transactions": [
 {
 "amount": 1,
 "recipient": "8bbcb347e0634905b0cac7955bae152b",
 "sender": "0"
 }
 ]
 }
 ],
 "length": 3
}

第四步:形成共识

终于写到共识了,共识机制是我认为区块链中最有意思的部分。 在上面的步骤中,我们已经创建完成了一个简单的区块链,并且能够实现交易、挖矿等基本功能。 不过,区块链上的节点应该是分散的。 如果它们是分散的,我们究竟如何确保它们记录的都是同一条链? 这就叫共识问题。如果我们的网络中需要多个节点,我们必须实现共识算法。

注册新节点

在我们实现共识算法之前,需要解决一个问题:在同一个网络上,让其中一个节点知道它的相邻节点有哪些。每一个节点需要网络上的其他节点进行注册。因此,我们将需要更多的节点:

  1. /nodes/register 接受URL形式的新节点列表。
  2. /nodes/resolve 实现我们的共识算法,它可以解决任何争议,保证节点具有正确的链。

下面,我们需要修改Blockchain类的结构,以及找到注册节点实现的方法。

...
from urllib.parse import urlparse
...
class Blockchain(object):
 def __init__(self):
 ...
 self.nodes = set()
 ...
 def register_node(self, address):
 """
 Add a new node to the list of nodes
 :param address: <str> Address of node. Eg. 'http://192.168.0.5:5000'
 :return: None
 """
 parsed_url = urlparse(address)
 self.nodes.add(parsed_url.netloc)

这儿需要注意一下,set()函数用来保存节点列表。 它是确保添加的新节点具有幂等性的方法,意思是无论我们使用这个方法添加特定节点多少次,它都只会出现一次。

实现共识算法

先前提到的,当某个节点与另一个节点的记录不一致时,会「打架」。为了解决这个冲突,我们需要制定一个规则,即最长而有效的链是最有权威性的。换句话说,网络上最长的链就是事实。使用这个算法,我们就可以在我们的网络上达成共识。

...
import requests
class Blockchain(object)
 ...
 def valid_chain(self, chain):
 """
 Determine if a given blockchain is valid
 :param chain: <list> A blockchain
 :return: <bool> True if valid, False if not
 """
 last_block = chain[0]
 current_index = 1
 while current_index < len(chain):
 block = chain[current_index]
 print(f'{last_block}')
 print(f'{block}')
 print("
-----------
")
 # Check that the hash of the block is correct
 if block['previous_hash'] != self.hash(last_block):
 return False
 # Check that the Proof of Work is correct
 if not self.valid_proof(last_block['proof'], block['proof']):
 return False
 last_block = block
 current_index += 1
 return True
 def resolve_conflicts(self):
 """
 This is our Consensus Algorithm, it resolves conflicts
 by replacing our chain with the longest one in the network.
 :return: <bool> True if our chain was replaced, False if not
 """
 neighbours = self.nodes
 new_chain = None
 # We're only looking for chains longer than ours
 max_length = len(self.chain)
 # Grab and verify the chains from all the nodes in our network
 for node in neighbours:
 response = requests.get(f'http://{node}/chain')
 if response.status_code == 200:
 length = response.json()['length']
 chain = response.json()['chain']
 # Check if the length is longer and the chain is valid
 if length > max_length and self.valid_chain(chain):
 max_length = length
 new_chain = chain
 # Replace our chain if we discovered a new, valid chain longer than ours
 if new_chain:
 self.chain = new_chain
 return True
 return False

其中,valid_chain() 方法是负责校验这一条链是否是有效的,怎么校验呢?遍历每一个区块,验证它们的哈希值和工作量证明。

resolve_conflicts() 是遍历我们所有相邻节点的方法,会下载它们的链,然后使用上述方法去验证它们。如果找到一个有效的链,其长度大于我们的链,就将我们的链条替换为该链。

下面我们在API中,注册两个节点,一个用于添加相邻节点,另一个用于解决冲突:

@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
@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

此时,你可以使用不同机器(或使用同一台机器的不同端口)启动不同的节点。 我是使用的同一台机器,在另外一个端口上创建了另一个节点,并将其注册到当前节点。 因此,我有两个节点:http:// localhost:5000 和 http:// localhost:5001。

程序员们,只需三步,教你搭建一个区块链程序

注册一个新的节点

然后,我在第二个节点上挖出了一些新的区块,以确保第二个节点的链条比第一个节点的链条更长。 之后,我在第一个节点上调用 GET / nodes / resolve,使其中链通过共识算法被第二个节点的链条取代:

程序员们,只需三步,教你搭建一个区块链程序

在工作的共识算法

好啦,你已经成功创建好了一个区块链程序,快去叫上你的朋友们来测试一下吧!

http://xzh.i3geek.com
jp6754
ChainDAO管理员,不喜交流,无事退朝!

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据