480 lines
12 KiB
JavaScript
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;
|