SenShaMart/blockchain/blockchain.js

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;