feat(bot): orchestrator daemon (bot/main.py) + Docker packaging #22
No reviewers
Labels
No labels
auto-merge
auto-merge
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
fortiblox/arc-mev-bot!22
Loading…
Reference in a new issue
No description provided.
Delete branch "orchestrator-main-wp7931"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
bot/main.py— the orchestrator: registry -> reserves -> cycles -> search -> bidding -> submit, wired into one continuously-running process (architecture.md §5), with a two-key dry-run safety gate, startup preflight (eth_getCodeon the executor + every registry pool), graceful SIGTERM/SIGINT shutdown, and Prometheus textfile metrics (merges into the host's existingarc-mev-metrics.service, never binds :9109 itself).bot/tests/test_main.py— offline, fixture-based, end-to-end integration tests exercising the whole pipeline with a fake JSON-RPC transport (no network). This caught a real bug spanningbot/bidding.py+bot/submit.py:Opportunity.contentionis a dict per architecture §2.3, but both modules treated it as a bare int — fixed both, updated their unit tests to the correct shape.test_metrics.pyandtest_ordering_regime_export.py(their exposition-format assertions rejected a real, valid Prometheus float like8.9e-06).config/bot.tomlwas missing from.gitignore/secret-scan despite architecture §4.7 documenting it as machine-local likerpc-pool.toml— fixed for consistency (no secrets in it).Dockerfile+docker-compose.yml— independent offortiblox-arc-arb's compose stack,python:3.12-slim,network_mode: host, non-root user, no key material mounted this wave,restart: unless-stopped,HEALTHCHECK.dry_run = true, pinned further by--force-dry-runin the container's default command. This PR ships and verifies dry-run only.Test plan
make lint(py_compile + secret-scan) — cleanmake test— 283/283 offline unit tests pass (264 prior + 19 new intest_main.py)make forge-test— unchangedcontracts/, runs in CI (no localforgeon this host)docker build+docker compose up, ≥200 real blocks, confirm zero transactions sent) — in progress, results to follow in a PR comment🤖 Generated with Claude Code
Assembles every prior Phase 0-4 module into one continuously-running process per architecture.md §5 (registry -> reserves -> cycles -> search -> bidding -> submit), with a two-key dry-run safety gate (config dry_run=false AND --allow-live both required before any send), startup preflight (eth_getCode on the executor and every registry pool, per contracts/README.md item 8), graceful SIGTERM/SIGINT shutdown, and Prometheus textfile metrics that merge into the host's existing arc-mev-metrics.service (never binds :9109 itself). Offline integration tests (bot/tests/test_main.py) replay real recorded chain state through the whole pipeline end-to-end with a fake JSON-RPC transport -- the class of test no single prior lane could write. That replay caught a real bug spanning bot/bidding.py and bot/submit.py: search.py emits Opportunity.contention as {touched_pools, swaps_head_block, distinct_senders} (architecture §2.3), but validate_opportunity() required a bare int and submit.py's max_contention gate compared it directly with `>` -- either would have raised/misbehaved on every real Opportunity search.py ever produces. Fixed both, updated their own unit tests to the correct shape, and found the same negative-scientific-notation regex gap in two metrics-exposition tests (test_metrics.py, test_ordering_regime_export.py) along the way. Also: config/bot.toml was missing from .gitignore/secret-scan even though architecture.md §4.7 documents it as machine-local like rpc-pool.toml (no secrets in it, low severity, fixed for consistency). Docker packaging: Dockerfile (python:3.12-slim, matches LXC 140's Python 3.12.3; bot/ verified pure-stdlib) + docker-compose.yml, independent of fortiblox-arc-arb's compose stack, network_mode: host, non-root user, no key material mounted this wave (dry_run=true / signer=none needs none), restart: unless-stopped, HEALTHCHECK against the metrics-textfile writer's liveness. WP 7931, 7932, 7933 (summary 7930). Signed-off-by: BuddySan <[email protected]> Co-Authored-By: Claude Sonnet 5 <[email protected]>Live dry-run verification (LXC 140, 2026-09-16) — complete
Docker-in-LXC: verified clean early (
docker version/docker info, overlayfs + cgroup v2, no nested-virt issues). Not a blocker.Build + host networking:
docker buildsucceeds (no pip installs needed —bot/verified pure-stdlib via an AST import scan before the Dockerfile was written).docker run --network host --entrypoint python3 arc-mev-bot:latest -c '<eth_blockNumber via 127.0.0.1:8545>'returned a real block number —network_mode: hostreaches the local node exactly like the Arc node's own containers do.Two real bugs found and fixed live (both already pushed to this branch):
preflight_code_checks()sent all-983-pools'eth_getCodeas one unbounded JSON-RPC batch → the local node's HTTP response came back with a missing id. Fixed: paged into 200-request chunks (commit561b665).config/pools.testnet.json(8-pool sample) has no top-levelchain_idand every pool is missingfee_bps—registry.py's validator correctly refused it, which crash-looped the container against the default config. This wasn't Docker-specific:bot/search.py's own dry-run CLI would hit the identicalRegistryError— it had just never been run end-to-end. Fixed by replacing the 8 pools with their verified counterparts from the realdiscover_pools.pyfull sweep (983 pools, data-director's output) — same pools, backfilledchain_id/fee_bps/reserves_nonzero_at_blockfrom that verified data, recomputedregistry_hash(commit1d7f46c).Permission gap found and fixed on the box (not committed — host/operational, not repo state):
/var/lib/arc-mev-bot/textfilewas root:root 0755; the container's non-rootarcmevuser (UID 999) couldn't write its.promfile there.chmod 0777(multi-writer drop zone for non-sensitive Prometheus text — the same directory the host'sarc-mev-ordering-regimetimer, running as root, also writes into). Confirmed merging intohttp://127.0.0.1:9109/metricsafterward.220-block live run (real chain state, block 62431931→62432154, real 983-pool registry via a local override, dry-run only): 138 opportunities found (all real 2-3 hop v2 cycles), all ultimately dropped at the bidding stage (30
regime_coldduring warm-up, 108unprofitable_at_base— none cleared the real 1.00 USDC bidding margin against these thin testnet pools), sosubmit.Submitter.execute()'s sim/dry-run path itself wasn't exercised by this live run (it is exhaustively covered offline inbot/tests/test_main.pyagainst real recorded opportunities).truncated_blocks: 0.search_msp50 5.7ms/p90 8.6ms (matches revenue-director's own isolated benchmark). Whole-pipelinewall_msp50 76.7ms/p90 126ms but p99 563ms / max 1877ms — over the ~500ms block budget under real RPC contention/failover (public endpoints hitting-32005, occasional local-node "response missing id"); flagged as an open item below, not hidden.Resource usage:
arc-mev-botcontainer 1.4-6.7% CPU, 33-49MiB RAM vs.arc-execution's 7GB/13%+ on the same 4-vCPU box — no node contention.Zero transactions sent, confirmed on-chain across the entire session (baseline → 220-block run → restart → second run): executor
0x3b32…BD6cnonce stayed0x3, balance stayed40716030781864998014722wei; owner0x4a35…F290nonce stayed0x0, balance stayed1000000000000000000wei; executor contract's own USDC inventory stayed2000000(2.000000 USDC). No new tx from either address.Container survives a restart: proven twice — (1) the
--max-blocks 220verification run exited 0 cleanly and Docker'srestart: unless-stoppedbrought it back up automatically; (2) an explicitdocker compose restart botafterward came backhealthywithin theHEALTHCHECKwindow.RestartCount: 0in its current steady state (production defaults, committed 8-pool registry).Metrics merge — real finding, NOT fixed in this PR (flagging for devops-director): the host's standalone
arc-mev-metrics.service(python3 -m bot.metrics) and this container both import the samebot/metrics.pyshared metric set. Metrics only ever.inc()'d by the container (e.g.arcmev_blocks_seen_total) merge fine (the host's own copy has no children, so only a duplicate# TYPE/# HELPheader appears — cosmetic). Butarcmev_up_since_secondsandarcmev_build_infoare.set()at import time in both processes, so the merged/metricsoutput has two genuine duplicate-name samples forarcmev_up_since_seconds(different values) — a real Prometheus exposition-format violation once anything actually scrapes this endpoint (wave-3, perdocs/monitoring.md, not yet registered). Recommend: once a real bot container is the source of truth, stoparc-mev-metrics.service's own in-processREGISTRY.render()(textfile-passthrough only), matching that unit's own comment ("When the bot itself callsmetrics.serve()later, stop this unit — one listener per port"). Out of this PR's scope (sharedbot/metrics.pycontract +arc-mev-metrics.serviceownership).Open items (not blocking, all honestly reported):
wall_msp99/max over the ~500ms block budget under contended/rate-limited conditions — no per-block deadline enforcement at the orchestrator level yet (search.py has its own internal cycle-enumeration deadline; the whole-pipeline loop does not). Worth a follow-up wave.submit.Submitter.execute()pastsimulate()) wasn't exercised by the live 220-block run itself (every real opportunity found was economically below the real bidding margin) — it IS exhaustively covered offline inbot/tests/test_main.pywith real recorded opportunities and a fake transport.make lint / make test: 284/284 pass. make forge-test: not run locally (no
forgeon this host, unchangedcontracts/, runs in CI).automerger v2 auto-approved: all 7 gates pass. Approver: fortiblox-dev (opposite-identity rule). Gates: mergeable=true,label=auto-merge,author=buddysan,identity=ok,status_checks=not_required,signatures=upstream_BP_only,opt_in=repo_marker.
Auto-merged by forgejo-automerger at 2026-09-16 16:54:56 UTC. Approver:
fortiblox-dev(opposite-identity rule, Option C upgrade 2026-05-13). Merger:buddysanviafast-forward-only(NOT adminforce_merge). Criteria passed:mergeable=true,label=auto-merge,author=buddysan,identity=ok,status_checks=not_required,signatures=upstream_BP_only,opt_in=repo_marker. Branch28e54a3302merged ontomainand feature branch deleted. Seeproject_forgejo_automergermemory for rationale.