608 lines
No EOL
17 KiB
JavaScript
608 lines
No EOL
17 KiB
JavaScript
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; |