fcoin/lib/bcoin/mempool.js
2016-03-23 15:46:07 -07:00

605 lines
15 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;
var BufferWriter = require('./writer');
var BufferReader = require('./reader');
var DUMMY_PEER = { sendReject: function() {} };
/**
* 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.db;
this.db = bcoin.ldb('mempool', {
db: 'memdown'
});
this.tx = new bcoin.txdb('m', this.db, {
indexSpent: true,
indexExtra: false,
indexAddress: false,
mapAddress: false,
verify: false
});
this.loaded = false;
this.jobs = [];
this.busy = false;
this.pending = [];
this.pendingTX = {};
this.pendingSize = 0;
this.pendingLimit = 20 << 20;
this.locker = new bcoin.locker(this, this.add, this.pendingLimit);
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.flags = constants.flags.STANDARD_VERIFY_FLAGS;
Mempool.mandatory = constants.flags.MANDATORY_VERIFY_FLAGS;
Mempool.prototype._lock = function _lock(func, args, force) {
return this.locker.lock(func, args, force);
};
Mempool.prototype.purgePending = function purgePending() {
return this.locker.purgePending();
};
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, 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.slice().reverse(), 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, 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 hash, ts, height, now;
var ret = {};
var unlock = this._lock(addTX, [tx, peer, callback], force);
if (!unlock)
return;
if (typeof peer === 'function') {
callback = peer;
peer = null;
}
if (!peer)
peer = DUMMY_PEER;
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 VerifyError('CheckTransaction failed', -1));
if (tx.isCoinbase()) {
peer.sendReject(tx, 'coinbase', 100);
return callback(new VerifyError('coinbase as individual tx', 100));
}
ts = utils.now();
height = this.chain.height + 1;
if (this.requireStandard && !tx.isStandard(Mempool.flags, ts, height, ret)) {
peer.sendReject(tx, ret.reason, 0);
return callback(new VerifyError(ret.reason, 0));
}
this._hasTX(tx, function(err, exists) {
if (err)
return callback(err);
if (exists)
return callback();
self.node.fillCoin(tx, function(err) {
if (err)
return callback(err);
if (!tx.hasPrevout()) {
return self.tx.isDoubleSpend(tx, function(err, result) {
if (err)
return callback(err);
if (result) {
peer.sendReject(tx, 'bad-txns-inputs-spent', 0);
return callback(new VerifyError('bad-txns-inputs-spent', 0));
}
return self.storeOrphan(tx, callback);
});
}
self.verify(tx, function(err) {
if (err) {
if (err.type === 'VerifyError' && err.score >= 0)
peer.sendReject(tx, err.reason, err.score);
return callback(err);
}
self.addUnchecked(tx, peer, callback);
});
});
});
};
Mempool.prototype.addUnchecked = function addUnchecked(tx, peer, callback) {
var self = this;
self.tx.add(tx, function(err) {
if (err)
return callback(err);
self.emit('tx', tx);
self.resolveOrphans(tx, function(err, resolved) {
if (err)
return callback(err);
utils.forEachSerial(resolved, function(tx, next) {
self.addUnchecked(tx, peer, function(err) {
if (err) {
self.emit('error', err);
}
next();
}, true);
}, callback);
});
});
};
Mempool.prototype.verify = function verify(tx, callback) {
var self = this;
var total, input, coin, i, fee, now;
if (this.requireStandard && !tx.isStandardInputs(Mempool.flags))
return callback(new VerifyError('TX inputs are not standard.', -1));
if (tx.getSigops(true) > constants.script.maxSigops)
return callback(new VerifyError('bad-txns-too-many-sigops', 0));
total = new bn(0);
for (i = 0; i < tx.inputs.length; i++) {
input = tx.inputs[i];
coin = input.output;
if (coin.coinbase) {
if (this.chain.height - coin.height < constants.tx.coinbaseMaturity)
return callback(new VerifyError('bad-txns-premature-spend-of-coinbase', 0));
}
if (coin.value.cmpn(0) < 0 || coin.value.cmp(constants.maxMoney) > 0)
return callback(new VerifyError('bad-txns-inputvalues-outofrange', 100));
total.iadd(coin.value);
}
if (total.cmpn(0) < 0 || total.cmp(constants.maxMoney) > 0)
return callback(new VerifyError('bad-txns-inputvalues-outofrange', 100));
if (tx.getOutputValue().cmp(total) > 0)
return callback(new VerifyError('bad-txns-in-belowout', 100));
fee = total.sub(tx.getOutputValue());
if (fee.cmpn(0) < 0)
return callback(new VerifyError('bad-txns-fee-negative', 100));
if (fee.cmp(constants.maxMoney) > 0)
return callback(new VerifyError('bad-txns-fee-outofrange', 100));
if (this.limitFree && fee.cmp(tx.getMinFee(true)) < 0)
return callback(new VerifyError('insufficient fee', 0));
if (this.limitFree && fee.cmpn(tx.getMinFee()) < 0) {
now = utils.now();
if (!this.lastTime)
this.lastTime = now;
this.freeCount *= Math.pow(1 - 1 / 600, now - this.lastTime);
this.lastTime = now;
if (this.freeCount > this.limitFreeRelay * 10 * 1000)
return callback(new VerifyError('insufficient priority', 0));
this.freeCount += tx.getVirtualSize();
}
if (this.rejectInsaneFees && fee.cmpn(tx.getMinFee().muln(10000)) > 0)
return callback(new VerifyError('TX has an insane fee.', -1));
// Do this in the worker pool.
tx.verifyAsync(null, true, Mempool.flags, function(err, result) {
if (err)
return callback(err);
if (!result) {
return tx.verifyAsync(null, true, Mempool.mandatory, function(err, result) {
if (err)
return callback(err);
if (!result)
return callback(new VerifyError('mandatory-script-verify-flag', 0));
return callback(new VerifyError('non-mandatory-script-verify-flag', 0));
});
}
return callback();
});
};
Mempool.prototype._hasTX = function hasTX(tx, callback, force) {
var self = this;
var hash = tx.hash('hex');
this.node.hasTX(hash, function(err, result) {
if (err)
return callback(err);
if (result)
return callback(null, result);
self.db.get('D/' + hash, function(err, tx) {
if (err && err.type !== 'NotFoundError')
return callback(err);
return callback(null, !!tx);
});
});
};
Mempool.prototype.storeOrphan = function storeOrphan(tx, callback, force) {
var self = this;
var outputs = {};
var batch = this.db.batch();
var hash = tx.hash('hex');
var i, input, p;
for (i = 0; i < tx.inputs.length; i++) {
input = tx.inputs[i];
if (!input.output)
outputs[input.prevout.hash] = true;
}
outputs = Object.keys(outputs);
assert(outputs.length > 0);
utils.forEachSerial(outputs, function(key, next) {
self.db.get('d/' + key, function(err, buf) {
if (err && err.type !== 'NotFoundError')
return next(err);
p = new BufferWriter();
if (buf)
p.writeBytes(buf);
p.writeHash(hash);
batch.put('d/' + key, p.render());
next();
});
}, function(err) {
if (err)
return callback(err);
batch.put('D/' + hash, tx.toExtended(true));
batch.write(callback);
});
};
Mempool.prototype.getBalance = function getBalance(callback) {
return this.tx.getBalance(callback);
};
Mempool.prototype.getAll = function getAll(callback) {
return this.tx.getAll(callback);
};
Mempool.prototype.resolveOrphans = function resolveOrphans(tx, callback, force) {
var self = this;
var hash = tx.hash('hex');
var hashes = [];
var resolved = [];
var batch = this.db.batch();
var p;
this.db.get('d/' + hash, function(err, buf) {
if (err && err.type !== 'NotFoundError')
return callback(err);
if (!buf)
return callback(null, resolved);
p = new BufferReader(buf);
p.start();
try {
while (p.left())
hashes.push(p.readHash('hex'));
} catch (e) {
return callback(e);
}
p.end();
utils.forEachSerial(hashes, function(orphanHash, next, i) {
self.db.get('D/' + orphanHash, function(err, orphan) {
if (err && err.type !== 'NotFoundError')
return next(err);
if (!orphan)
return next();
try {
orphan = bcoin.tx.fromExtended(orphan, true);
} catch (e) {
return next(e);
}
orphan.fillPrevout(tx);
if (orphan.hasPrevout()) {
batch.del('D/' + orphanHash);
return self.verify(orphan, function(err) {
if (err) {
if (err.type === 'VerifyError')
return next();
return next(err);
}
resolved.push(orphan);
return next();
});
}
batch.put('D/' + orphanHash, orphan.toExtended(true));
next();
});
}, function(err) {
if (err)
return callback(err);
function done(err) {
if (err)
return callback(err);
return callback(null, resolved);
}
batch.del('d/' + hash);
return batch.write(done);
});
});
};
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 instanceof bcoin.tx)
return callback(null, hash);
return self.getTX(hash, function(err, tx) {
if (err)
return callback(err);
if (!tx)
return callback();
return self.node.fillTX(tx, callback);
});
}
return 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) {
return Mempool.checkTX(tx, peer);
};
Mempool.checkTX = function checkTX(tx, peer) {
var i, input, output, size;
var total = new bn(0);
var uniq = {};
if (!peer)
peer = DUMMY_PEER;
if (tx.inputs.length === 0)
return peer.sendReject(tx, 'bad-txns-vin-empty', 100);
if (tx.outputs.length === 0)
return peer.sendReject(tx, 'bad-txns-vout-empty', 100);
if (tx.getVirtualSize() > constants.block.maxSize)
return peer.sendReject(tx, 'bad-txns-oversize', 100);
for (i = 0; i < tx.outputs.length; i++) {
output = tx.outputs[i];
if (output.value.cmpn(0) < 0)
return peer.sendReject(tx, 'bad-txns-vout-negative', 100);
if (output.value.cmp(constants.maxMoney) > 0)
return peer.sendReject(tx, 'bad-txns-vout-toolarge', 100);
total.iadd(output.value);
if (total.cmpn(0) < 0 || total.cmp(constants.maxMoney) > 0)
return peer.sendReject(tx, 'bad-txns-txouttotal-toolarge', 100);
}
for (i = 0; i < tx.inputs.length; i++) {
input = tx.inputs[i];
if (uniq[input.prevout.hash])
return peer.sendReject(tx, 'bad-txns-inputs-duplicate', 100);
uniq[input.prevout.hash] = true;
}
if (tx.isCoinbase()) {
size = tx.inputs[0].script.getSize();
if (size < 2 || size > 100)
return peer.sendReject(tx, 'bad-cb-length', 100);
} else {
for (i = 0; i < tx.inputs.length; i++) {
input = tx.inputs[i];
if (+input.prevout.hash === 0)
return peer.sendReject(tx, 'bad-txns-prevout-null', 10);
}
}
return true;
};
function VerifyError(reason, score) {
Error.call(this);
if (Error.captureStackTrace)
Error.captureStackTrace(this, VerifyError);
this.type = 'VerifyError';
this.message = reason;
this.reason = score === -1 ? null : reason;
this.score = score;
}
utils.inherits(VerifyError, Error);
/**
* Expose
*/
module.exports = Mempool;