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.
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.

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 insteadBelohnungen 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.
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
Wo jede Schicht jetzt steht
| Schicht | Mechanismus |
|---|---|
| Identität | SIWS-Wallet-Signatur-Login über eine Server-Nonce + TOTP-2FA + Einmal-Backup-Codes |
| Transport | Wallet-abgeleitete Node-Tokens bei Shard-Downloads; Build-Fingerprint-Abgleich am Relay |
| Zahlung | Sender-Bindung bei Stake-Transfers; Einmal-usedSignatures; private requestId bei Inferenz |
| Settlement | Schlüssel-Locks um jede Guthaben-Schreibung; erst auszahlen, dann abbuchen; idempotentes Wiedereinreichen |
| Meldung | Belohnungswirksame Fakten nur vom M2M-Service-Token oder Admin, zeitkonstant verglichen |
| Verwahrung | Non-custodial Wallets — der Dienst kann nur bewegen, was der Vault hält, nie Nutzerschlüssel |
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.