Skip to main content

miden_node_proto/domain/
note.rs

1use std::sync::Arc;
2
3use miden_protocol::crypto::merkle::SparseMerklePath;
4use miden_protocol::note::{
5    Note,
6    NoteAttachmentHeader,
7    NoteAttachmentScheme,
8    NoteAttachments,
9    NoteDetails,
10    NoteDetailsCommitment,
11    NoteHeader,
12    NoteId,
13    NoteInclusionProof,
14    NoteMetadata,
15    NoteScript,
16    NoteTag,
17    NoteType,
18    PartialNoteMetadata,
19};
20use miden_protocol::utils::serde::Serializable;
21use miden_protocol::{MastForest, MastNodeId, Word};
22use miden_standards::note::AccountTargetNetworkNote;
23
24use crate::decode::{ConversionResultExt, DecodeBytesExt, GrpcDecodeExt};
25use crate::errors::ConversionError;
26use crate::{decode, generated as proto};
27
28// NOTE TYPE
29// ================================================================================================
30
31impl From<NoteType> for proto::note::NoteType {
32    fn from(note_type: NoteType) -> Self {
33        match note_type {
34            NoteType::Public => proto::note::NoteType::Public,
35            NoteType::Private => proto::note::NoteType::Private,
36        }
37    }
38}
39
40impl TryFrom<proto::note::NoteType> for NoteType {
41    type Error = ConversionError;
42
43    fn try_from(note_type: proto::note::NoteType) -> Result<Self, Self::Error> {
44        match note_type {
45            proto::note::NoteType::Public => Ok(NoteType::Public),
46            proto::note::NoteType::Private => Ok(NoteType::Private),
47            proto::note::NoteType::Unspecified => {
48                Err(ConversionError::message("enum variant discriminant out of range"))
49            },
50        }
51    }
52}
53
54// NOTE METADATA
55// ================================================================================================
56
57impl From<NoteMetadata> for proto::note::NoteMetadata {
58    fn from(val: NoteMetadata) -> Self {
59        let sender = Some(val.sender().into());
60        let note_type = proto::note::NoteType::from(val.note_type()) as i32;
61        let tag = val.tag().as_u32();
62        let attachment_schemes = val
63            .attachment_headers()
64            .iter()
65            .map(|header| u32::from(header.scheme().map_or(0, |s| s.as_u16())))
66            .collect();
67        let attachments_commitment = Some(val.attachments_commitment().into());
68
69        proto::note::NoteMetadata {
70            sender,
71            note_type,
72            tag,
73            attachment_schemes,
74            attachments_commitment,
75        }
76    }
77}
78
79impl TryFrom<proto::note::NoteMetadata> for NoteMetadata {
80    type Error = ConversionError;
81
82    fn try_from(value: proto::note::NoteMetadata) -> Result<Self, Self::Error> {
83        let decoder = value.decoder();
84        let sender = decode!(decoder, value.sender)?;
85        let note_type = proto::note::NoteType::try_from(value.note_type)
86            .map_err(|_| ConversionError::message("enum variant discriminant out of range"))?
87            .try_into()
88            .context("note_type")?;
89        let tag = NoteTag::new(value.tag);
90        let attachments_commitment: Word = decode!(decoder, value.attachments_commitment)?;
91
92        if value.attachment_schemes.len() > NoteAttachments::MAX_COUNT {
93            return Err(ConversionError::message("too many attachment schemes"));
94        }
95        let mut attachment_headers = [NoteAttachmentHeader::absent(); NoteAttachments::MAX_COUNT];
96        for (slot, raw) in attachment_headers.iter_mut().zip(value.attachment_schemes) {
97            let raw = u16::try_from(raw)
98                .map_err(|_| ConversionError::message("attachment scheme out of u16 range"))?;
99            *slot = if raw == 0 {
100                NoteAttachmentHeader::absent()
101            } else {
102                NoteAttachmentHeader::new(NoteAttachmentScheme::new(raw)?)
103            };
104        }
105
106        let partial = PartialNoteMetadata::new(sender, note_type).with_tag(tag);
107        Ok(NoteMetadata::from_parts(partial, attachment_headers, attachments_commitment))
108    }
109}
110
111// NOTE
112// ================================================================================================
113
114impl From<Note> for proto::note::NetworkNote {
115    fn from(note: Note) -> Self {
116        let metadata = Some(proto::note::NoteMetadata::from(*note.metadata()));
117        let attachments = note.attachments().to_bytes();
118        let details = NoteDetails::from(note).to_bytes();
119        Self { metadata, details, attachments }
120    }
121}
122
123impl From<Note> for proto::note::Note {
124    fn from(note: Note) -> Self {
125        let metadata = Some(proto::note::NoteMetadata::from(*note.metadata()));
126        let attachments = note.attachments().to_bytes();
127        let details = Some(NoteDetails::from(note).to_bytes());
128        Self { metadata, details, attachments }
129    }
130}
131
132impl From<AccountTargetNetworkNote> for proto::note::NetworkNote {
133    fn from(note: AccountTargetNetworkNote) -> Self {
134        note.into_note().into()
135    }
136}
137
138impl TryFrom<proto::note::NetworkNote> for AccountTargetNetworkNote {
139    type Error = ConversionError;
140
141    fn try_from(value: proto::note::NetworkNote) -> Result<Self, Self::Error> {
142        let proto::note::NetworkNote { metadata, details, attachments } = value;
143
144        let metadata = metadata
145            .ok_or(ConversionError::missing_field::<proto::note::NetworkNote>("metadata"))?;
146        let partial_metadata = partial_note_metadata_from_proto(metadata)?;
147
148        let note_details = NoteDetails::decode_bytes(&details, "NoteDetails")?;
149        let (assets, recipient) = note_details.into_parts();
150        let attachments = decode_attachments(&attachments)?;
151
152        let note = Note::with_attachments(assets, partial_metadata, recipient, attachments);
153        AccountTargetNetworkNote::new(note).map_err(ConversionError::from)
154    }
155}
156
157impl TryFrom<proto::note::Note> for Note {
158    type Error = ConversionError;
159
160    fn try_from(proto_note: proto::note::Note) -> Result<Self, Self::Error> {
161        let proto::note::Note { metadata, details, attachments } = proto_note;
162
163        let metadata =
164            metadata.ok_or(ConversionError::missing_field::<proto::note::Note>("metadata"))?;
165        let partial_metadata = partial_note_metadata_from_proto(metadata)?;
166
167        let details =
168            details.ok_or(ConversionError::missing_field::<proto::note::Note>("details"))?;
169        let note_details = NoteDetails::decode_bytes(&details, "NoteDetails")?;
170        let (assets, recipient) = note_details.into_parts();
171        let attachments = decode_attachments(&attachments)?;
172
173        Ok(Note::with_attachments(assets, partial_metadata, recipient, attachments))
174    }
175}
176
177// NOTE ID
178// ================================================================================================
179
180impl From<Word> for proto::note::NoteId {
181    fn from(digest: Word) -> Self {
182        Self { id: Some(digest.into()) }
183    }
184}
185
186impl TryFrom<proto::note::NoteId> for Word {
187    type Error = ConversionError;
188
189    fn try_from(note_id: proto::note::NoteId) -> Result<Self, Self::Error> {
190        note_id
191            .id
192            .as_ref()
193            .ok_or(ConversionError::missing_field::<proto::note::NoteId>("id"))?
194            .try_into()
195    }
196}
197
198impl From<&NoteId> for proto::note::NoteId {
199    fn from(note_id: &NoteId) -> Self {
200        Self { id: Some(note_id.into()) }
201    }
202}
203
204impl From<(&NoteId, &NoteInclusionProof)> for proto::note::NoteInclusionInBlockProof {
205    fn from((note_id, proof): (&NoteId, &NoteInclusionProof)) -> Self {
206        Self {
207            note_id: Some(note_id.into()),
208            block_num: proof.location().block_num().as_u32(),
209            note_index_in_block: proof.location().block_note_tree_index().into(),
210            inclusion_path: Some(proof.note_path().clone().into()),
211        }
212    }
213}
214
215impl TryFrom<&proto::note::NoteInclusionInBlockProof> for (NoteId, NoteInclusionProof) {
216    type Error = ConversionError;
217
218    fn try_from(
219        proof: &proto::note::NoteInclusionInBlockProof,
220    ) -> Result<(NoteId, NoteInclusionProof), Self::Error> {
221        let inclusion_path = SparseMerklePath::try_from(
222            proof
223                .inclusion_path
224                .as_ref()
225                .ok_or(ConversionError::missing_field::<proto::note::NoteInclusionInBlockProof>(
226                    "inclusion_path",
227                ))?
228                .clone(),
229        )
230        .context("inclusion_path")?;
231
232        let note_id = Word::try_from(
233            proof
234                .note_id
235                .as_ref()
236                .ok_or(ConversionError::missing_field::<proto::note::NoteInclusionInBlockProof>(
237                    "note_id",
238                ))?
239                .id
240                .as_ref()
241                .ok_or(ConversionError::missing_field::<proto::note::NoteId>("id"))?,
242        )
243        .context("note_id")?;
244
245        Ok((
246            NoteId::from_raw(note_id),
247            NoteInclusionProof::new(
248                proof.block_num.into(),
249                proof.note_index_in_block.try_into().context("note_index_in_block")?,
250                inclusion_path,
251            )?,
252        ))
253    }
254}
255
256// NOTE HEADER
257// ================================================================================================
258
259impl From<NoteHeader> for proto::note::NoteHeader {
260    fn from(header: NoteHeader) -> Self {
261        Self {
262            details_commitment: Some(header.details_commitment().as_word().into()),
263            metadata: Some(header.into_metadata().into()),
264        }
265    }
266}
267
268impl TryFrom<proto::note::NoteHeader> for NoteHeader {
269    type Error = ConversionError;
270
271    fn try_from(value: proto::note::NoteHeader) -> Result<Self, Self::Error> {
272        let decoder = value.decoder();
273        let details_commitment_word: Word = decode!(decoder, value.details_commitment)?;
274        let metadata: NoteMetadata = decode!(decoder, value.metadata)?;
275
276        Ok(NoteHeader::new(
277            NoteDetailsCommitment::from_raw(details_commitment_word),
278            metadata,
279        ))
280    }
281}
282
283// NOTE SCRIPT
284// ================================================================================================
285
286impl From<NoteScript> for proto::note::NoteScript {
287    fn from(script: NoteScript) -> Self {
288        Self {
289            entrypoint: script.entrypoint().into(),
290            mast: script.mast().to_bytes(),
291        }
292    }
293}
294
295impl TryFrom<proto::note::NoteScript> for NoteScript {
296    type Error = ConversionError;
297
298    fn try_from(value: proto::note::NoteScript) -> Result<Self, Self::Error> {
299        let proto::note::NoteScript { entrypoint, mast } = value;
300
301        let mast = MastForest::decode_bytes(&mast, "note_script.mast")?;
302        let entrypoint = MastNodeId::from_u32_safe(entrypoint, &mast)
303            .map_err(|err| ConversionError::deserialization("note_script.entrypoint", err))?;
304
305        Ok(Self::from_parts(Arc::new(mast), entrypoint))
306    }
307}
308
309// HELPERS
310// ================================================================================================
311
312/// Decodes the `(sender, note_type, tag)` triple from a proto `NoteMetadata` into a
313/// [`PartialNoteMetadata`]. The attachment-related fields on the proto are ignored — when full
314/// attachments are also transmitted, the receiver derives the canonical headers and commitment from
315/// those instead.
316fn partial_note_metadata_from_proto(
317    value: proto::note::NoteMetadata,
318) -> Result<PartialNoteMetadata, ConversionError> {
319    let decoder = value.decoder();
320    let sender = decode!(decoder, value.sender)?;
321    let note_type = proto::note::NoteType::try_from(value.note_type)
322        .map_err(|_| ConversionError::message("enum variant discriminant out of range"))?
323        .try_into()
324        .context("note_type")?;
325    let tag = NoteTag::new(value.tag);
326    Ok(PartialNoteMetadata::new(sender, note_type).with_tag(tag))
327}
328
329/// Decodes a serialized [`NoteAttachments`] payload. Empty bytes are treated as an empty collection
330/// so that proto3's default value round-trips cleanly.
331fn decode_attachments(bytes: &[u8]) -> Result<NoteAttachments, ConversionError> {
332    if bytes.is_empty() {
333        Ok(NoteAttachments::empty())
334    } else {
335        NoteAttachments::decode_bytes(bytes, "NoteAttachments")
336    }
337}
338
339#[cfg(test)]
340mod tests {
341    use miden_protocol::account::{AccountId, AccountIdVersion, AccountType};
342
343    use super::*;
344
345    #[test]
346    fn note_header_roundtrip_preserves_id() {
347        // Build a NoteHeader with a known details_commitment and metadata.
348        let details_commitment =
349            NoteDetailsCommitment::from_raw(Word::try_from([1u64, 2, 3, 4]).unwrap());
350        let sender = AccountId::dummy([1; 15], AccountIdVersion::Version1, AccountType::Public);
351        let metadata = NoteMetadata::new(
352            PartialNoteMetadata::new(sender, NoteType::Public).with_tag(NoteTag::from(7u32)),
353            &NoteAttachments::default(),
354        );
355
356        let original = NoteHeader::new(details_commitment, metadata);
357
358        // Round-trip through proto.
359        let proto_header: proto::note::NoteHeader = original.into();
360        let decoded = NoteHeader::try_from(proto_header).expect("proto NoteHeader should decode");
361
362        // Both the derived id and the details_commitment must match — guards against the historical
363        // bug where the encoder wrote `id` into the same wire field the decoder interpreted as
364        // `details_commitment`.
365        assert_eq!(decoded.id(), original.id());
366        assert_eq!(decoded.details_commitment(), original.details_commitment());
367        assert_eq!(decoded.metadata(), original.metadata());
368    }
369}