Use Swift and Vapor to build a blockchain server

Use Swift and Vapor to build a blockchain server

This article references


In the last article , I discussed how to implement a basic blockchain in Swift language. In this article, we will use the server-side Swift framework Vapor to implement blockchain in the cloud. The blockchain Web API is constructed through the HTTP protocol, and different routes are used to provide the necessary functions. To read this article , you need to install the Vapor framework on your computer , and you need to have a basic understanding of the Swift language.

Implementation model

The first step is to create the necessary model for the blockchain Web API, as shown below.

Block : The Block (block) class represents a block that contains the input and output of the transaction.

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)
    }
}
 

The attributes of the Block class are explained as follows:

  • index The position of the block in the blockchain. An index of 0 means that the block is the first block in the blockchain. An index of 1 means the second block in the blockchain...and so on!
  • dateCreated -the date the block was created
  • previousHash -the hash value of the previous block
  • hash -the hash value of the current block
  • message -the memo description of each block. Just for example use
  • nonce -an increasing number, which is critical for generating a hash value
  • transactions -a series of transactions. Every transaction represents a transfer of goods/value
  • key calculated attribute, provided to the function that generates the hash value

Transaction : Transaction consists of sender (sender), recipient (recipient) and the transferred amount (amount). The implementation is as follows:

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
    }
}
 

The implementation of the Transaction class is very intuitive. It consists of from, to, and amount fields. For the sake of simplicity, the from and to fields will be represented by virtual names. In practice, these two fields will also contain the wallet ID.

Blockchain : The Blockchain (blockchain) class is the main class that represents the block list. Each block points to the previous block in the chain. Each block can contain multiple transactions, representing credit or debit.

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
    }
}
 

Each model follows the Codable protocol in order to be converted into JSON objects. If you read the previous article , then the above implementation of the very familiar. The next step is to configure routing for Web API, which will be implemented using the Vapor framework in the next section.

Use Vapor to implement Web API

There are several different ways to implement Web API with Vapor. I will create a custom controller here to handle all blockchain requests, so I don't have to put all the code into the Routes class. BlockchainController is implemented as follows:

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 starts with three basic endpoints.

  • Mining : This endpoint will start the mining program. Mining allows us to achieve proof of work and then add blocks to the blockchain.
  • Transaction : This endpoint is used to add new transactions. The transaction contains information about the sender, receiver, and amount.
  • Blockchain : This endpoint returns the complete blockchain .

BlockchainController uses BlockChainService to perform the required operations. The implementation of BlockChainService is as follows:

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
    }
}
 

Let's check the endpoint of Web API. Start the Vapor server and send a request to the "mine" endpoint.

The proof-of-work algorithm generates a hash value starting with " 000 ". After the block is mined, it is immediately converted to JSON format and returned back. Implemented through the Codable protocol of Swift 4.0.

Now add a simple transaction to the blockchain to transfer $10 from Zhang Jiafu to Jack Ma.

The last step is to check whether the blockchain contains newly added blocks. Visit the " blockchain " endpoint to view the complete chain.

perfect! Our blockchain Web API can now work normally.

Another regret is that the blockchain should be decentralized, but currently we do not have a mechanism for adding new nodes. In the next section we will update the blockchain implementation so that it supports multiple nodes.

Add nodes to the blockchain

Before adding a node to the blockchain, the node must first be defined. The realization of the node model is as follows:

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
    }
    
}
 

The BlockChainNode class is very simple, with only one address attribute, which is used to identify the URL of the node server. Then update the BlockchainController to add the function of registering new nodes. As follows:

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)
        }
 

Also update the BlockchainService to register new nodes.

	  func getNodes() -> [BlockchainNode] {
        return self.blockchain.nodes
    }
    
    func registerNode(_ blockchainNode: BlockchainNode) {
        self.blockchain.addNode(blockchainNode)
    }
 

Let's test it. Start the new Vapor server and try to register the new node.

After the node is registered, you can use the nodes endpoint to get it, as shown below:

Now you can register a new node, the next step is to resolve conflicts between nodes. If the blockchain on a node is larger than that of other nodes, conflicts will occur. In this case, it is common to obtain neighboring nodes and update them with a larger blockchain.

Resolve conflicts between nodes

In order to create a conflict, we need a second server or a server running on another port. This article will use the latter method to start the Vapor server on another port. After these two nodes are initialized, some blocks and transactions are created, and these blocks will be added to their respective blockchains. Finally, call resolve endpoint to resolve conflicts between nodes, and update the node to the larger blockchain.

Add a new endpoint to BlockchainController to resolve conflicts.

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())
                }
            }
        }
 

The async response function of the Vapor framework is used above to process the response asynchronously. Then update the BlockchainService to resolve the conflict. The implementation is as follows:

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()
        }
    }
 

The resolve function traverses the list of nodes and obtains the blockchain of each node. If a block chain is larger than the current block chain, replace the current block chain with the larger one, otherwise directly return to the current block chain, because the current block chain is already a larger block chain.

To test, we need to open two servers on different ports, add three transactions on port 8080 , and add two transactions on port 8081 . You can enter the following command in the terminal to start the Vapor server.

vapor run serve - port=8081
 

Add three transactions on port 8080, as shown below:

Then add two transactions on the 8081 port node, as shown below:

Make sure that the node with the 8080 address is registered, as shown below:

Finally, let's test the resolve endpoint. Visit the " resolve " endpoint in Postman as follows:

As you can see, the resolve endpoint returned a larger blockchain and also updated the node's blockchain. This completes the conflict resolution program.

[ GitHub ]