use alloc::collections::{BTreeMap, BTreeSet};
use alloc::vec::Vec;
use anyhow::Context;
use miden_block_prover::LocalBlockProver;
use miden_processor::serde::DeserializationError;
use miden_protocol::MIN_PROOF_SECURITY_LEVEL;
use miden_protocol::account::auth::{AuthSecretKey, PublicKey};
use miden_protocol::account::delta::AccountUpdateDetails;
use miden_protocol::account::{Account, AccountId, PartialAccount};
use miden_protocol::batch::{ProposedBatch, ProvenBatch};
use miden_protocol::block::account_tree::{AccountTree, AccountWitness};
use miden_protocol::block::nullifier_tree::{NullifierTree, NullifierWitness};
use miden_protocol::block::{
BlockHeader,
BlockInputs,
BlockNumber,
Blockchain,
ProposedBlock,
ProvenBlock,
};
use miden_protocol::crypto::dsa::ecdsa_k256_keccak::SecretKey;
use miden_protocol::note::{Note, NoteHeader, NoteId, NoteInclusionProof, Nullifier};
use miden_protocol::transaction::{
ExecutedTransaction,
InputNote,
InputNotes,
OutputNote,
PartialBlockchain,
ProvenTransaction,
TransactionInputs,
};
use miden_tx::LocalTransactionProver;
use miden_tx::auth::BasicAuthenticator;
use miden_tx::utils::serde::{ByteReader, ByteWriter, Deserializable, Serializable};
use miden_tx_batch_prover::LocalBatchProver;
use super::note::MockChainNote;
use crate::{MockChainBuilder, TransactionContextBuilder};
#[derive(Debug, Clone)]
pub struct MockChain {
chain: Blockchain,
blocks: Vec<ProvenBlock>,
nullifier_tree: NullifierTree,
account_tree: AccountTree,
pending_transactions: Vec<ProvenTransaction>,
pending_batches: Vec<ProvenBatch>,
committed_notes: BTreeMap<NoteId, MockChainNote>,
committed_accounts: BTreeMap<AccountId, Account>,
account_authenticators: BTreeMap<AccountId, AccountAuthenticator>,
validator_secret_key: SecretKey,
}
impl MockChain {
pub const TIMESTAMP_START_SECS: u32 = 1700000000;
pub const TIMESTAMP_STEP_SECS: u32 = 10;
pub fn new() -> Self {
Self::builder().build().expect("empty chain should be valid")
}
pub fn builder() -> MockChainBuilder {
MockChainBuilder::new()
}
pub(super) fn from_genesis_block(
genesis_block: ProvenBlock,
account_tree: AccountTree,
account_authenticators: BTreeMap<AccountId, AccountAuthenticator>,
secret_key: SecretKey,
genesis_notes: Vec<Note>,
) -> anyhow::Result<Self> {
let mut chain = MockChain {
chain: Blockchain::default(),
blocks: vec![],
nullifier_tree: NullifierTree::default(),
account_tree,
pending_transactions: Vec::new(),
pending_batches: Vec::new(),
committed_notes: BTreeMap::new(),
committed_accounts: BTreeMap::new(),
account_authenticators,
validator_secret_key: secret_key,
};
chain
.apply_block(genesis_block)
.context("failed to build account from builder")?;
for note in genesis_notes {
if let Some(MockChainNote::Private(_, _, inclusion_proof)) =
chain.committed_notes.get(¬e.id())
{
chain.committed_notes.insert(
note.id(),
MockChainNote::Public(note.clone(), inclusion_proof.clone()),
);
}
}
debug_assert_eq!(chain.blocks.len(), 1);
debug_assert_eq!(chain.committed_accounts.len(), chain.account_tree.num_accounts());
Ok(chain)
}
pub fn blockchain(&self) -> &Blockchain {
&self.chain
}
pub fn latest_partial_blockchain(&self) -> PartialBlockchain {
let block_headers =
self.blocks.iter().map(|b| b.header()).take(self.blocks.len() - 1).cloned();
PartialBlockchain::from_blockchain(&self.chain, block_headers)
.expect("blockchain should be valid by construction")
}
pub fn latest_selective_partial_blockchain(
&self,
reference_blocks: impl IntoIterator<Item = BlockNumber>,
) -> anyhow::Result<(BlockHeader, PartialBlockchain)> {
let latest_block_header = self.latest_block_header();
self.selective_partial_blockchain(latest_block_header.block_num(), reference_blocks)
}
pub fn selective_partial_blockchain(
&self,
reference_block: BlockNumber,
reference_blocks: impl IntoIterator<Item = BlockNumber>,
) -> anyhow::Result<(BlockHeader, PartialBlockchain)> {
let reference_block_header = self.block_header(reference_block.as_usize());
let reference_blocks: BTreeSet<_> = reference_blocks.into_iter().collect();
let mut block_headers = Vec::new();
for block_ref_num in &reference_blocks {
let block_index = block_ref_num.as_usize();
let block = self
.blocks
.get(block_index)
.ok_or_else(|| anyhow::anyhow!("block {} not found in chain", block_ref_num))?;
let block_header = block.header().clone();
if block_header.commitment() != reference_block_header.commitment() {
block_headers.push(block_header);
}
}
let partial_blockchain =
PartialBlockchain::from_blockchain_at(&self.chain, reference_block, block_headers)?;
Ok((reference_block_header, partial_blockchain))
}
pub fn account_witnesses(
&self,
account_ids: impl IntoIterator<Item = AccountId>,
) -> BTreeMap<AccountId, AccountWitness> {
let mut account_witnesses = BTreeMap::new();
for account_id in account_ids {
let witness = self.account_tree.open(account_id);
account_witnesses.insert(account_id, witness);
}
account_witnesses
}
pub fn nullifier_witnesses(
&self,
nullifiers: impl IntoIterator<Item = Nullifier>,
) -> BTreeMap<Nullifier, NullifierWitness> {
let mut nullifier_proofs = BTreeMap::new();
for nullifier in nullifiers {
let witness = self.nullifier_tree.open(&nullifier);
nullifier_proofs.insert(nullifier, witness);
}
nullifier_proofs
}
pub fn unauthenticated_note_proofs(
&self,
notes: impl IntoIterator<Item = NoteId>,
) -> BTreeMap<NoteId, NoteInclusionProof> {
let mut proofs = BTreeMap::default();
for note in notes {
if let Some(input_note) = self.committed_notes.get(¬e) {
proofs.insert(note, input_note.inclusion_proof().clone());
}
}
proofs
}
pub fn genesis_block_header(&self) -> BlockHeader {
self.block_header(BlockNumber::GENESIS.as_usize())
}
pub fn latest_block_header(&self) -> BlockHeader {
let chain_tip =
self.chain.chain_tip().expect("chain should contain at least the genesis block");
self.blocks[chain_tip.as_usize()].header().clone()
}
pub fn latest_block(&self) -> ProvenBlock {
let chain_tip =
self.chain.chain_tip().expect("chain should contain at least the genesis block");
self.blocks[chain_tip.as_usize()].clone()
}
pub fn block_header(&self, block_number: usize) -> BlockHeader {
self.blocks[block_number].header().clone()
}
pub fn proven_blocks(&self) -> &[ProvenBlock] {
&self.blocks
}
pub fn native_asset_id(&self) -> AccountId {
self.genesis_block_header().fee_parameters().native_asset_id()
}
pub fn nullifier_tree(&self) -> &NullifierTree {
&self.nullifier_tree
}
pub fn committed_notes(&self) -> &BTreeMap<NoteId, MockChainNote> {
&self.committed_notes
}
pub fn get_public_note(&self, note_id: &NoteId) -> Option<InputNote> {
let note = self.committed_notes.get(note_id)?;
note.clone().try_into().ok()
}
pub fn committed_account(&self, account_id: AccountId) -> anyhow::Result<&Account> {
self.committed_accounts
.get(&account_id)
.with_context(|| format!("account {account_id} not found in committed accounts"))
}
pub fn account_tree(&self) -> &AccountTree {
&self.account_tree
}
pub fn propose_transaction_batch<I>(
&self,
txs: impl IntoIterator<Item = ProvenTransaction, IntoIter = I>,
) -> anyhow::Result<ProposedBatch>
where
I: Iterator<Item = ProvenTransaction> + Clone,
{
let transactions: Vec<_> = txs.into_iter().map(alloc::sync::Arc::new).collect();
let (batch_reference_block, partial_blockchain, unauthenticated_note_proofs) = self
.get_batch_inputs(
transactions.iter().map(|tx| tx.ref_block_num()),
transactions
.iter()
.flat_map(|tx| tx.unauthenticated_notes().map(NoteHeader::id)),
)?;
Ok(ProposedBatch::new(
transactions,
batch_reference_block,
partial_blockchain,
unauthenticated_note_proofs,
)?)
}
pub fn prove_transaction_batch(
&self,
proposed_batch: ProposedBatch,
) -> anyhow::Result<ProvenBatch> {
let batch_prover = LocalBatchProver::new(0);
Ok(batch_prover.prove_dummy(proposed_batch)?)
}
pub fn propose_block_at<I>(
&self,
batches: impl IntoIterator<Item = ProvenBatch, IntoIter = I>,
timestamp: u32,
) -> anyhow::Result<ProposedBlock>
where
I: Iterator<Item = ProvenBatch> + Clone,
{
let batches: Vec<_> = batches.into_iter().collect();
let block_inputs = self
.get_block_inputs(batches.iter())
.context("could not retrieve block inputs")?;
let proposed_block = ProposedBlock::new_at(block_inputs, batches, timestamp)
.context("failed to create proposed block")?;
Ok(proposed_block)
}
pub fn propose_block<I>(
&self,
batches: impl IntoIterator<Item = ProvenBatch, IntoIter = I>,
) -> anyhow::Result<ProposedBlock>
where
I: Iterator<Item = ProvenBatch> + Clone,
{
let timestamp = self.latest_block_header().timestamp() + 1;
self.propose_block_at(batches, timestamp)
}
pub fn build_tx_context_at(
&self,
reference_block: impl Into<BlockNumber>,
input: impl Into<TxContextInput>,
note_ids: &[NoteId],
unauthenticated_notes: &[Note],
) -> anyhow::Result<TransactionContextBuilder> {
let input = input.into();
let reference_block = reference_block.into();
let authenticator = self.account_authenticators.get(&input.id());
let authenticator =
authenticator.and_then(|authenticator| authenticator.authenticator().cloned());
anyhow::ensure!(
reference_block.as_usize() < self.blocks.len(),
"reference block {reference_block} is out of range (latest {})",
self.latest_block_header().block_num()
);
let account = match input {
TxContextInput::AccountId(account_id) => {
if account_id.is_private() {
return Err(anyhow::anyhow!(
"transaction contexts for private accounts should be created with TxContextInput::Account"
));
}
self.committed_accounts
.get(&account_id)
.with_context(|| {
format!("account {account_id} not found in committed accounts")
})?
.clone()
},
TxContextInput::Account(account) => account,
};
let tx_inputs = self
.get_transaction_inputs_at(reference_block, &account, note_ids, unauthenticated_notes)
.context("failed to gather transaction inputs")?;
let tx_context_builder = TransactionContextBuilder::new(account)
.authenticator(authenticator)
.tx_inputs(tx_inputs);
Ok(tx_context_builder)
}
pub fn build_tx_context(
&self,
input: impl Into<TxContextInput>,
note_ids: &[NoteId],
unauthenticated_notes: &[Note],
) -> anyhow::Result<TransactionContextBuilder> {
let reference_block = self.latest_block_header().block_num();
self.build_tx_context_at(reference_block, input, note_ids, unauthenticated_notes)
}
pub fn get_transaction_inputs_at(
&self,
reference_block: BlockNumber,
account: impl Into<PartialAccount>,
notes: &[NoteId],
unauthenticated_notes: &[Note],
) -> anyhow::Result<TransactionInputs> {
let ref_block = self.block_header(reference_block.as_usize());
let mut input_notes = vec![];
let mut block_headers_map: BTreeMap<BlockNumber, BlockHeader> = BTreeMap::new();
for note in notes {
let input_note: InputNote = self
.committed_notes
.get(note)
.with_context(|| format!("note with id {note} not found"))?
.clone()
.try_into()
.with_context(|| {
format!("failed to convert mock chain note with id {note} into input note")
})?;
let note_block_num = input_note
.location()
.with_context(|| format!("note location not available: {note}"))?
.block_num();
if note_block_num > ref_block.block_num() {
anyhow::bail!(
"note with ID {note} was created in block {note_block_num} which is larger than the reference block number {}",
ref_block.block_num()
)
}
if note_block_num != ref_block.block_num() {
let block_header = self
.blocks
.get(note_block_num.as_usize())
.with_context(|| format!("block {note_block_num} not found in chain"))?
.header()
.clone();
block_headers_map.insert(note_block_num, block_header);
}
input_notes.push(input_note);
}
for note in unauthenticated_notes {
input_notes.push(InputNote::Unauthenticated { note: note.clone() })
}
let block_headers = block_headers_map.values();
let (_, partial_blockchain) = self.selective_partial_blockchain(
reference_block,
block_headers.map(BlockHeader::block_num),
)?;
let input_notes = InputNotes::new(input_notes)?;
Ok(TransactionInputs::new(
account.into(),
ref_block.clone(),
partial_blockchain,
input_notes,
)?)
}
pub fn get_transaction_inputs(
&self,
account: impl Into<PartialAccount>,
notes: &[NoteId],
unauthenticated_notes: &[Note],
) -> anyhow::Result<TransactionInputs> {
let latest_block_num = self.latest_block_header().block_num();
self.get_transaction_inputs_at(latest_block_num, account, notes, unauthenticated_notes)
}
pub fn get_batch_inputs(
&self,
tx_reference_blocks: impl IntoIterator<Item = BlockNumber>,
unauthenticated_notes: impl Iterator<Item = NoteId>,
) -> anyhow::Result<(BlockHeader, PartialBlockchain, BTreeMap<NoteId, NoteInclusionProof>)>
{
let unauthenticated_note_proofs = self.unauthenticated_note_proofs(unauthenticated_notes);
let required_blocks = tx_reference_blocks.into_iter().chain(
unauthenticated_note_proofs
.values()
.map(|note_proof| note_proof.location().block_num()),
);
let (batch_reference_block, partial_block_chain) =
self.latest_selective_partial_blockchain(required_blocks)?;
Ok((batch_reference_block, partial_block_chain, unauthenticated_note_proofs))
}
pub fn get_foreign_account_inputs(
&self,
account_id: AccountId,
) -> anyhow::Result<(Account, AccountWitness)> {
let account = self.committed_account(account_id)?.clone();
let account_witness = self.account_tree().open(account_id);
assert_eq!(account_witness.state_commitment(), account.to_commitment());
Ok((account, account_witness))
}
pub fn get_block_inputs<'batch, I>(
&self,
batch_iter: impl IntoIterator<Item = &'batch ProvenBatch, IntoIter = I>,
) -> anyhow::Result<BlockInputs>
where
I: Iterator<Item = &'batch ProvenBatch> + Clone,
{
let batch_iterator = batch_iter.into_iter();
let unauthenticated_note_proofs =
self.unauthenticated_note_proofs(batch_iterator.clone().flat_map(|batch| {
batch.input_notes().iter().filter_map(|note| note.header().map(NoteHeader::id))
}));
let (block_reference_block, partial_blockchain) = self
.latest_selective_partial_blockchain(
batch_iterator.clone().map(ProvenBatch::reference_block_num).chain(
unauthenticated_note_proofs.values().map(|proof| proof.location().block_num()),
),
)?;
let account_witnesses =
self.account_witnesses(batch_iterator.clone().flat_map(ProvenBatch::updated_accounts));
let nullifier_proofs =
self.nullifier_witnesses(batch_iterator.flat_map(ProvenBatch::created_nullifiers));
Ok(BlockInputs::new(
block_reference_block,
partial_blockchain,
account_witnesses,
nullifier_proofs,
unauthenticated_note_proofs,
))
}
pub fn prove_next_block(&mut self) -> anyhow::Result<ProvenBlock> {
self.prove_and_apply_block(None)
}
pub fn prove_next_block_at(&mut self, timestamp: u32) -> anyhow::Result<ProvenBlock> {
self.prove_and_apply_block(Some(timestamp))
}
pub fn prove_until_block(
&mut self,
target_block_num: impl Into<BlockNumber>,
) -> anyhow::Result<ProvenBlock> {
let target_block_num = target_block_num.into();
let latest_block_num = self.latest_block_header().block_num();
assert!(
target_block_num > latest_block_num,
"target block number must be greater than the number of the latest block in the chain"
);
let mut last_block = None;
for _ in latest_block_num.as_usize()..target_block_num.as_usize() {
last_block = Some(self.prove_next_block()?);
}
Ok(last_block.expect("at least one block should have been created"))
}
pub fn add_pending_executed_transaction(
&mut self,
transaction: &ExecutedTransaction,
) -> anyhow::Result<()> {
let proven_tx = LocalTransactionProver::default()
.prove_dummy(transaction.clone())
.context("failed to dummy-prove executed transaction into proven transaction")?;
self.pending_transactions.push(proven_tx);
Ok(())
}
pub fn add_pending_proven_transaction(&mut self, transaction: ProvenTransaction) {
self.pending_transactions.push(transaction);
}
pub fn add_pending_batch(&mut self, batch: ProvenBatch) {
self.pending_batches.push(batch);
}
fn apply_block(&mut self, proven_block: ProvenBlock) -> anyhow::Result<()> {
for account_update in proven_block.body().updated_accounts() {
self.account_tree
.insert(account_update.account_id(), account_update.final_state_commitment())
.context("failed to insert account update into account tree")?;
}
for nullifier in proven_block.body().created_nullifiers() {
self.nullifier_tree
.mark_spent(*nullifier, proven_block.header().block_num())
.context("failed to mark block nullifier as spent")?;
}
for account_update in proven_block.body().updated_accounts() {
match account_update.details() {
AccountUpdateDetails::Delta(account_delta) => {
if account_delta.is_full_state() {
let account = Account::try_from(account_delta)
.context("failed to convert full state delta into full account")?;
self.committed_accounts.insert(account.id(), account.clone());
} else {
let committed_account = self
.committed_accounts
.get_mut(&account_update.account_id())
.ok_or_else(|| {
anyhow::anyhow!("account delta in block for non-existent account")
})?;
committed_account
.apply_delta(account_delta)
.context("failed to apply account delta")?;
}
},
AccountUpdateDetails::Private => {},
}
}
let notes_tree = proven_block.body().compute_block_note_tree();
for (block_note_index, created_note) in proven_block.body().output_notes() {
let note_path = notes_tree.open(block_note_index);
let note_inclusion_proof = NoteInclusionProof::new(
proven_block.header().block_num(),
block_note_index.leaf_index_value(),
note_path,
)
.context("failed to create inclusion proof for output note")?;
if let OutputNote::Public(public_note) = created_note {
self.committed_notes.insert(
public_note.id(),
MockChainNote::Public(public_note.as_note().clone(), note_inclusion_proof),
);
} else {
self.committed_notes.insert(
created_note.id(),
MockChainNote::Private(
created_note.id(),
created_note.metadata().clone(),
note_inclusion_proof,
),
);
}
}
debug_assert_eq!(
self.chain.commitment(),
proven_block.header().chain_commitment(),
"current mock chain commitment and new block's chain commitment should match"
);
debug_assert_eq!(
BlockNumber::from(self.chain.as_mmr().forest().num_leaves() as u32),
proven_block.header().block_num(),
"current mock chain length and new block's number should match"
);
self.chain.push(proven_block.header().commitment());
self.blocks.push(proven_block);
Ok(())
}
fn pending_transactions_to_batches(&mut self) -> anyhow::Result<Vec<ProvenBatch>> {
if self.pending_transactions.is_empty() {
return Ok(vec![]);
}
let pending_transactions = core::mem::take(&mut self.pending_transactions);
let proposed_batch = self.propose_transaction_batch(pending_transactions)?;
let proven_batch = self.prove_transaction_batch(proposed_batch)?;
Ok(vec![proven_batch])
}
fn prove_and_apply_block(&mut self, timestamp: Option<u32>) -> anyhow::Result<ProvenBlock> {
let mut batches = self.pending_transactions_to_batches()?;
batches.extend(core::mem::take(&mut self.pending_batches));
let block_timestamp =
timestamp.unwrap_or(self.latest_block_header().timestamp() + Self::TIMESTAMP_STEP_SECS);
let proposed_block = self
.propose_block_at(batches.clone(), block_timestamp)
.context("failed to create proposed block")?;
let proven_block = self.prove_block(proposed_block.clone())?;
self.apply_block(proven_block.clone()).context("failed to apply block")?;
Ok(proven_block)
}
pub fn prove_block(&self, proposed_block: ProposedBlock) -> anyhow::Result<ProvenBlock> {
let (header, body) = proposed_block.clone().into_header_and_body()?;
let inputs = self.get_block_inputs(proposed_block.batches().as_slice())?;
let block_proof = LocalBlockProver::new(MIN_PROOF_SECURITY_LEVEL).prove_dummy(
proposed_block.batches().clone(),
header.clone(),
inputs,
)?;
let signature = self.validator_secret_key.sign(header.commitment());
Ok(ProvenBlock::new_unchecked(header, body, signature, block_proof))
}
}
impl Default for MockChain {
fn default() -> Self {
MockChain::new()
}
}
impl Serializable for MockChain {
fn write_into<W: ByteWriter>(&self, target: &mut W) {
self.chain.write_into(target);
self.blocks.write_into(target);
self.nullifier_tree.write_into(target);
self.account_tree.write_into(target);
self.pending_transactions.write_into(target);
self.committed_accounts.write_into(target);
self.committed_notes.write_into(target);
self.account_authenticators.write_into(target);
self.validator_secret_key.write_into(target);
}
}
impl Deserializable for MockChain {
fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
let chain = Blockchain::read_from(source)?;
let blocks = Vec::<ProvenBlock>::read_from(source)?;
let nullifier_tree = NullifierTree::read_from(source)?;
let account_tree = AccountTree::read_from(source)?;
let pending_transactions = Vec::<ProvenTransaction>::read_from(source)?;
let committed_accounts = BTreeMap::<AccountId, Account>::read_from(source)?;
let committed_notes = BTreeMap::<NoteId, MockChainNote>::read_from(source)?;
let account_authenticators =
BTreeMap::<AccountId, AccountAuthenticator>::read_from(source)?;
let secret_key = SecretKey::read_from(source)?;
Ok(Self {
chain,
blocks,
nullifier_tree,
account_tree,
pending_transactions,
pending_batches: Vec::new(),
committed_notes,
committed_accounts,
account_authenticators,
validator_secret_key: secret_key,
})
}
}
pub enum AccountState {
New,
Exists,
}
#[derive(Debug, Clone)]
pub(super) struct AccountAuthenticator {
authenticator: Option<BasicAuthenticator>,
}
impl AccountAuthenticator {
pub fn new(authenticator: Option<BasicAuthenticator>) -> Self {
Self { authenticator }
}
pub fn authenticator(&self) -> Option<&BasicAuthenticator> {
self.authenticator.as_ref()
}
}
impl PartialEq for AccountAuthenticator {
fn eq(&self, other: &Self) -> bool {
match (&self.authenticator, &other.authenticator) {
(Some(a), Some(b)) => {
a.keys().keys().zip(b.keys().keys()).all(|(a_key, b_key)| a_key == b_key)
},
(None, None) => true,
_ => false,
}
}
}
impl Serializable for AccountAuthenticator {
fn write_into<W: ByteWriter>(&self, target: &mut W) {
self.authenticator
.as_ref()
.map(|auth| {
auth.keys()
.values()
.map(|(secret_key, public_key)| (secret_key, public_key.as_ref().clone()))
.collect::<Vec<_>>()
})
.write_into(target);
}
}
impl Deserializable for AccountAuthenticator {
fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
let authenticator = Option::<Vec<(AuthSecretKey, PublicKey)>>::read_from(source)?;
let authenticator = authenticator.map(|keys| BasicAuthenticator::from_key_pairs(&keys));
Ok(Self { authenticator })
}
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Clone)]
pub enum TxContextInput {
AccountId(AccountId),
Account(Account),
}
impl TxContextInput {
fn id(&self) -> AccountId {
match self {
TxContextInput::AccountId(account_id) => *account_id,
TxContextInput::Account(account) => account.id(),
}
}
}
impl From<AccountId> for TxContextInput {
fn from(account: AccountId) -> Self {
Self::AccountId(account)
}
}
impl From<Account> for TxContextInput {
fn from(account: Account) -> Self {
Self::Account(account)
}
}
#[cfg(test)]
mod tests {
use miden_protocol::account::auth::AuthScheme;
use miden_protocol::account::{AccountBuilder, AccountStorageMode};
use miden_protocol::asset::{Asset, FungibleAsset};
use miden_protocol::note::NoteType;
use miden_protocol::testing::account_id::{
ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
ACCOUNT_ID_SENDER,
};
use miden_standards::account::wallets::BasicWallet;
use super::*;
use crate::Auth;
#[test]
fn prove_until_block() -> anyhow::Result<()> {
let mut chain = MockChain::new();
let block = chain.prove_until_block(5)?;
assert_eq!(block.header().block_num(), 5u32.into());
assert_eq!(chain.proven_blocks().len(), 6);
Ok(())
}
#[tokio::test]
async fn private_account_state_update() -> anyhow::Result<()> {
let faucet_id = ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into()?;
let account_builder = AccountBuilder::new([4; 32])
.storage_mode(AccountStorageMode::Private)
.with_component(BasicWallet);
let mut builder = MockChain::builder();
let auth_scheme = AuthScheme::EcdsaK256Keccak;
let account = builder.add_account_from_builder(
Auth::BasicAuth { auth_scheme },
account_builder,
AccountState::New,
)?;
let account_id = account.id();
assert_eq!(account.nonce().as_canonical_u64(), 0);
let note_1 = builder.add_p2id_note(
ACCOUNT_ID_SENDER.try_into().unwrap(),
account.id(),
&[Asset::Fungible(FungibleAsset::new(faucet_id, 1000u64).unwrap())],
NoteType::Private,
)?;
let mut mock_chain = builder.build()?;
mock_chain.prove_next_block()?;
let tx = mock_chain
.build_tx_context(TxContextInput::Account(account), &[], &[note_1])?
.build()?
.execute()
.await?;
mock_chain.add_pending_executed_transaction(&tx)?;
mock_chain.prove_next_block()?;
assert!(tx.final_account().nonce().as_canonical_u64() > 0);
assert_eq!(
tx.final_account().to_commitment(),
mock_chain.account_tree.open(account_id).state_commitment()
);
Ok(())
}
#[tokio::test]
async fn mock_chain_serialization() {
let mut builder = MockChain::builder();
let mut notes = vec![];
for i in 0..10 {
let account = builder
.add_account_from_builder(
Auth::BasicAuth {
auth_scheme: AuthScheme::Falcon512Poseidon2,
},
AccountBuilder::new([i; 32]).with_component(BasicWallet),
AccountState::New,
)
.unwrap();
let note = builder
.add_p2id_note(
ACCOUNT_ID_SENDER.try_into().unwrap(),
account.id(),
&[Asset::Fungible(
FungibleAsset::new(
ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET.try_into().unwrap(),
1000u64,
)
.unwrap(),
)],
NoteType::Private,
)
.unwrap();
notes.push((account, note));
}
let mut chain = builder.build().unwrap();
for (account, note) in notes {
let tx = chain
.build_tx_context(TxContextInput::Account(account), &[], &[note])
.unwrap()
.build()
.unwrap()
.execute()
.await
.unwrap();
chain.add_pending_executed_transaction(&tx).unwrap();
chain.prove_next_block().unwrap();
}
let bytes = chain.to_bytes();
let deserialized = MockChain::read_from_bytes(&bytes).unwrap();
assert_eq!(chain.chain.as_mmr().peaks(), deserialized.chain.as_mmr().peaks());
assert_eq!(chain.blocks, deserialized.blocks);
assert_eq!(chain.nullifier_tree, deserialized.nullifier_tree);
assert_eq!(chain.account_tree, deserialized.account_tree);
assert_eq!(chain.pending_transactions, deserialized.pending_transactions);
assert_eq!(chain.committed_accounts, deserialized.committed_accounts);
assert_eq!(chain.committed_notes, deserialized.committed_notes);
assert_eq!(chain.account_authenticators, deserialized.account_authenticators);
}
#[test]
fn mock_chain_block_signature() -> anyhow::Result<()> {
let mut builder = MockChain::builder();
builder.add_existing_mock_account(Auth::IncrNonce)?;
let mut chain = builder.build()?;
let genesis_block = chain.latest_block();
assert!(
genesis_block.signature().verify(
genesis_block.header().commitment(),
genesis_block.header().validator_key()
)
);
chain.prove_next_block()?;
let next_block = chain.latest_block();
assert!(
next_block
.signature()
.verify(next_block.header().commitment(), next_block.header().validator_key())
);
assert_eq!(next_block.header().validator_key(), next_block.header().validator_key());
Ok(())
}
#[tokio::test]
async fn add_pending_batch() -> anyhow::Result<()> {
let mut builder = MockChain::builder();
let account = builder.add_existing_mock_account(Auth::IncrNonce)?;
let mut chain = builder.build()?;
let tx = chain.build_tx_context(account.id(), &[], &[])?.build()?.execute().await?;
let proven_tx = LocalTransactionProver::default().prove_dummy(tx)?;
let proposed_batch = chain.propose_transaction_batch(vec![proven_tx])?;
let proven_batch = chain.prove_transaction_batch(proposed_batch)?;
let num_blocks_before = chain.proven_blocks().len();
chain.add_pending_batch(proven_batch);
chain.prove_next_block()?;
assert_eq!(chain.proven_blocks().len(), num_blocks_before + 1);
Ok(())
}
}