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::AssetVaultKey;
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        has_client_notes: bool,
282    ) -> Result<(), StoreError> {
283        let block_header = block_header.clone();
284        self.interact_with_connection(move |conn| {
285            SqliteStore::insert_block_header(conn, &block_header, has_client_notes)
286        })
287        .await
288    }
289
290    async fn untrack_and_prune_irrelevant_blocks(
291        &self,
292        blocks_to_untrack: &[BlockNumber],
293        node_indices_to_remove: &[InOrderIndex],
294    ) -> Result<(), StoreError> {
295        let blocks_to_untrack = blocks_to_untrack.to_vec();
296        let node_indices_to_remove = node_indices_to_remove.to_vec();
297        self.interact_with_connection(move |conn| {
298            SqliteStore::prune_irrelevant_blocks(conn, &blocks_to_untrack, &node_indices_to_remove)
299        })
300        .await
301    }
302
303    async fn prune_account_history(
304        &self,
305        account_id: AccountId,
306        up_to_nonce: Felt,
307    ) -> Result<usize, StoreError> {
308        self.interact_with_connection(move |conn| {
309            SqliteStore::prune_account_history(conn, account_id, up_to_nonce)
310        })
311        .await
312    }
313
314    async fn get_block_headers(
315        &self,
316        block_numbers: &BTreeSet<BlockNumber>,
317    ) -> Result<Vec<(BlockHeader, BlockRelevance)>, StoreError> {
318        let block_numbers = block_numbers.clone();
319        Ok(self
320            .interact_with_connection(move |conn| {
321                SqliteStore::get_block_headers(conn, &block_numbers)
322            })
323            .await?)
324    }
325
326    async fn get_tracked_block_headers(&self) -> Result<Vec<BlockHeader>, StoreError> {
327        self.interact_with_connection(SqliteStore::get_tracked_block_headers).await
328    }
329
330    async fn get_tracked_block_header_numbers(&self) -> Result<BTreeSet<usize>, StoreError> {
331        self.interact_with_connection(SqliteStore::get_tracked_block_header_numbers)
332            .await
333    }
334
335    async fn get_partial_blockchain_nodes(
336        &self,
337        filter: PartialBlockchainFilter,
338    ) -> Result<BTreeMap<InOrderIndex, Word>, StoreError> {
339        self.interact_with_connection(move |conn| {
340            SqliteStore::get_partial_blockchain_nodes(conn, &filter)
341        })
342        .await
343    }
344
345    async fn insert_partial_blockchain_nodes(
346        &self,
347        nodes: &[(InOrderIndex, Word)],
348    ) -> Result<(), StoreError> {
349        let nodes = nodes.to_vec();
350        self.interact_with_connection(move |conn| {
351            SqliteStore::insert_partial_blockchain_nodes(conn, &nodes)
352        })
353        .await
354    }
355
356    async fn get_current_blockchain_peaks(&self) -> Result<MmrPeaks, StoreError> {
357        self.interact_with_connection(SqliteStore::get_current_blockchain_peaks).await
358    }
359
360    async fn insert_account(
361        &self,
362        account: &Account,
363        initial_address: Address,
364        client_account_type: ClientAccountType,
365    ) -> Result<(), StoreError> {
366        let cloned_account = account.clone();
367        let smt_forest = self.smt_forest.clone();
368
369        self.interact_with_connection(move |conn| {
370            SqliteStore::insert_account(
371                conn,
372                &smt_forest,
373                &cloned_account,
374                &initial_address,
375                client_account_type,
376            )
377        })
378        .await
379    }
380
381    async fn update_account(&self, account: &Account) -> Result<(), StoreError> {
382        let cloned_account = account.clone();
383        let smt_forest = self.smt_forest.clone();
384
385        self.interact_with_connection(move |conn| {
386            SqliteStore::update_account(conn, &smt_forest, &cloned_account)
387        })
388        .await
389    }
390
391    async fn get_account_ids(&self) -> Result<Vec<AccountId>, StoreError> {
392        self.interact_with_connection(SqliteStore::get_account_ids).await
393    }
394
395    async fn get_account_headers(&self) -> Result<Vec<(AccountHeader, AccountStatus)>, StoreError> {
396        self.interact_with_connection(SqliteStore::get_account_headers).await
397    }
398
399    async fn get_account_header(
400        &self,
401        account_id: AccountId,
402    ) -> Result<Option<(AccountHeader, AccountStatus)>, StoreError> {
403        self.interact_with_connection(move |conn| SqliteStore::get_account_header(conn, account_id))
404            .await
405    }
406
407    async fn get_account_header_by_commitment(
408        &self,
409        account_commitment: Word,
410    ) -> Result<Option<AccountHeader>, StoreError> {
411        self.interact_with_connection(move |conn| {
412            SqliteStore::get_account_header_by_commitment(conn, account_commitment)
413        })
414        .await
415    }
416
417    async fn get_account(
418        &self,
419        account_id: AccountId,
420    ) -> Result<Option<AccountRecord>, StoreError> {
421        self.interact_with_connection(move |conn| SqliteStore::get_account(conn, account_id))
422            .await
423    }
424
425    async fn get_account_code(
426        &self,
427        account_id: AccountId,
428    ) -> Result<Option<AccountCode>, StoreError> {
429        self.interact_with_connection(move |conn| {
430            SqliteStore::get_account_code_by_id(conn, account_id)
431        })
432        .await
433    }
434
435    async fn upsert_foreign_account_code(
436        &self,
437        account_id: AccountId,
438        code: AccountCode,
439    ) -> Result<(), StoreError> {
440        self.interact_with_connection(move |conn| {
441            SqliteStore::upsert_foreign_account_code(conn, account_id, &code)
442        })
443        .await
444    }
445
446    async fn get_foreign_account_code(
447        &self,
448        account_ids: Vec<AccountId>,
449    ) -> Result<BTreeMap<AccountId, AccountCode>, StoreError> {
450        self.interact_with_connection(move |conn| {
451            SqliteStore::get_foreign_account_code(conn, account_ids)
452        })
453        .await
454    }
455
456    async fn set_setting(&self, key: String, value: Vec<u8>) -> Result<(), StoreError> {
457        self.interact_with_connection(move |conn| {
458            set_setting(conn, &key, &value).into_store_error()
459        })
460        .await
461    }
462
463    async fn get_setting(&self, key: String) -> Result<Option<Vec<u8>>, StoreError> {
464        self.interact_with_connection(move |conn| get_setting(conn, &key)).await
465    }
466
467    async fn remove_setting(&self, key: String) -> Result<(), StoreError> {
468        self.interact_with_connection(move |conn| remove_setting(conn, &key)).await
469    }
470
471    async fn list_setting_keys(&self) -> Result<Vec<String>, StoreError> {
472        self.interact_with_connection(move |conn| list_setting_keys(conn)).await
473    }
474
475    async fn apply_settings_mutations(
476        &self,
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                        set_setting(&tx, key, value).into_store_error()?;
485                    },
486                    SettingMutation::Remove { key } => remove_setting(&tx, key)?,
487                }
488            }
489            tx.commit().into_store_error()?;
490            Ok(())
491        })
492        .await
493    }
494
495    async fn get_unspent_input_note_nullifiers(&self) -> Result<Vec<Nullifier>, StoreError> {
496        self.interact_with_connection(SqliteStore::get_unspent_input_note_nullifiers)
497            .await
498    }
499
500    async fn get_account_vault(&self, account_id: AccountId) -> Result<AssetVault, StoreError> {
501        self.interact_with_connection(move |conn| SqliteStore::get_account_vault(conn, account_id))
502            .await
503    }
504
505    async fn get_account_asset(
506        &self,
507        account_id: AccountId,
508        vault_key: AssetVaultKey,
509    ) -> Result<Option<(Asset, AssetWitness)>, StoreError> {
510        let smt_forest = self.smt_forest.clone();
511        self.interact_with_connection(move |conn| {
512            SqliteStore::get_account_asset(conn, &smt_forest, account_id, vault_key)
513        })
514        .await
515    }
516
517    async fn get_account_storage(
518        &self,
519        account_id: AccountId,
520        filter: AccountStorageFilter,
521    ) -> Result<AccountStorage, StoreError> {
522        self.interact_with_connection(move |conn| {
523            SqliteStore::get_account_storage(conn, account_id, &filter)
524        })
525        .await
526    }
527
528    async fn get_account_map_item(
529        &self,
530        account_id: AccountId,
531        slot_name: StorageSlotName,
532        key: StorageMapKey,
533    ) -> Result<(Word, StorageMapWitness), StoreError> {
534        let smt_forest = self.smt_forest.clone();
535
536        self.interact_with_connection(move |conn| {
537            SqliteStore::get_account_map_item(conn, &smt_forest, account_id, slot_name, key)
538        })
539        .await
540    }
541
542    async fn get_addresses_by_account_id(
543        &self,
544        account_id: AccountId,
545    ) -> Result<Vec<Address>, StoreError> {
546        self.interact_with_connection(move |conn| {
547            SqliteStore::get_account_addresses(conn, account_id)
548        })
549        .await
550    }
551
552    async fn insert_address(
553        &self,
554        address: Address,
555        account_id: AccountId,
556    ) -> Result<(), StoreError> {
557        self.interact_with_connection(move |conn| {
558            let tx = conn.transaction().into_store_error()?;
559            SqliteStore::insert_address(&tx, &address, account_id)?;
560            tx.commit().into_store_error()
561        })
562        .await
563    }
564
565    async fn remove_address(&self, address: Address) -> Result<(), StoreError> {
566        self.interact_with_connection(move |conn| SqliteStore::remove_address(conn, &address))
567            .await
568    }
569
570    async fn get_minimal_partial_account(
571        &self,
572        account_id: AccountId,
573    ) -> Result<Option<AccountRecord>, StoreError> {
574        self.interact_with_connection(move |conn| {
575            SqliteStore::get_minimal_partial_account(conn, account_id)
576        })
577        .await
578    }
579}
580
581// UTILS
582// ================================================================================================
583
584/// Returns the current UTC timestamp as `u64` (non-leap seconds since Unix epoch).
585pub(crate) fn current_timestamp_u64() -> u64 {
586    let now = chrono::Utc::now();
587    u64::try_from(now.timestamp()).expect("timestamp is always after epoch")
588}
589
590/// Gets a `u64` value from the database.
591///
592/// `Sqlite` uses `i64` as its internal representation format, and so when retrieving
593/// we need to make sure we cast as `u64` to get the original value
594pub fn column_value_as_u64<I: rusqlite::RowIndex>(
595    row: &rusqlite::Row<'_>,
596    index: I,
597) -> rusqlite::Result<u64> {
598    let value: i64 = row.get(index)?;
599    #[allow(
600        clippy::cast_sign_loss,
601        reason = "We store u64 as i64 as sqlite only allows the latter."
602    )]
603    Ok(value as u64)
604}
605
606/// Converts a `u64` into a [Value].
607///
608/// `Sqlite` uses `i64` as its internal representation format. Note that the `as` operator performs
609/// a lossless conversion from `u64` to `i64`.
610pub fn u64_to_value(v: u64) -> Value {
611    #[allow(
612        clippy::cast_possible_wrap,
613        reason = "We store u64 as i64 as sqlite only allows the latter."
614    )]
615    Value::Integer(v as i64)
616}
617
618// TESTS
619// ================================================================================================
620
621#[cfg(test)]
622pub mod tests {
623    use std::boxed::Box;
624
625    use miden_client::store::Store;
626    use miden_client::testing::common::create_test_store_path;
627
628    use super::SqliteStore;
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}