Skip to content

fix(ssv_types): pin Gloas block proposer index in the proposer value check - #1314

Merged
shane-moore merged 1 commit into
sigp:epbsfrom
shane-moore:feat/gloas-proposer-index-pin
Sep 26, 2026
Merged

shane-moore merged 1 commit into
sigp:epbsfrom
shane-moore:feat/gloas-proposer-index-pin

Conversation

@shane-moore

Copy link
Copy Markdown
Member

Problem, Evidence, and Context (Required)

SIP-94 §4 requires the Gloas proposer value check to reject a value unless block.proposer_index == duty.ValidatorIndex. validate_block_proposal pins the version, block slot and payload root, but never reads proposer_index.

For example, if a round leader proposes a Gloas block whose proposer_index is not the duty validator, Anchor operators accept and sign it. The beacon node then rejects the signature at publication and the slot is lost with no retry. Lighthouse's proposer-index check covers only the locally produced block before QBFT, not the decided one. ssv-spec (value_check.go#L215) and go-ssv ssvlabs/ssv#2901 (713d014, protocol/v2/ssv/value_check.go:383) already enforce it.

Change Overview (Required)

  • Add BlockProposerIndexMismatch in validate_block_proposal, after the block-slot pin and before the slashing check, at Gloas slots only.
  • QBFT already runs this validator on proposals, round-change justifications and decided messages, so a mismatch now forces a round change instead of a lost slot.
  • Pre-Gloas behavior is unchanged, matching SIP-94, ssv-spec and go-ssv.

Risks, Trade-offs, and Mitigations (Required)

An honest Gloas value can mismatch only when the duty went stale after a reorg. That block cannot be published anyway, and Lighthouse already refuses it locally, so no publishable block is rejected. Applying the pin at every fork was considered and rejected: it would change live pre-Gloas value checks with no observed failure.

Validation (Required)

  • A Gloas proposer-index mismatch is rejected with BlockProposerIndexMismatch before the slashing check (tested with slashing protection off and on).
  • A pre-Gloas mismatch still validates.

Executed:

  • cargo test -p ssv_types --lib: 138 passed.
  • cargo test -p anchor_validator_store: 171 passed.
  • cargo +nightly fmt --all -- --check, make lint, git diff --check: passed.
  • Mutation check: disabling the pin fails only the Gloas test; removing the Gloas gate fails only the pre-Gloas test.

Not exercised: a QBFT-level test driving the rejection. Anchor's spec tests do not run ssv-spec's value-check vectors.

Rollback (Required for behavior or runtime changes; optional otherwise)

Revert the commit. No config, data or wire-format impact.

🤖 Generated with Claude Code

…check

SIP-94 §4 requires the proposer value check at Gloas slots to reject a
value whose block proposer_index is not the duty validator. Operators sign
the block root with the duty validator's key and the beacon node verifies
it against proposer_index, so without the pin a foreign index loses the
slot at publication instead of forcing a QBFT round change. Lighthouse's
own proposer-index check only covers the locally produced block, not the
decided one.

The pin runs after the block-slot pin and before the slashing check, and
is Gloas-only to match SIP-94, ssv-spec and go-ssv.
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.59459% with 2 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (epbs@50885d0). Learn more about missing BASE report.

Files with missing lines Patch % Lines
anchor/common/ssv_types/src/consensus.rs 94.59% 2 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             epbs    #1314   +/-   ##
=======================================
  Coverage        ?   80.73%           
=======================================
  Files           ?      179           
  Lines           ?    41053           
  Branches        ?        0           
=======================================
  Hits            ?    33146           
  Misses          ?     7907           
  Partials        ?        0           
Flag Coverage Δ
rust 80.73% <94.59%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@shane-moore

shane-moore commented Sep 26, 2026 •

Copy link
Copy Markdown
Member Author

Fresh-eyes pass at 967d735 by Claude Fable 5.1 (autonomous review, posted from my account), no findings.

Checked the pin's scope, order and comparison target against SIP-94 §4, ssv-spec ssv/value_check.go:215 (d55207b) and go-ssv protocol/v2/ssv/value_check.go:383 (713d014): all three are Gloas-only and sit between the block-slot pin and the slashing check, same as here. Honest values cannot trip it: the duty index comes from validator.index in the store, and LH block_service.rs:862 at pin 12802d0 compares the produced block against that same index and bails with a recoverable error before sign_block runs. Traced all three QBFT call sites (proposal, round-change justification, decided). Ran the two new tests on an exported head tree, both pass.

Considered and dropped: applying the pin at every fork. Low leverage, since all three references scope it to Gloas and LH's local gate already covers the honest leader pre-Gloas.

@shane-moore
shane-moore merged commit e9996e8 into sigp:epbs Sep 26, 2026
21 checks passed
@shane-moore
shane-moore deleted the feat/gloas-proposer-index-pin branch September 26, 2026 03:30
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.

2 participants