Skip to content

feat: reject same-slot FULL votes the head event already rules out (SIP-94 §2) - #1301

Merged
shane-moore merged 2 commits into
sigp:epbsfrom
shane-moore:feat/same-slot-index-check
Sep 22, 2026
Merged

shane-moore merged 2 commits into
sigp:epbsfrom
shane-moore:feat/same-slot-index-check

Conversation

@shane-moore

Copy link
Copy Markdown
Member

Problem, Evidence, and Context

SIP-94 section 2 now says an operator should reject a Gloas beacon vote with AttestationDataIndex = 1 when its own view already establishes that the block at BlockRoot has slot duty.slot, evaluated against knowledge fixed at instance start. A block proposed in the current slot has no payload yet, so a FULL vote on it is invalid and the beacon node gossip-rejects every attestation the committee signs over it. SSV Labs QA measured exactly that on ssv-mini with a leader flipping the index (SIPs#94 comment); go-ssv tracks its side as ssvlabs/ssv#3035. Anchor today range-checks the index to {0, 1} and nothing more.

Change Overview

  • The metadata service already learns the needed fact: when a head event for the current slot triggers the voting context, that event names a block whose slot is the current slot, and a reorg never changes a block's slot. VotingContext gains same_slot_head_root, set from that event only when the event's slot equals the slot the context is built for (the two clock reads can straddle a boundary). Timer-triggered contexts carry None.
  • The Gloas vote validator takes the root at construction, which happens once per committee instance, so proposals, round-change justifications, and decided messages in that instance evaluate the same knowledge. After the existing range check, index 1 on that root is rejected with a new SameSlotFullIndex error; every other vote is unchanged.
  • No new task, channel, lock, or cache. With --disable-beacon-head-monitor the root is always None and behavior is unchanged. Pre-Gloas validation is untouched.

Risks, Trade-offs, and Mitigations

Coverage is a deliberately partial snapshot: a second same-slot head arriving after the first event, or a head event landing after the attestation-due timer, is unknown and the check is skipped, which the SIP permits. A rejection has no false positive, since an honest beacon node never yields index 1 for a same-slot block; it returns from receive and the shared committee instance (attestation and sync signing) waits for the round timer or another valid proposal, which is existing behavior for any value-check rejection. Reusing the block-header slot that weighted attestation data fetches was considered and left out: it is opt-in and only helps the timer path.

Validation

  • cargo test -p ssv_types --lib -- gloas: 30 passed, including four new cases (reject on the known root, index 0 accepted, other block accepted, no knowledge accepted).
  • cargo test -p anchor_validator_store --lib: 174 passed, including three cases for the slot guard and one test through the production constructor path, because the committee tests' mock decider discards the validator.
  • cargo fmt --check, cargo clippy -p ssv_types -p anchor_validator_store --tests -- -D warnings, git diff --check: clean.

Rollback

Revert this PR. The field is in memory; no configuration or database change.

🤖 Generated with Claude Code

https://claude.ai/code/session_019jwWeHCBBac5Ma7Vrfonaw

…IP-94 §2)

A Gloas block proposed in the current slot has no payload yet, so an
attestation with index 1 (FULL) on it is invalid and the beacon node
gossip-rejects every attestation the committee signs over it. SSV Labs QA
measured this on ssv-mini with a leader flipping the index.

The metadata service already learns the fact needed to catch it: when a
head event for the current slot triggers the voting context, that event
names a block whose slot is the current slot, and a reorg never changes
a block's slot. Record that root on VotingContext (only when the event's
slot matches the slot the context is built for, since the two clock
reads can straddle a boundary) and hand it to the Gloas vote validator
at instance start, so proposals, round-change justifications and
decided messages in that instance all evaluate the same knowledge.
Index 1 on that root is rejected; everything else is unchanged, and a
timer-triggered context or a disabled head monitor carries no root and
skips the check.
@codecov-commenter

codecov-commenter commented Sep 10, 2026

Copy link
Copy Markdown

Codecov Report

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

Files with missing lines Patch % Lines
anchor/validator_store/src/metadata_service.rs 92.30% 2 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             epbs    #1301   +/-   ##
=======================================
  Coverage        ?   79.96%           
=======================================
  Files           ?      179           
  Lines           ?    40081           
  Branches        ?        0           
=======================================
  Hits            ?    32052           
  Misses          ?     8029           
  Partials        ?        0           
Flag Coverage Δ
rust 79.96% <98.76%> (?)

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 left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Reviewed bbe6fc46. No correctness or design findings. The same-slot check uses a fixed instance snapshot and preserves the permitted behavior for unknown roots.

Formatting is fixed. CI passed all 965 tests in both debug and release, including the eight new cases; formatting and Clippy also passed. Still running: coverage, local-testnet.

Reviewed by gpt-6-astra xhigh.

@shane-moore
shane-moore merged commit cc5c414 into sigp:epbs Sep 22, 2026
20 checks passed
@shane-moore
shane-moore deleted the feat/same-slot-index-check branch September 22, 2026 01:12
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