Skip to content

p2p: add message validation spec tests#618

Open
nkryuchkov wants to merge 4 commits into
mainfrom
p2p-msg-validation-spectests
Open

p2p: add message validation spec tests#618
nkryuchkov wants to merge 4 commits into
mainfrom
p2p-msg-validation-spectests

Conversation

@nkryuchkov
Copy link
Copy Markdown
Contributor

@nkryuchkov nkryuchkov commented Apr 3, 2026

Summary

This PR adds a proper P2P spectest suite for the implemented P2P message-validation surface and brings p2p/spectest in line with the repo’s other spectest packages.

It also hardens p2p/validation/msg_validation.go so gossip rejects malformed or structurally invalid signed messages earlier at the P2P boundary.

What changed

P2P validation hardening

  • enforce SignedSSVMessage.Validate()
  • verify outer operator signatures against the committee
  • reject partial-signature gossip with multiple outer signers
  • reject partial-signature gossip when the outer signer and inner partial-signature signer do not match

New P2P spectest suite

Added a new data-driven p2p/spectest structure with:

  • all_tests.go
  • run_test.go
  • TestJson
  • testdoc/
  • generate/
  • grouped test cases under:
    • tests/msgvalidation/general
    • tests/msgvalidation/consensus
    • tests/msgvalidation/preconsensus
    • tests/msgvalidation/postconsensus

Coverage added

  • malformed pubsub payloads
  • structural SignedSSVMessage rejects:
    • no signers
    • no signatures
    • empty signature
    • signer/signature count mismatch
    • zero signer
    • duplicate signers
  • unsupported top-level SSV message type
  • consensus validation:
    • valid existing instance
    • unknown instance
    • invalid consensus signature
    • wrong identifier
    • future multi-signer rejection
    • valid future decided message
    • decided message with bad FullData
  • pre-consensus partial-signature validation
  • post-consensus partial-signature validation

Generated artifacts

  • added generated per-test JSON vectors under p2p/spectest/generate/tests/

Verification

  • go generate ./p2p/spectest/generate
  • go test ./p2p/...

Task

F-ssv-spec-034

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 3, 2026

Greptile Summary

This PR adds a new p2p/spectest suite with 29 structured test cases covering message validation across general structural, consensus, pre-consensus, and post-consensus scenarios, and hardens validatePartialSigMsg in msg_validation.go to enforce single-signer count, RSA outer-signature verification, and inner signer matching — directly addressing the previously identified gap for partial-sig messages.

The implementation is well-structured and consistent with other spectest packages in the repo. The generate/tests.json is intentionally gitignored (same as qbft/spectest), and the type-switch in TestJson correctly compares against the unsanitized reflect type string stored as the JSON map key.

Confidence Score: 5/5

Safe to merge; the validation hardening is correct and the spectest suite is well-structured with comprehensive coverage.

All remaining findings are P2 style suggestions. The previously flagged RSA signature verification gap is now resolved, the single-signer constraint is enforced, and the new spectest infrastructure matches established repo patterns.

No files require special attention.

Important Files Changed

Filename Overview
p2p/validation/msg_validation.go Hardens validatePartialSigMsg to enforce single-signer constraint, RSA outer-signature verification via types.Verify, and inner BLS signer matching — all in the correct order before delegating to runner-level validation.
p2p/spectest/tests/msgvalidation/test.go Core test harness: MsgValidationSpecTest, builder helpers, and runner setup ops are well-structured; StateWithRunningInstanceOp exposes a positional boolean that is non-obvious at call sites.
p2p/spectest/run_test.go TestAll and TestJson both use t.Parallel() for sub-tests; TestJson correctly reads the gitignored tests.json (generated by go generate) and dispatches on the unsanitized reflect type key.
p2p/spectest/generate/main.go Correctly generates per-test JSON files (sanitized filenames) and a tests.json with unsanitized keys, matching the pattern established by qbft/spectest.
p2p/spectest/all_tests.go Registers all 29 test functions across general, consensus, pre-consensus, and post-consensus groups; no issues found.
p2p/spectest/testdoc/testdoc.go Documentation constants for all test cases; used only for JSON output metadata, not for test dispatch.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[pubsub.Message] --> B[DecodePubsubMsg]
    B -->|decode error| REJ1[ValidationReject]
    B --> C{SSVMessage.GetType}
    C -->|SSVConsensusMsgType| D[validateConsensusMsg]
    C -->|SSVPartialSignatureMsgType| E[validatePartialSigMsg]
    C -->|default| REJ2[ValidationReject]
    D --> D1[qbft.NewProcessingMessage]
    D1 -->|error| REJ3[ValidationReject]
    D1 --> D2[contr.BaseMsgValidation]
    D2 -->|error| REJ3
    D2 --> D3{isDecided?}
    D3 -->|yes| D4[qbft.ValidateDecided]
    D4 -->|error| REJ3
    D4 --> ACC[ValidationAccept]
    D3 -->|no| D5{Height > contr.Height?}
    D5 -->|yes| D6[validateFutureMsg]
    D6 -->|error| REJ3
    D6 --> ACC
    D5 -->|no| D7{StoredInstances.FindInstance?}
    D7 -->|found| D8[inst.BaseMsgValidation]
    D8 -->|error| REJ3
    D8 --> ACC
    D7 -->|not found| REJ3
    E --> E1[signedMsg.Validate]
    E1 -->|error| REJ4[ValidationReject]
    E1 --> E2{len OperatorIDs == 1?}
    E2 -->|no| REJ4
    E2 -->|yes| E3[types.Verify RSA sig]
    E3 -->|error| REJ4
    E3 --> E4[partialSigMsg.Decode + ValidateForSigner]
    E4 -->|error| REJ4
    E4 --> E5{PostConsensus?}
    E5 -->|yes| E6[ValidatePostConsensusMsg]
    E5 -->|no| E7[ValidatePreConsensusMsg]
    E6 -->|error| REJ4
    E7 -->|error| REJ4
    E6 --> ACC
    E7 --> ACC
Loading

Reviews (4): Last reviewed commit: "code review comments [2]" | Re-trigger Greptile

Comment thread p2p/validation/msg_validation.go
Comment thread p2p/spectest/run_test.go
Comment thread p2p/spectest/tests/msgvalidation/test.go
@nkryuchkov
Copy link
Copy Markdown
Contributor Author

@greptileai please review this again

@nkryuchkov
Copy link
Copy Markdown
Contributor Author

nkryuchkov commented Apr 3, 2026

@greptileai This does not reproduce on the current branch.

p2p/spectest/generate/tests.json keeps the aggregate keys as *msgvalidation.MsgValidationSpecTest_<test name>, so they still match reflect.TypeOf(&msgvalidation.MsgValidationSpecTest{}).String() in p2p/spectest/run_test.go.

sanitize() only affects the per-test filenames under p2p/spectest/generate/tests/; it does not rewrite the keys in the aggregate tests.json consumed by TestJson.

I ran go generate and then TestJson, and it passed.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 3, 2026

You're right, I was mistaken. The implementation is correct.

The aggregate tests.json preserves the full type string including the * in the keys (line 51-54 in main.go marshals all without sanitization), so *msgvalidation.MsgValidationSpecTest_<test name> splits correctly to match the type check in run_test.go:38.

The sanitize() call on line 47 only affects the individual test file paths, not the map keys used in the aggregate JSON. Sorry for the confusion.

@nkryuchkov
Copy link
Copy Markdown
Contributor Author

@greptileai review it again

Comment thread p2p/validation/msg_validation.go
@nkryuchkov
Copy link
Copy Markdown
Contributor Author

@greptileai review it again

@GalRogozinski
Copy link
Copy Markdown
Contributor

we need to decide whether we should go with #491 or this

Copy link
Copy Markdown
Contributor

@iurii-ssv iurii-ssv left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@momosh-ssv momosh-ssv self-requested a review April 21, 2026 10:21
Copy link
Copy Markdown

@momosh-ssv momosh-ssv left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two small nits on the partial-sig path — the hardening itself looks good.

return errors.New("partial signature msg allows 1 signer")
}
if err := types.Verify(signedMsg, runner.GetBaseRunner().QBFTController.CommitteeMember.Committee); err != nil {
return types.WrapError(types.MessageSignatureInvalidErrorCode, fmt.Errorf("msg signature invalid: %w", err))
Copy link
Copy Markdown

@momosh-ssv momosh-ssv Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason we're wrapping with MessageSignatureInvalidErrorCode here? The only caller (MsgValidation) checks err != nil and returns ValidationReject, so the code never surfaces — same story in validateFutureMsg:128.

Either propagate it upward for logs/metrics or drop the wrap to match the other plain-error returns in this function.

return err
}
if len(signedMsg.OperatorIDs) != 1 {
return errors.New("partial signature msg allows 1 signer")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be worth aligning this error string with validateFutureMsg:123 ("allows 1 signer"). An operator reading gossip-reject logs will hit both messages for effectively the same rule, and the slight difference in wording makes them look like distinct conditions.

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.

5 participants