test: add message type validation spec tests#837
test: add message type validation spec tests#837shane-moore wants to merge 9 commits intosigp:unstablefrom
Conversation
|
Claude finished @shane-moore's task in 2m 22s —— View job Code Review CompleteThis PR adds comprehensive spec test coverage for SSV message type serialization and validation. The implementation is well-structured with good separation of concerns between deserializers, type modifications, and test infrastructure. However, there are several issues that need attention before merging. Critical Issues (Must Fix)1. Production-Unsafe
|
cc92b71 to
a4cdeb7
Compare
|
@claude, fixed your point 4.
|
|
Claude finished @shane-moore's task in 1m 36s —— View job Review Response AnalysisVerified Fixes ✓
Disputed Points ReviewYou're right on all disputed points. Here's my analysis: #1 - #3 - #5 - Intermediate structs documentation: Valid pushback. The structs already have clear doc comments:
#6 - #7 - Missing JSON examples: Valid pushback. These are internal #8 - Submodule documentation: Out of scope. While #9 - No unit tests for deserializers: Valid pushback. The deserializers are exercised by the spec test fixtures. Integration tests that use the deserializers provide coverage - separate unit tests would be redundant. ConclusionPoints 2 and 4 are properly fixed. The disputed points are all reasonable pushback - the original review was overly pedantic in several areas and missed existing documentation in others. The PR is in good shape. Recommendation: Approve the PR. The critical issues have been addressed, and the disputed points don't warrant changes. |
|
Claude finished @shane-moore's task in 1m 21s —— View job Re-evaluation Complete ✓Verified FixesPoint 2:
Point 4: Magic Number Error Code ✓ FIXED
Review ReassessmentPoint 1:
Point 3:
Point 5: Intermediate structs documentation → Valid pushback
Points 6-9: Suggestions, not issues → Reasonable to defer
SummaryCritical issues (points 2 & 4): ✓ Fixed The PR is in good shape. The original review was overly strict and contained three errors (points 1, 3, 5) where it flagged non-issues. Your pushback is justified. |
a4cdeb7 to
df8499a
Compare
48a3677 to
88de502
Compare
88de502 to
5840028
Compare
|
@claude, please take another look with fresh eyes and comment if have any findings |
| @@ -1,51 +0,0 @@ | |||
| --- | |||
There was a problem hiding this comment.
Has this been deleted by accident?
There was a problem hiding this comment.
Let's agree that the PR author does a first review pass to catch issues.
There was a problem hiding this comment.
yes, accidentally deleted. resurrected in b8cdd86. Totally agree on the first pass suggestion. I've been trying to move too fast to crank out these spec test PR's. Slowing down and catching things like this would respect reviewers time better, thanks!
| @@ -0,0 +1,93 @@ | |||
| //! Custom serde deserializers for Go JSON fixture format. | |||
There was a problem hiding this comment.
I have a design concern here, we’re mixing fixture-adapter logic into ssv_types.
The new serde deserializers in ssv_types are primarily handling Go fixture JSON shape differences (hex/base64/string-number fields, PascalCase names), e.g.:
MsgIDhex string ->MessageIdDatabase64 string ->VariableList<u8, ...>Slot/ValidatorIndexstring -> numeric typesPartialSignaturehex ->bls::Signature
I think we should separate concerns:
- Keep domain validation logic in production crates (e.g.
PartialSignatureMessages::validate()is production-relevant and belongs inssv_types). - Move fixture parsing adapters out of
ssv_typesand intospec_tests(DTOs +TryFromintossv_typestypes).
This keeps ssv_types focused on runtime/domain behavior and keeps fixture-format compatibility where it belongs (test harness).
There was a problem hiding this comment.
luv the idea of DTO's! much cleaner than polluting the production structs with all this serde logic. implemented in 281ff7d
Scope mismatch:
|
| if partial_signature_messages | ||
| .messages | ||
| .first() | ||
| .expect("validate() ensures non-empty") |
There was a problem hiding this comment.
.expect() in production code — this function is not behind #[cfg(test)].
While the invariant holds right after the validate() call above, a future refactor that reorders or removes that call turns this into a production panic. Prefer fallible handling:
let first_signer = partial_signature_messages
.messages
.first()
.ok_or(ValidationFailure::NoPartialSignatureMessages)?
.signer;
if first_signer != signer {
return Err(ValidationFailure::InconsistentSigners);
}Same safety, zero panic risk, and no coupling to the validate() call ordering.
There was a problem hiding this comment.
certainly agree with this defense-in-depth suggestion, resolved in cb2c3bc
| .ok_or(PartialSignatureMessagesError::Empty)?; | ||
|
|
||
| for m in self.messages.iter() { | ||
| if m.signer != first.signer { |
There was a problem hiding this comment.
Error-precedence concern: InconsistentSigners is checked before ZeroSigner here. If the first signer is valid (e.g. OperatorId(1)) and a later signer is OperatorId(0), the returned error is InconsistentSigners — the ZeroSigner branch is never reached for that element.
Worth verifying this matches Go's PartialSignatureMessages.Validate() error ordering. If Go checks zero-signer first (or uses a different loop structure), the error codes will diverge on mixed-invalid inputs, which can cause spec test failures for edge cases not yet covered.
There was a problem hiding this comment.
Great to check. The error ordering matches Go. In Go's https://github.com/ssvlabs/ssv-spec/blob/45153e4e4b8c61b929f701b7af52e3f725668421/types/partial_sig_message.go#L46-L62, the loop checks signer != m.Signer before calling https://github.com/ssvlabs/ssv-spec/blob/45153e4e4b8c61b929f701b7af52e3f725668421/types/partial_sig_message.go#L97-L102 (which is where the zero-signer check lives):
for _, m := range msgs.Messages {
if signer != m.Signer { // consistency first
return NewError(InconsistentSignersErrorCode, ...)
}
if err := m.Validate(); err != nil { // then per-message validation (zero check)
return errors.Wrap(err, ...)
}
}
And we do same in the rust version
|
|
||
| impl SpecTest for SignedSSVMessageTest { | ||
| fn run(&self) -> Result<(), String> { | ||
| for msg in &self.messages { |
There was a problem hiding this comment.
Inconsistent loop semantics with PartialSigMsgSpecTest.
This loop uses fail-fast: it returns on the first message where the error code doesn't match expected_error_code.
In contrast, PartialSigMsgSpecTest::check_validation() uses last-error-wins: it iterates all messages and only compares the final error code.
// Here (fail-fast):
for msg in &self.messages {
let actual_code = ...;
if actual_code != self.expected_error_code {
return Err(...); // stops at first mismatch
}
}
// PartialSigMsgSpecTest (last-error-wins):
let mut last_error_code = NO_ERROR;
for msg in &self.messages {
if let Err(code) = Self::validate_message(msg) {
last_error_code = code;
}
}
// compare last_error_code at the endIf Go uses the same runner pattern for both test types, one of these is wrong. Worth verifying against the upstream Go test runners.
There was a problem hiding this comment.
I see the inconsistency between the two tests. And yep, they follow the same flow as the Go code in:
https://github.com/ssvlabs/ssv-spec/blob/45153e4e4b8c61b929f701b7af52e3f725668421/types/spectest/tests/signedssvmsg/test.go
Since they're different runners, seems fine for them to use different patterns, and the Rust impl mirrors the Go counterparts, so we're all good on this one
| } | ||
|
|
||
| /// Deserializes an optional vector of base64 strings into `Option<Vec<Vec<u8>>>`. | ||
| #[expect(dead_code)] |
There was a problem hiding this comment.
#[expect(dead_code)] on a newly added function — if nothing uses it yet, prefer removing it entirely and adding it when a test type actually needs it. Avoids carrying dead code that suppresses the compiler's signal.
There was a problem hiding this comment.
i was torn on this one, and I agree with you. easy to resurrect when we'll need it in subsequent PR. Killed the fn for now
| pub use encoding_helpers::{check_roundtrip, check_roundtrip_with_root, decode_base64}; | ||
|
|
||
| pub use encoding_helpers::{check_roundtrip, check_roundtrip_with_root}; | ||
| pub static TESTING_VALIDATOR_PUBKEY: LazyLock<PublicKeyBytes> = LazyLock::new(|| { |
There was a problem hiding this comment.
Nit: a comment citing the upstream Go fixture that defines this key would help maintainers verify it stays in sync (e.g. a link to the relevant ssv-spec test file or constant).
|
|
||
| impl SignedSSVMessageTest { | ||
| /// Validate via `SignedSSVMessage::new()` (which calls `validate()` internally). | ||
| fn validate_message(&self, msg: &TestSignedSSVMessage) -> Result<(), i64> { |
There was a problem hiding this comment.
Spec-parity: validation order doesn't match Go's SignedSSVMessage.Validate().
This function checks:
- SSVMessage nil (line 64-67) →
NIL_SSV_MESSAGE - Signature normalization via
prepare_signatures(line 69) →EMPTY_SIGNATURE - Signer validation inside
SignedSSVMessage::new()(line 71) →NO_SIGNERS,ZERO_SIGNER, etc.
Go's messages.go:156 checks in the opposite order — signers first, then signatures, then SSVMessage content. The Go test harness (test.go:32) calls Validate() directly, so error precedence matters.
For mixed-invalid inputs (e.g. both no signers AND nil SSVMessage), this will return NIL_SSV_MESSAGE where Go returns NO_SIGNERS. This can cause spec test failures on edge-case fixtures.
Fix: reorder to match Go — pass through SignedSSVMessage::new() first (signer checks), then nil-message / signature checks, or restructure so the harness mirrors Go's Validate() call order.
There was a problem hiding this comment.
This seems to be a current issue, before this PR, but validation seems duplicated in new and the validate called inside it.
There was a problem hiding this comment.
On the first point, looked a bit more deeply into the validation order mismatch. Seems fine actually since every fixture tests exactly one error condition in isolation. Validate() short circuits on the first error. The ordering divergence in our harness (nil check before signer checks) is a Rust type system constraint (SSVMessage is a value, not a pointer) and can't produce wrong results against the existing fixtures. A little extra context:
The test harness deserializes into Option<SSVMessage> to handle Go's null pointer case, but it must unwrap that Option before it can pass the value into SignedSSVMessage::new(). That forces the nil check to happen first, so we can't defer it like Go does with *SSVMessage.
There was a problem hiding this comment.
On the second point about pre-existing issue of duplicate validation between new and validate, great point! made issue #842 to handle
makes sense! opened pr #843 to handle the production changes and will keep this as draft PR til that's merged |
Issue Addressed
Continuation of spec test PR's
Proposed Changes
SSVMessage,SignedSSVMessage, andPartialSignatureMessages.Additional Info
Blocked by #843 since it's dependent on its changes