Skip to content

fix: ignore repeat proposal during block publishing#9235

Open
lodekeeper wants to merge 2 commits intoChainSafe:unstablefrom
lodekeeper:fix/block-repeat-proposal-graceful-handling
Open

fix: ignore repeat proposal during block publishing#9235
lodekeeper wants to merge 2 commits intoChainSafe:unstablefrom
lodekeeper:fix/block-repeat-proposal-graceful-handling

Conversation

@lodekeeper
Copy link
Copy Markdown
Contributor

Motivation

Fixes #9228.

In multi-node setups (e.g. DVT clusters or fallback configurations), the same block is often submitted to multiple beacon nodes. When one node receives the block via gossip slightly before the POST eth/v2/beacon/blocks REST call, the gossip validation raises BLOCK_ERROR_REPEAT_PROPOSAL from the seenBlockProposers check, which currently propagates as an unhandled exception and returns HTTP 500 to the caller. Nothing is actually wrong with the submitted block.

Description

Extend the existing ALREADY_KNOWN handler in publishBlock to also swallow REPEAT_PROPOSAL. Both are thrown as BlockGossipError with GossipAction.IGNORE, so they share the same remediation — log at debug and return silently so the API caller sees a successful response.

This mirrors the handler added in #6457 for ALREADY_KNOWN.

Only the gossip broadcast-validation path is affected; consensus / consensusAndEquivocation paths use verifyBlocksInEpoch and do not go through validateGossipBlock, so they cannot hit REPEAT_PROPOSAL.

No new tests added — the existing handler for ALREADY_KNOWN was introduced without tests (#6457), and the underlying validation error is already tested at the gossip layer in test/unit/chain/validation/block.test.ts.

Extend the existing ALREADY_KNOWN handler in publishBlock to also swallow
REPEAT_PROPOSAL. In multi-node setups (DVT clusters, fallback configs) the
same block can reach a node via gossip before the API, causing the seen
block proposer check to throw BLOCK_ERROR_REPEAT_PROPOSAL. Both errors use
GossipAction.IGNORE semantics and should return a silent 2xx rather than a
500 response.

Fixes ChainSafe#9228

🤖 Generated with AI assistance
@lodekeeper lodekeeper requested a review from a team as a code owner April 18, 2026 12:20
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the beacon block API to ignore REPEAT_PROPOSAL errors in addition to ALREADY_KNOWN errors when publishing blocks. A review comment suggests refining the debug log message and including the specific error code in the metadata to more accurately reflect the reason for ignoring the block and improve observability.

error instanceof BlockGossipError &&
(error.type.code === BlockErrorCode.ALREADY_KNOWN || error.type.code === BlockErrorCode.REPEAT_PROPOSAL)
) {
chain.logger.debug("Ignoring known block during publishing", valLogMeta);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The log message "Ignoring known block" is slightly inaccurate when the error is REPEAT_PROPOSAL (which indicates a block for the same slot/proposer was seen, but not necessarily the same block root). Including the actual error code in the log metadata would improve observability and help distinguish between these two cases during debugging.

Suggested change
chain.logger.debug("Ignoring known block during publishing", valLogMeta);
chain.logger.debug("Ignoring block during publishing", {...valLogMeta, reason: error.type.code});

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — applied in b68ea2b. The log is now "Ignoring block during publishing" with reason: error.type.code in metadata, so ALREADY_KNOWN vs REPEAT_PROPOSAL is distinguishable.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 04573899a8

ℹ️ 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".

Comment on lines +196 to +197
(error.type.code === BlockErrorCode.ALREADY_KNOWN || error.type.code === BlockErrorCode.REPEAT_PROPOSAL)
) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Don't swallow equivocation under REPEAT_PROPOSAL

REPEAT_PROPOSAL is raised for any second proposal by the same proposer/slot, not just benign duplicates of an already-known root. By returning success for this code path, publishBlock() now silently drops blocks that may be a different root (equivocation or failover mismatch) before the publish/import section runs, so the caller receives a false-positive success even though this block was neither broadcast nor processed. Consider only suppressing when the same block root is already known/in-flight, and keep a surfaced error for non-identical repeat proposals.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point — worth discussing the tradeoff.

REPEAT_PROPOSAL does fire purely on (slot, proposerIndex) (seenBlockProposers.ts tracks no root), so the suppression is semantically wider than ALREADY_KNOWN. The cases it collapses:

  1. Same root, race window. API publish arrives after gossip validation has run for the same block (adds to seenBlockProposers at block.ts:248) but before fork-choice import finishes. ALREADY_KNOWN at block.ts:59 returns null; REPEAT_PROPOSAL at block.ts:68 fires. This is the case issue POST eth/v2/beacon/blocks returning BLOCK_ERROR_REPEAT_PROPOSAL in multi-node setup #9228 describes.

  2. Distinct root (equivocation / failover mismatch / VC bug). Same (slot, proposer), different body.

In both cases the block is rejected by gossip validation before the publishBlock/import section — so nothing is broadcast or imported either way. The only observable difference for the caller is 500 vs 2xx.

Reliably distinguishing (1) from (2) would need SeenBlockProposers to track seen roots per (slot, proposer), which is a structural cache change beyond the scope of this targeted fix. The reason: error.type.code metadata now in the log (commit b68ea2b, per @gemini-code-assist suggestion) does surface the REPEAT_PROPOSAL case to operators so case (2) is still observable.

Happy to tighten if @nflaig prefers — options I see:

  • (a) keep current behavior and rely on the reason log field for observability (current PR),
  • (b) extend SeenBlockProposers with a seenRootsByProposer: Map<Slot, Map<ValidatorIndex, Set<RootHex>>> and only suppress when seenRootsByProposer.get(slot)?.get(proposerIndex)?.has(blockRoot) is true, surfacing otherwise,
  • (c) keep current behavior but add a warn-level log for the REPEAT_PROPOSAL branch to make distinct-root re-submissions stand out in logs.

The previous "Ignoring known block" log was misleading for REPEAT_PROPOSAL,
which covers a different-root block from the same slot/proposer. Include
the error code in metadata so ALREADY_KNOWN vs REPEAT_PROPOSAL is
distinguishable in logs.

🤖 Generated with AI assistance

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

POST eth/v2/beacon/blocks returning BLOCK_ERROR_REPEAT_PROPOSAL in multi-node setup

1 participant