Skip to main content

State Model

Lyquid's state model eliminates manual serialization and gives you direct, byte-addressable access to persistent memory through Direct Memory Architecture (DMA).

Declare state with exactly one state! block in the crate root:

state! {
network my_shared_counter: u64 = 0;
network node_names: HashMap<NodeID, String> = new_hashmap();
// use std::collections::VecDeque
instance bounded_buffer: VecDeque<MyEntry> = VecDeque::new();
}

Both network state and instance state are backed by DMA and accessible through WASM memory during runtime. They differ in how they are coordinated:

  • Network state is versioned and consistent across all instances of the same Lyquid, because it is modified only by consensus-driven, sequenced network function calls.
  • Instance state is not versioned and belongs to one node instance, but otherwise has the same persistent, low-overhead access model.

Conceptually, declared network and instance variables are named entries in an embedded key-value store mirrored into memory. This is only an organizational analogy: state variables are not serialized object-store entries, and their types do not need to implement serde::Serialize or serde::Deserialize. Reads and writes use the declared Rust values directly, keeping access lightweight.

Supported syntax in the state definition block:

PurposeSyntax
Generic Network Variablenetwork <name>: <type> = <initializer expression>;
Generic Instance Variableinstance <name>: <type> = <initializer expression>;
Oracle Topic Network Variablenetwork oracle <name>;

<name> follows the standard Rust syntax of an identifier, and <type> follows the standard Rust syntax for a type. Ordinary network and instance variables require an <initializer expression> that yields the initial value for the matching type. Usually, use default initializers such as 0, String::new(), Vec::new(), new_hashmap(), or Type::default() for unsurprising values. Some state variables may instead need a meaningful initial value.

Network State (Global, Shared)

  • Identical across all nodes running the same Lyquid at a given state version.
  • Modified via network functions.
  • Versioned by LyquidNumber and tracked with standard "on-chain" guarantees.
  • Ideal for shared states like cloud-side metadata, ground truth, registry, role assignments, configurations, permissions, or account balances.
state! {
network config: Option<Config> = None;
network total_supply: U256 = U256::ZERO;
network balances: HashMap<Address, U256> = new_hashmap();
network allowances: HashMap<(Address, Address), U256> = new_hashmap();
}

Network state is the Lyquid equivalent of cloud or "on-chain" state. It is replicated in the versioned state store at each node that hosts the Lyquid and versioned by LyquidNumber. Network state can be:

  • read and written from mutable network contexts, ctx: &mut _,
  • read from immutable network contexts, ctx: &_,
  • read from mutable and immutable instance contexts.

Network state cannot be written from instance functions. If an instance function needs to change network state, it must submit a network call (directly signed or certified) through sequencing.

Network variables are accessed directly: read with a deref (*ctx.network.total_supply) or by calling methods on the value (ctx.network.balances.get(&account)), and write through a mutable network context (*ctx.network.total_supply = value).

Instance State (Local, Node-Specific)

  • The state data are private to each node (and the node's operator). There is no consistency guarantee across nodes, despite their same variable name/layout.
  • Read and written by instance functions.
  • Not versioned like network state.
  • Ideal for computationally intensive states, protocol states, caches, private secrets, or other node-local data.
state! {
instance counter: u64 = 0;
instance user_sessions: HashMap<Uuid, Session> = new_hashmap();
instance api_key: String = String::new();
instance tx_history: Vec<Transaction> = Vec::new();
}

Instance state persists within each node instance. The value for a given state variable may differ from node to node, depending on which instance functions have run locally. Instance state can be:

  • read and written from mutable instance contexts, ctx: &mut _, including UPC request handlers,
  • read from immutable instance contexts, ctx: &_.

Instance variables are wrapped in an RwLock. Acquire a guard with .read() or .write() before use, even inside a mutable ctx: &mut _ instance function: ctx.instance.user_sessions.write(), ctx.instance.api_key.read().

Because off-chain execution is done by multi-threaded workers, the read-write lock on each instance variable is what makes concurrent access safe (e.g., ctx.instance.tx_history.write().push(tx)).

State Variable Types

State variables can use normal Rust types. A state! root is a typed Rust value allocated in persistent memory; it does not need a storage schema, serialization traits, or special wrapper traits for structs, enums, or collections. See Memory and Persistence for the underlying memory model.

The main caveat is hidden state outside the declared value. Only state declared in state! is intended to persist from call to call, so do not use mutable static variables for Lyquid state. This applies to third-party libraries too: avoid state variable types that depend on thread-local caches, OS handles, singleton registries, or other process-local resources. Those resources are not part of Lyquid state, even if a type that uses them can compile. If you need pseudo-randomness, store the PRNG seed and evolving state in declared state.

The common pitfall is hash maps and hash sets. std::collections::HashMap and std::collections::HashSet use randomized hash state by default, chosen from process-local randomness when a map or set is constructed. Ordered collections such as std::collections::BTreeMap and std::collections::BTreeSet do not have this hasher issue. For hash-based maps and sets, use the deterministic collection aliases exported by the LDK prelude:

pub type HashMap<K, V> = hashbrown::HashMap<K, V, ahash::RandomState>;
pub type HashSet<K> = hashbrown::HashSet<K, ahash::RandomState>;
note

Use new_hashmap() / new_hashset() (from the prelude) instead of HashMap::new() to initialize map/set state variables. They construct the deterministically seeded HashMap/HashSet types that the LDK re-exports, keeping iteration order consistent across all nodes. This matters for deterministic behavior in network state.

For a complete specification, see state!.