当前位置: 首页 > 工具软件 > Vapor > 使用案例 >

使用 Swift 和 Vapor 构建区块链服务器

耿敏达
2023-12-01

本文参考


我在上一篇文章中讨论了如何用 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]

 类似资料: