From f02f743666e7db37b923f1fff1a6525f9426d655 Mon Sep 17 00:00:00 2001
From: cgeek <cem.moreau@gmail.com>
Date: Tue, 15 Jan 2019 18:49:39 +0100
Subject: [PATCH] [fix] #1330 Revert of UD consumption was incorrectly done

---
 .../dal/indexDAL/common/DividendDaoHandler.ts | 39 +++++----
 app/service/BlockchainService.ts              |  9 +-
 test/integration/branches/branches_revert2.ts |  2 +-
 .../consumption-of-several-uds-with-revert.ts | 56 ++++++++++++
 .../consumption-on-two-branches.ts            | 86 +++++++++++++++++++
 test/integration/sources/source-replay.ts     | 62 +++++++++++++
 test/integration/tools/toolbox.ts             | 20 ++++-
 test/integration/transactions-csv-cltv-sig.ts |  1 +
 8 files changed, 250 insertions(+), 25 deletions(-)
 create mode 100644 test/integration/fork-resolution/consumption-of-several-uds-with-revert.ts
 create mode 100644 test/integration/fork-resolution/consumption-on-two-branches.ts
 create mode 100644 test/integration/sources/source-replay.ts

diff --git a/app/lib/dal/indexDAL/common/DividendDaoHandler.ts b/app/lib/dal/indexDAL/common/DividendDaoHandler.ts
index dc7506c5d..71649a379 100644
--- a/app/lib/dal/indexDAL/common/DividendDaoHandler.ts
+++ b/app/lib/dal/indexDAL/common/DividendDaoHandler.ts
@@ -121,26 +121,31 @@ export class DividendDaoHandler {
   }
 
   static unconsumeDividends(m: DividendEntry, number: number, consumedUDsRecoveredByRevert: SimpleUdEntryForWallet[]) {
-    const index = m.consumed.indexOf(number)
+    let index;
+    do {
+      index = m.consumed.indexOf(number)
 
-    const src = m.consumedUDs[index].dividend
-    consumedUDsRecoveredByRevert.push({
-      conditions: 'SIG(' + m.pub + ')',
-      pos: m.consumedUDs[index].dividendNumber,
-      identifier: m.pub,
-      amount: src.amount,
-      base: src.base,
-      srcType: 'D',
-      op: 'CREATE'
-    })
+      if (index !== -1) {
+        const src = m.consumedUDs[index].dividend
+        consumedUDsRecoveredByRevert.push({
+          conditions: 'SIG(' + m.pub + ')',
+          pos: m.consumedUDs[index].dividendNumber,
+          identifier: m.pub,
+          amount: src.amount,
+          base: src.base,
+          srcType: 'D',
+          op: 'CREATE'
+        })
 
-    // We put it back as available
-    m.availables.push(m.consumedUDs[index].dividendNumber)
-    m.dividends.push(m.consumedUDs[index].dividend)
+        // We put it back as available
+        m.availables.push(m.consumedUDs[index].dividendNumber)
+        m.dividends.push(m.consumedUDs[index].dividend)
 
-    // We remove it from consumed
-    m.consumed.splice(index, 1)
-    m.consumedUDs.splice(index, 1)
+        // We remove it from consumed
+        m.consumed.splice(index, 1)
+        m.consumedUDs.splice(index, 1)
+      }
+    } while (index !== -1);
   }
 
   static trimConsumed(m: DividendEntry, belowNumber: number) {
diff --git a/app/service/BlockchainService.ts b/app/service/BlockchainService.ts
index fbda8d34e..a6f96216c 100644
--- a/app/service/BlockchainService.ts
+++ b/app/service/BlockchainService.ts
@@ -219,7 +219,7 @@ export class BlockchainService extends FIFOService {
     })
   }
 
-  async blockResolution(max = 0): Promise<BlockDTO|null> {
+  async blockResolution(filterFunc: (b: DBBlock) => boolean = () => true): Promise<BlockDTO|null> {
     let lastAdded:BlockDTO|null = null
     let added:BlockDTO|null
     let nbAdded = 0
@@ -227,15 +227,15 @@ export class BlockchainService extends FIFOService {
       const current = await this.current()
       let potentials = []
       if (current) {
-        potentials = await this.dal.getForkBlocksFollowing(current)
+        potentials = (await this.dal.getForkBlocksFollowing(current)).filter(filterFunc)
         this.logger.info('Block resolution: %s potential blocks after current#%s...', potentials.length, current.number)
       } else {
-        potentials = await this.dal.getPotentialRootBlocks()
+        potentials = (await this.dal.getPotentialRootBlocks()).filter(filterFunc)
         this.logger.info('Block resolution: %s potential blocks for root block...', potentials.length)
       }
       added = null
       let i = 0
-      while (!added && i < potentials.length && (!max || nbAdded < max)) {
+      while (!added && i < potentials.length) {
         const dto = BlockDTO.fromJSONObject(potentials[i])
         try {
           if (dto.issuer === this.conf.pair.pub) {
@@ -274,6 +274,7 @@ export class BlockchainService extends FIFOService {
         block: newCurrent
       })
     }
+    return newCurrent
   }
 
   revertCurrentBlock() {
diff --git a/test/integration/branches/branches_revert2.ts b/test/integration/branches/branches_revert2.ts
index 0928ee375..78748f5e7 100644
--- a/test/integration/branches/branches_revert2.ts
+++ b/test/integration/branches/branches_revert2.ts
@@ -230,7 +230,7 @@ describe("Revert two blocks", function() {
 
     before(async () => {
       await s1.dal.txsDAL.removeAll()
-      await s1.resolveExistingBlock(1) // UD block
+      await s1.resolveExistingBlock(b => b.number === 2) // UD block
       await cat.sendMoney(19, toc);
       await s1.dal.blockDAL.removeForkBlock(3)
       await s1.commit({ time: now + 1 });
diff --git a/test/integration/fork-resolution/consumption-of-several-uds-with-revert.ts b/test/integration/fork-resolution/consumption-of-several-uds-with-revert.ts
new file mode 100644
index 000000000..828236e4c
--- /dev/null
+++ b/test/integration/fork-resolution/consumption-of-several-uds-with-revert.ts
@@ -0,0 +1,56 @@
+// 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, writeBasicTestWithConfAnd2Users} from "../tools/test-framework"
+
+describe('Consumption of several UDS with revert', () => writeBasicTestWithConfAnd2Users({
+  dt: 10,
+  udTime0: 1500000000 + 10,
+  ud0: 100,
+}, (test) => {
+
+  const now = 1500000000
+
+  test('should init with a Dividend at block#3', async (s1, cat, tac) => {
+    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 })
+    await s1.commit({ time: now })
+    await s1.commit({ time: now + 10 })
+    await s1.commit({ time: now + 20 })
+    await s1.commit({ time: now + 20 })
+    assertEqual((await s1._server.dal.dividendDAL.getUDSources(cat.pub)).length, 2)
+  })
+
+  test('should allow cat to send its money', async (s1, cat, tac) => {
+    await s1.commit({ time: now + 20 })
+    await cat.sendMoney(200, tac)
+    await s1.commit({ time: now + 20 })
+    assertEqual((await s1._server.dal.dividendDAL.getUDSources(cat.pub)).length, 0)
+    assertEqual((await s1._server.dal.dividendDAL.getUDSources(tac.pub)).length, 2)
+    assertEqual((await s1._server.dal.sindexDAL.getAvailableForPubkey(tac.pub)).length, 1)
+  })
+
+  test('revert and re-commit the transaction a block earlier than main branch', async (s1, cat, tac) => {
+    await s1.revert()
+    await s1.revert()
+    await s1.resolve()
+    assertEqual((await s1._server.dal.dividendDAL.getUDSources(cat.pub)).length, 0)
+    assertEqual((await s1._server.dal.dividendDAL.getUDSources(tac.pub)).length, 2)
+    assertEqual((await s1._server.dal.sindexDAL.getAvailableForPubkey(tac.pub)).length, 1)
+  })
+}))
diff --git a/test/integration/fork-resolution/consumption-on-two-branches.ts b/test/integration/fork-resolution/consumption-on-two-branches.ts
new file mode 100644
index 000000000..ee3c34f74
--- /dev/null
+++ b/test/integration/fork-resolution/consumption-on-two-branches.ts
@@ -0,0 +1,86 @@
+// 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, assertNotNull, writeBasicTestWithConfAnd2Users} from "../tools/test-framework"
+import {BlockDTO} from "../../../app/lib/dto/BlockDTO"
+
+describe('Parallel consumption', () => writeBasicTestWithConfAnd2Users({
+  dt: 10,
+  udTime0: 1500000000 + 10,
+  ud0: 100,
+  switchOnHeadAdvance: 0,
+}, (test) => {
+
+  const now = 1500000000
+
+  test('should init with a Dividend at block#3', async (s1, cat, tac) => {
+    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 })
+    await s1.commit({ time: now })
+    await s1.commit({ time: now + 10 })
+    await s1.commit({ time: now + 20 })
+    await s1.commit({ time: now + 20 })
+    assertEqual((await s1._server.dal.dividendDAL.getUDSources(cat.pub)).length, 2)
+  })
+
+  test('should allow cat to send its money', async (s1, cat, tac) => {
+    await s1.commit({ time: now + 20 })
+    await cat.sendMoney(200, tac)
+    await s1.commit({ time: now + 20 })
+    assertEqual((await s1._server.dal.dividendDAL.getUDSources(cat.pub)).length, 0)
+    assertEqual((await s1._server.dal.dividendDAL.getUDSources(tac.pub)).length, 2)
+    assertEqual((await s1._server.dal.sindexDAL.getAvailableForPubkey(tac.pub)).length, 1)
+  })
+
+  test('revert and re-commit the transaction a block earlier than main branch', async (s1, cat, tac) => {
+    await s1.revert()
+    await s1.revert()
+    await cat.sendMoney(200, tac)
+    await s1.justCommit({ time: now + 21 }) // Time with 11 is the fork
+    await s1.resolve(b => b.time === now + 21)
+    await s1.commit({ time: now + 21 })
+    await s1.commit({ time: now + 21 })
+    await s1.commit({ time: now + 21 })
+    assertEqual((await s1._server.dal.dividendDAL.getUDSources(cat.pub)).length, 0)
+    assertEqual((await s1._server.dal.dividendDAL.getUDSources(tac.pub)).length, 2)
+    assertEqual((await s1._server.dal.sindexDAL.getAvailableForPubkey(tac.pub)).length, 1)
+  })
+
+  test('switching on main branch should succeed', async (s1, cat, tac) => {
+    await s1.revert()
+    await s1.revert()
+    await s1.revert()
+    await s1.revert()
+    const resolved = await s1.resolve(b => b.time % 2 === 0)
+    assertEqual(resolved.number, 6)
+    assertEqual((await s1._server.dal.dividendDAL.getUDSources(cat.pub)).length, 0)
+    assertEqual((await s1._server.dal.dividendDAL.getUDSources(cat.pub)).length, 0)
+    assertEqual((await s1._server.dal.dividendDAL.getUDSources(tac.pub)).length, 2)
+    assertEqual((await s1._server.dal.sindexDAL.getAvailableForPubkey(tac.pub)).length, 1)
+  })
+
+  test('switching on fork branch should succeed', async (s1, cat, tac) => {
+    const newHead = await s1.resolveFork()
+    assertNotNull(newHead)
+    assertEqual((newHead as BlockDTO).number, 7)
+    assertEqual((await s1._server.dal.dividendDAL.getUDSources(cat.pub)).length, 0)
+    assertEqual((await s1._server.dal.dividendDAL.getUDSources(cat.pub)).length, 0)
+    assertEqual((await s1._server.dal.dividendDAL.getUDSources(tac.pub)).length, 2)
+    assertEqual((await s1._server.dal.sindexDAL.getAvailableForPubkey(tac.pub)).length, 1)
+  })
+}))
diff --git a/test/integration/sources/source-replay.ts b/test/integration/sources/source-replay.ts
new file mode 100644
index 000000000..e03c56b64
--- /dev/null
+++ b/test/integration/sources/source-replay.ts
@@ -0,0 +1,62 @@
+// 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, writeBasicTestWithConfAnd2Users} from "../tools/test-framework"
+
+describe('Source replay', () => writeBasicTestWithConfAnd2Users({
+  dt: 10,
+  udTime0: 1500000000 + 10,
+  ud0: 100,
+}, (test) => {
+
+  const now = 1500000000
+
+  test('should init with a Dividend at block#3', async (s1, cat, tac) => {
+    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 })
+    await s1.commit({ time: now })
+    await s1.commit({ time: now + 10 })
+    await s1.commit({ time: now + 10 })
+    assertEqual((await s1._server.dal.dividendDAL.getUDSources(cat.pub)).length, 1)
+  })
+
+  test('should allow cat to send its money', async (s1, cat, tac) => {
+    await cat.sendMoney(100, tac)
+    await s1.commit({ time: now + 10 })
+    assertEqual((await s1._server.dal.dividendDAL.getUDSources(cat.pub)).length, 0)
+    assertEqual((await s1._server.dal.dividendDAL.getUDSources(tac.pub)).length, 1)
+    assertEqual((await s1._server.dal.sindexDAL.getAvailableForPubkey(tac.pub)).length, 1)
+  })
+
+  test('revert and re-commit should not change anything', async (s1, cat, tac) => {
+    await s1.revert()
+    await s1.resolve()
+    assertEqual((await s1._server.dal.dividendDAL.getUDSources(cat.pub)).length, 0)
+    assertEqual((await s1._server.dal.dividendDAL.getUDSources(tac.pub)).length, 1)
+    assertEqual((await s1._server.dal.sindexDAL.getAvailableForPubkey(tac.pub)).length, 1)
+  })
+
+  test('revert and re-commit dividend should not change anything', async (s1, cat, tac) => {
+    await s1.revert()
+    await s1.revert()
+    await s1.resolve()
+    assertEqual((await s1._server.dal.dividendDAL.getUDSources(cat.pub)).length, 0)
+    assertEqual((await s1._server.dal.dividendDAL.getUDSources(tac.pub)).length, 1)
+    assertEqual((await s1._server.dal.sindexDAL.getAvailableForPubkey(tac.pub)).length, 1)
+  })
+}))
diff --git a/test/integration/tools/toolbox.ts b/test/integration/tools/toolbox.ts
index 7edb1fd25..39e3669e5 100644
--- a/test/integration/tools/toolbox.ts
+++ b/test/integration/tools/toolbox.ts
@@ -375,6 +375,20 @@ export class TestingServer {
     return this.server.revert()
   }
 
+  async resolve(filterFunc?: (b: DBBlock) => boolean): Promise<BlockDTO> {
+    const blocksResolved = await this.server.BlockchainService.blockResolution(filterFunc)
+    if (!blocksResolved) {
+      await new Promise(res => setTimeout(res, 200))
+      throw Error(DataErrors[DataErrors.BLOCK_WASNT_COMMITTED])
+    }
+    console.log(BlockDTO.fromJSONObject(blocksResolved).getRawSigned())
+    return blocksResolved
+  }
+
+  async resolveFork(): Promise<BlockDTO|null> {
+    return this.server.BlockchainService.forkResolution()
+  }
+
   resetHome() {
     return this.server.resetHome()
   }
@@ -490,7 +504,7 @@ export class TestingServer {
 
   async justCommit(options:any = null) {
     const proven = await this.generateNext(options)
-    await this.server.writeBlock(proven, true, false)
+    await this.server.writeBlock(proven, false, true)
     return proven
   }
 
@@ -505,8 +519,8 @@ export class TestingServer {
     return blocksResolved
   }
 
-  async resolveExistingBlock(max = 0) {
-    const blocksResolved = await this.server.BlockchainService.blockResolution(max)
+  async resolveExistingBlock(filterFunc: (b: DBBlock) => boolean) {
+    const blocksResolved = await this.server.BlockchainService.blockResolution(filterFunc)
     if (!blocksResolved) {
       throw Error(DataErrors[DataErrors.BLOCK_WASNT_COMMITTED])
     }
diff --git a/test/integration/transactions-csv-cltv-sig.ts b/test/integration/transactions-csv-cltv-sig.ts
index a9ada9607..accc64c27 100644
--- a/test/integration/transactions-csv-cltv-sig.ts
+++ b/test/integration/transactions-csv-cltv-sig.ts
@@ -60,6 +60,7 @@ describe("Transaction: CSV+CLTV+1of2SIG", function() {
     await cat.sendTX(tx)
     const block = await s1.justCommit({ time: now })
     block.should.have.property('transactions').length(1)
+    s1.resolve()
     await s1.waitToHaveBlock(2)
   })
 
-- 
GitLab