fcoin/lib/bcoin/mempool.js
Christopher Jeffrey 8f85f0b5c9 chaindb.
2016-03-11 15:53:15 -08:00

540 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 network = bcoin.protocol.network;
var utils = bcoin.utils;
var assert = utils.assert;
var fs = bcoin.fs;
/**
* Mempool
*/
function Mempool(node, options) {
if (!(this instanceof Mempool))
return new Mempool(node, options);
if (!options)
options = {};
this.options = options;
this.node = node;
this.chain = node.chain;
this.txs = {};
this.spent = {};
this.addresses = {};
this.size = 0;
this.count = 0;
this.locked = false;
Mempool.global = this;
this._init();
}
utils.inherits(Mempool, EventEmitter);
Mempool.prototype._init = function _init() {
;
};
Mempool.prototype.addBlock = function addBlock(block) {
var self = this;
// Remove now-mined transactions
block.txs.forEach(function(tx) {
var mtx = self.get(tx);
if (!mtx)
return;
mtx.ps = 0;
mtx.ts = block.ts;
mtx.block = block.hash('hex');
mtx.network = true;
self.removeTX(mtx);
});
};
Mempool.prototype.removeBlock = function removeBlock(block) {
var self = this;
block.txs.forEach(function(tx) {
var hash = tx.hash('hex');
// Remove anything that tries to redeem these outputs
tx.outputs.forEach(function(output, i) {
var mtx = self.spent[hash + '/' + i];
if (!mtx)
return;
self.removeTX(mtx);
});
// Add transaction back into mempool
// tx = tx.clone();
tx.ps = utils.now();
tx.ts = 0;
tx.block = null;
tx.network = true;
self.addTX(tx);
});
};
Mempool.prototype.get =
Mempool.prototype.getTX = function getTX(hash) {
if (hash instanceof bcoin.tx)
hash = hash.hash('hex');
return this.txs[hash];
};
Mempool.prototype.getCoin = function getCoin(hash, index) {
var tx = this.get(hash);
if (!tx)
return;
return bcoin.coin(tx, index);
};
Mempool.prototype.isSpent = function isSpent(hash, index) {
return !!this.spent[hash + '/' + index];
};
Mempool.prototype.getCoinsByAddress = function getCoinsByAddress(addresses) {
var txs = this.getByAddress(addresses);
return txs.reduce(function(out, tx) {
return out.concat(tx.outputs.map(function(output, i) {
return bcoin.coin(tx, i);
}));
}, []);
};
Mempool.prototype.getByAddress =
Mempool.prototype.getTXByAddress = function getTXByAddress(addresses) {
var self = this;
var txs = [];
var uniq = {};
if (typeof addresses === 'string')
addresses = [addresses];
addresses = utils.uniqs(addresses);
addresses.forEach(function(address) {
var map = self.addresses[address];
if (!map)
return;
Object.keys(map).forEach(function(hash) {
var tx;
if (uniq[hash])
return;
uniq[hash] = true;
tx = self.get(hash);
assert(tx);
txs.push(tx);
});
});
return txs;
};
Mempool.prototype.fillCoin =
Mempool.prototype.fillTX = function fillTX(tx) {
var i, input, total;
total = 0;
for (i = 0; i < tx.inputs.length; i++) {
input = tx.inputs[i];
if (input.output) {
total++;
continue;
}
if (this.hasTX(input.prevout.hash)) {
input.output = this.getCoin(input.prevout.hash, input.prevout.index);
total++;
}
}
return total === tx.inputs.length;
};
Mempool.prototype.getAll = function getAll(hash) {
return Object.keys(this.txs).map(function(key) {
return this.txs[key];
}, this);
};
Mempool.prototype.has =
Mempool.prototype.hasTX = function hasTX(hash) {
return !!this.get(hash);
};
Mempool.prototype.add =
Mempool.prototype.addTX = function addTX(tx, peer, callback) {
var self = this;
var flags = constants.flags.STANDARD_VERIFY_FLAGS;
var hash = tx.hash('hex');
assert(tx.ts === 0);
callback = utils.asyncify(callback);
if (this.locked)
return callback(new Error('Mempool is locked.'));
if (this.count >= 50000)
return callback(new Error('Mempool is full.'));
if (this.size >= 20 * 1024 * 1024)
return callback(new Error('Mempool is full.'));
if (this.txs[hash])
return callback(new Error('Already have TX.'));
if (tx.isCoinbase())
return callback(new Error('What?'));
if (!this.checkTX(tx, peer))
return callback(new Error('TX failed checkTX.'));
assert(tx.ts === 0);
this._lockTX(tx);
this.chain.fillCoin(tx, function(err) {
var i, input, dup, height, ts, priority;
self._unlockTX(tx);
if (err)
return callback(err);
// Do this in the future.
// tx = self.fillCoin(tx);
if (!tx.hasPrevout()) {
return callback(new Error('Previous outputs not found.'));
peer.reject({
data: tx.hash(),
reason: 'no-prevout'
});
return callback(new Error('Previous outputs not found.'));
}
if (!tx.isStandard(flags)) {
return callback(new Error('TX is not standard.'));
peer.reject({
data: tx.hash(),
reason: 'non-standard'
});
self.node.pool.setMisbehavior(peer, 100);
return callback(new Error('TX is not standard.'));
}
if (!tx.isStandardInputs(flags)) {
return callback(new Error('TX inputs are not standard.'));
peer.reject({
data: tx.hash(),
reason: 'non-standard-inputs'
});
self.node.pool.setMisbehavior(peer, 100);
return callback(new Error('TX inputs are not standard.'));
}
if (tx.getOutputValue().cmp(tx.getInputValue()) > 0) {
return callback(new Error('TX is spending coins that it does not have.'));
peer.reject({
data: tx.hash(),
reason: 'nonexistent-coins'
});
self.node.pool.setMisbehavior(peer, 100);
return callback(new Error('TX is spending coins that it does not have.'));
}
height = self.node.pool.chain.height + 1;
ts = utils.now();
if (!tx.isFinal(height, ts)) {
return callback(new Error('TX is not final.'));
peer.reject({
data: tx.hash(),
reason: 'not-final'
});
self.node.pool.setMisbehavior(peer, 100);
return callback(new Error('TX is not final.'));
}
for (i = 0; i < tx.inputs.length; i++) {
input = tx.inputs[i];
dup = self.spent[input.prevout.hash + '/' + input.prevout.index];
if (dup) {
// Replace-by-fee
if (input.sequence === 0xffffffff - 1) {
if (dup.getFee().cmp(tx.getFee()) < 0) {
self.remove(dup);
continue;
}
}
return callback(new Error('TX is double spending.'));
peer.reject({
data: tx.hash(),
reason: 'double-spend'
});
return callback(new Error('TX is double spending.'));
}
}
for (i = 0; i < tx.outputs.length; i++) {
output = tx.outputs[i];
if (output.value.cmpn(0) < 0) {
return callback(new Error('TX is spending negative coins.'));
peer.reject({
data: tx.hash(),
reason: 'negative-value'
});
self.node.pool.setMisbehavior(peer, 100);
return callback(new Error('TX is spending negative coins.'));
}
}
if (!tx.verify(null, true, flags)) {
return callback(new Error('TX did not verify.'));
peer.reject({
data: tx.hash(),
reason: 'script-failed'
});
self.node.pool.setMisbehavior(peer, 100);
return callback(new Error('TX did not verify.'));
}
for (i = 0; i < tx.inputs.length; i++) {
input = tx.inputs[i];
self.spent[input.prevout.hash + '/' + input.prevout.index] = tx;
self.size += input.output.getSize();
}
// Possibly do something bitcoinxt-like here with priority
priority = tx.getPriority();
tx.inputs.forEach(function(input) {
var type = input.getType();
var address = input.getAddress();
if (type === 'pubkey' || type === 'multisig')
address = null;
if (!address)
return;
if (!self.addresses[address])
self.addresses[address] = {};
self.addresses[address][hash] = true;
});
tx.outputs.forEach(function(output) {
var type = output.getType();
var address = output.getAddress();
if (type === 'pubkey' || type === 'multisig')
address = null;
if (!address)
return;
if (!self.addresses[address])
self.addresses[address] = {};
self.addresses[address][hash] = true;
});
self.txs[hash] = tx;
self.count++;
self.size += tx.getSize();
self.emit('tx', tx);
});
};
// Lock a tx to prevent race conditions
Mempool.prototype._lockTX = function _lockTX(tx) {
var hash = tx.hash('hex');
var i, input, id;
if (!this.txs[hash])
this.txs[hash] = tx;
for (i = 0; i < tx.inputs.length; i++) {
input = tx.inputs[i];
id = input.prevout.hash + '/' + input.prevout.index;
if (!this.spent[id])
this.spent[id] = tx;
}
};
Mempool.prototype._unlockTX = function _unlockTX(tx) {
var hash = tx.hash('hex');
var i, input, id;
if (this.txs[hash] === tx)
delete this.txs[hash];
for (i = 0; i < tx.inputs.length; i++) {
input = tx.inputs[i];
id = input.prevout.hash + '/' + input.prevout.index;
if (this.spent[id] === tx)
delete this.spent[id];
}
};
Mempool.prototype.remove =
Mempool.prototype.removeTX = function removeTX(hash, callback) {
var self = this;
var tx, input, id;
callback = utils.asyncify(callback);
if (hash instanceof bcoin.tx)
hash = hash.hash('hex');
tx = this.txs[hash];
if (!tx)
return callback(new Error('TX does not exist in mempool.'));
for (i = 0; i < tx.inputs.length; i++) {
input = tx.inputs[i];
id = input.prevout.hash + '/' + input.prevout.index;
if (this.spent[id] === tx)
delete this.spent[id];
}
tx.inputs.forEach(function(input) {
var type = input.getType();
var address = input.getAddress();
if (type === 'pubkey' || type === 'multisig')
address = null;
if (!address)
return;
if (self.addresses[address]) {
delete self.addresses[address][hash];
if (Object.keys(self.addresses[address]).length === 0)
delete self.addresses[address];
}
});
tx.outputs.forEach(function(output) {
var type = output.getType();
var address = output.getAddress();
if (type === 'pubkey' || type === 'multisig')
address = null;
if (!address)
return;
if (self.addresses[address]) {
delete self.addresses[address][hash];
if (Object.keys(self.addresses[address]).length === 0)
delete self.addresses[address];
}
});
delete this.txs[hash];
this.count--;
this.size -= tx.getSize();
this.emit('remove tx', tx);
};
// Need to lock the mempool when
// downloading a new block.
Mempool.prototype.lock = function lock() {
this.locked = true;
};
Mempool.prototype.unlock = function unlock() {
this.locked = false;
};
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');
if (tx.outputs.length === 0)
return this.reject(peer, tx, 'bad-txns-vout-empty');
if (tx.getSize() > constants.block.maxSize)
return this.reject(peer, tx, 'bad-txns-oversize');
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');
if (output.value.cmp(constants.maxMoney) > 0)
return this.reject(peer, tx, 'bad-txns-vout-toolarge');
total.iadd(output.value);
if (total.cmpn(0) < 0 || total.cmp(constants.maxMoney))
return this.reject(peer, tx, 'bad-txns-txouttotal-toolarge');
}
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');
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');
} 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');
}
}
return true;
};
Mempool.prototype.reject = function reject(peer, obj, reason) {
return false;
if (!peer)
return false;
peer.reject({
reason: reason,
data: obj.hash ? obj.hash() : []
});
return false;
};
/**
* Expose
*/
module.exports = Mempool;