Skip to content

feat(hindsight): solver-first decoding - #431

Draft
tamaralipows wants to merge 7 commits into
feat/hindsight-calldata-decodingfrom
tnl/hindsight-solver-first
Draft

feat(hindsight): solver-first decoding#431
tamaralipows wants to merge 7 commits into
feat/hindsight-calldata-decodingfrom
tnl/hindsight-solver-first

Conversation

@tamaralipows

Copy link
Copy Markdown
Contributor

Inverts the hindsight decoder: a trade's authoritative terms live in the settling solver's own call, so decoding starts there, and the venue is attributed afterwards as a label. Supersedes #428.

The pipeline now

  1. Trace the whole block — one eth_getBlockReceipts call plus one debug_traceBlockByNumber call. The RPC must support debug_traceBlockByNumber.
  2. Decode from the solver's side — the declared decode reads the settling solver frame's own calldata (or CoW's Trade log) and anchors the settled output at the declared recipient's ledger receipt; it runs for every matched transaction, not only Relay's. Balance netting is the fallback, and its records are marked.
  3. Attribute — venue and solver are labels resolved after decoding, from address-book data.

What this changes structurally

  • Venues have no code. All five venue decoder modules are deleted; a venue is its address-book section (entry points, fee collectors, solver aliases). One solver's calldata read now serves every venue that routes through it, identically.
  • One SolverDecoder trait per solver (declared swap, output recipient, veto, integrator tag), joined onto the registry's solver entry at address-book load. Trade-time lookups are by address; no name is matched on a hot path.
  • Two decode tiers, marked on every record. New additive JSONL column decode: "declared" (calldata/logs — trusted) or "netted" (fallback — an unaccounted fee can sit inside the amounts). The report covers declared records by default; --include-netted adds the marked tier. All other wire fields are unchanged.
  • Matching also accepts any transaction whose trace contains a known solver frame, so trades through unknown wrappers now decode. Unknown routers with no known solver anywhere stay undecoded.
  • The calldata-vs-netting disagreement warning and the duplicated declared-swap recovery are deleted; declared columns (min_amount_out, declared_quote, quote_timestamp) appear only on declared records.
  • Fee-wallet venue attribution checks wallets in address order, fixing the run-to-run venue flip when two venues' wallets cut the same trade.

Verification

Decoded mainnet blocks 25741800–25741815 on this branch and on the base branch: all 87 base trades reproduce with identical tokens, amounts, sender, venue, and solver; this branch adds 9 trades the base could not match (known solver frames inside unknown wrappers). Tier split: 35 declared, 61 netted. 253 unit tests, clippy clean.

Design notes: .claude/plans/solver-first-decoding.md.

tamaralipows and others added 7 commits August 17, 2026 16:09
The headline verdict and every dashboard number compare gross amounts
since July; the net-of-gas figures were secondary record columns with
no consumer. Removed end to end: the trader-paid gas derivation, the
solver-frame gas isolation from the trace, the settled_gas trade
field, the settled_gas_cost / settled_amount_out_net_gas / net_bps
record columns, and the gas-to-token price conversion. The report's
per-trade and per-group bps switch to the gross delta (raw_bps),
which every recorded dataset already carries. Fynd's own quotes stay
gas-aware; this removes only the settled-side bookkeeping.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One debug_traceBlockByNumber call replaces the per-transaction
debug_traceTransaction wave. A transaction the tracer cannot process
still costs only that trade; a failure of the whole call is the
block's error.
SolverKnowledge becomes SolverDecoder — one trait per solver for its
calldata reads, veto, and integrator tag. The implementation is joined
onto the registry's solver entry when the address book loads;
trade-time lookups go through Registry::solver by address. The
per-method name-dispatch functions are deleted.
The declared decode — the trade as the settling solver's own calldata
states it — runs first for every matched transaction, not only Relay's.
RelayCalldata is deleted; its logic and guards live venue-agnostic in
decoder/declared.rs, with venue fees recorded from address-book data.
Netting decoders are the fallback, and every record now carries a
decode column: declared (calldata or logs) or netted. The
calldata-vs-netting disagreement warning and its second recovery are
deleted — declared columns appear only on declared records.
Venue decoders are deleted. A venue is now only its address-book
section: entry points and fee collectors feed the shared netting
fallback and the fee bookkeeping; solver aliases feed attribution.
One netting module replaces the three (sender, venue, intent arms
picked by the entry point). CoW settlements decode from the Trade log
as declared data. TraderRole, DecodeContext, TradeDecoder, and the
receipt-only matching module are deleted — matching now also accepts
any transaction whose trace contains a known solver frame. Fee-wallet
venue attribution checks wallets in address order, so a trade cut by
two venues' wallets resolves the same way on every run.
The report's main results cover only declared records; --include-netted
adds the marked netting-fallback trades. Records from datasets written
before the decode column existed carry no marker and are always kept.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant