對,是標(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ū)塊鏈。

“區(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)
- 需要增加智能合約支持
等等……

參考資料
- Learn Blockchains by Building One https://hackernoon.com/learn-blockchains-by-building-one-117428612f46
- Julia 1.0 Documentation https://docs.julialang.org/en/v1/
- https://learnxinyminutes.com/docs/julia/