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:
- Introduce raw wire-container types for
SSVMessage and SignedSSVMessage, preserving the current SSZ layout and Encode / Decode behavior.
- Make the validated message types constructible only through
new(...) or TryFrom<Raw...>.
- Update the message validator to decode raw types first, then convert to validated types before passing messages deeper into processing.
- Preserve the current error precedence deliberately: malformed SSZ remains a decode failure; semantic failures remain post-decode validation failures.
- 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.
- 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.
Description
ssv_types::message::SSVMessageandSignedSSVMessagecurrently represent two different states at once:That split is already visible at the network boundary. Anchor decodes
SignedSSVMessagefrom SSZ first, then runsSignedSSVMessage::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
unstablewhile reviewing PR #991. The PR head at the time of review was7dc557050966527fa5c48fa3c8ccead365ade56c.Present Behaviour
Anchor's
SSVMessagederivesDecode, so SSZ decoding can construct anSSVMessagewithout going throughSSVMessage::new().SSVMessage::validate()then performs semantic checks such as rejecting emptyDataand enforcing message-type-specific payload limits.SignedSSVMessage::validate()delegates toSSVMessage::validate().The message validator follows a decode-then-validate flow: it calls
SignedSSVMessage::from_ssz_bytes(...), thenvalidate_decoded_message(...), thensigned_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 emptyDatathere.message/validation.validateSSVMessageseparately rejects emptySSVMessage.Dataafter 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:
The same pattern would apply to
RawSignedSSVMessageandSignedSSVMessage.Decode boundaries would return raw types:
Normal production code would receive
SignedSSVMessageonly 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 invalidSignedSSVMessagevalue.Steps to resolve
A possible migration path:
SSVMessageandSignedSSVMessage, preserving the current SSZ layout andEncode/Decodebehavior.new(...)orTryFrom<Raw...>.test_support::encode_*helper that returns bytes rather than invalid validated message objects.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.