Skip to content
This repository has been archived by the owner on Jan 22, 2019. It is now read-only.

Commit

Permalink
feat: add hella tests for blockstream
Browse files Browse the repository at this point in the history
  • Loading branch information
shrugs committed Aug 12, 2018
1 parent 6741c07 commit 16e8a44
Show file tree
Hide file tree
Showing 2 changed files with 147 additions and 47 deletions.
1 change: 0 additions & 1 deletion packages/gnarly-core/src/Blockstream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ class BlockStream {
private rollbackTransaction: (blockHash: string) => Promise<void>,
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,
Expand Down
193 changes: 147 additions & 46 deletions packages/gnarly-core/test/Blockstream.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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<void>, extra: object) {
Expand Down Expand Up @@ -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 () {
Expand All @@ -87,7 +102,6 @@ describe.only('Blockstream', function () {
this.rollbackTransaction,
this.onNewBlock,
1,
1,
)
should.exist(bs)
})
Expand All @@ -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),
)
})
})
Expand Down

0 comments on commit 16e8a44

Please sign in to comment.