Oasis 推出了運行時鏈下邏輯(ROFL)框架,用於協助在鏈下建立和運行應用程式,同時確保隱私性並維持與鏈上的信任Oasis 推出了運行時鏈下邏輯(ROFL)框架,用於協助在鏈下建立和運行應用程式,同時確保隱私性並維持與鏈上的信任

使用 Oasis ROFL 進行跨鏈金鑰生成指南(EVM / Base)

2026/02/20 21:16
閱讀時長 14 分鐘

Oasis 推出了運行時鏈下邏輯(ROFL)框架,協助建構和運行鏈下應用程式,同時確保隱私性並透過鏈上可驗證性維持信任。使用 ROFL 建構應用程式涉及許多運作環節。
在本教學中,我將示範如何建構一個小型 TypeScript 應用程式,在 ROFL 內部生成 secp256k1 金鑰。它將使用 @oasisprotocol/rofl-client TypeScript SDK,該 SDK 在底層與 appd REST API 進行通訊。此 TypeScript 應用程式還將:

將會有一個簡單的煙霧測試,將訊息輸出到日誌。

前置條件

要執行本指南中描述的步驟,您需要:

  • Node.js 20+Docker(或 Podman)
  • Oasis CLI 以及錢包中至少 120 個 TEST 代幣(Oasis Testnet 水龍頭)
  • 一些 Base Sepiola 測試 ETH(Base Sepiola 水龍頭)

有關設定詳細資訊,請參閱快速入門前置條件文件。

初始化應用程式

第一步是使用 Oasis CLI 初始化一個新應用程式。

oasis rofl init rofl-keygen
cd rofl-keygen

建立應用程式

在 Testnet 上建立應用程式時,您需要存入代幣。此時分配 100 個 TEST 代幣。

oasis rofl create --network testnet

作為輸出,CLI 將產生 App ID,以 rofl1…. 表示。

初始化 Hardhat(TypeScript)專案

現在,您已準備好啟動專案。

npx hardhat init

由於我們展示的是 TypeScript 應用程式,當提示時選擇 TypeScript,然後接受預設設定。
下一步是新增小型運行時依賴項以便在 Hardhat 之外使用。

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

Hardhat 的 TypeScript 範本會自動建立 tsconfig.json。我們需要新增一個小腳本,以便應用程式程式碼可以編譯到 dist/

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

應用程式結構

在本節中,我們將新增幾個小型 TS 檔案和一個 Solidity 合約。

src/
├── appd.ts # @oasisprotocol/rofl-client 的精簡包裝器
├── evm.ts # ethers 輔助工具(provider、wallet、tx、deploy)
├── keys.ts # 微型輔助工具(checksum)
└── scripts/
├── deploy-contract.ts # 已編譯產物的通用部署腳本
└── smoke-test.ts # 端到端示範(日誌)
contracts/
└── Counter.sol # 範例合約

  1. src/appd.ts — SDK 的精簡包裝器 在這裡,您需要使用官方客戶端與 appd(UNIX socket)進行通訊。我們還需要在 ROFL 外部運行時保留一個明確的本地開發備案

src/appd.ts

import {existsSync} from 'node:fs';
import {[[OPEN_3]]
RoflClient,
KeyKind,
ROFL_SOCKET_PATH
} from '@oasisprotocol/rofl-client';
const client = new RoflClient(); // UDS: /run/rofl-appd.sock
export async function getAppId(): Promise<string> {[[OPEN_4]]
return client.getAppId();
}
/**
* 在 ROFL 內部生成(或確定性地重新衍生)secp256k1 金鑰,並
* 將其作為 0x 前綴的十六進位字串返回(用於 ethers.js Wallet)。
*
* 僅限本地開發(ROFL 外部):如果 socket 遺失且您設定了
* ALLOW_LOCAL_DEV=true 和 LOCAL_DEV_SK=0x<64-hex>,則使用該值。
*/
export async function getEvmSecretKey(keyId: string): Promise<string> {[[OPEN_5]]
if (existsSync(ROFL_SOCKET_PATH)) {[[OPEN_6]]
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 輔助工具

import {[[OPEN_7]]
JsonRpcProvider,
Wallet,
parseEther,
type TransactionReceipt,
ContractFactory
} from "ethers";
export function makeProvider(rpcUrl: string, chainId: number) {[[OPEN_8]]
return new JsonRpcProvider(rpcUrl, chainId);
}
export function connectWallet(
skHex: string,
rpcUrl: string,
chainId: number
): Wallet {[[OPEN_9]]
const w = new Wallet(skHex);
return w.connect(makeProvider(rpcUrl, chainId));
}
export async function signPersonalMessage(wallet: Wallet, msg: string) {[[OPEN_10]]
return wallet.signMessage(msg);
}
export async function sendEth(
wallet: Wallet,
to: string,
amountEth: string
): Promise<TransactionReceipt> {[[OPEN_11]]
const tx = await wallet.sendTransaction({[[OPEN_12]]
to,
value: parseEther(amountEth)
});
const receipt = await tx.wait();
if (receipt == null) {[[OPEN_13]]
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 }> {[[OPEN_14]]
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) {[[OPEN_15]]
throw new Error("Deployment TX not mined");
}
return { address: contract.target as string, receipt };
}

3. src/keys.ts — 微型輔助工具

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

4. src/scripts/smoke-test.ts — 單一端到端流程

這是重要的一步,因為此腳本具有多個功能:

  • 列印 App ID(在 ROFL 內部)、地址和簽署的訊息
  • 等待資金
  • 部署計數器合約

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> {[[OPEN_18]]
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> {[[OPEN_19]]
const start = Date.now();
while (Date.now() - start < timeoutMs) {[[OPEN_20]]
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() {[[OPEN_21]]
const appId = await getAppId().catch(() => null);
console.log(`ROFL App ID: ${appId ?? "(unavailable outside ROFL)"}`);
const sk = await getEvmSecretKey(KEY_ID);
// 注意:此示範信任已配置的 RPC 提供者。對於生產環境,建議使用
// 輕量級客戶端(例如 Helios),以便您可以驗證遠端鏈狀態。
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) {[[OPEN_22]]
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) {[[OPEN_23]]
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) => {[[OPEN_24]]
console.error(e);
process.exit(1);
});

5. contracts/Counter.sol — 最小範例

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Counter {[[OPEN_25]]
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 — 通用部署器

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");
/**
* 使用方法:
* npm run deploy-contract -- ./artifacts/MyContract.json '[arg0, arg1]'
* 產物必須包含 { abi, bytecode }。
*/
async function main() {[[OPEN_26]]
const [artifactPath, ctorJson = "[]"] = process.argv.slice(2);
if (!artifactPath) {[[OPEN_27]]
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) {[[OPEN_28]]
throw new Error("Artifact must contain { abi, bytecode }");
}
let args: unknown[];
try {[[OPEN_29]]
args = JSON.parse(ctorJson);
if (!Array.isArray(args)) throw new Error("constructor args must be a JSON array");
} catch (e) {[[OPEN_30]]
throw new Error(`Failed to parse constructor args JSON: ${String(e)}`);
}
const sk = await getEvmSecretKey(KEY_ID);
// 注意:此示範信任已配置的 RPC 提供者。對於生產環境,建議使用
// 輕量級客戶端(例如 Helios),以便您可以驗證遠端鏈狀態。
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) => {[[OPEN_31]]
console.error(e);
process.exit(1);
});

Hardhat(僅限合約)

在此階段,我們需要最小配置來編譯 Counter.sol

hardhat.config.ts

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

需要注意的是,本地編譯是可選的,因此如果您願意可以跳過它。下一步是一個選擇 — 刪除現有的 contracts/Lock.sol 檔案,或者您可以將其更新為 Solidity 版本 0.8.24

npx hardhat compile

容器化

這是必要的步驟。在這裡,您需要一個 Dockerfile 來建構 TS 並編譯合約。該檔案還將運行一次煙霧測試,然後在您檢查日誌時保持閒置狀態。

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

接下來,您必須掛載 ROFL 提供的 appd socket。請放心,在此過程中不會暴露任何公共端口。

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

建構映像檔

重要的是要記住,ROFL 僅在啟用 Intel TDX 的硬體上運行。因此,如果您在不同的主機(例如 macOS)上編譯映像檔,那麼傳遞 --platform linux/amd64 參數是必要的額外步驟。

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

這裡有一個有趣的要點需要注意,您可以選擇額外的安全性和可驗證性。您只需要固定摘要並在 compose.yaml 中使用 image: …@sha256:…

建構 ROFL 套件

在運行 oasis rofl build 命令之前,有一個您必須執行的步驟。由於建構映像檔段落是在容器化之後進行的,您需要將 compose.yaml 中的 services.demo.image 更新為您建構的映像檔。
對於像這樣的簡單 TypeScript 專案,有時映像檔大小可能會大於預期。因此建議將 rofl.yaml resources 部分至少更新為:memory: 1024storage.size: 4096
現在,您已準備就緒。

oasis rofl build

接下來您可以發布 enclave 身份和配置。

oasis rofl update

部署

這是一個相當簡單的步驟,您在此處部署到 Testnet 提供者。

oasis rofl deploy

端到端(Base Sepolia)

這是一個 2 步驟的過程,儘管第二步是可選的。
首先,您檢視煙霧測試日誌。

oasis rofl machine logs

如果您到目前為止正確完成了所有步驟,您將在輸出中看到:

  • App ID
  • EVM 地址和簽署的訊息
  • 資助地址的提示
  • 資助完成後,Counter.sol 部署

接下來,本地開發。在這裡,您需要運行 npm run build:all 來編譯 TypeScript 程式碼和 Solidity 合約。如果不需要,請跳過此步驟。

export ALLOW_LOCAL_DEV=true
export LOCAL_DEV_SK=0x<64-hex-dev-secret-key> # 請勿在生產環境中使用
npm run smoke-test

安全性與注意事項

  • 提供者日誌在靜態時不加密。因此,絕對不要記錄秘密金鑰。
  • appd socket /run/rofl-appd.sock 僅存在於 ROFL 內部
  • 公共 RPC 可能存在速率限制。因此,建議選擇專用的 Base RPC URL。

Oasis GitHub 中有一個金鑰生成示範,您可以將其作為本教學的範例參考。https://github.com/oasisprotocol/demo-rofl-keygen

現在您已成功在 ROFL 中使用 appd 生成金鑰、簽署訊息、部署合約並在 Base Sepolia 上轉移 ETH,請在評論區告訴我們您的反饋。如需與 Oasis 工程團隊快速交流以獲得特定問題的幫助,您可以在官方 Discord 的 dev-central channel 中留下評論。

最初發布於 https://dev.to,2026 年 2 月 20 日。


Guide To Cross-Chain Key Generation (EVM / Base) With Oasis ROFL 最初發布於 Medium 上的 Coinmonks,人們在那裡透過突出顯示和回應這個故事來繼續對話。

市場機遇
CROSS 圖標
CROSS實時價格 (CROSS)
$0.10034
$0.10034$0.10034
-2.43%
USD
CROSS (CROSS) 實時價格圖表
免責聲明: 本網站轉載的文章均來源於公開平台,僅供參考。這些文章不代表 MEXC 的觀點或意見。所有版權歸原作者所有。如果您認為任何轉載文章侵犯了第三方權利,請聯絡 crypto.news@mexc.com 以便將其刪除。MEXC 不對轉載文章的及時性、準確性或完整性作出任何陳述或保證,並且不對基於此類內容所採取的任何行動或決定承擔責任。轉載材料僅供參考,不構成任何商業、金融、法律和/或稅務決策的建議、認可或依據。

您可能也會喜歡

馬斯克SpaceX擬最快3月秘密遞交IPO!估值瞄準1.75兆美元將超越Meta、特斯拉

馬斯克SpaceX擬最快3月秘密遞交IPO!估值瞄準1.75兆美元將超越Meta、特斯拉

消息人士透露馬斯克旗下太空探索公司 SpaceX 正考慮最快於 3 月向美國 SEC 秘密遞交 IPO 申請, […] 〈馬斯克SpaceX擬最快3月秘密遞交IPO!估值瞄準1.75兆美元將超越Meta、特斯拉〉這篇文章最早發佈於動區BlockTempo《動區動趨-最具影響力的區塊鏈新聞媒體》。
分享
Blocktempo ZH2026/02/28 13:31
傳出以色列對伊朗發動「預防性」軍事攻擊!比特幣瞬跌破6.5萬美元,市場恐慌情緒飆升

傳出以色列對伊朗發動「預防性」軍事攻擊!比特幣瞬跌破6.5萬美元,市場恐慌情緒飆升

網路傳出以色列對伊朗發動「預防性」軍事攻擊,中東局勢急遽升溫,比特幣在消息傳出後快速跌破 6.5 萬美元關口, […] 〈傳出以色列對伊朗發動「預防性」軍事攻擊!比特幣瞬跌破6.5萬美元,市場恐慌情緒飆升〉這篇文章最早發佈於動區BlockTempo《動區動趨-最具影響力的區塊鏈新聞媒體》。
分享
Blocktempo ZH2026/02/28 14:28
iPhone用戶必載!這款App讓你精準掌握垃圾車動態,從此不再錯過!

iPhone用戶必載!這款App讓你精準掌握垃圾車動態,從此不再錯過!

對於租屋族、上班族來說,倒垃圾常常是一件「比想像中更困難」的日常任務,加班晚一點就錯過垃圾車,有時垃圾車臨時調整路線,等了半天卻沒看到車影,其實現在不必再用猜的或靠運氣追車了!對於租屋族、上班族來說,倒垃圾常常是一件「比想像中更困難」的日常任務,加班晚一點就錯過垃圾車,有時垃圾車臨時調整路線,等了半天卻沒看到車影,只能
分享
Techbang2026/02/28 13:30