mempool: persist to disk.

This commit is contained in:
Christopher Jeffrey 2017-02-28 11:32:32 -08:00
parent 08936c8970
commit 7bd89b35c3
No known key found for this signature in database
GPG Key ID: 8962AB9DE6666BBD
8 changed files with 334 additions and 33 deletions

View File

@ -46,6 +46,7 @@ limit-free: true
limit-free-relay: 15 limit-free-relay: 15
reject-absurd-fees: true reject-absurd-fees: true
replace-by-fee: false replace-by-fee: false
persistent-mempool: false
# #
# Pool # Pool
@ -89,6 +90,9 @@ known-peers: ./known-peers
coinbase-flags: mined by bcoin coinbase-flags: mined by bcoin
# payout-address: 1111111111111111111114oLvT2,1111111111111111111114oLvT2 # payout-address: 1111111111111111111114oLvT2,1111111111111111111114oLvT2
max-block-weight: 4000000
reserved-block-weight: 4000
reserved-block-sigops: 400
# #
# HTTP # HTTP

View File

@ -6,13 +6,14 @@
'use strict'; 'use strict';
var util = require('../utils/util');
var pad32 = util.pad32;
var layout = { var layout = {
R: 'R', R: 'R',
e: function e(id, hash) { V: 'V',
return 'e' + pad32(id) + hex(hash); e: function e(hash) {
return 'e' + hex(hash);
},
ee: function ee(key) {
return key.slice(1, 65);
} }
}; };

View File

@ -9,17 +9,21 @@
/* /*
* Database Layout: * Database Layout:
* R -> tip hash * R -> tip hash
* V -> db version
* e[id][hash] -> entry * e[id][hash] -> entry
*/ */
var layout = { var layout = {
R: new Buffer([0x52]), R: new Buffer([0x52]),
e: function e(id, hash) { V: new Buffer([0x76]),
var key = new Buffer(37); e: function e(hash) {
var key = new Buffer(33);
key[0] = 0x65; key[0] = 0x65;
key.writeUInt32BE(id, 1, true); write(key, hash, 1);
write(key, hash, 5);
return key; return key;
},
ee: function ee(key) {
return key.toString('hex', 1, 33);
} }
}; };

View File

@ -24,6 +24,9 @@ var Coin = require('../primitives/coin');
var TXMeta = require('../primitives/txmeta'); var TXMeta = require('../primitives/txmeta');
var MempoolEntry = require('./mempoolentry'); var MempoolEntry = require('./mempoolentry');
var Network = require('../protocol/network'); var Network = require('../protocol/network');
var encoding = require('../utils/encoding');
var layout = require('./layout');
var LDB = require('../db/ldb');
var VerifyError = errors.VerifyError; var VerifyError = errors.VerifyError;
var VerifyResult = errors.VerifyResult; var VerifyResult = errors.VerifyResult;
@ -73,11 +76,14 @@ function Mempool(options) {
this.locker = this.chain.locker; this.locker = this.chain.locker;
this.cache = new MempoolCache(this.options);
this.size = 0; this.size = 0;
this.totalOrphans = 0; this.totalOrphans = 0;
this.totalTX = 0; this.totalTX = 0;
this.freeCount = 0; this.freeCount = 0;
this.lastTime = 0; this.lastTime = 0;
this.lastUpdate = 0;
this.waiting = {}; this.waiting = {};
this.orphans = {}; this.orphans = {};
@ -100,7 +106,20 @@ util.inherits(Mempool, AsyncObject);
Mempool.prototype._open = co(function* open() { Mempool.prototype._open = co(function* open() {
var size = (this.options.maxSize / 1024).toFixed(2); var size = (this.options.maxSize / 1024).toFixed(2);
var i, entries, entry;
yield this.chain.open(); yield this.chain.open();
yield this.cache.open();
if (this.options.persistent) {
entries = yield this.cache.load();
for (i = 0; i < entries.length; i++) {
entry = entries[i];
this.trackEntry(entry);
}
}
this.logger.info('Mempool loaded (maxsize=%dkb).', size); this.logger.info('Mempool loaded (maxsize=%dkb).', size);
}); });
@ -110,9 +129,9 @@ Mempool.prototype._open = co(function* open() {
* @returns {Promise} * @returns {Promise}
*/ */
Mempool.prototype._close = function close() { Mempool.prototype._close = co(function* close() {
return Promise.resolve(); yield this.cache.close();
}; });
/** /**
* Notify the mempool that a new block has come * Notify the mempool that a new block has come
@ -127,7 +146,7 @@ Mempool.prototype._close = function close() {
Mempool.prototype.addBlock = co(function* addBlock(block, txs) { Mempool.prototype.addBlock = co(function* addBlock(block, txs) {
var unlock = yield this.locker.lock(); var unlock = yield this.locker.lock();
try { try {
return this._addBlock(block, txs); return yield this._addBlock(block, txs);
} finally { } finally {
unlock(); unlock();
} }
@ -142,7 +161,7 @@ Mempool.prototype.addBlock = co(function* addBlock(block, txs) {
* @returns {Promise} * @returns {Promise}
*/ */
Mempool.prototype._addBlock = function addBlock(block, txs) { Mempool.prototype._addBlock = co(function* addBlock(block, txs) {
var entries = []; var entries = [];
var i, entry, tx, hash; var i, entry, tx, hash;
@ -172,13 +191,17 @@ Mempool.prototype._addBlock = function addBlock(block, txs) {
// There may be a locktime in a TX that is now valid. // There may be a locktime in a TX that is now valid.
this.rejects.reset(); this.rejects.reset();
this.cache.sync(block.hash);
yield this.cache.flush();
if (entries.length === 0) if (entries.length === 0)
return; return;
this.logger.debug( this.logger.debug(
'Removed %d txs from mempool for block %d.', 'Removed %d txs from mempool for block %d.',
entries.length, block.height); entries.length, block.height);
}; });
/** /**
* Notify the mempool that a block has been disconnected * Notify the mempool that a block has been disconnected
@ -220,7 +243,7 @@ Mempool.prototype._removeBlock = co(function* removeBlock(block, txs) {
continue; continue;
try { try {
yield this._addTX(tx); yield this.insertTX(tx);
total++; total++;
} catch (e) { } catch (e) {
this.emit('error', e); this.emit('error', e);
@ -232,6 +255,10 @@ Mempool.prototype._removeBlock = co(function* removeBlock(block, txs) {
this.rejects.reset(); this.rejects.reset();
this.cache.sync(block.prevBlock);
yield this.cache.flush();
if (total === 0) if (total === 0)
return; return;
@ -249,7 +276,7 @@ Mempool.prototype._removeBlock = co(function* removeBlock(block, txs) {
Mempool.prototype.reset = co(function* reset() { Mempool.prototype.reset = co(function* reset() {
var unlock = yield this.locker.lock(); var unlock = yield this.locker.lock();
try { try {
return this._reset(); return yield this._reset();
} finally { } finally {
unlock(); unlock();
} }
@ -260,7 +287,7 @@ Mempool.prototype.reset = co(function* reset() {
* @private * @private
*/ */
Mempool.prototype._reset = function reset() { Mempool.prototype._reset = co(function* reset() {
this.logger.info('Mempool reset (%d txs removed).', this.totalTX); this.logger.info('Mempool reset (%d txs removed).', this.totalTX);
this.size = 0; this.size = 0;
@ -281,7 +308,12 @@ Mempool.prototype._reset = function reset() {
this.fees.reset(); this.fees.reset();
this.rejects.reset(); this.rejects.reset();
};
if (this.options.persistent) {
yield this.cache.wipe();
this.cache.clear();
}
});
/** /**
* Ensure the size of the mempool stays below 300mb. * Ensure the size of the mempool stays below 300mb.
@ -620,12 +652,6 @@ Mempool.prototype.addTX = co(function* addTX(tx) {
var unlock = yield this.locker.lock(hash); var unlock = yield this.locker.lock(hash);
try { try {
return yield this._addTX(tx); return yield this._addTX(tx);
} catch (err) {
if (err.type === 'VerifyError') {
if (!tx.hasWitness() && !err.malleated)
this.rejects.add(tx.hash());
}
throw err;
} finally { } finally {
unlock(); unlock();
} }
@ -640,6 +666,33 @@ Mempool.prototype.addTX = co(function* addTX(tx) {
*/ */
Mempool.prototype._addTX = co(function* _addTX(tx) { Mempool.prototype._addTX = co(function* _addTX(tx) {
var result;
try {
result = yield this.insertTX(tx);
} catch (err) {
if (err.type === 'VerifyError') {
if (!tx.hasWitness() && !err.malleated)
this.rejects.add(tx.hash());
}
throw err;
}
if (util.now() - this.lastUpdate > 10) {
yield this.cache.flush();
this.lastUpdate = util.now();
}
});
/**
* Add a transaction to the mempool without a lock.
* @method
* @private
* @param {TX} tx
* @returns {Promise}
*/
Mempool.prototype.insertTX = co(function* insertTX(tx) {
var lockFlags = common.lockFlags.STANDARD_LOCKTIME_FLAGS; var lockFlags = common.lockFlags.STANDARD_LOCKTIME_FLAGS;
var hash = tx.hash('hex'); var hash = tx.hash('hex');
var ret = new VerifyResult(); var ret = new VerifyResult();
@ -987,6 +1040,8 @@ Mempool.prototype.addEntry = co(function* addEntry(entry, view) {
this.logger.debug('Added tx %s to mempool.', tx.txid()); this.logger.debug('Added tx %s to mempool.', tx.txid());
this.cache.save(entry);
yield this.handleOrphans(tx); yield this.handleOrphans(tx);
}); });
@ -1017,6 +1072,8 @@ Mempool.prototype.removeEntry = function removeEntry(entry, limit) {
if (this.fees) if (this.fees)
this.fees.removeTX(hash); this.fees.removeTX(hash);
this.cache.remove(tx.hash());
this.emit('remove entry', entry); this.emit('remove entry', entry);
}; };
@ -1373,7 +1430,7 @@ Mempool.prototype.handleOrphans = co(function* handleOrphans(tx) {
orphan = resolved[i]; orphan = resolved[i];
try { try {
yield this._addTX(orphan); yield this.insertTX(orphan);
} catch (err) { } catch (err) {
if (err.type === 'VerifyError') { if (err.type === 'VerifyError') {
this.logger.debug( this.logger.debug(
@ -1621,8 +1678,10 @@ Mempool.prototype.trackEntry = function trackEntry(entry, view) {
this.spents[key] = entry; this.spents[key] = entry;
} }
if (this.options.indexAddress) if (this.options.indexAddress) {
assert(view, 'No view passed to trackEntry.');
this.indexEntry(entry, view); this.indexEntry(entry, view);
}
this.size += this.memUsage(tx); this.size += this.memUsage(tx);
this.totalTX++; this.totalTX++;
@ -1865,6 +1924,15 @@ function MempoolOptions(options) {
this.expiryTime = policy.MEMPOOL_EXPIRY_TIME; this.expiryTime = policy.MEMPOOL_EXPIRY_TIME;
this.minRelay = this.network.minRelay; this.minRelay = this.network.minRelay;
this.location = null;
this.db = 'memory';
this.maxFiles = 64;
this.cacheSize = 32 << 20;
this.compression = true;
this.bufferKeys = !util.isBrowser;
this.persistent = false;
this.fromOptions(options); this.fromOptions(options);
} }
@ -1962,6 +2030,38 @@ MempoolOptions.prototype.fromOptions = function fromOptions(options) {
this.minRelay = options.minRelay; this.minRelay = options.minRelay;
} }
if (options.location != null) {
assert(typeof options.location === 'string');
this.location = options.location;
}
if (options.db != null) {
assert(typeof options.db === 'string');
this.db = options.db;
}
if (options.maxFiles != null) {
assert(util.isNumber(options.maxFiles));
this.maxFiles = options.maxFiles;
}
if (options.cacheSize != null) {
assert(util.isNumber(options.cacheSize));
this.cacheSize = options.cacheSize;
}
if (options.compression != null) {
assert(typeof options.compression === 'boolean');
this.compression = options.compression;
}
if (options.persistent != null) {
assert(typeof options.persistent === 'boolean');
this.persistent = options.persistent;
assert(this.persistent !== this.indexAddress,
'Cannot have persistent mempool with address indexing.');
}
return this; return this;
}; };
@ -2162,8 +2262,12 @@ CoinIndex.prototype.remove = function remove(outpoint) {
delete this.map[key]; delete this.map[key];
}; };
/* /**
* Helpers * Orphan
* @constructor
* @ignore
* @param {TX} tx
* @param {Hash[]} missing
*/ */
function Orphan(tx, missing) { function Orphan(tx, missing) {
@ -2175,6 +2279,185 @@ Orphan.prototype.toTX = function toTX() {
return TX.fromRaw(this.raw); return TX.fromRaw(this.raw);
}; };
/**
* Mempool Cache
* @ignore
* @constructor
* @param {Object} options
*/
function MempoolCache(options) {
if (!(this instanceof MempoolCache))
return new MempoolCache(options);
this.logger = options.logger;
this.chain = options.chain;
this.db = null;
this.batch = null;
if (options.persistent)
this.db = LDB(options);
}
MempoolCache.VERSION = 0;
MempoolCache.prototype.getVersion = co(function* getVersion() {
var data = yield this.db.get(layout.V);
if (!data)
return -1;
return data.readUInt32LE(0, true);
});
MempoolCache.prototype.getTip = co(function* getTip() {
var hash = yield this.db.get(layout.R);
if (!hash)
return;
return hash.toString('hex');
});
MempoolCache.prototype.getEntries = function getEntries() {
return this.db.values({
gte: layout.e(encoding.ZERO_HASH),
lte: layout.e(encoding.MAX_HASH),
parse: MempoolEntry.fromRaw
});
};
MempoolCache.prototype.getKeys = function getKeys() {
return this.db.keys({
gte: layout.e(encoding.ZERO_HASH),
lte: layout.e(encoding.MAX_HASH)
});
};
MempoolCache.prototype.open = co(function* open() {
if (!this.db)
return;
yield this.db.open();
this.batch = this.db.batch();
});
MempoolCache.prototype.close = co(function* close() {
if (!this.db)
return;
yield this.db.close();
this.batch = null;
});
MempoolCache.prototype.save = function save(entry) {
if (!this.db)
return;
this.batch.put(layout.e(entry.tx.hash()), entry.toRaw());
};
MempoolCache.prototype.remove = function remove(hash) {
if (!this.db)
return;
this.batch.del(layout.e(hash));
};
MempoolCache.prototype.sync = function sync(hash) {
if (!this.db)
return;
this.batch.put(layout.R, new Buffer(hash, 'hex'));
};
MempoolCache.prototype.clear = function clear() {
this.batch.clear();
this.batch = this.db.batch();
};
MempoolCache.prototype.flush = co(function* flush() {
if (!this.db)
return;
yield this.batch.write();
this.batch = this.db.batch();
});
MempoolCache.prototype.init = co(function* init(hash) {
var batch = this.db.batch();
batch.put(layout.V, encoding.U32(MempoolCache.VERSION));
batch.put(layout.R, new Buffer(hash, 'hex'));
yield batch.write();
});
MempoolCache.prototype.load = co(function* load() {
var version = yield this.getVersion();
var i, tip, entries, entry;
if (version === -1) {
version = MempoolCache.VERSION;
tip = this.chain.tip.hash;
this.logger.info(
'Mempool cache is empty. Writing tip %s.',
util.revHex(tip));
yield this.init(tip);
}
if (version !== MempoolCache.VERSION) {
this.logger.warning(
'Mempool cache version mismatch (%d != %d)!',
version,
MempoolCache.VERSION);
this.logger.warning('Invalidating mempool cache.');
yield this.wipe();
return [];
}
tip = yield this.getTip();
if (tip !== this.chain.tip.hash) {
this.logger.warning(
'Mempool tip not consistent with chain tip (%s != %s)!',
util.revHex(tip),
this.chain.tip.rhash());
this.logger.warning('Invalidating mempool cache.');
yield this.wipe();
return [];
}
entries = yield this.getEntries();
this.logger.info(
'Loaded mempool from disk (%d entries).',
entries.length);
return entries;
});
MempoolCache.prototype.wipe = co(function* wipe() {
var batch = this.db.batch();
var keys = yield this.getKeys();
var i, key;
for (i = 0; i < keys.length; i++) {
key = keys[i];
batch.del(key);
}
batch.put(layout.V, encoding.U32(MempoolCache.VERSION));
batch.put(layout.R, new Buffer(this.chain.tip.hash, 'hex'));
yield batch.write();
this.logger.info('Removed %d mempool entries from disk.', keys.length);
});
/* /*
* Expose * Expose
*/ */

View File

@ -195,7 +195,7 @@ MempoolEntry.prototype.isFree = function isFree(height) {
*/ */
MempoolEntry.prototype.getSize = function getSize() { MempoolEntry.prototype.getSize = function getSize() {
return this.tx.getSize() + 49; return this.tx.getSize() + 53;
}; };
/** /**
@ -210,7 +210,7 @@ MempoolEntry.prototype.toRaw = function toRaw() {
bw.writeU32(this.size); bw.writeU32(this.size);
bw.writeU32(this.sigops); bw.writeU32(this.sigops);
bw.writeDouble(this.priority); bw.writeDouble(this.priority);
bw.writeU32(this.fee); bw.writeU64(this.fee);
bw.writeU32(this.ts); bw.writeU32(this.ts);
bw.writeU64(this.value); bw.writeU64(this.value);
bw.writeU8(this.dependencies ? 1 : 0); bw.writeU8(this.dependencies ? 1 : 0);
@ -233,7 +233,7 @@ MempoolEntry.prototype.fromRaw = function fromRaw(data) {
this.size = br.readU32(); this.size = br.readU32();
this.sigops = br.readU32(); this.sigops = br.readU32();
this.priority = br.readDouble(); this.priority = br.readDouble();
this.fee = br.readU32(); this.fee = br.readU64();
this.ts = br.readU32(); this.ts = br.readU32();
this.value = br.readU64(); this.value = br.readU64();
this.dependencies = br.readU8() === 1; this.dependencies = br.readU8() === 1;

View File

@ -256,6 +256,7 @@ config.toOptions = function toOptions(data) {
options.requireStandard = bool(data.requirestandard); options.requireStandard = bool(data.requirestandard);
options.rejectAbsurdFees = bool(data.rejectabsurdfees); options.rejectAbsurdFees = bool(data.rejectabsurdfees);
options.replaceByFee = bool(data.replacebyfee); options.replaceByFee = bool(data.replacebyfee);
options.persistentMempool = bool(data.persistentmempool);
// Pool // Pool
options.selfish = bool(data.selfish); options.selfish = bool(data.selfish);
@ -282,6 +283,8 @@ config.toOptions = function toOptions(data) {
options.payoutAddress = list(data.payoutaddress); options.payoutAddress = list(data.payoutaddress);
options.coinbaseFlags = str(data.coinbaseflags); options.coinbaseFlags = str(data.coinbaseflags);
options.maxBlockWeight = num(data.maxblockweight); options.maxBlockWeight = num(data.maxblockweight);
options.reservedBlockWeight = num(data.reservedblockweight);
options.reservedBlockSigops = num(data.reservedblocksigops);
// HTTP // HTTP
options.sslCert = file(data.sslcert, prefix); options.sslCert = file(data.sslcert, prefix);

View File

@ -75,6 +75,9 @@ function FullNode(options) {
logger: this.logger, logger: this.logger,
chain: this.chain, chain: this.chain,
fees: this.fees, fees: this.fees,
db: this.options.db,
location: this.location('mempool'),
persistent: this.options.persistentMempool,
maxSize: this.options.mempoolSize, maxSize: this.options.mempoolSize,
limitFree: this.options.limitFree, limitFree: this.options.limitFree,
limitFreeRelay: this.options.limitFreeRelay, limitFreeRelay: this.options.limitFreeRelay,
@ -120,7 +123,9 @@ function FullNode(options) {
fees: this.fees, fees: this.fees,
address: this.options.payoutAddress, address: this.options.payoutAddress,
coinbaseFlags: this.options.coinbaseFlags, coinbaseFlags: this.options.coinbaseFlags,
maxWeight: this.options.maxBlockWeight maxWeight: this.options.maxBlockWeight,
reservedWeight: this.options.reservedBlockWeight,
reservedSigops: this.options.reservedBlockSigops
}); });
// Wallet database needs access to fees. // Wallet database needs access to fees.

View File

@ -76,6 +76,7 @@
"./lib/http/rpcclient": "./browser/empty.js", "./lib/http/rpcclient": "./browser/empty.js",
"./lib/http/server": "./browser/empty.js", "./lib/http/server": "./browser/empty.js",
"./lib/http/wallet": "./browser/empty.js", "./lib/http/wallet": "./browser/empty.js",
"./lib/mempool/layout": "./lib/mempool/layout-browser.js",
"./lib/net/dns": "./lib/net/dns-browser.js", "./lib/net/dns": "./lib/net/dns-browser.js",
"./lib/net/tcp": "./lib/net/tcp-browser.js", "./lib/net/tcp": "./lib/net/tcp-browser.js",
"./lib/utils/native": "./browser/empty.js", "./lib/utils/native": "./browser/empty.js",