diff --git a/components/RustSdkExperimental.tsx b/components/RustSdkExperimental.tsx new file mode 100644 index 00000000..0f9d76e8 --- /dev/null +++ b/components/RustSdkExperimental.tsx @@ -0,0 +1,10 @@ +import { Callout } from 'nextra-theme-docs' + +export function RustSdkExperimental() { + return ( + + The Rust SDK is experimental. Support and bug fixes are low priority compared to the Python SDK. + See genlayer_sdk on crates.io for the latest published version. + + ) +} diff --git a/pages/developers/_meta.json b/pages/developers/_meta.json index a289e997..39d0d3d3 100644 --- a/pages/developers/_meta.json +++ b/pages/developers/_meta.json @@ -1,6 +1,7 @@ { "networks": "Networks & RPCs", "intelligent-contracts": "Intelligent Contracts", + "rust-contracts": "Rust Contracts", "decentralized-applications": "Frontend & SDK Integration", "staking-guide": "Staking Contract Guide" } diff --git a/pages/developers/rust-contracts/_meta.json b/pages/developers/rust-contracts/_meta.json new file mode 100644 index 00000000..c5cf92c4 --- /dev/null +++ b/pages/developers/rust-contracts/_meta.json @@ -0,0 +1,6 @@ +{ + "introduction": "Introduction", + "first-contract": "Your First Contract", + "storage": "Storage", + "first-intelligent-contract": "Your First Intelligent Contract" +} diff --git a/pages/developers/rust-contracts/first-contract.mdx b/pages/developers/rust-contracts/first-contract.mdx new file mode 100644 index 00000000..3553b339 --- /dev/null +++ b/pages/developers/rust-contracts/first-contract.mdx @@ -0,0 +1,184 @@ +import { Callout } from 'nextra-theme-docs' +import { RustSdkExperimental } from '../../../components/RustSdkExperimental' + +# Your First Contract + + + +### Project Setup + +Create a new Rust project and configure it for WebAssembly: + +```bash +cargo init --name my_contract +``` + +Your `Cargo.toml` should look like this: + +```toml +[package] +name = "my_contract" +version = "0.1.0" +edition = "2024" + +[dependencies] +genlayer_sdk = "0.0.2" +bytes = "1" +``` + + + Contracts **must** be compiled for the `wasm32-wasip1` target. Floating point operations are forbidden in deterministic mode and will cause a VM error. + + +### The Contract Trait + +A Rust contract is a struct that implements the `Contract` trait. The trait has three methods corresponding to GenVM's entry points: + +```rust +use genlayer_sdk::abi::entry::MessageData; +use genlayer_sdk::abi::entry::contract_def::Contract; +use genlayer_sdk::calldata::Value; + +#[derive(Default)] +pub struct MyContract; + +impl Contract for MyContract { + fn handle_main( + &mut self, + message: MessageData, + data: bytes::Bytes, + ) -> Result { + Ok(Value::Str("Hello from Rust!".to_owned())) + } + + fn handle_sandbox( + &mut self, + _message: MessageData, + _data: bytes::Bytes, + ) -> Result, String> { + unimplemented!() + } + + fn handle_consensus_stage( + &mut self, + _message: MessageData, + _data: bytes::Bytes, + _stage_data: genlayer_sdk::abi::entry::contract_def::ConsensusStageData, + ) -> Result { + unimplemented!() + } +} + +genlayer_sdk::contract_main!(MyContract); +``` + +The `contract_main!` macro generates the `main` function that reads the incoming message from stdin, dispatches to the correct handler, and sends the result back to GenVM. + + + Your struct must implement `Default`. GenVM creates a fresh instance for each invocation. + + +### Handling Deploy vs Method Calls + +The `Contract` trait gives you raw calldata bytes. For a more ergonomic API that splits deployment from method calls, implement `ContractExt` instead: + +```rust +use genlayer_sdk::abi::entry::MessageData; +use genlayer_sdk::abi::entry::contract_def::{Contract, ContractExt}; +use genlayer_sdk::calldata::{self, Map, Value}; + +#[derive(Default)] +pub struct MyContract; + +impl ContractExt for MyContract { + type Error = String; + + fn handle_deploy( + &mut self, + message: MessageData, + args: Vec, + kwargs: Map, + ) -> Result { + // Called when is_init == true + println!("Contract deployed by {:?}", message.sender_address); + Ok(Value::Null) + } + + fn handle_method( + &mut self, + message: MessageData, + method: String, + args: Vec, + kwargs: Map, + ) -> Result { + match method.as_str() { + "greet" => { + let name = args.first() + .and_then(|v| v.as_str()) + .unwrap_or("World"); + Ok(Value::Str(format!("Hello, {name}!"))) + } + _ => Err(format!("unknown method: {method}")), + } + } +} + +// ContractExt provides the Contract impl automatically, +// but you still need to implement handle_sandbox and handle_consensus_stage: +impl Contract for MyContract { + fn handle_main( + &mut self, + message: MessageData, + data: bytes::Bytes, + ) -> Result { + ContractExt::handle_main(self, message, data.to_vec()) + } + + fn handle_sandbox( + &mut self, + _message: MessageData, + _data: bytes::Bytes, + ) -> Result, String> { + unimplemented!() + } + + fn handle_consensus_stage( + &mut self, + _message: MessageData, + _data: bytes::Bytes, + _stage_data: genlayer_sdk::abi::entry::contract_def::ConsensusStageData, + ) -> Result { + unimplemented!() + } +} + +genlayer_sdk::contract_main!(MyContract); +``` + +### Message Data + +Every handler receives a `MessageData` struct with the transaction context: + +| Field | Type | Description | +|--------------------|----------------------------|-------------------------------------| +| `contract_address` | `Address` (`[u8; 20]`) | This contract's address | +| `sender_address` | `Address` | Immediate caller | +| `origin_address` | `Address` | Original transaction sender | +| `value` | `BigInt` | Value sent with the call | +| `is_init` | `bool` | Whether this is a deployment call | +| `datetime` | `DateTime` | Transaction timestamp | +| `chain_id` | `BigInt` | Chain identifier | + +### Building + +Compile your contract: + +```bash +cargo build --target wasm32-wasip1 --release +``` + +The resulting `.wasm` file will be in `target/wasm32-wasip1/release/my_contract.wasm`. + +### Debugging + +You can use `println!` for debug output. Print statements are included in the GenVM execution log and are visible during development. diff --git a/pages/developers/rust-contracts/first-intelligent-contract.mdx b/pages/developers/rust-contracts/first-intelligent-contract.mdx new file mode 100644 index 00000000..a84621cc --- /dev/null +++ b/pages/developers/rust-contracts/first-intelligent-contract.mdx @@ -0,0 +1,258 @@ +import { Callout } from 'nextra-theme-docs' +import { RustSdkExperimental } from '../../../components/RustSdkExperimental' + +# Your First **Intelligent** Contract + + + +Non-deterministic operations (web requests, LLM prompts) require the **leader/validator consensus pattern**. The leader executes the operation first, then validators independently verify the result. + +This is done through GenVM's `RunNondet` GL call, which triggers the `handle_consensus_stage` entry point on both leader and validator nodes. + +### How Consensus Works + +1. Your `handle_main` sends a `RunNondet` GL call with payload data +2. GenVM calls `handle_consensus_stage` on the **leader** node with `ConsensusStageData::Leader` +3. The leader performs the non-deterministic operation and returns a result +4. GenVM calls `handle_consensus_stage` on each **validator** with `ConsensusStageData::Validator { leaders_result }`, containing the leader's result +5. Validators perform their own operation, compare with the leader's result, and vote `true` (agree) or `false` (disagree) + +### GL Calls + +All interactions with the GenVM host go through `gl_call`. You encode a request as calldata, call `wasi::gl_call`, and read the response from the returned file descriptor: + +```rust +use genlayer_sdk::abi::wasi; +use genlayer_sdk::calldata::{self, Value}; +use std::collections::BTreeMap; +use std::io::Read; +use std::os::fd::FromRawFd; + +fn gl_call_with_response(message: &Value) -> Result { + let encoded = calldata::encode(message); + let fd = wasi::gl_call(&encoded).map_err(|e| e.to_string())?; + + if fd == u32::MAX { + return Ok(Value::Null); // no response data + } + + let mut file = unsafe { std::fs::File::from_raw_fd(fd as i32) }; + let mut buffer = Vec::new(); + file.read_to_end(&mut buffer).map_err(|e| e.to_string())?; + + let value = calldata::decode(&buffer).map_err(|e| format!("{e:?}"))?; + + // GL calls wrap responses in {"ok": ...} or {"error": ...} + if let Value::Map(ref map) = value { + if let Some(ok_value) = map.get("ok") { + return Ok(ok_value.clone()); + } + if let Some(err_value) = map.get("error") { + return Err(format!("{err_value:?}")); + } + } + + Ok(value) +} +``` + +### Fetching a Webpage + +To fetch a webpage from a consensus stage handler, use the `WebRender` GL call: + +```rust +fn fetch_webpage(url: &str) -> Result { + let message = Value::Map(BTreeMap::from([( + "WebRender".to_owned(), + Value::Map(BTreeMap::from([ + ("mode".to_owned(), Value::Str("text".to_owned())), + ("url".to_owned(), Value::Str(url.to_owned())), + ("wait_after_loaded".to_owned(), Value::Str("0ms".to_owned())), + ])), + )])); + + let response = gl_call_with_response(&message)?; + + #[derive(serde::Deserialize)] + struct WebRenderResponse { text: String } + + let parsed: WebRenderResponse = calldata::from_value(response) + .map_err(|e| e.to_string())?; + Ok(parsed.text) +} +``` + + + `WebRender` (and other non-deterministic operations) can only be called from within `handle_consensus_stage`. Calling them from `handle_main` directly will result in an error. + + +### Running a Non-Deterministic Operation + +From `handle_main`, you trigger the consensus flow using `RunNondet`. The `data_leader` and `data_validator` payloads are passed back to `handle_consensus_stage`: + +```rust +use genlayer_sdk::abi::entry::contract_def::LeaderResult; + +fn run_nondet(entry_data: &[u8]) -> Result { + #[derive(serde::Serialize)] + struct RunNondet { + #[serde(with = "serde_bytes")] + data_leader: Vec, + #[serde(with = "serde_bytes")] + data_validator: Vec, + } + + let request = RunNondet { + data_leader: entry_data.to_vec(), + data_validator: entry_data.to_vec(), + }; + + let message = calldata::to_value(&request).map_err(|e| e.to_string())?; + let wrapped = Value::Map(BTreeMap::from([ + ("RunNondet".to_owned(), message), + ])); + + let encoded = calldata::encode(&wrapped); + let fd = wasi::gl_call(&encoded).map_err(|e| e.to_string())?; + + if fd == u32::MAX { + return Err("no response from RunNondet".to_owned()); + } + + let mut file = unsafe { std::fs::File::from_raw_fd(fd as i32) }; + let mut buffer = Vec::new(); + file.read_to_end(&mut buffer).map_err(|e| e.to_string())?; + + let result = LeaderResult::parse(&buffer).map_err(|e| e.to_string())?; + + match result { + LeaderResult::Return(value) => Ok(value), + LeaderResult::UserError(msg) => Err(msg), + LeaderResult::VmError(msg) => Err(format!("vm error: {msg}")), + } +} +``` + +### Full Example: Fetch and Verify a Webpage + +This contract fetches a webpage and uses leader/validator consensus to verify the content: + +```rust +use std::collections::BTreeMap; +use std::io::Read; +use std::os::fd::FromRawFd; + +use genlayer_sdk::abi::entry::MessageData; +use genlayer_sdk::abi::entry::contract_def::{ + ConsensusStageData, Contract, LeaderResult, +}; +use genlayer_sdk::abi::wasi; +use genlayer_sdk::calldata::{self, Value}; + +const TARGET_URL: &str = "https://example.org"; + +// -- gl_call_with_response and fetch_webpage as shown above -- +# fn gl_call_with_response(message: &Value) -> Result { +# let encoded = calldata::encode(message); +# let fd = wasi::gl_call(&encoded).map_err(|e| e.to_string())?; +# if fd == u32::MAX { return Ok(Value::Null); } +# let mut file = unsafe { std::fs::File::from_raw_fd(fd as i32) }; +# let mut buffer = Vec::new(); +# file.read_to_end(&mut buffer).map_err(|e| e.to_string())?; +# let value = calldata::decode(&buffer).map_err(|e| format!("{e:?}"))?; +# if let Value::Map(ref map) = value { +# if let Some(ok) = map.get("ok") { return Ok(ok.clone()); } +# if let Some(err) = map.get("error") { return Err(format!("{err:?}")); } +# } +# Ok(value) +# } +# fn fetch_webpage(url: &str) -> Result { +# let message = Value::Map(BTreeMap::from([( +# "WebRender".to_owned(), +# Value::Map(BTreeMap::from([ +# ("mode".to_owned(), Value::Str("text".to_owned())), +# ("url".to_owned(), Value::Str(url.to_owned())), +# ("wait_after_loaded".to_owned(), Value::Str("0ms".to_owned())), +# ])), +# )])); +# let response = gl_call_with_response(&message)?; +# #[derive(serde::Deserialize)] +# struct R { text: String } +# let parsed: R = calldata::from_value(response).map_err(|e| e.to_string())?; +# Ok(parsed.text) +# } + +#[derive(Default)] +pub struct FetchContract; + +impl Contract for FetchContract { + fn handle_main( + &mut self, + _message: MessageData, + _data: bytes::Bytes, + ) -> Result { + // Trigger consensus -- both leader and validator will run + // handle_consensus_stage with this data + let entry_data = calldata::encode(&Value::Null); + run_nondet(&entry_data) + } + + fn handle_sandbox( + &mut self, + _message: MessageData, + _data: bytes::Bytes, + ) -> Result, String> { + unimplemented!() + } + + fn handle_consensus_stage( + &mut self, + _message: MessageData, + _data: bytes::Bytes, + stage_data: ConsensusStageData, + ) -> Result { + match stage_data { + ConsensusStageData::Leader => { + // Leader fetches the page and returns the content + let content = fetch_webpage(TARGET_URL)?; + Ok(Value::Str(content)) + } + ConsensusStageData::Validator { leaders_result } => { + let LeaderResult::Return(Value::Str(leader_content)) = leaders_result + else { + return Ok(Value::Bool(false)); // disagree + }; + + // Validator fetches independently and compares + let our_content = fetch_webpage(TARGET_URL)?; + Ok(Value::Bool(leader_content == our_content)) + } + } + } +} + +# fn run_nondet(entry_data: &[u8]) -> Result { +# // ... as shown above +# todo!() +# } + +genlayer_sdk::contract_main!(FetchContract); +``` + +### Other GL Call Operations + +Beyond `WebRender`, the GL call interface supports: + +| GL Call | Description | +|---------|-------------| +| `WebRender` | Render a webpage (text, HTML, or screenshot) | +| `WebRequest` | Make an HTTP request (GET, POST, etc.) | +| `ExecPrompt` | Run an LLM prompt | +| `ExecPromptTemplate` | Run a templated LLM prompt (comparative, non-comparative) | +| `CallContract` | Call another contract | +| `PostMessage` | Send a message to another contract | +| `DeployContract` | Deploy a new contract | +| `EthCall` / `EthSend` | EVM interoperability | +| `EmitEvent` | Emit a blockchain event | + +All follow the same pattern: construct a `Value::Map` with the operation name as key, encode with `calldata::encode`, and call `wasi::gl_call`. diff --git a/pages/developers/rust-contracts/introduction.mdx b/pages/developers/rust-contracts/introduction.mdx new file mode 100644 index 00000000..025b2a65 --- /dev/null +++ b/pages/developers/rust-contracts/introduction.mdx @@ -0,0 +1,31 @@ +import { RustSdkExperimental } from '../../../components/RustSdkExperimental' + +# Rust Contracts + + + +GenLayer supports writing intelligent contracts in Rust via the [`genlayer_sdk`](https://crates.io/crates/genlayer_sdk) crate. Rust contracts compile to WebAssembly (`wasm32-wasip1`) and run inside GenVM just like Python contracts. + +### Why Rust? + +- **Performance** -- Rust compiles to efficient WebAssembly with no runtime overhead. +- **Type safety** -- The storage system leverages Rust's type system to catch errors at compile time. +- **Low-level control** -- Direct access to GenVM host functions when needed. + +### How It Works + +Rust contracts implement the `Contract` trait (or the higher-level `ContractExt` trait), which defines handlers for GenVM's three entry points: + +- **Main** -- regular contract method calls and deployment +- **Sandbox** -- isolated execution context +- **ConsensusStage** -- leader/validator consensus for non-deterministic operations + +The `contract_main!` macro generates the `main` function that bridges GenVM's WASI interface to your trait implementation. + +### Prerequisites + +- Rust toolchain (stable) +- The `wasm32-wasip1` target: + ```bash + rustup target add wasm32-wasip1 + ``` diff --git a/pages/developers/rust-contracts/storage.mdx b/pages/developers/rust-contracts/storage.mdx new file mode 100644 index 00000000..94a7ff0b --- /dev/null +++ b/pages/developers/rust-contracts/storage.mdx @@ -0,0 +1,239 @@ +import { Callout } from 'nextra-theme-docs' +import { RustSdkExperimental } from '../../../components/RustSdkExperimental' + +# Storage + + + +Intelligent contracts store data on chain in persistent storage. The Rust SDK provides a type-safe storage system where each type maps to a **handle** that reads and writes directly from blockchain storage slots. Storage starts zero-initialized. + +### Storage Concepts + +Storage is organized around two core traits: + +- **`StorageType`** -- maps a value type to its storage handle and size +- **`StorageValue`** -- provides `get()` and `set()` on scalar handles + +A **`Slot`** is a 32-byte storage region. Complex types derive child slots via SHA3-256 hashing. + +### Defining Contract State with `record!` + +The `record!` macro defines a struct whose fields are laid out in storage: + +```rust +use genlayer_sdk::storage::{Root, DynArray}; +use genlayer_sdk::calldata::Address; + +genlayer_sdk::record!(MyState { + owner: Address, + counter: u32, + items: DynArray, +}); +``` + +Each field becomes a method that returns the field's storage handle. You use `.get()` and `.set()` on scalar handles: + +```rust +let root = Root::::get(); +let state = root.contract_instance().get(); + +// Read +let owner = state.owner().get(); +let count = state.counter().get(); + +// Write +state.counter().set(count + 1); +``` + + + `Root::::get()` returns the contract root, which wraps your state type `T` along with internal fields (contract code, locked slots, upgraders). Always access your state via `root.contract_instance().get()`. + + +### Scalar Types + +| Rust Type | Storage Handle | Size | +|------------|------------------|---------| +| `u8`..`u128` | `StorageU8`..`StorageU128` | 1..16 bytes | +| `i8`..`i128` | `StorageI8`..`StorageI128` | 1..16 bytes | +| `f32`, `f64` | `StorageF32`, `StorageF64` | 4, 8 bytes | +| `bool` | `StorageBool` | 1 byte | +| `Address` | `StorageAddress` | 20 bytes | +| `U256` | `StorageU256` | 32 bytes | +| `String` | `StorageStr` | indirect | +| `Vec` | `StorageBytes` | indirect | +| `()` | -- | 0 bytes | + +Scalar handles have `.get()` and `.set()` methods: + +```rust +state.counter().set(42); +let val = state.counter().get(); // 42 +``` + +`StorageStr` and `StorageBytes` have additional methods: + +```rust +state.name().store("Alice"); +let name = state.name().load(); // "Alice" +let len = state.name().len(); +``` + +### Fixed-Size Arrays + +Use `[T; N]` for fixed-size arrays. The handle is `StorageArray`: + +```rust +genlayer_sdk::record!(Board { + cells: [u8; 9], +}); + +let board = /* ... */; +board.cells().index(0).set(1); +let val = board.cells().index(4).get(); +``` + +### Dynamic Arrays (`DynArray`) + +`DynArray` is a variable-length array stored indirectly (like Python's `DynArray`): + +```rust +use genlayer_sdk::storage::{DynArray, Slot, StorageType}; + +let arr = >::handle_at(slot, 0); + +// Append elements +arr.append_slot().set(10); +arr.append_slot().set(20); + +// Read +let len = arr.len(); // 2 +let val = arr.index(0).get(); // 10 + +// Remove +arr.pop(); +arr.clear(); +``` + +### Tree Maps (`TreeMap`) + +`TreeMap` is a sorted key-value map backed by an AVL tree (like Python's `TreeMap`): + +```rust +use genlayer_sdk::storage::TreeMap; + +genlayer_sdk::record!(Balances { + balances: TreeMap, +}); + +let state = /* ... */; +let balances = state.balances(); + +// Insert / update (returns the value handle) +if let Some(handle) = balances.get(&addr) { + let cur = handle.get(); + handle.set(cur + amount); +} else { + balances.insert(&addr).set(amount); +} + +// Remove +balances.remove(&addr); +``` + +### Nested Records + +Records can be nested and generic: + +```rust +genlayer_sdk::record!(Pair[K, V] { + first: K, + second: V, +}); + +genlayer_sdk::record!(MyState { + pair: Pair[u32, u64], +}); +``` + +### Indirection + +`Indirection` stores the data at a derived slot rather than inline. This is used internally by `Root` and can be used for large nested structures: + +```rust +use genlayer_sdk::storage::Indirection; + +genlayer_sdk::record!(MyState { + big_data: Indirection>, +}); +``` + +### Default Values + +Storage is zero-initialized: + +| Type | Default value | +|------------|---------------| +| `u*`, `i*` | `0` | +| `f32/f64` | `0.0` | +| `bool` | `false` | +| `Address` | `[0; 20]` | +| `U256` | `0` | +| `String` | `""` | +| `DynArray` | empty (len 0) | +| `TreeMap` | empty | + +Records are zero-initialized recursively. + +### Full Example + +```rust +use genlayer_sdk::abi::entry::MessageData; +use genlayer_sdk::abi::entry::contract_def::Contract; +use genlayer_sdk::calldata::Value; +use genlayer_sdk::storage::Root; + +genlayer_sdk::record!(CounterState { + counter: u32, +}); + +#[derive(Default)] +pub struct CounterContract; + +impl Contract for CounterContract { + fn handle_main( + &mut self, + _message: MessageData, + _data: bytes::Bytes, + ) -> Result { + let root = Root::::get(); + let state = root.contract_instance().get(); + + let current = state.counter().get(); + state.counter().set(current + 1); + + Ok(Value::Map(std::collections::BTreeMap::from([( + "counter".to_owned(), + Value::Int(num_bigint::BigInt::from(current + 1)), + )]))) + } + + fn handle_sandbox( + &mut self, + _message: MessageData, + _data: bytes::Bytes, + ) -> Result, String> { + unimplemented!() + } + + fn handle_consensus_stage( + &mut self, + _message: MessageData, + _data: bytes::Bytes, + _stage_data: genlayer_sdk::abi::entry::contract_def::ConsensusStageData, + ) -> Result { + unimplemented!() + } +} + +genlayer_sdk::contract_main!(CounterContract); +```