Het geldpad verharden: transactiebeveiliging in KVR-settlement
Drie echte kwetsbaarheidsklassen — replay van betaalhandtekeningen, ongeauthenticeerd beloningen slaan en race-double-spends — gevonden, in tests uitgebuit en gesloten in de settlement-dienst van de gateway.
In een DePIN is het geldpad precies zo vijandig als het rekenpad: elk endpoint dat KVR bijschrijft wordt vroeg of laat afgetast door iemand die KVR wil zonder het werk te doen. Een beveiligingsronde over de settlement-dienst van de gateway — het proces dat on-chain betalingen verifieert en stakes, node-beloningen en inferentiekosten bijschrijft — vond en sloot drie echte kwetsbaarheidsklassen. Elk werd vóór de fix gedemonstreerd met een exploit-achtige test en erna opnieuw geverifieerd.
Verifieer on-chain feiten, geen clientbeweringen
Kvasirs bewaringsmodel laat de sleutels bij de gebruikers: wallets ondertekenen transacties, Solana registreert ze, en de taak van de settlement-dienst is verifiëren wat er werkelijk on-chain gebeurde voordat hij een saldo aanraakt. Betalingen volgen *quote → payment → inference*, waarbij elke verbruikte transactiehandtekening in een eenmalig usedSignatures-register wordt genoteerd zodat ze nooit twee keer kan worden aangeboden. Dat maakt de settlement-dienst het knelpunt — en de regel die hij nooit mag breken: alleen bijschrijven wat de chain bewijst, nooit wat de client beweert. De dienst houdt op devnet wel degelijk tegoeden aan: gestakete KVR en vooraf betaalde credits staan in zijn treasury en worden in zijn grootboek bijgehouden totdat een on-chain stakingprogramma wordt uitgebracht.

Bind de betaling aan de betaler
Solana-handtekeningen zijn openbaar. Het stake-verificatiepad controleerde dat de vault het verwachte KVR *ontving* — maar nooit *wie het stuurde*. Een aanvaller kon op devnet de KVR→vault-overdracht van een slachtoffer afwachten en dan {owner: aanvaller, signature: die van het slachtoffer} indienen: de vault-ontvangstcheck slaagde, de hoofdsom werd aan de aanvaller bijgeschreven, en één unstake later was het geld van hem. Directe diefstal, met niets meer dan een block explorer.
De fix: het KVR moet zijn afgeschreven van tokenrekeningen waarvan de eigenaar de bijgeschreven owner is.
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 insteadBeloningen alleen uit geauthenticeerde bronnen
De node-belonings-endpoints sloegen claimbaar KVR uit zelfgerapporteerde invoer: POST /api/node/contribution schreef de units bij die de client beweerde — units: 1e9 plus één claim-aanroep kon de vault leegtrekken — en register/heartbeat geloofden zelfverklaarde hub-/gatewayrollen (uurlijkse infra-beloningen) en prestatiescores (beloningsvermenigvuldigers). De fix zet elke beloningsbeïnvloedende bewering achter een vertrouwde rapporteur: alleen het M2M-servicetoken van de contributiepoll van de hub, of een geauthenticeerde admin, mag units, infra-rollen of prestatieniveaus beweren — afgedwongen zelfs in open LAN-modus, omdat dit KVR slaat. De tokenvergelijking is constant-tijd, en wallet↔node-koppeling blijft vrij; ze kan alleen niet meer haar eigen beloningen beweren.
Eén schrijver per saldo
De settlement-status was een lock-vrij read-modify-write, en elke geldoperatie *awaitet* halverwege een on-chain uitbetaling of verificatie — en geeft de event loop op met een verouderd saldo in de hand. Twee gelijktijdige claims konden hetzelfde openstaande saldo van 100 KVR lezen en het beide uitbetalen. Niet theoretisch: de exploit-test toonde drie gelijktijdige claims die 300 uitbetaalden voor een saldo van 100.
Asynchrone serialisatie per sleutel: geldoperaties met dezelfde sleutel draaien strikt na elkaar.
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
Waar elke laag nu staat
| Laag | Mechanisme |
|---|---|
| Identiteit | SIWS-wallet-handtekeninglogin over een server-nonce + TOTP-2FA + eenmalige back-upcodes |
| Transport | Wallet-afgeleide node-tokens bij shard-downloads; build-fingerprint-overeenstemming op de relay |
| Betaling | Afzenderbinding op stake-overdrachten; eenmalige usedSignatures; privé requestId bij inferentie |
| Settlement | Per-sleutel-locks rond elke saldoschrijving; eerst uitbetalen, dan afschrijven; idempotent opnieuw indienen |
| Rapportage | Beloningsbeïnvloedende feiten alleen van het M2M-servicetoken of admin, constant-tijd vergeleken |
| Bewaring | Non-custodial wallets — de dienst kan alleen bewegen wat de vault bevat, nooit gebruikerssleutels |
Elke fix draagt zijn eigen exploit-test
- Het opnieuw afspelen van de overdrachtshandtekening van een slachtoffer onder de wallet van een aanvaller wordt nu geweigerd ("not sent by owner"); legitieme stakes, over-claims en inferentiebetalingen gedragen zich ongewijzigd.
- Drie gelijktijdige claims op één saldo betalen precies één keer; het opnieuw indienen van een al betaalde inferentie geeft idempotent hetzelfde resultaat terug.
- Zelfbeweerde
units, hub-/gatewayrollen en prestatieniveaus van ongeauthenticeerde clients verplaatsen geen enkele lamport aan beloningen meer.
De rode draad van alle drie de fixes is één principe, drie keer toegepast: de chain is de bron van waarheid, de dienst is een verificateur, en elk saldo heeft precies één schrijver. De settlement-dienst draait nog op Solana devnet — precies waar je deze klassen wilt vinden, uitbuiten en repareren voordat mainnet de inzet verhoogt.