用200行Julia代碼開發(fā)一個區(qū)塊鏈

對,是標(biāo)題黨,民科的必備技能之一。至于為什么是標(biāo)題黨的原因后面你會看到。

事情的起因是Julia 1.0的發(fā)布。1.0出來也有些時間了,這段時間一直被人安利說他既有C++的性能,又像Python那樣開發(fā)友好,可以用來做科學(xué)計算、數(shù)據(jù)處理等等,讓人實在不可忽視。

而學(xué)習(xí)一個事情最好的方法就是使用它,所謂“以戰(zhàn)代練”。剛好以前看過一個用Python寫區(qū)塊鏈的例子,那么應(yīng)該也可以用Julia來開發(fā)一個區(qū)塊鏈試試看(Julia:我實在也不是謙虛。我一個科學(xué)計算的語言,怎么來開發(fā)區(qū)塊鏈了??)

所以標(biāo)題黨的原因之一在于,這次其中一個目的更多是學(xué)習(xí)Julia,而并不是開發(fā)一個區(qū)塊鏈。

這么鼓勵人造輪子……別鬧了,費曼先生 (圖片來源: https://github.com/danistefanovic/build-your-own-x)

“區(qū)塊鏈完備”

先劇透一下,這次我們仿照那個Python寫區(qū)塊鏈的樣例,在實現(xiàn)上不存在什么方向上的問題,最后實現(xiàn)了一個區(qū)塊鏈原型。在這過程中,除了學(xué)習(xí)了Julia語法之外,其實還有另外一個更為重要的感想:

以往評價一個編程語言功能是否完備,會說他的語法是否圖靈完備、是否適合企業(yè)級開發(fā)等等。我認(rèn)為以后也可以加上另一個標(biāo)準(zhǔn):是否能實現(xiàn)一個區(qū)塊鏈原型,即是否區(qū)塊鏈完備。如果可以,那么說明這個語言已經(jīng)具備了開發(fā)一個較為復(fù)雜系統(tǒng)的能力與生態(tài)環(huán)境,因為至少已包括:

  • 密碼學(xué)的庫或底層支持以保證安全
  • JSON等數(shù)據(jù)格式解析
  • 可用于生產(chǎn)開發(fā)的web框架

那么Julia如何?先放這次結(jié)論,Julia已經(jīng)具有區(qū)塊鏈完備的特性。

但是Julia 1.0和之前版本還是有較多升級,也有一些前后兼容性以及需要更好配套生態(tài)支持等問題,需要在開發(fā)中注意

0. 開發(fā)前的工作

開發(fā)環(huán)境

Julia 推薦使用Juno——基于Atom擴(kuò)展的編輯器。事實證明,這一說法是在現(xiàn)階段還是有道理的。

因為我平常用VS Code更多,所以一開始是找了一個Julia插件準(zhǔn)備來開發(fā)。但發(fā)現(xiàn)一直配置不成功,后來才找到原因是VS Code還沒支持Julia 0.7以后版本(https://github.com/JuliaEditorSupport/julia-vscode/issues/537

所以目前還是得老老實實的用Juno。這也從一個側(cè)面說明,目前Julia的一些生態(tài)支持還沒跟上

安裝

安裝過程按照官網(wǎng)指導(dǎo)還是比較順利:先安裝Julia,然后再安裝Atom上的Juno即可。所以詳細(xì)過程此處略過。

其他準(zhǔn)備

為了調(diào)試,也需要再準(zhǔn)備一個Postman或類似工具。

1. 搭建區(qū)塊鏈的整體結(jié)構(gòu)

這里我們先定義好Blockchain的類型,代表區(qū)塊鏈類型。其中會包含兩個數(shù)組:chain表示鏈;current_transactions表示待處理交易

mutable struct Blockchain
    chain::Array{Block}
    current_transaction::Array{Transaction}
end

function init(blockchain::Blockchain)
    # TODO
end

function new_block(;blockchain::Blockchain, proof::Int64, previous_hash = nothing)
    # TODO
end

function new_transaction(;blockchain::Blockchain, sender::String, recipient::String, amount::Float64)
    # TODO
end

function blockhash(block::Block)
    # TODO
end

當(dāng)時也不知道誰給我說過Julia語法很像Python。學(xué)了之后才發(fā)現(xiàn),至少從語法層面看一點都不像。

通過上述代碼也可以看到一些Julia特點:

  • 沒有class。Julia不算一種OOP語言,沒有自帶class這種類型。比較像的是struct
  • 可以不指定變量類型,交給Julia來推測,但指定后肯定會加快執(zhí)行速度。另外,像給函數(shù)參數(shù)也可以有一些默認(rèn)的值,但需要放在;之后

2. 區(qū)塊與交易

有了鏈之后,我們繼續(xù)來定義區(qū)塊和交易。每個區(qū)塊的結(jié)構(gòu)中主要會包括:

  • 區(qū)塊序號
  • 時間戳,用Unix時間表示
  • 交易列表,本區(qū)塊內(nèi)打包的交易數(shù)量
  • 工作量證明
  • 前一個區(qū)塊的哈希值

而每個交易的結(jié)構(gòu)定義的比較簡單,只有以下3部分:

  • 交易發(fā)送者
  • 交易接受者
  • 交易金額

具體代碼實現(xiàn)如下:

struct Transaction
    sender::String
    recipient::String
    amount::Float64
end

mutable struct Block
    index::Int64
    timestamp::Int64
    transaction_list::Array{Transaction}
    proof::Int64
    previous_hash::String
end

通過上述的格式,我們就可以得到一個比較典型的塊鏈?zhǔn)綌?shù)據(jù)結(jié)構(gòu)了。

3. 新增交易

有了基本的數(shù)據(jù)結(jié)構(gòu)之后,下面我們從上述數(shù)據(jù)的最小單元開始來定義操作。

首先是新增交易。區(qū)塊鏈上的交易一般要在待處理的交易隊列里追加進(jìn)去,并在函數(shù)最后返回將打包該交易的區(qū)塊。

function new_transaction(;blockchain::Blockchain, sender::String, recipient::String, amount::Float64)
    new_tx = Transaction(sender, recipient, amount)
    push!(blockchain.current_transaction, new_tx)
    return length(blockchain.chain) + 1
end

這部分的Julia特點(坑)包括:

  • 要追加元素的話,要記得用push這個函數(shù),而不是append。因為Julia里的append其實是concatenate……
  • 函數(shù)名后面的感嘆號意思是允許函數(shù)修改傳入的參數(shù)。所以在上面的例子里,我們必須得用push!

4. 新增區(qū)塊

當(dāng)需要處理交易時,就要將交易打包進(jìn)區(qū)塊了。而在能新增區(qū)塊之前,得先生成一個創(chuàng)世區(qū)塊。由于只需要生成一次創(chuàng)世區(qū)塊,這一工作可以在初始化時來完成。

初始化

如果是在一般的OOP里,初始化函數(shù)可以在對象的constructor里寫好;但Julia里沒有這種機制,還是需要單獨寫一個初始化函數(shù)來做這部分工作,包括:

  • 初始化鏈
  • 初始化待處理的交易列表
  • 新增創(chuàng)世區(qū)塊。其中為了統(tǒng)一起見,創(chuàng)世區(qū)塊也仍然需要工作量證明,也需要定義一個前區(qū)塊哈希。在本程序里我們不妨將兩者定義為100和"genesis_hash"
function init(blockchain::Blockchain)
    blockchain.chain = []
    blockchain.current_transaction = []
    new_block(blockchain=blockchain, proof=100, previous_hash="genesis_block_hash")
end

新增區(qū)塊

上面代碼里的new_block()這個函數(shù)就是我們用來新增區(qū)塊的。參數(shù)中的前一個區(qū)塊哈??刹惠斎耄J(rèn)為鏈中最后一個區(qū)塊的哈希值;新增區(qū)塊只要按照上面定義的區(qū)塊數(shù)據(jù)結(jié)構(gòu)進(jìn)行填寫即可。最后把這個區(qū)塊追加到目前區(qū)塊的鏈中,并清空待處理交易列表,實現(xiàn)如下:

function new_block(;blockchain::Blockchain, proof::Int64, previous_hash = nothing)
    if previous_hash == nothing
        previous_hash = blockhash(blockchain.chain[end])
    end
    block = Block(length(blockchain.chain)+1, round(Int64, time()), blockchain.current_transaction, proof, previous_hash)

    push!(blockchain.chain, block)
    blockchain.current_transaction = []
    return block
end

上面這段的Julia特點包括:

  • nothing,Julia里用來空值的一個常量
  • Julia和其他很多科學(xué)計算類語言一樣,下標(biāo)是從1開始的
  • end,表示數(shù)組中的最后一個元素下標(biāo)。Julia不支持Python那種負(fù)數(shù)下標(biāo),所以相對而言不是那么靈活

哈希值的計算

在增加區(qū)塊的時候還有一個要注意的是,需要計算最后一個區(qū)塊的哈希值。這時候需要借助一些額外的包來完成了,這里用到了SHA和JSON:JSON用來將一個結(jié)構(gòu)化的變量拍平為json格式;而SHA是根據(jù)這個json文本生成對應(yīng)的哈希值,具體如下

using SHA, JSON
...
function blockhash(block::Block)
    return bytes2hex(sha256(JSON.json(block)))
end

增加包,在1.0里面在REPL環(huán)境中,輸入],進(jìn)入pkg環(huán)境。pkg是Julia自帶的包管理器,輸入add命令即可,例如:

(v1.0) pkg> add JSON

也可以在REPL中通過下面這種方式來實現(xiàn)

using Pkg
Pkg.add("JSON")

另外,值得注意的是,Julia是支持函數(shù)式編程的,所以上面這個嵌套了多層的哈希函數(shù)完全可以用一種更現(xiàn)代化的形式來實現(xiàn)。

5. PoW

工作量證明,Proof of Work,是最為經(jīng)典的生成新區(qū)塊并可產(chǎn)生共識的一個協(xié)議。比特幣里采用的方式是碰撞到一種特定的哈希值,例如包含了若干個連續(xù)0的哈希值;位數(shù)越多,難度越大。這里我們也用類似的方式來實現(xiàn)。

程序片段如下:

...
difficulty = Int8(4)
...
function proof_of_work(last_proof)
    proof = 0
    while valid_proof(last_proof, proof) == false
        proof += 1
    end
    return proof
end

function valid_proof(last_proof, proof)
    header = "0"^difficulty
    guess = string(last_proof) * string(proof)
    guess_hash = bytes2hex(sha256(guess))
    return guess_hash[1:difficulty] == header
end

這段代碼里,我們先假定難度是4個0。在proof_of_work()這個函數(shù)里,我們循環(huán)去嘗試nonce是否可以符合我們區(qū)塊的要求,符合的話即返回該值,提供給產(chǎn)生區(qū)塊的代碼去打包。

驗證難度值的函數(shù)則根據(jù)嘗試的nonce來驗證是否符合我們4個0的要求。

這段Julia代碼和字符串、數(shù)組操作都比較相關(guān):

  • 要生成重復(fù)元素的時候,可以使用^這個操作符。例如例子里的"0"^difficulty
  • 連接字符串,可以使用*
  • 獲取數(shù)組的一部分元素可以使用冒號,例如:guess_hash[1:difficulty]

6. 對外API接口

光定義好了函數(shù)還不夠,還要能讓外部(客戶端、其他節(jié)點等)可以調(diào)用。在我們參考的Python實現(xiàn)中使用的是比較流行的Flask框架,實現(xiàn)了幾個必要的Restful接口。我們這里也是類似的,首先需要有以下幾個接口:

  • POST /transactions/new給區(qū)塊創(chuàng)建一個新交易
  • GET /mine挖礦(產(chǎn)生區(qū)塊)
  • GET /chain返回當(dāng)前所有的區(qū)塊數(shù)據(jù)

Julia里其實也已經(jīng)有不少web框架了,最有名的是Genie。目前Genie似乎還沒有加入到官方包管理器索引中,如果要使用的話需要手動輸入地址來添加:Pkg.clone("https://github.com/essenciary/Genie.jl")

但Genie實在太重,包含了太多不必要的程序部分。我們在實現(xiàn)區(qū)塊鏈的過程中只要關(guān)心對外的幾個Restful接口就夠了,因此我后來直接使用了Restful這個包,實現(xiàn)輕量化的功能即可

...
using Restful, Logging
using UUIDs
...
const app = Restful.app()

    blockchain = Blockchain([], [], [])
    node_identifier = replace(string(uuid4()), "-" => "")
    init(blockchain)
    
    app.get("/mine", json) do req, res, route
    end

    app.post("/transactions/new", json) do req, res, route
    end

    app.get("/chain", json) do req, res, route
        res.json(Dict("chain"=>blockchain.chain, "length"=>length(blockchain.chain)) |> collect)
    end

    @async with_logger(SimpleLogger(stderr, Logging.Warn)) do
        app.listen("127.0.0.1", 3001)
    end

其中UUID是用來生成一個唯一的ID號用來標(biāo)識本節(jié)點身份

交易接口

首先,我們按照交易變量的結(jié)構(gòu)定義好報文接口:

{
 "sender": "付款人地址",
 "recipient": "收款人地址",
 "amount": 金額
}

接著用post方式來實現(xiàn)這一接口:

    app.post("/transactions/new", json) do req, res, route
        required = ["sender", "recipient", "amount"]
        data = JSON.parse(req.body)

        if all(i->(i in keys(data)), required) == false
            res.code(400)
        else
            block_id = new_transaction(blockchain=blockchain, sender=data["sender"], recipient=data["recipient"], amount=data["amount"])
            res.json(Dict("message" => "Transaction will be added to Block " * string(block_id)) |> collect)
        end
    end

從上面我們也可以看到:Julia是支持匿名函數(shù)的。所以這里我也用這個特性進(jìn)行了快速判斷JSON參數(shù)

挖礦接口

    app.get("/mine", json) do req, res, route
        last_block = blockchain.chain[end]
        last_proof = last_block.proof
        proof = proof_of_work(last_proof)

        new_transaction(blockchain=blockchain, sender="0", recipient=node_identifier, amount=1.0)

        previous_hash = blockhash(last_block)
        block = new_block(blockchain=blockchain, proof=proof, previous_hash=previous_hash)
        res.json(Dict("message" => "New Block Forged",
        "index" => block.index,
        "transaction" => block.transaction_list,
        "proof" => block.proof,
        "previous_hash" => block.previous_hash) |> collect)
    end

在打包交易進(jìn)區(qū)塊時,首先會進(jìn)行大量哈希計算。得到nonce值后,在本案例中,我們再多追加一個發(fā)送者為0、接收者為本節(jié)點、金額為1的特殊交易。這其實就是表示挖礦獎勵:

  • 發(fā)送者為0,表示是一筆沒有發(fā)送者的新增金額,即比特幣中的coinbase。
  • 接收者是本出塊節(jié)點,代表挖礦獎勵應(yīng)計入到本節(jié)點的賬戶中
  • 挖礦金額獎勵為1。這個是簡化后的固定值,當(dāng)然也可以仿照比特幣每4年減半來設(shè)置一個更復(fù)雜的運算。

最后,再調(diào)用之前我們定義好的new_block函數(shù),完成區(qū)塊生成的全部過程。

7. 與其他節(jié)點通信

以上為止,我們的區(qū)塊鏈實際上還是單機版的,并沒有涉及到與其他節(jié)點通訊的部分。這顯然不符合實際情況,需要加上。

using HTTP, URIParser
...
mutable struct Blockchain
    ...
    nodes::Array{String}
end
...
function register_node(blockchain::Blockchain, address::String)
    push!(blockchain.nodes, address)
end
...
app.post("/nodes/register", json) do req, res, route
    data = JSON.parse(req.body)
    nodes = data["nodes"]

    if nodes == nothing
        res.code(400)
    end
    
    for node in nodes
        register_node(blockchain, node)
    end
    
    res.json(Dict("message" => "Nodes will be added to Blockchain") |> collect)
end

這里,我們在blockchain的數(shù)據(jù)結(jié)構(gòu)上增加登記節(jié)點列表,以最簡單粗暴的方式來實現(xiàn)“節(jié)點發(fā)現(xiàn)”的過程。

目前只有增加功能,因此只寫了一個函數(shù),將節(jié)點信息push進(jìn)這個節(jié)點列表中,最后以POST /nodes/register的接口來提供外部調(diào)用

8. 用最長鏈的方式進(jìn)行共識

有了不同節(jié)點之后就可以進(jìn)行PoW競爭記賬了。雖然PoW已經(jīng)是一個比較具體的方法,但在實現(xiàn)上仍有比較多的策略,例如比特幣是采用最長鏈方式、以太坊采用的是“最重子樹”(Heaviest Subtree)的方式。這里我們使用的是類似比特幣的只比較長度的簡單方式。

function valid_chain(chain::Array{Block})
    last_block = chain[end]
    current_index = 1
    while current_index < length(chain)
        block = chain[current_index]
        if block.previous_hash != blockhash(last_block)
            return false
        end

        if valid_proof(last_block.proof, block.proof) == false
            return false
        end

        last_block = block
        current_index += 1
    end

    return true
end

function resolve_conflict(blockchain::Blockchain)
    neighbours = blockchain.nodes
    new_chain = nothing

    my_height = length(blockchain.chain)

    for node in neighbours
        response = HTTP.request("GET", "http://" * node * "/chain")

        if response.status == 200
            response_body = JSON.Parser.parse(String(response.body))
            height = response_body[1]["length"]
            chain = response_body[2]["chain"]

            if height > my_height && valid_chain(chain)
                my_height = height
                new_chain = chain
            end
        end
    end

    if new_chain != nothing
        blockchain.chain = new_chain
        return true
    end

    return false
end
...
app.get("/nodes/resolve", json) do req, res, route
    replaced = resolve_conflict(blockchain)
    if replaced == true
        res.json(Dict("message" => "Our chain was replaced") |> collect)
    else
        res.json(Dict("message" => "Our chain is main chain") |> collect)
    end
end

以上代碼中,我們定義了一個接口GET /nodes/resolve來解決一致性問題,具體方式是調(diào)用到resolve_conflict函數(shù),遍歷所有已登記的節(jié)點,找到他們現(xiàn)在的鏈上區(qū)塊,比較其長度,并通過valid_chain函數(shù)來檢驗其區(qū)塊有效性。

而檢驗方法是通過哈希與nonce值計算后的比較進(jìn)行,由于哈希運算的單向特性,所以這部分的運算也比較快。檢驗完成后,如果是長度比本節(jié)點自己的鏈上而且是有效的,則進(jìn)行替換,在這個新鏈的基礎(chǔ)上繼續(xù)挖礦,完成達(dá)成一致性的過程。

9. 總結(jié)展望

以上,我們已基本完成一個區(qū)塊鏈的原型:

  • 可以收發(fā)交易
  • 可以PoW共識

具體代碼可在 https://github.com/HuLaTown/blockchain 上找到。

可以看到,經(jīng)過上述開發(fā),我們可以基本分析出一個開發(fā)語言的功能與生態(tài)情況,雖然“區(qū)塊鏈完備”這個概念目前只是我拍腦袋的想法,還沒有一個可以定量化的標(biāo)準(zhǔn)。

另外經(jīng)過區(qū)塊鏈原型開發(fā),也可以快速熟悉一個開發(fā)語言的基本特性,包括變量類型、空值等特殊取值、字符串與數(shù)組處理、函數(shù)傳參與修改、函數(shù)間的相互調(diào)用等語言特性,當(dāng)然也不可避免的要用工具來debug,這些都對于后續(xù)用該語言開發(fā)會很有幫助。

但還有很多地方可以繼續(xù)開發(fā)。這也是為啥是標(biāo)題黨的原因之二。這些要改進(jìn)的地方至少包括:

  • 代碼可能還有一些未發(fā)現(xiàn)的bug
  • 需要進(jìn)行很多的輸入邏輯校驗判斷
  • 需要Kademlia等DHT節(jié)點維護(hù)功能
  • 需要增加密碼學(xué)身份認(rèn)證,例如公私鑰等
  • 需要增加UTXO或賬戶的賬本結(jié)構(gòu)
  • 需要增加智能合約支持
    等等……
第5步就留作作業(yè)

參考資料

  1. Learn Blockchains by Building One https://hackernoon.com/learn-blockchains-by-building-one-117428612f46
  2. Julia 1.0 Documentation https://docs.julialang.org/en/v1/
  3. https://learnxinyminutes.com/docs/julia/
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容