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 self.note_type().write_into(target);
102 self.sender().write_into(target);
103 self.tag().write_into(target);
104 }
105
106 fn get_size_hint(&self) -> usize {
107 self.note_type().get_size_hint()
108 + self.sender().get_size_hint()
109 + self.tag().get_size_hint()
110 }
111}
112
113impl Deserializable for PartialNoteMetadata {
114 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
115 let note_type = NoteType::read_from(source)?;
116 let sender = AccountId::read_from(source)?;
117 let tag = NoteTag::read_from(source)?;
118
119 Ok(PartialNoteMetadata::new(sender, note_type).with_tag(tag))
120 }
121}
122
123#[derive(Debug, Clone, Copy, Eq, PartialEq)]
152pub struct NoteMetadata {
153 partial_metadata: PartialNoteMetadata,
154 attachment_headers: [NoteAttachmentHeader; NoteAttachments::MAX_COUNT],
155 attachments_commitment: Word,
156}
157
158impl NoteMetadata {
159 const NOTE_TYPE_SHIFT: u64 = 4;
164
165 const VERSION_1: u8 = 1;
170
171 pub fn new(partial_metadata: PartialNoteMetadata, attachments: &NoteAttachments) -> Self {
178 Self::from_parts(partial_metadata, attachments.to_headers(), attachments.to_commitment())
179 }
180
181 pub fn from_parts(
185 partial_metadata: PartialNoteMetadata,
186 attachment_headers: [NoteAttachmentHeader; NoteAttachments::MAX_COUNT],
187 attachments_commitment: Word,
188 ) -> Self {
189 Self {
190 partial_metadata,
191 attachment_headers,
192 attachments_commitment,
193 }
194 }
195
196 pub fn partial_metadata(&self) -> &PartialNoteMetadata {
201 &self.partial_metadata
202 }
203
204 pub fn sender(&self) -> AccountId {
206 self.partial_metadata.sender()
207 }
208
209 pub fn note_type(&self) -> NoteType {
211 self.partial_metadata.note_type()
212 }
213
214 pub fn tag(&self) -> NoteTag {
216 self.partial_metadata.tag()
217 }
218
219 pub fn attachment_headers(&self) -> &[NoteAttachmentHeader; NoteAttachments::MAX_COUNT] {
221 &self.attachment_headers
222 }
223
224 pub fn attachments_commitment(&self) -> Word {
226 self.attachments_commitment
227 }
228
229 pub fn has_attachments(&self) -> bool {
234 self.attachment_headers.iter().any(|header| !header.is_absent())
235 }
236
237 pub fn is_private(&self) -> bool {
239 self.partial_metadata.is_private()
240 }
241
242 pub fn is_public(&self) -> bool {
244 self.partial_metadata.is_public()
245 }
246
247 pub fn to_metadata_word(&self) -> Word {
251 let mut word = Word::empty();
252 word[0] = merge_sender_suffix_and_note_type(
253 self.partial_metadata.sender.suffix(),
254 self.partial_metadata.note_type,
255 );
256 word[1] = self.partial_metadata.sender.prefix().as_felt();
257 word[2] = self.partial_metadata.tag.into();
258 word[3] = merge_schemes(self.attachment_headers);
259 word
260 }
261
262 pub fn to_commitment(&self) -> Word {
268 Hasher::merge(&[self.to_metadata_word(), self.attachments_commitment])
269 }
270
271 pub fn into_partial_metadata(self) -> PartialNoteMetadata {
273 self.partial_metadata
274 }
275}
276
277impl Serializable for NoteMetadata {
278 fn write_into<W: ByteWriter>(&self, target: &mut W) {
279 self.partial_metadata.write_into(target);
280
281 let present_headers_iter =
282 self.attachment_headers.iter().filter(|header| !header.is_absent());
283
284 let num_headers_present = u8::try_from(present_headers_iter.clone().count())
285 .expect("num attachments is validated to be at most 4");
286 num_headers_present.write_into(target);
287 target.write_many(present_headers_iter);
288
289 self.attachments_commitment.write_into(target);
290 }
291
292 fn get_size_hint(&self) -> usize {
293 self.partial_metadata.get_size_hint()
294 + core::mem::size_of::<u8>()
295 + self
296 .attachment_headers
297 .iter()
298 .filter(|header| !header.is_absent())
299 .map(NoteAttachmentHeader::get_size_hint)
300 .sum::<usize>()
301 + self.attachments_commitment.get_size_hint()
302 }
303}
304
305impl Deserializable for NoteMetadata {
306 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
307 let partial_metadata = PartialNoteMetadata::read_from(source)?;
308
309 let num_headers_present = u8::read_from(source)? as usize;
310 if num_headers_present > NoteAttachments::MAX_COUNT {
311 return Err(DeserializationError::InvalidValue(format!(
312 "number of attachment headers ({num_headers_present}) exceeds maximum ({})",
313 NoteAttachments::MAX_COUNT
314 )));
315 }
316
317 let mut attachment_headers = [NoteAttachmentHeader::absent(); NoteAttachments::MAX_COUNT];
318 for header in attachment_headers.iter_mut().take(num_headers_present) {
319 *header = NoteAttachmentHeader::read_from(source)?;
320 }
321
322 let attachment_commitment = Word::read_from(source)?;
323
324 Ok(Self::from_parts(partial_metadata, attachment_headers, attachment_commitment))
325 }
326}
327
328fn merge_sender_suffix_and_note_type(sender_id_suffix: Felt, note_type: NoteType) -> Felt {
344 let mut merged = sender_id_suffix.as_canonical_u64();
345
346 let note_type_byte = note_type as u8;
347 debug_assert!(note_type_byte < 2, "note type must not contain values >= 2");
348 merged |= (note_type_byte as u64) << NoteMetadata::NOTE_TYPE_SHIFT;
350 merged |= NoteMetadata::VERSION_1 as u64;
351
352 Felt::try_from(merged).expect("encoded value should be a valid felt")
355}
356
357fn merge_schemes(headers: [NoteAttachmentHeader; NoteAttachments::MAX_COUNT]) -> Felt {
368 let mut merged: u64 = headers[0].as_u16() as u64;
369 merged |= (headers[1].as_u16() as u64) << 16;
370 merged |= (headers[2].as_u16() as u64) << 32;
371 merged |= (headers[3].as_u16() as u64) << 48;
372
373 Felt::try_from(merged).expect("encoded value should be a valid felt (schemes <= 65534)")
374}
375
376#[cfg(test)]
380mod tests {
381
382 use super::*;
383 use crate::note::{NoteAttachment, NoteAttachmentScheme};
384 use crate::testing::account_id::ACCOUNT_ID_MAX_ONES;
385
386 #[test]
387 fn note_metadata_word_encodes_attachment_header() -> anyhow::Result<()> {
388 let sender = AccountId::try_from(ACCOUNT_ID_MAX_ONES).unwrap();
389 let partial_metadata =
390 PartialNoteMetadata::new(sender, NoteType::Public).with_tag(NoteTag::new(0xff));
391 let attachment0 = NoteAttachment::with_word(
392 NoteAttachmentScheme::new(1)?,
393 Word::from([10, 20, 30, 40u32]),
394 );
395 let attachment1 = NoteAttachment::with_words(
396 NoteAttachmentScheme::new(0xfffe)?,
397 vec![Word::from([10, 20, 30, 40u32]), Word::from([10, 20, 30, 40u32])],
398 )?;
399 let attachments = NoteAttachments::new(vec![attachment0, attachment1])?;
400 let metadata = NoteMetadata::new(partial_metadata, &attachments);
401
402 let encoded = metadata.to_metadata_word();
403
404 let tag = encoded[2].as_canonical_u64();
405 assert_eq!(tag, 0x0000_0000_0000_00ff);
406
407 let schemes = encoded[3].as_canonical_u64();
408 assert_eq!(schemes, 0x0000_0000_fffe_0001);
410
411 Ok(())
412 }
413
414 #[rstest::rstest]
415 #[case::attachment_none([])]
416 #[case::attachment_two_words([
417 NoteAttachment::with_word(NoteAttachmentScheme::none(), Word::from([3, 4, 5, 6u32])),
418 NoteAttachment::with_word(NoteAttachmentScheme::none(), Word::from([3, 4, 5, 6u32])),
419 ])]
420 #[case::attachment_word_and_two_arrays([
421 NoteAttachment::with_word(NoteAttachmentScheme::none(), Word::from([3, 4, 5, 6u32])),
422 NoteAttachment::with_words(
423 NoteAttachmentScheme::MAX,
424 vec![Word::from([5, 5, 5, 5u32]); 2],
425 )?,
426 NoteAttachment::with_words(
427 NoteAttachmentScheme::MAX,
428 vec![Word::from([10, 10, 10, 10u32]); NoteAttachment::MAX_NUM_WORDS as usize],
429 )?,
430 ])]
431 #[test]
432 fn note_metadata_serde(
433 #[case] attachments: impl IntoIterator<Item = NoteAttachment>,
434 ) -> anyhow::Result<()> {
435 let sender = AccountId::try_from(ACCOUNT_ID_MAX_ONES).unwrap();
438 let note_type = NoteType::Public;
439 let tag = NoteTag::new(u32::MAX);
440 let partial_metadata = PartialNoteMetadata::new(sender, note_type).with_tag(tag);
441 let attachments = NoteAttachments::new(attachments.into_iter().collect())?;
442 let metadata = NoteMetadata::new(partial_metadata, &attachments);
443
444 let deserialized = PartialNoteMetadata::read_from_bytes(&partial_metadata.to_bytes())?;
446 assert_eq!(deserialized, partial_metadata);
447
448 let roundtripped = NoteMetadata::read_from_bytes(&metadata.to_bytes())?;
450 assert_eq!(roundtripped, metadata);
451
452 Ok(())
453 }
454
455 #[test]
456 fn note_metadata_header_encodes_v1_as_one() {
457 let sender = AccountId::try_from(ACCOUNT_ID_MAX_ONES).unwrap();
458 let metadata = PartialNoteMetadata::new(sender, NoteType::Private);
459 let metadata = NoteMetadata::new(metadata, &NoteAttachments::default());
460
461 let metadata = metadata.to_metadata_word();
462 let version = metadata[0].as_canonical_u64() & 0b1111;
463
464 assert_eq!(version, NoteMetadata::VERSION_1 as u64);
465 assert_eq!(version, 1);
466 }
467}