use core::slice;
use std::collections::BTreeMap;
use std::vec::Vec;
use assert_matches::assert_matches;
use miden_processor::crypto::merkle::MerklePath;
use miden_protocol::MAX_BATCHES_PER_BLOCK;
use miden_protocol::asset::FungibleAsset;
use miden_protocol::block::{BlockInputs, BlockNumber, ProposedBlock};
use miden_protocol::crypto::merkle::SparseMerklePath;
use miden_protocol::errors::ProposedBlockError;
use miden_protocol::note::{NoteAttachment, NoteInclusionProof, NoteType};
use miden_standards::note::P2idNote;
use miden_tx::LocalTransactionProver;
use crate::kernel_tests::block::utils::MockChainBlockExt;
use crate::utils::create_p2any_note;
use crate::{Auth, MockChain};
#[tokio::test]
async fn proposed_block_fails_on_too_many_batches() -> anyhow::Result<()> {
let count = MAX_BATCHES_PER_BLOCK + 1;
let (chain, batches) = {
let mut builder = MockChain::builder();
let mut accounts = Vec::new();
let mut notes = Vec::new();
for _ in 0..count {
let account = builder.add_existing_mock_account(Auth::IncrNonce)?;
let note = builder.add_p2any_note(
account.id(),
NoteType::Public,
[FungibleAsset::mock(42)],
)?;
accounts.push(account);
notes.push(note);
}
let chain = builder.build()?;
let mut batches = Vec::with_capacity(count);
for i in 0..count {
let proven_tx = chain
.create_authenticated_notes_proven_tx(accounts[i].id(), [notes[i].id()])
.await?;
batches.push(chain.create_batch(vec![proven_tx])?);
}
(chain, batches)
};
let block_inputs = BlockInputs::new(
chain.latest_block_header(),
chain.latest_partial_blockchain(),
BTreeMap::default(),
BTreeMap::default(),
BTreeMap::default(),
);
let error = ProposedBlock::new(block_inputs, batches).unwrap_err();
assert_matches!(error, ProposedBlockError::TooManyBatches);
Ok(())
}
#[tokio::test]
async fn proposed_block_fails_on_duplicate_batches() -> anyhow::Result<()> {
let mut builder = MockChain::builder();
let sender_account = builder.add_existing_mock_account(Auth::IncrNonce)?;
let note =
builder.add_p2any_note(sender_account.id(), NoteType::Public, [FungibleAsset::mock(42)])?;
let chain = builder.build()?;
let proven_tx0 = chain
.create_authenticated_notes_proven_tx(sender_account.id(), [note.id()])
.await?;
let batch0 = chain.create_batch(vec![proven_tx0])?;
let batches = vec![batch0.clone(), batch0.clone()];
let block_inputs = BlockInputs::new(
chain.latest_block_header(),
chain.latest_partial_blockchain(),
BTreeMap::default(),
BTreeMap::default(),
BTreeMap::default(),
);
let error = ProposedBlock::new(block_inputs, batches).unwrap_err();
assert_matches!(error, ProposedBlockError::DuplicateBatch { batch_id } if batch_id == batch0.id());
Ok(())
}
#[tokio::test]
async fn proposed_block_fails_on_expired_batches() -> anyhow::Result<()> {
let mut builder = MockChain::builder();
let account0 = builder.add_existing_mock_account(Auth::IncrNonce)?;
let account1 = builder.add_existing_mock_account(Auth::IncrNonce)?;
let mut chain = builder.build()?;
chain.prove_next_block()?;
let block1_num = chain.block_header(1).block_num();
let tx0 = chain.create_expiring_proven_tx(account0.id(), block1_num + 5).await?;
let tx1 = chain.create_expiring_proven_tx(account1.id(), block1_num + 1).await?;
let batch0 = chain.create_batch(vec![tx0])?;
let batch1 = chain.create_batch(vec![tx1])?;
let _block2 = chain.prove_next_block()?;
let batches = vec![batch0.clone(), batch1.clone()];
let block_inputs = chain.get_block_inputs(&batches).expect("failed to get block inputs");
let error = ProposedBlock::new(block_inputs.clone(), batches.clone()).unwrap_err();
assert_matches!(
error,
ProposedBlockError::ExpiredBatch {
batch_id,
batch_expiration_block_num,
current_block_num
} if batch_id == batch1.id() &&
batch_expiration_block_num.as_u32() == 2 &&
current_block_num.as_u32() == 3
);
Ok(())
}
#[tokio::test]
async fn proposed_block_fails_on_timestamp_not_increasing_monotonically() -> anyhow::Result<()> {
let mut builder = MockChain::builder();
let account = builder.add_existing_mock_account(Auth::IncrNonce)?;
let chain = builder.build()?;
let proven_tx0 = chain.create_authenticated_notes_proven_tx(account, []).await?;
let batch0 = chain.create_batch(vec![proven_tx0])?;
let batches = vec![batch0];
let block_inputs = BlockInputs::new(
chain.latest_block_header(),
chain.latest_partial_blockchain(),
BTreeMap::default(),
BTreeMap::default(),
BTreeMap::default(),
);
let prev_block_timestamp = block_inputs.prev_block_header().timestamp();
let error =
ProposedBlock::new_at(block_inputs.clone(), batches.clone(), prev_block_timestamp - 1)
.unwrap_err();
assert_matches!(error, ProposedBlockError::TimestampDoesNotIncreaseMonotonically { .. });
let error = ProposedBlock::new_at(block_inputs, batches, prev_block_timestamp).unwrap_err();
assert_matches!(error, ProposedBlockError::TimestampDoesNotIncreaseMonotonically { .. });
Ok(())
}
#[tokio::test]
async fn proposed_block_fails_on_partial_blockchain_and_prev_block_inconsistency()
-> anyhow::Result<()> {
let mut builder = MockChain::builder();
let account = builder.add_existing_mock_account(Auth::IncrNonce)?;
let chain = builder.build()?;
let proven_tx0 = chain.create_authenticated_notes_proven_tx(account, []).await?;
let batch0 = chain.create_batch(vec![proven_tx0])?;
let batches = vec![batch0];
let mut partial_blockchain = chain.latest_partial_blockchain();
let block2 = chain.clone().prove_next_block()?;
let block_inputs = BlockInputs::new(
block2.header().clone(),
partial_blockchain.clone(),
BTreeMap::default(),
BTreeMap::default(),
BTreeMap::default(),
);
let error = ProposedBlock::new(block_inputs.clone(), batches.clone()).unwrap_err();
assert_matches!(
error,
ProposedBlockError::ChainLengthNotEqualToPreviousBlockNumber {
chain_length,
prev_block_num
} if chain_length == partial_blockchain.chain_length() &&
prev_block_num == block2.header().block_num()
);
partial_blockchain.partial_mmr_mut().add(block2.header().nullifier_root(), true);
let block_inputs = BlockInputs::new(
block2.header().clone(),
partial_blockchain.clone(),
BTreeMap::default(),
BTreeMap::default(),
BTreeMap::default(),
);
let error = ProposedBlock::new(block_inputs.clone(), batches.clone()).unwrap_err();
assert_matches!(
error,
ProposedBlockError::ChainRootNotEqualToPreviousBlockChainCommitment { .. }
);
Ok(())
}
#[tokio::test]
async fn proposed_block_fails_on_missing_batch_reference_block() -> anyhow::Result<()> {
let mut builder = MockChain::builder();
let account = builder.add_existing_mock_account(Auth::IncrNonce)?;
let mut chain = builder.build()?;
chain.prove_next_block()?;
let proven_tx0 = chain.create_authenticated_notes_proven_tx(account, []).await?;
let batch0 = chain.create_batch(vec![proven_tx0.clone()])?;
let batches = vec![batch0.clone()];
let block2 = chain.prove_next_block()?;
let (_, partial_blockchain) =
chain.latest_selective_partial_blockchain([BlockNumber::GENESIS])?;
let block_inputs = BlockInputs::new(
block2.header().clone(),
partial_blockchain.clone(),
BTreeMap::default(),
BTreeMap::default(),
BTreeMap::default(),
);
let error = ProposedBlock::new(block_inputs.clone(), batches.clone()).unwrap_err();
assert_matches!(
error,
ProposedBlockError::BatchReferenceBlockMissingFromChain {
reference_block_num,
batch_id
} if reference_block_num == batch0.reference_block_num() &&
batch_id == batch0.id()
);
Ok(())
}
#[tokio::test]
async fn proposed_block_fails_on_duplicate_input_note() -> anyhow::Result<()> {
let mut builder = MockChain::builder();
let account0 = builder.add_existing_mock_account(Auth::IncrNonce)?;
let account1 = builder.add_existing_mock_account(Auth::IncrNonce)?;
let note0 = builder.add_p2any_note(account0.id(), NoteType::Public, [])?;
let note1 = builder.add_p2any_note(account0.id(), NoteType::Public, [])?;
let mut chain = builder.build()?;
assert_ne!(note0.id(), note1.id());
chain.prove_next_block()?;
let tx0 = chain
.create_authenticated_notes_proven_tx(account1.id(), [note0.id(), note1.id()])
.await?;
let tx1 = chain.create_authenticated_notes_proven_tx(account1.id(), [note0.id()]).await?;
let batch0 = chain.create_batch(vec![tx0])?;
let batch1 = chain.create_batch(vec![tx1])?;
let batches = vec![batch0.clone(), batch1.clone()];
let block_inputs = chain.get_block_inputs(&batches).expect("failed to get block inputs");
let error = ProposedBlock::new(block_inputs.clone(), batches.clone()).unwrap_err();
assert_matches!(error, ProposedBlockError::DuplicateInputNote { .. });
Ok(())
}
#[tokio::test]
async fn proposed_block_fails_on_duplicate_output_note() -> anyhow::Result<()> {
let mut builder = MockChain::builder();
let account = builder.add_existing_mock_account(Auth::IncrNonce)?;
let output_note = create_p2any_note(account.id(), NoteType::Private, [], builder.rng_mut());
let note0 = builder.add_spawn_note([&output_note])?;
let note1 = builder.add_spawn_note([&output_note])?;
let mut chain = builder.build()?;
chain.prove_next_block()?;
let tx0 = chain.create_authenticated_notes_proven_tx(account.id(), [note0.id()]).await?;
let tx1 = chain.create_authenticated_notes_proven_tx(account.id(), [note1.id()]).await?;
let batch0 = chain.create_batch(vec![tx0])?;
let batch1 = chain.create_batch(vec![tx1])?;
let batches = vec![batch0.clone(), batch1.clone()];
let block_inputs = chain.get_block_inputs(&batches)?;
let error = ProposedBlock::new(block_inputs.clone(), batches.clone()).unwrap_err();
assert_matches!(error, ProposedBlockError::DuplicateOutputNote { .. });
Ok(())
}
#[tokio::test]
async fn proposed_block_fails_on_invalid_proof_or_missing_note_inclusion_reference_block()
-> anyhow::Result<()> {
let mut builder = MockChain::builder();
let account0 = builder.add_existing_mock_account(Auth::IncrNonce)?;
let account1 = builder.add_existing_mock_account(Auth::IncrNonce)?;
let p2id_note = P2idNote::create(
account0.id(),
account1.id(),
vec![],
NoteType::Private,
NoteAttachment::default(),
builder.rng_mut(),
)?;
let spawn_note = builder.add_spawn_note([&p2id_note])?;
let mut chain = builder.build()?;
let tx0 = chain
.create_unauthenticated_notes_proven_tx(account1.id(), slice::from_ref(&p2id_note))
.await?;
let batch0 = chain.create_batch(vec![tx0])?;
let tx = chain
.build_tx_context(account0.id(), &[spawn_note.id()], &[])?
.build()?
.execute()
.await?;
chain.add_pending_executed_transaction(&tx)?;
let block2 = chain.prove_next_block()?;
let _block3 = chain.prove_next_block()?;
let batches = vec![batch0.clone()];
let original_block_inputs = chain.get_block_inputs(&batches)?;
let mut invalid_block_inputs = original_block_inputs.clone();
invalid_block_inputs
.partial_blockchain_mut()
.partial_mmr_mut()
.untrack(block2.header().block_num().as_usize());
invalid_block_inputs
.partial_blockchain_mut()
.block_headers_mut()
.remove(&block2.header().block_num())
.expect("block2 should have been fetched");
let error = ProposedBlock::new(invalid_block_inputs, batches.clone()).unwrap_err();
assert_matches!(error, ProposedBlockError::UnauthenticatedInputNoteBlockNotInPartialBlockchain {
block_number, note_id
} => {
assert_eq!(block_number, block2.header().block_num());
assert_eq!(note_id, p2id_note.id());
});
let original_note_proof = original_block_inputs
.unauthenticated_note_proofs()
.get(&p2id_note.id())
.expect("note proof should have been fetched")
.clone();
let mut original_merkle_path = MerklePath::from(original_note_proof.note_path().clone());
original_merkle_path.push(block2.header().commitment());
let invalid_note_path = SparseMerklePath::try_from(original_merkle_path).unwrap();
let invalid_note_proof = NoteInclusionProof::new(
original_note_proof.location().block_num(),
original_note_proof.location().block_note_tree_index(),
invalid_note_path,
)
.unwrap();
let mut invalid_block_inputs = original_block_inputs.clone();
invalid_block_inputs
.unauthenticated_note_proofs_mut()
.insert(p2id_note.id(), invalid_note_proof);
let error = ProposedBlock::new(invalid_block_inputs, batches.clone()).unwrap_err();
assert_matches!(error, ProposedBlockError::UnauthenticatedNoteAuthenticationFailed { block_num, note_id, .. } if block_num == block2.header().block_num() && note_id == p2id_note.id());
Ok(())
}
#[tokio::test]
async fn proposed_block_fails_on_missing_note_inclusion_proof() -> anyhow::Result<()> {
let mut builder = MockChain::builder();
let account0 = builder.add_existing_mock_account(Auth::IncrNonce)?;
let account1 = builder.add_existing_mock_account(Auth::IncrNonce)?;
let note0 = create_p2any_note(account0.id(), NoteType::Private, [], builder.rng_mut());
let chain = builder.build()?;
let tx0 = chain
.create_unauthenticated_notes_proven_tx(account1.id(), slice::from_ref(¬e0))
.await?;
let batch0 = chain.create_batch(vec![tx0])?;
let batches = vec![batch0.clone()];
let block_inputs = chain.get_block_inputs(&batches)?;
let error = ProposedBlock::new(block_inputs, batches.clone()).unwrap_err();
assert_matches!(error, ProposedBlockError::UnauthenticatedNoteConsumed { nullifier } if nullifier == note0.nullifier());
Ok(())
}
#[tokio::test]
async fn proposed_block_fails_on_missing_nullifier_witness() -> anyhow::Result<()> {
let mut builder = MockChain::builder();
let account = builder.add_existing_mock_account(Auth::IncrNonce)?;
let p2id_note =
builder.add_p2any_note(account.id(), NoteType::Public, [FungibleAsset::mock(50)])?;
let mut chain = builder.build()?;
chain.prove_next_block()?;
let tx0 = chain
.create_unauthenticated_notes_proven_tx(account.id(), slice::from_ref(&p2id_note))
.await?;
let batch0 = chain.create_batch(vec![tx0])?;
let batches = vec![batch0.clone()];
let block_inputs = chain.get_block_inputs(&batches)?;
let mut invalid_block_inputs = block_inputs.clone();
invalid_block_inputs
.nullifier_witnesses_mut()
.remove(&p2id_note.nullifier())
.expect("nullifier should have been fetched");
let error = ProposedBlock::new(invalid_block_inputs, batches.clone()).unwrap_err();
assert_matches!(error, ProposedBlockError::NullifierProofMissing(nullifier) => {
assert_eq!(nullifier, p2id_note.nullifier());
});
Ok(())
}
#[tokio::test]
async fn proposed_block_fails_on_spent_nullifier_witness() -> anyhow::Result<()> {
let mut builder = MockChain::builder();
let account0 = builder.add_existing_mock_account(Auth::IncrNonce)?;
let account1 = builder.add_existing_mock_account(Auth::IncrNonce)?;
let p2any_note =
builder.add_p2any_note(account0.id(), NoteType::Public, [FungibleAsset::mock(50)])?;
let mut chain = builder.build()?;
chain.prove_next_block()?;
let tx0 = chain
.create_authenticated_notes_proven_tx(account0.id(), [p2any_note.id()])
.await?;
chain.add_pending_proven_transaction(tx0);
chain.prove_next_block()?;
let tx1 = chain
.create_authenticated_notes_proven_tx(account1.id(), [p2any_note.id()])
.await?;
let batch1 = chain.create_batch(vec![tx1])?;
let batches = vec![batch1];
let block_inputs = chain.get_block_inputs(&batches)?;
assert!(block_inputs.nullifier_witnesses().contains_key(&p2any_note.nullifier()));
let error = ProposedBlock::new(block_inputs, batches).unwrap_err();
assert_matches!(error, ProposedBlockError::NullifierSpent(nullifier) => {
assert_eq!(nullifier, p2any_note.nullifier())
});
Ok(())
}
#[tokio::test]
async fn proposed_block_fails_on_conflicting_transactions_updating_same_account()
-> anyhow::Result<()> {
let mut builder = MockChain::builder();
let account1 = builder.add_existing_mock_account(Auth::IncrNonce)?;
let note0 =
builder.add_p2any_note(account1.id(), NoteType::Public, [FungibleAsset::mock(100)])?;
let note1 =
builder.add_p2any_note(account1.id(), NoteType::Public, [FungibleAsset::mock(200)])?;
let chain = builder.build()?;
assert_ne!(note0.id(), note1.id());
let tx0 = chain.create_authenticated_notes_proven_tx(account1.id(), [note0.id()]).await?;
let tx1 = chain.create_authenticated_notes_proven_tx(account1.id(), [note1.id()]).await?;
let batch0 = chain.create_batch(vec![tx0])?;
let batch1 = chain.create_batch(vec![tx1])?;
let batches = vec![batch0.clone(), batch1.clone()];
let block_inputs = chain.get_block_inputs(&batches).expect("failed to get block inputs");
let error = ProposedBlock::new(block_inputs.clone(), batches).unwrap_err();
assert_matches!(error, ProposedBlockError::ConflictingBatchesUpdateSameAccount {
account_id,
initial_state_commitment,
first_batch_id,
second_batch_id
} if account_id == account1.id() &&
initial_state_commitment == account1.initial_commitment() &&
first_batch_id == batch0.id() &&
second_batch_id == batch1.id()
);
Ok(())
}
#[tokio::test]
async fn proposed_block_fails_on_missing_account_witness() -> anyhow::Result<()> {
let mut builder = MockChain::builder();
let account = builder.add_existing_mock_account(Auth::IncrNonce)?;
let chain = builder.build()?;
let tx0 = chain.create_authenticated_notes_proven_tx(account.id(), []).await?;
let batch0 = chain.create_batch(vec![tx0])?;
let batches = vec![batch0.clone()];
let mut block_inputs = chain.get_block_inputs(&batches)?;
block_inputs
.account_witnesses_mut()
.remove(&account.id())
.expect("account witness should have been fetched");
let error = ProposedBlock::new(block_inputs, batches.clone()).unwrap_err();
assert_matches!(error, ProposedBlockError::MissingAccountWitness(account_id) if account_id == account.id());
Ok(())
}
#[tokio::test]
async fn proposed_block_fails_on_inconsistent_account_state_transition() -> anyhow::Result<()> {
let asset = FungibleAsset::mock(200);
let mut builder = MockChain::builder();
let mut account = builder.add_existing_mock_account(Auth::IncrNonce)?;
let note0 = builder.add_p2any_note(account.id(), NoteType::Public, [asset])?;
let note1 = builder.add_p2any_note(account.id(), NoteType::Public, [asset])?;
let note2 = builder.add_p2any_note(account.id(), NoteType::Public, [asset])?;
let chain = builder.build()?;
let executed_tx0 = chain.create_authenticated_notes_tx(account.clone(), [note0.id()]).await?;
account.apply_delta(executed_tx0.account_delta())?;
let executed_tx1 = chain.create_authenticated_notes_tx(account.clone(), [note1.id()]).await?;
account.apply_delta(executed_tx1.account_delta())?;
let executed_tx2 = chain.create_authenticated_notes_tx(account.clone(), [note2.id()]).await?;
let tx0 = LocalTransactionProver::default().prove_dummy(executed_tx0.clone())?;
let tx2 = LocalTransactionProver::default().prove_dummy(executed_tx2.clone())?;
let batch0 = chain.create_batch(vec![tx0])?;
let batch1 = chain.create_batch(vec![tx2])?;
let batches = vec![batch0.clone(), batch1.clone()];
let block_inputs = chain.get_block_inputs(&batches)?;
let error = ProposedBlock::new(block_inputs, batches).unwrap_err();
assert_matches!(error, ProposedBlockError::InconsistentAccountStateTransition {
account_id,
state_commitment,
remaining_state_commitments
} if account_id == account.id() &&
state_commitment == executed_tx0.final_account().to_commitment() &&
remaining_state_commitments == [executed_tx2.initial_account().to_commitment()]
);
Ok(())
}