Application
This chapter will show you how to quickly create, build and test a minimal ROFL app that serves as a simple oracle, fetching data from remote sources via HTTPS and posting it on chain for aggregation.
How do ROFL Apps Work?
Each ROFL app runs in its own Trusted Execution Environment (TEE) which is provisioned by an Oasis Node from its bundle (a zip archive containing the program binaries and metadata required for execution). ROFL apps register to the Oasis Network in order to be able to easily authenticate to on-chain smart contracts.
Inside the TEE, the ROFL app performs important functions that ensure its security and enable secure communication with the outside world. This includes using a light client to establish a fresh view of the Oasis consensus layer which provides a source of rough time and integrity for verification of all on-chain state. The ROFL app also generates a set of ephemeral cryptographic keys which are used in the process of remote attestation and on-chain registration. These processes ensure that the ROFL app can authenticate to on-chain modules (e.g. smart contracts running in the Sapphire runtime) by signing and submitting special transactions.
The ROFL app can then perform arbitrary work and interact with the outside world through (properly authenticated) network connections. Connections can be authenticated via HTTPS/TLS or use other methods (e.g. light clients for other chains).
Repository Structure and Dependencies
You can find the entire project inside the Oasis SDK repository under
examples/runtime-sdk/rofl-oracle
.
First we create the basic directory structure for the minimal runtime using
Rust's cargo
:
cargo init rofl-oracle
This will create the rofl-oracle
directory and populate it with some
boilerplate needed to describe a Rust application. It will also set up the
directory for version control using Git. The rest of the guide assumes that you
are executing commands from within this directory.
Since the Runtime SDK requires a nightly version of the Rust toolchain, you need
to specify a version to use by creating a special file called
rust-toolchain.toml
containing the following information:
[toolchain]
channel = "nightly-2024-07-09"
components = [ "rustfmt", "clippy" ]
targets = [ "x86_64-fortanix-unknown-sgx", "x86_64-unknown-linux-musl" ]
profile = "minimal"
Additionally, due to the requirements of some upstream dependencies, you need to
configure Cargo to always build with specific target CPU platform features
(namely AES-NI and SSE3) by creating a .cargo/config.toml
file with the
following content:
[build]
rustflags = ["-C", "target-feature=+aes,+ssse3"]
rustdocflags = ["-C", "target-feature=+aes,+ssse3"]
[test]
rustflags = ["-C", "target-feature=+aes,+ssse3"]
rustdocflags = ["-C", "target-feature=+aes,+ssse3"]
After you complete this guide, the directory structure will look as follows:
rofl-oracle
├── .cargo
│ └── config.toml # Cargo configuration.
├── Cargo.lock # Rust dependency tree checksums.
├── Cargo.toml # Rust crate defintion.
├── rust-toolchain.toml # Rust toolchain version configuration.
└── src
└── main.rs # The ROFL app definition.
App Definition
First you need to declare the required dependencies on oasis-runtime-sdk
and
related crates in order to be able to use the required features. To do this,
edit the [dependencies]
section in your Cargo.toml
to look like the
following:
[package]
name = "rofl-oracle"
version = "0.1.0"
edition = "2021"
[dependencies]
# Oasis SDK.
oasis-runtime-sdk = { git = "https://github.com/oasisprotocol/oasis-sdk", tag = "runtime-sdk/v0.9.6" }
module-evm = { git = "https://github.com/oasisprotocol/oasis-sdk", tag = "runtime-sdk/v0.9.6", package = "oasis-runtime-sdk-evm" }
rofl-utils = { git = "https://github.com/oasisprotocol/oasis-sdk", tag = "rofl-utils/v0.1.1" }
# Third-party dependencies.
anyhow = "1.0"
async-trait = "0.1.77"
ethabi = { version = "18.0.0", default-features = false, features = ["std"] }
tokio = { version = "1.38", features = ["rt", "rt-multi-thread"] }
serde_json = "1.0"
[features]
# Enables mock SGX in non-SGX builds.
debug-mock-sgx = ["oasis-runtime-sdk/debug-mock-sgx"]
After you have declared the required dependencies the next thing is to define
the ROFL app. To do this, create src/main.rs
with the following content:
use oasis_runtime_sdk::modules::rofl::app::prelude::*;
/// Address where the oracle contract is deployed.
// #region oracle-contract-address
const ORACLE_CONTRACT_ADDRESS: &str = "0x5FbDB2315678afecb367f032d93F642f64180aa3"; // TODO: Replace with your contract address.
// #endregion oracle-contract-address
struct OracleApp;
#[async_trait]
impl App for OracleApp {
/// Application version.
const VERSION: Version = sdk::version_from_cargo!();
/// Identifier of the application (used for registrations).
// #region app-id
fn id() -> AppId {
"rofl1qqn9xndja7e2pnxhttktmecvwzz0yqwxsquqyxdf".into() // TODO: Replace with your application ID.
}
// #endregion app-id
/// Return the consensus layer trust root for this runtime; if `None`, consensus layer integrity
/// verification will not be performed (e.g. Localnet).
// #region consensus-trust-root
fn consensus_trust_root() -> Option<TrustRoot> {
// The trust root below is for Sapphire Testnet at consensus height 22110615.
Some(TrustRoot {
height: 22110615,
hash: "95d1501f9cb88619050a5b422270929164ce739c5d803ed9500285b3b040985e".into(),
runtime_id: "000000000000000000000000000000000000000000000000a6d1e3ebf60dff6c".into(),
chain_context: "0b91b8e4e44b2003a7c5e23ddadb5e14ef5345c0ebcb3ddcae07fa2f244cab76"
.to_string(),
})
}
// #endregion consensus-trust-root
async fn run(self: Arc<Self>, _env: Environment<Self>) {
// We are running now!
println!("Hello ROFL world!");
}
async fn on_runtime_block(self: Arc<Self>, env: Environment<Self>, _round: u64) {
// This gets called for each runtime block. It will not be called again until the previous
// invocation returns and if invocation takes multiple blocks to run, those blocks will be
// skipped.
if let Err(err) = self.run_oracle(env).await {
println!("Failed to submit observation: {:?}", err);
}
}
}
impl OracleApp {
/// Fetch stuff from remote service via HTTPS and publish it on chain.
async fn run_oracle(self: Arc<Self>, env: Environment<Self>) -> Result<()> {
// Fetch data from remote service.
let observation = tokio::task::spawn_blocking(move || -> Result<_> {
// Request some data from Coingecko API.
let rsp: serde_json::Value = rofl_utils::https::agent()
.get("https://www.binance.com/api/v3/ticker/price?symbol=ROSEUSDT")
.call()?
.body_mut()
.read_json()?;
// Extract price and convert to integer.
let price = rsp
.pointer("/price")
.ok_or(anyhow::anyhow!("price not available"))?
.as_str().unwrap()
.parse::<f64>()?;
let price = (price * 1_000_000.0) as u128;
Ok(price)
}).await??;
// Prepare the oracle contract call.
let mut tx = self.new_transaction(
"evm.Call",
module_evm::types::Call {
address: ORACLE_CONTRACT_ADDRESS.parse().unwrap(),
value: 0.into(),
data: [
ethabi::short_signature("submitObservation", &[ethabi::ParamType::Uint(128)])
.to_vec(),
ethabi::encode(&[ethabi::Token::Uint(observation.into())]),
]
.concat(),
},
);
tx.set_fee_gas(200_000);
// Submit observation on chain.
env.client().sign_and_submit_tx(env.signer(), tx).await?;
Ok(())
}
}
fn main() {
OracleApp.start();
}
Testing it on Sapphire Localnet
The simplest way to test and debug your ROFL is with a local stack.
-
Disable trust root verification in
src/main.rs
. Replace:fn consensus_trust_root() -> Option<TrustRoot> {
// The trust root below is for Sapphire Testnet at consensus height 22110615.
Some(TrustRoot {
height: 22110615,
hash: "95d1501f9cb88619050a5b422270929164ce739c5d803ed9500285b3b040985e".into(),
runtime_id: "000000000000000000000000000000000000000000000000a6d1e3ebf60dff6c".into(),
chain_context: "0b91b8e4e44b2003a7c5e23ddadb5e14ef5345c0ebcb3ddcae07fa2f244cab76"
.to_string(),
})
}with an empty root:
fn consensus_trust_root() -> Option<TrustRoot> {
// DO NOT USE IN PRODUCTION!
None
} -
Compile ROFL in the unsafe mode:
oasis rofl build sgx --mode unsafe
-
Spin up the Sapphire Localnet docker container and mount your
rofl-oracle
folder to/rofls
inside the docker image:docker run -it -p8545:8545 -p8546:8546 -v rofl-oracle:/rofls ghcr.io/oasisprotocol/sapphire-localnet
In a few moments, the Sapphire Localnet will spin up and automatically launch your ROFL inside the compute node. See localnet for more information.
sapphire-localnet 2024-09-19-git2332dba (oasis-core: 24.2, sapphire-paratime: 0.8.2, oasis-web3-gateway: 5.1.0)
* Detected ROFL bundle: /rofls/rofl-oracle.orc
* Starting oasis-net-runner with sapphire...
* Waiting for Postgres to start....
* Waiting for Oasis node to start.....
* Starting oasis-web3-gateway...
* Bootstrapping network (this might take a minute)...
* Waiting for key manager......
* Populating accounts...
Available Accounts
==================
(0) 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 (10000 TEST)
(1) 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 (10000 TEST)
(2) 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC (10000 TEST)
(3) 0x90F79bf6EB2c4f870365E785982E1f101E93b906 (10000 TEST)
(4) 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 (10000 TEST)
Private Keys
==================
(0) 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
(1) 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
(2) 0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a
(3) 0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6
(4) 0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a
HD Wallet
==================
Mnemonic: test test test test test test test test test test test junk
Base HD Path: m/44'/60'/0'/0/%d
* Configuring ROFL /rofls/rofl-oracle.orc:
Enclave ID: 0+tTmlVjUvP0eIHXH7Dld3svPppCUdKDwYxnzplndLea/8+uR7hI7CyvHEm0soNTHhzEJfk1grNoBuUqQ9eNGg==
ROFL admin test:bob funded 10001 TEST
Compute node oasis1qp6tl30ljsrrqnw2awxxu2mtxk0qxyy2nymtsy90 funded 1000 TEST
App ID: rofl1qqn9xndja7e2pnxhttktmecvwzz0yqwxsquqyxdf
WARNING: The chain is running in ephemeral mode. State will be lost after restart!
* Listening on http://localhost:8545 and ws://localhost:8546. Chain ID: 0x5afd
* Container start-up took 65 seconds, node log level is set to warn.
Sapphire Localnet will always assign constant
0+tTmlVjUvP0eIHXH7Dld3svPppCUdKDwYxnzplndLea/8+uR7hI7CyvHEm0soNTHhzEJfk1grNoBuUqQ9eNGg==
enclave cryptographic identity regardless of your ROFL binary.
Sapphire Localnet will derive your ROFL app ID in deterministic order based on
the ROFL admin account nonce. By default the app ID of the first registered ROFL
will be rofl1qqn9xndja7e2pnxhttktmecvwzz0yqwxsquqyxdf
for ROFL admin account
test:bob
.
Any println!
calls you use in your Rust code will be logged inside the
/serverdir/node/net-runner/network/compute-0/node.log
file.
Now that you successfully compiled and tested your ROFL, proceed to the next chapter to deploy it.