You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Implement the SIP-94 §5/§7 receive-side guarantee for proposer-assignment validation without making proposer-duty freshness depend on Anchor's SSV validator snapshot.
Anchor already downloads the beacon node's complete proposer schedule. Retain that complete schedule for the current and next epoch, then validate Role::ProposerPreferences (8) assignments by the validator pubkey carried in the message ID. Return NoDuty only when a fetched, complete epoch view proves that the validator is not assigned at proposal_slot; an unfetched epoch remains unknown and is tolerated.
Expose the same pubkey-based assignment query for the §6 RoleEnvelopeProposer (9) arm in milestone 8.
Context / motivation
SIP-94 was updated for this in 0a8de9e, following iurii-ssv's devnet finding. A §5 preference partial is a deterministic one-shot gossip broadcast. Anchor derives the gossipsub message ID from the full message bytes and retains duplicate IDs for an epoch, so an identical rebroadcast cannot recover from an initial wrong drop.
The observed failure is:
A receiver fetches proposer duties before a validator registration wave.
The beacon node returns the complete epoch schedule, but Anchor filters it against NetworkState::validator_indices() at fetch time (duties_tracker/src/duties_tracker.rs:205-257).
A newly registered validator is therefore absent from the stored vector even though its beacon proposer assignment already existed.
The stale view is created by Anchor's local filtering, not by the beacon response. An epoch contains one proposer assignment per slot, so retaining the full response is small and makes the assignment view independent of SSV validator membership and index-update timing.
Pubkey lookup is also load-bearing. A newly registered validator can exist in NetworkState before its beacon index is populated: get_committee_info_by_validator_pk then returns committee membership with an empty validator_indices vector (database/src/state.rs:374-389). Role 8 must not turn that temporary index gap into UnexpectedFailure / REJECT when the complete proposer view can validate the assignment directly by pubkey.
Suggested approach
1. Retain complete current and next epoch proposer views (duties_tracker)
Refactor poll_beacon_proposers to fetch both current_epoch and current_epoch + 1, matching the role-8 proposer lookahead.
For each epoch:
call the existing proposer-duties endpoint;
store response.data unchanged, without filtering against network_state_rx or validator_indices();
preserve each successful epoch independently if the other request fails;
keep the existing historical pruning.
No database generation counter or validator-set invalidation protocol is needed: SSV registration changes no longer change the contents of a cached proposer view.
2. Add one atomic pubkey-based assignment query (DutiesProvider)
Keep the existing is_epoch_known_for_proposers and index-based is_validator_proposer_at_slot methods for the existing Role::Proposer path. Add:
return ValidationFailure::NoDuty only for Some(false);
accept Some(true) and tolerate None;
do not require committee_info.validator_indices.first() for this role.
The validator is still required to exist in Anchor's registry and the signer is still required to belong to its committee through the earlier message-validation path. Only the proposer-assignment lookup stops depending on the locally resolved validator index.
Acceptance criteria
Proposer duties returned by the beacon node are retained even when their validator pubkeys or indices are absent from NetworkState.
Successful current and next epoch fetches are both stored; failure for one epoch does not discard a successful fetch for the other.
proposer_assignment_at_slot returns:
Some(true) for the assigned pubkey and slot;
Some(false) for an unassigned pubkey in a fetched epoch;
None for an unfetched epoch.
A role-8 message for an assigned, registered validator is accepted even when its local validator index is still unresolved.
A role-8 message against a fetched view where the pubkey is not assigned at proposal_slot returns NoDuty (IGNORE).
A role-8 message for an unfetched epoch is accepted (existing unknown-view tolerance).
Existing Role::Proposer (6), Role::SyncCommittee, and database behavior are unchanged.
The future RoleEnvelopeProposer (9) assignment arm can reuse proposer_assignment_at_slot without adding validator-set freshness state.
Tests
Use the tester-subagent before authoring or modifying tests.
duties_tracker:
stores the complete proposer response when NetworkState::validator_indices() is empty;
stores current and next epoch views;
preserves one successful epoch when the other fetch fails;
covers the Some(true) / Some(false) / None assignment states.
The emitter-side validator-added trigger and recommended registration grace are tracked by correcting feat(client): spawn LH ProposerPreferencesService with AnchorValidatorStore backend #1064 separately. The current pinned Lighthouse service schedules once per (epoch, dependent_root), so a newly discovered validator under an unchanged root is not emitted without an upstream fix.
Out of scope: duty-view invalidation during a dependent_root reorg, the existing Role::Proposer (6) behavior, and the future role-9 validation arm itself.
Issues are directionally correct, not prescriptive; verify symbols at PR time.
Goal
Implement the SIP-94 §5/§7 receive-side guarantee for proposer-assignment validation without making proposer-duty freshness depend on Anchor's SSV validator snapshot.
Anchor already downloads the beacon node's complete proposer schedule. Retain that complete schedule for the current and next epoch, then validate
Role::ProposerPreferences(8) assignments by the validator pubkey carried in the message ID. ReturnNoDutyonly when a fetched, complete epoch view proves that the validator is not assigned atproposal_slot; an unfetched epoch remains unknown and is tolerated.Expose the same pubkey-based assignment query for the §6
RoleEnvelopeProposer(9) arm in milestone 8.Context / motivation
SIP-94 was updated for this in
0a8de9e, following iurii-ssv's devnet finding. A §5 preference partial is a deterministic one-shot gossip broadcast. Anchor derives the gossipsub message ID from the full message bytes and retains duplicate IDs for an epoch, so an identical rebroadcast cannot recover from an initial wrong drop.The observed failure is:
NetworkState::validator_indices()at fetch time (duties_tracker/src/duties_tracker.rs:205-257).is_epoch_known_for_proposersstill returnstrue.ValidationFailure::NoDuty->MessageAcceptance::Ignore, permanently losing the honest partial.The stale view is created by Anchor's local filtering, not by the beacon response. An epoch contains one proposer assignment per slot, so retaining the full response is small and makes the assignment view independent of SSV validator membership and index-update timing.
Pubkey lookup is also load-bearing. A newly registered validator can exist in
NetworkStatebefore its beacon index is populated:get_committee_info_by_validator_pkthen returns committee membership with an emptyvalidator_indicesvector (database/src/state.rs:374-389). Role 8 must not turn that temporary index gap intoUnexpectedFailure/ REJECT when the complete proposer view can validate the assignment directly by pubkey.Suggested approach
1. Retain complete current and next epoch proposer views (
duties_tracker)Refactor
poll_beacon_proposersto fetch bothcurrent_epochandcurrent_epoch + 1, matching the role-8 proposer lookahead.For each epoch:
response.dataunchanged, without filtering againstnetwork_state_rxorvalidator_indices();No
databasegeneration counter or validator-set invalidation protocol is needed: SSV registration changes no longer change the contents of a cached proposer view.2. Add one atomic pubkey-based assignment query (
DutiesProvider)Keep the existing
is_epoch_known_for_proposersand index-basedis_validator_proposer_at_slotmethods for the existingRole::Proposerpath. Add:Semantics:
Some(true): the epoch is fetched and the complete view assigns this pubkey atslot;Some(false): the epoch is fetched and the complete view proves this pubkey is not assigned atslot;None: the epoch is not fetched, so assignment is unknown and must be tolerated.Compute the result under one proposer-map read. Do not split freshness and assignment into separate predicate calls.
3. Consume the tri-state result in the role-8 arm (
message_validator)In the
Role::ProposerPreferencesarm introduced by #1062:DutyExecutor::Validatorin the message ID;proposer_assignment_at_slot(slot, &validator_pubkey);ValidationFailure::NoDutyonly forSome(false);Some(true)and tolerateNone;committee_info.validator_indices.first()for this role.The validator is still required to exist in Anchor's registry and the signer is still required to belong to its committee through the earlier message-validation path. Only the proposer-assignment lookup stops depending on the locally resolved validator index.
Acceptance criteria
NetworkState.proposer_assignment_at_slotreturns:Some(true)for the assigned pubkey and slot;Some(false)for an unassigned pubkey in a fetched epoch;Nonefor an unfetched epoch.proposal_slotreturnsNoDuty(IGNORE).Role::Proposer(6),Role::SyncCommittee, and database behavior are unchanged.RoleEnvelopeProposer(9) assignment arm can reuseproposer_assignment_at_slotwithout adding validator-set freshness state.Tests
Use the tester-subagent before authoring or modifying tests.
duties_tracker:NetworkState::validator_indices()is empty;Some(true)/Some(false)/Noneassignment states.message_validator(extend feat: add Role::ProposerPreferences with message-validator fork-gate #1062's role-8 harness):NoDuty;Verification:
make cargo-fmt && make lint, thencargo test -p duties_tracker -p message_validator.Notes
(epoch, dependent_root), so a newly discovered validator under an unchanged root is not emitted without an upstream fix.dependent_rootreorg, the existingRole::Proposer(6) behavior, and the future role-9 validation arm itself.Issues are directionally correct, not prescriptive; verify symbols at PR time.