Skip to main content

miden_client_sqlite_store/
lib.rs

1//! SQLite-backed Store implementation for miden-client.
2//! This crate provides `SqliteStore` and its full implementation.
3//!
4//! [`SqliteStore`] enables the persistence of accounts, transactions, notes, block headers, and MMR
5//! nodes using an `SQLite` database.
6
7use std::boxed::Box;
8use std::collections::{BTreeMap, BTreeSet};
9use std::path::{Path, PathBuf};
10use std::string::{String, ToString};
11use std::time::Duration;
12use std::vec::Vec;
13
14use db_management::migration::SqliteMigrator;
15use db_management::pool_manager::{Pool, SqlitePoolManager};
16use deadpool::Runtime;
17use miden_client::Word;
18use miden_client::account::{
19    Account,
20    AccountCode,
21    AccountHeader,
22    AccountId,
23    AccountStorage,
24    Address,
25    StorageMapKey,
26    StorageSlotName,
27};
28use miden_client::asset::{Asset, AssetVault, AssetWitness};
29use miden_client::block::BlockHeader;
30use miden_client::crypto::{InOrderIndex, MmrPeaks};
31use miden_client::note::{BlockNumber, NoteScript, NoteTag, Nullifier};
32use miden_client::store::{
33    AccountRecord,
34    AccountStatus,
35    AccountStorageFilter,
36    BlockRelevance,
37    ClientAccountType,
38    InputNoteCursor,
39    InputNoteRecord,
40    NoteFilter,
41    OutputNoteRecord,
42    PartialBlockchainFilter,
43    SettingMutation,
44    SettingScope,
45    Store,
46    StoreError,
47    TransactionFilter,
48};
49use miden_client::sync::{NoteTagRecord, StateSyncUpdate};
50use miden_client::transaction::{TransactionRecord, TransactionStoreUpdate};
51use miden_protocol::Felt;
52use miden_protocol::account::StorageMapWitness;
53use miden_protocol::asset::AssetId;
54use rusqlite::Connection;
55use rusqlite::types::Value;
56use sql_error::SqlResultExt;
57
58use crate::account::helpers::query_vault_assets;
59
60mod account;
61mod builder;
62mod chain_data;
63mod db_management;
64mod forest;
65mod note;
66mod settings;
67mod sql_error;
68mod sync;
69mod transaction;
70
71pub use builder::ClientBuilderSqliteExt;
72
73// SQLITE STORE
74// ================================================================================================
75
76/// `SQLite`-backed [`Store`] implementation.
77///
78/// Current table definitions are the result of applying every migration under `migrations/` in
79/// order.
80pub struct SqliteStore {
81    pub(crate) pool: Pool,
82    database_filepath: PathBuf,
83}
84
85impl SqliteStore {
86    // CONSTRUCTORS
87    // --------------------------------------------------------------------------------------------
88
89    /// Returns a new instance of [Store] instantiated with the specified configuration options.
90    pub async fn new(database_filepath: PathBuf) -> Result<Self, StoreError> {
91        if database_filepath.to_str().is_none() {
92            return Err(database_error(format!(
93                "database path is not valid UTF-8: {}",
94                database_filepath.display()
95            )));
96        }
97
98        let sqlite_pool_manager = SqlitePoolManager::new(database_filepath.clone());
99        let pool = Pool::builder(sqlite_pool_manager)
100            .wait_timeout(Some(Duration::from_secs(30)))
101            .runtime(Runtime::Tokio1)
102            .build()
103            .map_err(database_error)?;
104
105        Self::migrate(&pool, SqliteMigrator::client()).await?;
106
107        // Account SMT data is persisted in the forest tables and read on demand, so no state
108        // needs to be rebuilt here.
109        Ok(SqliteStore { pool, database_filepath })
110    }
111
112    /// Returns the path of the database file backing this store.
113    pub fn database_filepath(&self) -> &Path {
114        &self.database_filepath
115    }
116
117    /// Brings the database in `pool` up to the latest version of the schema `migration` builds.
118    ///
119    /// The upgrade is verified before it is committed, so a failure is rolled back by `SQLite` and
120    /// leaves the store exactly as it was.
121    async fn migrate(pool: &Pool, migration: &'static SqliteMigrator) -> Result<(), StoreError> {
122        let conn = pool.get().await.map_err(database_error)?;
123
124        conn.interact(move |conn| migration.apply(conn))
125            .await
126            .map_err(database_error)?
127            .map_err(database_error)
128    }
129
130    /// Interacts with the database by executing the provided function on a connection from the
131    /// pool.
132    ///
133    /// This function is a helper method which simplifies the process of making queries to the
134    /// database. It acquires a connection from the pool and executes the provided function,
135    /// returning the result.
136    async fn interact_with_connection<F, R>(&self, f: F) -> Result<R, StoreError>
137    where
138        F: FnOnce(&mut Connection) -> Result<R, StoreError> + Send + 'static,
139        R: Send + 'static,
140    {
141        self.pool
142            .get()
143            .await
144            .map_err(database_error)?
145            .interact(f)
146            .await
147            .map_err(database_error)?
148    }
149}
150
151// SQLite implementation of the Store trait
152//
153// To simplify, all implementations rely on inner SqliteStore functions that map 1:1 by name
154// This way, the actual implementations are grouped by entity types in their own sub-modules
155#[async_trait::async_trait]
156impl Store for SqliteStore {
157    fn identifier(&self) -> &str {
158        self.database_filepath
159            .to_str()
160            .expect("rejected by SqliteStore::new when not UTF-8")
161    }
162
163    fn get_current_timestamp(&self) -> Option<u64> {
164        Some(current_timestamp_u64())
165    }
166
167    async fn get_note_tags(&self) -> Result<Vec<NoteTagRecord>, StoreError> {
168        self.interact_with_connection(SqliteStore::get_note_tags).await
169    }
170
171    async fn get_unique_note_tags(&self) -> Result<BTreeSet<NoteTag>, StoreError> {
172        self.interact_with_connection(SqliteStore::get_unique_note_tags).await
173    }
174
175    async fn add_note_tag(&self, tag: NoteTagRecord) -> Result<bool, StoreError> {
176        self.interact_with_connection(move |conn| SqliteStore::add_note_tag(conn, tag))
177            .await
178    }
179
180    async fn remove_note_tag(&self, tag: NoteTagRecord) -> Result<usize, StoreError> {
181        self.interact_with_connection(move |conn| SqliteStore::remove_note_tag(conn, tag))
182            .await
183    }
184
185    async fn get_sync_height(&self) -> Result<BlockNumber, StoreError> {
186        self.interact_with_connection(SqliteStore::get_sync_height).await
187    }
188
189    async fn apply_state_sync(&self, state_sync_update: StateSyncUpdate) -> Result<(), StoreError> {
190        self.interact_with_connection(move |conn| {
191            SqliteStore::apply_state_sync(conn, state_sync_update)
192        })
193        .await
194    }
195
196    async fn get_transactions(
197        &self,
198        transaction_filter: TransactionFilter,
199    ) -> Result<Vec<TransactionRecord>, StoreError> {
200        self.interact_with_connection(move |conn| {
201            SqliteStore::get_transactions(conn, &transaction_filter)
202        })
203        .await
204    }
205
206    async fn apply_transaction(&self, tx_update: TransactionStoreUpdate) -> Result<(), StoreError> {
207        self.interact_with_connection(move |conn| SqliteStore::apply_transaction(conn, &tx_update))
208            .await
209    }
210
211    async fn apply_transaction_batch(
212        &self,
213        tx_updates: Vec<TransactionStoreUpdate>,
214    ) -> Result<(), StoreError> {
215        self.interact_with_connection(move |conn| {
216            SqliteStore::apply_transaction_batch(conn, &tx_updates)
217        })
218        .await
219    }
220
221    async fn get_input_notes(
222        &self,
223        filter: NoteFilter,
224    ) -> Result<Vec<InputNoteRecord>, StoreError> {
225        self.interact_with_connection(move |conn| SqliteStore::get_input_notes(conn, &filter))
226            .await
227    }
228
229    async fn get_output_notes(
230        &self,
231        note_filter: NoteFilter,
232    ) -> Result<Vec<OutputNoteRecord>, StoreError> {
233        self.interact_with_connection(move |conn| SqliteStore::get_output_notes(conn, &note_filter))
234            .await
235    }
236
237    async fn get_input_note_after(
238        &self,
239        filter: NoteFilter,
240        consumer: AccountId,
241        block_start: Option<BlockNumber>,
242        block_end: Option<BlockNumber>,
243        cursor: Option<InputNoteCursor>,
244    ) -> Result<Option<InputNoteRecord>, StoreError> {
245        self.interact_with_connection(move |conn| {
246            SqliteStore::get_input_note_after(
247                conn,
248                &filter,
249                consumer,
250                block_start,
251                block_end,
252                cursor,
253            )
254        })
255        .await
256    }
257
258    async fn upsert_input_notes(&self, notes: &[InputNoteRecord]) -> Result<(), StoreError> {
259        let notes = notes.to_vec();
260        self.interact_with_connection(move |conn| SqliteStore::upsert_input_notes(conn, &notes))
261            .await
262    }
263
264    async fn get_note_script(&self, script_root: Word) -> Result<NoteScript, StoreError> {
265        self.interact_with_connection(move |conn| SqliteStore::get_note_script(conn, script_root))
266            .await
267    }
268
269    async fn upsert_note_scripts(&self, note_scripts: &[NoteScript]) -> Result<(), StoreError> {
270        let note_scripts = note_scripts.to_vec();
271        self.interact_with_connection(move |conn| {
272            SqliteStore::upsert_note_scripts(conn, &note_scripts)
273        })
274        .await
275    }
276
277    async fn insert_block_header(
278        &self,
279        block_header: &BlockHeader,
280        nodes: &[(InOrderIndex, Word)],
281        has_client_notes: bool,
282    ) -> Result<(), StoreError> {
283        let block_header = block_header.clone();
284        let nodes = nodes.to_vec();
285        self.interact_with_connection(move |conn| {
286            SqliteStore::insert_block_header(conn, &block_header, &nodes, has_client_notes)
287        })
288        .await
289    }
290
291    async fn untrack_and_prune_irrelevant_blocks(
292        &self,
293        blocks_to_untrack: &[BlockNumber],
294        node_indices_to_remove: &[InOrderIndex],
295    ) -> Result<(), StoreError> {
296        let blocks_to_untrack = blocks_to_untrack.to_vec();
297        let node_indices_to_remove = node_indices_to_remove.to_vec();
298        self.interact_with_connection(move |conn| {
299            SqliteStore::prune_irrelevant_blocks(conn, &blocks_to_untrack, &node_indices_to_remove)
300        })
301        .await
302    }
303
304    async fn prune_account_history(
305        &self,
306        account_id: AccountId,
307        up_to_nonce: Felt,
308    ) -> Result<usize, StoreError> {
309        self.interact_with_connection(move |conn| {
310            SqliteStore::prune_account_history(conn, account_id, up_to_nonce)
311        })
312        .await
313    }
314
315    async fn get_block_headers(
316        &self,
317        block_numbers: &BTreeSet<BlockNumber>,
318    ) -> Result<Vec<(BlockHeader, BlockRelevance)>, StoreError> {
319        let block_numbers = block_numbers.clone();
320        Ok(self
321            .interact_with_connection(move |conn| {
322                SqliteStore::get_block_headers(conn, &block_numbers)
323            })
324            .await?)
325    }
326
327    async fn get_tracked_block_headers(&self) -> Result<Vec<BlockHeader>, StoreError> {
328        self.interact_with_connection(SqliteStore::get_tracked_block_headers).await
329    }
330
331    async fn get_tracked_block_header_numbers(&self) -> Result<BTreeSet<usize>, StoreError> {
332        self.interact_with_connection(SqliteStore::get_tracked_block_header_numbers)
333            .await
334    }
335
336    async fn get_partial_blockchain_nodes(
337        &self,
338        filter: PartialBlockchainFilter,
339    ) -> Result<BTreeMap<InOrderIndex, Word>, StoreError> {
340        self.interact_with_connection(move |conn| {
341            SqliteStore::get_partial_blockchain_nodes(conn, &filter)
342        })
343        .await
344    }
345
346    async fn get_current_blockchain_peaks(&self) -> Result<MmrPeaks, StoreError> {
347        self.interact_with_connection(SqliteStore::get_current_blockchain_peaks).await
348    }
349
350    async fn insert_account(
351        &self,
352        account: &Account,
353        initial_address: Address,
354        client_account_type: ClientAccountType,
355    ) -> Result<(), StoreError> {
356        let cloned_account = account.clone();
357
358        self.interact_with_connection(move |conn| {
359            SqliteStore::insert_account(
360                conn,
361                &cloned_account,
362                &initial_address,
363                client_account_type,
364            )
365        })
366        .await
367    }
368
369    async fn update_account(&self, account: &Account) -> Result<(), StoreError> {
370        let cloned_account = account.clone();
371
372        self.interact_with_connection(move |conn| {
373            SqliteStore::update_account(conn, &cloned_account)
374        })
375        .await
376    }
377
378    async fn get_account_ids(&self) -> Result<Vec<AccountId>, StoreError> {
379        self.interact_with_connection(SqliteStore::get_account_ids).await
380    }
381
382    async fn get_account_headers(&self) -> Result<Vec<(AccountHeader, AccountStatus)>, StoreError> {
383        self.interact_with_connection(SqliteStore::get_account_headers).await
384    }
385
386    async fn get_account_header(
387        &self,
388        account_id: AccountId,
389    ) -> Result<Option<(AccountHeader, AccountStatus)>, StoreError> {
390        self.interact_with_connection(move |conn| SqliteStore::get_account_header(conn, account_id))
391            .await
392    }
393
394    async fn get_account_header_by_commitment(
395        &self,
396        account_commitment: Word,
397    ) -> Result<Option<AccountHeader>, StoreError> {
398        self.interact_with_connection(move |conn| {
399            SqliteStore::get_account_header_by_commitment(conn, account_commitment)
400        })
401        .await
402    }
403
404    async fn get_account(
405        &self,
406        account_id: AccountId,
407    ) -> Result<Option<AccountRecord>, StoreError> {
408        self.interact_with_connection(move |conn| SqliteStore::get_account(conn, account_id))
409            .await
410    }
411
412    async fn get_account_code(
413        &self,
414        account_id: AccountId,
415    ) -> Result<Option<AccountCode>, StoreError> {
416        self.interact_with_connection(move |conn| {
417            SqliteStore::get_account_code_by_id(conn, account_id)
418        })
419        .await
420    }
421
422    async fn upsert_foreign_account_code(
423        &self,
424        account_id: AccountId,
425        code: AccountCode,
426    ) -> Result<(), StoreError> {
427        self.interact_with_connection(move |conn| {
428            SqliteStore::upsert_foreign_account_code(conn, account_id, &code)
429        })
430        .await
431    }
432
433    async fn get_foreign_account_code(
434        &self,
435        account_ids: Vec<AccountId>,
436    ) -> Result<BTreeMap<AccountId, AccountCode>, StoreError> {
437        self.interact_with_connection(move |conn| {
438            SqliteStore::get_foreign_account_code(conn, account_ids)
439        })
440        .await
441    }
442
443    async fn set_setting(
444        &self,
445        scope: SettingScope,
446        key: String,
447        value: Vec<u8>,
448    ) -> Result<(), StoreError> {
449        self.interact_with_connection(move |conn| {
450            SqliteStore::set_setting(conn, scope, &key, &value).into_store_error()
451        })
452        .await
453    }
454
455    async fn get_setting(
456        &self,
457        scope: SettingScope,
458        key: String,
459    ) -> Result<Option<Vec<u8>>, StoreError> {
460        self.interact_with_connection(move |conn| SqliteStore::get_setting(conn, scope, &key))
461            .await
462    }
463
464    async fn remove_setting(&self, scope: SettingScope, key: String) -> Result<bool, StoreError> {
465        self.interact_with_connection(move |conn| SqliteStore::remove_setting(conn, scope, &key))
466            .await
467    }
468
469    async fn list_setting_keys(&self, scope: SettingScope) -> Result<Vec<String>, StoreError> {
470        self.interact_with_connection(move |conn| SqliteStore::list_setting_keys(conn, scope))
471            .await
472    }
473
474    async fn apply_settings_mutations(
475        &self,
476        scope: SettingScope,
477        mutations: Vec<SettingMutation>,
478    ) -> Result<(), StoreError> {
479        self.interact_with_connection(move |conn| {
480            let tx = conn.transaction().into_store_error()?;
481            for mutation in &mutations {
482                match mutation {
483                    SettingMutation::Set { key, value } => {
484                        SqliteStore::set_setting(&tx, scope, key, value).into_store_error()?;
485                    },
486                    SettingMutation::Remove { key } => {
487                        SqliteStore::remove_setting(&tx, scope, key)?;
488                    },
489                }
490            }
491            tx.commit().into_store_error()?;
492            Ok(())
493        })
494        .await
495    }
496
497    async fn get_unspent_input_note_nullifiers(&self) -> Result<Vec<Nullifier>, StoreError> {
498        self.interact_with_connection(SqliteStore::get_unspent_input_note_nullifiers)
499            .await
500    }
501
502    async fn get_account_vault(&self, account_id: AccountId) -> Result<AssetVault, StoreError> {
503        self.interact_with_connection(move |conn| SqliteStore::get_account_vault(conn, account_id))
504            .await
505    }
506
507    async fn get_account_assets(&self, account_id: AccountId) -> Result<Vec<Asset>, StoreError> {
508        self.interact_with_connection(move |conn| query_vault_assets(conn, account_id))
509            .await
510    }
511
512    async fn get_vault_asset_witnesses(
513        &self,
514        account_id: AccountId,
515        vault_root: Word,
516        asset_ids: BTreeSet<AssetId>,
517    ) -> Result<Vec<AssetWitness>, StoreError> {
518        self.interact_with_connection(move |conn| {
519            SqliteStore::get_vault_asset_witnesses(conn, account_id, vault_root, asset_ids)
520        })
521        .await
522    }
523
524    async fn get_account_asset(
525        &self,
526        account_id: AccountId,
527        asset_id: AssetId,
528    ) -> Result<Option<(Asset, AssetWitness)>, StoreError> {
529        self.interact_with_connection(move |conn| {
530            SqliteStore::get_account_asset(conn, account_id, asset_id)
531        })
532        .await
533    }
534
535    async fn get_account_map_item(
536        &self,
537        account_id: AccountId,
538        slot_name: StorageSlotName,
539        key: StorageMapKey,
540    ) -> Result<(Word, StorageMapWitness), StoreError> {
541        self.interact_with_connection(move |conn| {
542            SqliteStore::get_account_map_item(conn, account_id, slot_name, key)
543        })
544        .await
545    }
546
547    async fn get_account_storage(
548        &self,
549        account_id: AccountId,
550        filter: AccountStorageFilter,
551    ) -> Result<AccountStorage, StoreError> {
552        self.interact_with_connection(move |conn| {
553            SqliteStore::get_account_storage(conn, account_id, &filter)
554        })
555        .await
556    }
557
558    async fn get_addresses_by_account_id(
559        &self,
560        account_id: AccountId,
561    ) -> Result<Vec<Address>, StoreError> {
562        self.interact_with_connection(move |conn| {
563            SqliteStore::get_account_addresses(conn, account_id)
564        })
565        .await
566    }
567
568    async fn insert_address(
569        &self,
570        address: Address,
571        account_id: AccountId,
572    ) -> Result<(), StoreError> {
573        self.interact_with_connection(move |conn| {
574            let tx = conn.transaction().into_store_error()?;
575            SqliteStore::insert_address(&tx, &address, account_id)?;
576            tx.commit().into_store_error()
577        })
578        .await
579    }
580
581    async fn remove_address(&self, address: Address) -> Result<bool, StoreError> {
582        self.interact_with_connection(move |conn| SqliteStore::remove_address(conn, &address))
583            .await
584    }
585
586    async fn get_minimal_partial_account(
587        &self,
588        account_id: AccountId,
589    ) -> Result<Option<AccountRecord>, StoreError> {
590        self.interact_with_connection(move |conn| {
591            SqliteStore::get_minimal_partial_account(conn, account_id)
592        })
593        .await
594    }
595}
596
597// UTILS
598// ================================================================================================
599
600fn database_error(err: impl core::fmt::Display) -> StoreError {
601    StoreError::DatabaseError(err.to_string())
602}
603
604/// Returns the current UTC timestamp as `u64` (non-leap seconds since Unix epoch).
605pub(crate) fn current_timestamp_u64() -> u64 {
606    let now = chrono::Utc::now();
607    u64::try_from(now.timestamp()).expect("timestamp is always after epoch")
608}
609
610/// Gets a `u64` value from the database.
611///
612/// `Sqlite` uses `i64` as its internal representation format, and so when retrieving
613/// we need to make sure we cast as `u64` to get the original value
614pub fn column_value_as_u64<I: rusqlite::RowIndex>(
615    row: &rusqlite::Row<'_>,
616    index: I,
617) -> rusqlite::Result<u64> {
618    let value: i64 = row.get(index)?;
619    #[allow(
620        clippy::cast_sign_loss,
621        reason = "We store u64 as i64 as sqlite only allows the latter."
622    )]
623    Ok(value as u64)
624}
625
626/// Converts a `u64` into a [Value].
627///
628/// `Sqlite` uses `i64` as its internal representation format. Note that the `as` operator performs
629/// a lossless conversion from `u64` to `i64`.
630pub fn u64_to_value(v: u64) -> Value {
631    #[allow(
632        clippy::cast_possible_wrap,
633        reason = "We store u64 as i64 as sqlite only allows the latter."
634    )]
635    Value::Integer(v as i64)
636}
637
638// TESTS
639// ================================================================================================
640
641#[cfg(test)]
642pub mod tests {
643    use std::boxed::Box;
644    use std::sync::LazyLock;
645
646    use miden_client::store::Store;
647    use miden_client::testing::common::create_test_store_path;
648
649    use super::db_management::migration::SqliteMigrator;
650    use super::db_management::migration::tests::damaging_migration;
651    use super::db_management::pool_manager::SqlitePoolManager;
652    use super::{Pool, SqliteStore};
653
654    /// A migration set that changes the store and is then rejected, which is the failure the
655    /// rollback has to undo.
656    static DAMAGING_MIGRATION: LazyLock<SqliteMigrator> = LazyLock::new(damaging_migration);
657
658    #[tokio::test]
659    async fn failed_migration_leaves_the_store_as_it_was() {
660        let database_filepath = create_test_store_path();
661        drop(SqliteStore::new(database_filepath.clone()).await.unwrap());
662
663        let pool = Pool::builder(SqlitePoolManager::new(database_filepath.clone()))
664            .build()
665            .unwrap();
666        let err = SqliteStore::migrate(&pool, &DAMAGING_MIGRATION).await.unwrap_err();
667
668        assert!(
669            err.to_string().contains("produced a schema this client does not expect"),
670            "the migration should have been rejected, got {err}"
671        );
672        // Reopening verifies the schema, so it only succeeds if the dropped table is still there.
673        SqliteStore::new(database_filepath).await.unwrap();
674    }
675
676    fn assert_send_sync<T: Send + Sync>() {}
677
678    #[test]
679    fn is_send_sync() {
680        assert_send_sync::<SqliteStore>();
681        assert_send_sync::<Box<dyn Store>>();
682    }
683
684    // Function that returns a `Send` future from a dynamic trait that must be `Sync`.
685    async fn dyn_trait_send_fut(store: Box<dyn Store>) {
686        // This wouldn't compile if `get_tracked_block_headers` doesn't return a `Send` future.
687        let res = store.get_tracked_block_headers().await;
688        assert!(res.is_ok());
689    }
690
691    #[tokio::test]
692    async fn future_is_send() {
693        let client = SqliteStore::new(create_test_store_path()).await.unwrap();
694        let client: Box<SqliteStore> = client.into();
695        tokio::task::spawn(async move { dyn_trait_send_fut(client).await });
696    }
697
698    pub(crate) async fn create_test_store() -> SqliteStore {
699        SqliteStore::new(create_test_store_path()).await.unwrap()
700    }
701}