1use alloc::collections::BTreeSet;
2use alloc::string::ToString;
3use alloc::vec::Vec;
4
5use miden_core::Word;
6
7use crate::block::{
8 BlockAccountUpdate,
9 BlockNoteIndex,
10 BlockNoteTree,
11 OutputNoteBatch,
12 ProposedBlock,
13};
14use crate::errors::BlockBodyError;
15use crate::note::Nullifier;
16use crate::transaction::{OrderedTransactionHeaders, OutputNote};
17use crate::utils::serde::{
18 ByteReader,
19 ByteWriter,
20 Deserializable,
21 DeserializationError,
22 Serializable,
23};
24use crate::{
25 MAX_ACCOUNTS_PER_BLOCK,
26 MAX_BATCHES_PER_BLOCK,
27 MAX_INPUT_NOTES_PER_BLOCK,
28 MAX_OUTPUT_NOTES_PER_BATCH,
29};
30
31#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct BlockBody {
37 updated_accounts: Vec<BlockAccountUpdate>,
39
40 output_note_batches: Vec<OutputNoteBatch>,
42
43 created_nullifiers: Vec<Nullifier>,
45
46 transactions: OrderedTransactionHeaders,
49}
50
51impl BlockBody {
52 pub fn new(
69 updated_accounts: Vec<BlockAccountUpdate>,
70 output_note_batches: Vec<OutputNoteBatch>,
71 created_nullifiers: Vec<Nullifier>,
72 transactions: OrderedTransactionHeaders,
73 ) -> Result<Self, BlockBodyError> {
74 if output_note_batches.len() > MAX_BATCHES_PER_BLOCK {
75 return Err(BlockBodyError::TooManyOutputNoteBatches(output_note_batches.len()));
76 }
77 if updated_accounts.len() > MAX_ACCOUNTS_PER_BLOCK {
78 return Err(BlockBodyError::TooManyAccountUpdates(updated_accounts.len()));
79 }
80 if created_nullifiers.len() > MAX_INPUT_NOTES_PER_BLOCK {
81 return Err(BlockBodyError::TooManyNullifiers(created_nullifiers.len()));
82 }
83
84 let new_account_ids: BTreeSet<_> = transactions.created_account_ids().collect();
85
86 let mut account_ids = BTreeSet::new();
87 for update in &updated_accounts {
88 if !account_ids.insert(update.account_id()) {
89 return Err(BlockBodyError::DuplicateAccountUpdate(update.account_id()));
90 }
91 if new_account_ids.contains(&update.account_id()) {
92 update.validate_new_account_patch().map_err(|source| {
93 BlockBodyError::InvalidNewAccountUpdate {
94 account_id: update.account_id(),
95 source,
96 }
97 })?;
98 }
99 }
100
101 let mut output_note_ids = BTreeSet::new();
102 for (batch_index, batch) in output_note_batches.iter().enumerate() {
103 if batch.len() > MAX_OUTPUT_NOTES_PER_BATCH {
104 return Err(BlockBodyError::TooManyOutputNotes {
105 batch_index,
106 note_count: batch.len(),
107 });
108 }
109 let mut note_indices = BTreeSet::new();
110 for (note_index, note) in batch {
111 if BlockNoteIndex::new(batch_index, *note_index).is_none() {
112 return Err(BlockBodyError::InvalidOutputNoteIndex {
113 batch_index,
114 note_index: *note_index,
115 });
116 }
117 if !note_indices.insert(*note_index) {
118 return Err(BlockBodyError::DuplicateOutputNoteIndex {
119 batch_index,
120 note_index: *note_index,
121 });
122 }
123 if !output_note_ids.insert(note.id()) {
124 return Err(BlockBodyError::DuplicateOutputNote(note.id()));
125 }
126 }
127 }
128
129 let mut nullifiers = BTreeSet::new();
130 for nullifier in &created_nullifiers {
131 if !nullifiers.insert(*nullifier) {
132 return Err(BlockBodyError::DuplicateNullifier(*nullifier));
133 }
134 }
135
136 let mut transaction_ids = BTreeSet::new();
137 for transaction in transactions.as_slice() {
138 if !transaction_ids.insert(transaction.id()) {
139 return Err(BlockBodyError::DuplicateTransaction(transaction.id()));
140 }
141 }
142
143 Ok(Self::new_unchecked(
144 updated_accounts,
145 output_note_batches,
146 created_nullifiers,
147 transactions,
148 ))
149 }
150
151 pub fn new_unchecked(
158 updated_accounts: Vec<BlockAccountUpdate>,
159 output_note_batches: Vec<OutputNoteBatch>,
160 created_nullifiers: Vec<Nullifier>,
161 transactions: OrderedTransactionHeaders,
162 ) -> Self {
163 Self {
164 updated_accounts,
165 output_note_batches,
166 created_nullifiers,
167 transactions,
168 }
169 }
170
171 pub fn updated_accounts(&self) -> &[BlockAccountUpdate] {
176 &self.updated_accounts
177 }
178
179 pub fn output_note_batches(&self) -> &[OutputNoteBatch] {
181 &self.output_note_batches
182 }
183
184 pub fn created_nullifiers(&self) -> &[Nullifier] {
186 &self.created_nullifiers
187 }
188
189 pub fn transactions(&self) -> &OrderedTransactionHeaders {
191 &self.transactions
192 }
193
194 pub fn transaction_commitment(&self) -> Word {
196 self.transactions.commitment()
197 }
198
199 pub fn output_notes(&self) -> impl Iterator<Item = (BlockNoteIndex, &OutputNote)> {
204 self.output_note_batches.iter().enumerate().flat_map(|(batch_idx, notes)| {
205 notes.iter().map(move |(note_idx_in_batch, note)| {
206 (
207 BlockNoteIndex::new(batch_idx, *note_idx_in_batch)
211 .expect("max batches in block and max notes in batches should be enforced"),
212 note,
213 )
214 })
215 })
216 }
217
218 pub fn compute_block_note_tree(&self) -> BlockNoteTree {
220 let entries = self.output_notes().map(|(note_index, note)| (note_index, note.into()));
221
222 BlockNoteTree::with_entries(entries)
227 .expect("the output notes of the block should not contain duplicates and contain at most the allowed maximum")
228 }
229
230 pub fn into_parts(
235 self,
236 ) -> (
237 Vec<BlockAccountUpdate>,
238 Vec<OutputNoteBatch>,
239 Vec<Nullifier>,
240 OrderedTransactionHeaders,
241 ) {
242 (
243 self.updated_accounts,
244 self.output_note_batches,
245 self.created_nullifiers,
246 self.transactions,
247 )
248 }
249}
250
251impl From<ProposedBlock> for BlockBody {
252 fn from(block: ProposedBlock) -> Self {
253 let (batches, account_updated_witnesses, output_note_batches, created_nullifiers, ..) =
255 block.into_parts();
256
257 let updated_accounts = account_updated_witnesses
259 .into_iter()
260 .map(|(account_id, update_witness)| {
261 let (
262 _initial_state_commitment,
263 final_state_commitment,
264 _initial_state_proof,
266 details,
267 ) = update_witness.into_parts();
268 BlockAccountUpdate::new_unchecked(account_id, final_state_commitment, details)
271 })
272 .collect();
273 let created_nullifiers = created_nullifiers.keys().copied().collect::<Vec<_>>();
274 let transactions = batches.into_transactions();
276 Self {
277 updated_accounts,
278 output_note_batches,
279 created_nullifiers,
280 transactions,
281 }
282 }
283}
284
285impl Serializable for BlockBody {
289 fn write_into<W: ByteWriter>(&self, target: &mut W) {
290 self.updated_accounts.write_into(target);
291 self.output_note_batches.write_into(target);
292 self.created_nullifiers.write_into(target);
293 self.transactions.write_into(target);
294 }
295}
296
297impl Deserializable for BlockBody {
298 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
299 Self::new(
300 Vec::read_from(source)?,
301 Vec::read_from(source)?,
302 Vec::read_from(source)?,
303 OrderedTransactionHeaders::read_from(source)?,
304 )
305 .map_err(|error| DeserializationError::InvalidValue(error.to_string()))
306 }
307}
308
309#[cfg(test)]
313mod tests {
314 use alloc::vec::Vec;
315
316 use assert_matches::assert_matches;
317 use rstest::rstest;
318
319 use super::BlockBody;
320 use crate::Word;
321 use crate::account::{Account, AccountId, AccountPatch, AccountType, AccountUpdateDetails};
322 use crate::block::BlockAccountUpdate;
323 use crate::errors::{BlockAccountUpdateError, BlockBodyError, TransactionHeaderError};
324 use crate::note::{Note, NoteHeader};
325 use crate::testing::account_id::ACCOUNT_ID_PRIVATE_SENDER;
326 use crate::testing::add_component::AddComponent;
327 use crate::testing::noop_auth_component::NoopAuthComponent;
328 use crate::transaction::{
329 InputNoteCommitment,
330 InputNotes,
331 OrderedTransactionHeaders,
332 OutputNote,
333 RawOutputNote,
334 TransactionHeader,
335 };
336 use crate::utils::serde::{Deserializable, Serializable};
337
338 fn account_id() -> AccountId {
339 AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap()
340 }
341
342 fn transaction_header(
343 initial_state_commitment: Word,
344 final_state_commitment: Word,
345 input_notes: InputNotes<InputNoteCommitment>,
346 output_notes: Vec<NoteHeader>,
347 ) -> Result<TransactionHeader, TransactionHeaderError> {
348 TransactionHeader::new(
349 account_id(),
350 initial_state_commitment,
351 final_state_commitment,
352 input_notes,
353 output_notes,
354 )
355 }
356
357 fn into_output_note(note: Note) -> OutputNote {
358 RawOutputNote::Full(note).into_output_note().unwrap()
359 }
360
361 fn public_account() -> anyhow::Result<Account> {
362 Ok(Account::builder([9; 32])
363 .account_type(AccountType::Public)
364 .with_component(NoopAuthComponent)
365 .with_component(AddComponent)
366 .build_existing()?)
367 }
368
369 fn public_account_body_parts(
372 account: &Account,
373 initial_state_commitment: Word,
374 final_state_commitment: Word,
375 ) -> anyhow::Result<(Vec<BlockAccountUpdate>, OrderedTransactionHeaders)> {
376 let update = BlockAccountUpdate::new(
377 account.id(),
378 final_state_commitment,
379 AccountUpdateDetails::Public(AccountPatch::try_from(account.clone())?),
380 )?;
381 let transactions = OrderedTransactionHeaders::new_unchecked(vec![TransactionHeader::new(
382 account.id(),
383 initial_state_commitment,
384 final_state_commitment,
385 InputNotes::default(),
386 vec![],
387 )?]);
388
389 Ok((vec![update], transactions))
390 }
391
392 #[rstest]
393 #[case::new_account_matching_commitment(true, true)]
394 #[case::existing_account_matching_commitment(false, true)]
395 #[case::existing_account_mismatching_commitment(false, false)]
396 fn accepts_public_account_update(
397 #[case] is_new_account: bool,
398 #[case] is_final_commitment_matching: bool,
399 ) -> anyhow::Result<()> {
400 let initial_state_commitment = if is_new_account {
401 Word::empty()
402 } else {
403 Word::from([1_u32, 2, 3, 4])
404 };
405 let account = public_account()?;
406 let final_state_commitment = if is_final_commitment_matching {
407 account.to_commitment()
408 } else {
409 Word::from([5_u32, 6, 7, 8])
410 };
411 let (updated_accounts, transactions) =
412 public_account_body_parts(&account, initial_state_commitment, final_state_commitment)?;
413
414 BlockBody::new(updated_accounts, vec![], vec![], transactions)?;
415
416 Ok(())
417 }
418
419 #[test]
420 fn rejects_new_public_account_final_commitment_mismatch() -> anyhow::Result<()> {
421 let account = public_account()?;
422 let final_state_commitment = Word::from([5_u32, 6, 7, 8]);
423 let (updated_accounts, transactions) =
424 public_account_body_parts(&account, Word::empty(), final_state_commitment)?;
425 let account_commitment = account.to_commitment();
426
427 let result = BlockBody::new(updated_accounts, vec![], vec![], transactions);
428
429 assert_matches!(
430 result,
431 Err(BlockBodyError::InvalidNewAccountUpdate {
432 account_id,
433 source: BlockAccountUpdateError::AccountFinalCommitmentMismatch {
434 final_state_commitment: actual_final_state_commitment,
435 account_commitment: actual_account_commitment,
436 },
437 }) if account_id == account.id()
438 && actual_final_state_commitment == final_state_commitment
439 && actual_account_commitment == account_commitment
440 );
441
442 Ok(())
443 }
444
445 #[rstest]
446 #[case::missing_from_body(true)]
447 #[case::unexpected_in_body(false)]
448 fn accepts_created_nullifiers_mismatch(#[case] transaction_has_input: bool) {
449 let note = Note::mock_noop(Word::from([1_u32, 2, 3, 4]));
450 let input =
451 InputNoteCommitment::from_parts_unchecked(note.nullifier(), Some(*note.header()));
452 let input_notes = if transaction_has_input {
453 InputNotes::new(vec![input]).unwrap()
454 } else {
455 InputNotes::default()
456 };
457 let created_nullifiers = if transaction_has_input {
458 vec![]
459 } else {
460 vec![note.nullifier()]
461 };
462 let transactions = OrderedTransactionHeaders::new_unchecked(vec![
463 transaction_header(
464 Word::from([1_u32, 2, 3, 4]),
465 Word::from([5_u32, 6, 7, 8]),
466 input_notes,
467 vec![],
468 )
469 .unwrap(),
470 ]);
471
472 BlockBody::new(vec![], vec![], created_nullifiers, transactions).unwrap();
473 }
474
475 #[rstest]
476 #[case::missing_from_body(true)]
477 #[case::unexpected_in_body(false)]
478 fn accepts_output_notes_mismatch(#[case] transaction_has_output: bool) {
479 let note = Note::mock_noop(Word::from([1_u32, 2, 3, 4]));
480 let output_notes = if transaction_has_output {
481 vec![]
482 } else {
483 vec![vec![(0, into_output_note(note.clone()))]]
484 };
485 let transaction_output_notes = if transaction_has_output {
486 vec![*note.header()]
487 } else {
488 vec![]
489 };
490 let transactions = OrderedTransactionHeaders::new_unchecked(vec![
491 transaction_header(
492 Word::from([1_u32, 2, 3, 4]),
493 Word::from([5_u32, 6, 7, 8]),
494 InputNotes::default(),
495 transaction_output_notes,
496 )
497 .unwrap(),
498 ]);
499
500 BlockBody::new(vec![], output_notes, vec![], transactions).unwrap();
501 }
502
503 #[test]
504 fn accepts_matching_transaction_notes() {
505 let input_note = Note::mock_noop(Word::from([1_u32, 2, 3, 4]));
506 let output_note = Note::mock_noop(Word::from([5_u32, 6, 7, 8]));
507 let input = InputNoteCommitment::from_parts_unchecked(
508 input_note.nullifier(),
509 Some(*input_note.header()),
510 );
511 let transactions = OrderedTransactionHeaders::new_unchecked(vec![
512 transaction_header(
513 Word::from([1_u32, 2, 3, 4]),
514 Word::from([5_u32, 6, 7, 8]),
515 InputNotes::new(vec![input]).unwrap(),
516 vec![*output_note.header()],
517 )
518 .unwrap(),
519 ]);
520
521 BlockBody::new(
522 vec![],
523 vec![vec![(0, into_output_note(output_note))]],
524 vec![input_note.nullifier()],
525 transactions,
526 )
527 .unwrap();
528 }
529
530 #[test]
531 fn rejects_duplicate_supplied_nullifier() {
532 let note = Note::mock_noop(Word::from([1_u32, 2, 3, 4]));
533 let nullifier = note.nullifier();
534 let transactions = OrderedTransactionHeaders::new_unchecked(vec![]);
535
536 let result = BlockBody::new(vec![], vec![], vec![nullifier, nullifier], transactions);
537
538 assert_matches!(
539 result,
540 Err(BlockBodyError::DuplicateNullifier(nullifier)) if nullifier == note.nullifier()
541 );
542 }
543
544 #[test]
545 fn rejects_duplicate_supplied_output_note() {
546 let note = Note::mock_noop(Word::from([1_u32, 2, 3, 4]));
547 let output_note = into_output_note(note.clone());
548 let transactions = OrderedTransactionHeaders::new_unchecked(vec![]);
549
550 let result = BlockBody::new(
551 vec![],
552 vec![vec![(0, output_note.clone()), (1, output_note)]],
553 vec![],
554 transactions,
555 );
556
557 assert_matches!(
558 result,
559 Err(BlockBodyError::DuplicateOutputNote(note_id)) if note_id == note.id()
560 );
561 }
562
563 #[test]
564 fn accepts_note_consumed_before_created_in_transaction_headers() {
565 let note = Note::mock_noop(Word::from([1_u32, 2, 3, 4]));
566 let input =
567 InputNoteCommitment::from_parts_unchecked(note.nullifier(), Some(*note.header()));
568 let transactions = OrderedTransactionHeaders::new_unchecked(vec![
569 transaction_header(
570 Word::from([1_u32, 2, 3, 4]),
571 Word::from([5_u32, 6, 7, 8]),
572 InputNotes::new(vec![input]).unwrap(),
573 vec![],
574 )
575 .unwrap(),
576 transaction_header(
577 Word::from([5_u32, 6, 7, 8]),
578 Word::from([9_u32, 10, 11, 12]),
579 InputNotes::default(),
580 vec![*note.header()],
581 )
582 .unwrap(),
583 ]);
584
585 BlockBody::new(
586 vec![],
587 vec![vec![(0, into_output_note(note.clone()))]],
588 vec![note.nullifier()],
589 transactions,
590 )
591 .unwrap();
592 }
593
594 #[test]
595 fn deserialization_accepts_output_notes_mismatch() {
596 let note = Note::mock_noop(Word::from([1_u32, 2, 3, 4]));
597 let transactions = OrderedTransactionHeaders::new_unchecked(vec![
598 transaction_header(
599 Word::from([1_u32, 2, 3, 4]),
600 Word::from([5_u32, 6, 7, 8]),
601 InputNotes::default(),
602 vec![*note.header()],
603 )
604 .unwrap(),
605 ]);
606 let invalid_body = BlockBody::new_unchecked(vec![], vec![], vec![], transactions);
607
608 BlockBody::read_from_bytes(&invalid_body.to_bytes()).unwrap();
609 }
610
611 #[test]
612 fn deserialization_accepts_created_nullifiers_mismatch() {
613 let note = Note::mock_noop(Word::from([1_u32, 2, 3, 4]));
614 let input =
615 InputNoteCommitment::from_parts_unchecked(note.nullifier(), Some(*note.header()));
616 let transactions = OrderedTransactionHeaders::new_unchecked(vec![
617 transaction_header(
618 Word::from([1_u32, 2, 3, 4]),
619 Word::from([5_u32, 6, 7, 8]),
620 InputNotes::new(vec![input]).unwrap(),
621 vec![],
622 )
623 .unwrap(),
624 ]);
625 let invalid_body = BlockBody::new_unchecked(vec![], vec![], vec![], transactions);
626
627 BlockBody::read_from_bytes(&invalid_body.to_bytes()).unwrap();
628 }
629}