{"content":{"title":"用Python从零开始创建区块链","body":"本文主要内容翻译自[Learn Blockchains by Building One](https://hackernoon.com/learn-blockchains-by-building-one-117428612f46)\r\n\b本文[原始链接](http://learnblockchain.cn/2017/10/27/build_blockchain_by_python/),转载请注明出处。\r\n作者认为最快的学习区块链的方式是自己创建一个，本文就跟随作者用Python来创建一个区块链。\r\n\r\n\r\n\r\n对数字货币的崛起感到新奇的我们，并且想知道其背后的技术——区块链是怎样实现的。\r\n\r\n但是完全搞懂区块链并非易事，我喜欢在实践中学习，通过写代码来学习技术会掌握得更牢固。通过构建一个区块链可以加深对区块链的理解。\r\n\r\n## 准备工作\r\n\r\n本文要求读者对Python有基本的理解，能读写基本的Python，并且需要对HTTP请求有基本的了解。\r\n\r\n我们知道区块链是由区块的记录构成的不可变、有序的链结构，记录可以是交易、文件或任何你想要的数据，重要的是它们是通过哈希值（hashes）链接起来的。\r\n\r\n如果你还不是很了解哈希，可以查看[这篇文章](https://learncryptography.com/hash-functions/what-are-hash-functions) \r\n\r\n### 环境准备\r\n环境准备，确保已经安装Python3.6+, pip , Flask, requests\r\n安装方法：\r\n```\r\npip install Flask==0.12.2 requests==2.18.4\r\n```\r\n\r\n同时还需要一个HTTP客户端，比如Postman，cURL或其它客户端。\r\n\r\n参考[源代码](https://github.com/xilibi2003/blockchain)（原代码在我翻译的时候，无法运行，我fork了一份，修复了其中的错误，并添加了翻译，感谢star）\r\n\r\n## 开始创建Blockchain\r\n新建一个文件 blockchain.py，本文所有的代码都写在这一个文件中，可以随时参考[源代码](https://github.com/xilibi2003/blockchain)\r\n\r\n### Blockchain类\r\n首先创建一个Blockchain类，在构造函数中创建了两个列表，一个用于储存区块链，一个用于储存交易。\r\n\r\n以下是Blockchain类的框架：\r\n```python\r\nclass Blockchain(object):\r\n    def __init__(self):\r\n        self.chain = []\r\n        self.current_transactions = []\r\n        \r\n    def new_block(self):\r\n        # Creates a new Block and adds it to the chain\r\n        pass\r\n    \r\n    def new_transaction(self):\r\n        # Adds a new transaction to the list of transactions\r\n        pass\r\n    \r\n    @staticmethod\r\n    def hash(block):\r\n        # Hashes a Block\r\n        pass\r\n\r\n    @property\r\n    def last_block(self):\r\n        # Returns the last Block in the chain\r\n        pass\r\n```\r\nBlockchain类用来管理链条，它能存储交易，加入新块等，下面我们来进一步完善这些方法。\r\n\r\n### 块结构\r\n每个区块包含属性：索引（index），Unix时间戳（timestamp），交易列表（transactions），工作量证明（稍后解释）以及前一个区块的Hash值。\r\n\r\n以下是一个区块的结构：\r\n```javascript\r\nblock = {\r\n    'index': 1,\r\n    'timestamp': 1506057125.900785,\r\n    'transactions': [\r\n        {\r\n            'sender': \"8527147fe1f5426f9dd545de4b27ee00\",\r\n            'recipient': \"a77f5cdfa2934df3954a5c7c7da5df1f\",\r\n            'amount': 5,\r\n        }\r\n    ],\r\n    'proof': 324984774000,\r\n    'previous_hash': \"2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824\"\r\n}\r\n```\r\n\r\n到这里，区块链的概念就清楚了，每个新的区块都包含上一个区块的Hash，这是关键的一点，它保障了区块链不可变性。如果攻击者破坏了前面的某个区块，那么后面所有区块的Hash都会变得不正确。不理解的话，慢慢消化，可参考[区块链记账原理](https://learnblockchain.cn/2017/10/25/whatbc)\r\n\r\n### 加入交易\r\n接下来我们需要添加一个交易，来完善下new_transaction方法\r\n```python\r\nclass Blockchain(object):\r\n    ...\r\n    \r\n    def new_transaction(self, sender, recipient, amount):\r\n        \"\"\"\r\n        生成新交易信息，信息将加入到下一个待挖的区块中\r\n        :param sender: <str> Address of the Sender\r\n        :param recipient: <str> Address of the Recipient\r\n        :param amount: <int> Amount\r\n        :return: <int> The index of the Block that will hold this transaction\r\n        \"\"\"\r\n\r\n        self.current_transactions.append({\r\n            'sender': sender,\r\n            'recipient': recipient,\r\n            'amount': amount,\r\n        })\r\n\r\n        return self.last_block['index'] + 1\r\n```\r\n\r\n方法向列表中添加一个交易记录，并返回该记录将被添加到的区块(下一个待挖掘的区块)的索引，等下在用户提交交易时会有用。\r\n\r\n### 创建新块\r\n\r\n当Blockchain实例化后，我们需要构造一个创世块（没有前区块的第一个区块），并且给它加上一个工作量证明。\r\n每个区块都需要经过工作量证明，俗称挖矿，稍后会继续讲解。\r\n\r\n为了构造创世块，我们还需要完善new_block(), new_transaction() 和hash() 方法：\r\n\r\n```python\r\nimport hashlib\r\nimport json\r\nfrom time import time\r\n\r\n\r\nclass Blockchain(object):\r\n    def __init__(self):\r\n        self.current_transactions = []\r\n        self.chain = []\r\n\r\n        # Create the genesis block\r\n        self.new_block(previous_hash=1, proof=100)\r\n\r\n    def new_block(self, proof, previous_hash=None):\r\n        \"\"\"\r\n        生成新块\r\n        :param proof: <int> The proof given by the Proof of Work algorithm\r\n        :param previous_hash: (Optional) <str> Hash of previous Block\r\n        :return: <dict> New Block\r\n        \"\"\"\r\n\r\n        block = {\r\n            'index': len(self.chain) + 1,\r\n            'timestamp': time(),\r\n            'transactions': self.current_transactions,\r\n            'proof': proof,\r\n            'previous_hash': previous_hash or self.hash(self.chain[-1]),\r\n        }\r\n\r\n        # Reset the current list of transactions\r\n        self.current_transactions = []\r\n\r\n        self.chain.append(block)\r\n        return block\r\n\r\n    def new_transaction(self, sender, recipient, amount):\r\n        \"\"\"\r\n        生成新交易信息，信息将加入到下一个待挖的区块中\r\n        :param sender: <str> Address of the Sender\r\n        :param recipient: <str> Address of the Recipient\r\n        :param amount: <int> Amount\r\n        :return: <int> The index of the Block that will hold this transaction\r\n        \"\"\"\r\n        self.current_transactions.append({\r\n            'sender': sender,\r\n            'recipient': recipient,\r\n            'amount': amount,\r\n        })\r\n\r\n        return self.last_block['index'] + 1\r\n\r\n    @property\r\n    def last_block(self):\r\n        return self.chain[-1]\r\n\r\n    @staticmethod\r\n    def hash(block):\r\n        \"\"\"\r\n        生成块的 SHA-256 hash值\r\n        :param block: <dict> Block\r\n        :return: <str>\r\n        \"\"\"\r\n\r\n        # We must make sure that the Dictionary is Ordered, or we'll have inconsistent hashes\r\n        block_string = json.dumps(block, sort_keys=True).encode()\r\n        return hashlib.sha256(block_string).hexdigest()\r\n\r\n\r\n```\r\n\r\n通过上面的代码和注释可以对区块链有直观的了解，接下来我们看看区块是怎么挖出来的。\r\n\r\n### 理解工作量证明\r\n\r\n新的区块依赖工作量证明算法（PoW）来构造。PoW的目标是找出一个符合特定条件的数字，**这个数字很难计算出来，但容易验证**。这就是工作量证明的核心思想。\r\n\r\n为了方便理解，举个例子：\r\n\r\n假设一个整数 x 乘以另一个整数 y 的积的 Hash 值必须以 0 结尾，即 hash(x * y) = ac23dc...0。设变量 x = 5，求 y 的值？\r\n\r\n用Python实现如下：\r\n\r\n```python\r\nfrom hashlib import sha256\r\nx = 5\r\ny = 0  # y未知\r\nwhile sha256(f'{x*y}'.encode()).hexdigest()[-1] != \"0\":\r\n    y += 1\r\nprint(f'The solution is y = {y}')\r\n```\r\n\r\n结果是y=21. 因为：\r\n```\r\nhash(5 * 21) = 1253e9373e...5e3600155e860\r\n```\r\n\r\n在比特币中，使用称为[Hashcash](https://en.wikipedia.org/wiki/Hashcash)的工作量证明算法，它和上面的问题很类似。矿工们为了争夺创建区块的权利而争相计算结果。通常，计算难度与目标字符串需要满足的特定字符的数量成正比，矿工算出结果后，会获得比特币奖励。\r\n当然，在网络上非常容易验证这个结果。\r\n\r\n### 实现工作量证明\r\n让我们来实现一个相似PoW算法，规则是：寻找一个数 p，使得它与前一个区块的 proof 拼接成的字符串的 Hash 值以 4 个零开头。\r\n\r\n```python\r\nimport hashlib\r\nimport json\r\n\r\nfrom time import time\r\nfrom uuid import uuid4\r\n\r\n\r\nclass Blockchain(object):\r\n    ...\r\n        \r\n    def proof_of_work(self, last_proof):\r\n        \"\"\"\r\n        简单的工作量证明:\r\n         - 查找一个 p' 使得 hash(pp') 以4个0开头\r\n         - p 是上一个块的证明,  p' 是当前的证明\r\n        :param last_proof: <int>\r\n        :return: <int>\r\n        \"\"\"\r\n\r\n        proof = 0\r\n        while self.valid_proof(last_proof, proof) is False:\r\n            proof += 1\r\n\r\n        return proof\r\n\r\n    @staticmethod\r\n    def valid_proof(last_proof, proof):\r\n        \"\"\"\r\n        验证证明: 是否hash(last_proof, proof)以4个0开头?\r\n        :param last_proof: <int> Previous Proof\r\n        :param proof: <int> Current Proof\r\n        :return: <bool> True if correct, False if not.\r\n        \"\"\"\r\n\r\n        guess = f'{last_proof}{proof}'.encode()\r\n        guess_hash = hashlib.sha256(guess).hexdigest()\r\n        return guess_hash[:4] == \"0000\"\r\n\r\n```\r\n\r\n衡量算法复杂度的办法是修改零开头的个数。使用4个来用于演示，你会发现多一个零都会大大增加计算出结果所需的时间。\r\n\r\n现在Blockchain类基本已经完成了，接下来使用HTTP requests来进行交互。\r\n\r\n\r\n## Blockchain作为API接口\r\n\r\n我们将使用Python Flask框架，这是一个轻量Web应用框架，它方便将网络请求映射到 Python函数，现在我们来让Blockchain运行在基于Flask web上。\r\n\r\n我们将创建三个接口：\r\n *    /transactions/new 创建一个交易并添加到区块\r\n *    /mine 告诉服务器去挖掘新的区块\r\n *    /chain 返回整个区块链\r\n\r\n### 创建节点\r\n\r\n我们的“Flask服务器”将扮演区块链网络中的一个节点。我们先添加一些框架代码：\r\n\r\n```python\r\nimport hashlib\r\nimport json\r\nfrom textwrap import dedent\r\nfrom time import time\r\nfrom uuid import uuid4\r\n\r\nfrom flask import Flask\r\n\r\n\r\nclass Blockchain(object):\r\n    ...\r\n\r\n\r\n# Instantiate our Node\r\napp = Flask(__name__)\r\n\r\n# Generate a globally unique address for this node\r\nnode_identifier = str(uuid4()).replace('-', '')\r\n\r\n# Instantiate the Blockchain\r\nblockchain = Blockchain()\r\n\r\n\r\n@app.route('/mine', methods=['GET'])\r\ndef mine():\r\n    return \"We'll mine a new Block\"\r\n  \r\n@app.route('/transactions/new', methods=['POST'])\r\ndef new_transaction():\r\n    return \"We'll add a new transaction\"\r\n\r\n@app.route('/chain', methods=['GET'])\r\ndef full_chain():\r\n    response = {\r\n        'chain': blockchain.chain,\r\n        'length': len(blockchain.chain),\r\n    }\r\n    return jsonify(response), 200\r\n\r\nif __name__ == '__main__':\r\n    app.run(host='0.0.0.0', port=5000)\r\n```\r\n简单的说明一下以上代码：\r\n    第15行: 创建一个节点. \r\n    第18行: 为节点创建一个随机的名字.\r\n    第21行: 实例Blockchain类.\r\n    第24–26行: 创建/mine GET接口。\r\n    第28–30行: 创建/transactions/new POST接口,可以给接口发送交易数据.\r\n    第32–38行: 创建 /chain 接口, 返回整个区块链。\r\n    第40–41行: 服务运行在端口5000上.\r\n\r\n### 发送交易\r\n\r\n发送到节点的交易数据结构如下：\r\n```json\r\n{\r\n \"sender\": \"my address\",\r\n \"recipient\": \"someone else's address\",\r\n \"amount\": 5\r\n}\r\n```\r\n\r\n之前已经有添加交易的方法，基于接口来添加交易就很简单了\r\n\r\n```python\r\nimport hashlib\r\nimport json\r\nfrom textwrap import dedent\r\nfrom time import time\r\nfrom uuid import uuid4\r\n\r\nfrom flask import Flask, jsonify, request\r\n\r\n...\r\n\r\n@app.route('/transactions/new', methods=['POST'])\r\ndef new_transaction():\r\n    values = request.get_json()\r\n\r\n    # Check that the required fields are in the POST'ed data\r\n    required = ['sender', 'recipient', 'amount']\r\n    if not all(k in values for k in required):\r\n        return 'Missing values', 400\r\n\r\n    # Create a new Transaction\r\n    index = blockchain.new_transaction(values['sender'], values['recipient'], values['amount'])\r\n\r\n    response = {'message': f'Transaction will be added to Block {index}'}\r\n    return jsonify(response), 201\r\n\r\n```\r\n\r\n### 挖矿\r\n\r\n挖矿正是神奇所在，它很简单，做了一下三件事：\r\n1. 计算工作量证明PoW\r\n2. 通过新增一个交易授予矿工（自己）一个币\r\n3. 构造新区块并将其添加到链中\r\n\r\n```python \r\nimport hashlib\r\nimport json\r\n\r\nfrom time import time\r\nfrom uuid import uuid4\r\n\r\nfrom flask import Flask, jsonify, request\r\n\r\n...\r\n\r\n@app.route('/mine', methods=['GET'])\r\ndef mine():\r\n    # We run the proof of work algorithm to get the next proof...\r\n    last_block = blockchain.last_block\r\n    last_proof = last_block['proof']\r\n    proof = blockchain.proof_of_work(last_proof)\r\n\r\n    # 给工作量证明的节点提供奖励.\r\n    # 发送者为 \"0\" 表明是新挖出的币\r\n    blockchain.new_transaction(\r\n        sender=\"0\",\r\n        recipient=node_identifier,\r\n        amount=1,\r\n    )\r\n\r\n    # Forge the new Block by adding it to the chain\r\n    block = blockchain.new_block(proof)\r\n\r\n    response = {\r\n        'message': \"New Block Forged\",\r\n        'index': block['index'],\r\n        'transactions': block['transactions'],\r\n        'proof': block['proof'],\r\n        'previous_hash': block['previous_hash'],\r\n    }\r\n    return jsonify(response), 200\r\n```\r\n注意交易的接收者是我们自己的服务器节点，我们做的大部分工作都只是围绕Blockchain类方法进行交互。到此，我们的区块链就算完成了，我们来实际运行下\r\n\r\n## 运行区块链\r\n\r\n你可以使用cURL 或Postman 去和API进行交互\r\n\r\n启动server:\r\n```bash\r\n$ python blockchain.py\r\n* Runing on http://127.0.0.1:5000/ (Press CTRL+C to quit)\r\n```\r\n\r\n让我们通过请求 http://localhost:5000/mine 来进行挖矿\r\n\r\n![用Postman请求挖矿](https://img.learnblockchain.cn/2017/postman_get_mine.png!wl)\r\n\r\n通过post请求，添加一个新交易\r\n\r\n![用Postman请求挖矿](https://img.learnblockchain.cn/2017/postman_post_new.png!wl)\r\n\r\n如果不是使用Postman，则用一下的cURL语句也是一样的：\r\n```bash\r\n$ curl -X POST -H \"Content-Type: application/json\" -d '{\r\n \"sender\": \"d4ee26eee15148ee92c6cd394edd974e\",\r\n \"recipient\": \"someone-other-address\",\r\n \"amount\": 5\r\n}' \"http://localhost:5000/transactions/new\"\r\n```\r\n\r\n在挖了两次矿之后，就有3个块了，通过请求 http://localhost:5000/chain 可以得到所有的块信息。\r\n\r\n```json\r\n{\r\n  \"chain\": [\r\n    {\r\n      \"index\": 1,\r\n      \"previous_hash\": 1,\r\n      \"proof\": 100,\r\n      \"timestamp\": 1506280650.770839,\r\n      \"transactions\": []\r\n    },\r\n    {\r\n      \"index\": 2,\r\n      \"previous_hash\": \"c099bc...bfb7\",\r\n      \"proof\": 35293,\r\n      \"timestamp\": 1506280664.717925,\r\n      \"transactions\": [\r\n        {\r\n          \"amount\": 1,\r\n          \"recipient\": \"8bbcb347e0634905b0cac7955bae152b\",\r\n          \"sender\": \"0\"\r\n        }\r\n      ]\r\n    },\r\n    {\r\n      \"index\": 3,\r\n      \"previous_hash\": \"eff91a...10f2\",\r\n      \"proof\": 35089,\r\n      \"timestamp\": 1506280666.1086972,\r\n      \"transactions\": [\r\n        {\r\n          \"amount\": 1,\r\n          \"recipient\": \"8bbcb347e0634905b0cac7955bae152b\",\r\n          \"sender\": \"0\"\r\n        }\r\n      ]\r\n    }\r\n  ],\r\n  \"length\": 3\r\n}\r\n```\r\n\r\n## 一致性（共识）\r\n\r\n我们已经有了一个基本的区块链可以接受交易和挖矿。但是区块链系统应该是分布式的。既然是分布式的，那么我们究竟拿什么保证所有节点有同样的链呢？这就是一致性问题，我们要想在网络上有多个节点，就必须实现一个一致性的算法。\r\n\r\n### 注册节点\r\n在实现一致性算法之前，我们需要找到一种方式让一个节点知道它相邻的节点。每个节点都需要保存一份包含网络中其它节点的记录。因此让我们新增几个接口：\r\n\r\n1. /nodes/register 接收URL形式的新节点列表\r\n2. /nodes/resolve 执行一致性算法，解决任何冲突，确保节点拥有正确的链\r\n\r\n我们修改下Blockchain的init函数并提供一个注册节点方法：\r\n```python\r\n...\r\nfrom urllib.parse import urlparse\r\n...\r\n\r\n\r\nclass Blockchain(object):\r\n    def __init__(self):\r\n        ...\r\n        self.nodes = set()\r\n        ...\r\n\r\n    def register_node(self, address):\r\n        \"\"\"\r\n        Add a new node to the list of nodes\r\n        :param address: <str> Address of node. Eg. 'http://192.168.0.5:5000'\r\n        :return: None\r\n        \"\"\"\r\n\r\n        parsed_url = urlparse(address)\r\n        self.nodes.add(parsed_url.netloc)\r\n\r\n```\r\n我们用 set 来储存节点，这是一种避免重复添加节点的简单方法。\r\n\r\n### 实现共识算法\r\n\r\n前面提到，冲突是指不同的节点拥有不同的链，为了解决这个问题，规定最长的、有效的链才是最终的链，换句话说，网络中有效最长链才是实际的链。\r\n\r\n我们使用一下的算法，来达到网络中的共识\r\n```python\r\n...\r\nimport requests\r\n\r\n\r\nclass Blockchain(object)\r\n    ...\r\n    \r\n    def valid_chain(self, chain):\r\n        \"\"\"\r\n        Determine if a given blockchain is valid\r\n        :param chain: <list> A blockchain\r\n        :return: <bool> True if valid, False if not\r\n        \"\"\"\r\n\r\n        last_block = chain[0]\r\n        current_index = 1\r\n\r\n        while current_index < len(chain):\r\n            block = chain[current_index]\r\n            print(f'{last_block}')\r\n            print(f'{block}')\r\n            print(\"\\n-----------\\n\")\r\n            # Check that the hash of the block is correct\r\n            if block['previous_hash'] != self.hash(last_block):\r\n                return False\r\n\r\n            # Check that the Proof of Work is correct\r\n            if not self.valid_proof(last_block['proof'], block['proof']):\r\n                return False\r\n\r\n            last_block = block\r\n            current_index += 1\r\n\r\n        return True\r\n\r\n    def resolve_conflicts(self):\r\n        \"\"\"\r\n        共识算法解决冲突\r\n        使用网络中最长的链.\r\n        :return: <bool> True 如果链被取代, 否则为False\r\n        \"\"\"\r\n\r\n        neighbours = self.nodes\r\n        new_chain = None\r\n\r\n        # We're only looking for chains longer than ours\r\n        max_length = len(self.chain)\r\n\r\n        # Grab and verify the chains from all the nodes in our network\r\n        for node in neighbours:\r\n            response = requests.get(f'http://{node}/chain')\r\n\r\n            if response.status_code == 200:\r\n                length = response.json()['length']\r\n                chain = response.json()['chain']\r\n\r\n                # Check if the length is longer and the chain is valid\r\n                if length > max_length and self.valid_chain(chain):\r\n                    max_length = length\r\n                    new_chain = chain\r\n\r\n        # Replace our chain if we discovered a new, valid chain longer than ours\r\n        if new_chain:\r\n            self.chain = new_chain\r\n            return True\r\n\r\n        return False\r\n\r\n```\r\n\r\n第一个方法 valid_chain() 用来检查是否是有效链，遍历每个块验证hash和proof.\r\n\r\n第2个方法 resolve_conflicts() 用来解决冲突，遍历所有的邻居节点，并用上一个方法检查链的有效性， ** 如果发现有效更长链，就替换掉自己的链 ** \r\n\r\n让我们添加两个路由，一个用来注册节点，一个用来解决冲突。\r\n```python\r\n@app.route('/nodes/register', methods=['POST'])\r\ndef register_nodes():\r\n    values = request.get_json()\r\n\r\n    nodes = values.get('nodes')\r\n    if nodes is None:\r\n        return \"Error: Please supply a valid list of nodes\", 400\r\n\r\n    for node in nodes:\r\n        blockchain.register_node(node)\r\n\r\n    response = {\r\n        'message': 'New nodes have been added',\r\n        'total_nodes': list(blockchain.nodes),\r\n    }\r\n    return jsonify(response), 201\r\n\r\n\r\n@app.route('/nodes/resolve', methods=['GET'])\r\ndef consensus():\r\n    replaced = blockchain.resolve_conflicts()\r\n\r\n    if replaced:\r\n        response = {\r\n            'message': 'Our chain was replaced',\r\n            'new_chain': blockchain.chain\r\n        }\r\n    else:\r\n        response = {\r\n            'message': 'Our chain is authoritative',\r\n            'chain': blockchain.chain\r\n        }\r\n\r\n    return jsonify(response), 200\r\n```\r\n\r\n你可以在不同的机器运行节点，或在一台机机开启不同的网络端口来模拟多节点的网络，这里在同一台机器开启不同的端口演示，在不同的终端运行一下命令，就启动了两个节点：http://localhost:5000 和 http://localhost:5001\r\n\r\n```shell\r\npipenv run python blockchain.py\r\npipenv run python blockchain.py -p 5001\r\n```\r\n\r\n![注册新节点](https://img.learnblockchain.cn/2017/postman_register.png!wl)\r\n\r\n然后在节点2上挖两个块，确保是更长的链，然后在节点1上访问接口/nodes/resolve ,这时节点1的链会通过共识算法被节点2的链取代。\r\n\r\n![共识算法解决冲突](https://img.learnblockchain.cn/2017/postman_resolve.png!wl)\r\n\r\n好啦，你可以邀请朋友们一起来测试你的区块链"},"author":{"user":"https://learnblockchain.cn/people/15","address":"0x3aA0c5bDce1EBedcB3FaabF69CE159da72bdD0A7"},"history":null,"timestamp":1704513079,"version":1}