1use super::{
2 AccountId,
3 ByteReader,
4 ByteWriter,
5 Deserializable,
6 DeserializationError,
7 Felt,
8 NoteTag,
9 NoteType,
10 Serializable,
11 Word,
12};
13use crate::Hasher;
14use crate::note::{NoteAttachmentHeader, NoteAttachments};
15
16#[derive(Debug, Clone, Copy, Eq, PartialEq)]
24pub struct PartialNoteMetadata {
25 sender: AccountId,
27
28 note_type: NoteType,
30
31 tag: NoteTag,
33}
34
35impl PartialNoteMetadata {
36 pub fn new(sender: AccountId, note_type: NoteType) -> Self {
44 Self {
45 sender,
46 note_type,
47 tag: NoteTag::default(),
48 }
49 }
50
51 pub fn sender(&self) -> AccountId {
56 self.sender
57 }
58
59 pub fn note_type(&self) -> NoteType {
61 self.note_type
62 }
63
64 pub fn tag(&self) -> NoteTag {
66 self.tag
67 }
68
69 pub fn is_private(&self) -> bool {
71 self.note_type == NoteType::Private
72 }
73
74 pub fn is_public(&self) -> bool {
76 self.note_type == NoteType::Public
77 }
78
79 pub fn set_tag(&mut self, tag: NoteTag) {
84 self.tag = tag;
85 }
86
87 pub fn with_tag(mut self, tag: NoteTag) -> Self {
91 self.tag = tag;
92 self
93 }
94}
95
96impl Serializable for PartialNoteMetadata {
100 fn write_into<W: ByteWriter>(&self, target: &mut W) {
101 NoteMetadata::VERSION_1.write_into(target);
102 self.note_type().write_into(target);
103 self.sender().write_into(target);
104 self.tag().write_into(target);
105 }
106
107 fn get_size_hint(&self) -> usize {
108 NoteMetadata::VERSION_1.get_size_hint()
109 + self.note_type().get_size_hint()
110 + self.sender().get_size_hint()
111 + self.tag().get_size_hint()
112 }
113}
114
115impl Deserializable for PartialNoteMetadata {
116 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
117 let version = u8::read_from(source)?;
118
119 if version != NoteMetadata::VERSION_1 {
120 return Err(DeserializationError::InvalidValue(format!(
121 "note version is {} but only version {} is supported",
122 version,
123 NoteMetadata::VERSION_1,
124 )));
125 }
126
127 let note_type = NoteType::read_from(source)?;
128 let sender = AccountId::read_from(source)?;
129 let tag = NoteTag::read_from(source)?;
130
131 Ok(PartialNoteMetadata::new(sender, note_type).with_tag(tag))
132 }
133}
134
135#[derive(Debug, Clone, Copy, Eq, PartialEq)]
164pub struct NoteMetadata {
165 partial_metadata: PartialNoteMetadata,
166 attachment_headers: [NoteAttachmentHeader; NoteAttachments::MAX_COUNT],
167 attachments_commitment: Word,
168}
169
170impl NoteMetadata {
171 const NOTE_TYPE_SHIFT: u64 = 6;
176
177 const VERSION_1: u8 = 1;
181
182 pub fn new(partial_metadata: PartialNoteMetadata, attachments: &NoteAttachments) -> Self {
189 Self::from_parts(partial_metadata, attachments.to_headers(), attachments.to_commitment())
190 }
191
192 pub fn from_parts(
196 partial_metadata: PartialNoteMetadata,
197 attachment_headers: [NoteAttachmentHeader; NoteAttachments::MAX_COUNT],
198 attachments_commitment: Word,
199 ) -> Self {
200 Self {
201 partial_metadata,
202 attachment_headers,
203 attachments_commitment,
204 }
205 }
206
207 pub fn partial_metadata(&self) -> &PartialNoteMetadata {
212 &self.partial_metadata
213 }
214
215 pub fn sender(&self) -> AccountId {
217 self.partial_metadata.sender()
218 }
219
220 pub fn note_type(&self) -> NoteType {
222 self.partial_metadata.note_type()
223 }
224
225 pub fn tag(&self) -> NoteTag {
227 self.partial_metadata.tag()
228 }
229
230 pub fn attachment_headers(&self) -> &[NoteAttachmentHeader; NoteAttachments::MAX_COUNT] {
232 &self.attachment_headers
233 }
234
235 pub fn attachments_commitment(&self) -> Word {
237 self.attachments_commitment
238 }
239
240 pub fn has_attachments(&self) -> bool {
245 self.attachment_headers.iter().any(|header| !header.is_absent())
246 }
247
248 pub fn is_private(&self) -> bool {
250 self.partial_metadata.is_private()
251 }
252
253 pub fn is_public(&self) -> bool {
255 self.partial_metadata.is_public()
256 }
257
258 pub fn to_metadata_word(&self) -> Word {
262 let mut word = Word::empty();
263 word[0] = merge_sender_suffix_and_note_type(
264 self.partial_metadata.sender.suffix(),
265 self.partial_metadata.note_type,
266 );
267 word[1] = self.partial_metadata.sender.prefix().as_felt();
268 word[2] = self.partial_metadata.tag.into();
269 word[3] = merge_schemes(self.attachment_headers);
270 word
271 }
272
273 pub fn to_commitment(&self) -> Word {
279 Hasher::merge(&[self.to_metadata_word(), self.attachments_commitment])
280 }
281
282 pub fn into_partial_metadata(self) -> PartialNoteMetadata {
284 self.partial_metadata
285 }
286}
287
288impl Serializable for NoteMetadata {
289 fn write_into<W: ByteWriter>(&self, target: &mut W) {
290 self.partial_metadata.write_into(target);
291
292 let present_headers_iter =
293 self.attachment_headers.iter().filter(|header| !header.is_absent());
294
295 let num_headers_present = u8::try_from(present_headers_iter.clone().count())
296 .expect("num attachments is validated to be at most 4");
297 num_headers_present.write_into(target);
298 target.write_many(present_headers_iter);
299
300 self.attachments_commitment.write_into(target);
301 }
302
303 fn get_size_hint(&self) -> usize {
304 self.partial_metadata.get_size_hint()
305 + core::mem::size_of::<u8>()
306 + self
307 .attachment_headers
308 .iter()
309 .filter(|header| !header.is_absent())
310 .map(NoteAttachmentHeader::get_size_hint)
311 .sum::<usize>()
312 + self.attachments_commitment.get_size_hint()
313 }
314}
315
316impl Deserializable for NoteMetadata {
317 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
318 let partial_metadata = PartialNoteMetadata::read_from(source)?;
319
320 let num_headers_present = u8::read_from(source)? as usize;
321 if num_headers_present > NoteAttachments::MAX_COUNT {
322 return Err(DeserializationError::InvalidValue(format!(
323 "number of attachment headers ({num_headers_present}) exceeds maximum ({})",
324 NoteAttachments::MAX_COUNT
325 )));
326 }
327
328 let mut attachment_headers = [NoteAttachmentHeader::absent(); NoteAttachments::MAX_COUNT];
329 for header in attachment_headers.iter_mut().take(num_headers_present) {
330 *header = NoteAttachmentHeader::read_from(source)?;
331 }
332
333 let attachment_commitment = Word::read_from(source)?;
334
335 Ok(Self::from_parts(partial_metadata, attachment_headers, attachment_commitment))
336 }
337}
338
339fn merge_sender_suffix_and_note_type(sender_id_suffix: Felt, note_type: NoteType) -> Felt {
355 let mut merged = sender_id_suffix.as_canonical_u64();
356
357 let note_type_byte = note_type as u8;
358 debug_assert!(note_type_byte < 2, "note type must not contain values >= 2");
359 merged |= (note_type_byte as u64) << NoteMetadata::NOTE_TYPE_SHIFT;
361 merged |= NoteMetadata::VERSION_1 as u64;
362
363 Felt::try_from(merged).expect("encoded value should be a valid felt")
366}
367
368fn merge_schemes(headers: [NoteAttachmentHeader; NoteAttachments::MAX_COUNT]) -> Felt {
379 let mut merged: u64 = headers[0].as_u16() as u64;
380 merged |= (headers[1].as_u16() as u64) << 16;
381 merged |= (headers[2].as_u16() as u64) << 32;
382 merged |= (headers[3].as_u16() as u64) << 48;
383
384 Felt::try_from(merged).expect("encoded value should be a valid felt (schemes <= 65534)")
385}
386
387#[cfg(test)]
391mod tests {
392
393 use assert_matches::assert_matches;
394
395 use super::*;
396 use crate::note::{NoteAttachment, NoteAttachmentScheme};
397 use crate::testing::account_id::ACCOUNT_ID_MAX_ONES;
398
399 #[test]
400 fn note_metadata_word_encodes_attachment_header() -> anyhow::Result<()> {
401 let sender = AccountId::try_from(ACCOUNT_ID_MAX_ONES).unwrap();
402 let partial_metadata =
403 PartialNoteMetadata::new(sender, NoteType::Public).with_tag(NoteTag::new(0xff));
404 let attachment0 = NoteAttachment::with_word(
405 NoteAttachmentScheme::new(1)?,
406 Word::from([10, 20, 30, 40u32]),
407 );
408 let attachment1 = NoteAttachment::with_words(
409 NoteAttachmentScheme::new(0xfffe)?,
410 vec![Word::from([10, 20, 30, 40u32]), Word::from([10, 20, 30, 40u32])],
411 )?;
412 let attachments = NoteAttachments::new(vec![attachment0, attachment1])?;
413 let metadata = NoteMetadata::new(partial_metadata, &attachments);
414
415 let encoded = metadata.to_metadata_word();
416
417 let tag = encoded[2].as_canonical_u64();
418 assert_eq!(tag, 0x0000_0000_0000_00ff);
419
420 let schemes = encoded[3].as_canonical_u64();
421 assert_eq!(schemes, 0x0000_0000_fffe_0001);
423
424 Ok(())
425 }
426
427 #[rstest::rstest]
428 #[case::attachment_none([])]
429 #[case::attachment_two_words([
430 NoteAttachment::with_word(NoteAttachmentScheme::none(), Word::from([3, 4, 5, 6u32])),
431 NoteAttachment::with_word(NoteAttachmentScheme::none(), Word::from([3, 4, 5, 6u32])),
432 ])]
433 #[case::attachment_word_and_two_arrays([
434 NoteAttachment::with_word(NoteAttachmentScheme::none(), Word::from([3, 4, 5, 6u32])),
435 NoteAttachment::with_words(
436 NoteAttachmentScheme::MAX,
437 vec![Word::from([5, 5, 5, 5u32]); 2],
438 )?,
439 NoteAttachment::with_words(
440 NoteAttachmentScheme::MAX,
441 vec![Word::from([10, 10, 10, 10u32]); NoteAttachment::MAX_NUM_WORDS as usize],
442 )?,
443 ])]
444 #[test]
445 fn note_metadata_serde(
446 #[case] attachments: impl IntoIterator<Item = NoteAttachment>,
447 ) -> anyhow::Result<()> {
448 let sender = AccountId::try_from(ACCOUNT_ID_MAX_ONES).unwrap();
451 let note_type = NoteType::Public;
452 let tag = NoteTag::new(u32::MAX);
453 let partial_metadata = PartialNoteMetadata::new(sender, note_type).with_tag(tag);
454 let attachments = NoteAttachments::new(attachments.into_iter().collect())?;
455 let metadata = NoteMetadata::new(partial_metadata, &attachments);
456
457 let deserialized = PartialNoteMetadata::read_from_bytes(&partial_metadata.to_bytes())?;
459 assert_eq!(deserialized, partial_metadata);
460
461 let roundtripped = NoteMetadata::read_from_bytes(&metadata.to_bytes())?;
463 assert_eq!(roundtripped, metadata);
464
465 Ok(())
466 }
467
468 #[rstest::rstest]
470 #[case::private(NoteType::Private, 0b0000_0001)]
471 #[case::public(NoteType::Public, 0b0100_0001)]
472 #[test]
473 fn note_metadata_first_felt_layout(
474 #[case] note_type: NoteType,
475 #[case] expected_low_byte: u64,
476 ) -> anyhow::Result<()> {
477 const SUFFIX_MASK: u64 = !0xff;
478
479 let sender = AccountId::try_from(ACCOUNT_ID_MAX_ONES)?;
482 let partial_metadata = PartialNoteMetadata::new(sender, note_type);
483 let metadata = NoteMetadata::new(partial_metadata, &NoteAttachments::default());
484
485 let first_felt = metadata.to_metadata_word()[0].as_canonical_u64();
486
487 assert_eq!(first_felt & !SUFFIX_MASK, expected_low_byte);
488 assert_eq!(first_felt & SUFFIX_MASK, sender.suffix().as_canonical_u64());
489 assert_eq!(u64::from(NoteMetadata::VERSION_1), 1);
490
491 Ok(())
492 }
493
494 #[test]
495 fn partial_note_metadata_deserialization_rejects_unsupported_version() {
496 let error = PartialNoteMetadata::read_from_bytes(&[0]).unwrap_err();
497
498 assert_matches!(error, DeserializationError::InvalidValue(message) => {
499 assert!(message.contains("note version is 0"));
500 });
501 }
502}