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

Add verifyBlocksSanityChecks #4318

Merged
merged 1 commit into from Jul 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
28 changes: 24 additions & 4 deletions packages/beacon-node/src/chain/blocks/index.ts
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
@@ -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
@@ -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.