Skip to content

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.

Who is running, in which context, and on whose behalf.

FunctionWhat it doesGotcha
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.

How a method’s return value (or error) leaves the WASM module.

FunctionWhat it doesGotcha
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.
FunctionWhat it doesGotcha
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.

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.

Structured notifications for off-chain consumers (WebSocket subscribers, the admin API). An event type implements AppEvent; in practice you derive it with #[app::event].

FunctionWhat it doesGotcha
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.
FunctionWhat it doesGotcha
env::ed25519_verify(signature: &[u8; 64], public_key: &[u8; 32], message: &[u8]) -> boolVerifies 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.

FunctionWhat it doesGotcha
env::time_now() -> u64The 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.
FunctionWhat it doesGotcha
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.

FunctionWhat it doesGotcha
env::blob_create() -> u64Opens a new write handle; returns a file descriptor.The descriptor is only valid for this execution.
env::blob_write(fd: u64, data: &[u8]) -> u64Appends 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]) -> u64Opens 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]) -> u64Reads 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]) -> boolAnnounces 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.

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.

FunctionWhat it doesGotcha
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]) -> boolWrites 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]) -> boolRemoves 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.

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 functionRole
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) -> i32Loads 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() -> i32Emits 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.