# ADR-0019: Money-path consistency (multi-tracker) ## Status: Accepted (design); implementation deferred post-alpha ## Context ADR-0015 assumes a **single settlement tracker** with Raft-replicated ledger events. The hive already gossips billing, accounts, and stats between trackers. When multiple trackers participate, several money-path races appear: - Settlement retries may double-pay if idempotency keys diverge from on-chain reality. - Any tracker follower can apply `forfeit` / `charge` events — fine under one trusted operator, dangerous with adversarial peers. - Raft term/vote state is in-memory — restart can duplicate leadership epochs. - `forfeit` events are not commutative with `charge`/`payout` ordering under concurrent replication. Alpha ships with **one settlement-capable tracker** (ADR-0016). This ADR records the accepted multi-tracker design so Bucket 2 issues do not block alpha while preventing silent architectural drift. ## Decisions ### 1. Settlement idempotency (C2) - Every on-chain payout batch carries a **stable `settlement_id`** already debiting pending before broadcast (`server.py` ~3353–3356, `_send_settlement` ~3358). - **Required hardening:** `solana_adapter.send_payouts` (~186–213) and the settlement loop must treat the Solana transaction signature as the idempotency anchor — retries reuse the same `settlement_id`; confirmed signatures must never spawn a second debit. - Deposit watcher already uses `deposit-` event ids — extend the same pattern to payouts. ### 2. Consensus-gated money mutations (C3/C4) Money-affecting events (`charge`, `payout`, `forfeit`, `credit`, `settlement`, `bind`) must commit through **Raft** (or successor consensus), not best-effort gossip alone: - Only the Raft leader appends money commands to the replicated log. - Followers apply committed entries in order; gossip becomes a transport, not the source of truth. - Settlement signing remains **leader-only** and **treasury-key-bearing** (ADR-0015). Shard-assignment commands already use Raft (`raft.py`); extend the log command set. ### 3. Durable Raft metadata (A3) Persist `currentTerm`, `votedFor`, and log index to disk (`raft.py` ~26 — today term lives only in memory). Without this, restart can elect split leaders and duplicate settlement epochs. ### 4. Commutative forfeit semantics (H1) Define explicit ordering rules when replicating `forfeit` vs `charge`/`payout`: - Forfeit amount is snapshotted at event creation time (`billing.py` ~287). - Apply forfeit **after** pending balance reflects all prior committed charges in the same epoch, or use signed pending snapshots in the event payload. - `setdefault` on `_pending_since` (~324) and wallet bindings (~351) must not resurrect stale state on out-of-order apply — use last-writer-wins with `(term, index)` tie-break. ### 5. Alpha posture | Item | Alpha | Post-alpha | |---|---|---| | Single treasury holder | Required | Optional multi-sig | | Gossip-authenticated billing | Required (ADR-0017) | Superseded by Raft commit for money | | On-chain idempotency | Best-effort resend | Hard guarantee (C2) | | Consensus-gated settlement | Leader check only (~3331) | Full Raft gate (C3/C4) | ## Consequences - Bucket 2 issues in `.scratch/alpha-hardening/` are **tracked but not alpha-blocking**. - Implementing C3/C4 before alpha would delay release without security benefit while only one operator runs settlement. - When enabling third-party settlement trackers, **all four** hardening items must ship together — partial deployment is worse than single-tracker alpha. ## Related - ADR-0015 (settlement loop, pending collateral) - ADR-0016 (single settlement tracker for alpha) - ADR-0017 (gossip auth prerequisite) - Issues: `12-c2-on-chain-idempotency.md` through `15-h1-commutative-forfeit.md`