Skip to main content

miden_protocol/block/
block_body.rs

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// BLOCK BODY
32// ================================================================================================
33
34/// Body of a block in the chain which contains data pertaining to all relevant state changes.
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct BlockBody {
37    /// Account updates for the block.
38    updated_accounts: Vec<BlockAccountUpdate>,
39
40    /// Note batches created by the transactions in this block.
41    output_note_batches: Vec<OutputNoteBatch>,
42
43    /// Nullifiers created by the transactions in this block through the consumption of notes.
44    created_nullifiers: Vec<Nullifier>,
45
46    /// The aggregated and flattened transaction headers of all batches in the order in which they
47    /// appeared in the proposed block.
48    transactions: OrderedTransactionHeaders,
49}
50
51impl BlockBody {
52    // CONSTRUCTOR
53    // --------------------------------------------------------------------------------------------
54
55    /// Creates a new [`BlockBody`] and validates its local structural constraints.
56    ///
57    /// This constructor does not verify that the created nullifiers and output notes correspond to
58    /// the ordered transaction headers. It also does not authenticate input notes, verify note
59    /// inclusion proofs, or verify that the account updates represent the transactions' state
60    /// transitions. Those checks must be performed while constructing the proposed block or by a
61    /// block verifier.
62    ///
63    /// # Errors
64    ///
65    /// Returns an error if a size, index, or uniqueness constraint is violated.
66    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    /// Creates a new [`BlockBody`] without performing any validation.
140    ///
141    /// # Warning
142    ///
143    /// Callers must ensure that the block body satisfies all invariants checked by
144    /// [`BlockBody::new`].
145    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    // PUBLIC ACCESSORS
160    // --------------------------------------------------------------------------------------------
161
162    /// Returns the slice of [`BlockAccountUpdate`]s for all accounts updated in the block.
163    pub fn updated_accounts(&self) -> &[BlockAccountUpdate] {
164        &self.updated_accounts
165    }
166
167    /// Returns the slice of [`OutputNoteBatch`]es for all output notes created in the block.
168    pub fn output_note_batches(&self) -> &[OutputNoteBatch] {
169        &self.output_note_batches
170    }
171
172    /// Returns a reference to the slice of nullifiers for all notes consumed in the block.
173    pub fn created_nullifiers(&self) -> &[Nullifier] {
174        &self.created_nullifiers
175    }
176
177    /// Returns the [`OrderedTransactionHeaders`] of all transactions included in this block.
178    pub fn transactions(&self) -> &OrderedTransactionHeaders {
179        &self.transactions
180    }
181
182    /// Returns the commitment of all transactions included in this block.
183    pub fn transaction_commitment(&self) -> Word {
184        self.transactions.commitment()
185    }
186
187    /// Returns an iterator over all [`OutputNote`]s created in this block.
188    ///
189    /// Each note is accompanied by a corresponding index specifying where the note is located
190    /// in the block's [`BlockNoteTree`].
191    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                    // SAFETY: The block body contains at most the max allowed number of
196                    // batches and each batch is guaranteed to contain
197                    // at most the max allowed number of output notes.
198                    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    /// Computes the [`BlockNoteTree`] containing all [`OutputNote`]s created in this block.
207    pub fn compute_block_note_tree(&self) -> BlockNoteTree {
208        let entries = self.output_notes().map(|(note_index, note)| (note_index, note.into()));
209
210        // SAFETY: We only construct block bodies that:
211        // - do not contain duplicates
212        // - contain at most the max allowed number of batches and each batch is guaranteed to
213        //   contain at most the max allowed number of output notes.
214        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    // DESTRUCTURING
219    // --------------------------------------------------------------------------------------------
220
221    /// Consumes the block body and returns its parts.
222    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        // Split the proposed block into its constituent parts.
242        let (batches, account_updated_witnesses, output_note_batches, created_nullifiers, ..) =
243            block.into_parts();
244
245        // Transform the account update witnesses into block account updates.
246        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                    // Note that compute_account_root took out this value so it should not be used.
253                    _initial_state_proof,
254                    details,
255                ) = update_witness.into_parts();
256                // The proposed block's account update witnesses were validated while the block
257                // was assembled.
258                BlockAccountUpdate::new_unchecked(account_id, final_state_commitment, details)
259            })
260            .collect();
261        let created_nullifiers = created_nullifiers.keys().copied().collect::<Vec<_>>();
262        // Aggregate the verified transactions of all batches.
263        let transactions = batches.into_transactions();
264        Self {
265            updated_accounts,
266            output_note_batches,
267            created_nullifiers,
268            transactions,
269        }
270    }
271}
272
273// SERIALIZATION
274// ================================================================================================
275
276impl 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// TESTS
298// ================================================================================================
299
300#[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}