Goal
Insert a Gloas short-circuit in RegistrationService::start_validator_registration_service's spawning loop (anchor/validator_store/src/registration_service.rs:60-92) so that the periodic register_validators call is skipped once current_slot >= GLOAS_FORK_EPOCH. The service stays spawned; the periodic loop sleeps to the next epoch instead of issuing a wasted BN call. Pre-Gloas behavior unchanged.
Do NOT delete the service. Pre-Gloas testnets and the Gloas transition window still need it. SIP-94 §4 removes the pre-fork relay-builder mechanism (along with blinded blocks); post_validator_register_validator becomes a no-op at the BN under Gloas. A Gloas-aware skip on the Anchor side avoids the wasted RPC call and operator-log noise.
Context
Per SIP-94 §4 (ssvlabs/SIPs#94), the pre-Gloas relay-builder registration flow is replaced under Gloas by the new proposer_preferences p2p mechanism (handled by #1062, #1063, #1064). The legacy RegistrationService continues to call post_validator_register_validator against the BN every slot; under Gloas the BN ignores those calls. Anchor should short-circuit the periodic loop so:
- Pre-Gloas testnets continue to publish registrations unchanged (Boole and earlier).
- The transition window (Anchor running post-LH-bump but pre-Gloas-activation on a given network) continues to publish.
- Once the slot clock crosses
GLOAS_FORK_EPOCH, the periodic loop sleeps without invoking the registration path.
LH's proposer_preferences_service.rs:77-87 and payload_attestation_service.rs:88-100 already use this pattern (with inverted polarity, skip-when-not-Gloas). For RegistrationService, the polarity is reversed: skip when Gloas is enabled.
Suggested approach
anchor/validator_store/src/registration_service.rs:
| Location |
Change |
start_validator_registration_service (around :70-85) |
Inside the if let Some(slot) = self.inner.slot_clock.now() branch, add a Gloas fork-gate that sleeps to the next epoch and continues if spec.fork_name_at_slot::<S::E>(slot).gloas_enabled(). |
Sketch (gate sits before the existing inner.register_validators(slot).await call):
if let Some(slot) = self.inner.slot_clock.now() {
// SIP-94 §4: under Gloas, the relay-builder registration mechanism is
// removed; the BN ignores post_validator_register_validator. Skip the
// periodic publish to avoid wasted RPC + operator-log noise. The
// service stays spawned so pre-Gloas networks continue unchanged.
if spec.fork_name_at_slot::<S::E>(slot).gloas_enabled() {
let sleep_duration = self
.inner
.slot_clock
.duration_to_next_epoch(S::E::slots_per_epoch())
.unwrap_or_else(|| slot_duration * S::E::slots_per_epoch() as u32);
sleep(sleep_duration).await;
continue;
}
let inner = self.inner.clone();
let executor = inner.executor.clone();
let future = async move {
if let Err(e) = inner.register_validators(slot).await {
error!(error = ?e, "Error during validator registration");
}
};
executor.spawn(future, "validator_registration");
} else {
error!("Slot clock can not return current slot");
}
spec is already in scope (cloned at the top of start_validator_registration_service); no struct changes to Inner required. S::E is the generic already used elsewhere in the file (e.g., S::E::slots_per_epoch() inside register_validators).
Testability
The register_validators function isn't called when the gate skips, so a unit test that exercises the gate directly is awkward (the spawning loop runs forever). Extract the gate decision into a small pure helper so it can be asserted without a runtime:
fn should_skip_for_gloas<E: EthSpec>(spec: &ChainSpec, slot: Slot) -> bool {
spec.fork_name_at_slot::<E>(slot).gloas_enabled()
}
Tests assert this helper directly (no async, no slot clock, no BN mock).
Acceptance criteria
- Pre-Gloas slot:
register_validators is invoked from the spawning loop unchanged (existing behavior).
- Gloas slot: the spawning loop sleeps to the next epoch without invoking
register_validators or making any BN call.
- Service stays spawned across the fork transition; the loop re-evaluates the gate each iteration.
- In-code comment references SIP-94 §4 (relay-builder removal) so a future reader understands why the gate is there.
cargo check --workspace green; existing RegistrationService tests unaffected.
Tests to deliver (against the extracted helper):
#[test]
fn should_skip_for_gloas_returns_true_after_fork() {
let mut spec = ChainSpec::mainnet();
spec.gloas_fork_epoch = Some(Epoch::new(0)); // active from genesis
assert!(should_skip_for_gloas::<MainnetEthSpec>(&spec, Slot::new(0)));
}
#[test]
fn should_skip_for_gloas_returns_false_before_fork() {
let mut spec = ChainSpec::mainnet();
spec.gloas_fork_epoch = Some(Epoch::new(100_000)); // far future
let pre_fork_slot = Slot::new(32); // epoch 1
assert!(!should_skip_for_gloas::<MainnetEthSpec>(&spec, pre_fork_slot));
}
#[test]
fn should_skip_for_gloas_returns_false_when_unscheduled() {
let mut spec = ChainSpec::mainnet();
spec.gloas_fork_epoch = None; // never
assert!(!should_skip_for_gloas::<MainnetEthSpec>(&spec, Slot::new(100_000)));
}
Risks
- Do not delete the service. Pre-Gloas testnets and transition-window operators still need it. Fork-gating to a sleeping no-op preserves backward compatibility; deleting would break Holesky / Hoodi / any non-Gloas network Anchor is run against.
- Gating choice (
gloas_enabled() vs explicit epoch comparison). Using LH's gloas_enabled() helper (consensus/types/src/fork/fork_name.rs:205) mirrors how LH gates its own Gloas-aware code in proposer_preferences_service.rs, payload_attestation_service.rs, block_service.rs, and elsewhere. If LH refactors the helper, Anchor inherits the change via the LH bump. Acceptable; no Anchor-side abstraction required.
- Sleep duration unwrap.
duration_to_next_epoch returns Option; on None (slot-clock failure) the fallback is slot_duration * slots_per_epoch, matching LH's proposer_preferences_service.rs:81-83 fallback shape.
Dependencies
Blocked by the LH-bump milestone (M0) for ChainSpec::fork_name_at_slot, ForkName::gloas_enabled, and the gloas_fork_epoch field on ChainSpec. Independent of #1062, #1063, #1064 — can land in parallel with the rest of the M5 arc.
Goal
Insert a Gloas short-circuit in
RegistrationService::start_validator_registration_service's spawning loop (anchor/validator_store/src/registration_service.rs:60-92) so that the periodicregister_validatorscall is skipped oncecurrent_slot >= GLOAS_FORK_EPOCH. The service stays spawned; the periodic loop sleeps to the next epoch instead of issuing a wasted BN call. Pre-Gloas behavior unchanged.Do NOT delete the service. Pre-Gloas testnets and the Gloas transition window still need it. SIP-94 §4 removes the pre-fork relay-builder mechanism (along with blinded blocks);
post_validator_register_validatorbecomes a no-op at the BN under Gloas. A Gloas-aware skip on the Anchor side avoids the wasted RPC call and operator-log noise.Context
Per SIP-94 §4 (ssvlabs/SIPs#94), the pre-Gloas relay-builder registration flow is replaced under Gloas by the new
proposer_preferencesp2p mechanism (handled by #1062, #1063, #1064). The legacyRegistrationServicecontinues to callpost_validator_register_validatoragainst the BN every slot; under Gloas the BN ignores those calls. Anchor should short-circuit the periodic loop so:GLOAS_FORK_EPOCH, the periodic loop sleeps without invoking the registration path.LH's
proposer_preferences_service.rs:77-87andpayload_attestation_service.rs:88-100already use this pattern (with inverted polarity, skip-when-not-Gloas). ForRegistrationService, the polarity is reversed: skip when Gloas is enabled.Suggested approach
anchor/validator_store/src/registration_service.rs:start_validator_registration_service(around:70-85)if let Some(slot) = self.inner.slot_clock.now()branch, add a Gloas fork-gate that sleeps to the next epoch andcontinues ifspec.fork_name_at_slot::<S::E>(slot).gloas_enabled().Sketch (gate sits before the existing
inner.register_validators(slot).awaitcall):specis already in scope (cloned at the top ofstart_validator_registration_service); no struct changes toInnerrequired.S::Eis the generic already used elsewhere in the file (e.g.,S::E::slots_per_epoch()insideregister_validators).Testability
The
register_validatorsfunction isn't called when the gate skips, so a unit test that exercises the gate directly is awkward (the spawning loop runs forever). Extract the gate decision into a small pure helper so it can be asserted without a runtime:Tests assert this helper directly (no async, no slot clock, no BN mock).
Acceptance criteria
register_validatorsis invoked from the spawning loop unchanged (existing behavior).register_validatorsor making any BN call.cargo check --workspacegreen; existingRegistrationServicetests unaffected.Tests to deliver (against the extracted helper):
Risks
gloas_enabled()vs explicit epoch comparison). Using LH'sgloas_enabled()helper (consensus/types/src/fork/fork_name.rs:205) mirrors how LH gates its own Gloas-aware code inproposer_preferences_service.rs,payload_attestation_service.rs,block_service.rs, and elsewhere. If LH refactors the helper, Anchor inherits the change via the LH bump. Acceptable; no Anchor-side abstraction required.duration_to_next_epochreturnsOption; onNone(slot-clock failure) the fallback isslot_duration * slots_per_epoch, matching LH'sproposer_preferences_service.rs:81-83fallback shape.Dependencies
Blocked by the LH-bump milestone (M0) for
ChainSpec::fork_name_at_slot,ForkName::gloas_enabled, and thegloas_fork_epochfield onChainSpec. Independent of #1062, #1063, #1064 — can land in parallel with the rest of the M5 arc.