Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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_root and events_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_transition and queries without 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_number and sender: [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_id allows for identifying events occurring on different chains (cross-chain friendly).
  • The inclusion of block_number allows for resequencing of events on a per-block basis within the state_transition function.

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 State that also implements KvStore: 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 specific events.

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 KvStore trait 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.