想學習區塊鏈?那就用 Python 構建一個

Daniel Van Flymen發表於2018-07-30

瞭解區塊鏈是如何工作的最快的方法是構建一個。

你看到這篇文章是因為和我一樣,對加密貨幣的大熱而感到興奮。並且想知道區塊鏈是如何工作的 —— 它們背後的技術基礎是什麼。

但是理解區塊鏈並不容易 —— 至少對我來說是這樣。我徜徉在各種難懂的影片中,並且因為示例太少而陷入深深的挫敗感中。

我喜歡在實踐中學習。這會使得我在程式碼層面上處理主要問題,從而可以讓我堅持到底。如果你也是這麼做的,在本指南結束的時候,你將擁有一個功能正常的區塊鏈,並且實實在在地理解了它的工作原理。

開始之前 …

記住,區塊鏈是一個 不可更改的、有序的 記錄(被稱為區塊)的鏈。它們可以包括交易transaction、檔案或者任何你希望的真實資料。最重要的是它們是透過使用雜湊連結到一起的。

如果你不知道雜湊是什麼,這裡有解釋

本指南的目標讀者是誰? 你應該能輕鬆地讀、寫一些基本的 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):
        # 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 類的原型

我們的 Blockchain 類負責管理鏈。它將儲存交易並且有一些為鏈中增加新區塊的輔助性質的方法。現在我們開始去充實一些類的方法。

區塊是什麼樣子的?

每個區塊有一個索引、一個時間戳(Unix 時間)、一個交易的列表、一個證明(後面會詳細解釋)、以及前一個區塊的雜湊。

單個區塊的示例應該是下面的樣子:

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() 執行後將在列表中新增一個交易,它返回新增交易後的那個區塊的索引 —— 那個區塊接下來將被挖礦。提交交易的使用者後面會用到這些。

建立新區塊

當我們的 Blockchain 被例項化後,我們需要一個創世區塊(一個沒有祖先的區塊)來播種它。我們也需要去新增一些 “證明” 到創世區塊,它是挖礦(工作量證明 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()

上面的內容簡單明瞭 —— 我新增了一些註釋和文件字串,以使程式碼清晰可讀。到此為止,表示我們的區塊鏈基本上要完成了。但是,你肯定想知道新區塊是如何被建立、打造或者挖礦的。

理解工作量證明

工作量證明Proof of Work(PoW)演算法是在區塊鏈上建立或者挖出新區塊的方法。PoW 的目標是去撞出一個能夠解決問題的數字。這個數字必須滿足“找到它很困難但是驗證它很容易”的條件 —— 網路上的任何人都可以計算它。這就是 PoW 背後的核心思想。

我們來看一個非常簡單的示例來幫助你瞭解它。

我們來解決一個問題,一些整數 x 乘以另外一個整數 y 的結果的雜湊值必須以 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。因為它產生的雜湊值是以 0 結尾的:

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

在比特幣中,工作量證明演算法被稱之為 Hashcash。與我們上面的例子沒有太大的差別。這就是礦工們進行競賽以決定誰來建立新塊的演算法。一般來說,其難度取決於在一個字串中所查詢的字元數量。然後礦工會因其做出的求解而得到獎勵的幣——在一個交易當中。

網路上的任何人都可以很容易地去核驗它的答案。

實現基本的 PoW

為我們的區塊鏈來實現一個簡單的演算法。我們的規則與上面的示例類似:

找出一個數字 p,它與前一個區塊的答案進行雜湊運算得到一個雜湊值,這個雜湊值的前四位必須是由 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 的數量每增加一,那麼找到正確答案所需要的時間難度將大幅增加。

我們的類基本完成了,現在我們開始去使用 HTTP 請求與它互動。

第 2 步:以 API 方式去訪問我們的區塊鏈

我們將使用 Python Flask 框架。它是個微框架,使用它去做端點到 Python 函式的對映很容易。這樣我們可以使用 HTTP 請求基於 web 來與我們的區塊鏈對話。

我們將建立三個方法:

  • /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:例項化我們的區塊鏈類。
  • Line 24–26:建立 /mine 端點,這是一個 GET 請求。
  • Line 28–30:建立 /transactions/new 端點,這是一個 POST 請求,因為我們要傳送資料給它。
  • Line 32–38:建立 /chain 端點,它返回全部區塊鏈。
  • 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. 因為礦工(我們)新增一個交易而獲得報酬,獎勵礦工(我們) 1 個幣
  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 類的方法進行互動的。到目前為止,我們已經做完了,現在開始與我們的區塊鏈去互動。

第 3 步:與我們的區塊鏈去互動

你可以使用簡單的 cURL 或者 Postman 透過網路與我們的 API 去互動。

啟動伺服器:

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

我們透過生成一個 GET 請求到 http://localhost:5000/mine 去嘗試挖一個區塊:

使用 Postman 去生成一個 GET 請求

我們透過生成一個 POST 請求到 http://localhost:5000/transactions/new 去建立一個區塊,請求資料包含我們的交易結構:

使用 Postman 去生成一個 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"

我重啟動我的伺服器,然後我挖到了兩個區塊,這樣總共有了 3 個區塊。我們透過請求 http://localhost: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 步:共識

這是很酷的一個地方。我們已經有了一個基本的區塊鏈,它可以接收交易並允許我們去挖掘出新區塊。但是區塊鏈的整個重點在於它是去中心化的decentralized。而如果它們是去中心化的,那我們如何才能確保它們表示在同一個區塊鏈上?這就是共識Consensus問題,如果我們希望在我們的網路上有多於一個的節點執行,那麼我們將必須去實現一個共識演算法。

註冊新節點

在我們能實現一個共識演算法之前,我們需要一個辦法去讓一個節點知道網路上的鄰居節點。我們網路上的每個節點都保留有一個該網路上其它節點的註冊資訊。因此,我們需要更多的端點:

  1. /nodes/register 以 URL 的形式去接受一個新節點列表
  2. /nodes/resolve 去實現我們的共識演算法,由它來解決任何的衝突 —— 確保節點有一個正確的鏈。

我們需要去修改我們的區塊鏈的建構函式,來提供一個註冊節點的方法:

...
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() 去儲存節點列表。這是一個非常合算的方式,它將確保新增的節點是冪等idempotent的 —— 這意味著不論你將特定的節點新增多少次,它都是精確地只出現一次。

實現共識演算法

正如前面提到的,當一個節點與另一個節點有不同的鏈時就會產生衝突。為解決衝突,我們制定一個規則,即最長的有效的鏈才是權威的鏈。換句話說就是,網路上最長的鏈就是事實上的區塊鏈。使用這個演算法,可以在我們的網路上節點之間達到共識。

...
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() 是負責來檢查鏈是否有效,它透過遍歷區塊鏈上的每個區塊並驗證它們的雜湊和工作量證明來檢查這個區塊鏈是否有效。

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 上挖出一些新區塊,以確保這個鏈是最長的。之後我在節點 1 上以 GET 方式呼叫了 /nodes/resolve,這時,節點 1 上的鏈被共識演算法替換成節點 2 上的鏈了:

工作中的共識演算法

然後將它們封裝起來 … 找一些朋友來幫你一起測試你的區塊鏈。


我希望以上內容能夠鼓舞你去建立一些新的東西。我是加密貨幣的狂熱擁護者,因此我相信區塊鏈將迅速改變我們對經濟、政府和記錄儲存的看法。

更新: 我正計劃繼續它的第二部分,其中我將擴充套件我們的區塊鏈,使它具備交易驗證機制,同時討論一些你可以在其上產生你自己的區塊鏈的方式。(LCTT 譯註:第二篇並沒有~!)


via: https://hackernoon.com/learn-blockchains-by-building-one-117428612f46

作者:Daniel van Flymen 譯者:qhwdw 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出

相關文章