Skip to content

feat(simulation): stamp sequence-matched Titan overrides into price-level states - #1598

Open
tvinagre wants to merge 3 commits into
tl/price-level-stream-v2from
tl/price-level-stream-overrides
Open

tvinagre wants to merge 3 commits into
tl/price-level-stream-v2from
tl/price-level-stream-overrides

Conversation

@tvinagre

@tvinagre tvinagre commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

Builds on #1597 (tl/price-level-stream-v2), which it targets, and must merge after it.

Summary

  • A pAMM ladder prices the block that is still being built. A client that simulates the swap on
    mined state therefore simulates a different venue state: the venue reverts StaleUpdate(), or
    the fallback router pays a fallback price.
  • The client cannot fix this alone. Only Titan knows the account state it priced the venue on.
  • Titan now publishes that state on a second socket. This change reads it, matches it to the
    ladders it belongs to, and hands it to the client on the quote.

Terms used below

  • snapshot — the account state Titan simulated a venue on: per account, the balance, the nonce
    and the storage words it overrode, and nothing else. An absent field means the simulation read
    the chain's value. The same shape eth_call accepts as stateOverrides.
  • pair version — a number Titan stamps on a pair when it prices it. Books carry it as
    calculated_from; venue entries on the new socket repeat it as pairSequences. It is the only
    link between the two sockets.
  • build — one Titan pricing pass, stamped built_at_ns.
  • slot — 12 s, the life of one block. Used below as a duration.
  • stamp (verb) — attach a snapshot to a served state.

Counter names are written without their price_level_stream_ prefix.

What this adds

  • A second socket, pamm_quote_stream_v2. Per venue it carries the snapshot Titan priced on, plus
    the pair versions of the books that snapshot produced.
  • The stream holds accepted snapshots in a bounded buffer and joins one to the ladders of the same
    venue, the same pair and the same pair version.
  • Clients read the result through PriceLevelStreamState::simulation_overrides() and replay the
    quote on that state.
  • Quoting never reads it. spot_price, get_amount_out and get_limits answer from the
    ladders alone, and a quote stream frame produces no update of its own.
flowchart LR
  BK["socket 1: pamm_price_levels_v2<br/>a book: venue + pair<br/>+ calculated_from = pair version"]
  EN["socket 2: pamm_quote_stream_v2<br/>a venue entry: snapshot<br/>+ pairSequences + built_at_ns"]
  EN --> BUF["bounded buffer<br/>key: venue + pair<br/>holds a build for one slot"]
  BK --> J{"same venue,<br/>same pair,<br/>same version?"}
  BUF --> J
  J -- yes --> ST["state carries the snapshot"]
  J -- no --> NS["state serves its ladders,<br/>carries no snapshot"]
Loading

Decisions a reviewer needs

  • The join matches the full identity, exactly. A pair version alone identifies nothing: Titan
    stamps a batch of pairs with one version, and one captured entry repeats a single version across
    eleven pairs. Being in a venue's entry is no match either — the capture holds a Fermi book whose
    version that same Fermi entry advertises, but for two other pairs, and that book stays
    unstamped. A neighbouring pair's snapshot is never substituted, however close the two look.
  • The buffer fails closed, under fixed caps. Validation refuses a whole venue entry past 32
    accounts, 512 storage words, or 64 KiB of raw slot and word bytes. The buffer holds 8 builds per
    venue-and-pair key and 1,024 keys, and reads at most 16 venue entries per frame and 32 pairs per
    entry. Past a bound it drops the oldest build of that pair, or the key whose traffic it saw
    longest ago. A rotating sweep reaches every key within 32 frames, so a pair Titan stops
    streaming does not keep its last build.
  • Books that disagree stamp nothing. When several books of one venue and pair name different
    versions, no single snapshot explains the served ladders. The state then carries none and counts
    as ambiguous. The ladders are unaffected.
  • The attached deadline is the earlier of the ladder deadline and the snapshot lifetime. The
    narrowing is written on a copy, so the snapshot the buffer still holds for other pairs of the
    same entry keeps its own deadline.
  • One switch controls the feature. without_simulation_overrides() opens no second socket and
    holds nothing, and the match, buffer and refusal series then do not exist at all. One counter
    remains either way — invalid_provenance_total, because a book's version is read off the price
    level socket whether or not anything stamps — so its presence is no evidence that stamping is
    on.

The replay fence

A build's serving life is fixed when it is first admitted. Titan re-sending an envelope the stream
already holds changes nothing, including the deadline.

That needs bookkeeping when the local clock trails Titan's. A build stamped ahead of the clock
that admits it takes its slot from the admission, not from its stamp. Forgetting such a build and
re-admitting it later would compute a later deadline, handing it a second and longer life. The
stream therefore keeps an empty record of that build — no payload — until one slot after the
build's own stamp. That record serves nothing, answers expired, and refuses the same payload
again. Every removal path leaves one behind, and eviction never takes one: under pressure the
arriving build is refused instead.

flowchart TD
  A["build admitted<br/>serving life fixed here"] --> B{"stamped ahead of<br/>the local clock?"}
  B -- no --> C["serves its snapshot,<br/>at most one slot"]
  C --> D["record removed;<br/>may be re-admitted later"]
  B -- yes --> E["serves its snapshot until<br/>the admission deadline"]
  E --> F["empty record kept:<br/>serves nothing, answers expired,<br/>refuses the same payload,<br/>cannot be evicted"]
  F --> G["one slot after the build's<br/>own stamp: removed"]
Loading

Limits worth knowing

  • Serving is at most one slot, 12 s. That is the only number bounding what a client can simulate
    on.
  • An empty record can hold its place for up to two slots, 24 s, when the local clock trails
    Titan's. Held is not served: it answers expired and never matches.
  • Those records sit under the same 8-builds-per-key bound as snapshots. A coherent skew as small
    as 1 ms is enough to fence every build; one distinct build per second then fills all eight
    places of a key, and the newest build is refused under rejected_key_saturated until a place
    frees.
  • Nothing is mis-served in that state — the pairs keep quoting, the states simply carry no
    snapshot — but recovery by a newer build needs a free place. It is a coverage cost, not a
    correctness one. Watch rejected_key_saturated, and keep the host's clock from drifting behind
    Titan's.
  • No snapshot does not mean none is needed. It means the join found nothing usable, and
    simulating that leg on mined state can still revert or pay the fallback price.
  • A snapshot does not cover a route. Each state carries the snapshot of its own venue and pair
    alone, so a route across several pAMM legs reaches only as far as its weakest leg.
  • A match is the producer's claim: one book says the venue priced at one version, one entry says a
    snapshot is the state it priced that version on. Nothing either socket sends lets this code prove
    a single simulation produced both. The live check below tests that claim against the chain.

Client-visible change

  • expires_at is now computed once, from the build's own deadline, and floored once. For some
    builds a client reads a value up to one second later than the previous form reported for the
    same build. It is never earlier, and never later than the moment the stream stops serving.
  • The same build re-sent by Titan now reports the same second however late the repeat arrives,
    including across a removal and a re-admission.

Operator notes

  • Two released series change identity. reconnects_total and frames_rejected_total now
    carry a socket label, price_levels or quote_stream. On deploy each old series goes stale
    and a labelled one starts at zero — benign for rate and increase, and indistinguishable from
    a restart. Any rule keyed on the exact label set sees both series for the staleness window.
  • Upgrading opens a second outbound socket. Stamping is on by default, so a consumer that
    rebuilds with defaults dials wss://eu.data.titanbuilder.xyz/ws/pamm_quote_stream_v2 with no
    code change. without_simulation_overrides() opts out.
  • An egress policy that allows only the price level endpoint costs nothing in quoting. The second
    socket is isolated: it yields no update, removes no component, and its reconnect backoff caps at
    32 s. Retries show on reconnects_total{socket="quote_stream"} and one warn! per attempt.
  • Name both endpoints when changing region. override_endpoint(url) names the second socket,
    falling back to TITAN_PAMM_QUOTE_STREAM_URL and then the EU default. The two sockets of one
    region describe one build round; whether one region's snapshot joins another region's ladders is
    unestablished.
  • New counters. override_match_total{outcome} (5 outcomes),
    override_buffer_total{event} (18 retention events),
    refused_override_entries_total{reason} (10 producer-side refusals), and
    invalid_provenance_total. Read the buffer series as retention, never as coverage. No label
    carries a streamed value.

Dependencies

Two changes in crates/tycho-simulation/Cargo.toml:

  • serde_json gains the raw_value feature: the lenient calculated_from decode reads a
    Box<RawValue>, and a dependent resolves features per crate.
  • strum joins strum_macros: the telemetry derives generate impls of IntoEnumIterator and
    EnumCount, whose traits live in strum.

The workspace already pins both, so Cargo.lock gains one edge and --locked passes.

Coverage, as one capture measured it

In a 75 s capture of both EU sockets, 676 of 1,076 resolved books had a prior exact match (62.8%).
The upper bound is 64.35% if books whose symbols never resolved are credited. For books that did
match, the snapshot arrived a median 495.8 ms, and at most 1.897 s, before its ladders.

Read these as finite-capture observations, not steady-state guarantees. They count raw match
opportunities — before contradiction, before the caps, and before anything admission refuses — so
they are not a predicted attachment rate. An unmatched version means Titan did not publish that
snapshot during this capture, and nothing more. Lower coverage is truthful best effort; it is
never a licence to stamp another pair's snapshot.

Checked against the chain

A snapshot is only worth carrying if it makes the venue price the swap the way the quote said it
would. crates/tycho-integration-test/tests/pamm_simulation_overrides.rs tests exactly that: it
captures live quotes, waits for the block each quote targets, and simulates the encoded swap at
that block twice — once with the quote's own overrides, once with none.

Two quotes on Ethereum at block 26184402, both USDC -> WETH:

venue with the quote's overrides with no overrides
0xb09aaa89… simulated 200810640733174892, quoted 200810640733174899 (0 bps) revert 0x666a2814
0x71e790dd… simulated 993623040774703815, quoted 993623040774703815 (exact) revert 0x666a2814

0x666a2814 is StaleUpdate(), the venue freshness guard. So the overrides are what makes the
swap simulate at all, and the price they produce is the quoted price.

The test is #[ignore]d, because it dials the live sockets and an RPC. CI runs it only when
crates/tycho-simulation/src/price_level_stream/** or the test itself changes, since each run
costs a live capture and real RPC calls. It skips itself when Titan serves no quote or the RPC
cannot settle the quoted block, so it fails only on a real mismatch. Run it by hand with:

RPC_URL=… cargo test -p tycho-integration-test --test pamm_simulation_overrides \
  -- --ignored --nocapture

Two limits on that evidence. The simulator applies the storage half of a snapshot, so balance and
nonce go unexercised — the freshness guard reads storage. And it simulates at the quoted block once
mined, which is not the parent-anchored call a client makes before the block exists.

Tests

The module suite goes from 151 to 298 tests. Every test runs against local sockets with injected
wall and monotonic clocks; none dials a live endpoint. The capture fixtures are byte-faithful,
because a serde_json::Value round trip would narrow every pair version to an f64.

Suggested review order

  1. The Simulated pre-state section of price_level_stream/mod.rs — the contract this change
    makes.
  2. overrides.rs — the buffer and the fence. The retention rules are the part most worth
    disagreeing with.
  3. The join and the stamping, in titan.rs and tracker.rs.
  4. telemetry.rs, and the Simulation overrides section of
    docs/for-solvers/price-level-stream.md.

…evel states

A pAMM ladder prices the block that Titan still builds. A client that
simulates the swap on mined state simulates a different venue state.
The stream now reads a second Titan socket. That socket carries the
account state that Titan priced each venue on. The stream joins a
snapshot to the ladders of the same venue, pair and pair version, and
it attaches the snapshot to the state. Snapshots live in a bounded
fail-closed buffer, and a build's serving life is fixed at its first
admission. The exported expiry second is now computed once from the
build's own deadline. The reconnect and frame-rejected counters gain a
socket label, so their previous series go stale on deploy. The module
suite grows from 151 to 298 tests.
Reviewers asked what "the wire" is. Every use now says what it means:
Titan, the message, the stream, a streamed value, or raw JSON. Test
constants and one parameter are renamed to match. No behaviour change.
Captures live pAMM quotes, waits for the block each one targets, and
simulates the encoded swap at that block with and without the quote's
own overrides. The gate is that the simulated output matches the quoted
output; the run without overrides is reported, since whether mined state
alone is fresh depends on who built the block.

Ignored by default and gated in CI on changes to the price level stream,
because it costs a live Titan capture and real RPC calls.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

1 participant