From 91fe95100cd42583b4c774b39edbe49afa27e550 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Sun, 1 Sep 2019 16:03:29 -0700 Subject: [PATCH 1/2] wallet: create txdb credit for known key paths when confirming --- lib/wallet/txdb.js | 14 +++++++-- test/wallet-test.js | 72 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 83 insertions(+), 3 deletions(-) diff --git a/lib/wallet/txdb.js b/lib/wallet/txdb.js index 47474d701..bd470c5a9 100644 --- a/lib/wallet/txdb.js +++ b/lib/wallet/txdb.js @@ -1157,8 +1157,18 @@ class TXDB { details.setOutput(i, path); - const credit = await this.getCredit(hash, i); - assert(credit); + let credit = await this.getCredit(hash, i); + + if (!credit) { + // This credit didn't belong to us the first time we + // saw the transaction (before confirmation or rescan). + // Create new credit for database. + credit = Credit.fromTX(tx, i, height); + + // Add coin to "unconfirmed" balance (which includes confirmed coins) + state.coin(path, 1); + state.unconfirmed(path, credit.coin.value); + } // Credits spent in the mempool add an // undo coin for ease. If this credit is diff --git a/test/wallet-test.js b/test/wallet-test.js index 20bbc67c6..33984dcfe 100644 --- a/test/wallet-test.js +++ b/test/wallet-test.js @@ -20,6 +20,10 @@ const Outpoint = require('../lib/primitives/outpoint'); const Script = require('../lib/script/script'); const PrivateKey = require('../lib/hd/private.js'); const policy = require('../lib/protocol/policy'); +const HD = require('../lib/hd'); +const Wallet = require('../lib/wallet/wallet'); +const nodejsUtil = require('util'); +const HDPrivateKey = require('../lib/hd/private'); const KEY1 = 'xprv9s21ZrQH143K3Aj6xQBymM31Zb4BVc7wxqfUhMZrzewdDVCt' + 'qUP9iWfcHgJofs25xbaUpCps9GDXj83NiWvQCAkWQhVj5J4CorfnpKX94AZ'; @@ -1220,7 +1224,7 @@ describe('Wallet', function() { ); } - const privateKey = PrivateKey.generate(); + const privateKey = HDPrivateKey.generate(); const xpub = privateKey.xpubkey('main'); watchWallet = await wdb.create({ watchOnly: true, @@ -1572,6 +1576,72 @@ describe('Wallet', function() { }); }); + it('should create credit if not found during confirmation', async () => { + // Create wallet and get one address + const wallet = await wdb.create(); + const addr1 = await wallet.receiveAddress(); + + // Outside the wallet, generate a second private key and address. + const key2 = HDPrivateKey.generate(); + const ring2 = KeyRing.fromPrivate(key2.privateKey); + const addr2 = ring2.getAddress(); + + // Build TX to both addresses, known and unknown + const mtx = new MTX(); + mtx.addOutpoint(new Outpoint(Buffer.alloc(32), 0)); + mtx.addOutput(addr1, 1020304); + mtx.addOutput(addr2, 4030201); + const tx = mtx.toTX(); + const hash = tx.hash(); + + // Add unconfirmed TX to txdb (no block provided) + await wallet.txdb.add(tx, null); + + // Check + const bal1 = await wallet.getBalance(); + assert.strictEqual(bal1.tx, 1); + assert.strictEqual(bal1.coin, 1); + assert.strictEqual(bal1.confirmed, 0); + assert.strictEqual(bal1.unconfirmed, 1020304); + + // Import private key into wallet + assert(!await wallet.hasAddress(addr2)); + await wallet.importKey('default', ring2); + assert(await wallet.hasAddress(addr2)); + + // Confirm TX with newly-added output address + // Create dummy block + const block = { + height: 100, + hash: Buffer.alloc(32), + time: Date.now() + }; + + // Get TX from txdb + const wtx = await wallet.txdb.getTX(hash); + + // Confirm TX with dummy block in txdb + const details = await wallet.txdb.confirm(wtx, block); + assert.bufferEqual(details.tx.hash(), hash); + + // Check balance + const bal2 = await wallet.getBalance(); + assert.strictEqual(bal2.confirmed, bal2.unconfirmed); + assert.strictEqual(bal2.confirmed, 5050505); + assert.strictEqual(bal2.coin, 2); + assert.strictEqual(bal2.tx, 1); + + // Check for unconfirmed transactions + const pending = await wallet.getPending(); + assert.strictEqual(pending.length, 0); + + // Check history for TX + const history = await wallet.getHistory(); + const wtxs = await wallet.toDetails(history); + assert.strictEqual(wtxs.length, 1); + assert.bufferEqual(wtxs[0].hash, hash); + }); + it('should cleanup', async () => { network.coinbaseMaturity = 2; await wdb.close(); From f42e0d93cce23e5d2c66cfb3664ebc404885ec66 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Mon, 23 Sep 2019 17:41:15 -0400 Subject: [PATCH 2/2] wallet: check ownership of newly created credit when confirming --- lib/wallet/txdb.js | 6 +++ test/wallet-test.js | 97 +++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 99 insertions(+), 4 deletions(-) diff --git a/lib/wallet/txdb.js b/lib/wallet/txdb.js index bd470c5a9..085be1a35 100644 --- a/lib/wallet/txdb.js +++ b/lib/wallet/txdb.js @@ -1083,6 +1083,7 @@ class TXDB { const details = new Details(wtx, block); const state = new BalanceDelta(); const view = new CoinView(); + let own = false; wtx.setBlock(block); @@ -1126,6 +1127,7 @@ class TXDB { const path = await this.getPath(coin); assert(path); + own = true; details.setInput(i, path, coin); @@ -1165,6 +1167,10 @@ class TXDB { // Create new credit for database. credit = Credit.fromTX(tx, i, height); + // If this tx spent any of our own coins, we "own" this output, + // meaning if it becomes unconfirmed, we can still confidently spend it. + credit.own = own; + // Add coin to "unconfirmed" balance (which includes confirmed coins) state.coin(path, 1); state.unconfirmed(path, credit.coin.value); diff --git a/test/wallet-test.js b/test/wallet-test.js index 33984dcfe..165454590 100644 --- a/test/wallet-test.js +++ b/test/wallet-test.js @@ -18,11 +18,7 @@ const KeyRing = require('../lib/primitives/keyring'); const Input = require('../lib/primitives/input'); const Outpoint = require('../lib/primitives/outpoint'); const Script = require('../lib/script/script'); -const PrivateKey = require('../lib/hd/private.js'); const policy = require('../lib/protocol/policy'); -const HD = require('../lib/hd'); -const Wallet = require('../lib/wallet/wallet'); -const nodejsUtil = require('util'); const HDPrivateKey = require('../lib/hd/private'); const KEY1 = 'xprv9s21ZrQH143K3Aj6xQBymM31Zb4BVc7wxqfUhMZrzewdDVCt' @@ -1640,6 +1636,99 @@ describe('Wallet', function() { const wtxs = await wallet.toDetails(history); assert.strictEqual(wtxs.length, 1); assert.bufferEqual(wtxs[0].hash, hash); + + // Both old and new credits are not "owned" + // (created by the wallet spending its own coins) + for (let i = 0; i < tx.outputs.length; i++) { + const credit = await wallet.txdb.getCredit(tx.hash(), i); + assert(!credit.own); + } + }); + + it('should create owned credit if not found during confirm', async () => { + // Create wallet and get one address + const wallet = await wdb.create(); + const addr1 = await wallet.receiveAddress(); + + // Outside the wallet, generate a second private key and address. + const key2 = HDPrivateKey.generate(); + const ring2 = KeyRing.fromPrivate(key2.privateKey); + const addr2 = ring2.getAddress(); + + // Create a confirmed, unspent, wallet-owned credit in txdb + const mtx1 = new MTX(); + mtx1.addOutpoint(new Outpoint(Buffer.alloc(32), 0)); + mtx1.addOutput(addr1, 1 * 1e8); + const tx1 = mtx1.toTX(); + await wallet.txdb.add(tx1, null); + + // Create dummy block + const block1 = { + height: 99, + hash: Buffer.alloc(32), + time: Date.now() + }; + // Get TX from txdb + const wtx1 = await wallet.txdb.getTX(tx1.hash()); + + // Confirm TX with dummy block in txdb + await wallet.txdb.confirm(wtx1, block1); + + // Build TX to both addresses, known and unknown + const mtx2 = new MTX(); + mtx2.addTX(tx1, 0, 99); + mtx2.addOutput(addr1, 1020304); + mtx2.addOutput(addr2, 4030201); + const tx2 = mtx2.toTX(); + const hash = tx2.hash(); + + // Add unconfirmed TX to txdb (no block provided) + await wallet.txdb.add(tx2, null); + + // Check + const bal1 = await wallet.getBalance(); + assert.strictEqual(bal1.tx, 2); + assert.strictEqual(bal1.coin, 1); + assert.strictEqual(bal1.confirmed, 1 * 1e8); + assert.strictEqual(bal1.unconfirmed, 1020304); + + // Import private key into wallet + assert(!await wallet.hasAddress(addr2)); + await wallet.importKey('default', ring2); + assert(await wallet.hasAddress(addr2)); + + // Confirm TX with newly-added output address + // Create dummy block + const block2 = { + height: 100, + hash: Buffer.alloc(32), + time: Date.now() + }; + + // Get TX from txdb + const wtx2 = await wallet.txdb.getTX(hash); + + // Confirm TX with dummy block in txdb + const details = await wallet.txdb.confirm(wtx2, block2); + assert.bufferEqual(details.tx.hash(), hash); + + // Check balance + const bal2 = await wallet.getBalance(); + assert.strictEqual(bal2.confirmed, bal2.unconfirmed); + assert.strictEqual(bal2.confirmed, 5050505); + assert.strictEqual(bal2.coin, 2); + assert.strictEqual(bal2.tx, 2); + + // Check for unconfirmed transactions + const pending = await wallet.getPending(); + assert.strictEqual(pending.length, 0); + + // Both old and new credits are "owned" + // (created by the wallet spending its own coins) + for (let i = 0; i < tx2.outputs.length; i++) { + const credit = await wallet.txdb.getCredit(tx2.hash(), i); + assert(credit.own); + } }); it('should cleanup', async () => {