Endureciendo la ruta del dinero: seguridad transaccional en la liquidación de KVR

Tres clases reales de vulnerabilidad — replay de firmas de pago, acuñación de recompensas sin autenticación y doble gasto por carrera — encontradas, explotadas en tests y cerradas en el servicio de liquidación del gateway.

En una DePIN, la ruta del dinero es exactamente tan adversarial como la del cómputo: cada endpoint que acredita KVR será tarde o temprano tanteado por alguien que quiere KVR sin hacer el trabajo. Una pasada de seguridad sobre el servicio de liquidación del gateway — el proceso que verifica pagos on-chain y acredita stakes, recompensas de nodo y cargos de inferencia — encontró y cerró tres clases reales de vulnerabilidad. Cada una se demostró con un test estilo exploit antes del arreglo y se re-verificó después.

El modelo de confianza

Verificar hechos on-chain, no afirmaciones del cliente

El modelo de custodia de Kvasir deja las claves con los usuarios: las wallets firman transacciones, Solana las registra, y el trabajo del servicio de liquidación es verificar qué ocurrió realmente en la cadena antes de tocar un saldo. Los pagos siguen *quote → payment → inference*, y cada firma de transacción consumida se registra en un registro de un solo uso usedSignatures, de modo que nunca puede presentarse dos veces. Eso convierte al servicio de liquidación en el cuello de botella — y la regla que jamás debe romper es: acreditar solo lo que la cadena demuestra, nunca lo que el cliente afirma. El servicio sí custodia fondos en devnet: el KVR en staking y los créditos prepago están en su tesorería y se registran en su libro contable hasta que se lance un programa de staking en cadena.

A settlement vault guarded by three locks: sender binding, trusted reporter, and a serialization gate
Arreglo #1 · vinculación del remitente

Atar el pago al pagador

Las firmas de Solana son públicas. La ruta de verificación de stake comprobaba que el vault *recibió* el KVR esperado — pero nunca *quién lo envió*. Un atacante podía vigilar en devnet la transferencia KVR→vault de una víctima y luego enviar {owner: atacante, signature: la de la víctima}: el chequeo de recepción del vault pasaba, el principal se acreditaba al atacante, y un unstake después los fondos eran suyos. Robo directo, usando nada más que un explorador de bloques.

El arreglo: el KVR debe haberse debitado de cuentas de token cuyo dueño sea el owner acreditado.

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
Arreglo #2 · reportero de confianza

Recompensas solo desde fuentes autenticadas

Los endpoints de recompensa de nodos acuñaban KVR reclamable a partir de entrada autoinformada: POST /api/node/contribution acreditaba las units que el cliente afirmara — con units: 1e9 y una llamada de claim se podía vaciar el vault — y register/heartbeat aceptaban roles autodeclarados de hub/gateway (recompensas de infraestructura por hora) y puntuaciones de rendimiento (multiplicadores). El arreglo pone cada afirmación que afecta a recompensas tras un reportero de confianza: solo el token de servicio M2M usado por el sondeo de contribución del hub, o un admin autenticado, puede afirmar units, roles de infra o niveles de rendimiento — impuesto incluso en modo LAN abierto, porque esto acuña KVR. La comparación del token es de tiempo constante, y el enlace wallet↔nodo sigue libre; solo que ya no puede autoafirmar sus recompensas.

Arreglo #3 · serialización de la liquidación

Un solo escritor por saldo

El estado de liquidación era un read-modify-write sin bloqueo, y cada operación de dinero hace *await* de un pago o verificación on-chain a mitad de camino — cediendo el event loop con un saldo obsoleto en la mano. Dos claims concurrentes podían leer el mismo saldo pendiente de 100 KVR y ambos pagarlo. No era teórico: el test de exploit mostró tres claims concurrentes pagando 300 por un saldo de 100.

Serialización asíncrona por clave: las operaciones de dinero con la misma clave corren estrictamente una tras otra.

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
Defensa en profundidad

Dónde queda cada capa

CapaMecanismo
IdentidadLogin por firma de wallet SIWS sobre un nonce del servidor + 2FA TOTP + códigos de respaldo de un solo uso
TransporteNode tokens derivados de la wallet en descargas de shards; acuerdo de huella de build en el relay
PagoVinculación del remitente en transferencias de stake; usedSignatures de un solo uso; requestId privado en inferencia
LiquidaciónBloqueos por clave en cada escritura de saldo; pagar primero y debitar después; reenvío idempotente
ReportesHechos que afectan recompensas solo del token de servicio M2M o admin, comparados en tiempo constante
CustodiaWallets no custodiales — el servicio solo puede mover lo que el vault contiene, nunca claves de usuario
Medido, no asumido

Cada arreglo lleva su propio test de exploit

  • Reproducir la firma de transferencia de una víctima bajo la wallet de un atacante ahora se rechaza ("not sent by owner"); stakes legítimos, sobre-reclamos y pagos de inferencia se comportan igual.
  • Tres claims concurrentes contra un saldo pagan exactamente una vez; reenviar una inferencia ya pagada devuelve idempotentemente el mismo resultado.
  • Las units autoafirmadas, los roles de hub/gateway y los niveles de rendimiento de clientes sin autenticar ya no mueven ni un lamport de recompensas.

El hilo conductor de los tres arreglos es un principio aplicado de tres maneras: la cadena es la fuente de verdad, el servicio es un verificador, y cada saldo tiene exactamente un escritor. El servicio de liquidación aún corre en Solana devnet — que es exactamente donde quieres encontrar, explotar y arreglar estas clases antes de que mainnet suba las apuestas.