use alloc::sync::Arc;
use core::slice;
use std::collections::BTreeMap;
use anyhow::Context;
use assert_matches::assert_matches;
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::block::BlockNumber;
use miden_protocol::crypto::merkle::MerkleError;
use miden_protocol::errors::{BatchAccountUpdateError, ProposedBatchError};
use miden_protocol::note::{
Note,
NoteAssets,
NoteAttachments,
NoteTag,
NoteType,
PartialNote,
PartialNoteMetadata,
};
use miden_protocol::testing::account_id::AccountIdBuilder;
use miden_protocol::transaction::{
InputNote,
InputNoteCommitment,
OutputNote,
PartialBlockchain,
ProvenTransaction,
RawOutputNote,
};
use miden_standards::account::interface::{AccountInterface, AccountInterfaceExt};
use miden_standards::note::P2idNoteStorage;
use miden_standards::testing::account_component::MockAccountComponent;
use miden_standards::testing::note::NoteBuilder;
use miden_tx::LocalTransactionProver;
use rand::rngs::SmallRng;
use rand::{Rng, SeedableRng};
use super::proven_tx_builder::MockProvenTxBuilder;
use crate::utils::create_p2any_note;
use crate::{AccountState, Auth, MockChain, MockChainBuilder};
fn mock_account_id(num: u8) -> AccountId {
AccountIdBuilder::new().build_with_rng(&mut SmallRng::from_seed([num; 32]))
}
pub fn mock_note(num: u8) -> Note {
let sender = mock_account_id(num);
NoteBuilder::new(sender, SmallRng::from_seed([num; 32])).build().unwrap()
}
pub fn mock_output_note(num: u8) -> OutputNote {
RawOutputNote::Full(mock_note(num)).into_output_note().unwrap()
}
struct TestSetup {
chain: MockChain,
account1: Account,
account2: Account,
note1: Note,
}
fn setup_chain() -> TestSetup {
let mut builder = MockChain::builder();
let account1 = generate_account(&mut builder);
let account2 = generate_account(&mut builder);
let note1 = builder
.add_p2id_note(account1.id(), account2.id(), &[], NoteType::Public)
.expect("adding p2id note1 should work");
let mut chain = builder.build().expect("genesis should be valid");
chain.prove_next_block().expect("valid setup");
TestSetup { chain, account1, account2, note1 }
}
fn generate_account(chain: &mut MockChainBuilder) -> Account {
let account_builder = Account::builder(rand::rng().random())
.account_type(AccountType::Private)
.with_component(MockAccountComponent::with_empty_slots());
chain
.add_account_from_builder(Auth::IncrNonce, account_builder, AccountState::Exists)
.expect("failed to add pending account from builder")
}
pub async fn setup_circular_note_dependency_test()
-> anyhow::Result<(MockChain, ProvenTransaction, ProvenTransaction)> {
let asset = NonFungibleAsset::mock(&[42]);
let mut builder = MockChain::builder();
let account = builder.add_existing_wallet_with_assets(Auth::IncrNonce, [])?;
let chain = builder.build()?;
let mut rng = RandomCoin::new(Word::from([1u32; 4]));
let note_x = create_p2any_note(account.id(), NoteType::Public, [asset], &mut rng);
let note_y = create_p2any_note(account.id(), NoteType::Public, [asset], &mut rng);
assert_eq!(note_x.metadata().sender(), note_y.metadata().sender());
assert_ne!(note_x.id(), note_y.id());
let tx_script_y = AccountInterface::from_account(&account)
.build_send_notes_script(&[PartialNote::from(note_y.clone())], None)?;
let executed_tx1 = chain
.build_tx_context(account.clone(), &[], slice::from_ref(¬e_x))?
.tx_script(tx_script_y)
.extend_expected_output_notes(vec![RawOutputNote::Full(note_y.clone())])
.build()?
.execute()
.await?;
let proven_tx1 = LocalTransactionProver::default().prove_dummy(executed_tx1.clone())?;
let mut updated_account = account.clone();
updated_account.apply_delta(executed_tx1.account_delta())?;
let tx_script_x = AccountInterface::from_account(&account)
.build_send_notes_script(&[PartialNote::from(note_x.clone())], None)?;
let executed_tx2 = chain
.build_tx_context(updated_account, &[], slice::from_ref(¬e_y))?
.tx_script(tx_script_x)
.extend_expected_output_notes(vec![RawOutputNote::Full(note_x.clone())])
.build()?
.execute()
.await?;
let proven_tx2 = LocalTransactionProver::default().prove_dummy(executed_tx2)?;
Ok((chain, proven_tx1, proven_tx2))
}
#[test]
fn empty_transaction_batch() -> anyhow::Result<()> {
let TestSetup { chain, .. } = setup_chain();
let block1 = chain.block_header(1);
let error =
ProposedBatch::new(vec![], block1, chain.latest_partial_blockchain(), BTreeMap::default())
.unwrap_err();
assert_matches!(error, ProposedBatchError::EmptyTransactionBatch);
Ok(())
}
#[test]
fn incorrectly_ordered_txs_rejected() -> anyhow::Result<()> {
let TestSetup { mut chain, account1, account2, .. } = setup_chain();
let block1 = chain.block_header(1);
let block2 = chain.prove_next_block()?;
let note = mock_note(40);
let tx1 =
MockProvenTxBuilder::with_account(account1.id(), Word::empty(), account1.to_commitment())
.reference_block(&block1)
.output_notes(vec![RawOutputNote::Full(note.clone()).into_output_note().unwrap()])
.build()?;
let tx2 =
MockProvenTxBuilder::with_account(account2.id(), Word::empty(), account2.to_commitment())
.reference_block(&block1)
.unauthenticated_notes(vec![note.clone()])
.build()?;
let error = ProposedBatch::new(
[tx2.clone(), tx1.clone()].into_iter().map(Arc::new).collect(),
block2.header().clone(),
chain.latest_partial_blockchain(),
BTreeMap::default(),
)
.unwrap_err();
assert_matches!(error, ProposedBatchError::NoteConsumedBeforeCreated { note_id, consumed_by, created_by } => {
assert_eq!(note_id, note.id());
assert_eq!(consumed_by, tx2.id());
assert_eq!(created_by, tx1.id());
});
Ok(())
}
#[test]
fn note_created_and_consumed_in_same_batch() -> anyhow::Result<()> {
let TestSetup { mut chain, account1, account2, .. } = setup_chain();
let block1 = chain.block_header(1);
let block2 = chain.prove_next_block()?;
let note = mock_note(40);
let tx1 =
MockProvenTxBuilder::with_account(account1.id(), Word::empty(), account1.to_commitment())
.reference_block(&block1)
.output_notes(vec![RawOutputNote::Full(note.clone()).into_output_note().unwrap()])
.build()?;
let tx2 =
MockProvenTxBuilder::with_account(account2.id(), Word::empty(), account2.to_commitment())
.reference_block(&block1)
.unauthenticated_notes(vec![note.clone()])
.build()?;
let batch = ProposedBatch::new(
[tx1, tx2].into_iter().map(Arc::new).collect(),
block2.header().clone(),
chain.latest_partial_blockchain(),
BTreeMap::default(),
)?;
assert_eq!(batch.input_notes().num_notes(), 0);
assert_eq!(batch.output_notes().len(), 0);
Ok(())
}
#[test]
fn same_details_different_metadata_not_erased_from_batch() -> anyhow::Result<()> {
let TestSetup { mut chain, account1, account2, .. } = setup_chain();
let block1 = chain.block_header(1);
let block2 = chain.prove_next_block()?;
let output_note = NoteBuilder::new(mock_account_id(7), SmallRng::from_seed([7; 32]))
.serial_number([1, 2, 3, 4u32].into())
.tag(100)
.note_type(NoteType::Public)
.build()?;
let input_note = Note::with_attachments(
output_note.assets().clone(),
output_note.metadata().partial_metadata().with_tag(NoteTag::from(200)),
output_note.recipient().clone(),
output_note.attachments().clone(),
);
let output_note_proven = RawOutputNote::Full(output_note.clone()).into_output_note().unwrap();
let tx1 =
MockProvenTxBuilder::with_account(account1.id(), Word::empty(), account1.to_commitment())
.reference_block(&block1)
.output_notes(vec![output_note_proven.clone()])
.build()?;
let tx2 =
MockProvenTxBuilder::with_account(account2.id(), Word::empty(), account2.to_commitment())
.reference_block(&block1)
.unauthenticated_notes(vec![input_note.clone()])
.build()?;
let batch = ProposedBatch::new(
[tx1, tx2].into_iter().map(Arc::new).collect(),
block2.header().clone(),
chain.latest_partial_blockchain(),
BTreeMap::default(),
)?;
assert_eq!(
batch.input_notes().clone().into_vec(),
vec![InputNoteCommitment::from(&InputNote::unauthenticated(input_note))],
);
assert_eq!(batch.output_notes()[0], output_note_proven);
Ok(())
}
#[test]
fn two_p2id_inputs_same_details_different_metadata_in_same_batch() -> anyhow::Result<()> {
let TestSetup { mut chain, account1, account2, .. } = setup_chain();
let block1 = chain.block_header(1);
let block2 = chain.prove_next_block()?;
let serial_num = Word::from([11, 22, 33, 44u32]);
let recipient = P2idNoteStorage::new(account2.id()).into_recipient(serial_num);
let note_300 = Note::with_attachments(
NoteAssets::default(),
PartialNoteMetadata::new(account1.id(), NoteType::Public).with_tag(NoteTag::from(300)),
recipient.clone(),
NoteAttachments::default(),
);
let note_301 = Note::with_attachments(
NoteAssets::default(),
PartialNoteMetadata::new(account1.id(), NoteType::Public).with_tag(NoteTag::from(301)),
recipient,
NoteAttachments::default(),
);
assert_eq!(note_300.assets(), note_301.assets());
assert_ne!(note_300.metadata(), note_301.metadata());
assert_eq!(note_300.recipient(), note_301.recipient());
assert_eq!(note_300.attachments(), note_301.attachments());
let tx =
MockProvenTxBuilder::with_account(account1.id(), Word::empty(), account1.to_commitment())
.reference_block(&block1)
.authenticated_notes(vec![note_300.clone(), note_301.clone()])
.build()?;
let batch = ProposedBatch::new(
vec![Arc::new(tx)],
block2.header().clone(),
chain.latest_partial_blockchain(),
BTreeMap::default(),
)?;
assert_eq!(batch.input_notes().num_notes(), 2);
Ok(())
}
#[test]
fn duplicate_unauthenticated_input_notes() -> anyhow::Result<()> {
let TestSetup { chain, account1, account2, .. } = setup_chain();
let block1 = chain.block_header(1);
let note = mock_note(50);
let tx1 =
MockProvenTxBuilder::with_account(account1.id(), Word::empty(), account1.to_commitment())
.reference_block(&block1)
.unauthenticated_notes(vec![note.clone()])
.build()?;
let tx2 =
MockProvenTxBuilder::with_account(account2.id(), Word::empty(), account2.to_commitment())
.reference_block(&block1)
.unauthenticated_notes(vec![note.clone()])
.build()?;
let error = ProposedBatch::new(
[tx1.clone(), tx2.clone()].into_iter().map(Arc::new).collect(),
block1,
chain.latest_partial_blockchain(),
BTreeMap::default(),
)
.unwrap_err();
assert_matches!(error, ProposedBatchError::DuplicateInputNote {
note_nullifier,
first_transaction_id,
second_transaction_id
} if note_nullifier == note.nullifier() &&
first_transaction_id == tx1.id() &&
second_transaction_id == tx2.id()
);
Ok(())
}
#[test]
fn duplicate_authenticated_input_notes() -> anyhow::Result<()> {
let TestSetup { mut chain, account1, account2, note1 } = setup_chain();
let block1 = chain.block_header(1);
let block2 = chain.prove_next_block()?;
let tx1 =
MockProvenTxBuilder::with_account(account1.id(), Word::empty(), account1.to_commitment())
.reference_block(&block1)
.authenticated_notes(vec![note1.clone()])
.build()?;
let tx2 =
MockProvenTxBuilder::with_account(account2.id(), Word::empty(), account2.to_commitment())
.reference_block(&block1)
.authenticated_notes(vec![note1.clone()])
.build()?;
let error = ProposedBatch::new(
[tx1.clone(), tx2.clone()].into_iter().map(Arc::new).collect(),
block2.header().clone(),
chain.latest_partial_blockchain(),
BTreeMap::default(),
)
.unwrap_err();
assert_matches!(error, ProposedBatchError::DuplicateInputNote {
note_nullifier,
first_transaction_id,
second_transaction_id
} if note_nullifier == note1.nullifier() &&
first_transaction_id == tx1.id() &&
second_transaction_id == tx2.id()
);
Ok(())
}
#[test]
fn duplicate_mixed_input_notes() -> anyhow::Result<()> {
let TestSetup { mut chain, account1, account2, note1 } = setup_chain();
let block1 = chain.block_header(1);
let block2 = chain.prove_next_block()?;
let tx1 =
MockProvenTxBuilder::with_account(account1.id(), Word::empty(), account1.to_commitment())
.reference_block(&block1)
.unauthenticated_notes(vec![note1.clone()])
.build()?;
let tx2 =
MockProvenTxBuilder::with_account(account2.id(), Word::empty(), account2.to_commitment())
.reference_block(&block1)
.authenticated_notes(vec![note1.clone()])
.build()?;
let error = ProposedBatch::new(
[tx1.clone(), tx2.clone()].into_iter().map(Arc::new).collect(),
block2.header().clone(),
chain.latest_partial_blockchain(),
BTreeMap::default(),
)
.unwrap_err();
assert_matches!(error, ProposedBatchError::DuplicateInputNote {
note_nullifier,
first_transaction_id,
second_transaction_id
} if note_nullifier == note1.nullifier() &&
first_transaction_id == tx1.id() &&
second_transaction_id == tx2.id()
);
Ok(())
}
#[test]
fn duplicate_output_notes() -> anyhow::Result<()> {
let TestSetup { chain, account1, account2, .. } = setup_chain();
let block1 = chain.block_header(1);
let note0 = mock_output_note(50);
let tx1 =
MockProvenTxBuilder::with_account(account1.id(), Word::empty(), account1.to_commitment())
.reference_block(&block1)
.output_notes(vec![note0.clone()])
.build()?;
let tx2 =
MockProvenTxBuilder::with_account(account2.id(), Word::empty(), account2.to_commitment())
.reference_block(&block1)
.output_notes(vec![note0.clone()])
.build()?;
let error = ProposedBatch::new(
[tx1.clone(), tx2.clone()].into_iter().map(Arc::new).collect(),
block1,
chain.latest_partial_blockchain(),
BTreeMap::default(),
)
.unwrap_err();
assert_matches!(error, ProposedBatchError::DuplicateOutputNote {
note_id,
first_transaction_id,
second_transaction_id
} if note_id == note0.id() &&
first_transaction_id == tx1.id() &&
second_transaction_id == tx2.id());
Ok(())
}
#[tokio::test]
async fn unauthenticated_note_converted_to_authenticated() -> anyhow::Result<()> {
let mut builder = MockChain::builder();
let account1 = generate_account(&mut builder);
let note1 = create_p2any_note(account1.id(), NoteType::Public, [], builder.rng_mut());
let note2 = create_p2any_note(account1.id(), NoteType::Public, [], builder.rng_mut());
let spawn_note = builder.add_spawn_note([¬e1, ¬e2])?;
let mut chain = builder.build()?;
let tx = chain
.build_tx_context(account1.clone(), &[spawn_note.id()], &[])?
.extend_expected_output_notes(vec![
RawOutputNote::Full(note1.clone()),
RawOutputNote::Full(note2.clone()),
])
.build()?
.execute()
.await?;
chain.add_pending_executed_transaction(&tx)?;
let block1 = chain.prove_next_block()?;
let block2 = chain.prove_next_block()?;
let block3 = chain.prove_next_block()?;
assert_eq!(
block1.body().output_notes().count(),
2,
"block 1 should contain note1 and note2"
);
assert!(
block1.body().output_notes().any(|(_, note)| note.id() == note1.id()),
"block 1 should contain note1"
);
assert!(
block1.body().output_notes().any(|(_, note)| note.id() == note2.id()),
"block 1 should contain note2"
);
let tx1 =
MockProvenTxBuilder::with_account(account1.id(), Word::empty(), account1.to_commitment())
.reference_block(block2.header())
.unauthenticated_notes(vec![note2.clone()])
.build()?;
let input_note1 = chain.get_public_note(¬e1.id()).expect("note not found");
let note_inclusion_proof1 = input_note1.proof().expect("note should be of type authenticated");
let input_note2 = chain.get_public_note(¬e2.id()).expect("note not found");
let note_inclusion_proof2 = input_note2.proof().expect("note should be of type authenticated");
let partial_blockchain = chain.latest_partial_blockchain();
let error = ProposedBatch::new(
[tx1.clone()].into_iter().map(Arc::new).collect(),
block3.header().clone(),
partial_blockchain.clone(),
BTreeMap::from_iter([(input_note2.id(), note_inclusion_proof1.clone())]),
)
.unwrap_err();
assert_matches!(error, ProposedBatchError::UnauthenticatedNoteAuthenticationFailed {
note_id,
block_num,
source: MerkleError::ConflictingRoots { .. },
} => {
assert_eq!(note_id, note2.id());
assert_eq!(block_num, block1.header().block_num());
}
);
let mut mmr = partial_blockchain.mmr().clone();
mmr.untrack(block1.header().block_num().as_usize());
let blocks = partial_blockchain
.block_headers()
.filter(|header| header.block_num() != block1.header().block_num())
.cloned();
let error = ProposedBatch::new(
[tx1.clone()].into_iter().map(Arc::new).collect(),
block3.header().clone(),
PartialBlockchain::new(mmr, blocks)
.context("failed to build partial blockchain with missing block")?,
BTreeMap::from_iter([(input_note2.id(), note_inclusion_proof2.clone())]),
)
.unwrap_err();
assert_matches!(
error,
ProposedBatchError::UnauthenticatedInputNoteBlockNotInPartialBlockchain {
block_number,
note_id
} => {
assert_eq!(block_number, note_inclusion_proof2.location().block_num());
assert_eq!(note_id, input_note2.id());
}
);
let batch = ProposedBatch::new(
[tx1].into_iter().map(Arc::new).collect(),
block3.header().clone(),
partial_blockchain,
BTreeMap::from_iter([(input_note2.id(), note_inclusion_proof2.clone())]),
)?;
assert_eq!(batch.input_notes().num_notes(), 1);
assert!(
batch
.input_notes()
.iter()
.any(|commitment| commitment == &InputNoteCommitment::from(&input_note2))
);
assert_eq!(batch.output_notes().len(), 0);
Ok(())
}
#[test]
fn authenticated_note_created_in_same_batch() -> anyhow::Result<()> {
let TestSetup { mut chain, account1, account2, note1 } = setup_chain();
let block1 = chain.block_header(1);
let block2 = chain.prove_next_block()?;
let note0 = mock_note(50);
let tx1 =
MockProvenTxBuilder::with_account(account1.id(), Word::empty(), account1.to_commitment())
.reference_block(&block1)
.output_notes(vec![RawOutputNote::Full(note0.clone()).into_output_note().unwrap()])
.build()?;
let tx2 =
MockProvenTxBuilder::with_account(account2.id(), Word::empty(), account2.to_commitment())
.reference_block(&block1)
.authenticated_notes(vec![note1.clone()])
.build()?;
let batch = ProposedBatch::new(
[tx1, tx2].into_iter().map(Arc::new).collect(),
block2.header().clone(),
chain.latest_partial_blockchain(),
BTreeMap::default(),
)?;
assert_eq!(batch.input_notes().num_notes(), 1);
assert_eq!(batch.output_notes().len(), 1);
Ok(())
}
#[test]
fn multiple_transactions_against_same_account() -> anyhow::Result<()> {
let TestSetup { chain, account1, .. } = setup_chain();
let block1 = chain.block_header(1);
let initial_state_commitment = Word::empty();
let tx1 = MockProvenTxBuilder::with_account(
account1.id(),
initial_state_commitment,
account1.to_commitment(),
)
.reference_block(&block1)
.output_notes(vec![mock_output_note(0)])
.build()?;
let final_state_commitment = mock_note(10).id().as_word();
let tx2 = MockProvenTxBuilder::with_account(
account1.id(),
account1.to_commitment(),
final_state_commitment,
)
.reference_block(&block1)
.build()?;
let batch = ProposedBatch::new(
[tx1.clone(), tx2.clone()].into_iter().map(Arc::new).collect(),
block1.clone(),
chain.latest_partial_blockchain(),
BTreeMap::default(),
)?;
assert_eq!(batch.account_updates().len(), 1);
assert_eq!(
batch.account_updates().get(&account1.id()).unwrap().initial_state_commitment(),
initial_state_commitment
);
assert_eq!(
batch.account_updates().get(&account1.id()).unwrap().final_state_commitment(),
final_state_commitment
);
let error = ProposedBatch::new(
[tx2.clone(), tx1.clone()].into_iter().map(Arc::new).collect(),
block1,
chain.latest_partial_blockchain(),
BTreeMap::default(),
)
.unwrap_err();
assert_matches!(
error,
ProposedBatchError::AccountUpdateError {
source: BatchAccountUpdateError::AccountUpdateInitialStateMismatch(tx_id),
..
} if tx_id == tx1.id()
);
Ok(())
}
#[test]
fn input_and_output_notes_commitment() -> anyhow::Result<()> {
let TestSetup { chain, account1, account2, .. } = setup_chain();
let block1 = chain.block_header(1);
let mut rng = rand::rng();
let note_num = rng.random();
let note0 = mock_output_note(note_num);
let note1 = mock_note(note_num.wrapping_add(1));
let note2 = mock_output_note(note_num.wrapping_add(2));
let note3 = mock_output_note(note_num.wrapping_add(3));
let note4 = mock_note(note_num.wrapping_add(4));
let note5 = mock_note(note_num.wrapping_add(5));
let note6 = mock_note(note_num.wrapping_add(6));
let tx1 =
MockProvenTxBuilder::with_account(account1.id(), Word::empty(), account1.to_commitment())
.reference_block(&block1)
.unauthenticated_notes(vec![note5.clone()])
.output_notes(vec![
RawOutputNote::Full(note1.clone()).into_output_note().unwrap(),
note0.clone(),
])
.build()?;
let tx2 =
MockProvenTxBuilder::with_account(account2.id(), Word::empty(), account2.to_commitment())
.reference_block(&block1)
.unauthenticated_notes(vec![note1, note4.clone(), note6.clone()])
.output_notes(vec![note2.clone(), note3.clone()])
.build()?;
let batch = ProposedBatch::new(
[tx1.clone(), tx2.clone()].into_iter().map(Arc::new).collect(),
block1,
chain.latest_partial_blockchain(),
BTreeMap::default(),
)?;
let mut expected_output_notes = [note0, note2, note3];
expected_output_notes.sort_unstable_by_key(OutputNote::id);
assert_eq!(batch.output_notes().len(), 3);
assert_eq!(batch.output_notes(), expected_output_notes);
let mut expected_input_notes = [
InputNoteCommitment::from(&InputNote::unauthenticated(note4)),
InputNoteCommitment::from(&InputNote::unauthenticated(note5)),
InputNoteCommitment::from(&InputNote::unauthenticated(note6)),
];
expected_input_notes.sort_unstable_by_key(InputNoteCommitment::nullifier);
assert_eq!(batch.input_notes().num_notes(), 3);
assert_eq!(batch.input_notes().clone().into_vec(), &expected_input_notes);
Ok(())
}
#[test]
fn batch_expiration() -> anyhow::Result<()> {
let TestSetup { chain, account1, account2, .. } = setup_chain();
let block1 = chain.block_header(1);
let tx1 =
MockProvenTxBuilder::with_account(account1.id(), Word::empty(), account1.to_commitment())
.reference_block(&block1)
.expiration_block_num(BlockNumber::from(35))
.build()?;
let tx2 =
MockProvenTxBuilder::with_account(account2.id(), Word::empty(), account2.to_commitment())
.reference_block(&block1)
.expiration_block_num(block1.block_num() + 1)
.build()?;
let batch = ProposedBatch::new(
[tx1, tx2].into_iter().map(Arc::new).collect(),
block1.clone(),
chain.latest_partial_blockchain(),
BTreeMap::default(),
)?;
assert_eq!(batch.batch_expiration_block_num(), block1.block_num() + 1);
Ok(())
}
#[test]
fn duplicate_transaction() -> anyhow::Result<()> {
let TestSetup { chain, account1, .. } = setup_chain();
let block1 = chain.block_header(1);
let tx1 =
MockProvenTxBuilder::with_account(account1.id(), Word::empty(), account1.to_commitment())
.reference_block(&block1)
.expiration_block_num(BlockNumber::from(35))
.build()?;
let error = ProposedBatch::new(
[tx1.clone(), tx1.clone()].into_iter().map(Arc::new).collect(),
block1,
chain.latest_partial_blockchain(),
BTreeMap::default(),
)
.unwrap_err();
assert_matches!(error, ProposedBatchError::DuplicateTransaction { transaction_id } if transaction_id == tx1.id());
Ok(())
}
#[tokio::test]
async fn cross_tx_circular_note_dependency_is_rejected() -> anyhow::Result<()> {
let (chain, proven_tx1, proven_tx2) = setup_circular_note_dependency_test().await?;
let error = ProposedBatch::new(
[proven_tx1, proven_tx2].into_iter().map(Arc::new).collect(),
chain.latest_block_header(),
chain.latest_partial_blockchain(),
BTreeMap::default(),
)
.unwrap_err();
assert_matches!(error, ProposedBatchError::NoteConsumedBeforeCreated { .. });
Ok(())
}
#[tokio::test]
async fn cross_tx_circular_note_dependency_is_rejected_2() -> anyhow::Result<()> {
let asset = NonFungibleAsset::mock(&[42]);
let mut builder = MockChain::builder();
let account = builder.add_existing_wallet_with_assets(Auth::IncrNonce, [])?;
let chain = builder.build()?;
let mut rng = RandomCoin::new(Word::from([1u32; 4]));
let note_x = create_p2any_note(account.id(), NoteType::Public, [asset], &mut rng);
let executed_tx1 = chain
.build_tx_context(account.clone(), &[], slice::from_ref(¬e_x))?
.build()?
.execute()
.await?;
let proven_tx1 = LocalTransactionProver::default().prove_dummy(executed_tx1.clone())?;
let mut updated_account = account.clone();
updated_account.apply_delta(executed_tx1.account_delta())?;
assert_eq!(updated_account.vault().get(asset.vault_key()).unwrap(), asset);
let tx_script_x = AccountInterface::from_account(&account)
.build_send_notes_script(&[PartialNote::from(note_x.clone())], None)?;
let executed_tx2 = chain
.build_tx_context(updated_account, &[], &[])?
.tx_script(tx_script_x)
.extend_expected_output_notes(vec![RawOutputNote::Full(note_x.clone())])
.build()?
.execute()
.await?;
assert_eq!(
executed_tx2.output_notes().get_note(0).assets().iter().next().unwrap(),
&asset,
"asset should have been moved to the note"
);
let proven_tx2 = LocalTransactionProver::default().prove_dummy(executed_tx2)?;
let error = ProposedBatch::new(
[proven_tx1, proven_tx2].into_iter().map(Arc::new).collect(),
chain.latest_block_header(),
chain.latest_partial_blockchain(),
BTreeMap::default(),
)
.unwrap_err();
assert_matches!(error, ProposedBatchError::NoteConsumedBeforeCreated { .. });
Ok(())
}
#[test]
fn expired_transaction() -> anyhow::Result<()> {
let TestSetup { chain, account1, account2, .. } = setup_chain();
let block1 = chain.block_header(1);
let tx1 =
MockProvenTxBuilder::with_account(account1.id(), Word::empty(), account1.to_commitment())
.reference_block(&block1)
.expiration_block_num(block1.block_num())
.build()?;
let tx2 =
MockProvenTxBuilder::with_account(account2.id(), Word::empty(), account2.to_commitment())
.reference_block(&block1)
.expiration_block_num(block1.block_num() + 3)
.build()?;
let error = ProposedBatch::new(
[tx1.clone(), tx2].into_iter().map(Arc::new).collect(),
block1.clone(),
chain.latest_partial_blockchain(),
BTreeMap::default(),
)
.unwrap_err();
assert_matches!(
error,
ProposedBatchError::ExpiredTransaction {
transaction_id,
transaction_expiration_num,
reference_block_num
} if transaction_id == tx1.id() &&
transaction_expiration_num == block1.block_num() &&
reference_block_num == block1.block_num()
);
Ok(())
}
#[test]
fn noop_tx_before_state_updating_tx_against_same_account() -> anyhow::Result<()> {
let TestSetup { mut chain, account1, note1, .. } = setup_chain();
let block1 = chain.block_header(1);
let block2 = chain.prove_next_block()?;
let random_final_state_commitment = Word::from([1, 2, 3, 4u32]);
let note = mock_note(40);
let noop_tx1 = MockProvenTxBuilder::with_account(
account1.id(),
account1.to_commitment(),
account1.to_commitment(),
)
.reference_block(&block1)
.authenticated_notes(vec![note1])
.output_notes(vec![RawOutputNote::Full(note.clone()).into_output_note().unwrap()])
.build()?;
assert_eq!(
noop_tx1.account_update().initial_state_commitment(),
noop_tx1.account_update().final_state_commitment()
);
let tx2 = MockProvenTxBuilder::with_account(
account1.id(),
account1.to_commitment(),
random_final_state_commitment,
)
.reference_block(&block1)
.unauthenticated_notes(vec![note.clone()])
.build()?;
let batch = ProposedBatch::new(
[noop_tx1, tx2].into_iter().map(Arc::new).collect(),
block2.header().clone(),
chain.latest_partial_blockchain(),
BTreeMap::default(),
)?;
let update = batch.account_updates().get(&account1.id()).unwrap();
assert_eq!(update.initial_state_commitment(), account1.to_commitment());
assert_eq!(update.final_state_commitment(), random_final_state_commitment);
Ok(())
}
#[test]
fn mismatched_ref_block_commitment_rejected() -> anyhow::Result<()> {
let account_builder = Account::builder([42; 32])
.account_type(AccountType::Private)
.with_component(MockAccountComponent::with_empty_slots());
let mut builder1 = MockChain::builder();
let account_a = builder1.add_account_from_builder(
Auth::IncrNonce,
account_builder.clone(),
AccountState::Exists,
)?;
let mut chain_a = builder1.build()?;
let chain_a_block1 = chain_a.prove_next_block()?;
let mut builder2 = MockChain::builder();
let account_b = builder2.add_account_from_builder(
Auth::IncrNonce,
account_builder,
AccountState::Exists,
)?;
let mut chain_b = builder2.build()?;
let chain_b_block1 = chain_b.prove_next_block()?;
let chain_b_block2 = chain_b.prove_next_block()?;
assert_eq!(
account_a.to_commitment(),
account_b.to_commitment(),
"accounts should have the same commitment"
);
assert_ne!(
chain_a_block1.header().commitment(),
chain_b_block1.header().commitment(),
"block 1 should have different commitments on the two chains"
);
let tx =
MockProvenTxBuilder::with_account(account_a.id(), Word::empty(), account_a.to_commitment())
.reference_block(chain_a_block1.header())
.build()?;
let result = ProposedBatch::new(
vec![Arc::new(tx.clone())],
chain_b_block2.header().clone(),
chain_b.latest_partial_blockchain(),
BTreeMap::default(),
)
.unwrap_err();
assert_matches!(
result,
ProposedBatchError::TransactionReferenceBlockCommitmentMismatch {
transaction_id, block_num, expected_block_commitment, actual_block_commitment
} => {
assert_eq!(transaction_id, tx.id());
assert_eq!(block_num, tx.ref_block_num());
assert_eq!(actual_block_commitment, tx.ref_block_commitment());
assert_eq!(expected_block_commitment, chain_b_block1.header().commitment());
}
);
let (ref_block, partial_blockchain) = chain_b.selective_partial_blockchain(
chain_b_block1.header().block_num(),
[BlockNumber::GENESIS],
)?;
assert_eq!(
ref_block.block_num(),
tx.ref_block_num(),
"tx and batch ref block num should match"
);
let result = ProposedBatch::new(
vec![Arc::new(tx.clone())],
ref_block.clone(),
partial_blockchain,
BTreeMap::default(),
)
.unwrap_err();
assert_matches!(
result,
ProposedBatchError::TransactionReferenceBlockCommitmentMismatch {
transaction_id, block_num, expected_block_commitment, actual_block_commitment
} => {
assert_eq!(transaction_id, tx.id());
assert_eq!(block_num, tx.ref_block_num());
assert_eq!(actual_block_commitment, tx.ref_block_commitment());
assert_eq!(expected_block_commitment, ref_block.commitment());
}
);
Ok(())
}
#[test]
fn noop_tx_after_state_updating_tx_against_same_account() -> anyhow::Result<()> {
let TestSetup { mut chain, account1, note1, .. } = setup_chain();
let block1 = chain.block_header(1);
let block2 = chain.prove_next_block()?;
let random_final_state_commitment = Word::from([1, 2, 3, 4u32]);
let note = mock_note(40);
let tx1 = MockProvenTxBuilder::with_account(
account1.id(),
account1.to_commitment(),
random_final_state_commitment,
)
.reference_block(&block1)
.output_notes(vec![RawOutputNote::Full(note.clone()).into_output_note().unwrap()])
.build()?;
let noop_tx2 = MockProvenTxBuilder::with_account(
account1.id(),
random_final_state_commitment,
random_final_state_commitment,
)
.reference_block(&block1)
.authenticated_notes(vec![note1])
.unauthenticated_notes(vec![note.clone()])
.build()?;
assert_eq!(
noop_tx2.account_update().initial_state_commitment(),
noop_tx2.account_update().final_state_commitment()
);
let batch = ProposedBatch::new(
[tx1, noop_tx2].into_iter().map(Arc::new).collect(),
block2.header().clone(),
chain.latest_partial_blockchain(),
BTreeMap::default(),
)?;
let update = batch.account_updates().get(&account1.id()).unwrap();
assert_eq!(update.initial_state_commitment(), account1.to_commitment());
assert_eq!(update.final_state_commitment(), random_final_state_commitment);
Ok(())
}