Operación
Vela (Horizen)
Vela 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 (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 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 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 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 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, 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: 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.
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 parauserpor 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 sinuser: 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, eldatade 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}];tokenpor 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, lossteps()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; elapplicationIdde 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:
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.
deployappnecesitaDEPLOYER_ROLE;requestreportrevierte conAuthorityNotAllowedhasta 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 devela-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.
forgeycastestán dentro de la imagenhorizen/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§
toyfromson 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_sha256yaes_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 bodybytesva 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 testen el crate del guest compila un.wasmque 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.