Skip to content

Commit

Permalink
refactor: move data availability validation out of state transition t…
Browse files Browse the repository at this point in the history
…o allow optimistic sync in future (#5178)

Move blob data availability and validation context out from state transition

  rebase fixes

  retain dataAvailabilityStatus flag in state transition

  reduce diff

  correctly propogate availability status while block processing
  • Loading branch information
g11tech committed May 8, 2023
1 parent a14bf0d commit 849f08b
Show file tree
Hide file tree
Showing 6 changed files with 73 additions and 58 deletions.
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
49 changes: 44 additions & 5 deletions packages/beacon-node/src/chain/blocks/verifyBlocksSanityChecks.ts
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");
}
}
}

0 comments on commit 849f08b

Please sign in to comment.