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