Oasis đã giới thiệu framework cho runtime off-chain logic (ROFL) để giúp xây dựng và chạy các ứng dụng off-chain trong khi vẫn đảm bảo quyền riêng tư và duy trì sự tin cậy với on-chainOasis đã giới thiệu framework cho runtime off-chain logic (ROFL) để giúp xây dựng và chạy các ứng dụng off-chain trong khi vẫn đảm bảo quyền riêng tư và duy trì sự tin cậy với on-chain

Hướng Dẫn Tạo Khóa Cross-Chain (EVM / Base) Với Oasis ROFL

2026/02/20 21:16
Đọc trong 11 phút
Đối với phản hồi hoặc thắc mắc liên quan đến nội dung này, vui lòng liên hệ với chúng tôi qua crypto.news@mexc.com

Oasis đã giới thiệu framework cho runtime off-chain logic (ROFL) để giúp xây dựng và chạy các ứng dụng off-chain trong khi đảm bảo quyền riêng tư và duy trì sự tin cậy với khả năng xác minh on-chain. Có nhiều phần di động khi xây dựng với ROFL.
Trong hướng dẫn này, tôi sẽ trình bày cách xây dựng một ứng dụng TypeScript nhỏ, tạo khóa secp256k1 bên trong ROFL. Nó sẽ sử dụng @oasisprotocol/rofl-client TypeScript SDK, giao tiếp với appd REST API ẩn bên dưới. Ứng dụng TypeScript cũng sẽ:

Sẽ có một smoke test đơn giản in ra logs.

Điều kiện tiên quyết

Để thực hiện các bước được mô tả trong hướng dẫn này, bạn sẽ cần:

  • Node.js 20+Docker (hoặc Podman)
  • Oasis CLI và tối thiểu 120 TEST token trong ví của bạn (vòi Oasis Testnet)
  • Một ít Base Sepiola test ETH (vòi Base Sepiola)

Để biết chi tiết về thiết lập, vui lòng tham khảo tài liệu về Điều kiện tiên quyết Khởi động nhanh.

Khởi tạo App

Bước đầu tiên là khởi tạo một ứng dụng mới bằng Oasis CLI.

oasis rofl init rofl-keygen
cd rofl-keygen

Tạo App

Tại thời điểm tạo ứng dụng trên Testnet, bạn sẽ được yêu cầu nạp token. Phân bổ 100 TEST token tại thời điểm này.

oasis rofl create --network testnet

Ở đầu ra, CLI sẽ tạo ra App ID, được ký hiệu là rofl1….

Khởi tạo dự án Hardhat (TypeScript)

Bây giờ, bạn đã sẵn sàng khởi động dự án.

npx hardhat init

Vì chúng ta đang giới thiệu một ứng dụng TypeScript, hãy chọn TypeScript khi được nhắc, sau đó chấp nhận các mặc định.
Bước tiếp theo sẽ là thêm các runtime deps nhỏ để sử dụng bên ngoài Hardhat.

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

Template TypeScript của Hardhat tự động tạo ra tsconfig.json. Chúng ta cần thêm một script nhỏ để mã ứng dụng có thể biên dịch sang dist/.

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

Cấu trúc App

Trong phần này, chúng ta sẽ thêm một vài file TS nhỏ và một hợp đồng Solidity.

src/
├── appd.ts # thin wrapper over @oasisprotocol/rofl-client
├── evm.ts # ethers helpers (provider, wallet, tx, deploy)
├── keys.ts # tiny helpers (checksum)
└── scripts/
├── deploy-contract.ts # generic deploy script for compiled artifacts
└── smoke-test.ts # end-to-end demo (logs)
contracts/
└── Counter.sol # sample contract

  1. src/appd.ts — thin wrapper over the SDK Ở đây, bạn sẽ cần sử dụng client chính thức để giao tiếp với appd (UNIX socket). Chúng ta cũng sẽ cần giữ một local‑dev fallback rõ ràng khi chạy bên ngoài 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();
}
/**
* Generates (or deterministically re-derives) a secp256k1 key inside ROFL and
* returns it as a 0x-prefixed hex string (for ethers.js Wallet).
*
* Local development ONLY (outside ROFL): If the socket is missing and you set
* ALLOW_LOCAL_DEV=true and LOCAL_DEV_SK=0x<64-hex>, that value is used.
*/
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(
'rofl-appd socket not found and no LOCAL_DEV_SK provided (dev only).'
);
}

2. src/evm.ts — ethers helpers

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 dropped or replaced before 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("Deployment TX not mined");
}
return { address: contract.target as string, receipt };
}

3. src/keys.ts — tiny helpers

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 — single end‑to‑end flow

Đây là một bước quan trọng vì script này có nhiều chức năng:

  • in App ID (bên trong ROFL), địa chỉ và một tin nhắn đã ký
  • chờ tài trợ
  • triển khai counter contract

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(`Waiting for funding... current balance=${formatEther(bal)} ETH`);
await sleep(pollMs);
}
throw new Error("Timed out waiting for funding.");
}
async function main() {
const appId = await getAppId().catch(() => null);
console.log(`ROFL App ID: ${appId ?? "(unavailable outside ROFL)"}`);
const sk = await getEvmSecretKey(KEY_ID);
// NOTE: This demo trusts the configured RPC provider. For production, prefer a
// light client (for example, Helios) so you can verify remote chain state.
const wallet = secretKeyToWallet(sk).connect(makeProvider(RPC_URL, CHAIN_ID));
const addr = checksumAddress(await wallet.getAddress());
console.log(`EVM address (Base Sepolia): ${addr}`);
const msg = "hello from rofl";
const sig = await signPersonalMessage(wallet, msg);
console.log(`Signed message: "${msg}"`);
console.log(`Signature: ${sig}`);
const provider = wallet.provider as JsonRpcProvider;
let bal = await provider.getBalance(addr);
if (bal === 0n) {
console.log("Please fund the above address with Base Sepolia ETH to continue.");
bal = await waitForFunding(provider, addr);
}
console.log(`Balance detected: ${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("Counter artifact missing abi/bytecode");
}
const { address: contractAddress, receipt: deployRcpt } =
await deployContract(wallet, artifact.abi, artifact.bytecode, []);
console.log(`Deployed Counter at ${contractAddress} (tx=${deployRcpt.hash})`);
console.log("Smoke test completed successfully!");
}
main().catch((e) => {
console.error(e);
process.exit(1);
});

5. contracts/Counter.sol — minimal sample

// 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 — generic deployer

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");
/**
* Usage:
* npm run deploy-contract -- ./artifacts/MyContract.json '[arg0, arg1]'
* The artifact must contain { abi, bytecode }.
*/
async function main() {
const [artifactPath, ctorJson = "[]"] = process.argv.slice(2);
if (!artifactPath) {
console.error("Usage: 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("Artifact must contain { abi, bytecode }");
}
let args: unknown[];
try {
args = JSON.parse(ctorJson);
if (!Array.isArray(args)) throw new Error("constructor args must be a JSON array");
} catch (e) {
throw new Error(`Failed to parse constructor args JSON: ${String(e)}`);
}
const sk = await getEvmSecretKey(KEY_ID);
// NOTE: This demo trusts the configured RPC provider. For production, prefer a
// light client (for example, Helios) so you can verify remote chain state.
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 (chỉ hợp đồng)

Ở giai đoạn này, chúng ta sẽ cần cấu hình tối thiểu để biên dịch 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;

Điểm cần lưu ý là biên dịch cục bộ là tùy chọn, vì vậy bạn có thể bỏ qua nếu muốn. Bước tiếp theo là một lựa chọn — xóa file contracts/Lock.sol hiện có hoặc bạn có thể cập nhật nó lên Solidity version 0.8.24.

npx hardhat compile

Containerize

Đây là một bước thiết yếu. Ở đây, bạn cần một Dockerfile xây dựng TS và biên dịch hợp đồng. File này cũng sẽ chạy smoke test một lần, sau đó đứng yên trong khi bạn kiểm tra logs.

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

Tiếp theo, bạn phải mount appd socket được cung cấp bởi ROFL. Yên tâm rằng không có cổng công khai nào được tiết lộ trong quá trình này.

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

Build image

Điều quan trọng cần nhớ là ROFL chỉ chạy trên phần cứng hỗ trợ Intel TDX. Vì vậy, nếu bạn đang biên dịch image trên máy chủ khác, chẳng hạn như macOS, thì việc truyền tham số — platform linux/amd64 là một bước bổ sung thiết yếu.

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

Một điểm thú vị cần lưu ý ở đây là bạn có thể chọn bảo mật và khả năng xác minh bổ sung. Bạn chỉ cần ghim digest và sử dụng image: …@sha256:… trong compose.yaml.

Build ROFL bundle

Có một bước mà bạn phải thực hiện trước khi chạy lệnh oasis rofl build. Vì xây dựng phân đoạn image đến sau quá trình containerization, bạn sẽ cần cập nhật services.demo.image trong compose.yaml thành image bạn đã xây dựng.
Đối với các dự án TypeScript đơn giản, như dự án này, đôi khi có khả năng kích thước image lớn hơn dự kiến. Do đó, nên cập nhật phần resources của rofl.yaml thành ít nhất: memory: 1024storage.size: 4096.
Bây giờ, bạn đã sẵn sàng.

oasis rofl build

Bạn có thể tiếp theo xuất bản các enclave identities và config.

oasis rofl update

Triển khai

Đây là một bước đủ dễ dàng để bạn triển khai lên nhà cung cấp Testnet.

oasis rofl deploy

End‑to‑end (Base Sepolia)

Đây là quy trình 2 bước, mặc dù bước thứ hai là tùy chọn.
Đầu tiên, bạn xem smoke‑test logs.

oasis rofl machine logs

Nếu bạn đã hoàn thành tất cả các bước cho đến nay một cách chính xác, bạn sẽ thấy trong đầu ra:

  • App ID
  • Địa chỉ EVM và một tin nhắn đã ký
  • Lời nhắc tài trợ địa chỉ
  • Sau khi tài trợ hoàn tất, một triển khai Counter.sol

Tiếp theo, local dev. Ở đây, bạn cần chạy npm run build:all để biên dịch mã TypeScript và hợp đồng Solidity. Bỏ qua bước này nếu không cần thiết.

export ALLOW_LOCAL_DEV=true
export LOCAL_DEV_SK=0x<64-hex-dev-secret-key> # DO NOT USE IN PROD
npm run smoke-test

Bảo mật & lưu ý cần nhớ

  • Logs của nhà cung cấp không được mã hóa khi lưu trữ. Vì vậy, không bao giờ ghi lại các khóa bí mật.
  • Appd socket /run/rofl-appd.sock chỉ tồn tại bên trong ROFL.
  • Có thể có giới hạn tốc độ trong các RPC công khai. Vì vậy, nên chọn URL RPC Base chuyên dụng.

Có một key generation demo trong Oasis GitHub, mà bạn có thể tham khảo như một ví dụ của hướng dẫn này. https://github.com/oasisprotocol/demo-rofl-keygen

Bây giờ bạn đã tạo thành công một khóa trong ROFL với appd, ký tin nhắn, triển khai hợp đồng và chuyển ETH trên Base Sepolia, hãy cho chúng tôi biết phản hồi của bạn trong phần bình luận. Để trò chuyện nhanh với đội ngũ kỹ thuật Oasis để được trợ giúp với các vấn đề cụ thể, bạn có thể để lại nhận xét của mình trong kênh dev-central trong Discord chính thức.

Được xuất bản ban đầu tại https://dev.to vào ngày 20 tháng 2 năm 2026.


Guide To Cross-Chain Key Generation (EVM / Base) With Oasis ROFL ban đầu được xuất bản trong Coinmonks trên Medium, nơi mọi người đang tiếp tục cuộc trò chuyện bằng cách làm nổi bật và phản hồi câu chuyện này.

Cơ hội thị trường
Logo CROSS
Giá CROSS(CROSS)
$0.09525
$0.09525$0.09525
-5.19%
USD
Biểu đồ giá CROSS (CROSS) theo thời gian thực
Tuyên bố miễn trừ trách nhiệm: Các bài viết được đăng lại trên trang này được lấy từ các nền tảng công khai và chỉ nhằm mục đích tham khảo. Các bài viết này không nhất thiết phản ánh quan điểm của MEXC. Mọi quyền sở hữu thuộc về tác giả gốc. Nếu bạn cho rằng bất kỳ nội dung nào vi phạm quyền của bên thứ ba, vui lòng liên hệ crypto.news@mexc.com để được gỡ bỏ. MEXC không đảm bảo về tính chính xác, đầy đủ hoặc kịp thời của các nội dung và không chịu trách nhiệm cho các hành động được thực hiện dựa trên thông tin cung cấp. Nội dung này không cấu thành lời khuyên tài chính, pháp lý hoặc chuyên môn khác, và cũng không được xem là khuyến nghị hoặc xác nhận từ MEXC.