Repository navigation
app: distribute cluster proposer config (fee recipient / gas limit) to validator clients #4692
Description
🎯 Problem to be solved
Charon currently ignores the VC's proposer config: prepare_beacon_proposer is a no-op (core/validatorapi/router.go, submitProposalPreparations) and register_validator is ignored (core/validatorapi/validatorapi.go, SubmitValidatorRegistrations). Charon injects the lock's fee recipient itself. As a result the VC-side fee recipient / gas limit are unmanaged and irrelevant today — CDVN VCs set none, and Teku even hardcodes --validators-proposer-default-fee-recipient 0x0000…0000.
Post-gloas the VC signs fee_recipient and target_gas_limit into SignedProposerPreferences (#4691), so its local config becomes load-bearing and must match the lock cluster-wide or the partial signatures won't aggregate. This issue: give Charon a way to push the cluster-canonical fee recipient + gas limit (lock FeeRecipientAddresses / TargetGasLimit) into the VCs.
This has independent, pre-gloas value — it makes VCs hold the correct fee recipient rather than relying on Charon's override — so it can land before the gloas duty.
Sub-task of the gloas epic #4324.
🛠️ Proposed solution
Findings
- No standard config file exists. The ecosystem is standardizing an API instead — keymanager-APIs#87 (
/eth/v1/validator/config, closed/contentious, not ratified), explicitly to kill the "different config dialect per client" burden it names for dappnode/eth-docker. - Per-client config surfaces (each gets a CDVN wrapper adapter rendering its native format):
- Prysm / Teku / Lodestar — one central
proposer_configJSON/YAML:--proposer-settings-file/--validators-proposer-config/--proposerSettingsFile. Rendered from the canonical file at container start (Teku/Prysm images gain a staticjq, Lodestar renders vianode). - Nimbus — per-validator files
<validators-dir>/<pubkey>/suggested_fee_recipient.hex+suggested_gas_limit.json, rendered per keystore. - Lighthouse —
validator_definitions.yml(keystore-entangled; standalone fee-recipient file removed); CDVN profile authors it from the canonical file on every container start (Add lighthouse validator client with proposer config charon-distributed-validator-node#487).
- Prysm / Teku / Lodestar — one central
- Applying updates (matters for coordinated
gas_limitbumps, which are applied staggered across operators): file rewrite + VC restart everywhere. File hot-reload exists only on Teku (--Xvalidators-proposer-config-refresh-enabled, per epoch) and Nimbus (per-validator files read live); Prysm/Lodestar re-read on restart; the CDVN Lighthouse adapter regeneratesvalidator_definitions.ymlfromproposer-config.jsonon every container start, so a restart applies updates there too.
Mechanism
File-based, exclusively. The keymanager API is ruled out permanently (decision 2026-09-17) — charon generates proposer-config.json, per-client adapters derive native formats from it, and updates apply via file rewrite + VC restart. Post-gloas this same file is the alignment surface for the values VCs sign (SignedProposerPreferences, and later the builder url/auth_data signed into BuilderRequestAuth), so it must be byte-consistent across the cluster.
Canonical charon schema (decision 2026-09-18, supersedes emitting Prysm's native format): the file is charon's own version: 1 schema — the Prysm/Teku shape minus all pre-gloas legacy fields (no builder.enabled, one gas_limit at the option level). It is sparse: default_config carries the per-field majority fee_recipient/gas_limit (plus the gloas builder block — min_bid, builder_boost_factor, max_execution_payment, builders[{url}] — when --builder-urls is set), and proposer_config lists only diverging validators, carrying only the diverging fields. Uniform contract: any absent field falls back one level up (entries → default_config, builders[] entries → the enclosing builder block); booleans are never optional (jq's // swallows false); all values are strings. A uniform cluster's file is ~330 bytes regardless of validator count. builder.enabled semantics moved out of the file entirely — wrappers source it from the BUILDER_API_ENABLED env var. All five VCs live-verified against the schema (charon + VC container + public hoodi BN).
Tasks
- Decide mechanism: (B) per-client file generation — one JSON parsed natively by Prysm/Teku/Lodestar (JSON is valid YAML); Nimbus + Lighthouse via generate-from-JSON adapters (LH: author the whole
validator_definitions.yml— a supported workflow — generate, not merge). Always-on (no feature flag — the diff is small and generation is best-effort/create-once, decided in review of app: generate proposer config file for validator clients #4700). - Resolve canonical config from the lock: keyed by this node's pubshares (the identities the VC manages);
fee_recipientfromFeeRecipientAddresses,gas_limitfromTargetGasLimitwith registration/30M fallback (app: generate proposer config file for validator clients #4700) -
keymanager pathruled out permanently — file-based is the mechanism - file path: generate
proposer-config.jsonnext to the lock, create-if-missing, best-effort (app: generate proposer config file for validator clients #4700) - Canonical sparse schema + builder section emission in charon: schema in app: generate proposer config file for validator clients #4700, builder flags +
builderblock +app_builder_config_*metrics + peerinfo config-hash mismatch detection in app: generate builder config for validator clients #4704 (both in review) - Adapters: CDVN
run.shrenders per VC (all 5) — Teku/Prysm render their native settings file at start (thin custom images adding a staticjq); Lodestar renders its settings YAML vianode; Nimbus renders per-validatorsuggested_fee_recipient.hex+suggested_gas_limit.jsonper keystore; Lighthouse authorsvalidator_definitions.ymlon every start. All resolve the sparse fallback (.proposer_config[$pk].X // .default_config.X). Wire charon proposer config into validator clients charon-distributed-validator-node#486 (+cmd: refactor to use create command #487 Lighthouse, app/log: fix logfmt bug #488 builder section for Prysm v2 + Lodestar) — in review, live-verified - CDVN wiring: mount the config dir per VC profile; replace Teku's hardcoded zero fee-recipient default (Wire charon proposer config into validator clients charon-distributed-validator-node#486, cmd: refactor to use create command #487)
- Forbid
charon alpha feerecipient fetchon post-gloas networks (cmd: forbid feerecipient fetch from the gloas fork #4707, in review) — post-gloas its overrides no longer reach the VC - New
charoncommand to modify the generatedvc-configfiles in sync (replaces the earlier "extendfeerecipient fetch" plan, which is parked): rewrite via temp-file+rename,sign→ quorum → fetch-style flow as the sync point, VC restart to apply; core: add gloas proposer preferences duty #4693's mismatch metric detects laggards - CDVN: dedicated env var feeding
--builder-urls(decided against reroutingMEV_RELAYS— relay URLs ≠ gloas builder URLs)
🔗 References
- Epic: Gloas hardfork #4324
- Proposer preferences duty: core: proposer preferences signed duty (gloas ePBS) #4691
Activity
Add lighthouse validator client with proposer config (#487)
Wire charon proposer config into validator clients (#486)