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}