Environment & host functions
Everything your app can ask the node to do at runtime goes through a host
function. The SDK wraps each one as a plain Rust function under
calimero_sdk::env, defined in crates/sdk/src/env.rs. On wasm32 these call
into the host across the WASM boundary; off-wasm32 (the in-process test
harness) they route to a native mock.
This page is the app-author reference: what can my app call, and what does it
do. For the wire-level ABI (registers, buffer descriptors, return-code
conventions) see execution and the runtime’s
crates/runtime/HOST_FUNCTIONS.md.
Context & identity
Section titled “Context & identity”Who is running, in which context, and on whose behalf.
| Function | What it does | Gotcha |
|---|---|---|
env::context_id() -> [u8; 32] | The 32-byte ID of the context this execution runs in. | Same for every member of the context. Not an identity — don’t use it to authorize a caller. |
env::executor_id() -> [u8; 32] | The 32-byte public key of the identity executing this call. | This is “who is calling” within the context. Use it as the actor for per-user logic. |
env::xcall_origin() -> Option<[u8; 32]> | The context that dispatched the current cross-context call, or None for a direct/RPC call. | Set by the node from the calling context — it cannot be forged. Return-None lets a method reject direct calls and require xcall-only invocation. |
env::input() -> Option<Vec<u8>> | The raw, undecoded input bytes for this call (typically JSON-encoded arguments). | #[app::logic] already decodes method arguments for you. You only need input() for custom entry points or raw payloads. |
Output
Section titled “Output”How a method’s return value (or error) leaves the WASM module.
| Function | What it does | Gotcha |
|---|---|---|
env::value_return(result: &Result<T, E>) | Sets the final return value — the Ok or Err variant the caller receives. | Macro-driven. You don’t call this; the #[app::logic] codegen serializes your method’s return value to JSON and calls it. |
env::commit(root_hash: &[u8; 32], artifact: &[u8]) | Commits the execution’s state root and CRDT artifact for persistence and sync. | Macro-driven. Emitted by the generated method wrapper. Called exactly once per mutating call; never call it yourself. |
Logging
Section titled “Logging”| Function | What it does | Gotcha |
|---|---|---|
env::log(message: &str) | Appends a UTF-8 line to the execution log, surfaced in the node logs and the execution outcome. | Logs are bounded (max_logs, max_log_size). Prefer the app::log!("..", x) macro for formatting. Logging is a side effect, not state — it does not sync. |
tracing integration
Section titled “tracing integration”With the optional tracing cargo feature enabled, the SDK installs a
host-backed tracing subscriber at method entry (env::init_logging(), wired
into every generated export). From then on, tracing::info!/debug!/… calls —
from your app and from any crate it depends on — are formatted and forwarded
to the same host log as env::log, with no per-app setup.
Events
Section titled “Events”Structured notifications for off-chain consumers (WebSocket subscribers, the
admin API). An event type implements AppEvent; in practice you derive it with
#[app::event].
| Function | What it does | Gotcha |
|---|---|---|
env::emit(event: &T) | Emits an event with a kind and serialized data. | Bounded by max_events / max_event_data_size. Events are observational — they don’t mutate state and aren’t replayed during sync. |
env::emit_with_handler(event: &T, handler: &str) | Emits an event and names a handler method to run after it is processed. | Handlers may run in parallel and out of order — they must be commutative, idempotent, and side-effect-free (CRDT-only). See the SDK event module notes. |
Crypto
Section titled “Crypto”| Function | What it does | Gotcha |
|---|---|---|
env::ed25519_verify(signature: &[u8; 64], public_key: &[u8; 32], message: &[u8]) -> bool | Verifies an Ed25519 signature over message for public_key. | Returns true/false; it does not bail on an invalid signature — check the bool. Has no native mock: it panics under TestHost. |
Example: accepting an off-chain-signed voucher
Section titled “Example: accepting an off-chain-signed voucher”A voucher is signed by a trusted issuer off the network; the app accepts it
only if the signature checks out, then records the redemption and emits an event
so off-chain consumers can react. This pairs env::ed25519_verify (trust the
payload) with app::emit!/env::emit (announce the outcome).
use calimero_sdk::{app, env};use calimero_storage::collections::UnorderedSet;
#[app::state(emits = for<'a> Event<'a>)]pub struct Shop { issuer_pk: [u8; 32], // the trusted issuer's public key redeemed: UnorderedSet<String>, // codes already used (anti-replay)}
#[app::event]pub enum Event<'a> { VoucherRedeemed { code: &'a str, amount: u64 },}
#[app::logic]impl Shop { pub fn redeem( &mut self, code: String, amount: u64, signature: [u8; 64], ) -> app::Result<()> { // The issuer signs the exact bytes `"<code>:<amount>"` off-chain. // Reconstruct them identically and verify before trusting anything. let message = format!("{code}:{amount}"); if !env::ed25519_verify(&signature, &self.issuer_pk, message.as_bytes()) { app::bail!("invalid voucher signature"); } // `ed25519_verify` returns a bool — a bad signature does not bail on // its own, so the guard above is load-bearing. if !self.redeemed.insert(code.clone())? { app::bail!("voucher already redeemed"); } app::emit!(Event::VoucherRedeemed { code: &code, amount }); Ok(()) }}UnorderedSet::insert returns false when the value was already present, which
doubles as the anti-replay check. The signed message must be byte-identical to
what the issuer signed — a different separator or number format fails
verification.
Time & randomness
Section titled “Time & randomness”| Function | What it does | Gotcha |
|---|---|---|
env::time_now() -> u64 | The current Unix timestamp in nanoseconds. | Wall-clock; differs node-to-node. Non-deterministic — see the caution above. |
env::random_bytes(buf: &mut [u8]) | Fills buf with random bytes from the host. | Non-deterministic in production. Under the test harness it is a deterministic, non-cryptographic PRNG — don’t rely on it for security in tests. |
Cross-context calls
Section titled “Cross-context calls”| Function | What it does | Gotcha |
|---|---|---|
env::xcall(context_id: &[u8; 32], function: &str, params: &[u8]) | Queues a call to function in another context, run locally after the current execution finishes. | Fire-and-queue: it returns no value to the caller and is not executed inline. Has no native mock (panics under TestHost). Prefer the typed #[app::xcall] surface. |
See xcall for the dispatch and namespace model, and
advanced-sdk for the typed wrapper.
Streaming large binary objects. You write through a handle, finalize to get a content-addressed 32-byte ID, then anyone can open that ID for reading.
| Function | What it does | Gotcha |
|---|---|---|
env::blob_create() -> u64 | Opens a new write handle; returns a file descriptor. | The descriptor is only valid for this execution. |
env::blob_write(fd: u64, data: &[u8]) -> u64 | Appends data to the write handle; returns bytes written. | Stream in chunks for large payloads rather than one huge buffer. |
env::blob_close(fd: u64) -> [u8; 32] | Finalizes a write handle and returns the blob’s 32-byte ID (or, for a read handle, returns its ID and cleans up). | Panics if finalization fails. The ID is content-addressed — identical bytes yield the same ID. |
env::blob_open(blob_id: &[u8; 32]) -> u64 | Opens an existing blob for reading; returns a file descriptor. | Returns 0 if the blob is not found — check before reading. |
env::blob_read(fd: u64, buffer: &mut [u8]) -> u64 | Reads into buffer; returns bytes read. | Returns 0 at end of blob — loop until it does. |
env::blob_announce_to_context(blob_id: &[u8; 32], target_context_id: &[u8; 32]) -> bool | Announces a blob to a context for network discovery. | A context may only announce blobs to itself — passing any other target_context_id returns false without announcing. |
See blobs for replication and discovery.
Private storage
Section titled “Private storage”Node-local key/value storage that is NOT synchronized across the network — for secrets, node-specific configuration, or anything that must not leave the node.
| Function | What it does | Gotcha |
|---|---|---|
env::private_storage_read(key: &[u8]) -> Option<Vec<u8>> | Reads a node-local value. | None means either “no such key” or “private storage unavailable on this node” — the two are indistinguishable. |
env::private_storage_write(key: &[u8], value: &[u8]) -> bool | Writes a node-local value. | Returns true on success, false if private storage is unavailable (a no-op). This bool is success/failure — unlike synchronized storage_write, whose bool means “a previous value was evicted”. |
env::private_storage_remove(key: &[u8]) -> bool | Removes a node-local value. | true if the key existed and was removed, false if absent or unavailable. |
See permissioned-storage for the
synchronized-vs-local storage model.
JS runtime collection bridge
Section titled “JS runtime collection bridge”The Rust SDK calls the storage layer directly, so its CRDT writes flow into the
Merkle DAG and the outcome artifact automatically. The QuickJS runtime cannot —
JavaScript has no access to the Rust storage interface. Instead it funnels its
state back through a dedicated set of host functions that reproduce, host-side,
what the Rust SDK does in-process. These are defined in
crates/runtime/src/logic/host_functions/system.rs and js_collections.rs.
The root-state bridge keeps the storage DAG and the outcome artifact in sync:
| Host function | Role |
|---|---|
persist_root_state(doc_ptr, created_at, updated_at) | Saves the serialized root document through the storage interface (recomputing parent hashes and emitting a CRDT action) rather than writing raw bytes. |
read_root_state(register_id) -> i32 | Loads the persisted root document into a register; returns 1 if it exists, 0 if not. |
apply_storage_delta(delta_ptr) | Replays a Borsh-encoded StorageDelta from another executor, updating CRDT entities and the root atomically, then refreshes the root hash. |
flush_delta() -> i32 | Emits the CRDT actions recorded since the last flush as a causal delta; returns 1 if a delta was emitted, 0 if there was nothing to commit. |