degenbot.runner.build_paths¶
Path discovery + registration for the settlement-arbitrage BotRunner.
Extracted from examples/eth_backrun_v2_v3_v4_rust.py.
Owns build_paths and its registration machinery:
ConstructionContext (registration-owned construction resources kept
out of the main-loop trim), PathRegistrationPipeline (the reusable,
pump-concurrent per-path registration / verify / dedup), and the bounded
producer/consumer helper that drives it.
The driver is Python-companion orchestration (stays-python): it registers
paths with the Rust-owned engine (EngineRegistry) but owns no pool state.
Module Contents¶
- degenbot.runner.build_paths.REG_INTAKE_WINDOW = 32¶
- class degenbot.runner.build_paths.RegistrationUnitOutcome¶
The per-path unit outcome, reported back to the driver.
The units run on fleet seats (plain threads, possibly concurrent), so they NEVER touch the pipeline counters — they return one of these and the single-loop driver folds it into the summary counters exactly as the retired inline
_consumedid (counter parity is the PRG-3/5 bar).
- degenbot.runner.build_paths.resolve_directions(pools: list[degenbot.UniswapV2Pool | degenbot.UniswapV3Pool | degenbot.UniswapV4Pool], input_token_address: str) list[bool]¶
Determine zero_for_one for each hop so the cycle closes.
The cycle: input_token → hop_0 → intermediate → hop_1 → … → input_token. Returns a list of zfo values (one per hop).
The mechanic lives in the core (degenbot_pathfinding::directions::resolve_directions); this adapter flattens the constructed pools into the typed seam’s hop tuples and re-raises the core’s refusal as
DirectionResolutionError. Resolution is kind-blind. V4 pools use NATIVE_CURRENCY_ADDRESS (address(0)) for ETH, which the core treats as equivalent to WETH — the profit token is always WETH. The core’s refusal surfaces asDirectionResolutionError— a hop carries neither tracked token, or the cycle does not close: an invariant violation (wrong pool built, stale subgraph, or a builder bug), never a skip.- Returns:
One zero-for-one value per hop, in hop order.
- class degenbot.runner.build_paths.ConstructionContext¶
Registration-owned construction resources, kept out of run()’s trim.
Bundles everything
build_pathsneeds to construct and register pools, so the registration task owns them as a single self-contained context for its lifetime.BotRunner.run()trims main-loop state (release_python_state()+self.bot = None); the context is a separate identity that a background registration task holds and that the trim never severs — the decoupling seam for Sub-B (background registration on the pump runtime).The context holds RESOLVED POLICY VALUES only: the construction route (CONTEXT.md, Construction route — the ordered factory rungs + the generic builder rung) and the WETH token, built once here. The core route entry (
pool_builder::route) owns the walk — route order, the DB two-step identity, get-or-register into session state — and classifies every failure on the build-refusal taxonomy, so no tracker or snapshot object lives here (the retired three-tracker fallback chain was the bare except-and-continue bug this replaces).- bot: degenbot.Bot¶
- database_path: pathlib.Path¶
- construction_route: degenbot.builders.request.ConstructionRoute¶
- weth: Any¶
- classmethod for_bot(bot: degenbot.Bot) ConstructionContext¶
Build the construction context for a bot.
Resolves the route policy (the mainnet V3 fork factories in policy order, generic builder rung armed) and builds WETH once. The core route entry owns everything else about construction.
- Returns:
The construction context.
- class degenbot.runner.build_paths.PathRegistrationPipeline(*, context: ConstructionContext, engine_registry: degenbot.arbitrage.engine_registry.EngineRegistry, retry_policy: degenbot.arbitrage.RetryPolicy | None = None, max_paths: int, discovery_batch_size: int, progress_interval_secs: float | None = None)¶
Reusable, pump-concurrent registration pipeline (D1c).
Owns the per-path registration work that
build_pathspreviously ran inline: construction (through the retainedConstructionContext— the RustPoolBuilder), engine registration + verification, direction resolution, registered-path dedup, per-path release, and the summary counters.It is LONG-LIVED by design: it keeps the
ConstructionContextAND theengine_registryfor the session’s lifetime, so an operator can add a specific path (enqueue_path) or trigger a bounded on-demand discovery (trigger_discovery) at ANY time — including afterrun()trims the main-loop bot. The context survives the trim (Sub-A seam), so these methods never need the dropped Pythonbot. The pipeline never awaits the pump, so adds/discovery cannot block update/solve/dispatch.max_pathsis the registered-path cap (0= uncapped) and is REQUIRED, because it is a configuration value: the caller that resolved it (max_registered_pathsin production) states it, and a default here would be a second authority that no config layer can reach. Registration stops accepting new paths once the engine path registry reaches the cap; the engine then reaches steady state with a bounded path universe, so solve performance is observable without ongoing registration load. The pipeline announces the cap it was given at startup, since the code default and the running environment may differ.The fail-fast tripwire is preserved: a fatal
VerificationMismatchError/VerificationRpcErroris NOT swallowed here — it propagates out of the worker and must abort the pipeline loudly.- constr_ctx¶
- constr_bot¶
- constr_chain_id¶
- constr_database_path¶
- construction_route¶
- weth¶
- engine_registry¶
- retry_policy_obj¶
- discovery_batch_size¶
- path_count = 0¶
- cap_skip_count = 0¶
- skip_count = 0¶
- token_filter_count = 0¶
- engine_reject_count = 0¶
- dup_count = 0¶
- register_fail_count = 0¶
- v4_pool_count = 0¶
- v4_hook_rejected = 0¶
- v4_dynamic_fee_rejected = 0¶
- other_exc_count = 0¶
- capped = False¶
- emit_registration_progress(*, force: bool = False) None¶
Log the registration counters + top skip-reason breakdown.
The legacy
[build_paths] Progressline only fires whenpath_countreaches a multiple of 1000. During a discovery-heavy crawl that registers few paths it never fires, so the skip/dup/reject counts (and their reasons) stay invisible. This is the same summary emitted onforce(a wall-clock cadence) so the cause is always observable mid-crawl.
- async run_registration(*, producer: collections.abc.AsyncIterable[object]) None¶
Run the crawl: submit each discovered path as ONE fleet unit.
PRG-5: the bounded producer/consumer queue retired with the crawl shell — discovery iterates directly and every path leaves as a single
PoolStateUpdaterintake unit (build + verify lifecycles + path registration inside the Rust core). The concurrency is the fleet’s (duty-counted seats + its own bounded queue), and the driver-side backpressure is the submission window: at mostREG_INTAKE_WINDOWreceipts are outstanding, so discovery can never outrun registration by more than the window.Units are resolved in FIFO submission order (the retired workers’ ordering guarantee), so the Progress summary’s counter drift and the 1000-boundary log lines keep their retired shapes exactly.
Returns with EVERY submitted receipt resolved (the completion clause that replaced the retired executor-drain: all cloned
Arc<SnapshotDb>handles acquired inside units are dropped beforebuild_pathsreturns, keeping the close_snapshot_tx() Arc::try_unwrap canary quiet). The unit’s fatal exception (VerificationMismatchError / VerificationRpcError / DirectionResolutionError) propagates through the receipt and aborts the crawl loudly — the “shut down” contract, unchanged. On a fatal the crawl stops submitting immediately (outstanding units still finish — fleet units are never cancelled, the Deferrable cordon class).
- async enqueue_path(path_steps: Any, directions: list[bool] | None = None) None¶
Add ONE specific path at any time (D1c operator surface).
- async trigger_discovery(*, bound: int | None = None) int¶
Trigger a bounded one-shot discovery sweep (D1c).
A sweep that runs to NATURAL completion (not bound-truncated, not capped) latches the structural graph edition; a later trigger over the same edition stops immediately and returns 0 — the unchanged structure can only re-yield paths the pipeline already processed. The latch re-arms itself when the edition changes (pool added or removed), when the probe is unavailable, and after any truncated sweep.
- Returns:
The number of paths processed by the sweep.
- discovery_sweep(*, find_paths_async: collections.abc.Callable[..., collections.abc.AsyncGenerator[object, None]] = find_paths_async) collections.abc.AsyncGenerator[object, None]¶
Run a single discovery sweep over the DB subgraph (V2/V3/V4 DFS).
find_paths_asyncis the discovery producer seam (tests inject a recording producer to observe the forwarded batch size); the default is the production adapter.- Returns:
The async generator of discovered candidate paths.
- class degenbot.runner.build_paths.BuildPathsOptions¶
The knobs
build_paths()takes, one options object.Bundling the construction/registration inputs keeps the
build_pathscall site to the two required resources plus one options object; each field mirrors the former keyword parameter.max_registered_pathsanddiscovery_batch_sizeare the non-defaulted fields, and they are required for the same reason the pipeline’smax_pathsanddiscovery_batch_sizeare: both are configuration values, so the caller states what it resolved and no code path invents one. A caller that suppliespipelinealready carries them on the pipeline it passes.The snapshot fields are retired: per-pool tick data resolves core-side at construction (the DB arm of the Rust builder, or the Chain arm), so a driver-side snapshot object has no consumer in the construction route.
- context: ConstructionContext | None = None¶
- pipeline: PathRegistrationPipeline | None = None¶
- async degenbot.runner.build_paths.build_paths(*, bot: degenbot.Bot, engine_registry: degenbot.arbitrage.engine_registry.EngineRegistry, options: BuildPathsOptions) None¶
Discover V2/V3/V4 arb paths, build Python pools, register with Rust engine.
V4 pools are discovered via find_paths_async and built through
bot.build_managed_pool(). V4 pool admission (amount-modifying hooks / dynamic fees) is enforced by the Rust core at registration time, surfacing as typed HookedPoolRejectedError / DynamicFeePoolRejectedError. Each per-pool verify lifecycle runs through the core-owned bounded retry dance with the resolved policy injected (transientVerificationRpcErroris retried;VerificationMismatchErroris never retried and crashes loudly).Discovery is a single pass over the DB subgraph driven through a reusable
PathRegistrationPipeline; after it completes the orphan sweep releases Tracked pools whose path was skipped beforeregister_vN_pool.optionsis required because it carries the registered-path cap: the caller that resolved the cap states it, and this function never invents one for a pipeline it builds itself.