Skip to content

feat(validator_store): sign builder request auth - #1282

Draft
shane-moore wants to merge 2 commits into
sigp:epbsfrom
shane-moore:feat/1278-sign-request-auth
Draft

feat(validator_store): sign builder request auth#1282
shane-moore wants to merge 2 commits into
sigp:epbsfrom
shane-moore:feat/1278-sign-request-auth

Conversation

@shane-moore

@shane-moore shane-moore commented Aug 27, 2026

Copy link
Copy Markdown
Member

Problem, Evidence, and Context

Anchor pins Lighthouse at e58ec88fe, which predates the Gloas builder-API stack (sigp/lighthouse#9803/#9804 merged; #9805/#9806/#9807 open, stacked) implementing beacon-APIs #630. At the stack head, ValidatorStore gains exactly one new required method, sign_request_auth_v1, so the pin bump and the implementation must land as one PR to compile. The signing side is the next step of the milestone after #1281 shipped the kind-9 admission rules.

Closes #1278. Plan and debate history live with the issue; the receive side is #1281; the config surface and service wiring follow in #1279/#1280.

Change Overview

  • Pin bump: 16 Lighthouse revs to the #9807 stack head, plus new builder_types/builder_store deps. Measured migration fallout across the 55 upstream commits: one missing trait method and one test reading fields that Use Gloas timings for aggregate attestations and sync committee messages lighthouse#9823 privatized (rewritten against the public basis-point inputs, which also makes it strictly stronger: it now pins the fork-gate polarity).
  • sign_request_auth_v1 mirrors sign_proposer_preferences: resolve validator and cluster, root = request_auth.signing_root(spec.get_request_auth_domain()) (fixed application domain, genesis fork version, zeroed genesis-validators-root; fork-epoch invariant, unlike kind 8's epoch-keyed domain), envelope slot = the proposal slot, CollectionMode::SingleValidator, failure classification and metrics siblings.
  • The collection bound is slot-aware rather than a copied constant: future proposal slots get 2 slots (caps head-of-line blocking of Lighthouse's sequential per-proposer preferences loop); the current slot gets a 1s fail-fast (the block service resolves the whole builder config before requesting a block, and post-restart current-slot quorum is essentially unreachable since peers broadcast partials once and their cache suppresses re-signing); elapsed slots are declined outright, without constructing the collection, broadcasting, or touching the failure metrics, because the Lighthouse preferences loop revisits every current-epoch proposer each tick and its cache prunes elapsed slots, making declines a steady-state path whose reporting would drown the real divergence signal.
  • The same elapsed-slot decline is applied to sign_proposer_preferences: after a restart the kind-8 service re-signs every unpublished current-epoch duty including passed slots, each burning the full bound sequentially for an unreachable quorum.
  • Client wiring: BlockServiceBuilder::build() at this pin fails at runtime without a BuilderStore and RequestAuthCache, so both are constructed. The builder definitions file starts empty, but empty-at-creation is not a gate: an operator who populates <data_dir>/builder_definitions.yml and restarts activates the produce-time direct-builder flow on this branch alone, with an insert-only RequestAuthCache (its only prune caller arrives in feat(client): wire builder preferences service and produce-time builder config #1280). Deployment gate: feat(client): wire builder preferences service and produce-time builder config #1280 must merge with or immediately after this PR, and no image or soak may be built from an epbs head containing only this PR.

Reading order: validator_store/src/lib.rs (consts + RequestAuthCollectionBound + the two sign paths), then metrics.rs, then client/src/lib.rs and data_dir.rs (wiring), then the test modules.

What did not change: the 23 previously implemented trait methods keep their signatures; kind-8 behavior for current and future slots is untouched; no slashing DB or doppelganger involvement (same class as validator registration); the Lighthouse [patch.crates-io] mirror is byte-identical between the pins.

Risks, Trade-offs, and Mitigations

Validation

  • cargo check --workspace --all-targets --locked: clean (this catches the test-target fallout a plain check misses).
  • make test (release): 945 passed, 0 failed, including the ssv-spec fixture suite.
  • New tests: 4 in testing/request_auth.rs (signing root with a fork-boundary falsifiability guard against regressing to the kind-8 domain, folded slashing-DB non-involvement; table-driven three-case timeout incl. decline with zero collector calls and zero metric delta; failure classification labels; independently derived known-answer signing-root vector with its derivation script in the comment) plus 1 in testing/proposer_preferences.rs (kind-8 elapsed-slot decline).
  • make cargo-fmt-check, make lint, make sort: clean.
  • Startup smoke of the new BlockService wiring needs a synced beacon node and is deferred to the milestone's ssv-mini soak; the wiring mirrors Lighthouse's own VC construction line for line.

Rollback

Revert the single commit: the branch is one commit on top of epbs, no schema or config migration. The only on-disk artifact is an empty builder_definitions.yml in the datadir, which older versions ignore.

Blockers / Dependencies

Bump the Lighthouse pin to the Gloas builder-API stack head (sigp/lighthouse#9807)
and implement the one new required trait method, sign_request_auth_v1, as a
distributed threshold signing round: kind 9 (RequestAuth) riding
Role::ProposerPreferences under the fixed builder-specs sigp#165 application domain,
with a slot-aware collection bound (future slots 2 slots, current slot 1s
fail-fast, elapsed slots declined without broadcast). Wire the BuilderStore and
RequestAuthCache the new BlockServiceBuilder requires at startup, decline
elapsed-slot proposer-preferences signing after restarts, and migrate the one
test broken by the pin (private attestation-due fields).
@shane-moore shane-moore changed the title feat(validator_store): sign builder request auth via the Lighthouse builder-API pin feat(validator_store): sign builder request auth Aug 27, 2026
@shane-moore

Copy link
Copy Markdown
Member Author

@claude, please take a look

@claude-code-actions-sigp

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

Copy link
Copy Markdown

Claude finished @shane-moore's task in 3m 58s —— View job


Review: feat(validator_store): sign builder request auth

Reviewed against origin/epbs. This is a well-constructed PR — the sign_request_auth_v1 path faithfully mirrors sign_proposer_preferences, the slot-aware bound is cleanly modeled as an enum, and the test suite (falsifiability guards, hand-derived KAT, table-driven timeout, first-poll decline assertions) is unusually thorough. The kind-8/kind-9 domain separation, the Role::ProposerPreferences re-use, and the PartialSignatureKind::RequestAuth = 9 discriminant all line up with the intent documented in partial_sig.rs.

I found no correctness or safety bugs. A few observations below, all minor / non-blocking.

Observations (non-blocking)

1. Declines are entirely silent in metrics (lib.rs:4197-4206, lib.rs:4282-4291)
The elapsed-slot decline path returns CollectionTimeout without touching any counter — deliberately, to keep structural noise out of the divergence metric, which I agree with. But it also means there is zero observability into decline frequency. If declines ever spike unexpectedly (a genuine bug signal — e.g. a clock/lookahead regression re-signing the wrong slots), nothing surfaces it except debug! logs. Consider a dedicated low-cardinality declined counter (separate from *_RECONSTRUCTION_FAILURES, so it can't pollute the divergence signal). Suggestion only.

2. Overloading CollectionError::CollectionTimeout for a decline (lib.rs:4204, lib.rs:4288-4290)
A decline is semantically distinct from a timeout (no collection was ever attempted), yet both surface the same error. The code comments justify this as "same outcome for the caller, minus the wait," and the receiver (Lighthouse) treats them identically, so this is defensible. Flagging only because a future reader debugging a CollectionTimeout in logs may be misled into looking for a stuck collector that never existed. A dedicated SpecificError variant would read more honestly, at the cost of a new error the caller maps the same way.

3. builder_definitions_dir() returns the datadir root (data_dir.rs:96-98)
The _dir suffix returning self.path.clone() (root, not a subdir) is intentional per the doc comment ("mirroring Lighthouse's placement... directly in its validators dir"), and BuilderStore::open_or_create presumably creates builder_definitions.yml inside it. Worth a quick sanity check that open_or_create treats the argument as a directory and not a file path — if it expects the file itself, this would be silently wrong. Low confidence; likely fine since the wiring mirrors Lighthouse.

4. Decline logic duplicated in two shapes
sign_proposer_preferences inlines proposal_slot < current_slot, while sign_request_auth_v1 routes through request_auth_collection_bound. They legitimately differ (kind-8 uses the full 2-slot bound for current+future; kind-9 fail-fasts the current slot), so a shared helper wouldn't fit cleanly. Noting only for awareness — not worth refactoring.

Verification

  • I did not run make test / make lint: per the PR body the pin points at the still-open Add Gloas bid selection, block production, and bid gossip processing (Gloas builder API 3/5) lighthouse#9805-#9807 stack, so a clean checkout here won't build until the pin is re-pointed. Relying on the PR's own reported cargo check --workspace --all-targets --locked + make test (945 passed) results.
  • The METRIC_TEST_LOCK split (one per module, guarding disjoint metric families) is correct — no cross-contamination between the kind-8 and kind-9 metric tests.

The blockers you already documented (re-point the pin to an unstable SHA, re-run the migration probe/KAT, re-verify RequestAuthPartialSig = 9 against ssv-spec#632) remain the right gates before undrafting.
· feat/1278-sign-request-auth

@codecov-commenter

codecov-commenter commented Aug 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.40449% with 16 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (epbs@7c428cf). Learn more about missing BASE report.

Files with missing lines Patch % Lines
anchor/client/src/lib.rs 0.00% 6 Missing ⚠️
anchor/common/global_config/src/data_dir.rs 0.00% 3 Missing ⚠️
anchor/validator_store/src/testing/request_auth.rs 98.95% 3 Missing ⚠️
anchor/validator_store/src/lib.rs 97.77% 2 Missing ⚠️
...alidator_store/src/testing/proposer_preferences.rs 95.74% 2 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             epbs    #1282   +/-   ##
=======================================
  Coverage        ?   78.67%           
=======================================
  Files           ?      176           
  Lines           ?    39430           
  Branches        ?        0           
=======================================
  Hits            ?    31020           
  Misses          ?     8410           
  Partials        ?        0           
Flag Coverage Δ
rust 78.67% <96.40%> (?)

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 e4f1fea9.

I traced the request-auth domain and root, kind-9 and role-8 wire path, proposal-slot timing, collection mode, BuilderStore and RequestAuthCache wiring, and tests against the current builder spec, SIP-94, ssv-spec, go-ssv, and the pinned Lighthouse stack. I found no correctness, safety, or specification defect in the implementation.

Two follow-ups before this draft is ready:

  1. check-fmt currently fails on the new doc comment around anchor/validator_store/src/lib.rs:1391. make cargo-fmt applies the required reflow.
  2. This pin range includes Lighthouse #9869, a lockfile-only update from blst 0.3.16 to 0.3.17 for the Pippenger divide-by-zero fixed in blst #283 and reproduced on a 96-core host. Anchor's downstream lock remains on 0.3.16, so the pin bump does not inherit that hardening. Please carry 0.3.17 during the final repin. I did not establish that a normal Anchor duty reaches the affected large-point branch, so I view this as dependency hardening, not a demonstrated duty-path bug.

The existing draft blocker on Lighthouse #9805 through #9807 landing and the pin being repointed remains valid. At this head, the local testnet and every completed code, test, MSRV, dependency, Docker, and CLI check pass except formatting; the aggregate suite skips because of that failure.

Reviewed by gpt-5.6-sol max.

Review follow-ups: apply the nightly rustfmt reflow check-fmt requires on the
request_auth_collection_bound doc comment, and carry the blst 0.3.16 -> 0.3.17
lockfile hardening (Pippenger divide-by-zero, blst sigp#283) that Lighthouse #9869
took upstream but Anchor's lock did not inherit.
@shane-moore

Copy link
Copy Markdown
Member Author

addressed both in d1a56cd: fmt reflow applied, blst carried to 0.3.17 now rather than at repin (full release suite green on the bump)

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