From 360078b24f3cf3a47b3c19788df80e39ea27622d Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Fri, 20 Feb 2026 18:38:02 +0000 Subject: [PATCH 1/4] initial codex draft --- .../src/api/impl/beacon/blocks/index.ts | 9 +- .../src/chain/blocks/blockInput/blockInput.ts | 100 +++--- .../src/chain/blocks/blockInput/types.ts | 6 +- .../beacon-node/src/chain/blocks/types.ts | 4 +- packages/beacon-node/src/chain/chain.ts | 2 +- packages/beacon-node/src/chain/emitter.ts | 4 +- .../chain/seenCache/seenGossipBlockInput.ts | 24 +- .../src/chain/validation/dataColumnSidecar.ts | 297 ++++++++++++++++-- packages/beacon-node/src/network/interface.ts | 4 +- packages/beacon-node/src/network/network.ts | 7 +- .../network/processor/extractSlotRootFns.ts | 4 +- .../src/network/processor/gossipHandlers.ts | 24 +- .../beacon-node/src/network/reqresp/types.ts | 13 +- .../src/sync/utils/downloadByRange.ts | 67 ++-- .../src/sync/utils/downloadByRoot.ts | 34 +- packages/beacon-node/src/util/blobs.ts | 50 ++- packages/beacon-node/src/util/dataColumns.ts | 37 ++- packages/beacon-node/src/util/execution.ts | 8 +- packages/beacon-node/src/util/sszBytes.ts | 57 +++- .../db/api/repositories/dataColumn.test.ts | 12 +- .../beacon-node/test/unit/util/kzg.test.ts | 8 +- 21 files changed, 573 insertions(+), 198 deletions(-) diff --git a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts index 37d34f6c3345..e79db8cfa71d 100644 --- a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts @@ -21,15 +21,16 @@ import { signedBlockToSignedHeader, } from "@lodestar/state-transition"; import { + DataColumnSidecars, ProducedBlockSource, SignedBeaconBlock, SignedBlindedBeaconBlock, SignedBlockContents, WithOptionalBytes, deneb, - fulu, gloas, isDenebBlockContents, + isGloasDataColumnSidecar, sszTypesFor, } from "@lodestar/types"; import {fromHex, sleep, toHex, toRootHex} from "@lodestar/utils"; @@ -103,7 +104,7 @@ export function getBeaconBlockApi({ seenTimestampSec, blockRootHex: blockRoot, }); - let blobSidecars: deneb.BlobSidecars, dataColumnSidecars: fulu.DataColumnSidecars; + let blobSidecars: deneb.BlobSidecars, dataColumnSidecars: DataColumnSidecars; if (isDenebBlockContents(signedBlockContents)) { if (isForkPostFulu(fork)) { @@ -350,7 +351,9 @@ export function getBeaconBlockApi({ blockRoot, slot, index: dataColumnSidecar.index, - kzgCommitments: dataColumnSidecar.kzgCommitments.map(toHex), + kzgCommitments: isGloasDataColumnSidecar(dataColumnSidecar) + ? [] + : dataColumnSidecar.kzgCommitments.map(toHex), }); } } diff --git a/packages/beacon-node/src/chain/blocks/blockInput/blockInput.ts b/packages/beacon-node/src/chain/blocks/blockInput/blockInput.ts index be8a4609b12d..5a6b326b02d5 100644 --- a/packages/beacon-node/src/chain/blocks/blockInput/blockInput.ts +++ b/packages/beacon-node/src/chain/blocks/blockInput/blockInput.ts @@ -1,5 +1,5 @@ -import {ForkName, ForkPostFulu, ForkPreDeneb, ForkPreGloas, NUMBER_OF_COLUMNS} from "@lodestar/params"; -import {BeaconBlockBody, BlobIndex, ColumnIndex, SignedBeaconBlock, Slot, deneb, fulu} from "@lodestar/types"; +import {ForkName, ForkPostFulu, ForkPreDeneb, NUMBER_OF_COLUMNS, isForkPostGloas} from "@lodestar/params"; +import {BlobIndex, ColumnIndex, DataColumnSidecar, SignedBeaconBlock, Slot, deneb, gloas} from "@lodestar/types"; import {byteArrayEquals, fromHex, prettyBytes, toRootHex, withTimeout} from "@lodestar/utils"; import {VersionedHashes} from "../../../execution/index.js"; import {kzgCommitmentToVersionedHash} from "../../../util/blobs.js"; @@ -553,9 +553,24 @@ function assertBlockAndBlobArePaired( } } +function isGloasDataColumnSidecar(sidecar: DataColumnSidecar): sidecar is gloas.DataColumnSidecar { + return (sidecar as gloas.DataColumnSidecar).beaconBlockRoot !== undefined; +} + +function getBlobKzgCommitmentsFromColumnsBlock( + block: SignedBeaconBlock, + forkName: ForkColumnsDA +): Uint8Array[] { + if (isForkPostGloas(forkName)) { + return (block as gloas.SignedBeaconBlock).message.body.signedExecutionPayloadBid.message.blobKzgCommitments; + } + + return (block.message.body as {blobKzgCommitments: Uint8Array[]}).blobKzgCommitments; +} + // Columns DA -export type ForkColumnsDA = ForkName.fulu; +export type ForkColumnsDA = ForkPostFulu; type BlockInputColumnsState = | { @@ -594,7 +609,7 @@ type BlockInputColumnsState = * - The block is not yet seen and all required sampled columns are seen * - The block is not yet seen and all required sampled columns are not yet seen */ -export class BlockInputColumns extends AbstractBlockInput { +export class BlockInputColumns extends AbstractBlockInput { type = DAType.Columns as const; state: BlockInputColumnsState; @@ -607,7 +622,7 @@ export class BlockInputColumns extends AbstractBlockInput(); + protected computedDataPromise = createPromise(); private constructor( init: BlockInputInit, @@ -629,15 +644,13 @@ export class BlockInputColumns extends AbstractBlockInput & CreateBlockInputMeta & {sampledColumns: ColumnIndex[]; custodyColumns: ColumnIndex[]} ): BlockInputColumns { - const hasAllData = - props.daOutOfRange || - props.block.message.body.blobKzgCommitments.length === 0 || - props.sampledColumns.length === 0; + const blobKzgCommitments = getBlobKzgCommitmentsFromColumnsBlock(props.block, props.forkName as ForkColumnsDA); + const hasAllData = props.daOutOfRange || blobKzgCommitments.length === 0 || props.sampledColumns.length === 0; const state = { hasBlock: true, hasAllData, hasComputedAllData: hasAllData, - versionedHashes: props.block.message.body.blobKzgCommitments.map(kzgCommitmentToVersionedHash), + versionedHashes: blobKzgCommitments.map(kzgCommitmentToVersionedHash), block: props.block, source: { source: props.source, @@ -668,21 +681,35 @@ export class BlockInputColumns extends AbstractBlockInput).blobKzgCommitments.length === 0 || - this.state.hasAllData; - const hasComputedAllData = - props.block.message.body.blobKzgCommitments.length === 0 || this.state.hasComputedAllData; + const blobKzgCommitments = getBlobKzgCommitmentsFromColumnsBlock(props.block, this.forkName as ForkColumnsDA); + const hasAllData = blobKzgCommitments.length === 0 || this.state.hasAllData; + const hasComputedAllData = blobKzgCommitments.length === 0 || this.state.hasComputedAllData; this.state = { ...this.state, hasBlock: true, hasAllData, hasComputedAllData, + versionedHashes: blobKzgCommitments.map(kzgCommitmentToVersionedHash), block: props.block, source: { source: props.source, @@ -753,6 +779,8 @@ export class BlockInputColumns extends AbstractBlockInput columnSidecar); } @@ -896,7 +924,7 @@ export class BlockInputColumns extends AbstractBlockInput { + waitForComputedAllData(timeout: number, signal?: AbortSignal): Promise { if (!this.state.hasComputedAllData) { return withTimeout(() => this.computedDataPromise.promise, timeout, signal); } diff --git a/packages/beacon-node/src/chain/blocks/blockInput/types.ts b/packages/beacon-node/src/chain/blocks/blockInput/types.ts index 252457113321..b454c2a697d6 100644 --- a/packages/beacon-node/src/chain/blocks/blockInput/types.ts +++ b/packages/beacon-node/src/chain/blocks/blockInput/types.ts @@ -1,5 +1,5 @@ import {ForkName} from "@lodestar/params"; -import {ColumnIndex, RootHex, SignedBeaconBlock, Slot, deneb, fulu} from "@lodestar/types"; +import {ColumnIndex, DataColumnSidecar, RootHex, SignedBeaconBlock, Slot, deneb} from "@lodestar/types"; import {VersionedHashes} from "../../../execution/index.js"; export enum DAType { @@ -8,7 +8,7 @@ export enum DAType { Columns = "columns", } -export type DAData = null | deneb.BlobSidecars | fulu.DataColumnSidecars; +export type DAData = null | deneb.BlobSidecars | DataColumnSidecar[]; /** * Represents were input originated. Blocks and Data can come from different @@ -55,7 +55,7 @@ export type BlockWithSource = SourceMeta & {block: SignedBeaconBlock; blockRootH export type BlobWithSource = SourceMeta & {blobSidecar: deneb.BlobSidecar}; -export type ColumnWithSource = SourceMeta & {columnSidecar: fulu.DataColumnSidecar}; +export type ColumnWithSource = SourceMeta & {columnSidecar: DataColumnSidecar}; export type BlockHeaderMeta = { forkName: ForkName; diff --git a/packages/beacon-node/src/chain/blocks/types.ts b/packages/beacon-node/src/chain/blocks/types.ts index 50ed0076515c..4e60db0c313a 100644 --- a/packages/beacon-node/src/chain/blocks/types.ts +++ b/packages/beacon-node/src/chain/blocks/types.ts @@ -2,7 +2,7 @@ import type {ChainForkConfig} from "@lodestar/config"; import {MaybeValidExecutionStatus} from "@lodestar/fork-choice"; import {ForkSeq} from "@lodestar/params"; import {CachedBeaconStateAllForks, DataAvailabilityStatus, computeEpochAtSlot} from "@lodestar/state-transition"; -import type {IndexedAttestation, Slot, fulu} from "@lodestar/types"; +import type {DataColumnSidecar, IndexedAttestation, Slot} from "@lodestar/types"; import {IBlockInput} from "./blockInput/types.js"; export enum GossipedInputType { @@ -12,7 +12,7 @@ export enum GossipedInputType { } type DataColumnData = { - dataColumn: fulu.DataColumnSidecar; + dataColumn: DataColumnSidecar; dataColumnBytes: Uint8Array | null; }; export type DataColumnsCacheMap = Map; diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index d579bd34c58e..9b73d5d929d4 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -824,7 +824,7 @@ export class BeaconChain implements IBeaconChain { if (!isBlockInputColumns(blockInput)) { throw new Error(`Expected block input to have columns: slot=${blockSlot} root=${blockRootHex}`); } - return blockInput.getAllColumns(); + return blockInput.getAllColumns() as DataColumnSidecars; } const sidecarsUnfinalized = await this.db.dataColumnSidecar.values(fromHex(blockRootHex)); if (sidecarsUnfinalized.length > 0) { diff --git a/packages/beacon-node/src/chain/emitter.ts b/packages/beacon-node/src/chain/emitter.ts index e8be09536048..dec6ea198a96 100644 --- a/packages/beacon-node/src/chain/emitter.ts +++ b/packages/beacon-node/src/chain/emitter.ts @@ -3,7 +3,7 @@ import {StrictEventEmitter} from "strict-event-emitter-types"; import {routes} from "@lodestar/api"; import {CheckpointWithHex} from "@lodestar/fork-choice"; import {CachedBeaconStateAllForks} from "@lodestar/state-transition"; -import {DataColumnSidecars, RootHex, deneb, phase0} from "@lodestar/types"; +import {DataColumnSidecar, RootHex, deneb, phase0} from "@lodestar/types"; import {PeerIdStr} from "../util/peerId.js"; import {BlockInputSource, IBlockInput} from "./blocks/blockInput/types.js"; @@ -88,7 +88,7 @@ export type IChainEvents = ApiEvents & { [ChainEvent.updateTargetCustodyGroupCount]: (targetGroupCount: number) => void; - [ChainEvent.publishDataColumns]: (sidecars: DataColumnSidecars) => void; + [ChainEvent.publishDataColumns]: (sidecars: DataColumnSidecar[]) => void; [ChainEvent.publishBlobSidecars]: (sidecars: deneb.BlobSidecar[]) => void; diff --git a/packages/beacon-node/src/chain/seenCache/seenGossipBlockInput.ts b/packages/beacon-node/src/chain/seenCache/seenGossipBlockInput.ts index 6b7a38a93e97..a6ae1e96ff30 100644 --- a/packages/beacon-node/src/chain/seenCache/seenGossipBlockInput.ts +++ b/packages/beacon-node/src/chain/seenCache/seenGossipBlockInput.ts @@ -1,21 +1,13 @@ import {ChainForkConfig} from "@lodestar/config"; import {CheckpointWithHex} from "@lodestar/fork-choice"; -import { - ForkName, - ForkPostFulu, - ForkPreGloas, - SLOTS_PER_EPOCH, - isForkPostDeneb, - isForkPostFulu, - isForkPostGloas, -} from "@lodestar/params"; +import {ForkName, ForkPostFulu, SLOTS_PER_EPOCH, isForkPostDeneb, isForkPostFulu} from "@lodestar/params"; import {computeStartSlotAtEpoch} from "@lodestar/state-transition"; -import {BLSSignature, RootHex, SignedBeaconBlock, Slot, deneb, fulu} from "@lodestar/types"; +import {BLSSignature, DataColumnSidecar, RootHex, SignedBeaconBlock, Slot, deneb} from "@lodestar/types"; import {LodestarError, Logger, byteArrayEquals, pruneSetToMax} from "@lodestar/utils"; import {Metrics} from "../../metrics/metrics.js"; import {MAX_LOOK_AHEAD_EPOCHS} from "../../sync/constants.js"; import {IClock} from "../../util/clock.js"; -import {CustodyConfig} from "../../util/dataColumns.js"; +import {CustodyConfig, getDataColumnSidecarSlot} from "../../util/dataColumns.js"; import { BlockInput, BlockInputBlobs, @@ -179,10 +171,6 @@ export class SeenBlockInput { if (!blockInput) { const {forkName, daOutOfRange} = this.buildCommonProps(block.message.slot); - // TODO GLOAS: Implement - if (isForkPostGloas(forkName)) { - throw Error("Not implemented"); - } // Pre-deneb if (!isForkPostDeneb(forkName)) { blockInput = BlockInputPreData.createFromBlock({ @@ -197,7 +185,7 @@ export class SeenBlockInput { // Fulu Only } else if (isForkPostFulu(forkName)) { blockInput = BlockInputColumns.createFromBlock({ - block: block as SignedBeaconBlock, + block: block as SignedBeaconBlock, blockRootHex, daOutOfRange, forkName, @@ -299,14 +287,14 @@ export class SeenBlockInput { seenTimestampSec, source, peerIdStr, - }: SourceMeta & {blockRootHex: RootHex; columnSidecar: fulu.DataColumnSidecar}, + }: SourceMeta & {blockRootHex: RootHex; columnSidecar: DataColumnSidecar}, opts: GetByBlobOptions = {} ): BlockInputColumns { let blockInput = this.blockInputs.get(blockRootHex); let created = false; if (!blockInput) { created = true; - const {forkName, daOutOfRange} = this.buildCommonProps(columnSidecar.signedBlockHeader.message.slot); + const {forkName, daOutOfRange} = this.buildCommonProps(getDataColumnSidecarSlot(columnSidecar)); blockInput = BlockInputColumns.createFromColumn({ columnSidecar, blockRootHex, diff --git a/packages/beacon-node/src/chain/validation/dataColumnSidecar.ts b/packages/beacon-node/src/chain/validation/dataColumnSidecar.ts index e10ce40f7a9d..ff573ce91cd7 100644 --- a/packages/beacon-node/src/chain/validation/dataColumnSidecar.ts +++ b/packages/beacon-node/src/chain/validation/dataColumnSidecar.ts @@ -3,6 +3,7 @@ import { KZG_COMMITMENTS_INCLUSION_PROOF_DEPTH, KZG_COMMITMENTS_SUBTREE_INDEX, NUMBER_OF_COLUMNS, + isForkPostGloas, } from "@lodestar/params"; import { computeEpochAtSlot, @@ -10,9 +11,10 @@ import { getBlockHeaderProposerSignatureSetByHeaderSlot, getBlockHeaderProposerSignatureSetByParentStateSlot, } from "@lodestar/state-transition"; -import {DataColumnSidecar, Root, Slot, SubnetID, fulu, ssz} from "@lodestar/types"; +import {DataColumnSidecar, Root, Slot, SubnetID, fulu, gloas, isGloasDataColumnSidecar, ssz} from "@lodestar/types"; import {byteArrayEquals, toRootHex, verifyMerkleBranch} from "@lodestar/utils"; import {Metrics} from "../../metrics/metrics.js"; +import {getDataColumnSidecarBlockRoot, getDataColumnSidecarSlot} from "../../util/dataColumns.js"; import {kzg} from "../../util/kzg.js"; import { DataColumnSidecarErrorCode, @@ -26,6 +28,40 @@ import {RegenCaller} from "../regen/interface.js"; // SPEC FUNCTION // https://github.com/ethereum/consensus-specs/blob/v1.6.0-alpha.4/specs/fulu/p2p-interface.md#data_column_sidecar_subnet_id export async function validateGossipDataColumnSidecar( + chain: IBeaconChain, + dataColumnSidecar: DataColumnSidecar, + gossipSubnet: SubnetID, + metrics: Metrics | null +): Promise { + const slot = getDataColumnSidecarSlot(dataColumnSidecar); + const fork = chain.config.getForkName(slot); + + if (isForkPostGloas(fork)) { + if (!isGloasDataColumnSidecar(dataColumnSidecar)) { + throw new DataColumnSidecarGossipError(GossipAction.REJECT, { + code: DataColumnSidecarErrorCode.MISMATCHED_LENGTHS, + columnLength: dataColumnSidecar.column.length, + commitmentsLength: 0, + proofsLength: dataColumnSidecar.kzgProofs.length, + }); + } + await validateGossipDataColumnSidecarGloas(chain, dataColumnSidecar, gossipSubnet, metrics); + return; + } + + if (isGloasDataColumnSidecar(dataColumnSidecar)) { + throw new DataColumnSidecarGossipError(GossipAction.REJECT, { + code: DataColumnSidecarErrorCode.MISMATCHED_LENGTHS, + columnLength: dataColumnSidecar.column.length, + commitmentsLength: 0, + proofsLength: dataColumnSidecar.kzgProofs.length, + }); + } + + await validateGossipDataColumnSidecarFulu(chain, dataColumnSidecar, gossipSubnet, metrics); +} + +async function validateGossipDataColumnSidecarFulu( chain: IBeaconChain, dataColumnSidecar: fulu.DataColumnSidecar, gossipSubnet: SubnetID, @@ -35,7 +71,7 @@ export async function validateGossipDataColumnSidecar( const blockRootHex = toRootHex(ssz.phase0.BeaconBlockHeader.hashTreeRoot(blockHeader)); // 1) [REJECT] The sidecar is valid as verified by verify_data_column_sidecar - verifyDataColumnSidecar(chain.config, dataColumnSidecar); + verifyDataColumnSidecarFulu(chain.config, dataColumnSidecar); // 2) [REJECT] The sidecar is for the correct subnet -- i.e. compute_subnet_for_data_column_sidecar(sidecar.index) == subnet_id if (computeSubnetForDataColumnSidecar(chain.config, dataColumnSidecar) !== gossipSubnet) { @@ -201,11 +237,75 @@ export async function validateGossipDataColumnSidecar( // -- Handled in seenGossipBlockInput } +async function validateGossipDataColumnSidecarGloas( + chain: IBeaconChain, + dataColumnSidecar: gloas.DataColumnSidecar, + gossipSubnet: SubnetID, + metrics: Metrics | null +): Promise { + const blockRoot = getDataColumnSidecarBlockRoot(dataColumnSidecar); + const blockRootHex = toRootHex(blockRoot); + const slot = getDataColumnSidecarSlot(dataColumnSidecar); + + const cachedBlockInput = chain.seenBlockInputCache.get(blockRootHex); + const blockData = cachedBlockInput?.hasBlock() + ? cachedBlockInput.getBlock() + : (await chain.getBlockByRoot(blockRootHex))?.block; + + if (!blockData) { + throw new DataColumnSidecarGossipError(GossipAction.IGNORE, { + code: DataColumnSidecarErrorCode.PARENT_UNKNOWN, + parentRoot: blockRootHex, + slot, + }); + } + + if (blockData.message.slot !== slot) { + throw new DataColumnSidecarGossipError(GossipAction.REJECT, { + code: DataColumnSidecarErrorCode.INCORRECT_BLOCK, + slot, + columnIndex: dataColumnSidecar.index, + expected: `${blockData.message.slot}`, + actual: `${slot}`, + }); + } + + const kzgCommitments = (blockData as gloas.SignedBeaconBlock).message.body.signedExecutionPayloadBid.message + .blobKzgCommitments; + verifyDataColumnSidecarGloas(dataColumnSidecar, kzgCommitments); + + if (computeSubnetForDataColumnSidecar(chain.config, dataColumnSidecar) !== gossipSubnet) { + throw new DataColumnSidecarGossipError(GossipAction.REJECT, { + code: DataColumnSidecarErrorCode.INVALID_SUBNET, + columnIndex: dataColumnSidecar.index, + gossipSubnet, + }); + } + + const kzgProofTimer = metrics?.peerDas.dataColumnSidecarKzgProofsVerificationTime.startTimer(); + try { + await verifyDataColumnSidecarKzgProofs( + kzgCommitments, + Array.from({length: dataColumnSidecar.column.length}, () => dataColumnSidecar.index), + dataColumnSidecar.column, + dataColumnSidecar.kzgProofs + ); + } catch { + throw new DataColumnSidecarGossipError(GossipAction.REJECT, { + code: DataColumnSidecarErrorCode.INVALID_KZG_PROOF, + slot, + columnIndex: dataColumnSidecar.index, + }); + } finally { + kzgProofTimer?.(); + } +} + /** * SPEC FUNCTION * https://github.com/ethereum/consensus-specs/blob/v1.6.0-alpha.4/specs/fulu/p2p-interface.md#verify_data_column_sidecar */ -function verifyDataColumnSidecar(config: ChainForkConfig, dataColumnSidecar: fulu.DataColumnSidecar): void { +function verifyDataColumnSidecarFulu(config: ChainForkConfig, dataColumnSidecar: fulu.DataColumnSidecar): void { if (dataColumnSidecar.index >= NUMBER_OF_COLUMNS) { throw new DataColumnSidecarGossipError(GossipAction.REJECT, { code: DataColumnSidecarErrorCode.INVALID_INDEX, @@ -248,6 +348,37 @@ function verifyDataColumnSidecar(config: ChainForkConfig, dataColumnSidecar: ful } } +function verifyDataColumnSidecarGloas(dataColumnSidecar: gloas.DataColumnSidecar, kzgCommitments: Uint8Array[]): void { + const slot = getDataColumnSidecarSlot(dataColumnSidecar); + if (dataColumnSidecar.index >= NUMBER_OF_COLUMNS) { + throw new DataColumnSidecarGossipError(GossipAction.REJECT, { + code: DataColumnSidecarErrorCode.INVALID_INDEX, + slot, + columnIndex: dataColumnSidecar.index, + }); + } + + if (dataColumnSidecar.column.length === 0) { + throw new DataColumnSidecarGossipError(GossipAction.REJECT, { + code: DataColumnSidecarErrorCode.NO_COMMITMENTS, + slot, + columnIndex: dataColumnSidecar.index, + }); + } + + if ( + dataColumnSidecar.column.length !== kzgCommitments.length || + dataColumnSidecar.column.length !== dataColumnSidecar.kzgProofs.length + ) { + throw new DataColumnSidecarGossipError(GossipAction.REJECT, { + code: DataColumnSidecarErrorCode.MISMATCHED_LENGTHS, + columnLength: dataColumnSidecar.column.length, + commitmentsLength: kzgCommitments.length, + proofsLength: dataColumnSidecar.kzgProofs.length, + }); + } +} + /** * SPEC FUNCTION * https://github.com/ethereum/consensus-specs/blob/v1.6.0-alpha.4/specs/fulu/p2p-interface.md#verify_data_column_sidecar_kzg_proofs @@ -297,7 +428,8 @@ export async function validateBlockDataColumnSidecars( blockSlot: Slot, blockRoot: Root, blockBlobCount: number, - dataColumnSidecars: fulu.DataColumnSidecars + dataColumnSidecars: DataColumnSidecar[], + blockKzgCommitments?: Uint8Array[] ): Promise { if (dataColumnSidecars.length === 0) { return; @@ -314,10 +446,9 @@ export async function validateBlockDataColumnSidecars( "Block has no blob commitments but data column sidecars were provided" ); } - // Hash the first sidecar block header and compare the rest via (cheaper) equality - const firstSidecarSignedBlockHeader = dataColumnSidecars[0].signedBlockHeader; - const firstSidecarBlockHeader = firstSidecarSignedBlockHeader.message; - const firstBlockRoot = ssz.phase0.BeaconBlockHeader.hashTreeRoot(firstSidecarBlockHeader); + + const firstSidecar = dataColumnSidecars[0]; + const firstBlockRoot = getDataColumnSidecarBlockRoot(firstSidecar); if (!byteArrayEquals(blockRoot, firstBlockRoot)) { throw new DataColumnSidecarValidationError( { @@ -331,12 +462,31 @@ export async function validateBlockDataColumnSidecars( ); } - if (chain !== null) { + const firstSidecarSlot = getDataColumnSidecarSlot(firstSidecar); + if (firstSidecarSlot !== blockSlot) { + throw new DataColumnSidecarValidationError( + { + code: DataColumnSidecarErrorCode.INCORRECT_BLOCK, + slot: blockSlot, + columnIndex: firstSidecar.index, + expected: `${blockSlot}`, + actual: `${firstSidecarSlot}`, + }, + "DataColumnSidecar slot doesn't match corresponding block" + ); + } + + const isGloasSidecar = isGloasDataColumnSidecar(firstSidecar); + const firstFuluSidecar = isGloasSidecar ? undefined : (firstSidecar as fulu.DataColumnSidecar); + if (chain !== null && firstFuluSidecar !== undefined) { const rootHex = toRootHex(blockRoot); - const slot = firstSidecarSignedBlockHeader.message.slot; - const signature = firstSidecarSignedBlockHeader.signature; + const slot = firstFuluSidecar.signedBlockHeader.message.slot; + const signature = firstFuluSidecar.signedBlockHeader.signature; if (!chain.seenBlockInputCache.isVerifiedProposerSignature(slot, rootHex, signature)) { - const signatureSet = getBlockHeaderProposerSignatureSetByHeaderSlot(chain.config, firstSidecarSignedBlockHeader); + const signatureSet = getBlockHeaderProposerSignatureSetByHeaderSlot( + chain.config, + firstFuluSidecar.signedBlockHeader + ); if ( !(await chain.bls.verifySignatureSets([signatureSet], { @@ -355,22 +505,66 @@ export async function validateBlockDataColumnSidecars( } } + if (blockKzgCommitments && blockKzgCommitments.length !== blockBlobCount) { + throw new DataColumnSidecarValidationError({ + code: DataColumnSidecarErrorCode.INCORRECT_KZG_COMMITMENTS_COUNT, + slot: blockSlot, + columnIndex: firstSidecar.index, + expected: blockBlobCount, + actual: blockKzgCommitments.length, + }); + } + const commitments: Uint8Array[] = []; const cellIndices: number[] = []; const cells: Uint8Array[] = []; const proofs: Uint8Array[] = []; for (let i = 0; i < dataColumnSidecars.length; i++) { const columnSidecar = dataColumnSidecars[i]; + const sidecarSlot = getDataColumnSidecarSlot(columnSidecar); + if (sidecarSlot !== blockSlot) { + throw new DataColumnSidecarValidationError({ + code: DataColumnSidecarErrorCode.INCORRECT_HEADER_ROOT, + slot: blockSlot, + expected: `${blockSlot}`, + actual: `${sidecarSlot}`, + }); + } + + const sidecarBlockRoot = getDataColumnSidecarBlockRoot(columnSidecar); + if (!byteArrayEquals(sidecarBlockRoot, blockRoot)) { + throw new DataColumnSidecarValidationError({ + code: DataColumnSidecarErrorCode.INCORRECT_HEADER_ROOT, + slot: blockSlot, + expected: toRootHex(blockRoot), + actual: toRootHex(sidecarBlockRoot), + }); + } + + if (isGloasDataColumnSidecar(columnSidecar) !== isGloasSidecar) { + throw new DataColumnSidecarValidationError({ + code: DataColumnSidecarErrorCode.INCORRECT_HEADER_ROOT, + slot: blockSlot, + expected: isGloasSidecar ? "gloas" : "fulu", + actual: isGloasDataColumnSidecar(columnSidecar) ? "gloas" : "fulu", + }); + } if ( + firstFuluSidecar !== undefined && i !== 0 && - !ssz.phase0.SignedBeaconBlockHeader.equals(firstSidecarSignedBlockHeader, columnSidecar.signedBlockHeader) + !ssz.phase0.SignedBeaconBlockHeader.equals( + firstFuluSidecar.signedBlockHeader, + (columnSidecar as fulu.DataColumnSidecar).signedBlockHeader + ) ) { throw new DataColumnSidecarValidationError({ code: DataColumnSidecarErrorCode.INCORRECT_HEADER_ROOT, slot: blockSlot, expected: toRootHex(blockRoot), - actual: toRootHex(ssz.phase0.BeaconBlockHeader.hashTreeRoot(columnSidecar.signedBlockHeader.message)), + actual: toRootHex( + ssz.phase0.BeaconBlockHeader.hashTreeRoot((columnSidecar as fulu.DataColumnSidecar).signedBlockHeader.message) + ), }); } @@ -395,16 +589,6 @@ export async function validateBlockDataColumnSidecars( }); } - if (columnSidecar.column.length !== columnSidecar.kzgCommitments.length) { - throw new DataColumnSidecarValidationError({ - code: DataColumnSidecarErrorCode.INCORRECT_KZG_COMMITMENTS_COUNT, - slot: blockSlot, - columnIndex: columnSidecar.index, - expected: columnSidecar.column.length, - actual: columnSidecar.kzgCommitments.length, - }); - } - if (columnSidecar.column.length !== columnSidecar.kzgProofs.length) { throw new DataColumnSidecarValidationError({ code: DataColumnSidecarErrorCode.INCORRECT_KZG_PROOF_COUNT, @@ -415,18 +599,67 @@ export async function validateBlockDataColumnSidecars( }); } - if (!verifyDataColumnSidecarInclusionProof(columnSidecar)) { - throw new DataColumnSidecarValidationError( - { - code: DataColumnSidecarErrorCode.INCLUSION_PROOF_INVALID, + let columnCommitments: Uint8Array[] | undefined; + if (isGloasSidecar) { + columnCommitments = blockKzgCommitments; + if (!columnCommitments) { + throw new DataColumnSidecarValidationError({ + code: DataColumnSidecarErrorCode.INCORRECT_KZG_COMMITMENTS_COUNT, slot: blockSlot, columnIndex: columnSidecar.index, - }, - "DataColumnSidecar has invalid inclusion proof" - ); + expected: columnSidecar.column.length, + actual: 0, + }); + } + if (columnSidecar.column.length !== columnCommitments.length) { + throw new DataColumnSidecarValidationError({ + code: DataColumnSidecarErrorCode.INCORRECT_KZG_COMMITMENTS_COUNT, + slot: blockSlot, + columnIndex: columnSidecar.index, + expected: columnSidecar.column.length, + actual: columnCommitments.length, + }); + } + } else { + const fuluSidecar = columnSidecar as fulu.DataColumnSidecar; + if (columnSidecar.column.length !== fuluSidecar.kzgCommitments.length) { + throw new DataColumnSidecarValidationError({ + code: DataColumnSidecarErrorCode.INCORRECT_KZG_COMMITMENTS_COUNT, + slot: blockSlot, + columnIndex: columnSidecar.index, + expected: columnSidecar.column.length, + actual: fuluSidecar.kzgCommitments.length, + }); + } + + if (blockKzgCommitments && fuluSidecar.kzgCommitments.length === blockKzgCommitments.length) { + for (let j = 0; j < blockKzgCommitments.length; j++) { + if (!byteArrayEquals(fuluSidecar.kzgCommitments[j], blockKzgCommitments[j])) { + throw new DataColumnSidecarValidationError({ + code: DataColumnSidecarErrorCode.INCORRECT_KZG_COMMITMENTS_COUNT, + slot: blockSlot, + columnIndex: columnSidecar.index, + expected: blockKzgCommitments.length, + actual: fuluSidecar.kzgCommitments.length, + }); + } + } + } + + if (!verifyDataColumnSidecarInclusionProof(fuluSidecar)) { + throw new DataColumnSidecarValidationError( + { + code: DataColumnSidecarErrorCode.INCLUSION_PROOF_INVALID, + slot: blockSlot, + columnIndex: columnSidecar.index, + }, + "DataColumnSidecar has invalid inclusion proof" + ); + } + columnCommitments = fuluSidecar.kzgCommitments; } - commitments.push(...columnSidecar.kzgCommitments); + commitments.push(...columnCommitments); cellIndices.push(...Array.from({length: columnSidecar.column.length}, () => columnSidecar.index)); cells.push(...columnSidecar.column); proofs.push(...columnSidecar.kzgProofs); diff --git a/packages/beacon-node/src/network/interface.ts b/packages/beacon-node/src/network/interface.ts index 998948644e5f..ce6407204484 100644 --- a/packages/beacon-node/src/network/interface.ts +++ b/packages/beacon-node/src/network/interface.ts @@ -77,11 +77,11 @@ export interface INetwork extends INetworkCorePublic { sendDataColumnSidecarsByRange( peerId: PeerIdStr, request: fulu.DataColumnSidecarsByRangeRequest - ): Promise; + ): Promise; sendDataColumnSidecarsByRoot( peerId: PeerIdStr, request: DataColumnSidecarsByRootRequest - ): Promise; + ): Promise; // Gossip publishBeaconBlock(signedBlock: SignedBeaconBlock): Promise; diff --git a/packages/beacon-node/src/network/network.ts b/packages/beacon-node/src/network/network.ts index 4aeccb04f8bf..e3903f9e232d 100644 --- a/packages/beacon-node/src/network/network.ts +++ b/packages/beacon-node/src/network/network.ts @@ -11,7 +11,6 @@ import {computeEpochAtSlot} from "@lodestar/state-transition"; import { AttesterSlashing, DataColumnSidecar, - DataColumnSidecars, LightClientBootstrap, LightClientFinalityUpdate, LightClientOptimisticUpdate, @@ -615,7 +614,7 @@ export class Network implements INetwork { async sendDataColumnSidecarsByRange( peerId: PeerIdStr, request: fulu.DataColumnSidecarsByRangeRequest - ): Promise { + ): Promise { return collectMaxResponseTyped( this.sendReqRespRequest(peerId, ReqRespMethod.DataColumnSidecarsByRange, [Version.V1], request), request.count * request.columns.length, @@ -626,7 +625,7 @@ export class Network implements INetwork { async sendDataColumnSidecarsByRoot( peerId: PeerIdStr, request: DataColumnSidecarsByRootRequest - ): Promise { + ): Promise { return collectMaxResponseTyped( this.sendReqRespRequest(peerId, ReqRespMethod.DataColumnSidecarsByRoot, [Version.V1], request), request.reduce((total, {columns}) => total + columns.length, 0), @@ -783,7 +782,7 @@ export class Network implements INetwork { this.core.setTargetGroupCount(count); }; - private onPublishDataColumns = (sidecars: DataColumnSidecars): Promise => { + private onPublishDataColumns = (sidecars: DataColumnSidecar[]): Promise => { return promiseAllMaybeAsync(sidecars.map((sidecar) => () => this.publishDataColumnSidecar(sidecar))); }; diff --git a/packages/beacon-node/src/network/processor/extractSlotRootFns.ts b/packages/beacon-node/src/network/processor/extractSlotRootFns.ts index 1cf09716bbc6..5f009dcc5dbd 100644 --- a/packages/beacon-node/src/network/processor/extractSlotRootFns.ts +++ b/packages/beacon-node/src/network/processor/extractSlotRootFns.ts @@ -2,6 +2,7 @@ import {ForkName} from "@lodestar/params"; import {SlotOptionalRoot, SlotRootHex} from "@lodestar/types"; import { getBlockRootFromBeaconAttestationSerialized, + getBlockRootFromDataColumnSidecarSerialized, getBlockRootFromSignedAggregateAndProofSerialized, getSlotFromBeaconAttestationSerialized, getSlotFromBlobSidecarSerialized, @@ -54,11 +55,12 @@ export function createExtractBlockSlotRootFns(): ExtractSlotRootFns { }, [GossipType.data_column_sidecar]: (data: Uint8Array): SlotOptionalRoot | null => { const slot = getSlotFromDataColumnSidecarSerialized(data); + const root = getBlockRootFromDataColumnSidecarSerialized(data); if (slot === null) { return null; } - return {slot}; + return {slot, root: root ?? undefined}; }, }; } diff --git a/packages/beacon-node/src/network/processor/gossipHandlers.ts b/packages/beacon-node/src/network/processor/gossipHandlers.ts index e196c90cc8ea..33b6d0ec96eb 100644 --- a/packages/beacon-node/src/network/processor/gossipHandlers.ts +++ b/packages/beacon-node/src/network/processor/gossipHandlers.ts @@ -3,6 +3,7 @@ import {BeaconConfig, ChainForkConfig} from "@lodestar/config"; import { ForkName, ForkPostElectra, + ForkPostFulu, ForkPreElectra, ForkSeq, NUMBER_OF_COLUMNS, @@ -10,6 +11,7 @@ import { } from "@lodestar/params"; import {computeTimeAtSlot} from "@lodestar/state-transition"; import { + DataColumnSidecar, Root, SignedBeaconBlock, SingleAttestation, @@ -17,7 +19,7 @@ import { SubnetID, UintNum64, deneb, - fulu, + isGloasDataColumnSidecar, ssz, sszTypesFor, } from "@lodestar/types"; @@ -70,6 +72,7 @@ import {validateGossipPayloadAttestationMessage} from "../../chain/validation/pa import {OpSource} from "../../chain/validatorMonitor.js"; import {Metrics} from "../../metrics/index.js"; import {kzgCommitmentToVersionedHash} from "../../util/blobs.js"; +import {getDataColumnSidecarBlockRoot, getDataColumnSidecarSlot} from "../../util/dataColumns.js"; import {INetworkCore} from "../core/index.js"; import {NetworkEventBus} from "../events.js"; import { @@ -288,16 +291,15 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand } async function validateBeaconDataColumn( - dataColumnSidecar: fulu.DataColumnSidecar, + dataColumnSidecar: DataColumnSidecar, _dataColumnBytes: Uint8Array, gossipSubnet: SubnetID, peerIdStr: string, seenTimestampSec: number ): Promise { metrics?.peerDas.dataColumnSidecarProcessingRequests.inc(); - const dataColumnBlockHeader = dataColumnSidecar.signedBlockHeader.message; - const slot = dataColumnBlockHeader.slot; - const blockRootHex = toRootHex(ssz.phase0.BeaconBlockHeader.hashTreeRoot(dataColumnBlockHeader)); + const slot = getDataColumnSidecarSlot(dataColumnSidecar); + const blockRootHex = toRootHex(getDataColumnSidecarBlockRoot(dataColumnSidecar)); // check to see if block has already been processed and BlockInput has been deleted (column received via reqresp or other means) if (chain.forkChoice.hasBlockHex(blockRootHex)) { @@ -358,7 +360,9 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand blockRoot: blockRootHex, slot, index: dataColumnSidecar.index, - kzgCommitments: dataColumnSidecar.kzgCommitments.map(toHex), + kzgCommitments: isGloasDataColumnSidecar(dataColumnSidecar) + ? [] + : dataColumnSidecar.kzgCommitments.map(toHex), }); } @@ -377,8 +381,9 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand return blockInput; } catch (e) { if (e instanceof DataColumnSidecarGossipError && e.action === GossipAction.REJECT) { + const dataColumnFork = config.getForkName(slot); chain.persistInvalidSszValue( - ssz.fulu.DataColumnSidecar, + sszTypesFor(dataColumnFork as ForkPostFulu).DataColumnSidecar, dataColumnSidecar, `gossip_reject_slot_${slot}_index_${dataColumnSidecar.index}` ); @@ -548,9 +553,8 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand seenTimestampSec, }: GossipHandlerParamGeneric) => { const {serializedData} = gossipData; - // TODO GLOAS: handle gloas.DataColumnSidecar - const dataColumnSidecar = sszDeserialize(topic, serializedData) as fulu.DataColumnSidecar; - const dataColumnSlot = dataColumnSidecar.signedBlockHeader.message.slot; + const dataColumnSidecar = sszDeserialize(topic, serializedData) as DataColumnSidecar; + const dataColumnSlot = getDataColumnSidecarSlot(dataColumnSidecar); const index = dataColumnSidecar.index; if (config.getForkSeq(dataColumnSlot) < ForkSeq.fulu) { diff --git a/packages/beacon-node/src/network/reqresp/types.ts b/packages/beacon-node/src/network/reqresp/types.ts index b9bcec66f618..6905fc32a847 100644 --- a/packages/beacon-node/src/network/reqresp/types.ts +++ b/packages/beacon-node/src/network/reqresp/types.ts @@ -1,8 +1,9 @@ import {Type} from "@chainsafe/ssz"; import {BeaconConfig} from "@lodestar/config"; -import {ForkName, ForkPostAltair, isForkPostAltair} from "@lodestar/params"; +import {ForkName, ForkPostAltair, isForkPostAltair, isForkPostFulu} from "@lodestar/params"; import {Protocol, ProtocolHandler, ReqRespRequest} from "@lodestar/reqresp"; import { + DataColumnSidecar, LightClientBootstrap, LightClientFinalityUpdate, LightClientOptimisticUpdate, @@ -76,8 +77,8 @@ type ResponseBodyByMethod = { [ReqRespMethod.BeaconBlocksByRoot]: SignedBeaconBlock; [ReqRespMethod.BlobSidecarsByRange]: deneb.BlobSidecar; [ReqRespMethod.BlobSidecarsByRoot]: deneb.BlobSidecar; - [ReqRespMethod.DataColumnSidecarsByRange]: fulu.DataColumnSidecar; - [ReqRespMethod.DataColumnSidecarsByRoot]: fulu.DataColumnSidecar; + [ReqRespMethod.DataColumnSidecarsByRange]: DataColumnSidecar; + [ReqRespMethod.DataColumnSidecarsByRoot]: DataColumnSidecar; [ReqRespMethod.LightClientBootstrap]: LightClientBootstrap; [ReqRespMethod.LightClientUpdatesByRange]: LightClientUpdate; @@ -135,8 +136,10 @@ export const responseSszTypeByMethod: {[K in ReqRespMethod]: ResponseTypeGetter< [ReqRespMethod.LightClientBootstrap]: (fork) => sszTypesFor(onlyPostAltairFork(fork)).LightClientBootstrap, [ReqRespMethod.LightClientUpdatesByRange]: (fork) => sszTypesFor(onlyPostAltairFork(fork)).LightClientUpdate, [ReqRespMethod.LightClientFinalityUpdate]: (fork) => sszTypesFor(onlyPostAltairFork(fork)).LightClientFinalityUpdate, - [ReqRespMethod.DataColumnSidecarsByRange]: () => ssz.fulu.DataColumnSidecar, - [ReqRespMethod.DataColumnSidecarsByRoot]: () => ssz.fulu.DataColumnSidecar, + [ReqRespMethod.DataColumnSidecarsByRange]: (fork) => + isForkPostFulu(fork) ? sszTypesFor(fork).DataColumnSidecar : ssz.fulu.DataColumnSidecar, + [ReqRespMethod.DataColumnSidecarsByRoot]: (fork) => + isForkPostFulu(fork) ? sszTypesFor(fork).DataColumnSidecar : ssz.fulu.DataColumnSidecar, [ReqRespMethod.LightClientOptimisticUpdate]: (fork) => sszTypesFor(onlyPostAltairFork(fork)).LightClientOptimisticUpdate, }; diff --git a/packages/beacon-node/src/sync/utils/downloadByRange.ts b/packages/beacon-node/src/sync/utils/downloadByRange.ts index ba7b4df98dfc..6795790a209f 100644 --- a/packages/beacon-node/src/sync/utils/downloadByRange.ts +++ b/packages/beacon-node/src/sync/utils/downloadByRange.ts @@ -1,6 +1,6 @@ import {ChainForkConfig} from "@lodestar/config"; import {ForkPostDeneb, ForkPostFulu, ForkPreFulu, isForkPostFulu} from "@lodestar/params"; -import {SignedBeaconBlock, Slot, deneb, fulu, phase0} from "@lodestar/types"; +import {DataColumnSidecar, SignedBeaconBlock, Slot, deneb, fulu, phase0} from "@lodestar/types"; import {LodestarError, Logger, byteArrayEquals, fromHex, prettyPrintIndices, toRootHex} from "@lodestar/utils"; import { BlockInputSource, @@ -13,7 +13,7 @@ import {SeenBlockInput} from "../../chain/seenCache/seenGossipBlockInput.js"; import {validateBlockBlobSidecars} from "../../chain/validation/blobSidecar.js"; import {validateBlockDataColumnSidecars} from "../../chain/validation/dataColumnSidecar.js"; import {INetwork} from "../../network/index.js"; -import {getBlobKzgCommitments} from "../../util/dataColumns.js"; +import {getBlobKzgCommitments, getDataColumnSidecarSlot} from "../../util/dataColumns.js"; import {PeerIdStr} from "../../util/peerId.js"; import {WarnResult} from "../../util/wrapError.js"; @@ -26,7 +26,7 @@ export type DownloadByRangeRequests = { export type DownloadByRangeResponses = { blocks?: SignedBeaconBlock[]; blobSidecars?: deneb.BlobSidecars; - columnSidecars?: fulu.DataColumnSidecars; + columnSidecars?: DataColumnSidecar[]; }; export type DownloadAndCacheByRangeProps = DownloadByRangeRequests & { @@ -56,7 +56,7 @@ export type ValidatedBlobSidecars = { export type ValidatedColumnSidecars = { blockRoot: Uint8Array; - columnSidecars: fulu.DataColumnSidecars; + columnSidecars: DataColumnSidecar[]; }; export type ValidatedResponses = { @@ -148,7 +148,7 @@ export function cacheByRangeResponses({ } for (const {blockRoot, columnSidecars} of responses.validatedColumnSidecars ?? []) { - const dataSlot = columnSidecars.at(0)?.signedBlockHeader.message.slot; + const dataSlot = columnSidecars.at(0) ? getDataColumnSidecarSlot(columnSidecars[0]) : undefined; if (dataSlot === undefined) { throw new Error( `Coding Error: empty columnSidecars returned for blockRoot=${toRootHex(blockRoot)} from validation functions` @@ -241,7 +241,7 @@ export async function requestByRange({ }): Promise { let blocks: undefined | SignedBeaconBlock[]; let blobSidecars: undefined | deneb.BlobSidecars; - let columnSidecars: undefined | fulu.DataColumnSidecars; + let columnSidecars: undefined | DataColumnSidecar[]; const requests: Promise[] = []; @@ -608,16 +608,16 @@ export async function validateColumnsByRangeResponse( config: ChainForkConfig, request: fulu.DataColumnSidecarsByRangeRequest, blocks: ValidatedBlock[], - columnSidecars: fulu.DataColumnSidecars + columnSidecars: DataColumnSidecar[] ): Promise> { const warnings: DownloadByRangeError[] = []; - const seenColumns = new Map>(); + const seenColumns = new Map>(); let currentSlot = -1; let currentIndex = -1; // Check for duplicates and order for (const columnSidecar of columnSidecars) { - const slot = columnSidecar.signedBlockHeader.message.slot; + const slot = getDataColumnSidecarSlot(columnSidecar); let seenSlotColumns = seenColumns.get(slot); if (!seenSlotColumns) { seenSlotColumns = new Map(); @@ -625,38 +625,30 @@ export async function validateColumnsByRangeResponse( } if (seenSlotColumns.has(columnSidecar.index)) { - warnings.push( - new DownloadByRangeError({ - code: DownloadByRangeErrorCode.DUPLICATE_COLUMN, - slot, - index: columnSidecar.index, - }) - ); - - continue; + throw new DownloadByRangeError({ + code: DownloadByRangeErrorCode.DUPLICATE_COLUMN, + slot, + index: columnSidecar.index, + }); } if (currentSlot > slot) { - warnings.push( - new DownloadByRangeError( - { - code: DownloadByRangeErrorCode.OUT_OF_ORDER_COLUMNS, - slot, - }, - "ColumnSidecars received out of slot order" - ) + throw new DownloadByRangeError( + { + code: DownloadByRangeErrorCode.OUT_OF_ORDER_COLUMNS, + slot, + }, + "ColumnSidecars received out of slot order" ); } if (currentSlot === slot && currentIndex > columnSidecar.index) { - warnings.push( - new DownloadByRangeError( - { - code: DownloadByRangeErrorCode.OUT_OF_ORDER_COLUMNS, - slot, - }, - "Column indices out of order within a slot" - ) + throw new DownloadByRangeError( + { + code: DownloadByRangeErrorCode.OUT_OF_ORDER_COLUMNS, + slot, + }, + "Column indices out of order within a slot" ); } @@ -676,12 +668,12 @@ export async function validateColumnsByRangeResponse( const slot = block.message.slot; const rootHex = toRootHex(blockRoot); const forkName = config.getForkName(slot); - const columnSidecarsMap: Map = seenColumns.get(slot) ?? new Map(); + const columnSidecarsMap: Map = seenColumns.get(slot) ?? new Map(); const columnSidecars = Array.from(columnSidecarsMap.values()).sort((a, b) => a.index - b.index); let blobCount: number; if (!isForkPostFulu(forkName)) { - const dataSlot = columnSidecars.at(0)?.signedBlockHeader.message.slot; + const dataSlot = columnSidecars.at(0) ? getDataColumnSidecarSlot(columnSidecars[0]) : undefined; throw new DownloadByRangeError({ code: DownloadByRangeErrorCode.MISMATCH_BLOCK_FORK, slot, @@ -764,7 +756,8 @@ export async function validateColumnsByRangeResponse( slot, blockRoot, blobCount, - columnSidecars + columnSidecars, + getBlobKzgCommitments(forkName, block as SignedBeaconBlock) ).then(() => ({ blockRoot, columnSidecars, diff --git a/packages/beacon-node/src/sync/utils/downloadByRoot.ts b/packages/beacon-node/src/sync/utils/downloadByRoot.ts index 6e5c6e551afe..c3682468d730 100644 --- a/packages/beacon-node/src/sync/utils/downloadByRoot.ts +++ b/packages/beacon-node/src/sync/utils/downloadByRoot.ts @@ -1,7 +1,15 @@ import {routes} from "@lodestar/api"; import {ChainForkConfig} from "@lodestar/config"; import {ForkPostDeneb, ForkPostFulu, ForkPreFulu, isForkPostDeneb, isForkPostFulu} from "@lodestar/params"; -import {BlobIndex, ColumnIndex, SignedBeaconBlock, Slot, deneb, fulu} from "@lodestar/types"; +import { + BlobIndex, + ColumnIndex, + DataColumnSidecar, + SignedBeaconBlock, + Slot, + deneb, + isGloasDataColumnSidecar, +} from "@lodestar/types"; import {LodestarError, byteArrayEquals, fromHex, prettyPrintIndices, toHex, toRootHex} from "@lodestar/utils"; import {isBlockInputBlobs, isBlockInputColumns} from "../../chain/blocks/blockInput/blockInput.js"; import {BlockInputSource, IBlockInput} from "../../chain/blocks/blockInput/types.js"; @@ -52,7 +60,7 @@ export type FetchByRootAndValidateColumnsProps = FetchByRootCoreProps & { export type FetchByRootResponses = { block: SignedBeaconBlock; blobSidecars?: deneb.BlobSidecars; - columnSidecars?: fulu.DataColumnSidecars; + columnSidecars?: DataColumnSidecar[]; }; export type DownloadByRootProps = FetchByRootCoreProps & { @@ -176,7 +184,7 @@ export async function downloadByRoot({ blockRoot: rootHex, slot: blockInput.slot, index: columnSidecar.index, - kzgCommitments: columnSidecar.kzgCommitments.map(toHex), + kzgCommitments: isGloasDataColumnSidecar(columnSidecar) ? [] : columnSidecar.kzgCommitments.map(toHex), }); } } @@ -213,7 +221,7 @@ export async function fetchByRoot({ }: FetchByRootProps): Promise> { let block: SignedBeaconBlock; let blobSidecars: deneb.BlobSidecars | undefined; - let columnSidecarResult: WarnResult | undefined; + let columnSidecarResult: WarnResult | undefined; const {peerId: peerIdStr} = peerMeta; if (isPendingBlockInput(cacheItem)) { @@ -376,7 +384,7 @@ export async function fetchAndValidateColumns({ block, blockRoot, missing, -}: FetchByRootAndValidateColumnsProps): Promise> { +}: FetchByRootAndValidateColumnsProps): Promise> { const {peerId: peerIdStr} = peerMeta; const slot = block.message.slot; const blobCount = getBlobKzgCommitments(forkName, block).length; @@ -440,7 +448,14 @@ export async function fetchAndValidateColumns({ ); } - await validateBlockDataColumnSidecars(chain, slot, blockRoot, blobCount, columnSidecars); + await validateBlockDataColumnSidecars( + chain, + slot, + blockRoot, + blobCount, + columnSidecars, + getBlobKzgCommitments(forkName, block) + ); return {result: columnSidecars, warnings: warnings.length > 0 ? warnings : null}; } @@ -451,10 +466,9 @@ export async function fetchColumnsByRoot({ peerMeta, blockRoot, missing, -}: Pick< - FetchByRootAndValidateColumnsProps, - "network" | "peerMeta" | "blockRoot" | "missing" ->): Promise { +}: Pick): Promise< + DataColumnSidecar[] +> { return await network.sendDataColumnSidecarsByRoot(peerMeta.peerId, [{blockRoot, columns: missing}]); } diff --git a/packages/beacon-node/src/util/blobs.ts b/packages/beacon-node/src/util/blobs.ts index 0424210916f4..bd7f3bd67397 100644 --- a/packages/beacon-node/src/util/blobs.ts +++ b/packages/beacon-node/src/util/blobs.ts @@ -13,7 +13,17 @@ import { VERSIONED_HASH_VERSION_KZG, } from "@lodestar/params"; import {signedBlockToSignedHeader} from "@lodestar/state-transition"; -import {BeaconBlockBody, DataColumnSidecars, SSZTypesFor, SignedBeaconBlock, deneb, fulu, ssz} from "@lodestar/types"; +import { + BeaconBlockBody, + DataColumnSidecar, + SSZTypesFor, + SignedBeaconBlock, + deneb, + fulu, + gloas, + isGloasDataColumnSidecar, + ssz, +} from "@lodestar/types"; import {kzg} from "./kzg.js"; type VersionHash = Uint8Array; @@ -71,8 +81,8 @@ export function getBlobSidecars( * See https://github.com/ethereum/consensus-specs/blob/v1.6.0-alpha.4/specs/fulu/das-core.md#recover_matrix */ export async function dataColumnMatrixRecovery( - partialSidecars: Map -): Promise { + partialSidecars: Map +): Promise { const columnCount = partialSidecars.size; if (columnCount < NUMBER_OF_COLUMNS / 2) { // We don't have enough columns to recover @@ -92,7 +102,7 @@ export async function dataColumnMatrixRecovery( // should not happen because we check the size of the cache before this throw new Error("No data column found in cache to recover from"); } - const blobCount = firstDataColumn.kzgCommitments.length; + const blobCount = firstDataColumn.column.length; const fullColumns: Array = Array.from( {length: NUMBER_OF_COLUMNS}, @@ -121,7 +131,7 @@ export async function dataColumnMatrixRecovery( } } - const result: fulu.DataColumnSidecars = new Array(NUMBER_OF_COLUMNS); + const result: DataColumnSidecar[] = new Array(NUMBER_OF_COLUMNS); for (let columnIndex = 0; columnIndex < NUMBER_OF_COLUMNS; columnIndex++) { let sidecar = partialSidecars.get(columnIndex); @@ -131,14 +141,24 @@ export async function dataColumnMatrixRecovery( continue; } - sidecar = { - index: columnIndex, - column: fullColumns[columnIndex], - kzgCommitments: firstDataColumn.kzgCommitments, - kzgProofs: Array.from({length: blobCount}, (_, rowIndex) => blobProofs[rowIndex][columnIndex]), - signedBlockHeader: firstDataColumn.signedBlockHeader, - kzgCommitmentsInclusionProof: firstDataColumn.kzgCommitmentsInclusionProof, - }; + if (isGloasDataColumnSidecar(firstDataColumn)) { + sidecar = { + index: columnIndex, + column: fullColumns[columnIndex], + kzgProofs: Array.from({length: blobCount}, (_, rowIndex) => blobProofs[rowIndex][columnIndex]), + slot: firstDataColumn.slot, + beaconBlockRoot: firstDataColumn.beaconBlockRoot, + } satisfies gloas.DataColumnSidecar; + } else { + sidecar = { + index: columnIndex, + column: fullColumns[columnIndex], + kzgCommitments: firstDataColumn.kzgCommitments, + kzgProofs: Array.from({length: blobCount}, (_, rowIndex) => blobProofs[rowIndex][columnIndex]), + signedBlockHeader: firstDataColumn.signedBlockHeader, + kzgCommitmentsInclusionProof: firstDataColumn.kzgCommitmentsInclusionProof, + } satisfies fulu.DataColumnSidecar; + } result[columnIndex] = sidecar; } @@ -149,7 +169,7 @@ export async function dataColumnMatrixRecovery( * Reconstruct blobs from a set of data columns, at least 50%+ of all the columns * must be provided to allow to reconstruct the full data matrix */ -export async function reconstructBlobs(sidecars: DataColumnSidecars, indices?: number[]): Promise { +export async function reconstructBlobs(sidecars: DataColumnSidecar[], indices?: number[]): Promise { if (sidecars.length < NUMBER_OF_COLUMNS / 2) { throw Error( `Expected at least ${NUMBER_OF_COLUMNS / 2} data columns to reconstruct blobs, received ${sidecars.length}` @@ -188,7 +208,7 @@ export async function reconstructBlobs(sidecars: DataColumnSidecars, indices?: n * Recover cells for specific blob indices from a set of data columns */ async function recoverBlobCells( - partialSidecars: DataColumnSidecars, + partialSidecars: DataColumnSidecar[], blobIndices: number[] ): Promise | null> { const columnCount = partialSidecars.length; diff --git a/packages/beacon-node/src/util/dataColumns.ts b/packages/beacon-node/src/util/dataColumns.ts index 69bca00577e4..6562ded2f608 100644 --- a/packages/beacon-node/src/util/dataColumns.ts +++ b/packages/beacon-node/src/util/dataColumns.ts @@ -16,6 +16,8 @@ import { BeaconBlockBody, ColumnIndex, CustodyIndex, + DataColumnSidecar, + DataColumnSidecars, Root, SSZTypesFor, SignedBeaconBlock, @@ -24,6 +26,7 @@ import { deneb, fulu, gloas, + isGloasDataColumnSidecar, ssz, } from "@lodestar/types"; import {bytesToBigInt} from "@lodestar/utils"; @@ -328,7 +331,7 @@ export function getDataColumnSidecarsFromBlock( config: ChainForkConfig, signedBlock: SignedBeaconBlock, cellsAndKzgProofs: {cells: Uint8Array[]; proofs: Uint8Array[]}[] -): fulu.DataColumnSidecars { +): DataColumnSidecars { const fork = config.getForkName(signedBlock.message.slot); const blobKzgCommitments = getBlobKzgCommitments(fork, signedBlock); @@ -336,6 +339,12 @@ export function getDataColumnSidecarsFromBlock( if (blobKzgCommitments.length === 0) { return []; } + + if (isForkPostGloas(fork)) { + const beaconBlockRoot = config.getForkTypes(signedBlock.message.slot).BeaconBlock.hashTreeRoot(signedBlock.message); + return getDataColumnSidecarsForGloas(signedBlock.message.slot, beaconBlockRoot, cellsAndKzgProofs); + } + const signedBlockHeader = signedBlockToSignedHeader(config, signedBlock); const kzgCommitmentsInclusionProof = computePostFuluKzgCommitmentsInclusionProof(fork, signedBlock.message.body); @@ -351,9 +360,13 @@ export function getDataColumnSidecarsFromBlock( * https://github.com/ethereum/consensus-specs/blob/v1.6.0-alpha.4/specs/fulu/validator.md#get_data_column_sidecars_from_column_sidecar */ export function getDataColumnSidecarsFromColumnSidecar( - sidecar: fulu.DataColumnSidecar, + sidecar: DataColumnSidecar, cellsAndKzgProofs: {cells: Uint8Array[]; proofs: Uint8Array[]}[] -): fulu.DataColumnSidecars { +): DataColumnSidecars { + if (isGloasDataColumnSidecar(sidecar)) { + return getDataColumnSidecarsForGloas(sidecar.slot, sidecar.beaconBlockRoot, cellsAndKzgProofs); + } + return getDataColumnSidecars( sidecar.signedBlockHeader, sidecar.kzgCommitments, @@ -362,6 +375,22 @@ export function getDataColumnSidecarsFromColumnSidecar( ); } +export function getDataColumnSidecarSlot(sidecar: DataColumnSidecar): Slot { + if (isGloasDataColumnSidecar(sidecar)) { + return sidecar.slot; + } + + return sidecar.signedBlockHeader.message.slot; +} + +export function getDataColumnSidecarBlockRoot(sidecar: DataColumnSidecar): Root { + if (isGloasDataColumnSidecar(sidecar)) { + return sidecar.beaconBlockRoot; + } + + return ssz.phase0.BeaconBlockHeader.hashTreeRoot(sidecar.signedBlockHeader.message); +} + /** * In Gloas, data column sidecars have a simplified structure with `slot` and `beaconBlockRoot` * instead of `signedBlockHeader`, `kzgCommitments`, and `kzgCommitmentsInclusionProof`. @@ -416,7 +445,7 @@ export async function recoverDataColumnSidecars( } metrics?.recoverDataColumnSidecars.custodyBeforeReconstruction.set(columnCount); - const partialSidecars = new Map(); + const partialSidecars = new Map(); for (const columnSidecar of existingColumns) { // the more columns we put, the slower the recover if (partialSidecars.size >= NUMBER_OF_COLUMNS / 2) { diff --git a/packages/beacon-node/src/util/execution.ts b/packages/beacon-node/src/util/execution.ts index c05393769829..b497cf27ad64 100644 --- a/packages/beacon-node/src/util/execution.ts +++ b/packages/beacon-node/src/util/execution.ts @@ -2,7 +2,7 @@ import {routes} from "@lodestar/api"; import {ChainForkConfig} from "@lodestar/config"; import {ForkPostFulu, ForkPreFulu} from "@lodestar/params"; import {signedBlockToSignedHeader} from "@lodestar/state-transition"; -import {deneb, fulu} from "@lodestar/types"; +import {DataColumnSidecar, SignedBeaconBlock, deneb, isGloasDataColumnSidecar} from "@lodestar/types"; import {toHex} from "@lodestar/utils"; import {isBlockInputBlobs, isBlockInputColumns} from "../chain/blocks/blockInput/blockInput.js"; import {BlockInputSource, IBlockInput} from "../chain/blocks/blockInput/types.js"; @@ -172,12 +172,12 @@ export async function getDataColumnSidecarsFromExecution( return DataColumnEngineResult.SuccessLate; } - let dataColumnSidecars: fulu.DataColumnSidecars; + let dataColumnSidecars: DataColumnSidecar[]; const cellsAndProofs = await getCellsAndProofs(blobs); if (blockInput.hasBlock()) { dataColumnSidecars = getDataColumnSidecarsFromBlock( config, - blockInput.getBlock() as fulu.SignedBeaconBlock, + blockInput.getBlock() as SignedBeaconBlock, cellsAndProofs ); } else { @@ -213,7 +213,7 @@ export async function getDataColumnSidecarsFromExecution( blockRoot: blockInput.blockRootHex, slot: blockInput.slot, index: columnSidecar.index, - kzgCommitments: columnSidecar.kzgCommitments.map(toHex), + kzgCommitments: isGloasDataColumnSidecar(columnSidecar) ? [] : columnSidecar.kzgCommitments.map(toHex), }); } } diff --git a/packages/beacon-node/src/util/sszBytes.ts b/packages/beacon-node/src/util/sszBytes.ts index 8ba5ddf3291e..7e7c6b8bdb97 100644 --- a/packages/beacon-node/src/util/sszBytes.ts +++ b/packages/beacon-node/src/util/sszBytes.ts @@ -8,6 +8,7 @@ import { ForkSeq, MAX_COMMITTEES_PER_SLOT, isForkPostElectra, + isForkPostGloas, } from "@lodestar/params"; import {BLSSignature, CommitteeIndex, RootHex, Slot, ValidatorIndex, ssz} from "@lodestar/types"; @@ -408,13 +409,54 @@ export function getSlotFromBlobSidecarSerialized(data: Uint8Array): Slot | null } */ -const SLOT_BYTES_POSITION_IN_SIGNED_DATA_COLUMN_SIDECAR = 20; +const DATA_COLUMN_SIDECAR_FIRST_OFFSET_POSITION = 8; +const GLOAS_DATA_COLUMN_SIDECAR_FIRST_OFFSET = 56; +const SLOT_BYTES_POSITION_IN_FULU_DATA_COLUMN_SIDECAR = 20; +const SLOT_BYTES_POSITION_IN_GLOAS_DATA_COLUMN_SIDECAR = 16; +const BLOCK_ROOT_POSITION_IN_GLOAS_DATA_COLUMN_SIDECAR = 24; + +function isGloasDataColumnSidecarSerialized(data: Uint8Array): boolean { + if (data.length < DATA_COLUMN_SIDECAR_FIRST_OFFSET_POSITION + 4) { + return false; + } + + const firstOffset = + data[DATA_COLUMN_SIDECAR_FIRST_OFFSET_POSITION] | + (data[DATA_COLUMN_SIDECAR_FIRST_OFFSET_POSITION + 1] << 8) | + (data[DATA_COLUMN_SIDECAR_FIRST_OFFSET_POSITION + 2] << 16) | + (data[DATA_COLUMN_SIDECAR_FIRST_OFFSET_POSITION + 3] << 24); + + return firstOffset === GLOAS_DATA_COLUMN_SIDECAR_FIRST_OFFSET; +} + export function getSlotFromDataColumnSidecarSerialized(data: Uint8Array): Slot | null { - if (data.length < SLOT_BYTES_POSITION_IN_SIGNED_DATA_COLUMN_SIDECAR + SLOT_SIZE) { + const slotOffset = isGloasDataColumnSidecarSerialized(data) + ? SLOT_BYTES_POSITION_IN_GLOAS_DATA_COLUMN_SIDECAR + : SLOT_BYTES_POSITION_IN_FULU_DATA_COLUMN_SIDECAR; + + if (data.length < slotOffset + SLOT_SIZE) { + return null; + } + + return getSlotFromOffset(data, slotOffset); +} + +export function getBlockRootFromDataColumnSidecarSerialized(data: Uint8Array): BlockRootHex | null { + if (!isGloasDataColumnSidecarSerialized(data)) { + return null; + } + + if (data.length < BLOCK_ROOT_POSITION_IN_GLOAS_DATA_COLUMN_SIDECAR + ROOT_SIZE) { return null; } - return getSlotFromOffset(data, SLOT_BYTES_POSITION_IN_SIGNED_DATA_COLUMN_SIDECAR); + blockRootBuf.set( + data.subarray( + BLOCK_ROOT_POSITION_IN_GLOAS_DATA_COLUMN_SIDECAR, + BLOCK_ROOT_POSITION_IN_GLOAS_DATA_COLUMN_SIDECAR + ROOT_SIZE + ) + ); + return `0x${blockRootBuf.toString("hex")}`; } /** @@ -482,9 +524,14 @@ export function getBlobKzgCommitmentsCountFromSignedBeaconBlockSerialized( if (slot === null) throw new Error("Can not parse the slot from block bytes"); if (config.getForkSeq(slot) < ForkSeq.deneb) return 0; + const forkName = config.getForkName(slot); + + if (isForkPostGloas(forkName)) { + const signedBlock = ssz[forkName].SignedBeaconBlock.deserialize(blockBytes); + return signedBlock.message.body.signedExecutionPayloadBid.message.blobKzgCommitments.length; + } - const {SignedBeaconBlock, BeaconBlock, BeaconBlockBody, KZGCommitment} = - ssz[config.getForkName(slot) as ForkPostDeneb]; + const {SignedBeaconBlock, BeaconBlock, BeaconBlockBody, KZGCommitment} = ssz[forkName as ForkPostDeneb]; const view = new DataView(blockBytes.buffer, blockBytes.byteOffset, blockBytes.byteLength); const singedBlockFieldRanges = SignedBeaconBlock.getFieldRanges(view, 0, blockBytes.length); diff --git a/packages/beacon-node/test/unit/db/api/repositories/dataColumn.test.ts b/packages/beacon-node/test/unit/db/api/repositories/dataColumn.test.ts index 150e79bbe2a6..b741818961de 100644 --- a/packages/beacon-node/test/unit/db/api/repositories/dataColumn.test.ts +++ b/packages/beacon-node/test/unit/db/api/repositories/dataColumn.test.ts @@ -40,7 +40,11 @@ describe("dataColumnSidecar repository", () => { const commitments = Array.from({length: blobKzgCommitmentsLen}, () => commitment); signedBlock.message.body.blobKzgCommitments = commitments; const cellsAndProofs = blobs.map((b) => kzg.computeCellsAndKzgProofs(b)); - allDataColumnSidecars = getDataColumnSidecarsFromBlock(config, signedBlock, cellsAndProofs); + allDataColumnSidecars = getDataColumnSidecarsFromBlock( + config, + signedBlock, + cellsAndProofs + ) as fulu.DataColumnSidecars; for (let j = 0; j < allDataColumnSidecars.length; j++) { allDataColumnSidecars[j].index = j; } @@ -145,7 +149,11 @@ describe("dataColumnSidecarArchive repository", () => { const commitments = Array.from({length: blobKzgCommitmentsLen}, () => commitment); signedBlock.message.body.blobKzgCommitments = commitments; const cellsAndProofs = blobs.map((b) => kzg.computeCellsAndKzgProofs(b)); - allDataColumnSidecars = getDataColumnSidecarsFromBlock(config, signedBlock, cellsAndProofs); + allDataColumnSidecars = getDataColumnSidecarsFromBlock( + config, + signedBlock, + cellsAndProofs + ) as fulu.DataColumnSidecars; for (let j = 0; j < allDataColumnSidecars.length; j++) { allDataColumnSidecars[j].index = j; } diff --git a/packages/beacon-node/test/unit/util/kzg.test.ts b/packages/beacon-node/test/unit/util/kzg.test.ts index 48359c5ef771..382da4d0358f 100644 --- a/packages/beacon-node/test/unit/util/kzg.test.ts +++ b/packages/beacon-node/test/unit/util/kzg.test.ts @@ -93,7 +93,11 @@ describe("KZG", () => { signedBeaconBlock.message.body.blobKzgCommitments.push(commitment); } - const sidecars = getDataColumnSidecarsFromBlock(config, signedBeaconBlock, cellsAndProofs); + const sidecars = getDataColumnSidecarsFromBlock( + config, + signedBeaconBlock, + cellsAndProofs + ) as fulu.DataColumnSidecars; const signedBlockHeader = signedBlockToSignedHeader(config, signedBeaconBlock); sidecars.forEach((sidecar, column) => { @@ -144,6 +148,6 @@ describe("KZG", () => { throw new Error("Recovered sidecars should not be null"); } expect(recoveredSidecars.length).toBe(NUMBER_OF_COLUMNS); - expect(ssz.fulu.DataColumnSidecars.equals(recoveredSidecars, sidecars)).toBeTruthy(); + expect(ssz.fulu.DataColumnSidecars.equals(recoveredSidecars as fulu.DataColumnSidecars, sidecars)).toBeTruthy(); }); }); From 123b9dc00139e29fcd478f24803f87b918175135 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Fri, 20 Feb 2026 18:44:36 +0000 Subject: [PATCH 2/4] clean up --- .../src/chain/blocks/blockInput/blockInput.ts | 100 +++++++----------- .../src/chain/blocks/blockInput/types.ts | 6 +- .../beacon-node/src/chain/blocks/types.ts | 4 +- 3 files changed, 41 insertions(+), 69 deletions(-) diff --git a/packages/beacon-node/src/chain/blocks/blockInput/blockInput.ts b/packages/beacon-node/src/chain/blocks/blockInput/blockInput.ts index 5a6b326b02d5..be8a4609b12d 100644 --- a/packages/beacon-node/src/chain/blocks/blockInput/blockInput.ts +++ b/packages/beacon-node/src/chain/blocks/blockInput/blockInput.ts @@ -1,5 +1,5 @@ -import {ForkName, ForkPostFulu, ForkPreDeneb, NUMBER_OF_COLUMNS, isForkPostGloas} from "@lodestar/params"; -import {BlobIndex, ColumnIndex, DataColumnSidecar, SignedBeaconBlock, Slot, deneb, gloas} from "@lodestar/types"; +import {ForkName, ForkPostFulu, ForkPreDeneb, ForkPreGloas, NUMBER_OF_COLUMNS} from "@lodestar/params"; +import {BeaconBlockBody, BlobIndex, ColumnIndex, SignedBeaconBlock, Slot, deneb, fulu} from "@lodestar/types"; import {byteArrayEquals, fromHex, prettyBytes, toRootHex, withTimeout} from "@lodestar/utils"; import {VersionedHashes} from "../../../execution/index.js"; import {kzgCommitmentToVersionedHash} from "../../../util/blobs.js"; @@ -553,24 +553,9 @@ function assertBlockAndBlobArePaired( } } -function isGloasDataColumnSidecar(sidecar: DataColumnSidecar): sidecar is gloas.DataColumnSidecar { - return (sidecar as gloas.DataColumnSidecar).beaconBlockRoot !== undefined; -} - -function getBlobKzgCommitmentsFromColumnsBlock( - block: SignedBeaconBlock, - forkName: ForkColumnsDA -): Uint8Array[] { - if (isForkPostGloas(forkName)) { - return (block as gloas.SignedBeaconBlock).message.body.signedExecutionPayloadBid.message.blobKzgCommitments; - } - - return (block.message.body as {blobKzgCommitments: Uint8Array[]}).blobKzgCommitments; -} - // Columns DA -export type ForkColumnsDA = ForkPostFulu; +export type ForkColumnsDA = ForkName.fulu; type BlockInputColumnsState = | { @@ -609,7 +594,7 @@ type BlockInputColumnsState = * - The block is not yet seen and all required sampled columns are seen * - The block is not yet seen and all required sampled columns are not yet seen */ -export class BlockInputColumns extends AbstractBlockInput { +export class BlockInputColumns extends AbstractBlockInput { type = DAType.Columns as const; state: BlockInputColumnsState; @@ -622,7 +607,7 @@ export class BlockInputColumns extends AbstractBlockInput(); + protected computedDataPromise = createPromise(); private constructor( init: BlockInputInit, @@ -644,13 +629,15 @@ export class BlockInputColumns extends AbstractBlockInput & CreateBlockInputMeta & {sampledColumns: ColumnIndex[]; custodyColumns: ColumnIndex[]} ): BlockInputColumns { - const blobKzgCommitments = getBlobKzgCommitmentsFromColumnsBlock(props.block, props.forkName as ForkColumnsDA); - const hasAllData = props.daOutOfRange || blobKzgCommitments.length === 0 || props.sampledColumns.length === 0; + const hasAllData = + props.daOutOfRange || + props.block.message.body.blobKzgCommitments.length === 0 || + props.sampledColumns.length === 0; const state = { hasBlock: true, hasAllData, hasComputedAllData: hasAllData, - versionedHashes: blobKzgCommitments.map(kzgCommitmentToVersionedHash), + versionedHashes: props.block.message.body.blobKzgCommitments.map(kzgCommitmentToVersionedHash), block: props.block, source: { source: props.source, @@ -681,35 +668,21 @@ export class BlockInputColumns extends AbstractBlockInput).blobKzgCommitments.length === 0 || + this.state.hasAllData; + const hasComputedAllData = + props.block.message.body.blobKzgCommitments.length === 0 || this.state.hasComputedAllData; this.state = { ...this.state, hasBlock: true, hasAllData, hasComputedAllData, - versionedHashes: blobKzgCommitments.map(kzgCommitmentToVersionedHash), block: props.block, source: { source: props.source, @@ -779,8 +753,6 @@ export class BlockInputColumns extends AbstractBlockInput columnSidecar); } @@ -924,7 +896,7 @@ export class BlockInputColumns extends AbstractBlockInput { + waitForComputedAllData(timeout: number, signal?: AbortSignal): Promise { if (!this.state.hasComputedAllData) { return withTimeout(() => this.computedDataPromise.promise, timeout, signal); } diff --git a/packages/beacon-node/src/chain/blocks/blockInput/types.ts b/packages/beacon-node/src/chain/blocks/blockInput/types.ts index b454c2a697d6..252457113321 100644 --- a/packages/beacon-node/src/chain/blocks/blockInput/types.ts +++ b/packages/beacon-node/src/chain/blocks/blockInput/types.ts @@ -1,5 +1,5 @@ import {ForkName} from "@lodestar/params"; -import {ColumnIndex, DataColumnSidecar, RootHex, SignedBeaconBlock, Slot, deneb} from "@lodestar/types"; +import {ColumnIndex, RootHex, SignedBeaconBlock, Slot, deneb, fulu} from "@lodestar/types"; import {VersionedHashes} from "../../../execution/index.js"; export enum DAType { @@ -8,7 +8,7 @@ export enum DAType { Columns = "columns", } -export type DAData = null | deneb.BlobSidecars | DataColumnSidecar[]; +export type DAData = null | deneb.BlobSidecars | fulu.DataColumnSidecars; /** * Represents were input originated. Blocks and Data can come from different @@ -55,7 +55,7 @@ export type BlockWithSource = SourceMeta & {block: SignedBeaconBlock; blockRootH export type BlobWithSource = SourceMeta & {blobSidecar: deneb.BlobSidecar}; -export type ColumnWithSource = SourceMeta & {columnSidecar: DataColumnSidecar}; +export type ColumnWithSource = SourceMeta & {columnSidecar: fulu.DataColumnSidecar}; export type BlockHeaderMeta = { forkName: ForkName; diff --git a/packages/beacon-node/src/chain/blocks/types.ts b/packages/beacon-node/src/chain/blocks/types.ts index 4e60db0c313a..50ed0076515c 100644 --- a/packages/beacon-node/src/chain/blocks/types.ts +++ b/packages/beacon-node/src/chain/blocks/types.ts @@ -2,7 +2,7 @@ import type {ChainForkConfig} from "@lodestar/config"; import {MaybeValidExecutionStatus} from "@lodestar/fork-choice"; import {ForkSeq} from "@lodestar/params"; import {CachedBeaconStateAllForks, DataAvailabilityStatus, computeEpochAtSlot} from "@lodestar/state-transition"; -import type {DataColumnSidecar, IndexedAttestation, Slot} from "@lodestar/types"; +import type {IndexedAttestation, Slot, fulu} from "@lodestar/types"; import {IBlockInput} from "./blockInput/types.js"; export enum GossipedInputType { @@ -12,7 +12,7 @@ export enum GossipedInputType { } type DataColumnData = { - dataColumn: DataColumnSidecar; + dataColumn: fulu.DataColumnSidecar; dataColumnBytes: Uint8Array | null; }; export type DataColumnsCacheMap = Map; From 9fa9e3c7877a9d7c6bfe17abf18ea7ba4a9318ff Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Thu, 26 Feb 2026 23:16:54 +0000 Subject: [PATCH 3/4] Review --- .../src/chain/validation/dataColumnSidecar.ts | 6 +++ packages/beacon-node/src/util/sszBytes.ts | 42 ++++++++++++++++++- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/packages/beacon-node/src/chain/validation/dataColumnSidecar.ts b/packages/beacon-node/src/chain/validation/dataColumnSidecar.ts index ff573ce91cd7..d390e17ff581 100644 --- a/packages/beacon-node/src/chain/validation/dataColumnSidecar.ts +++ b/packages/beacon-node/src/chain/validation/dataColumnSidecar.ts @@ -243,10 +243,13 @@ async function validateGossipDataColumnSidecarGloas( gossipSubnet: SubnetID, metrics: Metrics | null ): Promise { + // validate_data_column_sidecar + // https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.0/specs/gloas/p2p-interface.md const blockRoot = getDataColumnSidecarBlockRoot(dataColumnSidecar); const blockRootHex = toRootHex(blockRoot); const slot = getDataColumnSidecarSlot(dataColumnSidecar); + // [IGNORE] The sidecar's block must be known const cachedBlockInput = chain.seenBlockInputCache.get(blockRootHex); const blockData = cachedBlockInput?.hasBlock() ? cachedBlockInput.getBlock() @@ -270,10 +273,12 @@ async function validateGossipDataColumnSidecarGloas( }); } + // [REJECT] The sidecar must pass verify_data_column_sidecar against the block commitments const kzgCommitments = (blockData as gloas.SignedBeaconBlock).message.body.signedExecutionPayloadBid.message .blobKzgCommitments; verifyDataColumnSidecarGloas(dataColumnSidecar, kzgCommitments); + // [REJECT] The sidecar must be on the correct subnet if (computeSubnetForDataColumnSidecar(chain.config, dataColumnSidecar) !== gossipSubnet) { throw new DataColumnSidecarGossipError(GossipAction.REJECT, { code: DataColumnSidecarErrorCode.INVALID_SUBNET, @@ -282,6 +287,7 @@ async function validateGossipDataColumnSidecarGloas( }); } + // [REJECT] The sidecar kzg proofs must verify const kzgProofTimer = metrics?.peerDas.dataColumnSidecarKzgProofsVerificationTime.startTimer(); try { await verifyDataColumnSidecarKzgProofs( diff --git a/packages/beacon-node/src/util/sszBytes.ts b/packages/beacon-node/src/util/sszBytes.ts index 7e7c6b8bdb97..d53000316fda 100644 --- a/packages/beacon-node/src/util/sszBytes.ts +++ b/packages/beacon-node/src/util/sszBytes.ts @@ -410,6 +410,9 @@ export function getSlotFromBlobSidecarSerialized(data: Uint8Array): Slot | null */ const DATA_COLUMN_SIDECAR_FIRST_OFFSET_POSITION = 8; +// Gloas DataColumnSidecar SSZ layout: +// index (8 bytes) | column offset (4 bytes) | kzgProofs offset (4 bytes) | slot (8 bytes) | beaconBlockRoot (32 bytes) +// Fixed portion total: 8 + 4 + 4 + 8 + 32 = 56 const GLOAS_DATA_COLUMN_SIDECAR_FIRST_OFFSET = 56; const SLOT_BYTES_POSITION_IN_FULU_DATA_COLUMN_SIDECAR = 20; const SLOT_BYTES_POSITION_IN_GLOAS_DATA_COLUMN_SIDECAR = 16; @@ -527,8 +530,43 @@ export function getBlobKzgCommitmentsCountFromSignedBeaconBlockSerialized( const forkName = config.getForkName(slot); if (isForkPostGloas(forkName)) { - const signedBlock = ssz[forkName].SignedBeaconBlock.deserialize(blockBytes); - return signedBlock.message.body.signedExecutionPayloadBid.message.blobKzgCommitments.length; + // Gloas stores commitments under signedExecutionPayloadBid.message.blobKzgCommitments. + // Navigate the offset chain: SignedBeaconBlock → message → body → signedExecutionPayloadBid → message → blobKzgCommitments + const {SignedBeaconBlock: GloasSignedBlock, BeaconBlock: GloasBlock, BeaconBlockBody: GloasBody} = ssz[forkName]; + const {SignedExecutionPayloadBid, ExecutionPayloadBid} = ssz[forkName]; + const commitmentSize = ssz.deneb.KZGCommitment.fixedSize; + + const view = new DataView(blockBytes.buffer, blockBytes.byteOffset, blockBytes.byteLength); + + const signedBlockRanges = GloasSignedBlock.getFieldRanges(view, 0, blockBytes.length); + const messageIdx = Object.keys(GloasSignedBlock.fields).indexOf("message"); + const messageRange = signedBlockRanges[messageIdx]; + + const blockRanges = GloasBlock.getFieldRanges(view, messageRange.start, messageRange.end); + const bodyIdx = Object.keys(GloasBlock.fields).indexOf("body"); + const bodyRange = blockRanges[bodyIdx]; + const bodyStart = messageRange.start + bodyRange.start; + const bodyEnd = messageRange.start + bodyRange.end; + + const bodyRanges = GloasBody.getFieldRanges(view, bodyStart, bodyEnd); + const bidIdx = Object.keys(GloasBody.fields).indexOf("signedExecutionPayloadBid"); + const bidRange = bodyRanges[bidIdx]; + const bidStart = bodyStart + bidRange.start; + const bidEnd = bodyStart + bidRange.end; + + const bidRanges = SignedExecutionPayloadBid.getFieldRanges(view, bidStart, bidEnd); + const bidMsgIdx = Object.keys(SignedExecutionPayloadBid.fields).indexOf("message"); + const bidMsgRange = bidRanges[bidMsgIdx]; + const bidMsgStart = bidStart + bidMsgRange.start; + const bidMsgEnd = bidStart + bidMsgRange.end; + + const execBidRanges = ExecutionPayloadBid.getFieldRanges(view, bidMsgStart, bidMsgEnd); + const commitmentsIdx = Object.keys(ExecutionPayloadBid.fields).indexOf("blobKzgCommitments"); + const commitmentsRange = execBidRanges[commitmentsIdx]; + + const start = bidMsgStart + commitmentsRange.start; + const end = bidMsgStart + commitmentsRange.end; + return Math.round(((end > blockBytes.byteLength ? blockBytes.byteLength : end) - start) / commitmentSize); } const {SignedBeaconBlock, BeaconBlock, BeaconBlockBody, KZGCommitment} = ssz[forkName as ForkPostDeneb]; From c943b7996f0c5673cce030e9f863685e2980297f Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Thu, 26 Feb 2026 23:44:25 +0000 Subject: [PATCH 4/4] type checks --- .../beacon-node/src/api/impl/beacon/blocks/index.ts | 4 +++- .../src/chain/seenCache/seenGossipBlockInput.ts | 4 ++-- .../src/network/processor/gossipHandlers.ts | 10 ++++++++-- packages/beacon-node/src/sync/utils/downloadByRange.ts | 3 ++- packages/beacon-node/src/sync/utils/downloadByRoot.ts | 6 ++++-- packages/beacon-node/src/util/dataColumns.ts | 3 ++- packages/beacon-node/src/util/execution.ts | 5 +++-- 7 files changed, 24 insertions(+), 11 deletions(-) diff --git a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts index e79db8cfa71d..f8be33d4dcc6 100644 --- a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts @@ -28,6 +28,7 @@ import { SignedBlockContents, WithOptionalBytes, deneb, + fulu, gloas, isDenebBlockContents, isGloasDataColumnSidecar, @@ -141,7 +142,8 @@ export function getBeaconBlockApi({ blockForImport.addColumn( { blockRootHex: blockRoot, - columnSidecar: dataColumnSidecar, + // BlockInputColumns is fulu-only, safe to narrow + columnSidecar: dataColumnSidecar as fulu.DataColumnSidecar, source: BlockInputSource.api, seenTimestampSec, }, diff --git a/packages/beacon-node/src/chain/seenCache/seenGossipBlockInput.ts b/packages/beacon-node/src/chain/seenCache/seenGossipBlockInput.ts index 9215ae7d55d8..dab7237adcbc 100644 --- a/packages/beacon-node/src/chain/seenCache/seenGossipBlockInput.ts +++ b/packages/beacon-node/src/chain/seenCache/seenGossipBlockInput.ts @@ -11,7 +11,7 @@ import { isForkPostGloas, } from "@lodestar/params"; import {computeStartSlotAtEpoch} from "@lodestar/state-transition"; -import {BLSSignature, DataColumnSidecar, RootHex, SignedBeaconBlock, Slot, deneb} from "@lodestar/types"; +import {BLSSignature, RootHex, SignedBeaconBlock, Slot, deneb, fulu} from "@lodestar/types"; import {LodestarError, Logger, byteArrayEquals, pruneSetToMax} from "@lodestar/utils"; import {Metrics} from "../../metrics/metrics.js"; import {MAX_LOOK_AHEAD_EPOCHS} from "../../sync/constants.js"; @@ -308,7 +308,7 @@ export class SeenBlockInput { seenTimestampSec, source, peerIdStr, - }: SourceMeta & {blockRootHex: RootHex; columnSidecar: DataColumnSidecar}, + }: SourceMeta & {blockRootHex: RootHex; columnSidecar: fulu.DataColumnSidecar}, opts: GetByBlobOptions = {} ): BlockInputColumns { let blockInput = this.blockInputs.get(blockRootHex); diff --git a/packages/beacon-node/src/network/processor/gossipHandlers.ts b/packages/beacon-node/src/network/processor/gossipHandlers.ts index 0384b788257f..305be531ecbb 100644 --- a/packages/beacon-node/src/network/processor/gossipHandlers.ts +++ b/packages/beacon-node/src/network/processor/gossipHandlers.ts @@ -20,6 +20,7 @@ import { SubnetID, UintNum64, deneb, + fulu, isGloasDataColumnSidecar, ssz, sszTypesFor, @@ -73,7 +74,11 @@ import {validateGossipPayloadAttestationMessage} from "../../chain/validation/pa import {OpSource} from "../../chain/validatorMonitor.js"; import {Metrics} from "../../metrics/index.js"; import {kzgCommitmentToVersionedHash} from "../../util/blobs.js"; -import {getBlobKzgCommitments, getDataColumnSidecarBlockRoot, getDataColumnSidecarSlot} from "../../util/dataColumns.js"; +import { + getBlobKzgCommitments, + getDataColumnSidecarBlockRoot, + getDataColumnSidecarSlot, +} from "../../util/dataColumns.js"; import {INetworkCore} from "../core/index.js"; import {NetworkEventBus} from "../events.js"; import { @@ -341,9 +346,10 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand try { await validateGossipDataColumnSidecar(chain, dataColumnSidecar, gossipSubnet, metrics); + // TODO(gloas): handle gloas data columns separately via BlockInputNoData path const blockInput = chain.seenBlockInputCache.getByColumn({ blockRootHex, - columnSidecar: dataColumnSidecar, + columnSidecar: dataColumnSidecar as fulu.DataColumnSidecar, source: BlockInputSource.gossip, seenTimestampSec, peerIdStr, diff --git a/packages/beacon-node/src/sync/utils/downloadByRange.ts b/packages/beacon-node/src/sync/utils/downloadByRange.ts index 6795790a209f..3e7c19ff3e5d 100644 --- a/packages/beacon-node/src/sync/utils/downloadByRange.ts +++ b/packages/beacon-node/src/sync/utils/downloadByRange.ts @@ -172,9 +172,10 @@ export function cacheByRangeResponses({ } for (const columnSidecar of columnSidecars) { // will throw if root hex does not match (meaning we are following the wrong chain) + // BlockInputColumns is fulu-only, safe to narrow existing.addColumn( { - columnSidecar, + columnSidecar: columnSidecar as fulu.DataColumnSidecar, blockRootHex, seenTimestampSec, peerIdStr, diff --git a/packages/beacon-node/src/sync/utils/downloadByRoot.ts b/packages/beacon-node/src/sync/utils/downloadByRoot.ts index c3682468d730..0c077ccdd666 100644 --- a/packages/beacon-node/src/sync/utils/downloadByRoot.ts +++ b/packages/beacon-node/src/sync/utils/downloadByRoot.ts @@ -8,13 +8,14 @@ import { SignedBeaconBlock, Slot, deneb, + fulu, isGloasDataColumnSidecar, } from "@lodestar/types"; import {LodestarError, byteArrayEquals, fromHex, prettyPrintIndices, toHex, toRootHex} from "@lodestar/utils"; import {isBlockInputBlobs, isBlockInputColumns} from "../../chain/blocks/blockInput/blockInput.js"; import {BlockInputSource, IBlockInput} from "../../chain/blocks/blockInput/types.js"; import {ChainEventEmitter} from "../../chain/emitter.js"; -import {IBeaconChain} from "../../chain/interface.ts"; +import {IBeaconChain} from "../../chain/interface.js"; import {validateBlockBlobSidecars} from "../../chain/validation/blobSidecar.js"; import {validateBlockDataColumnSidecars} from "../../chain/validation/dataColumnSidecar.js"; import {INetwork} from "../../network/interface.js"; @@ -171,8 +172,9 @@ export async function downloadByRoot({ continue; } + // BlockInputColumns is fulu-only, safe to narrow blockInput.addColumn({ - columnSidecar, + columnSidecar: columnSidecar as fulu.DataColumnSidecar, blockRootHex: rootHex, seenTimestampSec: Date.now() / 1000, source: BlockInputSource.byRoot, diff --git a/packages/beacon-node/src/util/dataColumns.ts b/packages/beacon-node/src/util/dataColumns.ts index 6562ded2f608..111311822a6a 100644 --- a/packages/beacon-node/src/util/dataColumns.ts +++ b/packages/beacon-node/src/util/dataColumns.ts @@ -478,9 +478,10 @@ export async function recoverDataColumnSidecars( const sidecarsToPublish = []; for (const columnSidecar of fullSidecars) { if (!blockInput.hasColumn(columnSidecar.index)) { + // BlockInputColumns is fulu-only, safe to narrow blockInput.addColumn({ blockRootHex: blockInput.blockRootHex, - columnSidecar, + columnSidecar: columnSidecar as fulu.DataColumnSidecar, seenTimestampSec: Date.now() / 1000, source: BlockInputSource.recovery, }); diff --git a/packages/beacon-node/src/util/execution.ts b/packages/beacon-node/src/util/execution.ts index b497cf27ad64..f1d0adf2f476 100644 --- a/packages/beacon-node/src/util/execution.ts +++ b/packages/beacon-node/src/util/execution.ts @@ -2,7 +2,7 @@ import {routes} from "@lodestar/api"; import {ChainForkConfig} from "@lodestar/config"; import {ForkPostFulu, ForkPreFulu} from "@lodestar/params"; import {signedBlockToSignedHeader} from "@lodestar/state-transition"; -import {DataColumnSidecar, SignedBeaconBlock, deneb, isGloasDataColumnSidecar} from "@lodestar/types"; +import {DataColumnSidecar, SignedBeaconBlock, deneb, fulu, isGloasDataColumnSidecar} from "@lodestar/types"; import {toHex} from "@lodestar/utils"; import {isBlockInputBlobs, isBlockInputColumns} from "../chain/blocks/blockInput/blockInput.js"; import {BlockInputSource, IBlockInput} from "../chain/blocks/blockInput/types.js"; @@ -201,8 +201,9 @@ export async function getDataColumnSidecarsFromExecution( continue; } + // BlockInputColumns is fulu-only, safe to narrow blockInput.addColumn({ - columnSidecar, + columnSidecar: columnSidecar as fulu.DataColumnSidecar, blockRootHex: blockInput.blockRootHex, source: BlockInputSource.engine, seenTimestampSec,