diff --git a/ETHERSCAN_MIGRATION.md b/ETHERSCAN_MIGRATION.md new file mode 100644 index 00000000..d04613d7 --- /dev/null +++ b/ETHERSCAN_MIGRATION.md @@ -0,0 +1,129 @@ +# Etherscan migration — true resolution time through the whole stack + +**Goal:** every consumer of `res_t` reads the exact on-chain +`ConditionResolution` timestamp instead of endDate metadata. Measured rot +that motivates this (2026-07-08): the Jul-7 Brewers market carried +`end_date_iso` **2026-05-05** and cache res_t **29h before** true resolution +(04:51 next morning); a Jul-5 tennis match carried res_t **Jul-14** (future); +an entire class of in-play sells could never pass a timestamp test. +`payouts.resolution_time(cond)` (wired, verified) is the source; this plan +is the *consumption* migration. **No code until the plan is agreed.** + +## Ground rules (scar tissue from the 2026-07-08 alignment audit) + +1. **Overlay, never rewrite.** Chain timestamps live in `resolution_times` + (own table). `bets.res_t` is never mutated — provenance stays intact, + every flip is a one-line revert per consumer. +2. **One consumer per phase**, each with a before/after diff reviewed + *before* publishing. Never two flips in one day. +3. **Never couple a flip with a follow-set change** — attribution dies. +4. **Calibration continuity:** any phase that changes model output adds a + note (and a `rules_version` marker) to `live/history/calibration.csv` + that day, so the live-vs-model series stays interpretable. +5. Docs land with the phase, not after: FINDINGS gets the measured deltas, + README gotchas get amendments, HANDOFF tracks the phase counter. + +## Scale facts (measured 2026-07-08) + +- 974,155 distinct conds in the bets cache; **783,911 resolved**; + 141,396 in exits; 66,095 in `resolutions` (payout cache); 1 in + `resolution_times` (the Brewers proof). +- Per-cond Etherscan queries are a non-starter: 784k ÷ 5 req/s ≈ 43h of + calls against a 100k/day cap ≈ **8 days**. +- **The chain sweep is the enabler:** ONE topic0-only `getLogs` walk over + the CTF contract returns every ConditionResolution ever, 1,000 events per + call ≈ **~1,000 calls ≈ minutes**, resumable by block cursor. Nightly + increment afterward = 1–3 calls in `daily.sh`. +- `res_t` consumer weight (grep): trust.py 26 · portfolio.py 23 · + cache.py 11 · conviction_scan.py 5 · validate_timing.py 4 · skill.py 4 · + sync_floors.py 1. Flip order below runs shallowest → deepest. + +## Phase 0 — full-chain backfill (enabler, no behavior change) + +Build `live/resolution_sweep.py`: windowed topic0-only walker over the CTF +contract (chainid=137), resumable `last_block` cursor, throttled ≤5 req/s, +writes `(cond, res_ts)` into `resolution_times`. Wire the incremental call +into `daily.sh` (cache is single-writer — it runs inside the pipeline slot). + +Exit criteria / audits before any consumer flips: +- **Coverage:** ≥99% of *resolved* conds that appear in any scoring path + (trusted rows for scanned wallets, exits, portfolio replay) have a chain + timestamp; sample the misses by hand. +- **negRisk verification:** confirm ConditionResolution fires for negRisk + conds (oracle = the adapter) on a handful of known negRisk markets. +- **Uniqueness:** assert one event per cond in the sweep (if re-reports + exist, keep the LAST and document). +- **The rot histogram:** distribution of `res_ts − metadata res_t` across + all matched conds → FINDINGS. This quantifies 18 months of silent error + and predicts which downstream metrics will move. + +## Phase 1 — read-only shadow audit (no behavior change) + +Recompute both ways, publish nothing: +1. **validate_timing shadow:** med_lead_h and the held/timing gates with + chain res_ts — does the 35-sharp membership change? Which wallets flip? +2. **portfolio shadow:** replay with (a) settle/capital-recycle at true + res_ts (today's game-day-midnight settles free capital EARLY — expect an + equity haircut from honest capital lock), (b) timestamp sold-test + primary with the price test retained as fallback. Record equity/record + deltas per wallet. +3. **trust shadow:** agreement rate between consensus res_t and chain + res_ts — the empirical case for how much of trust.py survives Phase 5. + +Deliverable: one FINDINGS section with the three deltas. Flip order below +is confirmed or reshuffled *on this data*. + +## Phase 2 — flip validate_timing (lowest stakes) + +Lead metrics + timing gates read chain res_ts (fallback: old logic where a +cond has no event, e.g. genuinely unresolved). If sharp-list membership +changes, that's honest — document the entrants/leavers like a follow-set +change review. Sharps table republished; dashboards unchanged structurally. + +## Phase 3 — flip portfolio.py (moves published numbers) + +Settle times, sold-test primary, threshold windows (`res_t < START`) all +read chain res_ts. Then: full row-audit vs the live bot book (the 7/9-style +check — agreement must not regress), republish, calibration note (rule 4). +Expect and pre-announce the equity change; the honest number wins. + +## Phase 4 — flip conviction_scan + skill + floors (selection layer) + +Train/test splits and conviction windows on chain res_ts. Floors re-pin via +`sync_floors` and may shift a few % → coordinate a same-day +`deploy_bot.sh` so bot and backtest keep gating identically (rule 3 still +holds: no wallet adds/drops that day). + +## Phase 5 — simplify trust.py (LAST, biggest payoff) + +With chain res_ts + chain payouts covering all scoring conds, the consensus +res_t machinery, the v2 self-certification split, and the 14-day pulled_at +fallback reduce to: *"resolved on-chain? what payout? when?"* Keep consensus +only as a fallback for conds absent from chain (Phase 0 says ~none). +Regression bar: the sharp list before/after must be identical or every +difference individually explained. This deletes the most complex code in +the repo — do it after the chain data has soaked a week+. + +## Parallel track (independent, any time) + +Port insider.py's funding-cluster tracer from Alchemy `getLogs` (now capped +at 10 blocks on free tier) to Etherscan V2 logs. No interaction with the +res_t work. + +## Risks + +| risk | mitigation | +|------|------------| +| Etherscan limits/downtime | cache-forever table; resumable cursor; env `ETHERSCAN_KEY` override; sweep re-runnable idempotently | +| negRisk event semantics differ | Phase 0 verification gate before any flip | +| UMA disputes / duplicate events | uniqueness assert in Phase 0; disputes precede first report, immutable after | +| DuckDB single-writer | sweep runs in the daily pipeline slot only | +| numbers move and surprise | shadow audit first; pre-announced deltas; rules_version in calibration.csv | +| Etherscan V2 API drift | pin chainid=137 param form; smoke test in sweep preamble | + +## Sequencing + +Phase 0+1 fit one session (~2–3h, mostly sweep runtime + audit reads). +Each flip (2, 3, 4) is its own session with regen + publish + docs. +Phase 5 waits a week+ of soak. Total: ~4 working sessions spread over +~2 weeks, zero downtime, every step revertible. diff --git a/HANDOFF.md b/HANDOFF.md index 77f1c2f7..c4ad8281 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -42,11 +42,12 @@ The repo is authoritative; project memory has deeper history. ## Queued next-session work (in value order) -1. **Exact-res_t migration**: `payouts.resolution_time(cond)` (Etherscan V2, - wired+verified 2026-07-08) gives true resolution timestamps; migrate - trust.py / validate_timing / portfolio.py to consume them instead of - endDate metadata. Alignment-grade change — same care as the 2026-07-08 - audit. Backfill throttled ≤5 req/s (Etherscan free tier). +1. **Exact-res_t migration**: plan agreed 2026-07-08 — execute + [ETHERSCAN_MIGRATION.md](ETHERSCAN_MIGRATION.md) phase by phase + (0: chain sweep backfill → 1: shadow audit → 2: validate_timing → + 3: portfolio → 4: selection+floors → 5: trust simplification LAST). + One phase per session, before/after diffs, never coupled with + follow-set changes. 2. **Empirical fill model**: every copy now logs a `book` snapshot (spread + 5c depth) to the fills ledger; once a few weeks accumulate, fit slippage vs stake/depth and replace portfolio.py's flat SLIP haircut; add a depth