本文參考
- Daniel Van Flymen 的 “Learn Blockchains by Building One”
- Mohammad Azam 的文章
- 《比特幣開發者指南 專有名詞》
我在上一篇文章中討論瞭如何用 Swift 語言實現基本的區塊鏈。在這篇文章裡會使用伺服器端 Swift 框架 Vapor 在雲端實現區塊鏈。通過 HTTP 協議來構建區塊鏈 Web API,使用不同的路由來提供必要的功能。閱讀本文需要在電腦上安裝 Vapor 框架,還需要對 Swift 語言有基本的瞭解。
實現模型
第一步是為區塊鏈 Web API 建立必要的模型,如下所示。
Block:Block(區塊)類表示一個區塊,包含交易的輸入和輸出。
class Block: Codable {
var index: Int = 0
var dateCreated: String
var previousHash: String!
var hash: String!
var nonce: Int
var message: String = ""
private (set) var transactions: [Transaction] = [Transaction]()
var key: String {
get {
let transactionsData = try! JSONEncoder().encode(self.transactions)
let transactionsJSONString = String(data: transactionsData, encoding: .utf8)
return String(self.index) + self.dateCreated + self.previousHash + transactionsJSONString! + String(self.nonce)
}
}
func addTransaction(transaction: Transaction) {
self.transactions.append(transaction)
}
init() {
self.dateCreated = Date().toString()
self.nonce = 0
self.message = "挖出新的區塊"
}
init(transaction: Transaction) {
self.dateCreated = Date().toString()
self.nonce = 0
self.addTransaction(transaction: transaction)
}
}
複製程式碼
Block 類的屬性解釋如下:
- index——區塊位於區塊鏈中的位置。index 為 0 則表示該區塊是區塊鏈中的第一個區塊。index 為 1 則表示區塊鏈中的第二個區塊……以此類推!
- dateCreated——區塊建立的日期
- previousHash——前一個區塊的雜湊值
- hash——當前區塊的雜湊值
- message——每個區塊的備忘說明。只是為了例子使用
- nonce——遞增的數字,對生成雜湊值很關鍵
- transactions——一系列交易。每筆交易都代表貨物/價值的轉移
- key——計算屬性,提供給產生雜湊值的函式
Transaction:Transaction(交易)由 sender(傳送者)、recipient(接收者)和被轉移的 amount(金額)組成。實現如下:
class Transaction: Codable {
var from: String
var to: String
var amount: Double
init(from: String, to: String, amount: Double) {
self.from = from
self.to = to
self.amount = amount
}
init?(request: Request) {
guard let from = request.data["from"]?.string, let to = request.data["to"]?.string, let amount = request.data["amount"]?.double else {
return nil
}
self.from = from
self.to = to
self.amount = amount
}
}
複製程式碼
Transaction 類的實現很直觀。由 from、to 和 amount 欄位組成。為了簡單起見,from 和 to 欄位會用虛擬名字來表示,在實際中這兩個欄位還會包含包(wallet)ID 。
Blockchain:Blockchain(區塊鏈)類是表示區塊列表的主類。每個區塊都指向鏈中的前一個區塊。每個區塊可以包含多筆交易,表示信貸或借記。
class Blockchain: Codable {
var blocks: [Block] = [Block]()
init() {
}
init(_ genesisBlock: Block) {
self.addBlock(genesisBlock)
}
func addBlock(_ block: Block) {
if self.blocks.isEmpty {
// 新增創世區塊
// 第一個區塊沒有 previous hash
block.previousHash = "0"
} else {
let previousBlock = getPreviousBlock()
block.previousHash = previousBlock.hash
block.index = self.blocks.count
}
block.hash = generateHash(for: block)
self.blocks.append(block)
block.message = "此區塊已新增至區塊鏈"
}
private func getPreviousBlock() -> Block {
return self.blocks[self.blocks.count - 1]
}
private func displayBlock(_ block: Block) {
print("------ 第 (block.index) 個區塊 --------")
print("建立日期:(block.dateCreated)")
// print("資料:(block.data)")
print("Nonce:(block.nonce)")
print("前一個區塊的雜湊值:(block.previousHash!)")
print("雜湊值:(block.hash!)")
}
private func generateHash(for block: Block) -> String {
var hash = block.key.sha256()!
// 設定工作量證明
while(!hash.hasPrefix(DIFFICULTY)) {
block.nonce += 1
hash = block.key.sha256()!
print(hash)
}
return hash
}
}
複製程式碼
每個模型都遵循 Codable 協議,以便轉換為 JSON 物件。如果你看了上一篇文章的話,上面的實現方式就很眼熟了。下一步是為 Web API 配置路由,後面一節會用 Vapor 框架來實現。
使用 Vapor 實現 Web API
有幾種不同方式來用 Vapor 實現 Web API 。我在這裡會建立一個自定義的控制器來處理所有區塊鏈請求,這樣就不用把所有程式碼都塞進 Routes 類裡了。BlockchainController 實現如下:
class BlockchainController {
private (set) var drop: Droplet
private (set) var blockchainService: BlockchainService!
init(drop: Droplet) {
self.drop = drop
self.blockchainService = BlockchainService()
// 為控制器設定路由
setupRoutes()
}
private func setupRoutes() {
self.drop.get("mine") { request in
let block = Block()
self.blockchainService.addBlock(block)
return try JSONEncoder().encode(block)
}
// 新增新交易
self.drop.post("transaction") { request in
if let transaction = Transaction(request: request) {
// 新增交易至區塊
// 獲得最後一個挖出的區塊
let block = self.blockchainService.getLastBlock()
block.addTransaction(transaction: transaction)
return try JSONEncoder().encode(block)
}
return try JSONEncoder().encode(["message": "發生異常!"])
}
// 獲得鏈
self.drop.get("blockchain") { request in
if let blockchain = self.blockchainService.getBlockchain() {
return try JSONEncoder().encode(blockchain)
}
return try! JSONEncoder().encode(["message":"區塊鏈尚未初始化。請先挖礦"])
}
}
}
複製程式碼
Web API 從三個基本的 endpoint 開始。
- Mining(挖礦):這個 endpoint 會啟動挖礦程式。挖礦可以讓我們達到工作量證明,然後將區塊新增到區塊鏈。
- Transaction:這個 endpoint 用於新增新交易。交易包含有關傳送者、接收者和金額的資訊。
- Blockchain:這個 endpoint 返回完整的區塊鏈。
BlockchainController 使用 BlockChainService 來執行所需操作。BlockChainService 的實現如下:
import Foundation
import Vapor
class BlockchainService {
typealias JSONDictionary = [String:String]
private var blockchain: Blockchain = Blockchain()
init() {
}
func addBlock(_ block: Block) {
self.blockchain.addBlock(block)
}
func getLastBlock() -> Block {
return self.blockchain.blocks.last!
}
func getBlockchain() -> Blockchain? {
return self.blockchain
}
}
複製程式碼
下面我們就來檢查一下 Web API 的 endpoint。啟動 Vapor 伺服器然後傳送請求到 “mine” endpoint。
工作量證明演算法生成了以“000”開頭的雜湊值。區塊被挖出後就立即轉換為 JSON 格式返回回來。通過 Swift 4.0 的 Codable 協議實現。
現在給區塊鏈新增一筆簡單的交易,從張嘉夫那裡轉移10美元給馬雲。
最後一步是檢查區塊鏈是否含有新新增的區塊。訪問 “blockchain” endpoint 來檢視完整的鏈。
完美!我們的區塊鏈 Web API 現在可以正常工作了。
還有一點遺憾的是,區塊鏈應該是去中心化的,但目前我們沒有新增新節點的機制。在下一節我們會更新區塊鏈實現以便讓其支援多個節點。
給區塊鏈新增節點
在給區塊鏈新增節點之前,首先要定義節點。節點模型的實現如下:
class BlockchainNode :Codable {
var address :String
init(address :String) {
self.address = address
}
init?(request :Request) {
guard let address = request.data["address"]?.string else {
return nil
}
self.address = address
}
}
複製程式碼
BlockChainNode 類很簡單,只有一個 address 屬性,用於標識節點伺服器的 URL。然後更新 BlockchainController 來新增註冊新節點功能。如下所示:
self.drop.get("nodes") { request in
return try JSONEncoder().encode(self.blockchainService.getNodes())
}
self.drop.post("nodes/register") { request in
guard let blockchainNode = BlockchainNode(request: request) else {
return try JSONEncoder().encode(["message": "註冊節點出現錯誤"])
}
self.blockchainService.registerNode(blockchainNode)
return try JSONEncoder().encode(blockchainNode)
}
複製程式碼
還要更新 BlockchainService 以便註冊新節點。
func getNodes() -> [BlockchainNode] {
return self.blockchain.nodes
}
func registerNode(_ blockchainNode: BlockchainNode) {
self.blockchain.addNode(blockchainNode)
}
複製程式碼
下面來測試一下。啟動新的 Vapor 伺服器然後試著註冊新節點。
節點註冊好後,可以使用 nodes endpoint 來獲取它,如下所示:
現在可以註冊新節點了,下面要著重解決(resolve)節點間的衝突。如果某個節點上的區塊鏈比其它節點的要大,就會產生衝突。在這種情況下,一般都是獲得臨近節點並用較大的區塊鏈更新它們。
解決節點間的衝突
為了建立衝突,我們需要第二臺伺服器或是在另一個埠上執行伺服器。本文會用後一種方法,在另一個埠上啟動 Vapor 伺服器。這兩個節點初始化後,各建立一些區塊和交易,這些區塊會被新增到各自的區塊鏈上。最後,呼叫 resolve endpoint 來解決節點間的衝突,並將節點更新為較大的那個區塊鏈。
給 BlockchainController 新增新的 endpoint 來解決衝突。
self.drop.get("nodes/resolve") { request in
return try Response.async { portal in
self.blockchainService.resolve { blockchain in
let blockchain = try! JSONEncoder().encode(blockchain)
portal.close(with: blockchain.makeResponse())
}
}
}
複製程式碼
上面使用了 Vapor 框架的 async response 功能來非同步處理響應。然後再更新 BlockchainService 來解決衝突。實現如下所示:
func resolve(completion: @escaping(Blockchain) -> ()) {
//獲取節點
let nodes = self.blockchain.nodes
for node in nodes {
let url = URL(string: "http://(node.address)/blockchain")!
URLSession.shared.dataTask(with: url, completionHandler: { (data, _, _) in
if let data = data {
let blockchain = try! JSONDecoder().decode(Blockchain.self, from: data)
if self.blockchain.blocks.count > blockchain.blocks.count {
completion(self.blockchain)
} else {
self.blockchain.blocks = blockchain.blocks
completion(blockchain)
}
}
}).resume()
}
}
複製程式碼
resolve 函式遍歷節點列表並獲取每個節點的區塊鏈。如果某個區塊鏈比當前區塊鏈要大,則替換當前區塊鏈為更大的那個,否則直接返回當前區塊鏈,因為當前區塊鏈已經是更大的區塊鏈了。
為了測試我們要在不同的埠開啟兩臺伺服器,在 8080 埠上新增三筆交易,在 8081 上新增兩筆。可以在終端裡輸入下面的命令來啟動 Vapor 伺服器。
vapor run serve -—port=8081
複製程式碼
在 8080 埠上新增三筆交易,如下所示:
然後在 8081 埠節點上新增兩筆交易,如下所示:
確保註冊了 8080 地址的節點,如下所示:
最後,來一下測試 resolve endpoint。在 Postman 裡訪問 “resolve” endpoint,如下所示:
可以看到,resolve endpoint 返回了更大的區塊鏈,同時也更新了節點的區塊鏈。這樣解決衝突方案就完工了。
[GitHub]