OutLayer Documentation

Developer Guide: Random Numbers

⚡ TL;DR#

  • Write WebAssembly project using functions impossible in smart contracts
  • Push to public GitHub repo
  • Call Outlayer contract from your contract
  • Receive the off-chain execution result in the callback response
  • Payment based on actual resources used, unused funds auto-refunded
  • Settlement stays on Layer 1 (secure NEAR blockchain)

🎯 The Problem#

Smart contracts need randomness for gaming, lotteries, and fair selection. However, on-chain randomness is fundamentally deterministic and controllable by validators.

Validators can skip blocks until they get favorable random outcomes. Any randomness derived from block data (timestamp, hash, height) is predictable and exploitable by those who control block production.

To get truly random numbers, we can run external code off-chain with OutLayer. This code executes outside the blockchain environment where real entropy sources are available, then returns verifiable results back to your contract.

📝 Step 1: Write WASI Code#

You need a project that compiles to WebAssembly. OutLayer currently supports wasm32-wasip1 and wasm32-wasip2 targets.

Write code that accepts parameters from stdin and outputs results to stdout. Here's an example:

use serde::{Deserialize, Serialize};
use rand::Rng;
use std::io::{self, Read};

// Input structure from stdin
#[derive(Deserialize)]
struct Input {
    min: u32,
    max: u32,
}

// Output structure to stdout
#[derive(Serialize)]
struct Output {
    random_number: u32,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Read JSON from stdin
    let mut input_string = String::new();
    io::stdin().read_to_string(&mut input_string)?;
    let input: Input = serde_json::from_str(&input_string)?;

    // Generate TRUE random number (impossible on-chain!)
    let mut rng = rand::thread_rng();
    let random_number = rng.gen_range(input.min..=input.max);

    // Output JSON to stdout
    let output = Output { random_number };
    print!("{}", serde_json::to_string(&output)?);
    Ok(())
}

💡 Key point: rand::thread_rng() works here but fails in smart contracts.

Add Cargo.toml:

[package]
name = "random-ark"
version = "0.1.0"
edition = "2021"

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
rand = "0.8"

[[bin]]
name = "random-ark"
path = "src/main.rs"

📤 Step 2: Make Code Publicly Available#

Your code must be accessible in a public GitHub repository. OutLayer workers will compile it on-demand from the source.

You can reference your code by:

  • Branch name (e.g., main, develop) - always uses the latest code
  • Commit hash (e.g., a1b2c3d) - immutable, guarantees exact version

📝 Example repo: github.com/zavodil/random-ark

🔮 Coming soon: GitLab, Bitbucket, and other git hosting platforms

🚀 Step 3: Call OutLayer Contract#

Now you can run your code by calling a smart contract. This will pause the blockchain transaction, execute your code off-chain, and send the result back into the blockchain transaction:

# Option 1: GitHub source (compiles from source code)
near call outlayer.testnet request_execution '{
  "source": {
    "GitHub": {
      "repo": "https://github.com/zavodil/random-ark",
      "commit": "main",
      "build_target": "wasm32-wasip1"
    }
  },
  "input_data": "{\"min\": 1, \"max\": 100}",
  "resource_limits": {
    "max_instructions": 10000000000,
    "max_memory_mb": 128,
    "max_execution_seconds": 60
  },
  "response_format": "Json"
}' --accountId alice.testnet --deposit 0.1 --gas 300000000000000

# Option 2: WasmUrl source (pre-compiled WASM, instant execution)
near call outlayer.testnet request_execution '{
  "source": {
    "WasmUrl": {
      "url": "https://wasmhub.testnet.fastfs.io/fastfs.testnet/abc123...wasm",
      "hash": "41c1c7b3528565f3fd139943f439d61c0768e9abdb9b579bd0921ecbfcabeded",
      "build_target": "wasm32-wasip1"
    }
  },
  "input_data": "{\"min\": 1, \"max\": 100}",
  "response_format": "Json"
}' --accountId alice.testnet --deposit 0.1 --gas 300000000000000

# Option 3: Project source (registered project with persistent storage)
near call outlayer.testnet request_execution '{
  "source": {
    "Project": {
      "project_id": "alice.testnet/my-app",
      "version_key": null
    }
  },
  "input_data": "{}",
  "response_format": "Json"
}' --accountId alice.testnet --deposit 0.1 --gas 300000000000000

💰 Dynamic Pricing: Payment is calculated based on actual resources consumed (instructions executed, time spent). Unused deposit is automatically refunded at the end of the transaction.

💡 Try it now: Open in Playground →

✅ Step 4: Get Result#

Check execution result on the Executions page. Since we specified response_format: "Json", the result will be parsed as JSON:

{
  "status": "Completed",
  "result": {
    "random_number": 42
  },
  "resources_used": {
    "instructions": 1234567,
    "time_ms": 45
  }
}

🔥 Step 5: Use in Your Contract#

The most exciting part: you can integrate this off-chain code into smart contracts! For example, let's build a coin flip game where players guess heads or tails through a coin-toss contract:

use near_sdk::*;

// OutLayer contract address
// For testnet: "outlayer.testnet"
// For mainnet: "outlayer.near"
const OUTLAYER_CONTRACT_ID: &str = "outlayer.testnet";
const MIN_DEPOSIT: u128 = 10_000_000_000_000_000_000_000; // 0.01 NEAR

// External contract interface for OutLayer
#[ext_contract(ext_outlayer)]
trait OutLayer {
    fn request_execution(
        &mut self,
        source: serde_json::Value,  // ExecutionSource: GitHub, WasmUrl, or Project
        resource_limits: serde_json::Value,
        input_data: String,
        secrets_ref: Option<serde_json::Value>,
        response_format: String,
        payer_account_id: Option<AccountId>,
        params: Option<serde_json::Value>,
    );
}

// Callback interface
#[ext_contract(ext_self)]
trait ExtSelf {
    fn on_random_result(
        &mut self,
        player: AccountId,
        choice: String,
        #[callback_result] result: Result<Option<RandomResponse>, PromiseError>,
    ) -> String;
}

#[near_bindgen]
impl CoinFlipContract {
    // Player calls this to flip the coin
    #[payable]
    pub fn flip_coin(&mut self, choice: String) -> Promise {
        let player = env::predecessor_account_id();
        let attached = env::attached_deposit().as_yoctonear();

        assert!(attached >= MIN_DEPOSIT, "Attach 0.01 NEAR");

        // Request random number from OutLayer
        // Option 1: GitHub source (compiles from source)
        // Option 2: WasmUrl for pre-compiled WASM:
        //   json!({"url": "https://fastfs.io/.../random.wasm",
        //          "hash": "abc123...", "build_target": "wasm32-wasip1"})
        ext_outlayer::ext(OUTLAYER_CONTRACT_ID.parse().unwrap())
            .with_attached_deposit(NearToken::from_yoctonear(attached))
            .with_unused_gas_weight(1)
            .request_execution(
                json!({"repo": "https://github.com/zavodil/random-ark",
                       "commit": "main", "build_target": "wasm32-wasip1"}),
                json!({"max_instructions": 10000000000,
                       "max_memory_mb": 128, "max_execution_seconds": 60}),
                "{\"min\": 0, \"max\": 1}".to_string(),
                None,
                "Json".to_string(),
                Some(player.clone()), // ✅ Refund goes to player, not to this contract
            )
            .then(
                ext_self::ext(env::current_account_id())
                    .with_static_gas(Gas::from_gas(5_000_000_000_000))
                    .on_random_result(player, choice)
            )
    }

    // Callback receives the random result
    // NOTE: This example just displays the result without any bet payout logic,
    // but you can easily add token transfers, NFT minting, or other game mechanics here
    #[private]
    pub fn on_random_result(
        &mut self,
        player: AccountId,
        choice: String,
        #[callback_result] result: Result<Option<RandomResponse>, PromiseError>,
    ) -> String {
        match result {
            Ok(Some(response)) => {
                let result = if response.random_number == 0 { "Heads" } else { "Tails" };
                if choice == result {
                    format!("🎉 You won! Result: {}", result)
                } else {
                    format!("😢 You lost. Result: {}", result)
                }
            }
            _ => "Error: OutLayer execution failed".to_string()
        }
    }
}

🎮 Try it with your testnet account: Open Coin Flip in Playground →
(Mainnet is also supported)

📝 Example transaction: View on NEAR Explorer

🔧 Important Details for Developers#

💰 Payment Flexibility

You can choose who pays for off-chain execution: user or your contract.

🔒 Layer 1 Settlement

Off-chain execution is fast and cheap, but final settlement stays on NEAR Layer 1. Your contract receives results via callback on-chain, ensuring security and auditability.

🔐 Encrypted Secrets

Store API keys and credentials with sophisticated access control: whitelists, NEAR balance requirements, FT/NFT ownership, and complex logic conditions (AND/OR/NOT).

📡 Blockchain Data Access

WASI containers can receive blockchain context as input parameters (block height, account ID, timestamp, etc.).

Example: echo-ark - demonstrates how to pass and process blockchain data in off-chain code.

🎓 Key Takeaways#

True randomness - impossible on-chain, trivial with OutLayer

Simple integration - write Rust, push to GitHub, call from contract

Secure execution - runs in TEE with attestation (coming soon)

Automatic refunds - unused resources returned to caller

🚀 Next Steps#