🛡️ sandbox-labs GitHub ↗

07 · Runtime determinista de contratos#

En una frase, para cualquiera: si ejecutas el mismo programa con los mismos datos dos veces, debería salir exactamente lo mismo. Suena obvio, y casi nunca es cierto: basta un reloj o un número al azar para que no lo sea.

Estado real: 🟡 building — hay código y 6 comprobaciones automáticas, sin levantarse bajo bwrap en CI · Carpeta: cases/07-deterministic-contracts/ · Puerto: 8807


Por qué se realiza este caso#

Hay sistemas donde varias máquinas ejecutan el mismo programa por separado y tienen que llegar al mismo resultado, porque si no coinciden no hay acuerdo posible. Un contrato inteligente es el ejemplo conocido, pero no el único: una liquidación que se recalcula, una auditoría que reproduce un cierre, un peritaje que tiene que llegar a la misma cifra que el sistema original.

Lo que rompe el determinismo casi nunca es lo que uno espera:

Fuente de indeterminaciónPor qué rompe el acuerdo
El relojDos máquinas nunca lo leen en el mismo instante
AleatoriedadPor definición
El orden de recorrer un diccionarioDepende de la memoria, no del programa
Coma flotanteDistinto resultado según el procesador
Cualquier lectura de red o de discoEl mundo cambió entre una ejecución y otra
Un tiempo máximoLa máquina lenta corta donde la rápida siguió

Ese último es el más engañoso: acotar por tiempo destruye el determinismo. Si la ejecución se corta a los 5 segundos, el resultado depende de qué máquina lo ejecutó.

La idea que enseña, y que ningún otro caso enseña#

Medir el trabajo, no el tiempo. El presupuesto se cuenta en instrucciones —o en «gas»—, no en segundos. La máquina lenta y la rápida ejecutan el mismo número de instrucciones y se detienen en el mismo punto exacto.

Y con ello, un cambio de perspectiva sobre qué es aislar: los demás casos restringen lo que el código puede tocar; este restringe lo que el código puede saber. Sin reloj, sin aleatoriedad, sin entorno, sin mundo exterior.

Casos de uso reales#

decisión.

Cómo funcionará#

flowchart LR
  E0["📥 Estado inicial<br/>explícito"] --> R
  C["📜 Contrato"] --> R
  subgraph R["🔒 Runtime determinista (WASI)"]
    G["⛽ Presupuesto por instrucciones"]
    N1["🚫 Sin reloj"]
    N2["🚫 Sin red"]
    N3["🚫 Sin aleatoriedad"]
  end
  R --> OK["✅ Estado final<br/>serializado canónicamente"]
  R --> KO["↩️ Rollback:<br/>el estado no cambia"]
flowchart TB
  A["Ejecutar"] --> B{"¿Se agotó el<br/>presupuesto?"}
  B -- sí --> C["↩️ Rollback completo"]
  B -- no --> D{"¿Hubo error<br/>del contrato?"}
  D -- sí --> C
  D -- no --> E["✅ Aplicar estado final"]
  C --> F["📄 Log determinista:<br/>igual en toda máquina"]
  E --> F

Esquemas#

Entrada#

{
  "contract": "<módulo WASM>",
  "initialState": { "saldo": 1000 },
  "input": { "accion": "transferir", "monto": 250 },
  "gasLimit": 1000000
}

Salida#

{
  "outcome": "applied",
  "finalState": { "saldo": 750 },
  "stateHash": "sha256:…",
  "gasUsed": 41230,
  "logs": ["transferencia registrada"],
  "deterministic": true
}

producir el mismo hash, y comparar hashes es cómo se comprueba el acuerdo sin comparar estados enteros.

Software necesario#

ComponentePara qué¿Obligatorio?
wasmtimeEl motor WASI con contador de instrucciones
Rust 1.75+El supervisor y la serialización canónica
Un compilador a WASMPara escribir los contratos de ejemploSolo para los ejemplos

WASI se prefiere a una máquina virtual propia por una razón concreta: ya no tiene reloj ni red salvo que se los concedas. El determinismo es el punto de partida, no algo que haya que recortar.

Instalación#

curl https://wasmtime.dev/install.sh -sSf | bash
cargo build --release
cargo run -p sandboxctl -- doctor   # dirá si el runtime wasi está disponible

Procesos que se crearán#

sandboxctl contract run <módulo>
  │
  └─ wasmtime            ← un proceso, sin hijos
      └─ el módulo WASM  ← sin acceso a nada que no se le entregue

Es el caso con menos procesos de todo el proyecto, y no por casualidad: cada proceso adicional es una fuente potencial de indeterminación.

Tiempo de carga estimado#

OperaciónCoste esperado
Cargar y compilar el módulo WASM5–30 ms
Ejecución hasta agotar 1 000 000 de instrucciones10–50 ms
Serialización canónica del estado< 5 ms
Rollbackinmediato: no se aplicó nada

Qué hace falta para construirlo#

  1. Adaptador de runtime WASI con medición de instrucciones.
  2. Serialización canónica: un mismo estado, una misma representación en bytes.
  3. Estado inicial explícito y verificable por hash.
  4. Rollback ante fallo o presupuesto agotado.
  5. Contratos de ejemplo, incluido uno que intente leer el reloj y falle.

Si algo falla#

El caso ya tiene código: el núcleo en core.py y el servicio en app.py. Lo que sigue son sus fallos, la causa y la salida:

SituaciónCausaCómo se resuelve
Dos máquinas dan stateHash distintoSe coló una fuente de indeterminaciónEs el fallo que este caso existe para detectar. Se busca en el orden habitual: reloj, aleatoriedad, orden de recorrido de un diccionario, coma flotante. Los cuatro están cerrados por diseño; si aparece, es que uno se escapó
gasUsed llega al límite y no terminaEl contrato necesita más presupuesto, o tiene un bucle1. Subir gasLimit. 2. Si sube sin fin, es un bucle: el rollback ya dejó el estado intacto, que es lo que importa
El contrato intenta leer el reloj o la redWASI no se los concedeFalla dentro del contrato, de forma determinista y en todas las máquinas por igual. Se corrige el contrato, no el runtime
wasmtime no estáFalta el motor`curl https://wasmtime.dev/install.sh -sSf \bash. doctor dirá si el runtime wasi` está disponible antes de intentar nada
El estado final no se puede serializar igual dos vecesLa serialización no es canónicaEs un fallo del runtime, no del contrato: la representación en bytes de un mismo estado tiene que ser única. Sin eso, comparar hashes no significa nada

Los fallos que afectan a cualquier caso —no se puede crear el sandbox, no hay cgroups, un puerto ocupado, procesos huérfanos, la compilación en Windows— están resueltos uno a uno en Cuando algo falla.

Cómo se comprueba#

node scripts/verify-cases.mjs

Llama al núcleo del caso con situaciones concretas y comprueba qué hizo con ellas, no cómo está escrito. Son 6 comprobaciones, y corren en cada commit.

cargo run -p sandboxctl -- service up deterministic-contracts

Levanta el caso como producto en 127.0.0.1:8807. POST /api/run acepta el cuerpo que describen los esquemas de arriba.

Sigue en building, no en functional. El núcleo se comprueba, pero el servicio no se levanta bajo bwrap dentro de CI y el caso no emite evidencia firmada. La regla completa está en el ROADMAP.

Ver también: Catálogo completo · Caso 05 · Comparativa de fronteras