Zum Inhalt

Settlement Pipeline

Datei: cli/settle.ts (120 Zeilen) Trigger: Cron-Job täglich um 00:00 UTC Tracking: settlement_pipeline_runs-Tabelle

Übersicht

Die Settlement Pipeline ist der zentrale Tagesabschluss-Prozess. Sie orchestriert 8 sequentielle Schritte von der Datenextraktion bis zur Blockchain-Transaktion.

00:00 UTC  ┌─── settle.ts startet
           ├── waitUntilDayIsReady()     Warten auf finalisierte Blöcke
           ├── 1. export_inputsDB.ts     Gewinner aus DB → Artefakt
           ├── 2. export_affiliatesDB.ts  Affiliates aus DB → Artefakt
           ├── 3. run.ts                  Berechnung + Merkle-Trees
           ├── 4. approve_settlement.ts   7 Validierungs-Gates
           ├── 5. commit_from_db.ts       On-Chain-Commit
           ├── 6. updatePotSizes.ts       Pot-Sync (non-fatal)
           ├── 7. refresh-global-stats.ts Dashboard-Stats (non-fatal)
           └── 8. notifyWinners.ts        Push-Notifications (non-fatal)

Day-Readiness-Check

// settle.ts:14-44
async function waitUntilDayIsReady() {
  // Warten bis der aktuelle Block-Timestamp >= dayEnd + 15min Buffer
  // Verhindert Settlement auf nicht-finalisierte Blöcke

  while (true) {
    const block = await provider.getBlock("finalized");
    if (block.timestamp >= dayEndTimestamp + 900) break; // 15min Buffer
    await sleep(60_000); // 1 Minute warten
  }
}

Warum 15 Minuten Buffer?

  • Arbitrum-Blöcke können Timestamp-Lag haben
  • Finalisierte Blöcke sind ~10-15 Minuten hinter Realzeit
  • Buffer verhindert fehlende Tickets durch späte Blöcke

Pipeline-Schritte im Detail

Schritt 1: Export Inputs

Datei: cli/export_inputsDB.ts (199 Zeilen)

Exportiert alle Gewinner-Tickets des Tages aus der Datenbank:

SELECT ticket_id, player, pool_id, matches, class_id
FROM tickets t
JOIN ticket_tips tt ON tt.ticket_id = t.ticket_id
WHERE day_id = $1 AND class_id IN (2, 3, 4, 5)
ORDER BY pool_id, class_id, ticket_id

Output: settlement_artifacts-Tabelle + artifacts/dayId=X/inputs.json

{
  "schemaVersion": 1,
  "dayId": 20500,
  "winnersByPool": {
    "0": { "2": [...], "3": [...], "4": [...], "5": [...] },
    "1": { ... }
  }
}

Schritt 2: Export Affiliates

Datei: cli/export_affiliatesDB.ts (195 Zeilen)

Exportiert Affiliate-Provisionen des Tages:

SELECT ticket_id, affiliate, affiliate_rate_bp, affiliate_base
FROM tickets
WHERE day_id = $1
  AND affiliate IS NOT NULL
  AND affiliate_rate_bp > 0
ORDER BY ticket_id

Berechnung: amount = (affiliate_base * affiliate_rate_bp) / 10000

Aggregation: Gruppiert nach Affiliate-Adresse für Merkle-Leaf.

Schritt 3: Compute Settlement

Datei: cli/run.ts (591 Zeilen)

Der Kernschritt - berechnet die gesamte Gewinnverteilung:

  1. Lädt Inputs-Artefakt aus DB (Source of Truth)
  2. Liest On-Chain-State via ChainReader:
  3. potAcc[5] - Regular Pots
  4. rolloverAcc[5] - Rollover-Akkumulatoren
  5. jackpotReserveAcc[5] - Jackpot-Reserven
  6. feeAcc(dayId) - Operator-Fees
  7. jackpotStates[5] - No-Hit-Streaks
  8. Ruft computeSettlementForDay() auf
  9. Baut Merkle-Trees (Winners + Affiliates)
  10. Berechnet Charity-Anteil
  11. Schreibt Artefakte (DB + Filesystem)
  12. Erstellt DB-Einträge:
  13. settlement_runs (Status: dry_run)
  14. settlement_proofs (Merkle-Daten)
  15. settlement_winner_payouts (pro Gewinner)
  16. settlement_affiliate_payouts (pro Affiliate)
  17. daily_prize_breakdown (für Dashboard)

Idempotenz: run_hash = sha256(commitBundle) - bei gleichem Hash wird der existierende Run wiederverwendet.

Schritt 4: Approve Settlement

Datei: cli/approve_settlement.ts (122 Zeilen)

Führt 7 Validierungs-Gates durch (siehe Approval Gateway).

Schritt 5: Commit On-Chain

Datei: cli/commit_from_db.ts (110 Zeilen)

Sendet die Transaktion an SettlementV4 (siehe Blockchain Commit).

Schritte 6-8: Non-Fatal Workers

Diese Schritte sind non-fatal - ein Fehler blockiert nicht die Pipeline:

// settle.ts:88-111
// Schritt 6: Pot-Sync
try {
  await step("updatePotSizes", () => execScript("updatePotSizes.ts"));
} catch (e) {
  console.warn("Pot sync failed (non-fatal):", e.message);
}

// Schritt 7: Global Stats
await execScript("refresh-global-stats.ts");

// Schritt 8: Winner Notifications
try {
  await notifyWinners(dayId);
} catch (e) {
  console.warn("Notifications failed (non-fatal):", e.message);
}

Pipeline-Tracking

Die settlement_pipeline_runs-Tabelle trackt jeden Schritt:

settlement_pipeline_runs (
  day_id       INT PRIMARY KEY,
  started_at   TIMESTAMP,
  finished_at  TIMESTAMP,
  status       VARCHAR,  -- RUNNING / DONE / FAILED
  export_ok    BOOLEAN,
  affiliate_ok BOOLEAN,
  compute_ok   BOOLEAN,
  approve_ok   BOOLEAN,
  commit_ok    BOOLEAN,
  stats_ok     BOOLEAN
)

Fehlerbehandlung

Szenario Verhalten
Schritt 1-5 fehlschlägt status='FAILED', Exit Code 1
Schritt 6-8 fehlschlägt Warning geloggt, Pipeline als DONE
Bereits gesettled daySettled(dayId) prüft On-Chain → Skip
RPC nicht erreichbar waitUntilDayIsReady() wartet endlos
Doppelter Start Pipeline-Status RUNNING → neuer Start schlägt fehl

Cron-Wrapper

# cli/cron-settle.sh
#!/bin/bash
set -e
cd /root/workspace/lotto-platform/bo-engine
LOG="logs/settlement/settle-$(date +%Y-%m-%d).log"
mkdir -p logs/settlement

npx ts-node --files cli/settle.ts >> "$LOG" 2>&1

# Log-Rotation: 30 Tage
find logs/settlement -name "*.log" -mtime +30 -delete

Zeitlicher Ablauf

00:00 UTC    Cron startet settle.ts
00:00-00:15  waitUntilDayIsReady() (wartet auf finalisierte Blöcke)
00:15-00:16  Export Inputs + Affiliates
00:16-00:17  Compute Settlement + Merkle-Trees
00:17        Approve Settlement (7 Gates)
00:17-00:18  Commit On-Chain (TX + Confirmation)
00:18-00:19  Pot-Sync + Stats + Notifications
~00:20 UTC   Pipeline DONE

Watchdog Grace Period: Bis 00:20 UTC werden keine Settlement-Alarme ausgelöst.