Skip to content

proposals: mev/commit-boost driven MEV flow - #2855

Open
iurii-ssv wants to merge 16 commits into
stagefrom
mev-considerations-revised
Open

proposals: mev/commit-boost driven MEV flow#2855
iurii-ssv wants to merge 16 commits into
stagefrom
mev-considerations-revised

Conversation

@iurii-ssv

@iurii-ssv iurii-ssv commented May 18, 2026

Copy link
Copy Markdown
Contributor

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 in cli/operator; beacon/goclient consumes the pre-resolved values as pure mechanism. Also rewrites docs/MEV_CONSIDERATIONS.md around PBS-side timing games, reframing ProposerDelay as 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

  • nothing set → legacy (default): race BNs in parallel, take the first blinded (MEV) block, fall back to the best response within a relative ProposalSoftTimeout (default 1800ms).
  • ProposerDelay / ProposalSoftTimeout set → legacy: same path, chosen explicitly.
  • ProposalSoftDeadline set → 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 unless AllowDangerousProposalSoftDeadline is also set (then allowed up to 3600ms, with a startup WARN) — mirrors ProposerDelay / AllowDangerousProposerDelay.
  • AllowDangerousProposalSoftDeadline (env ALLOW_DANGEROUS_PROPOSAL_SOFT_DEADLINE) — explicit acknowledgement of the above.

Single-BN operators that set ProposalSoftDeadline now 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.NewOptions is removed; goclient.New applies the network-timeout defaults directly.

Docs

docs/MEV_CONSIDERATIONS.md rewritten around PBS-side timing games (mev-boost v1.11+ -config, or commit-boost); config.example.yaml updated.

Test plan

  • go build ./..., go vet, go test -race ./beacon/goclient/... ./cli/operator/... green.
  • Per-path behavior — MEV-optimized: wait-to-deadline (no early-exit), best-bid-wins, deadline-past fallback, single-BN floor, all-BN-fail bail; legacy: early-exit-on-blinded, soft-timeout fallback.
  • Config layer: path selection, deadline range + safe-max gating, soft-timeout resolution/floor, advisory warnings.

@iurii-ssv
iurii-ssv requested review from a team as code owners May 18, 2026 14:17
@codecov

codecov Bot commented May 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.90909% with 17 lines in your changes missing coverage. Please review.
✅ Project coverage is 61.7%. Comparing base (8e37cce) to head (2fc48f2).
⚠️ Report is 5 commits behind head on stage.

Files with missing lines Patch % Lines
beacon/goclient/proposer.go 88.1% 9 Missing and 3 partials ⚠️
beacon/goclient/goclient.go 71.4% 1 Missing and 1 partial ⚠️
cli/operator/config.go 97.4% 1 Missing and 1 partial ⚠️
cli/operator/node.go 0.0% 1 Missing ⚠️

☔ 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.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@greptile-apps

greptile-apps Bot commented May 18, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds an opt-in MEV-optimized block-fetch path (ProposalSoftDeadline) alongside the preserved legacy path, rewires config resolution from goclient.NewOptions into cli/operator.resolveBlockFetch, and rewrites docs/MEV_CONSIDERATIONS.md around PBS-side timing games. Existing configurations are unaffected — default and explicit-legacy-knob setups resolve bit-for-bit to the original behavior.

  • New getProposalParallelByDeadline: collects proposals from all BNs until a slot-relative deadline (no early-exit on blinded), then returns the best-scored bid; single-BN operators get the same floor via waitUntilProposalSoftDeadline, aligning QBFT start across the cluster.
  • resolveBlockFetch in cli/operator/config.go: validates and resolves the three timing knobs at startup, mutually-excluding the MEV-optimized path from the legacy knobs; the resolved values are written onto ConsensusClient before goclient.New is called.
  • Tests: proposer_path_dispatch_test.go provides thorough behavioral coverage of both paths (deadline floor, best-bid selection, past-deadline fallback, all-fail bail, legacy early-exit); config_test.go adds path-selection and validation unit tests.

Confidence Score: 5/5

Safe 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

Filename Overview
beacon/goclient/proposer.go Core new dispatch logic: single-BN MEV-optimized floor, getProposalParallelByDeadline (slot-relative collect), waitForFirstValidProposal fallback, waitUntilProposalSoftDeadline. Uses time.After which leaks a timer if ctx cancels before deadline fires.
beacon/goclient/goclient.go Adds proposalSoftDeadline field; moves network-timeout defaulting from removed NewOptions into New; adds defensive multi-BN legacy guard. Well-structured.
beacon/goclient/options.go Removes NewOptions (logic absorbed into resolveBlockFetch + New); adds ProposalSoftDeadline field with clear doc comment. Clean removal.
cli/operator/config.go Adds resolveBlockFetch with determineBlockFetchPath/validateProposalSoftDeadline. Block-fetch path selection is clean and mutually exclusive; legacy defaulting is preserved bit-for-bit.
cli/operator/node.go Removes NewOptions call; passes pre-resolved cfg.ConsensusClient directly to goclient.New. Simplified correctly.
beacon/goclient/proposer_path_dispatch_test.go New test file with comprehensive per-path behavioral coverage: deadline floor, best-bid selection, past-deadline fallback, all-fail bail, single-BN floor/no-floor, legacy early-exit and soft-timeout fallback.
cli/operator/config_test.go Expands config-layer tests: path determination, blockFetchPath.String(), validateProposalSoftDeadline range, resolveBlockFetch defaults and warning scenarios. Good coverage.
beacon/goclient/proposer_test.go Introduces spectestingMu to serialize ssv-spec fixture mutations (fixing a race in concurrent multi-BN tests); refactors createClientForProposerTest and existing multi-client test to use dynamic-slot responses.
protocol/v2/ssv/runner/proposer.go Comment-only change: updates ProposerDelay docs to mark it as a legacy lever and recommend PBS-side timing games instead.
docs/MEV_CONSIDERATIONS.md Full rewrite around PBS-side timing games; documents both mev-boost and commit-boost configs, path selection logic, and the new ProposalSoftDeadline knob. Appendix preserves legacy ProposerDelay guidance.

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]
Loading

Reviews (3): Last reviewed commit: "adjustmets" | Re-trigger Greptile

Comment thread docs/MEV_CONSIDERATIONS.md Outdated
Comment thread docs/MEV_CONSIDERATIONS.md Outdated
iurii-ssv added a commit that referenced this pull request May 18, 2026
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.
iurii-ssv added a commit that referenced this pull request May 18, 2026
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.
@iurii-ssv

Copy link
Copy Markdown
Contributor Author

@greptile pls re-review

@iurii-ssv iurii-ssv changed the title docs/MEV_CONSIDERATIONS: rewrite around PBS-side timing games beacon/goclient: split block-fetch into safe / legacy / MEV-optimized paths + rewrite MEV doc May 19, 2026
@iurii-ssv
iurii-ssv force-pushed the mev-considerations-revised branch from 8e0c9df to d0c5c0e Compare May 19, 2026 08:44
@iurii-ssv
iurii-ssv force-pushed the mev-considerations-revised branch 4 times, most recently from 4c4bde2 to 35f1664 Compare May 29, 2026 18:50
@iurii-ssv
iurii-ssv changed the base branch from stage to operator-config-consolidation May 29, 2026 18:50
@iurii-ssv
iurii-ssv force-pushed the mev-considerations-revised branch from 35f1664 to 2006437 Compare May 29, 2026 18:58
@iurii-ssv iurii-ssv changed the title beacon/goclient: split block-fetch into safe / legacy / MEV-optimized paths + rewrite MEV doc proposals: mev/commit-boost driven MEV flow May 30, 2026
@iurii-ssv
iurii-ssv marked this pull request as draft May 30, 2026 10:54
@iurii-ssv
iurii-ssv force-pushed the operator-config-consolidation branch 6 times, most recently from 72758bf to f7fe8e6 Compare June 2, 2026 15:13
Base automatically changed from operator-config-consolidation to stage June 4, 2026 07:48
@iurii-ssv
iurii-ssv force-pushed the mev-considerations-revised branch 2 times, most recently from f9eb2ee to 92fc1b2 Compare June 4, 2026 10:57
iurii-ssv added 2 commits June 5, 2026 15:04
… 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.
@iurii-ssv
iurii-ssv force-pushed the mev-considerations-revised branch from 4054c1d to e40a5fe Compare June 5, 2026 12:06
iurii-ssv added 3 commits June 7, 2026 14:11
…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.
@iurii-ssv
iurii-ssv marked this pull request as ready for review June 8, 2026 08:14
iurii-ssv added 9 commits June 8, 2026 12:08
- 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
Comment thread cli/operator/config_test.go Outdated

fields := logs[0].ContextMap()
require.Equal(t, int64(1850), fields["proposal_soft_deadline_ms"])
require.Equal(t, int64(1450), fields["safe_max_ms"])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).
@iurii-ssv iurii-ssv mentioned this pull request Jul 2, 2026
12 tasks
@iurii-ssv

Copy link
Copy Markdown
Contributor Author

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:

  • ProposalSoftDeadline likely carries over with re-derived bounds for the tighter 25%-of-slot proposal deadline (vs 33%) — a reduced job, not a removed one.
  • Note the pre-Gloas proposalSoftTimeout does not apply on the Gloas produce path (it uses the first-client fetch and bypasses it), so there is no soft-timeout↔delay coupling to tune there.
  • Related knobs already landing in ePBS (EIP-7732 / Gloas) — SIP-94 #2901: ProposerDelay stays (now applies to pre-Gloas slots only) and a new ProposerDelayEPBS (default 0, hard-capped at 1s, no dangerous override) takes over from the Gloas fork on. The docs/MEV_CONSIDERATIONS.md ePBS rewrite is deferred and can land together with this PR's re-evaluation.

iurii-ssv added a commit that referenced this pull request Jul 2, 2026
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.
iurii-ssv added a commit that referenced this pull request Jul 3, 2026
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.
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