DEV Community

Zerod0wn Gaming
Zerod0wn Gaming

Posted on

Building a Cross-Chain Confidential "Trust Score Oracle" with Oasis Sapphire + OPL + ROFL

This tutorial shows how to add privacy + verifiable off-chain compute to any EVM dApp by combining:

  • Sapphire: Oasis’ confidential EVM (encrypted calldata + confidential contract state).
  • OPL (Oasis Privacy Layer): message bridge pattern to “bolt privacy onto” existing EVM apps by offloading sensitive logic to Sapphire.
  • ROFL (Runtime Off-Chain Logic): a TEE-managed app runtime (containers or single binaries) with secrets stored via built-in KMS and on-chain management through Sapphire.

Real use case

You run a lending/access-control dApp on a Home chain (Ethereum/L2). You want a wallet "Trust Score" derived from sensitive signals:

  • private allowlists (KYC provider, internal risk flags),
  • off-chain identity reputation (GitHub activity, paid subscription, enterprise customer),
  • model inference results (fraud classifier).

You don’t want to leak:

  • raw signals,
  • model features,
  • user identity mappings,
  • or scoring logic.

Design goal:
Home chain contract gets a public, verifiable decision (e.g., score bucket/allow/deny/rate tier) while sensitive scoring happens privately and verifiably.

Architecture

High-level diagram

What each piece does:

  • Home chain contract: minimal "public" state, accepts final decision and enforces it.
  • Sapphire contract (the enclave): holds private state (scores, salts, user-to-signal mappings), verifies authorized sources, emits public commitments only.
  • ROFL app: runs your scoring code in a TEE; keeps API keys/model weights/feature rules in ROFL secrets; posts updates on-chain.

ROFL apps are explicitly designed to run in TEEs and be managed via Sapphire, with built-in secrets support.

Repo blueprint

Monorepo layout:

oasis-trustscore-oracle/
  README.md
  .env.example

  contracts/
    hardhat.config.ts
    package.json
    contracts/
      HomeHost.sol
      SapphireEnclave.sol
    scripts/
      deploy-home.ts
      deploy-sapphire.ts
      link-endpoints.ts
    tasks/
      send-opl-message.ts

  rofl/
    compose.yaml
    rofl.yaml
    app/
      Dockerfile
      requirements.txt
      main.py
      scorer.py
      signals.py

  diagrams/
    architecture.mmd
    sequence.mmd
Enter fullscreen mode Exit fullscreen mode

Networks & prerequisites

Sapphire Testnet settings (for dev)

From Oasis docs, Sapphire Testnet:

You’ll also want the Sapphire wrapper for encrypted transactions (TS wrapper / ethers v6 wrapper).

ROFL prerequisites

ROFL quickstart expects:

  • a containerized app (OCI image),
  • Oasis CLI (latest),
  • testnet tokens from the faucet

Step 1 - Smart contracts: Home chain + Sapphire enclave

1A) SapphireEnclave.sol (runs confidentially)

This contract runs on Sapphire and receives cross-chain requests via OPL. It:

  • stores scores privately,
  • accepts updates from ROFL,
  • sends a public decision back to Home chain.

Minimal skeleton based on the official OPL SDK pattern (Enclave + endpoints)

// contracts/contracts/SapphireEnclave.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {Enclave, Result, autoswitch} from "@oasisprotocol/sapphire-contracts/contracts/OPL.sol";

contract SapphireEnclave is Enclave {
    // Private by design on Sapphire: state + calldata are confidential.
    mapping(address => uint256) private score; // example: 0..100

    // Optional: authorize a ROFL app address (or a set of them).
    address public roflUpdater;

    constructor(address homeContract, string memory homeChain)
        Enclave(homeContract, autoswitch(homeChain))
    {
        registerEndpoint("requestScore", onRequestScore);
    }

    function setRoflUpdater(address _rofl) external {
        // replace with proper access control for real deployments
        roflUpdater = _rofl;
    }

    // ROFL posts score updates (confidential write).
    function updateScore(address user, uint256 newScore) external returns (bool) {
        require(msg.sender == roflUpdater, "not authorized");
        score[user] = newScore;
        return true;
    }

    // OPL endpoint: called when Home chain requests a score decision.
    function onRequestScore(bytes calldata args) internal returns (Result) {
        address user = abi.decode(args, (address));

        uint256 s = score[user];

        // Only return a coarse bucket publicly (avoid leaking exact score).
        uint8 bucket =
            s >= 80 ? 3 :
            s >= 50 ? 2 :
            s >= 20 ? 1 : 0;

        // Send decision back to Home chain (publicly observable).
        postMessage("scoreDecision", abi.encode(user, bucket));
        return Result.Success;
    }
}
Enter fullscreen mode Exit fullscreen mode

Why this works:

  • Sapphire is an EVM ParaTime built for confidential computation.
  • OPL uses message bridges so your Home chain can trigger private computation on Sapphire.

1B) HomeHost.sol (your existing dApp stays here)
Based on official OPL Host pattern.

// contracts/contracts/HomeHost.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {Host, Result} from "@oasisprotocol/sapphire-contracts/contracts/OPL.sol";

contract HomeHost is Host {
    mapping(address => uint8) public lastBucket;

    constructor(address sapphireContract) Host(sapphireContract) {
        registerEndpoint("scoreDecision", onScoreDecision);
    }

    // User or app requests a score check.
    function requestScore(address user) external payable {
        postMessage("requestScore", abi.encode(user));
    }

    // Called by OPL bridge after Sapphire computed the decision.
    function onScoreDecision(bytes calldata args) internal returns (Result) {
        (address user, uint8 bucket) = abi.decode(args, (address, uint8));
        lastBucket[user] = bucket;
        return Result.Success;
    }

    function canBorrow(address user) external view returns (bool) {
        return lastBucket[user] >= 2;
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 2 - Hardhat setup (with Sapphire encryption)

Install the OPL/Sapphire contracts library and Sapphire tooling:

  • OPL SDK lives in @oasisprotocol/sapphire-contracts.
  • Sapphire wrapper guidance (TypeScript wrapper / ethers wrapper).

Your hardhat.config.ts should include:

  • Home chain network (your choice),
  • Sapphire testnet using the official RPC and chain id

Example network params:

// contracts/hardhat.config.ts (snippet)
networks: {
  sapphire_testnet: {
    url: "https://testnet.sapphire.oasis.io",
    chainId: 23295,
    accounts: [process.env.PRIVATE_KEY!],
  },
  // home: { ... } // e.g. sepolia/arbitrum etc.
}
Enter fullscreen mode Exit fullscreen mode

Step 3 - Link OPL endpoints (the "wiring")

OPL requires that each side knows the other side and which endpoints exist (1–1 paired contracts).

In your scripts:

  • deploy SapphireEnclave on Sapphire
  • deploy HomeHost on Home chain
  • set each other’s address + register endpoints appropriately

(Exactly how you do step (3) depends on your preferred OPL bridge route; the docs list multiple integration methods, including OPL SDK wrapper around Celer IM.)

Step 4 - ROFL app: compute score inside a TEE and push to Sapphire

ROFL gives you:

  • TEE execution (SGX/TDX)
  • app lifecycle managed via Oasis
  • built-in secrets stored on-chain and injected securely
  • ability to sign and submit special transactions back to Sapphire

ROFL gives you:

  • TEE execution (SGX/TDX)
  • app lifecycle managed via Oasis
  • built-in secrets stored on-chain and injected securely
  • ability to sign and submit special transactions back to Sapphire

4A) ROFL app code (Python example)
rofl/app/main.py:

  • loads secrets (API keys)
  • fetches signals
  • runs scoring
  • calls updateScore(user, score) on Sapphire
# rofl/app/main.py
import os
import time
from web3 import Web3

SAPPHIRE_RPC = os.environ["SAPPHIRE_RPC"]
ENCLAVE_ADDR = Web3.to_checksum_address(os.environ["ENCLAVE_ADDR"])
ROFL_SIGNER_KEY = os.environ["ROFL_SIGNER_KEY"]  # stored as ROFL secret

ABI = [...]  # contract ABI for updateScore

w3 = Web3(Web3.HTTPProvider(SAPPHIRE_RPC))
acct = w3.eth.account.from_key(ROFL_SIGNER_KEY)
enclave = w3.eth.contract(address=ENCLAVE_ADDR, abi=ABI)

def compute_score(user: str) -> int:
    # pull private signals using secrets (tokens), then score
    # keep it deterministic if you want auditability
    return 82

def submit_score(user: str, s: int):
    tx = enclave.functions.updateScore(Web3.to_checksum_address(user), s).build_transaction({
        "from": acct.address,
        "nonce": w3.eth.get_transaction_count(acct.address),
        "gas": 500_000,
    })
    signed = acct.sign_transaction(tx)
    return w3.eth.send_raw_transaction(signed.rawTransaction)

def main():
    users = os.environ["USERS"].split(",")
    while True:
        for u in users:
            s = compute_score(u)
            txh = submit_score(u, s)
            print("updated", u, s, txh.hex())
        time.sleep(60)

if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

4B) Containerize + ROFLize

ROFL quickstart is literally the flow:

  1. oasis rofl init
  2. oasis rofl create --network testnet
  3. oasis rofl build
  4. oasis rofl secret set ...
  5. oasis rofl update
  6. oasis rofl deploy

Example secrets:

# inside rofl/
echo -n "0xYOUR_SIGNER_KEY" | oasis rofl secret set ROFL_SIGNER_KEY -
echo -n "https://testnet.sapphire.oasis.io" | oasis rofl secret set SAPPHIRE_RPC -
echo -n "0xYourEnclaveAddress" | oasis rofl secret set ENCLAVE_ADDR -
Enter fullscreen mode Exit fullscreen mode

ROFL secrets are explicitly supported and meant for sensitive env vars like API keys.

Sequence diagram (end-to-end)

This is the core pattern: public app stays on its chain, sensitive logic runs confidentially, and you only export a minimal decision.

Threat model notes

  • Don’t export raw scores: export buckets, booleans, or commitments.
  • Secrets: keep API keys/model access tokens in ROFL secrets (not in Docker image).
  • RPC trust: Oasis docs explicitly warn RPC endpoints are a trust point (rate limits, censorship, MITM); run your own if your security bar is high.
  • Testnet disclaimer: Sapphire testnet confidentiality is not guaranteed and state may be wiped; treat it as public.

Sources

Top comments (0)