Oasis presentó el marco para la lógica fuera de cadena en tiempo de ejecución (ROFL) para ayudar a construir y ejecutar aplicaciones fuera de cadena mientras se garantiza la privacidad y se mantiene la confianza con la cadenaOasis presentó el marco para la lógica fuera de cadena en tiempo de ejecución (ROFL) para ayudar a construir y ejecutar aplicaciones fuera de cadena mientras se garantiza la privacidad y se mantiene la confianza con la cadena

Guía para la generación de claves entre cadenas (EVM / Base) con Oasis ROFL

2026/02/20 21:16
Lectura de 10 min
Si tienes comentarios o inquietudes sobre este contenido, comunícate con nosotros mediante crypto.news@mexc.com

Oasis introdujo el framework para lógica off-chain en tiempo de ejecución (ROFL) para ayudar a construir y ejecutar aplicaciones off-chain mientras se asegura la privacidad y se mantiene la confianza con verificabilidad en cadena. Hay muchas partes móviles al construir con ROFL.
En este tutorial, demostraré cómo construir una pequeña aplicación TypeScript, generando una clave secp256k1 dentro de ROFL. Utilizará el SDK de TypeScript @oasisprotocol/rofl-client, que se comunica con la API REST de appd internamente. La aplicación TypeScript también:

Habrá una simple prueba de humo que imprime en los registros.

Requisitos previos

Para realizar los pasos descritos en esta guía, necesitarás:

  • Node.js 20+ y Docker (o Podman)
  • Oasis CLI y un mínimo de 120 tokens TEST en tu billetera (faucet de Testnet de Oasis)
  • Algo de ETH de prueba de Base Sepiola (faucet de Base Sepiola)

Para los detalles de configuración, consulta la documentación sobre Requisitos previos de inicio rápido.

Inicializar la aplicación

El primer paso es inicializar una nueva aplicación usando Oasis CLI.

oasis rofl init rofl-keygen
cd rofl-keygen

Crear aplicación

Al momento de crear la aplicación en Testnet, se te requerirá depositar tokens. Asigna 100 tokens TEST en este punto.

oasis rofl create --network testnet

Como salida, el CLI producirá el App ID, denotado por rofl1….

Inicializar un proyecto Hardhat (TypeScript)

Ahora, estás listo para iniciar el proyecto.

npx hardhat init

Dado que estamos mostrando una aplicación TypeScript, elige TypeScript cuando se te solicite, y luego acepta los valores predeterminados.
El siguiente paso sería agregar las pequeñas dependencias de tiempo de ejecución para usar fuera de Hardhat.

npm i @oasisprotocol/rofl-client ethers dotenv @types/node
npm i -D tsx

La plantilla TypeScript de Hardhat crea automáticamente un tsconfig.json. Necesitamos agregar un pequeño script para que el código de la aplicación pueda compilarse en dist/.

// tsconfig.json
{
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist"
},
"include": ["src"]
}

Estructura de la aplicación

En esta sección, agregaremos algunos pequeños archivos TS y un contrato Solidity.

src/
├── appd.ts # envoltura delgada sobre @oasisprotocol/rofl-client
├── evm.ts # ayudantes de ethers (proveedor, billetera, tx, implementación)
├── keys.ts # pequeños ayudantes (suma de verificación)
└── scripts/
├── deploy-contract.ts # script de implementación genérico para artefactos compilados
└── smoke-test.ts # demostración de extremo a extremo (registros)
contracts/
└── Counter.sol # contrato de muestra

  1. src/appd.ts — envoltura delgada sobre el SDK Aquí, necesitarás usar el cliente oficial para comunicarte con appd (socket UNIX). También necesitaremos mantener un respaldo local-dev explícito cuando se ejecute fuera de ROFL.

src/appd.ts

import {existsSync} from 'node:fs';
import {
RoflClient,
KeyKind,
ROFL_SOCKET_PATH
} from '@oasisprotocol/rofl-client';
const client = new RoflClient(); // UDS: /run/rofl-appd.sock
export async function getAppId(): Promise<string> {
return client.getAppId();
}
/**
* Genera (o re-deriva determinísticamente) una clave secp256k1 dentro de ROFL y
* la devuelve como una cadena hexadecimal con prefijo 0x (para ethers.js Wallet).
*
* SOLO desarrollo local (fuera de ROFL): Si falta el socket y estableces
* ALLOW_LOCAL_DEV=true y LOCAL_DEV_SK=0x<64-hex>, se usa ese valor.
*/
export async function getEvmSecretKey(keyId: string): Promise<string> {
if (existsSync(ROFL_SOCKET_PATH)) {
const hex = await client.generateKey(keyId, KeyKind.SECP256K1);
return hex.startsWith('0x') ? hex : `0x${hex}`;
}
const allow = process.env.ALLOW_LOCAL_DEV === 'true';
const pk = process.env.LOCAL_DEV_SK;
if (allow && pk && /^0x[0-9a-fA-F]{64}$/.test(pk)) return pk;
throw new Error(
'socket rofl-appd no encontrado y no se proporcionó LOCAL_DEV_SK (solo desarrollo).'
);
}

2. src/evm.ts — ayudantes de ethers

import {
JsonRpcProvider,
Wallet,
parseEther,
type TransactionReceipt,
ContractFactory
} from "ethers";
export function makeProvider(rpcUrl: string, chainId: number) {
return new JsonRpcProvider(rpcUrl, chainId);
}
export function connectWallet(
skHex: string,
rpcUrl: string,
chainId: number
): Wallet {
const w = new Wallet(skHex);
return w.connect(makeProvider(rpcUrl, chainId));
}
export async function signPersonalMessage(wallet: Wallet, msg: string) {
return wallet.signMessage(msg);
}
export async function sendEth(
wallet: Wallet,
to: string,
amountEth: string
): Promise<TransactionReceipt> {
const tx = await wallet.sendTransaction({
to,
value: parseEther(amountEth)
});
const receipt = await tx.wait();
if (receipt == null) {
throw new Error("Transacción eliminada o reemplazada antes de la confirmación");
}
return receipt;
}
export async function deployContract(
wallet: Wallet,
abi: any[],
bytecode: string,
args: unknown[] = []
): Promise<{ address: string; receipt: TransactionReceipt }> {
const factory = new ContractFactory(abi, bytecode, wallet);
const contract = await factory.deploy(...args);
const deployTx = contract.deploymentTransaction();
const receipt = await deployTx?.wait();
await contract.waitForDeployment();
if (!receipt) {
throw new Error("TX de implementación no minado");
}
return { address: contract.target as string, receipt };
}

3. src/keys.ts — pequeños ayudantes

import { Wallet, getAddress } from "ethers";
export function secretKeyToWallet(skHex: string): Wallet {
return new Wallet(skHex);
}
export function checksumAddress(addr: string): string {
return getAddress(addr);
}

4. src/scripts/smoke-test.ts — flujo único de extremo a extremo

Este es un paso importante ya que este script tiene múltiples funciones:

  • imprimir el App ID (dentro de ROFL), dirección y un mensaje firmado
  • esperar financiamiento
  • implementar el contrato contador

import "dotenv/config";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { getAppId, getEvmSecretKey } from "../appd.js";
import { secretKeyToWallet, checksumAddress } from "../keys.js";
import { makeProvider, signPersonalMessage, sendEth, deployContract } from "../evm.js";
import { formatEther, JsonRpcProvider } from "ethers";
const RPC_URL = process.env.BASE_RPC_URL ?? "https://sepolia.base.org";
const CHAIN_ID = Number(process.env.BASE_CHAIN_ID ?? "84532");
const KEY_ID = process.env.KEY_ID ?? "evm:base:sepolia";
function sleep(ms: number): Promise<void> {
return new Promise((r) => setTimeout(r, ms));
}
async function waitForFunding(
provider: JsonRpcProvider,
addr: string,
minWei: bigint = 1n,
timeoutMs = 15 * 60 * 1000,
pollMs = 5_000
): Promise<bigint> {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
const bal = await provider.getBalance(addr);
if (bal >= minWei) return bal;
console.log(`Esperando financiamiento... saldo actual=${formatEther(bal)} ETH`);
await sleep(pollMs);
}
throw new Error("Tiempo de espera agotado esperando financiamiento.");
}
async function main() {
const appId = await getAppId().catch(() => null);
console.log(`ROFL App ID: ${appId ?? "(no disponible fuera de ROFL)"}`);
const sk = await getEvmSecretKey(KEY_ID);
// NOTA: Esta demostración confía en el proveedor RPC configurado. Para producción, prefiere un
// cliente ligero (por ejemplo, Helios) para que puedas verificar el estado de la cadena remota.
const wallet = secretKeyToWallet(sk).connect(makeProvider(RPC_URL, CHAIN_ID));
const addr = checksumAddress(await wallet.getAddress());
console.log(`Dirección EVM (Base Sepolia): ${addr}`);
const msg = "hola desde rofl";
const sig = await signPersonalMessage(wallet, msg);
console.log(`Mensaje firmado: "${msg}"`);
console.log(`Firma: ${sig}`);
const provider = wallet.provider as JsonRpcProvider;
let bal = await provider.getBalance(addr);
if (bal === 0n) {
console.log("Por favor, financia la dirección anterior con ETH de Base Sepolia para continuar.");
bal = await waitForFunding(provider, addr);
}
console.log(`Saldo detectado: ${formatEther(bal)} ETH`);
const artifactPath = join(process.cwd(), "artifacts", "contracts", "Counter.sol", "Counter.json");
const artifact = JSON.parse(readFileSync(artifactPath, "utf8"));
if (!artifact?.abi || !artifact?.bytecode) {
throw new Error("Artefacto Counter falta abi/bytecode");
}
const { address: contractAddress, receipt: deployRcpt } =
await deployContract(wallet, artifact.abi, artifact.bytecode, []);
console.log(`Counter implementado en ${contractAddress} (tx=${deployRcpt.hash})`);
console.log("¡Prueba de humo completada exitosamente!");
}
main().catch((e) => {
console.error(e);
process.exit(1);
});

5. contracts/Counter.sol — muestra mínima

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Counter {
uint256 private _value;
event Incremented(uint256 v);
event Set(uint256 v);
function current() external view returns (uint256) { return _value; }
function inc() external { unchecked { _value += 1; } emit Incremented(_value); }
function set(uint256 v) external { _value = v; emit Set(v); }
}

6. src/scripts/deploy-contract.ts — implementador genérico

import "dotenv/config";
import { readFileSync } from "node:fs";
import { getEvmSecretKey } from "../appd.js";
import { secretKeyToWallet } from "../keys.js";
import { makeProvider, deployContract } from "../evm.js";
const KEY_ID = process.env.KEY_ID ?? "evm:base:sepolia";
const RPC_URL = process.env.BASE_RPC_URL ?? "https://sepolia.base.org";
const CHAIN_ID = Number(process.env.BASE_CHAIN_ID ?? "84532");
/**
* Uso:
* npm run deploy-contract -- ./artifacts/MyContract.json '[arg0, arg1]'
* El artefacto debe contener { abi, bytecode }.
*/
async function main() {
const [artifactPath, ctorJson = "[]"] = process.argv.slice(2);
if (!artifactPath) {
console.error("Uso: npm run deploy-contract -- <artifact.json> '[constructorArgsJson]'");
process.exit(2);
}
const artifactRaw = readFileSync(artifactPath, "utf8");
const artifact = JSON.parse(artifactRaw);
const { abi, bytecode } = artifact ?? {};
if (!abi || !bytecode) {
throw new Error("El artefacto debe contener { abi, bytecode }");
}
let args: unknown[];
try {
args = JSON.parse(ctorJson);
if (!Array.isArray(args)) throw new Error("los argumentos del constructor deben ser un array JSON");
} catch (e) {
throw new Error(`Error al analizar JSON de argumentos del constructor: ${String(e)}`);
}
const sk = await getEvmSecretKey(KEY_ID);
// NOTA: Esta demostración confía en el proveedor RPC configurado. Para producción, prefiere un
// cliente ligero (por ejemplo, Helios) para que puedas verificar el estado de la cadena remota.
const wallet = secretKeyToWallet(sk).connect(makeProvider(RPC_URL, CHAIN_ID));
const { address, receipt } = await deployContract(wallet, abi, bytecode, args);
console.log(JSON.stringify({ contractAddress: address, txHash: receipt.hash, status: receipt.status }, null, 2));
}
main().catch((e) => {
console.error(e);
process.exit(1);
});

Hardhat (solo contratos)

En esta etapa, necesitaremos una configuración mínima para compilar Counter.sol

hardhat.config.ts

import type { HardhatUserConfig } from "hardhat/config";
const config: HardhatUserConfig = {
solidity: {
version: "0.8.24",
settings: {
optimizer: { enabled: true, runs: 200 }
}
},
paths: {
sources: "./contracts",
artifacts: "./artifacts",
cache: "./cache"
}
};
export default config;

Es importante notar que la compilación local es opcional, por lo que puedes omitirla si lo deseas. El siguiente paso es una elección: eliminar el archivo contracts/Lock.sol existente o puedes actualizarlo a Solidity versión 0.8.24.

npx hardhat compile

Contenerizar

Este es un paso esencial. Aquí, necesitas un Dockerfile que construya TS y compile el contrato. El archivo también ejecutará la prueba de humo una vez, y luego permanecerá inactivo mientras inspeccionas los registros.

Dockerfile

FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci
COPY tsconfig.json ./
COPY src ./src
COPY contracts ./contracts
COPY hardhat.config.ts ./
RUN npm run build && npx hardhat compile && npm prune --omit=dev
ENV NODE_ENV=production
CMD ["sh", "-c", "node dist/scripts/smoke-test.js || true; tail -f /dev/null"]

A continuación, debes montar el socket appd proporcionado por ROFL. Ten la seguridad de que no se exponen puertos públicos en el proceso.

compose.yaml

services:
demo:
image: docker.io/YOURUSER/rofl-keygen:0.1.0
platform: linux/amd64
environment:
- KEY_ID=${KEY_ID:-evm:base:sepolia}
- BASE_RPC_URL=${BASE_RPC_URL:-https://sepolia.base.org}
- BASE_CHAIN_ID=${BASE_CHAIN_ID:-84532}
volumes:
- /run/rofl-appd.sock:/run/rofl-appd.sock

Construir la imagen

Es importante recordar que ROFL solo se ejecuta en hardware habilitado con Intel TDX. Entonces, si estás compilando imágenes en un host diferente, como macOS, pasar el parámetro — platform linux/amd64 es un paso adicional esencial.

docker buildx build --platform linux/amd64 \
-t docker.io/YOURUSER/rofl-keygen:0.1.0 --push .

Un punto interesante a tener en cuenta aquí es que puedes optar por seguridad y verificabilidad adicionales. Solo necesitas fijar el digest y usar image: …@sha256:… en compose.yaml.

Construir paquete ROFL

Hay un paso que debes tomar antes de ejecutar el comando oasis rofl build. Dado que la construcción del segmento de imagen viene después de la contenerización, necesitarás actualizar services.demo.image en compose.yaml a la imagen que construiste.
Para proyectos TypeScript simples, como este, a veces existe la posibilidad de que el tamaño de la imagen sea mayor de lo anticipado. Por lo tanto, es aconsejable actualizar la sección resources de rofl.yaml a al menos: memory: 1024 y storage.size: 4096.
Ahora, estás listo.

oasis rofl build

Puedes publicar las identidades del enclave y la configuración a continuación.

oasis rofl update

Implementar

Este es un paso bastante sencillo donde implementas en un proveedor de Testnet.

oasis rofl deploy

Extremo a extremo (Base Sepolia)

Este es un proceso de 2 pasos, aunque el segundo paso es opcional.
Primero, ves los registros de la prueba de humo.

oasis rofl machine logs

Si has completado todos los pasos hasta ahora correctamente, verás en la salida:

  • App ID
  • Dirección EVM y un mensaje firmado
  • Un aviso para financiar la dirección
  • Una vez que se complete el financiamiento, una implementación de Counter.sol

A continuación, desarrollo local. Aquí, necesitas ejecutar npm run build:all para compilar el código TypeScript y el contrato Solidity. Omite este paso si no es necesario.

export ALLOW_LOCAL_DEV=true
export LOCAL_DEV_SK=0x<64-hex-dev-secret-key> # NO USAR EN PRODUCCIÓN
npm run smoke-test

Seguridad y notas para recordar

  • Los registros del proveedor no están cifrados en reposo. Por lo tanto, nunca registres claves secretas.
  • El socket appd /run/rofl-appd.sock existe solo dentro de ROFL.
  • Puede haber límites de velocidad en RPCs públicos. Por lo tanto, es aconsejable optar por una URL RPC dedicada de Base.

Hay una demostración de generación de claves en el GitHub de Oasis, que puedes consultar como ejemplo de este tutorial. https://github.com/oasisprotocol/demo-rofl-keygen

Ahora que has generado exitosamente una clave en ROFL con appd, firmado mensajes, implementado un contrato y movido ETH en Base Sepolia, déjanos saber en la sección de comentarios tus comentarios. Para una charla rápida con el equipo de ingeniería de Oasis para ayuda con problemas específicos, puedes dejar tus comentarios en el canal dev-central en el Discord oficial.

Publicado originalmente en https://dev.to el 20 de febrero de 2026.


Guía para la generación de claves Cross-Chain (EVM / Base) con Oasis ROFL fue publicado originalmente en Coinmonks en Medium, donde las personas continúan la conversación destacando y respondiendo a esta historia.

Oportunidad de mercado
Logo de CROSS
Precio de CROSS(CROSS)
$0.06059
$0.06059$0.06059
-0.63%
USD
Gráfico de precios en vivo de CROSS (CROSS)
Aviso legal: Los artículos republicados en este sitio provienen de plataformas públicas y se ofrecen únicamente con fines informativos. No reflejan necesariamente la opinión de MEXC. Todos los derechos pertenecen a los autores originales. Si consideras que algún contenido infringe derechos de terceros, comunícate a la dirección crypto.news@mexc.com para solicitar su eliminación. MEXC no garantiza la exactitud, la integridad ni la actualidad del contenido y no se responsabiliza por acciones tomadas en función de la información proporcionada. El contenido no constituye asesoría financiera, legal ni profesional, ni debe interpretarse como recomendación o respaldo por parte de MEXC.