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