test: data column sidecar gossip validation spec tests#9430
Conversation
Adds runner support for the fulu `gossip_data_column_sidecar` networking spec tests from consensus-specs PR ChainSafe#5246. Mirrors the existing `gossip_blob_sidecar` arm: SSZ-decodes `fulu.DataColumnSidecar` and invokes `validateGossipFuluDataColumnSidecar` with the fixture's `subnet_id`. Builds on top of ChainSafe#9372. Does not address `gossip_partial_data_column_sidecar` (optional feature) nor the open `gossip_beacon_block__valid_at_blob_parameters_limit` zero-parent harness gap. 🤖 Generated with AI assistance
…a column sidecars Implements spec checks 9 and 12 from `validate_data_column_sidecar_gossip` that the harness previously hit as gaps: - 9) REJECT a sidecar whose parent block does not descend from the current finalized checkpoint. Mirrors the blob sidecar pattern using `forkChoice.getAncestor(parentRoot, finalizedSlot)`. - 12) IGNORE a subsequent sidecar matching an already-seen `(slot, proposer_index, column_index)` tuple, with `mark` after all signature, inclusion-proof, and KZG checks pass. Tracking lives on `SeenBlockInput` alongside the existing blob-tuple cache so the dedup applies at the validator entry point (not just in `gossipHandlers`). Adds `FINALIZED_NOT_ANCESTOR` and `ALREADY_SEEN_TUPLE` error codes. Removes a stale "Handled by 7)" placeholder comment. spec-minimal `gossip_data_column_sidecar`: 17/17 pass. spec-mainnet `gossip_data_column_sidecar`: 17/17 pass. spec-minimal `gossip_blob_sidecar`: 32/32 still pass (no regression). beacon-node unit `dataColumnSidecar.test.ts`: 19/19 pass. 🤖 Generated with AI assistance
There was a problem hiding this comment.
Code Review
This pull request implements gossip validation rules 9 and 12 for Fulu data column sidecars, including tracking seen sidecar tuples and verifying that the finalized checkpoint is an ancestor of the sidecar's block. It also integrates these validations into the spec tests. Feedback points out a critical issue in the ancestor check where calling chain.forkChoice.getAncestor with a slot higher than the parent block's slot throws an error, causing valid sidecars to be incorrectly rejected. A fix is suggested to handle cases where the parent block's slot is less than or equal to the finalized ancestor slot.
| let ancestorRoot: RootHex | undefined; | ||
| try { | ||
| ancestorRoot = chain.forkChoice.getAncestor(parentRoot, finalizedAncestorSlot).blockRoot; | ||
| } catch { | ||
| // parent not in fork-choice or finalized slot before parent's chain start | ||
| } |
There was a problem hiding this comment.
If parentBlock.slot is less than or equal to finalizedAncestorSlot (which can happen when there are skipped slots or when the parent block is before the finalized slot), calling chain.forkChoice.getAncestor with a slot higher than the block's slot will throw an error. This causes ancestorRoot to remain undefined, leading to an incorrect FINALIZED_NOT_ANCESTOR rejection for a valid sidecar.
According to the consensus spec's get_ancestor logic, if the block's slot is less than or equal to the requested ancestor slot, the ancestor is the block itself (parentRoot). We should handle this case explicitly to avoid throwing an error and incorrectly rejecting valid gossip messages.
let ancestorRoot: RootHex | undefined;
if (parentBlock.slot <= finalizedAncestorSlot) {
ancestorRoot = parentRoot;
} else {
try {
ancestorRoot = chain.forkChoice.getAncestor(parentRoot, finalizedAncestorSlot).blockRoot;
} catch {
// parent not in fork-choice or finalized slot before parent's chain start
}
}There was a problem hiding this comment.
Thanks for the careful read, but I think the premise here is incorrect.
getAncestor in packages/fork-choice/src/protoArray/protoArray.ts:1523-1541 follows the consensus-specs get_ancestor algorithm:
if (block.slot <= ancestorSlot) {
return block;
}It only throws in two cases:
MISSING_PROTO_ARRAY_BLOCKifblockRootisn't in the proto-array. We already confirmedparentBlockexists viachain.forkChoice.getBlockHexDefaultStatus(parentRoot)just above (line 79-96), so this can't fire.UNKNOWN_ANCESTORif walking up hits a missing parent — but that requiresblock.slot > ancestorSlot, i.e. it's not the boundary case being described.
For parentBlock.slot <= finalizedAncestorSlot, getAncestor returns parentBlock directly, so ancestorRoot = parentRoot. The subsequent ancestorRoot !== finalizedCheckpoint.rootHex check then resolves correctly:
- Skipped slots at the checkpoint boundary (finalized root anchored at slot < finalizedAncestorSlot, parentRoot equals that root):
ancestorRoot = parentRoot = finalizedRoot→ PASS ✅ - Parent on a different fork at/before the finalized slot:
ancestorRoot = parentRoot ≠ finalizedRoot→ REJECT ✅ (correct rejection — that parent isn't on the finalized fork)
This pattern directly mirrors blobSidecar.ts:144-159, which has been live for some time without producing false rejections. The proposed fix is functionally equivalent (it just inlines the early-return that getAncestor already does internally).
Spec reference: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/fork-choice.md#get_ancestor
No change needed here.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3435d9b7a6
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| } | ||
|
|
||
| case GossipType.data_column_sidecar: { | ||
| const dataColumnSidecar = rejectOnInvalidSerializedBytes(() => ssz.fulu.DataColumnSidecar.deserialize(bytes)); |
There was a problem hiding this comment.
Use the fork SSZ type for data-column gossip tests
When these networking tests run for gloas/gossip_data_column_sidecar, this always deserializes the message as the Fulu container, which still expects signedBlockHeader, kzgCommitments, and an inclusion proof. Gloas data-column sidecars instead contain slot and beaconBlockRoot, so valid Gloas fixtures will be mapped to SPEC_INVALID_SERIALIZED_BYTES and rejected before the Gloas validation path can be exercised; the harness should deserialize with sszTypesFor(fork).DataColumnSidecar and dispatch to the appropriate validator.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
You're right that the Gloas DataColumnSidecar differs from Fulu — packages/types/src/gloas/sszTypes.ts:301-313 drops signedBlockHeader / kzgCommitments / kzgCommitmentsInclusionProof and adds slot + beaconBlockRoot. Deserializing Gloas bytes through the Fulu container would indeed reject as SPEC_INVALID_SERIALIZED_BYTES.
Scope-deferring this for two reasons:
-
No Gloas DCS gossip fixtures yet. Both consensus-specs PRs (Track sync committee metrics #5146, Add LC header validations #5246) and the current test corpus only include
gossip_data_column_sidecarunder Fulu:packages/beacon-node/spec-tests/tests/{minimal,mainnet}/fulu/networking/gossip_data_column_sidecarThe Gloas
networking/folder only has slashings, BLS-to-execution, sync committee — no DCS fixtures. Adding the dispatch now would be untested code that the runner never reaches. -
Different validator signature.
validateGossipGloasDataColumnSidecar(chain, payloadInput, sidecar, …)takes aPayloadEnvelopeInput, which isn't present in the standalone-DCS gossip fixture format. Once upstream defines the Gloas DCS fixture layout (likely coupled with thegossip_execution_payload_envelopeflow), the harness can construct the right input shape and dispatch viasszTypesFor(fork).DataColumnSidecar.
Tracking as a follow-up — I'll wire the per-fork dispatch when Gloas DCS gossip fixtures land in consensus-specs.
Motivation
Stacks on top of #9372 to extend Lodestar's gossip-validation spec-test coverage from
data_column_sidecar(PeerDAS / Fulu+).Description
Two follow-up commits on
nflaig/deneb-gossip-spec-tests:feat(test): wire gossip_data_column_sidecar into spec test harness— adds harness support for thedata_column_sidecartopic in the gossip validation spec tests (consensus-specs PR Add executable gossip validation functions for deneb ethereum/consensus-specs#5146 / Add LC header validations #5246).feat(beacon-node): validate finalized-ancestor and seen-tuple for data column sidecars— fills in the source-side gaps the harness surfaced: finalized-ancestor check on the sidecar's block root and the(slot, proposer_index, column_index)seen-tuple check, matching the spec's validation conditions.Spec test results
Full networking suite passes on both presets against
nflaig/deneb-gossip-spec-testsbase:logs/gossip-spec-rerun-minimal-2026-05-30-v2.log)logs/gossip-spec-rerun-mainnet-2026-05-30.log)Notes
nflaig/deneb-gossip-spec-tests(PR test: deneb and electra gossip validation spec tests #9372). Will rebase ontounstableonce test: deneb and electra gossip validation spec tests #9372 lands.data_column_sidecar.pnpm lintclean.🤖 Generated with AI assistance