Saltar al contenido principal

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ónDescripció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ónCosto de Gas
Costo base de invocaciónFijo por invocación
Ejecución de instrucciones WASMMedició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​

TransactionDescripción
DEPLOY_SMART_CONTRACTDespliega un nuevo contrato. Valida el binario WAT/WASM, lo compila y almacena el bytecode. El propietario es la billetera que despliega.
INVOKE_SMART_CONTRACTLlama a una función del contrato con argumentos y un límite de gas.
UPGRADE_SMART_CONTRACTReemplaza el bytecode del contrato. La versión se incrementa. Solo el propietario (o una billetera con permiso de actualización) puede actualizar.
ROLLBACK_SMART_CONTRACTRevierte 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ónQuién la llamaDescripción
addMember(address)Solo el propietarioAgrega una billetera como miembro del council.
removeMember(address)Solo el propietarioElimina a un miembro del council.
isMember(address) → boolCualquieraComprueba si una billetera es miembro.
authorizeAgent(agentAddr)MiembroDelega el voto a una billetera de agente.
revokeAgent(agentAddr)MiembroRevoca la delegación al agente.
createProposal(title, desc, durationBlocks, quorum) → idMiembroCrea una propuesta con fecha límite por altura de bloque.
issueWorkToken(agentAddr, proposalId)MiembroEmite un token que autoriza al agente a votar en la propuesta.
submitWork(proposalId, vote, evidence)AgenteEmite un voto (1=sí, 2=no, 3=abstención) con una cadena de evidencia opcional.
finalizeProposal(proposalId)CualquieraContabiliza los votos y cierra la propuesta tras la fecha límite.
getProposalStatus(proposalId) → stringCualquieraLee el estado actual de la propuesta.

Valores de Voto​

ValorSignificado
1Sí
2No
3Abstenció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)