Python實現一條基於POS演算法的區塊鏈

Tiny熊發表於2018-08-07

區塊鏈中的共識演算法

在比特幣公鏈架構解析中,就曾提到過為了實現去中介化的設計,比特幣設計了一套共識協議,並通過此協議來保證系統的穩定性和防攻擊性。 並且我們知道,截止目前使用最廣泛,也是最被大家接受的共識演算法,是我們先前介紹過的POW(proof of work)工作量證明演算法。目前市值排名前二的比特幣和以太坊也是採用的此演算法。

雖然POW共識演算法取得了巨大的成功,但對它的質疑也從來未曾停止過。 其中最主要的一個原因就是電力消耗。據不完全統計,基於POW的挖礦機制所消耗的電量是非常巨大的,甚至比絕大多數國家耗電量還要多。這對我們的資源造成了極大的浪費,此外隨著位元大陸等公司的強勢崛起,造成了算力的高度集中。

基於以上種種原因,更多的共識演算法被提出來 POS、DPOS、BPFT等等。 今天我們就來認識POS(proof of stake)演算法。

Proof of stake,譯為權益證明。你可能已經猜到了,權益證明簡單理解就是擁有更多token的人,有更大的概率獲得記賬權利,然後獲得獎勵。 這個概率具體有多大呢? 下面我們在程式碼實現中會展示,分析也放在後面。 當然,POS是會比POW更好嗎? 會更去中心化嗎? 現在看來未必,所以我們這裡也不去對比誰優誰劣。 我們站在中立的角度,單純的來討論討論POS這種演算法。

程式碼實戰

生成一個Block

既然要實現POS演算法,那麼就難免要生成一條鏈,鏈又是由一個個Block生成的,所以下面我們首先來看看如何生成Block,當然在前面的內容裡面,關於如何生成Block,以及交易、UTXO等等都已經介紹過了。由於今天我們的核心是實現POS,所以關於Block的生成,我們就用最簡單的實現方式,好讓大家把目光聚焦在核心的內容上面。

我們用三個方法來實現生成一個合法的區塊

  • calculate_hash 計算區塊的hash值
  • is_block_valid 校驗區塊是否合法
  • generate_block 生成一個區塊

from hashlib import sha256
from datetime import datetime

def generate_block(oldblock, bpm, address):
    """

    :param oldblock:
    :param bpm:
    :param address:
    :return:
    """
    newblock = {
        "Index": oldblock["Index"] + 1,
        "BPM": bpm,
        "Timestamp": str(datetime.now()),
        "PrevHash": oldblock["Hash"],
        "Validator": address
    }

    newblock["Hash"] = calculate_hash(newblock)
    return newblock


def calculate_hash(block):
    record = "".join([
        str(block["Index"]),
        str(block["BPM"]),
        block["Timestamp"],
        block["PrevHash"]
    ])

    return sha256(record.encode()).hexdigest()


def is_block_valid(newblock, oldblock):
    """

    :param newblock:
    :param oldblock:
    :return:
    """

    if oldblock["Index"] + 1 != newblock["Index"]:
        return False

    if oldblock["Hash"] != newblock["PrevHash"]:
        return False

    if calculate_hash(newblock) != newblock["Hash"]:
        return False

    return True

這裡為了更靈活,我們沒有用類的實現方式,直接採用函式來實現了Block生成,相信很容易看懂。

建立一個TCP伺服器

由於我們需要用權益證明演算法來選擇記賬人,所以需要從很多Node(節點)中選擇記賬人,也就是需要一個server讓節點連結上來,同時要同步資訊給節點。因此需要一個TCP長連結。

from socketserver import BaseRequestHandler, ThreadingTCPServer

def run():
    # start a tcp server
    serv = ThreadingTCPServer(('', 9090), HandleConn)
    serv.serve_forever()

在這裡我們用了python內庫socketserver來建立了一個TCPServer。 需要注意的是,這裡我們是採用的多執行緒的建立方式,這樣可以保證有多個客戶端同時連線上來,而不至於被阻塞。當然,這裡這個server也是存在問題的,那就是有多少個客戶端連線,就會建立多少個執行緒,更好的方式是建立一個執行緒池。由於這裡是測試,所以就採用更簡單的方式了。

相信大家已經看到了,在我們建立TCPServer的時候,使用到了HandleConn,但是我們還沒有定義,所以接下來我們就來定義一個HandleConn

訊息處理器

下面我們來實現Handler函式,Handler函式在跟Client Node通訊的時候,需要我們的Node實現下面的功能

  • Node可以輸入balance(token數量) 也就是股權數目
  • Node需要能夠接收廣播,方便Server同步區塊以及記賬人資訊
  • 新增自己到候選人名單 (候選人為持有token的人)
  • 輸入BPM生成Block
  • 驗證一個區塊的合法性

感覺任務還是蠻多的,接下來我們看程式碼實現

import threading
from queue import Queue, Empty

# 定義變數
block_chain = []
temp_blocks = []
candidate_blocks = Queue()  # 建立佇列,用於執行緒間通訊
announcements = Queue()
validators = {}

My_Lock = threading.Lock()

class HandleConn(BaseRequestHandler):
    def handle(self):
        print("Got connection from", self.client_address)

        # validator address
        self.request.send(b"Enter token balance:")
        balance = self.request.recv(8192)
        try:
            balance = int(balance)
        except Exception as e:
            print(e)

        t = str(datetime.now())
        address = sha256(t.encode()).hexdigest()
        validators[address] = balance
        print(validators)

        while True:
            announce_winner_t = threading.Thread(target=annouce_winner, args=(announcements, self.request,),
                                                 daemon=True)
            announce_winner_t.start()

            self.request.send(b"\nEnter a new BPM:")
            bpm = self.request.recv(8192)
            try:
                bpm = int(bpm)
            except Exception as e:
                print(e)
                del validators[address]
                break

            # with My_Lock:
            last_block = block_chain[-1]

            new_block = generate_block(last_block, bpm, address)

            if is_block_valid(new_block, last_block):
                print("new block is valid!")
                candidate_blocks.put(new_block)

            self.request.send(b"\nEnter a new BPM:\n")

            annouce_blockchain_t = threading.Thread(target=annouce_blockchain, args=(self.request,), daemon=True)
            annouce_blockchain_t.start()

這段程式碼,可能對大多數同學來說是有難度的,在這裡我們採用了多執行緒的方式,同時為了能夠讓訊息線上程間通訊,我們使用了佇列。 這裡使用佇列,也是為了我們的系統可以更好的擴充,後面如果可能,這一節的程式很容易擴充為分散式系統。 將多執行緒裡面處理的任務拆分出去成獨立的服務,然後用訊息佇列進行通訊,就是一個簡單的分散式系統啦。(是不是很激動?)

由於這裡有難度,所以程式碼還是講一講吧

    # validator address
        self.request.send(b"Enter token balance:")
        balance = self.request.recv(8192)
        try:
            balance = int(balance)
        except Exception as e:
            print(e)

        t = str(datetime.now())
        address = sha256(t.encode()).hexdigest()
        validators[address] = balance
        print(validators)

這一段就是我們提到的Node 客戶端新增自己到候選人的程式碼,每連結一個客戶端,就會新增一個候選人。 這裡我們用新增的時間戳的hash來記錄候選人。 當然也可以用其他的方式,比如我們程式碼裡面的client_address


announce_winner_t = threading.Thread(target=annouce_winner, args=(announcements, self.request,),
                                                daemon=True)
        announce_winner_t.start()

def annouce_winner(announcements, request):
    """

    :param announcements:
    :param request:
    :return:
    """
    while True:
        try:
            msg = announcements.get(block=False)
            request.send(msg.encode())
            request.send(b'\n')
        except Empty:
            time.sleep(3)
            continue

然後接下來我們起了一個執行緒去廣播獲得記賬權的節點資訊到所有節點。

self.request.send(b"\nEnter a new BPM:")
            bpm = self.request.recv(8192)
            try:
                bpm = int(bpm)
            except Exception as e:
                print(e)
                del validators[address]
                break

            # with My_Lock:
            last_block = block_chain[-1]

            new_block = generate_block(last_block, bpm, address)

            if is_block_valid(new_block, last_block):
                print("new block is valid!")
                candidate_blocks.put(new_block)

根據節點輸入的BPM值生成一個區塊,並校驗區塊的有效性。 將有效的區塊放到候選區塊當中,等待記賬人將區塊新增到鏈上。

annouce_blockchain_t = threading.Thread(target=annouce_blockchain, args=(self.request,), daemon=True)
        annouce_blockchain_t.start()

def annouce_blockchain(request):
    """

    :param request:
    :return:
    """
    while True:
        time.sleep(30)
        with My_Lock:
            output = json.dumps(block_chain)
        try:
            request.send(output.encode())
            request.send(b'\n')
        except OSError:
            pass

最後起一個執行緒,同步區塊鏈到所有節點。

看完了,節點跟Server互動的部分,接下來是最重要的部分,

POS演算法實現

def pick_winner(announcements):
    """
    選擇記賬人
    :param announcements:
    :return:
    """
    time.sleep(10)

    while True:
        with My_Lock:
            temp = temp_blocks

        lottery_pool = []  #

        if temp:
            for block in temp:
                if block["Validator"] not in lottery_pool:
                    set_validators = validators
                    k = set_validators.get(block["Validator"])
                    if k:
                        for i in range(k):
                            lottery_pool.append(block["Validator"])

            lottery_winner = choice(lottery_pool)
            print(lottery_winner)
            # add block of winner to blockchain and let all the other nodes known
            for block in temp:
                if block["Validator"] == lottery_winner:
                    with My_Lock:
                        block_chain.append(block)

                    # write message in queue.
                    msg = "\n{0} 贏得了記賬權利\n".format(lottery_winner)
                    announcements.put(msg)

                    break

        with My_Lock:
            temp_blocks.clear()

這裡我們用pick_winner 來選擇記賬權利,我們根據token數量構造了一個列表。 一個人獲得記賬權利的概率為:

p = mount['NodeA']/mount['All']

文字描述就是其token數目在總數中的佔比。 比如總數有100個,他有10個,那麼其獲得記賬權的概率就是0.1, 到這裡核心的部分就寫的差不多了,接下來,我們來新增節點,開始測試吧

測試POS的記賬方式

在測試之前,起始還有一部分工作要做,前面我們的run方法需要完善下,程式碼如下:

def run():
    # create a genesis block
    t = str(datetime.now())
    genesis_block = {
        "Index": 0,
        "Timestamp": t,
        "BPM": 0,
        "PrevHash": "",
        "Validator": ""
    }

    genesis_block["Hash"] = calculate_hash(genesis_block)
    print(genesis_block)
    block_chain.append(genesis_block)

    thread_canditate = threading.Thread(target=candidate, args=(candidate_blocks,), daemon=True)
    thread_pick = threading.Thread(target=pick_winner, args=(announcements,), daemon=True)

    thread_canditate.start()
    thread_pick.start()

    # start a tcp server
    serv = ThreadingTCPServer(('', 9090), HandleConn)
    serv.serve_forever()

def candidate(candidate_blocks):
    """

    :param candidate_blocks:
    :return:
    """
    while True:
        try:
            candi = candidate_blocks.get(block=False)
        except Empty:
            time.sleep(5)
            continue
        temp_blocks.append(candi)

if __name__ == '__main__':
    run()

新增節點連線到TCPServer

為了充分減少程式的複雜性,tcp client我們這裡就不實現了,可以放在後面擴充部分。 畢竟我們這個系統是很容易擴充套件的,後面我們拆分了多執行緒的部分,在實現tcp client就是一個完整的分散式系統了。

所以,我們這裡用linux自帶的命令 nc,不知道nc怎麼用的同學可以google或者 man nc
Python實現一條基於POS演算法的區塊鏈

  • 啟動服務 執行 python pos.py
  • 開啟3個終端
  • 分別輸入下面命令
    • nc localhost 9090

終端如果輸出

Enter token balance: 

說明你client已經連結伺服器ok啦.

測試POS的記賬方式

接下來依次按照提示操作。 balance可以按心情來操作,因為這裡是測試,我們輸入100,
緊接著會提示輸入BPM,我們前面提到過,輸入BPM是為了生成Block,那麼就輸入吧,隨便輸入個9. ok, 接下來就稍等片刻,等待記賬。
輸出如同所示
Python實現一條基於POS演算法的區塊鏈
依次在不同的終端,根據提示輸入數字,等待訊息同步。

生成區塊鏈

下面是我這邊獲得的3個block資訊。
Python實現一條基於POS演算法的區塊鏈

總結

在上面的程式碼中,我們實現了一個完整的基於POS演算法記賬的鏈,當然這裡有許多值得擴充套件與改進的地方。

  • python中多執行緒開銷比較大,可以改成協程的方式
  • TCP建立的長連結是基於TCPServer,是中心化的方式,可以改成P2P對等網路
  • 鏈的資訊不夠完整
  • 系統可以擴充成分散式,讓其更健壯

大概列了以上幾點,其他還有很多可以擴充的地方,感興趣的朋友可以先玩玩, 後者等到我們後面的教程。 (廣告打的措手不及,哈哈)

當然了,語言不是重點,所以在這裡,我也實現了go語言的版本原始碼地址

go語言的實現感覺要更好理解一點,也顯得要優雅一點。這也是為什麼go語言在分散式領域要更搶手的原因之一吧!

專案地址

  • https://github.com/csunny/py-bitcoin/

參考

  • https://medium.com/@mycoralhealth/code-your-own-proof-of-stake-blockchain-in-go-610cd99aa658

本文來自深入淺出區塊鏈作者Magic_陳,他的專欄專注區塊鏈底層技術開發,P2P網路、加密技術、MerkleTree、DAG、DHT等等,另外對於分散式系統的學習也很有幫助。歡迎大家交流。
深入淺出區塊鏈 - 系統學習區塊鏈,打造最好的區塊鏈技術部落格,歡迎大家一起加入。

相關文章