@activeledger/activeprotocol
Version:
Underlying protocol which handles consensus and the smart contract virtual machine of Activeledger
712 lines • 32.3 kB
JavaScript
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} 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