proposals: mev/commit-boost driven MEV flow - #2855
Conversation
Codecov Report❌ Patch coverage is ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Greptile SummaryThis PR adds an opt-in MEV-optimized block-fetch path (
Confidence Score: 5/5Safe to merge: the new MEV-optimized path is purely additive and opt-in, and the legacy path is preserved bit-for-bit for all existing configurations. The change is well-structured with clear mutual exclusion between paths, comprehensive behavioral and config-layer tests, and correct context-cancellation handling throughout. The two flagged items are minor resource/observability concerns that do not affect correctness. No files require special attention; beacon/goclient/proposer.go carries the most logic but is well-tested by the new dispatch test file. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[GetBeaconBlock called] --> B{len clients == 1?}
B -- yes --> C[fetchProposal direct]
C --> D{useSlotRelativeFetch?}
D -- yes --> E[waitUntilProposalSoftDeadline\nblock until slot_start + deadline]
D -- no --> F[return block immediately]
E --> F
B -- no --> G{useSlotRelativeFetch?}
G -- no --> H[getProposalParallelLegacy\nrace BNs, early-exit on blinded\nrelative proposalSoftTimeout]
G -- yes --> I[getProposalParallelByDeadline\nspawn per-BN fetchers]
I --> J{collect loop\nuntil softCtx deadline}
J -- res received --> K{all failed AND\nbestProposal==nil?}
K -- yes --> L[bail early\nwaitForFirstValidProposal]
K -- no --> M[update bestProposal\nif higher score]
M --> J
J -- softCtx.Done --> N{bestProposal != nil?}
N -- yes --> O[return bestProposal]
N -- no --> L
L --> P{ctx.Done?}
P -- yes --> Q[return ctx.Err]
P -- no --> R{first valid\nproposal received?}
R -- yes --> O
R -- no, all failed --> S[return all-clients-failed error]
Reviews (3): Last reviewed commit: "adjustmets" | Re-trigger Greptile |
Address review feedback on #2855: - cli/operator/node.go: ms-unit consistency in validateProposerDelayConfig error message and warn-log fields. The zap.Duration fields are renamed to proposer_delay_ms / max_safe_proposer_delay_ms (Int64) so the unit is explicit in structured log output. - docs/MEV_CONSIDERATIONS.md: - Configuration knobs: state enable_timing_games defaults to false. - Example A: parenthetical noting the 50ms overhead assumes BN and PBS co-located with SSV. - Example B: add second relay to match Example A's structure. - Appendix A: clarify that the 200ms MEVBoostRelayTimeout figure assumes legacy single-shot PBS behavior; not relevant when running timing-games-capable PBS.
Test_validateProposerDelayConfig was asserting on the old zap field names (proposer_delay, max_safe_proposer_delay) and time.Duration values. The previous follow-up commit renamed these to proposer_delay_ms / max_safe_proposer_delay_ms with int64 type. Update the assertions to match. Fixes the unit-test job on #2855.
|
@greptile pls re-review |
8e0c9df to
d0c5c0e
Compare
4c4bde2 to
35f1664
Compare
35f1664 to
2006437
Compare
72758bf to
f7fe8e6
Compare
f9eb2ee to
92fc1b2
Compare
… paths + rewrite MEV doc Config resolution + validation lives in cli/operator/config.go behind config.resolveAndValidate (consolidated in stage via #2866); this commit extends that resolution with block-fetch path selection and has goclient consume the resolved values. beacon/goclient: - New BlockFetchPath enum (safe / legacy / MEV-optimized) + dispatch in GetBeaconBlock. - getProposalParallelByDeadline (slot-relative deadline; safe early-exits on first blinded, MEV-optimized collects best-scored bid) + getProposalParallelLegacy (relative-timeout, preserved). Options gains ProposalSoftDeadline + BlockFetchPath. - New() applies the CommonTimeout/LongTimeout defaults directly; NewOptions removed. goclient consumes pre-resolved, pre-validated values — it owns mechanism, not config policy. cli/operator/config.go: - resolveAndValidate extended with block-fetch resolution (resolveBlockFetch): determine the path, validate the path-specific knob (legacy ProposerDelay / MEV ProposalSoftDeadline), resolve defaults onto cfg.ConsensusClient, set BlockFetchPath, and emit advisory warnings. - determineBlockFetchPath + validateProposalSoftDeadline + the bound consts moved here from goclient (business policy lives at the config boundary). cli/operator/node.go: drop the separate goclient.NewOptions step — resolveAndValidate (already run before goclient.New) resolves the block-fetch values onto cfg.ConsensusClient, which goclient.New now consumes directly. docs/MEV_CONSIDERATIONS.md + config.example.yaml: rewrite around PBS-side timing games, with ProposerDelay reframed as the legacy path.
- goclient.New: reject multi-BN clients whose selected path lacks its timing knob (safe/mev need ProposalSoftDeadline > 0, legacy needs ProposalSoftTimeout > 0). Such Options previously degraded silently to "return first valid response" via an already-expired collection window. - cli/operator: de-conflate resolveBlockFetch — snapshot the raw operator inputs up front so path selection never observes a value resolution produced; determineBlockFetchPath now takes the timing durations directly instead of the whole goclient.Options. Document the run-once invariant. - Fix the ProposerDelay env-description's dead doc anchor (the MEV-doc rewrite removed its target heading) and restore the 500ms-floor note on the ProposalSoftTimeout env-description. - Add end-to-end coverage for the legacy block-fetch path (early-exit-on-blinded + relative-soft-timeout fallback), which lost its behavioral test when TestGetProposalParallel_MultiClient was repointed to the safe path. - Assert the mev-optimized "exceeds safe-max" startup warning.
goclient no longer carries the operator-facing safe/legacy/MEV-optimized
block-fetch "path" enum or its three-way dispatch. The multi-BN block-fetch
strategy is now expressed as two mechanical Options knobs that cli/operator
resolves the path down to:
- ProposalCollectionSlotRelative: slot-relative deadline (true) vs legacy
relative timeout (false)
- EarlyExitOnBlinded: stop collecting on the first blinded (MEV) response
GetBeaconBlock dispatches on these via a 2-way branch instead of switching on
the enum, and New's multi-BN precondition validates the matching duration knob.
The path concept (and its String() label for the startup log line) moves into
cli/operator as policy vocabulary; resolveBlockFetch maps each path to the knob
pair. Runtime behavior is unchanged.
Multi-BN test clients now set the mechanical knobs explicitly; the path String()
test moves to cli/operator alongside the type.
4054c1d to
e40a5fe
Compare
…op safe path ProposalSoftDeadline (the MEV-optimized path) is now a slot-relative floor applied identically to single- and multi-BN setups: the fetched block is held until slot_start + ProposalSoftDeadline before QBFT starts, so every operator in the cluster starts QBFT at the same slot-relative time — aligning their proposer round timers and improving consensus convergence. Previously single-BN ignored the deadline entirely and multi-BN early-exited on the first blinded block, so neither delivered a cluster-aligned QBFT start. Multi-BN MEV-optimized no longer early-exits; it collects to the deadline and proposes the best-scored bid (bailing early only if every BN failed). Remove the `safe` block-fetch path: the default reverts to `legacy` (the original relative-timeout behavior). safe's early-exit meant it never aligned QBFT start, so it added a path and a knob without the benefit — operators who want MEV/alignment opt into ProposalSoftDeadline, the rest stay on legacy. Remove the now-redundant EarlyExitOnBlinded and ProposalCollectionSlotRelative Options knobs; goclient derives the path from ProposalSoftDeadline > 0 (useSlotRelativeFetch). Update tests (incl. new single-BN floor coverage) and docs/MEV_CONSIDERATIONS.md + config.example.yaml.
- simplify the per-request deadline to a single min() expression - explain the ~50ms BN→SSV transport margin and distinguish it from BlockSubmission - retarget 6 broken internal anchors to existing sections
…side links - remove timeout_get_payload_ms = 4000 (just restates both PBSes' default) - close unbalanced paren in Example A SSV-side caption - standardize #ssv-side-configuration link text to "SSV-side configuration"
…100ms - express the margin as a measured ~50–100ms range; note it's BN block-assembly + one-way network and to round up (under-shooting costs MEV, not slots) - set examples to the conservative 100ms end: ProposalSoftDeadline 1150ms / 1900ms, with header-arrival and slot-budget figures updated to match
…ngerousProposalSoftDeadline - ProposalSoftDeadline > 1450ms now fails startup unless AllowDangerousProposalSoftDeadline is set (then allowed up to the 3600ms hard max), mirroring AllowDangerousProposerDelay - add the flag to goclient.Options (eth2) + ALLOW_DANGEROUS_PROPOSAL_SOFT_DEADLINE env - keep the safe-max WARN (now reached only once the flag is set) - update tests + docs (Example B now requires the flag) + config.example.yaml
|
|
||
| fields := logs[0].ContextMap() | ||
| require.Equal(t, int64(1850), fields["proposal_soft_deadline_ms"]) | ||
| require.Equal(t, int64(1450), fields["safe_max_ms"]) |
There was a problem hiding this comment.
Seems that this assertion expects safe_max_ms, but resolveBlockFetch logs the field as safe_max_proposal_soft_deadline_ms (config.go:230), so ContextMap() returns nil here and this subtest fails:
go test -run Test_resolveBlockFetch ./cli/operator/
--- FAIL: .../mev-optimized_above_safe-max_with_flag_-_warns_with_ms_fields
expected: int64(1450) actual: <nil>
Maybe align the two — updating the assertion to safe_max_proposal_soft_deadline_ms would match the legacy branch's max_safe_proposer_delay_ms style?
A ~300ms BlockSubmission better reflects reality than the prior ~100ms. The +200ms shift propagates through the slot-budget math: - ProposalSoftDeadline safe-max 1450ms -> 1250ms (maxSafeProposalSoftDeadline) - legacy ProposerDelay theoretical max 1250ms -> 1050ms, headroom 550ms -> 350ms - worst-case 2-round QBFT budget note 2500ms -> 2700ms Mirror the new safe-max across env-descriptions, config.example.yaml, and the boundary tests. Also fix a pre-existing test that asserted the wrong WARN field key (safe_max_ms -> safe_max_proposal_soft_deadline_ms).
|
Recording a decision made during the ePBS work (#2901) that affects this PR, so it doesn't live only in ephemeral planning notes: Decision: hold this PR — merge ePBS (#2901) first, then reconsider its role. Rationale: ePBS removes the out-of-protocol apparatus this PR's flow configures (mev-boost/commit-boost relay polling is gone post-Gloas), but not the underlying "select the bid as late as the deadline allows" dynamic. Post-Gloas that lever relocates: for a non-self-building proposer it moves into SSV (the timing of the Gloas block-produce call) and/or the BN's own bid selection, while the larger late-MEV lever moves to the builder — or to SSV's §6 envelope timing when self-building. Consequences for this PR when re-evaluated:
|
The temporary in-branch scratch doc required removal once #2901 was in review; its still-open content now lives in durable homes — the devnet e2e runbook and verify items in #2920, the follow-ups and known limitations in the #2901 description, and the MEV-timing-games decision on #2855. The research/decision history stays retrievable from this branch's log.
The temporary in-branch scratch doc required removal once #2901 was in review; its still-open content now lives in durable homes — the devnet e2e runbook and verify items in #2920, the follow-ups and known limitations in the #2901 description, and the MEV-timing-games decision on #2855. The research/decision history stays retrievable from this branch's log.
Summary
Adds an opt-in MEV-optimized block-fetch path (selected by
ProposalSoftDeadline) alongside the existing legacy path, which stays the default. Config is resolved and validated once incli/operator;beacon/goclientconsumes the pre-resolved values as pure mechanism. Also rewritesdocs/MEV_CONSIDERATIONS.mdaround PBS-side timing games, reframingProposerDelayas the legacy lever.Additive — no behavior change for existing configs. Both nothing-set and the explicit legacy knobs resolve to the legacy path, bit-for-bit identical to
stage.Path selection
ProposalSoftTimeout(default 1800ms).ProposerDelay/ProposalSoftTimeoutset → legacy: same path, chosen explicitly.ProposalSoftDeadlineset → MEV-optimized: a slot-relative deadline that (a) collects multi-BN bids until the deadline without early-exiting, then proposes the best-scored bid, and (b) holds the block until the deadline before starting QBFT (single- and multi-BN alike), so every operator in the cluster starts QBFT at the same slot time (aligned round timers). Mutually exclusive with the legacy knobs — combining them is rejected at startup.New config (under
eth2)ProposalSoftDeadline— slot-relative deadline, range[1000ms, 3600ms]. Above the safe-max of 1250ms the node refuses to start unlessAllowDangerousProposalSoftDeadlineis also set (then allowed up to 3600ms, with a startup WARN) — mirrorsProposerDelay/AllowDangerousProposerDelay.AllowDangerousProposalSoftDeadline(envALLOW_DANGEROUS_PROPOSAL_SOFT_DEADLINE) — explicit acknowledgement of the above.Single-BN operators that set
ProposalSoftDeadlinenow get a slot-relative QBFT-start floor (previously single-BN ignored all block-fetch knobs and fetched directly). Worth a release note for the new knobs.Refactor:
goclient.NewOptionsis removed;goclient.Newapplies the network-timeout defaults directly.Docs
docs/MEV_CONSIDERATIONS.mdrewritten around PBS-side timing games (mev-boost v1.11+-config, or commit-boost);config.example.yamlupdated.Test plan
go build ./...,go vet,go test -race ./beacon/goclient/... ./cli/operator/...green.