Skip to content

Comments

feat: implement deferred state updates for message validation#777

Draft
diegomrsantos wants to merge 24 commits intosigp:unstablefrom
diegomrsantos:feat/deferred-validation-state
Draft

feat: implement deferred state updates for message validation#777
diegomrsantos wants to merge 24 commits intosigp:unstablefrom
diegomrsantos:feat/deferred-validation-state

Conversation

@diegomrsantos
Copy link
Member

Issue Addressed

Improves message validation architecture by separating validation logic from state mutation.

Proposed Changes

  1. Add StateUpdate types - New types to capture deferred state changes:

    • StateUpdate enum with None, Consensus, and PartialSignature variants
    • ConsensusStateUpdate and PartialSignatureStateUpdate structs
  2. Add apply methods - New methods to apply deferred state updates:

    • DutyState::apply_consensus_update()
    • DutyState::apply_partial_signature_update()
  3. Refactor validation - Validation functions now return (ValidatedSSVMessage, StateUpdate) instead of mutating state directly

  4. Explicit commit - Validator::commit() method to apply state updates when processing

Motivation

Validation should be a pure operation that doesn't produce side effects. By returning a StateUpdate instead of mutating state during validation, callers have explicit control over when state changes are applied. This makes the code easier to reason about and test.

Additional Info

  • All existing tests pass
  • The old update_* methods are kept for test compatibility
  • Added #[must_use] to StateUpdate to prevent accidental omission of commit

diegomrsantos and others added 23 commits January 8, 2026 11:56
Introduce a Fork enum representing SSV protocol forks:
- Genesis: initial protocol version
- Alan: current fork (committee_id % 128 subnet topology)
- Boole: upcoming fork (MinHash subnet topology)

The enum provides utilities for fork ordering, navigation, and
serialization, enabling other components to query which fork
applies at any given point.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Introduce ForkSchedule to manage fork activation epochs and transitions:
- Tracks which fork is active at any epoch
- Provides preparation window detection for dual-subscribe scenarios
- Automatically activates predecessor forks when scheduling a fork

Integrate ForkSchedule into SsvNetworkConfig:
- Built-in networks default to Alan fork at epoch 0
- Custom configs can specify Boole epoch via ssv_boole_fork_epoch.txt

This separates "when" (schedule) from "what" (fork-specific behavior),
allowing subsystems to query the active fork independently.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Remove speculative functions not needed for current implementation:
- Fork: next(), previous()
- ForkSchedule: is_fork_active(), next_fork_after(), has_pending_transition()

Keep only what's necessary: active_fork(), in_preparation_window(), fork_epoch()
- Create shared Arc<ForkSchedule> at client startup
- Log fork configuration (current fork and Boole epoch)
- Infrastructure ready for components to query active fork

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Create new `fork` crate under anchor/common/fork/
- Move Fork enum from ssv_types to fork crate
- Move ForkSchedule from ssv_network_config to fork crate
- Add fork monitor for logging fork transitions
- Re-export fork types from ssv_types and ssv_network_config for compatibility

The fork crate now contains all fork-related code:
- fork.rs: Fork enum (Genesis, Alan, Boole)
- schedule.rs: ForkSchedule for managing activation epochs
- monitor.rs: Standalone task for logging fork transitions

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The early fork logging was incorrectly checking active_fork(epoch 0)
instead of the current epoch. The fork_monitor already handles this
correctly by using the actual current epoch from slot_clock.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove Fork::Genesis variant since Alan is the initial fork
- Simplify ForkSchedule::new() to create Alan at epoch 0
- Simplify set_fork_epoch() to just insert without validation
- Remove ForkScheduleError enum and with_fork() constructor
- Use MinimalEthSpec and ChainSpec for test constants instead of magic numbers
- Replace custom TestSlotClock with ManualSlotClock in monitor tests

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…refs

- Fix Cargo.toml missing closing bracket in authors field
- Update lib.rs doc comment to remove Genesis from fork list
- Update test comment to remove Genesis reference

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace individual text files with a single YAML file for fork schedule
configuration. This provides a cleaner and more maintainable approach
for managing fork activations.

- Add `from_fork_epochs()` to ForkSchedule for loading from config
- Alan (genesis) is hardcoded at epoch 0 and rejected if in config
- Validate chronological ordering of fork epochs
- Add ssv_fork_schedule.yaml files for mainnet, holesky, and hoodi
Instead of waking up every epoch to check for state changes, the monitor
now calculates when the next interesting event occurs (preparation window
start or fork activation) and sleeps directly until that time.

This is more efficient for forks scheduled far in the future and more
precise since we wake up exactly when needed.
Remove redundant 'forks:' wrapper since the filename
ssv_fork_schedule.yaml already indicates the content type.
Add new types to represent deferred state changes from message validation:
- StateUpdate enum with None, Consensus, and PartialSignature variants
- ConsensusStateUpdate captures signers, slot, epoch, round, message type
- PartialSignatureStateUpdate captures signer, slot, epoch, kind

This separation allows validation checks to be decoupled from state
mutations, enabling callers to decide when/if to apply state changes.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add methods to DutyState, OperatorState, and SignerState to apply
deferred state updates:
- DutyState::apply_consensus_update()
- DutyState::apply_partial_signature_update()
- OperatorState::apply_consensus_update()
- SignerState::apply_update()

These methods mirror the existing update methods but take pre-computed
StateUpdate values instead of raw messages, enabling deferred application.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Refactor validation functions to return StateUpdate alongside the
validated message instead of mutating state directly:

- validate_consensus_message returns (ValidatedSSVMessage, StateUpdate)
- validate_partial_signature_message returns (ValidatedSSVMessage, StateUpdate)
- ValidatedMessage now includes state_update field
- Validator::commit() applies deferred state updates

This enables callers to decide when to apply state changes, which is
important for fork transitions where messages may be validated on
multiple topics but should only update state once.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Call Validator::commit() to apply deferred state updates when a
validated message is actually processed.

The commit happens after all checks pass (ownership verification,
doppelganger check) but before dispatching to QBFT or signature
collector, ensuring state is only updated for messages we will process.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@claude-code-actions-sigp
Copy link

claude-code-actions-sigp bot commented Jan 15, 2026

Claude finished @diegomrsantos's task in 1m 41s —— View job


Code Review Complete

I've completed a comprehensive review of PR #777. This PR implements deferred state updates for message validation and includes fork infrastructure (which appears to be a prerequisite for the validation changes).


Architecture & Design

Strengths:

  1. Clean Separation of Concerns: The separation of validation from state mutation is well-executed. The validation functions now return (ValidatedSSVMessage, StateUpdate) instead of mutating state directly, making the code more testable and easier to reason about.

  2. Fork Transition Support: The PR description mentions fork transitions as motivation, and the implementation correctly allows messages to be validated multiple times (on different topics) but only have state committed once. This is demonstrated in message_receiver/src/manager.rs:178-181.

  3. Type Safety: The #[must_use] attribute on StateUpdate (line 24 of state_update.rs) is excellent - it prevents accidental omission of the commit call at compile time.

  4. Explicit Commit Point: The Validator::commit() method (line 398 in lib.rs) provides a clear, explicit point where state changes are applied, improving control flow visibility.


Code Quality Issues

1. Missing Error Handling in apply_partial_signature_update

Location: duty_state.rs:128-151

The update_for_partial_signature method returns Result<(), ValidationFailure> (line 80), but the new apply_partial_signature_update method doesn't return a Result. This is inconsistent and could hide errors.

Issue: If state creation fails in the apply method, the error is silently ignored, whereas the old method would have returned an error.

// Current code (line 128-151)
pub(crate) fn apply_partial_signature_update(&mut self, update: &PartialSignatureStateUpdate) {
    let operator_state = self.get_or_create_operator(&update.signer);
    // ... state creation that could conceptually fail
}

Recommendation: Either:

  • Make apply_* methods infallible by design (document why), or
  • Return Result to match the pattern of update_* methods

2. Code Duplication Between update and apply_* Methods

Locations:

  • duty_state.rs:58-76 vs duty_state.rs:117-122
  • duty_state.rs:80-110 vs duty_state.rs:128-151
  • duty_state.rs:245-267 vs duty_state.rs:272-291

The logic is nearly identical between the old update_* methods and new apply_* methods. This creates maintenance burden.

Example:

// update (lines 58-76) and apply_consensus_update (lines 117-122) do the same thing
// except one takes SignedSSVMessage/QbftMessage and the other takes ConsensusStateUpdate

Recommendation: The PR description states "The old update_* methods are kept for test compatibility" but this creates technical debt. Consider:

  1. Updating all tests to use the new pattern
  2. Removing the old methods
  3. Or clearly document that old methods are deprecated with #[deprecated] attributes

3. Fork Infrastructure Scope Concern

Location: Multiple files in anchor/common/fork/

The PR adds significant fork infrastructure (~1000 lines):

  • anchor/common/fork/ - entire new crate
  • YAML fork schedule configuration files
  • Fork monitoring service

Issue: The PR description focuses solely on "deferred state updates for message validation" with no mention of fork infrastructure. This makes the PR harder to review and understand.

Recommendation:

  • Either split fork infrastructure into a separate PR, or
  • Update the PR description to explicitly document the fork changes and explain why they're included together

Testing

Strengths:

  1. Existing tests pass according to PR description
  2. Tests cover key validation scenarios (duty_state.rs:415-518, consensus_message.rs:479-1434, partial_signature.rs:278-1159)

Gaps:

  1. No tests for the new commit() method (lib.rs:398-410)
  2. No tests demonstrating the fork transition use case mentioned in the PR description
  3. No tests verifying #[must_use] works as intended (though this is enforced at compile time)
  4. No tests for apply_* methods directly - they're only exercised indirectly

Recommendation: Add tests demonstrating:

  • Calling validate() multiple times without commit() doesn't duplicate state
  • Calling commit() correctly applies state updates
  • Fork transition scenario (validate on multiple topics, commit once)

Security Considerations

No Security Issues Found

The changes maintain existing security properties:

  • Signature verification still occurs before state updates
  • Validation logic unchanged
  • State mutation is deferred but not bypassed

Performance

Potential Concern: State updates are now cloned when creating ValidatedMessage (lib.rs:389):

ValidatedMessage::new(signed_ssv_message.clone(), validated, state_update)

Impact: Minimal - StateUpdate variants are lightweight (mostly primitive types and small vectors), and this only happens for valid messages.


Documentation

Strengths:

  1. Excellent module-level documentation in state_update.rs:1-12 explaining the why
  2. Clear comments at the commit point (manager.rs:178-180)
  3. Good inline comments explaining fork transition reasoning

Gaps:

  1. No documentation update for the changed validation architecture
  2. Validator::commit() method lacks doc comments explaining when/why to call it
  3. Missing examples of the new validation + commit pattern

Recommendation: Add rustdoc examples showing:

/// # Example
/// ```
/// let result = validator.validate(&message_data);
/// if let ValidationResult::Success(validated) = result {
///     // Only commit if we're processing the message
///     validator.commit(&msg_id, &validated.state_update);
/// }
/// ```

Specific Recommendations

  1. HIGH PRIORITY: Clarify PR scope - either split fork infrastructure to separate PR or update description to explain it

  2. MEDIUM PRIORITY: Add tests for Validator::commit() method and fork transition scenarios

  3. MEDIUM PRIORITY: Remove deprecated update_* methods or add #[deprecated] attributes

  4. LOW PRIORITY: Make error handling consistent between update_* and apply_* methods

  5. LOW PRIORITY: Add rustdoc examples demonstrating the new validation + commit pattern


Summary

This is a well-designed refactoring that successfully separates validation from state mutation. The core implementation is solid with good type safety and clear intent. The main concerns are:

  • Scope clarity: Fork infrastructure isn't mentioned in PR description
  • Test coverage: Missing tests for the new commit() method
  • Code duplication: Old and new update methods coexist

The changes are safe to merge once the scope question is addressed and test coverage is improved.

Verdict: Approve with recommendations for follow-up improvements.


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.

1 participant