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