323 lines
7.6 KiB
JavaScript
323 lines
7.6 KiB
JavaScript
/**
|
|
* tx-pool.js - transaction pool for bcoin
|
|
* Copyright (c) 2014-2015, Fedor Indutny (MIT License)
|
|
* https://github.com/indutny/bcoin
|
|
*/
|
|
|
|
var bn = require('bn.js');
|
|
var inherits = require('inherits');
|
|
var bcoin = require('../bcoin');
|
|
var utils = bcoin.utils;
|
|
var assert = bcoin.utils.assert;
|
|
var EventEmitter = require('events').EventEmitter;
|
|
|
|
/**
|
|
* TXPool
|
|
*/
|
|
|
|
function TXPool(wallet) {
|
|
if (!(this instanceof TXPool))
|
|
return new TXPool(wallet);
|
|
|
|
EventEmitter.call(this);
|
|
|
|
this._wallet = wallet;
|
|
this._storage = wallet.storage;
|
|
this._prefix = wallet.prefix + 'tx/';
|
|
this._all = {};
|
|
this._unspent = {};
|
|
this._orphans = {};
|
|
this._lastTs = 0;
|
|
this._loaded = false;
|
|
|
|
// Load TXs from storage
|
|
this._init();
|
|
}
|
|
|
|
inherits(TXPool, EventEmitter);
|
|
|
|
TXPool.prototype._init = function init() {
|
|
var self = this;
|
|
|
|
if (!this._storage) {
|
|
this._loaded = true;
|
|
return;
|
|
}
|
|
|
|
var s = this._storage.createReadStream({
|
|
keys: false,
|
|
start: this._prefix,
|
|
end: this._prefix + 'z'
|
|
});
|
|
|
|
s.on('data', function(data) {
|
|
self.add(bcoin.tx.fromJSON(data), true);
|
|
});
|
|
|
|
s.on('error', function(err) {
|
|
self.emit('error', err);
|
|
});
|
|
|
|
s.on('end', function() {
|
|
self._loaded = true;
|
|
self.emit('load', self._lastTs);
|
|
});
|
|
};
|
|
|
|
TXPool.prototype.add = function add(tx, noWrite) {
|
|
var hash = tx.hash('hex');
|
|
var updated;
|
|
var i, input, key, unspent, index, orphan;
|
|
var out, key, orphans, some;
|
|
|
|
// Ignore stale pending transactions
|
|
if (tx.ts === 0 && tx.ps + 2 * 24 * 3600 < utils.now()) {
|
|
this._removeTX(tx, noWrite);
|
|
return;
|
|
}
|
|
|
|
// Do not add TX two times
|
|
if (this._all[hash]) {
|
|
// Transaction was confirmed, update it in storage
|
|
if (tx.ts !== 0 && this._all[hash].ts === 0) {
|
|
this._all[hash].ts = tx.ts;
|
|
this._all[hash].block = tx.block;
|
|
this._storeTX(hash, tx, noWrite);
|
|
}
|
|
return false;
|
|
}
|
|
this._all[hash] = tx;
|
|
|
|
updated = false;
|
|
|
|
// Consume unspent money or add orphans
|
|
for (i = 0; i < tx.inputs.length; i++) {
|
|
input = tx.inputs[i];
|
|
key = input.out.hash + '/' + input.out.index;
|
|
unspent = this._unspent[key];
|
|
|
|
if (unspent) {
|
|
// Add TX to inputs and spend money
|
|
index = tx._inputIndex(unspent.tx.hash('hex'), unspent.index);
|
|
assert(index !== -1);
|
|
assert(tx.inputs[index] === input);
|
|
assert(tx.inputs[index].out.hash === unspent.tx.hash('hex'));
|
|
assert(tx.inputs[index].out.index === unspent.index);
|
|
input.out.tx = unspent.tx;
|
|
|
|
// Skip invalid transactions
|
|
if (!tx.verify(index))
|
|
return;
|
|
|
|
delete this._unspent[key];
|
|
updated = true;
|
|
continue;
|
|
}
|
|
|
|
// Only add orphans if this input is ours.
|
|
// If there is no previous output, there's no way to truly
|
|
// verify this is ours, so we assume it is. If we add the
|
|
// signature checking code to ownInput for p2sh and p2pk,
|
|
// we could in theory use ownInput here (and down below)
|
|
// instead.
|
|
// if (this._wallet.ownInput(input.out.tx, input.out.index))
|
|
if (tx.inputs[i].out.tx) {
|
|
if (!this._wallet.ownOutput(input.out.tx, input.out.index))
|
|
continue;
|
|
}
|
|
|
|
// Add orphan, if no parent transaction is yet known
|
|
orphan = { tx: tx, index: input.out.index };
|
|
if (this._orphans[key])
|
|
this._orphans[key].push(orphan);
|
|
else
|
|
this._orphans[key] = [orphan];
|
|
}
|
|
|
|
if (!this._wallet.ownOutput(tx)) {
|
|
if (updated)
|
|
this.emit('update', this._lastTs, tx);
|
|
|
|
// Save spending TXs without adding unspents
|
|
// if (this._wallet.ownInput(tx))
|
|
this._storeTX(hash, tx, noWrite);
|
|
return;
|
|
}
|
|
|
|
function checkOrphan(orphan) {
|
|
var index = orphan.tx._inputIndex(tx.hash('hex'), orphan.index);
|
|
assert(index !== -1);
|
|
assert(orphan.tx.inputs[index].out.hash === tx.hash('hex'));
|
|
assert(orphan.tx.inputs[index].out.index === i);
|
|
orphan.tx.inputs[index].out.tx = tx;
|
|
|
|
// Verify that input script is correct, if not - add output to unspent
|
|
// and remove orphan from storage
|
|
if (!orphan.tx.verify(index)) {
|
|
this._removeTX(orphan.tx, noWrite);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Add unspent outputs or fullfill orphans
|
|
for (i = 0; i < tx.outputs.length; i++) {
|
|
out = tx.outputs[i];
|
|
|
|
// Do not add unspents for outputs that aren't ours.
|
|
if (!this._wallet.ownOutput(tx, i))
|
|
continue;
|
|
|
|
key = hash + '/' + i;
|
|
orphans = this._orphans[key];
|
|
|
|
// Add input to orphan
|
|
if (orphans) {
|
|
some = orphans.some(checkOrphan, this);
|
|
if (!some)
|
|
orphans = null;
|
|
}
|
|
|
|
delete this._orphans[key];
|
|
if (!orphans) {
|
|
this._unspent[key] = { tx: tx, index: i };
|
|
updated = true;
|
|
}
|
|
}
|
|
|
|
this._lastTs = Math.max(tx.ts, this._lastTs);
|
|
if (updated)
|
|
this.emit('update', this._lastTs, tx);
|
|
|
|
this._storeTX(hash, tx, noWrite);
|
|
|
|
this.emit('tx', tx);
|
|
|
|
return true;
|
|
};
|
|
|
|
TXPool.prototype._storeTX = function _storeTX(hash, tx, noWrite) {
|
|
var self = this;
|
|
|
|
if (!this._storage || noWrite)
|
|
return;
|
|
|
|
this._storage.put(this._prefix + hash, tx.toJSON(), function(err) {
|
|
if (err)
|
|
self.emit('error', err);
|
|
});
|
|
};
|
|
|
|
TXPool.prototype._removeTX = function _removeTX(tx, noWrite) {
|
|
var self = this;
|
|
|
|
for (var i = 0; i < tx.outputs.length; i++)
|
|
delete this._unspent[tx.hash('hex') + '/' + i];
|
|
|
|
if (!this._storage || noWrite)
|
|
return;
|
|
|
|
this._storage.del(this._prefix + tx.hash('hex'), function(err) {
|
|
if (err)
|
|
self.emit('error', err);
|
|
});
|
|
};
|
|
|
|
TXPool.prototype.all = function all() {
|
|
return Object.keys(this._all).map(function(key) {
|
|
return this._all[key];
|
|
}, this).filter(function(tx) {
|
|
return this._wallet.ownOutput(tx)
|
|
|| this._wallet.ownInput(tx);
|
|
}, this);
|
|
};
|
|
|
|
TXPool.prototype.unspent = function unspent() {
|
|
return Object.keys(this._unspent).map(function(key) {
|
|
return this._unspent[key];
|
|
}, this).filter(function(item) {
|
|
return this._wallet.ownOutput(item.tx, item.index);
|
|
}, this);
|
|
};
|
|
|
|
TXPool.prototype.hasUnspent = function hasUnspent(hash, unspent) {
|
|
var has;
|
|
|
|
if (Array.isArray(hash) && hash.length && typeof hash[0] !== 'number') {
|
|
unspent = this.unspent();
|
|
has = hash.map(function(hash) {
|
|
var h = this.hasUnspent(hash, unspent);
|
|
if (!h)
|
|
return false;
|
|
return h[0];
|
|
}, this).filter(Boolean);
|
|
if (has.length !== hash.length)
|
|
return null;
|
|
return has;
|
|
}
|
|
|
|
if (Array.isArray(hash))
|
|
hash = utils.toHex(hash);
|
|
else if (hash.out)
|
|
hash = hash.out.hash;
|
|
else if (hash.tx)
|
|
hash = hash.tx.hash('hex');
|
|
else if (hash instanceof bcoin.tx)
|
|
hash = hash.hash('hex');
|
|
|
|
unspent = unspent || this.unspent();
|
|
|
|
has = unspent.filter(function(item) {
|
|
return item.tx.hash('hex') === hash;
|
|
});
|
|
|
|
if (!has.length)
|
|
return null;
|
|
|
|
return has;
|
|
};
|
|
|
|
TXPool.prototype.pending = function pending() {
|
|
return Object.keys(this._all).map(function(key) {
|
|
return this._all[key];
|
|
}, this).filter(function(tx) {
|
|
return tx.ts === 0;
|
|
});
|
|
};
|
|
|
|
TXPool.prototype.balance = function balance() {
|
|
var acc = new bn(0);
|
|
var unspent = this.unspent();
|
|
if (unspent.length === 0)
|
|
return acc;
|
|
|
|
return unspent.reduce(function(acc, item) {
|
|
return acc.iadd(item.tx.outputs[item.index].value);
|
|
}, acc);
|
|
};
|
|
|
|
TXPool.prototype.toJSON = function toJSON() {
|
|
return {
|
|
v: 1,
|
|
type: 'tx-pool',
|
|
txs: Object.keys(this._all).map(function(hash) {
|
|
return this._all[hash].toJSON();
|
|
}, this)
|
|
};
|
|
};
|
|
|
|
TXPool.prototype.fromJSON = function fromJSON(json) {
|
|
assert.equal(json.v, 1);
|
|
assert.equal(json.type, 'tx-pool');
|
|
|
|
json.txs.forEach(function(tx) {
|
|
this.add(bcoin.tx.fromJSON(tx));
|
|
}, this);
|
|
};
|
|
|
|
/**
|
|
* Expose
|
|
*/
|
|
|
|
module.exports = TXPool;
|