SenShaMart/network/blockchain-prop.js
2023-07-13 11:32:02 +10:00

441 lines
No EOL
11 KiB
JavaScript

const Websocket = require('ws');
const ChainUtil = require('../util/chain-util');
const Block = require('../blockchain/block');
const BrokerRegistration = require('../blockchain/broker-registration');
const SensorRegistration = require('../blockchain/sensor-registration');
const Integration = require('../blockchain/integration');
const Payment = require('../blockchain/payment');
const Compensation = require('../blockchain/compensation');
const Transaction = require('../blockchain/transaction');
const STATE_INIT = 0;
const STATE_CONNECTING = 1;
const STATE_WAITING = 2;
const STATE_READY = 3;
const PEER_OK = 0;
const PEER_DEAD = 1;
const chainValidation = {
start: ChainUtil.createValidateIsIntegerWithMin(1),
blocks: ChainUtil.createValidateArray(Block.verify)
};
const txsValidation = {
SensorRegistration: ChainUtil.createValidateOptional(
ChainUtil.createValidateArray(SensorRegistration.verify)),
BrokerRegistration: ChainUtil.createValidateOptional(
ChainUtil.createValidateArray(BrokerRegistration.verify)),
Integration: ChainUtil.createValidateOptional(
ChainUtil.createValidateArray(Integration.verify)),
Compensation: ChainUtil.createValidateOptional(
ChainUtil.createValidateArray(Compensation.verify)),
Payment: ChainUtil.createValidateOptional(
ChainUtil.createValidateArray(Payment.verify))
};
class Connection {
constructor(parent) {
this.parent = parent;
this.address = null;
this.socket = null;
this.state = STATE_INIT;
this.prev = null;
this.next = null;
this.differing = null;
this.lastBlockHash = "";
this.queue = null;
this.queueTimer = null;
this.sub = {
txs: false
};
this.logName = `${parent.logName}:${parent.connectionCounter}`;
parent.connectionCounter++;
}
accepted(socket) {
this.socket = socket;
this.socket.addEventListener("error", (err) => {
this.onError("Error event");
});
this.socket.addEventListener("message", (data) => {
this.onMessage(data);
});
this.onConnection(false);
}
connect(address) {
this.address = address;
this.state = STATE_CONNECTING;
this.reconnectWait = 1;
this.reconnect();
}
reconnect() {
console.log(`${this.logName} connecting`);
this.socket = new Websocket(this.address);
this.socket.addEventListener("error", (err) => {
this.onError("Error event");
});
this.socket.addEventListener("open", () => {
this.onConnection(true);
});
this.socket.addEventListener("message", (data) => {
this.onMessage(data);
});
}
retryDead(address) {
}
onError(message) {
console.log(this.logName + ": " + message);
switch (this.state) {
case STATE_CONNECTING:
//this.reconnectWait seconds + random [0,1000] ms
setTimeout(() => this.reconnect(),
1000 * this.reconnectWait + Math.floor(Math.random() * 1000));
this.reconnectWait *= 2;
if (this.reconnectWait > 64) {
this.reconnectWait = 64;
}
break;
default:
this.socket.close();
this.next.prev = this.prev;
this.prev.next = this.next;
this.next = null;
this.prev = null;
if (this.address !== null) {
this.state = STATE_CONNECTING;
this.reconnectWait = 1;
this.reconnect();
} else {
//do nothing?
}
break;
}
}
onConnection(weInitiated) {
console.log(`${this.logName} connected, initiated: ${weInitiated}`);
if (weInitiated) {
this.state = STATE_WAITING;
} else {
this.state = STATE_READY;
}
this.prev = this.parent.connected;
this.next = this.parent.connected.next;
this.next.prev = this;
this.prev.next = this;
this.queue = {
sub: {
txs: this.parent.txsCallback !== null
},
address: this.parent.myAddress
};
this.differing = 1;
this.lastBlockHash = "";
this.checkSend();
}
onQueueTimer() {
this.queueTimer = null;
this.checkSend();
}
handleChain(chain) {
const validationRes = ChainUtil.validateObject(chain, chainValidation);
if (!validationRes.result) {
this.onError("Couldn't validate chain message: " + validationRes.reason);
return false;
}
const ourChain = this.parent.blockchain.blocks();
if (chain.start > ourChain.length) {
this.onError("Recved start that's out of bounds of our current chain");
return false;
}
if (this.differing < chain.start) {
return true;
} else if (chain.start < this.differing) {
this.differing = chain.start;
}
if (chain.start + chain.blocks.length <= ourChain.length) {
for (let i = 0; i < chain.blocks.length; i++) {
const newBlock = chain.blocks[i];
const oldBlock = ourChain[chain.start + i];
if (newBlock.hash !== oldBlock.hash) {
this.differing = chain.start + i;
return true;
}
}
this.differing = chain.start + chain.blocks.length;
return true;
}
const newBlocks = ourChain.slice(0, chain.start).concat(chain.blocks);
this.parent.updatingConnection = this;
const replaceRes = this.parent.blockchain.replaceChain(newBlocks);
this.parent.updatingConnection = null;
if (replaceRes.result === true) {
this.differing = newBlocks.length;
}
return true;
}
handleTxs(txs) {
console.log("Recved msg with txs:");
const validationRes = ChainUtil.validateObject(txs, txsValidation);
if (!validationRes.result) {
this.onError("Couldn't validate txs message: " + validationRes.reason);
return false;
}
for (const type of Transaction.ALL_TYPES) {
const key = type.name();
if (key in txs) {
console.log(`${key} txs found`);
for (const tx of txs[key]) {
if (!this.parent.txsSeen.has(type.hashToSign(tx))) {
const newTx = new Transaction(tx, type);
this.parent.updatingConnection = this;
this.parent.sendTx(newTx);
this.parent.updatingConnection = null;
if (this.parent.txsCallback !== null) {
this.parent.txsCallback(newTx);
}
}
}
}
}
return true;
}
onMessage(event) {
if (this.state !== STATE_WAITING || this.socket.bufferedAmount !== 0) {
//how did we recv, if we haven't finished sending?
//our partner isn't waiting for a recv before sending, error
this.onError("Partner isn't following simplex protocol");
return;
}
this.state = STATE_READY;
let recved = null;
try {
recved = JSON.parse(event.data);
} catch (ex) {
this.onError("Bad message, not a json object: " + ex.message);
return;
}
if ("sub" in recved) {
if ("txs" in recved.sub) {
this.sub.txs = recved.sub.txs === true;
console.log(`${this.logName} set sub to txs to ${this.sub.txs}`);
}
}
if ("chain" in recved) {
if (!this.handleChain(recved.chain)) {
return;
}
}
if (this.parent.txsCallback !== null && "txs" in recved) {
if (!this.handleTxs(recved.txs)) {
return;
}
}
this.checkSend();
}
newChain(oldBlocks, blocks, difference) {
if (difference < this.differing) {
this.differing = difference;
}
this.checkSend();
}
sendTx(transaction) {
if (!this.sub.txs) {
return;
}
if (this.queue === null) {
this.queue = {};
}
if (!("txs" in this.queue)) {
this.queue.txs = {};
}
const key = transaction.type.name();
if (!(key in this.queue.txs)) {
this.queue.txs[key] = [];
}
this.queue.txs[key].push(transaction.transaction);
this.checkSend();
}
setTimer() {
if (this.queueTimer !== null) {
return;
} else {
this.queueTimer = setTimeout(() => {
this.onQueueTimer();
}, 1000);
}
}
checkSend() {
if (this.state !== STATE_READY) {
return;
}
const blocks = this.parent.blockchain.blocks();
const lastBlock = blocks[blocks.length - 1];
if ((this.differing < blocks.length && this.lastBlockHash !== lastBlock.hash) || this.queue !== null) {
if (this.queue === null) {
this.queue = {};
}
this.queue.chain = {
start: this.differing,
blocks: blocks.slice(this.differing)
};
this.differing = blocks.length;
this.lastBlockHash = lastBlock.hash;
}
if (this.queue === null) {
this.setTimer();
//set queue to force a send the next time we check (either naturally or due to timer)
this.queue = {};
return;
}
const sending = JSON.stringify(this.queue);
this.socket.send(sending);
this.state = STATE_WAITING;
this.queue = null;
}
}
function updateBlocksImpl(server, newBlocks, oldBlocks, difference) {
if (server.updatingConnection !== null) {
server.updatingConnection.blockIndex = newBlocks.length;
}
for (var connection = server.connected.next; connection !== server.connected; connection = connection.next) {
if (connection === server.updatingConnection) {
continue;
}
connection.newChain(oldBlocks, newBlocks, difference);
}
}
//this acts as a publisher, and subscriber
class PropServer {
constructor(logName, blockchain, txsCallback) {
this.logName = logName;
this.peerState = new Map();
this.connected = {
next: null,
prev: null
};
this.connected.next = this.connected;
this.connected.prev = this.connected;
this.blockchain = blockchain;
this.blockchain.addListener((newBlocks, oldBlocks, difference) => {
updateBlocksImpl(this, newBlocks, oldBlocks, difference);
});
this.txsSeen = new Set();
this.port = null;
this.myAddress = null;
this.server = null;
this.connectionCounter = 0;
if (typeof txsCallback === "undefined") {
this.txsCallback = null;
} else {
this.txsCallback = txsCallback;
}
this.updatingConnection = null;
}
start(port, myAddress, peers) {
if (this.port !== null) {
console.log(`Couldn't start BlockchainPub '${this.logName}', already started`);
return;
}
this.port = port;
this.myAddress = myAddress;
for (const peer of peers) {
if (!this.peerState.has(peer)) {
this.peerState.set(peer, PEER_OK);
const connection = new Connection(this);
connection.connect(peer);
}
}
this.server = new Websocket.Server({ port: port });
this.server.on('connection', socket => {
const connection = new Connection(this);
connection.accepted(socket);
});
}
sendTx(transaction) {
const hash = transaction.type.hashToSign(transaction.transaction);
if (this.txsSeen.has(hash)) {
return;
}
this.txsSeen.add(hash);
for (let connection = this.connected.next; connection !== this.connected; connection = connection.next) {
if (connection === this.updatingConnection) {
continue;
}
connection.sendTx(transaction);
}
}
}
module.exports = PropServer;