feat(client): spawn the builder preferences service - #1285
Conversation
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).
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.
Spawn Lighthouse's BuilderPreferencesService inside the Gloas-scheduled gate so builder preferences publish ahead of time for current- and next-epoch proposal duties, and hoist a single RequestAuthCache shared via Arc-backed clones with the block service; the service's per-slot tick is the cache's only prune caller at the Lighthouse pin.
|
@claude, please take a look |
|
Claude finished @shane-moore's task in 3m 0s —— View job Review: PR #1285 — spawn builder preferences service
Scope note: This is a stacked PR. The only commit owned here is Head commit (
|
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## epbs #1285 +/- ##
=======================================
Coverage ? 78.50%
=======================================
Files ? 176
Lines ? 39092
Branches ? 0
=======================================
Hits ? 30689
Misses ? 8403
Partials ? 0
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Reviewed only the top commit, 89d01ca5, against its direct parent, d1a56cdd.
The Anchor wiring itself is correct: the block service and builder-preferences service share one Arc-backed store and request-auth cache, and the new service starts under the existing Gloas gate. I found one upstream behavior worth noting during soak:
- The pinned service derives one
current_fork, gathers duties from both the current and next epochs, then submits every entry with that current fork. In epochgloas_fork_epoch - 1, it skips the current Fulu duties, gathers next-epoch Gloas duties, and sends their Gloas-only bodies withEth-Consensus-Version: fulu. The merged validator API says the header names the version the submitted preferences belong to, while the builder spec defines a Gloas request body and says the header names the fork of the body's type. A builder that dispatches decoding by this required header can reject every pre-fork attempt, so the first Gloas epoch loses its intended ahead-of-time publication window. This behavior is in the pinned dependency, not the Anchor wiring reviewed here.
All CI checks, including run-local-testnet, pass at this head. The existing draft and soak gates remain.
Reviewed by gpt-5.6-sol max.
Note
Stacked on #1282 (
feat/1278-sign-request-auth); the diff shows its commits until it merges. Only the head commit is this PR. Merge/deploy order for the stack: #1282 -> this PR -> #1283, with no soak or release image from an intermediate head.Problem, Evidence, and Context
Closes #1280. Last piece of the direct-builder-connections milestone: #1282 wired the
BuilderStoreandRequestAuthCacheinto the block service (both are required byBlockServiceBuilder::build()at the Lighthouse pin), but nothing publishes builder preferences ahead of time and nothing prunes the cache.RequestAuthCache::prunehas exactly one caller in the entire pin, insideBuilderPreferencesService, so until that service runs the block service's cache is insert-only.One premise of the issue text went stale at the pin: there is no
BuilderConfigsetter on theBlockServiceBuilder. The block service resolves the builder config per proposal itself, and #1282 already shipped that produce-time half. What remained here is the service spawn plus single-instance sharing.Change Overview
One file,
anchor/client/src/lib.rs. A singleRequestAuthCacheis hoisted next to theBuilderStoreconstruction and both are clone-shared (both types are Arc-backed) between the block service and a newly spawnedBuilderPreferencesService, started inside the existingis_gloas_scheduledblock besideProposerPreferencesService. This mirrors Lighthouse's own VC wiring one for one.Read the diff top to bottom: the import, the hoist plus comment rewrite at the block-service builder, then the spawn in the Gloas block.
Not changed: the produce-time path (shipped in #1282), the builder definitions validation (#1283), and Gloas-unscheduled networks, where the service does not spawn and the cache has no insert path either.
Risks, Trade-offs, and Mitigations
The diff adds no Anchor logic but activates a pinned Lighthouse task. Known degraded modes at this pin, all confined to a feature that is off by default (empty builders file):
DeclinePastSlotfor every elapsed proposal slot for the rest of its epoch. The decline is free by design (feat(validator_store): sign builder request auth #1282: no collection, no broadcast, no partial-signature traffic, excluded from the failure reporter), so the cost is repeated per-slot resolution work rather than anything operators see. Re-pin ask: elapsed-slot filter in the service loop. CORRECTED after the live run: an earlier version of this line claimed the decline produced a per-builder error log from Lighthouse'sbuilder_storeevery slot. It does not. Anchor's log filter (anchor/logging/src/utils.rs) allowlists workspace crates plus five Lighthouse crates andbuilder_storeis not among them, so that crate's output is dropped entirely. Measured: 17 debug decline lines and zero error or warn lines mentioning builders, at--debug-level debug.None of these can reach a release: this PR stays draft until the Lighthouse re-pin, where the asks above get resolved or the residual modes get explicitly accepted.
Validation
make cargo-fmt,make cargo-fmt-check,make lint: clean.cargo check --workspace --all-targets: clean.PayloadAttestationServiceandProposerPreferencesServiceshipped identically) and the produce body is pinned Lighthouse code from feat(validator_store): sign builder request auth #1282, unchanged here.Validated live on ssv-mini 2026-08-27, the first end-to-end exercise of the Gloas direct-builder preference flow. Profile: 4 Anchor operators (quorum 3), 10 managed validators, Gloas at epoch 2, a beacon node built from Anchor's exact Lighthouse pin
44f442479(replacing Lodestar, which has no such route), and a builder receiving the submissions./eth/v1/validator/duties/proposer/2, those are exactly this cluster's three proposer duties in epoch 2 out of 32 slots, with no misses and nothing spurious. The per-epoch fork gate held: no pre-Gloas slot was ever published.async_tasks_count{async_task_count="builder_preferences_service"} 1.anchor_signed_request_auth_total{status="success"}held at 9, one per duty, while the service re-resolved the whole builder config every slot. Repeat resolutions are cache hits, not re-signings, which is the property the single hoistedRequestAuthCacheexists to provide.Rollback
Revert the commit; no config, data, or wire format changes. With the service unspawned, behavior returns to the #1282 state.
Blockers / Dependencies
Undraft gates, identical to the stack's plus soak scenarios: Lighthouse builder-API stack merges to unstable and Anchor re-pins (resolving or explicitly accepting the risk items above), migration probe rerun, ssv-spec kind-9 constant, go-ssv parity reverification, and an ssv-mini soak covering: a 2-of-4 sub-quorum entry plus a common builder across consecutive future proposer duties (common preference reaches the builder before each proposal slot), restart with past/current/future duties, empty config producing zero preference POSTs, with builder-side receipt as the observable.
Additional Info / Next Steps
After this merges, #1283 resolves a small known conflict in the same lib.rs region (its earlier
open_and_validateconstruction plus this PR's clones and spawn).