Skip to content
Snippets Groups Projects
Commit 0e96b4a6 authored by Cédric Moreau's avatar Cédric Moreau
Browse files

[fix] #1242 Set the depth limit for a chain of transactions to 5

parent 080f32dd
No related branches found
No related tags found
2 merge requests!12331.6,!1230Fix/1.6/tx chaining
......@@ -59,6 +59,7 @@ test/integration/documents-currency.d.ts
test/integration/forwarding.js
test/integration/branches_switch.js
test/integration/branches2.js
test/integration/transactions-chaining.js
test/fast/modules/crawler/block_pulling.js*
test/fast/modules/crawler/block_pulling.d.ts
test/fast/fork*.js*
......@@ -72,3 +73,6 @@ test/fast/modules/common/grammar.d.ts
test/fast/prover/pow-1-cluster.d.ts
test/fast/prover/pow-1-cluster.js
test/fast/prover/pow-1-cluster.js.map
test/fast/protocol-local-rule-chained-tx-depth.js
test/fast/protocol-local-rule-chained-tx-depth.js.map
test/fast/protocol-local-rule-chained-tx-depth.d.ts
......@@ -285,6 +285,8 @@ export const CommonConstants = {
BLOCK: find("Block: (" + INTEGER + "-" + FINGERPRINT + ")"),
SPECIAL_BLOCK
},
BLOCK_MAX_TX_CHAINING_DEPTH: 5
}
function exact (regexpContent:string) {
......
......@@ -33,6 +33,7 @@ export const ALIAS = {
await LOCAL_RULES_FUNCTIONS.checkTxRecipients(block);
await LOCAL_RULES_FUNCTIONS.checkTxAmounts(block);
await LOCAL_RULES_FUNCTIONS.checkTxSignature(block);
await LOCAL_RULES_FUNCTIONS.checkMaxTransactionChainingDepth(block, conf, index);
},
ALL_LOCAL_BUT_POW_AND_SIGNATURE: async (block:BlockDTO, conf:ConfDTO, index:IndexEntry[]) => {
......@@ -60,6 +61,7 @@ export const ALIAS = {
await LOCAL_RULES_FUNCTIONS.checkTxRecipients(block);
await LOCAL_RULES_FUNCTIONS.checkTxAmounts(block);
await LOCAL_RULES_FUNCTIONS.checkTxSignature(block);
await LOCAL_RULES_FUNCTIONS.checkMaxTransactionChainingDepth(block, conf, index);
}
}
......
......@@ -379,17 +379,61 @@ export const LOCAL_RULES_FUNCTIONS = {
}
}
return true;
},
checkMaxTransactionChainingDepth: async (block:BlockDTO, conf:ConfDTO, index:IndexEntry[]) => {
const sindex = Indexer.sindex(index)
const max = getMaxTransactionDepth(sindex)
//
const allowedMax = block.medianTime > 1517443200 ? CommonConstants.BLOCK_MAX_TX_CHAINING_DEPTH : 1
if (max > allowedMax) {
throw "The maximum transaction chaining length per block is " + CommonConstants.BLOCK_MAX_TX_CHAINING_DEPTH
}
return true
}
}
export interface SindexShortEntry {
op:string,
identifier:string,
pos:number,
tx:string|null
}
function getMaxTransactionDepth(sindex:SindexShortEntry[]) {
const ids = _.uniq(_.pluck(sindex, 'tx'))
let maxTxChainingDepth = 0
for (let id of ids) {
maxTxChainingDepth = Math.max(maxTxChainingDepth, getTransactionDepth(id, sindex, 0))
}
return maxTxChainingDepth
}
function getTransactionDepth(txHash:string, sindex:SindexShortEntry[], localDepth = 0) {
const inputs = _.filter(sindex, (s:SindexShortEntry) => s.op === 'UPDATE' && s.tx === txHash)
let depth = localDepth
for (let input of inputs) {
const consumedOutput = _.findWhere(sindex, { op: 'CREATE', identifier: input.identifier, pos: input.pos })
if (consumedOutput) {
if (localDepth < 5) {
const subTxDepth = getTransactionDepth(consumedOutput.tx, sindex, localDepth + 1)
depth = Math.max(depth, subTxDepth)
} else {
depth++
}
}
}
return depth
}
function checkSingleMembershipSignature(ms:any) {
return verify(ms.getRaw(), ms.signature, ms.issuer);
}
function checkBunchOfTransactions(transactions:TransactionDTO[], done:any = undefined){
const block:any = { transactions };
function checkBunchOfTransactions(transactions:TransactionDTO[], conf:ConfDTO, options?:{ dontCareAboutChaining?:boolean }){
const block:any = { transactions, identities: [], joiners: [], actives: [], leavers: [], revoked: [], excluded: [], certifications: [] };
const index = Indexer.localIndex(block, conf)
return (async () => {
try {
let local_rule = LOCAL_RULES_FUNCTIONS;
await local_rule.checkTxLen(block);
await local_rule.checkTxIssuers(block);
......@@ -397,10 +441,8 @@ function checkBunchOfTransactions(transactions:TransactionDTO[], done:any = unde
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;
if (!options || !options.dontCareAboutChaining) {
await local_rule.checkMaxTransactionChainingDepth(block, conf, index);
}
})()
}
......@@ -411,9 +453,13 @@ export const LOCAL_RULES_HELPERS = {
checkSingleMembershipSignature: checkSingleMembershipSignature,
checkBunchOfTransactions: checkBunchOfTransactions,
checkBunchOfTransactions,
getTransactionDepth,
getMaxTransactionDepth,
checkSingleTransactionLocally: (tx:any, done:any = undefined) => checkBunchOfTransactions([tx], done),
checkSingleTransactionLocally: (tx:any, conf:ConfDTO) => checkBunchOfTransactions([tx], conf),
checkTxAmountsValidity: (tx:TransactionDTO) => {
const inputs = tx.inputsAsObjects()
......
......@@ -65,7 +65,7 @@ export class BlockGenerator {
const wereExcludeds = await this.dal.getRevokedPubkeys();
const newCertsFromWoT = await generator.findNewCertsFromWoT(current);
const newcomersLeavers = await this.findNewcomersAndLeavers(current, (joinersData:any) => generator.filterJoiners(joinersData));
const transactions = await this.findTransactions(current);
const transactions = await this.findTransactions(current, manualValues);
const joinData = newcomersLeavers[2];
const leaveData = newcomersLeavers[3];
const newCertsFromNewcomers = newcomersLeavers[4];
......@@ -104,7 +104,8 @@ export class BlockGenerator {
return [cur, newWoTMembers, finalJoinData, leavers, updates];
}
private async findTransactions(current:DBBlock) {
private async findTransactions(current:DBBlock, options:{ dontCareAboutChaining?:boolean }) {
const ALSO_CHECK_PENDING_TXS = true
const versionMin = current ? Math.min(CommonConstants.LAST_VERSION_FOR_TX, current.version) : CommonConstants.DOCUMENTS_VERSION;
const txs = await this.dal.getTransactionsPending(versionMin);
const transactions = [];
......@@ -113,14 +114,9 @@ export class BlockGenerator {
obj.currency = this.conf.currency
const tx = TransactionDTO.fromJSONObject(obj);
try {
await new Promise((resolve, reject) => {
LOCAL_RULES_HELPERS.checkBunchOfTransactions(passingTxs.concat(tx), (err:any, res:any) => {
if (err) return reject(err)
return resolve(res)
})
})
await LOCAL_RULES_HELPERS.checkBunchOfTransactions(passingTxs.concat(tx), this.conf, options)
const nextBlockWithFakeTimeVariation = { medianTime: current.medianTime + 1 };
await GLOBAL_RULES_HELPERS.checkSingleTransaction(tx, nextBlockWithFakeTimeVariation, this.conf, this.dal);
await GLOBAL_RULES_HELPERS.checkSingleTransaction(tx, nextBlockWithFakeTimeVariation, this.conf, this.dal, ALSO_CHECK_PENDING_TXS);
await GLOBAL_RULES_HELPERS.checkTxBlockStamp(tx, this.dal);
transactions.push(tx);
passingTxs.push(tx);
......
......@@ -217,8 +217,9 @@ export class BlockchainService extends FIFOService {
} catch (e) {
this.logger.error(e)
added = false
const theError = e && (e.message || e)
this.push({
blockResolutionError: e && e.message
blockResolutionError: theError
})
}
i++
......
......@@ -41,7 +41,7 @@ export class TransactionService extends FIFOService {
// Start checks...
const nextBlockWithFakeTimeVariation = { medianTime: current.medianTime + 1 };
const dto = TransactionDTO.fromJSONObject(tx)
await LOCAL_RULES_HELPERS.checkSingleTransactionLocally(dto)
await LOCAL_RULES_HELPERS.checkSingleTransactionLocally(dto, this.conf)
await GLOBAL_RULES_HELPERS.checkTxBlockStamp(tx, this.dal);
await GLOBAL_RULES_HELPERS.checkSingleTransaction(dto, nextBlockWithFakeTimeVariation, this.conf, this.dal, CHECK_PENDING_TRANSACTIONS);
const server_pubkey = this.conf.pair && this.conf.pair.pub;
......
......@@ -1549,6 +1549,40 @@ TRUE
> Functionally: we cannot create nor lose money through transactions. We can only transfer coins we own.
> Functionally: also, we cannot convert a superiod unit base into a lower one.
##### Transactions chaining max depth
FUNCTION `getTransactionDepth(txHash, LOCAL_DEPTH)`:
INPUTS = LOCAL_SINDEX[op='UPDATE',tx=txHash]
DEPTH = LOCAL_DEPTH
FOR EACH `INPUT` OF `INPUTS`
CONSUMED = LOCAL_SINDEX[op='CREATE',identifier=INPUT.identifier,pos=INPUT.pos]
IF (CONSUMED != NULL)
IF (LOCAL_DEPTH < 5)
DEPTH = MAX(DEPTH, getTransactionDepth(CONSUMED.tx, LOCAL_DEPTH +1)
ELSE
DEPTH++
END_IF
END_IF
END_FOR
RETURN DEPTH
END_FUNCTION
Then:
maxTxChainingDepth = 0
For each `TX_HASH` of `UNIQ(PICK(LOCAL_SINDEX, 'tx))`:
maxTxChainingDepth = MAX(maxTxChainingDepth, getTransactionDepth(TX_HASH, 0))
Rule:
maxTxChainingDepth <= 5
#### Global
Global validation verifies the coherence of a locally-validated block, in the context of the whole blockchain, including the block.
......@@ -1580,6 +1614,7 @@ Function references:
> If values count is even, the median is computed over the 2 centered values by an arithmetical median on them, *NOT* rounded.
* *UNIQ* returns a list of the unique values in a list of values
* *PICK* returns a list of the values by picking a particular property on each record
* *INTEGER_PART* return the integer part of a number
* *FIRST* return the first element in a list of values matching the given condition
* *REDUCE* merges a set of elements into a single one, by extending the non-null properties from each record into the resulting record.
......
import {LOCAL_RULES_HELPERS} from "../../app/lib/rules/local_rules"
const _ = require('underscore')
const assert = require('assert')
describe("Protocol BR_G110 - chained tx depth", () => {
const sindex = [
{ tx: 'A', op: 'UPDATE', identifier: 'UD1', pos: 0 },
{ tx: 'A', op: 'CREATE', identifier: 'TXA', pos: 0 },
{ tx: 'B', op: 'UPDATE', identifier: 'TXA', pos: 0 },
{ tx: 'B', op: 'CREATE', identifier: 'TXB', pos: 0 },
{ tx: 'C', op: 'UPDATE', identifier: 'TXB', pos: 0 },
{ tx: 'C', op: 'CREATE', identifier: 'TXC', pos: 0 },
{ tx: 'D', op: 'UPDATE', identifier: 'TXC', pos: 0 },
{ tx: 'D', op: 'CREATE', identifier: 'TXD', pos: 0 },
{ tx: 'E', op: 'UPDATE', identifier: 'TXD', pos: 0 },
{ tx: 'E', op: 'CREATE', identifier: 'TXE', pos: 0 },
{ tx: 'F', op: 'UPDATE', identifier: 'TXE', pos: 0 },
{ tx: 'F', op: 'CREATE', identifier: 'TXF', pos: 0 },
{ tx: 'G', op: 'UPDATE', identifier: 'TXF', pos: 0 },
{ tx: 'G', op: 'CREATE', identifier: 'TXG', pos: 0 },
{ tx: 'H', op: 'UPDATE', identifier: 'TXG', pos: 0 },
{ tx: 'H', op: 'CREATE', identifier: 'TXH', pos: 0 },
]
it('should detected normal depth', () => {
assert.equal(0, LOCAL_RULES_HELPERS.getTransactionDepth('A', sindex))
assert.equal(1, LOCAL_RULES_HELPERS.getTransactionDepth('B', sindex))
assert.equal(2, LOCAL_RULES_HELPERS.getTransactionDepth('C', sindex))
assert.equal(3, LOCAL_RULES_HELPERS.getTransactionDepth('D', sindex))
assert.equal(4, LOCAL_RULES_HELPERS.getTransactionDepth('E', sindex))
assert.equal(5, LOCAL_RULES_HELPERS.getTransactionDepth('F', sindex))
assert.equal(6, LOCAL_RULES_HELPERS.getTransactionDepth('G', sindex))
})
it('should detected max the depth to 6', () => {
assert.equal(6, LOCAL_RULES_HELPERS.getTransactionDepth('H', sindex))
})
it('should find the max depth globally', () => {
assert.equal(6, LOCAL_RULES_HELPERS.getMaxTransactionDepth(sindex))
})
})
"use strict";
import {CommonConstants} from "../../app/lib/common-libs/constants"
import {TestUser} from "./tools/TestUser"
import {TestingServer} from "./tools/toolbox"
import {NewLogger} from "../../app/lib/logger"
const co = require('co');
const _ = require('underscore');
const should = require('should');
const assert = require('assert');
const constants = require('../../app/lib/constants');
const bma = require('../../app/modules/bma').BmaDependency.duniter.methods.bma;
const CommonConstants = require('../../app/lib/common-libs/constants').CommonConstants
const toolbox = require('./tools/toolbox');
const node = require('./tools/node');
const TestUser = require('./tools/TestUser').TestUser
const unit = require('./tools/unit');
const http = require('./tools/http');
describe("Transaction chaining", function() {
describe("Transaction chaining", () => {
const now = 1456644632;
let s1, tic, toc
let s1:TestingServer, tic:TestUser, toc:TestUser
before(() => co(function*() {
before(async () => {
s1 = toolbox.server({
pair: {
......@@ -35,58 +30,80 @@ describe("Transaction chaining", function() {
tic = new TestUser('tic', { pub: 'DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV', sec: '468Q1XtTq7h84NorZdWBZFJrGkB18CbmbHr9tkp9snt5GiERP7ySs3wM8myLccbAAGejgMRC9rqnXuW3iAfZACm7'}, { server: s1 });
toc = new TestUser('toc', { pub: 'DKpQPUL4ckzXYdnDRvCRKAm1gNvSdmAXnTrJZ7LvM5Qo', sec: '64EYRvdPpTfLGGmaX5nijLXRqWXaVz8r1Z1GtaahXwVSJGQRn7tqkxLb288zwSYzELMEG5ZhXSBYSxsTsz1m9y8F'}, { server: s1 });
yield s1.initDalBmaConnections();
yield tic.createIdentity();
yield toc.createIdentity();
yield tic.cert(toc);
yield toc.cert(tic);
yield tic.join();
yield toc.join();
yield s1.commit({ time: now });
yield s1.commit({ time: now + 7210 });
yield s1.commit({ time: now + 7210 });
}));
await s1.initDalBmaConnections();
await tic.createIdentity();
await toc.createIdentity();
await tic.cert(toc);
await toc.cert(tic);
await tic.join();
await toc.join();
await s1.commit({ time: now });
await s1.commit({ time: now + 7210 });
await s1.commit({ time: now + 7210 });
})
after(() => {
return Promise.all([
s1.closeCluster()
])
return s1.closeCluster()
})
describe("Sources", function(){
describe("Sources", () => {
it('it should exist block#2 with UD of 1200', () => s1.expect('/blockchain/block/2', (block) => {
it('it should exist block#2 with UD of 1200', () => s1.expect('/blockchain/block/2', (block: { number:number, dividend:number }) => {
should.exists(block);
assert.equal(block.number, 2);
assert.equal(block.dividend, 1200);
}));
});
}))
})
describe("Chaining", function(){
describe("Chaining", () => {
it('with SIG and XHX', () => co(function *() {
it('with SIG and XHX', async () => {
// Current state
let current = yield s1.get('/blockchain/current');
(yield s1.get('/tx/sources/DKpQPUL4ckzXYdnDRvCRKAm1gNvSdmAXnTrJZ7LvM5Qo')).should.have.property('sources').length(1);
let tx1 = yield toc.prepareITX(1040, tic); // Rest = 1200 - 1040 = 160
let tx2 = yield toc.prepareUTX(tx1, ['SIG(0)'], [{ qty: 160, base: 0, lock: 'SIG(' + tic.pub + ')' }], {
let current = await s1.get('/blockchain/current');
(await s1.get('/tx/sources/DKpQPUL4ckzXYdnDRvCRKAm1gNvSdmAXnTrJZ7LvM5Qo')).should.have.property('sources').length(1);
let tx1 = await toc.prepareITX(1040, tic); // Rest = 1200 - 1040 = 160
let tx2 = await toc.prepareUTX(tx1, ['SIG(0)'], [{ qty: 160, base: 0, lock: 'SIG(' + tic.pub + ')' }], {
comment: 'also take the remaining 160 units',
blockstamp: [current.number, current.hash].join('-'),
theseOutputsStart: 1
});
const tmp = CommonConstants.TRANSACTION_MAX_TRIES;
CommonConstants.TRANSACTION_MAX_TRIES = 2;
yield unit.shouldNotFail(toc.sendTX(tx1));
yield unit.shouldNotFail(toc.sendTX(tx2));
(yield s1.get('/tx/sources/DKpQPUL4ckzXYdnDRvCRKAm1gNvSdmAXnTrJZ7LvM5Qo')).should.have.property('sources').length(1);
(yield s1.get('/tx/sources/DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV')).should.have.property('sources').length(1);
yield s1.commit({ time: now + 7210 }); // TX1 commited
(yield s1.get('/tx/sources/DKpQPUL4ckzXYdnDRvCRKAm1gNvSdmAXnTrJZ7LvM5Qo')).should.have.property('sources').length(1); // The 160 remaining units
(yield s1.get('/tx/sources/DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV')).should.have.property('sources').length(2); // The UD + 1040 units sent by toc
yield s1.commit({ time: now + 7210 }); // TX2 commited
(yield s1.get('/tx/sources/DKpQPUL4ckzXYdnDRvCRKAm1gNvSdmAXnTrJZ7LvM5Qo')).should.have.property('sources').length(0);
(yield s1.get('/tx/sources/DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV')).should.have.property('sources').length(3); // The UD + 1040 + 160 units sent by toc
await unit.shouldNotFail(toc.sendTX(tx1));
await unit.shouldNotFail(toc.sendTX(tx2));
(await s1.get('/tx/sources/DKpQPUL4ckzXYdnDRvCRKAm1gNvSdmAXnTrJZ7LvM5Qo')).should.have.property('sources').length(1);
(await s1.get('/tx/sources/DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV')).should.have.property('sources').length(1);
await s1.commit({ time: now + 7210 }); // TX1 + TX2 commited
(await s1.get('/tx/sources/DKpQPUL4ckzXYdnDRvCRKAm1gNvSdmAXnTrJZ7LvM5Qo')).should.have.property('sources').length(0);
(await s1.get('/tx/sources/DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV')).should.have.property('sources').length(3); // The UD + 1040 + 160 units sent by toc
CommonConstants.TRANSACTION_MAX_TRIES = tmp;
})
it('should refuse a block with more than 5 chained tx in it', async () => {
// Current state
let current = await s1.get('/blockchain/current');
const blockstamp = [current.number, current.hash].join('-');
(await s1.get('/tx/sources/DKpQPUL4ckzXYdnDRvCRKAm1gNvSdmAXnTrJZ7LvM5Qo')).should.have.property('sources').length(0);
(await s1.get('/tx/sources/DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV')).should.have.property('sources').length(3);
// Ping-pong of 1200 units
let tx1 = await tic.prepareITX(1200, toc, "PING-PONG TX1");
let tx2 = await toc.prepareUTX(tx1, ['SIG(0)'], [{ qty: 1200, base: 0, lock: 'SIG(' + tic.pub + ')' }], { blockstamp, comment: "PING-PONG TX2" });
let tx3 = await tic.prepareUTX(tx2, ['SIG(0)'], [{ qty: 1200, base: 0, lock: 'SIG(' + toc.pub + ')' }], { blockstamp, comment: "PING-PONG TX3" });
let tx4 = await toc.prepareUTX(tx3, ['SIG(0)'], [{ qty: 1200, base: 0, lock: 'SIG(' + tic.pub + ')' }], { blockstamp, comment: "PING-PONG TX4" });
let tx5 = await tic.prepareUTX(tx4, ['SIG(0)'], [{ qty: 1200, base: 0, lock: 'SIG(' + toc.pub + ')' }], { blockstamp, comment: "PING-PONG TX5" });
let tx6 = await toc.prepareUTX(tx5, ['SIG(0)'], [{ qty: 1200, base: 0, lock: 'SIG(' + tic.pub + ')' }], { blockstamp, comment: "PING-PONG TX6" });
let tx7 = await tic.prepareUTX(tx6, ['SIG(0)'], [{ qty: 1200, base: 0, lock: 'SIG(' + toc.pub + ')' }], { blockstamp, comment: "PING-PONG TX7" });
const tmp = CommonConstants.TRANSACTION_MAX_TRIES;
CommonConstants.TRANSACTION_MAX_TRIES = 2;
await unit.shouldNotFail(toc.sendTX(tx1));
await unit.shouldNotFail(toc.sendTX(tx2));
await unit.shouldNotFail(toc.sendTX(tx3));
await unit.shouldNotFail(toc.sendTX(tx4));
await unit.shouldNotFail(toc.sendTX(tx5));
await unit.shouldNotFail(toc.sendTX(tx6));
await unit.shouldNotFail(toc.sendTX(tx7));
await s1.commitWaitError({ dontCareAboutChaining: true }, 'The maximum transaction chaining length per block is 5')
CommonConstants.TRANSACTION_MAX_TRIES = tmp;
}));
})
});
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment