use core::slice;
use std::collections::BTreeMap;
use std::vec::Vec;
use anyhow::Context;
use assert_matches::assert_matches;
use miden_protocol::Felt;
use miden_protocol::account::delta::AccountUpdateDetails;
use miden_protocol::account::{Account, AccountId, AccountStorageMode};
use miden_protocol::asset::FungibleAsset;
use miden_protocol::block::{BlockInputs, ProposedBlock};
use miden_protocol::note::{Note, NoteType};
use miden_protocol::testing::account_id::ACCOUNT_ID_SENDER;
use miden_protocol::transaction::{ExecutedTransaction, RawOutputNote, TransactionHeader};
use miden_standards::testing::account_component::MockAccountComponent;
use miden_standards::testing::note::NoteBuilder;
use miden_tx::LocalTransactionProver;
use rand::Rng;
use super::utils::MockChainBlockExt;
use crate::{AccountState, Auth, MockChain, TxContextInput};
#[tokio::test]
async fn proposed_block_succeeds_with_empty_batches() -> anyhow::Result<()> {
let mut chain = MockChain::builder().build()?;
chain.prove_next_block()?;
let block_inputs = BlockInputs::new(
chain.latest_block_header(),
chain.latest_partial_blockchain(),
BTreeMap::default(),
BTreeMap::default(),
BTreeMap::default(),
);
let block = ProposedBlock::new(block_inputs, Vec::new()).context("failed to propose block")?;
assert_eq!(block.transactions().count(), 0);
assert_eq!(block.output_note_batches().len(), 0);
assert_eq!(block.created_nullifiers().len(), 0);
assert_eq!(block.batches().as_slice().len(), 0);
Ok(())
}
#[tokio::test]
async fn proposed_block_basic_success() -> 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, [FungibleAsset::mock(42)])?;
let note1 =
builder.add_p2any_note(account1.id(), NoteType::Public, [FungibleAsset::mock(42)])?;
let chain = builder.build()?;
let proven_tx0 =
chain.create_authenticated_notes_proven_tx(account0.id(), [note0.id()]).await?;
let proven_tx1 =
chain.create_authenticated_notes_proven_tx(account1.id(), [note1.id()]).await?;
let batch0 = chain.create_batch(vec![proven_tx0.clone()])?;
let batch1 = chain.create_batch(vec![proven_tx1.clone()])?;
let batches = [batch0, batch1];
let block_inputs = chain.get_block_inputs(&batches)?;
let proposed_block = ProposedBlock::new(block_inputs.clone(), batches.to_vec()).unwrap();
assert_eq!(proposed_block.batches().as_slice(), batches);
assert_eq!(proposed_block.block_num(), block_inputs.prev_block_header().block_num() + 1);
let updated_accounts =
proposed_block.updated_accounts().iter().cloned().collect::<BTreeMap<_, _>>();
assert_eq!(updated_accounts.len(), 2);
assert!(proposed_block.transactions().any(|tx_header| {
tx_header.id() == proven_tx0.id() && tx_header.account_id() == account0.id()
}));
assert!(proposed_block.transactions().any(|tx_header| {
tx_header.id() == proven_tx1.id() && tx_header.account_id() == account1.id()
}));
assert_eq!(
updated_accounts[&account0.id()].final_state_commitment(),
proven_tx0.account_update().final_state_commitment()
);
assert_eq!(
updated_accounts[&account1.id()].final_state_commitment(),
proven_tx1.account_update().final_state_commitment()
);
assert_eq!(proposed_block.created_nullifiers().len(), 2);
assert!(
proposed_block
.created_nullifiers()
.contains_key(&proven_tx0.input_notes().get_note(0).nullifier())
);
assert!(
proposed_block
.created_nullifiers()
.contains_key(&proven_tx1.input_notes().get_note(0).nullifier())
);
assert_eq!(proposed_block.output_note_batches().len(), 2);
assert!(proposed_block.output_note_batches()[0].is_empty());
assert!(proposed_block.output_note_batches()[1].is_empty());
Ok(())
}
#[tokio::test]
async fn proposed_block_aggregates_account_state_transition() -> anyhow::Result<()> {
let asset = FungibleAsset::mock(100);
let sender_id = AccountId::try_from(ACCOUNT_ID_SENDER)?;
let mut builder = MockChain::builder();
let mut account1 = builder.add_existing_mock_account(Auth::IncrNonce)?;
let note0 = builder.add_p2id_note(sender_id, account1.id(), &[asset], NoteType::Private)?;
let note1 = builder.add_p2id_note(sender_id, account1.id(), &[asset], NoteType::Public)?;
let note2 = builder.add_p2id_note(sender_id, account1.id(), &[asset], NoteType::Public)?;
let mut chain = builder.build()?;
chain.prove_next_block()?;
let executed_tx0 = chain.create_authenticated_notes_tx(account1.id(), [note0.id()]).await?;
account1.apply_delta(executed_tx0.account_delta())?;
let executed_tx1 = chain.create_authenticated_notes_tx(account1.clone(), [note1.id()]).await?;
account1.apply_delta(executed_tx1.account_delta())?;
let executed_tx2 = chain.create_authenticated_notes_tx(account1.clone(), [note2.id()]).await?;
let [tx0, tx1, tx2] = [executed_tx0, executed_tx1, executed_tx2]
.into_iter()
.map(|tx| LocalTransactionProver::default().prove_dummy(tx).unwrap())
.collect::<Vec<_>>()
.try_into()
.expect("we should have provided three executed txs");
let batch0 = chain.create_batch(vec![tx2.clone()])?;
let batch1 = chain.create_batch(vec![tx0.clone(), tx1.clone()])?;
let batches = vec![batch0.clone(), batch1.clone()];
let block_inputs = chain.get_block_inputs(&batches).unwrap();
let block =
ProposedBlock::new(block_inputs, batches).context("failed to build proposed block")?;
assert_eq!(block.updated_accounts().len(), 1);
let (account_id, account_update) = &block.updated_accounts()[0];
assert_eq!(*account_id, account1.id());
assert_eq!(
account_update.initial_state_commitment(),
tx0.account_update().initial_state_commitment()
);
assert_eq!(
account_update.final_state_commitment(),
tx2.account_update().final_state_commitment()
);
assert_eq!(
block.transactions().map(TransactionHeader::id).collect::<Vec<_>>(),
[tx2.id(), tx0.id(), tx1.id()]
);
assert_matches!(account_update.details(), AccountUpdateDetails::Delta(delta) => {
assert_eq!(delta.vault().fungible().num_assets(), 1);
assert_eq!(delta.vault().fungible().amount(&asset.unwrap_fungible().vault_key()).unwrap(), 300);
});
Ok(())
}
#[tokio::test]
async fn proposed_block_authenticating_unauthenticated_notes() -> anyhow::Result<()> {
let sender_id = AccountId::try_from(ACCOUNT_ID_SENDER)?;
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_p2id_note(sender_id, account0.id(), &[], NoteType::Private)?;
let note1 = builder.add_p2id_note(sender_id, account1.id(), &[], NoteType::Public)?;
let chain = builder.build()?;
let tx0 = chain
.create_unauthenticated_notes_proven_tx(account0.id(), slice::from_ref(¬e0))
.await?;
let tx1 = chain
.create_unauthenticated_notes_proven_tx(account1.id(), slice::from_ref(¬e1))
.await?;
let batch0 = chain.create_batch(vec![tx0.clone()])?;
let batch1 = chain.create_batch(vec![tx1.clone()])?;
let batches = [batch0, batch1];
let block_inputs = chain.get_block_inputs(&batches)?;
assert!(block_inputs.nullifier_witnesses().contains_key(¬e0.nullifier()));
assert!(block_inputs.nullifier_witnesses().contains_key(¬e1.nullifier()));
let proposed_block = ProposedBlock::new(block_inputs.clone(), batches.to_vec())
.context("failed to build proposed block")?;
assert_eq!(proposed_block.created_nullifiers().len(), 2);
assert!(proposed_block.created_nullifiers().contains_key(¬e0.nullifier()));
assert!(proposed_block.created_nullifiers().contains_key(¬e1.nullifier()));
assert_eq!(proposed_block.output_note_batches().len(), 2);
assert!(proposed_block.output_note_batches()[0].is_empty());
assert!(proposed_block.output_note_batches()[1].is_empty());
Ok(())
}
#[tokio::test]
async fn proposed_block_with_batch_at_expiration_limit() -> 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 + 2).await?;
let batch0 = chain.create_batch(vec![tx0])?;
let batch1 = chain.create_batch(vec![tx1])?;
assert_eq!(batch1.batch_expiration_block_num().as_u32(), 3);
let _block2 = chain.prove_next_block()?;
let batches = vec![batch0.clone(), batch1.clone()];
let block_inputs = chain.get_block_inputs(&batches)?;
ProposedBlock::new(block_inputs.clone(), batches.clone())?;
Ok(())
}
#[tokio::test]
async fn noop_tx_and_state_updating_tx_against_same_account_in_same_block() -> anyhow::Result<()> {
let account_builder = Account::builder(rand::rng().random())
.storage_mode(AccountStorageMode::Public)
.with_component(MockAccountComponent::with_empty_slots());
let mut builder = MockChain::builder();
let mut account0 = builder.add_account_from_builder(
Auth::Conditional,
account_builder,
AccountState::Exists,
)?;
let noop_note0 =
NoteBuilder::new(ACCOUNT_ID_SENDER.try_into().unwrap(), &mut rand::rng()).build()?;
let noop_note1 =
NoteBuilder::new(ACCOUNT_ID_SENDER.try_into().unwrap(), &mut rand::rng()).build()?;
builder.add_output_note(RawOutputNote::Full(noop_note0.clone()));
builder.add_output_note(RawOutputNote::Full(noop_note1.clone()));
let mut chain = builder.build()?;
let noop_tx = generate_conditional_tx(&mut chain, account0.id(), noop_note0, false).await;
account0.apply_delta(noop_tx.account_delta())?;
let state_updating_tx =
generate_conditional_tx(&mut chain, account0.clone(), noop_note1, true).await;
assert_eq!(
noop_tx.initial_account().to_commitment(),
noop_tx.final_account().to_commitment()
);
assert_ne!(
state_updating_tx.initial_account().to_commitment(),
state_updating_tx.final_account().to_commitment()
);
let tx0 = LocalTransactionProver::default().prove_dummy(noop_tx)?;
let tx1 = LocalTransactionProver::default().prove_dummy(state_updating_tx)?;
let batch0 = chain.create_batch(vec![tx0])?;
let batch1 = chain.create_batch(vec![tx1.clone()])?;
let batches = vec![batch0.clone(), batch1.clone()];
let block_inputs = chain.get_block_inputs(&batches)?;
let block = ProposedBlock::new(block_inputs, batches.clone())?;
let (_, update) = block.updated_accounts().iter().next().unwrap();
assert_eq!(update.initial_state_commitment(), account0.to_commitment());
assert_eq!(update.final_state_commitment(), tx1.account_update().final_state_commitment());
Ok(())
}
async fn generate_conditional_tx(
chain: &mut MockChain,
input: impl Into<TxContextInput>,
noop_note: Note,
modify_storage: bool,
) -> ExecutedTransaction {
let auth_args = [
Felt::new(97),
Felt::new(98),
Felt::new(99),
if modify_storage { Felt::ONE } else { Felt::ZERO },
];
let tx_context = chain
.build_tx_context(input.into(), &[noop_note.id()], &[])
.unwrap()
.auth_args(auth_args.into())
.build()
.unwrap();
tx_context.execute().await.unwrap()
}