Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow blocks with empty batches #1172

Merged
merged 10 commits into from
Feb 24, 2025
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
- Added serialization for `ProposedBatch`, `BatchId`, `BatchNoteTree` and `ProvenBatch` (#1140).
- Added `prefix` to `Nullifier` (#1153).
- [BREAKING] Implemented a `RemoteBatchProver`. `miden-proving-service` workers can prove batches (#1142).
- [BREAKING] Implement `LocalBlockProver` and rename `Block` to `ProvenBlock` (#1152, #1168).
- [BREAKING] Implement `LocalBlockProver` and rename `Block` to `ProvenBlock` (#1152, #1168, #1172).
- [BREAKING] Added native types to `AccountComponentTemplate` (#1124).
- Implemented `RemoteBlockProver`. `miden-proving-service` workers can prove blocks (#1169).

Expand Down
13 changes: 12 additions & 1 deletion crates/miden-block-prover/src/local_block_prover.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,11 @@ fn compute_nullifiers(
prev_block_header: &BlockHeader,
block_num: BlockNumber,
) -> Result<(Vec<Nullifier>, Digest), ProvenBlockError> {
// If no nullifiers were created, the nullifier tree root is unchanged.
if created_nullifiers.is_empty() {
return Ok((Vec::new(), prev_block_header.nullifier_root()));
}

let nullifiers: Vec<Nullifier> = created_nullifiers.keys().copied().collect();

let mut partial_nullifier_tree = PartialNullifierTree::new();
Expand Down Expand Up @@ -234,6 +239,11 @@ fn compute_account_root(
updated_accounts: &mut Vec<(AccountId, AccountUpdateWitness)>,
prev_block_header: &BlockHeader,
) -> Result<Digest, ProvenBlockError> {
// If no accounts were updated, the account tree root is unchanged.
if updated_accounts.is_empty() {
return Ok(prev_block_header.account_root());
}

let mut partial_account_tree = PartialMerkleTree::new();

// First reconstruct the current account tree from the provided merkle paths.
Expand Down Expand Up @@ -286,7 +296,8 @@ fn compute_block_note_tree(output_note_batches: &[OutputNoteBatch]) -> BlockNote
// SAFETY: The proposed block contains at most the max allowed number of
// batches and each batch is guaranteed to contain at most
// the max allowed number of output notes.
BlockNoteIndex::new(batch_idx, *note_idx_in_batch),
BlockNoteIndex::new(batch_idx, *note_idx_in_batch)
.expect("max batches in block and max notes in batches should be enforced"),
note.id(),
*note.metadata(),
)
Expand Down
19 changes: 0 additions & 19 deletions crates/miden-block-prover/src/tests/proposed_block_errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,6 @@ use crate::tests::utils::{
generate_untracked_note_with_output_note, setup_chain, ProvenTransactionExt, TestSetup,
};

/// Tests that empty batches produce an error.
#[test]
fn proposed_block_fails_on_empty_batches() -> anyhow::Result<()> {
let TestSetup { chain, .. } = setup_chain(2);

let block_inputs = BlockInputs::new(
chain.latest_block_header(),
chain.latest_chain_mmr(),
BTreeMap::default(),
BTreeMap::default(),
BTreeMap::default(),
);
let error = ProposedBlock::new(block_inputs, Vec::new()).unwrap_err();

assert_matches!(error, ProposedBlockError::EmptyBlock);

Ok(())
}

/// Tests that too many batches produce an error.
#[test]
fn proposed_block_fails_on_too_many_batches() -> anyhow::Result<()> {
Expand Down
40 changes: 32 additions & 8 deletions crates/miden-block-prover/src/tests/proposed_block_success.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ use std::{collections::BTreeMap, vec::Vec};

use anyhow::Context;
use miden_objects::{
account::AccountId, block::ProposedBlock,
testing::account_id::ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN, transaction::ProvenTransaction,
account::AccountId,
block::{BlockInputs, ProposedBlock},
testing::account_id::ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN,
transaction::ProvenTransaction,
};

use crate::tests::utils::{
Expand All @@ -13,6 +15,28 @@ use crate::tests::utils::{
ProvenTransactionExt, TestSetup,
};

/// Tests that we can build empty blocks.
#[test]
fn proposed_block_succeeds_with_empty_batches() -> anyhow::Result<()> {
let TestSetup { chain, .. } = setup_chain(2);

let block_inputs = BlockInputs::new(
chain.latest_block_header(),
chain.latest_chain_mmr(),
BTreeMap::default(),
BTreeMap::default(),
BTreeMap::default(),
);
let block = ProposedBlock::new(block_inputs, Vec::new()).context("failed to propose block")?;

assert_eq!(block.affected_accounts().count(), 0);
assert_eq!(block.output_note_batches().len(), 0);
assert_eq!(block.created_nullifiers().len(), 0);
assert_eq!(block.batches().len(), 0);

Ok(())
}

/// Tests that a proposed block from two batches with one transaction each can be successfully
/// built.
#[test]
Expand Down Expand Up @@ -48,12 +72,12 @@ fn proposed_block_basic_success() -> anyhow::Result<()> {
proven_tx1.account_update().final_state_hash()
);
// Each tx consumes one note.
assert_eq!(proposed_block.nullifiers().len(), 2);
assert_eq!(proposed_block.created_nullifiers().len(), 2);
assert!(proposed_block
.nullifiers()
.created_nullifiers()
.contains_key(&proven_tx0.input_notes().get_note(0).nullifier()));
assert!(proposed_block
.nullifiers()
.created_nullifiers()
.contains_key(&proven_tx1.input_notes().get_note(0).nullifier()));

// There are two batches in the block...
Expand Down Expand Up @@ -184,9 +208,9 @@ fn proposed_block_authenticating_unauthenticated_notes() -> anyhow::Result<()> {

// We expect both notes to have been authenticated and therefore should be part of the
// nullifiers of this block.
assert_eq!(proposed_block.nullifiers().len(), 2);
assert!(proposed_block.nullifiers().contains_key(&note0.nullifier()));
assert!(proposed_block.nullifiers().contains_key(&note1.nullifier()));
assert_eq!(proposed_block.created_nullifiers().len(), 2);
assert!(proposed_block.created_nullifiers().contains_key(&note0.nullifier()));
assert!(proposed_block.created_nullifiers().contains_key(&note1.nullifier()));
// There are two batches in the block...
assert_eq!(proposed_block.output_note_batches().len(), 2);
// ... but none of them create notes.
Expand Down
2 changes: 1 addition & 1 deletion crates/miden-block-prover/src/tests/proven_block_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ fn proven_block_fails_on_nullifier_tree_root_mismatch() -> anyhow::Result<()> {

// Make the block inputs invalid by using a single stale nullifier witnesses.
let mut invalid_nullifier_witness_block_inputs = valid_block_inputs.clone();
let batch_nullifier0 = batches[0].produced_nullifiers().next().unwrap();
let batch_nullifier0 = batches[0].created_nullifiers().next().unwrap();
core::mem::swap(
invalid_nullifier_witness_block_inputs
.nullifier_witnesses_mut()
Expand Down
103 changes: 93 additions & 10 deletions crates/miden-block-prover/src/tests/proven_block_success.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
use std::{collections::BTreeMap, vec::Vec};

use anyhow::Context;
use miden_crypto::merkle::LeafIndex;
use miden_crypto::merkle::{LeafIndex, SimpleSmt, Smt};
use miden_objects::{
batch::BatchNoteTree,
block::{BlockNoteIndex, BlockNoteTree, ProposedBlock},
block::{BlockInputs, BlockNoteIndex, BlockNoteTree, ProposedBlock},
transaction::InputNoteCommitment,
Felt, FieldElement, MIN_PROOF_SECURITY_LEVEL,
Felt, FieldElement, ACCOUNT_TREE_DEPTH, MIN_PROOF_SECURITY_LEVEL,
};
use rand::Rng;

use crate::{
tests::utils::{
generate_batch, generate_output_note, generate_tx_with_authenticated_notes,
generate_batch, generate_executed_tx_with_authenticated_notes, generate_output_note,
generate_tracked_note, generate_tx_with_authenticated_notes,
generate_tx_with_unauthenticated_notes, generate_untracked_note_with_output_note,
setup_chain, TestSetup,
},
Expand Down Expand Up @@ -83,7 +84,11 @@ fn proven_block_success() -> anyhow::Result<()> {

let expected_block_note_tree = BlockNoteTree::with_entries(batch0_iter.chain(batch1_iter).map(
|(batch_idx, note_idx_in_batch, note)| {
(BlockNoteIndex::new(batch_idx, note_idx_in_batch), note.id(), *note.metadata())
(
BlockNoteIndex::new(batch_idx, note_idx_in_batch).unwrap(),
note.id(),
*note.metadata(),
)
},
))
.unwrap();
Expand All @@ -92,7 +97,7 @@ fn proven_block_success() -> anyhow::Result<()> {
// --------------------------------------------------------------------------------------------

let mut expected_nullifier_tree = chain.nullifiers().clone();
for nullifier in proposed_block.nullifiers().keys() {
for nullifier in proposed_block.created_nullifiers().keys() {
expected_nullifier_tree.insert(
nullifier.inner(),
[Felt::from(proposed_block.block_num()), Felt::ZERO, Felt::ZERO, Felt::ZERO],
Expand Down Expand Up @@ -265,10 +270,10 @@ fn proven_block_erasing_unauthenticated_notes() -> anyhow::Result<()> {

// The output note should have been erased, so we expect only the nullifiers of note0, note2 and
// note3 to be created.
assert_eq!(proposed_block.nullifiers().len(), 3);
assert!(proposed_block.nullifiers().contains_key(&note0.nullifier()));
assert!(proposed_block.nullifiers().contains_key(&note2.nullifier()));
assert!(proposed_block.nullifiers().contains_key(&note3.nullifier()));
assert_eq!(proposed_block.created_nullifiers().len(), 3);
assert!(proposed_block.created_nullifiers().contains_key(&note0.nullifier()));
assert!(proposed_block.created_nullifiers().contains_key(&note2.nullifier()));
assert!(proposed_block.created_nullifiers().contains_key(&note3.nullifier()));

// There are two batches in the block.
assert_eq!(proposed_block.output_note_batches().len(), 2);
Expand Down Expand Up @@ -324,3 +329,81 @@ fn proven_block_erasing_unauthenticated_notes() -> anyhow::Result<()> {

Ok(())
}

/// Tests that we can build empty blocks.
#[test]
fn proven_block_succeeds_with_empty_batches() -> anyhow::Result<()> {
// Setup a chain with a non-empty nullifier tree by consuming some notes.
// --------------------------------------------------------------------------------------------

let TestSetup { mut chain, mut accounts, .. } = setup_chain(2);

let account0 = accounts.remove(&0).unwrap();
let account1 = accounts.remove(&1).unwrap();

// Add notes to the chain we can consume.
let note0 = generate_tracked_note(&mut chain, account1.id(), account0.id());
let note1 = generate_tracked_note(&mut chain, account0.id(), account1.id());
chain.seal_block(None);

let tx0 =
generate_executed_tx_with_authenticated_notes(&mut chain, account0.id(), &[note0.id()]);
let tx1 =
generate_executed_tx_with_authenticated_notes(&mut chain, account1.id(), &[note1.id()]);

chain.apply_executed_transaction(&tx0);
chain.apply_executed_transaction(&tx1);
let blockx = chain.seal_block(None);

// Build a block with empty inputs whose account tree and nullifier tree root are not the empty
// roots.
// If they are the empty roots, we do not run the branches of code that handle empty blocks.
// --------------------------------------------------------------------------------------------

let latest_block_header = chain.latest_block_header();
assert_eq!(latest_block_header.hash(), blockx.hash());

// Sanity check: The account and nullifier tree roots should not be the empty tree roots.
assert_ne!(
latest_block_header.account_root(),
SimpleSmt::<ACCOUNT_TREE_DEPTH>::new().unwrap().root()
);
assert_ne!(latest_block_header.nullifier_root(), Smt::new().root());

let (_, empty_chain_mmr) = chain.latest_selective_chain_mmr([]);
assert_eq!(empty_chain_mmr.block_headers().count(), 0);

let block_inputs = BlockInputs::new(
latest_block_header,
empty_chain_mmr.clone(),
BTreeMap::default(),
BTreeMap::default(),
BTreeMap::default(),
);

let proposed_block =
ProposedBlock::new(block_inputs, Vec::new()).context("failed to propose block")?;

let proven_block = LocalBlockProver::new(MIN_PROOF_SECURITY_LEVEL)
.prove_without_batch_verification(proposed_block)
.context("failed to prove proposed block")?;

// Nothing should be created or updated.
assert_eq!(proven_block.updated_accounts().len(), 0);
assert_eq!(proven_block.output_note_batches().len(), 0);
assert_eq!(proven_block.created_nullifiers().len(), 0);
assert!(proven_block.build_output_note_tree().is_empty());

// Account and nullifier root should match the previous block header's roots, since nothing has
// changed.
assert_eq!(proven_block.header().account_root(), latest_block_header.account_root());
assert_eq!(proven_block.header().nullifier_root(), latest_block_header.nullifier_root());
// Block note tree should be the empty root.
assert_eq!(proven_block.header().note_root(), BlockNoteTree::empty().root());

// The previous block header should have been added to the chain.
assert_eq!(proven_block.header().chain_root(), chain.block_chain().peaks().hash_peaks());
assert_eq!(proven_block.header().block_num(), latest_block_header.block_num() + 1);

Ok(())
}
Loading
Loading