从零开始撸一个区块链

如果你看到这篇文章,那么你一定也为数字加密货币的崛起而感到激动。与此同时你也想了解其工作原理——区块链。

但是理解区块链并不容易——至少我这么觉得。我看了许多晦涩的视频,学了很多漏洞百出的教程,并且尝试试了些例子,效果不容乐观。

我乐于通过实践来学习知识,这使我一定要深入代码来解决这个问题,那才是最棘手的问题。如果你和我一样,那么读完这篇文章,你便可以在认识到原理的基础上,实现一个区块链。

准备工作…

要知道,区块链是不可变的有序的记录的链,记录也称为区块。它可以包含交易、文件或任何你想要的数据。其中,最重要的是它们是由hash值链在一起的。

如果你对hash有疑问,可参考此文章

本文目标读者是谁?你应该能熟练地读写Python代码,并且对HTTP请求有一定理解,因为我们实现区块链需要以HTTP请求为基础。

还需做什么?确保安装了Python 3.6+环境(包括pip)。还需安装Flask及Requests库:

pip install Flask==0.12.2 requests==2.18.4

对了,你还得准备个HTTP客户端,比如 Postman 或 cURL。

完整代码在哪?源代码在这


第一步:创建Blockchain类

打开你最爱的编辑器或IDE,个人而言我喜欢PyCharm.新建一个文件,命名为blockchain.py

表示出一个区块链

我们将创建一个Blockchain类,它的构造函数包括两个空的list(分别用来存储区块链和交易记录 )。如下:

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类负责管理链。将用来存储交易记录和添加新的区块到区块链。接下来让我们来实现这些方法。

区块是什么样的?

每个区块都包含索引时间戳(Unix),交易记录列表凭证(后续解释),和前一个区块的hash值

这是一个区块的样例:

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

此时,链的概念应该已经很清晰了——每个新的区块包含它自身以及前一个区块的hash值。这是至关重要的,因为这决定了区块链的不可变性:如果攻击者篡改了链中的一个区块,那么所有之后的区块都会包含错误的hash值。

理解了吗?如果没有,花些时间在上面——那是区块链的核心概念。

向区块中添加交易记录

我们需要一个方法来向区块中添加交易记录。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()添加一条交易记录到list中后,会返回下一个区块的索引—也就是下一个要被“挖”的。这在用户提交交易时很有用。

创建一个新区块

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 = []

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

以上的代码相当直观——我添加了注释和docstrings来使其条理清晰。现在,我们差不多已经可以表示区块链了。但是,你一定好奇新的区块如何被创建、打造和挖的。

理解工作量证明

新的区块通过工作量证明算法 (PoW)来创建。PoW 的目的是寻找一个符合特定条件的数值。对于网络中的任何一个人,这个数值都是很难计算得到但易于验证的。这是工作量证明的核心理念。

我们来通过一个很简单的例子来辅助理解。

规定某个整数x和另一个整数y的乘积的hash值必须以0结尾。那么hash(x * y) = ac23dc...0。就此例而言,设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。这样得到的hash末尾为0

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

在比特币中,使用的工作量证明算法是 Hashcash。它和上面的例子区别不大。矿工为了得到新的区块争相求解结果。总体上来说,难度取决于目标字符串需要满足的特定字符的数量。求出结果就会在交易中获得比特币奖励。

实现工作量证明

让我们来给区块链实现一个相似的算法吧。规则与前文类似:

找出数字p,它和前一个区块的凭证求得的hash开头是四个0

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是合适的。因为你会发现哪怕是增加一个0,计算结果的复杂程度都会大大地增加。

我们的区块链基本已经实现完成了,那么我们就开始实现HTTP交互层了。


第二步:实现Blockchain API

我们将使用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):
    ...


# 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)

简要说明上述代码:

  • Line 15:初始化节点。了解Flask
  • Line 18:给节点随机命名。
  • Line 21:实例化Blockchain类。
  • Line 24–26:创建/mine接口,GET方法。
  • Line 32–38:创建/transactions/new接口,POST方法。
  • Line 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
    block = blockchain.new_block(proof)

    response = {
        'message': "New Block Forged",
        'index': block['index'],
        'transactions': block['transactions'],
        'proof': block['proof'],
        'previous_hash': block['previous_hash'],
    }
    return jsonify(response), 200

需要注意的是,挖出的区块的接受者是我们自己节点的地址。我们所做的大部分工作就是和 Blockchain进行交互。现在,代码基本完成 了,接下来可以开始使用我们的区块链了。


第三步:与区块链交互

你可以使用简朴古老的cURL或者Postman在局域网内来和我们的API交互。

启动服务器:

$ python blockchain.py

* Running on [http://127.0.0.1:5000/](http://127.0.0.1:5000/) (Press CTRL+C to quit)

让我们通过向http://localhost:5000/mine发送GET请求来挖区块:

http://localhost:5000/transactions/new发送POST请求来创建新交易,内容包含如下交易数据:

如果你不用Postman,那你可以用等价的cURL命令:

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

重启服务器后,初始区块加上挖出的两个区块,总共三个区块。向 http://localhost:5000/chain发送GET请求获取所有区块:

{
  "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 接受新节点的list,形式为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()来保存节点list。这是避免重复添加同一个节点最方便的办法。

实现一致性算法

上面提到的冲突,其含义是一个节点的链和其他节点上的链不同。为了解决这个问题,我们制定一个规则:网络中最长链有效。换句话说,网络中的最长链才是实际上的链。通过运用这个算法,网络中的节点可以实现一致性。

...
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("\n-----------\n")
            # 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()负责通过遍历每个链并验证hash值和凭证来检查链是否有效。

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:5000http://localhost:5001

然后我在节点2上挖出了一些区块,保证它是最长链。之后,用GET请求节点1的 /nodes/resolve接口,发现节点1的链由于一致性算法被替换了:

好的,大功告成啦!找些朋友来一起测试一下你的区块链吧!


希望本文可以激发你的创造力。我是个数字加密货币狂热爱好者,因为我相信区块链将快速改变我们对经济,政府和信息记录的认识。

更新:我计划推出第二部分,拓展我们实现的区块链来包含交易验证机制并探讨区块链的产品落地。

如果喜欢这个教程或者有建议或疑问,欢迎评论。如果发现了任何错误,欢迎在这里贡献代码!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,376评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,126评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,966评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,432评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,519评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,792评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,933评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,701评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,143评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,488评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,626评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,292评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,896评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,742评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,977评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,324评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,494评论 2 348

推荐阅读更多精彩内容