A Oasis introduziu a estrutura para lógica off-chain de tempo de execução (ROFL) para ajudar a construir e executar aplicações off-chain, garantindo privacidade e mantendo a confiança com on-chainA Oasis introduziu a estrutura para lógica off-chain de tempo de execução (ROFL) para ajudar a construir e executar aplicações off-chain, garantindo privacidade e mantendo a confiança com on-chain

Guia para Geração de Chaves Cross-Chain (EVM / Base) com Oasis ROFL

2026/02/20 21:16
Leu 10 min
Para enviar feedbacks ou expressar preocupações a respeito deste conteúdo, contate-nos em crypto.news@mexc.com

A Oasis introduziu a estrutura para lógica off-chain de runtime (ROFL) para ajudar a construir e executar aplicações off-chain, garantindo privacidade e mantendo confiança com verificabilidade on-chain. Existem muitas partes móveis na construção com ROFL.
Neste tutorial, vou demonstrar como construir uma pequena aplicação TypeScript, gerando uma chave secp256k1 dentro do ROFL. Usará o @oasisprotocol/rofl-client TypeScript SDK, que comunica com a appd REST API nos bastidores. A aplicação TypeScript também irá:

Haverá um simples smoke test que imprime nos registos.

Pré-requisitos

Para realizar os passos descritos neste guia, vai precisar de:

  • Node.js 20+ e Docker (ou Podman)
  • Oasis CLI e um mínimo de 120 tokens TEST na sua carteira (faucet da Testnet Oasis)
  • Algum ETH de teste Base Sepiola (faucet da Base Sepiola)

Para os detalhes de configuração, consulte a documentação sobre Pré-requisitos do Início Rápido.

Inicializar App

O primeiro passo é inicializar uma nova aplicação usando a Oasis CLI.

oasis rofl init rofl-keygen
cd rofl-keygen

Criar App

No momento de criar a aplicação na Testnet, será necessário depositar tokens. Atribua 100 tokens TEST neste ponto.

oasis rofl create --network testnet

Como resultado, a CLI produzirá o App ID, indicado por rofl1….

Inicializar um projeto Hardhat (TypeScript)

Agora, está pronto para iniciar o projeto.

npx hardhat init

Como estamos a apresentar uma aplicação TypeScript, escolha TypeScript quando solicitado e, em seguida, aceite as predefinições.
O próximo passo seria adicionar as pequenas dependências de runtime para uso fora do Hardhat.

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

O modelo TypeScript do Hardhat cria automaticamente um tsconfig.json. Precisamos adicionar um pequeno script para que o código da aplicação possa ser compilado para dist/.

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

Estrutura da App

Nesta secção, vamos adicionar alguns pequenos ficheiros TS e um contrato Solidity.

src/
├── appd.ts # wrapper fino sobre @oasisprotocol/rofl-client
├── evm.ts # auxiliares ethers (provider, wallet, tx, deploy)
├── keys.ts # pequenos auxiliares (checksum)
└── scripts/
├── deploy-contract.ts # script de implementação genérico para artefactos compilados
└── smoke-test.ts # demo end-to-end (registos)
contracts/
└── Counter.sol # contrato de amostra

  1. src/appd.ts — wrapper fino sobre o SDK Aqui, vai precisar de usar o cliente oficial para comunicar com appd (UNIX socket). Também precisaremos manter um fallback local‑dev explícito ao executar fora do 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();
}
/**
* Gera (ou re-deriva deterministicamente) uma chave secp256k1 dentro do ROFL e
* devolve-a como uma string hex com prefixo 0x (para ethers.js Wallet).
*
* Desenvolvimento local APENAS (fora do ROFL): Se o socket estiver em falta e definir
* ALLOW_LOCAL_DEV=true e LOCAL_DEV_SK=0x<64-hex>, esse valor é usado.
*/
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 não encontrado e nenhum LOCAL_DEV_SK fornecido (apenas dev).'
);
}

2. src/evm.ts — auxiliares 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("Transação abandonada ou substituída antes da confirmação");
}
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 implementação não minerada");
}
return { address: contract.target as string, receipt };
}

3. src/keys.ts — pequenos auxiliares

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 — fluxo único end‑to‑end

Este é um passo importante, pois este script tem múltiplas funções:

  • imprimir o App ID (dentro do ROFL), endereço e uma mensagem assinada
  • aguardar financiamento
  • implementar o contrato counter

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(`A aguardar financiamento... saldo atual=${formatEther(bal)} ETH`);
await sleep(pollMs);
}
throw new Error("Tempo esgotado ao aguardar financiamento.");
}
async function main() {
const appId = await getAppId().catch(() => null);
console.log(`ROFL App ID: ${appId ?? "(indisponível fora do ROFL)"}`);
const sk = await getEvmSecretKey(KEY_ID);
// NOTA: Esta demo confia no provedor RPC configurado. Para produção, prefira um
// light client (por exemplo, Helios) para que possa verificar o estado da chain remota.
const wallet = secretKeyToWallet(sk).connect(makeProvider(RPC_URL, CHAIN_ID));
const addr = checksumAddress(await wallet.getAddress());
console.log(`Endereço EVM (Base Sepolia): ${addr}`);
const msg = "hello from rofl";
const sig = await signPersonalMessage(wallet, msg);
console.log(`Mensagem assinada: "${msg}"`);
console.log(`Assinatura: ${sig}`);
const provider = wallet.provider as JsonRpcProvider;
let bal = await provider.getBalance(addr);
if (bal === 0n) {
console.log("Por favor, financie o endereço acima com ETH da Base Sepolia para continuar.");
bal = await waitForFunding(provider, addr);
}
console.log(`Saldo detetado: ${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 em falta abi/bytecode");
}
const { address: contractAddress, receipt: deployRcpt } =
await deployContract(wallet, artifact.abi, artifact.bytecode, []);
console.log(`Counter implementado em ${contractAddress} (tx=${deployRcpt.hash})`);
console.log("Smoke test concluído com sucesso!");
}
main().catch((e) => {
console.error(e);
process.exit(1);
});

5. contracts/Counter.sol — amostra 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");
/**
* Utilização:
* npm run deploy-contract -- ./artifacts/MyContract.json '[arg0, arg1]'
* O artefacto deve conter { abi, bytecode }.
*/
async function main() {
const [artifactPath, ctorJson = "[]"] = process.argv.slice(2);
if (!artifactPath) {
console.error("Utilização: 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("Artefacto deve conter { abi, bytecode }");
}
let args: unknown[];
try {
args = JSON.parse(ctorJson);
if (!Array.isArray(args)) throw new Error("argumentos do construtor devem ser um array JSON");
} catch (e) {
throw new Error(`Falha ao analisar argumentos do construtor JSON: ${String(e)}`);
}
const sk = await getEvmSecretKey(KEY_ID);
// NOTA: Esta demo confia no provedor RPC configurado. Para produção, prefira um
// light client (por exemplo, Helios) para que possa verificar o estado da chain 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 (apenas contratos)

Nesta fase, precisaremos de configuração 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;

Ponto a observar é que a compilação local é opcional, pelo que pode ignorá-la se desejar. O próximo passo é uma escolha — elimine o ficheiro contracts/Lock.sol existente ou pode atualizá-lo para Solidity versão 0.8.24.

npx hardhat compile

Containerizar

Este é um passo essencial. Aqui, precisa de um Dockerfile que construa TS e compile o contrato. O ficheiro também executará o smoke test uma vez e depois ficará inativo enquanto inspecciona os registos.

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"]

Em seguida, deve montar o appd socket fornecido pelo ROFL. Fique tranquilo de que nenhuma porta pública é exposta no processo.

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 a imagem

É importante lembrar que o ROFL apenas é executado em hardware com Intel TDX ativado. Portanto, se estiver a compilar imagens num host diferente, como macOS, passar o parâmetro — platform linux/amd64 é um passo extra essencial.

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

Um ponto interessante a observar aqui é que pode optar por segurança e verificabilidade extra. Apenas precisa fixar o digest e usar image: …@sha256:… em compose.yaml.

Construir pacote ROFL

Existe um passo que deve executar antes de executar o comando oasis rofl build. Como a construção do segmento de imagem vem após a containerização, precisará atualizar services.demo.image em compose.yaml para a imagem que construiu.
Para projetos TypeScript simples, como este, existe por vezes a possibilidade de o tamanho da imagem ser maior do que o previsto. É assim aconselhável atualizar a secção resources de rofl.yaml para pelo menos: memory: 1024 e storage.size: 4096.
Agora, está pronto.

oasis rofl build

Pode em seguida publicar as identidades do enclave e config.

oasis rofl update

Implementar

Este é um passo suficientemente fácil onde implementa para um provedor Testnet.

oasis rofl deploy

End‑to‑end (Base Sepolia)

Este é um processo de 2 passos, embora o segundo passo seja opcional.
Primeiro, visualiza os registos do smoke‑test.

oasis rofl machine logs

Se concluiu todos os passos até agora corretamente, verá no resultado:

  • App ID
  • Endereço EVM e uma mensagem assinada
  • Um pedido para financiar o endereço
  • Depois de concluído o financiamento, uma implementação Counter.sol

Em seguida, dev local. Aqui, precisa executar npm run build:all para compilar o código TypeScript e o contrato Solidity. Ignore este passo se não for necessário.

export ALLOW_LOCAL_DEV=true
export LOCAL_DEV_SK=0x<64-hex-dev-secret-key> # NÃO USAR EM PRODUÇÃO
npm run smoke-test

Segurança & notas a lembrar

  • Os registos do provedor não são encriptados em repouso. Portanto, nunca registe chaves secretas.
  • O socket appd /run/rofl-appd.sock existe apenas dentro do ROFL.
  • Pode haver limites de taxa em RPCs públicos. Portanto, é aconselhável optar por um URL RPC Base dedicado.

Existe uma demo de geração de chaves no GitHub da Oasis, que pode consultar como exemplo deste tutorial. https://github.com/oasisprotocol/demo-rofl-keygen

Agora que gerou com sucesso uma chave no ROFL com appd, assinou mensagens, implementou um contrato e moveu ETH na Base Sepolia, informe-nos na secção de comentários o seu feedback. Para uma conversa rápida com a equipa de engenharia da Oasis para ajuda com questões específicas, pode deixar os seus comentários no canal dev-central no Discord oficial.

Originalmente publicado em https://dev.to a 20 de fevereiro de 2026.


Guide To Cross-Chain Key Generation (EVM / Base) With Oasis ROFL foi originalmente publicado em Coinmonks no Medium, onde as pessoas continuam a conversa ao destacar e responder a esta história.

Oportunidade de mercado
Logo de CROSS
Cotação CROSS (CROSS)
$0.09469
$0.09469$0.09469
+0.13%
USD
Gráfico de preço em tempo real de CROSS (CROSS)
Isenção de responsabilidade: Os artigos republicados neste site são provenientes de plataformas públicas e são fornecidos apenas para fins informativos. Eles não refletem necessariamente a opinião da MEXC. Todos os direitos permanecem com os autores originais. Se você acredita que algum conteúdo infringe direitos de terceiros, entre em contato pelo e-mail crypto.news@mexc.com para solicitar a remoção. A MEXC não oferece garantias quanto à precisão, integridade ou atualidade das informações e não se responsabiliza por quaisquer ações tomadas com base no conteúdo fornecido. O conteúdo não constitui aconselhamento financeiro, jurídico ou profissional, nem deve ser considerado uma recomendação ou endosso por parte da MEXC.

USD1 Genesis: 0 Fees + 12% APR

USD1 Genesis: 0 Fees + 12% APRUSD1 Genesis: 0 Fees + 12% APR

New users: stake for up to 600% APR. Limited time!