Skip to content

feat(message_validator): admit builder request auth partial signatures - #1281

Merged
shane-moore merged 1 commit into
sigp:epbsfrom
shane-moore:feat/1277-request-auth-admission
Aug 26, 2026
Merged

feat(message_validator): admit builder request auth partial signatures#1281
shane-moore merged 1 commit into
sigp:epbsfrom
shane-moore:feat/1277-request-auth-admission

Conversation

@shane-moore

Copy link
Copy Markdown
Member

Problem, Evidence, and Context (Required)

Change Overview (Required)

  • PartialSignatureKind::RequestAuth = 9 added; the role-8 gate admits kinds 8 and 9; kind 9 gets its own budget of 8 distinct signing roots per (MessageId, operator, proposal_slot), tracked separately from the 4-root preferences budget (SIP-94 §7: neither consumes the other). Membership is checked before capacity so a recorded root stays IGNORE when the set is full; duplicate-root and over-cap are both Ignore-class.
  • Both kinds' root sets now live in one lazily-boxed SigningRootBudgets on SignerState, allocated on the first role-8 packet, so the other roles' ring entries shrink from an inline map to one pointer.
  • Reading order: ssv_types/partial_sig.rs (wire kind) -> partial_signature.rs (role gate) -> duty_state.rs (budget + storage) -> message_counts.rs / lib.rs (compile-forced no-op arms, doc generalization).
  • Intentionally unchanged: every other role-8 rule is role-keyed and inherited with zero edits (fork gate, earliness/lateness, duty limit, one-entry rule, proposer-assignment check, monotonic-slot exemption, doppelganger freshness exemption). No code emits kind 9. Kind-8 classification behavior is unchanged (error got strings now use the kind's Debug name).

Risks, Trade-offs, and Mitigations (Required)

  • Rollout: nodes without this change REJECT kind-9 traffic and penalize forwarding peers, which is why this ships and propagates well before any emitter.
  • Accepted kind-9 roots spawn passive signature collectors on share-holding nodes (same shape as kind 8 today); bounded by the 8-root budget and the collector slot-cutoff cleaner, and reaching the ceiling requires a registered operator's key.
  • The 8 cap is the SIP §5 policy bound (wire container allows 64); provenance is documented at the constant, and feat(client): add builder definitions config for direct builder connections #1279 carries a config-load fail-fast acceptance criterion so a >8-entry config cannot silently lose auth roots on gossip.

Validation (Required)

  • New tests pin the full request-auth state machine: 8 distinct roots accept, the 9th is IGNORE and stays unrecorded on retry, a repeat of an accepted root at a full set is IGNORE as a duplicate (membership before capacity), a root recorded under kind 8 is not a duplicate under kind 9, and an interleaved test proves the two budgets are independent in both directions.
  • Role sweep asserts every non-role-8 role rejects kind 9, with a compile-time exhaustiveness guard on the role list; SSZ layout test pins [9,0,0,0,0,0,0,0].
  • make cargo-fmt-check and make lint clean; cargo test -p ssv_types --lib 142 passed; cargo test -p message_validator --lib 132 passed; cargo check --workspace green.
  • Reviewed against SIP-94 §7 rule-by-rule (role table, dedup bullet, deployment note) plus four independent review lanes; the SIP's "type-9 adds no distinct duty slots" line is implemented via the stricter shared-ring-entry reading, noted at the duty-limit arm.

Rollback (Required for behavior or runtime changes; optional otherwise)

  • Revert the commit. No config, data, or operational impact: no emitter exists, and reverted nodes simply return to REJECTing kind-9 traffic.

Blockers / Dependencies (Optional)

Accept and forward RequestAuthPartialSig (kind 9) on
Role::ProposerPreferences without emitting any, per SIP-94 §5/§7: add
the ssv_types variant, widen the role's kind gate, and give the new
kind its own budget of 8 distinct signing roots per (MessageId,
operator, proposal_slot), independent of the 4-root preferences budget.
Lands ahead of any emitter because a container with an unknown kind
fails SSZ decode and is REJECT-classed, penalizing forwarding peers.

Closes sigp#1277
@shane-moore

Copy link
Copy Markdown
Member Author

@claude, please take a look

@claude-code-actions-sigp

claude-code-actions-sigp Bot commented Aug 26, 2026

Copy link
Copy Markdown

Claude finished @shane-moore's task in 6m 18s —— View job


Review: feat(message_validator): admit builder request auth partial signatures

  • Gather context (read diff & changed files)
  • Review ssv_types/partial_sig.rs (wire kind)
  • Review partial_signature.rs (role gate)
  • Review duty_state.rs (budget + storage)
  • Review message_counts.rs / lib.rs
  • Post review findings

Reviewed against origin/epbs. This is a clean, admission-only change and I found no correctness, safety, or security issues. The core mechanics hold up under scrutiny:

Exhaustiveness is enforced everywhere it matters. PartialSignatureKind::try_from (partial_sig.rs:57), root_budget (duty_state.rs), and both message_counts.rs arms use explicit variant lists rather than wildcards, so a future kind can't silently fall through to "no budget + no counter." The invalid-variant SSZ/try_from tests correctly slid from 9 to 10. Good discipline.

The dual-budget refactor is sound. root_budget(kind) returns Option<(&mut HashMap<…>, usize)>; the &mut self borrow is released before record_partial_signature is called (duty_state.rs:147), so no borrow conflict. update_for_partial_signature runs after verify_message_signature (partial_signature.rs:63-76), so only fully-verified packets ever allocate the lazily-boxed SigningRootBudgets — the "invalid packets don't poison the set" tests confirm this. Membership-before-capacity ordering is preserved for both kinds.

Slot/topic-validation gap that motivated the PR is genuinely closed. extract_slot (message.rs:294) decodes any well-formed PartialSignatureMessages regardless of kind, so a kind-9 container no longer fails decode → UnknownMessageSlot → REJECT. Kind 9 inherits the role-8 fork gate, one-entry cap (partial_signature.rs:329), and the duty_limit/shared-ring-entry reading unchanged.

Test coverage matches the claims. The independent-budgets interleave test is the strongest one — it proves both directions (kind-8 cap hit with kind-9 headroom left, cross-kind root accepted, kind-9 cap on its own budget) and its const _: () = assert!(PREF_CAP < REQ_CAP) guards the interleave's structural assumption. The role sweep's _role_list_is_exhaustive compile-time guard is a nice touch.

Non-blocking notes (no action required)

  • Option::get_or_insert_default (duty_state.rs:328,332) stabilized in Rust 1.83. Since the PR reports cargo check --workspace green, the toolchain is fine — flagging only so it's on record as an MSRV floor for anyone backporting.
  • Passive-collector fan-out is correctly called out in the PR's Risks section: an accepted kind-9 root can spawn a passive collector on share-holding nodes, bounded by the 8-root budget × SLOTS_PER_EPOCH and gated behind a registered operator's key + proposer-assignment. Worth keeping an eye on during the soak, but the bound is real and this is the same shape kind-8 already has.
  • Signature-collector / doppelganger paths (signature_collector/src/lib.rs:102, operator_doppelganger) use _ => None wildcards, so kind 9 falls through as a no-op there — correct for admission-only, since no emitter exists yet. Just note these are the spots the feat(validator_store): sign builder request auth via the Lighthouse builder-API pin #1278/feat(client): wire builder preferences service and produce-time builder config #1280 emitter work will need to extend.

One thing to double-check before merge, per your own blocker note: re-confirm the ssv-spec #632 wire constant (RequestAuthPartialSig = 9) is still 9 since that PR is open — the whole admission relies on that value.

Verification note: I did not execute the test suite (cargo test/make lint require approval in this environment), so the pass counts are the author's reported results, not independently re-run here.
· feat/1277-request-auth-admission

@shane-moore

shane-moore commented Aug 26, 2026

Copy link
Copy Markdown
Member Author

@claude, re-checked ssv-spec#632 at head de34c611: RequestAuthPartialSig = PartialSigMsgType(9) still pinned by its golden test, and the workspace already sets rust-version = "1.91.0" so the get_or_insert_default MSRV note is covered. no changes needed.

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.14126% with 5 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (epbs@ed44fa0). Learn more about missing BASE report.

Files with missing lines Patch % Lines
anchor/message_validator/src/partial_signature.rs 97.87% 5 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             epbs    #1281   +/-   ##
=======================================
  Coverage        ?   78.45%           
=======================================
  Files           ?      175           
  Lines           ?    38988           
  Branches        ?        0           
=======================================
  Hits            ?    30589           
  Misses          ?     8399           
  Partials        ?        0           
Flag Coverage Δ
rust 78.45% <98.14%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@shane-moore shane-moore left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Reviewed at 4a4a82dd.

Ship-shaped. I found no correctness, safety, security, or specification-alignment issues. The kind-9 wire value, role binding, independent per-kind root budgets, Ignore classifications, and update-after-signature-verification ordering match the current SIP-94 and ssv-spec #632 heads.

Non-blocking: two added test comments in anchor/message_validator/src/partial_signature.rs (lines 3997 and 4075) use U+2014 punctuation, which conflicts with the repository style rule.

Reviewed by gpt-5.6-sol max.

@shane-moore
shane-moore merged commit 7c428cf into sigp:epbs Aug 26, 2026
21 checks passed
@shane-moore
shane-moore deleted the feat/1277-request-auth-admission branch August 26, 2026 23:42
@claude-code-actions-sigp

claude-code-actions-sigp Bot commented Aug 26, 2026

Copy link
Copy Markdown

Claude finished @shane-moore's task in 48s —— View job


Acknowledged — both loose ends closed, no changes needed

Thanks for chasing these down.

  • ssv-spec#632 wire constant — you re-checked at head de34c611: RequestAuthPartialSig = PartialSigMsgType(9), still pinned by its golden test. That was the one external dependency the whole admission path rested on, so with it confirmed at 9 the kind binding is solid.
  • get_or_insert_default MSRV — confirmed independently: the workspace Cargo.toml sets rust-version = "1.91.0" (anchor/Cargo.toml:6), well above the 1.83 stabilization floor I'd flagged. The note was only ever a backport-awareness flag, and it's fully covered.

Nothing outstanding from my review. The one nit from the human review (U+2014 em-dashes in the two added test comments in partial_signature.rs) is a style-rule item, not a correctness one — worth a quick sweep if it hasn't already been folded in, but not a blocker.

Since this is already merged to epbs, the operational reminder still stands per your PR notes: the #1278/#1280 emitters must not ship until this admission change has propagated network-wide. Good to go.
· claude/pr-1281-20260826-2342

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.

2 participants