ObolNetwork / ObolNetwork/charon
core: proposer preferences signed duty (gloas ePBS)
@KaloyanTanev is already working on this.
Since Sep 9, 2026.
- Dominant language
- Go
- Stars
- 222
- Forks
- 138
- Avg merge
- 2d 6h
- Merged PRs (30d)
- 35
Description
🎯 Problem to be solved
From the Gloas fork onwards, prepare_beacon_proposer and register_validator are superseded by validator-signed SignedProposerPreferences, gossiped on the proposer_preferences topic, as the source of proposer fee_recipient and target_gas_limit. For a distributed validator this becomes a new ahead-of-time signed duty: aggregate the VCs' partial signatures and gossip the reconstructed signature before the proposal slot.
Sub-task of the gloas epic #4324 (Block production / ePBS). Gated upstream on attestantio/go-eth2-client#316 (submit proposer preferences).
🛠️ Proposed solution
Treat proposer preferences like sync committee messages: ungated (no consensus round), accept the VC's partial signature, aggregate on threshold, broadcast — returning 2XX to the VC on submission without blocking for threshold.
Key design facts (spec + Lighthouse impl)
- Signed message:
ProposerPreferences{dependent_root, proposal_slot, validator_index, fee_recipient, target_gas_limit}, domainDOMAIN_PROPOSER_PREFERENCESatproposal_epoch. There is no wall-clock timestamp — unlikeValidatorRegistrationV1.timestamp(the field that forces us to pre-sign registrations and ignore VC ones), so the VCs' partial signatures are aggregatable. - Only path is aggregating VC partials at runtime. Charon can't self-sign (no key material in the running process) and can't pre-sign offline (
proposal_slot+dependent_rootare live consensus values). dependent_root= shuffling anchor,compute_shuffling_dependent_slot(E) = start_slot(E-1)-1= last block of epochE-2(MIN_SEED_LOOKAHEAD=1). It points ~1–2 epochs into the past, so it's stable and agreed cluster-wide except on a pathological deep reorg. Charon serves it via the v2 proposer-duties response, so the cluster converges on identical messages by construction.- Ahead-of-time, not at-trigger. Broadcast window is the current epoch up to
MIN_SEED_LOOKAHEADepochs ahead; gossip validation[IGNORE] The proposal slot has not started yet. Missing it means the validator "will not accept any trustless bids for that slot" → self-build → lost MEV. - VC data source = v2 proposer duties (
apis/validator/duties/proposer.v2.yaml; v1proposer.yamlis deprecated). v2dependent_root = get_block_root_at_slot(state, start_slot(epoch-1)-1)= last ofE-2= exactly the anchor the preference needs (v1 usedstart_slot(epoch)-1=E-1, wrong here). Confirmed in Lighthousevalidator_services/src/proposer_preferences_service.rs(v8.2.0): reads(dependent_root, duties)from the proposer-duties cache, copiesdependent_rootverbatim, signs per-duty,POSTs SSZ to one BN; dedup republishes only whendependent_rootchanges. - Aggregation identity includes
dependent_root. On reorg it changes and VCs resubmit, so ParSigDB keying must treat a changeddependent_rootfor aproposal_slotas a fresh aggregation, not a collision with the stale one.
Content consistency (fee_recipient / gas_limit)
The VC signs fee_recipient and target_gas_limit from its own local config, not from the lock (Charon has no BN→VC channel for these). For the partials to aggregate they must match cluster-wide. Rather than a hard reject (which fragments intake during a staggered gas_limit bump — each node validates against its own lock), warn + emit a metric on mismatch vs the lock value and aggregate whatever reaches threshold (majority wins, self-heals). Getting the correct values onto the VCs in the first place is #4692.
Tasks
- Adopt
go-eth2-clientproposer-preferences interfaces (gated on attestantio/go-eth2-client#316) -
DutyProposerPreferencesduty type +DOMAIN_PROPOSER_PREFERENCESsigning domain - Core
(Versioned)ProposerPreferencestype +NewPartialSignedProposerPreferences(SSZ/JSON/proto encode-decode, parsig wire carries version) - Serve v2 proposer duties (
GET /eth/v2/validator/duties/proposer/{epoch}) with theE-2shufflingdependent_rootso cluster VCs sign identical messages -
validatorapi.SubmitProposerPreferencesintake — sync-message shaped: verify partial sig, group, fan out to subscribers, return2XXnon-blocking - ParSigDB / ParSigEx / SigAgg wiring; aggregation keyed by
(validator, proposal_slot, dependent_root); treat a changeddependent_root(reorg) as a fresh aggregation - Bcast: submit the reconstructed
SignedProposerPreferencesto the BN (→ gossip) ahead of the slot - Content check: warn + metric when VC-submitted
fee_recipient/target_gas_limitdiffer from the lock value (do not reject) - Tracker / reasons integration for the new duty
- validatormock support + simnet e2e test
🔗 References
- Epic: #4324
- VC config distribution: #4692
- Consensus spec — gloas validator.md (
get_signed_proposer_preferences,get_upcoming_proposal_slots) get_shuffling_dependent_root/compute_shuffling_dependent_slot- Beacon API — v2 proposer duties + proposer_preferences
- Upstream gate: attestantio/go-eth2-client#316
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.