miden_client/store/
mod.rs

1//! Defines the storage interfaces used by the Miden client.
2//!
3//! It provides mechanisms for persisting and retrieving data, such as account states, transaction
4//! history, block headers, notes, and MMR nodes.
5//!
6//! ## Overview
7//!
8//! The storage module is central to the Miden client’s persistence layer. It defines the
9//! [`Store`] trait which abstracts over any concrete storage implementation. The trait exposes
10//! methods to (among others):
11//!
12//! - Retrieve and update transactions, notes, and accounts.
13//! - Store and query block headers along with MMR peaks and authentication nodes.
14//! - Manage note tags for synchronizing with the node.
15//!
16//! These are all used by the Miden client to provide transaction execution in the correct contexts.
17//!
18//! In addition to the main [`Store`] trait, the module provides types for filtering queries, such
19//! as [`TransactionFilter`] and [`NoteFilter`], to narrow down the set of returned transactions or
20//! notes. For more advanced usage, see the documentation of individual methods in the [`Store`]
21//! trait.
22
23use alloc::boxed::Box;
24use alloc::collections::{BTreeMap, BTreeSet};
25use alloc::string::{String, ToString};
26use alloc::vec::Vec;
27use core::fmt::Debug;
28
29use miden_objects::account::{
30    Account,
31    AccountCode,
32    AccountHeader,
33    AccountId,
34    AccountIdPrefix,
35    AccountStorage,
36    StorageMapWitness,
37    StorageSlot,
38};
39use miden_objects::address::Address;
40use miden_objects::asset::{Asset, AssetVault, AssetWitness};
41use miden_objects::block::{BlockHeader, BlockNumber};
42use miden_objects::crypto::merkle::{InOrderIndex, MmrPeaks, PartialMmr};
43use miden_objects::note::{NoteId, NoteScript, NoteTag, Nullifier};
44use miden_objects::transaction::TransactionId;
45use miden_objects::{AccountError, Word};
46
47use crate::note_transport::{NOTE_TRANSPORT_CURSOR_STORE_SETTING, NoteTransportCursor};
48use crate::sync::{NoteTagRecord, StateSyncUpdate};
49use crate::transaction::{TransactionRecord, TransactionStatusVariant, TransactionStoreUpdate};
50
51/// Contains [`ClientDataStore`] to automatically implement [`DataStore`] for anything that
52/// implements [`Store`]. This isn't public because it's an implementation detail to instantiate the
53/// executor.
54///
55/// The user is tasked with creating a [`Store`] which the client will wrap into a
56/// [`ClientDataStore`] at creation time.
57pub(crate) mod data_store;
58
59mod errors;
60pub use errors::*;
61
62mod account;
63pub use account::{AccountRecord, AccountStatus, AccountUpdates};
64mod note_record;
65pub use note_record::{
66    InputNoteRecord,
67    InputNoteState,
68    NoteExportType,
69    NoteRecordError,
70    OutputNoteRecord,
71    OutputNoteState,
72    input_note_states,
73};
74
75// STORE TRAIT
76// ================================================================================================
77
78/// The [`Store`] trait exposes all methods that the client store needs in order to track the
79/// current state.
80///
81/// All update functions are implied to be atomic. That is, if multiple entities are meant to be
82/// updated as part of any single function and an error is returned during its execution, any
83/// changes that might have happened up to that point need to be rolled back and discarded.
84///
85/// Because the [`Store`]'s ownership is shared between the executor and the client, interior
86/// mutability is expected to be implemented, which is why all methods receive `&self` and
87/// not `&mut self`.
88#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
89#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
90pub trait Store: Send + Sync {
91    /// Returns the current timestamp tracked by the store, measured in non-leap seconds since
92    /// Unix epoch. If the store implementation is incapable of tracking time, it should return
93    /// `None`.
94    ///
95    /// This method is used to add time metadata to notes' states. This information doesn't have a
96    /// functional impact on the client's operation, it's shown to the user for informational
97    /// purposes.
98    fn get_current_timestamp(&self) -> Option<u64>;
99
100    // TRANSACTIONS
101    // --------------------------------------------------------------------------------------------
102
103    /// Retrieves stored transactions, filtered by [`TransactionFilter`].
104    async fn get_transactions(
105        &self,
106        filter: TransactionFilter,
107    ) -> Result<Vec<TransactionRecord>, StoreError>;
108
109    /// Applies a transaction, atomically updating the current state based on the
110    /// [`TransactionStoreUpdate`].
111    ///
112    /// An update involves:
113    /// - Updating the stored account which is being modified by the transaction.
114    /// - Storing new input/output notes and payback note details as a result of the transaction
115    ///   execution.
116    /// - Updating the input notes that are being processed by the transaction.
117    /// - Inserting the new tracked tags into the store.
118    /// - Inserting the transaction into the store to track.
119    async fn apply_transaction(&self, tx_update: TransactionStoreUpdate) -> Result<(), StoreError>;
120
121    // NOTES
122    // --------------------------------------------------------------------------------------------
123
124    /// Retrieves the input notes from the store.
125    async fn get_input_notes(&self, filter: NoteFilter)
126    -> Result<Vec<InputNoteRecord>, StoreError>;
127
128    /// Retrieves the output notes from the store.
129    async fn get_output_notes(
130        &self,
131        filter: NoteFilter,
132    ) -> Result<Vec<OutputNoteRecord>, StoreError>;
133
134    /// Returns the nullifiers of all unspent input notes.
135    ///
136    /// The default implementation of this method uses [`Store::get_input_notes`].
137    async fn get_unspent_input_note_nullifiers(&self) -> Result<Vec<Nullifier>, StoreError> {
138        self.get_input_notes(NoteFilter::Unspent)
139            .await?
140            .iter()
141            .map(|input_note| Ok(input_note.nullifier()))
142            .collect::<Result<Vec<_>, _>>()
143    }
144
145    /// Inserts the provided input notes into the database. If a note with the same ID already
146    /// exists, it will be replaced.
147    async fn upsert_input_notes(&self, notes: &[InputNoteRecord]) -> Result<(), StoreError>;
148
149    /// Returns the note script associated with the given root.
150    async fn get_note_script(&self, script_root: Word) -> Result<NoteScript, StoreError>;
151
152    /// Inserts the provided note scripts into the database. If a script with the same root already
153    /// exists, it will be replaced.
154    async fn upsert_note_scripts(&self, note_scripts: &[NoteScript]) -> Result<(), StoreError>;
155
156    // CHAIN DATA
157    // --------------------------------------------------------------------------------------------
158
159    /// Retrieves a vector of [`BlockHeader`]s filtered by the provided block numbers.
160    ///
161    /// The returned vector may not contain some or all of the requested block headers. It's up to
162    /// the callee to check whether all requested block headers were found.
163    ///
164    /// For each block header an additional boolean value is returned representing whether the block
165    /// contains notes relevant to the client.
166    async fn get_block_headers(
167        &self,
168        block_numbers: &BTreeSet<BlockNumber>,
169    ) -> Result<Vec<(BlockHeader, BlockRelevance)>, StoreError>;
170
171    /// Retrieves a [`BlockHeader`] corresponding to the provided block number and a boolean value
172    /// that represents whether the block contains notes relevant to the client. Returns `None` if
173    /// the block is not found.
174    ///
175    /// The default implementation of this method uses [`Store::get_block_headers`].
176    async fn get_block_header_by_num(
177        &self,
178        block_number: BlockNumber,
179    ) -> Result<Option<(BlockHeader, BlockRelevance)>, StoreError> {
180        self.get_block_headers(&[block_number].into_iter().collect())
181            .await
182            .map(|mut block_headers_list| block_headers_list.pop())
183    }
184
185    /// Retrieves a list of [`BlockHeader`] that include relevant notes to the client.
186    async fn get_tracked_block_headers(&self) -> Result<Vec<BlockHeader>, StoreError>;
187
188    /// Retrieves all MMR authentication nodes based on [`PartialBlockchainFilter`].
189    async fn get_partial_blockchain_nodes(
190        &self,
191        filter: PartialBlockchainFilter,
192    ) -> Result<BTreeMap<InOrderIndex, Word>, StoreError>;
193
194    /// Inserts blockchain MMR authentication nodes.
195    ///
196    /// In the case where the [`InOrderIndex`] already exists on the table, the insertion is
197    /// ignored.
198    async fn insert_partial_blockchain_nodes(
199        &self,
200        nodes: &[(InOrderIndex, Word)],
201    ) -> Result<(), StoreError>;
202
203    /// Returns peaks information from the blockchain by a specific block number.
204    ///
205    /// If there is no partial blockchain info stored for the provided block returns an empty
206    /// [`MmrPeaks`].
207    async fn get_partial_blockchain_peaks_by_block_num(
208        &self,
209        block_num: BlockNumber,
210    ) -> Result<MmrPeaks, StoreError>;
211
212    /// Inserts a block header into the store, alongside peaks information at the block's height.
213    ///
214    /// `has_client_notes` describes whether the block has relevant notes to the client; this means
215    /// the client might want to authenticate merkle paths based on this value.
216    /// If the block header exists and `has_client_notes` is `true` then the `has_client_notes`
217    /// column is updated to `true` to signify that the block now contains a relevant note.
218    async fn insert_block_header(
219        &self,
220        block_header: &BlockHeader,
221        partial_blockchain_peaks: MmrPeaks,
222        has_client_notes: bool,
223    ) -> Result<(), StoreError>;
224
225    /// Removes block headers that do not contain any client notes and aren't the genesis or last
226    /// block.
227    async fn prune_irrelevant_blocks(&self) -> Result<(), StoreError>;
228
229    // ACCOUNT
230    // --------------------------------------------------------------------------------------------
231
232    /// Returns the account IDs of all accounts stored in the database.
233    async fn get_account_ids(&self) -> Result<Vec<AccountId>, StoreError>;
234
235    /// Returns a list of [`AccountHeader`] of all accounts stored in the database along with their
236    /// statuses.
237    ///
238    /// Said accounts' state is the state after the last performed sync.
239    async fn get_account_headers(&self) -> Result<Vec<(AccountHeader, AccountStatus)>, StoreError>;
240
241    /// Retrieves an [`AccountHeader`] object for the specified [`AccountId`] along with its status.
242    /// Returns `None` if the account is not found.
243    ///
244    /// Said account's state is the state according to the last sync performed.
245    async fn get_account_header(
246        &self,
247        account_id: AccountId,
248    ) -> Result<Option<(AccountHeader, AccountStatus)>, StoreError>;
249
250    /// Returns an [`AccountHeader`] corresponding to the stored account state that matches the
251    /// given commitment. If no account state matches the provided commitment, `None` is returned.
252    async fn get_account_header_by_commitment(
253        &self,
254        account_commitment: Word,
255    ) -> Result<Option<AccountHeader>, StoreError>;
256
257    /// Retrieves a full [`AccountRecord`] object, this contains the account's latest state along
258    /// with its status. Returns `None` if the account is not found.
259    async fn get_account(&self, account_id: AccountId)
260    -> Result<Option<AccountRecord>, StoreError>;
261
262    /// Inserts an [`Account`] to the store.
263    /// Receives an [`Address`] as the initial address to associate with the account. This address
264    /// will be tracked for incoming notes and its derived note tag will be monitored.
265    ///
266    /// # Errors
267    ///
268    /// - If the account is new and does not contain a seed
269    async fn insert_account(
270        &self,
271        account: &Account,
272        initial_address: Address,
273    ) -> Result<(), StoreError>;
274
275    /// Upserts the account code for a foreign account. This value will be used as a cache of known
276    /// script roots and added to the `GetForeignAccountCode` request.
277    async fn upsert_foreign_account_code(
278        &self,
279        account_id: AccountId,
280        code: AccountCode,
281    ) -> Result<(), StoreError>;
282
283    /// Retrieves the cached account code for various foreign accounts.
284    async fn get_foreign_account_code(
285        &self,
286        account_ids: Vec<AccountId>,
287    ) -> Result<BTreeMap<AccountId, AccountCode>, StoreError>;
288
289    /// Retrieves all [`Address`] objects that correspond to the provided account ID.
290    async fn get_addresses_by_account_id(
291        &self,
292        account_id: AccountId,
293    ) -> Result<Vec<Address>, StoreError>;
294
295    /// Updates an existing [`Account`] with a new state.
296    ///
297    /// # Errors
298    ///
299    /// Returns a `StoreError::AccountDataNotFound` if there is no account for the provided ID.
300    async fn update_account(&self, new_account_state: &Account) -> Result<(), StoreError>;
301
302    /// Adds an [`Address`] to an [`Account`], alongside its derived note tag.
303    async fn insert_address(
304        &self,
305        address: Address,
306        account_id: AccountId,
307    ) -> Result<(), StoreError>;
308
309    /// Removes an [`Address`] from an [`Account`], alongside its derived note tag.
310    async fn remove_address(
311        &self,
312        address: Address,
313        account_id: AccountId,
314    ) -> Result<(), StoreError>;
315
316    // SETTINGS
317    // --------------------------------------------------------------------------------------------
318
319    /// Adds a value to the `settings` table.
320    async fn set_setting(&self, key: String, value: Vec<u8>) -> Result<(), StoreError>;
321
322    /// Retrieves a value from the `settings` table.
323    async fn get_setting(&self, key: String) -> Result<Option<Vec<u8>>, StoreError>;
324
325    /// Deletes a value from the `settings` table.
326    async fn remove_setting(&self, key: String) -> Result<(), StoreError>;
327
328    /// Returns all the keys from the `settings` table.
329    async fn list_setting_keys(&self) -> Result<Vec<String>, StoreError>;
330
331    // SYNC
332    // --------------------------------------------------------------------------------------------
333
334    /// Returns the note tag records that the client is interested in.
335    async fn get_note_tags(&self) -> Result<Vec<NoteTagRecord>, StoreError>;
336
337    /// Returns the unique note tags (without source) that the client is interested in.
338    async fn get_unique_note_tags(&self) -> Result<BTreeSet<NoteTag>, StoreError> {
339        Ok(self.get_note_tags().await?.into_iter().map(|r| r.tag).collect())
340    }
341
342    /// Adds a note tag to the list of tags that the client is interested in.
343    ///
344    /// If the tag was already being tracked, returns false since no new tags were actually added.
345    /// Otherwise true.
346    async fn add_note_tag(&self, tag: NoteTagRecord) -> Result<bool, StoreError>;
347
348    /// Removes a note tag from the list of tags that the client is interested in.
349    ///
350    /// If the tag wasn't present in the store returns false since no tag was actually removed.
351    /// Otherwise returns true.
352    async fn remove_note_tag(&self, tag: NoteTagRecord) -> Result<usize, StoreError>;
353
354    /// Returns the block number of the last state sync block.
355    async fn get_sync_height(&self) -> Result<BlockNumber, StoreError>;
356
357    /// Applies the state sync update to the store. An update involves:
358    ///
359    /// - Inserting the new block header to the store alongside new MMR peaks information.
360    /// - Updating the corresponding tracked input/output notes.
361    /// - Removing note tags that are no longer relevant.
362    /// - Updating transactions in the store, marking as `committed` or `discarded`.
363    ///   - In turn, validating private account's state transitions. If a private account's
364    ///     commitment locally does not match the `StateSyncUpdate` information, the account may be
365    ///     locked.
366    /// - Storing new MMR authentication nodes.
367    /// - Updating the tracked public accounts.
368    async fn apply_state_sync(&self, state_sync_update: StateSyncUpdate) -> Result<(), StoreError>;
369
370    // TRANSPORT
371    // --------------------------------------------------------------------------------------------
372
373    /// Gets the note transport cursor.
374    ///
375    /// This is used to reduce the number of fetched notes from the note transport network.
376    async fn get_note_transport_cursor(&self) -> Result<NoteTransportCursor, StoreError> {
377        let cursor_bytes = self
378            .get_setting(NOTE_TRANSPORT_CURSOR_STORE_SETTING.into())
379            .await?
380            .ok_or(StoreError::NoteTransportCursorNotFound)?;
381        let array: [u8; 8] = cursor_bytes
382            .as_slice()
383            .try_into()
384            .map_err(|e: core::array::TryFromSliceError| StoreError::ParsingError(e.to_string()))?;
385        let cursor = u64::from_be_bytes(array);
386        Ok(cursor.into())
387    }
388
389    /// Updates the note transport cursor.
390    ///
391    /// This is used to track the last cursor position when fetching notes from the note transport
392    /// network.
393    async fn update_note_transport_cursor(
394        &self,
395        cursor: NoteTransportCursor,
396    ) -> Result<(), StoreError> {
397        let cursor_bytes = cursor.value().to_be_bytes().to_vec();
398        self.set_setting(NOTE_TRANSPORT_CURSOR_STORE_SETTING.into(), cursor_bytes)
399            .await?;
400        Ok(())
401    }
402
403    // PARTIAL MMR
404    // --------------------------------------------------------------------------------------------
405
406    /// Builds the current view of the chain's [`PartialMmr`]. Because we want to add all new
407    /// authentication nodes that could come from applying the MMR updates, we need to track all
408    /// known leaves thus far.
409    ///
410    /// The default implementation is based on [`Store::get_partial_blockchain_nodes`],
411    /// [`Store::get_partial_blockchain_peaks_by_block_num`] and [`Store::get_block_header_by_num`]
412    async fn get_current_partial_mmr(&self) -> Result<PartialMmr, StoreError> {
413        let current_block_num = self.get_sync_height().await?;
414
415        let tracked_nodes = self.get_partial_blockchain_nodes(PartialBlockchainFilter::All).await?;
416        let current_peaks =
417            self.get_partial_blockchain_peaks_by_block_num(current_block_num).await?;
418
419        // FIXME: Because each block stores the peaks for the MMR for the leaf of pos `block_num-1`,
420        // we can get an MMR based on those peaks, add the current block number and align it with
421        // the set of all nodes in the store.
422        // Otherwise, by doing `PartialMmr::from_parts` we would effectively have more nodes than
423        // we need for the passed peaks. The alternative here is to truncate the set of all nodes
424        // before calling `from_parts`
425        //
426        // This is a bit hacky but it works. One alternative would be to _just_ get nodes required
427        // for tracked blocks in the MMR. This would however block us from the convenience of
428        // just getting all nodes from the store.
429
430        let (current_block, has_client_notes) = self
431            .get_block_header_by_num(current_block_num)
432            .await?
433            .expect("Current block should be in the store");
434
435        let mut current_partial_mmr = PartialMmr::from_peaks(current_peaks);
436        let has_client_notes = has_client_notes.into();
437        current_partial_mmr.add(current_block.commitment(), has_client_notes);
438
439        let current_partial_mmr =
440            PartialMmr::from_parts(current_partial_mmr.peaks(), tracked_nodes, has_client_notes);
441
442        Ok(current_partial_mmr)
443    }
444
445    // ACCOUNT VAULT AND STORE
446    // --------------------------------------------------------------------------------------------
447
448    /// Retrieves the asset vault for a specific account.
449    async fn get_account_vault(&self, account_id: AccountId) -> Result<AssetVault, StoreError>;
450
451    /// Retrieves a specific asset from the account's vault along with its Merkle witness.
452    ///
453    /// The default implementation of this method uses [`Store::get_account_vault`].
454    async fn get_account_asset(
455        &self,
456        account_id: AccountId,
457        faucet_id_prefix: AccountIdPrefix,
458    ) -> Result<Option<(Asset, AssetWitness)>, StoreError> {
459        let vault = self.get_account_vault(account_id).await?;
460        let Some(asset) = vault.assets().find(|a| a.faucet_id_prefix() == faucet_id_prefix) else {
461            return Ok(None);
462        };
463
464        let witness = AssetWitness::new(vault.open(asset.vault_key()).into())?;
465
466        Ok(Some((asset, witness)))
467    }
468
469    /// Retrieves the storage for a specific account.
470    async fn get_account_storage(
471        &self,
472        account_id: AccountId,
473    ) -> Result<AccountStorage, StoreError>;
474
475    /// Retrieves a specific item from the account's storage map along with its Merkle proof.
476    ///
477    /// The default implementation of this method uses [`Store::get_account_storage`].
478    async fn get_account_map_item(
479        &self,
480        account_id: AccountId,
481        index: u8,
482        key: Word,
483    ) -> Result<(Word, StorageMapWitness), StoreError> {
484        let storage = self.get_account_storage(account_id).await?;
485        let Some(StorageSlot::Map(map)) = storage.slots().get(index as usize) else {
486            return Err(StoreError::AccountError(AccountError::StorageSlotNotMap(index)));
487        };
488
489        let value = map.get(&key);
490        let witness = map.open(&key);
491
492        Ok((value, witness))
493    }
494}
495
496// PARTIAL BLOCKCHAIN NODE FILTER
497// ================================================================================================
498
499/// Filters for searching specific MMR nodes.
500// TODO: Should there be filters for specific blocks instead of nodes?
501pub enum PartialBlockchainFilter {
502    /// Return all nodes.
503    All,
504    /// Filter by the specified in-order indices.
505    List(Vec<InOrderIndex>),
506}
507
508// TRANSACTION FILTERS
509// ================================================================================================
510
511/// Filters for narrowing the set of transactions returned by the client's store.
512#[derive(Debug, Clone)]
513pub enum TransactionFilter {
514    /// Return all transactions.
515    All,
516    /// Filter by transactions that haven't yet been committed to the blockchain as per the last
517    /// sync.
518    Uncommitted,
519    /// Return a list of the transaction that matches the provided [`TransactionId`]s.
520    Ids(Vec<TransactionId>),
521    /// Return a list of the expired transactions that were executed before the provided
522    /// [`BlockNumber`]. Transactions created after the provided block number are not
523    /// considered.
524    ///
525    /// A transaction is considered expired if is uncommitted and the transaction's block number
526    /// is less than the provided block number.
527    ExpiredBefore(BlockNumber),
528}
529
530// TRANSACTIONS FILTER HELPERS
531// ================================================================================================
532
533impl TransactionFilter {
534    /// Returns a [String] containing the query for this Filter.
535    pub fn to_query(&self) -> String {
536        const QUERY: &str = "SELECT tx.id, script.script, tx.details, tx.status \
537            FROM transactions AS tx LEFT JOIN transaction_scripts AS script ON tx.script_root = script.script_root";
538        match self {
539            TransactionFilter::All => QUERY.to_string(),
540            TransactionFilter::Uncommitted => format!(
541                "{QUERY} WHERE tx.status_variant = {}",
542                TransactionStatusVariant::Pending as u8,
543            ),
544            TransactionFilter::Ids(_) => {
545                // Use SQLite's array parameter binding
546                format!("{QUERY} WHERE tx.id IN rarray(?)")
547            },
548            TransactionFilter::ExpiredBefore(block_num) => {
549                format!(
550                    "{QUERY} WHERE tx.block_num < {} AND tx.status_variant != {} AND tx.status_variant != {}",
551                    block_num.as_u32(),
552                    TransactionStatusVariant::Discarded as u8,
553                    TransactionStatusVariant::Committed as u8
554                )
555            },
556        }
557    }
558}
559
560// NOTE FILTER
561// ================================================================================================
562
563/// Filters for narrowing the set of notes returned by the client's store.
564#[derive(Debug, Clone)]
565pub enum NoteFilter {
566    /// Return a list of all notes ([`InputNoteRecord`] or [`OutputNoteRecord`]).
567    All,
568    /// Return a list of committed notes ([`InputNoteRecord`] or [`OutputNoteRecord`]). These
569    /// represent notes that the blockchain has included in a block.
570    Committed,
571    /// Filter by consumed notes ([`InputNoteRecord`] or [`OutputNoteRecord`]). notes that have
572    /// been used as inputs in transactions.
573    Consumed,
574    /// Return a list of expected notes ([`InputNoteRecord`] or [`OutputNoteRecord`]). These
575    /// represent notes for which the store doesn't have anchor data.
576    Expected,
577    /// Return a list containing any notes that match with the provided [`NoteId`] vector.
578    List(Vec<NoteId>),
579    /// Return a list containing any notes that match the provided [`Nullifier`] vector.
580    Nullifiers(Vec<Nullifier>),
581    /// Return a list of notes that are currently being processed. This filter doesn't apply to
582    /// output notes.
583    Processing,
584    /// Return a list containing the note that matches with the provided [`NoteId`]. The query will
585    /// return an error if the note isn't found.
586    Unique(NoteId),
587    /// Return a list containing notes that haven't been nullified yet, this includes expected,
588    /// committed, processing and unverified notes.
589    Unspent,
590    /// Return a list containing notes with unverified inclusion proofs. This filter doesn't apply
591    /// to output notes.
592    Unverified,
593}
594
595// BLOCK RELEVANCE
596// ================================================================================================
597
598/// Expresses metadata about the block header.
599#[derive(Debug, Clone)]
600pub enum BlockRelevance {
601    /// The block header includes notes that the client may consume.
602    HasNotes,
603    /// The block header does not contain notes relevant to the client.
604    Irrelevant,
605}
606
607impl From<BlockRelevance> for bool {
608    fn from(val: BlockRelevance) -> Self {
609        match val {
610            BlockRelevance::HasNotes => true,
611            BlockRelevance::Irrelevant => false,
612        }
613    }
614}
615
616impl From<bool> for BlockRelevance {
617    fn from(has_notes: bool) -> Self {
618        if has_notes {
619            BlockRelevance::HasNotes
620        } else {
621            BlockRelevance::Irrelevant
622        }
623    }
624}