miner mines continuously,

moved to an account model,
transactions working,
querying working,
more checking of valid data
This commit is contained in:
Josip Milovac 2023-01-10 17:11:56 +11:00
parent 59bb42be11
commit 9847b2056b
12 changed files with 663 additions and 365 deletions

View file

@ -27,16 +27,17 @@
* to monitor the node * to monitor the node
* *
*/ */
const LoggerPretty = require("@comunica/logger-pretty").LoggerPretty;
const express = require('express'); const express = require('express');
const bodyParser = require('body-parser'); const bodyParser = require('body-parser');
const Blockchain = require('../blockchain');
const P2pServer = require('./p2p-server'); const P2pServer = require('./p2p-server');
const Wallet = require('../wallet'); const Wallet = require('../wallet');
const TransactionPool = require('../wallet/transaction-pool'); const TransactionPool = require('../wallet/transaction-pool');
const QueryEngine = require('@comunica/query-sparql').QueryEngine; const QueryEngine = require('@comunica/query-sparql').QueryEngine;
const ChainUtil = require('../chain-util'); const ChainUtil = require('../chain-util');
const N3 = require('n3');
const jsonld = require('jsonld'); const jsonld = require('jsonld');
var mqtt = require('mqtt'); var mqtt = require('mqtt');
var aedes = require('aedes')(); /* aedes is a stream-based MQTT broker */ 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". '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.*/ 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 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(); const myEngine = new QueryEngine();
function getBlockchain() {
return p2pServer.blockchain;
}
app.use(bodyParser.json()); app.use(bodyParser.json());
//initialising a local storage for storing metadata file initially before storing it in the tripple store //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 //filtering the type of uploaded Metadata files
const fileFilter = (req, file, cb) => { const fileFilter = (req, file, cb) => {
// reject a file // 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); cb(null, true);
} else { } else {
cb(null, false); 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('/uploads', express.static('uploads')); // to store uploaded metadata to '/uploads' folder
app.use(bodyParser.json()); // app.use(bodyParser.json()); //
//API HELPERS
function
// GET APIs // GET APIs
app.get('/blocks', (req, res) => { app.get('/blocks', (req, res) => {
res.json(bc.chain); res.json(bc.chain);
@ -131,7 +164,12 @@ app.get('/public-key', (req, res) => {
}); });
/////////////// ///////////////
app.get('/Balance', (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) => { (err, nquads) => {
//console.log(nquads) //console.log(nquads)
var metadata = wallet.createMetadata( var metadata = wallet.createMetadata(
nquads, tp); nquads);
p2pServer.newMetadata(metadata); p2pServer.newMetadata(metadata);
}); });
}); });
res.json("MetadataTransactionCreated"); res.json("MetadataTransactionCreated");
}); });
app.get('/storeSize', (req, res) => {
res.json({
size: getBlockchain().store.size
});
});
////////////////////////////////////////////////// //////////////////////////////////////////////////
// POST APIs // POST APIs
//this doesn't work well with the continious miner //this doesn't work well with the continious miner
//app.post('/mine', (req, res) => { //app.post('/mine', (req, res) => {
// const block = bc.addBlock(req.body.data); // const block = bc.addBlock(req.body.data);
@ -190,18 +235,38 @@ app.get('/IoTdeviceRegistration', (req, res)=> {
//}); //});
/////////////// ///////////////
app.post('/PaymentTransaction', (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 { recipient, amount } = req.body;
const transaction = wallet.createTransaction(recipient, amount, bc, tp); const transaction = wallet.createTransaction(recipient, amount, getBlockchain());
if (transaction === null) { if (transaction === null) {
res.json("Couldn't create transaction"); res.json("Couldn't create transaction");
return; return;
} }
p2pServer.newTransaction(transaction); p2pServer.newTransaction(transaction);
res.redirect('/transactions'); res.json(transaction);
}); });
/////////////// ///////////////
app.post('/IoTdevicePaymentTransaction', (req, res) => { 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, const { Recipient_payment_address, Amount_of_money, Payment_method,
Further_details} = req.body; Further_details} = req.body;
if (Payment_method == "SensorCoin") { if (Payment_method == "SensorCoin") {
@ -239,24 +304,22 @@ app.post("/UploadMetafile", upload.single('file'), (req, res) => {
///////////////////// /////////////////////
//Start of comunica sparql query code //Start of comunica sparql query code
app.post('/sparql', (req, res) => { app.post('/sparql', (req, res) => {
console.log(req.body);
const start = async function () { const start = async function () {
try { try {
let result = []; let result = [];
const bindingsStream = await myEngine.queryBindings( const bindingsStream = await myEngine.queryBindings(
req.body, req.body.query,
{ {
log: new LoggerPretty({ level: 'trace' }),
readOnly: true, readOnly: true,
sources: [{ sources: [getBlockchain().store]
type: 'rdfjsSource',
value: p2pServer.store
}]
}); });
bindingsStream.on('data', (binding) => { bindingsStream.on('data', (binding) => {
console.log(binding.toString()); console.log(binding.toString());
result.push(binding); result.push(binding);
}); });
bindingsStream.on('end', () => { bindingsStream.on('end', () => {
console.log('end');
res.json(JSON.stringify(result)); res.json(JSON.stringify(result));
}); });
bindingsStream.on('error', (err) => { bindingsStream.on('error', (err) => {

View file

@ -1,22 +1,26 @@
const Wallet = require('../wallet');
const Transaction = require('../wallet/transaction');
const Block = require('../blockchain/block'); const Block = require('../blockchain/block');
const ITERATIONS = 1;
class Miner { class Miner {
static STATE_WAITING = 0; static STATE_RUNNING = 0;
static STATE_RUNNING = 1; static STATE_INTERRUPTED = 1;
static STATE_INTERRUPTED = 2;
static STATE_RESTARTING = 3;
constructor(blockchain, transactionPool, wallet, p2pServer) { constructor(blockchain, transactionPool, reward, p2pServer) {
this.blockchain = blockchain; this.blockchain = blockchain;
this.transactionPool = transactionPool; this.transactionPool = transactionPool;
this.wallet = wallet;
this.p2pServer = p2pServer; this.p2pServer = p2pServer;
this.state = Miner.STATE_WAITING; this.state = Miner.STATE_INTERRUPTED;
this.mining = [[], []];
this.lastBlock = null; this.lastBlock = null;
this.minedStartTime = null;
this.mining = {};
this.mining.transactions = [];
this.mining.reward = reward;
this.mining.metadatas = [];
this.startMine();
} }
interrupt() { interrupt() {
@ -26,64 +30,76 @@ class Miner {
} }
interruptIfContainsTransaction(transaction) { 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; this.state = Miner.STATE_INTERRUPTED;
} }
} }
interruptIfContainsMetadata(metadata) { 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; this.state = Miner.STATE_INTERRUPTED;
} }
} }
startMine() { startMine() {
//only continue if state is waiting or restarting //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; return;
} }
const validTransactions = this.transactionPool.validTransactions(); this.minedStartTime = process.hrtime.bigint();
const validMetadataS = this.transactionPool.validMetadataS();
if (validTransactions.length === 0 && validMetadataS.length === 0) { this.mining.transactions = this.transactionPool.validTransactionsCopy();
this.state = Miner.STATE_WAITING; this.mining.metadatas = this.transactionPool.validMetadatasCopy();
return;
}
validTransactions.push(
Transaction.rewardTransaction(this.wallet, Wallet.blockchainWallet())
);
this.lastBlock = this.blockchain.chain[this.blockchain.chain.length - 1]; this.lastBlock = this.blockchain.chain[this.blockchain.chain.length - 1];
this.nonce = 0;
this.state = Miner.STATE_RUNNING; this.state = Miner.STATE_RUNNING;
this.mining = [validTransactions, validMetadataS];
this.nonce = 0;
this.mine(); this.mine();
} }
mine() { mine() {
if (this.state !== Miner.STATE_RUNNING) { if (this.state !== Miner.STATE_RUNNING) {
this.state = Miner.STATE_RESTARTING; this.state = Miner.STATE_RESTARTING;
startMine(); this.startMine();
return; return;
} }
const timestamp = Date.now(); const timestamp = Date.now();
const difficulty = Block.adjustDifficulty(this.lastBlock, timestamp); const difficulty = Block.adjustDifficulty(this.lastBlock, timestamp);
const hash = Block.hash(timestamp, this.lastBlock.hash, this.mining, this.nonce, difficulty);
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)) { if (hash.substring(0, difficulty) === '0'.repeat(difficulty)) {
//success //success
this.p2pServer.newBlock(new Block(timestamp, this.lastBlock.hash, hash, this.mining, this.nonce, difficulty)); 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; this.state = Miner.STATE_RESTARTING;
setImmediate(() => { this.startMine() }); setImmediate(() => { this.startMine() });
} else { } else {
//failure //failure
this.nonce++; this.nonce++;
setImmediate(() => { this.mine() });
} }
} }
setImmediate(() => { this.mine() });
}
} }
module.exports = Miner; module.exports = Miner;

View file

@ -1,12 +1,12 @@
const Websocket = require('ws'); const Websocket = require('ws');
const N3 = require('n3');
const DataFactory = require('n3').DataFactory;
const fs = require('fs'); const fs = require('fs');
const process = require('process'); const process = require('process');
const Miner = require('./miner'); const Miner = require('./miner');
const Transaction = require('../wallet/transaction'); const Transaction = require('../wallet/transaction');
const TransactionPool = require('../wallet/transaction-pool'); const TransactionPool = require('../wallet/transaction-pool');
const Metadata = require('../wallet/metadata'); const Metadata = require('../wallet/metadata');
const Blockchain = require('../blockchain');
const P2P_PORT = process.env.P2P_PORT || 5000; const P2P_PORT = process.env.P2P_PORT || 5000;
const peers = process.env.PEERS ? process.env.PEERS.split(',') : []; const peers = process.env.PEERS ? process.env.PEERS.split(',') : [];
@ -18,22 +18,26 @@ const MESSAGE_TYPES = {
}; };
class P2pServer { class P2pServer {
constructor(blockchain, transactionPool, wallet, chainStorageLocation) { constructor(transactionPool, rewardPublicKey, chainStorageLocation) {
this.blockchain = blockchain; this.blockchain = new Blockchain();
this.transactionPool = transactionPool; this.transactionPool = transactionPool;
this.sockets = []; this.sockets = [];
this.store = new N3.Store();
this.chainStorageLocation = chainStorageLocation; 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 //possible race if deleted after check, but we live with it I guess
if (fs.existsSync(this.chainStorageLocation)) { if (fs.existsSync(this.chainStorageLocation)) {
const rawPersistedChain = fs.readFileSync(this.chainStorageLocation, 'utf8'); const rawPersistedChain = fs.readFileSync(this.chainStorageLocation, 'utf8');
const chain = JSON.parse(rawPersistedChain); const deserialized = Blockchain.deserialize(rawPersistedChain);
this.newChain(chain, false); if (deserialized === null) {
console.log(`Couldn't deserialize chain at '${this.chainStorageLocation}', starting from genesis`);
} else {
this.blockchain = deserialized;
}
} else { } else {
console.log("Didn't find a persisted chain, starting from genesis"); console.log("Didn't find a persisted chain, starting from genesis");
} }
this.miner = new Miner(this.blockchain, this.transactionPool, rewardPublicKey, this);
} }
listen() { listen() {
@ -106,7 +110,7 @@ class P2pServer {
} }
newTransaction(transaction, broadcast) { newTransaction(transaction, broadcast) {
if (!Transaction.verifyTransaction(transaction)) { if (!Transaction.verify(transaction)) {
console.log("Couldn't add transaction to p2pServer, couldn't verify"); console.log("Couldn't add transaction to p2pServer, couldn't verify");
return false; return false;
} }
@ -128,32 +132,32 @@ class P2pServer {
} }
} }
newBlock(block) { blockMined(block) {
if (!this.blockchain.addBlock(block)) { if (!this.blockchain.addBlock(block)) {
//invalid block, return //invalid block, return
return; return;
} }
this.onNewBlock(block); this.transactionPool.clearFromBlock(block);
this.persistChain(this.blockchain.chain); this.miner.interrupt();
this.persistChain(this.blockchain);
this.syncChains(); this.syncChains();
} }
newChain(chain, persist) { newChain(chain, persist) {
const oldChain = this.blockchain.chain; const replaceResult = this.blockchain.replaceChain(chain);
const divergence = this.blockchain.replaceChain(chain); if (!replaceResult.result) {
if (divergence === null) {
//failed to replace //failed to replace
return; return;
} }
for (let i = 0; i < replaceResult.chainDifference; i++) {
this.transactionPool.clearFromBlock(this.blockchain.chain[i]);
}
this.miner.interrupt();
if (typeof persist === "undefined" || persist) { if (typeof persist === "undefined" || persist) {
this.persistChain(chain); this.persistChain(this.blockchain);
}
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]);
} }
} }
@ -161,70 +165,33 @@ class P2pServer {
try { try {
fs.writeFileSync( fs.writeFileSync(
this.chainStorageLocation, this.chainStorageLocation,
JSON.stringify(chain)); chain.serialize());
} catch (err) { } catch (err) {
console.error("Couldn't persist chain, aborting"); console.error(`Couldn't persist chain, aborting: ${err}`);
process.exit(-1); 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) { sendChain(socket) {
socket.send(JSON.stringify({ socket.send(JSON.stringify({
type: MESSAGE_TYPES.chain, type: MESSAGE_TYPES.chain,
chain: this.blockchain.chain chain: this.blockchain.serialize()
})); }));
} }
//sendTransaction(socket, transaction) { sendTransaction(socket, transaction) {
// socket.send(JSON.stringify({ socket.send(JSON.stringify({
// type: MESSAGE_TYPES.transaction, type: MESSAGE_TYPES.transaction,
// transaction transaction
// })); }));
//} }
//sendMetadata(socket, metadata) { sendMetadata(socket, metadata) {
// socket.send(JSON.stringify({ socket.send(JSON.stringify({
// type: MESSAGE_TYPES.metadata, type: MESSAGE_TYPES.metadata,
// metadata metadata
// })); }));
//} }
syncChains() { syncChains() {
this.sockets.forEach(socket => this.sendChain(socket)); this.sockets.forEach(socket => this.sendChain(socket));
} }

View file

@ -1,12 +1,20 @@
const ChainUtil = require('../chain-util'); const ChainUtil = require('../chain-util');
const { DIFFICULTY, MINE_RATE } = require('../config'); const { DIFFICULTY, MINE_RATE } = require('../config');
function concatIfNotUndefined(concatTo, concatting) {
if (typeof concatting !== "undefined") {
concatTo += `${concatting}`;
}
}
class Block { class Block {
constructor(timestamp, lastHash, hash, data, nonce, difficulty) { constructor(timestamp, lastHash, hash, reward, transactions, metadatas, nonce, difficulty) {
this.timestamp = timestamp; this.timestamp = timestamp;
this.lastHash = lastHash; this.lastHash = lastHash;
this.hash = hash; this.hash = hash;
this.data = data; this.reward = reward;
this.transactions = transactions;
this.metadatas = metadatas;
this.nonce = nonce; this.nonce = nonce;
if (difficulty === undefined) { if (difficulty === undefined) {
this.difficulty = DIFFICULTY; this.difficulty = DIFFICULTY;
@ -15,6 +23,22 @@ 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() { toString() {
return `Block - return `Block -
Timestamp : ${this.timestamp} Timestamp : ${this.timestamp}
@ -22,47 +46,67 @@ class Block {
Hash : ${this.hash.substring(0, 10)} Hash : ${this.hash.substring(0, 10)}
Nonce : ${this.nonce} Nonce : ${this.nonce}
Difficulty : ${this.difficulty} Difficulty : ${this.difficulty}
Data : ${this.data}`; Reward : ${this.reward}
Transactions : ${this.transactions}
Metadatas : ${this.metadatas}`;
} }
static genesis() { 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 hash(timestamp, lastHash, reward, transactions, metadatas, nonce, difficulty) {
static checkHash(hash, timestamp, lastHash, data, nonce, difficulty) { //backwards compatible hashing:
const computedHash = Block.hash(timestamp, lastHash, data, nonce, difficulty); //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; return false;
} }
if (hash.substring(0, difficulty) !== '0'.repeat(difficulty)) { if (block.hash.substring(0, block.difficulty) !== '0'.repeat(block.difficulty)) {
return false; return false;
} }
return true; 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) { static adjustDifficulty(lastBlock, currentTime) {
let { difficulty } = lastBlock; let prevDifficulty = lastBlock.difficulty;
difficulty = lastBlock.timestamp + MINE_RATE > currentTime ? if (lastBlock.timestamp + MINE_RATE > currentTime) {
difficulty + 1 : difficulty - 1; return prevDifficulty + 1;
return Math.max(0, difficulty); } else {
return Math.max(0, prevDifficulty - 1);
}
} }
} }

View file

@ -1,81 +1,276 @@
const Block = require('./block'); const Block = require('./block');
const N3 = require('n3');
const DataFactory = require('n3').DataFactory;
const Transaction = require('../wallet/transaction');
const { MINING_REWARD } = require('../config');
class Blockchain { function getBalanceCopyGeneric(publicKey, maps) {
constructor() { for (const map of maps) {
this.chain = [Block.genesis()]; if (map.hasOwnProperty(publicKey)) {
const found = map[publicKey];
return {
balance: found.balance,
counter: found.counter
};
}
} }
//adds an existing block to the blockchain, returns false if the block can't be added, true if it was added return {
addBlock(newBlock) { balance: 0,
if (newBlock.lastHash !== this.chain[this.chain.length - 1].hash) { counter: 0
console.log("Tried to add invalid block, last hash didn't match our last hash"); };
return false; }
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 //how to check if new block's timestamp is believable
if (newBlock.difficulty !== Block.adjustDifficulty(this.chain[this.chain.length - 1], newBlock.timestamp)) { if (verifyingBlock.difficulty !== Block.adjustDifficulty(prevBlock, verifyingBlock.timestamp)) {
console.log("Tried to add invalid block, difficulty is incorrect"); return {
return false; result: false,
reason: "difficulty is incorrect"
};
} }
if (!Block.checkBlock(newBlock)) { if (!Block.checkHash(verifyingBlock)) {
console.log("Tried to add invalid block, block's hash doesn't match its contents"); return {
return false; result: false,
reason: "hash is invalid failed"
};
} }
this.chain.push(newBlock); const changedBalances = {};
console.log("Added new block: "); const rewardBalanceCopy = getBalanceCopyGeneric(verifyingBlock.reward, [prevBalances]);
//console.log(newBlock);
return true; 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" };
} }
isValidChain(chain) { 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) { if (chain.length === 0) {
return false; return {
result: false,
reason: "zero length"
};
} }
if (JSON.stringify(chain[0]) !== JSON.stringify(Block.genesis())) { if (JSON.stringify(chain[0]) !== JSON.stringify(Block.genesis())) {
return false; return {
result: false,
reason: "initial block isn't genesis"
};
} }
const balances = {};
for (let i = 1; i < chain.length; i++) { for (let i = 1; i < chain.length; i++) {
const block = chain[i]; const block = chain[i];
const lastBlock = chain[i - 1]; const lastBlock = chain[i - 1];
if (block.lastHash !== lastBlock.hash || const verifyResult = verifyBlock(balances, lastBlock, block);
block.hash !== Block.blockHash(block)) {
return false; if (verifyResult.result === false) {
return {
result: false,
reason: `Chain is invalid on block ${i}: ${verifyResult.reason}`
};
} }
if (!Block.checkBlock(block)) {
return false; for (const publicKey in verifyResult.changedBalances) {
balances[publicKey] = verifyResult.changedBalances[publicKey];
} }
} }
return true; return {
result: true,
balances: balances
};
} }
//return null on fail, returns the index of where they differ //returns the first index where the two chains differ
replaceChain(newChain) { function findChainDifference(oldChain, newChain) {
if (newChain.length <= this.chain.length) {
console.log('Received chain is not longer than the current chain.');
return false;
} else if (!this.isValidChain(newChain)) {
console.log('The received chain is not valid.');
return false;
}
console.log('Replacing blockchain with the new chain.');
const oldChain = this.chain;
this.chain = newChain;
//find where they differ
for (let i = 1; i < oldChain.length; ++i) { for (let i = 1; i < oldChain.length; ++i) {
if (oldChain[i].hash !== newChain[i].hash) { if (oldChain[i].hash !== newChain[i].hash) {
return i; return i;
} }
} }
//if they didn't differ in the length of the old chain, must be one after return 1;
return oldChain.length; }
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) {
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--) {
this.store.deleteGraph(oldChain[i].hash);
}
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
};
} }
} }

View file

@ -19,6 +19,14 @@ class ChainUtil {
static verifySignature(publicKey, signature, dataHash) { static verifySignature(publicKey, signature, dataHash) {
return ec.keyFromPublic(publicKey, 'hex').verify(dataHash, signature); 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; module.exports = ChainUtil;

3
settings.json Normal file
View file

@ -0,0 +1,3 @@
{
"wallet-private-key": "557360924eae1b0a7ff4727c4226300788b2e98e50b91195f1048e671d4d5d6e"
}

View file

@ -5,9 +5,9 @@ const ChainUtil = require('../chain-util');
class Wallet { class Wallet {
constructor(keyPair) { constructor(keyPair) {
this.balance = INITIAL_BALANCE;
this.keyPair = keyPair; this.keyPair = keyPair;
this.publicKey = this.keyPair.getPublic().encode('hex'); this.publicKey = this.keyPair.getPublic().encode('hex');
this.counter = 0;
} }
toString() { toString() {
@ -20,63 +20,72 @@ class Wallet {
return this.keyPair.sign(dataHash); return this.keyPair.sign(dataHash);
} }
createTransaction(recipient, amount, blockchain, transactionPool) { createTransaction(recipient, amount, blockchain) {
this.balance = this.calculateBalance(blockchain); const balance = blockchain.getBalanceCopy(this.publicKey);
if (amount > this.balance) { if (balance.counter > this.counter) {
console.log(`Amount: ${amount} exceceds current balance: ${this.balance}`); this.counter = balance.counter;
}
if (amount > balance.balance) {
console.log(`Amount: ${amount} exceceds current balance: ${balance.balance}`);
return null; 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) { createMetadata(SSNmetadata) {
return Metadata.newMetadata(this, SSNmetadata); return Metadata.newMetadata(this, SSNmetadata);
} }
calculateBalance(blockchain) { //calculateBalance(blockchain) {
let balance = this.balance; // let balance = this.balance;
let transactions = []; // let transactions = [];
blockchain.chain.forEach(block => block.data.forEach(transaction => { // blockchain.chain.forEach(block => block.data.forEach(transaction => {
transactions.push(transaction); // transactions.push(transaction);
})); // }));
console.log("transactions of balance") // console.log("transactions of balance")
console.log(transactions); // console.log(transactions);
const PaymentTransactions = transactions[0]; // const PaymentTransactions = transactions[0];
console.log("Payment transactions ") // console.log("Payment transactions ")
console.log(PaymentTransactions); // console.log(PaymentTransactions);
const walletInputTs = PaymentTransactions.filter(transaction => transaction.input.address === this.publicKey); // const walletInputTs = PaymentTransactions.filter(transaction => transaction.input.address === this.publicKey);
let startTime = 0; // let startTime = 0;
if (walletInputTs.length > 0) { // if (walletInputTs.length > 0) {
const recentInputT = walletInputTs.reduce( // const recentInputT = walletInputTs.reduce(
(prev, current) => prev.input.timestamp > current.input.timestamp ? prev : current // (prev, current) => prev.input.timestamp > current.input.timestamp ? prev : current
); // );
balance = recentInputT.outputs.find(output => output.address === this.publicKey).amount; // balance = recentInputT.outputs.find(output => output.address === this.publicKey).amount;
startTime = recentInputT.input.timestamp; // startTime = recentInputT.input.timestamp;
} // }
PaymentTransactions.forEach(transaction => { // PaymentTransactions.forEach(transaction => {
if (transaction.input.timestamp > startTime) { // if (transaction.input.timestamp > startTime) {
transaction.outputs.find(output => { // transaction.outputs.find(output => {
if (output.address === this.publicKey) { // if (output.address === this.publicKey) {
balance += output.amount; // balance += output.amount;
} // }
}); // });
} // }
}); // });
return balance; // return balance;
} //}
static blockchainWallet() { //static blockchainWallet() {
const blockchainWallet = new this(ChainUtil.genKeyPair()); // const blockchainWallet = new this(ChainUtil.genKeyPair());
blockchainWallet.address = 'blockchain-wallet'; // blockchainWallet.address = 'blockchain-wallet';
return blockchainWallet; // return blockchainWallet;
} //}
} }
module.exports = Wallet; module.exports = Wallet;

View file

@ -18,12 +18,14 @@ describe('Wallet', () => {
beforeEach(() => { beforeEach(() => {
sendAmount = 50; sendAmount = 50;
recipient = 'r4nd0m-4ddr355'; 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', () => { describe('and doing the same transaction', () => {
beforeEach(() => { 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', () => { it('doubles the `sendAmount` subtracted from the wallet balance', () => {
@ -46,7 +48,8 @@ describe('Wallet', () => {
addBalance = 100; addBalance = 100;
repeatAdd = 3; repeatAdd = 3;
for (let i=0; i<repeatAdd; i++) { for (let i=0; i<repeatAdd; i++) {
senderWallet.createTransaction(wallet.publicKey, addBalance, bc, tp); const transaction = senderWallet.createTransaction(wallet.publicKey, addBalance, bc);
tp.updateOrAddTransaction(transaction);
} }
bc.addBlock(tp.transactions); bc.addBlock(tp.transactions);
}); });
@ -66,14 +69,16 @@ describe('Wallet', () => {
tp.clear(); tp.clear();
subtractBalance = 60; subtractBalance = 60;
recipientBalance = wallet.calculateBalance(bc); 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); bc.addBlock(tp.transactions);
}); });
describe('and the sender sends another transaction to the recipient', () => { describe('and the sender sends another transaction to the recipient', () => {
beforeEach(() => { beforeEach(() => {
tp.clear(); tp.clear();
senderWallet.createTransaction(wallet.publicKey, addBalance, bc, tp); const transaction = senderWallet.createTransaction(wallet.publicKey, addBalance, bc);
tp.updateOrAddTransaction(transaction);
bc.addBlock(tp.transactions); bc.addBlock(tp.transactions);
}); });

View file

@ -63,9 +63,7 @@ class Metadata {
} }
static newMetadata(senderWallet,SSNmetadata){ static newMetadata(senderWallet,SSNmetadata){
return Metadata.MetadataOfIoTDevice(senderWallet, SSNmetadata return Metadata.MetadataOfIoTDevice(senderWallet, SSNmetadata);
);
} }
static signMetadata (metadata, senderWallet) { static signMetadata (metadata, senderWallet) {
@ -76,7 +74,6 @@ class Metadata {
} }
} }
static verifyMetadata(metadata) { static verifyMetadata(metadata) {
return ChainUtil.verifySignature( return ChainUtil.verifySignature(
metadata.Signiture.address, metadata.Signiture.address,

View file

@ -1,5 +1,6 @@
const Transaction = require('../wallet/transaction'); const Transaction = require('../wallet/transaction');
const Metadata = require('../wallet/metadata') const Metadata = require('../wallet/metadata');
const Block = require('../blockchain/block');
const Return = { const Return = {
add: 1, add: 1,
@ -10,16 +11,16 @@ const Return = {
class TransactionPool { class TransactionPool {
constructor() { constructor() {
this.transactions = []; this.transactions = [];
this.metadataS =[]; this.metadatas = [];
} }
//returns true on update, false on add //returns true on update, false on add
updateOrAddTransaction(transaction) { updateOrAddTransaction(transaction) {
if (!Transaction.verifyTransaction(transaction)) { if (!Transaction.verify(transaction)) {
console.log("Couldn't update or add transaction, transaction couldn't be verified"); console.log("Couldn't update or add transaction, transaction couldn't be verified");
return Return.error; 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) { if (foundIndex !== -1) {
this.transactions[foundIndex] = transaction; this.transactions[foundIndex] = transaction;
@ -36,13 +37,13 @@ class TransactionPool {
return Return.error; 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) { if (foundIndex !== -1) {
this.metadataS[foundIndex] = metadata; this.metadatas[foundIndex] = metadata;
return Return.update; return Return.update;
} else { } else {
this.metadataS.push(metadata); this.metadatas.push(metadata);
return Return.add; return Return.add;
} }
} }
@ -52,62 +53,41 @@ class TransactionPool {
} }
existingMetadata(address) { existingMetadata(address) {
return this.metadataS.find(t => t.Signiture.address === address); return this.metadatas.find(t => t.Signiture.address === address);
} }
validTransactions() { //we could check for possible double spends here
return this.transactions.filter(transaction => { validTransactionsCopy() {
const outputTotal = transaction.outputs.reduce((total, output) => { return [...this.transactions];
return total + output.amount;
}, 0);
if (transaction.input.amount !== outputTotal) {
console.log(`Invalid transaction from ${transaction.input.address}.`);
return;
} }
if (!Transaction.verifyTransaction(transaction)) { validMetadatasCopy(){
console.log(`Invalid signature from ${transaction.input.address}.`); return [...this.metadatas];
return;
}
return transaction;
});
}
validMetadataS(){
return this.metadataS.filter(metadata => {
if (!Metadata.verifyMetadata(metadata)) {
console.log(`Invalid signature from ${metadata.Signiture.address}.`);
return;
}
return metadata;
});
} }
clearFromBlock(block) { clearFromBlock(block) {
const transactions = block.data[0]; const blockTransactions = Block.getTransactions(block);
const metadatas = block.data[1]; const blockMetadatas = Block.getMetadatas(block);
for (const transaction of transactions) {
for (const transaction of blockTransactions) {
const foundTransaction = this.transactions.findIndex(t => t.id === transaction.id); const foundTransaction = this.transactions.findIndex(t => t.id === transaction.id);
if (foundTransaction !== -1) { if (foundTransaction !== -1) {
this.transactions.splice(foundTransaction, 1); this.transactions.splice(foundTransaction, 1);
} }
} }
for (const metadata of blockMetadatas) {
for (const metadata of metadatas) { const foundMetadata = this.metadatas.findIndex(m => m.id === metadata.id);
const foundMetadata = this.metadataS.findIndex(m => m.id === metadata.id);
if (foundMetadata !== -1) { if (foundMetadata !== -1) {
this.metadataS.splice(foundMetadata, 1); this.metadatas.splice(foundMetadata, 1);
} }
} }
} }
clearAll() { clearAll() {
this.transactions = []; this.transactions = [];
this.metadataS = []; this.metadatas = [];
} }
} }

View file

@ -2,66 +2,77 @@ const ChainUtil = require('../chain-util');
const { MINING_REWARD } = require('../config'); const { MINING_REWARD } = require('../config');
class Transaction { class Transaction {
constructor() { constructor(senderPublicKey, counter, outputs) {
this.id = ChainUtil.id(); this.input = senderPublicKey;
this.input = null; this.signature = null;
this.outputs = []; this.counter = counter;
this.outputs = outputs;
} }
update(senderWallet, recipient, amount) { addSignature(signature) {
const senderOutput = this.outputs.find(output => output.address === senderWallet.publicKey); if (!ChainUtil.verifySignature(
this.input,
if (amount > senderOutput.amount) { signature,
console.log(`Amount: ${amount} exceeds balance.`); Transaction.hashToSign(this))) {
return; 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;
} }
senderOutput.amount = senderOutput.amount - amount; static hashToSign(transaction) {
this.outputs.push({ amount, address: recipient }); return ChainUtil.hash({
Transaction.signTransaction(this, senderWallet); counter: transaction.counter,
outputs: transaction.outputs
return this; });
} }
static transactionWithOutputs(senderWallet, outputs) { static createOutput(recipient, amount) {
const transaction = new this(); return {
transaction.outputs.push(...outputs); publicKey: recipient,
Transaction.signTransaction(transaction, senderWallet); amount: amount
return transaction; };
} }
static newTransaction(senderWallet, recipient, amount) { //update(senderWallet, recipients) {
if (amount > senderWallet.balance) { // const senderOutput = this.outputs.find(output => output.address === senderWallet.publicKey);
console.log(`Amount: ${amount} exceeds balance.`);
return;
}
return Transaction.transactionWithOutputs(senderWallet, [ // if (amount > senderOutput.amount) {
{ amount: senderWallet.balance - amount, address: senderWallet.publicKey }, // console.log(`Amount: ${amount} exceeds balance.`);
{ amount, address: recipient } // return;
]); // }
}
static rewardTransaction(minerWallet, blockchainWallet) { // senderOutput.amount = senderOutput.amount - amount;
return Transaction.transactionWithOutputs(blockchainWallet, [{ // this.outputs.push({ amount, address: recipient });
amount: MINING_REWARD, address: minerWallet.publicKey // Transaction.signTransaction(this, senderWallet);
}]);
}
static signTransaction(transaction, senderWallet) { // return this;
transaction.input = { //}
timestamp: Date.now(), //static signTransaction(transaction, senderWallet) {
amount: senderWallet.balance, // transaction.input = {
address: senderWallet.publicKey, // timestamp: Date.now(),
signature: senderWallet.sign(ChainUtil.hash(transaction.outputs)) // 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;
} }
} }
static verifyTransaction(transaction) {
return ChainUtil.verifySignature( return ChainUtil.verifySignature(
transaction.input.address, transaction.input,
transaction.input.signature, transaction.signature,
ChainUtil.hash(transaction.outputs) Transaction.hashToSign(transaction)
); );
} }
} }