miden_client/note/
note_screener.rs

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