diff --git a/packages/lodestar/src/chain/blocks/process.ts b/packages/lodestar/src/chain/blocks/process.ts index 73218946c8bb..622ba7dce7f4 100644 --- a/packages/lodestar/src/chain/blocks/process.ts +++ b/packages/lodestar/src/chain/blocks/process.ts @@ -1,48 +1,181 @@ +import {computeEpochAtSlot} from "@chainsafe/lodestar-beacon-state-transition"; +import {IBeaconConfig} from "@chainsafe/lodestar-config"; import {IForkChoice} from "@chainsafe/lodestar-fork-choice"; +import { + getAllBlockSignatureSets, + getAllBlockSignatureSetsExceptProposer, + ISignatureSet, +} from "@chainsafe/lodestar-beacon-state-transition/lib/fast/signatureSets"; import {ChainEventEmitter} from "../emitter"; -import {IBlockJob} from "../interface"; +import {IBlockJob, IChainSegmentJob} from "../interface"; import {runStateTransition} from "./stateTransition"; -import {IStateRegenerator} from "../regen"; -import {BlockError, BlockErrorCode} from "../errors"; +import {IStateRegenerator, RegenError} from "../regen"; +import {BlockError, BlockErrorCode, ChainSegmentError} from "../errors"; import {IBeaconDb} from "../../db"; -import {ITreeStateContext} from "../../db/api/beacon/stateContextCache"; +import {verifySignatureSetsBatch} from "../bls"; +import {findLastIndex} from "../../util/array"; -export async function processBlocks({ +export async function processBlock({ forkChoice, regen, emitter, db, - jobs, + job, }: { forkChoice: IForkChoice; regen: IStateRegenerator; emitter: ChainEventEmitter; db: IBeaconDb; - jobs: IBlockJob[]; + job: IBlockJob; }): Promise { - let preStateContext: ITreeStateContext; + if (!forkChoice.hasBlock(job.signedBlock.message.parentRoot)) { + throw new BlockError({ + code: BlockErrorCode.PARENT_UNKNOWN, + parentRoot: job.signedBlock.message.parentRoot.valueOf() as Uint8Array, + job, + }); + } + try { - preStateContext = await regen.getPreState(jobs[0].signedBlock.message); + const preStateContext = await regen.getPreState(job.signedBlock.message); + + if (!job.validSignatures) { + const {epochCtx, state} = preStateContext; + const signatureSets = job.validProposerSignature + ? getAllBlockSignatureSetsExceptProposer(epochCtx, state, job.signedBlock) + : getAllBlockSignatureSets(epochCtx, state, job.signedBlock); + + if (!verifySignatureSetsBatch(signatureSets)) { + throw new BlockError({ + code: BlockErrorCode.INVALID_SIGNATURE, + job, + }); + } + + job.validProposerSignature = true; + job.validSignatures = true; + } + + await runStateTransition(emitter, forkChoice, db, preStateContext, job); } catch (e) { + if (e instanceof RegenError) { + throw new BlockError({ + code: BlockErrorCode.PRESTATE_MISSING, + job, + }); + } + + if (e instanceof BlockError) { + throw e; + } + throw new BlockError({ - code: BlockErrorCode.PRESTATE_MISSING, - job: jobs[0], + code: BlockErrorCode.BEACON_CHAIN_ERROR, + error: e, + job, }); } +} + +export async function processChainSegment({ + config, + forkChoice, + regen, + emitter, + db, + job, +}: { + config: IBeaconConfig; + forkChoice: IForkChoice; + regen: IStateRegenerator; + emitter: ChainEventEmitter; + db: IBeaconDb; + job: IChainSegmentJob; +}): Promise { + let importedBlocks = 0; + let blocks = job.signedBlocks; + + // Process segment epoch by epoch + while (blocks.length) { + const firstBlock = blocks[0]; + // First ensure that the segment's parent has been processed + if (!forkChoice.hasBlock(firstBlock.message.parentRoot)) { + throw new ChainSegmentError({ + code: BlockErrorCode.PARENT_UNKNOWN, + parentRoot: firstBlock.message.parentRoot.valueOf() as Uint8Array, + job, + importedBlocks, + }); + } + const startEpoch = computeEpochAtSlot(config, firstBlock.message.slot); + + // The `lastIndex` indicates the position of the last block that is in the current + // epoch of `startEpoch`. + const lastIndex = findLastIndex(blocks, (block) => computeEpochAtSlot(config, block.message.slot) === startEpoch); + + // Split off the first section blocks that are all either within the current epoch of + // the first block. These blocks can all be signature-verified with the same + // `BeaconState`. + const blocksInEpoch = blocks.slice(0, lastIndex); + blocks = blocks.slice(lastIndex); - for (const job of jobs) { try { - preStateContext = await runStateTransition(emitter, forkChoice, db, preStateContext, job); + let preStateContext = await regen.getPreState(firstBlock.message); + + // Verify the signature of the blocks, returning early if the signature is invalid. + if (!job.validSignatures) { + const signatureSets: ISignatureSet[] = []; + for (const block of blocksInEpoch) { + const {epochCtx, state} = preStateContext; + signatureSets.push( + ...(job.validProposerSignature + ? getAllBlockSignatureSetsExceptProposer(epochCtx, state, block) + : getAllBlockSignatureSets(epochCtx, state, block)) + ); + } + + if (!verifySignatureSetsBatch(signatureSets)) { + throw new ChainSegmentError({ + code: BlockErrorCode.INVALID_SIGNATURE, + job, + importedBlocks, + }); + } + } + + for (const block of blocksInEpoch) { + preStateContext = await runStateTransition(emitter, forkChoice, db, preStateContext, { + reprocess: job.reprocess, + prefinalized: job.prefinalized, + signedBlock: block, + validProposerSignature: true, + validSignatures: true, + }); + importedBlocks++; + } } catch (e) { + if (e instanceof RegenError) { + throw new ChainSegmentError({ + code: BlockErrorCode.PRESTATE_MISSING, + job, + importedBlocks, + }); + } + if (e instanceof BlockError) { - throw e; + throw new ChainSegmentError({ + ...e.type, + job, + importedBlocks, + }); } - throw new BlockError({ + throw new ChainSegmentError({ code: BlockErrorCode.BEACON_CHAIN_ERROR, error: e, job, + importedBlocks, }); } } diff --git a/packages/lodestar/src/chain/blocks/processor.ts b/packages/lodestar/src/chain/blocks/processor.ts index 21a84da9c883..2901238fe762 100644 --- a/packages/lodestar/src/chain/blocks/processor.ts +++ b/packages/lodestar/src/chain/blocks/processor.ts @@ -1,16 +1,18 @@ import {AbortSignal} from "abort-controller"; import {IBeaconConfig} from "@chainsafe/lodestar-config"; import {IForkChoice} from "@chainsafe/lodestar-fork-choice"; +import {SignedBeaconBlock} from "@chainsafe/lodestar-types"; import {IBlockJob, IChainSegmentJob} from "../interface"; import {ChainEvent, ChainEventEmitter} from "../emitter"; import {IBeaconClock} from "../clock"; import {IStateRegenerator} from "../regen"; import {JobQueue} from "../../util/queue"; - -import {processBlocks} from "./process"; -import {validateBlocks} from "./validate"; import {IBeaconDb} from "../../db"; +import {BlockError, BlockErrorCode, ChainSegmentError} from "../errors"; + +import {processBlock, processChainSegment} from "./process"; +import {validateBlock} from "./validate"; type BlockProcessorModules = { config: IBeaconConfig; @@ -61,8 +63,8 @@ export class BlockProcessor { */ export async function processBlockJob(modules: BlockProcessorModules, job: IBlockJob): Promise { try { - await validateBlocks({...modules, jobs: [job]}); - await processBlocks({...modules, jobs: [job]}); + validateBlock({...modules, job}); + await processBlock({...modules, job}); } catch (e) { // above functions only throw BlockError modules.emitter.emit(ChainEvent.errorBlock, e); @@ -72,20 +74,85 @@ export async function processBlockJob(modules: BlockProcessorModules, job: IBloc /** * Similar to processBlockJob but this process a chain segment */ -export async function processChainSegmentJob( - modules: BlockProcessorModules, - chainSegmentJob: IChainSegmentJob -): Promise { - try { - const blockJobs: IBlockJob[] = chainSegmentJob.signedBlocks.map((signedBlock) => ({ - signedBlock, - ...chainSegmentJob, - })); - await validateBlocks({...modules, jobs: blockJobs}); - await processBlocks({...modules, jobs: blockJobs}); - } catch (e) { - // above functions only throw BlockError - modules.emitter.emit(ChainEvent.errorBlock, e); - throw e; +export async function processChainSegmentJob(modules: BlockProcessorModules, job: IChainSegmentJob): Promise { + const blocks = job.signedBlocks; + + // Validate and filter out irrelevant blocks + const filteredChainSegment: SignedBeaconBlock[] = []; + for (const [i, block] of blocks.entries()) { + const child = blocks[i + 1]; + if (child) { + // If this block has a child in this chain segment, ensure that its parent root matches + // the root of this block. + // + // Without this check it would be possible to have a block verified using the + // incorrect shuffling. That would be bad, mmkay. + if ( + !modules.config.types.Root.equals( + modules.config.types.BeaconBlock.hashTreeRoot(block.message), + child.message.parentRoot + ) + ) { + throw new ChainSegmentError({ + code: BlockErrorCode.NON_LINEAR_PARENT_ROOTS, + job, + importedBlocks: 0, + }); + } + // Ensure that the slots are strictly increasing throughout the chain segment. + if (child.message.slot <= block.message.slot) { + throw new ChainSegmentError({ + code: BlockErrorCode.NON_LINEAR_SLOTS, + job, + importedBlocks: 0, + }); + } + } + + try { + validateBlock({...modules, job: {...job, signedBlock: block}}); + // If the block is relevant, add it to the filtered chain segment. + filteredChainSegment.push(block); + } catch (e) { + switch ((e as BlockError).type.code) { + // If the block is already known, simply ignore this block. + case BlockErrorCode.BLOCK_IS_ALREADY_KNOWN: + continue; + // If the block is the genesis block, simply ignore this block. + case BlockErrorCode.GENESIS_BLOCK: + continue; + // If the block is is for a finalized slot, simply ignore this block. + // + // The block is either: + // + // 1. In the canonical finalized chain. + // 2. In some non-canonical chain at a slot that has been finalized already. + // + // In the case of (1), there's no need to re-import and later blocks in this + // segement might be useful. + // + // In the case of (2), skipping the block is valid since we should never import it. + // However, we will potentially get a `ParentUnknown` on a later block. The sync + // protocol will need to ensure this is handled gracefully. + case BlockErrorCode.WOULD_REVERT_FINALIZED_SLOT: + continue; + // Any other error whilst determining if the block was invalid, return that + // error. + default: + throw new ChainSegmentError({ + job: e.job, + ...e.type, + importedBlocks: 0, + }); + } + } } + + await processChainSegment({ + ...modules, + job: { + ...job, + signedBlocks: filteredChainSegment, + }, + }); } diff --git a/packages/lodestar/src/chain/blocks/stateTransition.ts b/packages/lodestar/src/chain/blocks/stateTransition.ts index 477a65dd1c66..0ebbf998000e 100644 --- a/packages/lodestar/src/chain/blocks/stateTransition.ts +++ b/packages/lodestar/src/chain/blocks/stateTransition.ts @@ -10,10 +10,6 @@ import { toIStateContext, } from "@chainsafe/lodestar-beacon-state-transition"; import {processSlots} from "@chainsafe/lodestar-beacon-state-transition/lib/fast/slot"; -import { - getAllBlockSignatureSets, - getAllBlockSignatureSetsExceptProposer, -} from "@chainsafe/lodestar-beacon-state-transition/lib/fast/signatureSets"; import {IBlockSummary, IForkChoice} from "@chainsafe/lodestar-fork-choice"; import {LodestarEpochContext, ITreeStateContext} from "../../db/api/beacon/stateContextCache"; @@ -21,8 +17,6 @@ import {ChainEvent, ChainEventEmitter} from "../emitter"; import {IBlockJob} from "../interface"; import {sleep} from "@chainsafe/lodestar-utils"; import {IBeaconDb} from "../../db"; -import {BlockError, BlockErrorCode} from "../errors"; -import {verifySignatureSetsBatch} from "../bls"; import {StateTransitionEpochContext} from "@chainsafe/lodestar-beacon-state-transition/lib/fast/util/epochContext"; /** @@ -145,28 +139,10 @@ export async function runStateTransition( const config = stateContext.epochCtx.config; const {SLOTS_PER_EPOCH} = config.params; const postSlot = job.signedBlock.message.slot; - const checkpointStateContext = await processSlotsToNearestCheckpoint(emitter, stateContext, postSlot - 1); - - if (!job.validSignatures) { - const {epochCtx, state} = checkpointStateContext; - const signatureSets = job.validProposerSignature - ? getAllBlockSignatureSetsExceptProposer(epochCtx, state, job.signedBlock) - : getAllBlockSignatureSets(epochCtx, state, job.signedBlock); - - if (!verifySignatureSetsBatch(signatureSets)) { - throw new BlockError({ - code: BlockErrorCode.INVALID_SIGNATURE, - job, - }); - } - - job.validProposerSignature = true; - job.validSignatures = true; - } // if block is trusted don't verify proposer or op signature const postStateContext = toTreeStateContext( - fastStateTransition(checkpointStateContext, job.signedBlock, { + fastStateTransition(stateContext, job.signedBlock, { verifyStateRoot: true, verifyProposer: !job.validSignatures && !job.validProposerSignature, verifySignatures: !job.validSignatures, diff --git a/packages/lodestar/src/chain/blocks/validate.ts b/packages/lodestar/src/chain/blocks/validate.ts index fb9052af27e5..ff6ac9b4431c 100644 --- a/packages/lodestar/src/chain/blocks/validate.ts +++ b/packages/lodestar/src/chain/blocks/validate.ts @@ -6,86 +6,63 @@ import {IBlockJob} from "../interface"; import {IBeaconClock} from "../clock"; import {BlockError, BlockErrorCode} from "../errors"; -export async function validateBlocks({ +export function validateBlock({ config, forkChoice, clock, - jobs, + job, }: { config: IBeaconConfig; forkChoice: IForkChoice; clock: IBeaconClock; - jobs: IBlockJob[]; -}): Promise { - if (!jobs || !jobs.length) throw new Error("No block job to validate"); - const ancestorRoot = jobs[0].signedBlock.message.parentRoot; - let parentRoot = ancestorRoot; - for (const job of jobs) { - try { - const blockHash = config.types.BeaconBlock.hashTreeRoot(job.signedBlock.message); - const blockSlot = job.signedBlock.message.slot; - if (blockSlot === 0) { - throw new BlockError({ - code: BlockErrorCode.GENESIS_BLOCK, - job, - }); - } - - if (!job.reprocess && forkChoice.hasBlock(blockHash)) { - throw new BlockError({ - code: BlockErrorCode.BLOCK_IS_ALREADY_KNOWN, - job, - }); - } - - const finalizedCheckpoint = forkChoice.getFinalizedCheckpoint(); - const finalizedSlot = computeStartSlotAtEpoch(config, finalizedCheckpoint.epoch); - if (blockSlot <= finalizedSlot) { - throw new BlockError({ - code: BlockErrorCode.WOULD_REVERT_FINALIZED_SLOT, - blockSlot, - finalizedSlot, - job, - }); - } + job: IBlockJob; +}): void { + try { + const blockHash = config.types.BeaconBlock.hashTreeRoot(job.signedBlock.message); + const blockSlot = job.signedBlock.message.slot; + if (blockSlot === 0) { + throw new BlockError({ + code: BlockErrorCode.GENESIS_BLOCK, + job, + }); + } - const currentSlotWithGossipDisparity = clock.currentSlotWithGossipDisparity; - if (blockSlot > currentSlotWithGossipDisparity) { - throw new BlockError({ - code: BlockErrorCode.FUTURE_SLOT, - blockSlot, - currentSlot: currentSlotWithGossipDisparity, - job, - }); - } + if (!job.reprocess && forkChoice.hasBlock(blockHash)) { + throw new BlockError({ + code: BlockErrorCode.BLOCK_IS_ALREADY_KNOWN, + job, + }); + } - if (!config.types.Root.equals(parentRoot, job.signedBlock.message.parentRoot)) { - throw new BlockError({ - code: BlockErrorCode.NON_LINEAR_PARENT_ROOTS, - blockSlot, - job, - }); - } - parentRoot = blockHash; - } catch (e) { - if (e instanceof BlockError) { - throw e; - } + const finalizedCheckpoint = forkChoice.getFinalizedCheckpoint(); + const finalizedSlot = computeStartSlotAtEpoch(config, finalizedCheckpoint.epoch); + if (blockSlot <= finalizedSlot) { + throw new BlockError({ + code: BlockErrorCode.WOULD_REVERT_FINALIZED_SLOT, + blockSlot, + finalizedSlot, + job, + }); + } + const currentSlot = clock.currentSlot; + if (blockSlot > currentSlot) { throw new BlockError({ - code: BlockErrorCode.BEACON_CHAIN_ERROR, - error: e, + code: BlockErrorCode.FUTURE_SLOT, + blockSlot, + currentSlot, job, }); } - } + } catch (e) { + if (e instanceof BlockError) { + throw e; + } - // only validate PARENT_UNKNOWN condition for 1st block - if (!forkChoice.hasBlock(ancestorRoot)) { throw new BlockError({ - code: BlockErrorCode.PARENT_UNKNOWN, - parentRoot: jobs[0].signedBlock.message.parentRoot.valueOf() as Uint8Array, - job: jobs[0], + code: BlockErrorCode.BEACON_CHAIN_ERROR, + error: e, + job, }); } } diff --git a/packages/lodestar/src/chain/errors/blockError.ts b/packages/lodestar/src/chain/errors/blockError.ts index efe2e3c8eea3..f79e1ad88409 100644 --- a/packages/lodestar/src/chain/errors/blockError.ts +++ b/packages/lodestar/src/chain/errors/blockError.ts @@ -1,7 +1,7 @@ import {Root, Slot, ValidatorIndex} from "@chainsafe/lodestar-types"; import {LodestarError} from "@chainsafe/lodestar-utils"; -import {IBlockJob} from "../interface"; +import {IBlockJob, IChainSegmentJob} from "../interface"; export enum BlockErrorCode { /** @@ -98,21 +98,40 @@ export type BlockErrorType = | {code: BlockErrorCode.UNKNOWN_PROPOSER; proposer: ValidatorIndex} | {code: BlockErrorCode.INVALID_SIGNATURE} | {code: BlockErrorCode.BLOCK_IS_NOT_LATER_THAN_PARENT; blockSlot: Slot; stateSlot: Slot} - | {code: BlockErrorCode.NON_LINEAR_PARENT_ROOTS; blockSlot: Slot} + | {code: BlockErrorCode.NON_LINEAR_PARENT_ROOTS} | {code: BlockErrorCode.NON_LINEAR_SLOTS} | {code: BlockErrorCode.PER_BLOCK_PROCESSING_ERROR; error: Error} | {code: BlockErrorCode.BEACON_CHAIN_ERROR; error: Error} | {code: BlockErrorCode.KNOWN_BAD_BLOCK}; -type JobObject = { +type BlockJobObject = { job: IBlockJob; }; export class BlockError extends LodestarError { public job: IBlockJob; - constructor({job, ...type}: BlockErrorType & JobObject) { + constructor({job, ...type}: BlockErrorType & BlockJobObject) { super(type); this.job = job; } } + +type ChainSegmentJobObject = { + job: IChainSegmentJob; + importedBlocks: number; +}; + +export class ChainSegmentError extends LodestarError { + public job: IChainSegmentJob; + /** + * Number of blocks successfully imported before the error + */ + public importedBlocks: number; + + constructor({job, importedBlocks, ...type}: BlockErrorType & ChainSegmentJobObject) { + super(type); + this.job = job; + this.importedBlocks = importedBlocks; + } +} diff --git a/packages/lodestar/src/util/array.ts b/packages/lodestar/src/util/array.ts new file mode 100644 index 000000000000..a79dd5bf713b --- /dev/null +++ b/packages/lodestar/src/util/array.ts @@ -0,0 +1,12 @@ +/** + * Return the last index in the array that matches the predicate + */ +export function findLastIndex(array: T[], predicate: (value: T) => boolean): number { + let i = array.length; + while (i--) { + if (predicate(array[i])) { + return i; + } + } + return -1; +} diff --git a/packages/lodestar/test/unit/chain/blocks/process.test.ts b/packages/lodestar/test/unit/chain/blocks/process.test.ts index 331682f7862a..76650fd49d93 100644 --- a/packages/lodestar/test/unit/chain/blocks/process.test.ts +++ b/packages/lodestar/test/unit/chain/blocks/process.test.ts @@ -6,7 +6,7 @@ import {ForkChoice} from "@chainsafe/lodestar-fork-choice"; import {ChainEventEmitter} from "../../../../src/chain"; import {BlockErrorCode} from "../../../../src/chain/errors"; -import {processBlocks} from "../../../../src/chain/blocks/process"; +import {processBlock} from "../../../../src/chain/blocks/process"; import {RegenError, RegenErrorCode, StateRegenerator} from "../../../../src/chain/regen"; import {StubbedBeaconDb} from "../../../utils/stub"; import {getNewBlockJob} from "../../../utils/block"; @@ -27,18 +27,32 @@ describe("processBlock", function () { sinon.restore(); }); + it("should throw on unknown parent", async function () { + const signedBlock = config.types.SignedBeaconBlock.defaultValue(); + signedBlock.message.slot = 1; + const job = getNewBlockJob(signedBlock); + forkChoice.hasBlock.returns(false); + try { + await processBlock({forkChoice, db: dbStub, regen, emitter, job}); + expect.fail("block should throw"); + } catch (e) { + expect(e.type.code).to.equal(BlockErrorCode.PARENT_UNKNOWN); + } + }); + it("should throw on missing prestate", async function () { const signedBlock = config.types.SignedBeaconBlock.defaultValue(); signedBlock.message.slot = 1; const job = getNewBlockJob(signedBlock); + forkChoice.hasBlock.returns(true); regen.getPreState.rejects(new RegenError({code: RegenErrorCode.STATE_TRANSITION_ERROR, error: new Error()})); try { - await processBlocks({ + await processBlock({ forkChoice, db: dbStub, regen, emitter, - jobs: [job], + job, }); expect.fail("block should throw"); } catch (e) { diff --git a/packages/lodestar/test/unit/chain/blocks/validate.test.ts b/packages/lodestar/test/unit/chain/blocks/validate.test.ts index 893ba5c028a2..b23eabbf963d 100644 --- a/packages/lodestar/test/unit/chain/blocks/validate.test.ts +++ b/packages/lodestar/test/unit/chain/blocks/validate.test.ts @@ -4,7 +4,7 @@ import sinon, {SinonStubbedInstance} from "sinon"; import {config} from "@chainsafe/lodestar-config/minimal"; import {ForkChoice} from "@chainsafe/lodestar-fork-choice"; -import {validateBlocks} from "../../../../src/chain/blocks/validate"; +import {validateBlock} from "../../../../src/chain/blocks/validate"; import {LocalClock} from "../../../../src/chain/clock"; import {BlockErrorCode} from "../../../../src/chain/errors"; import {getNewBlockJob} from "../../../utils/block"; @@ -26,7 +26,7 @@ describe("validateBlock", function () { const signedBlock = config.types.SignedBeaconBlock.defaultValue(); const job = getNewBlockJob(signedBlock); try { - await validateBlocks({config, forkChoice, clock, jobs: [job]}); + await validateBlock({config, forkChoice, clock, job}); expect.fail("block should throw"); } catch (e) { expect(e.type.code).to.equal(BlockErrorCode.GENESIS_BLOCK); @@ -39,7 +39,7 @@ describe("validateBlock", function () { const job = getNewBlockJob(signedBlock); forkChoice.hasBlock.returns(true); try { - await validateBlocks({config, forkChoice, clock, jobs: [job]}); + await validateBlock({config, forkChoice, clock, job}); expect.fail("block should throw"); } catch (e) { expect(e.type.code).to.equal(BlockErrorCode.BLOCK_IS_ALREADY_KNOWN); @@ -53,7 +53,7 @@ describe("validateBlock", function () { forkChoice.hasBlock.returns(false); forkChoice.getFinalizedCheckpoint.returns({epoch: 5, root: Buffer.alloc(32)}); try { - await validateBlocks({config, forkChoice, clock, jobs: [job]}); + await validateBlock({config, forkChoice, clock, job}); expect.fail("block should throw"); } catch (e) { expect(e.type.code).to.equal(BlockErrorCode.WOULD_REVERT_FINALIZED_SLOT); @@ -66,28 +66,12 @@ describe("validateBlock", function () { const job = getNewBlockJob(signedBlock); forkChoice.hasBlock.returns(false); forkChoice.getFinalizedCheckpoint.returns({epoch: 0, root: Buffer.alloc(32)}); - sinon.stub(clock, "currentSlotWithGossipDisparity").get(() => 0); + sinon.stub(clock, "currentSlot").get(() => 0); try { - await validateBlocks({config, forkChoice, clock, jobs: [job]}); + await validateBlock({config, forkChoice, clock, job}); expect.fail("block should throw"); } catch (e) { expect(e.type.code).to.equal(BlockErrorCode.FUTURE_SLOT); } }); - - it("should throw on unknown parent", async function () { - const signedBlock = config.types.SignedBeaconBlock.defaultValue(); - signedBlock.message.slot = 1; - const job = getNewBlockJob(signedBlock); - forkChoice.hasBlock.returns(false); - forkChoice.getFinalizedCheckpoint.returns({epoch: 0, root: Buffer.alloc(32)}); - sinon.stub(clock, "currentSlotWithGossipDisparity").get(() => 1); - forkChoice.hasBlock.returns(false); - try { - await validateBlocks({config, forkChoice, clock, jobs: [job]}); - expect.fail("block should throw"); - } catch (e) { - expect(e.type.code).to.equal(BlockErrorCode.PARENT_UNKNOWN); - } - }); }); diff --git a/packages/lodestar/test/unit/util/array.test.ts b/packages/lodestar/test/unit/util/array.test.ts new file mode 100644 index 000000000000..d27460b956e2 --- /dev/null +++ b/packages/lodestar/test/unit/util/array.test.ts @@ -0,0 +1,16 @@ +import {expect} from "chai"; + +import {findLastIndex} from "../../../src/util/array"; + +describe("findLastIndex", () => { + it("should return the last index that matches a predicate", () => { + expect(findLastIndex([1, 2, 3, 4], (n) => n % 2 == 0)).to.eql(3); + expect(findLastIndex([1, 2, 3, 4, 5], (n) => n % 2 == 0)).to.eql(3); + expect(findLastIndex([1, 2, 3, 4, 5], () => true)).to.eql(4); + }); + + it("should return -1 if there are no matches", () => { + expect(findLastIndex([1, 3, 5], (n) => n % 2 == 0)).to.eql(-1); + expect(findLastIndex([1, 2, 3, 4, 5], () => false)).to.eql(-1); + }); +});