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:
66    /// - a size, index, or uniqueness constraint is violated.
67    /// - the update of a new public account does not reconstruct to its final state commitment.
68    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    /// Creates a new [`BlockBody`] without performing any validation.
152    ///
153    /// # Warning
154    ///
155    /// Callers must ensure that the block body satisfies all invariants checked by
156    /// [`BlockBody::new`].
157    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    // PUBLIC ACCESSORS
172    // --------------------------------------------------------------------------------------------
173
174    /// Returns the slice of [`BlockAccountUpdate`]s for all accounts updated in the block.
175    pub fn updated_accounts(&self) -> &[BlockAccountUpdate] {
176        &self.updated_accounts
177    }
178
179    /// Returns the slice of [`OutputNoteBatch`]es for all output notes created in the block.
180    pub fn output_note_batches(&self) -> &[OutputNoteBatch] {
181        &self.output_note_batches
182    }
183
184    /// Returns a reference to the slice of nullifiers for all notes consumed in the block.
185    pub fn created_nullifiers(&self) -> &[Nullifier] {
186        &self.created_nullifiers
187    }
188
189    /// Returns the [`OrderedTransactionHeaders`] of all transactions included in this block.
190    pub fn transactions(&self) -> &OrderedTransactionHeaders {
191        &self.transactions
192    }
193
194    /// Returns the commitment of all transactions included in this block.
195    pub fn transaction_commitment(&self) -> Word {
196        self.transactions.commitment()
197    }
198
199    /// Returns an iterator over all [`OutputNote`]s created in this block.
200    ///
201    /// Each note is accompanied by a corresponding index specifying where the note is located
202    /// in the block's [`BlockNoteTree`].
203    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                    // SAFETY: The block body contains at most the max allowed number of
208                    // batches and each batch is guaranteed to contain
209                    // at most the max allowed number of output notes.
210                    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    /// Computes the [`BlockNoteTree`] containing all [`OutputNote`]s created in this block.
219    pub fn compute_block_note_tree(&self) -> BlockNoteTree {
220        let entries = self.output_notes().map(|(note_index, note)| (note_index, note.into()));
221
222        // SAFETY: We only construct block bodies that:
223        // - do not contain duplicates
224        // - contain at most the max allowed number of batches and each batch is guaranteed to
225        //   contain at most the max allowed number of output notes.
226        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    // DESTRUCTURING
231    // --------------------------------------------------------------------------------------------
232
233    /// Consumes the block body and returns its parts.
234    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        // Split the proposed block into its constituent parts.
254        let (batches, account_updated_witnesses, output_note_batches, created_nullifiers, ..) =
255            block.into_parts();
256
257        // Transform the account update witnesses into block account updates.
258        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                    // Note that compute_account_root took out this value so it should not be used.
265                    _initial_state_proof,
266                    details,
267                ) = update_witness.into_parts();
268                // The proposed block's account update witnesses were validated while the block
269                // was assembled.
270                BlockAccountUpdate::new_unchecked(account_id, final_state_commitment, details)
271            })
272            .collect();
273        let created_nullifiers = created_nullifiers.keys().copied().collect::<Vec<_>>();
274        // Aggregate the verified transactions of all batches.
275        let transactions = batches.into_transactions();
276        Self {
277            updated_accounts,
278            output_note_batches,
279            created_nullifiers,
280            transactions,
281        }
282    }
283}
284
285// SERIALIZATION
286// ================================================================================================
287
288impl 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// TESTS
310// ================================================================================================
311
312#[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    /// Returns the body parts of a block with a single transaction against `account`, whose update
370    /// reconstructs `account` but claims `final_state_commitment`.
371    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}