從0開始構建區塊鏈(Blockchain)

區塊鏈現在有多火看看比特幣的價格也就知道了,不過作爲一個有逼格的程序員,我們不應只關注到幣的價值(爲啥我沒去買比特幣,T-T),而應該去關注技術的本質,這個號稱“第四次工業革命”的區塊鏈技術。不過很多人估計對這個技術不太瞭解,包括我自己。既然不懂不如自己動手擼一個,實踐出真知。在翻閱文章的時候剛好找到了這篇文章,下面讓我們自己動手搭建一個簡單的區塊鏈。

簡單的說,區塊鏈就是一個不可變、有序的鏈(chain)結構,鏈中保存着稱之爲塊(block)的記錄,這些記錄可以是交易,文件或是任意你想要的數據。其中重要的是它們通過哈希鏈接在一起。

如果你不懂哈希,可以查看一下這裏,不過作爲開發者應該都懂吧。。。

準備

這裏我們會以python實現一個區塊鏈,因此需要首先安裝python,這裏我是用的是python2.7版本,需要安裝flask以及Requests庫。

pip install Flask requests

同時還需要一個http客戶端,比如postman或curl。

1、創建區塊鏈

新建一個blockchain.py文件,我們這裏只使用這一個文件。

區塊鏈表示

我們首先創建一個Blockchain類,並且在構造函數中創建兩個空的list,一個用於儲存我們的區塊鏈,另一個用於儲存交易。

Blockchain類的定義如下:

class Blockchain(object):
    def __init__(self):
        self.chain = []
        self.current_transactions = []
        
    def new_block(self):
        # 創建一個新的塊,並添加到鏈中
        pass
    
    def new_transaction(self):
        # 添加一筆新的交易到transactions中
        pass
    
    @staticmethod
    def hash(block):
        # 對一個塊做hash
        pass

    @property
    def last_block(self):】
        # 返回鏈中的最後一個塊
        pass

我們的Blockchain類用來管理鏈,它會存儲交易信息,以及一些添加新的塊到鏈中的輔助方法,讓我們開始實現這些方法。

塊(Block)長啥樣?

每個塊都包含如下屬性:索引(index),時間戳(Unix時間),交易列表(transactions),工作量證明(proof,稍後解釋)以及前一個塊的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):
        """
        添加一筆新的交易到transactions中
        :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實例化之後,我們需要爲它創建一個創世塊(第一個區塊,沒有前區塊,類似於鏈表的head),並且給該創世塊添加一個工作量證明(proof of work),工作量證明是挖礦的結果。我們後面在來討論挖礦。

除了在我們的構造函數中創建一個創世塊之外,我們也要實現new_block(), new_transaction() 和 hash()方法:

import hashlib
import json
from time import time

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

        # 創建創世塊
        self.new_block(previous_hash=1, proof=100)
        
    def new_block(self, proof, previous_hash=None):
        """
        創建一個新的塊,並添加到鏈中
        :param proof: <int> 證明
        :param previous_hash: (Optional) <str> 前一個塊的hash值
        :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]),
        }

        # 重置存儲交易信息的list
        self.current_transactions = []

        self.chain.append(block)
        return block
    
    def new_transaction(self, sender, recipient, amount)):
        """
        添加一筆新的交易到transactions中
        :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> 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),簡單的說就是一份證明,該證明可以用來確認你做過一定量的工作。工作量證明的目標就是找到一個能解決一個問題的數字,這個數字不好找但是很容易證明(該數字就是問題的答案),這就是工作量證明背後的核心思想(俗稱挖礦)。

我們舉一個簡單的例子,方便理解:

假設有一個問題,一個整數x乘以整數y,對積做hash,hash值必須以0結尾,即hash(x * y) = ac23dc…0。我們假設X的值爲5,求y的值大小?用Python實現如下:

from hashlib import sha256
x = 5
y = 0  # y是未知數
while sha256(str(x*y).encode()).hexdigest()[-1] != "0":
    y += 1
print('The solution is y = ' + str(y))

可以看到如下的結果:

The solution is y = 21

在比特幣中使用的工作量證明算法叫做Hashcash,它和我們上面的簡單例子相差不大。礦工們爲了獲得能夠創建新的區塊權利,需要使用該算法參與計算競爭。通常,問題的難度取決於需要在字符串中查找的字符個數,礦工算出結果後,會獲得相應的獎勵幣。

tips:比特幣網絡中任何一個節點,如果想生成一個新的區塊並寫入區塊鏈,必須能夠解出比特幣網絡給出的工作量證明問題。這道題關鍵的三個要素是工作量證明函數、區塊及難度值。工作量證明函數是這道題的計算方法,區塊決定了這道題的輸入數據,難度值決定了這道題的所需要的計算量,誰最先解出問題,誰就能生成新的區塊並寫入區塊鏈。
工作量證明簡單實現

爲我們的區塊鏈實現一個相似的算法,規則跟上述的例子差不多:
找到一個數字p,該數字與前一個塊的proof值的hash值以4個0開頭

import hashlib
import json

from time import time
from uuid import uuid4

class Blockchain(object):
    ...

    def proof_of_work(self, last_proof):
            """
            簡單工作量證明(POW)算法:
             - 找到一個數字p',使得hash(pp')值的開頭包含4個0, p是上一個塊的proof,  p'是新的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):
            """
            驗證Proof: hash(last_proof, proof)值開頭是否包含4個0?
            :param last_proof: <int> 上一個Proof
            :param proof: <int> 當前Proof
            :return: <bool>
            """

            guess = (str(last_proof)+str(proof)).encode()
            guess_hash = hashlib.sha256(guess).hexdigest()
            return guess_hash[:4] == "0000"

爲了調整算法的難度,我們可以修改需要匹配的0的個數。但4已經足夠了,你會發現增加一個數字會大大增加計算的時間。

我們的Blockchain類差不多完成了,接下來可以開始用http請求進行交互了。

2、Blockchain作爲api

我們將使用Python Flask框架,這是一個輕量級的Web應用框架,它能方便的將請求映射到 Python函數,這允許我們通過http請求跟區塊鏈進行交互。

我們會創建3個方法:

/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__)

# 爲這個節點生成一個全局的唯一地址
node_identifier = str(uuid4()).replace('-', '')

# 實例化區塊鏈
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)

代碼相對簡單,就不做說明了。

交易請求

用戶發給服務器的交易請求如下:

{
 "sender": "my address",
 "recipient": "someone else's address",
 "amount": 5
}

我們在Blockchain類中已經定義了添加交易的方法,因此接下來的事情比較簡單:

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

    # 檢查需要的字段是不是都有
    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), 200
挖礦

挖礦是裏面的神奇所在,而且很簡單,只需要做如下三件事:

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():
    # 運行工作量證明算法,獲取下一個proof
    last_block = blockchain.last_block
    last_proof = last_block['proof']
    proof = blockchain.proof_of_work(last_proof)

    # 由於找到了proof,我們獲得一筆獎勵
    # 發送者爲"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://localhost:5000/mine來挖一個塊:

mine.png

讓我們通過POST請求http://localhost:5000/transactions/new創建一筆新的交易,post body包含交易的結構信息:

new.png

我重啓服務器然後挖了兩個塊之後,現在總共有三個塊(包含創世塊),我們可以通過http://localhost:5000/chain請求獲取所有的塊信息。

chain.png

4、一致性

我們已經有了一個可以接受交易以及挖礦的基礎區塊鏈。但是區塊鏈系統應該是去中心化的,既然區塊鏈是去中心化,我們該如何確保所有的節點都有同樣的鏈呢?這就是一致性問題,如果想要在我們的區塊鏈網絡中運行多個節點,我們就需要實現一個一致性算法。

註冊新節點

在實現一致性算法之前,我們需要找到一種方法讓一個節點知道它相鄰的節點。我們網絡中的每個節點都需要保存一份其它節點的記錄信息。因此讓我們增加一些接口:

1、/nodes/register 接收以url表示的新節點列表
2、/nodes/resolve 實現一致性算法,解決衝突,確保每個節點都有正確的鏈信息

我們需要修改Blockchain的構造函數,添加一個註冊節點的方法:

...
from urlparse 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]
            # 檢查block的hash值是否正確
            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)

        # Grab and verify the chains from all the nodes in our network
        for node in neighbours:
            response = requests.get('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()方法主要負責檢查鏈是否有效,歷遍鏈中的所有塊,檢查塊的hash值以及工作量證明是否正確。

resolve_conflicts()負責解決衝突,通過歷遍附近節點,通過valid_chain方法驗證該節點的鏈是否正確,如果該節點鏈正確且該鏈比我們節點的鏈長度長,則用該鏈替換我們的鏈。

讓我們添加兩個方法,一個用來註冊附近節點,一個用來解決衝突:

@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

運行兩個節點,並將第二節點註冊到第一個節點中:

register.png

然後在第二個節點(5001端口)挖了一些塊,確保該節點的鏈長度比第一個節點長。然後在第一個節點調用GET /nodes/resolve請求,可以看到節點1的鏈通過一致性算法被替換掉了。

resolve.png

嗯。。現在可以邀請你的朋友來測試你的區塊鏈了。可以到這裏查看Blockhain.py

參考:

learn-blockchains-by-building-one
工作量證明

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章