Skip to main content

miden_objects/
note_file.rs

1//! The note file format.
2
3use alloc::vec::Vec;
4#[cfg(feature = "std")]
5use std::path::Path;
6
7use miden_protocol::block::BlockNumber;
8use miden_protocol::note::{Note, NoteDetails, NoteId, NoteInclusionProof, NoteTag};
9
10use crate::{ConversionError, DecodeMessageExt, proto};
11
12#[cfg(test)]
13mod tests;
14
15// NOTE SYNC HINT
16// ================================================================================================
17
18/// Hints used by a client to find a note on chain after importing a note file.
19///
20/// The values in this type are intended to guide note synchronization without requiring an exact
21/// [`NoteId`] lookup. A client can sync notes by `tag` (starting from `after_block_num`) and get a
22/// set of notes that may contain the expected note. Because a tag does not uniquely identify a note
23/// but rather expresses a use-case, the client does not need to leak a commitment to a specific
24/// note (i.e., its ID) when syncing.
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub struct NoteSyncHint {
27    /// The block after which the note is expected to appear on chain.
28    ///
29    /// This should be treated as a lower-bound hint: there is no guarantee that the note will
30    /// appear on chain, or that it will appear after this block.
31    after_block_num: BlockNumber,
32    /// The tag expected to be associated with the note.
33    tag: NoteTag,
34}
35
36impl NoteSyncHint {
37    /// Returns a new [`NoteSyncHint`] instantiated from the provided parameters.
38    pub fn new(after_block_num: BlockNumber, tag: NoteTag) -> Self {
39        Self { after_block_num, tag }
40    }
41
42    /// Returns the block after which the note is expected to appear on chain.
43    pub fn after_block_num(&self) -> BlockNumber {
44        self.after_block_num
45    }
46
47    /// Returns the tag expected to be associated with the note.
48    pub fn tag(&self) -> NoteTag {
49        self.tag
50    }
51}
52
53// NOTE FILE
54// ================================================================================================
55
56/// A serialized representation of a note.
57///
58/// A [`NoteFile`] can be used to communicate details of a note across network clients.
59/// Each variant covers a specific subset of use-cases and commit to specific trade-offs.
60#[derive(Clone, Debug, PartialEq, Eq)]
61#[allow(clippy::large_enum_variant)]
62pub enum NoteFile {
63    /// The note's details aren't known, only its ID is.
64    /// A client can import all the note details from the network. As such, the note that the ID
65    /// commits to should be public.
66    NoteId(NoteId),
67    /// The note's details are known, but its metadata and attachments must be recovered from the
68    /// chain.
69    ///
70    /// This is useful for importing an expected note while avoiding an exact [`NoteId`] lookup.
71    /// Looking a note up by its exact ID would reveal to the node which note the importer is
72    /// interested in, leaking the receiver's privacy. Instead, the importer can use the sync hint
73    /// to search for matching notes by tag, then, for each returned note, recompute the note ID as
74    /// `NoteId::new(details_commitment, returned_metadata)` using this variant's details
75    /// commitment; the returned note whose recomputed ID matches is the expected one. This recovers
76    /// its metadata without revealing the note ID to the node.
77    ///
78    /// Only the note's details are carried here, as they may be private. Metadata and attachments
79    /// are always public, so they are recovered from the chain rather than carried: once the note
80    /// is found via the tag-based sync, its attachments can be fetched for the whole returned set
81    /// (e.g. via `get_notes_by_id`) without revealing which note is the expected one.
82    ExpectedNote {
83        details: NoteDetails,
84        sync_hint: NoteSyncHint,
85    },
86    /// The note has been committed to the chain and its inclusion proof is known.
87    Committed { note: Note, proof: NoteInclusionProof },
88}
89
90impl NoteFile {
91    // SERIALIZATION
92    // --------------------------------------------------------------------------------------------
93
94    /// Returns the encoded file as a Protobuf message.
95    pub fn to_bytes(&self) -> Vec<u8> {
96        prost::Message::encode_to_vec(&proto::note_file::NoteFile::from(self))
97    }
98
99    /// Decodes a [`NoteFile`] from the provided bytes.
100    ///
101    /// The encoded note carries its script, whose size is unbounded. A caller that decodes
102    /// untrusted bytes must cap their length first.
103    ///
104    /// # Errors
105    ///
106    /// Returns an error if the bytes are not a valid note file.
107    pub fn try_from_bytes(bytes: &[u8]) -> Result<Self, NoteFileError> {
108        <proto::note_file::NoteFile as prost::Message>::decode(bytes)
109            .map_err(|error| NoteFileError::Decode(ConversionError::new(error)))?
110            .decode_and_verify()
111            .map_err(NoteFileError::Decode)
112    }
113
114    /// Writes the encoded file to the provided path.
115    #[cfg(feature = "std")]
116    pub fn write(&self, path: impl AsRef<Path>) -> Result<(), NoteFileError> {
117        std::fs::write(path, self.to_bytes()).map_err(NoteFileError::Io)
118    }
119
120    /// Reads a [`NoteFile`] from the provided path.
121    ///
122    /// # Errors
123    ///
124    /// Returns an error if the file cannot be read, or if [`Self::try_from_bytes`] rejects its
125    /// contents.
126    #[cfg(feature = "std")]
127    pub fn read(path: impl AsRef<Path>) -> Result<Self, NoteFileError> {
128        let bytes = std::fs::read(path).map_err(NoteFileError::Io)?;
129        Self::try_from_bytes(&bytes)
130    }
131}
132
133impl From<Note> for NoteFile {
134    fn from(note: Note) -> Self {
135        let (assets, metadata, recipient, _attachments) = note.into_parts();
136        NoteFile::ExpectedNote {
137            details: NoteDetails::new(assets, recipient),
138            sync_hint: NoteSyncHint::new(0.into(), metadata.tag()),
139        }
140    }
141}
142
143impl From<NoteId> for NoteFile {
144    fn from(note_id: NoteId) -> Self {
145        NoteFile::NoteId(note_id)
146    }
147}
148
149// NOTE FILE ERROR
150// ================================================================================================
151
152#[derive(Debug, thiserror::Error)]
153#[non_exhaustive]
154pub enum NoteFileError {
155    #[error("failed to decode the note file")]
156    Decode(#[source] ConversionError),
157    #[cfg(feature = "std")]
158    #[error("failed to read or write the note file")]
159    Io(#[source] std::io::Error),
160}