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