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