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(
67 updated_accounts: Vec<BlockAccountUpdate>,
68 output_note_batches: Vec<OutputNoteBatch>,
69 created_nullifiers: Vec<Nullifier>,
70 transactions: OrderedTransactionHeaders,
71 ) -> Result<Self, BlockBodyError> {
72 if output_note_batches.len() > MAX_BATCHES_PER_BLOCK {
73 return Err(BlockBodyError::TooManyOutputNoteBatches(output_note_batches.len()));
74 }
75 if updated_accounts.len() > MAX_ACCOUNTS_PER_BLOCK {
76 return Err(BlockBodyError::TooManyAccountUpdates(updated_accounts.len()));
77 }
78 if created_nullifiers.len() > MAX_INPUT_NOTES_PER_BLOCK {
79 return Err(BlockBodyError::TooManyNullifiers(created_nullifiers.len()));
80 }
81
82 let mut account_ids = BTreeSet::new();
83 for update in &updated_accounts {
84 if !account_ids.insert(update.account_id()) {
85 return Err(BlockBodyError::DuplicateAccountUpdate(update.account_id()));
86 }
87 }
88
89 let mut output_note_ids = BTreeSet::new();
90 for (batch_index, batch) in output_note_batches.iter().enumerate() {
91 if batch.len() > MAX_OUTPUT_NOTES_PER_BATCH {
92 return Err(BlockBodyError::TooManyOutputNotes {
93 batch_index,
94 note_count: batch.len(),
95 });
96 }
97 let mut note_indices = BTreeSet::new();
98 for (note_index, note) in batch {
99 if BlockNoteIndex::new(batch_index, *note_index).is_none() {
100 return Err(BlockBodyError::InvalidOutputNoteIndex {
101 batch_index,
102 note_index: *note_index,
103 });
104 }
105 if !note_indices.insert(*note_index) {
106 return Err(BlockBodyError::DuplicateOutputNoteIndex {
107 batch_index,
108 note_index: *note_index,
109 });
110 }
111 if !output_note_ids.insert(note.id()) {
112 return Err(BlockBodyError::DuplicateOutputNote(note.id()));
113 }
114 }
115 }
116
117 let mut nullifiers = BTreeSet::new();
118 for nullifier in &created_nullifiers {
119 if !nullifiers.insert(*nullifier) {
120 return Err(BlockBodyError::DuplicateNullifier(*nullifier));
121 }
122 }
123
124 let mut transaction_ids = BTreeSet::new();
125 for transaction in transactions.as_slice() {
126 if !transaction_ids.insert(transaction.id()) {
127 return Err(BlockBodyError::DuplicateTransaction(transaction.id()));
128 }
129 }
130
131 Ok(Self::new_unchecked(
132 updated_accounts,
133 output_note_batches,
134 created_nullifiers,
135 transactions,
136 ))
137 }
138
139 pub fn new_unchecked(
146 updated_accounts: Vec<BlockAccountUpdate>,
147 output_note_batches: Vec<OutputNoteBatch>,
148 created_nullifiers: Vec<Nullifier>,
149 transactions: OrderedTransactionHeaders,
150 ) -> Self {
151 Self {
152 updated_accounts,
153 output_note_batches,
154 created_nullifiers,
155 transactions,
156 }
157 }
158
159 pub fn updated_accounts(&self) -> &[BlockAccountUpdate] {
164 &self.updated_accounts
165 }
166
167 pub fn output_note_batches(&self) -> &[OutputNoteBatch] {
169 &self.output_note_batches
170 }
171
172 pub fn created_nullifiers(&self) -> &[Nullifier] {
174 &self.created_nullifiers
175 }
176
177 pub fn transactions(&self) -> &OrderedTransactionHeaders {
179 &self.transactions
180 }
181
182 pub fn transaction_commitment(&self) -> Word {
184 self.transactions.commitment()
185 }
186
187 pub fn output_notes(&self) -> impl Iterator<Item = (BlockNoteIndex, &OutputNote)> {
192 self.output_note_batches.iter().enumerate().flat_map(|(batch_idx, notes)| {
193 notes.iter().map(move |(note_idx_in_batch, note)| {
194 (
195 BlockNoteIndex::new(batch_idx, *note_idx_in_batch)
199 .expect("max batches in block and max notes in batches should be enforced"),
200 note,
201 )
202 })
203 })
204 }
205
206 pub fn compute_block_note_tree(&self) -> BlockNoteTree {
208 let entries = self.output_notes().map(|(note_index, note)| (note_index, note.into()));
209
210 BlockNoteTree::with_entries(entries)
215 .expect("the output notes of the block should not contain duplicates and contain at most the allowed maximum")
216 }
217
218 pub fn into_parts(
223 self,
224 ) -> (
225 Vec<BlockAccountUpdate>,
226 Vec<OutputNoteBatch>,
227 Vec<Nullifier>,
228 OrderedTransactionHeaders,
229 ) {
230 (
231 self.updated_accounts,
232 self.output_note_batches,
233 self.created_nullifiers,
234 self.transactions,
235 )
236 }
237}
238
239impl From<ProposedBlock> for BlockBody {
240 fn from(block: ProposedBlock) -> Self {
241 let (batches, account_updated_witnesses, output_note_batches, created_nullifiers, ..) =
243 block.into_parts();
244
245 let updated_accounts = account_updated_witnesses
247 .into_iter()
248 .map(|(account_id, update_witness)| {
249 let (
250 _initial_state_commitment,
251 final_state_commitment,
252 _initial_state_proof,
254 details,
255 ) = update_witness.into_parts();
256 BlockAccountUpdate::new_unchecked(account_id, final_state_commitment, details)
259 })
260 .collect();
261 let created_nullifiers = created_nullifiers.keys().copied().collect::<Vec<_>>();
262 let transactions = batches.into_transactions();
264 Self {
265 updated_accounts,
266 output_note_batches,
267 created_nullifiers,
268 transactions,
269 }
270 }
271}
272
273impl Serializable for BlockBody {
277 fn write_into<W: ByteWriter>(&self, target: &mut W) {
278 self.updated_accounts.write_into(target);
279 self.output_note_batches.write_into(target);
280 self.created_nullifiers.write_into(target);
281 self.transactions.write_into(target);
282 }
283}
284
285impl Deserializable for BlockBody {
286 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
287 Self::new(
288 Vec::read_from(source)?,
289 Vec::read_from(source)?,
290 Vec::read_from(source)?,
291 OrderedTransactionHeaders::read_from(source)?,
292 )
293 .map_err(|error| DeserializationError::InvalidValue(error.to_string()))
294 }
295}
296
297#[cfg(test)]
301mod tests {
302 use alloc::vec::Vec;
303
304 use assert_matches::assert_matches;
305 use rstest::rstest;
306
307 use super::BlockBody;
308 use crate::Word;
309 use crate::account::AccountId;
310 use crate::errors::{BlockBodyError, TransactionHeaderError};
311 use crate::note::{Note, NoteHeader};
312 use crate::testing::account_id::ACCOUNT_ID_PRIVATE_SENDER;
313 use crate::transaction::{
314 InputNoteCommitment,
315 InputNotes,
316 OrderedTransactionHeaders,
317 OutputNote,
318 RawOutputNote,
319 TransactionHeader,
320 };
321 use crate::utils::serde::{Deserializable, Serializable};
322
323 fn account_id() -> AccountId {
324 AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap()
325 }
326
327 fn transaction_header(
328 initial_state_commitment: Word,
329 final_state_commitment: Word,
330 input_notes: InputNotes<InputNoteCommitment>,
331 output_notes: Vec<NoteHeader>,
332 ) -> Result<TransactionHeader, TransactionHeaderError> {
333 TransactionHeader::new(
334 account_id(),
335 initial_state_commitment,
336 final_state_commitment,
337 input_notes,
338 output_notes,
339 )
340 }
341
342 fn into_output_note(note: Note) -> OutputNote {
343 RawOutputNote::Full(note).into_output_note().unwrap()
344 }
345
346 #[rstest]
347 #[case::missing_from_body(true)]
348 #[case::unexpected_in_body(false)]
349 fn accepts_created_nullifiers_mismatch(#[case] transaction_has_input: bool) {
350 let note = Note::mock_noop(Word::from([1_u32, 2, 3, 4]));
351 let input =
352 InputNoteCommitment::from_parts_unchecked(note.nullifier(), Some(*note.header()));
353 let input_notes = if transaction_has_input {
354 InputNotes::new(vec![input]).unwrap()
355 } else {
356 InputNotes::default()
357 };
358 let created_nullifiers = if transaction_has_input {
359 vec![]
360 } else {
361 vec![note.nullifier()]
362 };
363 let transactions = OrderedTransactionHeaders::new_unchecked(vec![
364 transaction_header(
365 Word::from([1_u32, 2, 3, 4]),
366 Word::from([5_u32, 6, 7, 8]),
367 input_notes,
368 vec![],
369 )
370 .unwrap(),
371 ]);
372
373 BlockBody::new(vec![], vec![], created_nullifiers, transactions).unwrap();
374 }
375
376 #[rstest]
377 #[case::missing_from_body(true)]
378 #[case::unexpected_in_body(false)]
379 fn accepts_output_notes_mismatch(#[case] transaction_has_output: bool) {
380 let note = Note::mock_noop(Word::from([1_u32, 2, 3, 4]));
381 let output_notes = if transaction_has_output {
382 vec![]
383 } else {
384 vec![vec![(0, into_output_note(note.clone()))]]
385 };
386 let transaction_output_notes = if transaction_has_output {
387 vec![*note.header()]
388 } else {
389 vec![]
390 };
391 let transactions = OrderedTransactionHeaders::new_unchecked(vec![
392 transaction_header(
393 Word::from([1_u32, 2, 3, 4]),
394 Word::from([5_u32, 6, 7, 8]),
395 InputNotes::default(),
396 transaction_output_notes,
397 )
398 .unwrap(),
399 ]);
400
401 BlockBody::new(vec![], output_notes, vec![], transactions).unwrap();
402 }
403
404 #[test]
405 fn accepts_matching_transaction_notes() {
406 let input_note = Note::mock_noop(Word::from([1_u32, 2, 3, 4]));
407 let output_note = Note::mock_noop(Word::from([5_u32, 6, 7, 8]));
408 let input = InputNoteCommitment::from_parts_unchecked(
409 input_note.nullifier(),
410 Some(*input_note.header()),
411 );
412 let transactions = OrderedTransactionHeaders::new_unchecked(vec![
413 transaction_header(
414 Word::from([1_u32, 2, 3, 4]),
415 Word::from([5_u32, 6, 7, 8]),
416 InputNotes::new(vec![input]).unwrap(),
417 vec![*output_note.header()],
418 )
419 .unwrap(),
420 ]);
421
422 BlockBody::new(
423 vec![],
424 vec![vec![(0, into_output_note(output_note))]],
425 vec![input_note.nullifier()],
426 transactions,
427 )
428 .unwrap();
429 }
430
431 #[test]
432 fn rejects_duplicate_supplied_nullifier() {
433 let note = Note::mock_noop(Word::from([1_u32, 2, 3, 4]));
434 let nullifier = note.nullifier();
435 let transactions = OrderedTransactionHeaders::new_unchecked(vec![]);
436
437 let result = BlockBody::new(vec![], vec![], vec![nullifier, nullifier], transactions);
438
439 assert_matches!(
440 result,
441 Err(BlockBodyError::DuplicateNullifier(nullifier)) if nullifier == note.nullifier()
442 );
443 }
444
445 #[test]
446 fn rejects_duplicate_supplied_output_note() {
447 let note = Note::mock_noop(Word::from([1_u32, 2, 3, 4]));
448 let output_note = into_output_note(note.clone());
449 let transactions = OrderedTransactionHeaders::new_unchecked(vec![]);
450
451 let result = BlockBody::new(
452 vec![],
453 vec![vec![(0, output_note.clone()), (1, output_note)]],
454 vec![],
455 transactions,
456 );
457
458 assert_matches!(
459 result,
460 Err(BlockBodyError::DuplicateOutputNote(note_id)) if note_id == note.id()
461 );
462 }
463
464 #[test]
465 fn accepts_note_consumed_before_created_in_transaction_headers() {
466 let note = Note::mock_noop(Word::from([1_u32, 2, 3, 4]));
467 let input =
468 InputNoteCommitment::from_parts_unchecked(note.nullifier(), Some(*note.header()));
469 let transactions = OrderedTransactionHeaders::new_unchecked(vec![
470 transaction_header(
471 Word::from([1_u32, 2, 3, 4]),
472 Word::from([5_u32, 6, 7, 8]),
473 InputNotes::new(vec![input]).unwrap(),
474 vec![],
475 )
476 .unwrap(),
477 transaction_header(
478 Word::from([5_u32, 6, 7, 8]),
479 Word::from([9_u32, 10, 11, 12]),
480 InputNotes::default(),
481 vec![*note.header()],
482 )
483 .unwrap(),
484 ]);
485
486 BlockBody::new(
487 vec![],
488 vec![vec![(0, into_output_note(note.clone()))]],
489 vec![note.nullifier()],
490 transactions,
491 )
492 .unwrap();
493 }
494
495 #[test]
496 fn deserialization_accepts_output_notes_mismatch() {
497 let note = Note::mock_noop(Word::from([1_u32, 2, 3, 4]));
498 let transactions = OrderedTransactionHeaders::new_unchecked(vec![
499 transaction_header(
500 Word::from([1_u32, 2, 3, 4]),
501 Word::from([5_u32, 6, 7, 8]),
502 InputNotes::default(),
503 vec![*note.header()],
504 )
505 .unwrap(),
506 ]);
507 let invalid_body = BlockBody::new_unchecked(vec![], vec![], vec![], transactions);
508
509 BlockBody::read_from_bytes(&invalid_body.to_bytes()).unwrap();
510 }
511
512 #[test]
513 fn deserialization_accepts_created_nullifiers_mismatch() {
514 let note = Note::mock_noop(Word::from([1_u32, 2, 3, 4]));
515 let input =
516 InputNoteCommitment::from_parts_unchecked(note.nullifier(), Some(*note.header()));
517 let transactions = OrderedTransactionHeaders::new_unchecked(vec![
518 transaction_header(
519 Word::from([1_u32, 2, 3, 4]),
520 Word::from([5_u32, 6, 7, 8]),
521 InputNotes::new(vec![input]).unwrap(),
522 vec![],
523 )
524 .unwrap(),
525 ]);
526 let invalid_body = BlockBody::new_unchecked(vec![], vec![], vec![], transactions);
527
528 BlockBody::read_from_bytes(&invalid_body.to_bytes()).unwrap();
529 }
530}