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