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    AccountIdPrefix,
16    AccountStorage,
17    Address,
18    PartialAccount,
19    PartialStorage,
20    PartialStorageMap,
21    StorageMap,
22    StorageMapKey,
23    StorageSlotName,
24    StorageSlotType,
25};
26use miden_client::asset::{Asset, AssetVault, AssetWitness, FungibleAsset};
27use miden_client::store::{
28    AccountRecord,
29    AccountRecordData,
30    AccountSmtForest,
31    AccountStatus,
32    AccountStorageFilter,
33    StoreError,
34};
35use miden_client::sync::NoteTagRecord;
36use miden_client::utils::Serializable;
37use miden_client::{AccountError, Word};
38use miden_protocol::account::{AccountStorageHeader, StorageMapWitness, StorageSlotHeader};
39use miden_protocol::asset::{AssetVaultKey, PartialVault};
40use miden_protocol::crypto::merkle::MerkleError;
41use rusqlite::types::Value;
42use rusqlite::{Connection, Transaction, named_params, params};
43
44use crate::account::helpers::{
45    query_account_addresses,
46    query_account_code,
47    query_historical_account_headers,
48    query_latest_account_headers,
49    query_storage_slots,
50    query_storage_values,
51    query_vault_assets,
52};
53use crate::sql_error::SqlResultExt;
54use crate::sync::{add_note_tag_tx, remove_note_tag_tx};
55use crate::{SqliteStore, column_value_as_u64, insert_sql, subst, u64_to_value};
56
57impl SqliteStore {
58    // READER METHODS
59    // --------------------------------------------------------------------------------------------
60
61    pub(crate) fn get_account_ids(conn: &mut Connection) -> Result<Vec<AccountId>, StoreError> {
62        const QUERY: &str = "SELECT id FROM latest_account_headers";
63
64        conn.prepare_cached(QUERY)
65            .into_store_error()?
66            .query_map([], |row| row.get(0))
67            .expect("no binding parameters used in query")
68            .map(|result| {
69                let id: String = result.map_err(|e| StoreError::ParsingError(e.to_string()))?;
70                Ok(AccountId::from_hex(&id).expect("account id is valid"))
71            })
72            .collect::<Result<Vec<AccountId>, StoreError>>()
73    }
74
75    pub(crate) fn get_account_headers(
76        conn: &mut Connection,
77    ) -> Result<Vec<(AccountHeader, AccountStatus)>, StoreError> {
78        query_latest_account_headers(conn, "1=1 ORDER BY id", params![])
79    }
80
81    pub(crate) fn get_account_header(
82        conn: &mut Connection,
83        account_id: AccountId,
84    ) -> Result<Option<(AccountHeader, AccountStatus)>, StoreError> {
85        Ok(query_latest_account_headers(conn, "id = ?", params![account_id.to_hex()])?.pop())
86    }
87
88    pub(crate) fn get_account_header_by_commitment(
89        conn: &mut Connection,
90        account_commitment: Word,
91    ) -> Result<Option<AccountHeader>, StoreError> {
92        let account_commitment_str: String = account_commitment.to_string();
93        Ok(query_historical_account_headers(
94            conn,
95            "account_commitment = ?",
96            params![account_commitment_str],
97        )?
98        .pop()
99        .map(|(header, _)| header))
100    }
101
102    /// Retrieves a complete account record with full vault and storage data.
103    pub(crate) fn get_account(
104        conn: &mut Connection,
105        account_id: AccountId,
106    ) -> Result<Option<AccountRecord>, StoreError> {
107        let Some((header, status)) = Self::get_account_header(conn, account_id)? else {
108            return Ok(None);
109        };
110
111        let assets = query_vault_assets(conn, account_id)?;
112        let vault = AssetVault::new(&assets)?;
113
114        let slots = query_storage_slots(conn, account_id, &AccountStorageFilter::All)?
115            .into_values()
116            .collect();
117
118        let storage = AccountStorage::new(slots)?;
119
120        let Some(account_code) = query_account_code(conn, header.code_commitment())? else {
121            return Ok(None);
122        };
123
124        let account = Account::new_unchecked(
125            header.id(),
126            vault,
127            storage,
128            account_code,
129            header.nonce(),
130            status.seed().copied(),
131        );
132
133        let account_data = AccountRecordData::Full(account);
134        Ok(Some(AccountRecord::new(account_data, status)))
135    }
136
137    /// Retrieves a minimal partial account record with storage and vault witnesses.
138    pub(crate) fn get_minimal_partial_account(
139        conn: &mut Connection,
140        account_id: AccountId,
141    ) -> Result<Option<AccountRecord>, StoreError> {
142        let Some((header, status)) = Self::get_account_header(conn, account_id)? else {
143            return Ok(None);
144        };
145
146        // Partial vault retrieval
147        let partial_vault = PartialVault::new(header.vault_root());
148
149        // Partial storage retrieval
150        let mut storage_header = Vec::new();
151        let mut maps = vec![];
152
153        let storage_values = query_storage_values(conn, account_id)?;
154
155        // Storage maps are always minimal here (just roots, no entries).
156        // New accounts that need full storage data are handled by the DataStore layer,
157        // which fetches the full account via `get_account()` when nonce == 0.
158        for (slot_name, (slot_type, value)) in storage_values {
159            storage_header.push(StorageSlotHeader::new(slot_name.clone(), slot_type, value));
160            if slot_type == StorageSlotType::Map {
161                maps.push(PartialStorageMap::new(value));
162            }
163        }
164        storage_header.sort_by_key(StorageSlotHeader::id);
165        let storage_header =
166            AccountStorageHeader::new(storage_header).map_err(StoreError::AccountError)?;
167        let partial_storage =
168            PartialStorage::new(storage_header, maps).map_err(StoreError::AccountError)?;
169
170        let Some(account_code) = query_account_code(conn, header.code_commitment())? else {
171            return Ok(None);
172        };
173
174        let partial_account = PartialAccount::new(
175            header.id(),
176            header.nonce(),
177            account_code,
178            partial_storage,
179            partial_vault,
180            status.seed().copied(),
181        )?;
182        let account_record_data = AccountRecordData::Partial(partial_account);
183        Ok(Some(AccountRecord::new(account_record_data, status)))
184    }
185
186    pub fn get_foreign_account_code(
187        conn: &mut Connection,
188        account_ids: Vec<AccountId>,
189    ) -> Result<BTreeMap<AccountId, AccountCode>, StoreError> {
190        let params: Vec<Value> =
191            account_ids.into_iter().map(|id| Value::from(id.to_hex())).collect();
192        const QUERY: &str = "
193            SELECT account_id, code
194            FROM foreign_account_code JOIN account_code ON foreign_account_code.code_commitment = account_code.commitment
195            WHERE account_id IN rarray(?)";
196
197        conn.prepare_cached(QUERY)
198            .into_store_error()?
199            .query_map([Rc::new(params)], |row| Ok((row.get(0)?, row.get(1)?)))
200            .expect("no binding parameters used in query")
201            .map(|result| {
202                result.map_err(|err| StoreError::ParsingError(err.to_string())).and_then(
203                    |(id, code): (String, Vec<u8>)| {
204                        Ok((
205                            AccountId::from_hex(&id).map_err(|err| {
206                                StoreError::AccountError(
207                                    AccountError::FinalAccountHeaderIdParsingFailed(err),
208                                )
209                            })?,
210                            AccountCode::from_bytes(&code).map_err(StoreError::AccountError)?,
211                        ))
212                    },
213                )
214            })
215            .collect::<Result<BTreeMap<AccountId, AccountCode>, _>>()
216    }
217
218    /// Retrieves the full asset vault for a specific account.
219    pub fn get_account_vault(
220        conn: &Connection,
221        account_id: AccountId,
222    ) -> Result<AssetVault, StoreError> {
223        let assets = query_vault_assets(conn, account_id)?;
224        Ok(AssetVault::new(&assets)?)
225    }
226
227    /// Retrieves the full storage for a specific account.
228    pub fn get_account_storage(
229        conn: &Connection,
230        account_id: AccountId,
231        filter: &AccountStorageFilter,
232    ) -> Result<AccountStorage, StoreError> {
233        let slots = query_storage_slots(conn, account_id, filter)?.into_values().collect();
234        Ok(AccountStorage::new(slots)?)
235    }
236
237    /// Fetches a specific asset from the account's vault without the need of loading the entire
238    /// vault. The witness is retrieved from the [`AccountSmtForest`].
239    pub(crate) fn get_account_asset(
240        conn: &mut Connection,
241        smt_forest: &Arc<RwLock<AccountSmtForest>>,
242        account_id: AccountId,
243        vault_key: AssetVaultKey,
244    ) -> Result<Option<(Asset, AssetWitness)>, StoreError> {
245        let header = Self::get_account_header(conn, account_id)?
246            .ok_or(StoreError::AccountDataNotFound(account_id))?
247            .0;
248
249        let smt_forest = smt_forest.read().expect("smt_forest read lock not poisoned");
250        match smt_forest.get_asset_and_witness(header.vault_root(), vault_key) {
251            Ok((asset, witness)) => Ok(Some((asset, witness))),
252            Err(StoreError::MerkleStoreError(MerkleError::UntrackedKey(_))) => Ok(None),
253            Err(err) => Err(err),
254        }
255    }
256
257    /// Retrieves a specific item from the account's storage map without loading the entire storage.
258    /// The witness is retrieved from the [`AccountSmtForest`].
259    pub(crate) fn get_account_map_item(
260        conn: &mut Connection,
261        smt_forest: &Arc<RwLock<AccountSmtForest>>,
262        account_id: AccountId,
263        slot_name: StorageSlotName,
264        key: StorageMapKey,
265    ) -> Result<(Word, StorageMapWitness), StoreError> {
266        let header = Self::get_account_header(conn, account_id)?
267            .ok_or(StoreError::AccountDataNotFound(account_id))?
268            .0;
269
270        let mut storage_values = query_storage_values(conn, account_id)?;
271        let (slot_type, map_root) = storage_values
272            .remove(&slot_name)
273            .ok_or(StoreError::AccountStorageRootNotFound(header.storage_commitment()))?;
274        if slot_type != StorageSlotType::Map {
275            return Err(StoreError::AccountError(AccountError::StorageSlotNotMap(slot_name)));
276        }
277
278        let smt_forest = smt_forest.read().expect("smt_forest read lock not poisoned");
279        let witness = smt_forest.get_storage_map_item_witness(map_root, key)?;
280        let item = witness.get(key).unwrap_or(miden_client::EMPTY_WORD);
281
282        Ok((item, witness))
283    }
284
285    pub(crate) fn get_account_addresses(
286        conn: &mut Connection,
287        account_id: AccountId,
288    ) -> Result<Vec<Address>, StoreError> {
289        query_account_addresses(conn, account_id)
290    }
291
292    /// Retrieves the account code for a specific account by ID.
293    pub(crate) fn get_account_code_by_id(
294        conn: &mut Connection,
295        account_id: AccountId,
296    ) -> Result<Option<AccountCode>, StoreError> {
297        let Some((header, _)) =
298            query_latest_account_headers(conn, "id = ?", params![account_id.to_hex()])?
299                .into_iter()
300                .next()
301        else {
302            return Ok(None);
303        };
304
305        query_account_code(conn, header.code_commitment())
306    }
307
308    // MUTATOR/WRITER METHODS
309    // --------------------------------------------------------------------------------------------
310
311    pub(crate) fn insert_account(
312        conn: &mut Connection,
313        smt_forest: &Arc<RwLock<AccountSmtForest>>,
314        account: &Account,
315        initial_address: &Address,
316    ) -> Result<(), StoreError> {
317        let tx = conn.transaction().into_store_error()?;
318
319        Self::insert_account_code(&tx, account.code())?;
320
321        let account_id = account.id();
322        let nonce = account.nonce().as_int();
323
324        Self::insert_storage_slots(&tx, account_id, nonce, account.storage().slots().iter())?;
325
326        Self::insert_assets(&tx, account_id, nonce, account.vault().assets())?;
327        Self::insert_account_header(&tx, &account.into(), account.seed())?;
328
329        Self::insert_address(&tx, initial_address, account.id())?;
330
331        tx.commit().into_store_error()?;
332
333        let mut smt_forest = smt_forest.write().expect("smt_forest write lock not poisoned");
334        smt_forest.insert_and_register_account_state(
335            account.id(),
336            account.vault(),
337            account.storage(),
338        )?;
339
340        Ok(())
341    }
342
343    pub(crate) fn update_account(
344        conn: &mut Connection,
345        smt_forest: &Arc<RwLock<AccountSmtForest>>,
346        new_account_state: &Account,
347    ) -> Result<(), StoreError> {
348        const QUERY: &str = "SELECT id FROM latest_account_headers WHERE id = ?";
349        if conn
350            .prepare(QUERY)
351            .into_store_error()?
352            .query_map(params![new_account_state.id().to_hex()], |row| row.get(0))
353            .into_store_error()?
354            .map(|result| {
355                result.map_err(|err| StoreError::ParsingError(err.to_string())).and_then(
356                    |id: String| {
357                        AccountId::from_hex(&id).map_err(|err| {
358                            StoreError::AccountError(
359                                AccountError::FinalAccountHeaderIdParsingFailed(err),
360                            )
361                        })
362                    },
363                )
364            })
365            .next()
366            .is_none()
367        {
368            return Err(StoreError::AccountDataNotFound(new_account_state.id()));
369        }
370
371        let mut smt_forest = smt_forest.write().expect("smt_forest write lock not poisoned");
372        let tx = conn.transaction().into_store_error()?;
373        Self::update_account_state(&tx, &mut smt_forest, new_account_state)?;
374        tx.commit().into_store_error()
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_hex(), code.commitment().to_string()])
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        let derived_note_tag = address.to_note_tag();
402        let note_tag_record = NoteTagRecord::with_account_source(derived_note_tag, account_id);
403
404        add_note_tag_tx(tx, &note_tag_record)?;
405        Self::insert_address_internal(tx, address, account_id)?;
406
407        Ok(())
408    }
409
410    pub(crate) fn remove_address(
411        conn: &mut Connection,
412        address: &Address,
413        account_id: AccountId,
414    ) -> Result<(), StoreError> {
415        let derived_note_tag = address.to_note_tag();
416        let note_tag_record = NoteTagRecord::with_account_source(derived_note_tag, account_id);
417
418        let tx = conn.transaction().into_store_error()?;
419        remove_note_tag_tx(&tx, note_tag_record)?;
420        Self::remove_address_internal(&tx, address)?;
421
422        tx.commit().into_store_error()
423    }
424
425    /// Inserts an [`AccountCode`].
426    pub(crate) fn insert_account_code(
427        tx: &Transaction<'_>,
428        account_code: &AccountCode,
429    ) -> Result<(), StoreError> {
430        const QUERY: &str = insert_sql!(account_code { commitment, code } | IGNORE);
431        tx.execute(QUERY, params![account_code.commitment().to_hex(), account_code.to_bytes()])
432            .into_store_error()?;
433        Ok(())
434    }
435
436    /// Applies the account delta to the account state, updating the vault and storage maps.
437    ///
438    /// Writes only changed entries to historical (at the new nonce) and updates latest via
439    /// INSERT OR REPLACE.
440    pub(crate) fn apply_account_delta(
441        tx: &Transaction<'_>,
442        smt_forest: &mut AccountSmtForest,
443        init_account_state: &AccountHeader,
444        final_account_state: &AccountHeader,
445        updated_fungible_assets: BTreeMap<AccountIdPrefix, FungibleAsset>,
446        old_map_roots: &BTreeMap<StorageSlotName, Word>,
447        delta: &AccountDelta,
448    ) -> Result<(), StoreError> {
449        let account_id = final_account_state.id();
450
451        // Insert the new account header
452        Self::insert_account_header(tx, final_account_state, None)?;
453
454        Self::apply_account_vault_delta(
455            tx,
456            smt_forest,
457            account_id,
458            init_account_state,
459            final_account_state,
460            updated_fungible_assets,
461            delta,
462        )?;
463
464        // Build the final roots from the init state's registered roots:
465        // - Replace vault root with the final one
466        // - Replace changed map roots with their new values (done by apply_account_storage_delta)
467        // - Unchanged map roots continue as they were
468        let mut final_roots = smt_forest
469            .get_roots(&init_account_state.id())
470            .cloned()
471            .ok_or(StoreError::AccountDataNotFound(init_account_state.id()))?;
472
473        // First element is always the vault root
474        if let Some(vault_root) = final_roots.first_mut() {
475            *vault_root = final_account_state.vault_root();
476        }
477
478        let default_map_root = StorageMap::default().root();
479        let updated_storage_slots =
480            Self::apply_account_storage_delta(smt_forest, old_map_roots, delta)?;
481
482        // Update map roots in final_roots with new values from the delta
483        for (slot_name, (new_root, slot_type)) in &updated_storage_slots {
484            if *slot_type == StorageSlotType::Map {
485                let old_root = old_map_roots.get(slot_name).copied().unwrap_or(default_map_root);
486                if let Some(root) = final_roots.iter_mut().find(|r| **r == old_root) {
487                    *root = *new_root;
488                } else {
489                    // New map slot not in the old roots — append it
490                    final_roots.push(*new_root);
491                }
492            }
493        }
494
495        Self::write_storage_delta(
496            tx,
497            account_id,
498            final_account_state.nonce().as_int(),
499            &updated_storage_slots,
500            delta,
501        )?;
502
503        smt_forest.stage_roots(final_account_state.id(), final_roots);
504
505        Ok(())
506    }
507
508    /// Removes discarded account states from the database, restores previous
509    /// roots in the SMT forest, and rebuilds the latest tables for affected accounts.
510    pub(crate) fn undo_account_state(
511        tx: &Transaction<'_>,
512        smt_forest: &mut AccountSmtForest,
513        discarded_states: &[(AccountId, Word)],
514    ) -> Result<(), StoreError> {
515        if discarded_states.is_empty() {
516            return Ok(());
517        }
518
519        let commitment_params = Rc::new(
520            discarded_states
521                .iter()
522                .map(|(_, commitment)| Value::from(commitment.to_hex()))
523                .collect::<Vec<_>>(),
524        );
525
526        // Resolve (account_id, nonce) pairs for the accounts being undone
527        const RESOLVE_QUERY: &str = "SELECT id, nonce FROM historical_account_headers WHERE account_commitment IN rarray(?)";
528        let id_nonce_pairs: Vec<(String, u64)> = tx
529            .prepare(RESOLVE_QUERY)
530            .into_store_error()?
531            .query_map(params![commitment_params.clone()], |row| {
532                let id: String = row.get(0)?;
533                let nonce: u64 = column_value_as_u64(row, 1)?;
534                Ok((id, nonce))
535            })
536            .into_store_error()?
537            .filter_map(Result::ok)
538            .collect();
539
540        // Delete historical entries for these (account_id, nonce) pairs
541        for (account_id_hex, nonce) in &id_nonce_pairs {
542            let nonce_val = u64_to_value(*nonce);
543            tx.execute(
544                "DELETE FROM historical_account_storage WHERE account_id = ? AND nonce = ?",
545                params![account_id_hex, nonce_val],
546            )
547            .into_store_error()?;
548            tx.execute(
549                "DELETE FROM historical_storage_map_entries WHERE account_id = ? AND nonce = ?",
550                params![account_id_hex, nonce_val],
551            )
552            .into_store_error()?;
553            tx.execute(
554                "DELETE FROM historical_account_assets WHERE account_id = ? AND nonce = ?",
555                params![account_id_hex, nonce_val],
556            )
557            .into_store_error()?;
558        }
559
560        // Delete from historical_account_headers table
561        const DELETE_QUERY: &str =
562            "DELETE FROM historical_account_headers WHERE account_commitment IN rarray(?)";
563        tx.execute(DELETE_QUERY, params![commitment_params]).into_store_error()?;
564
565        // Rebuild latest tables for affected accounts
566        let mut unique_ids: Vec<String> = id_nonce_pairs.iter().map(|(id, _)| id.clone()).collect();
567        unique_ids.sort();
568        unique_ids.dedup();
569
570        for account_id_hex in &unique_ids {
571            Self::rebuild_latest_for_account(tx, account_id_hex)?;
572        }
573
574        // Discard each rolled-back state from the in-memory forest,
575        // restoring the previous roots for each account.
576        for (account_id, _) in discarded_states {
577            smt_forest.discard_roots(*account_id);
578        }
579
580        Ok(())
581    }
582
583    /// Replaces the account state with a completely new one from the network.
584    ///
585    /// Inserts all vault/storage data, writes tombstones to historical for removed entries,
586    /// and atomically replaces the SMT forest roots.
587    pub(crate) fn update_account_state(
588        tx: &Transaction<'_>,
589        smt_forest: &mut AccountSmtForest,
590        new_account_state: &Account,
591    ) -> Result<(), StoreError> {
592        let account_id = new_account_state.id();
593        let account_id_hex = account_id.to_hex();
594        let nonce = new_account_state.nonce().as_int();
595        let nonce_val = u64_to_value(nonce);
596
597        // Insert and register account state in the SMT forest (handles old root cleanup)
598        smt_forest.insert_and_register_account_state(
599            account_id,
600            new_account_state.vault(),
601            new_account_state.storage(),
602        )?;
603
604        // Write tombstones for all current entries before deleting latest.
605        // insert_storage_slots/insert_assets will overwrite entries that still exist
606        // (INSERT OR REPLACE), leaving only genuinely removed entries as tombstones.
607        tx.execute(
608            "INSERT OR REPLACE INTO historical_storage_map_entries \
609             (account_id, nonce, slot_name, key, value) \
610             SELECT account_id, ?, slot_name, key, NULL \
611             FROM latest_storage_map_entries WHERE account_id = ?",
612            params![&nonce_val, &account_id_hex],
613        )
614        .into_store_error()?;
615        tx.execute(
616            "INSERT OR REPLACE INTO historical_account_assets \
617             (account_id, nonce, vault_key, faucet_id_prefix, asset) \
618             SELECT account_id, ?, vault_key, faucet_id_prefix, NULL \
619             FROM latest_account_assets WHERE account_id = ?",
620            params![&nonce_val, &account_id_hex],
621        )
622        .into_store_error()?;
623
624        // Delete all latest entries for this account
625        tx.execute(
626            "DELETE FROM latest_account_storage WHERE account_id = ?",
627            params![&account_id_hex],
628        )
629        .into_store_error()?;
630        tx.execute(
631            "DELETE FROM latest_storage_map_entries WHERE account_id = ?",
632            params![&account_id_hex],
633        )
634        .into_store_error()?;
635        tx.execute(
636            "DELETE FROM latest_account_assets WHERE account_id = ?",
637            params![&account_id_hex],
638        )
639        .into_store_error()?;
640
641        // Insert all new entries into latest + historical (overwrites tombstones for
642        // entries that still exist)
643        Self::insert_storage_slots(
644            tx,
645            account_id,
646            nonce,
647            new_account_state.storage().slots().iter(),
648        )?;
649        Self::insert_assets(tx, account_id, nonce, new_account_state.vault().assets())?;
650        Self::insert_account_header(tx, &new_account_state.into(), None)?;
651
652        Ok(())
653    }
654
655    /// Locks the account if the mismatched digest doesn't belong to a previous account state (stale
656    /// data).
657    pub(crate) fn lock_account_on_unexpected_commitment(
658        tx: &Transaction<'_>,
659        account_id: &AccountId,
660        mismatched_digest: &Word,
661    ) -> Result<(), StoreError> {
662        // Mismatched digests may be due to stale network data. If the mismatched digest is
663        // tracked in the db and corresponds to the mismatched account, it means we
664        // got a past update and shouldn't lock the account.
665        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)";
666        let account_id_hex = account_id.to_hex();
667        let digest_str = mismatched_digest.to_string();
668        let params = named_params! {
669            ":account_id": account_id_hex,
670            ":digest": digest_str
671        };
672
673        let query = format!("UPDATE latest_account_headers SET locked = true {LOCK_CONDITION}");
674        tx.execute(&query, params).into_store_error()?;
675
676        // Also lock historical rows so that rebuild_latest_for_account preserves the lock.
677        let query = format!("UPDATE historical_account_headers SET locked = true {LOCK_CONDITION}");
678        tx.execute(&query, params).into_store_error()?;
679
680        Ok(())
681    }
682
683    // HELPERS
684    // --------------------------------------------------------------------------------------------
685
686    /// Rebuilds the latest tables for a single account from its historical data.
687    /// If the account has no remaining states, clears its latest entries.
688    fn rebuild_latest_for_account(
689        tx: &Transaction<'_>,
690        account_id_hex: &str,
691    ) -> Result<(), StoreError> {
692        let remaining: Option<u64> = tx
693            .query_row(
694                "SELECT MAX(nonce) FROM historical_account_headers WHERE id = ?",
695                params![account_id_hex],
696                |row| row.get(0),
697            )
698            .into_store_error()?;
699
700        // Clear all latest entries first
701        tx.execute("DELETE FROM latest_account_headers WHERE id = ?", params![account_id_hex])
702            .into_store_error()?;
703        tx.execute(
704            "DELETE FROM latest_account_storage WHERE account_id = ?",
705            params![account_id_hex],
706        )
707        .into_store_error()?;
708        tx.execute(
709            "DELETE FROM latest_storage_map_entries WHERE account_id = ?",
710            params![account_id_hex],
711        )
712        .into_store_error()?;
713        tx.execute(
714            "DELETE FROM latest_account_assets WHERE account_id = ?",
715            params![account_id_hex],
716        )
717        .into_store_error()?;
718
719        if remaining.is_none() {
720            return Ok(());
721        }
722
723        // Rebuild latest_account_headers from historical
724        tx.execute(
725            "INSERT INTO latest_account_headers (id, account_commitment, code_commitment, storage_commitment, vault_root, nonce, account_seed, locked)
726             SELECT h.id, h.account_commitment, h.code_commitment, h.storage_commitment, h.vault_root, h.nonce, h.account_seed, h.locked
727             FROM historical_account_headers h
728             WHERE h.id = ?1 AND h.nonce = (SELECT MAX(nonce) FROM historical_account_headers WHERE id = ?1)",
729            params![account_id_hex],
730        )
731        .into_store_error()?;
732
733        // Rebuild from historical using MAX(nonce) per key
734        tx.execute(
735            "INSERT INTO latest_account_storage (account_id, slot_name, slot_value, slot_type)
736             SELECT h.account_id, h.slot_name, h.slot_value, h.slot_type
737             FROM historical_account_storage h
738             INNER JOIN (
739                 SELECT account_id, slot_name, MAX(nonce) AS max_nonce
740                 FROM historical_account_storage WHERE account_id = ?1
741                 GROUP BY account_id, slot_name
742             ) latest ON h.account_id = latest.account_id
743                 AND h.slot_name = latest.slot_name AND h.nonce = latest.max_nonce",
744            params![account_id_hex],
745        )
746        .into_store_error()?;
747
748        tx.execute(
749            "INSERT INTO latest_storage_map_entries (account_id, slot_name, key, value)
750             SELECT h.account_id, h.slot_name, h.key, h.value
751             FROM historical_storage_map_entries h
752             INNER JOIN (
753                 SELECT account_id, slot_name, key, MAX(nonce) AS max_nonce
754                 FROM historical_storage_map_entries WHERE account_id = ?1
755                 GROUP BY account_id, slot_name, key
756             ) latest ON h.account_id = latest.account_id
757                 AND h.slot_name = latest.slot_name AND h.key = latest.key
758                 AND h.nonce = latest.max_nonce
759             WHERE h.value IS NOT NULL",
760            params![account_id_hex],
761        )
762        .into_store_error()?;
763
764        tx.execute(
765            "INSERT INTO latest_account_assets (account_id, vault_key, faucet_id_prefix, asset)
766             SELECT h.account_id, h.vault_key, h.faucet_id_prefix, h.asset
767             FROM historical_account_assets h
768             INNER JOIN (
769                 SELECT account_id, vault_key, MAX(nonce) AS max_nonce
770                 FROM historical_account_assets WHERE account_id = ?1
771                 GROUP BY account_id, vault_key
772             ) latest ON h.account_id = latest.account_id
773                 AND h.vault_key = latest.vault_key AND h.nonce = latest.max_nonce
774             WHERE h.asset IS NOT NULL",
775            params![account_id_hex],
776        )
777        .into_store_error()?;
778
779        Ok(())
780    }
781
782    /// Inserts a new account record into the database.
783    ///
784    /// Writes to both `historical_account_headers` (append-only log of all state transitions)
785    /// and `latest_account_headers` (current state, one row per account via REPLACE).
786    fn insert_account_header(
787        tx: &Transaction<'_>,
788        account: &AccountHeader,
789        account_seed: Option<Word>,
790    ) -> Result<(), StoreError> {
791        let id: String = account.id().to_hex();
792        let code_commitment = account.code_commitment().to_string();
793        let storage_commitment = account.storage_commitment().to_string();
794        let vault_root = account.vault_root().to_string();
795        let nonce = u64_to_value(account.nonce().as_int());
796        let commitment = account.to_commitment().to_string();
797
798        let account_seed = account_seed.map(|seed| seed.to_bytes());
799
800        const HISTORICAL_QUERY: &str = insert_sql!(
801            historical_account_headers {
802                id,
803                code_commitment,
804                storage_commitment,
805                vault_root,
806                nonce,
807                account_seed,
808                account_commitment,
809                locked
810            } | REPLACE
811        );
812
813        tx.execute(
814            HISTORICAL_QUERY,
815            params![
816                id,
817                code_commitment,
818                storage_commitment,
819                vault_root,
820                nonce,
821                account_seed,
822                commitment,
823                false,
824            ],
825        )
826        .into_store_error()?;
827
828        const LATEST_QUERY: &str = insert_sql!(
829            latest_account_headers {
830                id,
831                code_commitment,
832                storage_commitment,
833                vault_root,
834                nonce,
835                account_seed,
836                account_commitment,
837                locked
838            } | REPLACE
839        );
840
841        tx.execute(
842            LATEST_QUERY,
843            params![
844                id,
845                code_commitment,
846                storage_commitment,
847                vault_root,
848                nonce,
849                account_seed,
850                commitment,
851                false,
852            ],
853        )
854        .into_store_error()?;
855
856        Ok(())
857    }
858
859    fn insert_address_internal(
860        tx: &Transaction<'_>,
861        address: &Address,
862        account_id: AccountId,
863    ) -> Result<(), StoreError> {
864        const QUERY: &str = insert_sql!(addresses { address, account_id } | REPLACE);
865        let serialized_address = address.to_bytes();
866        tx.execute(QUERY, params![serialized_address, account_id.to_hex(),])
867            .into_store_error()?;
868
869        Ok(())
870    }
871
872    fn remove_address_internal(tx: &Transaction<'_>, address: &Address) -> Result<(), StoreError> {
873        let serialized_address = address.to_bytes();
874
875        const DELETE_QUERY: &str = "DELETE FROM addresses WHERE address = ?";
876        tx.execute(DELETE_QUERY, params![serialized_address]).into_store_error()?;
877
878        Ok(())
879    }
880}