diff --git a/lib/blockchain/chain.js b/lib/blockchain/chain.js index 8bc94475..28173741 100644 --- a/lib/blockchain/chain.js +++ b/lib/blockchain/chain.js @@ -1136,6 +1136,21 @@ Chain.prototype._invalidate = co(function* _invalidate(hash) { this.chain.setInvalid(hash); }); +/** + * Retroactively prune the database. + * @method + * @returns {Promise} + */ + +Chain.prototype.retroPrune = co(function* retroPrune() { + var unlock = yield this.locker.lock(); + try { + return yield this.db.retroPrune(this.tip.hash); + } finally { + unlock(); + } +}); + /** * Scan the blockchain for transactions containing specified address hashes. * @method @@ -2346,6 +2361,7 @@ function ChainOptions(options) { this.indexTX = false; this.indexAddress = false; this.forceWitness = false; + this.forcePrune = false; this.coinCache = 0; this.entryCache = 5000; @@ -2430,6 +2446,13 @@ ChainOptions.prototype.fromOptions = function fromOptions(options) { this.forceWitness = options.forceWitness; } + if (options.forcePrune != null) { + assert(typeof options.forcePrune === 'boolean'); + this.forcePrune = options.forcePrune; + if (options.forcePrune) + this.prune = true; + } + if (options.coinCache != null) { assert(util.isNumber(options.coinCache)); this.coinCache = options.coinCache; diff --git a/lib/blockchain/chaindb.js b/lib/blockchain/chaindb.js index 1245001f..ee68e6a2 100644 --- a/lib/blockchain/chaindb.js +++ b/lib/blockchain/chaindb.js @@ -91,7 +91,7 @@ ChainDB.prototype.open = co(function* open() { if (state) { // Verify options have not changed. - yield this.verifyFlags(); + yield this.verifyFlags(state); // Verify deployment params have not changed. yield this.verifyDeployments(); @@ -492,18 +492,64 @@ ChainDB.prototype.getFlags = co(function* getFlags() { /** * Verify current options against db options. * @method + * @param {ChainState} state * @returns {Promise} */ -ChainDB.prototype.verifyFlags = co(function* verifyFlags() { +ChainDB.prototype.verifyFlags = co(function* verifyFlags(state) { + var options = this.options; var flags = yield this.getFlags(); + var needsWitness = false; + var needsPrune = false; - assert(flags, 'No flags found.'); + if (!flags) + throw new Error('No flags found.'); - flags.verify(this.options); + if (options.network !== flags.network) + throw new Error('Network mismatch for chain.'); - if (this.options.forceWitness) + if (options.spv && !flags.spv) + throw new Error('Cannot retroactively enable SPV.'); + + if (!options.spv && flags.spv) + throw new Error('Cannot retroactively disable SPV.'); + + if (!flags.witness) { + if (!options.forceWitness) + throw new Error('Cannot retroactively enable witness.'); + needsWitness = true; + } + + if (options.prune && !flags.prune) { + if (!options.forcePrune) + throw new Error('Cannot retroactively prune.'); + needsPrune = true; + } + + if (!options.prune && flags.prune) + throw new Error('Cannot retroactively unprune.'); + + if (options.indexTX && !flags.indexTX) + throw new Error('Cannot retroactively enable TX indexing.'); + + if (!options.indexTX && flags.indexTX) + throw new Error('Cannot retroactively disable TX indexing.'); + + if (options.indexAddress && !flags.indexAddress) + throw new Error('Cannot retroactively enable address indexing.'); + + if (!options.indexAddress && flags.indexAddress) + throw new Error('Cannot retroactively disable address indexing.'); + + if (needsWitness) { + yield this.logger.info('Writing witness bit to chain flags.'); yield this.saveFlags(); + } + + if (needsPrune) { + yield this.logger.info('Retroactively pruning chain.'); + yield this.retroPrune(state.hash()); + } }); /** @@ -653,6 +699,58 @@ ChainDB.prototype.invalidateCache = co(function* invalidateCache(bit, batch) { } }); +/** + * Retroactively prune the database. + * @method + * @param {Hash} tip + * @returns {Promise} + */ + +ChainDB.prototype.retroPrune = co(function* retroPrune(tip) { + var options = this.options; + var keepBlocks = this.network.block.keepBlocks; + var pruneAfter = this.network.block.pruneAfterHeight; + var flags = yield this.getFlags(); + var height = yield this.getHeight(tip); + var i, start, end, batch, hash; + + if (flags.prune) + throw new Error('Already pruned.'); + + if (height <= pruneAfter + keepBlocks) + return false; + + batch = this.db.batch(); + start = pruneAfter + 1; + end = height - keepBlocks; + + for (i = start; i <= end; i++) { + hash = yield this.getHash(i); + + if (!hash) + throw new Error('Cannot find hash for ' + i); + + batch.del(layout.b(hash)); + batch.del(layout.u(hash)); + } + + try { + options.prune = true; + + flags = ChainFlags.fromOptions(options); + assert(flags.prune); + + batch.put(layout.O, flags.toRaw()); + + yield batch.write(); + } catch (e) { + options.prune = false; + throw e; + } + + return true; +}); + /** * Get the _next_ block hash (does not work by height). * @method @@ -1946,40 +2044,6 @@ ChainFlags.fromOptions = function fromOptions(data) { return new ChainFlags().fromOptions(data); }; -ChainFlags.prototype.verify = function verify(options) { - if (options.network !== this.network) - throw new Error('Network mismatch for chain.'); - - if (options.spv && !this.spv) - throw new Error('Cannot retroactively enable SPV.'); - - if (!options.spv && this.spv) - throw new Error('Cannot retroactively disable SPV.'); - - if (!options.forceWitness) { - if (!this.witness) - throw new Error('Cannot retroactively enable witness.'); - } - - if (options.prune && !this.prune) - throw new Error('Cannot retroactively prune.'); - - if (!options.prune && this.prune) - throw new Error('Cannot retroactively unprune.'); - - if (options.indexTX && !this.indexTX) - throw new Error('Cannot retroactively enable TX indexing.'); - - if (!options.indexTX && this.indexTX) - throw new Error('Cannot retroactively disable TX indexing.'); - - if (options.indexAddress && !this.indexAddress) - throw new Error('Cannot retroactively enable address indexing.'); - - if (!options.indexAddress && this.indexAddress) - throw new Error('Cannot retroactively disable address indexing.'); -}; - ChainFlags.prototype.toRaw = function toRaw() { var bw = new StaticWriter(12); var flags = 0; diff --git a/lib/http/rpc.js b/lib/http/rpc.js index 19ec3563..298d90bb 100644 --- a/lib/http/rpc.js +++ b/lib/http/rpc.js @@ -94,6 +94,7 @@ RPC.prototype.init = function init() { this.add('getrawmempool', this.getRawMempool); this.add('gettxout', this.getTXOut); this.add('gettxoutsetinfo', this.getTXOutSetInfo); + this.add('pruneblockchain', this.pruneBlockchain); this.add('verifychain', this.verifyChain); this.add('invalidateblock', this.invalidateBlock); @@ -459,7 +460,12 @@ RPC.prototype.setBan = co(function* setBan(args, help) { 'setban "ip(/netmask)" "add|remove" (bantime) (absolute)'); } - addr = NetAddress.fromHostname(addr, this.network); + try { + addr = NetAddress.fromHostname(addr, this.network); + } catch (e) { + throw new RPCError(errs.CLIENT_INVALID_IP_OR_SUBNET, + 'Invalid IP or subnet.'); + } switch (action) { case 'add': @@ -571,7 +577,7 @@ RPC.prototype.getBlock = co(function* getBlock(args, help) { if (this.chain.options.prune) throw new RPCError(errs.MISC_ERROR, 'Block not available (pruned data)'); - throw new RPCError(errs.DATABASE_ERROR, 'Can\'t read block from disk'); + throw new RPCError(errs.MISC_ERROR, 'Can\'t read block from disk'); } if (!verbose) @@ -997,6 +1003,26 @@ RPC.prototype.getTXOutSetInfo = co(function* getTXOutSetInfo(args, help) { }; }); +RPC.prototype.pruneBlockchain = co(function* pruneBlockchain(args, help) { + if (help || args.length !== 0) + throw new RPCError(errs.MISC_ERROR, 'pruneblockchain'); + + if (this.chain.options.spv) + throw new RPCError(errs.MISC_ERROR, 'Cannot prune chain in SPV mode.'); + + if (this.chain.options.prune) + throw new RPCError(errs.MISC_ERROR, 'Chain is already pruned.'); + + if (this.chain.height < this.network.block.pruneAfterHeight) + throw new RPCError(errs.MISC_ERROR, 'Chain is too short for pruning.'); + + try { + yield this.chain.retroPrune(); + } catch (e) { + throw new RPCError(errs.DATABASE_ERROR, e.message); + } +}); + RPC.prototype.verifyChain = co(function* verifyChain(args, help) { var valid = new Validator([args]); var level = valid.u32(0); diff --git a/lib/node/fullnode.js b/lib/node/fullnode.js index 4905c294..d80ff4ba 100644 --- a/lib/node/fullnode.js +++ b/lib/node/fullnode.js @@ -57,6 +57,7 @@ function FullNode(options) { maxFiles: this.config.num('max-files'), cacheSize: this.config.mb('cache-size'), forceWitness: this.config.bool('force-witness'), + forcePrune: this.config.bool('force-prune'), prune: this.config.bool('prune'), checkpoints: this.config.bool('checkpoints'), coinCache: this.config.mb('coin-cache'),