---
slug: 73-vela
title: Vela (Horizen)
description: Synsema como app de Vela — el coprocesador confidencial de Horizen corre tu .syn dentro de un TEE a través de un adaptador fino, con el contrato del programa (deploy/deposit/process/deanonymize/trusted), un cliente en Synsema para el otro lado del enclave (registro, deploy, requests cifrados, reportes, eventos, facilitador), trigger contracts, ERC-20 y la receta del starter kit local. Todo verificado contra el stack real.
example_ids: []
---

# Vela (Horizen)

[Vela](https://docs.horizen.io/vela/introduction/) es el **coprocesador confidencial** de Horizen:
una aplicación corre como módulo WebAssembly dentro de un TEE (un AWS Nitro Enclave en producción,
un proceso aislado en el kit local), su estado viaja cifrado y cada resultado se liquida on-chain
como un state root firmado. El toolchain oficial es Go + TinyGo. Desde el paquete de guests, **la
aplicación puede ser un archivo `.syn`**, y el lado de afuera del enclave — claves, deploys, requests
cifrados, eventos, reportes, meta-transacciones — también puede ser Synsema.

Esta página es el cuadro completo. El directorio del repositorio es el contrato:
[`packages/guests/vela`](https://github.com/kitecosmic/synsema/tree/main/packages/guests/vela)
(README, adaptador, tres apps de ejemplo, un cliente, un trigger contract, un ERC-20, dos sondas).
Nada de esto es una suposición: el ABI se leyó del código Go de Horizen (`vela`, `vela-common-go`,
`vela-nova`, `vela-starterkit`, v0.2.0) y cada flujo de abajo corrió contra el starter kit en Docker.

**Empezá por el kit.** [`synsema/vela-app`](https://github.com/synsema/vela-app) es un repositorio plantilla: la app con sus tests, el cliente, `scripts/build.sh` (tu programa dentro del módulo guest de la release: sin compilador, unos segundos), `scripts/smoke.mjs`, `scripts/devnet.sh` (el starter kit de Horizen en Docker) y `scripts/e2e.sh` (claves → deploy → registro → depósito → un request cifrado → tus eventos), más un workflow de CI que arma `app.wasm` en cada push. "Use this template" y los cuatro comandos de su README. ¿Sin Docker? `synsema run vela_client.syn -- devnet` escribe en `client/.env` un token propio del devnet público (abajo).

**Una app completa para bifurcar.** [`synsema/vela-payroll`](https://github.com/synsema/vela-payroll) es nómina privada en Vela, construida sobre el kit: una empresa fondea la app con una stablecoin y corre la nómina desde un CSV; cada persona ve sólo sus recibos; la cadena ve depósitos, retiros y un recibo público por corrida (número de corrida, cantidad de personas, un hash de los ítems); las personas retiran como pull-payment y no necesitan ETH, porque el facilitador de la empresa envía por ellas; un auditor autorizado obtiene el cuadro en claro desde el enclave. Su cliente agrega `fund`, `payrun <csv>`, `payslips`, `withdraw`, `pending` y `claim-for`, con montos en tokens convertidos por texto con los `decimals()` del token; trae el ERC-20 de prueba con `permit` que su `scripts/devnet.sh` despliega y permite localmente; y su `scripts/e2e.sh` corre el ciclo entero. Verificado de punta a punta en el devnet público.

**Política adentro, LLM afuera.** [`synsema/vela-treasury`](https://github.com/synsema/vela-treasury) es una tesorería para agentes: un agente (cualquier proceso con una clave de proponente: un worker en la plataforma Synsema, una laptop) propone pagos; el enclave aplica la política del dueño (quién propone, a quién se paga y hasta cuánto, el límite automático por pago, la asignación otorgada) y o bien paga al instante por un contrato trigger (`TreasuryTrigger.sol`, uno por app, ERC-20 o ETH) o retiene la propuesta con todas las razones para el `approve` / `reject` del dueño; la respuesta del trigger liquida el pago o lo devuelve. El worker autónomo del agente (`agent.syn`) convierte las facturas de `inbox/` en propuestas (JSON tal cual; texto libre con un LLM configurado) y no tiene fondos: lo peor que puede hacer un bug o una inyección de prompt es una propuesta que la política frena. El protocolo del cliente es un módulo (`client/vela_lib.syn`) que el worker y la CLI usan con `use`. Verificado de punta a punta en el devnet público, ciclo del trigger incluido.

**Pujas que nadie ve.** [`synsema/vela-auction`](https://github.com/synsema/vela-auction) es una subasta de sobre cerrado: una parte vendedora ofrece un bloque de un token a cambio de otro; quienes pujan depositan el token de pago y mandan sus pujas cifradas al enclave: nadie, ni la parte vendedora, ve una puja antes del cierre; el matching corre adentro (precio uniforme o pay-as-bid, pujas ordenadas por multiplicación cruzada de pares `(cantidad, total)`, enteros exactos, con una división larga propia porque `/` pasa por float); ganadores y perdedores se liquidan desde su escrow como pull-payments, cada quien conoce sólo su resultado y el precio de cierre, las pujas perdedoras no se revelan nunca, y la cadena ve recibos `opened` y `cleared` sin nadie adentro. Tres partes, una subasta, dos pujas cifradas, retiros verificados en cadena: `scripts/e2e.sh`, verificado en el devnet público.

**Cada kit es una app web.** La entrada de la receta en los cuatro es una consola (`web.syn`, `kind = web`): la consola de nómina (fondear, dar de alta personas, corridas, los recibos y retiros de cada persona), la consola de tesorería (desplegar con un trigger creado por un contrato fábrica en el stack, política, beneficiarios, las propuestas del agente, aprobar o rechazar, recibos), la consola de la subasta (la mesa del vendedor y una mesa por postor) y la mesa de trabajo del kit base (desplegar tu `app.syn`, registrar, depositar, cualquier payload cifrado al Executor, tus eventos descifrados, usuarios por el facilitador, reportes). Cada acción es un request al enclave; el estado de la consola vive en el volumen del proyecto. Desplegada desde [synsema.com](https://synsema.com), el entorno del proyecto se aprovisiona desde el devnet público al crearlo (`[provision] env_url` en `syn.toml`: un token propio, las direcciones, las claves); en local, `synsema serve web.syn` con las mismas líneas en `.env`. Las consolas custodian las claves de las cuentas que crean (una persona a la que un portal le guarda las claves, un postor de demostración, un agente de demostración): la biblioteca firma con cualquier clave que tenga (`send_tx_as`, `submit_as`) o envía por ella a través del facilitador (`submit_for_with`); una cuenta con billetera propia usa el cliente de línea de comandos. Los cuatro verificados de punta a punta en el devnet público desde el navegador.

## Dónde encaja: un guest, no un artefacto nuevo

Vela no carga un comando WASI (sin `_start`, sin argv) ni provee los imports `synsema_host` del
[artefacto embebible](/es/0.6.x/72-wasm): su Executor instancia un módulo una vez y llama a **sus
propios exports** — `allocate`, `deallocate`, `load_module`, `deploy`, `deposit`,
`process_request`, `trusted_request` — con punteros a la memoria del módulo. Por eso el motor queda
como está, y un **adaptador fino** en `packages/guests/vela/` mapea esos exports sobre el ABI genérico
`synsema_call`, con tu programa embebido: **un `.wasm` es exactamente una app**, y el SHA-256 que
Vela verifica on-chain cubre el intérprete y tu lógica juntos.

Ese es el *eje host* de la regla de dos ejes. En el *eje cliente*, lo que Vela necesitaba y era
genérico — primitivas WebCrypto (`ecdh_*`, `hkdf_sha256`, `aes_gcm_*`), `--deterministic`,
`steps()` — entró al lenguaje con nombres genéricos. El nombre de ninguna empresa está en el motor.

## Tu programa → un módulo (sin compilador)

El módulo es el intérprete más tu programa. Cada release de Synsema publica el intérprete como
guest de Vela — `synsema-vela-guest.wasm`, un asset de la release — con un **slot de app** adentro:
un bloque de datos de tamaño fijo con cabecera. `embed.syn` (en el kit como `scripts/embed.syn`,
en el repositorio como `packages/guests/vela/tools/embed.syn`) encuentra el slot en el archivo y lo
sobreescribe con tu `.syn`. El resultado es un módulo que es exactamente tu app, y el SHA-256 que
Vela verifica en cadena cubre intérprete y programa juntos. Programas de hasta 512 KB; el embed
tarda un segundo.

```sh
synsema test app/app.syn                                   # la app, nativa — el mismo código corre en el enclave
sh scripts/build.sh                                        # baja el guest de la release una vez, embebe app/app.syn → build/app.wasm (+ .sha256)
synsema run scripts/embed.syn -- synsema-vela-guest.wasm app/app.syn build/app.wasm   # lo mismo, a mano
node scripts/smoke.mjs build/app.wasm                      # opcional: Node 20 o 24+ (no 22, ver abajo) — imports, exports, load_module, deploy, determinismo
```

El guest siempre apunta a `wasm32-wasip1`: el linker de Vela define WASI y nada más, así que `print`
se convierte en el log del Executor (`INF …`). El límite de subida de Vela es 50 MB; el guest de la
release trae el ledger de ejemplo, así que se puede desplegar tal como viene.

**El adaptador en sí** — Rust, sólo si cambiás el adaptador (`packages/guests/vela`):
`rustup target add wasm32-wasip1` y `cargo build --profile wasm` (→ `engine/target/wasm32-wasip1/wasm/synsema_vela_guest.wasm`;
`SYNSEMA_VELA_APP=… cargo build` llena el slot al compilar), `cargo test --target <tu host triple>` para sus tests
unitarios, `node tests/vela_guest.probe.mjs <el .wasm>` y `cd tests/wasmtime-go && go run . <el .wasm>` para las dos
sondas (el WASI de Node, y wasmtime-go v1.0.0 — el runtime exacto del Executor).

Corré la sonda de Node con Node 20 o Node 24+, no con 22: Node 22.x se cae de forma intermitente
dentro de V8 (un segmentation fault por la carrera del tier-up concurrente) al ejecutar este
módulo — reproducido con 22.23.2 en Linux, una corrida de cada tres; `node --no-wasm-dynamic-tiering …`
lo evita. El bug es de ese host, no del guest: wasmtime-go v1.0.0, el runtime del Executor, no se
ve afectado, y CI corre las dos sondas.

## El contrato de tu `.syn`

El adaptador llama **una task por punto de entrada de Vela**; cada una recibe **un mapa** y devuelve
**un mapa**. Las tasks que no definís caen como se indica.

| Export de Vela | task | `ctx` que recibe la task | mapa que devuelve |
|---|---|---|---|
| `deploy(appId, params)` | `deploy(ctx)` | `{app_id, kind: "deploy", params}` — `params` es el JSON del constructor ya decodificado (`nothing` si está vacío) | `{state, fuel?}` — `state` obligatorio |
| `load_module(appId)` | `load_module(ctx)` → cae en `deploy` con `params: nothing` | `{app_id, kind: "load_module", params: nothing}` | `{state, fuel?}` — el Executor lo llama sólo para calentar su cache de módulos tras un reinicio y **descarta el estado**; si el `deploy` de respaldo falla (necesitaba params) el adaptador responde un estado vacío con un aviso, en vez de un error que dejaría la app sin poder cargarse |
| `deposit(appId, sender, token, value, state)` | `deposit(ctx)` | `{app_id, kind: "deposit", sender, token, value, value_hex, state}` — direcciones `0x` + 40 hex, minúsculas; `value` como **texto decimal exacto**, `value_hex` como `0x…`; `state` decodificado de JSON (texto si no es JSON) | `{state?, events?, app_events?, fuel?, error?}` |
| `process_request(…, requestType = 1)` | `process(ctx)` | `{app_id, kind: "process", request_type: 1, sender, payload, payload_hex, state}` — `payload` decodificado de JSON, o texto; `payload_hex` los bytes crudos como `0x…` | `{state?, events?, app_events?, withdrawals?, fuel?, error?}` |
| `process_request(…, requestType = 2)` (deanonimización) | `deanonymize(ctx)` → cae en `process` | igual, `kind: "deanonymize"`, `request_type: 2` | `{report, state?, fuel?}` — `report` **obligatorio** (Vela rechaza un resultado tipo 2 sin él; en cualquier otro tipo un reporte se descarta con aviso) |
| `trusted_request(appId, payload, state)` (TRUSTPROCESS desde un trigger contract) | `trusted(ctx)` → cae en `process` | `{app_id, kind: "trusted", request_type: 4, sender: nothing, payload, payload_hex, state}` — el payload es lo que devolvió `getTrustProcessPayload` del trigger: **bytes ABI, en claro**; leé `payload_hex` y hacele `abi_decode` | como `process`, y **sin `app_events`** (un app event vuelve a disparar el trigger; uno vacío termina el ciclo) |

Dos tipos de request nunca llegan al programa: `AssociateKey` (3, un usuario registrando su clave
P-521 y su seed — lo resuelve el Executor) y un `PROCESS` con **payload vacío** (el Executor
devuelve el estado intacto sin llamar al módulo; un depósito en ese request sí corre `deposit`).
Un depósito y un process del mismo request on-chain corren uno tras otro, el segundo sobre el
estado que devolvió el primero.

### Las formas dentro del mapa devuelto

- `state` — cualquier valor; un texto va tal cual, cualquier otra cosa como JSON compacto en el
  orden de claves que armó tu programa (determinista). Omitido (o `nothing`) en `deposit`/`process`:
  el estado queda como vino, byte a byte.
- `events` — `[{user: "0x…", subtype?, data?}]`, cifrados para `user` por el Executor. **Todo
  `user` tiene que haber registrado una clave P-521** (`novaw registeruser`, o el `register` del
  cliente): un evento para una dirección sin registrar hace que el Executor tire el request entero
  (`CodePubKeyNotRegistered`, código 9) — no emitas a un destinatario que no podés garantizar. Un
  `subtype` ausente son 32 bytes en cero (para un usuario con seed el Executor lo reemplaza por un
  HMAC de la seed igual).
- `app_events` — lo mismo sin `user`: públicos, sin cifrar, indexados por el subgraph y entregados
  a un trigger contract durante `stateUpdate`.
- `subtype` — `"0x"` + 64 hex (32 bytes), o una **etiqueta corta** de hasta 32 bytes
  (`"execute_requested"`) que queda alineada a la izquierda y rellenada con ceros — la convención
  `subtypeToBytes32` del starter kit, contra la que compara un trigger contract.
- **Campos de bytes** (`state`, `report`, el `data` de un evento) admiten tres formas, gana la
  primera que aparece: `<campo>_hex` (`"0x…"`, bytes exactos — lo que un contrato decodifica con
  `abi.decode`; armalos con `abi_encode` y `decode(b, "hex")`), `<campo>_base64`, o `<campo>` (un
  texto tal cual, un mapa o lista como JSON compacto). Un valor `bytes` de Synsema dentro de `data`
  saldría como texto base64 en JSON — usá `data_hex` cuando los bytes importan.
- `withdrawals` — `[{token?: "0x…", to: "0x…", amount}]`; `token` por defecto es la dirección cero
  (ETH). Pull-payment: el receptor después llama `claim(token, payee)` en el ProcessorEndpoint. En
  una app con trigger, `to` es el trigger contract y el app event correspondiente lleva la llamada.
- `amount`, `fuel`, `value` — enteros como **texto decimal** (`text(n)`) para mantenerse exactos a
  256 bits; también se acepta un entero JSON o un texto `0x…`. El adaptador los emite como el
  `Uint256` hex de Vela.
- `error` — un texto no vacío hace fallar el request con ese mensaje (ni estado ni eventos se
  aplican). Un error de runtime del programa hace lo mismo, con el mensaje del motor. Nunca un trap.
- `fuel` — lo que declarás o, si falta, los `steps()` del intérprete en esa llamada: un conteo
  determinista de sentencias ejecutadas. Vela cobra `fuel × EXECUTOR_FUEL_PRICE_PER_UNIT` (al menos
  `MIN_FEE_PER_REQUEST`, 10 wei en el starter kit) contra el `maxFeeValue` del request y **hace
  fallar el request si no alcanza** — con `steps()` un handler son típicamente unos cientos de
  unidades y `novaw` reserva 100 wei por defecto, así que declará una constante como la app de
  referencia (`5`/`20`/`35`/`50`) o decile a tus usuarios que reserven más.

`print` dentro del programa va al log del Executor; no es un canal de datos. El resultado viaja
sólo por el mapa devuelto.

### Determinismo, por construcción

El programa corre bajo el techo `stdout`: **sin `now()`, sin `random()`/`token()`, sin red, sin
archivos, sin LLM**. Vela firma el state root, así que las mismas entradas tienen que producir los
mismos bytes — el techo lo vuelve una propiedad del runtime y no de la disciplina (la app Go de
referencia estampa `time.Now()` en cada transacción; acá esa línea no puede compilar). Los mapas
conservan el orden de inserción y el JSON se emite en ese orden, así que los bytes del estado son
los mismos entre corridas e instancias (verificado: cinco procesos, hashes idénticos). Todo lo puro
está disponible: tipos, JSON, `decimal` (exacto, 28 dígitos), `bytes_to_int`/`int_to_bytes`
(enteros exactos de 256 bits), `keccak256`, `sha256`, `abi_encode`/`abi_decode`, `match`,
`try`/`recover`, y bloques `test` que corren nativos.

Dos cosas que saber sobre números. Un `decimal` o entero grande se escribe como número JSON pelado,
y un número pelado por encima de 2⁵³ vuelve como float cuando el estado se decodifica en la
siguiente llamada — guardá los montos como **texto** en el estado (`text(n)`, o el hex Uint256 que
usa la payment app) y convertí al usarlos. Y `bytes(h, "hex")` no acepta el prefijo `0x` y necesita
una cantidad par de dígitos, mientras que el hex `Uint256` de Vela quita los ceros a la izquierda
— toda app escribe los mismos dos helpers (ver `hex_to_int`/`int_to_hex` en
`examples/payment_app.syn`).

## Las apps de ejemplo

| Programa | Qué muestra | Tests |
|---|---|---|
| `app.syn` | Un ledger privado: saldos por cuenta y token, transferencias, retiros con un **recibo ABI** público (`app_events` + `data_hex` + un subtipo etiqueta), un reporte de deanonimización, una task `trusted` que decodifica `(address,address,uint256)` de `payload_hex`. La sonda de Node lo maneja. | 8 |
| `examples/payment_app.syn` | La Private Transfer app de Horizen (`payment_app` de `vela-nova`) en Synsema, **compatible byte a byte con el wallet `novaw`**: mismas instrucciones de payload, mismos cuerpos de eventos (`balance` en hex Uint256), params de deploy `allowedTokens`, reportes `balances`/`tx_history`, recibos keccak por invoice — `novaw deployapp / registeruser / deposit / privatetransfer / withdraw / getprivatebalance / requestreport` la manejan sin cambios. Sin timestamps (no hay reloj en el enclave): `tx_history` filtra por dirección. | 9 |
| `examples/trigger_app.syn` | Un pool de ejecución para el ciclo del trigger (abajo): `execute` bloquea fondos, retira al trigger y emite `execute_requested` con abi.encode`(bytes16,address,uint256,bytes)`; `trusted` decodifica `(bytes16,uint256,uint8)`, acredita el resto, borra el lock y no emite app events. | 6 |

## El cliente: el otro lado del enclave, en Synsema

`novaw` sólo habla el payload de la payment app; cualquier otra app necesita su propio cliente.
`examples/client/vela_client.syn` hace todo lo que un cliente puede hacer, nativo, con los builtins
que llegaron en v0.6.20 — sin Go, sin TypeScript. Copiá `.env.example` a `.env`: `VELA_RPC_URL`,
`VELA_PROCESSOR`, `VELA_TEE_AUTHENTICATOR`, `VELA_AUTHORITY_URL`, `VELA_SUBGRAPH_URL`,
`VELA_APP_ID`, `VELA_MAX_FEE` (wei), `VELA_SECP_KEY` (firma transacciones, paga), `VELA_P521_KEY` /
`VELA_P521_PUB` (de `keys`), `VELA_USER_KEY` (sólo para los comandos de facilitador).

| `synsema run vela_client.syn -- …` | Qué hace |
|---|---|
| `keys` | un par P-521 nuevo, impreso para `.env` (`ecdh_keypair` + `reveal`) |
| `address` | la dirección que firma (`VELA_SECP_KEY`) |
| `tee` | la clave P-521 de comunicación del Executor, leída de `TeeAuthenticator.getPubSecp521r1()` |
| `deploy <wasm> [params-json\|-] [trigger]` | subida multipart a `<authority>/deploy/upload` (`multipart_encode`), luego `submitDeployRequest(0, descriptor)` o `submitDeployRequestWithTrigger(0, descriptor, trigger)`; `applicationId` y `requestId` del log `DeployRequestSubmitted`; espera por el subgraph; imprime el `VELA_APP_ID` a configurar |
| `register` | AssociateKey: tu clave pública P-521 ‖ la seed de privacidad cifrada para el Executor (226 bytes); la seed es `secp256k1_sign(keccak256("subtype-key-v1"))`, exactamente como `novaw` |
| `deposit <monto> [token]` | ETH (wei) por `submitRequest` con payload vacío, o un ERC-20 allowlisted tras `approve(processor, monto)` |
| `send '<json>' [wei]` | PROCESS con el payload cifrado para el Executor; un depósito de ETH opcional viaja en el mismo request |
| `report '<json>'`, `report-download <id>` | DEANONYMIZATION (quien llama tiene que ser una autoridad permitida), luego el reporte traído del authority service — GET `/nonce`, una firma EIP-191 de `chainId(8) ‖ appId(8) ‖ reportId(32) ‖ nonce(32)`, POST `/getreport` — y descifrado |
| `events [n]`, `app-events [n]` | tus eventos, filtrados por tus 50 subtipos HMAC y descifrados; los eventos públicos de la app, con la etiqueta decodificada cuando el subtipo es una |
| `status <requestId>` | la fila `requestCompleteds` / `deployRequestCompleteds` del subgraph |
| `user`, `register-for`, `send-for '<json>' [monto token]`, `events-for [n]` | **facilitador / meta-transacciones** (`submitRequestFor`): el usuario (`VELA_USER_KEY`, **sin ETH**) firma un `RequestAuthorization` EIP-712 (dominio `Vela` / versión `"0"` / chainId / el endpoint, `nonce = getFacilitatorNonce(user)`) y, para depositar un ERC-20, un `Permit` EIP-2612 bajo el dominio del token; `VELA_SECP_KEY` manda la transacción y paga gas y fee. Sólo ASSOCIATEKEY y PROCESS; el depósito sólo puede ser un ERC-20, nunca ETH |

Cada request espera `RequestCompleted` por el subgraph e informa el código y mensaje de error cuando
`status ≠ 0`. Las partes puras tienen tests (`synsema test vela_client.syn`): el ida y vuelta del
cifrado, seed y subtipos, parseo de logs, calldata, los digests EIP-712.

### Los formatos de cable, para que nadie tenga que releer Go

- **Payloads y eventos**: `nonce(12) ‖ AES-256-GCM(clave, datos)`, sin AAD, con
  `clave = HKDF-SHA256(X compartida de ECDH P-521, sin salt, sin info, 32 bytes)` entre la clave
  P-521 del usuario y la clave de comunicación del Executor (133 bytes, sin comprimir). Los reportes
  se cifran para la clave registrada de quien los pide. En Synsema: `ecdh_shared_secret` →
  `hkdf_sha256` → `aes_gcm_encrypt`.
- **AssociateKey** (tipo 3): la clave pública P-521 del usuario (133) ‖ la seed cifrada (93) = 226
  bytes (133 solos también se aceptan). La seed es una firma secp256k1 de 65 bytes; los subtipos de
  eventos del usuario son `HMAC-SHA256(seed, byte(i))`, i = 1..50 — el Executor estampa uno en cada
  evento, y un cliente filtra el subgraph por ese conjunto.
- **Ids de request**: el topic 2 de `RequestSubmitted`; el `applicationId` de un deploy es el topic 1
  de `DeployRequestSubmitted` y su `requestId` la data del evento.
- **Descriptor de deploy** (el payload on-chain): `{"mode": "artifact_ref", "artifactId":
  "sha256:…", "wasmSha256": "…", "constructorParams": {…}}` tras `POST /deploy/upload` (campo
  multipart `wasm`).
- **Regla de valor** de `submitRequest`: ETH → `msg.value = assetAmount + maxFeeValue`; ERC-20 →
  `msg.value = maxFeeValue` y el endpoint tira de los tokens (allowance, o el permit en
  `submitRequestFor`).

## Trigger contracts: llamadas on-chain desde adentro del enclave

Un trigger contract es la única autonomía que tiene una app hoy: sin cron, sin oráculo, sin red. El
ciclo, según el doc de diseño del starter kit y verificado de punta a punta:

1. Un usuario manda `process` (acá `{"command": "execute", "execute": {target, value, data}}`). La
   app bloquea el monto, devuelve un **withdrawal al trigger contract** y **un app event** cuyo
   `data_hex` es `abi.encode(bytes16 lockId, address target, uint256 value, bytes data)` con el
   subtipo etiqueta `execute_requested`.
2. Durante `stateUpdate` el ProcessorEndpoint reclama el ETH retirado hacia el trigger, llama
   `execute(appEventData)` — la llamada corre **desde la dirección del pool** — luego `withdraw()`
   devuelve lo que sobró, y `getTrustProcessPayload(...)` devuelve `abi.encode(bytes16 lockId,
   uint256 remain, uint8 outcome)`. Un payload no vacío encola un **TRUSTPROCESS** (cola prioritaria,
   sin sender, sin fee).
3. El Executor llama `trusted_request`: la app decodifica `payload_hex`, acredita `remain` al dueño
   del lock, borra el lock, emite un `execution_outcome` cifrado al dueño — y **ningún app event**,
   que es lo que termina el ciclo (la guarda `events.length == 0` del trigger devuelve `""`).

`examples/trigger/src/PoolTrigger.sol` es el contrato compañero (extiende el `AbstractTrigger` de
Vela; `build.sh` clona `HorizenOfficial/vela` v0.2.0 — esos contratos son BSL y no se vendorizan — y
lo despliega con `forge`). La app se despliega con `{"triggerContract": "0x…"}` en los params del
constructor **y** la misma dirección en `submitDeployRequestWithTrigger`
(`vela_client.syn -- deploy pool.wasm '{"triggerContract":"0x…"}' 0x…`). Medido: el target recibió
exactamente 0,01 ETH durante `stateUpdate`, y el TRUSTPROCESS volvió unos 25 s después.

## ERC-20 y el facilitador

Los tokens tienen que estar en el `TokenAllowlist` (`addAllowedToken`, rol ADMIN) y, para una app,
en sus `allowedTokens` al desplegar. `examples/erc20/src/TestToken.sol` es un ERC-20 mínimo **con
`permit` EIP-2612**; `build.sh` lo despliega y lo da de alta. Desde el cliente: `deploy payment.wasm
'{"allowedTokens":["0x…"]}'`, `deposit 1000000 0x…` (approve + pull), después transferencias con
`"tokenAddress"` en el payload; los eventos traen `tokenAddress` y la custodia del endpoint guarda
los tokens.

El patrón **facilitador** es un flujo del lado cliente, no una limitación de la plataforma:
`submitRequestFor` deja que otro pague. El usuario sólo firma typed data; un usuario con **0 ETH**
registró su clave (`register-for`) y depositó 2 TST más una transferencia en un solo request
(`send-for … 2000000 0x…`), con el facilitador pagando gas y 85 wei de fees; los eventos del usuario
se descifran con su propia seed (`events-for`). Un detalle con el que te vas a cruzar:
`ECDSA.recover` de OpenZeppelin y `permit` quieren `v = 27/28`, mientras que `secp256k1_sign`
devuelve el recovery id 0/1 — sumale 27.

## El devnet público

`devnet.synsema.app` corre el starter kit v0.2.0 de Horizen (Anvil, los contratos de Vela, el
subgraph, el Executor como TEE emulado, el Manager, el Authority Service) detrás de HTTPS, para
cualquiera que quiera desplegar una app Synsema en Vela sin nueve contenedores en su máquina. Un
token propio está a un comando; el cliente escribe las líneas en `client/.env`:

```sh
synsema run vela_client.syn -- devnet            # o: curl -X POST https://devnet.synsema.app/token
```

```
VELA_RPC_URL=https://devnet.synsema.app/<tu token>/rpc
VELA_AUTHORITY_URL=https://devnet.synsema.app/<tu token>/authority
VELA_SUBGRAPH_URL=https://devnet.synsema.app/<tu token>/subgraph/subgraphs/name/hcce
VELA_PROCESSOR=0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9
VELA_TEE_AUTHENTICATOR=0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0
VELA_TOKEN_ALLOWLIST=0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9
VELA_DEFAULT_AUTHORITY=0x5FbDB2315678afecb367f032d93F642f64180aa3
VELA_TEST_TOKEN=0x610178dA211FEF7D417bC0e6FeD39F05609AD788
VELA_TRIGGER_FACTORY=0xC9a43158891282A2B1475592D5719c001986Aaec
VELA_TOKEN=0x610178dA211FEF7D417bC0e6FeD39F05609AD788
VELA_SECP_KEY=…   VELA_P521_KEY=…   VELA_P521_PUB=…   (Anvil #0 y un par P-521 nuevo: todo lo que un cliente necesita)
```

Es un **devnet**: las claves son las públicas de Anvil, no hay attestation, se resetea cada tanto
(las direcciones de los contratos vuelven iguales; los ids de app, saldos y claves registradas no),
y nada de lo que pongas ahí es privado frente a quien administra la máquina: un lugar para iterar,
no para guardar valor. El admin es la cuenta #0 de Anvil (su clave privada es la conocida
`ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80`, la `VELA_SECP_KEY` por defecto
del kit), así que todo lo que hace un admin lo hacés vos: desplegar (`DEPLOYER_ROLE`), dar de alta un
ERC-20 (`vela_client.syn -- allow-token <dirección>`), habilitar un auditor para tu app
(`vela_client.syn -- allow-authority <appId> <dirección>`). Un ERC-20 de prueba con `permit` (TST, 6
decimales, todo el supply en Anvil #0) ya está en la allowlist: `VELA_TEST_TOKEN`.
`https://devnet.synsema.app/status` muestra el número de bloque y el uso. El token es un nombre en la
ruta que deja afuera a los escáneres y permite contar el uso por persona, no un secreto que valga algo.
Diez tokens por hora por dirección, salvo para el control de la plataforma Synsema, que pide uno por
cada proyecto que crea desde una receta. El ProcessorEndpoint del kit se despliega con lugar para diez
aplicaciones; el devnet lo sube a 100 000 (`devnet/admin.syn` en el repositorio de la plataforma), así
que un revert `MaxNumOfApplicationsExceeded` es un stack en su valor por defecto, no un error de tu deploy.

## El stack local (starter kit v0.2.0) — una receta que funciona

- `git clone HorizenOfficial/vela-starterkit`, `cp dockerfiles/.env.dev dockerfiles/.env`, `docker
  compose up -d` en `dockerfiles/` (nueve imágenes, alrededor de 1,5 GB). La cadena queda publicada en
  `localhost:8545`, el authority service en `:8081`, el subgraph en `:8000`; la red interna es
  `dockerfiles_pes_network`. El deployer escribe las direcciones en el volumen `deploy-data`:
  ProcessorEndpoint `0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9`, TeeAuthenticator
  `0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0`, TokenAllowlist
  `0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9`, DefaultAuthority
  `0x5FbDB2315678afecb367f032d93F642f64180aa3`. La cuenta #0 de Anvil es deployer y admin.
- `deployapp` necesita `DEPLOYER_ROLE`; `requestreport` revierte con `AuthorityNotAllowed` hasta que
  el admin llama `DefaultAuthority.addAllowedAuthority(appId, caller)` (`cast` está dentro del
  contenedor `vela-skit-chain`). Multi-app funciona: cada deploy recibe su propio `applicationId`.
- `novaw-linux` (la release de `vela-nova`) en Windows: no se puede ejecutar directo desde un bind
  mount — copialo primero al filesystem de un contenedor: `docker run --rm --network
  dockerfiles_pes_network --entrypoint sh -v <carpeta wallet>:/wallet -w /wallet postgres:14 -c 'cp
  /wallet/novaw-linux /usr/local/bin/novaw && chmod +x /usr/local/bin/novaw && novaw <cmd>'`, con
  `MSYS_NO_PATHCONV=1` en Git Bash y un `wallet.conf` apuntando a `10.10.40.30:8545`,
  `10.10.40.40:8081`, `vela-skit-subgraph-node:8000`.
- `forge` y `cast` están dentro de la imagen `horizen/cce-chain:v0.2.0`. Compilá contratos en un
  contenedor sobre la red **por defecto** de Docker con `RPC_URL=http://host.docker.internal:8545`
  (el contenedor de la cadena en marcha no tiene DNS para bajar solc ni de GitHub).
- Tiempos: un deploy confirma en unos 12 s, un request en 10–15 s, un ida y vuelta de TRUSTPROCESS
  en unos 25 s. Límites del Executor: wasmtime-go v1.0.0, un request por bloque, timeout de
  comunicación de 30 s, sin medición de fuel (el fuel es autodeclarado), memoria por debajo de 2 GB.

## Tropiezos al escribir guests y clientes

- `to` y `from` son palabras reservadas — ni siquiera como nombres de parámetro. No hay literales
  `0xab` (escribí `171`). `index_of` toma una lista, no texto. Los mapas se comparten por
  referencia: una task que hace `set` dentro del estado que recibió muta el mapa del que llama —
  importa en tests que reutilizan un estado.
- `ecdh_shared_secret`, `hkdf_sha256` y `aes_gcm_*` toman las claves como bytes o secret de bytes:
  una clave hex del `.env` es `as_secret(bytes(env("K"), "hex"), "K")` (un `secret()` de texto son
  los caracteres hex, no la clave). `hmac_sha256(data, key)` estringifica un valor `bytes` —
  envolvé ambos en `as_secret`. La clave privada de `ecdh_keypair` lleva la etiqueta
  `ecdh_keypair.private` (`require reveal("ecdh_keypair.private")` para imprimirla una vez).
- `http_post(url, mapa)` manda JSON con el Content-Type; un body `bytes` va crudo; el mapa de
  respuesta tiene `status`, `ok`, `body`, `json`, `headers`. `multipart_encode(partes)` → `{body,
  content_type}`. `require file.read("*")` para una ruta dada por línea de comandos; `args()` no
  necesita capability; `sleep` necesita `time`; `secp256k1_sign` necesita `sign("<etiqueta del
  secret>")`.
- (Para quien desarrolla el adaptador) `cargo test` en el crate del guest compila un `.wasm` que no puede
  correr — pasá `--target <host triple>`.

## Agregar otro host

Copiá `packages/guests/vela/`, renombrá el crate, y leé el contrato del host **desde su código
fuente**: qué exports llama, sus firmas, cómo escribe las entradas y lee los resultados, qué imports
provee, qué prohíbe. Escribilo arriba de `src/lib.rs` con el archivo y la versión de donde salió.
Mantené la forma — decodificar entradas → un mapa `ctx` → `run_app(task, fallback, ctx)` →
codificar el mapa de la task al resultado exacto del host — elegí `wasm32-wasip1` cuando el host
enlaza WASI y `wasm32-unknown-unknown` sólo si provee los imports `synsema_host`, tratá el techo
como el contrato de determinismo, escribí la sonda, cableá CI y documentá el contrato del `.syn` en
el README del guest. Si al ABI genérico le falta algo, agregá una operación `synsema_call`
documentada y útil para todos los hosts — nunca un export especial para uno.
