Smart Contracts
Descripción General
Los smart contracts de ULedgerNET son programas escritos en WebAssembly (WASM) y ejecutados en la ULVM (ULedgerNET Virtual Machine, el entorno de ejecución aislado (sandbox) y con medición de gas de ULedgerNET, construido sobre wazero, un motor WASM puro en Go sin dependencias de CGo).
Los contratos definen la lógica a nivel de aplicación para la gobernanza, las operaciones con tokens, la autorización de agentes, la gestión de datos y cualquier otro cómputo personalizado on-chain. El protocolo garantiza que lo que el contrato registra efectivamente ocurrió; la capa de pruebas STARK garantiza la pista de auditoría.
El Runtime de la ULVM
Contract binary (WASM)
│
▼
wazero compilation (cached per bytecode SHA256)
│
▼
Module instantiated per invocation
│
▼
Execution with:
- Gas metering (base + per-byte costs)
- Wall-clock timeout (5 seconds)
- Storage transaction (commit on success, rollback on failure)
- Access to host functions via ulvm.env.*
│
▼
Success → storage committed, events emitted
Failure → storage rolled back, gas consumed, error recorded
Concurrencia
El runtime limita las invocaciones concurrentes de contratos a NumCPU() mediante un semáforo. Esto limita el uso de memoria lineal bajo carga alta. Las invocaciones que exceden el límite del semáforo se ponen en cola, no se rechazan.
Caché de Compilación
Los módulos WASM se compilan una sola vez y se almacenan en caché de forma global en un sync.Map indexado por el SHA256 del bytecode. Las invocaciones posteriores del mismo bytecode de contrato omiten la recompilación, lo que hace económicas las invocaciones repetidas.
Funciones del Host (Interfaz de la ULVM)
Los contratos interactúan con la cadena mediante un conjunto de funciones del host proporcionadas por el módulo ulvm.env:
| Función | Descripción |
|---|---|
get_caller() | Devuelve la dirección de la billetera que invocó el contrato. |
get_owner() | Devuelve la dirección de la billetera propietaria del contrato. |
get_block_height() | Devuelve la altura de bloque actual al momento de la ejecución. |
get_storage(key) | Lee un valor del almacenamiento persistente del contrato. |
set_storage(key, value) | Escribe un valor en el almacenamiento persistente del contrato. |
delete_storage(key) | Elimina una clave del almacenamiento persistente del contrato. |
has_key(key) | Comprueba si existe una clave en el almacenamiento. |
emit_event(data) | Emite un evento (visible en los recibos de bloque y en las suscripciones a eventos). |
hash_data(dataPtr, strategy) | Calcula el hash de datos usando SHA256, SHA3-256 o SHA3-512. |
log(string) | Emite un mensaje de registro (visible en los registros del nodo, no on-chain). |
abort() | Interrumpe el módulo WASM con un trap (señala un error fatal). |
Todas las operaciones de almacenamiento (get_storage, set_storage, delete_storage) se realizan dentro de una storage transaction. Si la ejecución del contrato falla o se revierte, todos los cambios de almacenamiento se deshacen de forma atómica.
Medición de Gas
Cada operación en una invocación de contrato cuesta gas:
| Operación | Costo de Gas |
|---|---|
| Costo base de invocación | Fijo por invocación |
| Ejecución de instrucciones WASM | Medición por instrucción |
get_storage(key) | Base + por byte de la clave |
set_storage(key, value) | Base + por byte de la clave + por byte del valor |
delete_storage(key) | Base + por byte de la clave |
emit_event(data) | Base + por byte de los datos del evento |
hash_data(data) | Base + por byte de la entrada |
Los límites de gas se establecen en el momento de la invocación mediante el payload de la transaction INVOKE_SMART_CONTRACT. Si la ejecución agota el límite de gas antes de completarse, la invocación falla; el almacenamiento se revierte pero el gas se consume.
El contrato de ejemplo council-voting usa un límite de gas de 100_000 por invocación.
Transactions del Ciclo de Vida del Contrato
| Transaction | Descripción |
|---|---|
DEPLOY_SMART_CONTRACT | Despliega un nuevo contrato. Valida el binario WAT/WASM, lo compila y almacena el bytecode. El propietario es la billetera que despliega. |
INVOKE_SMART_CONTRACT | Llama a una función del contrato con argumentos y un límite de gas. |
UPGRADE_SMART_CONTRACT | Reemplaza el bytecode del contrato. La versión se incrementa. Solo el propietario (o una billetera con permiso de actualización) puede actualizar. |
ROLLBACK_SMART_CONTRACT | Revierte al bytecode de la versión anterior. |
Direcciones de Contrato
La dirección de un contrato es el ID de la transaction DEPLOY_SMART_CONTRACT que lo desplegó, determinista a partir del contexto de despliegue.
Versionado
Los contratos llevan un contador Version y una referencia PreviousVersion. Actualizar incrementa la versión; el rollback restaura el bytecode anterior. El historial de versiones está on-chain.
Escritura de Contratos (AssemblyScript → WAT → WASM)
Los contratos se escriben en AssemblyScript (un lenguaje similar a TypeScript que se compila a WASM) y luego se compilan a .wat (formato de texto de WebAssembly) y a .wasm binario.
Estructura del Contrato
// Import host functions
@external("env", "get_caller")
declare function get_caller(outPtr: i32): void;
@external("env", "set_storage")
declare function set_storage(keyPtr: i32, keyLen: i32, valPtr: i32, valLen: i32): void;
@external("env", "get_storage")
declare function get_storage(keyPtr: i32, keyLen: i32, outPtr: i32, outLen: i32): i32;
@external("env", "emit_event")
declare function emit_event(dataPtr: i32, dataLen: i32): void;
// Exported contract functions
export function myFunction(arg: i32): i32 {
// Implementation
}
Diseño de Claves de Almacenamiento
El estado del contrato se almacena como pares clave-valor en el espacio de nombres de almacenamiento aislado del contrato. El diseño de las claves determina la capacidad de consulta:
council:member:<address> → membership marker ("1")
proposal:<id>:status → "active" | "passed" | "failed"
proposal:<id>:votes:yes → vote count (stringified integer)
voted:<proposalId>:<memberAddr> → voter marker ("1")
token:<memberAddr>:<id>:agent → work token marker
Al usar AssemblyScript, las claves son cadenas codificadas en UTF-16 en la memoria lineal de WASM (2 bytes por carácter).
El Contrato Council Voting
El contrato council-voting.wat (desplegado en la demo board-council y en la suite E2E del council) es el ejemplo de referencia para los contratos de gobernanza en ULedger.
Funciones Exportadas
| Función | Quién la llama | Descripción |
|---|---|---|
addMember(address) | Solo el propietario | Agrega una billetera como miembro del council. |
removeMember(address) | Solo el propietario | Elimina a un miembro del council. |
isMember(address) → bool | Cualquiera | Comprueba si una billetera es miembro. |
authorizeAgent(agentAddr) | Miembro | Delega el voto a una billetera de agente. |
revokeAgent(agentAddr) | Miembro | Revoca la delegación al agente. |
createProposal(title, desc, durationBlocks, quorum) → id | Miembro | Crea una propuesta con fecha límite por altura de bloque. |
issueWorkToken(agentAddr, proposalId) | Miembro | Emite un token que autoriza al agente a votar en la propuesta. |
submitWork(proposalId, vote, evidence) | Agente | Emite un voto (1=sí, 2=no, 3=abstención) con una cadena de evidencia opcional. |
finalizeProposal(proposalId) | Cualquiera | Contabiliza los votos y cierra la propuesta tras la fecha límite. |
getProposalStatus(proposalId) → string | Cualquiera | Lee el estado actual de la propuesta. |
Valores de Voto
| Valor | Significado |
|---|---|
1 | Sí |
2 | No |
3 | Abstención |
Patrón de Control de Acceso
El contrato aplica su propio control de acceso usando get_caller() y consultas al almacenamiento, no los permisos de billetera de ULedger. Este es el patrón estándar: las capabilities de ULedger controlan si la billetera puede invocar el contrato en absoluto; la lógica interna del contrato controla qué funciones puede ejecutar el llamante dentro del contrato.
Eventos del Contrato
Los contratos emiten eventos mediante emit_event(). Los eventos aparecen en:
- Los recibos de bloque (disponibles mediante la API del nodo).
- Las suscripciones a eventos por WebSocket (transmisión en tiempo real).
- El listener de eventos del SDK (usado por el driver de board-council para la transmisión SSE al frontend).
Los eventos forman parte del registro de Merkle del bloque y, por lo tanto, son demostrables mediante la pista de auditoría STARK.
Ejemplo de Despliegue (Go SDK)
// Deploy the council voting contract
contractAddr, err := deployer.DeployContract(ctx, contractBinary, sdk.DeployOpts{
GasLimit: 1_000_000,
IsUpgradeable: true,
})
// Invoke addMember
receipt, err := sdk.InvokeContract(ctx, contractAddr, "addMember", memberWalletAddr, sdk.InvokeOpts{
GasLimit: 100_000,
CallerWallet: ownerWallet,
})
// Verify state
status, err := sdk.ReadContractState(ctx, contractAddr, "council:member:"+memberWalletAddr)