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`], [`NoteFilter`], `StorageFilter` to narrow down the set of returned
20//! transactions, account data, or notes. For more advanced usage, see the documentation of
21//! individual methods in the [`Store`] trait.
22
23use alloc::boxed::Box;
24use alloc::collections::{BTreeMap, BTreeSet};
25use alloc::string::{String, ToString};
26use alloc::vec::Vec;
27use core::fmt::Debug;
28
29use miden_protocol::account::{
30 Account,
31 AccountCode,
32 AccountHeader,
33 AccountId,
34 AccountStorage,
35 StorageMapKey,
36 StorageMapWitness,
37 StorageSlot,
38 StorageSlotContent,
39 StorageSlotName,
40};
41use miden_protocol::address::Address;
42use miden_protocol::asset::{Asset, AssetId, AssetVault, AssetWitness};
43use miden_protocol::block::{BlockHeader, BlockNumber};
44use miden_protocol::crypto::merkle::MerkleError;
45use miden_protocol::crypto::merkle::mmr::{Forest, InOrderIndex, MmrPeaks, PartialMmr};
46use miden_protocol::errors::AccountError;
47use miden_protocol::note::{
48 NoteDetailsCommitment,
49 NoteId,
50 NoteScript,
51 NoteScriptRoot,
52 NoteTag,
53 Nullifier,
54};
55use miden_protocol::transaction::TransactionId;
56use miden_protocol::{Felt, Word};
57use miden_tx::utils::serde::{Deserializable, Serializable};
58
59use crate::note_transport::{NOTE_TRANSPORT_CURSOR_STORE_SETTING, NoteTransportCursor};
60use crate::rpc::encryption::{TRANSACTION_ENCRYPTION_KEY_STORE_SETTING, TransactionEncryptionKey};
61use crate::rpc::{RPC_LIMITS_STORE_SETTING, RpcLimits};
62use crate::sync::{NoteTagRecord, StateSyncUpdate};
63use crate::transaction::{TransactionRecord, TransactionStatusVariant, TransactionStoreUpdate};
64
65/// Contains [`ClientDataStore`] to automatically implement [`DataStore`] for anything that
66/// implements [`Store`]. This isn't public because it's an implementation detail to instantiate the
67/// executor.
68///
69/// The user is tasked with creating a [`Store`] which the client will wrap into a
70/// [`ClientDataStore`] at creation time.
71pub(crate) mod data_store;
72
73mod errors;
74pub use errors::*;
75
76mod smt_forest;
77pub use smt_forest::{AccountSmtForest, AccountUpdate};
78
79mod account;
80pub use account::{
81 AccountRecord,
82 AccountRecordData,
83 AccountStatus,
84 AccountUpdates,
85 ClientAccountType,
86};
87
88pub use crate::sync::PublicAccountUpdate;
89mod note_record;
90pub use note_record::{
91 InputNoteRecord,
92 InputNoteState,
93 NoteExportType,
94 NoteRecordError,
95 OutputNoteRecord,
96 OutputNoteState,
97 input_note_states,
98};
99
100// SETTING SCOPE
101// ================================================================================================
102
103/// Which side of the client/user boundary a `settings` row belongs to.
104///
105/// The discriminants are what a store persists, so they are part of its schema.
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107#[repr(u8)]
108pub enum SettingScope {
109 /// Owned by the client itself. A store persists these rows but the public settings API on
110 /// [`Client`](crate::Client) never reaches them.
111 Client = 0,
112 /// Owned by the user of the client.
113 User = 1,
114}
115
116impl SettingScope {
117 /// Returns the value this scope is stored as.
118 pub fn as_u8(self) -> u8 {
119 self as u8
120 }
121}
122
123// SETTING MUTATION
124// ================================================================================================
125
126/// A single mutation against the `settings` KV store, applied as part of an atomic batch via
127/// [`Store::apply_settings_mutations`].
128#[derive(Debug, Clone)]
129pub enum SettingMutation {
130 /// Insert or overwrite `key` with `value`.
131 Set { key: String, value: Vec<u8> },
132 /// Delete `key`.
133 Remove { key: String },
134}
135
136// INPUT NOTE CURSOR
137// ================================================================================================
138
139/// Identifies a position in the per-account consumption order of input notes.
140///
141/// Obtained from a record returned by [`Store::get_input_note_after`] and passed back to fetch
142/// the note that follows it.
143#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
144pub struct InputNoteCursor {
145 consumed_block_height: BlockNumber,
146 consumed_tx_order: u32,
147 details_commitment: NoteDetailsCommitment,
148}
149
150impl InputNoteCursor {
151 /// Returns the cursor pointing at `record`, or `None` if the note is not consumed.
152 pub fn from_record(record: &InputNoteRecord) -> Option<Self> {
153 Some(Self {
154 consumed_block_height: record.state().consumed_block_height()?,
155 consumed_tx_order: record.state().consumed_tx_order()?,
156 details_commitment: record.details_commitment(),
157 })
158 }
159
160 /// Returns the block height at which the note was consumed.
161 pub fn consumed_block_height(&self) -> BlockNumber {
162 self.consumed_block_height
163 }
164
165 /// Returns the per-account position of the consuming transaction within the block.
166 pub fn consumed_tx_order(&self) -> u32 {
167 self.consumed_tx_order
168 }
169
170 /// Returns the commitment to the note's details.
171 pub fn details_commitment(&self) -> NoteDetailsCommitment {
172 self.details_commitment
173 }
174}
175
176// STORE TRAIT
177// ================================================================================================
178
179/// The [`Store`] trait exposes all methods that the client store needs in order to track the
180/// current state.
181///
182/// All update functions are implied to be atomic. That is, if multiple entities are meant to be
183/// updated as part of any single function and an error is returned during its execution, any
184/// changes that might have happened up to that point need to be rolled back and discarded.
185///
186/// Because the [`Store`]'s ownership is shared between the executor and the client, interior
187/// mutability is expected to be implemented, which is why all methods receive `&self` and
188/// not `&mut self`.
189#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
190#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
191pub trait Store: Send + Sync {
192 /// Returns an identifier for this store (e.g. `IndexedDB` database name, `SQLite` file path).
193 ///
194 /// This allows callers to retrieve store-specific identity information (such as the `IndexedDB`
195 /// database name) for standalone operations like `exportStore`/`importStore`, without making
196 /// import/export a responsibility of the client.
197 fn identifier(&self) -> &str;
198
199 /// Returns the current timestamp tracked by the store, measured in non-leap seconds since
200 /// Unix epoch. If the store implementation is incapable of tracking time, it should return
201 /// `None`.
202 ///
203 /// This method is used to add time metadata to notes' states. This information doesn't have a
204 /// functional impact on the client's operation, it's shown to the user for informational
205 /// purposes.
206 fn get_current_timestamp(&self) -> Option<u64>;
207
208 // TRANSACTIONS
209 // --------------------------------------------------------------------------------------------
210
211 /// Retrieves stored transactions, filtered by [`TransactionFilter`].
212 async fn get_transactions(
213 &self,
214 filter: TransactionFilter,
215 ) -> Result<Vec<TransactionRecord>, StoreError>;
216
217 /// Applies a transaction, atomically updating the current state based on the
218 /// [`TransactionStoreUpdate`].
219 ///
220 /// An update involves:
221 /// - Updating the stored account which is being modified by the transaction.
222 /// - Storing new input/output notes and payback note details as a result of the transaction
223 /// execution.
224 /// - Updating the input notes that are being processed by the transaction.
225 /// - Inserting the new tracked tags into the store.
226 /// - Inserting the transaction into the store to track.
227 async fn apply_transaction(&self, tx_update: TransactionStoreUpdate) -> Result<(), StoreError>;
228
229 /// Applies a batch of [`TransactionStoreUpdate`]s atomically. Semantically equivalent to
230 /// calling [`Store::apply_transaction`] for each update in order, but with an all-or-nothing
231 /// guarantee — on any error no update is visible.
232 ///
233 /// Used by `BatchBuilder::submit` to persist a batch's results. Backends that cannot provide
234 /// true atomicity must document that limitation explicitly in their impl — there is no blanket
235 /// default.
236 async fn apply_transaction_batch(
237 &self,
238 tx_updates: Vec<TransactionStoreUpdate>,
239 ) -> Result<(), StoreError>;
240
241 // NOTES
242 // --------------------------------------------------------------------------------------------
243
244 /// Retrieves the input notes from the store.
245 ///
246 /// When `filter` is [`NoteFilter::Consumed`], notes are sorted by their on-chain execution
247 /// order.
248 async fn get_input_notes(&self, filter: NoteFilter)
249 -> Result<Vec<InputNoteRecord>, StoreError>;
250
251 /// Retrieves the output notes from the store.
252 async fn get_output_notes(
253 &self,
254 filter: NoteFilter,
255 ) -> Result<Vec<OutputNoteRecord>, StoreError>;
256
257 /// Retrieves the input note following `cursor` in the filtered set for the given consumer
258 /// account, or the first matching note when `cursor` is `None`. Optionally restricts to a
259 /// block range via `block_start` and `block_end`. Returns `None` when no matching note
260 /// follows the cursor.
261 ///
262 /// Build the cursor for the next call from the returned record with
263 /// [`InputNoteCursor::from_record`].
264 ///
265 /// # Ordering
266 ///
267 /// Notes are sorted by their per-account on-chain execution order: block number, then
268 /// per-account transaction order within the block. Notes consumed by the same transaction
269 /// are ordered deterministically and consistently across calls.
270 async fn get_input_note_after(
271 &self,
272 filter: NoteFilter,
273 consumer: AccountId,
274 block_start: Option<BlockNumber>,
275 block_end: Option<BlockNumber>,
276 cursor: Option<InputNoteCursor>,
277 ) -> Result<Option<InputNoteRecord>, StoreError>;
278
279 /// Returns the nullifiers of all unspent input notes.
280 ///
281 /// The default implementation of this method uses [`Store::get_input_notes`].
282 async fn get_unspent_input_note_nullifiers(&self) -> Result<Vec<Nullifier>, StoreError> {
283 Ok(self
284 .get_input_notes(NoteFilter::Unspent)
285 .await?
286 .iter()
287 .filter_map(InputNoteRecord::nullifier)
288 .collect())
289 }
290
291 /// Inserts the provided input notes into the database. If a note with the same ID already
292 /// exists, it will be replaced.
293 async fn upsert_input_notes(&self, notes: &[InputNoteRecord]) -> Result<(), StoreError>;
294
295 /// Returns the note script associated with the given root.
296 async fn get_note_script(&self, script_root: Word) -> Result<NoteScript, StoreError>;
297
298 /// Inserts the provided note scripts into the database. If a script with the same root already
299 /// exists, it will be replaced.
300 async fn upsert_note_scripts(&self, note_scripts: &[NoteScript]) -> Result<(), StoreError>;
301
302 // CHAIN DATA
303 // --------------------------------------------------------------------------------------------
304
305 /// Retrieves a vector of [`BlockHeader`]s filtered by the provided block numbers.
306 ///
307 /// The returned vector may not contain some or all of the requested block headers. It's up to
308 /// the callee to check whether all requested block headers were found.
309 ///
310 /// For each block header an additional boolean value is returned representing whether the block
311 /// contains notes relevant to the client.
312 async fn get_block_headers(
313 &self,
314 block_numbers: &BTreeSet<BlockNumber>,
315 ) -> Result<Vec<(BlockHeader, BlockRelevance)>, StoreError>;
316
317 /// Retrieves a [`BlockHeader`] corresponding to the provided block number and a boolean value
318 /// that represents whether the block contains notes relevant to the client. Returns `None` if
319 /// the block is not found.
320 ///
321 /// The default implementation of this method uses [`Store::get_block_headers`].
322 async fn get_block_header_by_num(
323 &self,
324 block_number: BlockNumber,
325 ) -> Result<Option<(BlockHeader, BlockRelevance)>, StoreError> {
326 self.get_block_headers(&[block_number].into_iter().collect())
327 .await
328 .map(|mut block_headers_list| block_headers_list.pop())
329 }
330
331 /// Retrieves a list of [`BlockHeader`] that include relevant notes to the client.
332 async fn get_tracked_block_headers(&self) -> Result<Vec<BlockHeader>, StoreError>;
333
334 /// Retrieves the block numbers of block headers that include relevant notes to the client.
335 ///
336 /// This is a lightweight alternative to [`Store::get_tracked_block_headers`] that avoids
337 /// deserializing full block headers when only the block numbers are needed.
338 async fn get_tracked_block_header_numbers(&self) -> Result<BTreeSet<usize>, StoreError>;
339
340 /// Retrieves all MMR authentication nodes based on [`PartialBlockchainFilter`].
341 async fn get_partial_blockchain_nodes(
342 &self,
343 filter: PartialBlockchainFilter,
344 ) -> Result<BTreeMap<InOrderIndex, Word>, StoreError>;
345
346 /// Returns the chain MMR peaks at the current sync height (peaks at `forest = block_num`,
347 /// i.e. excluding `block_num` itself as a leaf).
348 ///
349 /// The peaks' `forest().num_leaves()` equals the current sync height by construction,
350 /// so callers can derive the synced block number from the returned peaks without a
351 /// second query.
352 ///
353 /// Before the first sync, returns an empty [`MmrPeaks`].
354 async fn get_current_blockchain_peaks(&self) -> Result<MmrPeaks, StoreError>;
355
356 /// Inserts a block header together with its MMR authentication nodes in a single
357 /// transaction, so the header and the nodes that rebuild its `PartialMmr` are committed
358 /// together.
359 ///
360 /// The header is inserted-if-not-exists with a one-way `has_client_notes` upgrade: on
361 /// conflict the stored `header` is preserved and the flag only moves from `false` to
362 /// `true`, never back. The MMR nodes are likewise inserted-if-not-exists: an
363 /// `InOrderIndex` already present is left untouched (auth paths of tracked blocks share
364 /// internal nodes, so re-inserting an existing index must be a no-op, not an error).
365 async fn insert_block_header(
366 &self,
367 block_header: &BlockHeader,
368 nodes: &[(InOrderIndex, Word)],
369 has_client_notes: bool,
370 ) -> Result<(), StoreError>;
371
372 /// Prunes irrelevant block data from the store.
373 ///
374 /// This performs three operations atomically:
375 /// 1. Deletes MMR authentication nodes at the given `node_indices`.
376 /// 2. Sets `has_client_notes = false` for `blocks_to_untrack` (blocks whose notes have all been
377 /// consumed).
378 /// 3. Deletes block headers with `has_client_notes = false` that are not the genesis or
379 /// sync-height block.
380 async fn untrack_and_prune_irrelevant_blocks(
381 &self,
382 blocks_to_untrack: &[BlockNumber],
383 node_indices_to_remove: &[InOrderIndex],
384 ) -> Result<(), StoreError>;
385
386 /// Prunes historical account states for the specified account up to the given nonce.
387 ///
388 /// Deletes all historical entries with `replaced_at_nonce <= up_to_nonce` from the
389 /// historical tables (headers, storage, storage map entries, and assets).
390 ///
391 /// Also removes orphaned `account_code` entries that are no longer referenced by any
392 /// account header.
393 ///
394 /// Returns the total number of rows deleted, including historical entries and orphaned
395 /// account code.
396 async fn prune_account_history(
397 &self,
398 account_id: AccountId,
399 up_to_nonce: Felt,
400 ) -> Result<usize, StoreError>;
401
402 // ACCOUNT
403 // --------------------------------------------------------------------------------------------
404
405 /// Returns the account IDs of all accounts stored in the database.
406 async fn get_account_ids(&self) -> Result<Vec<AccountId>, StoreError>;
407
408 /// Returns a list of [`AccountHeader`] of all accounts stored in the database along with their
409 /// statuses.
410 ///
411 /// Said accounts' state is the state after the last performed sync.
412 async fn get_account_headers(&self) -> Result<Vec<(AccountHeader, AccountStatus)>, StoreError>;
413
414 /// Retrieves an [`AccountHeader`] object for the specified [`AccountId`] along with its status.
415 /// Returns `None` if the account is not found.
416 ///
417 /// Said account's state is the state according to the last sync performed.
418 async fn get_account_header(
419 &self,
420 account_id: AccountId,
421 ) -> Result<Option<(AccountHeader, AccountStatus)>, StoreError>;
422
423 /// Returns an [`AccountHeader`] corresponding to the stored account state that matches the
424 /// given commitment. If no account state matches the provided commitment, `None` is returned.
425 async fn get_account_header_by_commitment(
426 &self,
427 account_commitment: Word,
428 ) -> Result<Option<AccountHeader>, StoreError>;
429
430 /// Retrieves a full [`AccountRecord`] object, this contains the account's latest state along
431 /// with its status. Returns `None` if the account is not found.
432 async fn get_account(&self, account_id: AccountId)
433 -> Result<Option<AccountRecord>, StoreError>;
434
435 /// Retrieves the [`AccountCode`] for the specified account.
436 /// Returns `None` if the account is not found.
437 async fn get_account_code(
438 &self,
439 account_id: AccountId,
440 ) -> Result<Option<AccountCode>, StoreError>;
441
442 /// Inserts an [`Account`] to the store, alongside its initial [`Address`].
443 ///
444 /// Tag registration is the caller's responsibility — see [`Self::add_note_tag`].
445 ///
446 /// # Errors
447 ///
448 /// - If the account is new and does not contain a seed
449 async fn insert_account(
450 &self,
451 account: &Account,
452 initial_address: Address,
453 client_account_type: ClientAccountType,
454 ) -> Result<(), StoreError>;
455
456 /// Upserts the account code for a foreign account. This value will be used as a cache of known
457 /// script roots and added to the `GetForeignAccountCode` request.
458 async fn upsert_foreign_account_code(
459 &self,
460 account_id: AccountId,
461 code: AccountCode,
462 ) -> Result<(), StoreError>;
463
464 /// Retrieves the cached account code for various foreign accounts.
465 async fn get_foreign_account_code(
466 &self,
467 account_ids: Vec<AccountId>,
468 ) -> Result<BTreeMap<AccountId, AccountCode>, StoreError>;
469
470 /// Retrieves all [`Address`] objects that correspond to the provided account ID.
471 async fn get_addresses_by_account_id(
472 &self,
473 account_id: AccountId,
474 ) -> Result<Vec<Address>, StoreError>;
475
476 /// Updates an existing [`Account`] with a new state.
477 ///
478 /// # Errors
479 ///
480 /// Returns a `StoreError::AccountDataNotFound` if there is no account for the provided ID.
481 async fn update_account(&self, new_account_state: &Account) -> Result<(), StoreError>;
482
483 /// Adds an [`Address`] to an [`Account`].
484 ///
485 /// Tag registration is the caller's responsibility — see [`Self::add_note_tag`].
486 async fn insert_address(
487 &self,
488 address: Address,
489 account_id: AccountId,
490 ) -> Result<(), StoreError>;
491
492 /// Removes an [`Address`]. Returns `true` if the address was tracked.
493 ///
494 /// Tag removal is the caller's responsibility — see [`Self::remove_note_tag`].
495 async fn remove_address(&self, address: Address) -> Result<bool, StoreError>;
496
497 // SETTINGS
498 // --------------------------------------------------------------------------------------------
499
500 /// Adds a value to `scope` in the `settings` table.
501 async fn set_setting(
502 &self,
503 scope: SettingScope,
504 key: String,
505 value: Vec<u8>,
506 ) -> Result<(), StoreError>;
507
508 /// Retrieves a value from `scope` in the `settings` table.
509 async fn get_setting(
510 &self,
511 scope: SettingScope,
512 key: String,
513 ) -> Result<Option<Vec<u8>>, StoreError>;
514
515 /// Deletes a value from `scope` in the `settings` table. Returns `true` if the key was present.
516 async fn remove_setting(&self, scope: SettingScope, key: String) -> Result<bool, StoreError>;
517
518 /// Returns the keys held by `scope` in the `settings` table.
519 async fn list_setting_keys(&self, scope: SettingScope) -> Result<Vec<String>, StoreError>;
520
521 /// Applies a batch of [`SettingMutation`]s against `scope`. Use this when several `settings`
522 /// entries must stay mutually consistent (e.g. a record and its secondary index).
523 async fn apply_settings_mutations(
524 &self,
525 scope: SettingScope,
526 mutations: Vec<SettingMutation>,
527 ) -> Result<(), StoreError>;
528
529 // SYNC
530 // --------------------------------------------------------------------------------------------
531
532 /// Returns the note tag records that the client is interested in.
533 async fn get_note_tags(&self) -> Result<Vec<NoteTagRecord>, StoreError>;
534
535 /// Returns the unique note tags (without source) that the client is interested in.
536 async fn get_unique_note_tags(&self) -> Result<BTreeSet<NoteTag>, StoreError> {
537 Ok(self.get_note_tags().await?.into_iter().map(|r| r.tag).collect())
538 }
539
540 /// Adds a note tag to the list of tags that the client is interested in.
541 ///
542 /// If the tag was already being tracked, returns false since no new tags were actually added.
543 /// Otherwise true.
544 async fn add_note_tag(&self, tag: NoteTagRecord) -> Result<bool, StoreError>;
545
546 /// Removes a note tag from the list of tags that the client is interested in.
547 ///
548 /// Returns the number of tags that were removed.
549 async fn remove_note_tag(&self, tag: NoteTagRecord) -> Result<usize, StoreError>;
550
551 /// Returns the block number of the last state sync block.
552 async fn get_sync_height(&self) -> Result<BlockNumber, StoreError>;
553
554 /// Applies the state sync update to the store. An update involves:
555 ///
556 /// - Inserting the new block header to the store alongside new MMR peaks information.
557 /// - Updating the corresponding tracked input/output notes. Consumed notes carry consumption
558 /// metadata — `consumed_block_height`, `consumed_tx_order`, and `consumer_account_id` — in
559 /// their note state. Implementations must persist these fields so that ordered queries (see
560 /// [`Store::get_input_note_after`]) work correctly.
561 /// - Removing note tags that are no longer relevant.
562 /// - Updating transactions in the store, marking as `committed` or `discarded`.
563 /// - In turn, validating private account's state transitions. If a private account's
564 /// commitment locally does not match the `StateSyncUpdate` information, the account may be
565 /// locked.
566 /// - Storing new MMR authentication nodes.
567 /// - Updating the tracked public accounts.
568 async fn apply_state_sync(&self, state_sync_update: StateSyncUpdate) -> Result<(), StoreError>;
569
570 // TRANSPORT
571 // --------------------------------------------------------------------------------------------
572
573 /// Gets the note transport cursor.
574 ///
575 /// This is used to reduce the number of fetched notes from the note transport network.
576 /// If no cursor exists, initializes it to 0.
577 async fn get_note_transport_cursor(&self) -> Result<NoteTransportCursor, StoreError> {
578 let cursor_bytes = if let Some(bytes) = self
579 .get_setting(SettingScope::Client, NOTE_TRANSPORT_CURSOR_STORE_SETTING.into())
580 .await?
581 {
582 bytes
583 } else {
584 // Lazy initialization: create cursor if not present
585 let initial = 0u64.to_be_bytes().to_vec();
586 self.set_setting(
587 SettingScope::Client,
588 NOTE_TRANSPORT_CURSOR_STORE_SETTING.into(),
589 initial.clone(),
590 )
591 .await?;
592 initial
593 };
594 let array: [u8; 8] = cursor_bytes
595 .as_slice()
596 .try_into()
597 .map_err(|e: core::array::TryFromSliceError| StoreError::ParsingError(e.to_string()))?;
598 let cursor = u64::from_be_bytes(array);
599 Ok(cursor.into())
600 }
601
602 /// Updates the note transport cursor.
603 ///
604 /// This is used to track the last cursor position when fetching notes from the note transport
605 /// network.
606 async fn update_note_transport_cursor(
607 &self,
608 cursor: NoteTransportCursor,
609 ) -> Result<(), StoreError> {
610 let cursor_bytes = cursor.value().to_be_bytes().to_vec();
611 self.set_setting(
612 SettingScope::Client,
613 NOTE_TRANSPORT_CURSOR_STORE_SETTING.into(),
614 cursor_bytes,
615 )
616 .await?;
617 Ok(())
618 }
619
620 // RPC LIMITS
621 // --------------------------------------------------------------------------------------------
622
623 /// Gets persisted RPC limits. Returns `None` if not stored.
624 async fn get_rpc_limits(&self) -> Result<Option<RpcLimits>, StoreError> {
625 let Some(bytes) =
626 self.get_setting(SettingScope::Client, RPC_LIMITS_STORE_SETTING.into()).await?
627 else {
628 return Ok(None);
629 };
630 let limits = RpcLimits::read_from_bytes(&bytes)?;
631 Ok(Some(limits))
632 }
633
634 /// Persists RPC limits to the store.
635 async fn set_rpc_limits(&self, limits: RpcLimits) -> Result<(), StoreError> {
636 self.set_setting(SettingScope::Client, RPC_LIMITS_STORE_SETTING.into(), limits.to_bytes())
637 .await
638 }
639
640 // TRANSACTION ENCRYPTION KEY
641 // --------------------------------------------------------------------------------------------
642
643 /// Gets the cached transaction encryption key. Returns `None` if not stored.
644 ///
645 /// The key is public data shared by the whole validator set, so it is cached rather than
646 /// treated as a secret.
647 async fn get_transaction_encryption_key(
648 &self,
649 ) -> Result<Option<TransactionEncryptionKey>, StoreError> {
650 let Some(bytes) = self
651 .get_setting(SettingScope::Client, TRANSACTION_ENCRYPTION_KEY_STORE_SETTING.into())
652 .await?
653 else {
654 return Ok(None);
655 };
656 let key = TransactionEncryptionKey::read_from_bytes(&bytes)?;
657 Ok(Some(key))
658 }
659
660 /// Caches the transaction encryption key, replacing any previously cached key.
661 async fn set_transaction_encryption_key(
662 &self,
663 key: &TransactionEncryptionKey,
664 ) -> Result<(), StoreError> {
665 self.set_setting(
666 SettingScope::Client,
667 TRANSACTION_ENCRYPTION_KEY_STORE_SETTING.into(),
668 key.to_bytes(),
669 )
670 .await
671 }
672
673 /// Removes the cached transaction encryption key, so the next submission fetches and verifies
674 /// a fresh one. Used when the node rejects a submission sealed against a retired key.
675 async fn remove_transaction_encryption_key(&self) -> Result<(), StoreError> {
676 self.remove_setting(SettingScope::Client, TRANSACTION_ENCRYPTION_KEY_STORE_SETTING.into())
677 .await?;
678 Ok(())
679 }
680
681 // PARTIAL MMR
682 // --------------------------------------------------------------------------------------------
683
684 /// Builds the current view of the chain's [`PartialMmr`]. Because we want to add all new
685 /// authentication nodes that could come from applying the MMR updates, we need to track all
686 /// known leaves thus far.
687 ///
688 /// The default implementation is based on [`Store::get_partial_blockchain_nodes`],
689 /// [`Store::get_current_blockchain_peaks`] and [`Store::get_block_header_by_num`]
690 async fn get_current_partial_mmr(&self) -> Result<PartialMmr, StoreError> {
691 let current_peaks = self.get_current_blockchain_peaks().await?;
692 let current_block_num = u32::try_from(current_peaks.num_leaves())
693 .map_err(|err| StoreError::ParsingError(err.to_string()))?
694 .into();
695
696 let (current_block, has_client_notes) = self
697 .get_block_header_by_num(current_block_num)
698 .await?
699 .ok_or(StoreError::BlockHeaderNotFound(current_block_num))?;
700
701 let mut current_partial_mmr = PartialMmr::from_peaks(current_peaks);
702 let has_client_notes = has_client_notes.into();
703 current_partial_mmr
704 .add(current_block.commitment(), has_client_notes)
705 .map_err(StoreError::MmrError)?;
706
707 // Build tracked_leaves from blocks that have client notes.
708 let mut tracked_leaves = self.get_tracked_block_header_numbers().await?;
709
710 // Also track the latest leaf if it is relevant (it has client notes) _and_ the forest
711 // actually has a single leaf tree bit.
712 if has_client_notes && current_partial_mmr.forest().has_single_leaf_tree() {
713 let latest_leaf = current_partial_mmr.forest().num_leaves().saturating_sub(1);
714 tracked_leaves.insert(latest_leaf);
715 }
716
717 let tracked_nodes = self
718 .get_partial_blockchain_nodes(PartialBlockchainFilter::Forest(
719 current_partial_mmr.forest(),
720 ))
721 .await?;
722
723 let current_partial_mmr =
724 PartialMmr::from_parts(current_partial_mmr.peaks(), tracked_nodes, tracked_leaves)?;
725
726 Ok(current_partial_mmr)
727 }
728
729 // ACCOUNT VAULT AND STORE
730 // --------------------------------------------------------------------------------------------
731
732 /// Retrieves the asset vault for a specific account.
733 async fn get_account_vault(&self, account_id: AccountId) -> Result<AssetVault, StoreError>;
734
735 /// Retrieves all assets in the account's vault as a plain list, without building the vault's
736 /// Merkle tree.
737 ///
738 /// Prefer this over [`Store::get_account_vault`] when only asset values are needed (e.g.
739 /// balance checks): it avoids hashing every asset into an SMT.
740 ///
741 /// The default implementation of this method uses [`Store::get_account_vault`].
742 async fn get_account_assets(&self, account_id: AccountId) -> Result<Vec<Asset>, StoreError> {
743 Ok(self.get_account_vault(account_id).await?.assets().collect())
744 }
745
746 /// Returns vault asset witnesses for `asset_ids` against the account's vault with root
747 /// `vault_root`. An asset absent from the vault yields an emptiness proof rather than an
748 /// error, which the executor needs when an asset is being added to the vault.
749 ///
750 /// The default implementation reconstructs the vault via [`Store::get_account_vault`] and
751 /// opens each witness from it; backends that keep an in-memory Merkle forest (e.g.
752 /// `SqliteStore`) override it to open the witnesses directly, without materializing the
753 /// vault.
754 async fn get_vault_asset_witnesses(
755 &self,
756 account_id: AccountId,
757 vault_root: Word,
758 asset_ids: BTreeSet<AssetId>,
759 ) -> Result<Vec<AssetWitness>, StoreError> {
760 let vault = self.get_account_vault(account_id).await?;
761 if vault.root() != vault_root {
762 return Err(StoreError::MerkleStoreError(MerkleError::ConflictingRoots {
763 expected_root: vault_root,
764 actual_root: vault.root(),
765 }));
766 }
767 Ok(asset_ids.into_iter().map(|asset_id| vault.open(asset_id)).collect())
768 }
769
770 /// Retrieves a specific asset (by vault id) from the account's vault along with its Merkle
771 /// witness.
772 ///
773 /// The default implementation of this method uses [`Store::get_account_vault`].
774 async fn get_account_asset(
775 &self,
776 account_id: AccountId,
777 asset_id: AssetId,
778 ) -> Result<Option<(Asset, AssetWitness)>, StoreError> {
779 let vault = self.get_account_vault(account_id).await?;
780 let Some(asset) = vault.assets().find(|a| a.id() == asset_id) else {
781 return Ok(None);
782 };
783
784 let witness = vault.open(asset_id);
785
786 Ok(Some((asset, witness)))
787 }
788
789 /// Retrieves the storage for a specific account.
790 ///
791 /// Can take an optional map root to retrieve only part of the storage,
792 /// If it does, it will either return an account storage with a single
793 /// slot (the one requested), or an error if not found.
794 async fn get_account_storage(
795 &self,
796 account_id: AccountId,
797 filter: AccountStorageFilter,
798 ) -> Result<AccountStorage, StoreError>;
799
800 /// Retrieves a storage slot value by name.
801 ///
802 /// For `Value` slots, returns the stored word.
803 /// For `Map` slots, returns the map root.
804 ///
805 /// The default implementation of this method uses [`Store::get_account_storage`].
806 async fn get_account_storage_item(
807 &self,
808 account_id: AccountId,
809 slot_name: StorageSlotName,
810 ) -> Result<Word, StoreError> {
811 let storage = self
812 .get_account_storage(account_id, AccountStorageFilter::SlotName(slot_name.clone()))
813 .await?;
814 storage
815 .get(&slot_name)
816 .map(StorageSlot::value)
817 .ok_or(StoreError::AccountError(AccountError::StorageSlotNameNotFound { slot_name }))
818 }
819
820 /// Retrieves a specific item from the account's storage map along with its Merkle proof.
821 ///
822 /// The default implementation of this method uses [`Store::get_account_storage`].
823 async fn get_account_map_item(
824 &self,
825 account_id: AccountId,
826 slot_name: StorageSlotName,
827 key: StorageMapKey,
828 ) -> Result<(Word, StorageMapWitness), StoreError> {
829 let storage = self
830 .get_account_storage(account_id, AccountStorageFilter::SlotName(slot_name.clone()))
831 .await?;
832 match storage.get(&slot_name).map(StorageSlot::content) {
833 Some(StorageSlotContent::Map(map)) => {
834 let value = map.get(&key);
835 let witness = map.open(&key);
836
837 Ok((value, witness))
838 },
839 Some(_) => Err(StoreError::AccountError(AccountError::StorageSlotNotMap(slot_name))),
840 None => {
841 Err(StoreError::AccountError(AccountError::StorageSlotNameNotFound { slot_name }))
842 },
843 }
844 }
845
846 // IN-BATCH (STAGED) WITNESSES
847 // --------------------------------------------------------------------------------------------
848
849 // PARTIAL ACCOUNTS
850 // --------------------------------------------------------------------------------------------
851
852 /// Retrieves an [`AccountRecord`] object, this contains the account's latest partial
853 /// state along with its status. Returns `None` if the partial account is not found.
854 async fn get_minimal_partial_account(
855 &self,
856 account_id: AccountId,
857 ) -> Result<Option<AccountRecord>, StoreError>;
858}
859
860// PARTIAL BLOCKCHAIN NODE FILTER
861// ================================================================================================
862
863/// Filters for searching specific MMR nodes.
864// TODO: Should there be filters for specific blocks instead of nodes?
865pub enum PartialBlockchainFilter {
866 /// Return all nodes.
867 All,
868 /// Filter by the specified in-order indices.
869 List(Vec<InOrderIndex>),
870 /// Return nodes with in-order indices within the specified forest.
871 Forest(Forest),
872}
873
874// TRANSACTION FILTERS
875// ================================================================================================
876
877/// Filters for narrowing the set of transactions returned by the client's store.
878#[derive(Debug, Clone)]
879pub enum TransactionFilter {
880 /// Return all transactions.
881 All,
882 /// Filter by transactions that haven't yet been committed to the blockchain as per the last
883 /// sync.
884 Uncommitted,
885 /// Return a list of the transaction that matches the provided [`TransactionId`]s.
886 Ids(Vec<TransactionId>),
887}
888
889// TRANSACTIONS FILTER HELPERS
890// ================================================================================================
891
892impl TransactionFilter {
893 /// Returns a [String] containing the query for this Filter.
894 pub fn to_query(&self) -> String {
895 const QUERY: &str = "SELECT tx.id, script.script, tx.details, tx.status \
896 FROM transactions AS tx LEFT JOIN transaction_scripts AS script ON tx.script_root = script.script_root";
897 match self {
898 TransactionFilter::All => QUERY.to_string(),
899 TransactionFilter::Uncommitted => format!(
900 "{QUERY} WHERE tx.status_variant = {}",
901 TransactionStatusVariant::Pending as u8,
902 ),
903 TransactionFilter::Ids(_) => {
904 // Use SQLite's array parameter binding
905 format!("{QUERY} WHERE tx.id IN rarray(?)")
906 },
907 }
908 }
909}
910
911// NOTE FILTER
912// ================================================================================================
913
914/// Filters for narrowing the set of notes returned by the client's store.
915#[derive(Debug, Clone)]
916pub enum NoteFilter {
917 /// Return a list of all notes ([`InputNoteRecord`] or [`OutputNoteRecord`]).
918 All,
919 /// Return a list of committed notes ([`InputNoteRecord`] or [`OutputNoteRecord`]). These
920 /// represent notes that the blockchain has included in a block.
921 Committed,
922 /// Filter by consumed notes ([`InputNoteRecord`] or [`OutputNoteRecord`]). notes that have
923 /// been used as inputs in transactions.
924 Consumed,
925 /// Return a list of expected notes ([`InputNoteRecord`] or [`OutputNoteRecord`]). These
926 /// represent notes for which the store doesn't have anchor data.
927 Expected,
928 /// Return a list containing any notes that match with the provided [`NoteId`] vector.
929 List(Vec<NoteId>),
930 /// Return a list containing any notes whose details commitment matches one of the provided
931 /// [`NoteDetailsCommitment`] vector. Unlike [`NoteFilter::List`], this matches the
932 /// metadata-independent details commitment, so it also resolves metadata-less notes (which
933 /// have a NULL `note_id`).
934 DetailsCommitments(Vec<NoteDetailsCommitment>),
935 /// Return a list containing any notes that match the provided [`Nullifier`] vector.
936 Nullifiers(Vec<Nullifier>),
937 /// Return a list of notes that are currently being processed. This filter doesn't apply to
938 /// output notes.
939 Processing,
940 /// Return a list containing any notes whose script root matches one of the provided
941 /// [`NoteScriptRoot`]s. Notes whose script isn't known (e.g. partial output notes) never
942 /// match.
943 ScriptRoots(Vec<NoteScriptRoot>),
944 /// Return a list containing the note that matches with the provided [`NoteId`]. The query will
945 /// return an error if the note isn't found.
946 Unique(NoteId),
947 /// Return a list containing notes that haven't been nullified yet, this includes expected,
948 /// committed, processing and unverified notes.
949 Unspent,
950 /// Return a list containing notes with unverified inclusion proofs. This filter doesn't apply
951 /// to output notes.
952 Unverified,
953}
954
955// BLOCK RELEVANCE
956// ================================================================================================
957
958/// Expresses metadata about the block header.
959#[derive(Debug, Clone)]
960pub enum BlockRelevance {
961 /// The block header includes notes that the client may consume.
962 HasNotes,
963 /// The block header does not contain notes relevant to the client.
964 Irrelevant,
965}
966
967impl From<BlockRelevance> for bool {
968 fn from(val: BlockRelevance) -> Self {
969 match val {
970 BlockRelevance::HasNotes => true,
971 BlockRelevance::Irrelevant => false,
972 }
973 }
974}
975
976impl From<bool> for BlockRelevance {
977 fn from(has_notes: bool) -> Self {
978 if has_notes {
979 BlockRelevance::HasNotes
980 } else {
981 BlockRelevance::Irrelevant
982 }
983 }
984}
985
986// STORAGE FILTER
987// ================================================================================================
988
989/// Filters for narrowing the storage slots returned by the client's store.
990#[derive(Debug, Clone)]
991pub enum AccountStorageFilter {
992 /// Return an [`AccountStorage`] with all available slots.
993 All,
994 /// Return an [`AccountStorage`] with a single slot that matches the provided [`Word`] map root.
995 Root(Word),
996 /// Return an [`AccountStorage`] with a single slot that matches the provided slot name.
997 SlotName(StorageSlotName),
998 /// Return an [`AccountStorage`] containing only the slots whose names are in the provided
999 /// list. Useful to avoid loading the full storage when only a known subset of slots is needed
1000 /// (e.g. when applying a delta to a large account).
1001 SlotNames(Vec<StorageSlotName>),
1002}