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