diff --git a/app/lib/common-libs/constants.ts b/app/lib/common-libs/constants.ts index a0b6151cfe8e3c02064c8f402fb3c2908655aced..0cf4179ea590650db2a016c346314a97e7771b3c 100755 --- a/app/lib/common-libs/constants.ts +++ b/app/lib/common-libs/constants.ts @@ -53,7 +53,7 @@ const CONDITIONS = "\\)|CSV\\(" + CSV_INTEGER + "\\))))*"; - +const CONDITION_SIG_PUBKEY = "SIG\\((" + PUBKEY + ")\\)"; const BMA_REGEXP = /^BASIC_MERKLED_API( ([a-z_][a-z0-9-_.]*))?( ([0-9.]+))?( ([0-9a-f:]+))?( ([0-9]+))$/; const BMAS_REGEXP = /^BMAS( ([a-z_][a-z0-9-_.]*))?( ([0-9.]+))?( ([0-9a-f:]+))?( ([0-9]+))( (\/.+))?$/; const BMATOR_REGEXP = /^BMATOR( ([a-z0-9]{16})\.onion)( ([0-9.]+))?( ([0-9a-f:]+))?( ([0-9]+))$/; @@ -533,6 +533,8 @@ export const CommonConstants = { LOCKTIME: find("Locktime: (" + INTEGER + ")"), INLINE_COMMENT: exact(COMMENT), OUTPUT_CONDITION: exact(CONDITIONS), + OUTPUT_CONDITION_SIG_PUBKEY: find(CONDITION_SIG_PUBKEY), + OUTPUT_CONDITION_SIG_PUBKEY_UNIQUE: exact(CONDITION_SIG_PUBKEY), }, PEER: { BLOCK: find("Block: (" + INTEGER + "-" + FINGERPRINT + ")"), diff --git a/app/lib/dal/indexDAL/leveldb/LevelDBSindex.ts b/app/lib/dal/indexDAL/leveldb/LevelDBSindex.ts index a8b4aceba9bf7314135e8a1a47c41b7ad96f7ffd..efb081aebf1d551f84e801b7cd35eac37756546e 100644 --- a/app/lib/dal/indexDAL/leveldb/LevelDBSindex.ts +++ b/app/lib/dal/indexDAL/leveldb/LevelDBSindex.ts @@ -12,12 +12,14 @@ import { SIndexDAO } from "../abstract/SIndexDAO"; import { Underscore } from "../../../common-libs/underscore"; import { pint } from "../../../common-libs/pint"; import { arrayPruneAllCopy } from "../../../common-libs/array-prune"; +import { CommonConstants } from "../../../common-libs/constants"; export class LevelDBSindex extends LevelDBTable<SindexEntry> implements SIndexDAO { private indexForTrimming: LevelDBTable<string[]>; private indexForConsumed: LevelDBTable<string[]>; private indexForConditions: LevelDBTable<string[]>; + private indexOfComplexeConditionForPubkeys: LevelDBTable<string[]>; constructor(protected getLevelDB: (dbName: string) => Promise<LevelUp>) { super("level_sindex", getLevelDB); @@ -41,9 +43,14 @@ export class LevelDBSindex extends LevelDBTable<SindexEntry> "level_sindex/conditions", this.getLevelDB ); + this.indexOfComplexeConditionForPubkeys = new LevelDBTable<string[]>( + "level_sindex/complex_condition_pubkeys", + this.getLevelDB + ); await this.indexForTrimming.init(); await this.indexForConsumed.init(); await this.indexForConditions.init(); + await this.indexOfComplexeConditionForPubkeys.init(); } async close(): Promise<void> { @@ -51,6 +58,7 @@ export class LevelDBSindex extends LevelDBTable<SindexEntry> await this.indexForTrimming.close(); await this.indexForConsumed.close(); await this.indexForConditions.close(); + await this.indexOfComplexeConditionForPubkeys.close(); } /** @@ -127,14 +135,14 @@ export class LevelDBSindex extends LevelDBTable<SindexEntry> pos: number; }[] > { - // TODO: very costly: needs a full scan, would be better to change this implementatio - const entries = await this.findWhere((e) => - e.conditions.includes(`SIG(${pubkey})`) + const forSimpleConditions = await this.getForConditions(`SIG(${pubkey})`); + const forComplexConditions = await this.getForComplexeConditionPubkey( + pubkey + ); + const reduced = Indexer.DUP_HELPERS.reduceBy( + forSimpleConditions.concat(forComplexConditions), + ["identifier", "pos"] ); - const reduced = Indexer.DUP_HELPERS.reduceBy(entries, [ - "identifier", - "pos", - ]); return reduced.filter((r) => !r.consumed); } @@ -269,6 +277,20 @@ export class LevelDBSindex extends LevelDBTable<SindexEntry> return found; } + async getForComplexeConditionPubkey(pubkey: string): Promise<SindexEntry[]> { + const ids = + (await this.indexOfComplexeConditionForPubkeys.getOrNull(pubkey)) || []; + 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)); + } + return found; + } + async removeBlock(blockstamp: string): Promise<void> { const writtenOn = pint(blockstamp); // We look at records written on this blockstamp: `indexForTrimming` allows to get them @@ -288,7 +310,8 @@ export class LevelDBSindex extends LevelDBTable<SindexEntry> const updateRecord = await this.getOrNull(updateKey); // Undo consumption if (updateRecord && updateRecord.writtenOn === writtenOn) { - conditions.push(updateRecord.conditions); + // Delete from condition index only if no createRecord exists - fix #1446 + if (!createRecord) conditions.push(updateRecord.conditions); await this.del(updateKey); } // Undo creation? @@ -296,11 +319,10 @@ export class LevelDBSindex extends LevelDBTable<SindexEntry> conditions.push(createRecord.conditions); await this.del(createKey); } - // Update balance - // 1. Conditions + // Update condition index const uniqConditions = Underscore.uniq(conditions); for (const condition of uniqConditions) { - // Remove this source from the balance + // Remove this source from the condition await this.trimConditions(condition, id); } } @@ -316,24 +338,25 @@ export class LevelDBSindex extends LevelDBTable<SindexEntry> } private async trimConditions(condition: string, id: string) { - // Get all the account's TX sources + // Get all the condition's sources const existing = (await this.indexForConditions.getOrNull(condition)) || []; - // Prune the source from the account + // Prune the source from the condition const trimmed = arrayPruneAllCopy(existing, id); if (trimmed.length) { - // If some sources are left for this "account", persist what remains + // If some sources are left for this "condition", persist what remains await this.indexForConditions.put(condition, trimmed); } else { - // Otherwise just delete the "account" + // Otherwise just delete the "condition" await this.indexForConditions.del(condition); } + + // If complex conditions + if (this.isComplexCondition(condition)) { + const pubkeys = this.getDistinctPubkeysFromCondition(condition); + await this.trimComplexeConditionPubkeys(pubkeys, id); + } } - /** - * Duplicate with trimConditions?! - * @param writtenOn - * @param id - */ private async trimWrittenOn(writtenOn: number, id: string) { const k = LevelDBSindex.trimWrittenOnKey(writtenOn); const existing = await this.getWrittenOnSourceIds(writtenOn); @@ -356,6 +379,28 @@ export class LevelDBSindex extends LevelDBTable<SindexEntry> } } + private async trimComplexeConditionPubkeys(pubkeys: string[], id: string) { + if (!pubkeys || !pubkeys.length) return; + for (const p of pubkeys) { + await this.trimComplexeConditionPubkey(p, id); + } + } + + private async trimComplexeConditionPubkey(pubkey: string, id: string) { + // Get all the condition's sources + const existing = + (await this.indexOfComplexeConditionForPubkeys.getOrNull(pubkey)) || []; + // Prune the source from the condition + const trimmed = arrayPruneAllCopy(existing, id); + if (trimmed.length) { + // If some sources are left for this "condition", persist what remains + await this.indexOfComplexeConditionForPubkeys.put(pubkey, trimmed); + } else { + // Otherwise just delete the "account" + await this.indexOfComplexeConditionForPubkeys.del(pubkey); + } + } + private async getWrittenOnSourceIds(writtenOn: number) { const indexForTrimmingId = LevelDBSindex.trimWrittenOnKey(writtenOn); return (await this.indexForTrimming.getOrNull(indexForTrimmingId)) || []; @@ -393,6 +438,7 @@ export class LevelDBSindex extends LevelDBTable<SindexEntry> const byConsumed: { [k: number]: SindexEntry[] } = {}; const byWrittenOn: { [k: number]: SindexEntry[] } = {}; const byConditions: { [k: string]: SindexEntry[] } = {}; + const byPubkeys: { [k: string]: SindexEntry[] } = {}; records .filter((r) => r.consumed) .forEach((r) => { @@ -410,12 +456,24 @@ export class LevelDBSindex extends LevelDBTable<SindexEntry> arrWO = byWrittenOn[r.writtenOn] = []; } arrWO.push(r); - // Conditiosn + // Conditions let arrCN = byConditions[r.conditions]; if (!arrCN) { arrCN = byConditions[r.conditions] = []; } arrCN.push(r); + + // If complex condition + if (this.isComplexCondition(r.conditions)) { + const pubkeys = this.getDistinctPubkeysFromCondition(r.conditions); + pubkeys.forEach((pub) => { + let arrPub = byPubkeys[pub]; + if (!arrPub) { + arrPub = byPubkeys[pub] = []; + } + arrPub.push(r); + }); + } }); // Index consumed => (identifier + pos)[] for (const k of Underscore.keys(byConsumed)) { @@ -446,5 +504,47 @@ export class LevelDBSindex extends LevelDBTable<SindexEntry> Underscore.uniq(existing.concat(newSources)) ); } + // Index pubkeys => (identifier + pos)[] + for (const k of Underscore.keys(byPubkeys).map(String)) { + const existing = + (await this.indexOfComplexeConditionForPubkeys.getOrNull(k)) || []; + const newSources = byPubkeys[k].map((r) => + LevelDBSindex.trimPartialKey(r.identifier, r.pos) + ); + await this.indexOfComplexeConditionForPubkeys.put( + k, + Underscore.uniq(existing.concat(newSources)) + ); + } + } + + private isComplexCondition(condition: string): boolean { + return ( + (condition && + !CommonConstants.TRANSACTION.OUTPUT_CONDITION_SIG_PUBKEY_UNIQUE.test( + condition + )) || + false + ); + } + /** + * Get all pubkeys used by an output condition (e.g. 'SIG(A) && SIG(B)' will return ['A', 'B'] + * @param condition + * @private + */ + private getDistinctPubkeysFromCondition(condition: string): string[] { + const pubKeys: string[] = []; + if (!condition) return pubKeys; + let match: RegExpExecArray | null; + while ( + (match = CommonConstants.TRANSACTION.OUTPUT_CONDITION_SIG_PUBKEY.exec( + condition + )) !== null + ) { + pubKeys.push(match[1]); + condition = condition.substring(match.index + match[0].length); + } + + return Underscore.uniq(pubKeys); } } diff --git a/test/integration/fork-resolution/block-with-transaction-revert.ts b/test/integration/fork-resolution/block-with-transaction-revert.ts index ddf726fc6b1d662022fee075aa78943a9795ed90..47f0918445c9234bf717d30520971cef46f201fd 100644 --- a/test/integration/fork-resolution/block-with-transaction-revert.ts +++ b/test/integration/fork-resolution/block-with-transaction-revert.ts @@ -17,6 +17,7 @@ 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" +import {LevelDBSindex} from "../../../app/lib/dal/indexDAL/leveldb/LevelDBSindex"; describe('Block revert with transaction sources', () => writeBasicTestWithConfAnd2Users({ dt: 10, @@ -46,7 +47,7 @@ describe('Block revert with transaction sources', () => writeBasicTestWithConfAn 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: 200, base: 0, lock: 'SIG(' + toc.pub + ')' }, // Send money also to toc, to test that his money is kept safe during a revert { qty: 700, base: 0, lock: 'SIG(' + cat.pub + ')' }, // REST ], { @@ -74,10 +75,14 @@ describe('Block revert with transaction sources', () => writeBasicTestWithConfAn }) 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) + await s1.revert() // Revert b#4 + await assertBlock3(s1, cat, tac, toc) // Current is b#3 + + await s1.revert() // Revert b#3 + await assertBlock2(s1, cat, tac, toc) // Current is b#2 + + await s1.resolve(b => b.number === 3) // Waiting b#3 to commit + await assertBlock3(s1, cat, tac, toc) // Current is b#3 }) test('re-commit block#4 should be ok', async (s1, cat, tac, toc) => {