diff --git a/app/lib/common-libs/array-prune.ts b/app/lib/common-libs/array-prune.ts index 524f9a5d91cc63185d10ed681abf93540ad6edd2..0e262d538bcf44bf7a6727066df6b58bfd073e97 100644 --- a/app/lib/common-libs/array-prune.ts +++ b/app/lib/common-libs/array-prune.ts @@ -11,6 +11,11 @@ export function arrayPruneAll<T>(array: T[], value: T) { } while (index !== -1) } +/** + * Returs a copy of given array WITHOUT any record of `value`. + * @param original The array we want records, with `value` being excluded. + * @param value The value we don't want to see in our copy array. + */ export function arrayPruneAllCopy<T>(original: T[], value: T) { const array = original.slice() let index diff --git a/app/lib/common-libs/constants.ts b/app/lib/common-libs/constants.ts index 728e506b7eb5f85bcb41f239bf87b3448cf61b56..9e44451cb5686124d7ef50e7d2be3f00c557045d 100755 --- a/app/lib/common-libs/constants.ts +++ b/app/lib/common-libs/constants.ts @@ -301,6 +301,7 @@ export const CommonConstants = { }, BLOCK_MAX_TX_CHAINING_DEPTH: 5, + BLOCK_TX_CHAINING_ACTIVATION_MT: 1519862400, ARCHIVES_BLOCKS_CHUNK: 250, SYNC_BLOCKS_CHUNK: 250, diff --git a/app/lib/dal/indexDAL/leveldb/LevelDBSindex.ts b/app/lib/dal/indexDAL/leveldb/LevelDBSindex.ts index f4b7c469b750e13ab6712f3a6fec080af333d6ae..099a565bf2683cd8a3bdf2c7c8da45d998fa31b6 100644 --- a/app/lib/dal/indexDAL/leveldb/LevelDBSindex.ts +++ b/app/lib/dal/indexDAL/leveldb/LevelDBSindex.ts @@ -177,11 +177,13 @@ export class LevelDBSindex extends LevelDBTable<SindexEntry> implements SIndexDA } async getWrittenOn(blockstamp: string): Promise<SindexEntry[]> { - const ids = (await this.indexForTrimming.getOrNull(LevelDBSindex.trimWrittenOnKey(pint(blockstamp)))) || [] + const ids = Underscore.uniq((await this.indexForTrimming.getOrNull(LevelDBSindex.trimWrittenOnKey(pint(blockstamp)))) || []) const found: SindexEntry[] = [] for (const id of ids) { const entries = await this.findByIdentifierAndPos(id.split('-')[0], pint(id.split('-')[1])) - entries.forEach(e => found.push(e)) + entries + .filter(e => e.written_on === blockstamp) + .forEach(e => found.push(e)) } return found } @@ -198,42 +200,65 @@ export class LevelDBSindex extends LevelDBTable<SindexEntry> implements SIndexDA async removeBlock(blockstamp: string): Promise<void> { const writtenOn = pint(blockstamp) + // We look at records written on this blockstamp: `indexForTrimming` allows to get them const ids = (await this.indexForTrimming.getOrNull(LevelDBSindex.trimWrittenOnKey(writtenOn))) || [] + // `ids` contains both CREATE and UPDATE sources for (const id of ids) { // Remove sources const identifier = id.split('-')[0] + const pos = parseInt(id.split('-')[1]) const conditions: string[] = [] - await this.applyAllKeyValue(async kv => { - conditions.push(kv.value.conditions) - await this.del(kv.key) - }, { - gte: identifier, - lt: LevelDBSindex.upperIdentifier(identifier) - }) - // Remove indexations - // 1. WrittenOn - await this.indexForTrimming.del(LevelDBSindex.trimWrittenOnKey(writtenOn)) - // 2. Conditions + const createKey = LevelDBSindex.trimKey(identifier, pos, false) + const updateKey = LevelDBSindex.trimKey(identifier, pos, true) + const createRecord = await this.getOrNull(createKey) + const updateRecord = await this.getOrNull(updateKey) + // Undo consumption + if (updateRecord && updateRecord.writtenOn === writtenOn) { + conditions.push(updateRecord.conditions) + await this.del(updateKey) + } + // Undo creation? + if (createRecord && createRecord.writtenOn === writtenOn) { + conditions.push(createRecord.conditions) + await this.del(createKey) + } + // Update balance + // 1. Conditions const uniqConditions = Underscore.uniq(conditions) for (const condition of uniqConditions) { + // Remove this source from the balance await this.trimConditions(condition, id) } } + if (ids.length) { + // 2. WrittenOn + await this.indexForTrimming.del(LevelDBSindex.trimWrittenOnKey(writtenOn)) + await this.indexForConsumed.del(LevelDBSindex.trimWrittenOnKey(writtenOn)) + } } private async trimConditions(condition: string, id: string) { + // Get all the account's TX sources const existing = (await this.indexForConditions.getOrNull(condition)) || [] + // Prune the source from the account const trimmed = arrayPruneAllCopy(existing, id) if (trimmed.length) { + // If some sources are left for this "account", persist what remains await this.indexForConditions.put(condition, trimmed) } else { + // Otherwise just delete the "account" await this.indexForConditions.del(condition) } } + /** + * Duplicate with trimConditions?! + * @param writtenOn + * @param id + */ private async trimWrittenOn(writtenOn: number, id: string) { const k = LevelDBSindex.trimWrittenOnKey(writtenOn) - const existing = (await this.indexForTrimming.getOrNull(k)) || [] + const existing = await this.getWrittenOnSourceIds(writtenOn) const trimmed = arrayPruneAllCopy(existing, id) if (trimmed.length) { await this.indexForConditions.put(k, trimmed) @@ -253,6 +278,11 @@ export class LevelDBSindex extends LevelDBTable<SindexEntry> implements SIndexDA } } + private async getWrittenOnSourceIds(writtenOn: number) { + const indexForTrimmingId = LevelDBSindex.trimWrittenOnKey(writtenOn) + return (await this.indexForTrimming.getOrNull(indexForTrimmingId)) || [] + } + private static trimKey(identifier: string, pos: number, consumed: boolean) { return `${identifier}-${String(pos).padStart(10, '0')}-${consumed ? 1 : 0}` } diff --git a/app/lib/indexer.ts b/app/lib/indexer.ts index cca42a91f56e0175db425989c1b49501ba365de1..1ce20345022e992f6adcdba560c0f517b7086a0a 100644 --- a/app/lib/indexer.ts +++ b/app/lib/indexer.ts @@ -30,6 +30,7 @@ import {Tristamp} from "./common/Tristamp" import {Underscore} from "./common-libs/underscore" import {DataErrors} from "./common-libs/errors" import {MonitorExecutionTime} from "./debug/MonitorExecutionTime" +import {NewLogger} from "./logger" const constants = CommonConstants @@ -1033,6 +1034,9 @@ export class Indexer { ENTRY.base, ENTRY.srcType === 'D' ); + if (!reducable.length) { + NewLogger().debug('Source %s:%s NOT FOUND', ENTRY.identifier, ENTRY.pos) + } source = reduce(reducable) } return source @@ -2157,6 +2161,9 @@ function txSourceUnlock(ENTRY:SindexEntry, source:{ conditions: string, written_ const unlockParams:string[] = TransactionDTO.unlock2params(ENTRY.unlock || '') const unlocksMetadata:UnlockMetadata = {} const sigResult = TransactionDTO.fromJSONObject(tx).getTransactionSigResult() + if (!source.conditions) { + return false // Unlock fail + } if (source.conditions.match(/CLTV/)) { unlocksMetadata.currentTime = HEAD.medianTime; } diff --git a/app/lib/rules/local_rules.ts b/app/lib/rules/local_rules.ts index d6c56b1c3da7e61d07b939a047e15b3407f10ec2..35ed12a8a1ac1fad33d040cadd9dab3d198cc6ef 100644 --- a/app/lib/rules/local_rules.ts +++ b/app/lib/rules/local_rules.ts @@ -396,7 +396,7 @@ export const LOCAL_RULES_FUNCTIONS = { const sindex = Indexer.sindex(index) const max = getMaxTransactionDepth(sindex) // - const allowedMax = block.medianTime > 1519862400 ? CommonConstants.BLOCK_MAX_TX_CHAINING_DEPTH : 0 + const allowedMax = block.medianTime > CommonConstants.BLOCK_TX_CHAINING_ACTIVATION_MT ? CommonConstants.BLOCK_MAX_TX_CHAINING_DEPTH : 0 if (max > allowedMax) { throw "The maximum transaction chaining length per block is " + CommonConstants.BLOCK_MAX_TX_CHAINING_DEPTH } diff --git a/test/fast/modules/common/grammar.ts b/test/fast/modules/common/grammar.ts index ebae873b89bce665f96040afa9061486b58d1868..198e3ee4051f64fd76d954204f43d3a1e5b0dd38 100644 --- a/test/fast/modules/common/grammar.ts +++ b/test/fast/modules/common/grammar.ts @@ -23,6 +23,10 @@ describe('Grammar', () => { let Ha = "CA978112CA1BBDCAFAC231B39A23DC4DA786EFF8147C4E72B9807785AFEE48BB" let Hz = "594E519AE499312B29433B7DD8A97FF068DEFCBA9755B6D5D00E84C524D67B06" + it('EMPTY string should work (but not unlock)', () => { + assert.equal(unlock('', [], { sigs: [] }), null) + }) + it('SIG should work', () => { assert.equal(unlock('SIG(' + k1 + ')', ['SIG(0)'], { sigs: [{ k:k1, ok:true }] }), true) assert.equal(unlock('SIG(' + k1 + ')', ['SIG(0)'], { sigs: [{ k:k1, ok:false }] }), null) diff --git a/test/integration/fork-resolution/block-with-transaction-revert.ts b/test/integration/fork-resolution/block-with-transaction-revert.ts new file mode 100644 index 0000000000000000000000000000000000000000..ddf726fc6b1d662022fee075aa78943a9795ed90 --- /dev/null +++ b/test/integration/fork-resolution/block-with-transaction-revert.ts @@ -0,0 +1,135 @@ +// Source file from duniter: Crypto-currency software to manage libre currency such as Äž1 +// Copyright (C) 2018 Cedric Moreau <cem.moreau@gmail.com> +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +import {assertEqual, assertNull, writeBasicTestWithConfAnd2Users} from "../tools/test-framework" +import {DBWallet} from "../../../app/lib/db/DBWallet" +import {DBBlock} from "../../../app/lib/db/DBBlock" +import {CommonConstants} from "../../../app/lib/common-libs/constants" +import {TestUser} from "../tools/TestUser" +import {TestingServer} from "../tools/toolbox" + +describe('Block revert with transaction sources', () => writeBasicTestWithConfAnd2Users({ + dt: 10, + udTime0: CommonConstants.BLOCK_TX_CHAINING_ACTIVATION_MT + 10, + ud0: 1000, + switchOnHeadAdvance: 0, +}, (test) => { + + const now = CommonConstants.BLOCK_TX_CHAINING_ACTIVATION_MT + + test('(b#1) should init with a Dividend at block#1', async (s1, cat, tac, toc) => { + await cat.createIdentity() + await tac.createIdentity() + await cat.cert(tac) + await tac.cert(cat) + await cat.join() + await tac.join() + await s1.commit({ time: now + 11 }) + await s1.commit({ time: now + 11 }) + await assertBlock1(s1, cat, tac, toc) + }) + + test('(b#2) tac sends to both tac and toc', async (s1, cat, tac, toc) => { + // Using transaction chaining to also test this case + const current = await s1._server.dal.getCurrentBlockOrNull() as DBBlock + const tx1 = await cat.prepareITX(1000, cat) + const tx2 = await cat.prepareUTX(tx1, ['SIG(0)'], + [ + { qty: 100, base: 0, lock: 'SIG(' + tac.pub + ')' }, + { qty: 200, base: 0, lock: 'SIG(' + toc.pub + ')' }, // Send money also to toc, to test that his money is ketp safe during a revert + { qty: 700, base: 0, lock: 'SIG(' + cat.pub + ')' }, // REST + ], + { + comment: 'CHAINED TX to 2 recipients', blockstamp: [current.number, current.hash].join('-') + } + ) + await cat.sendTX(tx1) + await cat.sendTX(tx2) + + await s1.commit({ time: now + 11 }) + await assertBlock2(s1, cat, tac, toc) + }) + + test('(b#3) tac gives all to cat', async (s1, cat, tac, toc) => { + await tac.sendMoney(1100, cat) + await s1.commit({ time: now + 11 }) + await assertBlock3(s1, cat, tac, toc) + }) + + test('(b#4) toc spends some received money', async (s1, cat, tac, toc) => { + // Using normal transaction + await toc.sendMoney(100, cat) + await s1.commit({ time: now + 11 }) + await assertBlock4(s1, cat, tac, toc) + }) + + test('revert b#3-4 and re-commit block#3 should be ok', async (s1, cat, tac, toc) => { + await s1.revert() + await s1.revert() + await s1.resolve(b => b.number === 3) + await assertBlock3(s1, cat, tac, toc) + }) + + test('re-commit block#4 should be ok', async (s1, cat, tac, toc) => { + await s1.resolve(b => b.number === 4) + await assertBlock4(s1, cat, tac, toc) + }) +})) + +async function assertBlock1(s1: TestingServer, cat: TestUser, tac: TestUser, toc: TestUser) { + assertEqual((await s1._server.dal.dividendDAL.getUDSources(cat.pub)).length, 1) + assertEqual((await s1._server.dal.dividendDAL.getUDSources(tac.pub)).length, 1) + assertEqual((await s1._server.dal.dividendDAL.getUDSources(toc.pub)).length, 0) // toc is not a member + assertEqual((await s1._server.dal.sindexDAL.getAvailableForPubkey(cat.pub)).length, 0) + assertEqual((await s1._server.dal.sindexDAL.getAvailableForPubkey(tac.pub)).length, 0) + assertEqual((await s1._server.dal.sindexDAL.getAvailableForPubkey(toc.pub)).length, 0) + assertEqual((await s1._server.dal.walletDAL.getWallet('SIG(' + cat.pub + ')') as DBWallet).balance, 1000) + assertEqual((await s1._server.dal.walletDAL.getWallet('SIG(' + tac.pub + ')') as DBWallet).balance, 1000) + assertNull(await s1._server.dal.walletDAL.getWallet('SIG(' + toc.pub + ')')) +} + +async function assertBlock2(s1: TestingServer, cat: TestUser, tac: TestUser, toc: TestUser) { + assertEqual((await s1._server.dal.dividendDAL.getUDSources(cat.pub)).length, 0) // <-- The UD gets consumed + assertEqual((await s1._server.dal.dividendDAL.getUDSources(tac.pub)).length, 1) + assertEqual((await s1._server.dal.dividendDAL.getUDSources(toc.pub)).length, 0) // toc is not a member + assertEqual((await s1._server.dal.sindexDAL.getAvailableForPubkey(cat.pub)).length, 1) + assertEqual((await s1._server.dal.sindexDAL.getAvailableForPubkey(tac.pub)).length, 1) + assertEqual((await s1._server.dal.sindexDAL.getAvailableForPubkey(toc.pub)).length, 1) + assertEqual((await s1._server.dal.walletDAL.getWallet('SIG(' + cat.pub + ')') as DBWallet).balance, 700) // <-- -300 here + assertEqual((await s1._server.dal.walletDAL.getWallet('SIG(' + tac.pub + ')') as DBWallet).balance, 1100) // <-- +100 here + assertEqual((await s1._server.dal.walletDAL.getWallet('SIG(' + toc.pub + ')') as DBWallet).balance, 200) // <-- +200 here +} + +async function assertBlock3(s1: TestingServer, cat: TestUser, tac: TestUser, toc: TestUser) { + assertEqual((await s1._server.dal.dividendDAL.getUDSources(cat.pub)).length, 0) + assertEqual((await s1._server.dal.dividendDAL.getUDSources(tac.pub)).length, 0) // <-- The UD gets consumed + assertEqual((await s1._server.dal.dividendDAL.getUDSources(toc.pub)).length, 0) // toc is not a member + assertEqual((await s1._server.dal.sindexDAL.getAvailableForPubkey(cat.pub)).length, 2) // <-- Cat receives a new source + assertEqual((await s1._server.dal.sindexDAL.getAvailableForPubkey(tac.pub)).length, 0) // <-- Every TX source gets consumed + assertEqual((await s1._server.dal.sindexDAL.getAvailableForPubkey(toc.pub)).length, 1) + assertEqual((await s1._server.dal.walletDAL.getWallet('SIG(' + cat.pub + ')') as DBWallet).balance, 1800) // <-- +1100 here + assertEqual((await s1._server.dal.walletDAL.getWallet('SIG(' + tac.pub + ')') as DBWallet).balance, 0) // <-- -1100 here + assertEqual((await s1._server.dal.walletDAL.getWallet('SIG(' + toc.pub + ')') as DBWallet).balance, 200) +} + +async function assertBlock4(s1: TestingServer, cat: TestUser, tac: TestUser, toc: TestUser) { + assertEqual((await s1._server.dal.dividendDAL.getUDSources(cat.pub)).length, 0) + assertEqual((await s1._server.dal.dividendDAL.getUDSources(tac.pub)).length, 0) + assertEqual((await s1._server.dal.dividendDAL.getUDSources(toc.pub)).length, 0) // toc is not a member + assertEqual((await s1._server.dal.sindexDAL.getAvailableForPubkey(cat.pub)).length, 3) // <-- Cat receives a new source + assertEqual((await s1._server.dal.sindexDAL.getAvailableForPubkey(tac.pub)).length, 0) + assertEqual((await s1._server.dal.sindexDAL.getAvailableForPubkey(toc.pub)).length, 1) // <-- Consume everything + Create a rest + assertEqual((await s1._server.dal.walletDAL.getWallet('SIG(' + cat.pub + ')') as DBWallet).balance, 1900)// <-- +100 here + assertEqual((await s1._server.dal.walletDAL.getWallet('SIG(' + tac.pub + ')') as DBWallet).balance, 0) + assertEqual((await s1._server.dal.walletDAL.getWallet('SIG(' + toc.pub + ')') as DBWallet).balance, 100)// <-- -100 here +}