diff --git a/app/miner.js b/app/miner.js deleted file mode 100644 index 3c0f4f5..0000000 --- a/app/miner.js +++ /dev/null @@ -1,106 +0,0 @@ -const Block = require('../blockchain/block'); - -const ITERATIONS = 1; - -class Miner { - static STATE_RUNNING = 0; - static STATE_INTERRUPTED = 1; - - constructor(blockchain, transactionPool, reward, p2pServer) { - this.blockchain = blockchain; - this.transactionPool = transactionPool; - this.p2pServer = p2pServer; - this.state = Miner.STATE_INTERRUPTED; - this.lastBlock = null; - - this.minedStartTime = null; - - this.mining = {}; - this.mining.transactions = []; - this.mining.reward = reward; - this.mining.metadatas = []; - - this.startMine(); - } - - interrupt() { - if (this.state === Miner.STATE_RUNNING) { - this.state = Miner.STATE_INTERRUPTED; - } - } - - interruptIfContainsTransaction(transaction) { - if (this.state === Miner.STATE_RUNNING && this.mining.metadatas.find(t => t.id === transaction.id)) { - this.state = Miner.STATE_INTERRUPTED; - } - } - interruptIfContainsMetadata(metadata) { - if (this.state === Miner.STATE_RUNNING && this.mining.transactions.find(t => t.id === metadata.id)) { - this.state = Miner.STATE_INTERRUPTED; - } - } - - startMine() { - //only continue if state is waiting or restarting - if (this.state !== Miner.STATE_INTERRUPTED && this.state !== Miner.STATE_RESTARTING) { - return; - } - - this.minedStartTime = process.hrtime.bigint(); - - this.mining.transactions = this.transactionPool.validTransactionsCopy(); - this.mining.metadatas = this.transactionPool.validMetadatasCopy(); - - this.lastBlock = this.blockchain.chain[this.blockchain.chain.length - 1]; - - this.nonce = 0; - this.state = Miner.STATE_RUNNING; - - this.mine(); - } - - mine() { - if (this.state !== Miner.STATE_RUNNING) { - this.state = Miner.STATE_RESTARTING; - this.startMine(); - return; - } - const timestamp = Date.now(); - const difficulty = Block.adjustDifficulty(this.lastBlock, timestamp); - - for (let i = 0; i < ITERATIONS; ++i) { - const hash = Block.hash( - timestamp, - this.lastBlock.hash, - this.mining.reward, - this.mining.transactions, - this.mining.metadatas, - this.nonce, - difficulty); - - if (hash.substring(0, difficulty) === '0'.repeat(difficulty)) { - //success - const endTime = process.hrtime.bigint(); - console.log(`Mined a block of difficulty ${difficulty} in ${Number(endTime - this.minedStartTime) / 1000000}ms`); - this.p2pServer.blockMined(new Block( - timestamp, - this.lastBlock.hash, - hash, - this.mining.reward, - this.mining.transactions, - this.mining.metadatas, - this.nonce, - difficulty)); - this.state = Miner.STATE_RESTARTING; - setImmediate(() => { this.startMine() }); - } else { - //failure - this.nonce++; - } - } - setImmediate(() => { this.mine() }); - } -} - -module.exports = Miner; - diff --git a/app/p2p-server.js b/app/p2p-server.js deleted file mode 100644 index c17cf2b..0000000 --- a/app/p2p-server.js +++ /dev/null @@ -1,214 +0,0 @@ -const Websocket = require('ws'); - -const fs = require('fs'); -const process = require('process'); -const Miner = require('./miner'); -const Transaction = require('../wallet/transaction'); -const TransactionPool = require('../wallet/transaction-pool'); -const Metadata = require('../wallet/metadata'); -const Blockchain = require('../blockchain'); - -const P2P_PORT = process.env.P2P_PORT || 5000; -const peers = process.env.PEERS ? process.env.PEERS.split(',') : []; -const MESSAGE_TYPES = { - chain: 'CHAIN', - transaction: 'TRANSACTION', - clear_transactions: 'CLEAR_TRANSACTIONS', - metadata: 'METADATA' -}; - -class P2pServer { - constructor(transactionPool, rewardPublicKey, chainStorageLocation) { - this.blockchain = new Blockchain(); - this.transactionPool = transactionPool; - this.sockets = []; - this.chainStorageLocation = chainStorageLocation; - - //possible race if deleted after check, but we live with it I guess - if (fs.existsSync(this.chainStorageLocation)) { - const rawPersistedChain = fs.readFileSync(this.chainStorageLocation, 'utf8'); - const deserialized = Blockchain.deserialize(rawPersistedChain); - if (deserialized === null) { - console.log(`Couldn't deserialize chain at '${this.chainStorageLocation}', starting from genesis`); - } else { - this.blockchain = deserialized; - } - } else { - console.log("Didn't find a persisted chain, starting from genesis"); - } - - this.miner = new Miner(this.blockchain, this.transactionPool, rewardPublicKey, this); - } - - listen() { - const server = new Websocket.Server({ port: P2P_PORT }); - server.on('connection', socket => this.connectSocket(socket)); - - this.connectToPeers(); - - console.log(`Listening for peer-to-peer connections on: ${P2P_PORT}`); - } - - connectToPeers() { - peers.forEach(peer => { - const socket = new Websocket(peer); - - socket.on('open', () => this.connectSocket(socket)); - }); - } - - connectSocket(socket) { - this.sockets.push(socket); - console.log('Socket connected'); - - this.messageHandler(socket); - - this.sendChain(socket); - } - - messageHandler(socket) { - socket.on('message', message => { - const data = JSON.parse(message); - switch(data.type) { - case MESSAGE_TYPES.chain: - this.newChain(data.chain); - break; - case MESSAGE_TYPES.transaction: - this.newTransaction(data.transaction, false); - break; - case MESSAGE_TYPES.metadata: - this.newMetadata(data.metadata, false); - break; - //case MESSAGE_TYPES.clear_transactions: - // this.transactionPool.clear(); - // break; - } - }); - } - - newMetadata(metadata, broadcast) { - if (!Metadata.verifyMetadata(metadata)) { - console.log("Couldn't add metadata to p2pServer, couldn't verify"); - return; - } - - switch (this.transactionPool.updateOrAddMetadata(metadata)) { - case TransactionPool.Return.add: - this.miner.startMine(); - break; - case TransactionPool.Return.update: - this.miner.interruptIfContainsMetadata(metadata); - break; - case TransactionPool.Return.error: - console.log("Couldn't add metadata to p2pServer, couldn't updateOrAdd"); - return; - } - - if (broadcast === undefined || broadcast) { - this.broadcastMetadata(metadata); - } - } - - newTransaction(transaction, broadcast) { - if (!Transaction.verify(transaction)) { - console.log("Couldn't add transaction to p2pServer, couldn't verify"); - return false; - } - - switch (this.transactionPool.updateOrAddTransaction(transaction)) { - case TransactionPool.Return.add: - this.miner.startMine(); - break; - case TransactionPool.Return.update: - this.miner.interruptIfContainsTransaction(transaction); - break; - case TransactionPool.Return.error: - console.log("Couldn't add transaction to p2pServer, couldn't updateOrAdd"); - return; - } - - if (broadcast === undefined || broadcast) { - this.broadcastTransaction(transaction); - } - } - - blockMined(block) { - if (!this.blockchain.addBlock(block)) { - //invalid block, return - return; - } - this.transactionPool.clearFromBlock(block); - this.miner.interrupt(); - this.persistChain(this.blockchain); - this.syncChains(); - } - - newChain(chain, persist) { - const replaceResult = this.blockchain.replaceChain(chain); - if (!replaceResult.result) { - //failed to replace - return; - } - - for (let i = 0; i < replaceResult.chainDifference; i++) { - this.transactionPool.clearFromBlock(this.blockchain.chain[i]); - } - - this.miner.interrupt(); - - if (typeof persist === "undefined" || persist) { - this.persistChain(this.blockchain); - } - } - - persistChain(chain) { - try { - fs.writeFileSync( - this.chainStorageLocation, - chain.serialize()); - } catch (err) { - console.error(`Couldn't persist chain, aborting: ${err}`); - process.exit(-1); - } - } - - sendChain(socket) { - socket.send(JSON.stringify({ - type: MESSAGE_TYPES.chain, - chain: this.blockchain.serialize() - })); - } - - sendTransaction(socket, transaction) { - socket.send(JSON.stringify({ - type: MESSAGE_TYPES.transaction, - transaction - })); - } - - sendMetadata(socket, metadata) { - socket.send(JSON.stringify({ - type: MESSAGE_TYPES.metadata, - metadata - })); - } - syncChains() { - this.sockets.forEach(socket => this.sendChain(socket)); - } - - broadcastTransaction(transaction) { - this.sockets.forEach(socket => this.sendTransaction(socket, transaction)); - } - - broadcastMetadata(metadata) { - this.sockets.forEach(socket => this.sendMetadata(socket, metadata)); - } - - //broadcastClearTransactions() { - // this.sockets.forEach(socket => socket.send(JSON.stringify({ - // type: MESSAGE_TYPES.clear_transactions - // }))); - //} -} - -module.exports = P2pServer; \ No newline at end of file diff --git a/blockchain/block.js b/blockchain/block.js index 9c88485..fe73f3c 100644 --- a/blockchain/block.js +++ b/blockchain/block.js @@ -1,20 +1,43 @@ const ChainUtil = require('../chain-util'); -const { DIFFICULTY, MINE_RATE } = require('../config'); +const { DIFFICULTY, MINE_RATE } = require('../constants'); -function concatIfNotUndefined(concatTo, concatting) { - if (typeof concatting !== "undefined") { - concatTo += `${concatting}`; +function concatIfNotUndefined(concatTo, prefix, concatting) { + if (typeof concatting !== "undefined" && concatting.length !== 0) { + return concatTo + `${prefix}${concatting}`; + } else { + return concatTo; + } +} + +function getData(block, key) { + + const got = block[key]; + + if (typeof got !== "undefined" && got !== null) { + return got; + } else { + return []; } } class Block { - constructor(timestamp, lastHash, hash, reward, transactions, metadatas, nonce, difficulty) { + constructor(timestamp, lastHash, hash, reward, payments, sensorRegistrations, brokerRegistrations, integrations, nonce, difficulty) { this.timestamp = timestamp; this.lastHash = lastHash; this.hash = hash; this.reward = reward; - this.transactions = transactions; - this.metadatas = metadatas; + if (payments !== null && payments.length !== 0) { + this.payments = payments; + } + if (sensorRegistrations !== null && sensorRegistrations.length !== 0) { + this.sensorRegistrations = sensorRegistrations; + } + if (brokerRegistrations !== null && brokerRegistrations.length !== 0) { + this.brokerRegistrations = brokerRegistrations; + } + if (integrations !== null && integrations.length !== 0) { + this.integrations = integrations; + } this.nonce = nonce; if (difficulty === undefined) { this.difficulty = DIFFICULTY; @@ -23,20 +46,20 @@ class Block { } } - static getTransactions(block) { - if (typeof block.transactions !== "undefined" && block.transactions !== null) { - return block.transactions; - } else { - return []; - } + static getPayments(block) { + return getData(block, "payments"); } - static getMetadatas(block) { - if (typeof block.metadatas !== "undefined" && block.metadatas !== null) { - return block.metadatas; - } else { - return []; - } + static getSensorRegistrations(block) { + return getData(block, "sensorRegistrations"); + } + + static getBrokerRegistrations(block) { + return getData(block, "brokerRegistrations"); + } + + static getIntegrations(block) { + return getData(block, "integrations"); } toString() { @@ -52,16 +75,17 @@ class Block { } static genesis() { - return new this('Genesis time', '-----', 'f1r57-h45h', null, null, null, 0, DIFFICULTY); + return new this('Genesis time', '-----', 'f1r57-h45h', null, null, null, null, null, 0, DIFFICULTY); } - static hash(timestamp, lastHash, reward, transactions, metadatas, nonce, difficulty) { + static hash(timestamp, lastHash, reward, payments, sensorRegistrations, brokerRegistrations, integrations, nonce, difficulty) { //backwards compatible hashing: - //if we add a new type of thing to the chain, the hash of previous blocks won't change as if will be undefined - let hashing = `${timestamp}${lastHash}${nonce}${difficulty}`; - concatIfNotUndefined(hashing, reward); - concatIfNotUndefined(hashing, transactions); - concatIfNotUndefined(hashing, metadatas); + //if we add a new type of thing to the chain, the hash of previous blocks won't change as it will be undefined + let hashing = `${timestamp}${lastHash}${nonce}${difficulty}${reward}`; + hashing = concatIfNotUndefined(hashing, 'payments', payments); + hashing = concatIfNotUndefined(hashing, 'sensorRegistrations', sensorRegistrations); + hashing = concatIfNotUndefined(hashing, 'brokerRegistrations', brokerRegistrations); + hashing = concatIfNotUndefined(hashing, 'integrations', integrations); return ChainUtil.hash(hashing).toString(); } @@ -71,23 +95,17 @@ class Block { block.timestamp, block.lastHash, block.reward, - block.transactions, - block.metadatas, + block.payments, + block.sensorRegistrations, + block.brokerRegistrations, + block.integrations, block.nonce, block.difficulty); } //returns false if block's hash doesn't match internals static checkHash(block) { - - const computedHash = Block.hash( - block.timestamp, - block.lastHash, - block.reward, - block.transactions, - block.metadatas, - block.nonce, - block.difficulty); + const computedHash = Block.blockHash(block); if (computedHash !== block.hash) { return false; @@ -108,6 +126,40 @@ class Block { return Math.max(0, prevDifficulty - 1); } } + + static debugMine(lastBlock, reward, payments, sensorRegistrations,brokerRegistrations,integrations) { + const timestamp = Date.now(); + const difficulty = Block.adjustDifficulty(lastBlock, timestamp); + + let nonce = 0; + let hash = ''; + + do { + nonce++; + hash = Block.hash( + timestamp, + lastBlock.hash, + reward, + payments, + sensorRegistrations, + brokerRegistrations, + integrations, + nonce, + difficulty); + } while (hash.substring(0, difficulty) !== '0'.repeat(difficulty)); + + return new Block( + timestamp, + lastBlock.hash, + hash, + reward, + payments, + sensorRegistrations, + brokerRegistrations, + integrations, + nonce, + difficulty); + } } module.exports = Block; \ No newline at end of file diff --git a/blockchain/block.test.js b/blockchain/block.test.js index 0ba542a..9d4b567 100644 --- a/blockchain/block.test.js +++ b/blockchain/block.test.js @@ -4,13 +4,13 @@ describe('Block', () => { let data, lastBlock, block; beforeEach(() => { - data = 'bar'; + reward = 'bar'; lastBlock = Block.genesis(); - block = Block.mineBlock(lastBlock, data); + block = Block.debugMine(lastBlock, reward,[],[]); }); it('sets the `data` to match the input', () => { - expect(block.data).toEqual(data); + expect(block.reward).toEqual(reward); }); it('sets the `lastHash` to match the hash of the last block', () => { diff --git a/blockchain/blockchain.js b/blockchain/blockchain.js new file mode 100644 index 0000000..3cc7903 --- /dev/null +++ b/blockchain/blockchain.js @@ -0,0 +1,608 @@ +const Block = require('./block'); +const N3 = require('n3'); +const DataFactory = require('n3').DataFactory; +const Payment = require('./payment'); +const SensorRegistration = require('./sensor-registration'); +const BrokerRegistration = require('./broker-registration'); +const Integration = require('./integration'); +const fs = require('fs'); +const ChainUtil = require('../chain-util'); +const { + MINING_REWARD} = require('../constants'); + +function addRDF(store, metadata) { + for (const triple of metadata) { + store.addQuad(DataFactory.quad( + DataFactory.namedNode(triple.s), + DataFactory.namedNode(triple.p), + DataFactory.namedNode(triple.o))); + } +} + +function getBalanceCopyGeneric(publicKey, maps) { + for (const map of maps) { + if (map.hasOwnProperty(publicKey)) { + const found = map[publicKey]; + return { + balance: found.balance, + counter: found.counter + }; + } + } + + return { + balance: 0, + counter: 0 + }; +} + +function verifyPayment(changedBalances, prevBalances, reward, payment) { + const verifyRes = Payment.verify(payment); + if (!verifyRes.result) { + return { + result: false, + reason: "couldn't verify a payment: " + verifyRes.reason + }; + } + + const inputBalance = getBalanceCopyGeneric(payment.input, [changedBalances, prevBalances]); + + if (payment.counter <= inputBalance.counter) { + return { + result: false, + reason: "payment has invalid counter" + }; + } + inputBalance.counter = payment.counter; + + //first loop is to check it can be payed, second loop does the paying + if (inputBalance.balance < payment.rewardAmount) { + return { + result: false, + reason: "payment rewarding more than they have" + }; + } + inputBalance.balance -= payment.rewardAmount; + + for (const output of payment.outputs) { + if (inputBalance.balance < output.amount) { + return { + result: false, + reason: "payment spending more than they have" + }; + } + inputBalance.balance -= output.amount; + } + changedBalances[payment.input] = inputBalance; + + for (const output of payment.outputs) { + const outputBalance = getBalanceCopyGeneric(output.publicKey, [changedBalances, prevBalances]); + outputBalance.balance += output.amount; + changedBalances[output.publicKey] = outputBalance; + } + const rewardBalance = getBalanceCopyGeneric(reward, [changedBalances, prevBalances]); + rewardBalance.balance += payment.rewardAmount; + changedBalances[reward] = rewardBalance; + + return { + result: true + }; +} + +function verifyIntegration(changedBalances, prevBalances, reward, integration) { + const verifyRes = Integration.verify(integration); + if (!verifyRes.result) { + return { + result: false, + reason: "couldn't verify a integration: " + verifyRes.reason + }; + } + + const inputBalance = getBalanceCopyGeneric(integration.input, [changedBalances, prevBalances]); + + if (integration.counter <= inputBalance.counter) { + return { + result: false, + reason: "integration has invalid counter" + }; + } + inputBalance.counter = integration.counter; + + //first loop is to check it can be payed, second loop does the paying + if (inputBalance.balance < integration.rewardAmount) { + return { + result: false, + reason: "integration rewarding more than they have" + }; + } + inputBalance.balance -= integration.rewardAmount; + + for (const output of integration.outputs) { + if (inputBalance.balance < output.amount) { + return { + result: false, + reason: "integration spending more than they have" + }; + } + inputBalance.balance -= output.amount; + } + changedBalances[integration.input] = inputBalance; + + for (const output of integration.outputs) { + const outputBalance = getBalanceCopyGeneric(output.publicKey, [changedBalances, prevBalances]); + outputBalance.balance += output.amount; + changedBalances[output.publicKey] = outputBalance; + } + const rewardBalance = getBalanceCopyGeneric(reward, [changedBalances, prevBalances]); + rewardBalance.balance += integration.rewardAmount; + changedBalances[reward] = rewardBalance; + + return { + result: true + }; +} + +function verifySensorRegistration(changedBalances, prevBalances, reward, sensorRegistration, brokers) { + const verifyRes = SensorRegistration.verify(sensorRegistration); + if (!verifyRes.result) { + return { + result: false, + reason: "Couldn't verify a sensor registration: " + verifyRes.reason + }; + } + + const extInfo = SensorRegistration.getExtInformation(sensorRegistration); + + if (!extInfo.result) { + return { + result: false, + reason: "Couldn't get sensor registration ext information: " + extMetadata.reason + }; + } + + if (!(extInfo.metadata.integrationBroker in brokers)) { + console.log(brokers); + console.log(extInfo.metadata.integrationBroker); + return { + result: false, + reason: "Couldn't find sensor registration's nominated broker in the broker list" + }; + } + + const inputBalance = getBalanceCopyGeneric(sensorRegistration.input, [changedBalances, prevBalances]); + + if (sensorRegistration.counter <= inputBalance.counter) { + return { + result: false, + reason: "Sensor registration has invalid counter" + }; + } + inputBalance.counter = sensorRegistration.counter; + + if (inputBalance.balance < sensorRegistration.rewardAmount) { + return { + result: false, + reason: "Sensor registration rewarding more than they have" + }; + } + inputBalance.balance -= sensorRegistration.rewardAmount; + + changedBalances[sensorRegistration.input] = inputBalance; + + const rewardBalance = getBalanceCopyGeneric(reward, [changedBalances, prevBalances]); + rewardBalance.balance += sensorRegistration.rewardAmount; + changedBalances[reward] = rewardBalance; + + return { + result: true + }; +} + +function verifyBrokerRegistration(changedBalances, prevBalances, reward, brokerRegistration) { + const verifyRes = BrokerRegistration.verify(brokerRegistration); + if (!verifyRes.result) { + return { + result: false, + reason: "Couldn't verify a broker registration: " + verifyRes.reason + }; + } + + const inputBalance = getBalanceCopyGeneric(brokerRegistration.input, [changedBalances, prevBalances]); + + if (brokerRegistration.counter <= inputBalance.counter) { + return { + result: false, + reason: "Broker registration has invalid counter" + }; + } + inputBalance.counter = brokerRegistration.counter; + + if (inputBalance.balance < brokerRegistration.rewardAmount) { + return { + result: false, + reason: "Broker registration rewarding more than they have" + }; + } + inputBalance.balance -= brokerRegistration.rewardAmount; + + changedBalances[brokerRegistration.input] = inputBalance; + + const rewardBalance = getBalanceCopyGeneric(reward, [changedBalances, prevBalances]); + rewardBalance.balance += brokerRegistration.rewardAmount; + changedBalances[reward] = rewardBalance; + + return { + result: true + }; +} + +function verifyTxs(prevBalances, reward, brokers, payments, sensorRegistrations, brokerRegistrations, integrations) { + const changedBalances = {}; + + const rewardBalanceCopy = getBalanceCopyGeneric(reward, [prevBalances]); + + changedBalances[reward] = { + balance: rewardBalanceCopy.balance + MINING_REWARD, + counter: rewardBalanceCopy.counter + }; + + for (const payment of payments) { + const res = verifyPayment(changedBalances, prevBalances, reward, payment); + if (!res.result) { + return res; + } + } + + for (const integration of integrations) { + const res = verifyIntegration(changedBalances, prevBalances, reward, integration); + if (!res.result) { + return res; + } + } + + for (const brokerRegistration of brokerRegistrations) { + const res = verifyBrokerRegistration(changedBalances, prevBalances, reward, brokerRegistration); + if (!res.result) { + return res; + } + } + + for (const sensorRegistration of sensorRegistrations) { + const res = verifySensorRegistration(changedBalances, prevBalances, reward, sensorRegistration, brokers); + if (!res.result) { + return res; + } + } + + return { + result: true, + changedBalances: changedBalances + }; +} + +function verifyBlock(prevBalances, prevBlock, verifyingBlock, brokers) { + if (verifyingBlock.lastHash !== prevBlock.hash) { + return { + result: false, + reason: "last hash didn't match our last hash" + }; + } + //TODO how to check if new block's timestamp is believable + if (verifyingBlock.difficulty !== Block.adjustDifficulty(prevBlock, verifyingBlock.timestamp)) { + return { + result: false, + reason: "difficulty is incorrect" + }; + } + if (!Block.checkHash(verifyingBlock)) { + return { + result: false, + reason: "hash is invalid failed" + }; + } + + return verifyTxs(prevBalances, verifyingBlock.reward, brokers, + Block.getPayments(verifyingBlock), + Block.getSensorRegistrations(verifyingBlock), + Block.getBrokerRegistrations(verifyingBlock), + Block.getIntegrations(verifyingBlock)); +} + +function verifyChain(chain) { + if (chain.length === 0) { + return { + result: false, + reason: "zero length" + }; + } + if (ChainUtil.stableStringify(chain[0]) !== ChainUtil.stableStringify(Block.genesis())) { + return { + result: false, + reason: "initial block isn't genesis" + }; + } + + const balances = {}; + const brokers = {}; + + for (let i = 1; i < chain.length; i++) { + const block = chain[i]; + const lastBlock = chain[i - 1]; + + const verifyResult = verifyBlock(balances, lastBlock, block, brokers); + + if (verifyResult.result === false) { + return { + result: false, + reason: `Chain is invalid on block ${i}: ${verifyResult.reason}` + }; + } + + for (const publicKey in verifyResult.changedBalances) { + balances[publicKey] = verifyResult.changedBalances[publicKey]; + } + + const blockMetadata = getBlockMetadata(chain[i]); + addBlockMetadata(brokers, blockMetadata.brokers); + } + + return { + result: true, + balances: balances + }; +} + +//returns the first index where the two chains differ +function findChainDifference(oldChain, newChain) { + for (let i = 1; i < oldChain.length; ++i) { + if (oldChain[i].hash !== newChain[i].hash) { + return i; + } + } + return oldChain.length; +} + +function getBlockMetadata(block) { + + const returning = { + sensors: {}, + brokers: {}, + store: new N3.Store() + }; + + returning.store.addQuad( + DataFactory.namedNode(block.hash), + DataFactory.namedNode("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"), + DataFactory.namedNode("http://SSM/Block")); + returning.store.addQuad( + DataFactory.namedNode(block.hash), + DataFactory.namedNode("http://SSM/lastBlock"), + DataFactory.namedNode(block.lastHash)); + + for (const tx of Block.getSensorRegistrations(block)) { + addRDF(returning.store, tx.metadata); + + const extData = SensorRegistration.getExtInformation(tx).metadata; + returning.store.addQuad( + DataFactory.namedNode(block.hash), + DataFactory.namedNode("http://SSM/Transaction"), + DataFactory.namedNode(extData.sensorName)); + returning.store.addQuad( + DataFactory.namedNode(block.hash), + DataFactory.namedNode("http://SSM/SensorRegistration"), + DataFactory.namedNode(extData.sensorName)); + + returning.sensors[extData.sensorName] = extData; + } + for (const tx of Block.getBrokerRegistrations(block)) { + addRDF(returning.store, tx.metadata); + + const extData = BrokerRegistration.getExtInformation(tx).metadata; + returning.store.addQuad( + DataFactory.namedNode(block.hash), + DataFactory.namedNode("http://SSM/Transaction"), + DataFactory.namedNode(extData.brokerName)); + returning.store.addQuad( + DataFactory.namedNode(block.hash), + DataFactory.namedNode("http://SSM/SBrokerRegistration"), + DataFactory.namedNode(extData.brokerName)); + + returning.brokers[extData.brokerName] = extData; + } + + return returning; +} + +//returns the undoing object +function addBlockMetadata(map, metadatas) { + + const returning = {}; + + for (const key in metadatas) { + const value = metadatas[key]; + + if (key in map) { + returning[key] = map[key]; + } else { + returning[key] = null; + } + + map[key] = value; + } +} + +function undoBlockMetadata(map, undoer) { + for (const key in undoer) { + const value = undoer[key]; + + if (value === null) { + delete map[key]; + } else { + map[key] = value; + } + } +} + +class Blockchain { + constructor() { + this.chain = [Block.genesis()]; + this.balances = {}; + this.stores = []; + this.sensors = {}; + this.sensorUndos = []; + this.brokers = {}; + this.brokerUndos = []; + } + + getBalanceCopy(publicKey) { + return getBalanceCopyGeneric(publicKey, [this.balances]); + } + + getSensorInfo(sensorName) { + if (sensorName in this.sensors) { + return this.sensors[sensorName]; + } else { + return null; + } + } + + getBrokerInfo(brokerName) { + if (brokerName in this.brokers) { + return this.brokers[brokerName]; + } else { + return null; + } + } + + lastBlock() { + return this.chain[this.chain.length - 1]; + } + + serialize() { + return JSON.stringify(this.chain); + } + + static deserialize(serialized) { + return JSON.parse(serialized); + } + + saveToDisk(location) { + try { + fs.writeFileSync( + location, + this.serialize()); + } catch (err) { + console.log(`Couldn't save blockchain to disk: ${err}`); + return false; + } + return true; + } + + static loadFromDisk(location) { + //possible race if deleted after check, but we live with it I guess + if (fs.existsSync(location)) { + const rawPersistedChain = fs.readFileSync(location, 'utf8'); + const deserialized = Blockchain.deserialize(rawPersistedChain); + const returning = new Blockchain(); + const replaceResult = returning.replaceChain(deserialized); + if (!replaceResult.result) { + console.log(`Couldn't deserialize chain at '${location}', starting from genesis`); + } + return returning; + } else { + console.log("Didn't find a persisted chain, starting from genesis"); + return new Blockchain(); + } + } + + //adds an existing block to the blockchain, returns false if the block can't be added, true if it was added + addBlock(newBlock) { + const verifyResult = verifyBlock(this.balances, this.lastBlock(), newBlock, this.brokers); + + if (!verifyResult.result) { + console.log(`Couldn't add block: ${verifyResult.reason}`); + return false; + } + + //all seems to be good, persist + this.chain.push(newBlock); + + for (const publicKey in verifyResult.changedBalances) { + this.balances[publicKey] = verifyResult.changedBalances[publicKey]; + } + + const metadata = getBlockMetadata(newBlock); + + this.stores.push(metadata.store); + this.sensorUndos.push(addBlockMetadata(this.sensors, metadata.sensors)); + this.brokerUndos.push(addBlockMetadata(this.brokers, metadata.brokers)); + + //console.log("Added new block"); + //console.log(newBlock); + + return true; + } + + wouldBeValidBlock(rewardee, payments, sensorRegistrations, brokerRegistrations, integrations) { + return verifyTxs(this.balances, rewardee, this.brokers, payments, sensorRegistrations, brokerRegistrations, integrations).result; + } + + static isValidChain(chain) { + const res = verifyChain(chain); + + return res.result; + } + + //return result: false on fail, result: true on success + //TODO: faster verification of the new chain by only verifying from divergence, would require saving some historical balance state + replaceChain(newChain) { + if (newChain.length <= this.chain.length) { + return { + result: false, + reason: "Received chain is not longer than the current chain." + }; + } + const verifyResult = verifyChain(newChain); + if (!verifyResult.result) { + return { + result: false, + reason: `The received chain is not valid: ${verifyResult.reason}` + }; + } + + //Replacing blockchain with the new chain + + const oldChain = this.chain; + this.chain = newChain; + + //find where they differ + const chainDifference = findChainDifference(oldChain, newChain); + + //fix metadata + for (let i = oldChain.length - 1; i >= chainDifference; i--) { + this.stores.pop(); + undoBlockMetadata(this.sensors, this.sensorUndos[i]); + this.sensorUndos.pop(); + undoBlockMetadata(this.brokers, this.brokerUndos[i]); + this.brokerUndos.pop(); + } + for (let i = chainDifference; i < newChain.length; ++i) { + const metadata = getBlockMetadata(newChain[i]); + + this.stores.push(metadata.store); + this.sensorUndos.push(addBlockMetadata(this.sensors, metadata.sensors)); + this.brokerUndos.push(addBlockMetadata(this.brokers, metadata.brokers)); + } + + //fix balance + this.balances = verifyResult.balances; + + return { + result: true, + chainDifference: chainDifference, + oldChain: oldChain + }; + } +} + +module.exports = Blockchain; \ No newline at end of file diff --git a/blockchain/blockchain.test.js b/blockchain/blockchain.test.js new file mode 100644 index 0000000..a295669 --- /dev/null +++ b/blockchain/blockchain.test.js @@ -0,0 +1,55 @@ +const Blockchain = require('./index'); +const Block = require('./block'); + +describe('Blockchain', () => { + let bc, bc2; + + beforeEach(() => { + bc = new Blockchain(); + bc2 = new Blockchain(); + }); + + it('starts with genesis block', () => { + expect(bc.chain[0]).toEqual(Block.genesis()); + }); + + it('adds a new block', () => { + const reward = 'test-reward-key'; + expect(bc.addBlock(Block.debugMine(bc.lastBlock(),reward,[],[]))).toBe(true); + + expect(bc.lastBlock().reward).toEqual(reward); + }); + + it('validates a valid chain', () => { + expect(bc2.addBlock(Block.debugMine(bc2.lastBlock(), 'test-reward-key', [], []))).toBe(true); + + expect(Blockchain.isValidChain(bc2.chain)).toBe(true); + }); + + it('invalidates a chain with a corrupt genesis block', () => { + bc2.chain[0].hash = 'Bad data'; + + expect(Blockchain.isValidChain(bc2.chain)).toBe(false); + }); + + it('invalidates a corrupt chain', () => { + expect(bc2.addBlock(Block.debugMine(bc2.lastBlock(), 'test-reward-key', [], []))).toBe(true); + bc2.chain[1].reward = 'Not foo'; + + expect(Blockchain.isValidChain(bc2.chain)).toBe(false); + }); + + it('replaces the chain with a valid chain', () => { + expect(bc2.addBlock(Block.debugMine(bc2.lastBlock(), 'test-reward-key', [], []))).toBe(true); + expect(bc.replaceChain(bc2.chain).result).toBe(true); + + expect(bc.chain).toEqual(bc2.chain); + }); + + it('does not replace the chain with one of less than or equal to length', () => { + expect(bc.addBlock(Block.debugMine(bc.lastBlock(), 'test-reward-key', [], []))).toBe(true); + expect(bc.replaceChain(bc2.chain).result).toBe(false); + + expect(bc.chain).not.toEqual(bc2.chain); + }) +}); \ No newline at end of file diff --git a/blockchain/broker-registration.js b/blockchain/broker-registration.js new file mode 100644 index 0000000..8e5b4a4 --- /dev/null +++ b/blockchain/broker-registration.js @@ -0,0 +1,189 @@ +const ChainUtil = require('../chain-util'); + +const tripleValidator = { + s: ChainUtil.validateIsString, + p: ChainUtil.validateIsString, + o: ChainUtil.validateIsString +}; + +function validateMetadata(t) { + + let isBroker = []; + let costPerMinute = []; + let costPerKB = []; + let integrationEndpoint = []; + + const validationRes = ChainUtil.validateArray(t, ChainUtil.createValidateObject(tripleValidator)); + + if (!validationRes.result) { + return validationRes; + } + + for (const triple of t) { + switch (triple.p) { + case "IoT device metadata/Cost_of_Using_IoT_Devices/Cost_Per_Minute": costPerMinute.push(triple); break; + case "IoT device metadata/Cost_of_Using_IoT_Devices/Cost_Per_Kbyte": costPerKB.push(triple); break; + case "http://www.w3.org/1999/02/22-rdf-syntax-ns#type": + if (triple.o === "SSM/Broker") { + isBroker.push(triple.s); + } + break; + case "IoT device metadata/Integration/Endpoint": integrationEndpoint.push(triple); break; + } + } + + if (isBroker.length === 0) { + return { + result: false, + reason: "No broker is defined" + }; + } else if (isBroker.length > 1) { + return { + result: false, + reason: "Multiple brokers are defined" + }; + } + + const brokerName = isBroker[0]; + + if (costPerMinute.length === 0) { + return { + result: false, + reason: "No cost per minute was defined" + }; + } else if (costPerMinute.length > 1) { + return { + result: false, + reason: "Multiple cost per minutes were defined" + } + } + const CostPerMinuteValue = Number.parseInt(costPerMinute[0].o); + if (CostPerMinuteValue === NaN) { + return { + result: false, + reason: "Couldn't parse cost per minute as an integer" + }; + } else if (CostPerMinuteValue < 1) { + return { + result: false, + reason: "Cost per minute was negative" + } + } else if (costPerMinute[0].s != brokerName) { + return { + result: false, + reason: "Cost per minute object isn't the broker" + }; + } + + if (costPerKB.length === 0) { + return { + result: false, + reason: "No cost per KB was defined" + }; + } else if (costPerKB.length > 1) { + return { + result: false, + reason: "Multiple cost per KB were defined" + } + } + const CostPerKBValue = Number.parseInt(costPerKB[0].o); + if (CostPerKBValue === NaN) { + return { + result: false, + reason: "Couldn't parse cost per KB as an integer" + }; + } else if (CostPerKBValue < 1) { + return { + result: false, + reason: "Cost per KB was negative" + } + } else if (costPerKB[0].s != brokerName) { + return { + result: false, + reason: "Cost per KB object isn't the broker" + }; + } + + if (integrationEndpoint.length === 0) { + return { + result: false, + reason: "No integration endpoint was defined" + }; + } else if (integrationEndpoint.length > 1) { + return { + result: false, + reason: "Multiple integration endpoints were defined" + }; + } else if (integrationEndpoint[0].s != brokerName) { + return { + result: false, + reason: "Integration endpoint object isn't the broker" + }; + } + + return { + result: true, + metadata: { + brokerName: brokerName, + costPerMinute: CostPerMinuteValue, + costPerKB: CostPerKBValue, + integrationEndpoint: integrationEndpoint[0].o + } + }; +} + +const baseValidation = { + input: ChainUtil.validateIsPublicKey, + counter: ChainUtil.validateIsInteger, + rewardAmount: ChainUtil.createValidateIsIntegerWithMin(0), + metadata: validateMetadata, + signature: ChainUtil.validateIsSignature +}; + +class BrokerRegistration { + constructor(senderKeyPair, counter, metadata, rewardAmount) { + this.input = senderKeyPair.getPublic().encode('hex'); + this.counter = counter; + this.rewardAmount = rewardAmount; + this.metadata = metadata; + this.signature = senderKeyPair.sign(BrokerRegistration.hashToSign(this)); + + const verification = BrokerRegistration.verify(this); + if (!verification.result) { + throw new Error(verification.reason); + } + } + + static hashToSign(registration) { + return ChainUtil.hash([ + registration.counter, + registration.rewardAmount, + registration.metadata]); + } + + static verify(registration) { + const validationRes = ChainUtil.validateObject(registration, baseValidation); + if (!validationRes.result) { + return validationRes; + } + + const signatureRes = ChainUtil.verifySignature( + registration.input, + registration.signature, + BrokerRegistration.hashToSign(registration)); + + if (!signatureRes.result) { + return signatureRes.reason; + } + + return { + result: true + }; + } + + static getExtInformation(registration) { + return validateMetadata(registration.metadata); + } +} + +module.exports = BrokerRegistration; \ No newline at end of file diff --git a/blockchain/index.js b/blockchain/index.js deleted file mode 100644 index 0b215d6..0000000 --- a/blockchain/index.js +++ /dev/null @@ -1,278 +0,0 @@ -const Block = require('./block'); -const N3 = require('n3'); -const DataFactory = require('n3').DataFactory; -const Transaction = require('../wallet/transaction'); -const { MINING_REWARD } = require('../config'); - -function getBalanceCopyGeneric(publicKey, maps) { - for (const map of maps) { - if (map.hasOwnProperty(publicKey)) { - const found = map[publicKey]; - return { - balance: found.balance, - counter: found.counter - }; - } - } - - return { - balance: 0, - counter: 0 - }; -} - -function verifyBlock(prevBalances, prevBlock, verifyingBlock) { - if (verifyingBlock.lastHash !== prevBlock.hash) { - return { - result: false, - reason: "last hash didn't match our last hash" - }; - } - //how to check if new block's timestamp is believable - if (verifyingBlock.difficulty !== Block.adjustDifficulty(prevBlock, verifyingBlock.timestamp)) { - return { - result: false, - reason: "difficulty is incorrect" - }; - } - if (!Block.checkHash(verifyingBlock)) { - return { - result: false, - reason: "hash is invalid failed" - }; - } - - const changedBalances = {}; - - const rewardBalanceCopy = getBalanceCopyGeneric(verifyingBlock.reward, [prevBalances]); - - changedBalances[verifyingBlock.reward] = { - balance: rewardBalanceCopy.balance + MINING_REWARD, - counter: rewardBalanceCopy.counter - }; - - for (const transaction of Block.getTransactions(verifyingBlock)) { - if (!Transaction.verify(transaction)) { - return { - result: false, - reason: "couldn't verify a transaction" }; - } - - const inputBalance = getBalanceCopyGeneric(transaction.input, [changedBalances, prevBalances]); - - if (transaction.counter <= inputBalance.counter) { - return { - result: false, - reason: "transaction has invalid counter" - }; - } - - inputBalance.counter = transaction.counter; - - for (const output of transaction.outputs) { - const outputBalance = getBalanceCopyGeneric(output.publicKey, [changedBalances, prevBalances]); - - if (output.amount > inputBalance.balance) { - return { - result: false, - reason: "transaction spending more than they have" - }; - } - inputBalance.balance -= output.amount; - outputBalance.balance += output.amount; - changedBalances[output.publicKey] = outputBalance; - } - - changedBalances[transaction.input] = inputBalance; - } - - return { - result: true, - changedBalances: changedBalances - }; -} - -function verifyChain(chain) { - if (chain.length === 0) { - return { - result: false, - reason: "zero length" - }; - } - if (JSON.stringify(chain[0]) !== JSON.stringify(Block.genesis())) { - return { - result: false, - reason: "initial block isn't genesis" - }; - } - - const balances = {}; - - for (let i = 1; i < chain.length; i++) { - const block = chain[i]; - const lastBlock = chain[i - 1]; - - const verifyResult = verifyBlock(balances, lastBlock, block); - - if (verifyResult.result === false) { - return { - result: false, - reason: `Chain is invalid on block ${i}: ${verifyResult.reason}` - }; - } - - for (const publicKey in verifyResult.changedBalances) { - balances[publicKey] = verifyResult.changedBalances[publicKey]; - } - } - - return { - result: true, - balances: balances - }; -} - -//returns the first index where the two chains differ -function findChainDifference(oldChain, newChain) { - for (let i = 1; i < oldChain.length; ++i) { - if (oldChain[i].hash !== newChain[i].hash) { - return i; - } - } - return 1; -} - -function addBlockMetadata(blockchain, block) { - const metadatas = Block.getMetadatas(block); - for (const metadata of metadatas) { - if (!("SSNmetadata" in metadata)) { - //assert? - return; - } - - var ssn = metadata.SSNmetadata; - - const parser = new N3.Parser(); - - parser.parse( - ssn, - (error, quadN, prefixes) => { - if (quadN) { - blockchain.store.addQuad(DataFactory.quad( - DataFactory.namedNode(quadN.subject.id), - DataFactory.namedNode(quadN.predicate.id), - DataFactory.namedNode(quadN.object.id), - DataFactory.namedNode(metadata.id))); - } - }); - } -} - -class Blockchain { - constructor() { - this.chain = [Block.genesis()]; - this.balances = {}; - this.store = new N3.Store(); - } - - getBalanceCopy(publicKey) { - return getBalanceCopyGeneric(publicKey, [this.balances]); - } - - lastBlock() { - return this.chain[this.chain.length - 1]; - } - - serialize() { - return JSON.stringify(this.chain); - } - - static deserialize(serialized) { - const returning = new Blockchain(); - const replaceResult = returning.replaceChain(JSON.parse(serialized)); - if(!replaceResult.result) { - //chain wasn't valid - return null; - } else { - return returning; - } - } - - //adds an existing block to the blockchain, returns false if the block can't be added, true if it was added - addBlock(newBlock) { - const verifyResult = verifyBlock(this.balances, this.lastBlock(), newBlock); - - if (!verifyResult.result) { - console.log(`Couldn't add block: ${verifyResult.reason}`); - return false; - } - - //all seems to be good, persist - this.chain.push(newBlock); - - for (const publicKey in verifyResult.changedBalances) { - this.balances[publicKey] = verifyResult.changedBalances[publicKey]; - } - - addBlockMetadata(this, newBlock); - - //console.log("Added new block"); - //console.log(newBlock); - - return true; - } - - static isValidChain(chain) { - const res = verifyChain(chain); - - return res.result; - } - - //return false on fail, true on success - //TODO: faster verification of the new chain by only verifying from divergence, would require saving some historical balance state - replaceChain(newChain) { - if (newChain.length <= this.chain.length) { - return { - result: false, - reason: "Received chain is not longer than the current chain." - }; - } - const verifyResult = verifyChain(newChain); - if (!verifyResult.result) { - return { - result: false, - reason: `The received chain is not valid: ${verifyResult.reason}` - }; - } - - //Replacing blockchain with the new chain - - const oldChain = this.chain; - this.chain = newChain; - - //find where they differ - const chainDifference = findChainDifference(oldChain, newChain); - console.log(`chain difference was ${chainDifference}`); - - //fix metadata - for (let i = oldChain.length - 1; i >= chainDifference; i--) { - for (const metadata of Block.getMetadatas(oldChain[i])) { - this.store.deleteGraph(metadata.id); - } - } - for (let i = chainDifference; i < newChain.length; ++i) { - addBlockMetadata(this, newChain[i]); - } - - //fix balance - this.balances = verifyResult.balances; - - return { - result: true, - chainDifference: chainDifference, - oldChain: oldChain - }; - } -} - -module.exports = Blockchain; \ No newline at end of file diff --git a/blockchain/index.test.js b/blockchain/index.test.js deleted file mode 100644 index c599871..0000000 --- a/blockchain/index.test.js +++ /dev/null @@ -1,55 +0,0 @@ -const Blockchain = require('./index'); -const Block = require('./block'); - -describe('Blockchain', () => { - let bc, bc2; - - beforeEach(() => { - bc = new Blockchain(); - bc2 = new Blockchain(); - }); - - it('starts with genesis block', () => { - expect(bc.chain[0]).toEqual(Block.genesis()); - }); - - it('adds a new block', () => { - const data = 'foo'; - bc.addBlock(data); - - expect(bc.chain[bc.chain.length-1].data).toEqual(data); - }); - - it('validates a valid chain', () => { - bc2.addBlock('foo'); - - expect(bc.isValidChain(bc2.chain)).toBe(true); - }); - - it('invalidates a chain with a corrupt genesis block', () => { - bc2.chain[0].data = 'Bad data'; - - expect(bc.isValidChain(bc2.chain)).toBe(false); - }); - - it('invalidates a corrupt chain', () => { - bc2.addBlock('foo'); - bc2.chain[1].data = 'Not foo'; - - expect(bc.isValidChain(bc2.chain)).toBe(false); - }); - - it('replaces the chain with a valid chain', () => { - bc2.addBlock('goo'); - bc.replaceChain(bc2.chain); - - expect(bc.chain).toEqual(bc2.chain); - }); - - it('does not replace the chain with one of less than or equal to length', () => { - bc.addBlock('foo'); - bc.replaceChain(bc2.chain); - - expect(bc.chain).not.toEqual(bc2.chain); - }) -}); \ No newline at end of file diff --git a/blockchain/integration.js b/blockchain/integration.js new file mode 100644 index 0000000..dcc8191 --- /dev/null +++ b/blockchain/integration.js @@ -0,0 +1,81 @@ +const ChainUtil = require('../chain-util'); + +const outputValidation = { + publicKey: ChainUtil.validateIsPublicKey, + sensor: ChainUtil.validateIsString, + amount: ChainUtil.createValidateIsIntegerWithMin(1), +}; + +function validateOutputs(t) { + if (!ChainUtil.validateArray(t, (output) => { + return ChainUtil.validateObject(output, outputValidation).result; + })) { + return false; + } + + if (t.outputs.length <= 0) { + return false; + } + + return true; +} + +const baseValidation = { + input: ChainUtil.validateIsPublicKey, + counter: ChainUtil.createValidateIsIntegerWithMin(1), + rewardAmount: ChainUtil.createValidateIsIntegerWithMin(0), + outputs: validateOutputs, + signature: ChainUtil.validateIsSignature +}; + +class Integration { + constructor(senderKeyPair, counter, outputs, rewardAmount) { + this.input = senderKeyPair.getPublic().encode('hex'); + this.counter = counter; + this.rewardAmount = rewardAmount; + this.outputs = outputs; + this.signature = senderKeyPair.sign(Integration.hashToSign(this)); + + + const verification = Integration.verify(this); + if (!verification.result) { + throw new Error(verification.reason); + } + } + + static createOutput(recipientPublicKey, sensorId, amount) { + return { + publicKey: recipientPublicKey, + sensor: sensorId, + amount: amount + }; + } + + static hashToSign(registration) { + return ChainUtil.hash([ + registration.counter, + registration.rewardAmount, + registration.outputs]); + } + + static verify(registration) { + const validationRes = ChainUtil.validateObject(registration, baseValidation); + if (!validationRes.result) { + return validationRes; + } + + const verifyRes = ChainUtil.verifySignature( + registration.input, + registration.signature, + Integration.hashToSign(registration)); + if (!verifyRes.result) { + return verifyRes; + } + + return { + result: true + }; + } +} + +module.exports = Integration; \ No newline at end of file diff --git a/blockchain/payment.js b/blockchain/payment.js new file mode 100644 index 0000000..471697b --- /dev/null +++ b/blockchain/payment.js @@ -0,0 +1,78 @@ +const ChainUtil = require('../chain-util'); + +const outputValidation = { + publicKey: ChainUtil.validateIsPublicKey, + amount: ChainUtil.createValidateIsIntegerWithMin(1) +}; + +function validateOutputs(t) { + if (!ChainUtil.validateArray(t, function (output) { + return ChainUtil.validateObject(output, outputValidation).result; + })) { + return false; + } + + if (t.length <= 0) { + return false; + } + + return true; +} + +const baseValidation = { + input: ChainUtil.validateIsPublicKey, + counter: ChainUtil.createValidateIsIntegerWithMin(1), + rewardAmount: ChainUtil.createValidateIsIntegerWithMin(0), + outputs: validateOutputs, + signature: ChainUtil.validateIsSignature +} + +class Payment { + constructor(senderKeyPair, counter, outputs, rewardAmount) { + this.input = senderKeyPair.getPublic().encode('hex'); + this.counter = counter; + this.rewardAmount = rewardAmount; + this.outputs = outputs; + this.signature = senderKeyPair.sign(Payment.hashToSign(this)); + + const verification = Payment.verify(this); + if (!verification.result) { + throw new Error(verification.reason); + } + } + + static hashToSign(transaction) { + return ChainUtil.hash([ + transaction.counter, + transaction.rewardAmount, + transaction.outputs]); + } + + static createOutput(recipient, amount) { + return { + publicKey: recipient, + amount: amount + }; + } + + static verify(transaction) { + const validationRes = ChainUtil.validateObject(transaction, baseValidation); + if (!validationRes.result) { + return validationRes; + } + + const verifyRes = ChainUtil.verifySignature( + transaction.input, + transaction.signature, + Payment.hashToSign(transaction)); + if (!verifyRes.result) { + return verifyRes; + } + + return { + result: true, + }; + } +} + +module.exports = Payment; \ No newline at end of file diff --git a/wallet/transaction.test.js b/blockchain/payment.test.js similarity index 97% rename from wallet/transaction.test.js rename to blockchain/payment.test.js index e00c75c..66f1f80 100644 --- a/wallet/transaction.test.js +++ b/blockchain/payment.test.js @@ -1,6 +1,6 @@ const Transaction = require('./transaction'); const Wallet = require('./index'); -const { MINING_REWARD } = require('../config'); +const { MINING_REWARD } = require('../constants'); describe('Transaction', () => { let transaction, wallet, recipient, amount; diff --git a/blockchain/sensor-registration.js b/blockchain/sensor-registration.js new file mode 100644 index 0000000..d190e6b --- /dev/null +++ b/blockchain/sensor-registration.js @@ -0,0 +1,188 @@ +const ChainUtil = require('../chain-util'); + +const tripleValidator = { + s: ChainUtil.validateIsString, + p: ChainUtil.validateIsString, + o: ChainUtil.validateIsString +}; + +function validateMetadata(t) { + let isSensor = []; + let costPerMinute = []; + let costPerKB = []; + let integrationBroker = []; + + const validationRes = ChainUtil.validateArray(t, ChainUtil.createValidateObject(tripleValidator)); + + if (!validationRes.result) { + return validationRes; + } + + for (const triple of t) { + switch (triple.p) { + case "IoT device metadata/Cost_of_Using_IoT_Devices/Cost_Per_Minute": costPerMinute.push(triple); break; + case "IoT device metadata/Cost_of_Using_IoT_Devices/Cost_Per_Kbyte": costPerKB.push(triple); break; + case "http://www.w3.org/1999/02/22-rdf-syntax-ns#type": + if (triple.o === "http://www.w3.org/ns/sosa/Sensor") { + isSensor.push(triple.s); + } + break; + case "IoT device metadata/Integration/Broker": integrationBroker.push(triple); break; + } + } + + if (isSensor.length === 0) { + return { + result: false, + reason: "No sensor is defined" + }; + } else if (isSensor.length > 1) { + return { + result: false, + reason: "Multiple sensors are defined" + }; + } + + const sensorName = isSensor[0]; + + if (costPerMinute.length === 0) { + return { + result: false, + reason: "No cost per minute was defined" + }; + } else if (costPerMinute.length > 1) { + return { + result: false, + reason: "Multiple cost per minutes were defined" + } + } + const CostPerMinuteValue = Number.parseInt(costPerMinute[0].o); + if (CostPerMinuteValue === NaN) { + return { + result: false, + reason: "Couldn't parse cost per minute as an integer" + }; + } else if (CostPerMinuteValue < 1) { + return { + result: false, + reason: "Cost per minute was negative" + } + } else if (costPerMinute[0].s != sensorName) { + return { + result: false, + reason: "Cost per minute object isn't the broker" + }; + } + + if (costPerKB.length === 0) { + return { + result: false, + reason: "No cost per KB was defined" + }; + } else if (costPerKB.length > 1) { + return { + result: false, + reason: "Multiple cost per KB were defined" + } + } + const CostPerKBValue = Number.parseInt(costPerKB[0].o); + if (CostPerKBValue === NaN) { + return { + result: false, + reason: "Couldn't parse cost per KB as an integer" + }; + } else if (CostPerKBValue < 1) { + return { + result: false, + reason: "Cost per KB was negative" + } + } else if (costPerKB[0].s != sensorName) { + return { + result: false, + reason: "Cost per KB object isn't the broker" + }; + } + + if (integrationBroker.length === 0) { + return { + result: false, + reason: "No integration broker was defined" + }; + } else if (integrationBroker.length > 1) { + return { + result: false, + reason: "Multiple integration brokers were defined" + }; + } else if (integrationBroker[0].s != sensorName) { + return { + result: false, + reason: "Integration broker subjsect isn't the sensor" + }; + } + + return { + result: true, + metadata: { + sensorName: sensorName, + costPerMinute: CostPerMinuteValue, + costPerKB: CostPerKBValue, + integrationBroker: integrationBroker[0].o + } + }; +} + +const baseValidation = { + input: ChainUtil.validateIsPublicKey, + counter: ChainUtil.createValidateIsIntegerWithMin(1), + rewardAmount: ChainUtil.createValidateIsIntegerWithMin(0), + metadata: validateMetadata, + signature: ChainUtil.validateIsSignature +}; + +class SensorRegistration { + constructor(senderKeyPair, counter, metadata, rewardAmount) { + this.input = senderKeyPair.getPublic().encode('hex'); + this.counter = counter; + this.rewardAmount = rewardAmount; + this.metadata = metadata; + this.signature = senderKeyPair.sign(SensorRegistration.hashToSign(this)); + + const verification = SensorRegistration.verify(this); + if (!verification.result) { + throw new Error(verification.reason); + } + } + + static hashToSign(registration) { + return ChainUtil.hash([ + registration.counter, + registration.rewardAmount, + registration.metadata]); + } + + static verify(registration) { + const validationResult = ChainUtil.validateObject(registration, baseValidation); + if (!validationResult.result) { + console.log(`Failed validation: ${validationResult.reason}`); + return false; + } + + const verifyRes = ChainUtil.verifySignature( + registration.input, + registration.signature, + SensorRegistration.hashToSign(registration)); + if (!verifyRes.result) { + return verifyRes; + } + + return { + result: true + }; + } + + static getExtInformation(registration) { + return validateMetadata(registration.metadata); + } +} + +module.exports = SensorRegistration; \ No newline at end of file diff --git a/wallet/metadata.test.js b/blockchain/sensor-registration.test.js similarity index 98% rename from wallet/metadata.test.js rename to blockchain/sensor-registration.test.js index 695b187..a364093 100644 --- a/wallet/metadata.test.js +++ b/blockchain/sensor-registration.test.js @@ -1,7 +1,7 @@ const Transaction = require('./transaction'); const Metadata = require('./metadata'); const Wallet = require('./index'); -const { MINING_REWARD } = require('../config'); +const { MINING_REWARD } = require('../constants'); describe('Transaction & Metadata', () => { let transaction, metadata, wallet, recipient, amount, diff --git a/blockchain/transaction.js b/blockchain/transaction.js new file mode 100644 index 0000000..df99ffb --- /dev/null +++ b/blockchain/transaction.js @@ -0,0 +1,18 @@ +const Payment = require('./payment'); +const Integration = require('./integration'); +const SensorRegistration = require('./sensor-registration'); +const BrokerRegistration = require('./broker-registration'); + +class Transaction { + constructor(transaction, type) { + this.transaction = transaction; + this.verify = type.verify; + this.type = type; + } + + static mapId(type) { + return type.name(); + } +}; + +module.exports = Transaction; \ No newline at end of file diff --git a/broker/broker-app.js b/broker/broker-app.js new file mode 100644 index 0000000..cbc7801 --- /dev/null +++ b/broker/broker-app.js @@ -0,0 +1,219 @@ +//BROKER +const express = require('express'); +const bodyParser = require('body-parser'); +const P2pServer = require('../p2p-server'); +const Broker = require('./broker'); + +const Aedes = require('aedes'); + +const Config = require('../config'); +const ChainUtil = require('../chain-util'); + +const QueryEngine = require('@comunica/query-sparql-rdfjs').QueryEngine; +const Blockchain = require('../blockchain/blockchain'); + +'use strict'; + +const { + DEFAULT_PORT_BROKER_API, + DEFAULT_PORT_BROKER_CHAIN, + DEFAULT_PORT_BROKER_SENSOR_HANDSHAKE, + DEFAULT_PORT_BROKER_SENSOR_MQTT, + DEFAULT_PORT_BROKER_CLIENT_MQTT, + DEFAULT_PORT_MINER_CHAIN +} = require('../constants'); + +const CONFIGS_STORAGE_LOCATION = "./settings.json"; + +const config = new Config(CONFIGS_STORAGE_LOCATION); + +const broker = new Broker(config.get({ + key: "broker-keypair", + default: ChainUtil.genKeyPair(), + transform: ChainUtil.deserializeKeyPair +})); +const broker_name = config.get({ + key: "broker-name", + default: broker.keyPair.getPublic().encode('hex') +}); +const apiPort = config.get({ + key: "broker-api-port", + default: DEFAULT_PORT_BROKER_API +}); +const blockchainLocation = config.get({ + key: "broker-blockchain-location", + default: "./broker_blockchain.json" +}); +const chainServerPort = config.get({ + key: "broker-chain-server-port", + default: DEFAULT_PORT_BROKER_CHAIN +}); +const chainServerPeers = config.get({ + key: "broker-chain-server-peers", + default: ["ws://127.0.0.1:" + DEFAULT_PORT_MINER_CHAIN] +}); +const sensorHandshakePort = config.get({ + key: "broker-sensor-handshake-port", + default: DEFAULT_PORT_BROKER_SENSOR_HANDSHAKE +}); +const sensorMQTTPort = config.get({ + key: "broker-sensor-MQTT-port", + default: DEFAULT_PORT_BROKER_SENSOR_MQTT +}); +const clientMQTTPort = config.get({ + key: "broker-client-MQTT-port", + default: DEFAULT_PORT_BROKER_CLIENT_MQTT +}); + +const blockchain = Blockchain.loadFromDisk(blockchainLocation); + +let sensorsServing = {}; + +const sensorMQTT = new Aedes({ + id: broker_name +}); +const sensorMQTTServer = require('net').createServer(sensorMQTT.handle); +const sensorMQTTSubscriptions = {}; +const clientMQTT = new Aedes({ + id: broker_name +}); +const clientMQTTServer = require('net').createServer(clientMQTT.handle); + +function onNewPacket(sensor, data) { + //check to see if sensor has been paid for + + clientMQTT.publish({ + topic: sensor, + payload: data + }); +} + +function onChainServerRecv(data) { + const replaceResult = blockchain.replaceChain(Blockchain.deserialize(data)); + if (!replaceResult.result) { + console.log(`Failed to replace chain: ${replaceResult.reason}`); + //failed to replace + return; + } + + blockchain.saveToDisk(blockchainLocation); + + sensorsServing = {}; + + for (const sensorName in blockchain.sensors) { + const sensorData = blockchain.sensors[sensorName]; + + if (sensorData.integrationBroker === broker_name) { + sensorsServing[sensorName] = sensorData; + } + } + + //UNSUBSCRIBE + for (const sensorName in sensorMQTTSubscriptions) { + if (!(sensorName in sensorsServing)) { + + const deliverFunction = sensorMQTTSubscriptions[sensorName]; + + sensorMQTT.unsubscribe(sensorName, deliverFunction, () => { }); + + delete sensorMQTTSubscriptions[sensorName]; + } + } + + //SUBSCRIBE + for (const sensorName in sensorsServing) { + if (!(sensorName in sensorMQTTSubscriptions)) { + const deliverFunction = (packet, cb) => { + onNewPacket(packet.topic, packet.payload); + cb(); + }; + + sensorMQTTSubscriptions[sensorName] = deliverFunction; + + sensorMQTT.subscribe(sensorName, deliverFunction, () => { }); + } + } +} +function onSensorHandshakeMsg(sensor, data) { + onNewPacket(sensor, data); +} + +const chainServer = new P2pServer("Chain-server"); +chainServer.start(chainServerPort, chainServerPeers, (_) => { }, onChainServerRecv); + +broker.start(sensorHandshakePort, onSensorHandshakeMsg); +sensorMQTTServer.listen(sensorMQTTPort, () => { + console.log("Sensor MQTT started"); +}); +clientMQTTServer.listen(clientMQTTPort, () => { + console.log("Client MQTT started"); +}); + +const app = express(); +app.use(bodyParser.json()); + + +app.listen(apiPort, () => console.log(`Listening on port ${apiPort}`)); + +app.get('/sensors', (req, res) => { + res.json(sensorsServing); +}); + +app.get('/ChainServer/sockets', (req, res) => { + res.json(chainServer.sockets); +}); +app.post('/ChainServer/connect', (req, res) => { + chainServer.connect(req.body.url); + res.json("Connecting"); +}); + +app.get('/public-key', (req, res) => { + res.json(wallet.publicKey); +}); + +app.get('/key-pair', (req, res) => { + res.json(ChainUtil.serializeKeyPair(wallet.keyPair)); +}); + +app.get('/MyBalance', (req, res) => { + res.json(blockchain.getBalanceCopy(wallet.publicKey)); +}); +app.get('/Balance', (req, res) => { + const balance = blockchain.getBalanceCopy(req.body.publicKey); + res.json(balance); +}); +app.get('/Balances', (req, res) => { + const balances = blockchain.balances; + res.json(balances); +}); + +const myEngine = new QueryEngine(); + +app.post('/sparql', (req, res) => { + const start = async function () { + try { + let result = []; + const bindingsStream = await myEngine.queryBindings( + req.body.query, + { + readOnly: true, + sources: getBlockchain().stores + }); + bindingsStream.on('data', (binding) => { + result.push(binding); + }); + bindingsStream.on('end', () => { + res.json(JSON.stringify(result)); + }); + bindingsStream.on('error', (err) => { + console.error(err); + }); + } catch (err) { + console.error(err); + res.json("Error occured while querying"); + } + }; + + start() + +}); \ No newline at end of file diff --git a/broker/broker.js b/broker/broker.js new file mode 100644 index 0000000..38cf298 --- /dev/null +++ b/broker/broker.js @@ -0,0 +1,102 @@ +const Websocket = require('ws'); +//const Mqtt = require('mqtt'); +//const Aedes = require('aedes')(); /* aedes is a stream-based MQTT broker */ +//const MQTTserver = require('net').createServer(aedes.handle); + +const ChainUtil = require('../chain-util'); +const crypto = require('crypto'); + +const STATE_CLIENT_HELLOING = 0; +const STATE_OPERATIONAL = 1; + +function onClientHelloing(parent, socket, data) { + const asJson = JSON.parse(data); + const owner = asJson.owner; + const sensor = asJson.sensor; + const signature = asJson.signature; + const clientNonce = asJson.clientNonce; + + if (typeof owner !== 'string' || typeof sensor !== 'string' || typeof signature !== 'object' || typeof clientNonce !== 'string') { + console.log("Bad client hello"); + socket.close(); + return; + } + socket.owner = owner; + socket.sensor = sensor; + socket.clientNonce = clientNonce; + const verifyRes = ChainUtil.verifySignature(owner, signature, socket.serverNonce + clientNonce); + if (!verifyRes.result) { + console.log("Bad client hello signature: " + verifyRes.reason); + socket.close(); + return; + } + + const ourSig = parent.keyPair.sign(clientNonce + socket.serverNonce); + socket.send(JSON.stringify(ourSig)); + socket.state = STATE_OPERATIONAL; + console.log(`Sensor ${socket.owner}:${socket.sensor} is operational`); +} + +function onOperational(parent, socket, data) { + parent.onMessage(socket.sensor, data); +} + +function onSocketMessage(parent, socket, data) { + switch (socket.state) { + case STATE_CLIENT_HELLOING: onClientHelloing(parent, socket, data); break; + case STATE_OPERATIONAL: onOperational(parent, socket, data); break; + default: throw Error("Invalid internal state"); + } +} + +class Socket { + constructor(parent, socket, serverNonce) { + this.parent = parent; + this.socket = socket; + this.serverNonce = serverNonce; + this.state = STATE_CLIENT_HELLOING; + this.socket.on('message', (data) => { + onSocketMessage(parent, this, data); + }); + this.send(serverNonce); + } + + send(data) { + this.socket.send(data); + } + + close() { + this.socket.close(); + } +} + +function onConnection(broker, rawSocket) { + console.log("Sensor connected"); + crypto.randomBytes(2048, (err, buf) => { + if (err) { + console.log(`Couldn't generate server nonce: ${err}`); + rawSocket.close(); + return; + } + new Socket(broker, rawSocket, buf.toString('hex')); + }); +} + +class Broker { + constructor(keyPair) { + //owner:sensor->mqtt channel + this.brokering = {}; + this.keyPair = keyPair; + } + + start(sensorPort, onMessage) { + this.onMessage = onMessage; + this.sensorPort = sensorPort; + this.server = new Websocket.Server({ port: this.sensorPort }); + this.server.on('connection', socket => onConnection(this, socket)); + + console.log(`Broker listening for sensors on: ${this.sensorPort}`); + } +} + +module.exports = Broker; \ No newline at end of file diff --git a/chain-util.js b/chain-util.js index 2585723..1840a02 100644 --- a/chain-util.js +++ b/chain-util.js @@ -3,6 +3,22 @@ const SHA256 = require('crypto-js/sha256'); const { v1 : uuidV1 } = require ('uuid'); const ec = new EC('secp256k1'); +function convertJsonKeyValueToRDFImpl(key, object) { + const returning = []; + + for (const key in object) { + const value = object[key]; + + if (value instanceof Array) { + } else if (value instanceof Object) { + returning.push({ + o + }); + } else { + } + } +} + class ChainUtil { static genKeyPair() { return ec.genKeyPair(); @@ -13,11 +29,21 @@ class ChainUtil { } static hash(data) { - return SHA256(JSON.stringify(data)).toString(); + return SHA256(ChainUtil.stableStringify(data)).toString(); } static verifySignature(publicKey, signature, dataHash) { - return ec.keyFromPublic(publicKey, 'hex').verify(dataHash, signature); + //TODO, validate signature object + if (!ec.keyFromPublic(publicKey, 'hex').verify(dataHash, signature)) { + return { + result: false, + reason: "Couldn't verify signature" + }; + } else { + return { + result: true + }; + } } static deserializeKeyPair(serialized) { @@ -27,6 +53,221 @@ class ChainUtil { static serializeKeyPair(keyPair) { return keyPair.getPrivate().toString('hex'); } + + //stable stringify for hashing + static stableStringify(object) { + + if (object instanceof Array) { + let returning = '['; + + for (let i = 0; i < object.length; i++) { + if (typeof object[i] === "undefined" || object[i] === null) { + returning += "null"; + } else { + returning += ChainUtil.stableStringify(object[i]); + } + if (i !== object.length - 1) { + returning += ','; + } + } + + returning += ']'; + return returning; + } + if (!(object instanceof Object)) { + return JSON.stringify(object); + } + + let returning = '{'; + + const keys = Object.keys(object).sort(); + + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + if (!object.hasOwnProperty(key)) { + continue; + } + if (typeof object[key] === "undefined" || object[key] === null) { + continue; + } else { + returning += `"${key}":${ChainUtil.stableStringify(object[key])}`; + if (i !== keys.length - 1) { + returning += ','; + } + } + } + returning += '}'; + return returning; + } + + static validateAlways(_) { + return { + result: true + }; + } + + static validateIsObject(t) { + if (typeof t !== 'object') { + return { + result: false, + reason: "Is not an object" + }; + } + + return { + result: true + }; + } + + static validateIsString(t) { + if (typeof t === 'string') { + return { + result: true + }; + } else { + return { + result: false, + reason: "Is not string" + }; + } + } + + static validateIsInteger(t) { + if (typeof t !== 'number') { + return { + result: false, + reason: "Is not number" + }; + + } else if (!Number.isInteger(t)) { + return { + result: false, + reason: "Is not integer" + }; + } + return { + result: true + }; + } + + //includes minimum + static validateIsIntegerWithMin(t, minimum) { + if (typeof t !== 'number') { + return { + result: false, + reason: "Is not number" + }; + } else if (!Number.isInteger(t)) { + return { + result: false, + reason: "Is not integer" + }; + } else if (t < minimum) { + return { + result: false, + reason: "Is below minimum" + } + } + return { + result: true + }; + } + + //includes minimum + static createValidateIsIntegerWithMin(minimum) { + return (t) => { + return ChainUtil.validateIsIntegerWithMin(t, minimum); + }; + } + + static validateIsPublicKey(t) { + //TODO + return { + result: true + }; + } + + static validateIsSignature(t) { + //TODO + return { + result: true + }; + } + + static validateArray(t, memberValidator) { + if (!(t instanceof Array)) { + return { + result: false, + reason: "Is not an Array" + }; + } + for (const member of t) { + const res = memberValidator(member); + if (!res.result) { + return { + result: false, + reason: "Array member validation failed: " + res.reason + }; + } + } + return { + result: true + }; + } + + static createValidateArray(memberValidator) { + return function (t) { + return ChainUtil.validateArray(t, memberValidator); + }; + } + + static validateObject(t, memberValidator) { + if (!(t instanceof Object)) { + return { + result: false, + reason: "Is not an object" + }; + } + + for (const key in memberValidator) { + const validator = memberValidator[key]; + + if (!(key in t)) { + return { + result: false, + reason: "Couldn't find key: " + key + }; + } + + const res = validator(t[key]); + + if (!res.result) { + return { + result: false, + reason: `Validator for key '${key}' failed: ${res.reason}` + }; + } + } + + for (const key in t) { + if (!(key in memberValidator)) { + return { + result: false, + reason: "Verifying has key not in validators" + } + } + } + + return { + result: true + }; + } + + static createValidateObject(memberValidator) { + return function (t) { + return ChainUtil.validateObject(t, memberValidator); + } + } } module.exports = ChainUtil; \ No newline at end of file diff --git a/config.js b/config.js index 6da75a5..91cc3ca 100644 --- a/config.js +++ b/config.js @@ -1,6 +1,37 @@ -const DIFFICULTY = 3; -const MINE_RATE = 3000; -const INITIAL_BALANCE = 500; -const MINING_REWARD = 50; +const fs = require('fs'); +const process = require('process'); -module.exports = { DIFFICULTY, MINE_RATE, INITIAL_BALANCE, MINING_REWARD }; \ No newline at end of file +class Config { + constructor(location, disallow_arg_overide) { + //possible race if deleted after check, but we live with it I guess + + const looking = location; + + if (typeof disallow_arg_overide === undefined || disallow_arg_overide === null || !disallow_arg_overide) { + const args = process.argv.slice(2); + if (args.length > 0) { + looking = args[0]; + } + } + + if (fs.existsSync(looking)) { + const rawSettings = fs.readFileSync(looking, 'utf8'); + this.settings = JSON.parse(rawSettings); + } + } + + get(config) { + if (this.settings.hasOwnProperty(config.key)) { + const value = this.settings[config.key]; + if (config.hasOwnProperty('transform')) { + return config.transform(value); + } else { + return value; + } + } else { + return config.default; + } + } +} + +module.exports = Config; \ No newline at end of file diff --git a/constants.js b/constants.js new file mode 100644 index 0000000..938ca03 --- /dev/null +++ b/constants.js @@ -0,0 +1,46 @@ +const DIFFICULTY = 3; +const MINE_RATE = 3000; +const MINING_REWARD = 50; + +const DEFAULT_PORT_MINER_BASE = 3000; +const DEFAULT_PORT_MINER_API = DEFAULT_PORT_MINER_BASE + 1; +const DEFAULT_PORT_MINER_CHAIN = DEFAULT_PORT_MINER_BASE + 2; +const DEFAULT_PORT_MINER_TX_SHARE = DEFAULT_PORT_MINER_BASE + 3; +const DEFAULT_PORT_MINER_TX_RECV = DEFAULT_PORT_MINER_BASE + 4; + + +const DEFAULT_PORT_WALLET_BASE = 4000; +const DEFAULT_PORT_WALLET_API = DEFAULT_PORT_WALLET_BASE + 1; +const DEFAULT_PORT_WALLET_CHAIN = DEFAULT_PORT_WALLET_BASE + 2; + +const DEFAULT_PORT_BROKER_BASE = 5000; +const DEFAULT_PORT_BROKER_API = DEFAULT_PORT_BROKER_BASE + 1; +const DEFAULT_PORT_BROKER_CHAIN = DEFAULT_PORT_BROKER_BASE + 2; +const DEFAULT_PORT_BROKER_SENSOR_HANDSHAKE = DEFAULT_PORT_BROKER_BASE + 3; +const DEFAULT_PORT_BROKER_SENSOR_MQTT = DEFAULT_PORT_BROKER_BASE + 4; +const DEFAULT_PORT_BROKER_CLIENT_MQTT = DEFAULT_PORT_BROKER_BASE + 5; + +const DEFAULT_PORT_SENSOR_BASE = 6000; +const DEFAULT_PORT_SENSOR_API = DEFAULT_PORT_SENSOR_BASE + 1; + +module.exports = { + DIFFICULTY, + MINE_RATE, + MINING_REWARD, + + DEFAULT_PORT_MINER_API, + DEFAULT_PORT_MINER_CHAIN, + DEFAULT_PORT_MINER_TX_SHARE, + DEFAULT_PORT_MINER_TX_RECV, + + DEFAULT_PORT_WALLET_API, + DEFAULT_PORT_WALLET_CHAIN, + + DEFAULT_PORT_BROKER_API, + DEFAULT_PORT_BROKER_CHAIN, + DEFAULT_PORT_BROKER_SENSOR_HANDSHAKE, + DEFAULT_PORT_BROKER_SENSOR_MQTT, + DEFAULT_PORT_BROKER_CLIENT_MQTT, + + DEFAULT_PORT_SENSOR_API +}; \ No newline at end of file diff --git a/app/index.js b/miner/miner-app.js similarity index 64% rename from app/index.js rename to miner/miner-app.js index d8cefab..6fcb89a 100644 --- a/app/index.js +++ b/miner/miner-app.js @@ -28,147 +28,145 @@ * */ -const LoggerPretty = require("@comunica/logger-pretty").LoggerPretty; - const express = require('express'); const bodyParser = require('body-parser'); -const P2pServer = require('./p2p-server'); -const Wallet = require('../wallet'); -const TransactionPool = require('../wallet/transaction-pool'); -const QueryEngine = require('@comunica/query-sparql').QueryEngine; -const ChainUtil = require('../chain-util'); +const P2pServer = require('../p2p-server'); +const QueryEngine = require('@comunica/query-sparql-rdfjs').QueryEngine; -const jsonld = require('jsonld'); -var mqtt = require('mqtt'); -var aedes = require('aedes')(); /* aedes is a stream-based MQTT broker */ -var MQTTserver = require('net').createServer(aedes.handle); -const fs = require('fs'); /* file system (fs) module allows you to work with - the file system on your computer*/ -const multer = require('multer');/* Multer is a node.js middleware for handling multipart/form-data - , which is primarily used for uploading files.*/ +const Blockchain = require('../blockchain/blockchain'); +const Miner = require('./miner'); 'use strict';/* "use strict" is to indicate that the code should be executed in "strict mode". With strict mode, you can not, for example, use undeclared variables.*/ -const SETTINGS_STORAGE_LOCATION = "./settings.json"; -const SETTING_MINER_PUBLIC_KEY = "miner-public-key"; -const SETTING_WALLET_PRIVATE_KEY = "wallet-private-key"; +const Config = require('../config'); -var settings = {}; +const Payment = require('../blockchain/payment'); +const Integration = require('../blockchain/integration'); +const SensorRegistration = require('../blockchain/sensor-registration'); +const BrokerRegistration = require('../blockchain/broker-registration'); +const Transaction = require('../blockchain/transaction'); -//possible race if deleted after check, but we live with it I guess -if (fs.existsSync(SETTINGS_STORAGE_LOCATION)) { - const rawSettings = fs.readFileSync(SETTINGS_STORAGE_LOCATION, 'utf8'); - settings = JSON.parse(rawSettings); +const { + DEFAULT_PORT_MINER_API, + DEFAULT_PORT_MINER_CHAIN, + DEFAULT_PORT_MINER_TX_SHARE, + DEFAULT_PORT_MINER_TX_RECV +} = require('../constants'); + +const CONFIGS_STORAGE_LOCATION = "./settings.json"; + +const config = new Config(CONFIGS_STORAGE_LOCATION); + +const minerPublicKey = config.get({ + key: "miner-public-key", + default: "" +}); +const blockchainLocation = config.get({ + key: "miner-blockchain-location", + default: "./miner_blockchain.json" +}); +const chainServerPort = config.get({ + key: "miner-chain-server-port", + default: DEFAULT_PORT_MINER_CHAIN +}); +const chainServerPeers = config.get({ + key: "miner-chain-server-peers", + default: [] +}); +const txShareServerPort = config.get({ + key: "miner-tx-share-server-port", + default: DEFAULT_PORT_MINER_TX_SHARE +}); +const txShareServerPeers = config.get({ + key: "miner-tx-share-server-peers", + default: [] +}); +const txRecvServerPort = config.get({ + key: "miner-tx-recv-port", + default: DEFAULT_PORT_MINER_TX_RECV +}); +const apiPort = config.get({ + key: "miner-api-port", + default: DEFAULT_PORT_MINER_API +}); + +const blockchain = Blockchain.loadFromDisk(blockchainLocation); + +function onMined(block) { + if (!blockchain.addBlock(block)) { + //invalid block, return + return; + } + + miner.onNewBlock(block); + blockchain.saveToDisk(blockchainLocation); + chainServer.broadcast(blockchain.serialize()); } +function onChainServerConnect(socket) { + console.log("onChainServerConnect"); + P2pServer.send(socket, blockchain.serialize()); +} + +function onChainServerRecv(data) { + const replaceResult = blockchain.replaceChain(data); + if (!replaceResult.result) { + //failed to replace + return; + } + + for (let i = replaceResult.chainDifference; i < blockchain.chain.length; i++) { + miner.onNewBlock(blockchain.chain[i]); + } + + blockchain.saveToDisk(blockchainLocation); +} + +const chainServer = new P2pServer("Chain-server"); +const txShareServer = new P2pServer("Tx-share-server"); +const txRecvServer = new P2pServer("Tx-share-server"); +const miner = new Miner(blockchain, minerPublicKey, onMined); + +chainServer.start(chainServerPort, chainServerPeers, onChainServerConnect, onChainServerRecv); + const app = express(); - -//wallet init -var wallet = null; - -if (settings.hasOwnProperty(SETTING_WALLET_PRIVATE_KEY)) { - wallet = new Wallet(ChainUtil.deserializeKeyPair(settings[SETTING_WALLET_PRIVATE_KEY])); -} else { - wallet = new Wallet(ChainUtil.genKeyPair()); -} - -//miner public key init -var minerPublicKey = null; - -if (settings.hasOwnProperty(SETTING_MINER_PUBLIC_KEY)) { - minerPublicKey = settings[SETTING_MINER_PUBLIC_KEY]; -} else { - minerPublicKey = wallet.publicKey; -} - -const tp = new TransactionPool(); -const p2pServer = new P2pServer(tp, minerPublicKey, './persist_block_chain.json'); const myEngine = new QueryEngine(); -function getBlockchain() { - return p2pServer.blockchain; -} - app.use(bodyParser.json()); -//initialising a local storage for storing metadata file initially before storing it in the tripple store -const storage = multer.diskStorage({ - destination: function(req, file, cb) { - cb(null, './uploads/'); - }, - filename: function(req, file, cb) { - cb(null, new Date().toISOString() + file.originalname); - } -}); - //filtering the type of uploaded Metadata files - const fileFilter = (req, file, cb) => { - // reject a file - if (file.mimetype === 'application/json' || file.mimetype === 'text/plain' || file.mimetype === 'turtle') { - cb(null, true); - } else { - cb(null, false); - } -}; -// defining a storage and setup limits for storing metadata file initially before storing it in the tripple store -const upload = multer({ - storage: storage, - limits: { - fileSize: 1024 * 1024 * 5 - }, - fileFilter: fileFilter -}); - -// innitialising the HTTP PORT to listen -const port = process.env.HTTP_PORT || 3000; -app.listen(port, () => console.log(`Listening on port ${port}`)); -p2pServer.listen(); +// initialising the HTTP PORT to listen +app.listen(apiPort, () => console.log(`Listening on port ${apiPort}`)); //aedes mqtt server intialization -const MQTTport = process.env.MQTT_PORT || 1882; -MQTTserver.listen(MQTTport, function () { - console.log('MQTTserver listening on port', MQTTport) -}) +//const MQTTport = process.env.MQTT_PORT || 1882; +//MQTTserver.listen(MQTTport, function () { +// console.log('MQTTserver listening on port', MQTTport) +//}) -app.use('/uploads', express.static('uploads')); // to store uploaded metadata to '/uploads' folder -app.use(bodyParser.json()); // - -//API HELPERS -function +app.use(bodyParser.json()); // GET APIs app.get('/blocks', (req, res) => { - res.json(bc.chain); -}); -/////////////// -app.get('/MetaDataTransactions', (req, res) => { - res.json(tp.metadataS); -}); -/////////////// -app.get('/PaymentTransactions', (req, res) => { - res.json(tp.transactions); + res.json(blockchain.chain); }); /////////////// app.get('/Transactions', (req, res) => { - res.json(tp); + res.json(miner.txs); }); -/////////////// -//app.get('/mine-transactions', (req, res) => { -// const block = miner.mine(); -// console.log(`New block added: ${block.toString()}`); -// res.redirect('/blocks'); -// // res.json("Block mined"); -//}); -/////////////// app.get('/public-key', (req, res) => { - res.json({ publicKey: wallet.publicKey }); + res.json(minerPublicKey); }); /////////////// +app.get('/MinerBalance', (req, res) => { + const balance = blockchain.getBalanceCopy(minerPublicKey); + res.json(balance); +}); app.get('/Balance', (req, res) => { - const balance = getBlockchain().getBalanceCopy(wallet.publicKey); - res.json({ Balance: balance.balance }); + const balance = blockchain.getBalanceCopy(req.body.publicKey); + res.json(balance); }); app.get('/Balances', (req, res) => { - const balances = getBlockchain().balances; + const balances = blockchain.balances; res.json(balances); }); @@ -177,128 +175,51 @@ app.get('/Balances', (req, res) => { app.get('/quads', (req, res) => { //for (const quad of store) //console.log(quad); - res.json(store); - + res.json(blockchain.stores); }); -app.get('/IoTdeviceRegistration', (req, res)=> { - fs.readdir('./uploads', function(err, files) { - //console.log(files[files.length-2]); - var FileName = files[files.length-2]; - let rawdata = fs.readFileSync(`./uploads/${FileName}`); - let SenShaMartDesc = JSON.parse(rawdata); - /* the following piece of code is used to genrate JSON object out of name-value pairs submitted - let SenShaMartExtNames = ['Name','Geo' ,'IP_URL' , 'Topic_Token', 'Permission', 'RequestDetail', - 'OrgOwner', 'DepOwner','PrsnOwner', 'PaymentPerKbyte', - 'PaymentPerMinute','Protocol', 'MessageAttributes', 'Interval', - 'FurtherDetails'] - let SenShaMartExtValues = [Name,Geo ,IP_URL , Topic_Token, Permission, RequestDetail, - OrgOwner, DepOwner,PrsnOwner, PaymentPerKbyte, - PaymentPerMinute,Protocol, MessageAttributes, Interval, - FurtherDetails] - let SenSHaMArtExt = {}; - for (let i =0; i { - //console.log(nquads) - var metadata = wallet.createMetadata( - nquads); - p2pServer.newMetadata(metadata); - }); - }); - res.json("MetadataTransactionCreated"); - }); - -app.get('/storeSize', (req, res) => { - res.json({ - size: getBlockchain().store.size - }); +app.get('/brokers', (req, res) => { + res.json(blockchain.brokers); }); -////////////////////////////////////////////////// -// POST APIs +app.get('/sensors', (req, res) => { + res.json(blockchain.sensors); +}); -//this doesn't work well with the continious miner -//app.post('/mine', (req, res) => { -// const block = bc.addBlock(req.body.data); -// console.log(`New block added: ${block.toString()}`); -// p2pServer.newBlock(block); +app.get('/ChainServer/sockets', (req, res) => { + res.json(chainServer.sockets); +}); +app.post('/ChainServer/connect', (req, res) => { + chainServer.connect(req.body.url); + res.json("Connecting"); +}); -// res.redirect('/blocks'); -//}); -/////////////// -app.post('/PaymentTransaction', (req, res) => { - if (!req.body.hasOwnProperty('recpient')) { - res.json({ - result: false, - reason: "Missing \"recipient\" in body" - }); +function newTransaction(res, body, type) { + const verifyRes = type.verify(body); + if (!verifyRes.result) { + res.json(`Failed to verify ${type.name}: ${verifyRes.reason}`); return; } - if (!req.body.hasOwnProperty('amount')) { - res.json({ - result: false, - reason: "Missing \"amount\" in body" - }); - return; - } - const { recipient, amount } = req.body; - const transaction = wallet.createTransaction(recipient, amount, getBlockchain()); - if (transaction === null) { - res.json("Couldn't create transaction"); - return; - } - p2pServer.newTransaction(transaction); - res.json(transaction); -}); -/////////////// -app.post('/IoTdevicePaymentTransaction', (req, res) => { - if (!req.body.hasOwnProperty("Recipient_payment_address")) { - req.json({ - result: false, - reason: "Missing \"Recipient_ - } - } - const { Recipient_payment_address, Amount_of_money, Payment_method, - Further_details} = req.body; - if (Payment_method == "SensorCoin") { - //create coin transaction doesn't exist yet - const PaymentTransaction = wallet.createCoinTransaction( - Recipient_payment_address, Amount_of_money, bc, tp); - p2pServer.broadcastCoinTransaction(PaymentTransaction); - res.json("PaymentTransactionCreated"); - } - else if (Payment_method == "Bitcoin") { - res.redirect('/BitcoinTransaction') - } - else if (Payment_method == "PayPal") { - res.redirect('/PayPalTransaction') - } + miner.addTransaction(new Transaction(body, type)); + res.json("Added to pool"); +} + +app.post('/Payment', (req, res) => { + newTransaction(res, req.body, Payment); }); -/////////////// -app.post("/UploadMetafile", upload.single('file'), (req, res) => { - // recipient: req.body.recipient, - // amount : req.body.amount, - // const Geo = req.body.Geo; - // const IPSO = req.body.IPSO; - // const Type = req.body.Type; - // const Permission = req.body.Permission; - // const OrgOwner = req.body.OrgOwner; - const file = req.file; - //file : req.body.file - - res.status(201).json({ - message: 'Uploading Metadata was successful', - MetadataFile : file + +app.post('/Integration', (req, res) => { + newTransaction(res, req.body, Integration); }); + +app.post('/BrokerRegistration', (req, res) => { + newTransaction(res, req.body, BrokerRegistration); +}); + +app.post('/SensorRegistration', (req, res) => { + newTransaction(res, req.body, SensorRegistration); }); ///////////////////// @@ -310,16 +231,17 @@ app.post('/sparql', (req, res) => { const bindingsStream = await myEngine.queryBindings( req.body.query, { - log: new LoggerPretty({ level: 'trace' }), readOnly: true, - sources: [getBlockchain().store] + sources: blockchain.stores }); bindingsStream.on('data', (binding) => { - console.log(binding.toString()); - result.push(binding); + const pushing = {}; + for (const [key, value] of binding) { + pushing[key.value] = value.value; + } + result.push(pushing); }); bindingsStream.on('end', () => { - console.log('end'); res.json(JSON.stringify(result)); }); bindingsStream.on('error', (err) => { @@ -335,7 +257,7 @@ app.post('/sparql', (req, res) => { }); - ///////////////////////////////////////////////////////////Integration/////////////////////////////////////////////////////////// +/* ///////////////////////////////////////////////////////////Integration/////////////////////////////////////////////////////////// DistributedBrokers = ["mqtt.eclipse.org", "test.mosquitto.org","broker.hivemq.com"]; DistributedBrokersPorts = [1883,1883,1883]; function makeTopic(length) { @@ -367,9 +289,10 @@ app.post('/IoTdeviceIntegration-Control', (req, res) => { MassagiesRecived.push(false); data =bc.chain.map (a => a.data); MetaANDTransFound = false; - for (let j= data.length-1; j>0; j-- ){/** this for loop load - Blockchain and search for metadata and payment transaction that match - the provided MetadataID and TransactionID */ + for (let j= data.length-1; j>0; j-- ){ + //this for loop load + //Blockchain and search for metadata and payment transaction that match + //the provided MetadataID and TransactionID var metadata = data[j][1]; var transaction = data [j][0]; var pickedMetadata = lodash.find(metadata, x=> @@ -404,11 +327,10 @@ app.post('/IoTdeviceIntegration-Control', (req, res) => { - app.get ('/IoTdataObtainingAndForward', (req, res) => { +app.get ('/IoTdataObtainingAndForward', (req, res) => { console.log (`transaction of IoT Application ${i} approved`) BrokerRandomNumber = (Math.floor( - Math.random()*DistributedBrokers.length)+1)-1 /** collect - a random number to select a random broker*/ + Math.random()*DistributedBrokers.length)+1)-1 //collect a random number to select a random broker MiddlewareBroker = DistributedBrokers[BrokerRandomNumber]; MiddlewareTopic = makeTopic(5);// generate random topic MiddlewarePort = DistributedBrokersPorts[BrokerRandomNumber]; @@ -416,8 +338,7 @@ app.post('/IoTdeviceIntegration-Control', (req, res) => { configurationMessage = {"host/broker":MiddlewareBroker, "topic":MiddlewareTopic, "port":MiddlewarePort, - "duration":Duration} /** add pk of the node - connect to the IoT device and send the configuration massage*/ + "duration":Duration} //add pk of the node connect to the IoT device and send the configuration massage var IoTDeviceClient = mqtt.connect(IoTDeviceBroker); MiddlewareClients.push(mqtt.connect(`mqtt://${MiddlewareBroker}`)) var MiddlewareClient = MiddlewareClients[i] @@ -432,8 +353,7 @@ app.post('/IoTdeviceIntegration-Control', (req, res) => { IoTDeviceClient.on("message", (topic, message) => { console.log(message.toString()) IoTDeviceClient.end(true)}); - /** connect the randomly choosed mqtt middlware broker to - * listen to the transmitted massagies */ + //connect the randomly choosed mqtt middlware broker to listen to the transmitted massagies MiddlewareClient.on("connect", ack => { console.log("connected!"); console.log(MiddlewareBroker) @@ -441,21 +361,19 @@ app.post('/IoTdeviceIntegration-Control', (req, res) => { MiddlewareClient.subscribe(MiddlewareTopic, err => { console.log(err); });}); MiddlewareTracking.push({index:i, - TrackingTopic:MiddlewareTopic})/** this used to track the connection - in case there are multiple conection at the same time */ - MiddlewareClient.on("message", (topic, message) => {/** call back, - will run each time a massage recived, I did it in a way if there are - multiple connections, it will run for all the massagies, then truck the - massagies by MiddlwareTracking Array */ + TrackingTopic:MiddlewareTopic}) //this used to track the connection in case there are multiple conection at the same time + MiddlewareClient.on("message", (topic, message) => { + //call back, + //will run each time a massage recived, I did it in a way if there are + //multiple connections, it will run for all the massagies, then truck the + //massagies by MiddlwareTracking Array var MiddlewareFound = MiddlewareTracking.filter(function(item) { return item.TrackingTopic == topic;}); console.log(MiddlewareFound); console.log(message.toString()); - MiddlewareIndex = MiddlewareFound[0].index/** this is the index of - the connection or the Middleware*/ + MiddlewareIndex = MiddlewareFound[0].index// this is the index of the connection or the Middleware console.log(MiddlewareIndex) - MassageCounter[MiddlewareIndex]++;/** this used to track the number - of recived massagies of each connection */ + MassageCounter[MiddlewareIndex]++;//this used to track the number of recived massagies of each connection console.log(Date.now()-StartSending[MiddlewareIndex]) if (Date.now() - StartSending[MiddlewareIndex] >= (Durations[MiddlewareIndex]*1000) @@ -463,18 +381,17 @@ app.post('/IoTdeviceIntegration-Control', (req, res) => { console.log("sending time finished") if (MassageCounter[MiddlewareIndex] > 0.75*( Durations[MiddlewareIndex]/Intervals[MiddlewareIndex]) - ){/** which means most of massagies have been sent */ + ){// which means most of massagies have been sent console.log("massages recived") MassagiesRecived[MiddlewareIndex] = true;} - if (MassagiesRecived[MiddlewareIndex]){/** if massagies recived, - pay the 10% as service fees */ + if (MassagiesRecived[MiddlewareIndex]){// if massagies recived, pay the 10% as service fees const PaymentTransaction = wallet.createPaymentTransaction( NodeAddress,(0.1*paymentAmount[MiddlewareIndex]) , bc, tp); p2pServer.broadcastPaymentTransaction(PaymentTransaction); console.log("amount paid to the IoT device") console.log(MiddlewareIndex) MiddlewareClient = MiddlewareClients[MiddlewareIndex]; - /** disconnect the middleware mqtt broker */ + //disconnect the middleware mqtt broker MiddlewareClient.end(true)} else{// if massagies not recived, pay the IoT application back res.redirect('/IoTapplicationCompensationTransaction')}};}); @@ -686,4 +603,4 @@ i++; }); -}); +});*/ diff --git a/miner/miner.js b/miner/miner.js new file mode 100644 index 0000000..df4bd66 --- /dev/null +++ b/miner/miner.js @@ -0,0 +1,169 @@ +const Block = require('../blockchain/block'); + +const Payment = require('../blockchain/payment'); +const Integration = require('../blockchain/integration'); +const SensorRegistration = require('../blockchain/sensor-registration'); +const BrokerRegistration = require('../blockchain/broker-registration'); + +const ITERATIONS = 1; + +const STATE_RUNNING = 0; +const STATE_INTERRUPTED = 1; + +function mine(miner) { + if (miner.state !== STATE_RUNNING) { + this.startMine(); + return; + } + const timestamp = Date.now(); + const difficulty = Block.adjustDifficulty(miner.lastBlock, timestamp); + + for (let i = 0; i < ITERATIONS; ++i) { + const hash = Block.hash( + timestamp, + miner.lastBlock.hash, + miner.reward, + miner.txs.payments.mining, + miner.txs.sensorRegistrations.mining, + miner.txs.brokerRegistrations.mining, + miner.txs.integrations.mining, + miner.nonce, + difficulty); + + if (hash.substring(0, difficulty) === '0'.repeat(difficulty)) { + //success + const endTime = process.hrtime.bigint(); + console.log(`Mined a block of difficulty ${difficulty} in ${Number(endTime - miner.minedStartTime) / 1000000}ms`); + miner.onMined(new Block( + timestamp, + miner.lastBlock.hash, + hash, + miner.reward, + miner.txs.payments.mining, + miner.txs.sensorRegistrations.mining, + miner.txs.brokerRegistrations.mining, + miner.txs.integrations.mining, + miner.nonce, + difficulty)); + miner.state = STATE_INTERRUPTED; + setImmediate(() => { startMine(miner) }); + return; + } else { + //failure + miner.nonce++; + } + } + setImmediate(() => { mine(miner) }); +} + +function startMine(miner) { + //only continue if state is waiting or restarting + if (miner.state !== STATE_INTERRUPTED) { + return; + } + + miner.minedStartTime = process.hrtime.bigint(); + + //TODO make sure these transactions actually work as a collective instead of individually + miner.txs.payments.mining = [...miner.txs.payments.pool]; + miner.txs.integrations.mining = [...miner.txs.integrations.pool]; + miner.txs.sensorRegistrations.mining = [...miner.txs.sensorRegistrations.pool]; + miner.txs.brokerRegistrations.mining = [...miner.txs.brokerRegistrations.pool]; + + miner.lastBlock = miner.blockchain.chain[miner.blockchain.chain.length - 1]; + + miner.nonce = 0; + miner.state = STATE_RUNNING; + + mine(miner); +} + +function findTx(tx) { + return t => t.input === tx.input && t.counter === tx.counter; +} + +function clearFromBlock(miner, txs, blockTxs) { + for (const tx of blockTxs) { + const foundIndex = txs.pool.findIndex(findTx(tx)); + + if (foundIndex !== -1) { + txs.pool.splice(foundIndex, 1); + } + + if (txs.mining.some(findTx(tx))) { + miner.state = STATE_INTERRUPTED; + } + } +} + +class Miner { + constructor(blockchain, reward, onMined) { + this.blockchain = blockchain; + this.onMined = onMined; + this.state = STATE_INTERRUPTED; + this.lastBlock = null; + this.reward = reward; + + this.minedStartTime = null; + + this.txs = { + payments: { + pool: [], + mining: [] + }, + integrations: { + pool: [], + mining: [] + }, + sensorRegistrations: { + pool: [], + mining: [] + }, + brokerRegistrations: { + pool: [], + mining: [] + } + }; + + startMine(this); + } + + addTransaction(tx) { + const verifyRes = tx.type.verify(tx.transaction); + if (!verifyRes.result) { + console.log("Couldn't add tx to miner, tx couldn't be verified: " + verifyRes.reason); + return; + } + + let txs = null; + + switch (tx.type) { + case Payment: txs = this.txs.payments; break; + case Integration: txs = this.txs.integrations; break; + case SensorRegistration: txs = this.txs.sensorRegistrations; break; + case BrokerRegistration: txs = this.txs.brokerRegistrations; break; + default: throw new Error(`unknown tx type: ${tx.type.name()}`); + } + + const foundIndex = txs.pool.findIndex(findTx(tx.transaction)); + + if (foundIndex !== -1) { + txs.pool[foundIndex] = tx.transaction; + if (txs.mining.some(findTx(tx.transaction))) { + this.state = STATE_INTERRUPTED; + } + } else { + txs.pool.push(tx.transaction); + } + } + + onNewBlock(block) { + clearFromBlock(this, this.txs.payments, Block.getPayments(block)); + clearFromBlock(this, this.txs.integrations, Block.getIntegrations(block)); + clearFromBlock(this, this.txs.sensorRegistrations, Block.getSensorRegistrations(block)); + clearFromBlock(this, this.txs.brokerRegistrations, Block.getBrokerRegistrations(block)); + } +} + +module.exports = Miner; + diff --git a/p2p-server.js b/p2p-server.js new file mode 100644 index 0000000..5591f81 --- /dev/null +++ b/p2p-server.js @@ -0,0 +1,164 @@ +const Websocket = require('ws'); + +function messageHandler(p2pServer, socket) { + socket.on('message', (data) => { + p2pServer.onData(data); + }); +} + +function onConnection(p2pServer, socket) { + p2pServer.sockets.push(socket); + console.log(`${p2pServer.name} had a socket connect`); + + p2pServer.onConnect(socket); + + messageHandler(p2pServer, socket); +} + +/* DEAD CODE STORAGE + * + * + + this.miner = new Miner(this.blockchain, rewardPublicKey, this); + * + * socket.on('message', message => { + const data = JSON.parse(message); + switch (data.type) { + case MESSAGE_TYPES.chain: + newChain(p2pServer, data.data); + break; + case MESSAGE_TYPES.payment: + this.newTransaction(new Transaction(data.transaction, Payment), false); + break; + case MESSAGE_TYPES.integration: + this.newTransaction(new Transaction(data.transaction, Integration), false); + break; + case MESSAGE_TYPES.sensorRegistration: + this.newTransaction(new Transaction(data.transaction, SensorRegistration), false); + break; + case MESSAGE_TYPES.brokerRegistration: + this.newTransaction(new Transaction(data.transaction, BrokerRegistration), false); + break; + default: + console.log(`Unknown type '${data.type}' recved from socket ${socket}`); + break; + } + }); + * + * const MESSAGE_TYPES = { + chain: 'CHAIN', + payment: 'PAYMENT', + integration: 'INTEGRATION', + sensorRegistration: 'SENSORREGISTRATION', + brokerRegistration: 'BROKERREGISTRATION' +}; + +console.error(`Couldn't persist chain, aborting: ${err}`); + process.exit(-1); + +function newChain(p2pServer, chain, persist) { + const replaceResult = p2pServer.blockchain.replaceChain(chain); + if (!replaceResult.result) { + //failed to replace + return; + } + + for (let i = 0; i < replaceResult.chainDifference; i++) { + p2pServer.miner.onNewBlock(p2pServer.blockchain.chain[i]); + } + + p2pServer.miner.interrupt(); + + if (typeof persist === "undefined" || persist) { + persistChain(p2pServer, p2pServer.blockchain); + } +} + +function syncChains(p2pServer) { + const serializedBlockchain = p2pServer.blockchain.serialize(); + + for (const socket of p2pServer.sockets) { + send(socket, serializedBlockchain, MESSAGE_TYPES.chain); + } +} + +function broadcastTransaction(p2pServer, tx) { + let type = null; + + switch (tx.type) { + case Payment: type = MESSAGE_TYPES.payment; break; + case Integration: type = MESSAGE_TYPES.integration; break; + case BrokerRegistration: type = MESSAGE_TYPES.brokerRegistration; break; + case SensorRegistration: type = MESSAGE_TYPES.sensorRegistration; break; + default: throw Error("Unknown tx type"); + } + + for (const socket of p2pServer.sockets) { + send(socket, tx.transaction, type); + } +} + + newTransaction(transaction, broadcast) { + if (!transaction.verify(transaction.transaction)) { + console.log("Couldn't add transaction to p2pServer, couldn't verify"); + return; + } + + this.miner.addTransaction(transaction); + + if (broadcast === undefined || broadcast) { + broadcastTransaction(this, transaction); + } + } + + blockMined(block) { + if (!this.blockchain.addBlock(block)) { + //invalid block, return + return; + } + this.miner.onNewBlock(block); + persistChain(this, this.blockchain); + syncChains(this); + } + + * + */ + +class P2pServer { + constructor(name) { + this.name = name; + this.sockets = []; + } + + start(port, peers, onConnect, onData) { + this.port = port; + this.onConnect = onConnect; + this.onData = onData; + this.server = new Websocket.Server({ port: port }); + this.server.on('connection', socket => onConnection(this, socket)); + + for (const peer of peers) { + this.connect(peer); + } + + console.log(`Listening for peer-to-peer connections on: ${port}`); + } + + connect(to) { + const socket = new Websocket(to); + + socket.on('open', () => onConnection(this, socket)); + } + + broadcast(data) { + for (const socket of this.sockets) { + P2pServer.send(socket, data); + } + } + + static send(socket, data) { + socket.send(data); + } +} + +module.exports = P2pServer; \ No newline at end of file diff --git a/package.json b/package.json index 795e1f5..af58ee3 100644 --- a/package.json +++ b/package.json @@ -2,12 +2,12 @@ "name": "senshamartproject", "version": "1.0.0", "description": "A novel Marketplace for sharing sensors in decentralized environment", - "main": "app/index.js", + "main": "miner/miner-app.js", "scripts": { "test": "jest --watchAll", "dev-test": "nodemon dev-test", - "start": "node ./app", - "dev": "nodemon ./app" + "start": "node ./miner/miner-app", + "dev": "nodemon ./miner/miner-app" }, "jest": { "testEnvironment": "node" @@ -21,7 +21,7 @@ "nodemon": "^2.0.20" }, "dependencies": { - "@comunica/query-sparql": "^2.5.2", + "@comunica/query-sparql-rdfjs": "^2.5.2", "aedes": "^0.42.5", "body-parser": "^1.20.1", "crypto-js": "^4.1.1", diff --git a/sensor/sensor-app.js b/sensor/sensor-app.js new file mode 100644 index 0000000..9d3e802 --- /dev/null +++ b/sensor/sensor-app.js @@ -0,0 +1,54 @@ +//SENSOR +const express = require('express'); +const bodyParser = require('body-parser'); + +const Config = require('../config'); +const ChainUtil = require('../chain-util'); +const Sensor = require('./sensor'); + +const { + DEFAULT_PORT_BROKER_SENSOR_HANDSHAKE, + DEFAULT_PORT_SENSOR_API, +} = require('../constants'); + +'use strict'; + +const CONFIGS_STORAGE_LOCATION = "./settings.json"; + +const config = new Config(CONFIGS_STORAGE_LOCATION); + +const keyPair = config.get({ + key: "sensor-keypair", + default: ChainUtil.genKeyPair(), + transform: ChainUtil.deserializeKeyPair +}); +const apiPort = config.get({ + key: "sensor-api-port", + default: DEFAULT_PORT_SENSOR_API +}); +const sensorId = config.get({ + key: "sensor-id", + default: "Test sensor" +}); +const brokerLocation = config.get({ + key: "sensor-broker-location", + default: "ws://127.0.0.1:" + DEFAULT_PORT_BROKER_SENSOR_HANDSHAKE +}); +const brokerPublicKey = config.get({ + key: "sensor-broker-publickey", + default: null +}); + +const sensor = new Sensor(keyPair, sensorId, brokerLocation, brokerPublicKey); + +const app = express(); +app.use(bodyParser.json()); + + +app.listen(apiPort, () => console.log(`Listening on port ${apiPort}`)); + +app.post('/send', (req, res) => { + console.log(`sending: ${JSON.stringify(req.body)}`); + sensor.send(JSON.stringify(req.body)); + res.json("sent"); +}); \ No newline at end of file diff --git a/sensor/sensor.js b/sensor/sensor.js new file mode 100644 index 0000000..a64074c --- /dev/null +++ b/sensor/sensor.js @@ -0,0 +1,108 @@ +const Websocket = require('ws'); + +const ChainUtil = require('../chain-util'); +const crypto = require('crypto'); + +const STATE_SERVER_HELLOING = 0; +const STATE_SERVER_FINNING = 1; +const STATE_OPERATIONAL = 2; + +function onServerHelloing(sensor, data) { + const serverNonce = data.toString(); + + if (typeof serverNonce !== 'string') { + console.log("Bad server hello"); + sensor.close(); + return; + } + sensor.serverNonce = serverNonce; + + crypto.randomBytes(2048, (err, buf) => { + if (err) { + console.log(`Couldn't generate client nonce: ${err}`); + sensor.close(); + return; + } + + sensor.clientNonce = buf.toString('hex'); + + sensor.socket.send(JSON.stringify({ + owner: sensor.keyPair.getPublic().encode('hex'), + sensor: sensor.sensorId, + signature: sensor.keyPair.sign(sensor.serverNonce + sensor.clientNonce), + clientNonce: sensor.clientNonce + })); + sensor.state = STATE_SERVER_FINNING; + }); +} + +function onServerFinning(sensor, data) { + const signature = JSON.parse(data); + if (typeof signature !== 'object') { + console.log("Bad server fin"); + sensor.close(); + return; + } + + if (sensor.brokerPublicKey !== null) { + const verifyRes = ChainUtil.verifySignature(sensor.brokerPublicKey, data, sensor.clientNonce + sensor.serverNonce); + if (!verifyRes.result) { + console.log("Bad server fin singature: " + verifyRes.reason); + sensor.close(); + return; + } + console.log("Broker authed, operational"); + } else { + console.log("No broker public key stored, blindly trusting the broker"); + } + + sensor.state = STATE_OPERATIONAL; + for (const msg of sensor.queue) { + sensor.send(msg); + } +} + +function onOperational(_, _) { +} + +function onSocketMessage(sensor, data) { + switch (sensor.state) { + case STATE_SERVER_HELLOING: onServerHelloing(sensor, data); break; + case STATE_SERVER_FINNING: onServerFinning(sensor, data); break; + case STATE_OPERATIONAL: onOperational(sensor, data); break; + default: throw Error("Invalid internal state"); + } +} + +function onConnection(sensor) { + sensor.socket.on('message', (data) => { + onSocketMessage(sensor, data); + }); +} + +class Sensor { + constructor(keyPair, sensorId, brokerLocation, brokerPublicKey) { + this.keyPair = keyPair; + this.sensorId = sensorId; + this.brokerPublicKey = brokerPublicKey; + this.state = STATE_SERVER_HELLOING; + this.queue = []; + + this.socket = new Websocket(brokerLocation); + this.socket.on('open', (_) => onConnection(this)); + } + + send(data) { + if (this.state != STATE_OPERATIONAL) { + this.queue.push(data); + } else { + this.socket.send(data); + } + } + + close() { + this.socket.close(); + } +} + +module.exports = Sensor; \ No newline at end of file diff --git a/settings.json b/settings.json index 75b14f1..dbbe694 100644 --- a/settings.json +++ b/settings.json @@ -1,3 +1,6 @@ { - "wallet-private-key": "557360924eae1b0a7ff4727c4226300788b2e98e50b91195f1048e671d4d5d6e" + "miner-public-key": "041171cd24b7ddb430cb0764c117ab26170af80a664210edd4b901d17a44857d68dfd92552487480efa9d64a3e57dd9853404044b593c8cb593def5234e3954c60", + "wallet-keypair": "edfb811d963164da2c37de34ae6c30fd36b87551ffb8c1dba98dd63d1926a90a", + "broker-keypair": "edfb811d963164da2c37de34ae6c30fd36b87551ffb8c1dba98dd63d1926a90a", + "broker-name": "broker1" } \ No newline at end of file diff --git a/wallet/index.js b/wallet/index.js deleted file mode 100644 index e04f428..0000000 --- a/wallet/index.js +++ /dev/null @@ -1,92 +0,0 @@ -const Transaction = require('./transaction'); -const { INITIAL_BALANCE } = require('../config'); -const Metadata = require('./metadata'); -const ChainUtil = require('../chain-util'); - -class Wallet { - constructor(keyPair) { - this.keyPair = keyPair; - this.publicKey = this.keyPair.getPublic().encode('hex'); - this.counter = 0; - } - - toString() { - return `Wallet - - publicKey: ${this.publicKey.toString()} - balance : ${this.balance}` - } - - sign(dataHash) { - return this.keyPair.sign(dataHash); - } - - createTransaction(recipient, amount, blockchain) { - const balance = blockchain.getBalanceCopy(this.publicKey); - - if (balance.counter > this.counter) { - this.counter = balance.counter; - } - - if (amount > balance.balance) { - console.log(`Amount: ${amount} exceceds current balance: ${balance.balance}`); - return null; - } - - const counterToUse = this.counter + 1; - this.counter++; - - const newTransaction = new Transaction(this.publicKey, counterToUse, [Transaction.createOutput(recipient, amount)]); - newTransaction.addSignature(this.sign(Transaction.hashToSign(newTransaction))); - return newTransaction; - } - - createMetadata(SSNmetadata) { - return Metadata.newMetadata(this, SSNmetadata); - } - - //calculateBalance(blockchain) { - // let balance = this.balance; - // let transactions = []; - // blockchain.chain.forEach(block => block.data.forEach(transaction => { - // transactions.push(transaction); - // })); - // console.log("transactions of balance") - // console.log(transactions); - // const PaymentTransactions = transactions[0]; - // console.log("Payment transactions ") - // console.log(PaymentTransactions); - // const walletInputTs = PaymentTransactions.filter(transaction => transaction.input.address === this.publicKey); - - // let startTime = 0; - - // if (walletInputTs.length > 0) { - // const recentInputT = walletInputTs.reduce( - // (prev, current) => prev.input.timestamp > current.input.timestamp ? prev : current - // ); - - // balance = recentInputT.outputs.find(output => output.address === this.publicKey).amount; - // startTime = recentInputT.input.timestamp; - // } - - // PaymentTransactions.forEach(transaction => { - // if (transaction.input.timestamp > startTime) { - // transaction.outputs.find(output => { - // if (output.address === this.publicKey) { - // balance += output.amount; - // } - // }); - // } - // }); - - // return balance; - //} - - //static blockchainWallet() { - // const blockchainWallet = new this(ChainUtil.genKeyPair()); - // blockchainWallet.address = 'blockchain-wallet'; - // return blockchainWallet; - //} -} - -module.exports = Wallet; - diff --git a/wallet/metadata.js b/wallet/metadata.js deleted file mode 100644 index 8f7b317..0000000 --- a/wallet/metadata.js +++ /dev/null @@ -1,86 +0,0 @@ -const ChainUtil = require('../chain-util'); - -class Metadata { - constructor() { - this.id = null; - this.Signiture = null; - // this.Name = null; - // this.Geo = null; - - // this.GeospatialLocation = []; - // this.Owenership = null; - // this.Cost = null; - // this.Identifications = null; - // this.Integration = null; - - // this.IP_URL = null; - // this.Topic_Token = null; - // this.Permission = null; - // this.RequestDetail = null; - // this.OrgOwner = null; - // this.DepOwner = null; - // this.PrsnOwner = null; - // this.MetaHash = null; - // this.PaymentPerKbyte = null; - // this.PaymentPerMinute = null; - // this.Protocol = null; - // this.MessageAttributes= {}; - // this.Interval = null; - // this.FurtherDetails = null; - this.SSNmetadata = null; - } - - static MetadataOfIoTDevice(senderWallet, SSNmetadata) { - const metadata = new this(); - metadata.id = ChainUtil.id(); - // metadata.Name = Name; - // metadata.Geo = Geo; - // metadata.IP_URL = IP_URL; - // metadata.Topic_Token = Topic_Token; - // metadata.Permission = Permission; - // metadata.RequestDetail = RequestDetail - // metadata.OrgOwner = OrgOwner; - // metadata.DepOwner = DepOwner; - // metadata.PrsnOwner = PrsnOwner; - // metadata.PaymentPerKbyte = PaymentPerKbyte ; - // metadata.PaymentPerMinute = PaymentPerMinute; - // metadata.Protocol = Protocol; - // metadata.MessageAttributes = MessageAttributes; - - - // metadata.MessageAttributes['DeviceID'] = metadata.id; - // metadata.MessageAttributes['DeviceName'] = Name; - // metadata.MessageAttributes['Sensors'] =[{"SensorName":"","Value":"" , "Unit":""}]; - // metadata.MessageAttributes['TimeStamp'] = ""; - - - // metadata.Interval = Intrval; - // metadata.FurtherDetails = FurtherDetails; - metadata.SSNmetadata = SSNmetadata; - metadata.MetaHash = ChainUtil.hash(SSNmetadata); - Metadata.signMetadata(metadata, senderWallet); - return metadata; - } - - static newMetadata(senderWallet,SSNmetadata){ - return Metadata.MetadataOfIoTDevice(senderWallet, SSNmetadata); - } - - static signMetadata (metadata, senderWallet) { - metadata.Signiture = { - timestamp: Date.now(), - address: senderWallet.publicKey, - signature: senderWallet.sign(ChainUtil.hash(metadata.SSNmetadata)) - } - } - - static verifyMetadata(metadata) { - return ChainUtil.verifySignature( - metadata.Signiture.address, - metadata.Signiture.signature, - ChainUtil.hash(metadata.SSNmetadata) - ); - } -} - -module.exports = Metadata; \ No newline at end of file diff --git a/wallet/transaction-pool.js b/wallet/transaction-pool.js deleted file mode 100644 index ace972f..0000000 --- a/wallet/transaction-pool.js +++ /dev/null @@ -1,95 +0,0 @@ -const Transaction = require('../wallet/transaction'); -const Metadata = require('../wallet/metadata'); -const Block = require('../blockchain/block'); - -const Return = { - add: 1, - update: 2, - error: 3 -}; - -class TransactionPool { - constructor() { - this.transactions = []; - this.metadatas = []; - } - - //returns true on update, false on add - updateOrAddTransaction(transaction) { - if (!Transaction.verify(transaction)) { - console.log("Couldn't update or add transaction, transaction couldn't be verified"); - return Return.error; - } - const foundIndex = this.transactions.findIndex(t => t.input === transaction.input && t.counter === transaction.counter); - - if (foundIndex !== -1) { - this.transactions[foundIndex] = transaction; - return Return.update; - } else { - this.transactions.push(transaction); - return Return.add; - } - } - - updateOrAddMetadata(metadata) { - if (!Metadata.verifyMetadata(metadata)) { - console.log("Couldn't update metdata, metadata couldn't be verified"); - return Return.error; - } - - const foundIndex = this.metadatas.findIndex(t => t.id === metadata.id); - - if (foundIndex !== -1) { - this.metadatas[foundIndex] = metadata; - return Return.update; - } else { - this.metadatas.push(metadata); - return Return.add; - } - } - - existingTransaction(address) { - return this.transactions.find(t => t.input.address === address); - } - - existingMetadata(address) { - return this.metadatas.find(t => t.Signiture.address === address); - } - - //we could check for possible double spends here - validTransactionsCopy() { - return [...this.transactions]; - } - - validMetadatasCopy(){ - return [...this.metadatas]; - } - - clearFromBlock(block) { - const blockTransactions = Block.getTransactions(block); - const blockMetadatas = Block.getMetadatas(block); - - for (const transaction of blockTransactions) { - const foundTransaction = this.transactions.findIndex(t => t.id === transaction.id); - - if (foundTransaction !== -1) { - this.transactions.splice(foundTransaction, 1); - } - } - for (const metadata of blockMetadatas) { - const foundMetadata = this.metadatas.findIndex(m => m.id === metadata.id); - - if (foundMetadata !== -1) { - this.metadatas.splice(foundMetadata, 1); - } - } - } - - clearAll() { - this.transactions = []; - this.metadatas = []; - } -} - -module.exports = TransactionPool; -module.exports.Return = Return; \ No newline at end of file diff --git a/wallet/transaction-pool.test.js b/wallet/transaction-pool.test.js deleted file mode 100644 index 0907861..0000000 --- a/wallet/transaction-pool.test.js +++ /dev/null @@ -1,86 +0,0 @@ -const TransactionPool = require('./transaction-pool'); -const Transaction = require('./transaction'); -const Metadata = require('./metadata') -const Wallet = require('./index'); -const Blockchain = require('../blockchain'); - -describe('TransactionPool', () => { - let tp, wallet, transaction, metadata, bc; - - beforeEach(() => { - tp = new TransactionPool(); - wallet = new Wallet(); - wallet2 =new Wallet(); - bc = new Blockchain(); - transaction = wallet.createTransaction('r4nd-4dr355', 30, bc, tp); - // senderWallet = 'address'; - // Name = 'IoT_Lab_Temp_Sensor' - // Geo = [1.045,0.0135] - // IP_URL = 'www.IoT-locationbar.com/sensors/temp' - // Topic_Token = 'ACCESS_TOKEN' - // Permission = 'Public' - // RequestDetail = 'Null' - // OrgOwner = 'Swinburne_University' - // DepOwner = 'Computer_Science' - // PrsnOwner = 'Anas_Dawod' - // PaymentPerKbyte = 10 - // PaymentPerMinute = 5 - // Protocol = 'MQTT' - // MessageAttributes = 'null' - // Interval = 10 - // FurtherDetails = 'null' - // SSNmetadata = 'null' - - metadata = wallet.createMetadata('IoT_Lab_Temp_Sensor',[1.045,0.0135],"www.IoT-locationbar.com/sensors/temp" ,'ACCESS_TOKEN' , 'Public', - 'Null', 'Swinburne_University', 'Computer_Science','Anas_Dawod', 10, - 5, 'MQTT', 'null', 10, - 'FurtherDetails', 'SSNmetadata',tp); - }); - - it('adds a transaction to the pool', () => { - expect(tp.transactions.find(t => t.id === transaction.id)).toEqual(transaction); - }); - it('adds a metadata to the pool', () => { - expect(tp.metadataS.find(t => t.id === metadata.id)).toEqual(metadata); - }); - - it('updates a transaction in the pool', () => { - const oldTransaction = JSON.stringify(transaction); - const newTransaction = transaction.update(wallet, 'foo-4ddr355', 40); - tp.updateOrAddTransaction(newTransaction); - - expect(JSON.stringify(tp.transactions.find(t => t.id === newTransaction.id))) - .not.toEqual(oldTransaction); - }); - - it('clears transactions and metadata', () => { - tp.clear(); - expect(tp.transactions).toEqual([]); - expect(tp.metadataS).toEqual([]); - }); - - describe('mixing valid and corrupt transactions', () => { - let validTransactions; - - beforeEach(() => { - validTransactions = [...tp.transactions]; - for (let i=0; i<6; i++) { - wallet = new Wallet(); - transaction = wallet.createTransaction('r4nd-4dr355', 30, bc, tp); - if (i%2==0) { - transaction.input.amount = 99999; - } else { - validTransactions.push(transaction); - } - } - }); - - it('shows a difference between valid and corrupt transactions', () => { - expect(JSON.stringify(tp.transactions)).not.toEqual(JSON.stringify(validTransactions)); - }); - - it('grabs valid transactions', () => { - expect(tp.validTransactions()).toEqual(validTransactions); - }); - }); -}); \ No newline at end of file diff --git a/wallet/transaction.js b/wallet/transaction.js deleted file mode 100644 index 97fd95c..0000000 --- a/wallet/transaction.js +++ /dev/null @@ -1,80 +0,0 @@ -const ChainUtil = require('../chain-util'); -const { MINING_REWARD } = require('../config'); - -class Transaction { - constructor(senderPublicKey, counter, outputs) { - this.input = senderPublicKey; - this.signature = null; - this.counter = counter; - this.outputs = outputs; - } - - addSignature(signature) { - if (!ChainUtil.verifySignature( - this.input, - signature, - Transaction.hashToSign(this))) { - console.log("Tried to add an invalid signature to a transaction"); - throw new Error("Tried to add an invalid signature to a transaction"); - } - this.signature = signature; - } - - static hashToSign(transaction) { - return ChainUtil.hash({ - counter: transaction.counter, - outputs: transaction.outputs - }); - } - - static createOutput(recipient, amount) { - return { - publicKey: recipient, - amount: amount - }; - } - - //update(senderWallet, recipients) { - // const senderOutput = this.outputs.find(output => output.address === senderWallet.publicKey); - - // if (amount > senderOutput.amount) { - // console.log(`Amount: ${amount} exceeds balance.`); - // return; - // } - - // senderOutput.amount = senderOutput.amount - amount; - // this.outputs.push({ amount, address: recipient }); - // Transaction.signTransaction(this, senderWallet); - - // return this; - //} - //static signTransaction(transaction, senderWallet) { - // transaction.input = { - // timestamp: Date.now(), - // address: senderWallet.publicKey, - // signature: senderWallet.sign(ChainUtil.hash(transaction.outputs)) - // } - //} - - static verify(transaction) { - if (transaction.outputs.length === 0) { - return false; - } - for (const output of transaction.outputs) { - if (!output.hasOwnProperty('amount')) { - return false; - } - if (!output.hasOwnProperty('publicKey')) { - return false; - } - } - - return ChainUtil.verifySignature( - transaction.input, - transaction.signature, - Transaction.hashToSign(transaction) - ); - } -} - -module.exports = Transaction; \ No newline at end of file diff --git a/wallet/wallet-app.js b/wallet/wallet-app.js new file mode 100644 index 0000000..c7f52bc --- /dev/null +++ b/wallet/wallet-app.js @@ -0,0 +1,287 @@ +//WALLET +const express = require('express'); +const bodyParser = require('body-parser'); +const P2pServer = require('../p2p-server'); + +const N3 = require('n3'); + +const Wallet = require('./wallet'); +const Config = require('../config'); +const ChainUtil = require('../chain-util'); + +const QueryEngine = require('@comunica/query-sparql-rdfjs').QueryEngine; +const Blockchain = require('../blockchain/blockchain'); + +const { + DEFAULT_PORT_WALLET_API, + DEFAULT_PORT_WALLET_CHAIN, + DEFAULT_PORT_MINER_CHAIN +} = require('../constants'); + +'use strict'; + +const CONFIGS_STORAGE_LOCATION = "./settings.json"; + +const config = new Config(CONFIGS_STORAGE_LOCATION); + +const wallet = new Wallet(config.get({ + key: "wallet-keypair", + default: ChainUtil.genKeyPair(), + transform: ChainUtil.deserializeKeyPair +})); +const apiPort = config.get({ + key: "wallet-api-port", + default: DEFAULT_PORT_WALLET_API +}); +const blockchainLocation = config.get({ + key: "wallet-blockchain-location", + default: "./wallet_blockchain.json" +}); +const chainServerPort = config.get({ + key: "wallet-chain-server-port", + default: DEFAULT_PORT_WALLET_CHAIN +}); +const chainServerPeers = config.get({ + key: "wallet-chain-server-peers", + default: ["ws://127.0.0.1:" + DEFAULT_PORT_MINER_CHAIN] +}); + +const blockchain = Blockchain.loadFromDisk(blockchainLocation); + +function onChainServerRecv(data) { + const replaceResult = blockchain.replaceChain(Blockchain.deserialize(data)); + if (!replaceResult.result) { + console.log(`Failed to replace chain: ${replaceResult.reason}`); + //failed to replace + return; + } + + blockchain.saveToDisk(blockchainLocation); +} + +const chainServer = new P2pServer("Chain-server"); + +chainServer.start(chainServerPort, chainServerPeers, (_) => { }, onChainServerRecv); +const app = express(); +app.use(bodyParser.json()); + +app.listen(apiPort, () => console.log(`Listening on port ${apiPort}`)); + +app.get('/ChainServer/sockets', (req, res) => { + res.json(chainServer.sockets); +}); +app.post('/ChainServer/connect', (req, res) => { + chainServer.connect(req.body.url); + res.json("Connecting"); +}); + +app.get('/public-key', (req, res) => { + res.json(wallet.publicKey); +}); + +app.get('/key-pair', (req, res) => { + res.json(ChainUtil.serializeKeyPair(wallet.keyPair)); +}); + +app.get('/MyBalance', (req, res) => { + res.json(blockchain.getBalanceCopy(wallet.publicKey)); +}); +app.get('/Balance', (req, res) => { + const balance = blockchain.getBalanceCopy(req.body.publicKey); + res.json(balance); +}); +app.get('/Balances', (req, res) => { + const balances = blockchain.balances; + res.json(balances); +}); + +app.post('/Payment', (req, res) => { + res.json(wallet.createPayment( + req.body.rewardAmount, + req.body.outputs, + blockchain)); +}); + +app.post('/Integration', (req, res) => { + res.json(wallet.createIntegration( + req.body.rewardAmount, + req.body.outputs, + blockchain)); +}); + +function extToRdf(triples, sensorId, parentString, obj) { + for (const key in obj) { + const value = obj[key]; + + const type = typeof value; + + switch (typeof value) { + case "string": + triples.push({ + s: sensorId, + p: parentString + key, + o: value + }); + break; + case "object": + extToRdf(triples, sensorId, parentString + key + '/', value); + break; + default: + console.log("Unsupported value type: " + type); + break; + } + } +} + +const brokerRegistrationValidators = { + ssnMetadata: ChainUtil.validateIsString, + rewardAmount: ChainUtil.createValidateIsIntegerWithMin(0), + extMetadata: ChainUtil.validateIsObject +}; + +app.post('/BrokerRegistration', (req, res) => { + const validateRes = ChainUtil.validateObject(req.body, brokerRegistrationValidators); + + if (!validateRes.result) { + res.json(validateRes.reason); + return; + } + + const brokers = []; + const triples = []; + + const parser = new N3.Parser(); + parser.parse( + req.body.ssnMetadata, + (error, quad, prefixes) => { + if (error) { + res.json(error); + return; + } + if (quad) { + triples.push({ + s: quad.subject.id, + p: quad.predicate.id, + o: quad.object.id + }); + + if (quad.predicate.id === "http://www.w3.org/1999/02/22-rdf-syntax-ns#type" + && quad.object.id === "SSM/Broker") { + brokers.push(quad.subject.id); + } + return; + } + //quad is null, we come here, and we are finished parsing + if (brokers.length === 0) { + res.json("Couldn't find a defined broker"); + return; + } else if (brokers.length > 1) { + res.json("Found multiple defined brokers"); + return; + } + + extToRdf(triples, brokers[0], "", req.body.extMetadata); + + try { + res.json(wallet.createBrokerRegistration( + triples, + req.body.rewardAmount, + blockchain)); + } catch (err) { + console.log(err); + res.json(err.message); + } + }); +}); + +const sensorRegistrationValidators = { + ssnMetadata: ChainUtil.validateIsString, + rewardAmount: ChainUtil.createValidateIsIntegerWithMin(0), + extMetadata: ChainUtil.validateIsObject +}; + +app.post('/SensorRegistration', (req, res) => { + const validateRes = ChainUtil.validateObject(req.body, sensorRegistrationValidators); + + if (!validateRes.result) { + res.json(validateRes.reason); + return; + } + + const sensors = []; + const triples = []; + + const parser = new N3.Parser(); + parser.parse( + req.body.ssnMetadata, + (error, quad, prefixes) => { + if (error) { + res.json(error); + return; + } + if (quad) { + triples.push({ + s: quad.subject.id, + p: quad.predicate.id, + o: quad.object.id + }); + + if (quad.predicate.id === "http://www.w3.org/1999/02/22-rdf-syntax-ns#type" + && quad.object.id === "http://www.w3.org/ns/sosa/Sensor") { + sensors.push(quad.subject.id); + } + return; + } + //quad is null, we come here, and we are finished parsing + if (sensors.length === 0) { + res.json("Couldn't find a defined sensor"); + return; + } else if (sensors.length > 1) { + res.json("Found multiple defined sensors"); + return; + } + + extToRdf(triples, sensors[0], "", req.body.extMetadata); + + try { + res.json(wallet.createSensorRegistration( + triples, + req.body.rewardAmount, + blockchain)); + } catch (err) { + console.log(err); + res.json(err.message); + } + }); +}); + +const myEngine = new QueryEngine(); + +app.post('/sparql', (req, res) => { + const start = async function () { + try { + let result = []; + const bindingsStream = await myEngine.queryBindings( + req.body.query, + { + readOnly: true, + sources: blockchain.stores + }); + bindingsStream.on('data', (binding) => { + result.push(binding); + }); + bindingsStream.on('end', () => { + res.json(JSON.stringify(result)); + }); + bindingsStream.on('error', (err) => { + console.error(err); + }); + } catch (err) { + console.error(err); + res.json("Error occured while querying"); + } + }; + + start() + +}); \ No newline at end of file diff --git a/wallet/wallet.js b/wallet/wallet.js new file mode 100644 index 0000000..1f26bbf --- /dev/null +++ b/wallet/wallet.js @@ -0,0 +1,133 @@ +const Payment = require('../blockchain/payment'); +const Integration = require('../blockchain/integration'); +const SensorRegistration = require('../blockchain/sensor-registration'); +const BrokerRegistration = require('../blockchain/broker-registration'); +const Transaction = require('../blockchain/transaction'); + +//TODO: keep track of issued transactions, so we don't accidently try and double spend +class Wallet { + constructor(keyPair) { + this.keyPair = keyPair; + this.publicKey = this.keyPair.getPublic().encode('hex'); + this.counter = 0; + } + + sign(dataHash) { + return this.keyPair.sign(dataHash); + } + + //TODO: API for multiple outputs + //returns Transaction + createPayment(rewardAmount, outputs, blockchain) { + const balance = blockchain.getBalanceCopy(this.publicKey); + + if (balance.counter > this.counter) { + this.counter = balance.counter; + } + + let totalAmount = 0; + for (const output of outputs) { + totalAmount += output.amount; + } + + if (totalAmount + rewardAmount > balance.balance) { + console.log(`Total amount: ${totalAmount} + reward amount: ${rewardAmount} exceeds current balance: ${balance.balance}`); + return null; + } + + const counterToUse = this.counter + 1; + this.counter++; + + return new Payment(this.keyPair, counterToUse, outputs, rewardAmount); + } + + createPaymentAsTransaction(rewardAmount, outputs, blockchain) { + return new Transaction( + this.createPayment(rewardAmount, outputs, blockchain), + Payment); + } + + //TODO: API for multiple sensors + //returns Transaction + createIntegration(rewardAmount, outputs, blockchain) { + const balance = blockchain.getBalanceCopy(this.publicKey); + + if (balance.counter > this.counter) { + this.counter = balance.counter; + } + + let totalAmount = 0; + for (const output of outputs) { + totalAmount += output.amount; + } + + if (totalAmount + rewardAmount > balance.balance) { + console.log(`Total amount: ${totalAmount} + reward amount: ${rewardAmount} exceeds current known balance: ${balance.balance}`); + return null; + } + + const counterToUse = this.counter + 1; + this.counter++; + + return new Integration(this.keyPair, counterToUse, outputs, rewardAmount); + } + + createIntegrationAsTransaction(rewardAmount, outputs, blockchain) { + return new Transaction( + this.createIntegration(rewardAmount, outputs, blockchain), + Integration); + } + + //returns Transaction + createBrokerRegistration(metadata, rewardAmount, blockchain) { + const balance = blockchain.getBalanceCopy(this.publicKey); + + if (balance.counter > this.counter) { + this.counter = balance.counter; + } + + if (rewardAmount > balance.balance) { + console.log(`Reward amount: ${rewardAmount} exceeds current balance: ${balance.balance}`); + return null; + } + + const counterToUse = this.counter + 1; + this.counter++; + + return new BrokerRegistration(this.keyPair, counterToUse, metadata, rewardAmount) + } + + createBrokerRegistrationAsTransaction(metadata, rewardAmount, blockchain) { + return new Transaction( + this.createBrokerRegistration(metadata, rewardAmount, blockchain), + BrokerRegistration); + } + + //return Transaction + createSensorRegistration(metadata, rewardAmount, blockchain) { + const balance = blockchain.getBalanceCopy(this.publicKey); + + if (balance.counter > this.counter) { + this.counter = balance.counter; + } + + if (rewardAmount > balance.balance) { + console.log(`Reward amount: ${rewardAmount} exceeds current balance: ${balance.balance}`); + return null; + } + + const counterToUse = this.counter + 1; + this.counter++; + + return new SensorRegistration(this.keyPair, counterToUse, metadata, rewardAmount); + } + + createSensorRegistrationAsTransaction(metadata, rewardAmount, blockchain) { + return new Transaction( + this.createSensorRegistration(metadata, rewardAmount, blockchain), + SensorRegistration); + } +} + +module.exports = Wallet; + diff --git a/wallet/index.test.js b/wallet/wallet.test.js similarity index 95% rename from wallet/index.test.js rename to wallet/wallet.test.js index 2e9f100..536639e 100644 --- a/wallet/index.test.js +++ b/wallet/wallet.test.js @@ -1,14 +1,13 @@ const Wallet = require('./index'); -const TransactionPool = require('./transaction-pool'); const Blockchain = require('../blockchain'); +const ChainUtil = require('../chain-util'); const { INITIAL_BALANCE } = require('../config'); describe('Wallet', () => { - let wallet, tp, bc; + let wallet, bc; beforeEach(() => { - wallet = new Wallet(); - tp = new TransactionPool(); + wallet = new Wallet(ChainUtil.genKeyPair()); bc = new Blockchain(); }); @@ -28,6 +27,7 @@ describe('Wallet', () => { tp.updateOrAddTransaction(transaction); }); + //? it('doubles the `sendAmount` subtracted from the wallet balance', () => { expect(transaction.outputs.find(output => output.address === wallet.publicKey).amount) .toEqual(wallet.balance - sendAmount * 2);