Linux中國

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

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

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

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

開始之前 …

記住,區塊鏈是一個 不可更改的、有序的 記錄(被稱為區塊)的鏈。它們可以包括 交易 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({
            &apos;sender&apos;: sender,
            &apos;recipient&apos;: recipient,
            &apos;amount&apos;: amount,
        })

        return self.last_block[&apos;index&apos;] + 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 = {
            &apos;index&apos;: len(self.chain) + 1,
            &apos;timestamp&apos;: time(),
            &apos;transactions&apos;: self.current_transactions,
            &apos;proof&apos;: proof,
            &apos;previous_hash&apos;: 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({
            &apos;sender&apos;: sender,
            &apos;recipient&apos;: recipient,
            &apos;amount&apos;: amount,
        })

        return self.last_block[&apos;index&apos;] + 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&apos;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&apos;t know what y should be yet...

while sha256(f&apos;{x*y}&apos;.encode()).hexdigest()[-1] != "0":
    y += 1

print(f&apos;The solution is y = {y}&apos;)

在這裡的答案是 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&apos; such that hash(pp&apos;) contains leading 4 zeroes, where p is the previous p&apos;
         - p is the previous proof, and p&apos; 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&apos;{last_proof}{proof}&apos;.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(&apos;-&apos;, &apos;&apos;)

# Instantiate the Blockchain
blockchain = Blockchain()

@app.route(&apos;/mine&apos;, methods=[&apos;GET&apos;])
def mine():
    return "We&apos;ll mine a new Block"

@app.route(&apos;/transactions/new&apos;, methods=[&apos;POST&apos;])
def new_transaction():
    return "We&apos;ll add a new transaction"

@app.route(&apos;/chain&apos;, methods=[&apos;GET&apos;])
def full_chain():
    response = {
        &apos;chain&apos;: blockchain.chain,
        &apos;length&apos;: len(blockchain.chain),
    }
    return jsonify(response), 200

if __name__ == &apos;__main__&apos;:
    app.run(host=&apos;0.0.0.0&apos;, 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&apos;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(&apos;/transactions/new&apos;, methods=[&apos;POST&apos;])
def new_transaction():
    values = request.get_json()

    # Check that the required fields are in the POST&apos;ed data
    required = [&apos;sender&apos;, &apos;recipient&apos;, &apos;amount&apos;]
    if not all(k in values for k in required):
        return &apos;Missing values&apos;, 400

    # Create a new Transaction
    index = blockchain.new_transaction(values[&apos;sender&apos;], values[&apos;recipient&apos;], values[&apos;amount&apos;])

    response = {&apos;message&apos;: f&apos;Transaction will be added to Block {index}&apos;}
    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(&apos;/mine&apos;, methods=[&apos;GET&apos;])
def mine():
    # We run the proof of work algorithm to get the next proof...
    last_block = blockchain.last_block
    last_proof = last_block[&apos;proof&apos;]
    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 = {
        &apos;message&apos;: "New Block Forged",
        &apos;index&apos;: block[&apos;index&apos;],
        &apos;transactions&apos;: block[&apos;transactions&apos;],
        &apos;proof&apos;: block[&apos;proof&apos;],
        &apos;previous_hash&apos;: block[&apos;previous_hash&apos;],
    }
    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 &apos;{
 "sender": "d4ee26eee15148ee92c6cd394edd974e",
 "recipient": "someone-other-address",
 "amount": 5
}&apos; "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. &apos;http://192.168.0.5:5000&apos;
        :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&apos;{last_block}&apos;)
            print(f&apos;{block}&apos;)
            print("n-----------n")
            # Check that the hash of the block is correct
            if block[&apos;previous_hash&apos;] != self.hash(last_block):
                return False

            # Check that the Proof of Work is correct
            if not self.valid_proof(last_block[&apos;proof&apos;], block[&apos;proof&apos;]):
                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&apos;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&apos;http://{node}/chain&apos;)

            if response.status_code == 200:
                length = response.json()[&apos;length&apos;]
                chain = response.json()[&apos;chain&apos;]

                # 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(&apos;/nodes/register&apos;, methods=[&apos;POST&apos;])
def register_nodes():
    values = request.get_json()

    nodes = values.get(&apos;nodes&apos;)
    if nodes is None:
        return "Error: Please supply a valid list of nodes", 400

    for node in nodes:
        blockchain.register_node(node)

    response = {
        &apos;message&apos;: &apos;New nodes have been added&apos;,
        &apos;total_nodes&apos;: list(blockchain.nodes),
    }
    return jsonify(response), 201

@app.route(&apos;/nodes/resolve&apos;, methods=[&apos;GET&apos;])
def consensus():
    replaced = blockchain.resolve_conflicts()

    if replaced:
        response = {
            &apos;message&apos;: &apos;Our chain was replaced&apos;,
            &apos;new_chain&apos;: blockchain.chain
        }
    else:
        response = {
            &apos;message&apos;: &apos;Our chain is authoritative&apos;,
            &apos;chain&apos;: 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中國 榮譽推出


本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive

對這篇文章感覺如何?

太棒了
0
不錯
0
愛死了
0
不太好
0
感覺很糟
0
雨落清風。心向陽

    You may also like

    Leave a reply

    您的電子郵箱地址不會被公開。 必填項已用 * 標註

    此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

    More in:Linux中國