Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
495 changes: 495 additions & 0 deletions apps/transfers/README.md

Large diffs are not rendered by default.

97 changes: 93 additions & 4 deletions apps/transfers/src/api_state.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,29 @@
//! API-specific state management for the Transfers bridge.
//!
//! This module manages state that is used for serving API queries but is **not included**
//! in the cryptographic state commitments. This includes:
//! - Pending withdrawal proofs waiting to be retrieved by users
//! - Latest signed state commitment for attaching to withdrawal proofs
//! - Block height/hash tracking for synchronization
//!
//! # Separation of Concerns
//!
//! The split between `AppState` (proven) and `ApiState` (not proven) enables:
//! - **Minimal commitments**: Only essential state (merkle roots) is signed
//! - **Efficient queries**: Withdrawal proofs are pre-computed and cached
//! - **Flexible storage**: API state could be moved to a separate database in the future
//!
//! # Withdrawal Proof Lifecycle
//!
//! 1. User calls `requestWithdrawal()` on-chain
//! 2. Off-chain app processes `WithdrawalRequest` event
//! 3. `handle_withdrawal_request` creates `PendingWithdrawal` and stores in `stored_proofs`
//! 4. State transition completes and commitment is signed
//! 5. User queries `/get-merkle-proof/{withdrawal_id}` API
//! 6. API combines `PendingWithdrawal` + `latest_commitment` → `Withdrawal`
//! 7. User submits `Withdrawal` to `executeWithdrawal()` on-chain
//! 8. After execution, `WithdrawalCompleted` event triggers proof cleanup

use std::collections::HashMap;

use alloy::primitives::{Address, B256, U256};
Expand All @@ -13,15 +39,30 @@ use crate::{
state::State,
};

/// API-only state for serving queries and managing withdrawal proofs.
///
/// This state is not included in cryptographic commitments and is used purely
/// for API functionality. It tracks:
/// - Block synchronization state (height and hash)
/// - Pending withdrawal proofs waiting for user retrieval
/// - Latest signed commitment for attaching to withdrawal proofs
#[derive(Default)]
pub struct ApiState {
/// Height of the most recent processed block
pub latest_block_height: Option<u64>,
/// Hash of the most recent processed block
pub latest_block_hash: [u8; 32],

/// Stored withdrawal proofs (burn_index -> proof)
/// Pending withdrawal proofs indexed by withdrawal_id
///
/// Proofs are added when `WithdrawalRequest` events are processed
/// and removed when `WithdrawalCompleted` events are processed.
pub stored_proofs: HashMap<B256, PendingWithdrawal>,

/// Signed Commitment of the latest state
/// Most recent signed state commitment
///
/// This is attached to withdrawal proofs so users can submit them
/// on-chain with a signature that verifies the burn tree root.
// TODO: Consider removing the Option and always having a commitment (a signed empty commitment
// at genesis)
pub latest_commitment: Option<Signed<Height<Commit>>>,
Expand All @@ -37,11 +78,31 @@ impl UpdateLatestBlock for ApiState {
}
}

/// Combined view of application state and API state.
///
/// This struct provides simultaneous access to both:
/// - `app`: Read-only access to application state (for querying balances and generating proofs)
/// - `api`: Mutable access to API state (for storing withdrawal proofs and commitments)
///
/// Used by `api_state::update` to create withdrawal proofs after state transitions complete.
pub struct AllState<'a, App: AccountStateRef> {
pub app: &'a App,
pub api: &'a mut ApiState,
}

/// Updates API state after a block's state transitions are complete.
///
/// This function runs after `app_state::state_transition` has updated the merkle trees.
/// It processes withdrawal-related events to manage the proof storage:
/// - `WithdrawalRequest`: Generates and stores merkle proof for user retrieval
/// - `WithdrawalCompleted`: Cleans up stored proof after on-chain execution
///
/// Other event types (`Deposit`, `TransferRequest`) don't require API state updates.
///
/// # Arguments
///
/// * `block` - The block of events that was just processed
/// * `state` - Combined access to app state (read-only) and API state (mutable)
pub fn update(block: &Block, mut state: AllState<'_, impl AccountStateRef + AccountProofs>) {
for bytes in &block.events {
let Ok(input) = decode_input(bytes) else {
Expand All @@ -67,7 +128,26 @@ pub fn update(block: &Block, mut state: AllState<'_, impl AccountStateRef + Acco
}
}

/// Process a withdrawal request - burns tokens and generates withdrawal proof
/// Generates and stores a merkle proof for a withdrawal request.
///
/// After the application state has burned the user's tokens and recorded the burn event
/// in the burn tree, this function:
/// 1. Creates a `Burn` struct with the withdrawal details
/// 2. Captures the balance tree root at the time of the burn
/// 3. Generates a merkle proof from the burn tree
/// 4. Stores the `PendingWithdrawal` in `stored_proofs` for API retrieval
///
/// Users can then query `/get-merkle-proof/{withdrawal_id}` to retrieve the proof.
///
/// # Arguments
///
/// * `state` - Combined app and API state access
/// * `user` - The account requesting withdrawal
/// * `token` - The ERC20 token being withdrawn
/// * `amount` - The amount of tokens to withdraw
/// * `withdrawal_id` - Unique identifier for this withdrawal
/// * `salt` - Random value used to generate the withdrawal ID
/// * `chain_id` - Destination chain for the withdrawal
pub fn handle_withdrawal_request(
state: &mut AllState<'_, impl AccountStateRef + AccountProofs>,
user: Address,
Expand Down Expand Up @@ -108,7 +188,16 @@ pub fn handle_withdrawal_request(
.insert(withdrawal_id, pending_withdrawal.clone());
}

/// Handle withdrawal completed event - cleanup stored proof
/// Cleans up a withdrawal proof after on-chain execution.
///
/// When a `WithdrawalCompleted` event is received, it means the user successfully
/// executed their withdrawal on-chain. The stored proof is no longer needed and
/// is removed to free memory.
///
/// # Arguments
///
/// * `state` - Combined app and API state access
/// * `withdrawal_id` - The unique identifier of the completed withdrawal
pub fn handle_withdrawal_completed(
state: &mut AllState<'_, impl AccountStateRef>,
withdrawal_id: B256,
Expand Down
Loading