audit hardening: book fully serialized, chain-gated sweep, boot-id single-writer guard, TLS on user-ws, atomic valuebot state, hot-path throttle, newest-kept seen_tx, daily lock, ingest txn, AIcAIc pin

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jaxperro
2026-07-19 14:07:41 -04:00
parent f826815885
commit baa4182343
8 changed files with 126 additions and 13 deletions
+22
View File
@@ -1,5 +1,27 @@
# Session handoff — 2026-07-19 (rev 15: tape+bench+gate; rev 14: +$44 bankroll, exact paper-parity config; rev 13: FAK parity; rev 12: chain-seed)
## 2026-07-19 (rev 16): audit hardening shipped (top-10 items 1-9)
Deep-audit fixes, all deployed same day: (1) the book is now fully
serialized — resolve_pendings/retry_stuck_exits/write_feed take bot.lock,
_drain_fills snapshot-swaps the shared fills list (kills the last July-12
phantom-cash race class); (2) sweep_dust gates every action on CHAIN share
balance and credits proportionally (data-api ghosts book ~$0 forever);
(4) BOOT-ID single-writer guard — a stale process that survived machine-stop
sees a newer boot in origin's state at publish time and EXITS instead of
overwriting surgery (gotcha 15c is now an invariant, not a procedure);
(5) user-ws verifies TLS (it carries L2 creds; RTDS stays CERT_NONE — public
data); (6) valuebot state is tmp+rename atomic with a loud ⚠ STATE RESET
alarm; (7) settle_resolved throttled 20s off the webhook hot path (faster
copies in bursts); (8) daily.sh mkdir-lock (macOS has no flock) + ingest's
two inserts are one transaction; (9) seen_tx keeps the NEWEST 5000 via a
recency dict (was an arbitrary set slice); (10) AIcAIc live floor pinned to
paper's 341.23 (was boot-nondeterministic, violating mirror-exactly).
STILL QUEUED from the audit: redeem.py pUSD/deposit-wallet fix before any
auto_redeem re-enable (audit 3.3 — it would book phantom credits today);
listener-start-after-seed reorder (3.9); dead-code sweep (copytrade
LiveExecutor + broken CLI, _book_snapshot dedupe); reconcile_entries
positions-pull diet; sftp tape transport; rpc-down heartbeat flag.
## 2026-07-19 (rev 15): the rest of the day
- **RTDS TAPE live end-to-end**: wwf-recorder (own silo, no repo clone —
code baked in image) tapes every platform trade to hour-rotated gz
+2 -1
View File
@@ -69,7 +69,8 @@
{
"wallet": "0x921433c93558b9a4ba807ec824d02aad7ea2ddbf",
"name": "AIcAIc",
"class": "volume"
"class": "volume",
"floor": 341.23
}
],
"feed_path": "live/copybot_live_real.json",
+56 -5
View File
@@ -48,6 +48,7 @@ Usage:
"""
import argparse
import uuid
import hashlib
import hmac
import json
@@ -846,7 +847,9 @@ class UserFillsListener:
try:
app = websocket.WebSocketApp(self.URL, on_open=on_open,
on_message=on_message)
app.run_forever(sslopt={"cert_reqs": ssl.CERT_NONE})
app.run_forever() # audit 3.6: this socket carries live L2
# API creds — VERIFY certs (CERT_NONE was cargo-culted from the
# read-only RTDS pattern, where it stays)
except Exception as e:
log(f"user-ws: listener error {str(e)[:80]}")
self.state = "down"
@@ -914,8 +917,11 @@ class Copybot:
it overstates the edge."""
ex = self.engine.ex
buys = []
if hasattr(ex, "fills") and ex.fills:
for f in ex.fills:
# atomic snapshot-and-swap (audit 3.1): two threads draining the same
# shared list each applied every fill's cash delta before one cleared
fills, ex.fills = (ex.fills if hasattr(ex, "fills") else []), []
if fills:
for f in fills:
sign = -1 if f["side"] == "BUY" else 1
fee = taker_fee(f["shares"], f["price"], self.fee_rate)
f["fee"] = round(fee, 4)
@@ -945,7 +951,6 @@ class Copybot:
}) + "\n")
except Exception:
pass
ex.fills.clear()
return buys
def ledger_drift(self):
@@ -1239,6 +1244,10 @@ class Copybot:
return drift
def write_feed(self):
with self.lock: # audit 3.1: reconcile mutates bet records
return self._write_feed_locked()
def _write_feed_locked(self):
"""Publish the bot's live book to live/copybot_live.json — the feed the
top of jaxperro.com/trading reads. Reconciles any open bet no longer held
(mirror-sold) to 'closed'."""
@@ -1402,6 +1411,20 @@ class Copybot:
subprocess.run(["git", "-C", repo, "commit", "-q", "-m",
"copybot: live paper feed (resynced after "
"conflicted rebase) [skip ci]"], capture_output=True)
try: # audit 3.4: stale-writer suicide guard
up = subprocess.run(["git", "-C", repo, "show",
"@{u}:" + self.engine.state_path],
capture_output=True, text=True)
if up.returncode == 0:
ub = (json.loads(up.stdout).get("boot") or {})
ours = st.get("boot") or {}
if (ub.get("id") and ours.get("id") and ub["id"] != ours["id"]
and ub.get("ts", 0) > ours.get("ts", 0)):
log("⚠ a NEWER boot owns the book on origin — this "
"stale writer is exiting instead of overwriting it")
os._exit(0)
except Exception:
pass
p = subprocess.run(["git", "-C", repo, "push", "-q", "origin", "main"],
capture_output=True, text=True)
log("published live feed → dashboard" if p.returncode == 0
@@ -1459,7 +1482,8 @@ class Copybot:
if not rl.acquire(blocking=False):
return
try:
self._resolve_pendings_locked(st)
with self.lock: # audit 3.1: adoption mutates cash/my_pos/spend
self._resolve_pendings_locked(st)
finally:
rl.release()
@@ -1584,6 +1608,10 @@ class Copybot:
retries = st.get("exit_retries") or []
if not retries:
return
with self.lock: # audit 3.1: sells + fill drains mutate the book
self._retry_stuck_exits_locked(st, retries)
def _retry_stuck_exits_locked(self, st, retries):
keep = []
for r in retries:
tok = r["token"]
@@ -1671,6 +1699,19 @@ class Copybot:
continue
if tried >= 10:
break
# audit 3.2: the data-api LAGS the chain — a residual the chain
# already zeroed (platform auto-redeem) keeps reappearing here.
# Gate every action on exchange-view truth; act only on confirmed
# shares; credit proportionally so a stale row books ~$0.
try:
chain_sh = self.engine.ex._shares_held(tok)
except Exception:
continue # no chain truth -> no action
if chain_sh < max(0.01, shares * 0.5):
continue # data-api ghost — skip forever
if chain_sh < shares:
val *= chain_sh / shares
shares = chain_sh
if p.get("redeemable") or cur >= 0.99:
# resolved WINNER residual (the 2026-07-17 $1.90 case): the
# tracked-position redeemer never sees untracked tokens, so
@@ -2176,6 +2217,12 @@ class Copybot:
self.engine.persist()
def settle_resolved(self):
# audit 4: this ran on EVERY webhook POST ahead of the copy itself —
# dozens of redundant CLOB fetches during clip bursts. 20s throttle;
# the 60s heartbeat still guarantees timely settles.
if time.time() - getattr(self, "_settle_last", 0) < 20:
return
self._settle_last = time.time()
"""Free capital like the dashboard: when an open position's market has
resolved, settle it at the winner price (1/0), recycle the cash, and tally
realized P&L. This is the resolution path the engine's sell-only mirror
@@ -2486,6 +2533,10 @@ def main():
if want_live else "")
filt = FollowFilter(cfg)
bot = Copybot(cfg, engine, filt, redeemer=redeemer)
# single-writer invariant (audit 3.4): every boot stamps the state; a
# stale process that survived `machine stop` (gotcha 15c) sees a newer
# boot on origin at publish time and yields instead of overwriting it.
engine.state["boot"] = {"id": uuid.uuid4().hex[:12], "ts": int(time.time())}
# T0 detection — the RTDS trade stream (~1s, wallet-attributed). Paper
# runs it by default (the 24h shadow run, 2026-07-10); the REAL-MONEY
+24 -2
View File
@@ -129,6 +129,26 @@ def save_json(path, data):
os.replace(tmp, path)
class _SeenTx(dict):
"""Set-compatible seen-tx registry that remembers insertion recency so
persist() can keep the NEWEST N (membership: `tx in seen`; value = a
monotonically increasing sequence number)."""
_n = 0
def __init__(self, items=()):
super().__init__(items)
if self: # resume the sequence ABOVE loaded entries, or fresh
_SeenTx._n = max(_SeenTx._n, max(self.values()) + 1)
def add(self, tx):
_SeenTx._n += 1
self[tx] = _SeenTx._n
def update(self, txs):
for t in txs:
self.add(t)
def new_state():
return {
"started_at": time.time(),
@@ -310,7 +330,7 @@ class CopyTrader:
self.ex = executor
self.state_path = state_path
self.risk = cfg["risk"]
self.seen = set(state["seen_tx"])
self.seen = _SeenTx((tx, i) for i, tx in enumerate(state["seen_tx"]))
self.webhook = cfg.get("discord_webhook", "")
self._discord_warned = False
@@ -397,7 +417,9 @@ class CopyTrader:
del missed[:-200] # keep the recent 200
def persist(self):
self.state["seen_tx"] = list(self.seen)[-5000:]
# newest-5000 by recency (audit 3.8: a set slice kept an ARBITRARY
# 5000 — a crash+fast-restart could re-copy a fresh trade)
self.state["seen_tx"] = sorted(self.seen, key=self.seen.get)[-5000:]
save_json(self.state_path, self.state)
# -- risk gate: returns (allowed_usd, reason_if_blocked) --
+5 -1
View File
@@ -30,7 +30,11 @@ if [ -z "${DAILY_COPY:-}" ]; then
export DAILY_SRC DAILY_COPY
exec /bin/bash "$DAILY_COPY" "$@"
fi
trap 'rm -f "$DAILY_COPY"' EXIT
LOCKDIR="${TMPDIR:-/tmp}/wwf-daily.lock.d"
if ! mkdir "$LOCKDIR" 2>/dev/null; then
echo "[daily] another run holds the lock ($LOCKDIR) — exiting"; exit 0
fi
trap 'rmdir "$LOCKDIR" 2>/dev/null; rm -f "$DAILY_COPY"' EXIT
cd "$DAILY_SRC"
# heads-up ping so the run's start is visible in Discord (digest comes at the end)
python3 discord_daily.py --ping "🔄 Daily pipeline started $(date '+%H:%M') — refreshing the bet cache (takes a while); sharp digest lands when it finishes." || true
+3 -1
View File
@@ -57,10 +57,12 @@ def main():
d.get("size"), d.get("tx"), d.get("title")))
except Exception:
pass
if rows:
con.execute("BEGIN") # audit 3.10: atomic pair — a crash
if rows: # between the two inserts re-ingested
con.executemany("INSERT INTO trades VALUES (?,?,?,?,?,?,?,?,?)", rows)
con.execute("INSERT INTO ingested VALUES (?,?,?)",
[s, len(rows), int(time.time())])
con.execute("COMMIT")
box(f"rm {SEG}/{s}")
total += len(rows)
print(f"[ingest] {s}: {len(rows)} rows")
+4
View File
@@ -15,6 +15,10 @@ class Ex:
def __init__(self):
self.fills, self.sold = [], []
def _shares_held(self, tok):
# chain truth for the audit-3.2 gate: DUST really exists on chain
return {"DUST": 1.0}.get(tok, 0.0)
def sell(self, tok, shares, price, meta):
self.sold.append(tok)
self.fills.append({"side": "SELL", "token": tok, "shares": shares,
+10 -3
View File
@@ -227,8 +227,13 @@ def onchain_payouts(cond, rpc):
def load_state():
try:
return json.load(open(STATE))
except Exception:
return {"cash": BANK, "my_pos": {}, "resolved": [], "missed": [],
except FileNotFoundError:
pass
except Exception as e:
# audit 3.5: a corrupt state must NEVER silently become a fresh book
log(f"⚠⚠ STATE RESET — {STATE} unreadable ({e}); starting a fresh "
"$1k book. History is in git if this was a torn write.")
return {"cash": BANK, "my_pos": {}, "resolved": [], "missed": [],
"attempted": {}, "stats": {"attempts": 0, "fills": 0,
"misses": 0, "resolved": 0, "wins": 0, "refunds": 0,
"losses": 0, "staked": 0.0, "returned": 0.0, "fees": 0.0},
@@ -236,7 +241,9 @@ def load_state():
def save_state(st):
json.dump(st, open(STATE, "w"))
tmp = STATE + ".tmp" # audit 3.5: tmp+rename — a crash mid-write
json.dump(st, open(tmp, "w")) # can't torch the book
os.replace(tmp, STATE)
def open_positions(st, cands, budget):