Oasis представила фреймворк для runtime off-chain logic (ROFL), помогающий создавать и запускать приложения вне цепочки, обеспечивая при этом конфиденциальность и поддерживая доверие к он-чейнOasis представила фреймворк для runtime off-chain logic (ROFL), помогающий создавать и запускать приложения вне цепочки, обеспечивая при этом конфиденциальность и поддерживая доверие к он-чейн

Руководство по кроссчейн-генерации ключей (EVM / Base) с помощью Oasis ROFL

2026/02/20 21:16
9м. чтение

Oasis представил фреймворк для внецепочечной логики выполнения (ROFL), который помогает создавать и запускать приложения вне цепи, обеспечивая при этом конфиденциальность и поддерживая доверие с помощью возможности верификации в цепи. Создание с использованием ROFL включает множество компонентов.
В этом руководстве я покажу, как создать небольшое приложение TypeScript, генерирующее ключ secp256k1 внутри ROFL. Оно будет использовать @oasisprotocol/rofl-client TypeScript SDK, который взаимодействует с appd REST API под капотом. Приложение TypeScript также будет:

Будет простой тест работоспособности, который выводит информацию в логи.

Предварительные требования

Чтобы выполнить шаги, описанные в этом руководстве, вам потребуется:

  • Node.js 20+ и Docker (или Podman)
  • Oasis CLI и минимум 120 токенов TEST в вашем кошельке (Oasis Testnet faucet)
  • Немного тестовых ETH Base Sepiola (Base Sepiola faucet)

Для получения подробной информации о настройке обратитесь к документации по предварительным требованиям для быстрого старта.

Инициализация приложения

Первый шаг — инициализировать новое приложение с помощью 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

Шаблон TypeScript от Hardhat автоматически создает tsconfig.json. Нам нужно добавить небольшой скрипт, чтобы код приложения мог компилироваться в dist/.

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

Структура приложения

В этом разделе мы добавим несколько небольших файлов TS и один контракт Solidity.

src/
├── appd.ts # тонкая обертка над @oasisprotocol/rofl-client
├── evm.ts # помощники ethers (провайдер, кошелек, транзакция, развертывание)
├── keys.ts # небольшие помощники (контрольная сумма)
└── scripts/
├── deploy-contract.ts # универсальный скрипт развертывания для скомпилированных артефактов
└── smoke-test.ts # сквозная демонстрация (логи)
contracts/
└── Counter.sol # образец контракта

  1. src/appd.ts — тонкая обертка над SDK. Здесь вам нужно будет использовать официальный клиент для взаимодействия с appd (UNIX-сокет). Нам также потребуется явный локальный резервный вариант для разработки при запуске вне 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();
}
/**
* Генерирует (или детерминированно перевыводит) ключ secp256k1 внутри ROFL и
* возвращает его как шестнадцатеричную строку с префиксом 0x (для ethers.js Wallet).
*
* ТОЛЬКО для локальной разработки (вне ROFL): если сокет отсутствует и вы установили
* ALLOW_LOCAL_DEV=true и LOCAL_DEV_SK=0x<64-hex>, используется это значение.
*/
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 сокет не найден и LOCAL_DEV_SK не предоставлен (только для разработки).'
);
}

2. src/evm.ts — помощники 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("Транзакция отброшена или заменена перед подтверждением");
}
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("Транзакция развертывания не добыта");
}
return { address: contract.target as string, receipt };
}

3. src/keys.ts — небольшие помощники

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 — единый сквозной поток

Это важный шаг, так как этот скрипт выполняет несколько функций:

  • вывод 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> {
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(`Ожидание пополнения... текущий баланс=${formatEther(bal)} ETH`);
await sleep(pollMs);
}
throw new Error("Истекло время ожидания пополнения.");
}
async function main() {
const appId = await getAppId().catch(() => null);
console.log(`ROFL App ID: ${appId ?? "(недоступен вне 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 адрес (Base Sepolia): ${addr}`);
const msg = "hello from rofl";
const sig = await signPersonalMessage(wallet, msg);
console.log(`Подписанное сообщение: "${msg}"`);
console.log(`Подпись: ${sig}`);
const provider = wallet.provider as JsonRpcProvider;
let bal = await provider.getBalance(addr);
if (bal === 0n) {
console.log("Пожалуйста, пополните указанный выше адрес ETH Base Sepolia для продолжения.");
bal = await waitForFunding(provider, addr);
}
console.log(`Баланс обнаружен: ${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 отсутствует abi/bytecode");
}
const { address: contractAddress, receipt: deployRcpt } =
await deployContract(wallet, artifact.abi, artifact.bytecode, []);
console.log(`Развернут Counter по адресу ${contractAddress} (tx=${deployRcpt.hash})`);
console.log("Тест работоспособности успешно завершен!");
}
main().catch((e) => {
console.error(e);
process.exit(1);
});

5. contracts/Counter.sol — минимальный образец

// 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 — универсальный деплойер

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() {
const [artifactPath, ctorJson = "[]"] = process.argv.slice(2);
if (!artifactPath) {
console.error("Использование: 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("Артефакт должен содержать { abi, bytecode }");
}
let args: unknown[];
try {
args = JSON.parse(ctorJson);
if (!Array.isArray(args)) throw new Error("аргументы конструктора должны быть JSON-массивом");
} catch (e) {
throw new Error(`Не удалось разобрать 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) => {
console.error(e);
process.exit(1);
});

Hardhat (только контракты)

На этом этапе нам понадобится минимальная конфигурация для компиляции 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;

Важно отметить, что локальная компиляция необязательна, поэтому вы можете пропустить этот шаг, если хотите. Следующий шаг — выбор: либо удалить существующий файл 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"]

Далее вы должны смонтировать сокет appd, предоставляемый ROFL. Будьте уверены, что никакие публичные порты не раскрываются в процессе.

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 .

Интересно отметить, что вы можете выбрать дополнительную безопасность и проверяемость. Вам просто нужно закрепить дайджест и использовать image: …@sha256:… в compose.yaml.

Сборка пакета ROFL

Есть шаг, который вы должны выполнить перед запуском команды oasis rofl build. Поскольку сборка сегмента образа происходит после контейнеризации, вам нужно обновить services.demo.image в compose.yaml на образ, который вы собрали.
Для простых проектов TypeScript, подобных этому, иногда возможно, что размер образа больше, чем ожидалось. Поэтому рекомендуется обновить раздел resources в rofl.yaml как минимум до: memory: 1024 и storage.size: 4096.
Теперь вы готовы.

oasis rofl build

Затем вы можете опубликовать идентификаторы анклава и конфигурацию.

oasis rofl update

Развертывание

Это достаточно простой шаг, где вы разворачиваете на провайдере Testnet.

oasis rofl deploy

Сквозное тестирование (Base Sepolia)

Это двухэтапный процесс, хотя второй шаг необязателен.
Сначала вы просматриваете логи теста работоспособности.

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 /run/rofl-appd.sock существует только внутри ROFL.
  • В публичных RPC могут быть ограничения по скорости. Поэтому рекомендуется выбрать выделенный Base RPC URL.

В GitHub Oasis есть демонстрация генерации ключей, на которую вы можете ссылаться в качестве примера этого руководства. https://github.com/oasisprotocol/demo-rofl-keygen

Теперь, когда вы успешно сгенерировали ключ в ROFL с помощью appd, подписали сообщения, развернули контракт и переместили ETH на Base Sepolia, дайте нам знать в разделе комментариев ваш отзыв. Для быстрого общения с командой инженеров Oasis для помощи с конкретными вопросами вы можете оставить свои комментарии в канале dev-central в официальном Discord.

Первоначально опубликовано на https://dev.to 20 февраля 2026.


Руководство по кроссчейн-генерации ключей (EVM / Base) с Oasis ROFL было первоначально опубликовано в Coinmonks на Medium, где люди продолжают обсуждение, выделяя эту историю и отвечая на неё.

Возможности рынка
Логотип CROSS
CROSS Курс (CROSS)
$0.10063
$0.10063$0.10063
-2.34%
USD
График цены CROSS (CROSS) в реальном времени
Отказ от ответственности: Статьи, размещенные на этом веб-сайте, взяты из общедоступных источников и предоставляются исключительно в информационных целях. Они не обязательно отражают точку зрения MEXC. Все права принадлежат первоисточникам. Если вы считаете, что какой-либо контент нарушает права третьих лиц, пожалуйста, обратитесь по адресу crypto.news@mexc.com для его удаления. MEXC не дает никаких гарантий в отношении точности, полноты или своевременности контента и не несет ответственности за любые действия, предпринятые на основе предоставленной информации. Контент не является финансовой, юридической или иной профессиональной консультацией и не должен рассматриваться как рекомендация или одобрение со стороны MEXC.

Вам также может быть интересно