Skip to content

feat(message_validator): reject ValidatorRegistration partial-sig at/after Gloas #1115

Description

@shane-moore

Goal

Reject inbound ValidatorRegistration partial-signature messages whose envelope
(send) slot is at or after the Ethereum Gloas fork. SIP-94 §5 deprecates the
ValidatorRegistration duty at Gloas: proposer preferences replace relay
registrations, and the relay path is gone with blinded blocks. This is the
receive-side half of that deprecation; the emit-side half (stop signing/broadcasting
post-Gloas) is #1065.

Add a fourth branch to validate_role_for_fork (message_validator/src/lib.rs:849)
and a new RoleNotActiveAfterEthFork error variant. Do NOT remove
Role::ValidatorRegistration (wire byte [4, 0, 0, 0], msgid.rs:34),
PartialSignatureKind::ValidatorRegistration = 4 (partial_sig.rs:34), or
BEACON_ROLE_VALIDATOR_REGISTRATION = BeaconRole(5) (consensus.rs:490, the
BNRoleValidatorRegistration the SIP names): SIP-94 §5 reserves those numeric values
for pre-Gloas operation and backward-compat decoding.

Context / motivation

validate_role_for_fork already gates role-by-fork in both directions and on both
the SSV Fork axis and the Ethereum ForkName axis:

  • RoleNotActiveBeforeFork (SSV): AggregatorCommittee before Boole.
  • RoleNotActiveAfterFork (SSV): Aggregator / SyncCommittee deprecated after Boole.
  • RoleNotActiveBeforeEthFork (eth): PTCAttester before Gloas.

The ValidatorRegistration deprecation is the missing symmetric case:
RoleNotActiveAfterEthFork on the eth axis. It reuses the exact PTCAttester
mechanism (spec.fork_name_at_epoch(epoch).gloas_enabled(), lib.rs:877) with inverted
polarity: reject when Gloas IS enabled.

validate_role_for_fork runs on the partial-sig path at partial_signature.rs:36
with messages.slot (the current send slot), so a single branch there covers every
inbound ValidatorRegistration message (the role is non-QBFT; partial-sig is its only
message type). The gate is defense-in-depth: honest post-Gloas operators emit nothing
(#1065); a Gloas-or-later-slot VR message is a buggy/stale/malicious sender.

Boundary safety: a VR message legitimately sent in the last pre-Gloas slot has a
pre-Gloas envelope slot, so gloas_enabled() is false and it is still accepted. Only
Gloas-or-later envelope slots are rejected.

Suggested approach

message_validator/src/lib.rs:

  • Add error variant next to RoleNotActiveBeforeEthFork (~lib.rs:226):
    RoleNotActiveAfterEthFork { role: Role, current_fork: ForkName, deprecated_since_fork: ForkName }.

  • Add a branch in validate_role_for_fork (after the PTCAttester block, ~lib.rs:885):

    // SIP-94 §5: the ValidatorRegistration duty is deprecated at Gloas. Proposer
    // preferences replace relay registrations and the relay path is gone with
    // blinded blocks. The variant + wire byte are retained for pre-Gloas decode.
    if role == Role::ValidatorRegistration {
    let current_fork = validation_context.spec.fork_name_at_epoch(epoch);
    if current_fork.gloas_enabled() {
    return Err(ValidationFailure::RoleNotActiveAfterEthFork {
    role,
    current_fork,
    deprecated_since_fork: ForkName::Gloas,
    });
    }
    }

  • Update the validate_role_for_fork doc comment (lib.rs:843-848) to list the VR
    Gloas deprecation.

  • No change to From<&ValidationFailure> for MessageAcceptance (lib.rs:233): the new
    variant is not in the Ignore list, so it falls through _ => Reject (lib.rs:257),
    matching RoleNotActiveBeforeEthFork. "Invalid" in the SIP = REJECT.

No changes to msgid.rs / partial_sig.rs / message_counts.rs / duty_limit /
message_lateness: post-Gloas VR is rejected up front, and pre-Gloas VR flows through
the existing arms unchanged.

Acceptance criteria

  • Pre-Gloas slot: validate_role_for_fork returns Ok(()) for Role::ValidatorRegistration
    (existing behavior, unchanged).
  • Gloas-or-later slot: validate_role_for_fork returns
    RoleNotActiveAfterEthFork { deprecated_since_fork: ForkName::Gloas, .. }, and
    end-to-end validate_partial_signature_message REJECTs the message.
  • Exactly at GLOAS_FORK_EPOCH slot 0: rejected (fork is enabled from its first slot).
  • Role::ValidatorRegistration wire byte stays [4, 0, 0, 0],
    PartialSignatureKind::ValidatorRegistration stays 4, and
    BEACON_ROLE_VALIDATOR_REGISTRATION stays BeaconRole(5) (msgid.rs / partial_sig.rs
    / consensus.rs untouched; reserved wire values per SIP-94 §5).
  • cargo check --workspace green; pre-existing ValidatorRegistration / partial-sig
    tests unaffected (default spec_with_gloas(None) keeps them pre-Gloas).

Tests

Mirror test_ptc_attester_rejected_before_gloas (partial_signature.rs:1745), inverting polarity:

  • test_validator_registration_accepted_before_gloas: default spec (no Gloas),
    validate_role_for_fork(Slot::new(0), &ctx) is Ok.
  • test_validator_registration_rejected_after_gloas: ctx.spec = spec_with_gloas(Some(0))
    (partial_signature.rs:495), assert RoleNotActiveAfterEthFork.
  • test_validator_registration_rejected_at_gloas_boundary: spec_with_gloas(Some(N)),
    message slot in epoch N, assert rejected end-to-end through
    validate_partial_signature_message.

Use the tester-subagent before writing these tests (project rule).

Notes

Emit-side half of the SIP-94 §5 deprecation is #1065 (RegistrationService fork-gate).
Together #1065 + this issue fully implement the deprecation. Numeric values are
retained per SIP-94 §5, same treatment as RunnerRole values 1 and 3 in §3.

Issues are directionally correct, not prescriptive; verify symbols at PR time.

Metadata

Metadata

Assignees

Labels

epbsePBS / EIP-7732 / Gloas implementation

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions