diff --git a/app/index.js b/app/index.js index 3ddde96..d8cefab 100644 --- a/app/index.js +++ b/app/index.js @@ -27,16 +27,17 @@ * to monitor the node * */ + +const LoggerPretty = require("@comunica/logger-pretty").LoggerPretty; + const express = require('express'); const bodyParser = require('body-parser'); -const Blockchain = require('../blockchain'); 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 N3 = require('n3'); const jsonld = require('jsonld'); var mqtt = require('mqtt'); var aedes = require('aedes')(); /* aedes is a stream-based MQTT broker */ @@ -48,17 +49,46 @@ const multer = require('multer');/* Multer is a node.js middleware for '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"; + +var settings = {}; + +//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 app = express(); -const bc = new Blockchain(); -//currently gen a new keypair per run, we probably want to load this from something else in the future -const wallet = new Wallet(ChainUtil.genKeyPair()); -const tp = new TransactionPool(); -const p2pServer = new P2pServer(bc, tp, wallet, './persist_block_chain.json'); -const parser = new N3.Parser(); //({format: 'application/n-quads'}); +//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 @@ -73,7 +103,7 @@ const storage = multer.diskStorage({ //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.mimettype === 'turtle') { + if (file.mimetype === 'application/json' || file.mimetype === 'text/plain' || file.mimetype === 'turtle') { cb(null, true); } else { cb(null, false); @@ -102,6 +132,9 @@ MQTTserver.listen(MQTTport, function () { app.use('/uploads', express.static('uploads')); // to store uploaded metadata to '/uploads' folder app.use(bodyParser.json()); // +//API HELPERS +function + // GET APIs app.get('/blocks', (req, res) => { res.json(bc.chain); @@ -131,7 +164,12 @@ app.get('/public-key', (req, res) => { }); /////////////// app.get('/Balance', (req, res) => { - res.json({ Balance: wallet.balance }); + const balance = getBlockchain().getBalanceCopy(wallet.publicKey); + res.json({ Balance: balance.balance }); +}); +app.get('/Balances', (req, res) => { + const balances = getBlockchain().balances; + res.json(balances); }); /////////////// @@ -170,15 +208,22 @@ app.get('/IoTdeviceRegistration', (req, res)=> { (err, nquads) => { //console.log(nquads) var metadata = wallet.createMetadata( - nquads, tp); + nquads); p2pServer.newMetadata(metadata); }); }); res.json("MetadataTransactionCreated"); }); +app.get('/storeSize', (req, res) => { + res.json({ + size: getBlockchain().store.size + }); +}); + ////////////////////////////////////////////////// // POST APIs + //this doesn't work well with the continious miner //app.post('/mine', (req, res) => { // const block = bc.addBlock(req.body.data); @@ -190,18 +235,38 @@ app.get('/IoTdeviceRegistration', (req, res)=> { //}); /////////////// app.post('/PaymentTransaction', (req, res) => { + if (!req.body.hasOwnProperty('recpient')) { + res.json({ + result: false, + reason: "Missing \"recipient\" in body" + }); + 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, bc, tp); + const transaction = wallet.createTransaction(recipient, amount, getBlockchain()); if (transaction === null) { res.json("Couldn't create transaction"); return; } p2pServer.newTransaction(transaction); - res.redirect('/transactions'); + 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") { @@ -239,24 +304,22 @@ app.post("/UploadMetafile", upload.single('file'), (req, res) => { ///////////////////// //Start of comunica sparql query code app.post('/sparql', (req, res) => { - console.log(req.body); const start = async function () { try { let result = []; const bindingsStream = await myEngine.queryBindings( - req.body, + req.body.query, { + log: new LoggerPretty({ level: 'trace' }), readOnly: true, - sources: [{ - type: 'rdfjsSource', - value: p2pServer.store - }] + sources: [getBlockchain().store] }); bindingsStream.on('data', (binding) => { console.log(binding.toString()); result.push(binding); }); bindingsStream.on('end', () => { + console.log('end'); res.json(JSON.stringify(result)); }); bindingsStream.on('error', (err) => { diff --git a/app/miner.js b/app/miner.js index ad053e8..3c0f4f5 100644 --- a/app/miner.js +++ b/app/miner.js @@ -1,22 +1,26 @@ -const Wallet = require('../wallet'); -const Transaction = require('../wallet/transaction'); const Block = require('../blockchain/block'); +const ITERATIONS = 1; class Miner { - static STATE_WAITING = 0; - static STATE_RUNNING = 1; - static STATE_INTERRUPTED = 2; - static STATE_RESTARTING = 3; + static STATE_RUNNING = 0; + static STATE_INTERRUPTED = 1; - constructor(blockchain, transactionPool, wallet, p2pServer) { + constructor(blockchain, transactionPool, reward, p2pServer) { this.blockchain = blockchain; this.transactionPool = transactionPool; - this.wallet = wallet; this.p2pServer = p2pServer; - this.state = Miner.STATE_WAITING; - this.mining = [[], []]; + 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() { @@ -26,65 +30,77 @@ class Miner { } interruptIfContainsTransaction(transaction) { - if (this.state === Miner.STATE_RUNNING && this.mining[0].find(t => t.id === transaction.id)) { + 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[1].find(t => t.id === metadata.id)) { + 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_WAITING && this.state !== Miner.STATE_RESTARTING) { + if (this.state !== Miner.STATE_INTERRUPTED && this.state !== Miner.STATE_RESTARTING) { return; } - const validTransactions = this.transactionPool.validTransactions(); - const validMetadataS = this.transactionPool.validMetadataS(); + this.minedStartTime = process.hrtime.bigint(); - if (validTransactions.length === 0 && validMetadataS.length === 0) { - this.state = Miner.STATE_WAITING; - return; - } - - validTransactions.push( - Transaction.rewardTransaction(this.wallet, Wallet.blockchainWallet()) - ); + 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.mining = [validTransactions, validMetadataS]; - this.nonce = 0; this.mine(); } mine() { if (this.state !== Miner.STATE_RUNNING) { this.state = Miner.STATE_RESTARTING; - startMine(); + this.startMine(); return; } const timestamp = Date.now(); const difficulty = Block.adjustDifficulty(this.lastBlock, timestamp); - const hash = Block.hash(timestamp, this.lastBlock.hash, this.mining, this.nonce, difficulty); - if (hash.substring(0, difficulty) === '0'.repeat(difficulty)) { - //success - this.p2pServer.newBlock(new Block(timestamp, this.lastBlock.hash, hash, this.mining, this.nonce, difficulty)); - this.state = Miner.STATE_RESTARTING; - setImmediate(() => { this.startMine() }); - } else { - //failure - this.nonce++; - setImmediate(() => { this.mine() }); + 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; +module.exports = Miner; diff --git a/app/p2p-server.js b/app/p2p-server.js index e0f0a88..c17cf2b 100644 --- a/app/p2p-server.js +++ b/app/p2p-server.js @@ -1,12 +1,12 @@ const Websocket = require('ws'); -const N3 = require('n3'); -const DataFactory = require('n3').DataFactory; + 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(',') : []; @@ -18,22 +18,26 @@ const MESSAGE_TYPES = { }; class P2pServer { - constructor(blockchain, transactionPool, wallet, chainStorageLocation) { - this.blockchain = blockchain; + constructor(transactionPool, rewardPublicKey, chainStorageLocation) { + this.blockchain = new Blockchain(); this.transactionPool = transactionPool; this.sockets = []; - this.store = new N3.Store(); this.chainStorageLocation = chainStorageLocation; - this.miner = new Miner(this.blockchain, this.transactionPool, wallet, this); //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 chain = JSON.parse(rawPersistedChain); - this.newChain(chain, false); + 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() { @@ -106,7 +110,7 @@ class P2pServer { } newTransaction(transaction, broadcast) { - if (!Transaction.verifyTransaction(transaction)) { + if (!Transaction.verify(transaction)) { console.log("Couldn't add transaction to p2pServer, couldn't verify"); return false; } @@ -128,32 +132,32 @@ class P2pServer { } } - newBlock(block) { + blockMined(block) { if (!this.blockchain.addBlock(block)) { //invalid block, return return; } - this.onNewBlock(block); - this.persistChain(this.blockchain.chain); + this.transactionPool.clearFromBlock(block); + this.miner.interrupt(); + this.persistChain(this.blockchain); this.syncChains(); } newChain(chain, persist) { - const oldChain = this.blockchain.chain; - const divergence = this.blockchain.replaceChain(chain); - - if (divergence === null) { + 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(chain); - } - for (let i = divergence; i < oldChain.length; i++) { - this.store.deleteGraph(oldChain[i].hash); - } - for (let i = divergence; i < this.blockchain.chain.length; i++) { - this.onNewBlock(this.blockchain.chain[i]); + this.persistChain(this.blockchain); } } @@ -161,70 +165,33 @@ class P2pServer { try { fs.writeFileSync( this.chainStorageLocation, - JSON.stringify(chain)); + chain.serialize()); } catch (err) { - console.error("Couldn't persist chain, aborting"); + console.error(`Couldn't persist chain, aborting: ${err}`); process.exit(-1); } } - onNewBlock(block) { - //block data is of form [transactions,metadatas] - if (block.data.length != 2) { - //assert? - return; - } - - this.transactionPool.clearFromBlock(block); - - this.miner.interrupt(); - - const metadatas = block.data[1]; - - 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) { - this.store.addQuad(DataFactory.quad( - DataFactory.namedNode(quadN.subject.id), - DataFactory.namedNode(quadN.predicate.id), - DataFactory.namedNode(quadN.object.id), - DataFactory.namedNode(block.hash))); - } - }); - } - } - sendChain(socket) { socket.send(JSON.stringify({ type: MESSAGE_TYPES.chain, - chain: this.blockchain.chain + chain: this.blockchain.serialize() })); } - //sendTransaction(socket, transaction) { - // socket.send(JSON.stringify({ - // type: MESSAGE_TYPES.transaction, - // transaction - // })); - //} + sendTransaction(socket, transaction) { + socket.send(JSON.stringify({ + type: MESSAGE_TYPES.transaction, + transaction + })); + } - //sendMetadata(socket, metadata) { - // socket.send(JSON.stringify({ - // type: MESSAGE_TYPES.metadata, - // metadata - // })); - //} + sendMetadata(socket, metadata) { + socket.send(JSON.stringify({ + type: MESSAGE_TYPES.metadata, + metadata + })); + } syncChains() { this.sockets.forEach(socket => this.sendChain(socket)); } diff --git a/blockchain/block.js b/blockchain/block.js index 7b227f1..9c88485 100644 --- a/blockchain/block.js +++ b/blockchain/block.js @@ -1,12 +1,20 @@ const ChainUtil = require('../chain-util'); const { DIFFICULTY, MINE_RATE } = require('../config'); +function concatIfNotUndefined(concatTo, concatting) { + if (typeof concatting !== "undefined") { + concatTo += `${concatting}`; + } +} + class Block { - constructor(timestamp, lastHash, hash, data, nonce, difficulty) { + constructor(timestamp, lastHash, hash, reward, transactions, metadatas, nonce, difficulty) { this.timestamp = timestamp; this.lastHash = lastHash; this.hash = hash; - this.data = data; + this.reward = reward; + this.transactions = transactions; + this.metadatas = metadatas; this.nonce = nonce; if (difficulty === undefined) { this.difficulty = DIFFICULTY; @@ -15,54 +23,90 @@ class Block { } } + static getTransactions(block) { + if (typeof block.transactions !== "undefined" && block.transactions !== null) { + return block.transactions; + } else { + return []; + } + } + + static getMetadatas(block) { + if (typeof block.metadatas !== "undefined" && block.metadatas !== null) { + return block.metadatas; + } else { + return []; + } + } + toString() { return `Block - - Timestamp : ${this.timestamp} - Last Hash : ${this.lastHash.substring(0, 10)} - Hash : ${this.hash.substring(0, 10)} - Nonce : ${this.nonce} - Difficulty: ${this.difficulty} - Data : ${this.data}`; + Timestamp : ${this.timestamp} + Last Hash : ${this.lastHash.substring(0, 10)} + Hash : ${this.hash.substring(0, 10)} + Nonce : ${this.nonce} + Difficulty : ${this.difficulty} + Reward : ${this.reward} + Transactions : ${this.transactions} + Metadatas : ${this.metadatas}`; } static genesis() { - return new this('Genesis time', '-----', 'f1r57-h45h', [], 0, DIFFICULTY); + return new this('Genesis time', '-----', 'f1r57-h45h', null, null, null, 0, DIFFICULTY); } - //returns false if hash doesn't match - static checkHash(hash, timestamp, lastHash, data, nonce, difficulty) { - const computedHash = Block.hash(timestamp, lastHash, data, nonce, difficulty); + static hash(timestamp, lastHash, reward, transactions, metadatas, 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 (computedHash !== hash) { + return ChainUtil.hash(hashing).toString(); + } + + static blockHash(block) { + return Block.hash( + block.timestamp, + block.lastHash, + block.reward, + block.transactions, + block.metadatas, + 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); + + if (computedHash !== block.hash) { return false; } - if (hash.substring(0, difficulty) !== '0'.repeat(difficulty)) { + if (block.hash.substring(0, block.difficulty) !== '0'.repeat(block.difficulty)) { return false; } return true; } - static hash(timestamp, lastHash, data, nonce, difficulty) { - return ChainUtil.hash(`${timestamp}${lastHash}${data}${nonce}${difficulty}`).toString(); - } - - static blockHash(block) { - const { timestamp, lastHash, data, nonce, difficulty } = block; - return Block.hash(timestamp, lastHash, data, nonce, difficulty); - } - - //returns false if block's hash doesn't match internals - static checkBlock(block) { - return Block.checkHash(block.hash, block.timestamp, block.lastHash, block.data, block.nonce, block.difficulty); - } - static adjustDifficulty(lastBlock, currentTime) { - let { difficulty } = lastBlock; - difficulty = lastBlock.timestamp + MINE_RATE > currentTime ? - difficulty + 1 : difficulty - 1; - return Math.max(0, difficulty); + let prevDifficulty = lastBlock.difficulty; + if (lastBlock.timestamp + MINE_RATE > currentTime) { + return prevDifficulty + 1; + } else { + return Math.max(0, prevDifficulty - 1); + } } } diff --git a/blockchain/index.js b/blockchain/index.js index f851175..2f1c6eb 100644 --- a/blockchain/index.js +++ b/blockchain/index.js @@ -1,81 +1,276 @@ 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 key in metadatas) { + const metadata = metadatas[key]; + 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(block.hash))); + } + }); + } +} 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) { - if (newBlock.lastHash !== this.chain[this.chain.length - 1].hash) { - console.log("Tried to add invalid block, last hash didn't match our last hash"); - return false; - } - //how to check if new block's timestamp is believable - if (newBlock.difficulty !== Block.adjustDifficulty(this.chain[this.chain.length - 1], newBlock.timestamp)) { - console.log("Tried to add invalid block, difficulty is incorrect"); - return false; - } - if (!Block.checkBlock(newBlock)) { - console.log("Tried to add invalid block, block's hash doesn't match its contents"); - return false; - } + 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); - console.log("Added new block: "); + 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; } - isValidChain(chain) { - if (chain.length === 0) { - return false; - } - if (JSON.stringify(chain[0]) !== JSON.stringify(Block.genesis())) { - return false; - } + static isValidChain(chain) { + const res = verifyChain(chain); - for (let i=1; i= chainDifference; i--) { + this.store.deleteGraph(oldChain[i].hash); } - //if they didn't differ in the length of the old chain, must be one after - return oldChain.length; + 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 + }; } } diff --git a/chain-util.js b/chain-util.js index bba6e36..2585723 100644 --- a/chain-util.js +++ b/chain-util.js @@ -19,6 +19,14 @@ class ChainUtil { static verifySignature(publicKey, signature, dataHash) { return ec.keyFromPublic(publicKey, 'hex').verify(dataHash, signature); } + + static deserializeKeyPair(serialized) { + return ec.keyFromPrivate(serialized, 'hex'); + } + + static serializeKeyPair(keyPair) { + return keyPair.getPrivate().toString('hex'); + } } module.exports = ChainUtil; \ No newline at end of file diff --git a/settings.json b/settings.json new file mode 100644 index 0000000..75b14f1 --- /dev/null +++ b/settings.json @@ -0,0 +1,3 @@ +{ + "wallet-private-key": "557360924eae1b0a7ff4727c4226300788b2e98e50b91195f1048e671d4d5d6e" +} \ No newline at end of file diff --git a/wallet/index.js b/wallet/index.js index 655b8c6..e04f428 100644 --- a/wallet/index.js +++ b/wallet/index.js @@ -5,9 +5,9 @@ const ChainUtil = require('../chain-util'); class Wallet { constructor(keyPair) { - this.balance = INITIAL_BALANCE; this.keyPair = keyPair; this.publicKey = this.keyPair.getPublic().encode('hex'); + this.counter = 0; } toString() { @@ -20,63 +20,72 @@ class Wallet { return this.keyPair.sign(dataHash); } - createTransaction(recipient, amount, blockchain, transactionPool) { - this.balance = this.calculateBalance(blockchain); + createTransaction(recipient, amount, blockchain) { + const balance = blockchain.getBalanceCopy(this.publicKey); - if (amount > this.balance) { - console.log(`Amount: ${amount} exceceds current balance: ${this.balance}`); + if (balance.counter > this.counter) { + this.counter = balance.counter; + } + + if (amount > balance.balance) { + console.log(`Amount: ${amount} exceceds current balance: ${balance.balance}`); return null; } - return Transaction.newTransaction(this, recipient, amount); + 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); + //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; + // let startTime = 0; - if (walletInputTs.length > 0) { - const recentInputT = walletInputTs.reduce( - (prev, current) => prev.input.timestamp > current.input.timestamp ? prev : current - ); + // 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; - } + // 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; - } - }); - } - }); + // PaymentTransactions.forEach(transaction => { + // if (transaction.input.timestamp > startTime) { + // transaction.outputs.find(output => { + // if (output.address === this.publicKey) { + // balance += output.amount; + // } + // }); + // } + // }); - return balance; - } + // return balance; + //} - static blockchainWallet() { - const blockchainWallet = new this(ChainUtil.genKeyPair()); - blockchainWallet.address = 'blockchain-wallet'; - return blockchainWallet; - } + //static blockchainWallet() { + // const blockchainWallet = new this(ChainUtil.genKeyPair()); + // blockchainWallet.address = 'blockchain-wallet'; + // return blockchainWallet; + //} } module.exports = Wallet; diff --git a/wallet/index.test.js b/wallet/index.test.js index e7cc0fc..2e9f100 100644 --- a/wallet/index.test.js +++ b/wallet/index.test.js @@ -18,12 +18,14 @@ describe('Wallet', () => { beforeEach(() => { sendAmount = 50; recipient = 'r4nd0m-4ddr355'; - transaction = wallet.createTransaction(recipient, sendAmount, bc, tp); + transaction = wallet.createTransaction(recipient, sendAmount, bc); + tp.updateOrAddTransaction(transaction); }); describe('and doing the same transaction', () => { beforeEach(() => { - wallet.createTransaction(recipient, sendAmount, bc, tp); + transaction = wallet.createTransaction(recipient, sendAmount, bc); + tp.updateOrAddTransaction(transaction); }); it('doubles the `sendAmount` subtracted from the wallet balance', () => { @@ -46,7 +48,8 @@ describe('Wallet', () => { addBalance = 100; repeatAdd = 3; for (let i=0; i { tp.clear(); subtractBalance = 60; recipientBalance = wallet.calculateBalance(bc); - wallet.createTransaction(senderWallet.publicKey, subtractBalance, bc, tp); + const transaction = wallet.createTransaction(senderWallet.publicKey, subtractBalance, bc); + tp.updateOrAddTransaction(transaction); bc.addBlock(tp.transactions); }); describe('and the sender sends another transaction to the recipient', () => { beforeEach(() => { tp.clear(); - senderWallet.createTransaction(wallet.publicKey, addBalance, bc, tp); + const transaction = senderWallet.createTransaction(wallet.publicKey, addBalance, bc); + tp.updateOrAddTransaction(transaction); bc.addBlock(tp.transactions); }); diff --git a/wallet/metadata.js b/wallet/metadata.js index d787f6e..8f7b317 100644 --- a/wallet/metadata.js +++ b/wallet/metadata.js @@ -63,9 +63,7 @@ class Metadata { } static newMetadata(senderWallet,SSNmetadata){ - return Metadata.MetadataOfIoTDevice(senderWallet, SSNmetadata - ); - + return Metadata.MetadataOfIoTDevice(senderWallet, SSNmetadata); } static signMetadata (metadata, senderWallet) { @@ -76,14 +74,13 @@ class Metadata { } } - - static verifyMetadata(metadata) { - return ChainUtil.verifySignature( - metadata.Signiture.address, - metadata.Signiture.signature, - 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 index 71648f6..ace972f 100644 --- a/wallet/transaction-pool.js +++ b/wallet/transaction-pool.js @@ -1,5 +1,6 @@ const Transaction = require('../wallet/transaction'); -const Metadata = require('../wallet/metadata') +const Metadata = require('../wallet/metadata'); +const Block = require('../blockchain/block'); const Return = { add: 1, @@ -10,16 +11,16 @@ const Return = { class TransactionPool { constructor() { this.transactions = []; - this.metadataS =[]; + this.metadatas = []; } //returns true on update, false on add updateOrAddTransaction(transaction) { - if (!Transaction.verifyTransaction(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.id === transaction.id); + const foundIndex = this.transactions.findIndex(t => t.input === transaction.input && t.counter === transaction.counter); if (foundIndex !== -1) { this.transactions[foundIndex] = transaction; @@ -36,13 +37,13 @@ class TransactionPool { return Return.error; } - const foundIndex = this.metadataS.findIndex(t => t.id === metadata.id); + const foundIndex = this.metadatas.findIndex(t => t.id === metadata.id); if (foundIndex !== -1) { - this.metadataS[foundIndex] = metadata; + this.metadatas[foundIndex] = metadata; return Return.update; } else { - this.metadataS.push(metadata); + this.metadatas.push(metadata); return Return.add; } } @@ -52,62 +53,41 @@ class TransactionPool { } existingMetadata(address) { - return this.metadataS.find(t => t.Signiture.address === address); + return this.metadatas.find(t => t.Signiture.address === address); } - validTransactions() { - return this.transactions.filter(transaction => { - const outputTotal = transaction.outputs.reduce((total, output) => { - return total + output.amount; - }, 0); - - if (transaction.input.amount !== outputTotal) { - console.log(`Invalid transaction from ${transaction.input.address}.`); - return; - } - - if (!Transaction.verifyTransaction(transaction)) { - console.log(`Invalid signature from ${transaction.input.address}.`); - return; - } - - return transaction; - }); + //we could check for possible double spends here + validTransactionsCopy() { + return [...this.transactions]; } - validMetadataS(){ - return this.metadataS.filter(metadata => { - if (!Metadata.verifyMetadata(metadata)) { - console.log(`Invalid signature from ${metadata.Signiture.address}.`); - return; - } - return metadata; - }); + validMetadatasCopy(){ + return [...this.metadatas]; } clearFromBlock(block) { - const transactions = block.data[0]; - const metadatas = block.data[1]; - for (const transaction of transactions) { + 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 metadatas) { - const foundMetadata = this.metadataS.findIndex(m => m.id === metadata.id); + for (const metadata of blockMetadatas) { + const foundMetadata = this.metadatas.findIndex(m => m.id === metadata.id); if (foundMetadata !== -1) { - this.metadataS.splice(foundMetadata, 1); + this.metadatas.splice(foundMetadata, 1); } } } clearAll() { this.transactions = []; - this.metadataS = []; + this.metadatas = []; } } diff --git a/wallet/transaction.js b/wallet/transaction.js index 64eb967..97fd95c 100644 --- a/wallet/transaction.js +++ b/wallet/transaction.js @@ -2,66 +2,77 @@ const ChainUtil = require('../chain-util'); const { MINING_REWARD } = require('../config'); class Transaction { - constructor() { - this.id = ChainUtil.id(); - this.input = null; - this.outputs = []; + constructor(senderPublicKey, counter, outputs) { + this.input = senderPublicKey; + this.signature = null; + this.counter = counter; + this.outputs = outputs; } - update(senderWallet, recipient, amount) { - const senderOutput = this.outputs.find(output => output.address === senderWallet.publicKey); + 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; + } - if (amount > senderOutput.amount) { - console.log(`Amount: ${amount} exceeds balance.`); - return; + 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; + } } - senderOutput.amount = senderOutput.amount - amount; - this.outputs.push({ amount, address: recipient }); - Transaction.signTransaction(this, senderWallet); - - return this; - } - - static transactionWithOutputs(senderWallet, outputs) { - const transaction = new this(); - transaction.outputs.push(...outputs); - Transaction.signTransaction(transaction, senderWallet); - return transaction; - } - - static newTransaction(senderWallet, recipient, amount) { - if (amount > senderWallet.balance) { - console.log(`Amount: ${amount} exceeds balance.`); - return; - } - - return Transaction.transactionWithOutputs(senderWallet, [ - { amount: senderWallet.balance - amount, address: senderWallet.publicKey }, - { amount, address: recipient } - ]); - } - - static rewardTransaction(minerWallet, blockchainWallet) { - return Transaction.transactionWithOutputs(blockchainWallet, [{ - amount: MINING_REWARD, address: minerWallet.publicKey - }]); - } - - static signTransaction(transaction, senderWallet) { - transaction.input = { - timestamp: Date.now(), - amount: senderWallet.balance, - address: senderWallet.publicKey, - signature: senderWallet.sign(ChainUtil.hash(transaction.outputs)) - } - } - - static verifyTransaction(transaction) { return ChainUtil.verifySignature( - transaction.input.address, - transaction.input.signature, - ChainUtil.hash(transaction.outputs) + transaction.input, + transaction.signature, + Transaction.hashToSign(transaction) ); } }