High-throughput DeFi swap simulation and route encoding on live Tycho-backed state.
Quick start · API · Verification · Docs
DSolver Simulator is a Rust service for DeFi quote simulation and route encoding. It subscribes to the internal Tycho broadcaster for native and VM state, maintains an in-memory view of pool state, and exposes fast HTTP endpoints for quoting, route settlement encoding, and readiness checks.
- Live-state simulation service for swap and routing workloads.
- Built on Axum and Tokio, with
tycho-simulationandtycho-executionhandling pricing and route execution details. - Designed for solver and routing integrations that need both quote coverage and deterministic route encoding behavior.
- Continuous ingestion of native and optional VM-backed pool state through the internal broadcaster service.
- Structured
/simulateresponses that distinguish usable quotes, partial coverage, warmup states, and request-level failures. /encoderoute resimulation before calldata generation, so settlement interactions are built from the same runtime view used for quoting.- Reporting-first local analysis tooling for readiness, latency, sampled evidence, and quick regression investigation.
| Category | Details |
|---|---|
| Language | Rust 2021 |
| Runtime | Axum + Tokio |
| Binary | dsolver-simulator-service |
| Endpoints | GET /status, GET /ready, POST /simulate, POST /encode |
| Supported chains | Defined in simulator-manifest.toml |
| Required inputs | CHAIN_ID, TYCHO_BROADCASTER_URL, Redis connection settings; the broadcaster also requires TYCHO_API_KEY |
| Common optional inputs | RPC_URL, ENABLE_VM_POOLS, ENABLE_RFQ_POOLS, HOST, PORT |
| License | MIT |
cp .env.example .env
scripts/start_server.sh --repo . --chain-id 1
scripts/wait_ready.sh --url http://localhost:3000/ready --expect-chain-id 1Use the explicit -p ... --bin ... form for local runs and builds so it's always clear which
workspace binary you're targeting.
For manual service runs, start dsolver-tycho-broadcaster-service first on the port used by
TYCHO_BROADCASTER_URL, then start dsolver-simulator-service.
Required runtime inputs:
TYCHO_API_KEYfor the broadcaster's Tycho access; the simulator does not use itCHAIN_IDfor chain selection fromsimulator-manifest.tomlTYCHO_BROADCASTER_URLpointing at the active broadcaster HTTP base URL, for examplehttp://127.0.0.1:3001BROADCASTER_REDIS_URLandBROADCASTER_REDIS_STREAM_KEYfor the Redis stream that carries broadcaster deltas after each HTTP snapshot replay boundary
Common optional inputs:
RPC_URLto enable on-chain helpers that need JSON-RPC access, such as ERC4626 depositsENABLE_VM_POOLSto enable or disable VM-backed pool feedsENABLE_RFQ_POOLSto enable or disable RFQ pool feeds (defaults tofalse)BEBOP_USER,BEBOP_KEY,HASHFLOW_USER,HASHFLOW_KEY,LIQUORICE_USER, andLIQUORICE_KEYonly whenENABLE_RFQ_POOLS=truefor chains that enable those RFQ providersHOSTandPORTto change the bind addressBROADCASTER_TOKEN_MIN_QUALITYto tune the broadcaster's startup Tycho token quality floorBROADCASTER_SNAPSHOT_MAX_PAYLOAD_BYTESto cap serialized HTTP snapshot payloadsBROADCASTER_SNAPSHOT_SESSION_TTL_SECSto set how long an unattached snapshot session can wait before cleanupBROADCASTER_REDIS_BLOCK_MS,BROADCASTER_REDIS_READ_COUNT, andBROADCASTER_REDIS_APPEND_RETRY_WINDOW_MSto tune Redis stream reads and append retriesBROADCASTER_REDIS_MAXLENcaps retained Redis entries (default5000) and forces simulators to rebootstrap if replay data is trimmed before catch-upTOKEN_SNAPSHOT_TIMEOUT_MSfor the simulator's startup load of the full broadcaster token snapshotTOKEN_REFRESH_TIMEOUT_MSfor RFQ provider token bootstrap and later single-token lookup misses- timeout and stream-health knobs from
crates/runtime/src/config/mod.rs
crates/runtime/src/config/mod.rs is the authoritative source for runtime defaults. .env.example
is an example setup, not the source of truth for every default.
The simulator bootstraps local state from the active broadcaster's HTTP snapshot session, then replays Redis Stream deltas after the snapshot replay boundary returned by that session. Redis is the delta transport, not the full-state bootstrap store.
The shared broadcaster-replay-client crate owns that bootstrap and Redis replay contract, so simulator and external consumers use the same checkpoint, gap detection, and recovery rules.
Broadcaster deployments use four modes:
Passivewarms upstream/cache state, but does not append Redis deltas or serve snapshot sessions.Activeis the only Redis writer and the only snapshot-session authority for an environment and chain.Retiredrejects appends and snapshot sessions after it has been replaced.Unhealthyfails closed until it recovers or is replaced.
Redis append and snapshot-session creation are fenced so stale writers cannot publish after promotion. A same-process reconnect keeps the writer generation and publishes a private full-replacement recovery transaction through the existing Redis stream. Consumers expose that replacement only after its commit, then apply ordered catch-up entries. Invalid, incomplete, or trimmed recovery data fails closed and falls back to a fresh HTTP bootstrap. Each simulator uses its own XREAD position; the design does not use Redis consumer groups or per-deployment stream keys.
POST /simulatereturns per-pool quotes across the requested amounts plusmetadescribing quote completeness, failures, and readiness-adjacent request outcomes.POST /encodeaccepts a client-provided route, re-simulates the swaps internally, and returns ordered settlementinteractions[].GET /statusis a liveness endpoint that always reports the current state with HTTP200.GET /readyuses HTTP200or503for native traffic readiness and includes the same backend detail.
/encode keeps its current HTTP control flow, but the server emits one structured completion log per request with route shape, protocol summary, and failure-stage fields. Detailed resimulation traces stay available at debug.
Detailed integration docs:
- docs/simulate_example.md for
/simulaterequest and response shape - docs/encode_example.md for
/encoderequest and response shape
/simulate uses a structured response contract. Clients should not treat HTTP 200 OK as quote success on its own.
QuoteStatus is the top-level request state:
readywarming_uptoken_missingno_liquidityinvalid_requestinternal_error
QuoteResultQuality describes result completeness:
completepartialno_resultsrequest_level_failure
meta.partial_kind appears only when result_quality=partial:
amount_ladderspool_coveragemixed
Summary matrix:
| Situation | meta.status |
meta.result_quality |
Usable For Quoting |
|---|---|---|---|
| Usable quotes with full coverage | ready |
complete |
yes |
| Usable quotes with partial requested-amount coverage or incomplete pool coverage | ready |
partial |
yes |
| No usable quote because liquidity is absent or exhausted | no_liquidity |
no_results |
no |
| Request completed with valid payload but request-level failure details | ready or internal_error |
request_level_failure |
no |
| Warm-up, token coverage, or request validation problem | warming_up, token_missing, or invalid_request |
request_level_failure |
no |
/simulate results are usable for quoting when:
meta.status=readymeta.result_quality=completeormeta.result_quality=partial
meta.failures is the request-level failure summary. It captures timeouts, cancellations, token coverage problems, no-pools reasons, and request-relevant simulation failures.
meta.pool_results is the per-pool outcome summary. It captures pool-local outcomes such as partial_output, zero_output, timed_out, simulator_error, and internal_error.
For partial results, emitted pool rows keep the original request order and request length in amounts_out. Requested amounts that fail are serialized in place as "0", and the matching gas_used entries are 0.
Treat "0" in amounts_out as "this requested amount did not produce a usable quote for that pool," not as a real quote. data[] only contains pools that produced at least one positive output across the requested amounts. Fully-zero rows are filtered out of data[] and stay visible only through meta.failures and meta.pool_results.
data[] order is deterministic, but it is not a best-to-worst ranking. Clients should rely on requested-amount alignment within each row and evaluate returned pools explicitly instead of inferring meaning from data[0].
meta.vm_unavailable=true means VM pools were skipped because VM state was not ready. When usable native quotes still exist, that typically surfaces as a ready + partial response rather than a hard non-ready status.
GET /status is the liveness view and always returns 200 OK. GET /ready is the readiness source of truth:
200 OKfrom/readywithstatus="ready"when native traffic can be served503 Service Unavailablefrom/readywhile native readiness is not readybackends.native.status="ready"when the broadcaster subscription is live, bootstrap is complete, and native state is ready and not stalebackends.native.status="warming_up"while initial native state is still loadingbackends.native.status="stale"when native updates are past the readiness freshness window
backends.vm.status is one of:
disabledwarming_uprebuildingstaleready
backends.rfq.status is one of disabled, warming_up, stale, or ready. Native and VM backend status use block_number; RFQ backend status uses update_timestamp for the current Tycho RFQ update cursor.
Timeout behavior differs by endpoint:
/simulaterequest-guard timeouts return200 OKwith a contract-valid payload whosemeta.status=ready,meta.result_quality=request_level_failure, andmeta.failuresincludes atimeout/simulaterouter-boundary timeouts also return200 OKwithresult_quality=request_level_failure/encoderouter-boundary timeouts return408 Request Timeoutwith{ "error": "..." }, plusrequestIdwhen it is available/statusand/readyare not wrapped in the router timeout layer
For ops, /encode timeout and failure logs include stable encode_error_kind and failure_stage fields so CloudWatch queries can separate validation, readiness, normalization, resimulation, encoding, handler_timeout, and router_timeout paths.
If you are integrating against /simulate, inspect meta on every successful HTTP response. If you are integrating against /encode, normal HTTP success and error handling is still the right control flow.
Production task definitions pin immutable git SHA image tags, so pushing an image is inert. The force redeploy steps only cycle tasks on the SHA already pinned. Go live by bumping imageTag in the solver-iac stack YAML and manually dispatching that stack there. The staging push flow still exists, but it deploys into a fully dark environment.
CI-equivalent commands:
cargo fmt --all -- --check
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo nextest run --workspace
cargo build -p apps --bin dsolver-simulator-service --release
cargo build -p apps --bin dsolver-tycho-broadcaster-service --releaseLocal analysis harness:
cargo run -p apps --bin sim-analysis -- --chain-id 1 --stop
cargo run -p apps --bin sim-analysis -- --chain-id 8453 --stopWhen TYCHO_BROADCASTER_URL points at local loopback, the analyzer uses the repo lifecycle
helper to start Redis, the broadcaster, then the simulator. Non-local broadcaster or Redis URLs
are treated as externally managed. To verify the Redis replay path while services are running:
scripts/verify_broadcaster_redis.sh --repo .To run the live mainnet VM wire e2e, point at an already running mainnet broadcaster plus Redis and enable the explicit live-test gate:
DSOLVER_LIVE_MAINNET_VM_E2E=1 \
TYCHO_BROADCASTER_URL=http://127.0.0.1:3001 \
BROADCASTER_REDIS_URL=redis://127.0.0.1:6379 \
cargo test -p runtime mainnet_vm_liquidity_survives_snapshot_and_redis_delta_wire \
-- --ignored --nocapture --test-threads=1The test expects Curve and Balancer VM deltas from live mainnet traffic. A normal run should usually finish in about 10 minutes, and it fails after 20 minutes.
Container builds:
docker build --secret id=github_token,env=GITHUB_TOKEN -f Dockerfile.simulator-service -t dsolver-simulator-service .
docker build --secret id=github_token,env=GITHUB_TOKEN -f Dockerfile.broadcaster-service -t dsolver-tycho-broadcaster-service .Useful helpers:
scripts/start_server.shto start the local broadcaster plus simulator stack with repo-local PID and log filesscripts/wait_ready.shto poll/readyand enforce chain, native, VM, and RFQ readiness expectations; native readiness remains the default gatescripts/verify_broadcaster_redis.shto check the broadcaster replay boundary, available Redis stream history, and simulator catch-up statusscripts/stop_server.shto stop services started by the repo helpercargo run -p apps --bin sim-analysis -- ...to generate a JSON and markdown local behavior report
The analyzer is intentionally reporting-first. It exercises representative /simulate and /encode flows, plus latency and light stress probes, then writes artifacts under logs/simulation-reports/, including simulator and broadcaster log excerpts, so reviewers can inspect anomalies, compare against previous local runs, and decide what matters instead of relying on a rigid pass or fail harness. On Base, RFQ-enabled runs also include a Bebop partial-fill encode diagnostic that inspects router calldata for the packed originalFilledTakerAmount token-in invariant.
- docs/simulate_example.md:
/simulateAPI examples and integration notes - docs/encode_example.md:
/encodeAPI examples and route-shape notes - docs/quote_service.md: maintainer deep dive for quote lifecycle, classification, observability, and integrations
- STRESS_TEST_README.md: local simulation analysis workflow and report artifacts
skills/simulation-service-analysis/SKILL.md: repo-local analysis skillskills/tycho-cloudwatch-logs/SKILL.md: CloudWatch log triage workflow
This project is licensed under the MIT License.
