From 8f77cf7173fd12044212c107c96e15f5f92e34b4 Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Tue, 1 Mar 2016 20:15:57 -0800 Subject: [PATCH] all tests passing. --- lib/bcoin/txdb.js | 291 +++++++++--------------------------- lib/bcoin/wallet.js | 38 +++-- lib/bcoin/walletdb.js | 43 ++++++ test/wallet-test.js | 332 +++++++++++++++++++++--------------------- 4 files changed, 302 insertions(+), 402 deletions(-) diff --git a/lib/bcoin/txdb.js b/lib/bcoin/txdb.js index f4b39425..0031b500 100644 --- a/lib/bcoin/txdb.js +++ b/lib/bcoin/txdb.js @@ -24,10 +24,49 @@ function TXPool(prefix, db) { this.db = db; this.prefix = prefix || 'pool'; + this.busy = false; + this.jobs = []; } utils.inherits(TXPool, EventEmitter); +TXPool.prototype._lock = function _lock(func, args, force) { + var self = this; + var called; + + if (force) { + assert(this.busy); + return function unlock() { + assert(!called); + called = true; + }; + } + + if (this.busy) { + this.jobs.push([func, args]); + return; + } + + this.busy = true; + + return function unlock() { + var item, block; + + assert(!called); + called = true; + + self.busy = false; + + if (self.jobs.length === 0) { + self.emit('flush'); + return; + } + + item = self.jobs.shift(); + item[0].apply(self, item[1]); + }; +}; + TXPool.prototype.add = function add(tx, callback) { var self = this; @@ -37,106 +76,33 @@ TXPool.prototype.add = function add(tx, callback) { }, callback); } - this._filter(tx, function(err, result) { - if (err) - return callback(err); - - if (!result) - return callback(null, false); - - return self._add(tx, callback); - }); -}; - -TXPool.prototype._filter = function _filter(tx, callback) { - return callback(null, true); + return self._add(tx, callback); }; TXPool.prototype._hasAddress = function _hasAddress(address, callback) { - var p = this.prefix + '/'; - var self = this; - callback = utils.ensure(callback); if (!address) return callback(null, false); - var iter = this.db.db.iterator({ - gte: p + 'a/' + address, - lte: p + 'a/' + address + '~', - keys: true, - values: false, - fillCache: false, - keyAsBuffer: false - }); - - (function next() { - iter.next(function(err, key, value) { - if (err) { - return iter.end(function() { - callback(err); - }); - } - - if (key === undefined) { - return iter.end(function(err) { - if (err) - return callback(err); - return callback(null, ids); - }); - } - - return iter.end(function(err) { - if (err) - return callback(err); - callback(null, true); - }); - }); - })(); -}; - -TXPool.prototype._addOrphan = function _addOrphan(orphans, orphan) { - var tx = orphan.tx.toExtended(true); - var buf; - - buf = new Buffer(tx.length + 4); - utils.copy(tx, buf, 0); - utils.writeU32(buf, orphan.index, tx.length); - - if (!orphans) - return buf; - - return Buffer.concat([orphans, buf]); -}; - -TXPool.prototype._fromOrphans = function _fromOrphans(buf) { - var txs = []; - var tx, index, buf; - - if (!buf) - return txs; - - while (buf.length) { - tx = bcoin.tx.fromExtended(buf, true); - index = utils.readU32(buf, tx._extendedSize); - buf = buf.slice(tx._extendedSize + 4); - txs.push({ tx: tx, index: index }); - } - - return txs; + return callback(null, true); }; +// This big scary function is what a persistent tx pool +// looks like. It's a semi mempool in that it can handle +// receiving txs out of order. TXPool.prototype._add = function add(tx, callback) { var self = this; var p = this.prefix + '/'; var hash = tx.hash('hex'); var updated = false; + var batch; callback = utils.ensure(callback); - var batch = this.db.batch(); + batch = this.db.batch(); - self.getTX(hash, function(err, existing) { + this.getTX(hash, function(err, existing) { if (err) return callback(err); @@ -219,6 +185,8 @@ TXPool.prototype._add = function add(tx, callback) { utils.forEachSerial(tx.inputs, function(input, next, i) { var key = input.prevout.hash + '/' + input.prevout.index; self.getCoin(input.prevout.hash, input.prevout.index, function(err, coin) { + var type, address; + if (err) return next(err); @@ -235,8 +203,8 @@ TXPool.prototype._add = function add(tx, callback) { updated = true; - var type = input.getType(); - var address = input.getAddress(); + type = input.getType(); + address = input.getAddress(); if (type === 'pubkey' || type === 'multisig') address = null; @@ -367,16 +335,21 @@ TXPool.prototype._add = function add(tx, callback) { } function finish(err) { + var type, adddress; + if (err) return next(err); if (!orphans) { - var type = output.getType(); - var address = output.getAddress(); + type = output.getType(); + address = output.getAddress(); + if (type === 'pubkey' || type === 'multisig') address = null; + if (address) batch.put(p + 'u/a/' + address + '/' + hash + '/' + i, new Buffer([])); + batch.put(p + 'u/t/' + hash + '/' + i, coin.toRaw()); updated = true; } @@ -653,8 +626,6 @@ TXPool.prototype.remove = function remove(hash, callback) { uaddr = null; } - coinRaw = bcoin.protocol.framer.coin(input.output, true); - if (uaddr) { batch.del(p + 't/a/' + uaddr + '/' + hash); if (tx.ts === 0) @@ -668,10 +639,15 @@ TXPool.prototype.remove = function remove(hash, callback) { new Buffer([])); } - batch.put(p + 'u/t/' - + input.prevout.hash - + '/' + input.prevout.index, - coinRaw); + if (input.output) { + coinRaw = bcoin.protocol.framer.coin(input.output, true); + batch.put(p + 'u/t/' + + input.prevout.hash + + '/' + input.prevout.index, + coinRaw); + } + + batch.del(p + 'o/' + input.prevout.hash + '/' + input.prevout.index); }); tx.outputs.forEach(function(output, i) { @@ -1148,139 +1124,6 @@ TXPool.prototype.getBalanceByAddress = function getBalanceByAddress(address, cal }); }; -function WalletPool(db) { - TXPool.call(this, 'w', db); -} - -utils.inherits(WalletPool, TXPool); - -WalletPool.prototype._filter = function _filter(tx, callback) { - return this.testTX(tx, callback); -}; - -WalletPool.prototype.getAll = function getAll(id, callback) { - var self = this; - return this.getAddresses(id, function(err, addresses) { - if (err) - return callback(err); - return self.getAllByAddress(addresses, callback); - }); -}; - -WalletPool.prototype.getUnspent = function getUnspent(id, callback) { - var self = this; - return this.getAddresses(id, function(err, addresses) { - if (err) - return callback(err); - return self.getUnspentByAddress(addresses, callback); - }); -}; - -WalletPool.prototype.getPending = function getPending(id, callback) { - var self = this; - return this.getAddresses(id, function(err, addresses) { - if (err) - return callback(err); - return self.getPendingByAddress(addresses, callback); - }); -}; - -WalletPool.prototype.getBalance = function getBalance(id, callback) { - var self = this; - return this.getAddresses(id, function(err, addresses) { - if (err) - return callback(err); - return self.getBalanceByAddress(addresses, callback); - }); -}; - -WalletPool.prototype.getAddresses = function getAddresses(id, callback) { - if (typeof id === 'string') - return callback(null, [id]); - - if (Array.isArray(id)) - return callback(null, id); - - if (id.addressMap) - return callback(null, Object.keys(id.addressMap)); - - if (typeof id === 'object') - return callback(null, Object.keys(id)); - - return this.db.get('w/w/' + id, function(err, buf) { - var json; - - if (err) - return callback(err); - - try { - json = JSON.parse(buf.toString('utf8')); - } catch (e) { - return callback(e); - } - - return callback(null, Object.keys(json.addressMap)); - }); -}; - -WalletPool.prototype.getIDs = function _getIDs(address, callback) { - var self = this; - var ids = []; - - var iter = this.db.db.iterator({ - gte: 'w/a/' + address, - lte: 'w/a/' + address + '~', - keys: true, - values: false, - fillCache: false, - keyAsBuffer: false - }); - - callback = utils.ensure(callback); - - (function next() { - iter.next(function(err, key, value) { - if (err) { - return iter.end(function() { - callback(err); - }); - } - - if (key === undefined) { - return iter.end(function(err) { - if (err) - return callback(err); - return callback(null, ids); - }); - } - - ids.push(key.split('/')[3]); - - next(); - }); - })(); -}; - -WalletPool.prototype.testTX = function test(tx, callback) { - var self = this; - - utils.forEachSerial(tx.getAddresses(), function(address, next) { - self.getIDs(address, function(err, ids) { - if (err) - return next(err); - - if (ids.length > 0) - return callback(null, true); - - next(); - }); - }, function(err) { - if (err) - return callback(err); - return callback(null, false); - }); -}; - /** * Expose */ diff --git a/lib/bcoin/wallet.js b/lib/bcoin/wallet.js index 344f9e48..4b98d4af 100644 --- a/lib/bcoin/wallet.js +++ b/lib/bcoin/wallet.js @@ -515,23 +515,39 @@ Wallet.prototype.ownOutput = function ownOutput(tx, index) { return tx.testOutputs(this.addressMap, index); }; -Wallet.prototype.fill = function fill(tx, options) { +Wallet.prototype.fill = function fill(tx, options, callback) { + var self = this; + + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!options) options = {}; assert(this._initialized); - tx.fill(this.getUnspent(), { - selection: options.selection || 'age', - fee: options.fee, - subtractFee: options.subtractFee, - changeAddress: this.changeAddress.getAddress(), - wallet: this, - m: this.m, - n: this.n - }); + this.getUnspent(function(err, unspent) { + if (err) + return callback(err); - return true; + try { + tx.fill(unspent, { + selection: options.selection || 'age', + fee: options.fee, + subtractFee: options.subtractFee, + changeAddress: self.changeAddress.getAddress(), + wallet: self, + m: self.m, + n: self.n + }); + } catch (e) { + return callback(e); + } + + return callback(); + }); }; Wallet.prototype.fillPrevout = function fillPrevout(tx, callback) { diff --git a/lib/bcoin/walletdb.js b/lib/bcoin/walletdb.js index e23c01cc..e0791d95 100644 --- a/lib/bcoin/walletdb.js +++ b/lib/bcoin/walletdb.js @@ -117,6 +117,7 @@ WalletDB.prototype._init = function _init() { this.db = WalletDB._db[this.file]; this.tx = new bcoin.txdb('w', this.db); + this.tx._hasAddress = this.hasAddress.bind(this); }; WalletDB.prototype.getJSON = function getJSON(id, callback) { @@ -602,6 +603,48 @@ WalletDB.prototype.testTX = function test(tx, callback) { }); }; +WalletDB.prototype.hasAddress = function hasAddress(address, callback) { + var self = this; + + callback = utils.ensure(callback); + + if (!address) + return callback(null, false); + + var iter = this.db.db.iterator({ + gte: 'w/a/' + address, + lte: 'w/a/' + address + '~', + keys: true, + values: false, + fillCache: false, + keyAsBuffer: false + }); + + (function next() { + iter.next(function(err, key, value) { + if (err) { + return iter.end(function() { + callback(err); + }); + } + + if (key === undefined) { + return iter.end(function(err) { + if (err) + return callback(err); + return callback(null, false); + }); + } + + return iter.end(function(err) { + if (err) + return callback(err); + callback(null, true); + }); + }); + })(); +}; + /** * Expose */ diff --git a/test/wallet-test.js b/test/wallet-test.js index db4ff28d..e9cda03b 100644 --- a/test/wallet-test.js +++ b/test/wallet-test.js @@ -174,69 +174,44 @@ describe('Wallet', function() { // Fake TX should temporarly change output // w.addTX(fake); - process.on('uncaughtException', function() { - return; - w.db.dump(function(err, records) { - console.log(records); - process.exit(1); - }); - }); - w.addTX(fake, function(err) { - if (err) throw err; assert(!err); - w.addTX(t4, function(err) { - if (err) throw err; - assert(!err); - if (0) { - w.db.tx.getCoinByAddress(Object.keys(w.addressMap), function(err, coins) { - console.log(coins); - cb(); - }); - return; - } - if (0) { - w.db.dump(function(err, records) { - console.log(records); - //process.exit(1); - return cb(); - }); - return; - } - w.getBalance(function(err, balance) { + w.addTX(t4, function(err) { assert(!err); - assert.equal(balance.toString(10), '22500'); - // assert.equal(balance.toString(10), '22000'); - w.addTX(t1, function(err) { - w.getBalance(function(err, balance) { - assert(!err); - assert.equal(balance.toString(10), '73000'); - w.addTX(t2, function(err) { + w.getBalance(function(err, balance) { + assert(!err); + assert.equal(balance.toString(10), '22500'); + w.addTX(t1, function(err) { + w.getBalance(function(err, balance) { assert(!err); - w.getBalance(function(err, balance) { + assert.equal(balance.toString(10), '73000'); + w.addTX(t2, function(err) { assert(!err); - assert.equal(balance.toString(10), '47000'); - w.addTX(t3, function(err) { + w.getBalance(function(err, balance) { assert(!err); - w.getBalance(function(err, balance) { + assert.equal(balance.toString(10), '47000'); + w.addTX(t3, function(err) { assert(!err); - assert.equal(balance.toString(10), '22000'); - w.addTX(f1, function(err) { + w.getBalance(function(err, balance) { assert(!err); - w.getBalance(function(err, balance) { + assert.equal(balance.toString(10), '22000'); + w.addTX(f1, function(err) { assert(!err); - assert.equal(balance.toString(10), '11000'); - w.getAll(function(err, txs) { - assert(txs.some(function(tx) { - return tx.hash('hex') === f1.hash('hex'); - })); + w.getBalance(function(err, balance) { + assert(!err); + assert.equal(balance.toString(10), '11000'); + w.getAll(function(err, txs) { + assert(txs.some(function(tx) { + return tx.hash('hex') === f1.hash('hex'); + })); - var w2 = bcoin.wallet.fromJSON(w.toJSON()); - // assert.equal(w2.getBalance().toString(10), '11000'); - // assert(w2.getAll().some(function(tx) { - // return tx.hash('hex') === f1.hash('hex'); - // })); - cb(); + var w2 = bcoin.wallet.fromJSON(w.toJSON()); + // assert.equal(w2.getBalance().toString(10), '11000'); + // assert(w2.getAll().some(function(tx) { + // return tx.hash('hex') === f1.hash('hex'); + // })); + cb(); + }); }); }); }); @@ -250,7 +225,6 @@ describe('Wallet', function() { }); }); }); - }); }); it('should fill tx with inputs', function(cb) { @@ -259,33 +233,34 @@ describe('Wallet', function() { // Coinbase var t1 = bcoin.mtx().addOutput(w1, 5460).addOutput(w1, 5460).addOutput(w1, 5460).addOutput(w1, 5460); + t1.addInput(dummyInput); // Fake TX should temporarly change output - w1.addTX(t1); + w1.addTX(t1, function(err) { + assert(!err); - // Create new transaction - var t2 = bcoin.mtx().addOutput(w2, 5460); - assert(w1.fill(t2)); - w1.sign(t2); - assert(t2.verify()); + // Create new transaction + var t2 = bcoin.mtx().addOutput(w2, 5460); + w1.fill(t2, function(err) { + assert(!err); + w1.sign(t2); + assert(t2.verify()); - assert.equal(t2.getInputValue().toString(10), 16380); - // If change < dust and is added to outputs: - // assert.equal(t2.getOutputValue().toString(10), 6380); - // If change < dust and is added to fee: - assert.equal(t2.getOutputValue().toString(10), 5460); + assert.equal(t2.getInputValue().toString(10), 16380); + // If change < dust and is added to outputs: + // assert.equal(t2.getOutputValue().toString(10), 6380); + // If change < dust and is added to fee: + assert.equal(t2.getOutputValue().toString(10), 5460); - // Create new transaction - var t3 = bcoin.mtx().addOutput(w2, 15000); - try { - w1.fill(t3); - } catch (e) { - var err = e; - } - assert(err); - assert.equal(err.requiredFunds.toString(10), 25000); - - cb(); + // Create new transaction + var t3 = bcoin.mtx().addOutput(w2, 15000); + w1.fill(t3, function(err) { + assert(err); + assert.equal(err.requiredFunds.toString(10), 25000); + cb(); + }); + }); + }); }); it('should sign multiple inputs using different keys', function(cb) { @@ -297,60 +272,69 @@ describe('Wallet', function() { var t1 = bcoin.mtx().addOutput(w1, 5460).addOutput(w1, 5460).addOutput(w1, 5460).addOutput(w1, 5460); t1.addInput(dummyInput); // Fake TX should temporarly change output - w1.addTX(t1); // Coinbase var t2 = bcoin.mtx().addOutput(w2, 5460).addOutput(w2, 5460).addOutput(w2, 5460).addOutput(w2, 5460); t2.addInput(dummyInput); // Fake TX should temporarly change output - w2.addTX(t2); - // Create our tx with an output - var tx = bcoin.mtx(); - tx.addOutput(to, 5460); + w1.addTX(t1, function(err) { + assert(!err); + w2.addTX(t2, function(err) { + assert(!err); - var cost = tx.getOutputValue(); - var total = cost.add(new bn(constants.tx.minFee)); + // Create our tx with an output + var tx = bcoin.mtx(); + tx.addOutput(to, 5460); - var unspent1 = w1.getUnspent(); - var unspent2 = w2.getUnspent(); + var cost = tx.getOutputValue(); + var total = cost.add(new bn(constants.tx.minFee)); - // Add dummy output (for `left`) to calculate maximum TX size - tx.addOutput(w1, new bn(0)); + w1.getUnspent(function(err, unspent1) { + assert(!err); + w2.getUnspent(function(err, unspent2) { + assert(!err); - // Add our unspent inputs to sign - tx.addInput(unspent1[0]); - tx.addInput(unspent1[1]); - tx.addInput(unspent2[0]); + // Add dummy output (for `left`) to calculate maximum TX size + tx.addOutput(w1, new bn(0)); - var left = tx.getInputValue().sub(total); - if (left.cmpn(constants.tx.dustThreshold) < 0) { - tx.outputs[tx.outputs.length - 2].value.iadd(left); - left = new bn(0); - } - if (left.cmpn(0) === 0) - tx.outputs.pop(); - else - tx.outputs[tx.outputs.length - 1].value = left; + // Add our unspent inputs to sign + tx.addInput(unspent1[0]); + tx.addInput(unspent1[1]); + tx.addInput(unspent2[0]); - // Sign transaction - assert.equal(w1.sign(tx), 2); - assert.equal(w2.sign(tx), 1); + var left = tx.getInputValue().sub(total); + if (left.cmpn(constants.tx.dustThreshold) < 0) { + tx.outputs[tx.outputs.length - 2].value.iadd(left); + left = new bn(0); + } + if (left.cmpn(0) === 0) + tx.outputs.pop(); + else + tx.outputs[tx.outputs.length - 1].value = left; - // Verify - assert.equal(tx.verify(), true); + // Sign transaction + assert.equal(w1.sign(tx), 2); + assert.equal(w2.sign(tx), 1); - // Sign transaction using `inputs` and `off` params. - tx.inputs.length = 0; - tx.addInput(unspent1[1]); - tx.addInput(unspent1[2]); - tx.addInput(unspent2[1]); - assert.equal(w1.sign(tx, 'all'), 2); - assert.equal(w2.sign(tx, 'all'), 1); + // Verify + assert.equal(tx.verify(), true); - // Verify - assert.equal(tx.verify(), true); + // Sign transaction using `inputs` and `off` params. + tx.inputs.length = 0; + tx.addInput(unspent1[1]); + tx.addInput(unspent1[2]); + tx.addInput(unspent2[1]); + assert.equal(w1.sign(tx, 'all'), 2); + assert.equal(w2.sign(tx, 'all'), 1); - cb(); + // Verify + assert.equal(tx.verify(), true); + + cb(); + }); + }); + }); + }); }); function multisig(witness, bullshitNesting, cb) { @@ -430,72 +414,86 @@ describe('Wallet', function() { assert.equal(w1.receiveDepth, 1); - w1.addTX(utx); - w2.addTX(utx); - w3.addTX(utx); + w1.addTX(utx, function(err) { + assert(!err); + w2.addTX(utx, function(err) { + assert(!err); + w3.addTX(utx, function(err) { + assert(!err); - assert.equal(w1.receiveDepth, 2); - assert.equal(w1.changeDepth, 1); + assert.equal(w1.receiveDepth, 2); + assert.equal(w1.changeDepth, 1); - assert(w1.getAddress() !== addr); - addr = w1.getAddress(); - assert.equal(w1.getAddress(), addr); - assert.equal(w2.getAddress(), addr); - assert.equal(w3.getAddress(), addr); + assert(w1.getAddress() !== addr); + addr = w1.getAddress(); + assert.equal(w1.getAddress(), addr); + assert.equal(w2.getAddress(), addr); + assert.equal(w3.getAddress(), addr); - // Create a tx requiring 2 signatures - var send = bcoin.mtx(); - send.addOutput({ address: receive.getAddress(), value: 5460 }); - assert(!send.verify(null, true, flags)); - var result = w1.fill(send, { m: w1.m, n: w1.n }); - assert(result); - w1.sign(send); + // Create a tx requiring 2 signatures + var send = bcoin.mtx(); + send.addOutput({ address: receive.getAddress(), value: 5460 }); + assert(!send.verify(null, true, flags)); + w1.fill(send, { m: w1.m, n: w1.n }, function(err) { + assert(!err); - assert(!send.verify(null, true, flags)); - w2.sign(send); + w1.sign(send); - assert(send.verify(null, true, flags)); + assert(!send.verify(null, true, flags)); + w2.sign(send); - assert.equal(w1.changeDepth, 1); - var change = w1.changeAddress.getAddress(); - assert.equal(w1.changeAddress.getAddress(), change); - assert.equal(w2.changeAddress.getAddress(), change); - assert.equal(w3.changeAddress.getAddress(), change); + assert(send.verify(null, true, flags)); - // Simulate a confirmation - send.ps = 0; - send.ts = 1; - send.height = 1; + assert.equal(w1.changeDepth, 1); + var change = w1.changeAddress.getAddress(); + assert.equal(w1.changeAddress.getAddress(), change); + assert.equal(w2.changeAddress.getAddress(), change); + assert.equal(w3.changeAddress.getAddress(), change); - w1.addTX(send); - w2.addTX(send); - w3.addTX(send); + // Simulate a confirmation + send.ps = 0; + send.ts = 1; + send.height = 1; - assert.equal(w1.receiveDepth, 2); - assert.equal(w1.changeDepth, 2); + w1.addTX(send, function(err) { + assert(!err); + w2.addTX(send, function(err) { + assert(!err); + w3.addTX(send, function(err) { + assert(!err); - assert(w1.getAddress() === addr); - assert(w1.changeAddress.getAddress() !== change); - change = w1.changeAddress.getAddress(); - assert.equal(w1.changeAddress.getAddress(), change); - assert.equal(w2.changeAddress.getAddress(), change); - assert.equal(w3.changeAddress.getAddress(), change); + assert.equal(w1.receiveDepth, 2); + assert.equal(w1.changeDepth, 2); - if (witness) - send.inputs[0].witness[2] = new Buffer([]); - else - send.inputs[0].script[2] = 0; + assert(w1.getAddress() === addr); + assert(w1.changeAddress.getAddress() !== change); + change = w1.changeAddress.getAddress(); + assert.equal(w1.changeAddress.getAddress(), change); + assert.equal(w2.changeAddress.getAddress(), change); + assert.equal(w3.changeAddress.getAddress(), change); - assert(!send.verify(null, true, flags)); - assert.equal(send.getFee().toNumber(), 10000); + if (witness) + send.inputs[0].witness[2] = new Buffer([]); + else + send.inputs[0].script[2] = 0; - w3 = bcoin.wallet.fromJSON(w3.toJSON()); - assert.equal(w3.receiveDepth, 2); - assert.equal(w3.changeDepth, 2); - assert.equal(w3.getAddress(), addr); - assert.equal(w3.changeAddress.getAddress(), change); + assert(!send.verify(null, true, flags)); + assert.equal(send.getFee().toNumber(), 10000); - cb(); + w3 = bcoin.wallet.fromJSON(w3.toJSON()); + assert.equal(w3.receiveDepth, 2); + assert.equal(w3.changeDepth, 2); + assert.equal(w3.getAddress(), addr); + assert.equal(w3.changeAddress.getAddress(), change); + + cb(); + }); + }); + }); + }); + }); + }); + }); } it('should verify 2-of-3 scripthash tx', function(cb) {