diff --git a/packages/beacon-node/src/network/processor/extractSlotRootFns.ts b/packages/beacon-node/src/network/processor/extractSlotRootFns.ts index 9ebc959e07e6..13a5ec1e3d65 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, ForkSeq} from "@lodestar/params"; import {SlotOptionalRoot, SlotRootHex} from "@lodestar/types"; import { getBeaconBlockRootFromDataColumnSidecarSerialized, + getBeaconBlockRootFromFuluDataColumnSidecarSerialized, getBlockRootFromBeaconAttestationSerialized, getBlockRootFromPayloadAttestationMessageSerialized, getBlockRootFromSignedAggregateAndProofSerialized, @@ -65,7 +66,8 @@ export function createExtractBlockSlotRootFns(): ExtractSlotRootFns { } if (ForkSeq[fork] < ForkSeq.gloas) { - return {slot}; + const root = getBeaconBlockRootFromFuluDataColumnSidecarSerialized(data); + return root !== null ? {slot, root} : {slot}; } const root = getBeaconBlockRootFromDataColumnSidecarSerialized(data); diff --git a/packages/beacon-node/src/network/processor/index.ts b/packages/beacon-node/src/network/processor/index.ts index 8e51e35d95d2..8209ab8a4d46 100644 --- a/packages/beacon-node/src/network/processor/index.ts +++ b/packages/beacon-node/src/network/processor/index.ts @@ -390,6 +390,12 @@ export class NetworkProcessor { preprocessResult = {action: PreprocessAction.PushToQueue}; } + if (topicType === GossipType.data_column_sidecar && ForkSeq[fork] < ForkSeq.gloas) { + // Fulu sidecar validation only needs the parent block/state, not the block referenced by the sidecar itself. + // Trigger unknown block sync above, but continue validation without waiting for the full block. + preprocessResult = {action: PreprocessAction.PushToQueue}; + } + if (ForkSeq[fork] >= ForkSeq.gloas) { // specific check for each topic // note that it's supposed to NOT queue beacon_block (handled above) and execution_payload because it's not a one-off; diff --git a/packages/beacon-node/src/util/sszBytes.ts b/packages/beacon-node/src/util/sszBytes.ts index 725d2fa6954c..2283fee7aa29 100644 --- a/packages/beacon-node/src/util/sszBytes.ts +++ b/packages/beacon-node/src/util/sszBytes.ts @@ -524,6 +524,7 @@ export function getSlotFromBlobSidecarSerialized(data: Uint8Array): Slot | null const SLOT_BYTES_POSITION_IN_SIGNED_DATA_COLUMN_SIDECAR_PRE_GLOAS = 20; const SLOT_BYTES_POSITION_IN_SIGNED_DATA_COLUMN_SIDECAR_POST_GLOAS = 16; const BEACON_BLOCK_ROOT_POSITION_IN_GLOAS_DATA_COLUMN_SIDECAR = 24; +const BEACON_BLOCK_HEADER_SIZE = SLOT_SIZE + 8 + ROOT_SIZE + ROOT_SIZE + ROOT_SIZE; export function getSlotFromDataColumnSidecarSerialized(data: Uint8Array, fork: ForkName): Slot | null { const offset = isForkPostGloas(fork) @@ -551,6 +552,26 @@ export function getBeaconBlockRootFromDataColumnSidecarSerialized(data: Uint8Arr return "0x" + blockRootBuf.toString("hex"); } +export function getBeaconBlockRootFromFuluDataColumnSidecarSerialized(data: Uint8Array): RootHex | null { + if (data.length < SLOT_BYTES_POSITION_IN_SIGNED_DATA_COLUMN_SIDECAR_PRE_GLOAS + BEACON_BLOCK_HEADER_SIZE) { + return null; + } + + try { + const blockHeader = ssz.phase0.BeaconBlockHeader.deserialize( + data.subarray( + SLOT_BYTES_POSITION_IN_SIGNED_DATA_COLUMN_SIDECAR_PRE_GLOAS, + SLOT_BYTES_POSITION_IN_SIGNED_DATA_COLUMN_SIDECAR_PRE_GLOAS + BEACON_BLOCK_HEADER_SIZE + ) + ); + const blockRoot = ssz.phase0.BeaconBlockHeader.hashTreeRoot(blockHeader); + blockRootBuf.set(blockRoot); + return `0x${blockRootBuf.toString("hex")}`; + } catch { + return null; + } +} + /** * SignedExecutionPayloadEnvelope SSZ Layout: * ├─ 4 bytes: message offset (points to byte 100) diff --git a/packages/beacon-node/test/unit/util/sszBytes.test.ts b/packages/beacon-node/test/unit/util/sszBytes.test.ts index b58562abd8cd..1aa80dd0e6aa 100644 --- a/packages/beacon-node/test/unit/util/sszBytes.test.ts +++ b/packages/beacon-node/test/unit/util/sszBytes.test.ts @@ -1,4 +1,4 @@ -import {describe, expect, it} from "vitest"; +import {describe, expect, it, vi} from "vitest"; import {BitArray} from "@chainsafe/ssz"; import {createChainForkConfig} from "@lodestar/config"; import {ForkName, MAX_COMMITTEES_PER_SLOT} from "@lodestar/params"; @@ -29,6 +29,7 @@ import { getAttesterIndexFromSingleAttestationSerialized, getBeaconBlockRootFromDataColumnSidecarSerialized, getBeaconBlockRootFromExecutionPayloadEnvelopeSerialized, + getBeaconBlockRootFromFuluDataColumnSidecarSerialized, getBlobKzgCommitmentsCountFromSignedBeaconBlockSerialized, getBlockRootFromAttestationSerialized, getBlockRootFromPayloadAttestationMessageSerialized, @@ -57,6 +58,7 @@ import { getSlotFromSignedExecutionPayloadBidSerialized, getSlotFromSingleAttestationSerialized, } from "../../../src/util/sszBytes.js"; +import {generateBlockWithColumnSidecars} from "../../utils/blocksAndData.js"; import {generateRandomBlob} from "../../utils/kzg.js"; describe("SinlgeAttestation SSZ serialized picking", () => { @@ -611,6 +613,13 @@ describe("DataColumnSidecar SSZ serialized picking (fork-aware)", () => { }); } + it("getBeaconBlockRootFromFuluDataColumnSidecarSerialized", () => { + const {columnSidecars, rootHex} = generateBlockWithColumnSidecars({forkName: ForkName.fulu}); + const bytes = ssz.fulu.DataColumnSidecar.serialize(columnSidecars[0]); + + expect(getBeaconBlockRootFromFuluDataColumnSidecarSerialized(bytes)).toBe(rootHex); + }); + it("getSlotFromDataColumnSidecarSerialized - invalid data", () => { // Slot is at offset 20 for pre-Gloas, need at least 28 bytes const invalidSizes = [0, 10, 27]; @@ -618,6 +627,25 @@ describe("DataColumnSidecar SSZ serialized picking (fork-aware)", () => { expect(getSlotFromDataColumnSidecarSerialized(Buffer.alloc(size), ForkName.fulu)).toBeNull(); } }); + + it("getBeaconBlockRootFromFuluDataColumnSidecarSerialized - invalid data", () => { + const invalidSizes = [0, 20, 131]; + for (const size of invalidSizes) { + expect(getBeaconBlockRootFromFuluDataColumnSidecarSerialized(Buffer.alloc(size))).toBeNull(); + } + }); + + it("getBeaconBlockRootFromFuluDataColumnSidecarSerialized - returns null if header parsing throws", () => { + const {columnSidecars} = generateBlockWithColumnSidecars({forkName: ForkName.fulu}); + const bytes = ssz.fulu.DataColumnSidecar.serialize(columnSidecars[0]); + const deserializeSpy = vi.spyOn(ssz.phase0.BeaconBlockHeader, "deserialize").mockImplementation(() => { + throw new Error(); + }); + + expect(getBeaconBlockRootFromFuluDataColumnSidecarSerialized(bytes)).toBeNull(); + + deserializeSpy.mockRestore(); + }); }); describe("Gloas", () => {