本文参考
- 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]