all tests passing.

This commit is contained in:
Christopher Jeffrey 2016-03-01 20:15:57 -08:00
parent b1232593d8
commit 8f77cf7173
4 changed files with 302 additions and 402 deletions

View File

@ -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
*/

View File

@ -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) {

View File

@ -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
*/

View File

@ -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) {