diff --git a/.eslintignore b/.eslintignore index eddb5e16e44bf44a8e8be5ffac3da5aed8f70591..43f35ac5216e0577bbde544a4dd37c5cc04667b9 100644 --- a/.eslintignore +++ b/.eslintignore @@ -11,5 +11,6 @@ app/lib/dal/sqliteDAL/index/*.js app/lib/dal/fileDALs/*.js app/lib/dal/fileDAL.js app/service/*.js +app/lib/rules/local_rules.js test/*.js test/**/*.js \ No newline at end of file diff --git a/.gitignore b/.gitignore index b0ce50ee330b11485ca8fa03fa767e722a70e2c5..21bbc63cf014d14df36e8b57d223f2e9411bfb05 100644 --- a/.gitignore +++ b/.gitignore @@ -47,6 +47,7 @@ app/lib/dal/sqliteDAL/*.js* app/lib/dal/sqliteDAL/index/*.js* app/lib/dal/fileDALs/*.js* app/lib/dal/fileDAL.js* +app/lib/rules/local_rules*.js* app/lib/logger*js* app/service/*.js* app/lib/wot.js* \ No newline at end of file diff --git a/app/lib/dto/TransactionDTO.ts b/app/lib/dto/TransactionDTO.ts index 70ea23cba0c23db4fe66cec75131ef486a507286..478935e5d70e24e5959f05adf8437090ff5d912b 100644 --- a/app/lib/dto/TransactionDTO.ts +++ b/app/lib/dto/TransactionDTO.ts @@ -1,6 +1,10 @@ import {hashf} from "../common" -export class InputDTO { +export interface BaseDTO { + base: number +} + +export class InputDTO implements BaseDTO { constructor( public amount: number, public base: number, @@ -11,7 +15,7 @@ export class InputDTO { ) {} } -export class OutputDTO { +export class OutputDTO implements BaseDTO { constructor( public amount: number, public base: number, diff --git a/app/lib/rules/local_rules.js b/app/lib/rules/local_rules.js index d60afac8fe53ec58427d38bbe7cd52883d59e4c7..3db085b94a4d3f4b44400dd4560d00c50ef16565 100644 --- a/app/lib/rules/local_rules.js +++ b/app/lib/rules/local_rules.js @@ -1,510 +1,475 @@ "use strict"; - -const co = require('co'); -const _ = require('underscore'); -const common = require('duniter-common'); -const indexer = require('../indexer').Indexer; -const BlockDTO = require('../dto/BlockDTO').BlockDTO - -const constants = common.constants -const hashf = common.hashf -const keyring = common.keyring -const rawer = common.rawer -const Block = common.document.Block -const Identity = common.document.Identity -const Membership = common.document.Membership -const Transaction = common.document.Transaction -const maxAcceleration = require('./helpers').maxAcceleration - -let rules = {}; - -// TODO: make a global variable 'index' instead of recomputing the index each time - -rules.FUNCTIONS = { - - checkParameters: (block) => co(function *() { - if (block.number == 0 && !block.parameters) { - throw Error('Parameters must be provided for root block'); - } - else if (block.number > 0 && block.parameters) { - throw Error('Parameters must not be provided for non-root block'); - } - return true; - }), - - checkProofOfWork: (block) => co(function *() { - let remainder = block.powMin % 16; - let nb_zeros = (block.powMin - remainder) / 16; - const powRegexp = new RegExp('^0{' + nb_zeros + '}'); - if (!block.hash.match(powRegexp)) { - throw Error('Not a proof-of-work'); - } - return true; - }), - - checkInnerHash: (block) => co(function *() { - let inner_hash = hashf(block.getRawInnerPart()).toUpperCase(); - if (block.inner_hash != inner_hash) { - throw Error('Wrong inner hash'); - } - return true; - }), - - checkPreviousHash: (block) => co(function *() { - if (block.number == 0 && block.previousHash) { - throw Error('PreviousHash must not be provided for root block'); - } - else if (block.number > 0 && !block.previousHash) { - throw Error('PreviousHash must be provided for non-root block'); - } - return true; - }), - - checkPreviousIssuer: (block) => co(function *() { - if (block.number == 0 && block.previousIssuer) - throw Error('PreviousIssuer must not be provided for root block'); - else if (block.number > 0 && !block.previousIssuer) - throw Error('PreviousIssuer must be provided for non-root block'); - return true; - }), - - checkUnitBase: (block) => co(function *() { - if (block.number == 0 && block.unitbase != 0) { - throw Error('UnitBase must equal 0 for root block'); - } - return true; - }), - - checkBlockSignature: (block) => co(function *() { - if (!keyring.verify(block.getSignedPart(), block.signature, block.issuer)) - throw Error('Block\'s signature must match'); - return true; - }), - - checkBlockTimes: (block, conf) => co(function *() { - const time = parseInt(block.time); - const medianTime = parseInt(block.medianTime); - if (block.number > 0 && (time < medianTime || time > medianTime + maxAcceleration(conf))) - throw Error('A block must have its Time between MedianTime and MedianTime + ' + maxAcceleration(conf)); - else if (block.number == 0 && time != medianTime) - throw Error('Root block must have Time equal MedianTime'); - return true; - }), - - checkIdentitiesSignature: (block) => co(function *() { - let i = 0; - let wrongSig = false; - while (!wrongSig && i < block.identities.length) { - const idty = Identity.fromInline(block.identities[i]); - idty.currency = block.currency; - wrongSig = !keyring.verify(idty.rawWithoutSig(), idty.sig, idty.pubkey); - if (wrongSig) { - throw Error('Identity\'s signature must match'); - } - i++; - } - return true; - }), - - checkIdentitiesUserIDConflict: (block, conf, index) => co(function *() { - const creates = indexer.iindexCreate(index); - const uids = _.chain(creates).pluck('uid').uniq().value(); - if (creates.length !== uids.length) { - throw Error('Block must not contain twice same identity uid'); - } - return true; - }), - - checkIdentitiesPubkeyConflict: (block, conf, index) => co(function *() { - const creates = indexer.iindexCreate(index); - const pubkeys = _.chain(creates).pluck('pub').uniq().value(); - if (creates.length !== pubkeys.length) { - throw Error('Block must not contain twice same identity pubkey'); - } - return true; - }), - - checkIdentitiesMatchJoin: (block, conf, index) => co(function *() { - const icreates = indexer.iindexCreate(index); - const mcreates = indexer.mindexCreate(index); - for (const icreate of icreates) { - const matching = _(mcreates).filter({ pub: icreate.pub }); - if (matching.length == 0) { - throw Error('Each identity must match a newcomer line with same userid and certts'); - } - } - return true; - }), - - checkRevokedAreExcluded: (block, conf, index) => co(function *() { - const iindex = indexer.iindex(index); - const mindex = indexer.mindex(index); - const revocations = _.chain(mindex) - .filter((row) => row.op == constants.IDX_UPDATE && row.revoked_on !== null) - .pluck('pub') - .value(); - for (const pub of revocations) { - const exclusions = _(iindex).where({ op: constants.IDX_UPDATE, member: false, pub }); - if (exclusions.length == 0) { - throw Error('A revoked member must be excluded'); - } - } - return true; - }), - - checkRevokedUnicity: (block, conf, index) => co(function *() { - try { - yield rules.FUNCTIONS.checkMembershipUnicity(block, conf, index); - } catch (e) { - throw Error('A single revocation per member is allowed'); - } - return true; - }), - - checkMembershipUnicity: (block, conf, index) => co(function *() { - const mindex = indexer.mindex(index); - const pubkeys = _.chain(mindex).pluck('pub').uniq().value(); - if (pubkeys.length !== mindex.length) { - throw Error('Unicity constraint PUBLIC_KEY on MINDEX is not respected'); - } - return true; - }), - - checkMembershipsSignature: (block) => co(function *() { - let i = 0; - let wrongSig = false, ms; - // Joiners - while (!wrongSig && i < block.joiners.length) { - ms = Membership.fromInline(block.joiners[i], 'IN', block.currency); - wrongSig = !checkSingleMembershipSignature(ms); - i++; - } - // Actives - i = 0; - while (!wrongSig && i < block.actives.length) { - ms = Membership.fromInline(block.actives[i], 'IN', block.currency); - wrongSig = !checkSingleMembershipSignature(ms); - i++; - } - // Leavers - i = 0; - while (!wrongSig && i < block.leavers.length) { - ms = Membership.fromInline(block.leavers[i], 'OUT', block.currency); - wrongSig = !checkSingleMembershipSignature(ms); - i++; - } - if (wrongSig) { - throw Error('Membership\'s signature must match'); - } - return true; - }), - - checkPubkeyUnicity: (block) => co(function *() { - const pubkeys = []; - let conflict = false; - let pubk; - // Joiners - let i = 0; - while (!conflict && i < block.joiners.length) { - pubk = block.joiners[i].split(':')[0]; - conflict = ~pubkeys.indexOf(pubk); - pubkeys.push(pubk); - i++; - } - // Actives - i = 0; - while (!conflict && i < block.actives.length) { - pubk = block.actives[i].split(':')[0]; - conflict = ~pubkeys.indexOf(pubk); - pubkeys.push(pubk); - i++; - } - // Leavers - i = 0; - while (!conflict && i < block.leavers.length) { - pubk = block.leavers[i].split(':')[0]; - conflict = ~pubkeys.indexOf(pubk); - pubkeys.push(pubk); - i++; - } - // Excluded - i = 0; - while (!conflict && i < block.excluded.length) { - pubk = block.excluded[i].split(':')[0]; - conflict = ~pubkeys.indexOf(pubk); - pubkeys.push(pubk); - i++; - } - if (conflict) { - throw Error('Block cannot contain a same pubkey more than once in joiners, actives, leavers and excluded'); - } - return true; - }), - - checkCertificationOneByIssuer: (block, conf, index) => co(function *() { - if (block.number > 0) { - const cindex = indexer.cindex(index); - const certFromA = _.uniq(cindex.map((row) => row.issuer)); - if (certFromA.length !== cindex.length) { - throw Error('Block cannot contain two certifications from same issuer'); - } - } - return true; - }), - - checkCertificationUnicity: (block, conf, index) => co(function *() { - const cindex = indexer.cindex(index); - const certAtoB = _.uniq(cindex.map((row) => row.issuer + row.receiver)); - if (certAtoB.length !== cindex.length) { - throw Error('Block cannot contain identical certifications (A -> B)'); - } - return true; - }), - - checkCertificationIsntForLeaverOrExcluded: (block, conf, index) => co(function *() { - const cindex = indexer.cindex(index); - const iindex = indexer.iindex(index); - const mindex = indexer.mindex(index); - const certified = cindex.map((row) => row.receiver); - for (const pub of certified) { - const exclusions = _(iindex).where({ op: constants.IDX_UPDATE, member: false, pub: pub }); - const leavers = _(mindex).where({ op: constants.IDX_UPDATE, leaving: true, pub: pub }); - if (exclusions.length > 0 || leavers.length > 0) { - throw Error('Block cannot contain certifications concerning leavers or excluded members'); - } - } - return true; - }), - - checkTxVersion: (block) => co(function *() { - const txs = block.transactions - // Check rule against each transaction - for (const tx of txs) { - if (tx.version != 10) { - throw Error('A transaction must have the version 10'); - } - } - return true; - }), - - checkTxLen: (block) => co(function *() { - const txs = block.transactions - // Check rule against each transaction - for (const tx of txs) { - const txLen = Transaction.getLen(tx); - if (txLen > constants.MAXIMUM_LEN_OF_COMPACT_TX) { - throw constants.ERRORS.A_TRANSACTION_HAS_A_MAX_SIZE; - } - } - // Check rule against each output of each transaction - for (const tx of txs) { - for (const output of tx.outputs) { - const out = typeof output === 'string' ? output : Transaction.outputObj2Str(output) - if (out.length > constants.MAXIMUM_LEN_OF_OUTPUT) { - throw constants.ERRORS.MAXIMUM_LEN_OF_OUTPUT - } - } - } - // Check rule against each unlock of each transaction - for (const tx of txs) { - for (const unlock of tx.unlocks) { - if (unlock.length > constants.MAXIMUM_LEN_OF_UNLOCK) { - throw constants.ERRORS.MAXIMUM_LEN_OF_UNLOCK +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const BlockDTO_1 = require("../dto/BlockDTO"); +const _ = require('underscore'); +const common = require('duniter-common'); +const indexer = require('../indexer').Indexer; +const constants = common.constants; +const hashf = common.hashf; +const keyring = common.keyring; +const Block = common.document.Block; +const Identity = common.document.Identity; +const Membership = common.document.Membership; +const Transaction = common.document.Transaction; +const maxAcceleration = require('./helpers').maxAcceleration; +exports.FUNCTIONS = { + checkParameters: (block) => __awaiter(this, void 0, void 0, function* () { + if (block.number == 0 && !block.parameters) { + throw Error('Parameters must be provided for root block'); } - } - } - return true; - }), - - checkTxIssuers: (block) => co(function *() { - const txs = block.transactions - // Check rule against each transaction - for (const tx of txs) { - if (tx.issuers.length == 0) { - throw Error('A transaction must have at least 1 issuer'); - } - } - return true; - }), - - checkTxSources: (block) => co(function *() { - const dto = BlockDTO.fromJSONObject(block) - for (const tx of dto.transactions) { - if (!tx.inputs || tx.inputs.length == 0) { - throw Error('A transaction must have at least 1 source'); - } - } - const sindex = indexer.localSIndex(dto); - const inputs = _.filter(sindex, (row) => row.op == constants.IDX_UPDATE).map((row) => [row.op, row.identifier, row.pos].join('-')); - if (inputs.length !== _.uniq(inputs).length) { - throw Error('It cannot exist 2 identical sources for transactions inside a given block'); - } - const outputs = _.filter(sindex, (row) => row.op == constants.IDX_CREATE).map((row) => [row.op, row.identifier, row.pos].join('-')); - if (outputs.length !== _.uniq(outputs).length) { - throw Error('It cannot exist 2 identical sources for transactions inside a given block'); - } - return true; - }), - - checkTxAmounts: (block) => co(function *() { - for (const tx of block.transactions) { - rules.HELPERS.checkTxAmountsValidity(tx); - } - }), - - checkTxRecipients: (block) => co(function *() { - const txs = block.transactions - // Check rule against each transaction - for (const tx of txs) { - if (!tx.outputs || tx.outputs.length == 0) { - throw Error('A transaction must have at least 1 recipient'); - } - else { - // Cannot have empty output condition - for (const output of tx.outputsAsObjects()) { - if (!output.conditions.match(/(SIG|XHX)/)) { - throw Error('Empty conditions are forbidden'); - } - } - } - } - return true; - }), - - checkTxSignature: (block) => co(function *() { - const txs = block.transactions - // Check rule against each transaction - for (const tx of txs) { - let sigResult = getSigResult(tx); - if (!sigResult.matching) { - throw Error('Signature from a transaction must match'); - } - } - return true; - }) + else if (block.number > 0 && block.parameters) { + throw Error('Parameters must not be provided for non-root block'); + } + return true; + }), + checkProofOfWork: (block) => __awaiter(this, void 0, void 0, function* () { + let remainder = block.powMin % 16; + let nb_zeros = (block.powMin - remainder) / 16; + const powRegexp = new RegExp('^0{' + nb_zeros + '}'); + if (!block.hash.match(powRegexp)) { + throw Error('Not a proof-of-work'); + } + return true; + }), + checkInnerHash: (block) => __awaiter(this, void 0, void 0, function* () { + let inner_hash = hashf(block.getRawInnerPart()).toUpperCase(); + if (block.inner_hash != inner_hash) { + throw Error('Wrong inner hash'); + } + return true; + }), + checkPreviousHash: (block) => __awaiter(this, void 0, void 0, function* () { + if (block.number == 0 && block.previousHash) { + throw Error('PreviousHash must not be provided for root block'); + } + else if (block.number > 0 && !block.previousHash) { + throw Error('PreviousHash must be provided for non-root block'); + } + return true; + }), + checkPreviousIssuer: (block) => __awaiter(this, void 0, void 0, function* () { + if (block.number == 0 && block.previousIssuer) + throw Error('PreviousIssuer must not be provided for root block'); + else if (block.number > 0 && !block.previousIssuer) + throw Error('PreviousIssuer must be provided for non-root block'); + return true; + }), + checkUnitBase: (block) => __awaiter(this, void 0, void 0, function* () { + if (block.number == 0 && block.unitbase != 0) { + throw Error('UnitBase must equal 0 for root block'); + } + return true; + }), + checkBlockSignature: (block) => __awaiter(this, void 0, void 0, function* () { + if (!keyring.verify(block.getSignedPart(), block.signature, block.issuer)) + throw Error('Block\'s signature must match'); + return true; + }), + checkBlockTimes: (block, conf) => __awaiter(this, void 0, void 0, function* () { + const time = block.time; + const medianTime = block.medianTime; + if (block.number > 0 && (time < medianTime || time > medianTime + maxAcceleration(conf))) + throw Error('A block must have its Time between MedianTime and MedianTime + ' + maxAcceleration(conf)); + else if (block.number == 0 && time != medianTime) + throw Error('Root block must have Time equal MedianTime'); + return true; + }), + checkIdentitiesSignature: (block) => __awaiter(this, void 0, void 0, function* () { + let i = 0; + let wrongSig = false; + while (!wrongSig && i < block.identities.length) { + const idty = Identity.fromInline(block.identities[i]); + idty.currency = block.currency; + wrongSig = !keyring.verify(idty.rawWithoutSig(), idty.sig, idty.pubkey); + if (wrongSig) { + throw Error('Identity\'s signature must match'); + } + i++; + } + return true; + }), + checkIdentitiesUserIDConflict: (block, conf, index) => __awaiter(this, void 0, void 0, function* () { + const creates = indexer.iindexCreate(index); + const uids = _.chain(creates).pluck('uid').uniq().value(); + if (creates.length !== uids.length) { + throw Error('Block must not contain twice same identity uid'); + } + return true; + }), + checkIdentitiesPubkeyConflict: (block, conf, index) => __awaiter(this, void 0, void 0, function* () { + const creates = indexer.iindexCreate(index); + const pubkeys = _.chain(creates).pluck('pub').uniq().value(); + if (creates.length !== pubkeys.length) { + throw Error('Block must not contain twice same identity pubkey'); + } + return true; + }), + checkIdentitiesMatchJoin: (block, conf, index) => __awaiter(this, void 0, void 0, function* () { + const icreates = indexer.iindexCreate(index); + const mcreates = indexer.mindexCreate(index); + for (const icreate of icreates) { + const matching = _(mcreates).filter({ pub: icreate.pub }); + if (matching.length == 0) { + throw Error('Each identity must match a newcomer line with same userid and certts'); + } + } + return true; + }), + checkRevokedAreExcluded: (block, conf, index) => __awaiter(this, void 0, void 0, function* () { + const iindex = indexer.iindex(index); + const mindex = indexer.mindex(index); + const revocations = _.chain(mindex) + .filter((row) => row.op == constants.IDX_UPDATE && row.revoked_on !== null) + .pluck('pub') + .value(); + for (const pub of revocations) { + const exclusions = _(iindex).where({ op: constants.IDX_UPDATE, member: false, pub }); + if (exclusions.length == 0) { + throw Error('A revoked member must be excluded'); + } + } + return true; + }), + checkRevokedUnicity: (block, conf, index) => __awaiter(this, void 0, void 0, function* () { + try { + yield exports.FUNCTIONS.checkMembershipUnicity(block, conf, index); + } + catch (e) { + throw Error('A single revocation per member is allowed'); + } + return true; + }), + checkMembershipUnicity: (block, conf, index) => __awaiter(this, void 0, void 0, function* () { + const mindex = indexer.mindex(index); + const pubkeys = _.chain(mindex).pluck('pub').uniq().value(); + if (pubkeys.length !== mindex.length) { + throw Error('Unicity constraint PUBLIC_KEY on MINDEX is not respected'); + } + return true; + }), + checkMembershipsSignature: (block) => __awaiter(this, void 0, void 0, function* () { + let i = 0; + let wrongSig = false, ms; + // Joiners + while (!wrongSig && i < block.joiners.length) { + ms = Membership.fromInline(block.joiners[i], 'IN', block.currency); + wrongSig = !checkSingleMembershipSignature(ms); + i++; + } + // Actives + i = 0; + while (!wrongSig && i < block.actives.length) { + ms = Membership.fromInline(block.actives[i], 'IN', block.currency); + wrongSig = !checkSingleMembershipSignature(ms); + i++; + } + // Leavers + i = 0; + while (!wrongSig && i < block.leavers.length) { + ms = Membership.fromInline(block.leavers[i], 'OUT', block.currency); + wrongSig = !checkSingleMembershipSignature(ms); + i++; + } + if (wrongSig) { + throw Error('Membership\'s signature must match'); + } + return true; + }), + checkPubkeyUnicity: (block) => __awaiter(this, void 0, void 0, function* () { + const pubkeys = []; + let conflict = false; + let pubk; + // Joiners + let i = 0; + while (!conflict && i < block.joiners.length) { + pubk = block.joiners[i].split(':')[0]; + conflict = !!(~pubkeys.indexOf(pubk)); + pubkeys.push(pubk); + i++; + } + // Actives + i = 0; + while (!conflict && i < block.actives.length) { + pubk = block.actives[i].split(':')[0]; + conflict = !!(~pubkeys.indexOf(pubk)); + pubkeys.push(pubk); + i++; + } + // Leavers + i = 0; + while (!conflict && i < block.leavers.length) { + pubk = block.leavers[i].split(':')[0]; + conflict = !!(~pubkeys.indexOf(pubk)); + pubkeys.push(pubk); + i++; + } + // Excluded + i = 0; + while (!conflict && i < block.excluded.length) { + pubk = block.excluded[i].split(':')[0]; + conflict = !!(~pubkeys.indexOf(pubk)); + pubkeys.push(pubk); + i++; + } + if (conflict) { + throw Error('Block cannot contain a same pubkey more than once in joiners, actives, leavers and excluded'); + } + return true; + }), + checkCertificationOneByIssuer: (block, conf, index) => __awaiter(this, void 0, void 0, function* () { + if (block.number > 0) { + const cindex = indexer.cindex(index); + const certFromA = _.uniq(cindex.map((row) => row.issuer)); + if (certFromA.length !== cindex.length) { + throw Error('Block cannot contain two certifications from same issuer'); + } + } + return true; + }), + checkCertificationUnicity: (block, conf, index) => __awaiter(this, void 0, void 0, function* () { + const cindex = indexer.cindex(index); + const certAtoB = _.uniq(cindex.map((row) => row.issuer + row.receiver)); + if (certAtoB.length !== cindex.length) { + throw Error('Block cannot contain identical certifications (A -> B)'); + } + return true; + }), + checkCertificationIsntForLeaverOrExcluded: (block, conf, index) => __awaiter(this, void 0, void 0, function* () { + const cindex = indexer.cindex(index); + const iindex = indexer.iindex(index); + const mindex = indexer.mindex(index); + const certified = cindex.map((row) => row.receiver); + for (const pub of certified) { + const exclusions = _(iindex).where({ op: constants.IDX_UPDATE, member: false, pub: pub }); + const leavers = _(mindex).where({ op: constants.IDX_UPDATE, leaving: true, pub: pub }); + if (exclusions.length > 0 || leavers.length > 0) { + throw Error('Block cannot contain certifications concerning leavers or excluded members'); + } + } + return true; + }), + checkTxVersion: (block) => __awaiter(this, void 0, void 0, function* () { + const txs = block.transactions; + // Check rule against each transaction + for (const tx of txs) { + if (tx.version != 10) { + throw Error('A transaction must have the version 10'); + } + } + return true; + }), + checkTxLen: (block) => __awaiter(this, void 0, void 0, function* () { + const txs = block.transactions; + // Check rule against each transaction + for (const tx of txs) { + const txLen = Transaction.getLen(tx); + if (txLen > constants.MAXIMUM_LEN_OF_COMPACT_TX) { + throw constants.ERRORS.A_TRANSACTION_HAS_A_MAX_SIZE; + } + } + // Check rule against each output of each transaction + for (const tx of txs) { + for (const output of tx.outputs) { + const out = typeof output === 'string' ? output : Transaction.outputObj2Str(output); + if (out.length > constants.MAXIMUM_LEN_OF_OUTPUT) { + throw constants.ERRORS.MAXIMUM_LEN_OF_OUTPUT; + } + } + } + // Check rule against each unlock of each transaction + for (const tx of txs) { + for (const unlock of tx.unlocks) { + if (unlock.length > constants.MAXIMUM_LEN_OF_UNLOCK) { + throw constants.ERRORS.MAXIMUM_LEN_OF_UNLOCK; + } + } + } + return true; + }), + checkTxIssuers: (block) => __awaiter(this, void 0, void 0, function* () { + const txs = block.transactions; + // Check rule against each transaction + for (const tx of txs) { + if (tx.issuers.length == 0) { + throw Error('A transaction must have at least 1 issuer'); + } + } + return true; + }), + checkTxSources: (block) => __awaiter(this, void 0, void 0, function* () { + const dto = BlockDTO_1.BlockDTO.fromJSONObject(block); + for (const tx of dto.transactions) { + if (!tx.inputs || tx.inputs.length == 0) { + throw Error('A transaction must have at least 1 source'); + } + } + const sindex = indexer.localSIndex(dto); + const inputs = _.filter(sindex, (row) => row.op == constants.IDX_UPDATE).map((row) => [row.op, row.identifier, row.pos].join('-')); + if (inputs.length !== _.uniq(inputs).length) { + throw Error('It cannot exist 2 identical sources for transactions inside a given block'); + } + const outputs = _.filter(sindex, (row) => row.op == constants.IDX_CREATE).map((row) => [row.op, row.identifier, row.pos].join('-')); + if (outputs.length !== _.uniq(outputs).length) { + throw Error('It cannot exist 2 identical sources for transactions inside a given block'); + } + return true; + }), + checkTxAmounts: (block) => __awaiter(this, void 0, void 0, function* () { + for (const tx of block.transactions) { + exports.HELPERS.checkTxAmountsValidity(tx); + } + }), + checkTxRecipients: (block) => __awaiter(this, void 0, void 0, function* () { + const txs = block.transactions; + // Check rule against each transaction + for (const tx of txs) { + if (!tx.outputs || tx.outputs.length == 0) { + throw Error('A transaction must have at least 1 recipient'); + } + else { + // Cannot have empty output condition + for (const output of tx.outputsAsObjects()) { + if (!output.conditions.match(/(SIG|XHX)/)) { + throw Error('Empty conditions are forbidden'); + } + } + } + } + return true; + }), + checkTxSignature: (block) => __awaiter(this, void 0, void 0, function* () { + const txs = block.transactions; + // Check rule against each transaction + for (const tx of txs) { + let sigResult = getSigResult(tx); + if (!sigResult.matching) { + throw Error('Signature from a transaction must match'); + } + } + return true; + }) }; - function checkSingleMembershipSignature(ms) { - return keyring.verify(ms.getRaw(), ms.signature, ms.issuer); + return keyring.verify(ms.getRaw(), ms.signature, ms.issuer); } - function getSigResult(tx) { - let sigResult = { sigs: {}, matching: true }; - let json = { "version": tx.version, "currency": tx.currency, "blockstamp": tx.blockstamp, "locktime": tx.locktime, "inputs": [], "outputs": [], "issuers": tx.issuers, "signatures": [], "comment": tx.comment }; - tx.inputs.forEach(function (input) { - json.inputs.push(input.raw); - }); - tx.outputs.forEach(function (output) { - json.outputs.push(output.raw); - }); - json.unlocks = tx.unlocks; - let i = 0; - let signaturesMatching = true; - const raw = tx.getRawTxNoSig() - while (signaturesMatching && i < tx.signatures.length) { - const sig = tx.signatures[i]; - const pub = tx.issuers[i]; - signaturesMatching = keyring.verify(raw, sig, pub); - sigResult.sigs[pub] = { - matching: signaturesMatching, - index: i - }; - i++; - } - sigResult.matching = signaturesMatching; - return sigResult; + let sigResult = { sigs: {}, matching: true }; + let json = { "version": tx.version, "currency": tx.currency, "blockstamp": tx.blockstamp, "locktime": tx.locktime, "inputs": [], "outputs": [], "issuers": tx.issuers, "signatures": [], "comment": tx.comment, unlocks: [] }; + tx.inputs.forEach(function (input) { + json.inputs.push(input.raw); + }); + tx.outputs.forEach(function (output) { + json.outputs.push(output.raw); + }); + json.unlocks = tx.unlocks; + let i = 0; + let signaturesMatching = true; + const raw = tx.getRawTxNoSig(); + while (signaturesMatching && i < tx.signatures.length) { + const sig = tx.signatures[i]; + const pub = tx.issuers[i]; + signaturesMatching = keyring.verify(raw, sig, pub); + sigResult.sigs[pub] = { + matching: signaturesMatching, + index: i + }; + i++; + } + sigResult.matching = signaturesMatching; + return sigResult; } - -function checkBunchOfTransactions(transactions, done){ - const block = { transactions }; - return co(function *() { - try { - let local_rule = rules.FUNCTIONS; - yield local_rule.checkTxLen(block); - yield local_rule.checkTxIssuers(block); - yield local_rule.checkTxSources(block); - yield local_rule.checkTxRecipients(block); - yield local_rule.checkTxAmounts(block); - yield local_rule.checkTxSignature(block); - done && done(); - } catch (err) { - if (done) return done(err); - throw err; - } - }); +function checkBunchOfTransactions(transactions, done) { + const block = { transactions }; + return (() => __awaiter(this, void 0, void 0, function* () { + try { + let local_rule = exports.FUNCTIONS; + yield local_rule.checkTxLen(block); + yield local_rule.checkTxIssuers(block); + yield local_rule.checkTxSources(block); + yield local_rule.checkTxRecipients(block); + yield local_rule.checkTxAmounts(block); + yield local_rule.checkTxSignature(block); + done && done(); + } + catch (err) { + if (done) + return done(err); + throw err; + } + }))(); } - -rules.HELPERS = { - - maxAcceleration: (conf) => maxAcceleration(conf), - - checkSingleMembershipSignature: checkSingleMembershipSignature, - - getSigResult: getSigResult, - - checkBunchOfTransactions: checkBunchOfTransactions, - - checkSingleTransactionLocally: (tx, done) => checkBunchOfTransactions([tx], done), - - checkTxAmountsValidity: (tx) => { - const inputs = tx.inputsAsObjects() - const outputs = tx.outputsAsObjects() - // Rule of money conservation - const commonBase = inputs.concat(outputs).reduce((min, input) => { - if (min === null) return input.base; - return Math.min(min, parseInt(input.base)); - }, null); - const inputSumCommonBase = inputs.reduce((sum, input) => { - return sum + input.amount * Math.pow(10, input.base - commonBase); - }, 0); - const outputSumCommonBase = outputs.reduce((sum, output) => { - return sum + output.amount * Math.pow(10, output.base - commonBase); - }, 0); - if (inputSumCommonBase !== outputSumCommonBase) { - throw constants.ERRORS.TX_INPUTS_OUTPUTS_NOT_EQUAL; - } - // Rule of unit base transformation - const maxOutputBase = outputs.reduce((max, output) => { - return Math.max(max, parseInt(output.base)); - }, 0); - // Compute deltas - const deltas = {}; - for (let i = commonBase; i <= maxOutputBase; i++) { - const inputBaseSum = inputs.reduce((sum, input) => { - if (input.base == i) { - return sum + input.amount * Math.pow(10, input.base - commonBase); - } else { - return sum; - } - }, 0); - const outputBaseSum = outputs.reduce((sum, output) => { - if (output.base == i) { - return sum + output.amount * Math.pow(10, output.base - commonBase); - } else { - return sum; - } - }, 0); - const delta = outputBaseSum - inputBaseSum; - let sumUpToBase = 0; - for (let j = commonBase; j < i; j++) { - sumUpToBase -= deltas[j]; - } - if (delta > 0 && delta > sumUpToBase) { - throw constants.ERRORS.TX_OUTPUT_SUM_NOT_EQUALS_PREV_DELTAS; - } - deltas[i] = delta; - } - }, - - getMaxPossibleVersionNumber: (current) => co(function*() { - // Looking at current blockchain, find what is the next maximum version we can produce - - // 1. We follow previous block's version - let version = current ? current.version : constants.BLOCK_GENERATED_VERSION; - - // 2. If we can, we go to the next version - return version; - }) +exports.HELPERS = { + maxAcceleration: (conf) => maxAcceleration(conf), + checkSingleMembershipSignature: checkSingleMembershipSignature, + getSigResult: getSigResult, + checkBunchOfTransactions: checkBunchOfTransactions, + checkSingleTransactionLocally: (tx, done) => checkBunchOfTransactions([tx], done), + checkTxAmountsValidity: (tx) => { + const inputs = tx.inputsAsObjects(); + const outputs = tx.outputsAsObjects(); + // Rule of money conservation + const commonBase = inputs.concat(outputs).reduce((min, input) => { + if (min === null) + return input.base; + return Math.min(min, input.base); + }, 0); + const inputSumCommonBase = inputs.reduce((sum, input) => { + return sum + input.amount * Math.pow(10, input.base - commonBase); + }, 0); + const outputSumCommonBase = outputs.reduce((sum, output) => { + return sum + output.amount * Math.pow(10, output.base - commonBase); + }, 0); + if (inputSumCommonBase !== outputSumCommonBase) { + throw constants.ERRORS.TX_INPUTS_OUTPUTS_NOT_EQUAL; + } + // Rule of unit base transformation + const maxOutputBase = outputs.reduce((max, output) => { + return Math.max(max, output.base); + }, 0); + // Compute deltas + const deltas = {}; + for (let i = commonBase; i <= maxOutputBase; i++) { + const inputBaseSum = inputs.reduce((sum, input) => { + if (input.base == i) { + return sum + input.amount * Math.pow(10, input.base - commonBase); + } + else { + return sum; + } + }, 0); + const outputBaseSum = outputs.reduce((sum, output) => { + if (output.base == i) { + return sum + output.amount * Math.pow(10, output.base - commonBase); + } + else { + return sum; + } + }, 0); + const delta = outputBaseSum - inputBaseSum; + let sumUpToBase = 0; + for (let j = commonBase; j < i; j++) { + sumUpToBase -= deltas[j]; + } + if (delta > 0 && delta > sumUpToBase) { + throw constants.ERRORS.TX_OUTPUT_SUM_NOT_EQUALS_PREV_DELTAS; + } + deltas[i] = delta; + } + }, + getMaxPossibleVersionNumber: (current) => __awaiter(this, void 0, void 0, function* () { + // Looking at current blockchain, find what is the next maximum version we can produce + // 1. We follow previous block's version + let version = current ? current.version : constants.BLOCK_GENERATED_VERSION; + // 2. If we can, we go to the next version + return version; + }) }; - -module.exports = rules; +//# sourceMappingURL=local_rules.js.map \ No newline at end of file diff --git a/app/lib/rules/local_rules.ts b/app/lib/rules/local_rules.ts new file mode 100644 index 0000000000000000000000000000000000000000..f6d53255624665cc4ba0196685c1f04b64b1e90f --- /dev/null +++ b/app/lib/rules/local_rules.ts @@ -0,0 +1,506 @@ +"use strict"; +import {BlockDTO} from "../dto/BlockDTO" +import {ConfDTO} from "../dto/ConfDTO" +import {CindexEntry, MindexEntry, SindexEntry} from "../indexer" +import {BaseDTO, TransactionDTO} from "../dto/TransactionDTO" +import {DBBlock} from "../db/DBBlock" + +const _ = require('underscore'); +const common = require('duniter-common'); +const indexer = require('../indexer').Indexer; + +const constants = common.constants +const hashf = common.hashf +const keyring = common.keyring +const Block = common.document.Block +const Identity = common.document.Identity +const Membership = common.document.Membership +const Transaction = common.document.Transaction +const maxAcceleration = require('./helpers').maxAcceleration + +export const FUNCTIONS = { + + checkParameters: async (block:BlockDTO) => { + if (block.number == 0 && !block.parameters) { + throw Error('Parameters must be provided for root block'); + } + else if (block.number > 0 && block.parameters) { + throw Error('Parameters must not be provided for non-root block'); + } + return true; + }, + + checkProofOfWork: async (block:BlockDTO) => { + let remainder = block.powMin % 16; + let nb_zeros = (block.powMin - remainder) / 16; + const powRegexp = new RegExp('^0{' + nb_zeros + '}'); + if (!block.hash.match(powRegexp)) { + throw Error('Not a proof-of-work'); + } + return true; + }, + + checkInnerHash: async (block:BlockDTO) => { + let inner_hash = hashf(block.getRawInnerPart()).toUpperCase(); + if (block.inner_hash != inner_hash) { + throw Error('Wrong inner hash'); + } + return true; + }, + + checkPreviousHash: async (block:BlockDTO) => { + if (block.number == 0 && block.previousHash) { + throw Error('PreviousHash must not be provided for root block'); + } + else if (block.number > 0 && !block.previousHash) { + throw Error('PreviousHash must be provided for non-root block'); + } + return true; + }, + + checkPreviousIssuer: async (block:BlockDTO) => { + if (block.number == 0 && block.previousIssuer) + throw Error('PreviousIssuer must not be provided for root block'); + else if (block.number > 0 && !block.previousIssuer) + throw Error('PreviousIssuer must be provided for non-root block'); + return true; + }, + + checkUnitBase: async (block:BlockDTO) => { + if (block.number == 0 && block.unitbase != 0) { + throw Error('UnitBase must equal 0 for root block'); + } + return true; + }, + + checkBlockSignature: async (block:BlockDTO) => { + if (!keyring.verify(block.getSignedPart(), block.signature, block.issuer)) + throw Error('Block\'s signature must match'); + return true; + }, + + checkBlockTimes: async (block:BlockDTO, conf:ConfDTO) => { + const time = block.time + const medianTime = block.medianTime + if (block.number > 0 && (time < medianTime || time > medianTime + maxAcceleration(conf))) + throw Error('A block must have its Time between MedianTime and MedianTime + ' + maxAcceleration(conf)); + else if (block.number == 0 && time != medianTime) + throw Error('Root block must have Time equal MedianTime'); + return true; + }, + + checkIdentitiesSignature: async (block:BlockDTO) => { + let i = 0; + let wrongSig = false; + while (!wrongSig && i < block.identities.length) { + const idty = Identity.fromInline(block.identities[i]); + idty.currency = block.currency; + wrongSig = !keyring.verify(idty.rawWithoutSig(), idty.sig, idty.pubkey); + if (wrongSig) { + throw Error('Identity\'s signature must match'); + } + i++; + } + return true; + }, + + checkIdentitiesUserIDConflict: async (block:BlockDTO, conf:ConfDTO, index:SindexEntry) => { + const creates = indexer.iindexCreate(index); + const uids = _.chain(creates).pluck('uid').uniq().value(); + if (creates.length !== uids.length) { + throw Error('Block must not contain twice same identity uid'); + } + return true; + }, + + checkIdentitiesPubkeyConflict: async (block:BlockDTO, conf:ConfDTO, index:SindexEntry) => { + const creates = indexer.iindexCreate(index); + const pubkeys = _.chain(creates).pluck('pub').uniq().value(); + if (creates.length !== pubkeys.length) { + throw Error('Block must not contain twice same identity pubkey'); + } + return true; + }, + + checkIdentitiesMatchJoin: async (block:BlockDTO, conf:ConfDTO, index:SindexEntry) => { + const icreates = indexer.iindexCreate(index); + const mcreates = indexer.mindexCreate(index); + for (const icreate of icreates) { + const matching = _(mcreates).filter({ pub: icreate.pub }); + if (matching.length == 0) { + throw Error('Each identity must match a newcomer line with same userid and certts'); + } + } + return true; + }, + + checkRevokedAreExcluded: async (block:BlockDTO, conf:ConfDTO, index:SindexEntry) => { + const iindex = indexer.iindex(index); + const mindex = indexer.mindex(index); + const revocations = _.chain(mindex) + .filter((row:MindexEntry) => row.op == constants.IDX_UPDATE && row.revoked_on !== null) + .pluck('pub') + .value(); + for (const pub of revocations) { + const exclusions = _(iindex).where({ op: constants.IDX_UPDATE, member: false, pub }); + if (exclusions.length == 0) { + throw Error('A revoked member must be excluded'); + } + } + return true; + }, + + checkRevokedUnicity: async (block:BlockDTO, conf:ConfDTO, index:SindexEntry) => { + try { + await FUNCTIONS.checkMembershipUnicity(block, conf, index); + } catch (e) { + throw Error('A single revocation per member is allowed'); + } + return true; + }, + + checkMembershipUnicity: async (block:BlockDTO, conf:ConfDTO, index:SindexEntry) => { + const mindex = indexer.mindex(index); + const pubkeys = _.chain(mindex).pluck('pub').uniq().value(); + if (pubkeys.length !== mindex.length) { + throw Error('Unicity constraint PUBLIC_KEY on MINDEX is not respected'); + } + return true; + }, + + checkMembershipsSignature: async (block:BlockDTO) => { + let i = 0; + let wrongSig = false, ms; + // Joiners + while (!wrongSig && i < block.joiners.length) { + ms = Membership.fromInline(block.joiners[i], 'IN', block.currency); + wrongSig = !checkSingleMembershipSignature(ms); + i++; + } + // Actives + i = 0; + while (!wrongSig && i < block.actives.length) { + ms = Membership.fromInline(block.actives[i], 'IN', block.currency); + wrongSig = !checkSingleMembershipSignature(ms); + i++; + } + // Leavers + i = 0; + while (!wrongSig && i < block.leavers.length) { + ms = Membership.fromInline(block.leavers[i], 'OUT', block.currency); + wrongSig = !checkSingleMembershipSignature(ms); + i++; + } + if (wrongSig) { + throw Error('Membership\'s signature must match'); + } + return true; + }, + + checkPubkeyUnicity: async (block:BlockDTO) => { + const pubkeys = []; + let conflict = false; + let pubk; + // Joiners + let i = 0; + while (!conflict && i < block.joiners.length) { + pubk = block.joiners[i].split(':')[0]; + conflict = !!(~pubkeys.indexOf(pubk)) + pubkeys.push(pubk); + i++; + } + // Actives + i = 0; + while (!conflict && i < block.actives.length) { + pubk = block.actives[i].split(':')[0]; + conflict = !!(~pubkeys.indexOf(pubk)) + pubkeys.push(pubk); + i++; + } + // Leavers + i = 0; + while (!conflict && i < block.leavers.length) { + pubk = block.leavers[i].split(':')[0]; + conflict = !!(~pubkeys.indexOf(pubk)) + pubkeys.push(pubk); + i++; + } + // Excluded + i = 0; + while (!conflict && i < block.excluded.length) { + pubk = block.excluded[i].split(':')[0]; + conflict = !!(~pubkeys.indexOf(pubk)) + pubkeys.push(pubk); + i++; + } + if (conflict) { + throw Error('Block cannot contain a same pubkey more than once in joiners, actives, leavers and excluded'); + } + return true; + }, + + checkCertificationOneByIssuer: async (block:BlockDTO, conf:ConfDTO, index:SindexEntry) => { + if (block.number > 0) { + const cindex = indexer.cindex(index); + const certFromA = _.uniq(cindex.map((row:CindexEntry) => row.issuer)); + if (certFromA.length !== cindex.length) { + throw Error('Block cannot contain two certifications from same issuer'); + } + } + return true; + }, + + checkCertificationUnicity: async (block:BlockDTO, conf:ConfDTO, index:SindexEntry) => { + const cindex = indexer.cindex(index); + const certAtoB = _.uniq(cindex.map((row:CindexEntry) => row.issuer + row.receiver)); + if (certAtoB.length !== cindex.length) { + throw Error('Block cannot contain identical certifications (A -> B)'); + } + return true; + }, + + checkCertificationIsntForLeaverOrExcluded: async (block:BlockDTO, conf:ConfDTO, index:SindexEntry) => { + const cindex = indexer.cindex(index); + const iindex = indexer.iindex(index); + const mindex = indexer.mindex(index); + const certified = cindex.map((row:CindexEntry) => row.receiver); + for (const pub of certified) { + const exclusions = _(iindex).where({ op: constants.IDX_UPDATE, member: false, pub: pub }); + const leavers = _(mindex).where({ op: constants.IDX_UPDATE, leaving: true, pub: pub }); + if (exclusions.length > 0 || leavers.length > 0) { + throw Error('Block cannot contain certifications concerning leavers or excluded members'); + } + } + return true; + }, + + checkTxVersion: async (block:BlockDTO) => { + const txs = block.transactions + // Check rule against each transaction + for (const tx of txs) { + if (tx.version != 10) { + throw Error('A transaction must have the version 10'); + } + } + return true; + }, + + checkTxLen: async (block:BlockDTO) => { + const txs = block.transactions + // Check rule against each transaction + for (const tx of txs) { + const txLen = Transaction.getLen(tx); + if (txLen > constants.MAXIMUM_LEN_OF_COMPACT_TX) { + throw constants.ERRORS.A_TRANSACTION_HAS_A_MAX_SIZE; + } + } + // Check rule against each output of each transaction + for (const tx of txs) { + for (const output of tx.outputs) { + const out = typeof output === 'string' ? output : Transaction.outputObj2Str(output) + if (out.length > constants.MAXIMUM_LEN_OF_OUTPUT) { + throw constants.ERRORS.MAXIMUM_LEN_OF_OUTPUT + } + } + } + // Check rule against each unlock of each transaction + for (const tx of txs) { + for (const unlock of tx.unlocks) { + if (unlock.length > constants.MAXIMUM_LEN_OF_UNLOCK) { + throw constants.ERRORS.MAXIMUM_LEN_OF_UNLOCK + } + } + } + return true; + }, + + checkTxIssuers: async (block:BlockDTO) => { + const txs = block.transactions + // Check rule against each transaction + for (const tx of txs) { + if (tx.issuers.length == 0) { + throw Error('A transaction must have at least 1 issuer'); + } + } + return true; + }, + + checkTxSources: async (block:BlockDTO) => { + const dto = BlockDTO.fromJSONObject(block) + for (const tx of dto.transactions) { + if (!tx.inputs || tx.inputs.length == 0) { + throw Error('A transaction must have at least 1 source'); + } + } + const sindex = indexer.localSIndex(dto); + const inputs = _.filter(sindex, (row:SindexEntry) => row.op == constants.IDX_UPDATE).map((row:SindexEntry) => [row.op, row.identifier, row.pos].join('-')); + if (inputs.length !== _.uniq(inputs).length) { + throw Error('It cannot exist 2 identical sources for transactions inside a given block'); + } + const outputs = _.filter(sindex, (row:SindexEntry) => row.op == constants.IDX_CREATE).map((row:SindexEntry) => [row.op, row.identifier, row.pos].join('-')); + if (outputs.length !== _.uniq(outputs).length) { + throw Error('It cannot exist 2 identical sources for transactions inside a given block'); + } + return true; + }, + + checkTxAmounts: async (block:BlockDTO) => { + for (const tx of block.transactions) { + HELPERS.checkTxAmountsValidity(tx); + } + }, + + checkTxRecipients: async (block:BlockDTO) => { + const txs = block.transactions + // Check rule against each transaction + for (const tx of txs) { + if (!tx.outputs || tx.outputs.length == 0) { + throw Error('A transaction must have at least 1 recipient'); + } + else { + // Cannot have empty output condition + for (const output of tx.outputsAsObjects()) { + if (!output.conditions.match(/(SIG|XHX)/)) { + throw Error('Empty conditions are forbidden'); + } + } + } + } + return true; + }, + + checkTxSignature: async (block:BlockDTO) => { + const txs = block.transactions + // Check rule against each transaction + for (const tx of txs) { + let sigResult = getSigResult(tx); + if (!sigResult.matching) { + throw Error('Signature from a transaction must match'); + } + } + return true; + } +} + +function checkSingleMembershipSignature(ms:any) { + return keyring.verify(ms.getRaw(), ms.signature, ms.issuer); +} + +function getSigResult(tx:any) { + let sigResult:any = { sigs: {}, matching: true }; + let json:any = { "version": tx.version, "currency": tx.currency, "blockstamp": tx.blockstamp, "locktime": tx.locktime, "inputs": [], "outputs": [], "issuers": tx.issuers, "signatures": [], "comment": tx.comment, unlocks: [] }; + tx.inputs.forEach(function (input:any) { + json.inputs.push(input.raw); + }); + tx.outputs.forEach(function (output:any) { + json.outputs.push(output.raw); + }); + json.unlocks = tx.unlocks; + let i = 0; + let signaturesMatching = true; + const raw = tx.getRawTxNoSig() + while (signaturesMatching && i < tx.signatures.length) { + const sig = tx.signatures[i]; + const pub = tx.issuers[i]; + signaturesMatching = keyring.verify(raw, sig, pub); + sigResult.sigs[pub] = { + matching: signaturesMatching, + index: i + }; + i++; + } + sigResult.matching = signaturesMatching; + return sigResult; +} + +function checkBunchOfTransactions(transactions:TransactionDTO[], done:any){ + const block:any = { transactions }; + return (async () => { + try { + let local_rule = FUNCTIONS; + await local_rule.checkTxLen(block); + await local_rule.checkTxIssuers(block); + await local_rule.checkTxSources(block); + await local_rule.checkTxRecipients(block); + await local_rule.checkTxAmounts(block); + await local_rule.checkTxSignature(block); + done && done(); + } catch (err) { + if (done) return done(err); + throw err; + } + })() +} + +export const HELPERS = { + + maxAcceleration: (conf:ConfDTO) => maxAcceleration(conf), + + checkSingleMembershipSignature: checkSingleMembershipSignature, + + getSigResult: getSigResult, + + checkBunchOfTransactions: checkBunchOfTransactions, + + checkSingleTransactionLocally: (tx:any, done:any) => checkBunchOfTransactions([tx], done), + + checkTxAmountsValidity: (tx:TransactionDTO) => { + const inputs = tx.inputsAsObjects() + const outputs = tx.outputsAsObjects() + // Rule of money conservation + const commonBase:number = (inputs as BaseDTO[]).concat(outputs).reduce((min:number, input) => { + if (min === null) return input.base; + return Math.min(min, input.base) + }, 0) + const inputSumCommonBase = inputs.reduce((sum, input) => { + return sum + input.amount * Math.pow(10, input.base - commonBase); + }, 0); + const outputSumCommonBase = outputs.reduce((sum, output) => { + return sum + output.amount * Math.pow(10, output.base - commonBase); + }, 0); + if (inputSumCommonBase !== outputSumCommonBase) { + throw constants.ERRORS.TX_INPUTS_OUTPUTS_NOT_EQUAL; + } + // Rule of unit base transformation + const maxOutputBase = outputs.reduce((max, output) => { + return Math.max(max, output.base) + }, 0) + // Compute deltas + const deltas:any = {}; + for (let i = commonBase; i <= maxOutputBase; i++) { + const inputBaseSum = inputs.reduce((sum, input) => { + if (input.base == i) { + return sum + input.amount * Math.pow(10, input.base - commonBase); + } else { + return sum; + } + }, 0); + const outputBaseSum = outputs.reduce((sum, output) => { + if (output.base == i) { + return sum + output.amount * Math.pow(10, output.base - commonBase); + } else { + return sum; + } + }, 0); + const delta = outputBaseSum - inputBaseSum; + let sumUpToBase = 0; + for (let j = commonBase; j < i; j++) { + sumUpToBase -= deltas[j]; + } + if (delta > 0 && delta > sumUpToBase) { + throw constants.ERRORS.TX_OUTPUT_SUM_NOT_EQUALS_PREV_DELTAS; + } + deltas[i] = delta; + } + }, + + getMaxPossibleVersionNumber: async (current:DBBlock) => { + // Looking at current blockchain, find what is the next maximum version we can produce + + // 1. We follow previous block's version + let version = current ? current.version : constants.BLOCK_GENERATED_VERSION; + + // 2. If we can, we go to the next version + return version; + } +} \ No newline at end of file