miden_client/note/
note_screener.rs

1use alloc::{sync::Arc, vec::Vec};
2use core::fmt;
3
4use miden_lib::{account::interface::AccountInterface, note::well_known_note::WellKnownNote};
5use miden_objects::{
6    AccountError, AssetError,
7    account::{Account, AccountId},
8    assembly::DefaultSourceManager,
9    note::{Note, NoteId},
10    transaction::{InputNote, InputNotes},
11};
12use miden_tx::{
13    NoteAccountExecution, NoteConsumptionChecker, TransactionExecutor, TransactionExecutorError,
14    TransactionMastStore,
15};
16use thiserror::Error;
17
18use crate::{
19    store::{Store, StoreError},
20    transaction::{TransactionRequestBuilder, TransactionRequestError},
21};
22
23/// Describes the relevance of a note based on the screening.
24#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
25pub enum NoteRelevance {
26    /// The note can be consumed in the client's current block.
27    Now,
28    /// The note can be consumed after the block with the specified number.
29    After(u32),
30}
31
32/// Represents the consumability of a note by a specific account.
33///
34/// The tuple contains the account ID that may consume the note and the moment it will become
35/// relevant.
36pub type NoteConsumability = (AccountId, NoteRelevance);
37
38impl fmt::Display for NoteRelevance {
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40        match self {
41            NoteRelevance::Now => write!(f, "Now"),
42            NoteRelevance::After(height) => write!(f, "After block {height}"),
43        }
44    }
45}
46
47/// Provides functionality for testing whether a note is relevant to the client or not.
48///
49/// Here, relevance is based on whether the note is able to be consumed by an account that is
50/// tracked in the provided `store`. This can be derived in a number of ways, such as looking
51/// at the combination of script root and note inputs. For example, a P2ID note is relevant
52/// for a specific account ID if this ID is its first note input.
53pub struct NoteScreener<'a> {
54    /// A reference to the client's store, used to fetch necessary data to check consumability.
55    store: Arc<dyn Store>,
56    /// A consumption checker, used to check whether a note can be consumed by an account.
57    consumption_checker: NoteConsumptionChecker<'a>,
58    /// A MAST store, used to provide code inputs to the VM.
59    mast_store: Arc<TransactionMastStore>,
60}
61
62impl<'a> NoteScreener<'a> {
63    pub fn new(
64        store: Arc<dyn Store>,
65        tx_executor: &'a TransactionExecutor,
66        mast_store: Arc<TransactionMastStore>,
67    ) -> Self {
68        Self {
69            store,
70            consumption_checker: NoteConsumptionChecker::new(tx_executor),
71            mast_store,
72        }
73    }
74
75    /// Returns a vector of tuples describing the relevance of the provided note to the
76    /// accounts monitored by this screener.
77    ///
78    /// Does a fast check for known scripts (P2ID, P2IDR, SWAP). We're currently
79    /// unable to execute notes that aren't committed so a slow check for other scripts is
80    /// currently not available.
81    ///
82    /// If relevance can't be determined, the screener defaults to setting the note as consumable.
83    pub async fn check_relevance(
84        &self,
85        note: &Note,
86    ) -> Result<Vec<NoteConsumability>, NoteScreenerError> {
87        let mut note_relevances = vec![];
88        for id in self.store.get_account_ids().await? {
89            let account_record = self
90                .store
91                .get_account(id)
92                .await?
93                .ok_or(NoteScreenerError::AccountDataNotFound(id))?;
94
95            match self.check_standard_consumability(account_record.account(), note).await {
96                Ok(Some(relevance)) => {
97                    note_relevances.push((id, relevance));
98                },
99                Ok(None) => {
100                    // The note might be consumable after a certain block height if the note is
101                    // p2idr
102                    let script_root = note.script().root();
103
104                    if script_root == WellKnownNote::P2IDR.script_root() {
105                        if let Some(relevance) = Self::check_p2idr_recall_consumability(note, &id)?
106                        {
107                            note_relevances.push((id, relevance));
108                        }
109                    }
110                },
111                // If an error occurs while checking consumability, we count it as not relevant for
112                // that account
113                Err(_) => {},
114            }
115        }
116
117        Ok(note_relevances)
118    }
119
120    /// Tries to execute a standard consume transaction to check if the note is consumable by the
121    /// account.
122    async fn check_standard_consumability(
123        &self,
124        account: &Account,
125        note: &Note,
126    ) -> Result<Option<NoteRelevance>, NoteScreenerError> {
127        let transaction_request =
128            TransactionRequestBuilder::new().build_consume_notes(vec![note.id()])?;
129
130        let tx_script =
131            transaction_request.build_transaction_script(&AccountInterface::from(account), true)?;
132
133        let tx_args = transaction_request.clone().into_transaction_args(tx_script, vec![]);
134        let input_notes = InputNotes::new(vec![InputNote::unauthenticated(note.clone())])
135            .expect("Single note should be valid");
136
137        self.mast_store.load_transaction_code(account.code(), &input_notes, &tx_args);
138
139        if let NoteAccountExecution::Success = self
140            .consumption_checker
141            .check_notes_consumability(
142                account.id(),
143                self.store.get_sync_height().await?,
144                input_notes,
145                tx_args,
146                Arc::new(DefaultSourceManager::default()),
147            )
148            .await?
149        {
150            return Ok(Some(NoteRelevance::Now));
151        }
152
153        Ok(None)
154    }
155
156    /// Special relevance check for P2IDR notes. It checks if the sender account can consume and
157    /// recall the note.
158    fn check_p2idr_recall_consumability(
159        note: &Note,
160        account_id: &AccountId,
161    ) -> Result<Option<NoteRelevance>, NoteScreenerError> {
162        let note_inputs = note.inputs().values();
163        if note_inputs.len() != 3 {
164            return Err(InvalidNoteInputsError::WrongNumInputs(note.id(), 3).into());
165        }
166
167        let recall_height_felt = note_inputs[2];
168
169        let sender = note.metadata().sender();
170        let recall_height: u32 = recall_height_felt.as_int().try_into().map_err(|_err| {
171            InvalidNoteInputsError::BlockNumberError(note.id(), recall_height_felt.as_int())
172        })?;
173
174        if sender == *account_id {
175            Ok(Some(NoteRelevance::After(recall_height)))
176        } else {
177            Ok(None)
178        }
179    }
180}
181
182// NOTE SCREENER ERRORS
183// ================================================================================================
184
185/// Error when screening notes to check relevance to a client.
186#[derive(Debug, Error)]
187pub enum NoteScreenerError {
188    #[error("error while processing note inputs")]
189    InvalidNoteInputsError(#[from] InvalidNoteInputsError),
190    #[error("account data wasn't found for account id {0}")]
191    AccountDataNotFound(AccountId),
192    #[error("error while fetching data from the store")]
193    StoreError(#[from] StoreError),
194    #[error("error while checking consume transaction")]
195    TransactionExecutionError(#[from] TransactionExecutorError),
196    #[error("error while building consume transaction request")]
197    TransactionRequestError(#[from] TransactionRequestError),
198}
199
200#[derive(Debug, Error)]
201pub enum InvalidNoteInputsError {
202    #[error("account error for note with id {0}: {1}")]
203    AccountError(NoteId, AccountError),
204    #[error("asset error for note with id {0}: {1}")]
205    AssetError(NoteId, AssetError),
206    #[error("expected {1} note inputs for note with id {0}")]
207    WrongNumInputs(NoteId, usize),
208    #[error("note input representing block with value {1} for note with id {0}")]
209    BlockNumberError(NoteId, u64),
210}