miden_client/store/mod.rs
1//! Defines the storage interfaces used by the Miden client.
2//!
3//! It provides mechanisms for persisting and retrieving data, such as account states, transaction
4//! history, block headers, notes, and MMR nodes.
5//!
6//! ## Overview
7//!
8//! The storage module is central to the Miden client’s persistence layer. It defines the
9//! [`Store`] trait which abstracts over any concrete storage implementation. The trait exposes
10//! methods to (among others):
11//!
12//! - Retrieve and update transactions, notes, and accounts.
13//! - Store and query block headers along with MMR peaks and authentication nodes.
14//! - Manage note tags for synchronizing with the node.
15//!
16//! These are all used by the Miden client to provide transaction execution in the correct contexts.
17//!
18//! In addition to the main [`Store`] trait, the module provides types for filtering queries, such
19//! as [`TransactionFilter`] and [`NoteFilter`], to narrow down the set of returned transactions or
20//! notes. For more advanced usage, see the documentation of individual methods in the [`Store`]
21//! trait.
22
23use alloc::boxed::Box;
24use alloc::collections::{BTreeMap, BTreeSet};
25use alloc::vec::Vec;
26use core::fmt::Debug;
27
28use miden_objects::Word;
29use miden_objects::account::{Account, AccountCode, AccountHeader, AccountId};
30use miden_objects::block::{BlockHeader, BlockNumber};
31use miden_objects::crypto::merkle::{InOrderIndex, MmrPeaks};
32use miden_objects::note::{NoteId, NoteTag, Nullifier};
33use miden_objects::transaction::TransactionId;
34
35use crate::sync::{NoteTagRecord, StateSyncUpdate};
36use crate::transaction::{TransactionRecord, TransactionStoreUpdate};
37
38/// Contains [`ClientDataStore`] to automatically implement [`DataStore`] for anything that
39/// implements [`Store`]. This isn't public because it's an implementation detail to instantiate the
40/// executor.
41///
42/// The user is tasked with creating a [`Store`] which the client will wrap into a
43/// [`ClientDataStore`] at creation time.
44pub(crate) mod data_store;
45
46mod errors;
47pub use errors::*;
48
49#[cfg(all(feature = "sqlite", feature = "idxdb"))]
50compile_error!("features `sqlite` and `idxdb` are mutually exclusive");
51
52#[cfg(feature = "sqlite")]
53pub mod sqlite_store;
54
55#[cfg(feature = "idxdb")]
56pub mod web_store;
57
58mod account;
59pub use account::{AccountRecord, AccountStatus, AccountUpdates};
60mod note_record;
61pub use note_record::{
62 InputNoteRecord,
63 InputNoteState,
64 NoteExportType,
65 NoteRecordError,
66 OutputNoteRecord,
67 OutputNoteState,
68 input_note_states,
69};
70
71// STORE TRAIT
72// ================================================================================================
73
74/// The [`Store`] trait exposes all methods that the client store needs in order to track the
75/// current state.
76///
77/// All update functions are implied to be atomic. That is, if multiple entities are meant to be
78/// updated as part of any single function and an error is returned during its execution, any
79/// changes that might have happened up to that point need to be rolled back and discarded.
80///
81/// Because the [`Store`]'s ownership is shared between the executor and the client, interior
82/// mutability is expected to be implemented, which is why all methods receive `&self` and
83/// not `&mut self`.
84#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
85#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
86pub trait Store: Send + Sync {
87 /// Returns the current timestamp tracked by the store, measured in non-leap seconds since
88 /// Unix epoch. If the store implementation is incapable of tracking time, it should return
89 /// `None`.
90 ///
91 /// This method is used to add time metadata to notes' states. This information doesn't have a
92 /// functional impact on the client's operation, it's shown to the user for informational
93 /// purposes.
94 fn get_current_timestamp(&self) -> Option<u64>;
95
96 // TRANSACTIONS
97 // --------------------------------------------------------------------------------------------
98
99 /// Retrieves stored transactions, filtered by [`TransactionFilter`].
100 async fn get_transactions(
101 &self,
102 filter: TransactionFilter,
103 ) -> Result<Vec<TransactionRecord>, StoreError>;
104
105 /// Applies a transaction, atomically updating the current state based on the
106 /// [`TransactionStoreUpdate`].
107 ///
108 /// An update involves:
109 /// - Updating the stored account which is being modified by the transaction.
110 /// - Storing new input/output notes and payback note details as a result of the transaction
111 /// execution.
112 /// - Updating the input notes that are being processed by the transaction.
113 /// - Inserting the new tracked tags into the store.
114 /// - Inserting the transaction into the store to track.
115 async fn apply_transaction(&self, tx_update: TransactionStoreUpdate) -> Result<(), StoreError>;
116
117 // NOTES
118 // --------------------------------------------------------------------------------------------
119
120 /// Retrieves the input notes from the store.
121 async fn get_input_notes(&self, filter: NoteFilter)
122 -> Result<Vec<InputNoteRecord>, StoreError>;
123
124 /// Retrieves the output notes from the store.
125 async fn get_output_notes(
126 &self,
127 filter: NoteFilter,
128 ) -> Result<Vec<OutputNoteRecord>, StoreError>;
129
130 /// Returns the nullifiers of all unspent input notes.
131 ///
132 /// The default implementation of this method uses [`Store::get_input_notes`].
133 async fn get_unspent_input_note_nullifiers(&self) -> Result<Vec<Nullifier>, StoreError> {
134 self.get_input_notes(NoteFilter::Unspent)
135 .await?
136 .iter()
137 .map(|input_note| Ok(input_note.nullifier()))
138 .collect::<Result<Vec<_>, _>>()
139 }
140
141 /// Inserts the provided input notes into the database. If a note with the same ID already
142 /// exists, it will be replaced.
143 async fn upsert_input_notes(&self, notes: &[InputNoteRecord]) -> Result<(), StoreError>;
144
145 // CHAIN DATA
146 // --------------------------------------------------------------------------------------------
147
148 /// Retrieves a vector of [`BlockHeader`]s filtered by the provided block numbers.
149 ///
150 /// The returned vector may not contain some or all of the requested block headers. It's up to
151 /// the callee to check whether all requested block headers were found.
152 ///
153 /// For each block header an additional boolean value is returned representing whether the block
154 /// contains notes relevant to the client.
155 async fn get_block_headers(
156 &self,
157 block_numbers: &BTreeSet<BlockNumber>,
158 ) -> Result<Vec<(BlockHeader, BlockRelevance)>, StoreError>;
159
160 /// Retrieves a [`BlockHeader`] corresponding to the provided block number and a boolean value
161 /// that represents whether the block contains notes relevant to the client. Returns `None` if
162 /// the block is not found.
163 ///
164 /// The default implementation of this method uses [`Store::get_block_headers`].
165 async fn get_block_header_by_num(
166 &self,
167 block_number: BlockNumber,
168 ) -> Result<Option<(BlockHeader, BlockRelevance)>, StoreError> {
169 self.get_block_headers(&[block_number].into_iter().collect())
170 .await
171 .map(|mut block_headers_list| block_headers_list.pop())
172 }
173
174 /// Retrieves a list of [`BlockHeader`] that include relevant notes to the client.
175 async fn get_tracked_block_headers(&self) -> Result<Vec<BlockHeader>, StoreError>;
176
177 /// Retrieves all MMR authentication nodes based on [`PartialBlockchainFilter`].
178 async fn get_partial_blockchain_nodes(
179 &self,
180 filter: PartialBlockchainFilter,
181 ) -> Result<BTreeMap<InOrderIndex, Word>, StoreError>;
182
183 /// Inserts blockchain MMR authentication nodes.
184 ///
185 /// In the case where the [`InOrderIndex`] already exists on the table, the insertion is
186 /// ignored.
187 async fn insert_partial_blockchain_nodes(
188 &self,
189 nodes: &[(InOrderIndex, Word)],
190 ) -> Result<(), StoreError>;
191
192 /// Returns peaks information from the blockchain by a specific block number.
193 ///
194 /// If there is no partial blockchain info stored for the provided block returns an empty
195 /// [`MmrPeaks`].
196 async fn get_partial_blockchain_peaks_by_block_num(
197 &self,
198 block_num: BlockNumber,
199 ) -> Result<MmrPeaks, StoreError>;
200
201 /// Inserts a block header into the store, alongside peaks information at the block's height.
202 ///
203 /// `has_client_notes` describes whether the block has relevant notes to the client; this means
204 /// the client might want to authenticate merkle paths based on this value.
205 /// If the block header exists and `has_client_notes` is `true` then the `has_client_notes`
206 /// column is updated to `true` to signify that the block now contains a relevant note.
207 async fn insert_block_header(
208 &self,
209 block_header: &BlockHeader,
210 partial_blockchain_peaks: MmrPeaks,
211 has_client_notes: bool,
212 ) -> Result<(), StoreError>;
213
214 /// Removes block headers that do not contain any client notes and aren't the genesis or last
215 /// block.
216 async fn prune_irrelevant_blocks(&self) -> Result<(), StoreError>;
217
218 // ACCOUNT
219 // --------------------------------------------------------------------------------------------
220
221 /// Returns the account IDs of all accounts stored in the database.
222 async fn get_account_ids(&self) -> Result<Vec<AccountId>, StoreError>;
223
224 /// Returns a list of [`AccountHeader`] of all accounts stored in the database along with their
225 /// statuses.
226 ///
227 /// Said accounts' state is the state after the last performed sync.
228 async fn get_account_headers(&self) -> Result<Vec<(AccountHeader, AccountStatus)>, StoreError>;
229
230 /// Retrieves an [`AccountHeader`] object for the specified [`AccountId`] along with its status.
231 /// Returns `None` if the account is not found.
232 ///
233 /// Said account's state is the state according to the last sync performed.
234 async fn get_account_header(
235 &self,
236 account_id: AccountId,
237 ) -> Result<Option<(AccountHeader, AccountStatus)>, StoreError>;
238
239 /// Returns an [`AccountHeader`] corresponding to the stored account state that matches the
240 /// given commitment. If no account state matches the provided commitment, `None` is returned.
241 async fn get_account_header_by_commitment(
242 &self,
243 account_commitment: Word,
244 ) -> Result<Option<AccountHeader>, StoreError>;
245
246 /// Retrieves a full [`AccountRecord`] object, this contains the account's latest state along
247 /// with its status. Returns `None` if the account is not found.
248 async fn get_account(&self, account_id: AccountId)
249 -> Result<Option<AccountRecord>, StoreError>;
250
251 /// Inserts an [`Account`] along with the seed used to create it.
252 async fn insert_account(
253 &self,
254 account: &Account,
255 account_seed: Option<Word>,
256 ) -> Result<(), StoreError>;
257
258 /// Upserts the account code for a foreign account. This value will be used as a cache of known
259 /// script roots and added to the `GetForeignAccountCode` request.
260 async fn upsert_foreign_account_code(
261 &self,
262 account_id: AccountId,
263 code: AccountCode,
264 ) -> Result<(), StoreError>;
265
266 /// Retrieves the cached account code for various foreign accounts.
267 async fn get_foreign_account_code(
268 &self,
269 account_ids: Vec<AccountId>,
270 ) -> Result<BTreeMap<AccountId, AccountCode>, StoreError>;
271
272 /// Updates an existing [`Account`] with a new state.
273 ///
274 /// # Errors
275 ///
276 /// Returns a `StoreError::AccountDataNotFound` if there is no account for the provided ID.
277 async fn update_account(&self, new_account_state: &Account) -> Result<(), StoreError>;
278
279 // SYNC
280 // --------------------------------------------------------------------------------------------
281
282 /// Returns the note tag records that the client is interested in.
283 async fn get_note_tags(&self) -> Result<Vec<NoteTagRecord>, StoreError>;
284
285 /// Returns the unique note tags (without source) that the client is interested in.
286 async fn get_unique_note_tags(&self) -> Result<BTreeSet<NoteTag>, StoreError> {
287 Ok(self.get_note_tags().await?.into_iter().map(|r| r.tag).collect())
288 }
289
290 /// Adds a note tag to the list of tags that the client is interested in.
291 ///
292 /// If the tag was already being tracked, returns false since no new tags were actually added.
293 /// Otherwise true.
294 async fn add_note_tag(&self, tag: NoteTagRecord) -> Result<bool, StoreError>;
295
296 /// Removes a note tag from the list of tags that the client is interested in.
297 ///
298 /// If the tag wasn't present in the store returns false since no tag was actually removed.
299 /// Otherwise returns true.
300 async fn remove_note_tag(&self, tag: NoteTagRecord) -> Result<usize, StoreError>;
301
302 /// Returns the block number of the last state sync block.
303 async fn get_sync_height(&self) -> Result<BlockNumber, StoreError>;
304
305 /// Applies the state sync update to the store. An update involves:
306 ///
307 /// - Inserting the new block header to the store alongside new MMR peaks information.
308 /// - Updating the corresponding tracked input/output notes.
309 /// - Removing note tags that are no longer relevant.
310 /// - Updating transactions in the store, marking as `committed` or `discarded`.
311 /// - In turn, validating private account's state transitions. If a private account's
312 /// commitment locally does not match the `StateSyncUpdate` information, the account may be
313 /// locked.
314 /// - Storing new MMR authentication nodes.
315 /// - Updating the tracked public accounts.
316 async fn apply_state_sync(&self, state_sync_update: StateSyncUpdate) -> Result<(), StoreError>;
317}
318
319// PARTIAL BLOCKCHAIN NODE FILTER
320// ================================================================================================
321
322/// Filters for searching specific MMR nodes.
323// TODO: Should there be filters for specific blocks instead of nodes?
324pub enum PartialBlockchainFilter {
325 /// Return all nodes.
326 All,
327 /// Filter by the specified in-order indices.
328 List(Vec<InOrderIndex>),
329}
330
331// TRANSACTION FILTERS
332// ================================================================================================
333
334/// Filters for narrowing the set of transactions returned by the client's store.
335#[derive(Debug, Clone)]
336pub enum TransactionFilter {
337 /// Return all transactions.
338 All,
339 /// Filter by transactions that haven't yet been committed to the blockchain as per the last
340 /// sync.
341 Uncommitted,
342 /// Return a list of the transaction that matches the provided [`TransactionId`]s.
343 Ids(Vec<TransactionId>),
344 /// Return a list of the expired transactions that were executed before the provided
345 /// [`BlockNumber`]. Transactions created after the provided block number are not
346 /// considered.
347 ///
348 /// A transaction is considered expired if is uncommitted and the transaction's block number
349 /// is less than the provided block number.
350 ExpiredBefore(BlockNumber),
351}
352
353// NOTE FILTER
354// ================================================================================================
355
356/// Filters for narrowing the set of notes returned by the client's store.
357#[derive(Debug, Clone)]
358pub enum NoteFilter {
359 /// Return a list of all notes ([`InputNoteRecord`] or [`OutputNoteRecord`]).
360 All,
361 /// Return a list of committed notes ([`InputNoteRecord`] or [`OutputNoteRecord`]). These
362 /// represent notes that the blockchain has included in a block.
363 Committed,
364 /// Filter by consumed notes ([`InputNoteRecord`] or [`OutputNoteRecord`]). notes that have
365 /// been used as inputs in transactions.
366 Consumed,
367 /// Return a list of expected notes ([`InputNoteRecord`] or [`OutputNoteRecord`]). These
368 /// represent notes for which the store doesn't have anchor data.
369 Expected,
370 /// Return a list containing any notes that match with the provided [`NoteId`] vector.
371 List(Vec<NoteId>),
372 /// Return a list containing any notes that match the provided [`Nullifier`] vector.
373 Nullifiers(Vec<Nullifier>),
374 /// Return a list of notes that are currently being processed. This filter doesn't apply to
375 /// output notes.
376 Processing,
377 /// Return a list containing the note that matches with the provided [`NoteId`]. The query will
378 /// return an error if the note isn't found.
379 Unique(NoteId),
380 /// Return a list containing notes that haven't been nullified yet, this includes expected,
381 /// committed, processing and unverified notes.
382 Unspent,
383 /// Return a list containing notes with unverified inclusion proofs. This filter doesn't apply
384 /// to output notes.
385 Unverified,
386}
387
388// BLOCK RELEVANCE
389// ================================================================================================
390
391/// Expresses metadata about the block header.
392#[derive(Debug, Clone)]
393pub enum BlockRelevance {
394 /// The block header includes notes that the client may consume.
395 HasNotes,
396 /// The block header does not contain notes relevant to the client.
397 Irrelevant,
398}
399
400impl From<BlockRelevance> for bool {
401 fn from(val: BlockRelevance) -> Self {
402 match val {
403 BlockRelevance::HasNotes => true,
404 BlockRelevance::Irrelevant => false,
405 }
406 }
407}
408
409impl From<bool> for BlockRelevance {
410 fn from(has_notes: bool) -> Self {
411 if has_notes {
412 BlockRelevance::HasNotes
413 } else {
414 BlockRelevance::Irrelevant
415 }
416 }
417}