使用 Swift 和 Vapor 構建區塊鏈伺服器

張嘉夫_Joseph發表於2019-03-04

本文參考


我在上一篇文章中討論瞭如何用 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 埠上新增三筆交易,如下所示:

8080埠上的區塊鏈

然後在 8081 埠節點上新增兩筆交易,如下所示:

8081埠上的區塊鏈

確保註冊了 8080 地址的節點,如下所示:

註冊節點

最後,來一下測試 resolve endpoint。在 Postman 裡訪問 “resolve” endpoint,如下所示:

resolve endpoint 返回了更大的區塊鏈

可以看到,resolve endpoint 返回了更大的區塊鏈,同時也更新了節點的區塊鏈。這樣解決衝突方案就完工了。

[GitHub]

相關文章