Skip to content

ssv_types: separate raw SSZ messages from validated messages #1009

Description

@diegomrsantos

Description

ssv_types::message::SSVMessage and SignedSSVMessage currently represent two different states at once:

  • an SSZ-shaped wire container that has decoded successfully, but may still be semantically invalid
  • a protocol message that has passed Anchor's semantic validation

That split is already visible at the network boundary. Anchor decodes SignedSSVMessage from SSZ first, then runs SignedSSVMessage::validate() and maps failures into message-validation errors. PR #991 makes the same boundary more explicit because it needs a way for spec tests to build SSZ-bounds-valid values that intentionally fail semantic validation.

I think we should track a cleaner design instead of adding more constructors that return semantically invalid production message values.

Version

Observed on unstable while reviewing PR #991. The PR head at the time of review was 7dc557050966527fa5c48fa3c8ccead365ade56c.

Present Behaviour

Anchor's SSVMessage derives Decode, so SSZ decoding can construct an SSVMessage without going through SSVMessage::new(). SSVMessage::validate() then performs semantic checks such as rejecting empty Data and enforcing message-type-specific payload limits. SignedSSVMessage::validate() delegates to SSVMessage::validate().

The message validator follows a decode-then-validate flow: it calls SignedSSVMessage::from_ssz_bytes(...), then validate_decoded_message(...), then signed_ssv_message.validate().

Compared with upstream behavior, this boundary is real rather than hypothetical:

  • ssv-spec/types.SSVMessage.Validate() checks nil, known message type, and role encoding. It does not check empty Data there.
  • The Go node's message/validation.validateSSVMessage separately rejects empty SSVMessage.Data after validating the signed message envelope.

So there are two useful concepts: raw SSZ message data and validated protocol message data. Anchor currently uses the same Rust type for both.

Expected Behaviour

Long term, the type model should make the state explicit:

pub struct RawSSVMessage { ... }        // SSZ-shaped wire container
pub struct SSVMessage { raw: RawSSVMessage } // semantically validated message

The same pattern would apply to RawSignedSSVMessage and SignedSSVMessage.

Decode boundaries would return raw types:

let raw = RawSignedSSVMessage::from_ssz_bytes(bytes)?;
let signed = SignedSSVMessage::try_from(raw)?;

Normal production code would receive SignedSSVMessage only after validation. Spec tests that need malformed-but-SSZ-bounds-valid fixtures could use the raw type or a test-support encoder without creating an invalid SignedSSVMessage value.

Steps to resolve

A possible migration path:

  1. Introduce raw wire-container types for SSVMessage and SignedSSVMessage, preserving the current SSZ layout and Encode / Decode behavior.
  2. Make the validated message types constructible only through new(...) or TryFrom<Raw...>.
  3. Update the message validator to decode raw types first, then convert to validated types before passing messages deeper into processing.
  4. Preserve the current error precedence deliberately: malformed SSZ remains a decode failure; semantic failures remain post-decode validation failures.
  5. Move spec-test max-size fixture construction onto raw types or a test_support::encode_* helper that returns bytes rather than invalid validated message objects.
  6. Add focused tests showing that raw types can represent SSZ-bounds-valid malformed fixtures, while validated types cannot be created without passing semantic validation.

This does not need to block narrow fixture work if a smaller test-only helper is enough for now, but it gives us a destination that keeps the production message types honest.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions