Skip to main content

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