From 0e7865ca1ba33aaeeb00bfcfcf2ec869697f0f45 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Tue, 8 Sep 2020 13:20:22 -0400 Subject: [PATCH] txdb: fix lockedUnconfirmed if FINALIZE is inserted with block --- lib/wallet/txdb.js | 8 +- test/wallet-test.js | 332 +++++++++++++++++++++++++++++++++----------- 2 files changed, 255 insertions(+), 85 deletions(-) diff --git a/lib/wallet/txdb.js b/lib/wallet/txdb.js index 9acc39677..efcc51464 100644 --- a/lib/wallet/txdb.js +++ b/lib/wallet/txdb.js @@ -960,10 +960,14 @@ class TXDB { // FINALIZE is a special case: locked coins _leave_ the wallet. if (tx.output(i) && tx.covenant(i).isFinalize()) { - if (!block) + if (!block) { state.ulocked(path, -tx.outputs[i].value); - else + } else { state.clocked(path, -tx.outputs[i].value); + // This is the first time we've seen this tx and it is in a block + // (probably from a rescan). Update unconfirmed locked balance also. + state.ulocked(path, -tx.outputs[i].value); + } } if (!block) { diff --git a/test/wallet-test.js b/test/wallet-test.js index 7627ed4f3..436929918 100644 --- a/test/wallet-test.js +++ b/test/wallet-test.js @@ -1933,6 +1933,8 @@ describe('Wallet', function() { let start; // The main test wallet, and wallet that will receive the FINALIZE. let wallet, recip; + // Store balance data before rescan to ensure rescan was complete + let recipBalBefore, senderBalBefore; // Hack required to focus test on txdb mechanics. // We don't otherwise need WalletDB or Blockchain @@ -2153,21 +2155,21 @@ describe('Wallet', function() { uTXCount++; // Check - let senderBal = await wallet.getBalance(); - assert.strictEqual(senderBal.tx, 6); - assert.strictEqual(senderBal.coin, 4); - assert.strictEqual(senderBal.confirmed, fund - (cTXCount * fee)); - assert.strictEqual(senderBal.unconfirmed, fund - (uTXCount * fee)); - assert.strictEqual(senderBal.ulocked, secondHighest); - assert.strictEqual(senderBal.clocked, secondHighest); - - let recipBal = await recip.getBalance(); - assert.strictEqual(recipBal.tx, 0); - assert.strictEqual(recipBal.coin, 0); - assert.strictEqual(recipBal.confirmed, 0); - assert.strictEqual(recipBal.unconfirmed, 0); - assert.strictEqual(recipBal.ulocked, 0); - assert.strictEqual(recipBal.clocked, 0); + const senderBal1 = await wallet.getBalance(); + assert.strictEqual(senderBal1.tx, 6); + assert.strictEqual(senderBal1.coin, 4); + assert.strictEqual(senderBal1.confirmed, fund - (cTXCount * fee)); + assert.strictEqual(senderBal1.unconfirmed, fund - (uTXCount * fee)); + assert.strictEqual(senderBal1.ulocked, secondHighest); + assert.strictEqual(senderBal1.clocked, secondHighest); + + const recipBal1 = await recip.getBalance(); + assert.strictEqual(recipBal1.tx, 0); + assert.strictEqual(recipBal1.coin, 0); + assert.strictEqual(recipBal1.confirmed, 0); + assert.strictEqual(recipBal1.unconfirmed, 0); + assert.strictEqual(recipBal1.ulocked, 0); + assert.strictEqual(recipBal1.clocked, 0); // Confirm TRANSFER const block = { @@ -2179,21 +2181,21 @@ describe('Wallet', function() { cTXCount++; // Check - senderBal = await wallet.getBalance(); - assert.strictEqual(senderBal.tx, 6); - assert.strictEqual(senderBal.coin, 4); - assert.strictEqual(senderBal.confirmed, fund - (cTXCount * fee)); - assert.strictEqual(senderBal.unconfirmed, fund - (uTXCount * fee)); - assert.strictEqual(senderBal.ulocked, secondHighest); - assert.strictEqual(senderBal.clocked, secondHighest); - - recipBal = await recip.getBalance(); - assert.strictEqual(recipBal.tx, 0); - assert.strictEqual(recipBal.coin, 0); - assert.strictEqual(recipBal.confirmed, 0); - assert.strictEqual(recipBal.unconfirmed, 0); - assert.strictEqual(recipBal.ulocked, 0); - assert.strictEqual(recipBal.clocked, 0); + const senderBal2 = await wallet.getBalance(); + assert.strictEqual(senderBal2.tx, 6); + assert.strictEqual(senderBal2.coin, 4); + assert.strictEqual(senderBal2.confirmed, fund - (cTXCount * fee)); + assert.strictEqual(senderBal2.unconfirmed, fund - (uTXCount * fee)); + assert.strictEqual(senderBal2.ulocked, secondHighest); + assert.strictEqual(senderBal2.clocked, secondHighest); + + const recipBal2 = await recip.getBalance(); + assert.strictEqual(recipBal2.tx, 0); + assert.strictEqual(recipBal2.coin, 0); + assert.strictEqual(recipBal2.confirmed, 0); + assert.strictEqual(recipBal2.unconfirmed, 0); + assert.strictEqual(recipBal2.ulocked, 0); + assert.strictEqual(recipBal2.clocked, 0); }); it('should send and confirm FINALIZE', async () => { @@ -2204,23 +2206,23 @@ describe('Wallet', function() { uTXCount++; // Check - let senderBal = await wallet.getBalance(); - assert.strictEqual(senderBal.tx, 7); + const senderBal3 = await wallet.getBalance(); + assert.strictEqual(senderBal3.tx, 7); // One less wallet coin because name UTXO belongs to recip now - assert.strictEqual(senderBal.coin, 3); - assert.strictEqual(senderBal.confirmed, fund - (cTXCount * fee)); - assert.strictEqual(senderBal.unconfirmed, fund - secondHighest - (uTXCount * fee)); - assert.strictEqual(senderBal.ulocked, 0); - assert.strictEqual(senderBal.clocked, secondHighest); + assert.strictEqual(senderBal3.coin, 3); + assert.strictEqual(senderBal3.confirmed, fund - (cTXCount * fee)); + assert.strictEqual(senderBal3.unconfirmed, fund - secondHighest - (uTXCount * fee)); + assert.strictEqual(senderBal3.ulocked, 0); + assert.strictEqual(senderBal3.clocked, secondHighest); // The name and its locked value now belong to recipient - let recipBal = await recip.getBalance(); - assert.strictEqual(recipBal.tx, 1); - assert.strictEqual(recipBal.coin, 1); - assert.strictEqual(recipBal.confirmed, 0); - assert.strictEqual(recipBal.unconfirmed, secondHighest); - assert.strictEqual(recipBal.ulocked, secondHighest); - assert.strictEqual(recipBal.clocked, 0); + const recipBal3 = await recip.getBalance(); + assert.strictEqual(recipBal3.tx, 1); + assert.strictEqual(recipBal3.coin, 1); + assert.strictEqual(recipBal3.confirmed, 0); + assert.strictEqual(recipBal3.unconfirmed, secondHighest); + assert.strictEqual(recipBal3.ulocked, secondHighest); + assert.strictEqual(recipBal3.clocked, 0); // Confirm FINALIZE const block = { @@ -2234,21 +2236,31 @@ describe('Wallet', function() { cTXCount++; // Check - senderBal = await wallet.getBalance(); - assert.strictEqual(senderBal.tx, 7); - assert.strictEqual(senderBal.coin, 3); - assert.strictEqual(senderBal.confirmed, fund - secondHighest - (cTXCount * fee)); - assert.strictEqual(senderBal.unconfirmed, fund - secondHighest - (uTXCount * fee)); - assert.strictEqual(senderBal.ulocked, 0); - assert.strictEqual(senderBal.clocked, 0); - - recipBal = await recip.getBalance(); - assert.strictEqual(recipBal.tx, 1); - assert.strictEqual(recipBal.coin, 1); - assert.strictEqual(recipBal.confirmed, secondHighest); - assert.strictEqual(recipBal.unconfirmed, secondHighest); - assert.strictEqual(recipBal.ulocked, secondHighest); - assert.strictEqual(recipBal.clocked, secondHighest); + senderBalBefore = await wallet.getBalance(); + assert.strictEqual(senderBalBefore.tx, 7); + assert.strictEqual(senderBalBefore.coin, 3); + assert.strictEqual(senderBalBefore.confirmed, fund - secondHighest - (cTXCount * fee)); + assert.strictEqual(senderBalBefore.unconfirmed, fund - secondHighest - (uTXCount * fee)); + assert.strictEqual(senderBalBefore.ulocked, 0); + assert.strictEqual(senderBalBefore.clocked, 0); + + recipBalBefore = await recip.getBalance(); + assert.strictEqual(recipBalBefore.tx, 1); + assert.strictEqual(recipBalBefore.coin, 1); + assert.strictEqual(recipBalBefore.confirmed, secondHighest); + assert.strictEqual(recipBalBefore.unconfirmed, secondHighest); + assert.strictEqual(recipBalBefore.ulocked, secondHighest); + assert.strictEqual(recipBalBefore.clocked, secondHighest); + }); + + it('should have correct balance after rescan', async () => { + await wdb.rescan(0); + + const senderBalAfter = await wallet.getBalance(); + const recipBalAfter = await recip.getBalance(); + + assert.deepStrictEqual(senderBalAfter, senderBalBefore); + assert.deepStrictEqual(recipBalAfter, recipBalBefore); }); it('should disconnect FINALIZE', async () => { @@ -2257,21 +2269,21 @@ describe('Wallet', function() { cTXCount--; // Check - const senderBal = await wallet.getBalance(); - assert.strictEqual(senderBal.tx, 7); - assert.strictEqual(senderBal.coin, 3); - assert.strictEqual(senderBal.confirmed, fund - (cTXCount * fee)); - assert.strictEqual(senderBal.unconfirmed, fund - secondHighest - (uTXCount * fee)); - assert.strictEqual(senderBal.ulocked, 0); - assert.strictEqual(senderBal.clocked, secondHighest); - - const recipBal = await recip.getBalance(); - assert.strictEqual(recipBal.tx, 1); - assert.strictEqual(recipBal.coin, 1); - assert.strictEqual(recipBal.confirmed, 0); - assert.strictEqual(recipBal.unconfirmed, secondHighest); - assert.strictEqual(recipBal.ulocked, secondHighest); - assert.strictEqual(recipBal.clocked, 0); + const senderBal4 = await wallet.getBalance(); + assert.strictEqual(senderBal4.tx, 7); + assert.strictEqual(senderBal4.coin, 3); + assert.strictEqual(senderBal4.confirmed, fund - (cTXCount * fee)); + assert.strictEqual(senderBal4.unconfirmed, fund - secondHighest - (uTXCount * fee)); + assert.strictEqual(senderBal4.ulocked, 0); + assert.strictEqual(senderBal4.clocked, secondHighest); + + const recipBal4 = await recip.getBalance(); + assert.strictEqual(recipBal4.tx, 1); + assert.strictEqual(recipBal4.coin, 1); + assert.strictEqual(recipBal4.confirmed, 0); + assert.strictEqual(recipBal4.unconfirmed, secondHighest); + assert.strictEqual(recipBal4.ulocked, secondHighest); + assert.strictEqual(recipBal4.clocked, 0); }); }); @@ -2280,11 +2292,23 @@ describe('Wallet', function() { const workers = new WorkerPool({ enabled }); const wdb = new WalletDB({ network, workers }); const name = 'satoshi'; + const nameHash = rules.hashName(name); const value = 1e6; const lockup = 2e6; + const secondHighest = value - 1; const fee = 10000; + const fund = 10e6; + let uTXCount = 0; + let cTXCount = 0; + let start; let wallet; + // Hack required to focus test on txdb mechanics. + // We don't otherwise need WalletDB or Blockchain + wdb.getRenewalBlock = () => { + return network.genesis.hash; + }; + before(async () => { await wdb.open(); wallet = await wdb.create(); @@ -2302,7 +2326,7 @@ describe('Wallet', function() { // Fund wallet const mtx = new MTX(); mtx.addOutpoint(new Outpoint(Buffer.alloc(32), 0)); - mtx.addOutput(addr, 10e6); + mtx.addOutput(addr, fund); const tx = mtx.toTX(); // Dummy block @@ -2319,8 +2343,8 @@ describe('Wallet', function() { const bal = await wallet.getBalance(); assert.strictEqual(bal.tx, 1); assert.strictEqual(bal.coin, 1); - assert.strictEqual(bal.confirmed, 10e6); - assert.strictEqual(bal.unconfirmed, 10e6); + assert.strictEqual(bal.confirmed, fund); + assert.strictEqual(bal.unconfirmed, fund); assert.strictEqual(bal.ulocked, 0); assert.strictEqual(bal.clocked, 0); }); @@ -2332,8 +2356,8 @@ describe('Wallet', function() { let bal = await wallet.getBalance(); assert.strictEqual(bal.tx, 1); assert.strictEqual(bal.coin, 1); - assert.strictEqual(bal.confirmed, 10e6); - assert.strictEqual(bal.unconfirmed, 10e6); + assert.strictEqual(bal.confirmed, fund); + assert.strictEqual(bal.unconfirmed, fund); assert.strictEqual(bal.ulocked, 0); assert.strictEqual(bal.clocked, 0); @@ -2344,13 +2368,16 @@ describe('Wallet', function() { time: Date.now() }; await wallet.txdb.add(open.toTX(), block); + start = wdb.height; + uTXCount++; + cTXCount++; // Check bal = await wallet.getBalance(); assert.strictEqual(bal.tx, 2); assert.strictEqual(bal.coin, 2); - assert.strictEqual(bal.confirmed, 10e6 - (1 * fee)); - assert.strictEqual(bal.unconfirmed, 10e6 - (1 * fee)); + assert.strictEqual(bal.confirmed, fund - (cTXCount * fee)); + assert.strictEqual(bal.unconfirmed, fund - (uTXCount * fee)); assert.strictEqual(bal.ulocked, 0); assert.strictEqual(bal.clocked, 0); }); @@ -2377,13 +2404,15 @@ describe('Wallet', function() { time: Date.now() }; await wallet.txdb.add(bid.toTX(), block); + uTXCount++; + cTXCount++; // Check bal = await wallet.getBalance(); assert.strictEqual(bal.tx, 3); assert.strictEqual(bal.coin, 3); - assert.strictEqual(bal.confirmed, 10e6 - (2 * fee)); - assert.strictEqual(bal.unconfirmed, 10e6 - (2 * fee)); + assert.strictEqual(bal.confirmed, fund - (cTXCount * fee)); + assert.strictEqual(bal.unconfirmed, fund - (uTXCount * fee)); assert.strictEqual(bal.ulocked, lockup); assert.strictEqual(bal.clocked, lockup); }); @@ -2410,15 +2439,152 @@ describe('Wallet', function() { time: Date.now() }; await wallet.txdb.add(reveal.toTX(), block); + uTXCount++; + cTXCount++; // Check bal = await wallet.getBalance(); assert.strictEqual(bal.tx, 4); assert.strictEqual(bal.coin, 4); + assert.strictEqual(bal.confirmed, fund - (cTXCount * fee)); + assert.strictEqual(bal.unconfirmed, fund - (uTXCount * fee)); + assert.strictEqual(bal.ulocked, value); + assert.strictEqual(bal.clocked, value); + }); + + it('should add external REVEAL to txdb', async () => { + // The goal is to have a "second-highest" bid + // so the wallet doesn't win the name for free. + // We can skip the whole BID/lockup thing for these tests. + + const output = new Output(); + output.value = secondHighest; + output.covenant.type = types.REVEAL; + output.covenant.pushHash(nameHash); + output.covenant.pushU32(start); + output.covenant.push(Buffer.from(name, 'ascii')); + output.covenant.pushHash(Buffer.alloc(32)); + + const mtx = new MTX(); + mtx.outputs.push(output); + + // Confirm external REVEAL + const block = { + height: wdb.height + 1, + hash: Buffer.alloc(32), + time: Date.now() + }; + await wallet.txdb.add(mtx.toTX(), block); + }); + + it('should confirm new REGISTER', async () => { + // Advance to close + wdb.height += network.names.revealPeriod; + + const resource = Resource.fromJSON({records: []}); + const register = await wallet.createUpdate(name, resource, {hardFee: fee}); + + // Check + let bal = await wallet.getBalance(); + assert.strictEqual(bal.tx, 4); + assert.strictEqual(bal.coin, 4); assert.strictEqual(bal.confirmed, 10e6 - (3 * fee)); assert.strictEqual(bal.unconfirmed, 10e6 - (3 * fee)); assert.strictEqual(bal.ulocked, value); assert.strictEqual(bal.clocked, value); + + // Confirm REGISTER + const block = { + height: wdb.height + 1, + hash: Buffer.alloc(32), + time: Date.now() + }; + await wallet.txdb.add(register.toTX(), block); + uTXCount++; + cTXCount++; + + // Check + bal = await wallet.getBalance(); + assert.strictEqual(bal.tx, 5); + // Wallet coin count doesn't change: + // REVEAL + fee money -> REGISTER + change + assert.strictEqual(bal.coin, 4); + assert.strictEqual(bal.confirmed, fund - (cTXCount * fee)); + assert.strictEqual(bal.unconfirmed, fund - (uTXCount * fee)); + assert.strictEqual(bal.ulocked, secondHighest); + assert.strictEqual(bal.clocked, secondHighest); + }); + + it('should confirm new TRANSFER', async () => { + const addr = new Address({ + version: 0, + hash: Buffer.alloc(20, 0x88) + }); + const transfer = await wallet.createTransfer(name, addr, {hardFee: fee}); + + // Check + let bal = await wallet.getBalance(); + assert.strictEqual(bal.tx, 5); + assert.strictEqual(bal.coin, 4); + assert.strictEqual(bal.confirmed, fund - (cTXCount * fee)); + assert.strictEqual(bal.unconfirmed, fund - (uTXCount * fee)); + assert.strictEqual(bal.ulocked, secondHighest); + assert.strictEqual(bal.clocked, secondHighest); + + // Confirm TRANSFER + const block = { + height: wdb.height + 1, + hash: Buffer.alloc(32), + time: Date.now() + }; + await wallet.txdb.add(transfer.toTX(), block); + uTXCount++; + cTXCount++; + + // Check + bal = await wallet.getBalance(); + assert.strictEqual(bal.tx, 6); + assert.strictEqual(bal.coin, 4); + assert.strictEqual(bal.confirmed, fund - (cTXCount * fee)); + assert.strictEqual(bal.unconfirmed, fund - (uTXCount * fee)); + assert.strictEqual(bal.ulocked, secondHighest); + assert.strictEqual(bal.clocked, secondHighest); + }); + + it('should confirm new FINALIZE', async () => { + // Advance past lockup + wdb.height += network.names.transferLockup + 1; + + const finalize = await wallet.createFinalize(name, {hardFee: fee}); + + // Check + let bal = await wallet.getBalance(); + assert.strictEqual(bal.tx, 6); + assert.strictEqual(bal.coin, 4); + assert.strictEqual(bal.confirmed, fund - (cTXCount * fee)); + assert.strictEqual(bal.unconfirmed, fund - (uTXCount * fee)); + assert.strictEqual(bal.ulocked, secondHighest); + assert.strictEqual(bal.clocked, secondHighest); + + // Confirm FINALIZE + const block = { + height: wdb.height + 1, + hash: Buffer.alloc(32), + time: Date.now() + }; + await wallet.txdb.add(finalize.toTX(), block); + uTXCount++; + cTXCount++; + + // Check + bal = await wallet.getBalance(); + assert.strictEqual(bal.tx, 7); + // Coin count reduced by giving away name UTXO + assert.strictEqual(bal.coin, 3); + assert.strictEqual(bal.confirmed, fund - secondHighest - (cTXCount * fee)); + assert.strictEqual(bal.unconfirmed, fund - secondHighest - (uTXCount * fee)); + assert.strictEqual(bal.ulocked, 0); + assert.strictEqual(bal.clocked, 0); }); });