Commit a8022b58 authored by Cédric Moreau's avatar Cédric Moreau
Browse files

[fix] #1350 Reverting a block with a transaction must NOT destroy TX sources

parent e8ee7c44
......@@ -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
......
......@@ -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,
......
......@@ -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}`
}
......
......@@ -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;
}
......
......@@ -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
}
......
......@@ -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)
......
// 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
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment