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

chain: emit 'reset' when reorganizing SPV chain #755

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/blockchain/chain.js
Original file line number Diff line number Diff line change
Expand Up @@ -923,7 +923,7 @@ class Chain extends AsyncEmitter {
// to the fork block, causing
// us to redownload the blocks
// on the new main chain.
await this._reset(fork.hash, true);
await this._reset(fork.hash, false);

// Emit disconnection events now that
// the chain has successfully reset.
Expand Down
237 changes: 237 additions & 0 deletions test/spv-node-sync-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
/* eslint-env mocha */
/* eslint prefer-arrow-callback: "off" */

'use strict';

const assert = require('./util/assert');
const FullNode = require('../lib/node/fullnode');
const SPVNode = require('../lib/node/spvnode');

const ports = {
full: {
p2p: 49331,
node: 49332,
wallet: 49333
},
spv: {
p2p: 49431,
node: 49432,
wallet: 49433
}
};

const node = new FullNode({
network: 'regtest',
workers: true,
listen: true,
bip37: true,
port: ports.full.p2p,
httpPort: ports.full.node,
maxOutbound: 1,
seeds: [],
memory: true,
plugins: [require('../lib/wallet/plugin')],
env: {
'BCOIN_WALLET_HTTP_PORT': (ports.full.wallet).toString()
}
});

const spvnode = new SPVNode({
network: 'regtest',
workers: true,
listen: true,
port: ports.spv.p2p,
httpPort: ports.spv.node,
maxOutbound: 1,
seeds: [],
nodes: [`127.0.0.1:${ports.full.p2p}`],
memory: true,
plugins: [require('../lib/wallet/plugin')],
env: {
'BCOIN_WALLET_HTTP_PORT': (ports.spv.wallet).toString()
}
});

const chain = node.chain;
const miner = node.miner;
const {wdb} = node.require('walletdb');
const {wdb: spvwdb} = spvnode.require('walletdb');

let wallet = null;
let spvwallet = null;
let spvaddr = null;
let tip1 = null;
let tip2 = null;

async function mineBlock(tip) {
const job = await miner.createJob(tip);
return await job.mineAsync();
}

async function event(obj, name) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should add this to test/util/common.js in #748, so it'll be available for other tests.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, it is a good test utility function

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you think #748 will get merged first, I'll wait and rebase and use common as a dependency. Or if this gets merged first, you can move it to common in #748 ?

Copy link
Contributor

@braydonf braydonf Apr 12, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I moved it to /test/util/common.js as from #748

return new Promise((resolve) => {
obj.once(name, resolve);
});
}

describe('SPV Node Sync', function() {
this.timeout(10000);

if (process.browser)
pinheadmz marked this conversation as resolved.
Show resolved Hide resolved
this.skip();

// const cbMaturity = consensus.COINBASE_MATURITY;
before(async () => {
// consensus.COINBASE_MATURITY = 0;
await node.open();
await spvnode.open();
await node.connect();
await spvnode.connect();
await spvnode.startSync();
});

after(async () => {
// consensus.COINBASE_MATURITY = cbMaturity;
await node.close();
await spvnode.close();
});

it('should check SPV is synced to fullnode', async () => {
assert.deepStrictEqual(node.chain.tip, spvnode.chain.tip);
});

it('should open miner and wallets', async () => {
wallet = await wdb.create();
miner.addresses.length = 0;
miner.addAddress(await wallet.receiveAddress());

spvwallet = await spvwdb.create();
spvaddr = await spvwallet.receiveAddress();
});

it('should mine 90 blocks', async () => {
for (let i = 0; i < 90; i++) {
const block = await miner.mineBlock();
assert(block);
await chain.add(block);

// Check SPV & Full nodes are in sync
await event(spvnode, 'block');
assert.deepStrictEqual(node.chain.tip, spvnode.chain.tip);
}
// Full node wallet needs to catch up to miner
await wdb.rescan(0);
});

it('should mine competing chains of 10 blocks', async function () {
for (let i = 0; i < 10; i++) {
const block1 = await mineBlock(tip1);
const block2 = await mineBlock(tip2);

await chain.add(block1);
await chain.add(block2);

assert.bufferEqual(chain.tip.hash, block1.hash());

tip1 = await chain.getEntry(block1.hash());
tip2 = await chain.getEntry(block2.hash());

assert(tip1);
assert(tip2);

assert(!await chain.isMainChain(tip2));

await new Promise(setImmediate);

// Check SPV & Full nodes are in sync after every block
await event(spvnode, 'block');
assert.deepStrictEqual(node.chain.tip, spvnode.chain.tip);
}
});

it('should send a tx from chain 1 to SPV node', async () => {
await wallet.send({
outputs: [{
value: 1012345678,
address: spvaddr
}]
});

await event(spvwallet, 'balance');
const balance = await spvwallet.getBalance();
assert.strictEqual(balance.unconfirmed, 1012345678);
});

it('should mine a block and confirm a tx', async () => {
const block = await miner.mineBlock();
assert(block);
await chain.add(block);

// Check SPV & Full nodes are in sync
await event(spvnode, 'block');
assert.deepStrictEqual(node.chain.tip, spvnode.chain.tip);

// Check SPV wallet balance
await event(spvwallet, 'balance');
const balance = await spvwallet.getBalance();
assert.strictEqual(balance.confirmed, 1012345678);
});

it('should handle a reorg', async () => {
assert.strictEqual(chain.height, 101);

// Main chain is ahead by 1 block now, catch the alt chain up
const entry = await chain.getEntry(tip2.hash);
const block1 = await miner.mineBlock(entry);
await chain.add(block1);
const entry1 = await chain.getEntry(block1.hash());
assert(entry1);

// Tie game!
assert.strictEqual(chain.height, entry1.height);

// Now reorg main chain by adding a block to alt chain
const block2 = await miner.mineBlock(entry1);
assert(block2);

let forked = false;
chain.once('reorganize', () => {
forked = true;
});

await chain.add(block2);

assert(forked);
assert.bufferEqual(chain.tip.hash, block2.hash());
assert(chain.tip.chainwork.gt(tip1.chainwork));

// Give SPV node a second to catch up before checking sync with fullnode.
// Waiting for specific events like 'block', 'full', or 'tip' is hard here
// because we don't really know when we are at the proper tip.
await new Promise(r => setTimeout(r, 5000));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can wait for an event with a specific value?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried a few things with while loops waiting for a certain chain height, can't really figure out a more elegant way to do this... any thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about something like this?

async function eventValue(obj, name, expected, timeout = 10000) {
  assert(typeof obj === 'object');
  assert(typeof name === 'string');

  return new Promise((resolve, reject) => {
    let timer = null;

    const check = (value) => {
      if (value === expected) {
        obj.removeListener(name, check);
        clearTimeout(timer);
        resolve();
      }
    };

    obj.addListener(name, check);

    timer = setTimeout(() => {
      obj.removeListener(name, check);
      reject(new Error('Timeout waiting for event value.'));
    }, timeout);
  });
}

I have been using this in #758 to wait for values:

async function forValue(obj, key, val, timeout = 60000) {
  assert(typeof obj === 'object');
  assert(typeof key === 'string');

  const ms = 10;
  let interval = null;
  let count = 0;

  return new Promise((resolve, reject) => {
    interval = setInterval(() => {
      if (obj[key] === val) {
        clearInterval(interval);
        resolve();
      } else if (count * ms >= timeout) {
        clearInterval(interval);
        reject(new Error('Timeout waiting for value.'));
      }
      count += 1;
    }, ms);
  });
};


assert.deepStrictEqual(node.chain.tip, spvnode.chain.tip);
});

it('should mine a block after a reorg', async () => {
const block = await mineBlock();

await chain.add(block);

// Check SPV & Full nodes are in sync
await event(spvnode, 'block');
assert.deepStrictEqual(node.chain.tip, spvnode.chain.tip);

const entry = await chain.getEntry(block.hash());
assert(entry);
assert.bufferEqual(chain.tip.hash, entry.hash);

const result = await chain.isMainChain(entry);
assert(result);
});

it('should unconfirm tx after reorg', async () => {
const balance = await spvwallet.getBalance();
assert.strictEqual(balance.unconfirmed, 1012345678);
});
});