ADR-058: The strategy crate split — submission is mechanism, degenbot-strategy owns the plane

Status: accepted (2026-09-21). Records the landed extraction (commits 442527b36, 5daa9d670, 8888d85fc, c8c56aab0) and the vocabulary ruling that preceded it (b3a3d91f5). Basis: ADR-055 (pending-transaction strategy seams), ADR-054 (frame evidence seams), ADR-057 (the strategy host), ADR-019 (strategy-vs-engine), and the executed design. The crate sources remain the last word on what the code does today.

Context

Before the split, degenbot-submission held two different things at once: the submission channel (signer, dispatcher, monitor, ledger, submit) and the backrun strategy that used it — the reaction pipeline, the frame feed, the anchored DFS, gap quarantine, and the pending-tx driver all lived beside the signer. The backrun arm’s decision layer and loop also straddled degenbot-bot. Nothing owned “concrete strategies” as a concept, so the umbrella crate was the only place a pure-Rust consumer could find one.

“Strategy” was also overloaded: ExecutionStrategy named the four-part executor-contract seam, and PendingTxStrategy named the source-side reaction contract, so the word could mean any of three different things.

Decision

D1 — Submission is mechanism only

degenbot-submission owns the channel and nothing strategy-shaped. Its surface is the signer, fee pricing, transaction params, the bundle target, the dispatcher, the monitor, the submission ledger, and submit; it names no strategy. A caller hands it a candidate and a SubmissionTarget and reads a SubmitOutcome.

D2 — degenbot-strategy owns the plane and the concrete compositions

The strategy-plane crate (442527b36) is the home for concrete strategy compositions and the vocabulary they compose over. It is pyo3-free. The backrun arm then moved in (5daa9d670): backrun.rs and backrun_engine.rs from degenbot-bot; backrun_strategy.rs, backrun_driver.rs plus its subtree, anchored_dfs.rs, frame_pipeline.rs, pending_tx.rs, market_context.rs, gap_probe.rs, gap_quarantine.rs, and gap_quarantine_journal.rs from degenbot-submission.

The dependency direction is strategy → capability crates. Capability implementations stay in their own crates and are re-exported here, never moved. No capability crate depends on degenbot-strategy; only the umbrella degenbot crate, degenbot-python, and (by the same allowance) any operator binary may.

D3 — Vocabulary rulings (b3a3d91f5)

The bare word “strategy” was reserved for the top-level composition only:

  • ExecutionStrategy → ExecutionAdapter (module strategy.rs → adapter.rs). The Python lift needed no class rename of its own: its pyclass (PyPayloadComposer, Python-visible PayloadComposer) was never named after the seam, and the trait appeared in the lift only in doc comments (renamed with the trait). The four-part Encode / Probe / Assess / Fee seam (ADR-025 D2) is an executor-contract adapter, not a strategy.

  • PendingTxStrategy → PendingTxReaction. The pending-transaction pipeline is the source-side contract behind backrun-type strategies, not a strategy.

  • Strategy names a top-level kind of profit opportunity, composed as values over six capability slots: source, infrastructure, calculation, encoder, simulator, submission.

D4 — Per-ecosystem types, composition over sub-traits

The backrun arm split into two distinct types (8888d85fc): MevblockerBackrun and TxpoolBackrun. Both parameterize the same reaction machinery (frame feed, anchored discovery, decide gate, simulation, dispatch) by composition; they differ only in the value of their SubmissionSlot:

  • Mevblocker { bundle_url, private_url } — an eth_sendBundle auction on the MEVBlocker searcher WebSocket, with the raw-broadcast fan-out leading with the private endpoint before the read-provider fallback.

  • PublicFanOut { relays } — public-mempool relay fan-out with the read provider as fallback.

There is no Backrun sub-trait. Distinct ecosystems get distinct types; shared reaction capabilities are mixed in by composition. Both compositions are independently activatable and can run in one StrategyHost process.

D5 — The plane contract is extracted by subtraction

strategy_plane.rs (c8c56aab0) holds the surface the three concrete strategies demonstrably share and nothing else: StrategyName (the facet name, the host registration id, and the one select() path) and trait Strategy { const NAME }. A capability slot with one consumer stays out of the plane. The hosted boot registers via StrategyName::ALL in plane order, and the config resolves the settlement broadcast endpoints through the same plane surface instead of a second config copy.

Settlement keeps a special-case ledger in settlement.rs, cited as the open-work map rather than normalizing the cases away:

  • Pump self-driving. The engine pump owns the settlement block clock, so the host registers no spawn factory and start_driving skips it; generalizing the pump is ADR-018’s on-demand work.

  • Unconditional Configured registration. The hosted boot registers the settlement facet as configured even when strategy.settlement.active is false; the runner resolves its live-mode gate after the engine exists.

  • Process-global submission lane. The settlement NonceLane is installed process-wide, because the settlement seam is the Python-driven arm of the one hosted process.

  • Sole channel registrar. EngineChannelHandles::register_on is the closure StrategyHost::mint hands the exclusive &mut Hub.

  • ADR-057 head-liveness coupling. The hosted head feed runs from the Python settlement consumer’s accepted-header clock, guarded on hosted activity.

D6 — Seam learnings from the exercise

What actually moved differed from the plan:

  • Tests and fixtures moved with their modules. trace_* tests and backrun_lane_e2e (from degenbot-bot) joined anchored_dfs, frame_pipeline, admit_probe, the v3/v4 window suites, frame_liveness, and quarantine_journal_roundtrip under the new crate. hosted_driver_ambient_runtime and mock_third_family stayed in degenbot-submission, because they exercise strategy_host plus the submission surface only. Two out-of-tree seams were blindly repointed to the moved paths: the degenbot-config env-read allowlist test and the frame_oracle capture fixture path.

  • A recorded conflict, resolved by carve-out. Gating settlement registration on strategy.settlement.active would have broken the pinned default-boot enable behavior (the runner resolves its live relay posture after the engine exists, and a dry-run boot must still enable the arm). The unconditional-Configured carve-out was kept and documented in settlement.rs rather than silently changed.

  • The config hard cut. strategy.backrun was replaced by strategy.mevblocker_backrun and strategy.txpool_backrun typed facets at the single declaration site, with env spellings DEGENBOT_STRATEGY_MEVBLOCKER_BACKRUN_* / DEGENBOT_STRATEGY_PEER_BACKRUN_* and StrategyName becoming Settlement | MevblockerBackrun | TxpoolBackrun. There are no aliases and no legacy parsing; readiness legality is per facet (MEVBlocker bid mode requires key_file and mevblocker_url). The retired single-arm strategy.name selector stays undeclared, pinned by strategy_arm_selector_is_retired.

Consequences

  • A pure-Rust consumer depends on degenbot-strategy for concrete strategies; degenbot-submission no longer names one.

  • Adding a strategy family is one composition type, one config facet, and one registration. The plane contract grows only when a shared slot gains a second consumer.

  • The retired names (ExecutionStrategy, PendingTxStrategy) and the old crate homes survive only in ADR bodies and the historical record.