Skip to main content

miden_standards/note/
file.rs

1#[cfg(feature = "std")]
2use std::{
3    fs::{self, File},
4    io::{self, Read},
5    path::Path,
6    vec::Vec,
7};
8
9use miden_protocol::block::BlockNumber;
10use miden_protocol::note::{Note, NoteDetails, NoteId, NoteInclusionProof, NoteTag};
11#[cfg(feature = "std")]
12use miden_protocol::utils::serde::SliceReader;
13use miden_protocol::utils::serde::{
14    ByteReader,
15    ByteWriter,
16    Deserializable,
17    DeserializationError,
18    Serializable,
19};
20
21const MAGIC: &str = "note";
22
23// NOTE SYNC HINT
24// ================================================================================================
25
26/// Hints used by a client to find a note on chain after importing a note file.
27///
28/// The values in this type are intended to guide note synchronization without requiring an exact
29/// [`NoteId`] lookup. A client can sync notes by `tag` (starting from `after_block_num`) and get a
30/// set of notes that may contain the expected note. Because a tag does not uniquely identify a note
31/// but rather expresses a use-case, the client does not need to leak a commitment to a specific
32/// note (i.e., its ID) when syncing.
33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
34pub struct NoteSyncHint {
35    /// The block after which the note is expected to appear on chain.
36    ///
37    /// This should be treated as a lower-bound hint: there is no guarantee that the note will
38    /// appear on chain, or that it will appear after this block.
39    after_block_num: BlockNumber,
40    /// The tag expected to be associated with the note.
41    tag: NoteTag,
42}
43
44impl NoteSyncHint {
45    /// Returns a new [`NoteSyncHint`] instantiated from the provided parameters.
46    pub fn new(after_block_num: BlockNumber, tag: NoteTag) -> Self {
47        Self { after_block_num, tag }
48    }
49
50    /// Returns the block after which the note is expected to appear on chain.
51    pub fn after_block_num(&self) -> BlockNumber {
52        self.after_block_num
53    }
54
55    /// Returns the tag expected to be associated with the note.
56    pub fn tag(&self) -> NoteTag {
57        self.tag
58    }
59}
60
61// NOTE FILE
62// ================================================================================================
63
64/// A serialized representation of a note.
65///
66/// A [`NoteFile`] can be used to communicate details of a note across network clients.
67/// Each variant covers a specific subset of use-cases and commit to specific trade-offs.
68#[derive(Clone, Debug, PartialEq, Eq)]
69#[allow(clippy::large_enum_variant)]
70pub enum NoteFile {
71    /// The note's details aren't known, only its ID is.
72    /// A client can import all the note details from the network. As such, the note that the ID
73    /// commits to should be public.
74    NoteId(NoteId),
75    /// The note's details are known, but its metadata and attachments must be recovered from the
76    /// chain.
77    ///
78    /// This is useful for importing an expected note while avoiding an exact [`NoteId`] lookup.
79    /// Looking a note up by its exact ID would reveal to the node which note the importer is
80    /// interested in, leaking the receiver's privacy. Instead, the importer can use the sync hint
81    /// to search for matching notes by tag, then, for each returned note, recompute the note ID as
82    /// `NoteId::new(details_commitment, returned_metadata)` using this variant's details
83    /// commitment; the returned note whose recomputed ID matches is the expected one. This recovers
84    /// its metadata without revealing the note ID to the node.
85    ///
86    /// Only the note's details are carried here, as they may be private. Metadata and attachments
87    /// are always public, so they are recovered from the chain rather than carried: once the note
88    /// is found via the tag-based sync, its attachments can be fetched for the whole returned set
89    /// (e.g. via `get_notes_by_id`) without revealing which note is the expected one.
90    ExpectedNote {
91        details: NoteDetails,
92        sync_hint: NoteSyncHint,
93    },
94    /// The note has been committed to the chain and its inclusion proof is known.
95    Committed { note: Note, proof: NoteInclusionProof },
96}
97
98#[cfg(feature = "std")]
99impl NoteFile {
100    /// Serializes and writes binary [NoteFile] to specified file
101    pub fn write(&self, filepath: impl AsRef<Path>) -> io::Result<()> {
102        fs::write(filepath, self.to_bytes())
103    }
104
105    /// Reads from file and tries to deserialize an [NoteFile]
106    pub fn read(filepath: impl AsRef<Path>) -> io::Result<Self> {
107        let mut file = File::open(filepath)?;
108        let mut buffer = Vec::new();
109
110        file.read_to_end(&mut buffer)?;
111        let mut reader = SliceReader::new(&buffer);
112
113        Ok(NoteFile::read_from(&mut reader).map_err(|_| io::ErrorKind::InvalidData)?)
114    }
115}
116
117impl From<Note> for NoteFile {
118    fn from(note: Note) -> Self {
119        let (assets, metadata, recipient, _attachments) = note.into_parts();
120        NoteFile::ExpectedNote {
121            details: NoteDetails::new(assets, recipient),
122            sync_hint: NoteSyncHint::new(0.into(), metadata.tag()),
123        }
124    }
125}
126
127impl From<NoteId> for NoteFile {
128    fn from(note_id: NoteId) -> Self {
129        NoteFile::NoteId(note_id)
130    }
131}
132
133// SERIALIZATION
134// ================================================================================================
135
136impl Serializable for NoteFile {
137    fn write_into<W: ByteWriter>(&self, target: &mut W) {
138        target.write_bytes(MAGIC.as_bytes());
139        match self {
140            NoteFile::NoteId(note_id) => {
141                target.write_u8(0);
142                note_id.write_into(target);
143            },
144            NoteFile::ExpectedNote { details, sync_hint } => {
145                target.write_u8(1);
146                details.write_into(target);
147                sync_hint.write_into(target);
148            },
149            NoteFile::Committed { note, proof } => {
150                target.write_u8(2);
151                note.write_into(target);
152                proof.write_into(target);
153            },
154        }
155    }
156}
157
158impl Serializable for NoteSyncHint {
159    fn write_into<W: ByteWriter>(&self, target: &mut W) {
160        self.after_block_num.write_into(target);
161        self.tag.write_into(target);
162    }
163
164    fn get_size_hint(&self) -> usize {
165        self.after_block_num.get_size_hint() + self.tag.get_size_hint()
166    }
167}
168
169impl Deserializable for NoteFile {
170    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
171        let magic_value = source.read_string(4)?;
172        if magic_value != MAGIC {
173            return Err(DeserializationError::InvalidValue(format!(
174                "invalid note file marker: {magic_value}"
175            )));
176        }
177        match source.read_u8()? {
178            0 => Ok(NoteFile::NoteId(NoteId::read_from(source)?)),
179            1 => {
180                let details = NoteDetails::read_from(source)?;
181                let sync_hint = NoteSyncHint::read_from(source)?;
182                Ok(NoteFile::ExpectedNote { details, sync_hint })
183            },
184            2 => {
185                let note = Note::read_from(source)?;
186                let proof = NoteInclusionProof::read_from(source)?;
187                Ok(NoteFile::Committed { note, proof })
188            },
189            v => {
190                Err(DeserializationError::InvalidValue(format!("unknown variant {v} for NoteFile")))
191            },
192        }
193    }
194}
195
196impl Deserializable for NoteSyncHint {
197    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
198        let after_block_num = BlockNumber::read_from(source)?;
199        let tag = NoteTag::read_from(source)?;
200        Ok(Self { after_block_num, tag })
201    }
202}
203
204// TESTS
205// ================================================================================================
206
207#[cfg(test)]
208mod tests {
209    use alloc::vec::Vec;
210
211    use miden_protocol::Word;
212    use miden_protocol::account::AccountId;
213    use miden_protocol::asset::{Asset, FungibleAsset};
214    use miden_protocol::block::BlockNumber;
215    use miden_protocol::note::{
216        Note,
217        NoteAssets,
218        NoteDetails,
219        NoteInclusionProof,
220        NoteRecipient,
221        NoteScript,
222        NoteStorage,
223        NoteTag,
224        NoteType,
225        PartialNoteMetadata,
226    };
227    use miden_protocol::testing::account_id::{
228        ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
229        ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE,
230    };
231    use miden_protocol::utils::serde::{Deserializable, Serializable};
232
233    use super::{NoteFile, NoteSyncHint};
234
235    fn create_example_note() -> Note {
236        let faucet = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap();
237        let target =
238            AccountId::try_from(ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE).unwrap();
239
240        let serial_num = Word::from([0, 1, 2, 3u32]);
241        let script = NoteScript::mock();
242        let note_storage = NoteStorage::new(vec![target.prefix().into()]).unwrap();
243        let recipient = NoteRecipient::new(serial_num, script, note_storage);
244
245        let asset = Asset::Fungible(FungibleAsset::new(faucet, 100).unwrap());
246        let metadata =
247            PartialNoteMetadata::new(faucet, NoteType::Public).with_tag(NoteTag::from(123));
248
249        Note::new(NoteAssets::new(vec![asset]).unwrap(), metadata, recipient)
250    }
251
252    #[test]
253    fn serialized_note_magic() {
254        let note = create_example_note();
255        let file = NoteFile::NoteId(note.id());
256        let mut buffer = Vec::new();
257        file.write_into(&mut buffer);
258
259        let magic_value = &buffer[..4];
260        assert_eq!(magic_value, b"note");
261    }
262
263    /// Asserts that `file` survives a serialization round-trip unchanged.
264    fn assert_roundtrip(file: NoteFile) {
265        assert_eq!(NoteFile::read_from_bytes(&file.to_bytes()).unwrap(), file);
266    }
267
268    #[test]
269    fn serialize_id() {
270        assert_roundtrip(NoteFile::NoteId(create_example_note().id()));
271    }
272
273    #[test]
274    fn serialize_expected_note() {
275        let note = create_example_note();
276        assert_roundtrip(NoteFile::ExpectedNote {
277            details: NoteDetails::from(&note),
278            sync_hint: NoteSyncHint::new(456.into(), NoteTag::from(123)),
279        });
280    }
281
282    #[test]
283    fn serialize_committed_note() {
284        let note = create_example_note();
285        let proof = NoteInclusionProof::new(BlockNumber::from(0), 0, Default::default()).unwrap();
286        assert_roundtrip(NoteFile::Committed { note, proof });
287    }
288}