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:
| Purpose | Syntax |
|---|---|
| Generic Network Variable | network <name>: <type> = <initializer expression>; |
| Generic Instance Variable | instance <name>: <type> = <initializer expression>; |
| Oracle Topic Network Variable | network 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>;
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!.