Rust settlement-bot consumer parity ledger¶
Epic: RGZG4S (ergo) — “Rust-owned settlement bot — consumer parity + gap discovery”.
This document is the living census for the epic’s core question: can a
cargo add degenbot consumer build the same settlement-arbitrage bot that
examples/eth_settlement_arbitrage_v2_v3_v4_rust.py builds via the Python
driver (src/degenbot/runner/, ~4.8K lines) — with zero Python? Per
AGENTS.md, Rust is the engine and Python is a driver shell; this ledger tests
that claim end-to-end instead of assuming it.
Methodology (the gap-smoking loop)¶
The parity example (rust/examples/settlement_bot) is built as the test:
Port the next Python-driver phase against the umbrella crate’s public surface and attempt to compile/run.
Where compilation or semantics fail, the failure is a numbered gap (G1..G5 below); each gap has an ergo task and a classified row in the ledger.
Close the gap in the core crates (never in the example), then advance.
Land the running dual-driver gate (RSP-8) so parity is continuously asserted, not re-audited.
The crate lives under rust/examples/ (not examples/rust/) because cargo
rejects workspace members not hierarchically below the workspace root
(“workspace member … is not hierarchically below the workspace root”).
Status vocabulary for the ledger:
REACHABLE — a pyo3-off umbrella consumer reaches the same behavior today (evidence linked).
PARTIAL — the leaf exists but the driver-level glue/semantics differ or are missing.
BLOCKED — no public path exists (hard wall).
DRIVER-POLICY — deliberately driver-owned (“stays-python” per
degenbot/runnerdesign); the Rust example reimplements it locally rather than the library owning it.
Ledger¶
# |
Python-driver surface (source) |
Rust status |
Evidence / gap |
|---|---|---|---|
1 |
CLI flags |
REACHABLE |
argv spelling is Rust-owned (ADR-051 D2): |
2 |
|
DRIVER-POLICY |
reimplemented driver-side in the example (mirrors constants byte-for-byte); |
3 |
Node URI cascade: |
REACHABLE |
|
4 |
DB path resolution ( |
REACHABLE |
|
5 |
DB snapshot load → seed block S ( |
REACHABLE |
|
6 |
Engine handshake: |
REACHED-via-EngineDriver |
|
7 |
Result-batch consumption (engine |
REACHED-via-EngineDriver |
|
8 |
Path registration |
REACHED-via-EngineDriver |
|
9 |
Registration verify lifecycles (quarantine → seed-verify → drain/pin → post-drain verify → live; sync + async entry points) |
REACHABLE |
The core lifecycles are re-exported by the umbrella and proven in |
10 |
Pool construction from RPC (+ DB arm) |
REACHABLE |
umbrella re-exports |
11 |
Candidate-pool enumeration from DB ( |
REACHED/VERIFIED |
|
12 |
Path discovery ( |
REACHABLE |
|
13 |
Path-composition policy (hop bounds, duplicate pool, permutation filter — |
DRIVER-POLICY |
example-implemented in |
14 |
Sim context + in-process sim ( |
REACHABLE |
|
15 |
Dispatch selection + encoding ( |
REACHABLE |
|
16 |
Sim fan-out + ordered single submitter ( |
REACHABLE |
|
17 |
Fee determination: |
REACHABLE |
|
18 |
Live submission: EIP-1559 sign + send + receipt monitor; dry-run guard that never signs |
REACHABLE |
|
19 |
Session watch / stuck-loop watchdog + session-end verdict ( |
DRIVER-POLICY |
Detection is core-owned: |
20 |
Operator Unix-socket channel ( |
DRIVER-POLICY |
|
21 |
Process diagnostics: GIL probe, tracemalloc, faulthandler, /proc-mem sampler ( |
DEPARTURE (documented) |
Python-interpreter-specific by construction; the Rust example substitutes tokio/tracing-native equivalents. Not a parity item. |
22 |
Logging/telemetry boot ( |
REACHABLE-AND-WIRED |
|
Gap inventory¶
G1 — engine driver exposure (ergo 5XOGRK, rows 6–8 + downstream 16): CLOSED by ADR-050. The public
degenbot::EngineDriver(degenbot_bot::arb_engine::EngineDriver) composes the one publicEngineStagesseam with the pump session state;ArbitrageEnginestayspub(crate)(the one-door invariant).EngineRegistry.start+ theBotRunnerphase machine remain Python-side policy over the Rust-owned sequencing contract. ThePyArbEngine/PumpStatepair now delegates the ritual to the same driver.G2 — DB discovery reads (ergo YFIOSF, row 11): CLOSED.
degenbot-dbships the additive, read-onlydiscovery_readsurface (DiscoveryPoolRow+fetch_discovery_rows/fetch_discovery_rows_on_conn) covering every columnbuild_paths.py’s construction path reads, on theSnapshotDbheld-deferred-tx handle; verified against the frozenparity.dbfixture (V3 aerodrome_v3 + V4 uniswap_v4, chain 8453) and the umbrellasettlement_botexample. The Python driver now consumes this Rust-owned surface throughdegenbot.db; the former SQLAlchemy/Alembic layer is retired. Balancer/Curve are outside the candidate graph by construction and are intentionally not enumerated.G3 — discovery batching + registration pipeline (ergo XFEJUG, rows 9, 12, 13 + claim TOCTOU): CLOSED (driver-side, offline).
rust/examples/settlement_bot/src/shipsdiscovery.rs(graph build from the G2 discovery rows on the held-tx snapshot + batched lazyOwnedPathFinder),policy.rs(allowlist/hop-bounds/duplicate/permutation),pipeline.rs(the_registration_unitprep stages + offline-dry run), andlive.rs(the per-candidatebuild_v2/v3/v4→BotStateregistration → core-owned retry-wrapped verify lifecycle →register_and_solve_patharm, gated onSMOKE_RPC_URL). The at-most-once claim and the four memos + typed build-refusal classification are the CORE’s (bot_core::verify_claims/bot_core::registration_ledger, ZTEUTA), so the example holds no claim table or ledger of its own. The live arm is exercised only against a live node.G4 — consume/dispatch/submission (ergo L4E7RI, rows 15–18): CLOSED (driver-side, offline).
rust/examples/settlement_bot/src/shipsconsume.rs(per-block ordered result-batch consumption +BlockClock+ single end-of-stream on driver stop),dispatch.rs(typedDispatchDecisionplanning +priority_fee/next_base_feewrappers + theclassify_revert/FailureKindtaxonomy), the shared coredegenbot-submission::sim_pipeline(boundedSemaphorefan-out + single ordered FIFO submitter + fail-loud; reached through the umbrella), andsubmission.rs(the dry-run-safeSubmissionSeamoverdispatch_and_submit+ the config-windowmonitor_with_confignonce-expiry accounting), with 22 offline unit tests. All RPC-bound arms compile but are only exercised against a live node; the two new reach claims (row 17eth_feeHistory, row 18TxSigner) are compile-verified through the umbrella and none required a new G-row. The example now depends onalloydirectly for theU256/Address/Bytesvalue types those public seams name (recorded as a nuance, not a gap: the umbrella exposes the functions but not the primitive aliases).G5 — session watch + operator channel + reconnect (ergo KPLWUM, rows 19–20): CLOSED (driver-side, offline; detection since lifted).
session_watch.rsranks the core session-end detection facts (degenbot::session_end::{SessionEndCause,SessionEndFacts,SessionEndDetection}) into its typed end-state verdict set over the liveEngineDriverresult-consumption loop, with the same-batch ranking and the observer-only cancellation discipline; theHeartbeat/stall_watchdogmachinery is now the core’s;operator_channel.rsships the--operator-socketJSON-lines channel (the four ops, Python byte-compatible response shapes, unknown-op/error framing, gracefulclose(), plus the--operator-inertRPC-free serve mode the integration check drives). Fleet posture is reachable standalone throughdegenbot::workers::posture::{process, PosturePolicyPatch}(row 20 is DRIVER-POLICY, not a new G-row). The WS reconnect/abort-policy sub-item was not part of KPLWUM’s landed slice (rows 19–20): the live arm keeps the existingEngineDriver::start/stopsequencing, and the SIGINT→stop→typed-consumer-report shutdown is covered by the inert mode + the live arm’sdriver.stop()ordering.G6 — telemetry boot parity (ergo ZOBXVC, row 22): CLOSED.
rust/examples/settlement_bot/src/telemetry.rsboots the same observability stack the Python driver boots, in its order: typedBotConfigLoaderinstall (standard operator file +DEGENBOT_*env),tracing_subscribercompact stderr console filtered throughdegenbot::bot::telemetry::resolve_filters, env-gated OTLP layer throughdegenbot::bot::otel, Prometheus scrape endpoint throughdegenbot::bot::metrics::init_global_metrics, theninstall_panic_hook()/warn_retired_env_names()/worker_census::emit_boot_table(). The umbrella now forwardsdegenbot-bot/otelas its ownotelfeature, socargo add degenbot, features = ["otel"]reachesdegenbot::bot::{otel, metrics, instruments}— without the passthrough those#[cfg(feature = "otel")]modules were unreachable through the umbrella, which is why the example had no telemetry at all. An absent OTLP endpoint is a quiet no-op (the exporter’slocalhost:4318default is deliberately not taken); every telemetry failure degrades loudly, never fatally;TelemetryBoot::dropflushes + shuts theOtelHandleand stops the scrape server (ADR-043 §6). Proof:tests/telemetry_boot.rs(boot announcements, the 20-row parity-ledger stdout unchanged, retired-env WARN, env-OTLP activation, live/metrics200 withdegenbot_metric_series), plus a 120 s mainnet dry-run (SMOKE_RPC_URL,DEGENBOT_MAX_PATHS=400, exit 0) whose log shows the four boot lines,session heartbeatper block,[session] run loop ended: WindowExpired,[engine] EngineDriver handshake OK, andtelemetry shutdown: flushing OTLP spans. Mid-run scrape (real exposition):degenbot_metric_series{otel_scope_name="degenbot-bot"} 0,degenbot_state_lock_hold_seconds_count{mode="read",site="core",otel_scope_name="degenbot-bot"} 1,degenbot_detached_degraded_cycles_total{otel_scope_name="degenbot-bot"} 0,degenbot_state_lock_wait_seconds_bucket{mode="read",site="core",le="0.0001",...} 1,target_info{...}.E2E running gate (ergo 23DLCY): CLOSED (offline). The ledger is executable: the CI-safe fixture boot gate runs on both axes (Rust
boot_gate.rs+ Pythontest_settlement_bot_boot_gate.py) against the sharedfixtures/settlement_bot_boot.jsonoracle, the recorded dual-driver decision diff (dual_driver_gate.py+test_settlement_bot_dual_driver_gate.py) diffs the Python/Rust streams modulo the documented permitted-divergence list, and seeded-divergence tests prove both comparators have teeth. The live anvil arm is wired behindDEGENBOT_DUAL_DRIVER_GATE=1+DEGENBOT_FORK_RPC(skip-by-default in CI). See Running parity gate.
Running parity gate (RSP-8, ergo 23DLCY)¶
The ledger is executable. The gate has two CI-safe, offline halves and one
opt-in live half; the extractor contract is grep '^parity-ledger row='.
1. Fixture boot gate (offline, no RPC)¶
Shared oracle: tests/standalone_parity/fixtures/settlement_bot_boot.json.
Both consumers read the same JSON and must reproduce it:
Rust consumer —
rust/examples/settlement_bot/tests/boot_gate.rsshells the built example againstrust/crates/foundation/degenbot-db/tests/fixtures/parity.dbwith--smoke-offlineand parses the machine-checkable stdout.Python consumer —
tests/standalone_parity/test_settlement_bot_boot_gate.pydrives the PyO3 seams (Bot.load_snapshot_from_db,build_path_graph).
The machine-checkable contract is the boot report itself: the
parity-ledger row=<id> status=<status> note=<note> lines, the
parity-ledger snapshot-seed-block S=<None|u64> line, the
[boot] discovery enumerated <n> candidate pools line, the
[g3] graph built: <n> nodes, <n> candidate tokens, <n> requested kinds [...]
line, and the [g3] offline-dry pipeline: key=value ... line. The
consume/dispatch decision rows the gate pins are:
Row |
Pinned status |
Decision contract |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
core |
|
|
core |
|
|
dry-run seam never signs; live seam is |
Seeded-divergence proof (teeth). The Rust test mutates one expected
ledger status in an in-memory copy of the oracle and asserts the comparator
fails; it also re-runs the real binary with the DEGENBOT_DISCOVERY_CHAIN_ID
seam removed (enumeration drops 2 → 0) and asserts the comparator catches the
live divergence. The Python test mutates expected.snapshot_seed_block and
python_reachable.graph_nodes in memory and asserts the real PyO3 decisions
do not match. The checked-in oracle is never modified.
2. Dual-driver decision diff (recorded; anvil opt-in)¶
tests/standalone_parity/dual_driver_gate.py diffs the Python driver’s and
the Rust driver’s decision streams against the recorded fixture
tests/standalone_parity/fixtures/dual_driver_decisions.json, modulo the
fixture’s permitted_divergence list (currently graph.candidate_tokens:
the Rust boot applies the 15-token ETH-mainnet discovery allowlist while the
Python probe reads the unfiltered graph — the documented row-13 split). The
pytest half is test_settlement_bot_dual_driver_gate.py.
Live mode (--live) requires DEGENBOT_DUAL_DRIVER_GATE=1 +
DEGENBOT_FORK_RPC (+ DEGENBOT_FORK_BLOCK): it starts
anvil --fork-url ... --fork-block-number ..., runs both drivers dry-run
against the pinned fork, and reads the per-batch decision streams named by
DEGENBOT_DECISION_STREAM (JSONL
{block, path_id, decision}), which are not emitted by either driver
yet — so live mode fails loudly on a missing stream rather than passing
silently. --record regenerates the recorded fixture from the offline
probes (no RPC).
Invocation¶
just test-settlement-parity— Rust boot gate + pytest gates + recorded diff.uv run pytest tests/standalone_parity -q— the standalone-parity axis.DEGENBOT_DUAL_DRIVER_GATE=1 DEGENBOT_FORK_RPC=<rpc> DEGENBOT_FORK_BLOCK=<n> uv run python tests/standalone_parity/dual_driver_gate.py --liveuv run python tests/standalone_parity/dual_driver_gate.py --record
Rust-ownership sweep (RSP-9 / ergo IUGFLH)¶
The horizontal census sibling to the vertical RSP-2..RSP-8 slices. Every Python-owned driver surface is classified as one of:
LIFT — the core owns it once; both the pure-Rust and the Python driver call in through the same seam.
KEEP-DRIVER — it must remain host-side (asyncio loop ownership, SIGINT policy, OS/env cascade, display rendering).
SPLIT — one named seam; the mechanism/core fact lifts, the policy or host binding stays.
(landed) marks a LIFT whose core implementation already exists (ADR-050 /
ergo 5XOGRK plus the driver-side XFEJUG / L4E7RI / KPLWUM slices).
(pending) marks a LIFT decided here whose core work is not yet landed; see
Lift follow-ups.
# |
Swept item (source) |
Decision |
Rationale |
Consumers affected |
|---|---|---|---|---|
S1 |
|
LIFT (landed) |
ADR-050 D2/D7 moved the sequencing contract into |
Python |
S2 |
|
LIFT (landed, |
ADR-050 D2: |
|
S3 |
Startup backfill/resume ordering (auto-backfill S+1..W, single result-batch gate) |
LIFT (landed, |
ADR-050 D2/D6: |
|
S4 |
Address→ |
LIFT (landed, |
|
|
S5 |
|
LIFT (landed, |
One owner: |
|
S6 |
|
SPLIT |
The guard is LIFT-landed — the core’s |
|
S7 |
Verification retry dance (bounded retry-with-backoff loop) ( |
LIFT (landed) |
|
|
S8 |
|
KEEP-DRIVER |
Deployment tuning (attempts/backoff/jitter); each driver parses its own env and injects the value into the lifted dance (S7). |
|
S9 |
Sim fan-out bounded-concurrency mechanism ( |
LIFT (landed, S9/S11) |
Landed as the whole pipeline in |
|
S10 |
Sim fan-out policy numbers ( |
KEEP-DRIVER |
Config knobs; the fleet sizes its seats from typed config and drivers may pass a cap. |
|
S11 |
Ordered single submitter (FIFO fan-in; one nonce fetch per submit) ( |
LIFT (landed, S9/S11) |
Same landed module as S9: the ordered submit lane ships beside the bound because they are one mechanism. Submission order = nonce order; one nonce fetch per submit at the moment of submit, byte-identical to the serialized loop the twins replaced. |
|
S12 |
Registration build-refusal taxonomy ( |
LIFT (landed, |
|
|
S13 |
Registration memos ( |
KEEP-DRIVER |
Bookkeeping and display layered over core facts, shaped per pipeline instance. |
|
S14 |
|
SPLIT (detection landed) |
Detection LIFTED to |
|
S15 |
Config cascades ( |
REACHABLE |
ADR-062 D1/D4/D7 assigns Rust the shared four-layer contract: |
|
S16 |
Process diagnostics (GIL probe, tracemalloc, faulthandler, |
KEEP-DRIVER |
Python-interpreter-specific by construction (audit ledger DEPARTURE row 21); the Rust driver substitutes tracing-native equivalents. |
example startup |
S17 |
SIGINT binding + shutdown ordering |
SPLIT |
The stop-before-cancel ordering contract is LIFT-landed (ADR-050 D6: |
|
S18 |
Result-batch consumption loop + |
SPLIT |
The receiver contract (hand out once; close on |
|
S19 |
Pool build + registration lifecycle ordering ( |
SPLIT |
Builds and the ADR-022 verify choreography are core-owned/reachable, and the sync lifecycles sit on |
|
S20 |
Nonce expiry accounting ( |
KEEP-DRIVER (mechanism landed) |
The window accounting is already core-owned ( |
|
S21 |
Operator Unix-socket channel ( |
KEEP-DRIVER |
Wire protocol + host deployment surface (ledger row 20); the underlying |
|
S22 |
Dispatch selection/encoding policy (candidate shaping, thin-margin, suppression) |
KEEP-DRIVER |
The sim/submit arithmetic and taxonomy leaves are core-owned; only candidate-list shaping + display rendering remain (ledger row 15). |
|
S23 |
Pool-cache trim / |
KEEP-DRIVER |
Python-object-lifetime concern with no Rust counterpart (ADR-050 D8). |
|
S24 |
DB snapshot load + V3 tracker pre-population ( |
SPLIT |
The engine’s DB snapshot load is core-owned/landed ( |
|
Lift follow-ups¶
The S14 detection LIFT has landed; the ranking half stays driver-owned. S4, S5, S7, S9, S11, S12, and S14 have since landed.
S4 key maps → landed (
ZTEUTA):BotState::pool_id_for_identity+EngineDriver::pool_id_for_identity; the Python mirrors are deleted.S5
VerifyClaims→ landed (ZTEUTA):bot_core::verify_claims::VerifyClaims, entered by the registration lifecycle; both twins deleted.S7 retry dance → landed:
bot_core::verification_retry::retry_verification_callowns the dance besideVerifyError; both twins deleted.S9/S11 sim fan-out + ordered submitter → landed (
L4E7RI):degenbot-submission::sim_pipelineowns the whole pipeline (crate-assignment revision from the originaldegenbot-workersrow);degenbot-workerssupplies only the injected cap value.S12 registration taxonomy → landed (
ZTEUTA):bot_core::registration_ledger; the example’sledger.rsis deleted and Python’s ledger is a thin adapter.S14 session-end detection → landed (
KPLWUM):degenbot_bot::arb_engine::session_endownsSessionEndCause+SessionEndFacts+Heartbeat/stall_watchdog; both drivers read the core fact and keep their own ranking.
RSP-1 ledger designation deltas¶
The sweep refines three RSP-1 rows; the rest stand. No row remains
BLOCKED after its lift landed (all lift-landed rows above cite their
ADR-050 / task evidence).
Ledger row |
Before |
After |
|---|---|---|
9 (verify lifecycles / claim TOCTOU) |
|
|
16 (sim fan-out + ordered submitter) |
|
LIFT landed ( |
19 (session watch verdicts) |
|
LIFT landed (detection; S14); ranking stays |
Supersedure: the two recorded “stays-python” statements that this sweep
reverses in part are (a) runner/bot_runner.py’s module docstring, which now
carries an ADR-050 pointer beside the doctrine, and (b) the epic 5TBT7L Q2b
crate-private-engine note in CONTEXT.md (“Engine seam deepening”), which now
carries a one-line ADR-050 supersedure. The pub(crate) one-door invariant
itself stands — ADR-050 adds the EngineDriver driver seam above
EngineStages, not a second engine door.
Launcher consolidation (RSP-16, ergo V6SUQO)¶
./run_bot.sh is the single launcher for both drivers:
./run_bot.sh [--python|--rust] [start|stop|status|foreground|print-cmd] [-- args...]
--python(the default, and the no-flag behavior) runsuv run python examples/eth_settlement_arbitrage_v2_v3_v4_rust.pywith the five documented exports — byte-identical to the pre-consolidation launcher.--rustrunsrust/target/<RUST_PROFILE>/degenbot-settlement-bot-example(packagedegenbot-settlement-bot-example, thecargo add degenbotconsumer), built on demand from a cheap staleness probe;cargo build -p degenbot-settlement-bot-exampleowns the real incremental work.RUST_PROFILEdefaults toreleaseand acceptsdevfor the workspaceopt-level = 1development profile.print-cmdis the CI-verifiable surface: the resolved driver, the full command array (passthrough included), the effectiveRUST_PROFILE, and every export, printed without building or launching (rc 0).stop/statuscover both driver process names (the Python example script and the Rust binary) in addition to the pidfile, which records the real driver pid whichever driver was started.--ends launcher parsing; the remaining tokens are appended verbatim to the driver argv. The launcher never implies--live.
Two recorded divergences between the drivers (deliberate, not defects):
Run-length default. The Python driver’s live arm runs until SIGINT. The Rust example’s live arm is gated on
SMOKE_RPC_URLand only bounded by the optionalDEGENBOT_SMOKE_MAX_SECSwindow; withoutSMOKE_RPC_URLit prints the offline parity ledger and exits (the CI-safe posture). The launcher bindsSMOKE_RPC_URLto the resolved ws cascade for--rust, so both drivers arm the same node; the bounded window remains a Rust-only knob.Telemetry arming. The Python driver arms OTLP when
DEGENBOT_OTEL=1(the launcher’s default), with endpoint precedenceOTEL_EXPORTER_OTLP_ENDPOINTenv > typedtelemetryconfig > exporter default (http://localhost:4318). The Rust example’s telemetry boot (rust/examples/settlement_bot/src/telemetry.rs) requires both a truthyDEGENBOT_OTELand an explicitly configured OTLP endpoint — an absent endpoint is a quiet no-op, never the localhost default. A--rustrun therefore needs the OTLP endpoint exported to emit spans, even though the launcher exportsDEGENBOT_OTEL=1for both drivers.
Guardrails¶
ADR-052 D6/D7 retirement is complete: the migration tree, session manager, ORM models, and SQLAlchemy dependency are gone from the Python runtime. The Rust database owner is authoritative; Python consumers use the stable
degenbot.dbmirror. This parity ledger’s Rust-side DB work remains read-only.Strategy semantics stay the shared contract:
classify_revertlabels and the 7-call bundle are compared via fixtures (per-leaf dual-driver parity tests exist, seetests/standalone_parity/); the running gate compares decisions, not log bytes.