reorg code, integration publish/subscribe working, still some TODOs
This commit is contained in:
parent
043a95d9ef
commit
1af6d56e2d
35 changed files with 3052 additions and 1401 deletions
|
@ -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;
|
|
@ -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', () => {
|
||||
|
|
608
blockchain/blockchain.js
Normal file
608
blockchain/blockchain.js
Normal file
|
@ -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;
|
55
blockchain/blockchain.test.js
Normal file
55
blockchain/blockchain.test.js
Normal file
|
@ -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);
|
||||
})
|
||||
});
|
189
blockchain/broker-registration.js
Normal file
189
blockchain/broker-registration.js
Normal file
|
@ -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;
|
|
@ -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;
|
|
@ -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);
|
||||
})
|
||||
});
|
81
blockchain/integration.js
Normal file
81
blockchain/integration.js
Normal file
|
@ -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;
|
78
blockchain/payment.js
Normal file
78
blockchain/payment.js
Normal file
|
@ -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;
|
79
blockchain/payment.test.js
Normal file
79
blockchain/payment.test.js
Normal file
|
@ -0,0 +1,79 @@
|
|||
const Transaction = require('./transaction');
|
||||
const Wallet = require('./index');
|
||||
const { MINING_REWARD } = require('../constants');
|
||||
|
||||
describe('Transaction', () => {
|
||||
let transaction, wallet, recipient, amount;
|
||||
|
||||
beforeEach(() => {
|
||||
wallet = new Wallet();
|
||||
amount = 50;
|
||||
recipient = 'r3c1p13nt';
|
||||
transaction = Transaction.newTransaction(wallet, recipient, amount);
|
||||
});
|
||||
|
||||
it('outputs the `amount` subtracted from the wallet balance', () => {
|
||||
expect(transaction.outputs.find(output => output.address === wallet.publicKey).amount)
|
||||
.toEqual(wallet.balance - amount);
|
||||
});
|
||||
|
||||
it('outputs the `amount` added to the recipient', () => {
|
||||
expect(transaction.outputs.find(output => output.address === recipient).amount)
|
||||
.toEqual(amount);
|
||||
});
|
||||
|
||||
it('inputs the balance of the wallet', () => {
|
||||
expect(transaction.input.amount).toEqual(wallet.balance);
|
||||
});
|
||||
|
||||
it('validates a valid transaction', () => {
|
||||
expect(Transaction.verifyTransaction(transaction)).toBe(true);
|
||||
});
|
||||
|
||||
it('invalidates a corrupt transaction', () => {
|
||||
transaction.outputs[0].amount = 50000;
|
||||
expect(Transaction.verifyTransaction(transaction)).toBe(false);
|
||||
});
|
||||
|
||||
describe('transacting with an amount that exceeds the balance', () => {
|
||||
beforeEach(() => {
|
||||
amount = 50000;
|
||||
transaction = Transaction.newTransaction(wallet, recipient, amount);
|
||||
});
|
||||
|
||||
it('does not create the transaction', () => {
|
||||
expect(transaction).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('and updating a transaction', () => {
|
||||
let nextAmount, nextRecipient;
|
||||
|
||||
beforeEach(() => {
|
||||
nextAmount = 20;
|
||||
nextRecipient = 'n3xt-4ddr355';
|
||||
transaction = transaction.update(wallet, nextRecipient, nextAmount);
|
||||
});
|
||||
|
||||
it(`subtracts the next amount from the sender's output`, () => {
|
||||
expect(transaction.outputs.find(output => output.address === wallet.publicKey).amount)
|
||||
.toEqual(wallet.balance - amount - nextAmount);
|
||||
});
|
||||
|
||||
it('outputs an amount for the next recipient', () => {
|
||||
expect(transaction.outputs.find(output => output.address === nextRecipient).amount)
|
||||
.toEqual(nextAmount);
|
||||
});
|
||||
});
|
||||
|
||||
describe('creating a reward transaction', () => {
|
||||
beforeEach(() => {
|
||||
transaction = Transaction.rewardTransaction(wallet, Wallet.blockchainWallet());
|
||||
});
|
||||
|
||||
it(`reward the miner's wallet`, () => {
|
||||
expect(transaction.outputs.find(output => output.address === wallet.publicKey).amount)
|
||||
.toEqual(MINING_REWARD);
|
||||
});
|
||||
});
|
||||
});
|
188
blockchain/sensor-registration.js
Normal file
188
blockchain/sensor-registration.js
Normal file
|
@ -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;
|
109
blockchain/sensor-registration.test.js
Normal file
109
blockchain/sensor-registration.test.js
Normal file
|
@ -0,0 +1,109 @@
|
|||
const Transaction = require('./transaction');
|
||||
const Metadata = require('./metadata');
|
||||
const Wallet = require('./index');
|
||||
const { MINING_REWARD } = require('../constants');
|
||||
|
||||
describe('Transaction & Metadata', () => {
|
||||
let transaction, metadata, wallet, recipient, amount,
|
||||
senderWallet,Name,Geo ,IP_URL , Topic_Token, Permission,
|
||||
RequestDetail, OrgOwner, DepOwner,PrsnOwner, PaymentPerKbyte,
|
||||
PaymentPerMinute, Protocol, MessageAttributes, Interval,
|
||||
FurtherDetails, SSNmetadata;
|
||||
|
||||
beforeEach(() => {
|
||||
wallet = new Wallet();
|
||||
amount = 50;
|
||||
recipient = 'r3c1p13nt';
|
||||
senderWallet = new Wallet();
|
||||
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'
|
||||
transaction = Transaction.newTransaction(wallet, recipient, amount);
|
||||
metadata = Metadata.newMetadata(senderWallet,Name,Geo ,IP_URL , Topic_Token, Permission,
|
||||
RequestDetail, OrgOwner, DepOwner,PrsnOwner, PaymentPerKbyte,
|
||||
PaymentPerMinute, Protocol, MessageAttributes, Interval,
|
||||
FurtherDetails, SSNmetadata)
|
||||
});
|
||||
|
||||
it('outputs the `amount` subtracted from the wallet balance', () => {
|
||||
expect(transaction.outputs.find(output => output.address === wallet.publicKey).amount)
|
||||
.toEqual(wallet.balance - amount);
|
||||
});
|
||||
|
||||
it('outputs the `amount` added to the recipient', () => {
|
||||
expect(transaction.outputs.find(output => output.address === recipient).amount)
|
||||
.toEqual(amount);
|
||||
});
|
||||
|
||||
it('inputs the balance of the wallet', () => {
|
||||
expect(transaction.input.amount).toEqual(wallet.balance);
|
||||
});
|
||||
|
||||
it('validates a valid transaction', () => {
|
||||
expect(Transaction.verifyTransaction(transaction)).toBe(true);
|
||||
});
|
||||
|
||||
it('validates a valid metadata', () => {
|
||||
expect(Metadata.verifyMetadata(metadata)).toBe(true);
|
||||
});
|
||||
|
||||
it('invalidates a corrupt transaction', () => {
|
||||
transaction.outputs[0].amount = 50000;
|
||||
expect(Transaction.verifyTransaction(transaction)).toBe(false);
|
||||
});
|
||||
|
||||
describe('transacting with an amount that exceeds the balance', () => {
|
||||
beforeEach(() => {
|
||||
amount = 50000;
|
||||
transaction = Transaction.newTransaction(wallet, recipient, amount);
|
||||
});
|
||||
|
||||
it('does not create the transaction', () => {
|
||||
expect(transaction).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('and updating a transaction', () => {
|
||||
let nextAmount, nextRecipient;
|
||||
|
||||
beforeEach(() => {
|
||||
nextAmount = 20;
|
||||
nextRecipient = 'n3xt-4ddr355';
|
||||
transaction = transaction.update(wallet, nextRecipient, nextAmount);
|
||||
});
|
||||
|
||||
it(`subtracts the next amount from the sender's output`, () => {
|
||||
expect(transaction.outputs.find(output => output.address === wallet.publicKey).amount)
|
||||
.toEqual(wallet.balance - amount - nextAmount);
|
||||
});
|
||||
|
||||
it('outputs an amount for the next recipient', () => {
|
||||
expect(transaction.outputs.find(output => output.address === nextRecipient).amount)
|
||||
.toEqual(nextAmount);
|
||||
});
|
||||
});
|
||||
|
||||
describe('creating a reward transaction', () => {
|
||||
beforeEach(() => {
|
||||
transaction = Transaction.rewardTransaction(wallet, Wallet.blockchainWallet());
|
||||
});
|
||||
|
||||
it(`reward the miner's wallet`, () => {
|
||||
expect(transaction.outputs.find(output => output.address === wallet.publicKey).amount)
|
||||
.toEqual(MINING_REWARD);
|
||||
});
|
||||
});
|
||||
});
|
18
blockchain/transaction.js
Normal file
18
blockchain/transaction.js
Normal file
|
@ -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;
|
Loading…
Add table
Add a link
Reference in a new issue