Core Architecture (app-swap)
The core architecture is a pure Rust state machine that processes events to update the application state. It’s designed for testability, zkVM portability, and a clean separation between state logic and the external runtime.
State Transition Function
Defined in app-swap/core/src/app.rs.
#![allow(unused)] fn main() { pub fn state_transition<S: Storage>(state: &mut S, events: &Events) { /* ... */ } }
- Role: consumes a batch of
events, routes them to domain handlers, and mutates the state store deterministically. - Handlers:
Deposit,Withdraw,Transfer,Swap,AddLiquidity,RemoveLiquidity. - Logging: emits withdrawal logs via
state.log(hasher, log_hash, chain_id)so withdrawals can be proven later (see Storage).
Why this pattern:
- Separation of concerns: pure state machine + generic node that feeds events and serves queries.
- Deterministic roots: identical inputs yield the same
state_rootandevents_hash. - Proving-ready: a lighter state object can be passed in that only holds enough data to execute the corresponding events (stateless execution in zkVM).
- Reusable node: plug your
state_transitionandquerieswithout bespoke node logic.
Events
Defined in app-swap/core/src/app/events.rs.
#![allow(unused)] fn main() { pub fn decode_event(event: &[u8]) -> Option<Event> { /* ... */ } }
- Role: take an event blob and parse it into event objects.
- Format: event data is data that was logged from the contract and represents a call that was made to the contract along with extra details like
chain_id,block_numberandsender:[4 bytes selector][4 bytes chain id][8 bytes block number][20 bytes sender][remaining calldata ...]. - Events:
Deposit,Withdraw,Transfer,Swap,AddLiquidity,RemoveLiquidity.
Why this pattern:
- Minimizes the Solidity needed to write while keeping interactions simple: users still just call a function on a contract; the node replays the logged call off-chain.
- The baked-in
chain_idallows for identifying events occurring on different chains (cross-chain friendly). - The inclusion of
block_numberallows for resequencing of events on a per-block basis within thestate_transitionfunction.
Storage
Defined in app-swap/core/src/app/storage.rs.
#![allow(unused)] fn main() { pub trait Storage { //account balances fn account_balance(&mut self, account: &Address, token: &U32) -> U64; fn update_balance(&mut self, account: &Address, token: &U32, amount: &U64); //account data fn account_data(&mut self, account: &Address, index: &U64) -> Option<&Bytes32>; fn update_data(&mut self, account: &Address, index: &U64, data: Option<&Bytes32>); //logs (withdrawals) fn next_log_nonce(&mut self, chain_id: Option<&U32>) -> Bytes32; fn log<H: Hasher>(&mut self, hasher: &H, log_hash: &Bytes32, chain_id: Option<&U32>) -> Bytes32; } }
- Role: abstract persistent state access and mutation, providing a unified interface for reading and updating account balances, per-account data, and withdrawal logs. This enables the state machine to remain agnostic to the underlying storage backend, whether it's in-memory for testing, a merkle trie for production, or a ZK-friendly variant for proving.
- Implementation: a generic implementation is provided for any
Statethat also implementsKvStore:impl<T: KvStore<Key = Bytes32, Value = Bytes32>> Storage for T { /* ... */ } - Types used: (see
app-swap/core/src/lib.rs)AppState = BinaryTrieStore<StorageHash>: persistent KV store over a binary Merkle trie; its root is the app state root.ZkAppState = ZkBinaryTrieStore<StorageHash>: same API plus witness generation for proving.ZkAppStateWitness = BinaryTrieWitness<StorageHash>: a lighter-weight version of the binary Merkle trie which only carries enough data to prove specificevents.
Why this pattern:
- Abstracts having to deal with key generation so the rest of the code can operate at a higher conceptual level of storage.
- Any backend implementing the
KvStoretrait can be swapped transparently. This enables switching to "witness" variants during zk proving and adopting more optimized state objects later without changing app logic. - Keys/values live in a Merkle trie, enabling compact membership proofs and stable state roots for verification.
Queries
Defined in app-swap/core/src/queries.rs.
#![allow(unused)] fn main() { pub fn account_balance_a<S: StorageReader>(store: &S, account: &Address) -> U64 { /* ... */ } pub fn account_balance_b<S: StorageReader>(store: &S, account: &Address) -> U64 { /* ... */ } pub fn account_balance_lp<S: StorageReader>(store: &S, account: &Address) -> U64 { /* ... */ } /* ... */ }
- Role: provide read-only accessors for querying the current state, such as balances and liquidity positions, without mutating storage.
- Serving: these functions enable clients and external services to retrieve up-to-date information from the state machine in a consistent and efficient manner.
Why this pattern:
- Pluggable into various server solutions (JSON-RPC, REST, GraphQL, serverless handlers) since queries are pure read helpers decoupled from transport.
- Safer servers: read-only boundaries reduce blast radius and simplify auth/rate limits.