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