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 sign_proposer_preferences LH trait method on AnchorValidatorStore, filling the existing TODO(gloas) stub (anchor/validator_store/src/lib.rs:3367-3374), as a SingleValidator-mode partial-signature collection (no QBFT): sign the supplied ProposerPreferences under DOMAIN_PROPOSER_PREFERENCES with the validator's BLS share and reconstruct via the signature collector. The trait signature is (validator_pubkey: PublicKeyBytes, preferences: ProposerPreferences) -> SignedProposerPreferences (validator_client/validator_store/src/lib.rs:217 at LH pin 81d576943, PR #1125's pin; re-verify the live pin in Cargo.toml before trusting line refs); the preferences arrive fully built from the LH ProposerPreferencesService, so no duty or config derivation happens in this method. Mirrors sign_validator_registration_data / sign_voluntary_exit in shape, and LH's own implementation (lighthouse_validator_store/src/lib.rs:1490 at the pin) for the domain and signing flow.
Context
Per SIP-94 §5, each operator independently derives ProposerPreferences { dependent_root, proposal_slot, validator_index, fee_recipient, target_gas_limit } and signs it; reconstruction succeeds only when 2f + 1 operators converge on one signing root. No QBFT: target_gas_limit (operator config) and dependent_root (per-operator BN observation) determine each operator's root locally.
Collection slot = proposal_slot (SIP-94 §5/§7; supersedes the earlier slot_clock.now() design in this issue's history). The slot passed to collect_signature becomes the PartialSignatureMessages.slot stamped on the wire (SignatureMetadata built at lib.rs:461 with its slot field at :469; wire stamp slot: metadata.slot at signature_collector/src/lib.rs:312-316), and SIP-94 §5 pins that slot to proposal_slot itself: one runner per proposal slot, matching ssv-spec's validatePartialSigMsgForSlot (msg.Slot == duty.DutySlot()). go-ssv parity: protocol/v2/ssv/runner/proposer_preferences.go:40,80,199 (one sub-runner per proposal slot; "duty.Slot is the proposal slot the preference targets, which is also the slot carried on the [wire]"). Message validation accepts the future slot via the role's earliness allowance and validates proposer assignment against it (#1062).
The collector retention story improves under this design. Collectors are reaped once for_slot < current_slot - SIGNATURE_COLLECTOR_RETAIN_SLOTS(1) (signature_collector/src/lib.rs:50,423-427), so a collector keyed to a future proposal_slot lives from creation until the proposal slot passes, and operators no longer race a 1-slot arrival window for lookahead emissions (the failure mode the earlier send-slot design had to work around). Collectors key on (signing_root, validator_index) (signature_collector/src/lib.rs:375-381), so a dependent_root re-emission for the same proposal slot creates a fresh collector under its new root; operators need only sign on the same trigger (epoch boundary or the shared re-emission trigger, §5).
Suggested approach
Look up the validator metadata and cluster for validator_pubkey (the same lookup sign_validator_registration_data / sign_voluntary_exit use). The preferences argument arrives fully built from the LH ProposerPreferencesService (proposer_preferences_service.rs:141-190 at the pin: dependent_root and proposal_slot from the duties map, fee_recipient and target_gas_limit from proposal_data(pubkey)); duty and config derivation is that service's layer, tracked under feat(client): spawn LH ProposerPreferencesService with AnchorValidatorStore backend #1064, and SIP-94 §5's byte-for-byte agreement requirement applies there. (The container field is now target_gas_limit, matching consensus-specs and SIP-94; older pins named it gas_limit.)
Compute the signing domain via the 2-arg wrapper self.get_domain(epoch, Domain::ProposerPreferences) (lib.rs:277-284) with epoch = preferences.proposal_slot.epoch(E::slots_per_epoch()): the domain epoch is keyed on proposal_slot so the signature verifies against the fork of the slot being proposed, including the §5 pre-fork emission case (Gloas domain while the chain is pre-fork). Same derivation as LH's reference implementation.
signing_root = preferences.signing_root(domain).
Collect with the duty's slot: collect_signature(PartialSignatureKind::ProposerPreferences, Role::ProposerPreferences, CollectionMode::SingleValidator, &validator, &cluster, signing_root, preferences.proposal_slot), following the RANDAO call shape at lib.rs:2550-2558. Do NOT stamp slot_clock.now() and do NOT stamp epoch-start; the wire slot is the duty's proposal_slot (see Context).
The broadcast PartialSignatureMessages.slot equals preferences.proposal_slot (assert via the collector/manager path, not just the call site).
Domain epoch equals epoch(proposal_slot); a preference for a first-Gloas-epoch slot signed pre-fork uses the Gloas fork domain.
Kind is PartialSignatureKind::ProposerPreferences, role Role::ProposerPreferences, SingleValidator mode.
No slashing-protection database read or write (registration-style non-slashable duty).
Re-emission for the same proposal_slot with a changed dependent_root produces a different signing root and a fresh collector; both reconstruct independently when quorums exist.
A lookahead emission (proposal slot up to (1 + min_seed_lookahead) epochs ahead) reconstructs even when partial sigs arrive several slots after emission (collector alive until proposal_slot + 1).
A missing quorum fails per-validator with a bounded collection timeout; it must never hang the caller indefinitely, so one failed validator cannot stall sibling validators' signing or publication (see Risks: batch liveness).
Collection failures are surfaced cause-neutrally (e.g. threshold not reached / timeout). The wire carries only the signing root, so telemetry must not claim attribution to target_gas_limit vs dependent_root divergence; either can only be listed as a possible cause.
No duty or config derivation inside validator_store: the signed message equals the supplied preferences verbatim (the method only looks up validator/cluster, computes domain and root, and collects).
No change to sign_validator_registration_data (registration keeps its send-slot semantics pre-Gloas).
Batch liveness (caller-side, upstream). At pin 81d576943 the service awaits each signer sequentially (proposer_preferences_service.rs:190), so one pending collection (e.g. a missing quorum running to its timeout) delays every later validator's publication in that batch. Fixed upstream by Sign and publish proposer preferences concurrently in the VC lighthouse#9617 (concurrent signing and publication, per-pair retry), open at the time of this edit and picked up on a future pin bump. The per-validator timeout criterion above is what makes the concurrent service effective for Anchor.
Goal
Implement the
sign_proposer_preferencesLH trait method onAnchorValidatorStore, filling the existingTODO(gloas)stub (anchor/validator_store/src/lib.rs:3367-3374), as aSingleValidator-mode partial-signature collection (no QBFT): sign the suppliedProposerPreferencesunderDOMAIN_PROPOSER_PREFERENCESwith the validator's BLS share and reconstruct via the signature collector. The trait signature is(validator_pubkey: PublicKeyBytes, preferences: ProposerPreferences) -> SignedProposerPreferences(validator_client/validator_store/src/lib.rs:217at LH pin81d576943, PR #1125's pin; re-verify the live pin inCargo.tomlbefore trusting line refs); the preferences arrive fully built from the LHProposerPreferencesService, so no duty or config derivation happens in this method. Mirrorssign_validator_registration_data/sign_voluntary_exitin shape, and LH's own implementation (lighthouse_validator_store/src/lib.rs:1490at the pin) for the domain and signing flow.Context
Per SIP-94 §5, each operator independently derives
ProposerPreferences { dependent_root, proposal_slot, validator_index, fee_recipient, target_gas_limit }and signs it; reconstruction succeeds only when2f + 1operators converge on one signing root. No QBFT:target_gas_limit(operator config) anddependent_root(per-operator BN observation) determine each operator's root locally.Collection slot =
proposal_slot(SIP-94 §5/§7; supersedes the earlierslot_clock.now()design in this issue's history). The slot passed tocollect_signaturebecomes thePartialSignatureMessages.slotstamped on the wire (SignatureMetadatabuilt atlib.rs:461with itsslotfield at:469; wire stampslot: metadata.slotatsignature_collector/src/lib.rs:312-316), and SIP-94 §5 pins that slot toproposal_slotitself: one runner per proposal slot, matching ssv-spec'svalidatePartialSigMsgForSlot(msg.Slot == duty.DutySlot()). go-ssv parity:protocol/v2/ssv/runner/proposer_preferences.go:40,80,199(one sub-runner per proposal slot; "duty.Slot is the proposal slot the preference targets, which is also the slot carried on the [wire]"). Message validation accepts the future slot via the role's earliness allowance and validates proposer assignment against it (#1062).The collector retention story improves under this design. Collectors are reaped once
for_slot < current_slot - SIGNATURE_COLLECTOR_RETAIN_SLOTS(1)(signature_collector/src/lib.rs:50,423-427), so a collector keyed to a futureproposal_slotlives from creation until the proposal slot passes, and operators no longer race a 1-slot arrival window for lookahead emissions (the failure mode the earlier send-slot design had to work around). Collectors key on(signing_root, validator_index)(signature_collector/src/lib.rs:375-381), so adependent_rootre-emission for the same proposal slot creates a fresh collector under its new root; operators need only sign on the same trigger (epoch boundary or the shared re-emission trigger, §5).Suggested approach
validator_pubkey(the same lookupsign_validator_registration_data/sign_voluntary_exituse). Thepreferencesargument arrives fully built from the LHProposerPreferencesService(proposer_preferences_service.rs:141-190at the pin:dependent_rootandproposal_slotfrom the duties map,fee_recipientandtarget_gas_limitfromproposal_data(pubkey)); duty and config derivation is that service's layer, tracked under feat(client): spawn LH ProposerPreferencesService with AnchorValidatorStore backend #1064, and SIP-94 §5's byte-for-byte agreement requirement applies there. (The container field is nowtarget_gas_limit, matching consensus-specs and SIP-94; older pins named itgas_limit.)self.get_domain(epoch, Domain::ProposerPreferences)(lib.rs:277-284) withepoch = preferences.proposal_slot.epoch(E::slots_per_epoch()): the domain epoch is keyed onproposal_slotso the signature verifies against the fork of the slot being proposed, including the §5 pre-fork emission case (Gloas domain while the chain is pre-fork). Same derivation as LH's reference implementation.signing_root = preferences.signing_root(domain).collect_signature(PartialSignatureKind::ProposerPreferences, Role::ProposerPreferences, CollectionMode::SingleValidator, &validator, &cluster, signing_root, preferences.proposal_slot), following the RANDAO call shape atlib.rs:2550-2558. Do NOT stampslot_clock.now()and do NOT stamp epoch-start; the wire slot is the duty'sproposal_slot(see Context).SignedProposerPreferencesto the caller (the LHProposerPreferencesServicespawned in feat(client): spawn LH ProposerPreferencesService with AnchorValidatorStore backend #1064 handles submission).Acceptance criteria
PartialSignatureMessages.slotequalspreferences.proposal_slot(assert via the collector/manager path, not just the call site).epoch(proposal_slot); a preference for a first-Gloas-epoch slot signed pre-fork uses the Gloas fork domain.PartialSignatureKind::ProposerPreferences, roleRole::ProposerPreferences,SingleValidatormode.proposal_slotwith a changeddependent_rootproduces a different signing root and a fresh collector; both reconstruct independently when quorums exist.(1 + min_seed_lookahead)epochs ahead) reconstructs even when partial sigs arrive several slots after emission (collector alive untilproposal_slot + 1).target_gas_limitvsdependent_rootdivergence; either can only be listed as a possible cause.validator_store: the signed message equals the suppliedpreferencesverbatim (the method only looks up validator/cluster, computes domain and root, and collects).sign_validator_registration_data(registration keeps its send-slot semantics pre-Gloas).Risks
proposal_slotmust come from thepreferencesargument end-to-end; deriving it from the clock anywhere reintroduces the send-slot design and breaks feat: add Role::ProposerPreferences with message-validator fork-gate #1062's proposer-assignment check.dependent_rootsplits roots (accepted §5 behavior, surfaces as a reconstruction timeout). This method signs whatever it is handed; cadence lives in the LHProposerPreferencesServicespawned in feat(client): spawn LH ProposerPreferencesService with AnchorValidatorStore backend #1064. At pin81d576943the service polls every slot, covers the current and next epoch (proposer lookahead), re-emits whendependent_rootchanges, and emits pre-fork for the first Gloas epoch (the fork gate is per preference epoch; duties race fixed in Fix race condition between validator duties service and proposer preferences lighthouse#9309).81d576943the service awaits each signer sequentially (proposer_preferences_service.rs:190), so one pending collection (e.g. a missing quorum running to its timeout) delays every later validator's publication in that batch. Fixed upstream by Sign and publish proposer preferences concurrently in the VC lighthouse#9617 (concurrent signing and publication, per-pair retry), open at the time of this edit and picked up on a future pin bump. The per-validator timeout criterion above is what makes the concurrent service effective for Anchor.