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