Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion crates/miden-protocol/src/batch/proposed_batch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use alloc::vec::Vec;

use crate::account::AccountId;
use crate::batch::note_tracker::{NoteTracker, TrackerOutput};
use crate::batch::{BatchAccountUpdate, BatchId};
use crate::batch::{BatchAccountUpdate, BatchId, BatchNoteTree};
use crate::block::{BlockHeader, BlockNumber};
use crate::errors::ProposedBatchError;
use crate::note::{NoteId, NoteInclusionProof};
Expand Down Expand Up @@ -63,6 +63,10 @@ pub struct ProposedBatch {
/// batch that are not consumed within the same batch. These are sorted by
/// [`OutputNote::id`].
output_notes: Vec<OutputNote>,
/// The [`BatchNoteTree`] built over the batch's output notes, with note IDs packed at
/// contiguous leaf indices in the same order as `output_notes`. Its root is the batch's
/// note tree root.
Comment on lines +66 to +68

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// The [`BatchNoteTree`] built over the batch's output notes, with note IDs packed at
/// contiguous leaf indices in the same order as `output_notes`. Its root is the batch's
/// note tree root.
/// The [`BatchNoteTree`] built over the batch's output notes, with note IDs packed at
/// contiguous leaf indices in the same order as `output_notes`.

batch_note_tree: BatchNoteTree,
}

impl ProposedBatch {
Expand Down Expand Up @@ -320,6 +324,12 @@ impl ProposedBatch {
return Err(ProposedBatchError::TooManyOutputNotes(output_notes.len()));
}

// Build the batch note tree over the final output notes. The number of output notes is
// bounded by the check above, so the tree's capacity cannot be exceeded.
let batch_note_tree =
BatchNoteTree::with_contiguous_leaves(output_notes.iter().map(Into::into))
.map_err(ProposedBatchError::NoteTreeRootError)?;

// Compute batch ID.
// --------------------------------------------------------------------------------------------

Expand All @@ -335,6 +345,7 @@ impl ProposedBatch {
batch_expiration_block_num,
input_notes,
output_notes,
batch_note_tree,
})
}

Expand Down Expand Up @@ -447,6 +458,11 @@ impl ProposedBatch {
&self.output_notes
}

/// Returns the [`BatchNoteTree`] built over the batch's output notes.
pub fn batch_note_tree(&self) -> &BatchNoteTree {
&self.batch_note_tree
}

/// Consumes the proposed batch and returns its underlying parts.
#[allow(clippy::type_complexity)]
pub fn into_parts(
Expand All @@ -461,6 +477,7 @@ impl ProposedBatch {
InputNotes<InputNoteCommitment>,
Vec<OutputNote>,
BlockNumber,
BatchNoteTree,
) {
(
self.transactions,
Expand All @@ -472,6 +489,7 @@ impl ProposedBatch {
self.input_notes,
self.output_notes,
self.batch_expiration_block_num,
self.batch_note_tree,
)
}
}
Expand Down
3 changes: 3 additions & 0 deletions crates/miden-protocol/src/errors/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -979,6 +979,9 @@ pub enum ProposedBatchError {
)]
TooManyOutputNotes(usize),

#[error("failed to construct the batch note tree from the batch's output notes")]
NoteTreeRootError(#[source] MerkleError),

#[error(
"transaction batch has {0} account updates but at most {MAX_ACCOUNTS_PER_BATCH} are allowed"
)]
Expand Down
81 changes: 80 additions & 1 deletion crates/miden-testing/src/kernel_tests/batch/proposed_batch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use miden_crypto::rand::RandomCoin;
use miden_protocol::Word;
use miden_protocol::account::{Account, AccountId, AccountType};
use miden_protocol::asset::NonFungibleAsset;
use miden_protocol::batch::ProposedBatch;
use miden_protocol::batch::{BatchNoteTree, ProposedBatch};
use miden_protocol::block::BlockNumber;
use miden_protocol::crypto::merkle::MerkleError;
use miden_protocol::errors::{BatchAccountUpdateError, ProposedBatchError};
Expand Down Expand Up @@ -229,6 +229,85 @@ fn note_created_and_consumed_in_same_batch() -> anyhow::Result<()> {
Ok(())
}

/// Tests that the batch note tree is built over the batch's final output notes: its root matches a
/// tree built independently from `output_notes()` and it has one leaf per output note.
#[test]
fn batch_note_tree_built_over_output_notes() -> anyhow::Result<()> {
let TestSetup { mut chain, account1, .. } = setup_chain();
let block1 = chain.block_header(1);
let block2 = chain.prove_next_block()?;

let output_notes = (40..43).map(mock_output_note).collect::<alloc::vec::Vec<_>>();
let tx =
MockProvenTxBuilder::with_account(account1.id(), Word::empty(), account1.to_commitment())
.reference_block(&block1)
.output_notes(output_notes.clone())
.build()?;

let batch = ProposedBatch::new_unverified(
[tx].into_iter().map(Arc::new).collect(),
block2.header().clone(),
chain.latest_partial_blockchain(),
BTreeMap::default(),
)?;

assert_eq!(batch.output_notes().len(), output_notes.len());

let expected_tree =
BatchNoteTree::with_contiguous_leaves(batch.output_notes().iter().map(Into::into))?;
assert_eq!(batch.batch_note_tree().root(), expected_tree.root());
assert_eq!(batch.batch_note_tree().num_leaves(), output_notes.len());
assert_ne!(
batch.batch_note_tree().root(),
BatchNoteTree::with_contiguous_leaves([])?.root()
);

Ok(())
}

/// Tests that notes erased within a batch (created and consumed in the same batch) are excluded
/// from the batch note tree, so its root matches a tree built from the post-erasure output notes.
#[test]
fn batch_note_tree_excludes_erased_notes() -> anyhow::Result<()> {
let TestSetup { mut chain, account1, account2, .. } = setup_chain();
let block1 = chain.block_header(1);
let block2 = chain.prove_next_block()?;

// tx1 creates an erased note (consumed by tx2) and a kept note.
let erased_note = mock_note(40);
let kept_note = mock_output_note(41);
let tx1 =
MockProvenTxBuilder::with_account(account1.id(), Word::empty(), account1.to_commitment())
.reference_block(&block1)
.output_notes(vec![
RawOutputNote::Full(erased_note.clone()).into_output_note().unwrap(),
kept_note.clone(),
])
.build()?;
let tx2 =
MockProvenTxBuilder::with_account(account2.id(), Word::empty(), account2.to_commitment())
.reference_block(&block1)
.unauthenticated_notes(vec![erased_note.clone()])
.build()?;

let batch = ProposedBatch::new_unverified(
[tx1, tx2].into_iter().map(Arc::new).collect(),
block2.header().clone(),
chain.latest_partial_blockchain(),
BTreeMap::default(),
)?;

// Only the kept note survives erasure.
assert_eq!(batch.output_notes(), slice::from_ref(&kept_note));
assert_eq!(batch.batch_note_tree().num_leaves(), 1);

let expected_tree =
BatchNoteTree::with_contiguous_leaves(slice::from_ref(&kept_note).iter().map(Into::into))?;
assert_eq!(batch.batch_note_tree().root(), expected_tree.root());

Ok(())
}

/// Notes with the same details but different metadata are not considered the same for batch
/// erasure.
#[test]
Expand Down
1 change: 1 addition & 0 deletions crates/miden-tx-batch-prover/src/local_batch_prover.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ impl LocalBatchProver {
input_notes,
output_notes,
batch_expiration_block_num,
_batch_note_tree,
) = proposed_batch.into_parts();

ProvenBatch::new_unchecked(
Expand Down
Loading