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

refactor: move data availability validation out of state transition to allow optimistic sync #5178

Merged
merged 1 commit into from
May 8, 2023
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
10 changes: 9 additions & 1 deletion packages/beacon-node/src/chain/blocks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,11 @@ export async function processBlocks(
}

try {
const {relevantBlocks, parentSlots, parentBlock} = verifyBlocksSanityChecks(this, blocks, opts);
const {relevantBlocks, dataAvailabilityStatuses, parentSlots, parentBlock} = verifyBlocksSanityChecks(
this,
blocks,
opts
);

// No relevant blocks, skip verifyBlocksInEpoch()
if (relevantBlocks.length === 0 || parentBlock === null) {
Expand All @@ -71,6 +75,7 @@ export async function processBlocks(
this,
parentBlock,
relevantBlocks,
dataAvailabilityStatuses,
opts
);

Expand All @@ -90,6 +95,9 @@ export async function processBlocks(
postState: postStates[i],
parentBlockSlot: parentSlots[i],
executionStatus: executionStatuses[i],
// Currently dataAvailableStatus is not used upstream but that can change if we
// start supporting optimistic syncing/processing
dataAvailableStatus: dataAvailabilityStatuses[i],
proposerBalanceDelta: proposerBalanceDeltas[i],
// TODO: Make this param mandatory and capture in gossip
seenTimestampSec: opts.seenTimestampSec ?? Math.floor(Date.now() / 1000),
Expand Down
3 changes: 2 additions & 1 deletion packages/beacon-node/src/chain/blocks/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {CachedBeaconStateAllForks, computeEpochAtSlot} from "@lodestar/state-transition";
import {CachedBeaconStateAllForks, computeEpochAtSlot, DataAvailableStatus} from "@lodestar/state-transition";
import {MaybeValidExecutionStatus} from "@lodestar/fork-choice";
import {allForks, deneb, Slot, WithOptionalBytes} from "@lodestar/types";
import {ForkSeq, MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS} from "@lodestar/params";
Expand Down Expand Up @@ -101,6 +101,7 @@ export type FullyVerifiedBlock = {
* used in optimistic sync or for merge block
*/
executionStatus: MaybeValidExecutionStatus;
dataAvailableStatus: DataAvailableStatus;
/** Seen timestamp seconds */
seenTimestampSec: number;
};
12 changes: 11 additions & 1 deletion packages/beacon-node/src/chain/blocks/verifyBlock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
CachedBeaconStateAllForks,
computeEpochAtSlot,
isStateValidatorsNodesPopulated,
DataAvailableStatus,
} from "@lodestar/state-transition";
import {bellatrix} from "@lodestar/types";
import {ForkName} from "@lodestar/params";
Expand Down Expand Up @@ -35,6 +36,7 @@ export async function verifyBlocksInEpoch(
this: BeaconChain,
parentBlock: ProtoBlock,
blocksInput: BlockInput[],
dataAvailabilityStatuses: DataAvailableStatus[],
opts: BlockProcessOpts & ImportBlockOpts
): Promise<{
postStates: CachedBeaconStateAllForks[];
Expand Down Expand Up @@ -87,7 +89,15 @@ export async function verifyBlocksInEpoch(
verifyBlocksExecutionPayload(this, parentBlock, blocks, preState0, abortController.signal, opts),
// Run state transition only
// TODO: Ensure it yields to allow flushing to workers and engine API
verifyBlocksStateTransitionOnly(preState0, blocksInput, this.logger, this.metrics, abortController.signal, opts),
verifyBlocksStateTransitionOnly(
preState0,
blocksInput,
dataAvailabilityStatuses,
this.logger,
this.metrics,
abortController.signal,
opts
),

// All signatures at once
verifyBlocksSignatures(this.bls, this.logger, this.metrics, preState0, blocks, opts),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import {computeStartSlotAtEpoch} from "@lodestar/state-transition";
import {computeStartSlotAtEpoch, DataAvailableStatus} from "@lodestar/state-transition";
import {ChainForkConfig} from "@lodestar/config";
import {IForkChoice, ProtoBlock} from "@lodestar/fork-choice";
import {Slot, WithOptionalBytes} from "@lodestar/types";
import {Slot, deneb, WithOptionalBytes} from "@lodestar/types";
import {toHexString} from "@lodestar/utils";
import {IClock} from "../../util/clock.js";
import {BlockError, BlockErrorCode} from "../errors/index.js";
import {BlockInput, ImportBlockOpts} from "./types.js";
import {validateBlobsSidecar} from "../validation/blobsSidecar.js";
import {BlockInput, BlockInputType, ImportBlockOpts} from "./types.js";

/**
* Verifies some early cheap sanity checks on the block before running the full state transition.
Expand All @@ -23,12 +24,18 @@ export function verifyBlocksSanityChecks(
chain: {forkChoice: IForkChoice; clock: IClock; config: ChainForkConfig},
blocks: WithOptionalBytes<BlockInput>[],
opts: ImportBlockOpts
): {relevantBlocks: WithOptionalBytes<BlockInput>[]; parentSlots: Slot[]; parentBlock: ProtoBlock | null} {
): {
relevantBlocks: WithOptionalBytes<BlockInput>[];
dataAvailabilityStatuses: DataAvailableStatus[];
parentSlots: Slot[];
parentBlock: ProtoBlock | null;
} {
if (blocks.length === 0) {
throw Error("Empty partiallyVerifiedBlocks");
}

const relevantBlocks: WithOptionalBytes<BlockInput>[] = [];
const dataAvailabilityStatuses: DataAvailableStatus[] = [];
const parentSlots: Slot[] = [];
let parentBlock: ProtoBlock | null = null;

Expand Down Expand Up @@ -57,6 +64,10 @@ export function verifyBlocksSanityChecks(
}
}

// Validate status of only not yet finalized blocks, we don't need yet to propogate the status
// as it is not used upstream anywhere
const dataAvailabilityStatus = maybeValidateBlobs(chain.config, blockInput, opts);

let parentBlockSlot: Slot;

if (relevantBlocks.length > 0) {
Expand Down Expand Up @@ -94,6 +105,7 @@ export function verifyBlocksSanityChecks(

// Block is relevant
relevantBlocks.push(blockInput);
dataAvailabilityStatuses.push(dataAvailabilityStatus);
parentSlots.push(parentBlockSlot);
}

Expand All @@ -103,5 +115,32 @@ export function verifyBlocksSanityChecks(
throw Error(`Internal error, parentBlock should not be null for relevantBlocks=${relevantBlocks.length}`);
}

return {relevantBlocks, parentSlots, parentBlock};
return {relevantBlocks, dataAvailabilityStatuses, parentSlots, parentBlock};
}

function maybeValidateBlobs(
config: ChainForkConfig,
blockInput: BlockInput,
opts: ImportBlockOpts
): DataAvailableStatus {
// TODO Deneb: Make switch verify it's exhaustive
switch (blockInput.type) {
case BlockInputType.postDeneb: {
if (opts.validBlobsSidecar) {
return DataAvailableStatus.available;
}

const {block, blobs} = blockInput;
const blockSlot = block.message.slot;
const {blobKzgCommitments} = (block as deneb.SignedBeaconBlock).message.body;
const beaconBlockRoot = config.getForkTypes(blockSlot).BeaconBlock.hashTreeRoot(block.message);
// TODO Deneb: This function throws un-typed errors
validateBlobsSidecar(blockSlot, beaconBlockRoot, blobKzgCommitments, blobs);

return DataAvailableStatus.available;
}

case BlockInputType.preDeneb:
return DataAvailableStatus.preDeneb;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,12 @@ import {
ExecutionPayloadStatus,
DataAvailableStatus,
} from "@lodestar/state-transition";
import {deneb} from "@lodestar/types";
import {ErrorAborted, Logger, sleep} from "@lodestar/utils";
import {ChainForkConfig} from "@lodestar/config";
import {Metrics} from "../../metrics/index.js";
import {BlockError, BlockErrorCode} from "../errors/index.js";
import {BlockProcessOpts} from "../options.js";
import {byteArrayEquals} from "../../util/bytes.js";
import {validateBlobsSidecar} from "../validation/blobsSidecar.js";
import {BlockInput, BlockInputType, ImportBlockOpts} from "./types.js";
import {BlockInput, ImportBlockOpts} from "./types.js";

/**
* Verifies 1 or more blocks are fully valid running the full state transition; from a linear sequence of blocks.
Expand All @@ -25,24 +22,20 @@ import {BlockInput, BlockInputType, ImportBlockOpts} from "./types.js";
export async function verifyBlocksStateTransitionOnly(
preState0: CachedBeaconStateAllForks,
blocks: BlockInput[],
dataAvailabilityStatuses: DataAvailableStatus[],
logger: Logger,
metrics: Metrics | null,
signal: AbortSignal,
opts: BlockProcessOpts & ImportBlockOpts
): Promise<{postStates: CachedBeaconStateAllForks[]; proposerBalanceDeltas: number[]}> {
const {config} = preState0;
const postStates: CachedBeaconStateAllForks[] = [];
const proposerBalanceDeltas: number[] = [];

for (let i = 0; i < blocks.length; i++) {
const {validProposerSignature, validSignatures} = opts;
const {block} = blocks[i];
const preState = i === 0 ? preState0 : postStates[i - 1];

// TODO Deneb: Is the best place here to call validateBlobsSidecar()?
// TODO Deneb: Gossip may already call validateBlobsSidecar, add some flag to de-dup from here
// TODO Deneb: For sync if this function is expensive, consider adding sleep(0) if metrics show it
const dataAvailableStatus = maybeValidateBlobs(config, blocks[i], opts);
const dataAvailableStatus = dataAvailabilityStatuses[i];

// STFN - per_slot_processing() + per_block_processing()
// NOTE: `regen.getPreState()` should have dialed forward the state already caching checkpoint states
Expand All @@ -54,7 +47,6 @@ export async function verifyBlocksStateTransitionOnly(
// NOTE: Assume valid for now while sending payload to execution engine in parallel
// Latter verifyBlocksInEpoch() will make sure that payload is indeed valid
executionPayloadStatus: ExecutionPayloadStatus.valid,
// TODO Deneb: Data is validated above for
dataAvailableStatus,
// false because it's verified below with better error typing
verifyStateRoot: false,
Expand Down Expand Up @@ -107,30 +99,3 @@ export async function verifyBlocksStateTransitionOnly(

return {postStates, proposerBalanceDeltas};
}

function maybeValidateBlobs(
config: ChainForkConfig,
blockInput: BlockInput,
opts: ImportBlockOpts
): DataAvailableStatus {
// TODO Deneb: Make switch verify it's exhaustive
switch (blockInput.type) {
case BlockInputType.postDeneb: {
if (opts.validBlobsSidecar) {
return DataAvailableStatus.available;
}

const {block, blobs} = blockInput;
const blockSlot = block.message.slot;
const {blobKzgCommitments} = (block as deneb.SignedBeaconBlock).message.body;
const beaconBlockRoot = config.getForkTypes(blockSlot).BeaconBlock.hashTreeRoot(block.message);
// TODO Deneb: This function throws un-typed errors
validateBlobsSidecar(blockSlot, beaconBlockRoot, blobKzgCommitments, blobs);

return DataAvailableStatus.available;
}

case BlockInputType.preDeneb:
return DataAvailableStatus.preDeneb;
}
}
16 changes: 4 additions & 12 deletions packages/state-transition/src/block/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,18 +63,10 @@ export function processBlock(

if (fork >= ForkSeq.deneb) {
processBlobKzgCommitments(block.body as deneb.BeaconBlockBody);

// New in Deneb, note: Can sync optimistically without this condition, see note on `is_data_available`
// NOTE: Ommitted and should be verified beforehand

// assert is_data_available(block.slot, hash_tree_root(block), block.body.blob_kzg_commitments)
switch (externalData.dataAvailableStatus) {
case DataAvailableStatus.preDeneb:
throw Error("dataAvailableStatus preDeneb");
case DataAvailableStatus.notAvailable:
throw Error("dataAvailableStatus notAvailable");
case DataAvailableStatus.available:
break; // ok
// Only throw preDeneb so beacon can also sync/process blocks optimistically
// and let forkChoice handle it
if (externalData.dataAvailableStatus === DataAvailableStatus.preDeneb) {
throw Error("dataAvailableStatus preDeneb");
}
}
}