From 16e8a44c63bb08603902f6da1c68840b9bb6ed13 Mon Sep 17 00:00:00 2001 From: Matt Condon Date: Sun, 12 Aug 2018 12:13:25 -0700 Subject: [PATCH] feat: add hella tests for blockstream --- packages/gnarly-core/src/Blockstream.ts | 1 - packages/gnarly-core/test/Blockstream.spec.ts | 193 +++++++++++++----- 2 files changed, 147 insertions(+), 47 deletions(-) diff --git a/packages/gnarly-core/src/Blockstream.ts b/packages/gnarly-core/src/Blockstream.ts index 0d72fc8..2b10e91 100644 --- a/packages/gnarly-core/src/Blockstream.ts +++ b/packages/gnarly-core/src/Blockstream.ts @@ -44,7 +44,6 @@ class BlockStream { private rollbackTransaction: (blockHash: string) => Promise, private onNewBlock: (block: IJSONBlock, syncing: boolean) => () => Promise < any > , private blockRetention: number = 100, - private interval: number = 5000, ) { this.streamer = new BlockAndLogStreamer(globalState.api.getBlockByHash, globalState.api.getLogs, { blockRetention: this.blockRetention, diff --git a/packages/gnarly-core/test/Blockstream.spec.ts b/packages/gnarly-core/test/Blockstream.spec.ts index 2e60d0a..64c0e87 100644 --- a/packages/gnarly-core/test/Blockstream.spec.ts +++ b/packages/gnarly-core/test/Blockstream.spec.ts @@ -6,7 +6,6 @@ import { globalState } from '../src/globalstate' import { IJSONBlock } from '../src/models/Block' import { forEach, timeout, toBN, toHex } from '../src/utils' -import BlockStream from '../src/Blockstream' import IJSONBlockFactory from './factories/IJSONBlockFactory' import MockIngestApi from './mocks/MockIngestApi' import MockPersistInterface from './mocks/MockPersistInterface' @@ -33,19 +32,21 @@ const buildChain = (from: IJSONBlock[], len: number = 10, fork: number = 1) => { return chain } +const RETENTION = 20 const MOCK_REDUCER_KEY = 'test' +const WAIT_FOR_HANDLERS = 100 // wait 100ms for handlers to be called const should = chai .use(require('chai-spies')) .should() -const bootstrapHistoricalBlocks = async (reducerKey: string, blocks, store: MockPersistInterface, bs: BlockStream) => { +const bootstrapHistoricalBlocks = async (reducerKey: string, blocks, store: MockPersistInterface, bs: Blockstream) => { await forEach(blocks, async (block) => store.saveHistoricalBlock(reducerKey, 100, block), { concurrency: 1 }) await bs.initWithHistoricalBlocks(blocks) } let realChain = [] as IJSONBlock[] -describe.only('Blockstream', function () { +describe('Blockstream', function () { beforeEach(async function () { this.processTransaction = chai.spy(async function (txId: string, fn: () => Promise, extra: object) { @@ -73,6 +74,20 @@ describe.only('Blockstream', function () { chai.spy.on(this.api, 'getBlockByHash', function (hash) { return realChain.find((b) => b.hash === hash) }) + chai.spy.on(this.api, 'getBlockByNumber', function (num) { + return realChain.find((b) => b.number === toHex(num)) + }) + + this.bs = new Blockstream( + MOCK_REDUCER_KEY, + this.processTransaction, + this.rollbackTransaction, + this.onNewBlock, + RETENTION, + ) + + this.onBlockAdd = chai.spy.on(this.bs, 'onBlockAdd') + this.onBlockInvalidated = chai.spy.on(this.bs, 'onBlockInvalidated') }) afterEach(async function () { @@ -87,7 +102,6 @@ describe.only('Blockstream', function () { this.rollbackTransaction, this.onNewBlock, 1, - 1, ) should.exist(bs) }) @@ -103,74 +117,161 @@ describe.only('Blockstream', function () { }) }) - context('with historical blocks', function () { - it('should init with historical blocks and not call any handlers', async function () { - realChain = buildChain(genesis(), 10) - const bs = new Blockstream( - MOCK_REDUCER_KEY, - this.processTransaction, - this.rollbackTransaction, - this.onNewBlock, + context('without historical blocks', function () { + + beforeEach(async function () { + realChain = buildChain(genesis(), 8) + + await this.bs.start() + await this.api.forceSendBlock() + await timeout(WAIT_FOR_HANDLERS) + }) + + it('should start from head and only add the first block', async function () { + await timeout(WAIT_FOR_HANDLERS) + await this.bs.stop() + + this.onBlockAdd.should.have.been.called.exactly(1) + this.onBlockInvalidated.should.not.have.been.called() + this.processTransaction.should.have.been.called.exactly(1) + this.rollbackTransaction.should.not.have.been.called() + this.onNewBlock.should.have.been.called.exactly(1) + }) + + it('should add new blocks when presented', async function () { + realChain = buildChain(realChain, 2) + const newBlocks = realChain.slice(-2) + + await this.api.forceSendBlock() + await timeout(WAIT_FOR_HANDLERS) + await this.bs.stop() + + newBlocks.forEach((block) => + this.onNewBlock.should.have.been.called.with(block), + ) + }) + + it('should invalidate forked blocks', async function () { + const shortFork = buildChain(realChain, 2, 1) + const invalidBlocks = shortFork.slice(-2) + const validChain = buildChain(realChain, 3, 2) + const validBlocks = validChain.slice(-3) + + realChain = shortFork + await this.api.forceSendBlock() + await timeout(WAIT_FOR_HANDLERS) + + invalidBlocks.forEach((block) => + this.onBlockAdd.should.have.been.called.with(block), ) - const onBlockAdd = chai.spy.on(bs, 'onBlockAdd') - const onBlockInvalidated = chai.spy.on(bs, 'onBlockInvalidated') - await bootstrapHistoricalBlocks(MOCK_REDUCER_KEY, realChain, this.store, bs) + realChain = validChain + await this.api.forceSendBlock() + await timeout(WAIT_FOR_HANDLERS) + await this.bs.stop() - onBlockAdd.should.not.have.been.called() - onBlockInvalidated.should.not.have.been.called() + invalidBlocks.forEach((block) => + this.onBlockInvalidated.should.have.been.called.with(block), + ) + validBlocks.forEach((block) => { + this.onBlockAdd.should.have.been.called.with(block) + this.onNewBlock.should.have.been.called.with(block) + }) + }) + }) + + context('with historical blocks', function () { + it('should init with historical blocks and not call any handlers', async function () { + realChain = buildChain(genesis(), 8) + await bootstrapHistoricalBlocks(MOCK_REDUCER_KEY, realChain, this.store, this.bs) + + this.onBlockAdd.should.not.have.been.called() + this.onBlockInvalidated.should.not.have.been.called() this.processTransaction.should.not.have.been.called() this.rollbackTransaction.should.not.have.been.called() this.onNewBlock.should.not.have.been.called() }) context('when presented with a future block within retention', function () { - it('should fast forward and trigger add handlers') + beforeEach(async function () { + this.localChain = buildChain(genesis(), 7) // 8 blocks long + this.remoteChain = buildChain(this.localChain, 12) // 20 blocks long + this.newBlocks = this.remoteChain.slice(-12) + realChain = this.localChain + + await bootstrapHistoricalBlocks(MOCK_REDUCER_KEY, realChain, this.store, this.bs) + // now set the real chain to the remote chain + realChain = this.remoteChain + await this.bs.start(this.localChain[this.localChain.length - 1].hash) // from HEAD + await this.api.forceSendBlock() + await timeout(WAIT_FOR_HANDLERS) + }) + + it('should fast forward and trigger add handlers', async function () { + await this.bs.stop() + this.newBlocks.forEach((block) => { + this.onBlockAdd.should.have.been.called.with(block) + this.onNewBlock.should.have.been.called.with(block) + }) + }) + + it('should not have invalided any blocks', async function () { + await this.bs.stop() + this.onBlockInvalidated.should.not.have.been.called() + }) }) context('when presented with a future block beyond retention', function () { - it('should fast forward manually and then defer to blockstream while triggering add handlers') + beforeEach(async function () { + realChain = buildChain(genesis(), 8) + await bootstrapHistoricalBlocks(MOCK_REDUCER_KEY, realChain, this.store, this.bs) + + const lastLocalBlock = realChain[realChain.length - 1] + const remoteLength = RETENTION + 10 // 10 blocks past retention limit + this.remoteChain = buildChain(realChain, remoteLength) + this.newBlocks = this.remoteChain.slice(-1 * remoteLength) + + realChain = this.remoteChain + await this.bs.start(lastLocalBlock.hash) // start from the local HEAD + + await this.api.forceSendBlock() + await timeout(WAIT_FOR_HANDLERS) + }) + + it('should fast forward manually and then defer to blockstream while triggering add handlers', async function () { + this.newBlocks.forEach((block) => { + this.onBlockAdd.should.have.been.called.with(block) + this.onNewBlock.should.have.been.called.with(block) + }) + }) }) context('when presentend with a short lived fork', function () { - it('calls rollbackTransaction with the offending blocks', async function () { + beforeEach(async function () { const accurateChain = buildChain(genesis(), 7) // 8 blocks long const shortLivedFork = buildChain(accurateChain, 2, 1) // 10 blocks long - const invalidBlocks = [ - shortLivedFork[shortLivedFork.length - 2], - shortLivedFork[shortLivedFork.length - 1], - ] + this.invalidBlocks = shortLivedFork.slice(-2) realChain = shortLivedFork - const bs = new Blockstream( - MOCK_REDUCER_KEY, - this.processTransaction, - this.rollbackTransaction, - this.onNewBlock, - ) - const onBlockAdd = chai.spy.on(bs, 'onBlockAdd') - const onBlockInvalidated = chai.spy.on(bs, 'onBlockInvalidated') - await bootstrapHistoricalBlocks(MOCK_REDUCER_KEY, realChain, this.store, bs) + await bootstrapHistoricalBlocks(MOCK_REDUCER_KEY, realChain, this.store, this.bs) // tell blockstreamer that the last two blocks it saw were actually invalid by giving it a new longer chain - await bs.start() + await this.bs.start() const newChain = buildChain(accurateChain, 3, 2) // 11 blocks long - const validBlocks = [ - newChain[newChain.length - 3], - newChain[newChain.length - 2], - newChain[newChain.length - 1], - ] + this.validBlocks = newChain.slice(-3) realChain = newChain await this.api.forceSendBlock() - await timeout(100) // give it time to call the handlers - await bs.stop() + await timeout(WAIT_FOR_HANDLERS) + }) - onBlockInvalidated.should.have.been.called() + it('calls rollbackTransaction with the offending blocks', async function () { + await this.bs.stop() - invalidBlocks.forEach((block) => - onBlockInvalidated.should.have.been.called.with(block), + this.onBlockInvalidated.should.have.been.called() + this.invalidBlocks.forEach((block) => + this.onBlockInvalidated.should.have.been.called.with(block), ) - validBlocks.forEach((block) => - onBlockAdd.should.have.been.called.with(block), + this.validBlocks.forEach((block) => + this.onBlockAdd.should.have.been.called.with(block), ) }) })