UNPKG

@activeledger/activeprotocol

Version:

Underlying protocol which handles consensus and the smart contract virtual machine of Activeledger

712 lines 32.3 kB
import * as fs from "fs"; import { EventEmitter } from "events"; import { VirtualMachine } from "./vm"; import { ActiveOptions } from "@activeledger/activeoptions"; import { ActiveLogger } from "@activeledger/activelogger"; import { Shared } from "./shared"; import { StreamUpdater } from "./streamUpdater"; import { PermissionsChecker } from "./permissionsChecker"; import path from "path"; const BROADCAST_TIMEOUT = 20 * 1000; export class Process extends EventEmitter { constructor(entry, selfHost, reference, right, db, dbe, dbev, secured) { super(); this.entry = entry; this.selfHost = selfHost; this.reference = reference; this.right = right; this.db = db; this.dbe = dbe; this.dbev = dbev; this.secured = secured; this.isDefault = false; this.checkRevs = true; this.commiting = false; this.voting = true; this.storeSingleError = false; this.errorOut = { code: 0, reason: "", priority: 0, }; this.shared = new Shared(this.storeSingleError, this.entry, this.dbe, this); this.nodeResponse = entry.$nodes[reference]; try { if (!Process.generalContractVM) { Process.generalContractVM = new VirtualMachine(this.selfHost, this.secured, this.db, this.dbev); Process.generalContractVM.initialiseVirtualMachine(); } } catch (error) { throw new Error(error); } if (!this.securityCache) { this.securityCache = ActiveOptions.get("security", null); } this.permissionChecker = new PermissionsChecker(this.entry, this.db, this.checkRevs, this.securityCache, this.shared); } destroy(umid) { clearTimeout(this.broadcastTimeout); if (this.isDefault) { if (Process.defaultContractsVM) Process.defaultContractsVM.destroy(umid); } else { this.contractRef ? Process.singleContractVMHolder[this.contractRef].destroy(umid) : Process.generalContractVM.destroy(umid); } delete this.entry; } sortVersions(a, b) { const padSorting = (v) => { return v .split(".") .map((p) => { return "00000000".substring(0, 8 - p.length) + p; }) .join("."); }; return padSorting(a).localeCompare(padSorting(b)); } async start(contractVersion) { ActiveLogger.debug("New TX : " + this.entry.$umid); const setupDefaultLocation = () => { this.isDefault = true; this.contractLocation = fs.realpathSync(`${process.cwd()}/default_contracts/${this.entry.$tx.$contract}.js`); }; const setupLocation = () => { var _a; let contract = this.entry.$tx.$contract; try { let contractId; let namespacePath; try { namespacePath = fs.realpathSync(`${process.cwd()}/contracts/${this.entry.$tx.$namespace}/`); const trueContractPath = fs.realpathSync(`${namespacePath}/${contract}.js`); contractId = path.basename(trueContractPath, path.extname(trueContractPath)); if (contractId.indexOf("@") > -1) { contractId = contractId.split("@")[0]; } } catch (_b) { throw new Error("Contract or Namespace not found"); } this.contractId = contractId; if (this.entry.$tx.$contract.indexOf("@") === -1) { if (contractVersion) { contract = contractVersion; } else { try { contract = ((_a = fs .readdirSync(namespacePath) .filter((fn) => fn.includes(`${this.contractId}@`)) .sort(this.sortVersions) .pop()) === null || _a === void 0 ? void 0 : _a.replace(".js", "")) || this.contractId; this.emit("contractLatestVersion", { contract: this.entry.$tx.$contract, file: contract, }); } catch (_c) { throw new Error(`${this.contractId}@latest not found`); } } } if (fs.existsSync(`${namespacePath}/_LOCK.${this.contractId}`)) { throw new Error("Contract Global Lock"); } if (fs.existsSync(`${namespacePath}/_LOCK.${contract}`)) { throw new Error(`Contract Version Lock ${contract.substring(contract.indexOf("@") + 1)}`); } this.contractLocation = fs.realpathSync(`${namespacePath}/${contract}.js`); } catch (e) { throw e; } }; try { this.entry.$tx.$namespace === "default" ? setupDefaultLocation() : setupLocation(); } catch (error) { this.entry.$nodes[this.reference].error = `Init Contract Error - ${error.message}`; this.emit("commited", { instant: true }); return; } const virtualMachine = this.isDefault ? Process.defaultContractsVM : this.contractRef ? Process.singleContractVMHolder[this.contractRef] : Process.generalContractVM; if (fs.existsSync(this.contractLocation)) { this.inputs = Object.keys(this.entry.$tx.$i || {}); if (this.inputs.length) { this.labelOrKey(); } this.outputs = Object.keys(this.entry.$tx.$o || {}); if (this.outputs.length) { this.labelOrKey(true); } if (!this.entry.$revs) { this.checkRevs = false; this.entry.$revs = { $i: {}, $o: {}, }; } if (!this.entry.$selfsign) { try { const inputStreams = await this.permissionChecker.process(this.inputs); const outputStreams = await this.permissionChecker.process(this.outputs, false); let contractData = undefined; if (!this.isDefault) { try { const contractDataStreams = await this.permissionChecker.process([`${this.contractId}:data`], false); if (contractDataStreams.length > 0) { contractData = contractDataStreams[0] .state; } } catch (e) { if (e.code === 1200) { throw e; } } } this.process(inputStreams, outputStreams, contractData); } catch (error) { this.postVote(virtualMachine, { code: error.code, reason: error.reason || error.message, }); } } else { ActiveLogger.debug("Self signed Transaction"); const inputs = Object.keys(this.entry.$tx.$i); if (inputs.length > 0) { let i = inputs.length; while (i--) { const signature = this.entry.$sigs[inputs[i]]; const input = this.entry.$tx.$i[inputs[i]]; if (!signature) { return this.shared.raiseLedgerError(1260, new Error("Self signed signature not found")); } const validSignature = () => this.shared.signatureCheck(input.publicKey, signature, input.type ? input.type : "rsa"); if (input) { if (input.publicKey) { if (!validSignature()) { return this.shared.raiseLedgerError(1250, new Error("Self signed signature not matching")); } } else { return this.shared.raiseLedgerError(1255, new Error("Self signed publicKey property not found in $i " + inputs[i])); } } } } try { const outputStreams = await this.permissionChecker.process(this.outputs, false); this.process([], outputStreams, undefined); } catch (error) { this.postVote(virtualMachine, { code: error.code, reason: error.reason || error.message, }); } } } else { this.postVote(virtualMachine, { code: 1401, reason: "Contract Not Found", }); } } updatedFromBroadcast(node) { if (this.isCommiting()) { return; } this.entry.$nodes = Object.assign(this.entry.$nodes, node); if (this.willEmit) { const nodes = Object.keys(this.entry.$nodes); if (!this.hasOutstandingVotes(nodes.length) && !this.canCommit()) { this.emitFailed(this.willEmitData); } else { for (let i = nodes.length; i--;) { if (this.entry.$nodes[nodes[i]].streams) { this.emitFailed(this.willEmitData); break; } } } } else { if (!this.isCommiting() && !this.voting) { this.nodeResponse = this.entry.$nodes[this.reference]; this.isDefault ? this.commit(Process.defaultContractsVM) : this.contractRef ? this.commit(Process.singleContractVMHolder[this.contractRef]) : this.commit(Process.generalContractVM); } } } isCommiting() { return this.commiting; } processDefaultContracts(payload, contractName) { if (!Process.defaultContractsVM) { try { Process.defaultContractsVM = new VirtualMachine(this.selfHost, this.secured, this.db, this.dbev); Process.defaultContractsVM.initialiseVirtualMachine(["fs", "path", "os", "crypto"], ["typescript"]); } catch (error) { throw new Error(error); } } this.handleVM(Process.defaultContractsVM, payload, contractName); } processUnsafeContracts(payload, namespace, contractName, extraBuiltins, extraExternals, extraMocks) { this.contractRef = namespace; if (!Process.singleContractVMHolder[this.contractRef]) { try { Process.singleContractVMHolder[this.contractRef] = new VirtualMachine(this.selfHost, this.secured, this.db, this.dbev); Process.singleContractVMHolder[this.contractRef].initialiseVirtualMachine(extraBuiltins, extraExternals, extraMocks); } catch (error) { throw new Error(error); } } this.handleVM(Process.singleContractVMHolder[this.contractRef], payload, contractName); } async handleVM(virtualMachine, payload, contractName) { const handleVoteError = async (error) => { this.nodeResponse.error = "Vote Failure - " + JSON.stringify(error); ActiveLogger.debug(this.nodeResponse.error, `Handle Vote Error ${payload.umid}`); ActiveLogger.debug(`Calling Contract Return Data - ${payload.umid}`); this.nodeResponse.return = virtualMachine.getReturnContractData(this.entry.$umid); this.postVote(virtualMachine); }; let continueProcessing = true; try { await virtualMachine.initialise(payload, contractName); } catch (error) { ActiveLogger.debug(error, "VM initialisation failed"); this.shared.raiseLedgerError(1401, new Error("VM Init Failure - " + JSON.stringify(error.message || error))); continueProcessing = false; } try { if (continueProcessing) ActiveLogger.debug(`Calling Contract Verify - ${payload.umid}`); await virtualMachine.verify(this.entry.$selfsign, payload.umid); } catch (error) { ActiveLogger.debug(error, "Verify Failure"); this.shared.raiseLedgerError(1310, error); continueProcessing = false; } if (this.entry.$tx.$i) { try { if (continueProcessing) ActiveLogger.debug(`Calling Contract Vote - ${payload.umid}`); const vote = await virtualMachine.vote(this.entry.$nodes, payload.umid); if (typeof (vote) !== 'boolean' && vote.leader) { this.nodeResponse.vote = this.nodeResponse.leader = true; this.commit(virtualMachine); return; } } catch (error) { handleVoteError(error); continueProcessing = false; } if (continueProcessing) { this.nodeResponse.vote = true; ActiveLogger.debug(`Calling Contract INC - ${payload.umid}`); this.nodeResponse.incomms = virtualMachine.getInternodeCommsFromVM(payload.umid); ActiveLogger.debug(`Calling Contract Return Data - ${payload.umid}`); this.nodeResponse.return = virtualMachine.getReturnContractData(payload.umid); this.entry = this.shared.clearAllComms(virtualMachine, this.nodeResponse.incomms); this.postVote(virtualMachine); } } else { try { ActiveLogger.debug(`Calling Contract Read - ${payload.umid}`); this.nodeResponse.return = await virtualMachine.read(payload.umid, this.entry.$tx.$entry || "read"); } catch (error) { this.nodeResponse.error = "Read Error - " + JSON.stringify(error); } this.nodeResponse.commit = true; this.emit("commited"); } } async process(inputs, outputs = [], contractData = undefined) { try { if (this.entry.$broadcast) { this.emit("broadcast", true); } const readonly = await this.getReadOnlyStreams(); const contractName = this.contractLocation.substring(this.contractLocation.lastIndexOf("/") + 1); const $sigs = { $sig: "", }; if (this.entry.$sigs) { const sigKeys = Object.keys(this.entry.$sigs); for (let i = sigKeys.length; i--;) { $sigs[this.shared.filterPrefix(sigKeys[i])] = this.entry.$sigs[sigKeys[i]]; } } const payload = { contractLocation: this.contractLocation, umid: this.entry.$umid, date: this.entry.$datetime, remoteAddress: this.entry.$remoteAddr, transaction: this.entry.$tx, signatures: $sigs, inputs, outputs, readonly, key: Math.floor(Math.random() * 100), contractData, }; if (!this.securityCache) { this.securityCache = ActiveOptions.get("security", null); } if (payload.transaction.$namespace === "default") { this.processDefaultContracts(payload, contractName); } else { if (this.securityCache && this.securityCache.namespace && this.securityCache.namespace[payload.transaction.$namespace]) { const builtin = []; const external = []; const mocks = []; const namespaceExtras = this.securityCache.namespace[payload.transaction.$namespace]; if (namespaceExtras) { if (namespaceExtras.std) { namespaceExtras.std.forEach((item) => { builtin.push(item); }); } if (namespaceExtras.external) { namespaceExtras.external.forEach((item) => { external.push(item); }); } if (namespaceExtras.mock) { namespaceExtras.mock.forEach((item) => { mocks.push(item); }); } } this.processUnsafeContracts(payload, payload.transaction.$namespace, contractName, builtin, external, mocks); } else { this.handleVM(Process.generalContractVM, payload, contractName); } } } catch (error) { this.shared.raiseLedgerError(1210, new Error("Read Only Stream Error")); throw error; } } postVote(virtualMachine, error = false) { this.voting = false; if (!this.entry) { ActiveLogger.debug(`postVote entry is missing?`); return; } if (!error && this.entry.$instant) { this.emit("commited", { instant: true }); this.entry.$instant = false; } if (this.entry.$broadcast) { if (error) { this.entry.$nodes[this.reference].error = error.reason; } this.emit("broadcast"); if (this.canCommit()) { this.commit(virtualMachine); } } else { if (this.right.reference != this.entry.$origin) { ActiveLogger.debug("Attempting commit with too early commit callback (to send right"); this.commit(virtualMachine, async () => { try { const response = await this.initRightKnock(); if (!this.nodeResponse.commit) { this.entry.$territoriality = response.data.$territoriality; this.entry.$nodes = response.data.$nodes; this.nodeResponse = this.entry.$nodes[this.reference]; if (error) { this.shared.raiseLedgerError(error.code, error.reason, true, 10); } ActiveLogger.debug("Sending Commit without callback"); this.commit(virtualMachine); } } catch (error) { ActiveLogger.debug(error, "Knock Failure"); ActiveOptions.get("debug", false) ? this.shared.raiseLedgerError(error.status || 1502, new Error(error.error || error)) : this.shared.raiseLedgerError(1501, new Error("Bad Knock Transaction")); } }); } else { ActiveLogger.debug("Origin is next (Sending Back)"); error ? this.shared.raiseLedgerError(error.code, error.reason) : this.commit(virtualMachine); } } } emitFailed(data) { if (this.willEmit) { clearTimeout(this.willEmit); this.emit("failed", this.willEmitData); } else { if (this.entry && this.entry.$broadcast && (this.hasOutstandingVotes() || this.canCommit())) { this.willEmitData = data; this.willEmit = setTimeout(() => { this.emit("failed", this.willEmitData); }, 10000); } else { this.emit("failed", data); } } } hasOutstandingVotes(nodes) { const neighbours = ActiveOptions.get("neighbourhood", []).length; return !!(neighbours - (nodes || Object.keys(this.entry.$nodes).length)); } async initRightKnock(retries = 0) { try { ActiveLogger.debug(`Sending -> ${this.right.reference} - ${this.entry.$umid}`); return await this.right.knock("init", this.entry); } catch (e) { if (retries <= 2) { await this.sleep(1000); ActiveLogger.debug(`Sending -> ${this.right.reference} - ${this.entry.$umid} attempt ${retries}`); return await this.initRightKnock(++retries); } else { throw new Error("3x Right Knock Error"); } } } sleep(time) { return new Promise((resolve) => setTimeout(resolve, time)); } canCommit() { let networkNodes = Object.keys(this.entry.$nodes); this.currentVotes = 0; for (let i = networkNodes.length; i--;) { if (this.entry.$nodes[networkNodes[i]].vote) this.currentVotes++; } const percent = this.entry.$unanimous ? 100 : ActiveOptions.get("consensus", {}).reached; return ((this.currentVotes / ActiveOptions.get("neighbourhood", []).length) * 100 >= percent || false); } async commit(virtualMachine, earlyCommit) { if (!this.nodeResponse.commit && !this.isCommiting()) { if (this.nodeResponse.vote && (this.nodeResponse.leader || this.canCommit())) { this.commiting = true; clearTimeout(this.broadcastTimeout); try { ActiveLogger.debug(`Calling Contract Commit - ${this.entry.$umid}`); await virtualMachine.commit(this.entry.$nodes, this.entry.$territoriality === this.reference, this.entry.$umid); this.nodeResponse.commit = true; ActiveLogger.debug(`Calling Contract INC X2 - ${this.entry.$umid}`); this.nodeResponse.incomms = virtualMachine.getInternodeCommsFromVM(this.entry.$umid); ActiveLogger.debug(`Calling Contract Return Data X2 - ${this.entry.$umid}`); this.nodeResponse.return = virtualMachine.getReturnContractData(this.entry.$umid); this.entry = this.shared.clearAllComms(virtualMachine, this.nodeResponse.incomms); let throws = virtualMachine.getThrowsFromVM(this.entry.$umid); if (throws && throws.length) { this.emit("throw", { locations: throws }); } const streamUpdater = new StreamUpdater(this.entry, virtualMachine, this.reference, this.nodeResponse, this.db, this.dbev, this, this.shared, this.contractId); earlyCommit ? streamUpdater.updateStreams(earlyCommit) : streamUpdater.updateStreams(); } catch (error) { if (earlyCommit) earlyCommit(); ActiveLogger.debug(error, "VM Commit Failure"); ActiveOptions.get("debug", false) ? this.shared.raiseLedgerError(1302, new Error("Commit Failure - " + JSON.stringify(error.message || error))) : this.shared.raiseLedgerError(1301, new Error("Failed Commit Transaction")); } } else { if (earlyCommit) return earlyCommit(); if (!this.nodeResponse.vote && !this.entry.$broadcast) { ActiveLogger.debug(this.nodeResponse, "VM Commit Failure, We voted NO (100)"); this.shared.raiseLedgerError(1505, new Error(this.nodeResponse.error), false); if (this.canCommit()) { ActiveLogger.debug("Network Consensus reached without me (Reconciling)"); try { ActiveLogger.debug(`Calling Contract Reconcile - ${this.entry.$umid}`); const reconciled = await virtualMachine.reconcile(this.entry.$nodes, this.entry.$umid); if (reconciled) { ActiveLogger.info("Self Reconcile Successful"); const streamUpdater = new StreamUpdater(this.entry, virtualMachine, this.reference, this.nodeResponse, this.db, this.dbev, this, this.shared, this.contractId); streamUpdater.updateStreams(); } else { if (this.errorOut.code === 1505) { ActiveLogger.info("Self Reconcile Failed & Upgrading Error Code for Auto Restore"); this.shared.storeSingleError = false; this.shared .storeError(1001, new Error(this.errorOut.reason), 11) .then(() => { this.emit("commited"); }) .catch(() => { this.emit("commited"); }); } else { this.emit("commited"); } } } catch (error) { ActiveLogger.debug(error); this.emit("commited"); } } } else { if (!this.entry.$broadcast) { ActiveLogger.debug("VM Commit Failure, NETWORK voted NO"); this.shared.raiseLedgerError(1510, new Error("Failed Network Voting Round - Non Broadcast")); } else { const neighbours = ActiveOptions.get("neighbourhood", []).length; const consensusNeeded = ActiveOptions.get("consensus", {}).reached; const outstandingVoters = neighbours - Object.keys(this.entry.$nodes).length; if (!outstandingVoters) { clearTimeout(this.broadcastTimeout); if (this.nodeResponse.error) { if (!this.storeSingleError) { ActiveLogger.debug(this.nodeResponse, "VM Commit Failure, We voted NO (200)"); this.storeSingleError = true; return this.shared.raiseLedgerError(1505, new Error(this.nodeResponse.error)); } } else { ActiveLogger.debug("VM Commit Failure, NETWORK voted NO"); return this.shared.raiseLedgerError(1510, new Error("Failed Network Voting Round - No More Voters")); } } else { clearTimeout(this.broadcastTimeout); if (((this.currentVotes + outstandingVoters) / neighbours) * 100 >= consensusNeeded) { this.broadcastTimeout = setTimeout(() => { ActiveLogger.debug("VM Commit Failure, NETWORK Timeout"); return this.shared.raiseLedgerError(1510, new Error("Failed Network Voting Timeout - Voters Timed Out")); }, BROADCAST_TIMEOUT); } else { if (this.nodeResponse.error) { if (!this.storeSingleError) { ActiveLogger.debug(this.nodeResponse, "VM Commit Failure, We voted NO (300)"); this.storeSingleError = true; return this.shared.raiseLedgerError(1505, new Error(this.nodeResponse.error)); } } else { ActiveLogger.debug("VM Commit Failure, NETWORK voted NO"); return this.shared.raiseLedgerError(1510, new Error("Failed Network Voting Round - No Quorum")); } } } } } } } else { if (earlyCommit) earlyCommit(); } } getReadOnlyStreams() { return new Promise(async (resolve, reject) => { const manageRevisions = (readOnly, reference) => { return new Promise(async (resolve, reject) => { try { const read = await this.db.get(readOnly[reference]); delete read._id; delete read._rev; readonlyStreams[reference] = read; resolve(read); } catch (error) { reject(error); } }); }; let readonlyStreams = {}; if (this.entry.$tx.$r) { let readOnly = this.entry.$tx.$r; let keyRefs = Object.keys(readOnly); const promiseCache = keyRefs.map((reference) => manageRevisions(readOnly, reference)); try { await Promise.all(promiseCache); resolve(readonlyStreams); } catch (error) { reject(error); } } else { resolve(readonlyStreams); } }); } labelOrKey(outputs = false) { const streams = outputs ? this.outputs : this.inputs; const txIO = outputs ? this.entry.$tx.$o : this.entry.$tx.$i; const map = outputs ? this.shared.ioLabelMap.o : this.shared.ioLabelMap.i; if (txIO[streams[0]].$stream) { for (let i = streams.length; i--;) { let streamId = txIO[streams[i]].$stream || streams[i]; map[streamId] = streams[i]; streams[i] = streamId; } } } } Process.singleContractVMHolder = {}; //# sourceMappingURL=process.js.map