Skip to content

Commit

Permalink
Add verifyBlocksSanityChecks
Browse files Browse the repository at this point in the history
  • Loading branch information
dapplion committed Jul 17, 2022
1 parent d6ce1a5 commit 63596b8
Show file tree
Hide file tree
Showing 5 changed files with 308 additions and 140 deletions.
28 changes: 24 additions & 4 deletions packages/beacon-node/src/chain/blocks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import {JobItemQueue} from "../../util/queue/index.js";
import {BlockError, BlockErrorCode} from "../errors/index.js";
import {BlockProcessOpts} from "../options.js";
import {IBeaconChain} from "../interface.js";
import {verifyBlock, VerifyBlockModules} from "./verifyBlock.js";
import {VerifyBlockModules, verifyBlockStateTransition} from "./verifyBlock.js";
import {importBlock, ImportBlockModules} from "./importBlock.js";
import {assertLinearChainSegment} from "./utils/chainSegment.js";
import {ImportBlockOpts} from "./types.js";
import {FullyVerifiedBlock, ImportBlockOpts} from "./types.js";
import {verifyBlocksSanityChecks} from "./verifyBlocksSanityChecks.js";
export {ImportBlockOpts} from "./types.js";

const QUEUE_MAX_LENGHT = 256;
Expand Down Expand Up @@ -62,8 +63,27 @@ export async function processBlocks(
}

try {
for (const block of blocks) {
const fullyVerifiedBlock = await verifyBlock(chain, block, opts);
const {relevantBlocks, parentSlots} = verifyBlocksSanityChecks(chain, blocks, opts);

// No relevant blocks, skip verifyBlocksInEpoch()
if (relevantBlocks.length === 0) {
return;
}

for (const [i, block] of relevantBlocks.entries()) {
// Fully verify a block to be imported immediately after. Does not produce any side-effects besides adding intermediate
// states in the state cache through regen.
const {postState, executionStatus, proposerBalanceDiff} = await verifyBlockStateTransition(chain, block, opts);

const fullyVerifiedBlock: FullyVerifiedBlock = {
block,
postState,
parentBlockSlot: parentSlots[i],
executionStatus,
proposerBalanceDiff,
// TODO: Make this param mandatory and capture in gossip
seenTimestampSec: opts.seenTimestampSec ?? Math.floor(Date.now() / 1000),
};

// No need to sleep(0) here since `importBlock` includes a disk write
// TODO: Consider batching importBlock too if it takes significant time
Expand Down
80 changes: 2 additions & 78 deletions packages/beacon-node/src/chain/blocks/verifyBlock.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {
CachedBeaconStateAllForks,
computeStartSlotAtEpoch,
isBellatrixStateType,
isBellatrixBlockBodyType,
isMergeTransitionBlock as isMergeTransitionBlockFn,
Expand All @@ -10,7 +9,7 @@ import {
} from "@lodestar/state-transition";
import {allForks, bellatrix} from "@lodestar/types";
import {toHexString} from "@chainsafe/ssz";
import {IForkChoice, ProtoBlock, ExecutionStatus, assertValidTerminalPowBlock} from "@lodestar/fork-choice";
import {IForkChoice, ExecutionStatus, assertValidTerminalPowBlock} from "@lodestar/fork-choice";
import {IChainForkConfig} from "@lodestar/config";
import {ILogger} from "@lodestar/utils";
import {IMetrics} from "../../metrics/index.js";
Expand All @@ -23,7 +22,7 @@ import {IBlsVerifier} from "../bls/index.js";
import {ExecutePayloadStatus} from "../../execution/engine/interface.js";
import {byteArrayEquals} from "../../util/bytes.js";
import {IEth1ForBlockProduction} from "../../eth1/index.js";
import {FullyVerifiedBlock, ImportBlockOpts} from "./types.js";
import {ImportBlockOpts} from "./types.js";
import {POS_PANDA_MERGE_TRANSITION_BANNER} from "./utils/pandaMergeTransitionBanner.js";

export type VerifyBlockModules = {
Expand All @@ -38,81 +37,6 @@ export type VerifyBlockModules = {
metrics: IMetrics | null;
};

/**
* Fully verify a block to be imported immediately after. Does not produce any side-effects besides adding intermediate
* states in the state cache through regen.
*/
export async function verifyBlock(
chain: VerifyBlockModules,
block: allForks.SignedBeaconBlock,
opts: ImportBlockOpts & BlockProcessOpts
): Promise<FullyVerifiedBlock> {
const parentBlock = verifyBlockSanityChecks(chain, block);

const {postState, executionStatus, proposerBalanceDiff} = await verifyBlockStateTransition(chain, block, opts);

return {
block,
postState,
parentBlockSlot: parentBlock.slot,
executionStatus,
proposerBalanceDiff,
// TODO: Make this param mandatory and capture in gossip
seenTimestampSec: opts.seenTimestampSec ?? Math.floor(Date.now() / 1000),
};
}

/**
* Verifies som early cheap sanity checks on the block before running the full state transition.
*
* - Parent is known to the fork-choice
* - Check skipped slots limit
* - check_block_relevancy()
* - Block not in the future
* - Not genesis block
* - Block's slot is < Infinity
* - Not finalized slot
* - Not already known
*/
export function verifyBlockSanityChecks(chain: VerifyBlockModules, block: allForks.SignedBeaconBlock): ProtoBlock {
const blockSlot = block.message.slot;

// Not genesis block
if (blockSlot === 0) {
throw new BlockError(block, {code: BlockErrorCode.GENESIS_BLOCK});
}

// Not finalized slot
const finalizedSlot = computeStartSlotAtEpoch(chain.forkChoice.getFinalizedCheckpoint().epoch);
if (blockSlot <= finalizedSlot) {
throw new BlockError(block, {code: BlockErrorCode.WOULD_REVERT_FINALIZED_SLOT, blockSlot, finalizedSlot});
}

// Parent is known to the fork-choice
const parentRoot = toHexString(block.message.parentRoot);
const parentBlock = chain.forkChoice.getBlockHex(parentRoot);
if (!parentBlock) {
throw new BlockError(block, {code: BlockErrorCode.PARENT_UNKNOWN, parentRoot});
}

// Check skipped slots limit
// TODO

// Block not in the future, also checks for infinity
const currentSlot = chain.clock.currentSlot;
if (blockSlot > currentSlot) {
throw new BlockError(block, {code: BlockErrorCode.FUTURE_SLOT, blockSlot, currentSlot});
}

// Not already known
const blockHash = toHexString(chain.config.getForkTypes(block.message.slot).BeaconBlock.hashTreeRoot(block.message));
if (chain.forkChoice.hasBlockHex(blockHash)) {
throw new BlockError(block, {code: BlockErrorCode.ALREADY_KNOWN, root: blockHash});
}

return parentBlock;
}

/**
* Verifies a block is fully valid running the full state transition. To relieve the main thread signatures are
* verified separately in workers with chain.bls worker pool.
Expand Down
100 changes: 100 additions & 0 deletions packages/beacon-node/src/chain/blocks/verifyBlocksSanityChecks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import {computeStartSlotAtEpoch} from "@lodestar/state-transition";
import {IChainForkConfig} from "@lodestar/config";
import {IForkChoice} from "@lodestar/fork-choice";
import {allForks, Slot} from "@lodestar/types";
import {toHexString} from "@lodestar/utils";
import {IBeaconClock} from "../clock/interface.js";
import {BlockError, BlockErrorCode} from "../errors/index.js";
import {ImportBlockOpts} from "./types.js";

/**
* Verifies some early cheap sanity checks on the block before running the full state transition.
*
* - Parent is known to the fork-choice
* - Check skipped slots limit
* - check_block_relevancy()
* - Block not in the future
* - Not genesis block
* - Block's slot is < Infinity
* - Not finalized slot
* - Not already known
*/
export function verifyBlocksSanityChecks(
chain: {forkChoice: IForkChoice; clock: IBeaconClock; config: IChainForkConfig},
blocks: allForks.SignedBeaconBlock[],
opts: ImportBlockOpts
): {relevantBlocks: allForks.SignedBeaconBlock[]; parentSlots: Slot[]} {
if (blocks.length === 0) {
throw Error("Empty partiallyVerifiedBlocks");
}

const relevantBlocks: allForks.SignedBeaconBlock[] = [];
const parentSlots: Slot[] = [];

for (let i = 0; i < blocks.length; i++) {
const block = blocks[i];
const blockSlot = block.message.slot;

// Not genesis block
// IGNORE if `partiallyVerifiedBlock.ignoreIfKnown`
if (blockSlot === 0) {
if (opts.ignoreIfKnown) {
continue;
} else {
throw new BlockError(block, {code: BlockErrorCode.GENESIS_BLOCK});
}
}

// Not finalized slot
// IGNORE if `partiallyVerifiedBlock.ignoreIfFinalized`
const finalizedSlot = computeStartSlotAtEpoch(chain.forkChoice.getFinalizedCheckpoint().epoch);
if (blockSlot <= finalizedSlot) {
if (opts.ignoreIfFinalized) {
continue;
} else {
throw new BlockError(block, {code: BlockErrorCode.WOULD_REVERT_FINALIZED_SLOT, blockSlot, finalizedSlot});
}
}

let parentBlockSlot: Slot;

// When importing a block segment, only the first NON-IGNORED block must be known to the fork-choice.
if (relevantBlocks.length > 0) {
parentBlockSlot = relevantBlocks[relevantBlocks.length - 1].message.slot;
} else {
// Parent is known to the fork-choice
const parentRoot = toHexString(block.message.parentRoot);
const parentBlock = chain.forkChoice.getBlockHex(parentRoot);
if (!parentBlock) {
throw new BlockError(block, {code: BlockErrorCode.PARENT_UNKNOWN, parentRoot});
} else {
parentBlockSlot = parentBlock.slot;
}
}

// Block not in the future, also checks for infinity
const currentSlot = chain.clock.currentSlot;
if (blockSlot > currentSlot) {
throw new BlockError(block, {code: BlockErrorCode.FUTURE_SLOT, blockSlot, currentSlot});
}

// Not already known
// IGNORE if `partiallyVerifiedBlock.ignoreIfKnown`
const blockHash = toHexString(
chain.config.getForkTypes(block.message.slot).BeaconBlock.hashTreeRoot(block.message)
);
if (chain.forkChoice.hasBlockHex(blockHash)) {
if (opts.ignoreIfKnown) {
continue;
} else {
throw new BlockError(block, {code: BlockErrorCode.ALREADY_KNOWN, root: blockHash});
}
}

// Block is relevant
relevantBlocks.push(block);
parentSlots.push(parentBlockSlot);
}

return {relevantBlocks, parentSlots};
}
58 changes: 0 additions & 58 deletions packages/beacon-node/test/unit/chain/blocks/verifyBlock.test.ts

This file was deleted.

Loading

0 comments on commit 63596b8

Please sign in to comment.