miden_client/note/
note_screener.rs

1use alloc::boxed::Box;
2use alloc::sync::Arc;
3use alloc::vec::Vec;
4use core::fmt;
5
6use miden_lib::account::interface::AccountInterface;
7use miden_lib::note::well_known_note::WellKnownNote;
8use miden_objects::account::{Account, AccountId};
9use miden_objects::note::{Note, NoteId};
10use miden_objects::transaction::InputNote;
11use miden_objects::{AccountError, AssetError};
12use miden_tx::auth::TransactionAuthenticator;
13use miden_tx::{
14    NoteCheckerError,
15    NoteConsumptionChecker,
16    NoteConsumptionStatus,
17    TransactionExecutor,
18};
19use thiserror::Error;
20use tonic::async_trait;
21
22use crate::ClientError;
23use crate::rpc::domain::note::CommittedNote;
24use crate::store::data_store::ClientDataStore;
25use crate::store::{InputNoteRecord, NoteFilter, Store, StoreError};
26use crate::sync::{NoteUpdateAction, OnNoteReceived};
27use crate::transaction::{TransactionRequestBuilder, TransactionRequestError};
28
29/// Describes the relevance of a note based on the screening.
30#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
31pub enum NoteRelevance {
32    /// The note can be consumed in the client's current block.
33    Now,
34    /// The note can be consumed after the block with the specified number.
35    After(u32),
36}
37
38/// Represents the consumability of a note by a specific account.
39///
40/// The tuple contains the account ID that may consume the note and the moment it will become
41/// relevant.
42pub type NoteConsumability = (AccountId, NoteRelevance);
43
44impl fmt::Display for NoteRelevance {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        match self {
47            NoteRelevance::Now => write!(f, "Now"),
48            NoteRelevance::After(height) => write!(f, "After block {height}"),
49        }
50    }
51}
52
53/// Provides functionality for testing whether a note is relevant to the client or not.
54///
55/// Here, relevance is based on whether the note is able to be consumed by an account that is
56/// tracked in the provided `store`. This can be derived in a number of ways, such as looking
57/// at the combination of script root and note inputs. For example, a P2ID note is relevant
58/// for a specific account ID if this ID is its first note input.
59pub struct NoteScreener<AUTH> {
60    /// A reference to the client's store, used to fetch necessary data to check consumability.
61    store: Arc<dyn Store>,
62    /// A reference to the transaction authenticator
63    authenticator: Option<Arc<AUTH>>,
64}
65
66impl<AUTH> NoteScreener<AUTH>
67where
68    AUTH: TransactionAuthenticator + Sync,
69{
70    pub fn new(store: Arc<dyn Store>, authenticator: Option<Arc<AUTH>>) -> Self {
71        Self { store, authenticator }
72    }
73
74    /// Returns a vector of tuples describing the relevance of the provided note to the
75    /// accounts monitored by this screener.
76    ///
77    /// If relevance can't be determined, the screener defaults to setting the note as consumable.
78    pub async fn check_relevance(
79        &self,
80        note: &Note,
81    ) -> Result<Vec<NoteConsumability>, NoteScreenerError> {
82        let mut note_relevances = vec![];
83        for id in self.store.get_account_ids().await? {
84            let account_record = self
85                .store
86                .get_account(id)
87                .await?
88                .ok_or(NoteScreenerError::AccountDataNotFound(id))?;
89
90            match self.check_standard_consumability(account_record.account(), note).await {
91                Ok(Some(relevance)) => {
92                    note_relevances.push((id, relevance));
93                },
94                Ok(None) => {
95                    // The note might be consumable after a certain block height if the note is
96                    // p2ide
97                    let script_root = note.script().root();
98
99                    if script_root == WellKnownNote::P2IDE.script_root()
100                        && let Some(relevance) = Self::check_p2ide_recall_consumability(note, &id)?
101                    {
102                        note_relevances.push((id, relevance));
103                    }
104                },
105                // If an error occurs while checking consumability, we count it as not relevant for
106                // that account
107                Err(_) => {},
108            }
109        }
110
111        Ok(note_relevances)
112    }
113
114    /// Tries to execute a standard consume transaction to check if the note is consumable by the
115    /// account.
116    async fn check_standard_consumability(
117        &self,
118        account: &Account,
119        note: &Note,
120    ) -> Result<Option<NoteRelevance>, NoteScreenerError> {
121        let transaction_request =
122            TransactionRequestBuilder::new().build_consume_notes(vec![note.id()])?;
123
124        let tx_script = transaction_request.build_transaction_script(
125            &AccountInterface::from(account),
126            crate::DebugMode::Enabled,
127        )?;
128
129        let tx_args = transaction_request.clone().into_transaction_args(tx_script, vec![]);
130
131        let data_store = ClientDataStore::new(self.store.clone());
132        let mut transaction_executor = TransactionExecutor::new(&data_store);
133        if let Some(authenticator) = &self.authenticator {
134            transaction_executor = transaction_executor.with_authenticator(authenticator.as_ref());
135        }
136
137        let consumption_checker = NoteConsumptionChecker::new(&transaction_executor);
138
139        data_store.mast_store().load_account_code(account.code());
140        let note_consumption_check = consumption_checker
141            .can_consume(
142                account.id(),
143                self.store.get_sync_height().await?,
144                InputNote::unauthenticated(note.clone()),
145                tx_args,
146            )
147            .await?;
148
149        let result = match note_consumption_check {
150            NoteConsumptionStatus::ConsumableAfter(block_number) => {
151                Some(NoteRelevance::After(block_number.as_u32()))
152            },
153            NoteConsumptionStatus::Consumable
154            | NoteConsumptionStatus::UnconsumableWithoutAuthorization => Some(NoteRelevance::Now),
155            // NOTE: NoteConsumptionStatus::Unconsumable means that state-related context does not
156            // allow for consumption, so don't keep for now. In the next version, we should be more
157            // careful about this
158            NoteConsumptionStatus::Unconsumable | NoteConsumptionStatus::Incompatible => None,
159        };
160        Ok(result)
161    }
162
163    /// Special relevance check for P2IDE notes. It checks if the sender account can consume and
164    /// recall the note.
165    fn check_p2ide_recall_consumability(
166        note: &Note,
167        account_id: &AccountId,
168    ) -> Result<Option<NoteRelevance>, NoteScreenerError> {
169        let note_inputs = note.inputs().values();
170        if note_inputs.len() != 4 {
171            return Err(InvalidNoteInputsError::WrongNumInputs(note.id(), 4).into());
172        }
173
174        let recall_height_felt = note_inputs[2];
175
176        let sender = note.metadata().sender();
177        let recall_height: u32 = recall_height_felt.as_int().try_into().map_err(|_err| {
178            InvalidNoteInputsError::BlockNumberError(note.id(), recall_height_felt.as_int())
179        })?;
180
181        if sender == *account_id {
182            Ok(Some(NoteRelevance::After(recall_height)))
183        } else {
184            Ok(None)
185        }
186    }
187}
188
189// DEFAULT CALLBACK IMPLEMENTATIONS
190// ================================================================================================
191
192#[async_trait(?Send)]
193impl<AUTH> OnNoteReceived for NoteScreener<AUTH>
194where
195    AUTH: TransactionAuthenticator + Sync,
196{
197    /// Default implementation of the [`OnNoteReceived`] callback. It queries the store for the
198    /// committed note to check if it's relevant. If the note wasn't being tracked but it came in
199    /// the sync response it may be a new public note, in that case we use the [`NoteScreener`]
200    /// to check its relevance.
201    async fn on_note_received(
202        &self,
203        committed_note: CommittedNote,
204        public_note: Option<InputNoteRecord>,
205    ) -> Result<NoteUpdateAction, ClientError> {
206        let note_id = *committed_note.note_id();
207
208        let input_note_present =
209            !self.store.get_input_notes(NoteFilter::Unique(note_id)).await?.is_empty();
210        let output_note_present =
211            !self.store.get_output_notes(NoteFilter::Unique(note_id)).await?.is_empty();
212
213        if input_note_present || output_note_present {
214            // The note is being tracked by the client so it is relevant
215            return Ok(NoteUpdateAction::Commit(committed_note));
216        }
217
218        match public_note {
219            Some(public_note) => {
220                // If tracked by the user, keep note regardless of inputs and extra checks
221                if let Some(metadata) = public_note.metadata()
222                    && self.store.get_unique_note_tags().await?.contains(&metadata.tag())
223                {
224                    return Ok(NoteUpdateAction::Insert(public_note));
225                }
226
227                // The note is not being tracked by the client and is public so we can screen it
228                let new_note_relevance = self
229                    .check_relevance(
230                        &public_note
231                            .clone()
232                            .try_into()
233                            .map_err(ClientError::NoteRecordConversionError)?,
234                    )
235                    .await?;
236                let is_relevant = !new_note_relevance.is_empty();
237                if is_relevant {
238                    Ok(NoteUpdateAction::Insert(public_note))
239                } else {
240                    Ok(NoteUpdateAction::Discard)
241                }
242            },
243            None => {
244                // The note is not being tracked by the client and is private so we can't determine
245                // if it is relevant
246                Ok(NoteUpdateAction::Discard)
247            },
248        }
249    }
250}
251
252// NOTE SCREENER ERRORS
253// ================================================================================================
254
255/// Error when screening notes to check relevance to a client.
256#[derive(Debug, Error)]
257pub enum NoteScreenerError {
258    #[error("error while processing note inputs")]
259    InvalidNoteInputsError(#[from] InvalidNoteInputsError),
260    #[error("account data wasn't found for account id {0}")]
261    AccountDataNotFound(AccountId),
262    #[error("error while fetching data from the store")]
263    StoreError(#[from] StoreError),
264    #[error("error while checking note")]
265    NoteCheckerError(#[from] NoteCheckerError),
266    #[error("error while building transaction request")]
267    TransactionRequestError(#[from] TransactionRequestError),
268}
269
270#[derive(Debug, Error)]
271pub enum InvalidNoteInputsError {
272    #[error("account error for note with id {0}: {1}")]
273    AccountError(NoteId, AccountError),
274    #[error("asset error for note with id {0}: {1}")]
275    AssetError(NoteId, AssetError),
276    #[error("expected {1} note inputs for note with id {0}")]
277    WrongNumInputs(NoteId, usize),
278    #[error("note input representing block with value {1} for note with id {0}")]
279    BlockNumberError(NoteId, u64),
280}