miden_client/note/
import.rs

1//! Provides note importing methods.
2//!
3//! This module allows users to import notes into the client's store.
4//! Depending on the variant of [`NoteFile`] provided, the client will either fetch note details
5//! from the network or create a new note record from supplied data. If a note already exists in
6//! the store, it is updated with the new information. Additionally, the appropriate note tag
7//! is tracked based on the imported note's metadata.
8//!
9//! For more specific information on how the process is performed, refer to the docs for
10//! [`Client::import_note()`].
11use alloc::string::ToString;
12
13use miden_objects::block::BlockNumber;
14use miden_objects::note::{
15    Note,
16    NoteDetails,
17    NoteFile,
18    NoteId,
19    NoteInclusionProof,
20    NoteMetadata,
21    NoteTag,
22};
23use miden_tx::auth::TransactionAuthenticator;
24
25use crate::rpc::RpcError;
26use crate::rpc::domain::note::FetchedNote;
27use crate::store::input_note_states::ExpectedNoteState;
28use crate::store::{InputNoteRecord, InputNoteState};
29use crate::sync::NoteTagRecord;
30use crate::{Client, ClientError};
31
32/// Note importing methods.
33impl<AUTH> Client<AUTH>
34where
35    AUTH: TransactionAuthenticator + Sync + 'static,
36{
37    // INPUT NOTE CREATION
38    // --------------------------------------------------------------------------------------------
39
40    /// Imports a new input note into the client's store. The information stored depends on the
41    /// type of note file provided. If the note existed previously, it will be updated with the
42    /// new information. The tag specified by the `NoteFile` will start being tracked.
43    ///
44    /// - If the note file is a [`NoteFile::NoteId`], the note is fetched from the node and stored
45    ///   in the client's store. If the note is private or doesn't exist, an error is returned.
46    /// - If the note file is a [`NoteFile::NoteDetails`], a new note is created with the provided
47    ///   details and tag.
48    /// - If the note file is a [`NoteFile::NoteWithProof`], the note is stored with the provided
49    ///   inclusion proof and metadata. The block header data is only fetched from the node if the
50    ///   note is committed in the past relative to the client.
51    ///
52    /// # Errors
53    ///
54    /// - If an attempt is made to overwrite a note that is currently processing.
55    pub async fn import_note(&mut self, note_file: NoteFile) -> Result<NoteId, ClientError> {
56        let id = match &note_file {
57            NoteFile::NoteId(id) => *id,
58            NoteFile::NoteDetails { details, .. } => details.id(),
59            NoteFile::NoteWithProof(note, _) => note.id(),
60        };
61
62        let previous_note = self.get_input_note(id).await?;
63
64        // If the note is already in the store and is in the state processing we return an error.
65        if let Some(true) = previous_note.as_ref().map(InputNoteRecord::is_processing) {
66            return Err(ClientError::NoteImportError(format!(
67                "Can't overwrite note with id {id} as it's currently being processed",
68            )));
69        }
70
71        let note = match note_file {
72            NoteFile::NoteId(id) => self.import_note_record_by_id(previous_note, id).await?,
73            NoteFile::NoteDetails { details, after_block_num, tag } => {
74                self.import_note_record_by_details(previous_note, details, after_block_num, tag)
75                    .await?
76            },
77            NoteFile::NoteWithProof(note, inclusion_proof) => {
78                self.import_note_record_by_proof(previous_note, note, inclusion_proof).await?
79            },
80        };
81
82        if let Some(note) = note {
83            if let InputNoteState::Expected(ExpectedNoteState { tag: Some(tag), .. }) = note.state()
84            {
85                self.store
86                    .add_note_tag(NoteTagRecord::with_note_source(*tag, note.id()))
87                    .await?;
88            }
89            self.store.upsert_input_notes(&[note]).await?;
90        }
91
92        Ok(id)
93    }
94
95    // HELPERS
96    // ================================================================================================
97
98    /// Builds a note record from the note ID. If a note with the same ID was already stored it is
99    /// passed via `previous_note` so it can be updated. The note information is fetched from the
100    /// node and stored in the client's store.
101    ///
102    /// # Errors:
103    /// - If the note doesn't exist on the node.
104    /// - If the note exists but is private.
105    async fn import_note_record_by_id(
106        &self,
107        previous_note: Option<InputNoteRecord>,
108        id: NoteId,
109    ) -> Result<Option<InputNoteRecord>, ClientError> {
110        let fetched_note = self.rpc_api.get_note_by_id(id).await.map_err(|err| match err {
111            RpcError::NoteNotFound(note_id) => ClientError::NoteNotFoundOnChain(note_id),
112            err => ClientError::RpcError(err),
113        })?;
114
115        let inclusion_proof = fetched_note.inclusion_proof().clone();
116
117        if let Some(mut previous_note) = previous_note {
118            if previous_note.inclusion_proof_received(inclusion_proof, *fetched_note.metadata())? {
119                self.store.remove_note_tag((&previous_note).try_into()?).await?;
120
121                Ok(Some(previous_note))
122            } else {
123                Ok(None)
124            }
125        } else {
126            let fetched_note = match fetched_note {
127                FetchedNote::Public(note, _) => note,
128                FetchedNote::Private(..) => {
129                    return Err(ClientError::NoteImportError(
130                        "Incomplete imported note is private".to_string(),
131                    ));
132                },
133            };
134
135            self.import_note_record_by_proof(previous_note, fetched_note, inclusion_proof)
136                .await
137        }
138    }
139
140    /// Builds a note record from the note and inclusion proof. If a note with the same ID was
141    /// already stored it is passed via `previous_note` so it can be updated. The note's
142    /// nullifier is used to determine if the note has been consumed in the node and gives it
143    /// the correct state.
144    ///
145    /// If the note isn't consumed and it was committed in the past relative to the client, then
146    /// the MMR for the relevant block is fetched from the node and stored.
147    pub(crate) async fn import_note_record_by_proof(
148        &self,
149        previous_note: Option<InputNoteRecord>,
150        note: Note,
151        inclusion_proof: NoteInclusionProof,
152    ) -> Result<Option<InputNoteRecord>, ClientError> {
153        let metadata = *note.metadata();
154        let mut note_record = previous_note.unwrap_or(InputNoteRecord::new(
155            note.into(),
156            self.store.get_current_timestamp(),
157            ExpectedNoteState {
158                metadata: Some(metadata),
159                after_block_num: inclusion_proof.location().block_num(),
160                tag: Some(metadata.tag()),
161            }
162            .into(),
163        ));
164
165        if let Some(block_height) = self
166            .rpc_api
167            .get_nullifier_commit_height(
168                &note_record.nullifier(),
169                inclusion_proof.location().block_num(),
170            )
171            .await?
172        {
173            if note_record.consumed_externally(note_record.nullifier(), block_height)? {
174                return Ok(Some(note_record));
175            }
176
177            Ok(None)
178        } else {
179            let block_height = inclusion_proof.location().block_num();
180            let current_block_num = self.get_sync_height().await?;
181
182            let mut note_changed =
183                note_record.inclusion_proof_received(inclusion_proof, metadata)?;
184
185            if block_height <= current_block_num {
186                // If the note is committed in the past we need to manually fetch the block
187                // header and MMR proof to verify the inclusion proof.
188                let mut current_partial_mmr = self.store.get_current_partial_mmr().await?;
189
190                let block_header = self
191                    .get_and_store_authenticated_block(block_height, &mut current_partial_mmr)
192                    .await?;
193
194                note_changed |= note_record.block_header_received(&block_header)?;
195            } else {
196                // If the note is in the future we import it as unverified. We add the note tag so
197                // that the note is verified naturally in the next sync.
198                self.store
199                    .add_note_tag(NoteTagRecord::with_note_source(metadata.tag(), note_record.id()))
200                    .await?;
201            }
202
203            if note_changed { Ok(Some(note_record)) } else { Ok(None) }
204        }
205    }
206
207    /// Builds a note record from the note details. If a note with the same ID was already stored it
208    /// is passed via `previous_note` so it can be updated.
209    async fn import_note_record_by_details(
210        &mut self,
211        previous_note: Option<InputNoteRecord>,
212        details: NoteDetails,
213        after_block_num: BlockNumber,
214        tag: Option<NoteTag>,
215    ) -> Result<Option<InputNoteRecord>, ClientError> {
216        let mut note_record = previous_note.unwrap_or({
217            InputNoteRecord::new(
218                details,
219                self.store.get_current_timestamp(),
220                ExpectedNoteState { metadata: None, after_block_num, tag }.into(),
221            )
222        });
223
224        let committed_note_data = if let Some(tag) = tag {
225            self.check_expected_note(after_block_num, tag, note_record.details()).await?
226        } else {
227            None
228        };
229
230        match committed_note_data {
231            Some((metadata, inclusion_proof)) => {
232                let mut current_partial_mmr = self.store.get_current_partial_mmr().await?;
233                let block_header = self
234                    .get_and_store_authenticated_block(
235                        inclusion_proof.location().block_num(),
236                        &mut current_partial_mmr,
237                    )
238                    .await?;
239
240                let note_changed =
241                    note_record.inclusion_proof_received(inclusion_proof, metadata)?;
242
243                if note_record.block_header_received(&block_header)? | note_changed {
244                    self.store
245                        .remove_note_tag(NoteTagRecord::with_note_source(
246                            metadata.tag(),
247                            note_record.id(),
248                        ))
249                        .await?;
250
251                    Ok(Some(note_record))
252                } else {
253                    Ok(None)
254                }
255            },
256            None => Ok(Some(note_record)),
257        }
258    }
259
260    /// Checks if a note with the given `note_tag` and ID is present in the chain between the
261    /// `request_block_num` and the current block. If found it returns its metadata and inclusion
262    /// proof.
263    async fn check_expected_note(
264        &mut self,
265        mut request_block_num: BlockNumber,
266        tag: NoteTag,
267        expected_note: &miden_objects::note::NoteDetails,
268    ) -> Result<Option<(NoteMetadata, NoteInclusionProof)>, ClientError> {
269        let current_block_num = self.get_sync_height().await?;
270        loop {
271            if request_block_num > current_block_num {
272                return Ok(None);
273            }
274
275            let sync_notes = self
276                .rpc_api
277                .sync_notes(request_block_num, None, &[tag].into_iter().collect())
278                .await?;
279
280            // This means that notes with that note_tag were found.
281            // Therefore, we should check if a note with the same id was found.
282            let committed_note =
283                sync_notes.notes.iter().find(|note| note.note_id() == &expected_note.id());
284
285            if let Some(note) = committed_note {
286                // This means that a note with the same id was found.
287                // Therefore, we should mark the note as committed.
288                let note_block_num = sync_notes.block_header.block_num();
289
290                if note_block_num > current_block_num {
291                    return Ok(None);
292                }
293
294                let note_inclusion_proof = NoteInclusionProof::new(
295                    note_block_num,
296                    note.note_index(),
297                    note.inclusion_path().clone(),
298                )?;
299
300                return Ok(Some((note.metadata(), note_inclusion_proof)));
301            }
302
303            // We might have reached the chain tip without having found the note, bail if so
304            if sync_notes.block_header.block_num() == sync_notes.chain_tip {
305                return Ok(None);
306            }
307
308            // This means that a note with the same id was not found.
309            // Therefore, we should request again for sync_notes with the same note_tag
310            // and with the block_num of the last block header
311            // (sync_notes.block_header.unwrap()).
312            request_block_num = sync_notes.block_header.block_num();
313        }
314    }
315}