Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wallet rescan: Add error for SPV mode and excessive depth in pruned mode #532

Closed
wants to merge 16 commits into from
Closed
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Bcoin Release Notes & Changelog

## vX.Y.Z

### Regtest pruning policy changed

In the old configuration, a pruned node on Regtest would start pruning at height
1000 and then keep the last 1000 blocks on disk (pruning anything older). This
has been changed to facilitate testing. The new parameters are
`pruneAfterHeight: 500, keepBlocks: 288`.

## v1.0.0

### Migration
Expand Down
4 changes: 3 additions & 1 deletion lib/node/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,9 @@ class HTTP extends Server {
chain: {
height: this.chain.height,
tip: this.chain.tip.rhash(),
progress: this.chain.getProgress()
progress: this.chain.getProgress(),
spv: this.chain.options.spv,
pinheadmz marked this conversation as resolved.
Show resolved Hide resolved
prune: this.chain.options.prune
},
pool: {
host: addr.host,
Expand Down
4 changes: 2 additions & 2 deletions lib/protocol/networks.js
Original file line number Diff line number Diff line change
Expand Up @@ -775,8 +775,8 @@ regtest.block = {
bip65hash: null,
bip66height: 1251,
bip66hash: null,
pruneAfterHeight: 1000,
keepBlocks: 10000,
pruneAfterHeight: 500,
pinheadmz marked this conversation as resolved.
Show resolved Hide resolved
keepBlocks: 288,
maxTipAge: 0xffffffff,
slowHeight: 0
};
Expand Down
4 changes: 4 additions & 0 deletions lib/wallet/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ class WalletClient extends NodeClient {

return super.rescan(start);
}

async getInfo() {
return (await super.getInfo());
}
}

/*
Expand Down
15 changes: 15 additions & 0 deletions lib/wallet/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class HTTP extends Server {
this.logger = this.options.logger.context('http');
this.wdb = this.options.node.wdb;
this.rpc = this.options.node.rpc;
this.chain = this.options.node.chain;

this.init();
}
Expand Down Expand Up @@ -190,9 +191,23 @@ class HTTP extends Server {
return;
}

// SPV doesn't have blocks on disk, so rescan is ineffective
enforce(!this.options.node.chainInfo.spv, 'Cannot rescan in SPV mode.' +
' Try rewinding blockchain with node `reset` command.');

const valid = Validator.fromRequest(req);
const height = valid.u32('height');

// Pruned node can only rescan the blocks it has on disk
if (this.options.node.chainInfo.prune) {
const tipHeight = await this.options.node.client.getTip();
const pruneDepth = this.network.block.keepBlocks;
const minHeight = tipHeight.height - pruneDepth;

enforce(height > minHeight,
'Rescan height must be greater than prune height of ' + minHeight);
}

res.json(200, { success: true });

await this.wdb.rescan(height);
Expand Down
6 changes: 6 additions & 0 deletions lib/wallet/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,12 @@ class WalletNode extends Node {
await this.http.open();
await this.handleOpen();

const clientInfo = await this.client.getInfo();
this.chainInfo = {
spv: clientInfo.chain.spv,
prune: clientInfo.chain.prune
};

this.logger.info('Wallet node is loaded.');
}

Expand Down
14 changes: 14 additions & 0 deletions lib/wallet/nodeclient.js
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,20 @@ class NodeClient extends AsyncEmitter {
return this.emitAsync('block rescan', entry, txs);
});
}

/**
* Get relevant node info for wallet
* @returns {Promise}
*/

async getInfo() {
return Promise.resolve({
chain: {
spv:this.node.chain.options.spv,
prune:this.node.chain.options.prune
}
});
}
}

/*
Expand Down
14 changes: 14 additions & 0 deletions lib/wallet/nullclient.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,20 @@ class NullClient extends EventEmitter {
async rescan(start) {
;
}

/**
* Get relevant node info for wallet
* @returns {Promise}
*/

async getInfo() {
return Promise.resolve({
chain: {
spv: false,
prune: false
}
});
}
}

/*
Expand Down
6 changes: 6 additions & 0 deletions lib/wallet/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ class Plugin extends EventEmitter {
await this.wdb.open();
this.rpc.wallet = this.wdb.primary;
await this.http.open();

const clientInfo = await this.client.getInfo();
this.chainInfo = {
spv: clientInfo.chain.spv,
prune: clientInfo.chain.prune
};
}

async close() {
Expand Down
16 changes: 16 additions & 0 deletions lib/wallet/walletdb.js
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,12 @@ class WalletDB extends EventEmitter {
this.state.height,
this.state.startHeight);

const clientInfo = await this.client.getInfo();
this.chainInfo = {
spv: clientInfo.chain.spv,
prune: clientInfo.chain.prune
};

const wallet = await this.ensure({
id: 'primary'
});
Expand Down Expand Up @@ -440,6 +446,16 @@ class WalletDB extends EventEmitter {

assert((height >>> 0) === height, 'WDB: Must pass in a height.');

// Pruned node can only rescan the blocks it has on disk
if (this.chainInfo.prune) {
const tipHeight = await this.client.getTip();
const pruneDepth = this.network.block.keepBlocks;
const minHeight = tipHeight.height - pruneDepth;

assert(height > minHeight,
'Rescan height must be greater than prune height of ' + minHeight);
}

this.logger.info(
'WalletDB is scanning %d blocks.',
this.state.height - height + 1);
Expand Down
64 changes: 64 additions & 0 deletions test/prune-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/* eslint-env mocha */
/* eslint prefer-arrow-callback: "off" */

'use strict';

const assert = require('./util/assert');
const FullNode = require('../lib/node/fullnode');
const Block = require('../lib/primitives/block');

const node = new FullNode({
network: 'regtest',
memory: true,
plugins: [require('../lib/wallet/plugin')],
prune: true
});

describe('Pruned node', function() {
this.timeout(60000);

it('should open node', async () => {
await node.open();
});

it('should indicate prune in getInfo', async () => {
assert.strictEqual(true, node.chain.options.prune);
});

it('should generate 1000 blocks', async () => {
for (let i = 0; i < 1000; i++) {
const block = await node.miner.cpu.mineBlock();
assert(block);
assert(await node.chain.add(block));
}
assert.strictEqual(1000, node.chain.height);
});

it('should fail to rescan past prune height', async () => {
const pruneHeight = 1000 - 288;

// This block is not on disk
assert.strictEqual(null, await node.getBlock(pruneHeight));

// Try to rescan it anyway
try {
await node.plugins.walletdb.wdb.rescan(pruneHeight);
} catch(e) {
assert.strictEqual(
e.message,
'Rescan height must be greater than prune height of ' + pruneHeight
);
}
});

it('should succeed to rescan within prune height', async () => {
const pruneHeight = 1000 - 288;
// This block *IS* on disk
assert((await node.getBlock(pruneHeight + 1)) instanceof Block);
await node.plugins.walletdb.wdb.rescan(pruneHeight + 1);
});

it('should cleanup', async () => {
await node.close();
});
});