Oasis a introduit le framework pour la logique off-chain d'exécution (ROFL) afin d'aider à créer et exécuter des applications off-chain tout en garantissant la confidentialité et en maintenant la confiance avec on-chainOasis a introduit le framework pour la logique off-chain d'exécution (ROFL) afin d'aider à créer et exécuter des applications off-chain tout en garantissant la confidentialité et en maintenant la confiance avec on-chain

Guide de génération de clés cross-chain (EVM / Base) avec Oasis ROFL

2026/02/20 21:16
Temps de lecture : 11 min

Oasis a introduit le framework pour la logique off-chain d'exécution (ROFL) afin d'aider à construire et exécuter des applications off-chain tout en garantissant la confidentialité et en maintenant la confiance grâce à la vérifiabilité on-chain. La construction avec ROFL comporte de nombreux éléments mobiles.
Dans ce tutoriel, je vais démontrer comment créer une petite application TypeScript, générant une clé secp256k1 dans ROFL. Elle utilisera le SDK TypeScript @oasisprotocol/rofl-client, qui communique avec l'API REST appd en arrière-plan. L'application TypeScript pourra également :

Il y aura un simple test de smoke qui affiche dans les journaux.

Prérequis

Pour effectuer les étapes décrites dans ce guide, vous aurez besoin de :

  • Node.js 20+ et Docker (ou Podman)
  • Oasis CLI et un minimum de 120 tokens TEST dans votre portefeuille (faucet Oasis Testnet)
  • Quelques ETH de test Base Sepolia (faucet Base Sepolia)

Pour les détails de configuration, veuillez vous référer à la documentation sur les prérequis du démarrage rapide.

Initialiser l'application

La première étape consiste à initialiser une nouvelle application à l'aide de l'Oasis CLI.

oasis rofl init rofl-keygen
cd rofl-keygen

Créer l'application

Au moment de créer l'application sur le Testnet, vous devrez déposer des tokens. Assignez 100 tokens TEST à ce stade.

oasis rofl create --network testnet

En sortie, le CLI produira l'App ID, indiqué par rofl1….

Initialiser un projet Hardhat (TypeScript)

Maintenant, vous êtes prêt à lancer le projet.

npx hardhat init

Puisque nous présentons une application TypeScript, choisissez TypeScript lorsque vous y êtes invité, puis acceptez les valeurs par défaut.
L'étape suivante consisterait à ajouter les petites dépendances d'exécution pour une utilisation en dehors de Hardhat.

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

Le modèle TypeScript de Hardhat crée automatiquement un tsconfig.json. Nous devons ajouter un petit script pour que le code de l'application puisse se compiler dans dist/.

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

Structure de l'application

Dans cette section, nous ajouterons quelques petits fichiers TS et un contrat Solidity.

src/
├── appd.ts # wrapper mince sur @oasisprotocol/rofl-client
├── evm.ts # assistants ethers (fournisseur, portefeuille, tx, déploiement)
├── keys.ts # petits assistants (somme de contrôle)
└── scripts/
├── deploy-contract.ts # script de déploiement générique pour les artefacts compilés
└── smoke-test.ts # démo de bout en bout (journaux)
contracts/
└── Counter.sol # contrat d'exemple

  1. src/appd.ts — wrapper mince sur le SDK Ici, vous devrez utiliser le client officiel pour communiquer avec appd (socket UNIX). Nous devrons également conserver un repli local-dev explicite lors de l'exécution en dehors 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();
}
/**
* Génère (ou re-dérive de manière déterministe) une clé secp256k1 dans ROFL et
* la renvoie sous forme de chaîne hexadécimale préfixée par 0x (pour ethers.js Wallet).
*
* Développement local UNIQUEMENT (en dehors de ROFL) : Si le socket est manquant et que vous définissez
* ALLOW_LOCAL_DEV=true et LOCAL_DEV_SK=0x<64-hex>, cette valeur est utilisée.
*/
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 introuvable et aucun LOCAL_DEV_SK fourni (dev uniquement).'
);
}

2. src/evm.ts — assistants 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("Transaction abandonnée ou remplacée avant confirmation");
}
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 déploiement non minée");
}
return { address: contract.target as string, receipt };
}

3. src/keys.ts — petits assistants

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 — flux de bout en bout unique

Il s'agit d'une étape importante car ce script a plusieurs fonctions :

  • afficher l'App ID (dans ROFL), l'adresse et un message signé
  • attendre le financement
  • déployer le contrat 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(`En attente du financement... solde actuel=${formatEther(bal)} ETH`);
await sleep(pollMs);
}
throw new Error("Délai d'attente dépassé pour le financement.");
}
async function main() {
const appId = await getAppId().catch(() => null);
console.log(`ROFL App ID: ${appId ?? "(non disponible en dehors de ROFL)"}`);
const sk = await getEvmSecretKey(KEY_ID);
// NOTE : Cette démo fait confiance au fournisseur RPC configuré. Pour la production, préférez un
// client léger (par exemple, Helios) afin de pouvoir vérifier l'état de la chaîne distante.
const wallet = secretKeyToWallet(sk).connect(makeProvider(RPC_URL, CHAIN_ID));
const addr = checksumAddress(await wallet.getAddress());
console.log(`Adresse EVM (Base Sepolia): ${addr}`);
const msg = "hello from rofl";
const sig = await signPersonalMessage(wallet, msg);
console.log(`Message signé : "${msg}"`);
console.log(`Signature : ${sig}`);
const provider = wallet.provider as JsonRpcProvider;
let bal = await provider.getBalance(addr);
if (bal === 0n) {
console.log("Veuillez financer l'adresse ci-dessus avec de l'ETH Base Sepolia pour continuer.");
bal = await waitForFunding(provider, addr);
}
console.log(`Solde détecté : ${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("Artefact Counter manquant abi/bytecode");
}
const { address: contractAddress, receipt: deployRcpt } =
await deployContract(wallet, artifact.abi, artifact.bytecode, []);
console.log(`Counter déployé à ${contractAddress} (tx=${deployRcpt.hash})`);
console.log("Test de smoke terminé avec succès !");
}
main().catch((e) => {
console.error(e);
process.exit(1);
});

5. contracts/Counter.sol — exemple minimal

// 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 — déployeur générique

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");
/**
* Utilisation :
* npm run deploy-contract -- ./artifacts/MyContract.json '[arg0, arg1]'
* L'artefact doit contenir { abi, bytecode }.
*/
async function main() {
const [artifactPath, ctorJson = "[]"] = process.argv.slice(2);
if (!artifactPath) {
console.error("Utilisation : 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("L'artefact doit contenir { abi, bytecode }");
}
let args: unknown[];
try {
args = JSON.parse(ctorJson);
if (!Array.isArray(args)) throw new Error("les arguments du constructeur doivent être un tableau JSON");
} catch (e) {
throw new Error(`Échec de l'analyse des arguments du constructeur JSON : ${String(e)}`);
}
const sk = await getEvmSecretKey(KEY_ID);
// NOTE : Cette démo fait confiance au fournisseur RPC configuré. Pour la production, préférez un
// client léger (par exemple, Helios) afin de pouvoir vérifier l'état de la chaîne distante.
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 (contrats uniquement)

À ce stade, nous aurons besoin d'une configuration minimale pour compiler 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;

Point à noter : la compilation locale est facultative, vous pouvez donc l'ignorer si vous le souhaitez. L'étape suivante est un choix — soit supprimer le fichier contracts/Lock.sol existant, soit le mettre à jour vers Solidity version 0.8.24.

npx hardhat compile

Conteneurisation

Il s'agit d'une étape essentielle. Ici, vous avez besoin d'un Dockerfile qui construit TS et compile le contrat. Le fichier exécutera également le test de smoke une fois, puis restera inactif pendant que vous inspectez les journaux.

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

Ensuite, vous devez monter le socket appd fourni par ROFL. Rassurez-vous, aucun port public n'est exposé dans le processus.

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

Construire l'image

Il est important de se rappeler que ROFL ne fonctionne que sur du matériel compatible Intel TDX. Donc, si vous compilez des images sur un hôte différent, tel que macOS, le passage du paramètre — platform linux/amd64 est une étape supplémentaire essentielle.

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

Un point intéressant à noter ici est que vous pouvez opter pour une sécurité et une vérifiabilité supplémentaires. Il vous suffit d'épingler le digest et d'utiliser image: …@sha256:… dans compose.yaml.

Construire le bundle ROFL

Il y a une étape que vous devez effectuer avant d'exécuter la commande oasis rofl build. Puisque la construction du segment d'image vient après la conteneurisation, vous devrez mettre à jour services.demo.image dans compose.yaml vers l'image que vous avez construite.
Pour les projets TypeScript simples, comme celui-ci, il existe parfois une possibilité que la taille de l'image soit plus grande que prévu. Il est donc conseillé de mettre à jour la section resources de rofl.yaml au moins vers : memory: 1024 et storage.size: 4096.
Maintenant, vous êtes prêt.

oasis rofl build

Vous pouvez ensuite publier les identités de l'enclave et la configuration.

oasis rofl update

Déploiement

Il s'agit d'une étape assez simple où vous déployez vers un fournisseur Testnet.

oasis rofl deploy

De bout en bout (Base Sepolia)

Il s'agit d'un processus en 2 étapes, bien que la deuxième étape soit facultative.
Tout d'abord, vous visualisez les journaux du test de smoke.

oasis rofl machine logs

Si vous avez correctement effectué toutes les étapes jusqu'à présent, vous verrez dans la sortie :

  • App ID
  • Adresse EVM et un message signé
  • Une invite pour financer l'adresse
  • Une fois le financement effectué, un déploiement Counter.sol

Ensuite, dev local. Ici, vous devez exécuter npm run build:all pour compiler le code TypeScript et le contrat Solidity. Ignorez cette étape si elle n'est pas nécessaire.

export ALLOW_LOCAL_DEV=true
export LOCAL_DEV_SK=0x<64-hex-dev-secret-key> # NE PAS UTILISER EN PROD
npm run smoke-test

Sécurité et notes à retenir

  • Les journaux du fournisseur ne sont pas chiffrés au repos. Donc, ne jamais enregistrer les clés secrètes.
  • Le socket appd /run/rofl-appd.sock existe uniquement dans ROFL.
  • Il peut y avoir des limites de débit dans les RPC publics. Il est donc conseillé d'opter pour une URL RPC Base dédiée.

Il existe une démo de génération de clés dans le GitHub Oasis, que vous pouvez consulter comme exemple de ce tutoriel. https://github.com/oasisprotocol/demo-rofl-keygen

Maintenant que vous avez réussi à générer une clé dans ROFL avec appd, signé des messages, déployé un contrat et déplacé de l'ETH sur Base Sepolia, faites-nous part de vos commentaires dans la section des commentaires. Pour une discussion rapide avec l'équipe d'ingénierie Oasis pour obtenir de l'aide sur des problèmes spécifiques, vous pouvez déposer vos commentaires dans le canal dev-central sur le Discord officiel.

Publié à l'origine sur https://dev.to le 20 février 2026.


Guide To Cross-Chain Key Generation (EVM / Base) With Oasis ROFL a été publié à l'origine dans Coinmonks sur Medium, où les gens poursuivent la conversation en mettant en évidence et en répondant à cette histoire.

Opportunité de marché
Logo de CROSS
Cours CROSS(CROSS)
$0.10938
$0.10938$0.10938
+1.11%
USD
Graphique du prix de CROSS (CROSS) en temps réel
Clause de non-responsabilité : les articles republiés sur ce site proviennent de plateformes publiques et sont fournis à titre informatif uniquement. Ils ne reflètent pas nécessairement les opinions de MEXC. Tous les droits restent la propriété des auteurs d'origine. Si vous estimez qu'un contenu porte atteinte aux droits d'un tiers, veuillez contacter service@support.mexc.com pour demander sa suppression. MEXC ne garantit ni l'exactitude, ni l'exhaustivité, ni l'actualité des contenus, et décline toute responsabilité quant aux actions entreprises sur la base des informations fournies. Ces contenus ne constituent pas des conseils financiers, juridiques ou professionnels, et ne doivent pas être interprétés comme une recommandation ou une approbation de la part de MEXC.