Oasis представил фреймворк для внецепочечной логики выполнения (ROFL), который помогает создавать и запускать приложения вне цепи, обеспечивая при этом конфиденциальность и поддерживая доверие с помощью возможности верификации в цепи. Создание с использованием ROFL включает множество компонентов.
В этом руководстве я покажу, как создать небольшое приложение TypeScript, генерирующее ключ secp256k1 внутри ROFL. Оно будет использовать @oasisprotocol/rofl-client TypeScript SDK, который взаимодействует с appd REST API под капотом. Приложение TypeScript также будет:
Будет простой тест работоспособности, который выводит информацию в логи.
Чтобы выполнить шаги, описанные в этом руководстве, вам потребуется:
Для получения подробной информации о настройке обратитесь к документации по предварительным требованиям для быстрого старта.
Первый шаг — инициализировать новое приложение с помощью Oasis CLI.
oasis rofl init rofl-keygen
cd rofl-keygen
При создании приложения в Testnet вам потребуется внести токены. Назначьте 100 токенов TEST на этом этапе.
oasis rofl create --network testnet
В качестве вывода CLI создаст App ID, обозначенный как rofl1….
Теперь вы готовы запустить проект.
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 # образец контракта
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 — единый сквозной поток
Это важный шаг, так как этот скрипт выполняет несколько функций:
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);
});
На этом этапе нам понадобится минимальная конфигурация для компиляции 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.
Есть шаг, который вы должны выполнить перед запуском команды 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
Это двухэтапный процесс, хотя второй шаг необязателен.
Сначала вы просматриваете логи теста работоспособности.
oasis rofl machine logs
Если вы правильно выполнили все шаги до сих пор, вы увидите в выводе:
Далее, локальная разработка. Здесь вам нужно запустить 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
В 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, где люди продолжают обсуждение, выделяя эту историю и отвечая на неё.


