Den Geldpfad härten: Transaktionssicherheit im KVR-Settlement

Drei echte Schwachstellenklassen — Replay von Zahlungssignaturen, unauthentifiziertes Belohnungsprägen und Race-Double-Spends — gefunden, in Tests ausgenutzt und im Settlement-Dienst des Gateways geschlossen.

In einer DePIN ist der Geldpfad genauso feindlich wie der Rechenpfad: Jeder Endpoint, der KVR gutschreibt, wird irgendwann von jemandem abgetastet, der KVR ohne Arbeit will. Ein Sicherheitsdurchgang über den Settlement-Dienst des Gateways — den Prozess, der On-Chain-Zahlungen verifiziert und Stakes, Node-Belohnungen und Inferenzgebühren gutschreibt — fand und schloss drei echte Schwachstellenklassen. Jede wurde vor dem Fix mit einem Exploit-artigen Test demonstriert und danach erneut verifiziert.

Das Vertrauensmodell

On-Chain-Fakten verifizieren, nicht Client-Behauptungen

Kvasirs Verwahrmodell lässt die Schlüssel bei den Nutzern: Wallets signieren Transaktionen, Solana zeichnet sie auf, und die Aufgabe des Settlement-Dienstes ist, zu verifizieren, was tatsächlich auf der Chain geschah, bevor er ein Guthaben anfasst. Zahlungen folgen *quote → payment → inference*, wobei jede verbrauchte Transaktionssignatur in einem Einmal-Register usedSignatures vermerkt wird und nie zweimal vorgelegt werden kann. Das macht den Settlement-Dienst zum Nadelöhr — und die Regel, die er nie brechen darf: nur gutschreiben, was die Chain beweist, nie, was der Client behauptet. Im Devnet verwahrt der Dienst allerdings durchaus Mittel: Gestakte KVR und Prepaid-Credits liegen in seiner Treasury und werden in seinem Ledger geführt, bis ein On-Chain-Staking-Programm verfügbar ist.

A settlement vault guarded by three locks: sender binding, trusted reporter, and a serialization gate
Fix #1 · Sender-Bindung

Die Zahlung an den Zahler binden

Solana-Signaturen sind öffentlich. Der Stake-Verifikationspfad prüfte, dass der Vault das erwartete KVR *erhielt* — aber nie, *wer es sandte*. Ein Angreifer konnte auf devnet die KVR→Vault-Überweisung eines Opfers beobachten und dann {owner: Angreifer, signature: die des Opfers} einreichen: Die Vault-Empfangsprüfung bestand, das Kapital wurde dem Angreifer gutgeschrieben, und ein Unstake später gehörten die Mittel ihm. Direkter Diebstahl, mit nichts als einem Block-Explorer.

Der Fix: Das KVR muss von Token-Konten abgebucht worden sein, deren Besitzer der gutgeschriebene Owner ist.

verifyStakeTransfer(signature, owner, amount):
  delta(vault)  >= amount            # vault actually received it (old check)
  Σ debits from token accounts
    whose owner == credited owner    # NEW — sender binding
                >= amount            # summed across that owner's accounts
  # inference path (no owner): bound by private requestId
  # + one-shot usedSignatures instead
Fix #2 · vertrauenswürdiger Melder

Belohnungen nur aus authentifizierten Quellen

Die Node-Belohnungs-Endpoints prägten einforderbares KVR aus selbstgemeldeten Eingaben: POST /api/node/contribution schrieb die vom Client behaupteten units gut — units: 1e9 plus ein Claim-Aufruf konnten den Vault leeren — und register/heartbeat glaubten selbstdeklarierten Hub-/Gateway-Rollen (stündliche Infra-Belohnungen) und Performance-Scores (Belohnungsmultiplikatoren). Der Fix stellt jede belohnungswirksame Behauptung hinter einen vertrauenswürdigen Melder: Nur das M2M-Service-Token des Hub-Beitrags-Polls oder ein authentifizierter Admin darf Units, Infra-Rollen oder Perf-Stufen behaupten — erzwungen selbst im offenen LAN-Modus, weil diese KVR prägen. Der Token-Vergleich ist zeitkonstant, und die Wallet↔Node-Verknüpfung bleibt frei; sie kann nur ihre eigenen Belohnungen nicht mehr selbst behaupten.

Fix #3 · Settlement-Serialisierung

Ein Schreiber pro Guthaben

Der Settlement-Zustand war ein sperrfreies Read-Modify-Write, und jede Geldoperation *awaitet* mittendrin eine On-Chain-Auszahlung oder Verifikation — und gibt die Event-Loop mit veraltetem Guthaben in der Hand ab. Zwei nebenläufige Claims konnten dasselbe 100-KVR-Guthaben lesen und beide auszahlen. Nicht theoretisch: Der Exploit-Test zeigte drei nebenläufige Claims, die 300 für ein 100er-Guthaben auszahlten.

Asynchrone Serialisierung je Schlüssel: gleichschlüsselige Geldoperationen laufen strikt nacheinander.

withLock(key, fn)         # per-key promise chain, self-cleaning map
  stake / unstake / claim  → keyed by owner
  inference settlement     → keyed by requestId
inside the lock:
  usedSignatures check + credit   # no same-signature double-credit
  pay out FIRST, then debit       # failed payout leaves balance intact
Verteidigung in der Tiefe

Wo jede Schicht jetzt steht

SchichtMechanismus
IdentitätSIWS-Wallet-Signatur-Login über eine Server-Nonce + TOTP-2FA + Einmal-Backup-Codes
TransportWallet-abgeleitete Node-Tokens bei Shard-Downloads; Build-Fingerprint-Abgleich am Relay
ZahlungSender-Bindung bei Stake-Transfers; Einmal-usedSignatures; private requestId bei Inferenz
SettlementSchlüssel-Locks um jede Guthaben-Schreibung; erst auszahlen, dann abbuchen; idempotentes Wiedereinreichen
MeldungBelohnungswirksame Fakten nur vom M2M-Service-Token oder Admin, zeitkonstant verglichen
VerwahrungNon-custodial Wallets — der Dienst kann nur bewegen, was der Vault hält, nie Nutzerschlüssel
Gemessen, nicht angenommen

Jeder Fix trägt seinen eigenen Exploit-Test

  • Das Abspielen der Transfersignatur eines Opfers unter der Wallet eines Angreifers wird nun abgelehnt („not sent by owner"); legitime Stakes, Überforderungen und Inferenzzahlungen verhalten sich unverändert.
  • Drei nebenläufige Claims gegen ein Guthaben zahlen genau einmal; das Wiedereinreichen einer bereits bezahlten Inferenz liefert idempotent dasselbe Ergebnis.
  • Selbstbehauptete units, Hub-/Gateway-Rollen und Perf-Stufen unauthentifizierter Clients bewegen keinen einzigen Lamport an Belohnungen mehr.

Der rote Faden aller drei Fixes ist ein Prinzip in drei Anwendungen: Die Chain ist die Wahrheitsquelle, der Dienst ist ein Verifizierer, und jedes Guthaben hat genau einen Schreiber. Der Settlement-Dienst läuft noch auf Solana devnet — genau dort will man diese Klassen finden, ausnutzen und beheben, bevor das Mainnet den Einsatz erhöht.