Skip to main content

miden_client_sqlite_store/account/
accounts.rs

1//! Account-related database operations.
2
3use std::collections::BTreeMap;
4use std::rc::Rc;
5use std::string::{String, ToString};
6use std::sync::{Arc, RwLock};
7use std::vec::Vec;
8
9use miden_client::account::{
10    Account,
11    AccountCode,
12    AccountDelta,
13    AccountHeader,
14    AccountId,
15    AccountStorage,
16    Address,
17    PartialAccount,
18    PartialStorage,
19    PartialStorageMap,
20    StorageMap,
21    StorageMapKey,
22    StorageSlotName,
23    StorageSlotType,
24};
25use miden_client::asset::{Asset, AssetVault, AssetWitness, FungibleAsset};
26use miden_client::store::{
27    AccountRecord,
28    AccountRecordData,
29    AccountSmtForest,
30    AccountStatus,
31    AccountStorageFilter,
32    ClientAccountType,
33    StoreError,
34};
35use miden_client::utils::{Deserializable, Serializable};
36use miden_client::{AccountError, Felt, Word};
37use miden_protocol::account::{AccountStorageHeader, StorageMapWitness, StorageSlotHeader};
38use miden_protocol::asset::{AssetVaultKey, PartialVault};
39use miden_protocol::crypto::merkle::MerkleError;
40use rusqlite::types::Value;
41use rusqlite::{Connection, OptionalExtension, Transaction, named_params, params};
42
43use crate::account::helpers::{
44    query_account_addresses,
45    query_account_code,
46    query_historical_account_headers,
47    query_latest_account_headers,
48    query_storage_slots,
49    query_storage_values,
50    query_vault_assets,
51};
52use crate::sql_error::SqlResultExt;
53use crate::transaction::with_forest_snapshot;
54use crate::{SqliteStore, column_value_as_u64, insert_sql, subst, u64_to_value};
55
56impl SqliteStore {
57    // READER METHODS
58    // --------------------------------------------------------------------------------------------
59
60    pub(crate) fn get_account_ids(conn: &mut Connection) -> Result<Vec<AccountId>, StoreError> {
61        const QUERY: &str = "SELECT id FROM latest_account_headers";
62
63        conn.prepare_cached(QUERY)
64            .into_store_error()?
65            .query_map([], |row| row.get(0))
66            .expect("no binding parameters used in query")
67            .map(|result| {
68                let id: String = result.map_err(|e| StoreError::ParsingError(e.to_string()))?;
69                Ok(AccountId::from_hex(&id).expect("account id is valid"))
70            })
71            .collect::<Result<Vec<AccountId>, StoreError>>()
72    }
73
74    pub(crate) fn get_account_headers(
75        conn: &mut Connection,
76    ) -> Result<Vec<(AccountHeader, AccountStatus)>, StoreError> {
77        Ok(query_latest_account_headers(conn, "1=1 ORDER BY id", params![])?
78            .into_iter()
79            .map(|(header, status, _)| (header, status))
80            .collect())
81    }
82
83    pub(crate) fn get_account_header(
84        conn: &Connection,
85        account_id: AccountId,
86    ) -> Result<Option<(AccountHeader, AccountStatus)>, StoreError> {
87        Ok(query_latest_account_headers(conn, "id = ?", params![account_id.to_hex()])?
88            .pop()
89            .map(|(header, status, _)| (header, status)))
90    }
91
92    pub(crate) fn get_account_header_by_commitment(
93        conn: &mut Connection,
94        account_commitment: Word,
95    ) -> Result<Option<AccountHeader>, StoreError> {
96        let account_commitment_str: String = account_commitment.to_string();
97        Ok(query_historical_account_headers(
98            conn,
99            "account_commitment = ?",
100            params![account_commitment_str],
101        )?
102        .pop()
103        .map(|(header, _)| header))
104    }
105
106    /// Retrieves a complete account record with full vault and storage data.
107    pub(crate) fn get_account(
108        conn: &mut Connection,
109        account_id: AccountId,
110    ) -> Result<Option<AccountRecord>, StoreError> {
111        let Some((header, status, client_account_type)) =
112            query_latest_account_headers(conn, "id = ?", params![account_id.to_hex()])?.pop()
113        else {
114            return Ok(None);
115        };
116
117        let assets = query_vault_assets(conn, account_id)?;
118        let vault = AssetVault::new(&assets)?;
119
120        let slots = query_storage_slots(conn, account_id, &AccountStorageFilter::All)?
121            .into_values()
122            .collect();
123
124        let storage = AccountStorage::new(slots)?;
125
126        let Some(account_code) = query_account_code(conn, header.code_commitment())? else {
127            return Ok(None);
128        };
129
130        let account = Account::new_unchecked(
131            header.id(),
132            vault,
133            storage,
134            account_code,
135            header.nonce(),
136            status.seed().copied(),
137        );
138
139        let account_data = AccountRecordData::Full(account);
140        Ok(Some(AccountRecord::new(account_data, status, client_account_type)))
141    }
142
143    /// Retrieves a minimal partial account record with storage and vault witnesses.
144    pub(crate) fn get_minimal_partial_account(
145        conn: &mut Connection,
146        account_id: AccountId,
147    ) -> Result<Option<AccountRecord>, StoreError> {
148        let Some((header, status, client_account_type)) =
149            query_latest_account_headers(conn, "id = ?", params![account_id.to_hex()])?.pop()
150        else {
151            return Ok(None);
152        };
153
154        // Partial vault retrieval
155        let partial_vault = PartialVault::new(header.vault_root());
156
157        // Partial storage retrieval
158        let mut storage_header = Vec::new();
159        let mut maps = vec![];
160
161        let storage_values = query_storage_values(conn, account_id)?;
162
163        // Storage maps are always minimal here (just roots, no entries).
164        // New accounts that need full storage data are handled by the DataStore layer,
165        // which fetches the full account via `get_account()` when nonce == 0.
166        for (slot_name, (slot_type, value)) in storage_values {
167            storage_header.push(StorageSlotHeader::new(slot_name.clone(), slot_type, value));
168            if slot_type == StorageSlotType::Map {
169                maps.push(PartialStorageMap::new(value));
170            }
171        }
172        storage_header.sort_by_key(StorageSlotHeader::id);
173        let storage_header =
174            AccountStorageHeader::new(storage_header).map_err(StoreError::AccountError)?;
175        let partial_storage =
176            PartialStorage::new(storage_header, maps).map_err(StoreError::AccountError)?;
177
178        let Some(account_code) = query_account_code(conn, header.code_commitment())? else {
179            return Ok(None);
180        };
181
182        let partial_account = PartialAccount::new(
183            header.id(),
184            header.nonce(),
185            account_code,
186            partial_storage,
187            partial_vault,
188            status.seed().copied(),
189        )?;
190        let account_record_data = AccountRecordData::Partial(partial_account);
191        Ok(Some(AccountRecord::new(account_record_data, status, client_account_type)))
192    }
193
194    pub fn get_foreign_account_code(
195        conn: &mut Connection,
196        account_ids: Vec<AccountId>,
197    ) -> Result<BTreeMap<AccountId, AccountCode>, StoreError> {
198        let params: Vec<Value> =
199            account_ids.into_iter().map(|id| Value::from(id.to_hex())).collect();
200        const QUERY: &str = "
201            SELECT account_id, code
202            FROM foreign_account_code JOIN account_code ON foreign_account_code.code_commitment = account_code.commitment
203            WHERE account_id IN rarray(?)";
204
205        conn.prepare_cached(QUERY)
206            .into_store_error()?
207            .query_map([Rc::new(params)], |row| Ok((row.get(0)?, row.get(1)?)))
208            .expect("no binding parameters used in query")
209            .map(|result| {
210                result.map_err(|err| StoreError::ParsingError(err.to_string())).and_then(
211                    |(id, code): (String, Vec<u8>)| {
212                        Ok((
213                            AccountId::from_hex(&id).map_err(|err| {
214                                StoreError::AccountError(
215                                    AccountError::FinalAccountHeaderIdParsingFailed(err),
216                                )
217                            })?,
218                            AccountCode::read_from_bytes(&code)
219                                .map_err(StoreError::DataDeserializationError)?,
220                        ))
221                    },
222                )
223            })
224            .collect::<Result<BTreeMap<AccountId, AccountCode>, _>>()
225    }
226
227    /// Retrieves the full asset vault for a specific account.
228    pub fn get_account_vault(
229        conn: &Connection,
230        account_id: AccountId,
231    ) -> Result<AssetVault, StoreError> {
232        let assets = query_vault_assets(conn, account_id)?;
233        Ok(AssetVault::new(&assets)?)
234    }
235
236    /// Retrieves the full storage for a specific account.
237    pub fn get_account_storage(
238        conn: &Connection,
239        account_id: AccountId,
240        filter: &AccountStorageFilter,
241    ) -> Result<AccountStorage, StoreError> {
242        let slots = query_storage_slots(conn, account_id, filter)?.into_values().collect();
243        Ok(AccountStorage::new(slots)?)
244    }
245
246    /// Fetches a specific asset from the account's vault without the need of loading the entire
247    /// vault. The witness is retrieved from the [`AccountSmtForest`].
248    pub(crate) fn get_account_asset(
249        conn: &mut Connection,
250        smt_forest: &Arc<RwLock<AccountSmtForest>>,
251        account_id: AccountId,
252        vault_key: AssetVaultKey,
253    ) -> Result<Option<(Asset, AssetWitness)>, StoreError> {
254        // Acquire forest lock before getting header in order to avoid concurrent writes to it.
255        let smt_forest = smt_forest
256            .read()
257            .map_err(|_| StoreError::DatabaseError("smt_forest read lock poisoned".to_string()))?;
258        let header = Self::get_account_header(conn, account_id)?
259            .ok_or(StoreError::AccountDataNotFound(account_id))?
260            .0;
261
262        match smt_forest.get_asset_and_witness(header.vault_root(), vault_key) {
263            Ok((asset, witness)) => Ok(Some((asset, witness))),
264            Err(StoreError::MerkleStoreError(MerkleError::UntrackedKey(_))) => Ok(None),
265            Err(err) => Err(err),
266        }
267    }
268
269    /// Retrieves a specific item from the account's storage map without loading the entire storage.
270    /// The witness is retrieved from the [`AccountSmtForest`].
271    pub(crate) fn get_account_map_item(
272        conn: &mut Connection,
273        smt_forest: &Arc<RwLock<AccountSmtForest>>,
274        account_id: AccountId,
275        slot_name: StorageSlotName,
276        key: StorageMapKey,
277    ) -> Result<(Word, StorageMapWitness), StoreError> {
278        // Acquire forest lock before getting header in order to avoid concurrent writes to it.
279        let smt_forest = smt_forest
280            .read()
281            .map_err(|_| StoreError::DatabaseError("smt_forest read lock poisoned".to_string()))?;
282        let header = Self::get_account_header(conn, account_id)?
283            .ok_or(StoreError::AccountDataNotFound(account_id))?
284            .0;
285
286        let mut storage_values = query_storage_values(conn, account_id)?;
287        let (slot_type, map_root) = storage_values
288            .remove(&slot_name)
289            .ok_or(StoreError::AccountStorageRootNotFound(header.storage_commitment()))?;
290        if slot_type != StorageSlotType::Map {
291            return Err(StoreError::AccountError(AccountError::StorageSlotNotMap(slot_name)));
292        }
293
294        let witness = smt_forest.get_storage_map_item_witness(map_root, key)?;
295        let item = witness.get(key).unwrap_or(miden_client::EMPTY_WORD);
296
297        Ok((item, witness))
298    }
299
300    pub(crate) fn get_account_addresses(
301        conn: &mut Connection,
302        account_id: AccountId,
303    ) -> Result<Vec<Address>, StoreError> {
304        query_account_addresses(conn, account_id)
305    }
306
307    /// Retrieves the account code for a specific account by ID.
308    pub(crate) fn get_account_code_by_id(
309        conn: &mut Connection,
310        account_id: AccountId,
311    ) -> Result<Option<AccountCode>, StoreError> {
312        let Some((header, ..)) =
313            query_latest_account_headers(conn, "id = ?", params![account_id.to_hex()])?
314                .into_iter()
315                .next()
316        else {
317            return Ok(None);
318        };
319
320        query_account_code(conn, header.code_commitment())
321    }
322
323    // MUTATOR/WRITER METHODS
324    // --------------------------------------------------------------------------------------------
325
326    pub(crate) fn insert_account(
327        conn: &mut Connection,
328        smt_forest: &Arc<RwLock<AccountSmtForest>>,
329        account: &Account,
330        initial_address: &Address,
331        client_account_type: ClientAccountType,
332    ) -> Result<(), StoreError> {
333        with_forest_snapshot(conn, smt_forest, |tx, smt_forest| {
334            Self::insert_account_code(tx, account.code())?;
335
336            let account_id = account.id();
337            Self::insert_storage_slots(tx, account_id, account.storage().slots().iter())?;
338            Self::insert_assets(tx, account_id, account.vault().assets())?;
339            let watched = matches!(client_account_type, ClientAccountType::Watched);
340            Self::insert_new_account_header(tx, &account.into(), account.seed(), watched)?;
341            Self::insert_address(tx, initial_address, account.id())?;
342
343            smt_forest.insert_and_register_account_state(
344                account.id(),
345                account.vault(),
346                account.storage(),
347            )?;
348            Ok(())
349        })
350    }
351
352    pub(crate) fn update_account(
353        conn: &mut Connection,
354        smt_forest: &Arc<RwLock<AccountSmtForest>>,
355        new_account_state: &Account,
356    ) -> Result<(), StoreError> {
357        const QUERY: &str = "SELECT id FROM latest_account_headers WHERE id = ?";
358        if conn
359            .prepare(QUERY)
360            .into_store_error()?
361            .query_map(params![new_account_state.id().to_hex()], |row| row.get(0))
362            .into_store_error()?
363            .map(|result| {
364                result.map_err(|err| StoreError::ParsingError(err.to_string())).and_then(
365                    |id: String| {
366                        AccountId::from_hex(&id).map_err(|err| {
367                            StoreError::AccountError(
368                                AccountError::FinalAccountHeaderIdParsingFailed(err),
369                            )
370                        })
371                    },
372                )
373            })
374            .next()
375            .is_none()
376        {
377            return Err(StoreError::AccountDataNotFound(new_account_state.id()));
378        }
379
380        with_forest_snapshot(conn, smt_forest, |tx, smt_forest| {
381            Self::update_account_state(tx, smt_forest, new_account_state)
382        })
383    }
384
385    pub fn upsert_foreign_account_code(
386        conn: &mut Connection,
387        account_id: AccountId,
388        code: &AccountCode,
389    ) -> Result<(), StoreError> {
390        let tx = conn.transaction().into_store_error()?;
391
392        Self::insert_account_code(&tx, code)?;
393
394        const QUERY: &str =
395            insert_sql!(foreign_account_code { account_id, code_commitment } | REPLACE);
396
397        tx.execute(QUERY, params![account_id.to_hex(), code.commitment().to_string()])
398            .into_store_error()?;
399
400        Self::insert_account_code(&tx, code)?;
401        tx.commit().into_store_error()
402    }
403
404    pub(crate) fn insert_address(
405        tx: &Transaction<'_>,
406        address: &Address,
407        account_id: AccountId,
408    ) -> Result<(), StoreError> {
409        const QUERY: &str = insert_sql!(addresses { address, account_id } | REPLACE);
410        let serialized_address = address.to_bytes();
411        tx.execute(QUERY, params![serialized_address, account_id.to_hex(),])
412            .into_store_error()?;
413
414        Ok(())
415    }
416
417    pub(crate) fn remove_address(
418        conn: &mut Connection,
419        address: &Address,
420    ) -> Result<(), StoreError> {
421        let tx = conn.transaction().into_store_error()?;
422        let serialized_address = address.to_bytes();
423        const DELETE_QUERY: &str = "DELETE FROM addresses WHERE address = ?";
424        tx.execute(DELETE_QUERY, params![serialized_address]).into_store_error()?;
425
426        tx.commit().into_store_error()
427    }
428
429    /// Inserts an [`AccountCode`].
430    pub(crate) fn insert_account_code(
431        tx: &Transaction<'_>,
432        account_code: &AccountCode,
433    ) -> Result<(), StoreError> {
434        const QUERY: &str = insert_sql!(account_code { commitment, code } | IGNORE);
435        tx.execute(QUERY, params![account_code.commitment().to_hex(), account_code.to_bytes()])
436            .into_store_error()?;
437        Ok(())
438    }
439
440    /// Applies the account delta to the account state, updating the vault and storage maps.
441    ///
442    /// Archives old values from latest to historical and updates latest via INSERT OR REPLACE.
443    pub(crate) fn apply_account_delta(
444        tx: &Transaction<'_>,
445        smt_forest: &mut AccountSmtForest,
446        init_account_state: &AccountHeader,
447        final_account_state: &AccountHeader,
448        updated_fungible_assets: BTreeMap<AssetVaultKey, FungibleAsset>,
449        old_map_roots: &BTreeMap<StorageSlotName, Word>,
450        delta: &AccountDelta,
451    ) -> Result<(), StoreError> {
452        let account_id = final_account_state.id();
453
454        // Archive old header and insert the new one
455        Self::replace_account_header(tx, final_account_state, init_account_state)?;
456
457        Self::apply_account_vault_delta(
458            tx,
459            smt_forest,
460            account_id,
461            init_account_state,
462            final_account_state,
463            updated_fungible_assets,
464            delta,
465        )?;
466
467        // Build the final roots from the init state's registered roots:
468        // - Replace vault root with the final one
469        // - Replace changed map roots with their new values (done by apply_account_storage_delta)
470        // - Unchanged map roots continue as they were
471        let mut final_roots = smt_forest
472            .get_roots(&init_account_state.id())
473            .cloned()
474            .ok_or(StoreError::AccountDataNotFound(init_account_state.id()))?;
475
476        // First element is always the vault root
477        if let Some(vault_root) = final_roots.first_mut() {
478            *vault_root = final_account_state.vault_root();
479        }
480
481        let default_map_root = StorageMap::default().root();
482        let updated_storage_slots =
483            Self::apply_account_storage_delta(smt_forest, old_map_roots, delta)?;
484
485        // Update map roots in final_roots with new values from the delta
486        for (slot_name, (new_root, slot_type)) in &updated_storage_slots {
487            if *slot_type == StorageSlotType::Map {
488                let old_root = old_map_roots.get(slot_name).copied().unwrap_or(default_map_root);
489                if let Some(root) = final_roots.iter_mut().find(|r| **r == old_root) {
490                    *root = *new_root;
491                } else {
492                    // New map slot not in the old roots — append it
493                    final_roots.push(*new_root);
494                }
495            }
496        }
497
498        Self::write_storage_delta(
499            tx,
500            account_id,
501            final_account_state.nonce().as_canonical_u64(),
502            &updated_storage_slots,
503            delta,
504        )?;
505
506        smt_forest.stage_roots(final_account_state.id(), final_roots);
507
508        Ok(())
509    }
510
511    /// Undoes discarded account states by restoring old values from historical.
512    pub(crate) fn undo_account_state(
513        tx: &Transaction<'_>,
514        smt_forest: &mut AccountSmtForest,
515        discarded_states: &[(AccountId, Word)],
516    ) -> Result<(), StoreError> {
517        if discarded_states.is_empty() {
518            return Ok(());
519        }
520
521        let commitment_params = Rc::new(
522            discarded_states
523                .iter()
524                .map(|(_, commitment)| Value::from(commitment.to_hex()))
525                .collect::<Vec<_>>(),
526        );
527
528        // Step 1: Resolve (account_id, nonce) pairs from both latest and historical headers.
529        // The most recent discarded state is in latest, older ones are in historical.
530        let mut id_nonce_pairs: Vec<(String, u64)> = Vec::new();
531        for query in [
532            "SELECT id, nonce FROM latest_account_headers WHERE account_commitment IN rarray(?)",
533            "SELECT id, nonce FROM historical_account_headers WHERE account_commitment IN rarray(?)",
534        ] {
535            id_nonce_pairs.extend(
536                tx.prepare(query)
537                    .into_store_error()?
538                    .query_map(params![commitment_params.clone()], |row| {
539                        let id: String = row.get(0)?;
540                        let nonce: u64 = column_value_as_u64(row, 1)?;
541                        Ok((id, nonce))
542                    })
543                    .into_store_error()?
544                    .filter_map(Result::ok),
545            );
546        }
547
548        // Step 2: Group nonces by account, sort descending (undo most recent first).
549        // Descending order is needed because each nonce's old value is the state before
550        // that nonce — processing most recent first lets earlier nonces overwrite with
551        // the correct final value.
552        let mut nonces_by_account: BTreeMap<String, Vec<u64>> = BTreeMap::new();
553        for (id, nonce) in &id_nonce_pairs {
554            nonces_by_account.entry(id.clone()).or_default().push(*nonce);
555        }
556        for nonces in nonces_by_account.values_mut() {
557            nonces.sort_unstable();
558            nonces.dedup();
559            nonces.reverse();
560        }
561
562        // Steps 3-5
563        for (account_id_hex, nonces) in &nonces_by_account {
564            Self::undo_account_nonces(tx, account_id_hex, nonces)?;
565        }
566
567        // Step 6: Discard rolled-back states from the in-memory forest
568        for (account_id, _) in discarded_states {
569            smt_forest.discard_roots(*account_id);
570        }
571
572        Ok(())
573    }
574
575    /// Undoes all nonces for a single account: restores old values, restores old header,
576    /// and cleans up consumed historical entries.
577    fn undo_account_nonces(
578        tx: &Transaction<'_>,
579        account_id_hex: &str,
580        nonces: &[u64],
581    ) -> Result<(), StoreError> {
582        // Step 3: Undo each nonce in descending order
583        for &nonce in nonces {
584            let nonce_val = u64_to_value(nonce);
585            Self::restore_old_values_for_nonce(tx, account_id_hex, &nonce_val)?;
586        }
587
588        // Step 4: Restore old header from the earliest discarded nonce
589        // SAFETY: `nonces` is non-empty because `undo_account_nonces` is only called for
590        // accounts that appear in `nonces_by_account`, which only contains entries built
591        // from at least one nonce being pushed — so the slice is guaranteed non-empty here.
592        let min_nonce = *nonces.last().unwrap();
593        let min_nonce_val = u64_to_value(min_nonce);
594
595        let old_header_exists: bool = tx
596            .query_row(
597                "SELECT COUNT(*) FROM historical_account_headers \
598                 WHERE id = ? AND replaced_at_nonce = ?",
599                params![account_id_hex, &min_nonce_val],
600                |row| row.get::<_, i64>(0),
601            )
602            .into_store_error()?
603            > 0;
604
605        if old_header_exists {
606            // `watched` is not carried in historical_account_headers, so this restore resets
607            // it to the column default (FALSE). This is safe because undo only fires for discarded
608            // local transactions, and watched accounts have none.
609            tx.execute(
610                "INSERT OR REPLACE INTO latest_account_headers \
611                 (id, account_commitment, code_commitment, storage_commitment, \
612                  vault_root, nonce, account_seed, locked) \
613                 SELECT id, account_commitment, code_commitment, storage_commitment, \
614                        vault_root, nonce, account_seed, locked \
615                 FROM historical_account_headers \
616                 WHERE id = ? AND replaced_at_nonce = ?",
617                params![account_id_hex, &min_nonce_val],
618            )
619            .into_store_error()?;
620        } else {
621            // No previous state — delete the account entirely
622            for table in [
623                "DELETE FROM latest_account_headers WHERE id = ?",
624                "DELETE FROM latest_account_storage WHERE account_id = ?",
625                "DELETE FROM latest_storage_map_entries WHERE account_id = ?",
626                "DELETE FROM latest_account_assets WHERE account_id = ?",
627            ] {
628                tx.execute(table, params![account_id_hex]).into_store_error()?;
629            }
630        }
631
632        // Step 5: Delete all consumed historical entries at the discarded nonces
633        let nonce_params = Rc::new(nonces.iter().map(|n| u64_to_value(*n)).collect::<Vec<_>>());
634        for table in [
635            "historical_account_storage",
636            "historical_storage_map_entries",
637            "historical_account_assets",
638        ] {
639            tx.execute(
640                &format!(
641                    "DELETE FROM {table} WHERE account_id = ? AND replaced_at_nonce IN rarray(?)"
642                ),
643                params![account_id_hex, nonce_params.clone()],
644            )
645            .into_store_error()?;
646        }
647        tx.execute(
648            "DELETE FROM historical_account_headers \
649             WHERE id = ? AND replaced_at_nonce IN rarray(?)",
650            params![account_id_hex, nonce_params],
651        )
652        .into_store_error()?;
653
654        Ok(())
655    }
656
657    /// Restores old values from historical entries for a given nonce.
658    /// Non-NULL old values overwrite latest, NULL old values (new entries) are deleted.
659    fn restore_old_values_for_nonce(
660        tx: &Transaction<'_>,
661        account_id_hex: &str,
662        nonce_val: &rusqlite::types::Value,
663    ) -> Result<(), StoreError> {
664        // Restore storage slots with non-NULL old values
665        tx.execute(
666            "INSERT OR REPLACE INTO latest_account_storage \
667             (account_id, slot_name, slot_value, slot_type) \
668             SELECT account_id, slot_name, old_slot_value, slot_type \
669             FROM historical_account_storage \
670             WHERE account_id = ? AND replaced_at_nonce = ? AND old_slot_value IS NOT NULL",
671            params![account_id_hex, nonce_val],
672        )
673        .into_store_error()?;
674
675        // Delete storage slots that were new (NULL old value)
676        tx.execute(
677            "DELETE FROM latest_account_storage \
678             WHERE account_id = ?1 AND slot_name IN (\
679                 SELECT slot_name FROM historical_account_storage \
680                 WHERE account_id = ?1 AND replaced_at_nonce = ?2 AND old_slot_value IS NULL\
681             )",
682            params![account_id_hex, nonce_val],
683        )
684        .into_store_error()?;
685
686        // Restore map entries with non-NULL old values
687        tx.execute(
688            "INSERT OR REPLACE INTO latest_storage_map_entries \
689             (account_id, slot_name, key, value) \
690             SELECT account_id, slot_name, key, old_value \
691             FROM historical_storage_map_entries \
692             WHERE account_id = ? AND replaced_at_nonce = ? AND old_value IS NOT NULL",
693            params![account_id_hex, nonce_val],
694        )
695        .into_store_error()?;
696
697        // Delete map entries that were new (NULL old value)
698        tx.execute(
699            "DELETE FROM latest_storage_map_entries \
700             WHERE account_id = ?1 AND EXISTS (\
701                 SELECT 1 FROM historical_storage_map_entries h \
702                 WHERE h.account_id = latest_storage_map_entries.account_id \
703                   AND h.slot_name = latest_storage_map_entries.slot_name \
704                   AND h.key = latest_storage_map_entries.key \
705                   AND h.replaced_at_nonce = ?2 AND h.old_value IS NULL\
706             )",
707            params![account_id_hex, nonce_val],
708        )
709        .into_store_error()?;
710
711        // Restore assets with non-NULL old values
712        tx.execute(
713            "INSERT OR REPLACE INTO latest_account_assets \
714             (account_id, vault_key, asset) \
715             SELECT account_id, vault_key, old_asset \
716             FROM historical_account_assets \
717             WHERE account_id = ? AND replaced_at_nonce = ? AND old_asset IS NOT NULL",
718            params![account_id_hex, nonce_val],
719        )
720        .into_store_error()?;
721
722        // Delete assets that were new (NULL old value)
723        tx.execute(
724            "DELETE FROM latest_account_assets \
725             WHERE account_id = ?1 AND vault_key IN (\
726                 SELECT vault_key FROM historical_account_assets \
727                 WHERE account_id = ?1 AND replaced_at_nonce = ?2 AND old_asset IS NULL\
728             )",
729            params![account_id_hex, nonce_val],
730        )
731        .into_store_error()?;
732
733        Ok(())
734    }
735
736    /// Replaces the account state with a completely new one from the network.
737    ///
738    /// Replaces the account state entirely: archives old state to historical, clears latest,
739    /// inserts new state to latest only. Preserves the `watched` flag.
740    pub(crate) fn update_account_state(
741        tx: &Transaction<'_>,
742        smt_forest: &mut AccountSmtForest,
743        new_account_state: &Account,
744    ) -> Result<(), StoreError> {
745        let account_id = new_account_state.id();
746        let account_id_hex = account_id.to_hex();
747
748        // Read old header before mutating the SMT snapshot or database rows. Sync filters stale
749        // full-account snapshots; if one still reaches storage, reject it before mutating.
750        let old_header = query_latest_account_headers(tx, "id = ?", params![&account_id_hex])?
751            .into_iter()
752            .next()
753            .map(|(header, ..)| header)
754            .ok_or(StoreError::AccountDataNotFound(account_id))?;
755
756        if new_account_state.nonce().as_canonical_u64() < old_header.nonce().as_canonical_u64() {
757            return Err(StoreError::DatabaseError(format!(
758                "update_account_state: new nonce {} is less than old nonce {} for account {}",
759                new_account_state.nonce().as_canonical_u64(),
760                old_header.nonce().as_canonical_u64(),
761                account_id,
762            )));
763        }
764
765        let nonce_val = u64_to_value(new_account_state.nonce().as_canonical_u64());
766
767        // Insert and register account state in the SMT forest (handles old root cleanup)
768        smt_forest.insert_and_register_account_state(
769            account_id,
770            new_account_state.vault(),
771            new_account_state.storage(),
772        )?;
773
774        // Archive all old entries from latest → historical
775        tx.execute(
776            "INSERT OR REPLACE INTO historical_account_storage \
777             (account_id, replaced_at_nonce, slot_name, old_slot_value, slot_type) \
778             SELECT account_id, ?, slot_name, slot_value, slot_type \
779             FROM latest_account_storage WHERE account_id = ?",
780            params![&nonce_val, &account_id_hex],
781        )
782        .into_store_error()?;
783        tx.execute(
784            "INSERT OR REPLACE INTO historical_storage_map_entries \
785             (account_id, replaced_at_nonce, slot_name, key, old_value) \
786             SELECT account_id, ?, slot_name, key, value \
787             FROM latest_storage_map_entries WHERE account_id = ?",
788            params![&nonce_val, &account_id_hex],
789        )
790        .into_store_error()?;
791        tx.execute(
792            "INSERT OR REPLACE INTO historical_account_assets \
793             (account_id, replaced_at_nonce, vault_key, old_asset) \
794             SELECT account_id, ?, vault_key, asset \
795             FROM latest_account_assets WHERE account_id = ?",
796            params![&nonce_val, &account_id_hex],
797        )
798        .into_store_error()?;
799
800        // Delete all latest entries for this account
801        tx.execute(
802            "DELETE FROM latest_account_storage WHERE account_id = ?",
803            params![&account_id_hex],
804        )
805        .into_store_error()?;
806        tx.execute(
807            "DELETE FROM latest_storage_map_entries WHERE account_id = ?",
808            params![&account_id_hex],
809        )
810        .into_store_error()?;
811        tx.execute(
812            "DELETE FROM latest_account_assets WHERE account_id = ?",
813            params![&account_id_hex],
814        )
815        .into_store_error()?;
816
817        // Insert all new entries into latest only
818        Self::insert_storage_slots(tx, account_id, new_account_state.storage().slots().iter())?;
819        Self::insert_assets(tx, account_id, new_account_state.vault().assets())?;
820
821        // Write NULL historical entries for genuinely new entries that didn't exist
822        // in the old state (INSERT OR IGNORE skips entries already archived above)
823        tx.execute(
824            "INSERT OR IGNORE INTO historical_account_storage \
825             (account_id, replaced_at_nonce, slot_name, old_slot_value, slot_type) \
826             SELECT account_id, ?, slot_name, NULL, slot_type \
827             FROM latest_account_storage WHERE account_id = ?",
828            params![&nonce_val, &account_id_hex],
829        )
830        .into_store_error()?;
831        tx.execute(
832            "INSERT OR IGNORE INTO historical_storage_map_entries \
833             (account_id, replaced_at_nonce, slot_name, key, old_value) \
834             SELECT account_id, ?, slot_name, key, NULL \
835             FROM latest_storage_map_entries WHERE account_id = ?",
836            params![&nonce_val, &account_id_hex],
837        )
838        .into_store_error()?;
839        tx.execute(
840            "INSERT OR IGNORE INTO historical_account_assets \
841             (account_id, replaced_at_nonce, vault_key, old_asset) \
842             SELECT account_id, ?, vault_key, NULL \
843             FROM latest_account_assets WHERE account_id = ?",
844            params![&nonce_val, &account_id_hex],
845        )
846        .into_store_error()?;
847
848        // Archive the old header to historical and write the new one to latest.
849        Self::replace_account_header(tx, &new_account_state.into(), &old_header)?;
850
851        Ok(())
852    }
853
854    /// Applies an incremental delta to a public account's state during sync.
855    pub(crate) fn apply_sync_account_delta(
856        tx: &Transaction<'_>,
857        smt_forest: &mut AccountSmtForest,
858        new_header: &AccountHeader,
859        delta: &AccountDelta,
860    ) -> Result<(), StoreError> {
861        let account_id = new_header.id();
862
863        // Read current header from the store.
864        let init_header = query_latest_account_headers(tx, "id = ?", params![account_id.to_hex()])?
865            .into_iter()
866            .next()
867            .map(|(header, ..)| header)
868            .ok_or(StoreError::AccountDataNotFound(account_id))?;
869
870        // Read the fungible assets that will be affected by the delta.
871        // Transaction derefs to Connection, so we can pass it where Connection is expected.
872        let updated_fungible_assets =
873            Self::get_account_fungible_assets_for_delta(tx, account_id, delta)?;
874
875        // Read the old map roots for slots affected by the delta.
876        let old_map_roots = Self::get_storage_map_roots_for_delta(tx, account_id, delta)?;
877
878        Self::apply_account_delta(
879            tx,
880            smt_forest,
881            &init_header,
882            new_header,
883            updated_fungible_assets,
884            &old_map_roots,
885            delta,
886        )
887    }
888
889    /// Locks the account if the mismatched digest doesn't belong to a previous account state (stale
890    /// data).
891    pub(crate) fn lock_account_on_unexpected_commitment(
892        tx: &Transaction<'_>,
893        account_id: &AccountId,
894        mismatched_digest: &Word,
895    ) -> Result<(), StoreError> {
896        // Mismatched digests may be due to stale network data. If the mismatched digest is
897        // tracked in the db and corresponds to the mismatched account, it means we
898        // got a past update and shouldn't lock the account.
899        const LOCK_CONDITION: &str = "WHERE id = :account_id AND NOT EXISTS (SELECT 1 FROM historical_account_headers WHERE id = :account_id AND account_commitment = :digest)";
900        let account_id_hex = account_id.to_hex();
901        let digest_str = mismatched_digest.to_string();
902        let params = named_params! {
903            ":account_id": account_id_hex,
904            ":digest": digest_str
905        };
906
907        let query = format!("UPDATE latest_account_headers SET locked = true {LOCK_CONDITION}");
908        tx.execute(&query, params).into_store_error()?;
909
910        // Also lock historical rows so that undo_account_state preserves the lock.
911        let query = format!("UPDATE historical_account_headers SET locked = true {LOCK_CONDITION}");
912        tx.execute(&query, params).into_store_error()?;
913
914        Ok(())
915    }
916
917    // HELPERS
918    // --------------------------------------------------------------------------------------------
919
920    /// Writes a new row into `latest_account_headers`.
921    ///
922    /// Does not archive any previous state, use [`Self::replace_account_header`] when a row
923    /// for this account already exists. If a row does exist it will be overwritten with the
924    /// provided `watched` value and no historical row added.
925    fn insert_new_account_header(
926        tx: &Transaction<'_>,
927        new_header: &AccountHeader,
928        account_seed: Option<Word>,
929        watched: bool,
930    ) -> Result<(), StoreError> {
931        let id = new_header.id().to_hex();
932        let code_commitment = new_header.code_commitment().to_string();
933        let storage_commitment = new_header.storage_commitment().to_string();
934        let vault_root = new_header.vault_root().to_string();
935        let nonce = u64_to_value(new_header.nonce().as_canonical_u64());
936        let commitment = new_header.to_commitment().to_string();
937        let account_seed = account_seed.map(|seed| seed.to_bytes());
938
939        const LATEST_QUERY: &str = insert_sql!(
940            latest_account_headers {
941                id,
942                code_commitment,
943                storage_commitment,
944                vault_root,
945                nonce,
946                account_seed,
947                account_commitment,
948                locked,
949                watched
950            } | REPLACE
951        );
952
953        tx.execute(
954            LATEST_QUERY,
955            params![
956                id,
957                code_commitment,
958                storage_commitment,
959                vault_root,
960                nonce,
961                account_seed,
962                commitment,
963                false,
964                watched,
965            ],
966        )
967        .into_store_error()?;
968
969        Ok(())
970    }
971
972    /// Replaces an account's latest header, archiving the previous one to historical.
973    ///
974    /// Preserves the `watched` flag from the existing latest row (mode is a per-account
975    /// property, not per-state). The new latest row is written with `account_seed = NULL`
976    /// and `locked = false`; the previous seed and lock state move into the historical row.
977    fn replace_account_header(
978        tx: &Transaction<'_>,
979        new_header: &AccountHeader,
980        old_header: &AccountHeader,
981    ) -> Result<(), StoreError> {
982        if new_header.id() != old_header.id() {
983            return Err(StoreError::DatabaseError(format!(
984                "replace_account_header: account id mismatch (new: {}, old: {})",
985                new_header.id(),
986                old_header.id(),
987            )));
988        }
989        if new_header.nonce().as_canonical_u64() < old_header.nonce().as_canonical_u64() {
990            return Err(StoreError::DatabaseError(format!(
991                "replace_account_header: new nonce {} is less than old nonce {} for account {}",
992                new_header.nonce().as_canonical_u64(),
993                old_header.nonce().as_canonical_u64(),
994                new_header.id(),
995            )));
996        }
997
998        let id_hex = new_header.id().to_hex();
999
1000        // `AccountHeader` doesn't carry the seed or per-account flags, so read them from the row
1001        // we're about to overwrite: `account_seed`/`locked` get archived into the historical row,
1002        // `watched` is carried into the new latest row.
1003        let (old_seed, old_locked, old_watched): (Option<Vec<u8>>, bool, bool) = tx
1004            .query_row(
1005                "SELECT account_seed, locked, watched FROM latest_account_headers WHERE id = ?",
1006                params![&id_hex],
1007                |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
1008            )
1009            .optional()
1010            .into_store_error()?
1011            .unwrap_or((None, false, false));
1012
1013        // Archive the old header to historical.
1014        let old_id = old_header.id().to_hex();
1015        let old_code_commitment = old_header.code_commitment().to_string();
1016        let old_storage_commitment = old_header.storage_commitment().to_string();
1017        let old_vault_root = old_header.vault_root().to_string();
1018        let old_nonce = u64_to_value(old_header.nonce().as_canonical_u64());
1019        let old_commitment = old_header.to_commitment().to_string();
1020        let replaced_at_nonce = u64_to_value(new_header.nonce().as_canonical_u64());
1021
1022        const HISTORICAL_QUERY: &str = insert_sql!(
1023            historical_account_headers {
1024                id,
1025                code_commitment,
1026                storage_commitment,
1027                vault_root,
1028                nonce,
1029                account_seed,
1030                account_commitment,
1031                locked,
1032                replaced_at_nonce
1033            } | REPLACE
1034        );
1035
1036        tx.execute(
1037            HISTORICAL_QUERY,
1038            params![
1039                old_id,
1040                old_code_commitment,
1041                old_storage_commitment,
1042                old_vault_root,
1043                old_nonce,
1044                old_seed,
1045                old_commitment,
1046                old_locked,
1047                replaced_at_nonce,
1048            ],
1049        )
1050        .into_store_error()?;
1051
1052        // Write the new latest row.
1053        Self::insert_new_account_header(tx, new_header, None, old_watched)
1054    }
1055
1056    /// Prunes historical account states for a single account up to the given nonce.
1057    ///
1058    /// Deletes all historical entries with `replaced_at_nonce <= up_to_nonce`
1059    /// (see DESIGN.md for why this threshold is safe), then removes any account
1060    /// code that was only referenced by the deleted headers.
1061    pub fn prune_account_history(
1062        conn: &mut Connection,
1063        account_id: AccountId,
1064        up_to_nonce: Felt,
1065    ) -> Result<usize, StoreError> {
1066        let tx = conn.transaction().into_store_error()?;
1067        let account_id_hex = account_id.to_hex();
1068        let boundary_val = u64_to_value(up_to_nonce.as_canonical_u64());
1069        let mut total_deleted: usize = 0;
1070
1071        // Collect code commitments from headers we are about to delete.
1072        let candidate_code_commitments: Vec<String> = {
1073            let mut stmt = tx
1074                .prepare(
1075                    "SELECT DISTINCT code_commitment FROM historical_account_headers \
1076                     WHERE id = ? AND replaced_at_nonce <= ?",
1077                )
1078                .into_store_error()?;
1079            let rows = stmt
1080                .query_map(params![&account_id_hex, &boundary_val], |row| row.get(0))
1081                .into_store_error()?;
1082            rows.collect::<Result<Vec<String>, _>>().into_store_error()?
1083        };
1084
1085        // Delete historical entries.
1086        total_deleted += tx
1087            .execute(
1088                "DELETE FROM historical_account_headers \
1089                 WHERE id = ? AND replaced_at_nonce <= ?",
1090                params![&account_id_hex, &boundary_val],
1091            )
1092            .into_store_error()?;
1093
1094        total_deleted += tx
1095            .execute(
1096                "DELETE FROM historical_account_storage \
1097                 WHERE account_id = ? AND replaced_at_nonce <= ?",
1098                params![&account_id_hex, &boundary_val],
1099            )
1100            .into_store_error()?;
1101
1102        total_deleted += tx
1103            .execute(
1104                "DELETE FROM historical_storage_map_entries \
1105                 WHERE account_id = ? AND replaced_at_nonce <= ?",
1106                params![&account_id_hex, &boundary_val],
1107            )
1108            .into_store_error()?;
1109
1110        total_deleted += tx
1111            .execute(
1112                "DELETE FROM historical_account_assets \
1113                 WHERE account_id = ? AND replaced_at_nonce <= ?",
1114                params![&account_id_hex, &boundary_val],
1115            )
1116            .into_store_error()?;
1117
1118        // Delete orphaned code: only check commitments from the deleted headers,
1119        // and only if they are not referenced by any remaining header or foreign code.
1120        for commitment in &candidate_code_commitments {
1121            let still_referenced: bool = tx
1122                .query_row(
1123                    "SELECT EXISTS(
1124                        SELECT 1 FROM latest_account_headers WHERE code_commitment = ?1
1125                        UNION ALL
1126                        SELECT 1 FROM historical_account_headers WHERE code_commitment = ?1
1127                        UNION ALL
1128                        SELECT 1 FROM foreign_account_code WHERE code_commitment = ?1
1129                    )",
1130                    params![commitment],
1131                    |row| row.get(0),
1132                )
1133                .into_store_error()?;
1134
1135            if !still_referenced {
1136                total_deleted += tx
1137                    .execute("DELETE FROM account_code WHERE commitment = ?", params![commitment])
1138                    .into_store_error()?;
1139            }
1140        }
1141
1142        tx.commit().into_store_error()?;
1143        Ok(total_deleted)
1144    }
1145}