From 7d635cf536ec248a80a7315a6c74d9cf9de9d0b0 Mon Sep 17 00:00:00 2001 From: Magnus Nevstad Date: Tue, 9 Apr 2019 15:07:35 +0200 Subject: [PATCH] Implement simple node network, not quite peer to peer, but close. --- README.md | 11 +- Sources/BlockchainSwift/Core/Block.swift | 2 +- Sources/BlockchainSwift/Core/Blockchain.swift | 172 +++--------- .../BlockchainSwift/Core/Serialization.swift | 2 +- .../BlockchainSwift/Core/Transaction.swift | 8 +- .../Core/TransactionInput.swift | 2 +- .../Core/TransactionOutPoint.swift | 2 +- .../Core/TransationOutput.swift | 6 +- Sources/BlockchainSwift/Network/Node.swift | 256 ++++++++++++++++++ .../BlockchainSwift/Network/NodeAddress.swift | 43 +++ .../BlockchainSwift/Network/NodeClient.swift | 74 +++++ .../Network/NodeMessages.swift | 86 ++++++ .../BlockchainSwift/Network/NodeServer.swift | 72 +++++ .../ProofOfWork/ProofOfWork.swift | 6 +- Sources/BlockchainSwift/Wallet/Wallet.swift | 15 +- .../BlockchainSwiftTests.swift | 123 +++++++-- 16 files changed, 700 insertions(+), 180 deletions(-) create mode 100644 Sources/BlockchainSwift/Network/Node.swift create mode 100644 Sources/BlockchainSwift/Network/NodeAddress.swift create mode 100644 Sources/BlockchainSwift/Network/NodeClient.swift create mode 100644 Sources/BlockchainSwift/Network/NodeMessages.swift create mode 100644 Sources/BlockchainSwift/Network/NodeServer.swift diff --git a/README.md b/README.md index 8bed3a3..360f355 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,22 @@ -# ⛓ A simple Blockchain implementation for macOS, written in Swift. +# ⛓ A simple Blockchain implementation written in Swift. A Blockchain implementation that loosely mimics Bitcoin's key features. ## ✅ Features -* 🎭 Secure and anonymous Wallets +* 🎭 Secure and Anonymous Wallets * 🔐 Verified Transations -* 🛠 A Proof-of-Work system - +* 🛠 Proof-of-Work System +* 🌐 Simple Decentralization ## ⛔️ Missing features: * 🗄 Persistent block store -* 🌐 Decentralization ## 🚦 Requirements * ✓ Swift 4.0+ -* ✓ macOS 10.12+, iOS 10.0+, tvOS 10.0+ +* ✓ macOS 10.14+, iOS 12.0+, tvOS 12.0+ ## 📣 Shoutout diff --git a/Sources/BlockchainSwift/Core/Block.swift b/Sources/BlockchainSwift/Core/Block.swift index da4c628..0e12d97 100644 --- a/Sources/BlockchainSwift/Core/Block.swift +++ b/Sources/BlockchainSwift/Core/Block.swift @@ -7,7 +7,7 @@ import Foundation -public struct Block: Serializable { +public struct Block: Codable, Serializable { /// The timestamp for when the block was generated and added to the chain public let timestamp: UInt32 diff --git a/Sources/BlockchainSwift/Core/Blockchain.swift b/Sources/BlockchainSwift/Core/Blockchain.swift index 84b1a7a..5617358 100644 --- a/Sources/BlockchainSwift/Core/Blockchain.swift +++ b/Sources/BlockchainSwift/Core/Blockchain.swift @@ -21,160 +21,78 @@ public class Blockchain { return subsidy / (1 + halvings) } } - - /// Transaction error types - public enum TxError: Error { - case invalidValue - case insufficientBalance - case unverifiedTransaction - } /// The blockchain - public private(set) var chain: [Block] = [] + public private(set) var blocks: [Block] = [] /// Proof of Work Algorithm public private(set) var pow = ProofOfWork(difficulty: 3) - /// Transation pool holds all transactions to go into the next block - public private(set) var mempool = [Transaction]() - /// Unspent Transaction Outputs /// - This class keeps track off all current UTXOs, providing a quick lookup for balances and creating new transactions. - /// - For now, any transaction will use all available utxos for that address, meaning we have an easier job of things. - /// - Also, since we have no decentralization, we don't have to worry about reloading this based on the current blockchain whenver we have to sync blocks with other nodes. - public private(set) var utxos = [TransactionOutput]() + /// - For now, any transaction must use all available utxos for that address, meaning we have an easier job of things. + private var utxos = [TransactionOutput]() - /// Explicitly define Codable properties - private enum CodingKeys: CodingKey { - case mempool - case chain - } - - /// Initialises our blockchain with a genesis block - public init(minerAddress: Data) { - mineGenesisBlock(minerAddress: minerAddress) + /// Returns the last block in the blockchain. Fatal error if we have no blocks. + public func lastBlockHash() -> Data { + return self.blocks.last?.hash ?? Data() } - /// Creates a coinbase transaction - /// - Parameter address: The miner's address to be awarded a block reward - /// - Returns: The index of the block to whitch this transaction will be added - @discardableResult - private func createCoinbaseTransaction(for address: Data) -> Int { - // Generate a coinbase tx to reward block miner - let coinbaseTx = Transaction.coinbase(address: address, blockValue: currentBlockValue()) - self.mempool.append(coinbaseTx) - self.utxos.append(contentsOf: coinbaseTx.outputs) - return self.chain.count + 1 + /// Get the block value, or the block reward, at current block height + public func currentBlockValue() -> UInt64 { + return Coin.blockReward(at: UInt64(self.blocks.count)) } - - /// Create a transaction to be added to the next block. - /// - Parameters: - /// - sender: The sender - /// - recipient: The recipient - /// - value: The value to transact - /// - Returns: The index of the block to whitch this transaction will be added - @discardableResult - public func createTransaction(sender: Wallet, recipientAddress: Data, value: UInt64) throws -> Int { - // You cannot send nothing - if value == 0 { - throw TxError.invalidValue - } - - // Calculate transaction value and change, based on the sender's balance and the transaction's value - // - All utxos for the sender must be spent, and are indivisible. - let balance = self.balance(for: sender.address) - if value > balance { - throw TxError.insufficientBalance - } - let change = balance - value - // Create a transaction and sign it, making sure first the sender has the right to claim the spendale outputs - let spendableOutputs = self.utxos.filter { $0.address == sender.address } - guard let signedTxIns = try? sender.sign(utxos: spendableOutputs) else { return -1 } - for (i, txIn) in signedTxIns.enumerated() { - let originalOutputData = spendableOutputs[i].serialized().sha256() - if !ECDSA.verify(publicKey: sender.publicKey, data: originalOutputData, signature: txIn.signature) { - throw TxError.unverifiedTransaction - } - } - - // Add transaction to the pool - let txOuts = [ - TransactionOutput(value: value, address: recipientAddress), - TransactionOutput(value: change, address: sender.address) - ] - self.mempool.append(Transaction(inputs: signedTxIns, outputs: txOuts)) - - // All spendable outputs for sender must be spent, and all outputs added - self.utxos.removeAll { $0.address == sender.address } - self.utxos.append(contentsOf: txOuts) - - return self.chain.count + 1 - } - - /// Finds a transaction by id, iterating through every block (to optimize this, look into Merkle trees). - /// - Parameter txId: The txId - public func findTransaction(txId: String) -> Transaction? { - for block in chain { - for transaction in block.transactions { - if transaction.txId == txId { - return transaction - } - } - } - return nil - } - - /// Create a new block in the chain, adding transactions curently in the mempool to the block - /// - Parameter proof: The proof of the PoW + /// Create a new block in the chain + /// - Parameter nonce: The Block nonce after successful PoW + /// - Parameter hash: The Block hash after successful PoW + /// - Parameter previousHash: The hash of the previous Block + /// - Parameter timestamp: The timestamp for when the Block was mined + /// - Parameter transactions: The transactions in the Block @discardableResult public func createBlock(nonce: UInt32, hash: Data, previousHash: Data, timestamp: UInt32, transactions: [Transaction]) -> Block { let block = Block(timestamp: timestamp, transactions: transactions, nonce: nonce, hash: hash, previousHash: previousHash) - self.chain.append(block) + self.blocks.append(block) + self.updateSpendableOutputs(with: block) return block } - /// Mines our genesis block placing circulating supply in the reward pool, - /// and awarding the first block to Magnus - @discardableResult - private func mineGenesisBlock(minerAddress: Data) -> Block { - return mineBlock(previousHash: Data(), minerAddress: minerAddress) - } - - /// Mines the next block using Proof of Work - /// - Parameter recipient: The miners address for block reward - public func mineBlock(previousHash: Data, minerAddress: Data) -> Block { - // Generate a coinbase tx to reward block miner - createCoinbaseTransaction(for: minerAddress) - - // Do Proof of Work to mine block with all currently registered transactions, the create our block - let transactions = mempool - mempool.removeAll() - let timestamp = UInt32(Date().timeIntervalSince1970) - let proof = pow.work(prevHash: previousHash, timestamp: timestamp, transactions: transactions) - return createBlock(nonce: proof.nonce, hash: proof.hash, previousHash: previousHash, timestamp: timestamp, transactions: transactions) + /// Finds UTXOs for a specified address + /// - Parameter address: The wallet address whose UTXOs we want to find + public func findSpendableOutputs(for address: Data) -> [TransactionOutput] { + return self.utxos.filter({ $0.address == address }) } - /// Returns the last block in the blockchain. Fatal error if we have no blocks. - public func lastBlock() -> Block { - guard let last = chain.last else { - fatalError("Blockchain needs at least a genesis block!") + /// Updates UTXOs when a new block is added + /// - Parameter block: The block that has been added, whose transactions we must go through to find the nes UTXO state + public func updateSpendableOutputs(with block: Block) { + let spentOutputs = block.transactions.flatMap { $0.inputs.map { $0.previousOutput } } + for spentTxOut in spentOutputs { + self.utxos.removeAll { (txOut) -> Bool in + txOut.hash == spentTxOut.hash + } } - return last - } - - /// Get the block value, or the block reward, at current block height - public func currentBlockValue() -> UInt64 { - return Coin.blockReward(at: UInt64(self.chain.count)) + self.utxos.append(contentsOf: block.transactions.flatMap({ $0.outputs })) } /// Returns the balannce for a specified address, defined by the sum of its unspent outputs + /// - Parameter address: The wallet address whose balance to find public func balance(for address: Data) -> UInt64 { - var balance: UInt64 = 0 - for output in utxos.filter({ $0.address == address }) { - balance += output.value + return findSpendableOutputs(for: address).map { $0.value }.reduce(0, +) + } + + /// Finds a transaction by id, iterating through every block (to optimize this, look into Merkle trees). + /// - Parameter txHash: The Transaction hash + public func findTransaction(txHash: Data) -> Transaction? { + for block in self.blocks { + for transaction in block.transactions { + if transaction.txHash == txHash { + return transaction + } + } } - return balance + return nil } + } diff --git a/Sources/BlockchainSwift/Core/Serialization.swift b/Sources/BlockchainSwift/Core/Serialization.swift index 374a868..c90b088 100644 --- a/Sources/BlockchainSwift/Core/Serialization.swift +++ b/Sources/BlockchainSwift/Core/Serialization.swift @@ -11,7 +11,7 @@ protocol Serializable { func serialized() -> Data } protocol Deserializable { - func deserialized() -> Self + static func deserialize(_ data: Data) throws -> Self } protocol BinaryConvertible { diff --git a/Sources/BlockchainSwift/Core/Transaction.swift b/Sources/BlockchainSwift/Core/Transaction.swift index ca77117..ae5b71a 100644 --- a/Sources/BlockchainSwift/Core/Transaction.swift +++ b/Sources/BlockchainSwift/Core/Transaction.swift @@ -7,7 +7,7 @@ import Foundation -public struct Transaction: Serializable { +public struct Transaction: Codable, Serializable { /// Transaction inputs, which are sources for coins public let inputs: [TransactionInput] @@ -48,3 +48,9 @@ public struct Transaction: Serializable { return Transaction(inputs: txIns, outputs: txOuts) } } + +extension Transaction: Equatable { + public static func == (lhs: Transaction, rhs: Transaction) -> Bool { + return lhs.txHash == rhs.txHash + } +} diff --git a/Sources/BlockchainSwift/Core/TransactionInput.swift b/Sources/BlockchainSwift/Core/TransactionInput.swift index d439a5c..3072eff 100644 --- a/Sources/BlockchainSwift/Core/TransactionInput.swift +++ b/Sources/BlockchainSwift/Core/TransactionInput.swift @@ -8,7 +8,7 @@ import Foundation /// Inputs to a transaction -public struct TransactionInput: Serializable { +public struct TransactionInput: Codable, Serializable { // A reference to the previous Transaction output public let previousOutput: TransactionOutPoint diff --git a/Sources/BlockchainSwift/Core/TransactionOutPoint.swift b/Sources/BlockchainSwift/Core/TransactionOutPoint.swift index a4bb3a1..30502c3 100644 --- a/Sources/BlockchainSwift/Core/TransactionOutPoint.swift +++ b/Sources/BlockchainSwift/Core/TransactionOutPoint.swift @@ -8,7 +8,7 @@ import Foundation /// The out-point of a transaction, referened in TransactionInput -public struct TransactionOutPoint: Serializable { +public struct TransactionOutPoint: Codable, Serializable { /// The hash of the referenced transaction public let hash: Data diff --git a/Sources/BlockchainSwift/Core/TransationOutput.swift b/Sources/BlockchainSwift/Core/TransationOutput.swift index e58afce..67a5012 100644 --- a/Sources/BlockchainSwift/Core/TransationOutput.swift +++ b/Sources/BlockchainSwift/Core/TransationOutput.swift @@ -7,7 +7,7 @@ import Foundation -public struct TransactionOutput: Serializable { +public struct TransactionOutput: Codable, Serializable { /// Transaction value public let value: UInt64 @@ -24,4 +24,8 @@ public struct TransactionOutput: Serializable { public var hash: Data { return serialized().sha256() } + + public func isLockedWith(publicKeyHash: Data) -> Bool { + return self.address == publicKeyHash + } } diff --git a/Sources/BlockchainSwift/Network/Node.swift b/Sources/BlockchainSwift/Network/Node.swift new file mode 100644 index 0000000..42e27c1 --- /dev/null +++ b/Sources/BlockchainSwift/Network/Node.swift @@ -0,0 +1,256 @@ +// +// NodeProtocol.swift +// BlockchainSwift +// +// Created by Magnus Nevstad on 11/04/2019. +// + +import Foundation + +/// In our simplistic network, we have _one_ central Node, with an arbitrary amount of Miners and Wallets. +/// - Central: The hub which all others connect to, and is responsible for syncronizing data accross them. There can only be one. +/// - Miner: Stores new transactions in a mempool, and will put them into blocks once mined. Needs to store the entire chainstate. +/// - Wallet: Sends coins between wallets, and (unlike Bitcoins optimized SPV nodes) needs to store the entire chainstate. +public class Node { + + /// Version lets us make sure all nodes run the same version of the blockchain + public let version: Int = 1 + + /// Our address in the Node network + public let address: NodeAddress + + /// Our network of nodes + public var knownNodes = [NodeAddress]() + public func knownNodes(except: [NodeAddress]) -> [NodeAddress] { + var nodes = knownNodes + except.forEach { exception in + nodes.removeAll(where: { $0 == exception }) + } + return nodes + } + + /// Local copy of the blockchain + public let blockchain: Blockchain + + /// Transaction pool holds all transactions to go into the next block + public var mempool = [Transaction]() + + // The wallet associated with this Node + public let wallet: Wallet + + /// Network IO + var client: NodeClient + let server: NodeServer + + /// Transaction error types + public enum TxError: Error { + case invalidValue + case insufficientBalance + case unverifiedTransaction + } + + /// Create a new Node + /// - Parameter address: This Node's address + /// - Parameter wallet: This Node's wallet, created if nil + init(address: NodeAddress, wallet: Wallet? = nil) { + self.blockchain = Blockchain() + self.wallet = wallet ?? Wallet()! + self.address = address + + // Set up client for outgoing requests + self.client = NodeClient() + + // Set up server to listen on incoming requests + self.server = NodeServer(port: UInt16(address.port)) { newState in + print(newState) + } + self.server.delegate = self + + // All nodes must know of the central node, and connect to it (unless self is central node) + let firstNodeAddr = NodeAddress.centralAddress() + self.knownNodes.append(firstNodeAddr) + if !self.address.isCentralNode { + let versionMessage = VersionMessage(version: 1, blockHeight: self.blockchain.blocks.count, fromAddress: self.address) + self.client.sendVersionMessage(versionMessage, to: firstNodeAddr) + } + } + + /// Create a transaction, sending coins + /// - Parameters: + /// - recipientAddress: The recipient's Wallet address + /// - value: The value to transact + public func createTransaction(recipientAddress: Data, value: UInt64) throws -> Transaction { + if value == 0 { + throw TxError.invalidValue + } + + // Calculate transaction value and change, based on the sender's balance and the transaction's value + // - All utxos for the sender must be spent, and are indivisible. + let balance = self.blockchain.balance(for: self.wallet.address) + if value > balance { + throw TxError.insufficientBalance + } + let change = balance - value + + // Create a transaction and sign it, making sure first the sender has the right to claim the spendale outputs + let spendableOutputs = self.blockchain.findSpendableOutputs(for: self.wallet.address) + guard let signedTxIns = try? self.wallet.sign(utxos: spendableOutputs) else { throw TxError.unverifiedTransaction } + for (i, txIn) in signedTxIns.enumerated() { + let originalOutputData = spendableOutputs[i].hash + if !ECDSA.verify(publicKey: self.wallet.publicKey, data: originalOutputData, signature: txIn.signature) { + throw TxError.unverifiedTransaction + } + } + + // Create the transaction with the correct ins and outs + let txOuts = [ + TransactionOutput(value: value, address: recipientAddress), + TransactionOutput(value: change, address: self.wallet.address) + ] + let transaction = Transaction(inputs: signedTxIns, outputs: txOuts) + // Add it to our mempool + self.mempool.append(transaction) + + // Broadcast new transaction to network + for node in knownNodes(except: [self.address]) { + client.sendTransactionsMessage(TransactionsMessage(transactions: [transaction], fromAddress: self.address), to: node) + } + + return transaction + } + + /// Attempts to mine the next block, placing Transactions currently in the mempool into the new block + public func mineBlock() -> Block? { + // Caution: Beware of state change mid-mine, ie. new transaction or (even worse) a new block. + // We need to reset mining if a new block arrives, we have to remove txs from mempool that are in this new received block, + // and we must update utxos... When resolving conflicts, the block timestamp is relevant + + // Generate a coinbase tx to reward block miner + let coinbaseTx = Transaction.coinbase(address: self.wallet.address, blockValue: self.blockchain.currentBlockValue()) + self.mempool.append(coinbaseTx) + + // TODO: Implement mining fees + + // Do Proof of Work to mine block with all currently registered transactions, the create our block + let transactions = self.mempool + let timestamp = UInt32(Date().timeIntervalSince1970) + let previousHash = self.blockchain.lastBlockHash() + let proof = self.blockchain.pow.work(prevHash: previousHash, timestamp: timestamp, transactions: transactions) + + // TODO: What if someone else has mined blocks and sent to us while working? + + // Create the new block + let block = self.blockchain.createBlock(nonce: proof.nonce, hash: proof.hash, previousHash: previousHash, timestamp: timestamp, transactions: transactions) + + // Clear mined transactions from the mempool + self.mempool.removeAll { (transaction) -> Bool in + return transactions.contains(transaction) + } + + // Notify nodes about new block + for node in self.knownNodes(except: [self.address]) { + self.client.sendBlocksMessage(BlocksMessage(blocks: [block], fromAddress: self.address), to: node) + } + + return block + } +} + +/// Handle incoming messages from the Node Network +extension Node: NodeServerDelegate { + + public func didReceiveVersionMessage(_ message: VersionMessage) { + let localVersion = VersionMessage(version: 1, blockHeight: self.blockchain.blocks.count, fromAddress: self.address) + + // Ignore nodes running a different blockchain protocol version + guard message.version == localVersion.version else { + print("* Node \(self.address.urlString) received invalid Version from \(message.fromAddress.urlString) (\(message.version))") + return + } + print("* Node \(self.address.urlString) received version from \(message.fromAddress.urlString)") + + // If we (as central node) have a new node, add it to our peers + if self.address.isCentralNode { + if !self.knownNodes.contains(message.fromAddress) { + self.knownNodes.append(message.fromAddress) + } + } + print("\t\t- Known peers:") + self.knownNodes.forEach { print("\t\t\t - \($0.urlString)") } + + // If the remote peer has a longer chain, request it's blocks starting from our latest block + // Otherwise, if the remote peer has a shorter chain, respond with our version + if localVersion.blockHeight < message.blockHeight { + print("\t\t- Remote node has longer chain, requesting blocks") + let getBlocksMessage = GetBlocksMessage(fromBlockHash: self.blockchain.lastBlockHash(), fromAddress: self.address) + self.client.sendGetBlocksMessage(getBlocksMessage, to: message.fromAddress) + } else if localVersion.blockHeight > message.blockHeight { + print("\t\t- Remote node has shorter chain, sending version") + self.client.sendVersionMessage(localVersion, to: message.fromAddress) + } + } + + public func didReceiveTransactionsMessage(_ message: TransactionsMessage) { + print("* Node \(self.address.urlString) received transactions from \(message.fromAddress.urlString)") + + // Verify and add transactions to blockchain + for transaction in message.transactions { + let verifiedInputs = transaction.inputs.filter { input in + // TODO: Do we need to look up a local version of the output used, in order to do proper verification? + return ECDSA.verify(publicKey: input.publicKey, data: input.previousOutput.hash, signature: input.signature) + } + if verifiedInputs.count == transaction.inputs.count { + print("\t- Added transaction \(transaction)") + self.mempool.append(transaction) + } else { + print("\t- Unable to verify transaction \(transaction)") + } + } + + // Central node is responsible for distributing the new transactions (nodes will handle verification internally) + if self.address.isCentralNode { + for node in knownNodes(except: [self.address, message.fromAddress]) { + self.client.sendTransactionsMessage(message, to: node) + } + } + } + + public func didReceiveGetBlocksMessage(_ message: GetBlocksMessage) { + print("* Node \(self.address.urlString) received getBlocks from \(message.fromAddress.urlString)") + if message.fromBlockHash.isEmpty { + self.client.sendBlocksMessage(BlocksMessage(blocks: self.blockchain.blocks, fromAddress: self.address), to: message.fromAddress) + } + if let fromHashIndex = self.blockchain.blocks.firstIndex(where: { $0.hash == message.fromBlockHash }) { + let requestedBlocks = Array(self.blockchain.blocks[fromHashIndex...]) + let blocksMessage = BlocksMessage(blocks: requestedBlocks, fromAddress: self.address) + print("\t - Sending blocks message \(blocksMessage)") + self.client.sendBlocksMessage(blocksMessage, to: message.fromAddress) + } else { + print("\t - Unable to generate blocks message to satisfy \(message)") + } + } + + public func didReceiveBlocksMessage(_ message: BlocksMessage) { + print("* Node \(self.address.urlString) received blocks from \(message.fromAddress.urlString)") + var validBlocks = [Block]() + for block in message.blocks { + if block.previousHash != self.blockchain.lastBlockHash() { + print("\t- Uh oh, we're out of sync!") + } + if self.blockchain.pow.validate(block: block, previousHash: self.blockchain.lastBlockHash()) { + self.blockchain.createBlock(nonce: block.nonce, hash: block.hash, previousHash: block.previousHash, timestamp: block.timestamp, transactions: block.transactions) + validBlocks.append(block) + print("\t Added block!") + } else { + print("\t- Unable to verify block: \(block)") + } + } + + // Central node is responsible for distributing the new transactions (nodes will handle verification internally) + if self.address.isCentralNode && !validBlocks.isEmpty { + for node in knownNodes(except: [self.address, message.fromAddress]) { + self.client.sendBlocksMessage(message, to: node) + } + } + } +} diff --git a/Sources/BlockchainSwift/Network/NodeAddress.swift b/Sources/BlockchainSwift/Network/NodeAddress.swift new file mode 100644 index 0000000..3eb9455 --- /dev/null +++ b/Sources/BlockchainSwift/Network/NodeAddress.swift @@ -0,0 +1,43 @@ +// +// NodeAddress.swift +// App +// +// Created by Magnus Nevstad on 10/04/2019. +// + +import Foundation + +public struct NodeAddress: Codable { + public let host: String + public let port: UInt32 + + public var urlString: String { + get { + return "http://\(host):\(port)" + } + } + public var url: URL { + get { + return URL(string: urlString)! + } + } +} + +extension NodeAddress: Equatable { + public static func == (lhs: NodeAddress, rhs: NodeAddress) -> Bool { + return lhs.port == rhs.port //&& lhs.host == rhs.host + } +} + +extension NodeAddress { + // For simplicity's sake we hard code the central node address + public static func centralAddress() -> NodeAddress { + return NodeAddress(host: "localhost", port: 8080) + } + + public var isCentralNode: Bool { + get { + return self == NodeAddress.centralAddress() + } + } +} diff --git a/Sources/BlockchainSwift/Network/NodeClient.swift b/Sources/BlockchainSwift/Network/NodeClient.swift new file mode 100644 index 0000000..9a36a85 --- /dev/null +++ b/Sources/BlockchainSwift/Network/NodeClient.swift @@ -0,0 +1,74 @@ +// +// NodeClient.swift +// BlockchainSwift +// +// Created by Magnus Nevstad on 13/04/2019. +// + +import Foundation +import Network + +/// The NodeClient handles a an outgoing connection to another Node +public class NodeClient { + public var queue: DispatchQueue + + public init(stateHandler: ((NWConnection.State) -> Void)? = nil) { + self.queue = DispatchQueue(label: "Node Client Queue") + } + + private func openConnection(to: NodeAddress) -> NWConnection { + let endpoint = NWEndpoint.hostPort(host: NWEndpoint.Host(to.host), port: NWEndpoint.Port(rawValue: UInt16(to.port))!) + let connection = NWConnection(to: endpoint, using: .udp) + connection.start(queue: queue) + return connection + } + + public func sendVersionMessage(_ versionMessage: VersionMessage, to: NodeAddress) { + let connection = openConnection(to: to) + let message = Message(command: Message.Commands.version.rawValue, payload: versionMessage.serialized()) + connection.send(content: message.serialized(), contentContext: .finalMessage, isComplete: true, completion: .contentProcessed({ (error) in + if let error = error { + print(error) + } else { + print("Sent \(message)") + } + })) + } + + public func sendTransactionsMessage(_ transactionsMessage: TransactionsMessage, to: NodeAddress) { + let connection = openConnection(to: to) + let message = Message(command: Message.Commands.transactions.rawValue, payload: transactionsMessage.serialized()) + connection.send(content: message.serialized(), contentContext: .finalMessage, isComplete: true, completion: .contentProcessed({ (error) in + if let error = error { + print(error) + } else { + print("Sent \(message)") + } + })) + } + + public func sendGetBlocksMessage(_ getBlocksMessage: GetBlocksMessage, to: NodeAddress) { + let connection = openConnection(to: to) + let message = Message(command: Message.Commands.getBlocks.rawValue, payload: getBlocksMessage.serialized()) + connection.send(content: message.serialized(), contentContext: .finalMessage, isComplete: true, completion: .contentProcessed({ (error) in + if let error = error { + print(error) + } else { + print("Sent \(message)") + } + })) + } + + public func sendBlocksMessage(_ blocksMessage: BlocksMessage, to: NodeAddress) { + let connection = openConnection(to: to) + let message = Message(command: Message.Commands.blocks.rawValue, payload: blocksMessage.serialized()) + connection.send(content: message.serialized(), contentContext: .finalMessage, isComplete: true, completion: .contentProcessed({ (error) in + if let error = error { + print(error) + } else { + print("Sent \(message)") + } + })) + } + +} diff --git a/Sources/BlockchainSwift/Network/NodeMessages.swift b/Sources/BlockchainSwift/Network/NodeMessages.swift new file mode 100644 index 0000000..16094f2 --- /dev/null +++ b/Sources/BlockchainSwift/Network/NodeMessages.swift @@ -0,0 +1,86 @@ +// +// NodeMessages.swift +// App +// +// Created by Magnus Nevstad on 10/04/2019. +// + +import Foundation + +/// All messages get wrapped +public struct Message: Serializable, Deserializable, Codable { + public enum Commands: String { + case version + case transactions + case getBlocks + case blocks + } + + public let command: String + public let payload: Data + + func serialized() -> Data { + return try! JSONEncoder().encode(self) + } + + static func deserialize(_ data: Data) throws -> Message { + return try JSONDecoder().decode(Message.self, from: data) + } +} + +/// The version message +public struct VersionMessage: Serializable, Deserializable, Codable { + public let version: Int + public let blockHeight: Int + public let fromAddress: NodeAddress + + func serialized() -> Data { + return try! JSONEncoder().encode(self) + } + + static func deserialize(_ data: Data) throws -> VersionMessage { + return try JSONDecoder().decode(VersionMessage.self, from: data) + } +} + +/// The transactions message contains new transations +public struct TransactionsMessage: Serializable, Deserializable, Codable { + public let transactions: [Transaction] + public let fromAddress: NodeAddress + + func serialized() -> Data { + return try! JSONEncoder().encode(self) + } + + static func deserialize(_ data: Data) throws -> TransactionsMessage { + return try JSONDecoder().decode(TransactionsMessage.self, from: data) + } +} + +/// The GetBlocksMessage object will request Blocks +public struct GetBlocksMessage: Serializable, Deserializable, Codable { + public let fromBlockHash: Data + public let fromAddress: NodeAddress + + func serialized() -> Data { + return try! JSONEncoder().encode(self) + } + + static func deserialize(_ data: Data) throws -> GetBlocksMessage { + return try JSONDecoder().decode(GetBlocksMessage.self, from: data) + } +} + +/// The BlocksMessage contains transferred Blocks +public struct BlocksMessage: Serializable, Deserializable, Codable { + public let blocks: [Block] + public let fromAddress: NodeAddress + + func serialized() -> Data { + return try! JSONEncoder().encode(self) + } + + static func deserialize(_ data: Data) throws -> BlocksMessage { + return try JSONDecoder().decode(BlocksMessage.self, from: data) + } +} diff --git a/Sources/BlockchainSwift/Network/NodeServer.swift b/Sources/BlockchainSwift/Network/NodeServer.swift new file mode 100644 index 0000000..45449a4 --- /dev/null +++ b/Sources/BlockchainSwift/Network/NodeServer.swift @@ -0,0 +1,72 @@ +// +// NodeServer.swift +// BlockchainSwift +// +// Created by Magnus Nevstad on 13/04/2019. +// + +import Foundation +import Network + +public protocol NodeServerDelegate { + func didReceiveVersionMessage(_ message: VersionMessage) + func didReceiveTransactionsMessage(_ message: TransactionsMessage) + func didReceiveGetBlocksMessage(_ message: GetBlocksMessage) + func didReceiveBlocksMessage(_ message: BlocksMessage) +} + +/// The NodeServer handles all incoming connections to a Node +public class NodeServer { + public let listener: NWListener + public let queue: DispatchQueue + public var connections = [NWConnection]() + + var delegate: NodeServerDelegate? + + init(port: UInt16, stateHandler: ((NWListener.State) -> Void)? = nil) { + self.queue = DispatchQueue(label: "Node Server Queue") + self.listener = try! NWListener(using: .udp, on: NWEndpoint.Port(rawValue: port)!) + listener.stateUpdateHandler = stateHandler + listener.newConnectionHandler = { [weak self] newConnection in + if let strongSelf = self { + newConnection.receiveMessage { (data, context, isComplete, error) in + if let data = data, let message = try? Message.deserialize(data) { + if message.command == Message.Commands.version.rawValue { + if let versionMessage = try? VersionMessage.deserialize(message.payload) { + strongSelf.delegate?.didReceiveVersionMessage(versionMessage) + } else { + print("Error: Received malformed \(message.command) message") + } + } else if message.command == Message.Commands.transactions.rawValue { + if let transactionsMessage = try? TransactionsMessage.deserialize(message.payload) { + strongSelf.delegate?.didReceiveTransactionsMessage(transactionsMessage) + } else { + print("Error: Received malformed \(message.command) message") + } + } else if message.command == Message.Commands.getBlocks.rawValue { + if let blocksMessage = try? GetBlocksMessage.deserialize(message.payload) { + strongSelf.delegate?.didReceiveGetBlocksMessage(blocksMessage) + } else { + print("Error: Received malformed \(message.command) message") + } + } else if message.command == Message.Commands.blocks.rawValue { + if let blocksMessage = try? BlocksMessage.deserialize(message.payload) { + strongSelf.delegate?.didReceiveBlocksMessage(blocksMessage) + } else { + print("Error: Received malformed \(message.command) message") + } + } else { + print("Received unknown Message: \(message)") + } + } else { + print("Could not deserialize Message!") + } + } + newConnection.start(queue: strongSelf.queue) + self?.connections.append(newConnection) + } + } + listener.start(queue: .main) + } +} + diff --git a/Sources/BlockchainSwift/ProofOfWork/ProofOfWork.swift b/Sources/BlockchainSwift/ProofOfWork/ProofOfWork.swift index 5533280..3087d77 100644 --- a/Sources/BlockchainSwift/ProofOfWork/ProofOfWork.swift +++ b/Sources/BlockchainSwift/ProofOfWork/ProofOfWork.swift @@ -52,7 +52,7 @@ public struct ProofOfWork { } /// Builds data based on a previousHash, nonce and Block data, to be used for generating hashes - private func prepareData(prevHash: Data, nonce: UInt32, timestamp: UInt32, transactions: [Transaction]) -> Data { + public func prepareData(prevHash: Data, nonce: UInt32, timestamp: UInt32, transactions: [Transaction]) -> Data { var data = Data() data += prevHash data += nonce @@ -65,8 +65,8 @@ public struct ProofOfWork { /// - SHA-256 Hashing this block's data should produce a valid PoW hash /// - Parameter block: The Block to validate /// - Returns: `true` if the block is valid, ie. PoW completed - public func validate(block: Block) -> Bool { - let data = prepareData(prevHash: block.previousHash, nonce: block.nonce, timestamp: block.timestamp, transactions: block.transactions) + public func validate(block: Block, previousHash: Data) -> Bool { + let data = prepareData(prevHash: previousHash, nonce: block.nonce, timestamp: block.timestamp, transactions: block.transactions) let hash = data.sha256() return validate(hash: hash) } diff --git a/Sources/BlockchainSwift/Wallet/Wallet.swift b/Sources/BlockchainSwift/Wallet/Wallet.swift index d888fd3..81278cc 100644 --- a/Sources/BlockchainSwift/Wallet/Wallet.swift +++ b/Sources/BlockchainSwift/Wallet/Wallet.swift @@ -37,7 +37,7 @@ public class Wallet { for (i, utxo) in utxos.enumerated() { // Sign transaction hash var error: Unmanaged? - let txOutputDataHash = utxo.serialized().sha256() + let txOutputDataHash = utxo.hash guard let signature = SecKeyCreateSignature(self.secPrivateKey, .ecdsaSignatureDigestX962SHA256, txOutputDataHash as CFData, @@ -65,16 +65,9 @@ public class Wallet { return signature } - public func canUnlock(utxo: TransactionOutput) -> Bool { - return utxo.address == self.address - } - public func canUnlock(utxos: [TransactionOutput]) -> Bool { - for utxo in utxos { - if !canUnlock(utxo: utxo) { - return false - } - } - return true + return utxos.reduce(true, { (res, output) -> Bool in + return res && output.isLockedWith(publicKeyHash: self.address) + }) } } diff --git a/Tests/BlockchainSwiftTests/BlockchainSwiftTests.swift b/Tests/BlockchainSwiftTests/BlockchainSwiftTests.swift index 812698f..a971082 100644 --- a/Tests/BlockchainSwiftTests/BlockchainSwiftTests.swift +++ b/Tests/BlockchainSwiftTests/BlockchainSwiftTests.swift @@ -5,11 +5,11 @@ final class BlockchainSwiftTests: XCTestCase { func testTxSigning() throws { let wallet1 = Wallet()! let wallet2 = Wallet()! - + // Wallet 2 will try to steal all of Wallet 1's balance, which is here set to 100 let wallet1utxo = TransactionOutput(value: 100, address: wallet1.address) let originalOutputData = wallet1utxo.serialized().sha256() - + // Create a transaction and sign it, making sure first the sender has the right to claim the spendale outputs let signature1 = try wallet1.sign(utxo: wallet1utxo) let signature2 = try wallet2.sign(utxo: wallet1utxo) @@ -18,48 +18,117 @@ final class BlockchainSwiftTests: XCTestCase { XCTAssert(verified1, "Wallet1 should have been verified") XCTAssert(!verified2, "Wallet2 should not have been verified") } - + func testTx() throws { // Two wallets, one blockchain - let wallet1 = Wallet()! - let wallet2 = Wallet()! - let blockchain = Blockchain(minerAddress: wallet1.address) + let node1 = Node(address: NodeAddress.centralAddress()) + let node2 = Node(address: NodeAddress(host: "localhost", port: 1337)) + let _ = node1.mineBlock() // Wallet1 has mined genesis block, and should have gotten the reward - XCTAssert(blockchain.balance(for: wallet1.address) == blockchain.currentBlockValue()) + XCTAssert(node1.blockchain.balance(for: node1.wallet.address) == node1.blockchain.currentBlockValue()) // Wallet2 is broke - XCTAssert(blockchain.balance(for: wallet2.address) == 0) - + XCTAssert(node1.blockchain.balance(for: node2.wallet.address) == 0) + // Send 1000 from Wallet1 to Wallet2, and again let wallet1 mine the next block - let _ = try blockchain.createTransaction(sender: wallet1, recipientAddress: wallet2.address, value: 1000) - XCTAssert(blockchain.mempool.count == 1) // One Tx should be in the pool, ready to go into the next block when mined - let _ = blockchain.mineBlock(previousHash: blockchain.lastBlock().hash, minerAddress: wallet1.address) - XCTAssert(blockchain.mempool.count == 0) // Tx pool should now be clear - + let _ = try node1.createTransaction(recipientAddress: node2.wallet.address, value: 1000) + XCTAssert(node1.mempool.count == 1) // One Tx should be in the pool, ready to go into the next block when mined + let _ = node1.mineBlock() + XCTAssert(node1.mempool.count == 0) // Tx pool should now be clear + // Wallet1 should now have a balance == two block rewards - 1000 - XCTAssert(blockchain.balance(for: wallet1.address) == (blockchain.currentBlockValue() * 2) - 1000) + XCTAssert(node1.blockchain.balance(for: node1.wallet.address) == (node1.blockchain.currentBlockValue() * 2) - 1000) // Wallet 2 should have a balance == 1000 - XCTAssert(blockchain.balance(for: wallet2.address) == 1000) - + XCTAssert(node1.blockchain.balance(for: node2.wallet.address) == 1000) + // Attempt to send more from Wallet1 than it currently has, expect failure do { - try blockchain.createTransaction(sender: wallet1, recipientAddress: wallet2.address, value: UInt64.max) + let _ = try node1.createTransaction(recipientAddress: node2.wallet.address, value: UInt64.max) XCTAssert(false, "Overdraft") } catch { } - + // Check sanity of utxo state, ensuring Wallet1 and Wallet2 has rights to their unspent outputs - let utxosWallet1 = blockchain.utxos.filter { $0.address == wallet1.address } - let utxosWallet2 = blockchain.utxos.filter { $0.address == wallet2.address } - XCTAssert(wallet1.canUnlock(utxos: utxosWallet1)) - XCTAssert(!wallet1.canUnlock(utxos: utxosWallet2)) - XCTAssert(wallet2.canUnlock(utxos: utxosWallet2)) - XCTAssert(!wallet2.canUnlock(utxos: utxosWallet1)) + let utxosWallet1 = node1.blockchain.findSpendableOutputs(for: node1.wallet.address) + let utxosWallet2 = node1.blockchain.findSpendableOutputs(for: node2.wallet.address) + XCTAssert(node1.wallet.canUnlock(utxos: utxosWallet1)) + XCTAssert(!node1.wallet.canUnlock(utxos: utxosWallet2)) + XCTAssert(node2.wallet.canUnlock(utxos: utxosWallet2)) + XCTAssert(!node2.wallet.canUnlock(utxos: utxosWallet1)) } - + func testNetworkSync() { + // Set up our network of 3 nodes, and letting the first node mine the genesis block + // Excpect the genesis block to propagate to all nodes + let initialSync = XCTestExpectation(description: "Initial sync") + let node1 = Node(address: NodeAddress.centralAddress()) + let _ = node1.mineBlock() + let node2 = Node(address: NodeAddress(host: "localhost", port: 1337)) + let node3 = Node(address: NodeAddress(host: "localhost", port: 7331)) + DispatchQueue.global().async { + while true { + if node2.blockchain.blocks.count == 1 && node3.blockchain.blocks.count == 1 { + initialSync.fulfill() + break + } + } + } + wait(for: [initialSync], timeout: 3) + + // Now create a transaction on node1 - from node1's wallet to node'2s wallet + // Expect everyone's mempool to update with the new transaction + let txSync = XCTestExpectation(description: "Sync transactions") + do { + let _ = try node1.createTransaction(recipientAddress: node2.wallet.address, value: 100) + } catch { + XCTAssert(false, "Overdraft") + } + DispatchQueue.global().async { + while true { + let requirements = [ + node1.mempool.count == node2.mempool.count, + node2.mempool.count == node3.mempool.count, + node3.mempool.count == 1 + ] + if requirements.allSatisfy({ $0 == true}) { + txSync.fulfill() + break + } + } + } + wait(for: [txSync], timeout: 3) + + // Now let node2 mine the next block, claiming the Coinbase reward as well as receiving 100 from the above transaction + // Expect every node's blocks to update, and everyones utxos to update appropriately + let mineSync = XCTestExpectation(description: "Mining sync") + let _ = node2.mineBlock() + DispatchQueue.global().async { + while true { + let requirements = [ + node1.blockchain.blocks.count == node2.blockchain.blocks.count, + node2.blockchain.blocks.count == node3.blockchain.blocks.count, + node3.blockchain.blocks.count == 2, + + node1.blockchain.balance(for: node2.wallet.address) == node2.blockchain.balance(for: node2.wallet.address), + node2.blockchain.balance(for: node2.wallet.address) == node3.blockchain.balance(for: node2.wallet.address), + node1.blockchain.balance(for: node2.wallet.address) == node1.blockchain.currentBlockValue() + 100, + + node1.blockchain.balance(for: node1.wallet.address) == node1.blockchain.currentBlockValue() - 100, + node2.blockchain.balance(for: node1.wallet.address) == node2.blockchain.currentBlockValue() - 100, + node3.blockchain.balance(for: node1.wallet.address) == node3.blockchain.currentBlockValue() - 100 + ] + if requirements.allSatisfy({ $0 == true}) { + mineSync.fulfill() + break + } + } + } + wait(for: [mineSync], timeout: 3) + + } static let allTests = [ ("testTxSigning", testTxSigning), - ("testTx", testTx) + ("testTx", testTx), + ("testNetworkSync", testNetworkSync) ] }