diff --git a/lib/bcoin.js b/lib/bcoin.js index bfe0b477..0f5f2b02 100644 --- a/lib/bcoin.js +++ b/lib/bcoin.js @@ -56,6 +56,7 @@ bcoin.ramdisk = require('./bcoin/ramdisk'); bcoin.chainblock = require('./bcoin/chainblock'); bcoin.chaindb = require('./bcoin/chaindb'); bcoin.chain = require('./bcoin/chain'); +bcoin.mempool = require('./bcoin/mempool'); bcoin.keypair = require('./bcoin/keypair'); bcoin.address = require('./bcoin/address'); bcoin.wallet = require('./bcoin/wallet'); diff --git a/lib/bcoin/chainblock.js b/lib/bcoin/chainblock.js index 70713196..51fdfb53 100644 --- a/lib/bcoin/chainblock.js +++ b/lib/bcoin/chainblock.js @@ -20,6 +20,9 @@ var fs = bcoin.fs; */ function ChainBlock(chain, data) { + if (!(this instanceof ChainBlock)) + return new ChainBlock(chain, data); + this.chain = chain; this.hash = data.hash; this.version = data.version; diff --git a/lib/bcoin/chaindb.js b/lib/bcoin/chaindb.js index eb165cb7..04a36a23 100644 --- a/lib/bcoin/chaindb.js +++ b/lib/bcoin/chaindb.js @@ -23,7 +23,7 @@ var BLOCK_SIZE = bcoin.chainblock.BLOCK_SIZE; function ChainDB(chain, options) { if (!(this instanceof ChainDB)) - return new ChainDB(chain); + return new ChainDB(chain, options); if (!options) options = {}; diff --git a/lib/bcoin/mempool.js b/lib/bcoin/mempool.js new file mode 100644 index 00000000..6bb2f31e --- /dev/null +++ b/lib/bcoin/mempool.js @@ -0,0 +1,264 @@ +/** + * mempool.js - mempool for bcoin + * Copyright (c) 2014-2015, Fedor Indutny (MIT License) + * https://github.com/indutny/bcoin + */ + +var inherits = require('inherits'); +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(pool, options) { + if (!(this instanceof Mempool)) + return new Mempool(pool, options); + + if (!options) + options = {}; + + this.options = options; + this.pool = pool; + this.storage = bcoin.db; + + this.txs = {}; + this.prevout = {}; + this.size = 0; + this.count = 0; + this.locked = false; + + this._init(); +} + +Mempool.prototype._init = function _init() { + var self = this; + + // Remove now-mined transactions + this.pool.on('block', function(block) { + 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'); + + self.remove(mtx); + }); + }); +}; + +Mempool.prototype.get = function get(hash) { + if (hash instanceof bcoin.tx) + hash = hash.hash('hex'); + return this.txs[hash]; +}; + +Mempool.prototype.getAll = function getAll(hash) { + return Object.keys(this.txs).map(function(key) { + return this.txs[key]; + }, this); +}; + +Mempool.prototype.has = function has(hash) { + return !!this.get(hash); +}; + +Mempool.prototype.add = function add(tx, peer, callback) { + var self = this; + 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.')); + + this._lockTX(tx); + + this.storage.fillTX(tx, function(err) { + var i, input, dup, height, ts, priority; + + self._unlockTX(tx); + + if (err) + return callback(err); + + if (!tx.hasPrevout()) { + pool.setMisbehavior(peer, 100); + return callback(new Error('Previous outputs not found.')); + } + + if (!tx.isStandard()) { + pool.setMisbehavior(peer, 100); + return callback(new Error('TX is not standard.')); + } + + if (!tx.isStandardInputs()) { + pool.setMisbehavior(peer, 100); + return callback(new Error('TX inputs are not standard.')); + } + + if (tx.getOutputValue().cmp(tx.getInputValue()) > 0) { + pool.setMisbehavior(peer, 100); + return callback(new Error('TX is spending coins that it does not have.')); + } + + height = self.pool.chain.height() + 1; + ts = utils.now(); + if (!tx.isFinal(height, ts)) { + pool.setMisbehavior(peer, 100); + return callback(new Error('TX is not final.')); + } + + for (i = 0; i < tx.inputs.length; i++) { + input = tx.inputs[i]; + if (input.output.spent) { + pool.setMisbehavior(peer, 100); + return callback(new Error('TX is spending old outputs.')); + } + dup = self.prevout[input.prevout.hash]; + if (dup) { + // Replace-by-fee + if (input.sequence === 0xffffffff - 1) { + if (dup.getFee().cmp(tx.getFee()) < 0) { + self.remove(dup); + continue; + } + } + pool.setMisbehavior(peer, 100); + 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) { + pool.setMisbehavior(peer, 100); + return callback(new Error('TX is spending negative coins.')); + } + } + + if (!tx.verify(true)) { + 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.prevout[input.prevout.hash] = tx; + } + + // Possibly do something bitcoinxt-like here with priority + priority = tx.getPriority(); + + self.txs[hash] = tx; + self.count++; + self.size += tx.getSize(); + + self.storage.saveMempoolTX(tx, function(err) { + if (err) + return callback(err); + + return callback(); + }); + }); +}; + +// Lock a tx to prevent race conditions +Mempool.prototype._lockTX = function _lockTX(tx) { + var hash = tx.hash('hex'); + var i, input; + + if (!this.txs[hash]) + this.txs[hash] = tx; + + for (i = 0; i < tx.inputs.length; i++) { + input = tx.inputs[i]; + if (!this.prevout[input.prevout.hash]) + this.prevout[input.prevout.hash] = tx; + } +}; + +Mempool.prototype._unlockTX = function _unlockTX(tx) { + var hash = tx.hash('hex'); + var i, input; + + if (this.txs[hash] === tx) + delete this.txs[hash]; + + for (i = 0; i < tx.inputs.length; i++) { + input = tx.inputs[i]; + if (this.prevout[input.prevout.hash] === tx) + delete this.prevout[input.prevout.hash]; + } +}; + +Mempool.prototype.remove = function remove(hash, callback) { + var self = this; + var tx, input; + + 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]; + if (this.prevout[input.prevout.hash] === tx) + delete this.prevout[input.prevout.hash]; + } + + delete this.txs[hash]; + + this.count--; + this.size -= tx.getSize(); + + this.storage.removeMempoolTX(tx, function(err) { + if (err) + return callback(err); + + return callback(); + }); +}; + +// 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; +}; + +/** + * Expose + */ + +module.exports = Mempool;