Swift+Vapor開發一個簡易區塊鏈
Code In ->REPO
最近身邊的許多人都開始玩比特幣,雖然本人不炒但是想稍微瞭解一下其中的原理,所以就練手寫了一個簡易版的區塊鏈系統。
So 、 What is the BlockChain (區塊鏈) ?
這裡引用一下Google的結果
所謂區塊鏈技術 , 簡稱BT(Blockchain technology),也被稱之為分散式賬本技術,是一種網際網路資料庫技術,其特點是去中心化、公開透明,讓每個人均可參與資料庫記錄。
Base (基礎概念)
- 交易(Transaction):一次操作,導致賬本狀態的一次改變,如新增一條記錄;
- 區塊(Block):記錄一段時間內發生的交易和狀態結果,是對當前賬本狀態的一次共識;
- 鏈(Chain):由一個個區塊按照發生順序串聯而成,是整個狀態變化的日誌記錄。
如果把區塊鏈作為一個狀態機,則每次交易就是試圖改變一次狀態,而每次共識生成的區塊,就是參與者對於區塊中所有交易內容導致狀態改變的結果進行確認。
簡單理解就是:
如果我們把資料庫假設成一本賬本,讀寫資料庫就可以看做一種記賬的行為,區塊鏈技術的原理就是在一段時間內找出記賬最快最好的人,由這個人來記賬,然後將賬本的這一頁資訊發給整個系統裡的其他所有人。這也就相當於改變資料庫所有的記錄,發給全網的其他每個節點,所以區塊鏈技術也稱為分散式賬本(distributed ledger)。
Vapor (用來開發服務端的Swift框架)
既然要用swift實現 , 我在這裡就選擇vapor作為服務端框架來使用 , vapor裡面有意思的東西很多 , 這裡只介紹基本的操作而不深究其原理 。
Install
前置條件 , 這裡我們使用macOS進行開發部署 , 以下是需要軟體和版本。
- Install Xcode 9.3 or greater from the Mac App Store.
- Vapor requires Swift 4.1 or greater.
- Vapor Toolbox: 3.1.7
- Vapor Framework: 3.0.8
- homeBrew
接著使用homeBrew安裝
brew install vapor/tap/vapor
如果一切輸出正常的話我們就可以繼續啦。
Get Started
現在vapor已經裝好了 , 我們可以先把基本的準備工作弄好
使用vapor初始化工程
vapor new blockChainServer
生成工程檔案
vapor xcode
接著開啟 blockChainServer.xcodeproj 檔案 , 在導航上的schema上選擇 run ,接著按下Command
+R
這時你應能夠在控制檯看到輸出的server地址了
Practice Begin
到現在我們一切準備工作都就緒了 , 那麼開始鼓搗個區塊鏈api出來吧 。
Base Model
區塊鏈的基本概念上面介紹了,包含以下幾類,我們使用oop可以抽象出以下一些class
- Transaction交易
- Block區塊
- Blockchain區塊鏈
- BlockChainNode區塊鏈節點
排列由下向上存在集合關係。
其實這裡面最後應該加上一個區塊網路不過這裡我們暫時不需要實現全部網路,這裡我們先搭建一個內網環境的來練練手
下面分別來看下幾個model的程式碼
(ps:這裡的框架新增的協議和擴充套件很多,不在此過多介紹,請把關注點放在class本身)
Transaction.swift
import Foundation import FluentSQLite import Vapor final class Transaction: Codable,SQLiteModel { var id: Int? 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 } } extension Transaction: Content { } extension Transaction: Migration { } extension Transaction: Parameter { }
這裡我們可以看到定義了幾個property ,
- id是SQLiteModel協議需要實現的 , 這裡只要記住是為了資料持久化就好
- to : 交易的接收方
- from : 交易的發起方
- amount : 金額
Block.swift
import FluentSQLite import Vapor final class Block: Codable,SQLiteModel { var id: Int? 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(transactions) let transactionsJSONString = String(data: transactionsData, encoding: .utf8) return String(index) + dateCreated + previousHash + transactionsJSONString! + String(nonce) } } @discardableResult func addTransaction(transaction: Transaction) -> Block{ transactions.append(transaction) return self } init() { dateCreated = Date().toString() nonce = 0 message = "挖出新的區塊" } init(transaction: Transaction) { dateCreated = Date().toString() nonce = 0 addTransaction(transaction: transaction) } } extension Block: Content { } extension Block: Migration { } extension Block: Parameter { }
- index : 區塊序號
- dateCreated : 建立日期
- previousHash : 前一個區塊的雜湊值
- hash : 當前區塊的雜湊值
- nonce : 先記住和工作量證明有關
- message : 這裡是為了我們看到輸出
Blockchain.swift
import Foundation import FluentSQLite import Vapor final class Blockchain: Codable,SQLiteModel { var id: Int? 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.sha1Hash() // 設定工作量證明 while(!hash.hasPrefix("11")) { block.nonce += 1 hash = block.key.sha1Hash() print(hash) } return hash } } extension Blockchain: Content { } extension Blockchain: Migration { } extension Blockchain: Parameter { }
BlockChainNode.swift
import FluentSQLite import Vapor final class BlockChainNode: Codable,SQLiteModel { var id: Int? var address :String init(addr:String) { address = addr } } extension BlockChainNode: Content { } extension BlockChainNode: Migration { } extension BlockChainNode: Parameter { }
- address : 節點地址
Action
這裡涉及到的事件主要是計算hash , 我們在這裡面給String 新增一個extension
extension String { func sha1Hash() -> String { let task = Process() task.launchPath = "/usr/bin/shasum" task.arguments = [] let inputPipe = Pipe() inputPipe.fileHandleForWriting.write(self.data(using: .utf8)!) inputPipe.fileHandleForWriting.closeFile() let outputPipe = Pipe() task.standardOutput = outputPipe task.standardInput = inputPipe task.launch() let data = outputPipe.fileHandleForReading.readDataToEndOfFile() let hash = String(data: data, encoding: .utf8)! return hash.replacingOccurrences(of: "-\n", with: "") } }
給date也新增一個便於我們輸出區塊建立時間
extension Date { func toString() -> String { let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd HH:mm:ss" return formatter.string(from: self) } }
Service
在這裡我們把所有對區塊鏈的操作抽象為一個service類
BlockchainService.swift
import Foundation import Vapor class BlockchainService { private var blockchain: Blockchain = Blockchain() private var nodes = [BlockChainNode]() init() { } func addBlock(_ block: Block) -> Block { self.blockchain.addBlock(block) return block } func getLastBlock() -> Block { guard let lastB = self.blockchain.blocks.last else { return addBlock(Block()) } return lastB; } func getBlockchain() -> Blockchain { return self.blockchain } func registerNode(_ node:BlockChainNode) -> BlockChainNode { self.nodes.append(node) return node } func getAllNodes() -> [BlockChainNode] { return nodes } }
Controller & Router
這裡我們基本完成了所有基本模型的搭建 , 現在需要對我們的server進行操作 , 讓我們可以方便的通過curl呼叫我們的區塊鏈系統 。
BlockChainController.swift
import Foundation import Vapor struct BCError:LocalizedError { let name:String } final class BlockChainController { let bcService = BlockchainService() func addBlock(_ req: Request) throws -> Future<Block> { return bcService.addBlock(Block()).save(on: req) } func addTransaction(_ req: Request) throws -> Future<Block> { return try req.content.decode(Transaction.self).flatMap{ transation in return self.bcService.getLastBlock().addTransaction(transaction: transation).save(on: req) } } func findBlockChain(_ req: Request) throws -> Future<Blockchain> { return bcService.getBlockchain().save(on: req) } func registeNode(_ req: Request) throws -> Future<BlockChainNode> { return try req.content.decode(BlockChainNode.self).flatMap{ node in return self.bcService.registerNode(node).save(on: req) } } func allNodes(_ req: Request) throws -> Future<[BlockChainNode]> { return BlockChainNode.query(on: req).all() } func resolve(_ req: Request) throws -> Future<Blockchain> { let promise = req.eventLoop.newPromise(Blockchain.self) bcService.getAllNodes().forEach { node in guard let url = URL(string: "http://\(node.address)/blockchain") else {return promise.fail(error: BCError(name: "node error"))} URLSession.shared.dataTask(with: url, completionHandler: { (data, _, _) in if let data = data { guard let bc = try? JSONDecoder().decode(Blockchain.self, from: data) else {return promise.fail(error: BCError(name: "json error"))} if self.bcService.getBlockchain().blocks.count < bc.blocks.count { self.bcService.getBlockchain().blocks = bc.blocks } promise.succeed(result: self.bcService.getBlockchain()) } else { promise.fail(error: BCError(name: "data Error")) } }).resume() } return promise.futureResult } }
routes.swift
import Vapor /// Register your application's routes here. public func routes(_ router: Router) throws { // Basic "Hello, world!" example router.get("hello") { req in return "Hello, world!" } let bcc = BlockChainController() router.post("block", use: bcc.addBlock) router.post("transaction", use: bcc.addTransaction) router.get("blockchain", use: bcc.findBlockChain) router.post("node", use: bcc.registeNode) router.get("node", use: bcc.allNodes) router.post("resolve", use: bcc.resolve) }
現在解釋一下我們註冊的api都是幹啥的
ps:API層遵守restfull
- post("block", use: bcc.addBlock) 增加一個區塊
- post("transaction", use: bcc.addTransaction) 新增一筆交易
- get("blockchain", use: bcc.findBlockChain) 查詢區塊鏈
- post("node", use: bcc.registeNode) 增加節點
- get("node", use: bcc.allNodes) 獲取全部節點
- post("resolve", use: bcc.resolve) 解決衝突
Example
下面我們可以讓服務執行起來 , 然後通過curl命令列來呼叫區塊鏈系統 ,
編譯工程
進入我們的工程目錄 , 執行命令
vapor build
你應能看到以下輸出
Building Project [Done]
這說明我們的工程可以運行了
執行工程
vapor run serve --port=8080
我們在本地8080埠上開啟我們的服務
這時應能看到如下輸出
Running blockChainServer ... [ INFO ] Migrating 'sqlite' database (/Users/felix/Documents/TestFolder/blockChainServer/.build/checkouts/fluent.git-6251908308727715749/Sources/Fluent/Migration/MigrationConfig.swift:69) [ INFO ] Preparing migration 'Todo' (/Users/felix/Documents/TestFolder/blockChainServer/.build/checkouts/fluent.git-6251908308727715749/Sources/Fluent/Migration/Migrations.swift:111) [ INFO ] Preparing migration 'Block' (/Users/felix/Documents/TestFolder/blockChainServer/.build/checkouts/fluent.git-6251908308727715749/Sources/Fluent/Migration/Migrations.swift:111) [ INFO ] Preparing migration 'Blockchain' (/Users/felix/Documents/TestFolder/blockChainServer/.build/checkouts/fluent.git-6251908308727715749/Sources/Fluent/Migration/Migrations.swift:111) [ INFO ] Preparing migration 'BlockChainNode' (/Users/felix/Documents/TestFolder/blockChainServer/.build/checkouts/fluent.git-6251908308727715749/Sources/Fluent/Migration/Migrations.swift:111) [ INFO ] Migrations complete (/Users/felix/Documents/TestFolder/blockChainServer/.build/checkouts/fluent.git-6251908308727715749/Sources/Fluent/Migration/MigrationConfig.swift:73) [Deprecated] --option=value syntax is deprecated. Please use --option value (with no =) instead. Server starting on http://localhost:8080
現在我們只要用api呼叫以下 , 本機的區塊鏈系統就會響應了 , 讓我們試一下吧
建立區塊
curl -s -X POST localhost:8080/block
你會在一段時間後看到響應
{ "dateCreated": "2018-08-11 17:19:01", "hash": "1124aa2a5867abee8b9cc3a3f4051b6665f89e26", "id": 1, "index": 0, "message": "\u6b64\u533a\u5757\u5df2\u6dfb\u52a0\u81f3\u533a\u5757\u94fe", "nonce": 193, "previousHash": "0", "transactions": [] }
我們的第一個block已經建立成功並且被加入區塊鏈裡了 , 他的previousHash為“0”是因為他是創世區塊 。
新增交易
我們可以在這裡新增幾筆交易看看
curl -s -X POST localhost:8080/transaction --data "from=Felix&to=mayun&amount=100" | python -m json.tool
data裡面表示 Felix向mayun轉賬100 每次新增後你將會得到區塊的最新資訊
{ "dateCreated": "2018-08-11 17:19:01", "hash": "1124aa2a5867abee8b9cc3a3f4051b6665f89e26", "id": 1, "index": 0, "message": "\u6b64\u533a\u5757\u5df2\u6dfb\u52a0\u81f3\u533a\u5757\u94fe", "nonce": 193, "previousHash": "0", "transactions": [ { "amount": 100, "from": "Felix", "to": "mayun" } ] }
查詢鏈
我們可以重複以上操作幾次,然後檢視整個區塊鏈資訊
curl -s -X GET localhost:8080/blockchain | python -m json.tool
會看到如下輸出
{ "blocks": [ { "dateCreated": "2018-08-11 17:19:01", "hash": "1124aa2a5867abee8b9cc3a3f4051b6665f89e26", "id": 1, "index": 0, "message": "\u6b64\u533a\u5757\u5df2\u6dfb\u52a0\u81f3\u533a\u5757\u94fe", "nonce": 193, "previousHash": "0", "transactions": [ { "amount": 100, "from": "Felix", "to": "mayun" }, { "amount": 100, "from": "Felix", "to": "mayun" } ] }, { "dateCreated": "2018-08-11 17:27:39", "hash": "11bec5f7bf8226c62119adfbb03ad37d24267092", "id": 2, "index": 1, "message": "\u6b64\u533a\u5757\u5df2\u6dfb\u52a0\u81f3\u533a\u5757\u94fe", "nonce": 277, "previousHash": "1124aa2a5867abee8b9cc3a3f4051b6665f89e26", "transactions": [ { "amount": 100, "from": "Felix", "to": "mayun" }, { "amount": 100, "from": "Felix", "to": "mayun" } ] } ], "id": 1 }
我們可以看到Felix不停的向mayun轉100塊 , 真不要臉 。
節點以及解決衝突
區塊鏈同時存在與多個主機上,也就是說會有很多的block-chain-server執行,而對於不同的運算會有多個解產生,這就是衝突問題了,那麼我們看下衝突解決的過程
- 首先,我們在8081埠同時開啟一個服務
vapor run serve --port=8081
- 然後新增幾個區塊和交易,因為我們要驗證解決衝突,所以應該比執行在8080埠上的區塊多。
curl -s -X POST localhost:8081/block | python -m json.tool
- 重複幾次操作 , 最後看下8081傷的blockchain
{ "blocks": [ { "dateCreated": "2018-08-11 17:35:15", "hash": "1152cb1aac50abd803a4589f28c7e054db207e23", "id": 1, "index": 0, "message": "\u6b64\u533a\u5757\u5df2\u6dfb\u52a0\u81f3\u533a\u5757\u94fe", "nonce": 215, "previousHash": "0", "transactions": [ { "amount": 200, "from": "mayun", "to": "Felix" }, { "amount": 200, "from": "mayun", "to": "Felix" }, { "amount": 200, "from": "mayun", "to": "Felix" } ] }, { "dateCreated": "2018-08-11 17:37:18", "hash": "1127f8c712ae3205ccbab9788392bcd190b8b6b1", "id": 2, "index": 1, "message": "\u6b64\u533a\u5757\u5df2\u6dfb\u52a0\u81f3\u533a\u5757\u94fe", "nonce": 380, "previousHash": "1152cb1aac50abd803a4589f28c7e054db207e23", "transactions": [ { "amount": 200, "from": "mayun", "to": "Felix" }, { "amount": 200, "from": "mayun", "to": "Felix" } ] }, { "dateCreated": "2018-08-11 17:37:39", "hash": "11b5d293c3068081f1771f14f96a3e450f282171", "id": 3, "index": 2, "message": "\u6b64\u533a\u5757\u5df2\u6dfb\u52a0\u81f3\u533a\u5757\u94fe", "nonce": 64, "previousHash": "1127f8c712ae3205ccbab9788392bcd190b8b6b1", "transactions": [ { "amount": 200, "from": "mayun", "to": "Felix" }, { "amount": 200, "from": "mayun", "to": "Felix" } ] }, { "dateCreated": "2018-08-11 17:37:45", "hash": "11d97dfca7cc7a67c22b9df06017768fca0a193f", "id": 4, "index": 3, "message": "\u6b64\u533a\u5757\u5df2\u6dfb\u52a0\u81f3\u533a\u5757\u94fe", "nonce": 125, "previousHash": "11b5d293c3068081f1771f14f96a3e450f282171", "transactions": [ { "amount": 200, "from": "mayun", "to": "Felix" }, { "amount": 200, "from": "mayun", "to": "Felix" } ] }, { "dateCreated": "2018-08-11 17:38:03", "hash": "115a991bd5d2ebe6e232b421965ab852b97a4202", "id": 5, "index": 4, "message": "\u6b64\u533a\u5757\u5df2\u6dfb\u52a0\u81f3\u533a\u5757\u94fe", "nonce": 220, "previousHash": "11d97dfca7cc7a67c22b9df06017768fca0a193f", "transactions": [ { "amount": 200, "from": "mayun", "to": "Felix" }, { "amount": 200, "from": "mayun", "to": "Felix" } ] } ], "id": 1 }
- 然後我們要讓8080知道有這麼一個節點
curl -s -X POST localhost:8080/node --data "address=localhost:8081" | python -m json.tool
- 我們會看到節點已經註冊成功了
{ "address": "localhost:8081", "id": 1 }
- 這時我們讓8080去主動解決衝突
curl -s -X POST localhost:8080/resolve | python -m json.tool
- 再檢視8080上的區塊鏈,發現已經被替換為較長的8081上的blocks了。
{ "blocks": [ { "dateCreated": "2018-08-11 17:35:15", "hash": "1152cb1aac50abd803a4589f28c7e054db207e23", "id": 1, "index": 0, "message": "\u6b64\u533a\u5757\u5df2\u6dfb\u52a0\u81f3\u533a\u5757\u94fe", "nonce": 215, "previousHash": "0", "transactions": [ { "amount": 200, "from": "mayun", "to": "Felix" }, { "amount": 200, "from": "mayun", "to": "Felix" }, { "amount": 200, "from": "mayun", "to": "Felix" } ] }, { "dateCreated": "2018-08-11 17:37:18", "hash": "1127f8c712ae3205ccbab9788392bcd190b8b6b1", "id": 2, "index": 1, "message": "\u6b64\u533a\u5757\u5df2\u6dfb\u52a0\u81f3\u533a\u5757\u94fe", "nonce": 380, "previousHash": "1152cb1aac50abd803a4589f28c7e054db207e23", "transactions": [ { "amount": 200, "from": "mayun", "to": "Felix" }, { "amount": 200, "from": "mayun", "to": "Felix" } ] }, { "dateCreated": "2018-08-11 17:37:39", "hash": "11b5d293c3068081f1771f14f96a3e450f282171", "id": 3, "index": 2, "message": "\u6b64\u533a\u5757\u5df2\u6dfb\u52a0\u81f3\u533a\u5757\u94fe", "nonce": 64, "previousHash": "1127f8c712ae3205ccbab9788392bcd190b8b6b1", "transactions": [ { "amount": 200, "from": "mayun", "to": "Felix" }, { "amount": 200, "from": "mayun", "to": "Felix" } ] }, { "dateCreated": "2018-08-11 17:37:45", "hash": "11d97dfca7cc7a67c22b9df06017768fca0a193f", "id": 4, "index": 3, "message": "\u6b64\u533a\u5757\u5df2\u6dfb\u52a0\u81f3\u533a\u5757\u94fe", "nonce": 125, "previousHash": "11b5d293c3068081f1771f14f96a3e450f282171", "transactions": [ { "amount": 200, "from": "mayun", "to": "Felix" }, { "amount": 200, "from": "mayun", "to": "Felix" } ] }, { "dateCreated": "2018-08-11 17:38:03", "hash": "115a991bd5d2ebe6e232b421965ab852b97a4202", "id": 5, "index": 4, "message": "\u6b64\u533a\u5757\u5df2\u6dfb\u52a0\u81f3\u533a\u5757\u94fe", "nonce": 220, "previousHash": "11d97dfca7cc7a67c22b9df06017768fca0a193f", "transactions": [ { "amount": 200, "from": "mayun", "to": "Felix" }, { "amount": 200, "from": "mayun", "to": "Felix" } ] } ], "id": 1 }
Summary (總結)
可以看到大概區塊鏈的設計還是比較有意思的,細節部分請不要在意(比如sha1和prefix“11” :smile:)
基本的介紹就到這裡,建議自己動手實踐一遍,還是蠻有意思的。