fcoin/lib/bcoin/mempool.js
Christopher Jeffrey 2797cd2f44 rewrite mempool.
2016-03-21 16:29:02 -07:00

480 lines
12 KiB
JavaScript

/**
* mempool.js - mempool for bcoin
* Copyright (c) 2014-2015, Fedor Indutny (MIT License)
* https://github.com/indutny/bcoin
*/
var EventEmitter = require('events').EventEmitter;
var bcoin = require('../bcoin');
var bn = require('bn.js');
var constants = bcoin.protocol.constants;
var utils = require('./utils');
var assert = utils.assert;
/**
* Mempool
*/
function Mempool(node, options) {
if (!(this instanceof Mempool))
return new Mempool(node, options);
EventEmitter.call(this);
if (!options)
options = {};
this.options = options;
this.node = node;
this.chain = node.chain;
this.db = node.chain.db;
this.tx = new bcoin.txdb('m', this.db, {
indexSpent: true,
indexExtra: false,
indexAddress: false,
mapAddress: false
});
this.txs = {};
this.spent = {};
this.addresses = {};
this.size = 0;
this.count = 0;
this.locked = false;
this.loaded = false;
this.jobs = [];
this.busy = false;
this.pending = [];
this.pendingTX = {};
this.pendingSize = 0;
this.pendingLimit = 20 << 20;
this.freeCount = 0;
this.lastTime = 0;
this.limitFreeRelay = this.options.limitFreeRelay || 15;
this.requireStandard = this.options.requireStandard !== false;
this.limitFree = this.options.limitFree !== false;
this.rejectInsaneFees = this.options.rejectInsaneFees !== false;
Mempool.global = this;
this._init();
}
utils.inherits(Mempool, EventEmitter);
Mempool.prototype._lock = function _lock(func, args, force) {
var self = this;
var block, called;
if (force) {
assert(this.busy);
return function unlock() {
assert(!called);
called = true;
};
}
if (this.busy) {
if (func === Mempool.prototype.add) {
tx = args[0];
this.pending.push(tx);
this.pendingTX[tx.hash('hex')] = true;
this.pendingSize += tx.getSize();
if (this.pendingSize > this.pendingLimit) {
this.purgePending();
return;
}
}
this.jobs.push([func, args]);
return;
}
this.busy = true;
return function unlock() {
var item, tx;
assert(!called);
called = true;
self.busy = false;
if (func === Chain.prototype.add) {
if (self.pending.length === 0)
self.emit('flush');
}
if (self.jobs.length === 0)
return;
item = self.jobs.shift();
if (item[0] === Mempool.prototype.add) {
tx = item[1][0];
assert(tx === self.pending.shift());
delete self.pendingTX[tx.hash('hex')];
self.pendingSize -= tx.getSize();
}
item[0].apply(self, item[1]);
};
};
Mempool.prototype.purgePending = function purgePending() {
var self = this;
utils.debug('Warning: %dmb of pending txs. Purging.',
utils.mb(this.pendingSize));
this.pending.forEach(function(tx) {
delete self.pendingTX[tx.hash('hex')];
});
this.pending.length = 0;
this.pendingSize = 0;
this.jobs = this.jobs.filter(function(item) {
return item[0] !== Mempool.prototype.add;
});
};
Mempool.prototype._init = function _init() {
var self = this;
if (this.db.loaded) {
this.loaded = true;
return;
}
this.db.once('open', function() {
self.loaded = true;
self.emit('open');
});
};
Mempool.prototype.open = function open(callback) {
return this.db.open(callback);
};
Mempool.prototype.addBlock = function addBlock(block) {
var self = this;
callback = utils.ensure(callback);
// Remove now-mined transactions
// XXX should batch this
utils.forEachSerial(block.txs.slice().reverse(), function(tx, next) {
self.tx.remove(tx, next);
}, callback);
};
Mempool.prototype.removeBlock = function removeBlock(block, callback) {
var self = this;
callback = utils.ensure(callback);
// XXX should batch this
utils.forEachSerial(block.txs, function(tx, next) {
self.tx.add(tx, next);
}, callback);
};
Mempool.prototype.get =
Mempool.prototype.getTX = function getTX(hash, callback) {
if (hash instanceof bcoin.tx)
hash = hash.hash('hex');
return this.tx.getTX(hash, index, callback);
};
Mempool.prototype.getCoin = function getCoin(hash, index, callback) {
return this.tx.getCoin(hash, index, callback);
};
Mempool.prototype.isSpent = function isSpent(hash, index, callback) {
return this.tx.isSpent(hash, index, callback);
};
Mempool.prototype.getCoinsByAddress = function getCoinsByAddress(addresses, callback) {
return this.tx.getCoinsByAddress(addresses, callback);
};
Mempool.prototype.getByAddress =
Mempool.prototype.getTXByAddress = function getTXByAddress(addresses) {
return this.tx.getTXByAddress(addresses, callback);
};
Mempool.prototype.fillTX = function fillTX(tx, callback) {
return this.tx.fillTX(tx, callback);
};
Mempool.prototype.fillCoin = function fillCoin(tx, callback) {
return this.tx.fillCoin(tx, callback);
};
Mempool.prototype.has =
Mempool.prototype.hasTX = function hasTX(hash, callback) {
return this.get(hash, function(err, tx) {
if (err)
return callback(err);
return callback(null, !!tx);
});
};
Mempool.prototype.add =
Mempool.prototype.addTX = function addTX(tx, peer, callback, force) {
var self = this;
var flags = constants.flags.STANDARD_VERIFY_FLAGS;
var hash, ts, height, now;
var ret = {};
var unlock = this._lock(addTX, [tx, peer, callback], force);
if (!unlock)
return;
hash = tx.hash('hex');
assert(tx.ts === 0);
callback = utils.wrap(callback, unlock);
callback = utils.asyncify(callback);
if (!this.checkTX(tx, peer))
return callback(new Error('CheckTransaction failed'));
if (tx.isCoinbase()) {
this.reject(peer, tx, 'coinbase', 100);
return callback(new Error('coinbase as individual tx'));
}
ts = utils.now();
height = this.chain.height + 1;
if (self.requireStandard && !tx.isStandard(flags, ts, height, ret)) {
self.reject(peer, tx, ret.reason, 0);
return callback(new Error('TX is not standard.'));
}
this.node.hasTX(tx, function(err, exists) {
if (err)
return callback(err);
if (exists)
return callback();
self.node.fillCoin(tx, function(err) {
var i, input, output, total, fee, coin;
if (err)
return callback(err);
if (!tx.hasPrevout()) {
// Store as orphan:
// return self.tx.add(tx, callback);
return callback(new Error('No prevouts yet.'));
}
if (self.requireStandard && !tx.isStandardInputs(flags))
return callback(new Error('TX inputs are not standard.'));
if (tx.getSigops(true) > constants.script.maxSigops) {
self.reject(peer, tx, 'bad-txns-too-many-sigops', 0);
return callback(new Error('TX has too many sigops.'));
}
total = new bn(0);
for (i = 0; i < tx.inputs.length; i++) {
input = tx.inputs[i];
coin = input.coin;
if (coin.isCoinbase()) {
if (self.chain.height - coin.height < constants.tx.coinbaseMaturity) {
self.reject(peer, tx, 'bad-txns-premature-spend-of-coinbase', 0);
return callback(new Error('Tried to spend coinbase prematurely.'));
}
}
if (coin.value.cmpn(0) < 0 || coin.value.cmp(constants.maxMoney) > 0)
return self.reject(peer, tx, 'bad-txns-inputvalues-outofrange', 100);
total.iadd(coin.value);
}
if (total.cmpn(0) < 0 || total.cmp(constants.maxMoney) > 0)
return self.reject(peer, tx, 'bad-txns-inputvalues-outofrange', 100);
if (tx.getOutputValue().cmp(total) > 0) {
self.reject(peer, tx, 'bad-txns-in-belowout', 100);
return callback(new Error('TX is spending coins it does not have.'));
}
fee = total.subn(tx.getOutputValue());
if (fee.cmpn(0) < 0) {
self.reject(peer, tx, 'bad-txns-fee-negative', 100);
return callback(new Error('TX has a negative fee.'));
}
if (fee.cmp(constants.maxMoney) > 0) {
return self.reject(peer, tx, 'bad-txns-fee-outofrange', 100);
return callback(new Error('TX has a fee higher than max money.'));
}
if (self.limitFree && fee.cmp(tx.getMinFee(true)) < 0) {
self.reject(peer, tx, 'insufficient fee', 0);
return callback(new Error('Insufficient fee.'));
}
if (self.limitFree && fee.cmpn(tx.getMinFee()) < 0) {
now = utils.now();
if (!self.lastTime)
self.lastTime = now;
self.freeCount *= Math.pow(1 - 1 / 600, now - self.lastTime);
self.lastTime = now;
if (self.freeCount > self.limitFreeRelay * 10 * 1000) {
self.reject(peer, tx, 'insufficient priority', 0);
return callback(new Error('Too many free txs at once!'));
}
self.freeCount += tx.getVirtualSize();
}
if (self.rejectInsaneFees && fee.cmpn(tx.getMinFee().muln(10000)) > 0)
return callback(new Error('TX has an insane fee.'));
// Do this in the worker pool.
tx.verifyAsync(null, true, flags, function(err, result) {
if (err)
return callback(err);
if (!result) {
// Just say it's non-mandatory for now.
self.reject(peer, tx, 'non-mandatory-script-verify-flag', 0);
return callback(new Error('TX did not verify.'));
}
self.tx.add(tx, function(err) {
if (err) {
if (err.message === 'Transaction is double-spending.') {
self.reject(peer, tx, 'bad-txns-inputs-spent', 0);
}
return callback(err);
}
self.emit('tx', tx);
return callback();
});
});
});
});
};
Mempool.prototype.getInv = function getInv(callback) {
return this.tx.getAllHashes(callback);
};
Mempool.prototype.remove =
Mempool.prototype.removeTX = function removeTX(hash, callback, force) {
var self = this;
var unlock = this._lock(removeTX, [hash, callback], force);
if (!unlock)
return;
function getTX() {
if (hash.hash) {
hash = hash.hash('hex');
return self.getTX(hash, function(err, tx) {
if (err)
return callback(err);
if (!tx)
return callback();
return self.node.fillTX(hash, callback);
});
}
return callback(null, hash);
}
getTX(function(err, tx) {
if (err)
return callback(err);
self.tx.remove(tx, function(err) {
if (err)
return callback(err);
self.emit('remove tx', tx);
});
});
};
Mempool.prototype.checkTX = function checkTX(tx, peer) {
var i, input, output, size;
var total = new bn(0);
var uniq = {};
if (tx.inputs.length === 0)
return this.reject(peer, tx, 'bad-txns-vin-empty', 100);
if (tx.outputs.length === 0)
return this.reject(peer, tx, 'bad-txns-vout-empty', 100);
if (tx.getSize() > constants.block.maxSize)
return this.reject(peer, tx, 'bad-txns-oversize', 100);
for (i = 0; i < tx.outputs.length; i++) {
output = tx.outputs[i];
if (output.value.cmpn(0) < 0)
return this.reject(peer, tx, 'bad-txns-vout-negative', 100);
if (output.value.cmp(constants.maxMoney) > 0)
return this.reject(peer, tx, 'bad-txns-vout-toolarge', 100);
total.iadd(output.value);
if (total.cmpn(0) < 0 || total.cmp(constants.maxMoney))
return this.reject(peer, tx, 'bad-txns-txouttotal-toolarge', 100);
}
for (i = 0; i < tx.inputs.length; i++) {
input = tx.inputs[i];
if (uniq[input.out.hash])
return this.reject(peer, tx, 'bad-txns-inputs-duplicate', 100);
uniq[input.out.hash] = true;
}
if (tx.isCoinbase()) {
size = bcoin.script.getSize(tx.inputs[0].script);
if (size < 2 || size > 100)
return this.reject(peer, tx, 'bad-cb-length', 100);
} else {
for (i = 0; i < tx.inputs.length; i++) {
input = tx.inputs[i];
if (+input.out.hash === 0)
return this.reject(peer, tx, 'bad-txns-prevout-null', 10);
}
}
return true;
};
Mempool.prototype.reject = function reject(peer, obj, reason, dos) {
utils.debug('Rejecting TX %s. Reason=%s.', obj.hash('hex'), reason);
if (dos != null)
this.node.pool.setMisbehavior(peer, dos);
if (!peer)
return false;
// peer.reject({
// reason: reason,
// data: obj.hash ? obj.hash() : []
// });
return false;
};
/**
* Expose
*/
module.exports = Mempool;