通过构建一个区块链来学习区块链

你之所以在这里,是因为和我一样,你也对加密货币的兴起感到兴奋。而且你想知道区块链——加密货币背后的基础技术,是如何工作的。

但理解区块链并不容易——至少对我来说是这样。那些密集的视频、漏洞百出的教程以及少的可怜的示例代码,把我的挫折无限放大。

我喜欢在做中学。它能让我聚焦在代码层来处理相关主题。如果你也是这样,通过本文,你将拥有一个能正常工作的区块链。

在开始之前……

请记住,区块链是一个名为区块的不可变的的、链序列的记录。它们可以包含交易、文件或你喜欢的任何数据。但重要的是它们使用哈希链接在一起。

如果你不知道什么是哈希,这里有说明。

本文面向的目标群体是谁?你应该能轻松阅读和编写一些基本的 Python,并且对 HTTP 请求的工作原理有一定了解,因为我们将通过 HTTP 与我们的区块链进行交互。

你需要准备什么?确保已安装了 Python 3.6(包括 pip)。你还需要安装 Flask 和强大的 requests 库:

pip install Flask==0.12.2 requests==2.18.4

是的,你还需要一个 HTTP 客户端,比如 Postman 或 cURL。

最终代码在哪里?本项目的源代码在这里

第1步:构建一个区块链

打开你喜欢的文本编辑器或 IDE,我个人喜欢 PyCharm。创建一个名为 blockchain.py 的文件。

描述区块链

我们将创建一个 Blockchain 类,在其构造函数中创建一个初始的空列表(用来存储我们的区块链),另一个用来存储交易。我们这个类的蓝图如下:

class Blockchain(object):
    def __init__(self):
        self.chain = []
        self.current_transactions = []

    def new_block(self):
        # 创建一个区块,并将其添加到区块链中
        pass

    def new_transaction(self):
        # 将一个新的交易添加到交易列表中
        pass

    @staticmethod
    def hash(block):
        # 计算区块的哈希
        pass

    @property
    def last_block(self):
        # 返回区块链中最后一个区块
        pass

我们的 Blockchain 负责管理整个区块链。它将存储所有交易,并提供一些助手方法来将新区块添加到区块链中。让我们开始填充一些方法。

区块是什么样的?

每个区块都有一个 index(索引)、timestamp(unix 时间戳)、transations(交易列表)、proof(证明)和 previous_hash (上一区块的哈希值)。

单个区块看起来像这样:

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):
        """
        创建一个新的交易,并将其放到下一个开采的区块中
        :param sender: <str> 发送者的地址
        :param recipient: <str> 接收者的地址
        :param amount: <int> 金额
        :return: <int> 包含该交易的区块的索引
        """

        self.current_transactions.append({
            'sender': sender,
            'recipient': recipient,
            'amount': amount,
        })

        return self.last_block['index'] + 1

new_transaction() 在将一个交易添加到交易列表之后,它返回这个交易将添加到哪个区块的索引——下一个将被挖矿的区块。这对稍后对交易进行提交的用户非常有用。

创建一个新区块

当我们的 Blockchain 被实例化后,我们需要为其提供一个起始区块——一个没有前置区块的区块。我们还需要给这个起始区块添加一个“证明”,这是挖矿(工作量证明)的结果。我们将在稍后来展开讨论挖矿。

为了在构造函数中创建起始区块,我们需要对 new_block()new_transaction()hash() 方法进行扩充:

import hashlib
import json
from time import time


class Blockchain(object):
    def __init__(self):
        self.current_transactions = []
        self.chain = []

        # 创建起始区块
        self.new_block(previous_hash=1, proof=100)

    def new_block(self, proof, previous_hash=None):
        """
        在区块链中创建一个新的区块
        :param proof: <int> 由工作量证明算法给出的证明
        :param previous_hash: (Optional) <str> 上一区块的哈希
        :return: <dict> 新区块
        """

        block = {
            'index': len(self.chain) + 1,
            'timestamp': time(),
            'transactions': self.current_transactions,
            'proof': proof,
            'previous_hash': previous_hash or self.hash(self.chain[-1]),
        }

        # 重置当前交易列表
        self.current_transactions = []

        self.chain.append(block)
        return block

    def new_transaction(self, sender, recipient, amount):
        """
        创建一个新的交易,并将其放到下一个开采的区块中
        :param sender: <str> 发送者的地址
        :param recipient: <str> 接收者的地址
        :param amount: <int> 金额
        :return: <int> 包含该交易的区块的索引
        """
        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):
        """
        创建区块的 SHA-256 哈希值
        :param block: <dict> 区块
        :return: <str>
        """

        # 我们必须确保字典是有序的,否则我们得到的哈希值不一致
        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。为了简化这个示例,我们设 x = 5,然后用 Python 实现:

from hashlib import sha256
x = 5
y = 0  # 我们并不知道 y 应该是什么数
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

比特币里的工作量证明叫做 Hashcash,它和我们上面的例子没有很大的不同。这是矿工争相解决的算法,以创建一个新的区块。通常,其难度取决于字符串中搜索的字符数。之后,矿工们通过交易获得比特币作为解决该问题的奖励。

网络能够轻松验证他们的解决方案。

实现最基本的工作量证明

现在,给我们的区块链实现一个简单的工作量证明算法。我们的规则很简单:

找到一个数字 p ,当与上一区块的解决方案进行哈希时,会产生一个具有4个前导 0 的哈希。

import hashlib
import json

from time import time
from uuid import uuid4


class Blockchain(object):
    # ...

    def proof_of_work(self, last_proof):
        """
        简单的工作量证明算法:
         - 找到一个数字 p ,当与上一区块的解决方案进行哈希时,会产生一个具有4个前导 `0` 的哈希:hash(pp')
         - p 是上一个证明, p' 是下一个证明
        :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):
        """
        验证证明:hash(last_proof, proof) 包含4个前导0?
        :param last_proof: <int> 上一证明
        :param proof: <int> 当前证明
        :return: <bool> 如果正确返回 True,否则返回 False
        """

        guess = f'{last_proof}{proof}'.encode()
        guess_hash = hashlib.sha256(guess).hexdigest()
        return guess_hash[:4] == "0000"

要调整该算法的难度,可以修改前导零的数量。但4个前导零已经足够了。你会发现,增加一个前导零与寻找解决方案所需的时间差异巨大。

我们的类已经接近完成,现在可以开始使用 HTTP 请求与之进行交互了。

第2步:我们的区块链 API

我们要开始使用 Python 的 Flask 框架了。它是一个微框架,通过它可以很容易地将端点与 Python 函数进行映射。通过它,我们就可以利用 HTTP 请求来和我们的区块链进行交互。

我们将创建三个方法:

  • /transactions/new:创建一个新的交易到区块里

  • /mine:告诉我们的服务器,对新区块进行挖矿

  • /chain:返回完整的区块链

设置 Flask

我们的“服务器”将在我们的区块链网络中形成单个节点。我们来创建一些代码蓝图:

import hashlib
import json
from textwrap import dedent
from time import time
from uuid import uuid4

from flask import Flask


class Blockchain(object):
    # ...


# 实例化我们的节点
app = Flask(__name__) # 15 行

# 为该节点生成唯一标识
node_identifier = str(uuid4()).replace('-', '') # 18 行

# 实例化区块链
blockchain = Blockchain() # 21 行


@app.route('/mine', methods=['GET']) # 24 行
def mine():
    return "We'll mine a new Block" # 26 行

@app.route('/transactions/new', methods=['POST']) # 28 行
def new_transaction():
    return "We'll add a new transaction" # 30 行

@app.route('/chain', methods=['GET']) # 32 行
def full_chain():
    response = {
        'chain': blockchain.chain,
        'length': len(blockchain.chain),
    }
    return jsonify(response), 200 # 38 行

if __name__ == '__main__': # 40 行
    app.run(host='0.0.0.0', port=5000) # 41 行

对上面添加的内容进行简要说明:

  • 15 行:实例化我们的节点。点击这里查看有关 Flask 的更多信息

  • 18 行:给我们的节点设置一个唯一标识

  • 21 行:实例化我们的 Blockchain

  • 24 - 26 行:创建 /mine 端点,响应 GET 请求

  • 28 - 30 行:创建 /transaction/new 端点,响应 POST 请求,因为我们要发送数据给它

  • 32 - 38 行:创建 /chain 端点,返回完整的区块链

  • 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()

    # 检查 post 必须提交的字段
    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. 通过将其添加到链中来伪造成新区块

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():
    # 通过工作量证明算法获取下一个证明
    last_block = blockchain.last_block
    last_proof = last_block['proof']
    proof = blockchain.proof_of_work(last_proof)

    # 当我们找到满足条件的证明的时候,需要接收奖励
    # 当 sender 为 "0" 时,表示这个节点挖到了一个新币
    blockchain.new_transaction(
        sender="0",
        recipient=node_identifier,
        amount=1,
    )

    # 假装这是个新区块,附加到区块链中
    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 类中的方法进行交互。至此,我们区块链已经完成了,现在可以开始和我们的区块链进行交互了。

第3步:和我们的区块链进行交互

你可以使用 cURL 或 Postman 来和我们的 API 进行交互。

启动服务器:

$ python blockchain.py
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
  • 我们来对一个区块进行挖矿:构建一个 GET 请求到 http://127.0.0.1:5000/mine

  • 创建一个新交易:构建一个 POST 请求到 http://127.0.0.1:5000/transactions/new,主体部分包含我们的交易结构:

{
 "sender": "d4ee26eee15148ee92c6cd394edd974e",
 "recipient": "someone-other-address",
 "amount": 5
}

对应的 cURL 命令:

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

我重启了我的服务器,并挖到了两个区块,总共给了3个区块。

  • 发送 GET 请求到 http://127.0.0.1:5000/chain 获取完整的区块链
{
  "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
}

第4步:共识

这非常酷。 我们有了一个接受交易,并允许我们挖掘新区块的基本区块链。但区块链的重点在于它们是分散的。既然它们是分散的,我们要如何确保它们都反映了同一条链?这被称为共识问题,如果我们的网络中需要多个节点,我们必须实现共识算法。

注册新节点

在我们实现共识算法之前,我们需要一种让节点了解网络上相邻节点的方法。我们网络上的每个节点都应该保留网络上其他节点的注册表。因此,我们需要更多的端点:

  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):
        """
        将一个新节点添加到节点列表中
        :param address: <str> 节点的地址。比如: '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):
        """
        确定给定的区块链是否有效
        :param chain: <list> 区块链
        :return: <bool> 有效,返回 True;否则返回 False
        """

        last_block = chain[0]
        current_index = 1

        while current_index < len(chain):
            block = chain[current_index]
            print(f'{last_block}')
            print(f'{block}')
            print("\n-----------\n")
            # 检查区块哈希值的正确性
            if block['previous_hash'] != self.hash(last_block):
                return False

            # 检查工作量证明的正确性
            if not self.valid_proof(last_block['proof'], block['proof']):
                return False

            last_block = block
            current_index += 1

        return True

    def resolve_conflicts(self):
        """
        这是我们的共识算法,它将使用网络中所有节点里最长的区块链替换当前节点的区块链
        :return: <bool> 如果区块链被替换,返回 True;否则返回 False
        """

        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

第一个 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://127.0.0.1:5000http://127.0.0.1:5001

之后,我在节点2上挖掘了一些新的区块,以确保区块链更长。然后,我在节点1上调用 GET /nodes/resolve,该节点上的区块链被共识算法进行替换。

叫上你的小伙伴们一起来测试你的区块链吧!


我希望这能激发你创造新的东西。我对加密货币感到欣喜,因为我相信区块链将迅速改变我们对经济等各领域的看法。

更新:我打算跟进第2部分,这部分将扩展我们的区块链以拥有交易验证机制,并讨论可以生产区块链的一些方法。

本文译自 Daniel van Flymen 的 《Learn Blockchains by Building One》。水平有限,如有错误敬请指教:[email protected]

flask 区块链 blockchain 2018-05-26 21:22 2444410