From 0c91efdf58d7c4201107c2eab2e4f38c541ac51d Mon Sep 17 00:00:00 2001 From: "Claude (Opus)" Date: Mon, 1 Jun 2026 11:41:44 +0000 Subject: [PATCH 1/4] feat(batch): construct BatchNoteTree during batch construction Build a BatchNoteTree over the batch's final (non-erased) output notes in ProposedBatch::new_batch_inner, store it on ProposedBatch, expose it via a batch_note_tree() accessor, and include it in into_parts. This is the Rust-side foundation for the batch kernel outputting BATCH_NOTE_TREE_ROOT. Part of #3020. --- .../src/batch/proposed_batch.rs | 20 ++++- crates/miden-protocol/src/errors/mod.rs | 3 + .../src/kernel_tests/batch/proposed_batch.rs | 81 ++++++++++++++++++- .../src/local_batch_prover.rs | 1 + 4 files changed, 103 insertions(+), 2 deletions(-) diff --git a/crates/miden-protocol/src/batch/proposed_batch.rs b/crates/miden-protocol/src/batch/proposed_batch.rs index 5065b96660..0b4589953f 100644 --- a/crates/miden-protocol/src/batch/proposed_batch.rs +++ b/crates/miden-protocol/src/batch/proposed_batch.rs @@ -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}; @@ -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, + /// 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. + batch_note_tree: BatchNoteTree, } impl ProposedBatch { @@ -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. // -------------------------------------------------------------------------------------------- @@ -335,6 +345,7 @@ impl ProposedBatch { batch_expiration_block_num, input_notes, output_notes, + batch_note_tree, }) } @@ -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( @@ -461,6 +477,7 @@ impl ProposedBatch { InputNotes, Vec, BlockNumber, + BatchNoteTree, ) { ( self.transactions, @@ -472,6 +489,7 @@ impl ProposedBatch { self.input_notes, self.output_notes, self.batch_expiration_block_num, + self.batch_note_tree, ) } } diff --git a/crates/miden-protocol/src/errors/mod.rs b/crates/miden-protocol/src/errors/mod.rs index 84142d394d..3c0d4deee3 100644 --- a/crates/miden-protocol/src/errors/mod.rs +++ b/crates/miden-protocol/src/errors/mod.rs @@ -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" )] diff --git a/crates/miden-testing/src/kernel_tests/batch/proposed_batch.rs b/crates/miden-testing/src/kernel_tests/batch/proposed_batch.rs index 9cf1c72f84..8cf2845348 100644 --- a/crates/miden-testing/src/kernel_tests/batch/proposed_batch.rs +++ b/crates/miden-testing/src/kernel_tests/batch/proposed_batch.rs @@ -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}; @@ -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::>(); + 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] diff --git a/crates/miden-tx-batch-prover/src/local_batch_prover.rs b/crates/miden-tx-batch-prover/src/local_batch_prover.rs index b9811d7ca6..5fc573a134 100644 --- a/crates/miden-tx-batch-prover/src/local_batch_prover.rs +++ b/crates/miden-tx-batch-prover/src/local_batch_prover.rs @@ -31,6 +31,7 @@ impl LocalBatchProver { input_notes, output_notes, batch_expiration_block_num, + _batch_note_tree, ) = proposed_batch.into_parts(); ProvenBatch::new_unchecked( From a3e00239910dfb2f400aee09ab1a241f65413232 Mon Sep 17 00:00:00 2001 From: "Claude (Opus)" Date: Mon, 1 Jun 2026 13:54:32 +0000 Subject: [PATCH 2/4] feat(batch): carry batch note tree root on ProvenBatch Propagate the BatchNoteTree root computed during batch construction onto ProvenBatch via a new note_tree_root field (serialization, new_unchecked, and note_tree_root() accessor), so the commitment flows to block construction and is ready for batch-kernel verification. LocalBatchProver passes the root from the ProposedBatch tree. Also renames the construction error variant to BatchNoteTreeConstructionFailed and adds an empty-output-notes batch tree test. Part of #3020. --- CHANGELOG.md | 4 +++ .../src/batch/proposed_batch.rs | 5 +-- .../miden-protocol/src/batch/proven_batch.rs | 14 ++++++++ crates/miden-protocol/src/errors/mod.rs | 2 +- .../src/kernel_tests/batch/proposed_batch.rs | 34 +++++++++++++++++++ .../src/local_batch_prover.rs | 3 +- 6 files changed, 58 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26513d26c3..9dc8724502 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## v0.16.0 (TBD) +### Features + +- Added `BatchNoteTree` construction to `ProposedBatch`, exposing it via a `batch_note_tree()` accessor and including it in `into_parts` ([#3022](https://github.com/0xMiden/protocol/pull/3022)). + ### Changes - [BREAKING] Renamed `AccountStorageDelta` to `AccountStoragePatch` ([#3002](https://github.com/0xMiden/protocol/pull/3002)). diff --git a/crates/miden-protocol/src/batch/proposed_batch.rs b/crates/miden-protocol/src/batch/proposed_batch.rs index 0b4589953f..ff263f6077 100644 --- a/crates/miden-protocol/src/batch/proposed_batch.rs +++ b/crates/miden-protocol/src/batch/proposed_batch.rs @@ -325,10 +325,11 @@ impl ProposedBatch { } // 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. + // bounded by the check above to at most the tree's capacity, so this is a defensive error + // path that cannot be triggered in practice. let batch_note_tree = BatchNoteTree::with_contiguous_leaves(output_notes.iter().map(Into::into)) - .map_err(ProposedBatchError::NoteTreeRootError)?; + .map_err(ProposedBatchError::BatchNoteTreeConstructionFailed)?; // Compute batch ID. // -------------------------------------------------------------------------------------------- diff --git a/crates/miden-protocol/src/batch/proven_batch.rs b/crates/miden-protocol/src/batch/proven_batch.rs index 32a0bf9d18..29bfc0d99e 100644 --- a/crates/miden-protocol/src/batch/proven_batch.rs +++ b/crates/miden-protocol/src/batch/proven_batch.rs @@ -28,6 +28,8 @@ pub struct ProvenBatch { account_updates: BTreeMap, input_notes: InputNotes, output_notes: Vec, + /// The root of the [`BatchNoteTree`](crate::batch::BatchNoteTree) built over `output_notes`. + note_tree_root: Word, batch_expiration_block_num: BlockNumber, transactions: OrderedTransactionHeaders, } @@ -45,6 +47,7 @@ impl ProvenBatch { /// /// Returns an error if the batch expiration block number is not greater than the reference /// block number. + #[allow(clippy::too_many_arguments)] pub fn new_unchecked( id: BatchId, reference_block_commitment: Word, @@ -52,6 +55,7 @@ impl ProvenBatch { account_updates: BTreeMap, input_notes: InputNotes, output_notes: Vec, + note_tree_root: Word, batch_expiration_block_num: BlockNumber, transactions: OrderedTransactionHeaders, ) -> Result { @@ -70,6 +74,7 @@ impl ProvenBatch { account_updates, input_notes, output_notes, + note_tree_root, batch_expiration_block_num, transactions, }) @@ -139,6 +144,12 @@ impl ProvenBatch { &self.output_notes } + /// Returns the root of the [`BatchNoteTree`](crate::batch::BatchNoteTree) built over the + /// batch's output notes. + pub fn note_tree_root(&self) -> Word { + self.note_tree_root + } + /// Returns the [`OrderedTransactionHeaders`] included in this batch. pub fn transactions(&self) -> &OrderedTransactionHeaders { &self.transactions @@ -164,6 +175,7 @@ impl Serializable for ProvenBatch { self.account_updates.write_into(target); self.input_notes.write_into(target); self.output_notes.write_into(target); + self.note_tree_root.write_into(target); self.batch_expiration_block_num.write_into(target); self.transactions.write_into(target); } @@ -177,6 +189,7 @@ impl Deserializable for ProvenBatch { let account_updates = BTreeMap::read_from(source)?; let input_notes = InputNotes::::read_from(source)?; let output_notes = Vec::::read_from(source)?; + let note_tree_root = Word::read_from(source)?; let batch_expiration_block_num = BlockNumber::read_from(source)?; let transactions = OrderedTransactionHeaders::read_from(source)?; @@ -187,6 +200,7 @@ impl Deserializable for ProvenBatch { account_updates, input_notes, output_notes, + note_tree_root, batch_expiration_block_num, transactions, ) diff --git a/crates/miden-protocol/src/errors/mod.rs b/crates/miden-protocol/src/errors/mod.rs index 3c0d4deee3..da0b2599fe 100644 --- a/crates/miden-protocol/src/errors/mod.rs +++ b/crates/miden-protocol/src/errors/mod.rs @@ -980,7 +980,7 @@ pub enum ProposedBatchError { TooManyOutputNotes(usize), #[error("failed to construct the batch note tree from the batch's output notes")] - NoteTreeRootError(#[source] MerkleError), + BatchNoteTreeConstructionFailed(#[source] MerkleError), #[error( "transaction batch has {0} account updates but at most {MAX_ACCOUNTS_PER_BATCH} are allowed" diff --git a/crates/miden-testing/src/kernel_tests/batch/proposed_batch.rs b/crates/miden-testing/src/kernel_tests/batch/proposed_batch.rs index 8cf2845348..ea61473775 100644 --- a/crates/miden-testing/src/kernel_tests/batch/proposed_batch.rs +++ b/crates/miden-testing/src/kernel_tests/batch/proposed_batch.rs @@ -308,6 +308,40 @@ fn batch_note_tree_excludes_erased_notes() -> anyhow::Result<()> { Ok(()) } +/// Tests that a batch without output notes has an empty batch note tree, and that the root carried +/// on the proven batch matches the proposed batch's tree root. +#[test] +fn batch_note_tree_empty_when_no_output_notes() -> anyhow::Result<()> { + let TestSetup { mut chain, account1, .. } = setup_chain(); + let block1 = chain.block_header(1); + let block2 = chain.prove_next_block()?; + + let tx = + MockProvenTxBuilder::with_account(account1.id(), Word::empty(), account1.to_commitment()) + .reference_block(&block1) + .build()?; + + let proposed_batch = ProposedBatch::new_unverified( + [tx].into_iter().map(Arc::new).collect(), + block2.header().clone(), + chain.latest_partial_blockchain(), + BTreeMap::default(), + )?; + + assert_eq!(proposed_batch.output_notes().len(), 0); + assert_eq!(proposed_batch.batch_note_tree().num_leaves(), 0); + assert_eq!( + proposed_batch.batch_note_tree().root(), + BatchNoteTree::with_contiguous_leaves([])?.root(), + ); + + // The proven batch carries the same note tree root. + let proven_batch = chain.prove_transaction_batch(proposed_batch.clone())?; + assert_eq!(proven_batch.note_tree_root(), proposed_batch.batch_note_tree().root()); + + Ok(()) +} + /// Notes with the same details but different metadata are not considered the same for batch /// erasure. #[test] diff --git a/crates/miden-tx-batch-prover/src/local_batch_prover.rs b/crates/miden-tx-batch-prover/src/local_batch_prover.rs index 5fc573a134..e0a4aa0b54 100644 --- a/crates/miden-tx-batch-prover/src/local_batch_prover.rs +++ b/crates/miden-tx-batch-prover/src/local_batch_prover.rs @@ -31,7 +31,7 @@ impl LocalBatchProver { input_notes, output_notes, batch_expiration_block_num, - _batch_note_tree, + batch_note_tree, ) = proposed_batch.into_parts(); ProvenBatch::new_unchecked( @@ -41,6 +41,7 @@ impl LocalBatchProver { updated_accounts, input_notes, output_notes, + batch_note_tree.root(), batch_expiration_block_num, tx_headers, ) From fa06fff16b297287d8726cc6ed29525e5a57746b Mon Sep 17 00:00:00 2001 From: "Claude (Opus)" Date: Mon, 1 Jun 2026 13:54:32 +0000 Subject: [PATCH 3/4] docs: add changelog entry for #3022 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9dc8724502..4ca4241e74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Features -- Added `BatchNoteTree` construction to `ProposedBatch`, exposing it via a `batch_note_tree()` accessor and including it in `into_parts` ([#3022](https://github.com/0xMiden/protocol/pull/3022)). +- [BREAKING] Constructed a `BatchNoteTree` over the batch's output notes during batch building: `ProposedBatch` exposes it via `batch_note_tree()` and includes it in `into_parts`, and `ProvenBatch` carries its root via `note_tree_root()` (changes the `ProvenBatch` serialization format and `new_unchecked` signature) ([#3022](https://github.com/0xMiden/protocol/pull/3022)). ### Changes From 8f6e032d59c17b495a15743a3d2663b44995d47f Mon Sep 17 00:00:00 2001 From: "Claude (Opus)" Date: Mon, 1 Jun 2026 14:08:00 +0000 Subject: [PATCH 4/4] test(batch): cover ProvenBatch note tree root serialization Address review feedback: add a ProvenBatch serialization round-trip test asserting note_tree_root survives, assert batch_note_tree equality in the ProposedBatch round-trip test, and document that ProvenBatch::note_tree_root is unvalidated and must not be trusted at a trust boundary until a consumer binds it to the output notes. --- .../src/batch/proposed_batch.rs | 10 ++++-- .../miden-protocol/src/batch/proven_batch.rs | 9 +++++ .../src/kernel_tests/batch/proposed_batch.rs | 34 ++++++++++++++++++- 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/crates/miden-protocol/src/batch/proposed_batch.rs b/crates/miden-protocol/src/batch/proposed_batch.rs index ff263f6077..9878b26a01 100644 --- a/crates/miden-protocol/src/batch/proposed_batch.rs +++ b/crates/miden-protocol/src/batch/proposed_batch.rs @@ -324,9 +324,10 @@ 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 to at most the tree's capacity, so this is a defensive error - // path that cannot be triggered in practice. + // Build the batch note tree over the final output notes. `MAX_OUTPUT_NOTES_PER_BATCH` + // equals the tree's capacity (`2^BATCH_NOTE_TREE_DEPTH`) and the check above bounds the + // number of output notes by it, so this is a defensive error path that cannot be triggered + // in practice. let batch_note_tree = BatchNoteTree::with_contiguous_leaves(output_notes.iter().map(Into::into)) .map_err(ProposedBatchError::BatchNoteTreeConstructionFailed)?; @@ -630,6 +631,9 @@ mod tests { assert_eq!(batch.batch_expiration_block_num, batch2.batch_expiration_block_num); assert_eq!(batch.input_notes, batch2.input_notes); assert_eq!(batch.output_notes, batch2.output_notes); + // The batch note tree is not serialized but deterministically recomputed on + // deserialization. + assert_eq!(batch.batch_note_tree, batch2.batch_note_tree); Ok(()) } diff --git a/crates/miden-protocol/src/batch/proven_batch.rs b/crates/miden-protocol/src/batch/proven_batch.rs index 29bfc0d99e..ed45540fed 100644 --- a/crates/miden-protocol/src/batch/proven_batch.rs +++ b/crates/miden-protocol/src/batch/proven_batch.rs @@ -29,6 +29,11 @@ pub struct ProvenBatch { input_notes: InputNotes, output_notes: Vec, /// The root of the [`BatchNoteTree`](crate::batch::BatchNoteTree) built over `output_notes`. + /// + /// This value is stored as-is and is **not** recomputed from or checked against `output_notes` + /// by [`ProvenBatch::new_unchecked`] or deserialization. It must therefore not be trusted at a + /// trust boundary until a consumer binds it to `output_notes` (e.g. the block kernel, which + /// will verify it against the per-batch note tree it reconstructs). note_tree_root: Word, batch_expiration_block_num: BlockNumber, transactions: OrderedTransactionHeaders, @@ -146,6 +151,10 @@ impl ProvenBatch { /// Returns the root of the [`BatchNoteTree`](crate::batch::BatchNoteTree) built over the /// batch's output notes. + /// + /// This root is not validated against [`Self::output_notes`] by the constructor or + /// deserialization, so it must not be trusted at a trust boundary until a consumer binds it to + /// the output notes. pub fn note_tree_root(&self) -> Word { self.note_tree_root } diff --git a/crates/miden-testing/src/kernel_tests/batch/proposed_batch.rs b/crates/miden-testing/src/kernel_tests/batch/proposed_batch.rs index ea61473775..c34cfc2bf9 100644 --- a/crates/miden-testing/src/kernel_tests/batch/proposed_batch.rs +++ b/crates/miden-testing/src/kernel_tests/batch/proposed_batch.rs @@ -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::{BatchNoteTree, ProposedBatch}; +use miden_protocol::batch::{BatchNoteTree, ProposedBatch, ProvenBatch}; use miden_protocol::block::BlockNumber; use miden_protocol::crypto::merkle::MerkleError; use miden_protocol::errors::{BatchAccountUpdateError, ProposedBatchError}; @@ -30,6 +30,7 @@ use miden_protocol::transaction::{ ProvenTransaction, RawOutputNote, }; +use miden_protocol::utils::serde::{Deserializable, Serializable}; use miden_standards::account::interface::{AccountInterface, AccountInterfaceExt}; use miden_standards::note::P2idNoteStorage; use miden_standards::testing::account_component::MockAccountComponent; @@ -342,6 +343,37 @@ fn batch_note_tree_empty_when_no_output_notes() -> anyhow::Result<()> { Ok(()) } +/// Tests that a proven batch's note tree root survives a serialization round-trip. +#[test] +fn proven_batch_serialization_preserves_note_tree_root() -> 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::>(); + let tx = + MockProvenTxBuilder::with_account(account1.id(), Word::empty(), account1.to_commitment()) + .reference_block(&block1) + .output_notes(output_notes) + .build()?; + + let proposed_batch = ProposedBatch::new_unverified( + [tx].into_iter().map(Arc::new).collect(), + block2.header().clone(), + chain.latest_partial_blockchain(), + BTreeMap::default(), + )?; + let expected_root = proposed_batch.batch_note_tree().root(); + let proven_batch = chain.prove_transaction_batch(proposed_batch)?; + assert_eq!(proven_batch.note_tree_root(), expected_root); + + let deserialized = ProvenBatch::read_from_bytes(&proven_batch.to_bytes()).unwrap(); + assert_eq!(deserialized, proven_batch); + assert_eq!(deserialized.note_tree_root(), expected_root); + + Ok(()) +} + /// Notes with the same details but different metadata are not considered the same for batch /// erasure. #[test]