From c6d7c43485c0b794e7cb724c9deb638950def375 Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Mon, 16 Oct 2017 21:09:49 -0700 Subject: [PATCH] wallet: tests passing. --- lib/wallet/txdb.js | 215 +++++++---------------------------------- lib/wallet/walletdb.js | 6 -- test/wallet-test.js | 211 ++++++++++++++++++++-------------------- 3 files changed, 138 insertions(+), 294 deletions(-) diff --git a/lib/wallet/txdb.js b/lib/wallet/txdb.js index f580da9d..ab0cafbe 100644 --- a/lib/wallet/txdb.js +++ b/lib/wallet/txdb.js @@ -199,10 +199,10 @@ TXDB.prototype.saveCredit = async function saveCredit(b, credit, path) { const coin = credit.coin; const raw = credit.toRaw(); - await this.addOutpointMap(b, coin.hash, coin.index); - b.put(layout.c(coin.hash, coin.index), raw); b.put(layout.C(path.account, coin.hash, coin.index), null); + + return this.addOutpointMap(b, coin.hash, coin.index); }; /** @@ -214,10 +214,10 @@ TXDB.prototype.saveCredit = async function saveCredit(b, credit, path) { TXDB.prototype.removeCredit = async function removeCredit(b, credit, path) { const coin = credit.coin; - await this.removeOutpointMap(b, coin.hash, coin.index); - b.del(layout.c(coin.hash, coin.index)); b.del(layout.C(path.account, coin.hash, coin.index)); + + return this.removeOutpointMap(b, coin.hash, coin.index); }; /** @@ -247,94 +247,6 @@ TXDB.prototype.unspendCredit = function unspendCredit(b, tx, index) { b.del(layout.d(spender.hash, spender.index)); }; -/** - * Write input record. - * @param {TX} tx - * @param {Number} index - */ - -TXDB.prototype.writeInput = function writeInput(b, tx, index) { - const prevout = tx.inputs[index].prevout; - const spender = Outpoint.fromTX(tx, index); - b.put(layout.s(prevout.hash, prevout.index), spender.toRaw()); -}; - -/** - * Remove input record. - * @param {TX} tx - * @param {Number} index - */ - -TXDB.prototype.removeInput = function removeInput(b, tx, index) { - const prevout = tx.inputs[index].prevout; - b.del(layout.s(prevout.hash, prevout.index)); -}; - -/** - * Resolve orphan input. - * @param {TX} tx - * @param {Number} index - * @param {Number} height - * @param {Path} path - * @returns {Boolean} - */ - -TXDB.prototype.resolveInput = async function resolveInput(b, state, tx, index, height, path, own) { - const hash = tx.hash('hex'); - const spent = await this.getSpent(hash, index); - - if (!spent) - return false; - - // If we have an undo coin, we - // already knew about this input. - if (await this.hasSpentCoin(spent)) - return false; - - // Get the spending transaction so - // we can properly add the undo coin. - const stx = await this.getTX(spent.hash); - assert(stx); - - // Crete the credit and add the undo coin. - const credit = Credit.fromTX(tx, index, height); - credit.own = own; - - this.spendCredit(b, credit, stx.tx, spent.index); - - // If the spender is unconfirmed, save - // the credit as well, and mark it as - // unspent in the mempool. This is the - // same behavior `insert` would have - // done for inputs. We're just doing - // it retroactively. - if (stx.height === -1) { - credit.spent = true; - await this.saveCredit(b, credit, path); - if (height !== -1) - state.confirmed += credit.coin.value; - } - - return true; -}; - -/** - * Test an entire transaction to see - * if any of its outpoints are a double-spend. - * @param {TX} tx - * @returns {Promise} - Returns Boolean. - */ - -TXDB.prototype.isDoubleSpend = async function isDoubleSpend(tx) { - for (const {prevout} of tx.inputs) { - const spent = await this.isSpent(prevout.hash, prevout.index); - if (spent) - return true; - } - - return false; -}; - /** * Test a whether a coin has been spent. * @param {Hash} hash @@ -461,23 +373,23 @@ TXDB.prototype.getBlock = async function getBlock(height) { TXDB.prototype.addBlock = async function addBlock(b, hash, meta) { const key = layout.b(meta.height); - - let data = await this.get(key); - let block; + const data = await this.get(key); if (!data) { - block = BlockRecord.fromMeta(meta); - data = block.toRaw(); + const block = BlockRecord.fromMeta(meta); + block.add(hash); + b.put(key, block.toRaw()); + return; } - block = Buffer.allocUnsafe(data.length + 32); - data.copy(block, 0); + const raw = Buffer.allocUnsafe(data.length + 32); + data.copy(raw, 0); - const size = block.readUInt32LE(40, true); - block.writeUInt32LE(size + 1, 40, true); - hash.copy(block, data.length); + const size = raw.readUInt32LE(40, true); + raw.writeUInt32LE(size + 1, 40, true); + hash.copy(raw, data.length); - b.put(key, block); + b.put(key, raw); }; /** @@ -504,10 +416,10 @@ TXDB.prototype.removeBlock = async function removeBlock(b, hash, height) { return; } - const block = data.slice(0, -32); - block.writeUInt32LE(size - 1, 40, true); + const raw = data.slice(0, -32); + raw.writeUInt32LE(size - 1, 40, true); - b.put(key, block); + b.put(key, raw); }; /** @@ -607,8 +519,7 @@ TXDB.prototype.add = async function add(tx, block) { TXDB.prototype.insert = async function insert(wtx, block) { const b = this.bucket(); const state = this.state.clone(); - const tx = wtx.tx; - const hash = wtx.hash; + const {tx, hash} = wtx; const height = block ? block.height : -1; const details = new Details(this, wtx, block); const accounts = new Set(); @@ -620,35 +531,13 @@ TXDB.prototype.insert = async function insert(wtx, block) { // We need to potentially spend some coins here. for (let i = 0; i < tx.inputs.length; i++) { const input = tx.inputs[i]; - const prevout = input.prevout; - const credit = await this.getCredit(prevout.hash, prevout.index); + const {hash, index} = input.prevout; + const credit = await this.getCredit(hash, index); - if (!credit) { - // Maintain an stxo list for every - // spent input (even ones we don't - // recognize). This is used for - // detecting double-spends (as best - // we can), as well as resolving - // inputs we didn't know were ours - // at the time. This built-in error - // correction is not technically - // necessary assuming no messages - // are ever missed from the mempool, - // but shit happens. - this.writeInput(b, tx, i); + if (!credit) continue; - } const coin = credit.coin; - - // Do some verification. - if (!block) { - if (!await this.verifyInput(tx, i, coin)) { - this.clear(); - return null; - } - } - const path = await this.getPath(coin); assert(path); @@ -705,13 +594,6 @@ TXDB.prototype.insert = async function insert(wtx, block) { details.setOutput(i, path); accounts.add(path.account); - // Attempt to resolve an input we - // did not know was ours at the time. - if (await this.resolveInput(b, state, tx, i, height, path, own)) { - updated = true; - continue; - } - const credit = Credit.fromTX(tx, i, height); credit.own = own; @@ -728,11 +610,8 @@ TXDB.prototype.insert = async function insert(wtx, block) { // If this didn't update any coins, // it's not our transaction. - if (!updated) { - // Clear the spent list inserts. - this.clear(); + if (!updated) return null; - } // Save and index the transaction record. b.put(layout.t(hash), wtx.toRaw()); @@ -821,7 +700,7 @@ TXDB.prototype.confirm = async function confirm(hash, block) { */ TXDB.prototype._confirm = async function _confirm(wtx, block) { - const b = this.batch(); + const b = this.bucket(); const state = this.state.clone(); const tx = wtx.tx; const hash = wtx.hash; @@ -839,14 +718,14 @@ TXDB.prototype._confirm = async function _confirm(wtx, block) { // from the utxo state. for (let i = 0; i < tx.inputs.length; i++) { const input = tx.inputs[i]; - const prevout = input.prevout; + const {hash, index} = input.prevout; let credit = credits[i]; // There may be new credits available // that we haven't seen yet. if (!credit) { - credit = await this.getCredit(prevout.hash, prevout.index); + credit = await this.getCredit(hash, index); if (!credit) continue; @@ -904,10 +783,8 @@ TXDB.prototype._confirm = async function _confirm(wtx, block) { // Update coin height and confirmed // balance. Save once again. - const coin = credit.coin; - coin.height = height; - state.confirmed += output.value; + credit.coin.height = height; await this.saveCredit(b, credit, path); } @@ -970,8 +847,7 @@ TXDB.prototype.remove = async function remove(hash) { TXDB.prototype.erase = async function erase(wtx, block) { const b = this.bucket(); const state = this.state.clone(); - const tx = wtx.tx; - const hash = wtx.hash; + const {tx, hash} = wtx; const height = block ? block.height : -1; const details = new Details(this, wtx, block); const accounts = new Set(); @@ -985,13 +861,8 @@ TXDB.prototype.erase = async function erase(wtx, block) { for (let i = 0; i < tx.inputs.length; i++) { const credit = credits[i]; - if (!credit) { - // This input never had an undo - // coin, but remove it from the - // stxo set. - this.removeInput(b, tx, i); + if (!credit) continue; - } const coin = credit.coin; const path = await this.getPath(coin); @@ -1166,9 +1037,7 @@ TXDB.prototype.unconfirm = async function unconfirm(hash) { TXDB.prototype.disconnect = async function disconnect(wtx, block) { const b = this.bucket(); const state = this.state.clone(); - const tx = wtx.tx; - const hash = wtx.hash; - const height = block.height; + const {tx, hash, height} = wtx; const details = new Details(this, wtx, block); const accounts = new Set(); @@ -1265,7 +1134,7 @@ TXDB.prototype.disconnect = async function disconnect(wtx, block) { this.state = state; this.emit('unconfirmed', tx, details); - this.emit('balance', this.pending.toBalance(), details); + this.emit('balance', state.toBalance(), details); return details; }; @@ -1352,24 +1221,6 @@ TXDB.prototype.removeConflicts = async function removeConflicts(tx, conf) { return true; }; -/** - * Attempt to verify an input. - * @private - * @param {TX} tx - * @param {Number} index - * @param {Coin} coin - * @returns {Promise} - */ - -TXDB.prototype.verifyInput = async function verifyInput(tx, index, coin) { - const flags = Script.flags.MANDATORY_VERIFY_FLAGS; - - if (!this.options.verify) - return true; - - return await tx.verifyInputAsync(index, coin, flags); -}; - /** * Lock all coins in a transaction. * @param {TX} tx @@ -2173,7 +2024,7 @@ TXDB.prototype.hasSpentCoin = function hasSpentCoin(spent) { * @returns {Promise} */ -TXDB.prototype.updateSpentCoin = async function updateSpentCoin(tx, index, height) { +TXDB.prototype.updateSpentCoin = async function updateSpentCoin(b, tx, index, height) { const prevout = Outpoint.fromTX(tx, index); const spent = await this.getSpent(prevout.hash, prevout.index); @@ -2187,7 +2038,7 @@ TXDB.prototype.updateSpentCoin = async function updateSpentCoin(tx, index, heigh coin.height = height; - this.put(layout.d(spent.hash, spent.index), coin.toRaw()); + b.put(layout.d(spent.hash, spent.index), coin.toRaw()); }; /** diff --git a/lib/wallet/walletdb.js b/lib/wallet/walletdb.js index 4ef1c468..4e9f2ea3 100644 --- a/lib/wallet/walletdb.js +++ b/lib/wallet/walletdb.js @@ -42,12 +42,6 @@ const U32 = encoding.U32; * @alias module:wallet.WalletDB * @constructor * @param {Object} options - * @param {String?} options.name - Database name. - * @param {String?} options.location - Database file location. - * @param {String?} options.db - Database backend (`"leveldb"` by default). - * @param {Boolean?} options.verify - Verify transactions as they - * come in (note that this will not happen on the worker pool). - * @property {Boolean} loaded */ function WalletDB(options) { diff --git a/test/wallet-test.js b/test/wallet-test.js index 2228a7ae..d76ba203 100644 --- a/test/wallet-test.js +++ b/test/wallet-test.js @@ -19,6 +19,7 @@ const Input = require('../lib/primitives/input'); const Outpoint = require('../lib/primitives/outpoint'); const Script = require('../lib/script/script'); const HD = require('../lib/hd'); +const U32 = encoding.U32; const KEY1 = 'xprv9s21ZrQH143K3Aj6xQBymM31Zb4BVc7wxqfUhMZrzewdDVCt' + 'qUP9iWfcHgJofs25xbaUpCps9GDXj83NiWvQCAkWQhVj5J4CorfnpKX94AZ'; @@ -26,15 +27,9 @@ const KEY1 = 'xprv9s21ZrQH143K3Aj6xQBymM31Zb4BVc7wxqfUhMZrzewdDVCt' const KEY2 = 'xprv9s21ZrQH143K3mqiSThzPtWAabQ22Pjp3uSNnZ53A5bQ4udp' + 'faKekc2m4AChLYH1XDzANhrSdxHYWUeTWjYJwFwWFyHkTMnMeAcW4JyRCZa'; -const workers = new WorkerPool({ - enabled: true -}); - -const wdb = new WalletDB({ - db: 'memory', - verify: true, - workers -}); +const enabled = true; +const workers = new WorkerPool({ enabled }); +const wdb = new WalletDB({ workers }); let currentWallet = null; let importedWallet = null; @@ -42,27 +37,32 @@ let importedKey = null; let doubleSpendWallet = null; let doubleSpendCoin = null; -let globalTime = util.now(); -let globalHeight = 1; +function prevBlock(wdb) { + assert(wdb.state.height > 0); + return fakeBlock(wdb.state.height - 1); +}; -function nextBlock() { - const height = globalHeight++; - const time = globalTime++; +function curBlock(wdb) { + return fakeBlock(wdb.state.height); +}; - const prevHead = encoding.U32(height - 1); - const prevHash = digest.hash256(prevHead); +function nextBlock(wdb) { + return fakeBlock(wdb.state.height + 1); +} - const head = encoding.U32(height); - const hash = digest.hash256(head); +function fakeBlock(height) { + const prev = digest.hash256(U32((height - 1) >>> 0)); + const hash = digest.hash256(U32(height >>> 0)); + const root = digest.hash256(U32((height | 0x80000000) >>> 0)); return { hash: hash.toString('hex'), - height: height, - prevBlock: prevHash.toString('hex'), - time: time, - merkleRoot: encoding.NULL_HASH, + prevBlock: prev.toString('hex'), + merkleRoot: root.toString('hex'), + time: 500000000 + (height * (10 * 60)), + bits: 0, nonce: 0, - bits: 0 + height: height }; } @@ -73,10 +73,7 @@ function dummyInput() { async function testP2PKH(witness, nesting) { const flags = Script.flags.STANDARD_VERIFY_FLAGS; - - const wallet = await wdb.create({ - witness - }); + const wallet = await wdb.create({ witness }); const addr = Address.fromString(wallet.getAddress('string')); @@ -157,11 +154,9 @@ async function testP2SH(witness, nesting) { fund.addOutput(nesting ? nestedAddr1 : addr1, 5460 * 10); // Simulate a confirmation - const block = nextBlock(); - assert.strictEqual(alice.account[receiveDepth], 1); - await wdb.addBlock(block, [fund.toTX()]); + await wdb.addBlock(nextBlock(wdb), [fund.toTX()]); assert.strictEqual(alice.account[receiveDepth], 2); assert.strictEqual(alice.account.changeDepth, 1); @@ -205,9 +200,7 @@ async function testP2SH(witness, nesting) { // Simulate a confirmation { - const block = nextBlock(); - - await wdb.addBlock(block, [tx]); + await wdb.addBlock(nextBlock(wdb), [tx]); assert.strictEqual(alice.account[receiveDepth], 2); assert.strictEqual(alice.account.changeDepth, 2); @@ -360,53 +353,39 @@ describe('Wallet', function() { // balance: 11000 await alice.sign(f1); - const fake = new MTX(); - fake.addTX(t1, 1); // 1000 (already redeemed) - fake.addOutput(alice.getAddress(), 500); - - // Script inputs but do not sign - await alice.template(fake); - // Fake signature - const input = fake.inputs[0]; - input.script.setData(0, encoding.ZERO_SIG); - input.script.compile(); - // balance: 11000 - - // Fake TX should temporarily change output. { - await wdb.addTX(fake.toTX()); await wdb.addTX(t4.toTX()); - const balance = await alice.getBalance(); - assert.strictEqual(balance.unconfirmed, 22500); - } - - { - await wdb.addTX(t1.toTX()); - - const balance = await alice.getBalance(); - assert.strictEqual(balance.unconfirmed, 72500); - } - - { - await wdb.addTX(t2.toTX()); - - const balance = await alice.getBalance(); - assert.strictEqual(balance.unconfirmed, 46500); - } - - { - await wdb.addTX(t3.toTX()); - const balance = await alice.getBalance(); assert.strictEqual(balance.unconfirmed, 22000); } + { + await wdb.addTX(t1.toTX()); + + const balance = await alice.getBalance(); + assert.strictEqual(balance.unconfirmed, 73000); + } + + { + await wdb.addTX(t2.toTX()); + + const balance = await alice.getBalance(); + assert.strictEqual(balance.unconfirmed, 71000); + } + + { + await wdb.addTX(t3.toTX()); + + const balance = await alice.getBalance(); + assert.strictEqual(balance.unconfirmed, 69000); + } + { await wdb.addTX(f1.toTX()); const balance = await alice.getBalance(); - assert.strictEqual(balance.unconfirmed, 11000); + assert.strictEqual(balance.unconfirmed, 58000); const txs = await alice.getHistory(); assert(txs.some((wtx) => { @@ -423,11 +402,46 @@ describe('Wallet', function() { return wtx.tx.hash('hex') === f1.hash('hex'); })); } + + // Should recover from missed txs on block. + await wdb.addBlock(nextBlock(wdb), [ + t1.toTX(), + t2.toTX(), + t3.toTX(), + t4.toTX(), + f1.toTX() + ]); + + { + const balance = await alice.getBalance(); + assert.strictEqual(balance.unconfirmed, 11000); + assert.strictEqual(balance.confirmed, 11000); + + const txs = await alice.getHistory(); + assert(txs.some((wtx) => { + return wtx.hash === f1.hash('hex'); + })); + } + + { + const balance = await bob.getBalance(); + assert.strictEqual(balance.unconfirmed, 10000); + assert.strictEqual(balance.confirmed, 10000); + + const txs = await bob.getHistory(); + assert(txs.some((wtx) => { + return wtx.tx.hash('hex') === f1.hash('hex'); + })); + } + }); it('should cleanup spenders after double-spend', async () => { const wallet = doubleSpendWallet; + // Reorg and unconfirm all previous txs. + await wdb.removeBlock(curBlock(wdb)); + { const txs = await wallet.getHistory(); assert.strictEqual(txs.length, 5); @@ -441,6 +455,7 @@ describe('Wallet', function() { { const balance = await wallet.getBalance(); assert.strictEqual(balance.unconfirmed, 11000); + assert.strictEqual(balance.confirmed, 0); } { @@ -468,14 +483,6 @@ describe('Wallet', function() { }); it('should handle missed txs without resolution', async () => { - const wdb = new WalletDB({ - name: 'wallet-test', - db: 'memory', - verify: false - }); - - await wdb.open(); - const alice = await wdb.create(); const bob = await wdb.create(); @@ -534,20 +541,20 @@ describe('Wallet', function() { { await wdb.addTX(t2.toTX()); const balance = await alice.getBalance(); - assert.strictEqual(balance.unconfirmed, 47000); + assert.strictEqual(balance.unconfirmed, 71000); } { await wdb.addTX(t3.toTX()); const balance = await alice.getBalance(); - assert.strictEqual(balance.unconfirmed, 22000); + assert.strictEqual(balance.unconfirmed, 69000); } { await wdb.addTX(f1.toTX()); const balance = await alice.getBalance(); - assert.strictEqual(balance.unconfirmed, 11000); + assert.strictEqual(balance.unconfirmed, 58000); const txs = await alice.getHistory(); assert(txs.some((wtx) => { @@ -565,10 +572,14 @@ describe('Wallet', function() { })); } - await wdb.addTX(t2.toTX()); - await wdb.addTX(t3.toTX()); - await wdb.addTX(t4.toTX()); - await wdb.addTX(f1.toTX()); + // Should recover from missed txs on block. + await wdb.addBlock(nextBlock(wdb), [ + t1.toTX(), + t2.toTX(), + t3.toTX(), + t4.toTX(), + f1.toTX() + ]); { const balance = await alice.getBalance(); @@ -1067,9 +1078,7 @@ describe('Wallet', function() { t2.addOutput(alice.getAddress(), 5460); t2.addOutput(alice.getAddress(), 5460); - const block = nextBlock(); - - await wdb.addBlock(block, [t2.toTX()]); + await wdb.addBlock(nextBlock(wdb), [t2.toTX()]); { const coins = await alice.getSmartCoins(); @@ -1077,7 +1086,7 @@ describe('Wallet', function() { for (let i = 0; i < coins.length; i++) { const coin = coins[i]; - assert.strictEqual(coin.height, block.height); + assert.strictEqual(coin.height, wdb.state.height); } } @@ -1104,7 +1113,7 @@ describe('Wallet', function() { assert(coin.value < 5460); found = true; } else { - assert.strictEqual(coin.height, block.height); + assert.strictEqual(coin.height, wdb.state.height); } total += coin.value; } @@ -1136,7 +1145,7 @@ describe('Wallet', function() { assert(coin.value < 5460); found = true; } else { - assert.strictEqual(coin.height, block.height); + assert.strictEqual(coin.height, wdb.state.height); } } @@ -1337,12 +1346,7 @@ describe('Wallet', function() { }); it('should recover from a missed tx', async () => { - const wdb = new WalletDB({ - name: 'wallet-test', - db: 'memory', - verify: false - }); - + const wdb = new WalletDB({ workers }); await wdb.open(); const alice = await wdb.create({ @@ -1393,21 +1397,16 @@ describe('Wallet', function() { assert.strictEqual((await alice.getBalance()).unconfirmed, 30000); // Bob sees t2 on the chain. - await bob.add(t2.toTX()); + await bob.add(t2.toTX(), nextBlock(wdb)); // Bob sees t3 on the chain. - await bob.add(t3.toTX()); + await bob.add(t3.toTX(), nextBlock(wdb)); assert.strictEqual((await bob.getBalance()).unconfirmed, 30000); }); it('should recover from a missed tx and double spend', async () => { - const wdb = new WalletDB({ - name: 'wallet-test', - db: 'memory', - verify: false - }); - + const wdb = new WalletDB({ workers }); await wdb.open(); const alice = await wdb.create({ @@ -1468,10 +1467,10 @@ describe('Wallet', function() { assert.strictEqual((await alice.getBalance()).unconfirmed, 30000); // Bob sees t2a on the chain. - await bob.add(t2a.toTX()); + await bob.add(t2a.toTX(), nextBlock(wdb)); // Bob sees t3 on the chain. - await bob.add(t3.toTX()); + await bob.add(t3.toTX(), nextBlock(wdb)); assert.strictEqual((await bob.getBalance()).unconfirmed, 30000); });