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    auth::TransactionAuthenticator,
15};
16use thiserror::Error;
17
18use crate::{
19    store::{Store, StoreError, data_store::ClientDataStore},
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 {
54    /// A reference to the client's store, used to fetch necessary data to check consumability.
55    store: Arc<dyn Store>,
56    /// A reference to the transaction authenticator
57    authenticator: Option<Arc<dyn TransactionAuthenticator>>,
58}
59
60impl NoteScreener {
61    pub fn new(
62        store: Arc<dyn Store>,
63        authenticator: Option<Arc<dyn TransactionAuthenticator>>,
64    ) -> Self {
65        Self { store, authenticator }
66    }
67
68    /// Returns a vector of tuples describing the relevance of the provided note to the
69    /// accounts monitored by this screener.
70    ///
71    /// Does a fast check for known scripts (P2ID, P2IDE, SWAP). We're currently
72    /// unable to execute notes that aren't committed so a slow check for other scripts is
73    /// currently not available.
74    ///
75    /// If relevance can't be determined, the screener defaults to setting the note as consumable.
76    pub async fn check_relevance(
77        &self,
78        note: &Note,
79    ) -> Result<Vec<NoteConsumability>, NoteScreenerError> {
80        let mut note_relevances = vec![];
81        for id in self.store.get_account_ids().await? {
82            let account_record = self
83                .store
84                .get_account(id)
85                .await?
86                .ok_or(NoteScreenerError::AccountDataNotFound(id))?;
87
88            match self.check_standard_consumability(account_record.account(), note).await {
89                Ok(Some(relevance)) => {
90                    note_relevances.push((id, relevance));
91                },
92                Ok(None) => {
93                    // The note might be consumable after a certain block height if the note is
94                    // p2ide
95                    let script_root = note.script().root();
96
97                    if script_root == WellKnownNote::P2IDE.script_root() {
98                        if let Some(relevance) = Self::check_p2ide_recall_consumability(note, &id)?
99                        {
100                            note_relevances.push((id, relevance));
101                        }
102                    }
103                },
104                // If an error occurs while checking consumability, we count it as not relevant for
105                // that account
106                Err(_) => {},
107            }
108        }
109
110        Ok(note_relevances)
111    }
112
113    /// Tries to execute a standard consume transaction to check if the note is consumable by the
114    /// account.
115    async fn check_standard_consumability(
116        &self,
117        account: &Account,
118        note: &Note,
119    ) -> Result<Option<NoteRelevance>, NoteScreenerError> {
120        let transaction_request =
121            TransactionRequestBuilder::new().build_consume_notes(vec![note.id()])?;
122
123        let tx_script =
124            transaction_request.build_transaction_script(&AccountInterface::from(account), true)?;
125
126        let tx_args = transaction_request.clone().into_transaction_args(tx_script, vec![]);
127        let input_notes = InputNotes::new(vec![InputNote::unauthenticated(note.clone())])
128            .expect("Single note should be valid");
129
130        let data_store = ClientDataStore::new(self.store.clone());
131        let transaction_executor =
132            TransactionExecutor::new(&data_store, self.authenticator.as_deref());
133        let consumption_checker = NoteConsumptionChecker::new(&transaction_executor);
134
135        data_store.mast_store().load_account_code(account.code());
136
137        if let NoteAccountExecution::Success = consumption_checker
138            .check_notes_consumability(
139                account.id(),
140                self.store.get_sync_height().await?,
141                input_notes,
142                tx_args,
143                Arc::new(DefaultSourceManager::default()),
144            )
145            .await?
146        {
147            return Ok(Some(NoteRelevance::Now));
148        }
149
150        Ok(None)
151    }
152
153    /// Special relevance check for P2IDE notes. It checks if the sender account can consume and
154    /// recall the note.
155    fn check_p2ide_recall_consumability(
156        note: &Note,
157        account_id: &AccountId,
158    ) -> Result<Option<NoteRelevance>, NoteScreenerError> {
159        let note_inputs = note.inputs().values();
160        if note_inputs.len() != 4 {
161            return Err(InvalidNoteInputsError::WrongNumInputs(note.id(), 4).into());
162        }
163
164        let recall_height_felt = note_inputs[2];
165
166        let sender = note.metadata().sender();
167        let recall_height: u32 = recall_height_felt.as_int().try_into().map_err(|_err| {
168            InvalidNoteInputsError::BlockNumberError(note.id(), recall_height_felt.as_int())
169        })?;
170
171        if sender == *account_id {
172            Ok(Some(NoteRelevance::After(recall_height)))
173        } else {
174            Ok(None)
175        }
176    }
177}
178
179// NOTE SCREENER ERRORS
180// ================================================================================================
181
182/// Error when screening notes to check relevance to a client.
183#[derive(Debug, Error)]
184pub enum NoteScreenerError {
185    #[error("error while processing note inputs")]
186    InvalidNoteInputsError(#[from] InvalidNoteInputsError),
187    #[error("account data wasn't found for account id {0}")]
188    AccountDataNotFound(AccountId),
189    #[error("error while fetching data from the store")]
190    StoreError(#[from] StoreError),
191    #[error("error while checking consume transaction")]
192    TransactionExecutionError(#[from] TransactionExecutorError),
193    #[error("error while building consume transaction request")]
194    TransactionRequestError(#[from] TransactionRequestError),
195}
196
197#[derive(Debug, Error)]
198pub enum InvalidNoteInputsError {
199    #[error("account error for note with id {0}: {1}")]
200    AccountError(NoteId, AccountError),
201    #[error("asset error for note with id {0}: {1}")]
202    AssetError(NoteId, AssetError),
203    #[error("expected {1} note inputs for note with id {0}")]
204    WrongNumInputs(NoteId, usize),
205    #[error("note input representing block with value {1} for note with id {0}")]
206    BlockNumberError(NoteId, u64),
207}