miden_testing/mock_chain/
chain.rs

1use alloc::{
2    boxed::Box,
3    collections::{BTreeMap, BTreeSet},
4    string::ToString,
5    vec::Vec,
6};
7
8use anyhow::Context;
9use miden_block_prover::{LocalBlockProver, ProvenBlockError};
10use miden_lib::{
11    account::{faucets::BasicFungibleFaucet, wallets::BasicWallet},
12    note::{create_p2id_note, create_p2ide_note},
13    transaction::{TransactionKernel, memory},
14};
15use miden_objects::{
16    MAX_BATCHES_PER_BLOCK, MAX_OUTPUT_NOTES_PER_BATCH, NoteError,
17    account::{
18        Account, AccountBuilder, AccountId, AccountStorageMode, AccountType, StorageSlot,
19        delta::AccountUpdateDetails,
20    },
21    asset::{Asset, TokenSymbol},
22    batch::{ProposedBatch, ProvenBatch},
23    block::{
24        AccountTree, AccountWitness, BlockAccountUpdate, BlockHeader, BlockInputs, BlockNoteTree,
25        BlockNumber, Blockchain, NullifierTree, NullifierWitness, ProposedBlock, ProvenBlock,
26    },
27    crypto::merkle::SmtProof,
28    note::{Note, NoteHeader, NoteId, NoteInclusionProof, NoteType, Nullifier},
29    transaction::{
30        AccountInputs, ExecutedTransaction, InputNote, InputNotes, OrderedTransactionHeaders,
31        OutputNote, PartialBlockchain, ProvenTransaction, TransactionHeader, TransactionInputs,
32    },
33};
34use rand::{Rng, SeedableRng};
35use rand_chacha::ChaCha20Rng;
36use vm_processor::{Digest, Felt, Word, ZERO, crypto::RpoRandomCoin};
37
38use super::note::MockChainNote;
39use crate::{
40    Auth, MockFungibleFaucet, ProvenTransactionExt, TransactionContextBuilder,
41    mock_chain::account::MockAccount,
42};
43
44// MOCK CHAIN
45// ================================================================================================
46
47/// The [`MockChain`] simulates a simplified blockchain environment for testing purposes.
48/// It allows creating and managing accounts, minting assets, executing transactions, and applying
49/// state updates.
50///
51/// This struct is designed to mock transaction workflows, asset transfers, and
52/// note creation in a test setting. Once entities are set up, [`TransactionContextBuilder`] objects
53/// can be obtained in order to execute transactions accordingly.
54///
55/// On a high-level, there are two ways to interact with the mock chain:
56/// - Generating transactions yourself and adding them to the mock chain "mempool" using
57///   [`MockChain::add_pending_executed_transaction`] or
58///   [`MockChain::add_pending_proven_transaction`]. Once some transactions have been added, they
59///   can be proven into a block using [`MockChain::prove_next_block`], which commits them to the
60///   chain state.
61/// - Using any of the other pending APIs to _magically_ add new notes, accounts or nullifiers in
62///   the next block. For example, [`MockChain::add_pending_p2id_note`] will create a new P2ID note
63///   in the next proven block, without actually containing a transaction that creates that note.
64///
65/// Both approaches can be mixed in the same block, within limits. In particular, avoid modification
66/// of the _same_ entities using both regular transactions and the magic pending APIs.
67///
68/// The mock chain uses the batch and block provers underneath to process pending transactions, so
69/// the generated blocks are realistic and indistinguishable from a real node. The only caveat is
70/// that no real ZK proofs are generated or validated as part of transaction, batch or block
71/// building. If realistic data is important for your use case, avoid using any pending APIs except
72/// for [`MockChain::add_pending_executed_transaction`] and
73/// [`MockChain::add_pending_proven_transaction`].
74///
75/// # Examples
76///
77/// ## Create mock objects and build a transaction context
78/// ```
79/// # use anyhow::Result;
80/// # use miden_objects::{Felt, asset::FungibleAsset, note::NoteType};
81/// # use miden_testing::{Auth, MockChain, TransactionContextBuilder};
82///
83/// # fn main() -> Result<()> {
84/// let mut mock_chain = MockChain::new();
85///
86/// let faucet = mock_chain.add_pending_new_faucet(Auth::BasicAuth, "USDT", 100_000)?;
87/// let _asset = faucet.mint(1000);
88///
89/// let sender = mock_chain.add_pending_new_wallet(Auth::BasicAuth);
90/// let target = mock_chain.add_pending_new_wallet(Auth::BasicAuth);
91///
92/// let note = mock_chain.add_pending_p2id_note(
93///     faucet.id(),
94///     target.id(),
95///     &[FungibleAsset::mock(10)],
96///     NoteType::Public,
97/// )?;
98///
99/// mock_chain.prove_next_block()?;
100///
101/// let tx_context = mock_chain.build_tx_context(sender.id(), &[note.id()], &[])?.build()?;
102/// let result = tx_context.execute();
103/// # Ok(())
104/// # }
105/// ```
106///
107/// ## Executing a simple transaction
108/// ```
109/// # use anyhow::Result;
110/// # use miden_objects::{
111/// #    asset::{Asset, FungibleAsset},
112/// #    note::NoteType,
113/// # };
114/// # use miden_testing::{Auth, MockChain};
115///
116/// # fn main() -> Result<()> {
117/// let mut mock_chain = MockChain::new();
118///
119/// // Add a recipient wallet.
120/// let receiver = mock_chain.add_pending_new_wallet(Auth::BasicAuth);
121///
122/// // Add a wallet with assets.
123/// let sender = mock_chain.add_pending_existing_wallet(Auth::IncrNonce, vec![]);
124/// let fungible_asset = FungibleAsset::mock(10).unwrap_fungible();
125///
126/// // Add a pending P2ID note to the chain.
127/// let note = mock_chain.add_pending_p2id_note(
128///     sender.id(),
129///     receiver.id(),
130///     &[Asset::Fungible(fungible_asset)],
131///     NoteType::Public,
132/// )?;
133///
134/// mock_chain.prove_next_block()?;
135///
136/// let transaction = mock_chain
137///     .build_tx_context(receiver.id(), &[note.id()], &[])?
138///     .build()?
139///     .execute()?;
140///
141/// // Add the transaction to the mock chain's "mempool" of pending transactions.
142/// mock_chain.add_pending_executed_transaction(&transaction);
143///
144/// // Prove the next block to include the transaction in the chain state.
145/// mock_chain.prove_next_block()?;
146///
147/// assert_eq!(
148///     mock_chain
149///         .committed_account(receiver.id())?
150///         .vault()
151///         .get_balance(fungible_asset.faucet_id())?,
152///     fungible_asset.amount()
153/// );
154/// # Ok(())
155/// # }
156/// ```
157#[derive(Debug, Clone)]
158pub struct MockChain {
159    /// An append-only structure used to represent the history of blocks produced for this chain.
160    chain: Blockchain,
161
162    /// History of produced blocks.
163    blocks: Vec<ProvenBlock>,
164
165    /// Tree containing all nullifiers.
166    nullifier_tree: NullifierTree,
167
168    /// Tree containing the state commitments of all accounts.
169    account_tree: AccountTree,
170
171    /// Objects that have not yet been finalized.
172    ///
173    /// These will become available once the block is proven.
174    ///
175    /// Note:
176    /// - The [Note]s in this container do not have the `proof` set.
177    pending_objects: PendingObjects,
178
179    /// Transactions that have been submitted to the chain but have not yet been included in a
180    /// block.
181    pending_transactions: Vec<ProvenTransaction>,
182
183    /// NoteID |-> MockChainNote mapping to simplify note retrieval.
184    committed_notes: BTreeMap<NoteId, MockChainNote>,
185
186    /// AccountId |-> MockAccount mapping to simplify transaction creation. Latest known account
187    /// state is maintained for each account here.
188    ///
189    /// The map always holds the most recent *public* state known for every account. For private
190    /// accounts, however, transactions do not emit the post-transaction state, so their entries
191    /// remain at the last observed state.
192    committed_accounts: BTreeMap<AccountId, MockAccount>,
193
194    // The RNG used to generate note serial numbers, account seeds or cryptographic keys.
195    rng: ChaCha20Rng,
196}
197
198impl MockChain {
199    // CONSTANTS
200    // ----------------------------------------------------------------------------------------
201
202    /// The timestamp of the genesis block of the chain. Chosen as an easily readable number.
203    pub const TIMESTAMP_START_SECS: u32 = 1700000000;
204
205    /// The number of seconds by which a block's timestamp increases over the previous block's
206    /// timestamp, unless overwritten when calling [`Self::prove_next_block_at`].
207    pub const TIMESTAMP_STEP_SECS: u32 = 10;
208
209    // CONSTRUCTORS
210    // ----------------------------------------------------------------------------------------
211
212    /// Creates a new `MockChain` with an empty genesis block.
213    pub fn new() -> Self {
214        Self::with_accounts(&[]).expect("empty mockchain is valid")
215    }
216
217    /// Creates a new `MockChain` with a genesis block containing the provided accounts.
218    pub fn with_accounts(accounts: &[Account]) -> anyhow::Result<Self> {
219        let (genesis_block, account_tree) = create_genesis_state(accounts.iter().cloned())
220            .context("failed to build account from builder")?;
221
222        let mut chain = MockChain {
223            chain: Blockchain::default(),
224            blocks: vec![],
225            nullifier_tree: NullifierTree::default(),
226            account_tree,
227            pending_objects: PendingObjects::new(),
228            pending_transactions: Vec::new(),
229            committed_notes: BTreeMap::new(),
230            committed_accounts: BTreeMap::new(),
231            // Initialize RNG with default seed.
232            rng: ChaCha20Rng::from_seed(Default::default()),
233        };
234
235        // We do not have to apply the tree changes, because the account tree is already initialized
236        // and the nullifier tree is empty at genesis.
237        chain
238            .apply_block(genesis_block)
239            .context("failed to build account from builder")?;
240
241        debug_assert_eq!(chain.blocks.len(), 1);
242        debug_assert_eq!(chain.account_tree.num_accounts(), accounts.len());
243        debug_assert_eq!(chain.committed_accounts.len(), accounts.len());
244        for added_account in accounts {
245            debug_assert_eq!(
246                chain.account_tree.get(added_account.id()),
247                added_account.commitment()
248            );
249            debug_assert_eq!(
250                chain.committed_account(added_account.id())?.commitment(),
251                added_account.commitment(),
252            );
253        }
254
255        Ok(chain)
256    }
257
258    // PUBLIC ACCESSORS
259    // ----------------------------------------------------------------------------------------
260
261    /// Returns a reference to the current [`Blockchain`].
262    pub fn blockchain(&self) -> &Blockchain {
263        &self.chain
264    }
265
266    /// Returns a [`PartialBlockchain`] instantiated from the current [`Blockchain`] and with
267    /// authentication paths for all all blocks in the chain.
268    pub fn latest_partial_blockchain(&self) -> PartialBlockchain {
269        // We have to exclude the latest block because we need to fetch the state of the chain at
270        // that latest block, which does not include itself.
271        let block_headers =
272            self.blocks.iter().map(|b| b.header()).take(self.blocks.len() - 1).cloned();
273
274        PartialBlockchain::from_blockchain(&self.chain, block_headers)
275            .expect("blockchain should be valid by construction")
276    }
277
278    /// Creates a new [`PartialBlockchain`] with all reference blocks in the given iterator except
279    /// for the latest block header in the chain and returns that latest block header.
280    ///
281    /// The intended use for the latest block header is to become the reference block of a new
282    /// transaction batch or block.
283    pub fn latest_selective_partial_blockchain(
284        &self,
285        reference_blocks: impl IntoIterator<Item = BlockNumber>,
286    ) -> anyhow::Result<(BlockHeader, PartialBlockchain)> {
287        let latest_block_header = self.latest_block_header();
288        // Deduplicate block numbers so each header will be included just once. This is required so
289        // PartialBlockchain::from_blockchain does not panic.
290        let reference_blocks: BTreeSet<_> = reference_blocks.into_iter().collect();
291
292        // Include all block headers of the reference blocks except the latest block.
293        let mut block_headers = Vec::new();
294
295        for block_ref_num in &reference_blocks {
296            let block_index = block_ref_num.as_usize();
297            let block = self
298                .blocks
299                .get(block_index)
300                .ok_or_else(|| anyhow::anyhow!("block {} not found in chain", block_ref_num))?;
301            let block_header = block.header().clone();
302            // Exclude the latest block header
303            if block_header.commitment() != latest_block_header.commitment() {
304                block_headers.push(block_header);
305            }
306        }
307
308        let partial_blockchain = PartialBlockchain::from_blockchain(&self.chain, block_headers)?;
309
310        Ok((latest_block_header, partial_blockchain))
311    }
312
313    /// Returns a map of [`AccountWitness`]es for the requested account IDs from the current
314    /// [`AccountTree`] in the chain.
315    pub fn account_witnesses(
316        &self,
317        account_ids: impl IntoIterator<Item = AccountId>,
318    ) -> BTreeMap<AccountId, AccountWitness> {
319        let mut account_witnesses = BTreeMap::new();
320
321        for account_id in account_ids {
322            let witness = self.account_tree.open(account_id);
323            account_witnesses.insert(account_id, witness);
324        }
325
326        account_witnesses
327    }
328
329    /// Returns a map of [`NullifierWitness`]es for the requested nullifiers from the current
330    /// [`NullifierTree`] in the chain.
331    pub fn nullifier_witnesses(
332        &self,
333        nullifiers: impl IntoIterator<Item = Nullifier>,
334    ) -> BTreeMap<Nullifier, NullifierWitness> {
335        let mut nullifier_proofs = BTreeMap::new();
336
337        for nullifier in nullifiers {
338            let witness = self.nullifier_tree.open(&nullifier);
339            nullifier_proofs.insert(nullifier, witness);
340        }
341
342        nullifier_proofs
343    }
344
345    /// Returns all note inclusion proofs for the requested note IDs, **if they are available for
346    /// consumption**. Therefore, not all of the requested notes will be guaranteed to have an entry
347    /// in the returned map.
348    pub fn unauthenticated_note_proofs(
349        &self,
350        notes: impl IntoIterator<Item = NoteId>,
351    ) -> BTreeMap<NoteId, NoteInclusionProof> {
352        let mut proofs = BTreeMap::default();
353        for note in notes {
354            if let Some(input_note) = self.committed_notes.get(&note) {
355                proofs.insert(note, input_note.inclusion_proof().clone());
356            }
357        }
358
359        proofs
360    }
361
362    /// Returns a reference to the latest [`BlockHeader`] in the chain.
363    pub fn latest_block_header(&self) -> BlockHeader {
364        let chain_tip =
365            self.chain.chain_tip().expect("chain should contain at least the genesis block");
366        self.blocks[chain_tip.as_usize()].header().clone()
367    }
368
369    /// Returns the [`BlockHeader`] with the specified `block_number`.
370    ///
371    /// # Panics
372    ///
373    /// - If the block number does not exist in the chain.
374    pub fn block_header(&self, block_number: usize) -> BlockHeader {
375        self.blocks[block_number].header().clone()
376    }
377
378    /// Returns a reference to slice of all created proven blocks.
379    pub fn proven_blocks(&self) -> &[ProvenBlock] {
380        &self.blocks
381    }
382
383    /// Returns a reference to the nullifier tree.
384    pub fn nullifier_tree(&self) -> &NullifierTree {
385        &self.nullifier_tree
386    }
387
388    /// Returns the map of note IDs to committed notes.
389    ///
390    /// These notes are committed for authenticated consumption.
391    pub fn committed_notes(&self) -> &BTreeMap<NoteId, MockChainNote> {
392        &self.committed_notes
393    }
394
395    /// Returns an [`InputNote`] for the given note ID. If the note does not exist or is not
396    /// public, `None` is returned.
397    pub fn get_public_note(&self, note_id: &NoteId) -> Option<InputNote> {
398        let note = self.committed_notes.get(note_id)?;
399        note.clone().try_into().ok()
400    }
401
402    /// Returns a reference to the account identified by the given account ID.
403    ///
404    /// The account is retrieved with the latest state known to the [`MockChain`].
405    pub fn committed_account(&self, account_id: AccountId) -> anyhow::Result<&Account> {
406        self.committed_accounts
407            .get(&account_id)
408            .map(|mock_account| mock_account.account())
409            .with_context(|| format!("account {account_id} not found in committed accounts"))
410    }
411
412    /// Returns a reference to the [`AccountTree`] of the chain.
413    pub fn account_tree(&self) -> &AccountTree {
414        &self.account_tree
415    }
416
417    // BATCH APIS
418    // ----------------------------------------------------------------------------------------
419
420    /// Proposes a new transaction batch from the provided transactions and returns it.
421    ///
422    /// This method does not modify the chain state.
423    pub fn propose_transaction_batch<I>(
424        &self,
425        txs: impl IntoIterator<Item = ProvenTransaction, IntoIter = I>,
426    ) -> anyhow::Result<ProposedBatch>
427    where
428        I: Iterator<Item = ProvenTransaction> + Clone,
429    {
430        let transactions: Vec<_> = txs.into_iter().map(alloc::sync::Arc::new).collect();
431
432        let (batch_reference_block, partial_blockchain, unauthenticated_note_proofs) = self
433            .get_batch_inputs(
434                transactions.iter().map(|tx| tx.ref_block_num()),
435                transactions
436                    .iter()
437                    .flat_map(|tx| tx.unauthenticated_notes().map(NoteHeader::id)),
438            )?;
439
440        Ok(ProposedBatch::new(
441            transactions,
442            batch_reference_block,
443            partial_blockchain,
444            unauthenticated_note_proofs,
445        )?)
446    }
447
448    /// Mock-proves a proposed transaction batch from the provided [`ProposedBatch`] and returns it.
449    ///
450    /// This method does not modify the chain state.
451    pub fn prove_transaction_batch(
452        &self,
453        proposed_batch: ProposedBatch,
454    ) -> anyhow::Result<ProvenBatch> {
455        let (
456            transactions,
457            block_header,
458            _partial_blockchain,
459            _unauthenticated_note_proofs,
460            id,
461            account_updates,
462            input_notes,
463            output_notes,
464            batch_expiration_block_num,
465        ) = proposed_batch.into_parts();
466
467        // SAFETY: This satisfies the requirements of the ordered tx headers.
468        let tx_headers = OrderedTransactionHeaders::new_unchecked(
469            transactions
470                .iter()
471                .map(AsRef::as_ref)
472                .map(TransactionHeader::from)
473                .collect::<Vec<_>>(),
474        );
475
476        Ok(ProvenBatch::new(
477            id,
478            block_header.commitment(),
479            block_header.block_num(),
480            account_updates,
481            input_notes,
482            output_notes,
483            batch_expiration_block_num,
484            tx_headers,
485        )?)
486    }
487
488    // BLOCK APIS
489    // ----------------------------------------------------------------------------------------
490
491    /// Proposes a new block from the provided batches with the given timestamp and returns it.
492    ///
493    /// This method does not modify the chain state.
494    pub fn propose_block_at<I>(
495        &self,
496        batches: impl IntoIterator<Item = ProvenBatch, IntoIter = I>,
497        timestamp: u32,
498    ) -> anyhow::Result<ProposedBlock>
499    where
500        I: Iterator<Item = ProvenBatch> + Clone,
501    {
502        let batches: Vec<_> = batches.into_iter().collect();
503
504        let block_inputs = self
505            .get_block_inputs(batches.iter())
506            .context("could not retrieve block inputs")?;
507
508        let proposed_block = ProposedBlock::new_at(block_inputs, batches, timestamp)
509            .context("failed to create proposed block")?;
510
511        Ok(proposed_block)
512    }
513
514    /// Proposes a new block from the provided batches and returns it.
515    ///
516    /// This method does not modify the chain state.
517    pub fn propose_block<I>(
518        &self,
519        batches: impl IntoIterator<Item = ProvenBatch, IntoIter = I>,
520    ) -> anyhow::Result<ProposedBlock>
521    where
522        I: Iterator<Item = ProvenBatch> + Clone,
523    {
524        // We can't access system time because we are in a no-std environment, so we use the
525        // minimally correct next timestamp.
526        let timestamp = self.latest_block_header().timestamp() + 1;
527
528        self.propose_block_at(batches, timestamp)
529    }
530
531    /// Mock-proves a proposed block into a proven block and returns it.
532    ///
533    /// This method does not modify the chain state.
534    pub fn prove_block(
535        &self,
536        proposed_block: ProposedBlock,
537    ) -> Result<ProvenBlock, ProvenBlockError> {
538        LocalBlockProver::new(0).prove_without_batch_verification(proposed_block)
539    }
540
541    // TRANSACTION APIS
542    // ----------------------------------------------------------------------------------------
543
544    /// Initializes a [`TransactionContextBuilder`].
545    ///
546    /// Depending on the provided `input`, the builder is initialized differently:
547    /// - [`TxContextInput::AccountId`]: Initialize the builder with [`TransactionInputs`] fetched
548    ///   from the chain for the public account identified by the ID.
549    /// - [`TxContextInput::Account`]: Initialize the builder with [`TransactionInputs`] where the
550    ///   account is passed as-is to the inputs.
551    /// - [`TxContextInput::ExecutedTransaction`]: Initialize the builder with [`TransactionInputs`]
552    ///   where the account passed to the inputs is the final account of the executed transaction.
553    ///   This is the initial account of the transaction with the account delta applied.
554    ///
555    /// In all cases, if the chain contains a seed or authenticator for the account, they are added
556    /// to the builder.
557    ///
558    /// [`TxContextInput::Account`] and [`TxContextInput::ExecutedTransaction`] can be used to build
559    /// a chain of transactions against the same account that build on top of each other. For
560    /// example, transaction A modifies an account from state 0 to 1, and transaction B modifies
561    /// it from state 1 to 2.
562    pub fn build_tx_context(
563        &self,
564        input: impl Into<TxContextInput>,
565        note_ids: &[NoteId],
566        unauthenticated_notes: &[Note],
567    ) -> anyhow::Result<TransactionContextBuilder> {
568        let mock_account = match input.into() {
569            TxContextInput::AccountId(account_id) => {
570                if account_id.is_private() {
571                    return Err(anyhow::anyhow!(
572                        "transaction contexts for private accounts should be created with TxContextInput::Account"
573                    ));
574                }
575
576                self.committed_accounts
577                    .get(&account_id)
578                    .with_context(|| {
579                        format!("account {account_id} not found in committed accounts")
580                    })?
581                    .clone()
582            },
583            TxContextInput::Account(account) => {
584                let committed_account = self.committed_accounts.get(&account.id());
585                let authenticator = committed_account.and_then(|a| a.authenticator());
586                let seed = committed_account.and_then(|a| a.seed());
587                MockAccount::new(account, seed.cloned(), authenticator.cloned())
588            },
589            TxContextInput::ExecutedTransaction(executed_transaction) => {
590                let mut initial_account = executed_transaction.initial_account().clone();
591                initial_account
592                    .apply_delta(executed_transaction.account_delta())
593                    .context("could not apply delta from previous transaction")?;
594
595                let committed_account = self.committed_accounts.get(&initial_account.id());
596                let authenticator = committed_account.and_then(|a| a.authenticator());
597                let seed = committed_account.and_then(|a| a.seed());
598                MockAccount::new(initial_account, seed.cloned(), authenticator.cloned())
599            },
600        };
601
602        let tx_inputs = self
603            .get_transaction_inputs(
604                mock_account.account().clone(),
605                mock_account.seed().cloned(),
606                note_ids,
607                unauthenticated_notes,
608            )
609            .context("failed to gather transaction inputs")?;
610
611        let tx_context_builder = TransactionContextBuilder::new(mock_account.account().clone())
612            .authenticator(mock_account.authenticator().cloned())
613            .account_seed(mock_account.seed().cloned())
614            .tx_inputs(tx_inputs);
615
616        Ok(tx_context_builder)
617    }
618
619    // INPUTS APIS
620    // ----------------------------------------------------------------------------------------
621
622    /// Returns a valid [`TransactionInputs`] for the specified entities.
623    pub fn get_transaction_inputs(
624        &self,
625        account: Account,
626        account_seed: Option<Word>,
627        notes: &[NoteId],
628        unauthenticated_notes: &[Note],
629    ) -> anyhow::Result<TransactionInputs> {
630        let block = self.blocks.last().expect("at least one block should have been created");
631
632        let mut input_notes = vec![];
633        let mut block_headers_map: BTreeMap<BlockNumber, BlockHeader> = BTreeMap::new();
634        for note in notes {
635            let input_note: InputNote = self
636                .committed_notes
637                .get(note)
638                .with_context(|| format!("note with id {note} not found"))?
639                .clone()
640                .try_into()
641                .context("note error")?;
642
643            let note_block_num = input_note
644                .location()
645                .with_context(|| format!("note location not available: {note}"))?
646                .block_num();
647
648            if note_block_num != block.header().block_num() {
649                let block_header = self
650                    .blocks
651                    .get(note_block_num.as_usize())
652                    .with_context(|| format!("block {note_block_num} not found in chain"))?
653                    .header()
654                    .clone();
655                block_headers_map.insert(note_block_num, block_header);
656            }
657
658            input_notes.push(input_note);
659        }
660
661        for note in unauthenticated_notes {
662            input_notes.push(InputNote::Unauthenticated { note: note.clone() })
663        }
664
665        let block_headers = block_headers_map.values().cloned();
666        let mmr = PartialBlockchain::from_blockchain(&self.chain, block_headers)?;
667
668        let input_notes = InputNotes::new(input_notes)?;
669
670        Ok(TransactionInputs::new(
671            account,
672            account_seed,
673            block.header().clone(),
674            mmr,
675            input_notes,
676        )?)
677    }
678
679    /// Returns inputs for a transaction batch for all the reference blocks of the provided
680    /// transactions.
681    pub fn get_batch_inputs(
682        &self,
683        tx_reference_blocks: impl IntoIterator<Item = BlockNumber>,
684        unauthenticated_notes: impl Iterator<Item = NoteId>,
685    ) -> anyhow::Result<(BlockHeader, PartialBlockchain, BTreeMap<NoteId, NoteInclusionProof>)>
686    {
687        // Fetch note proofs for notes that exist in the chain.
688        let unauthenticated_note_proofs = self.unauthenticated_note_proofs(unauthenticated_notes);
689
690        // We also need to fetch block inclusion proofs for any of the blocks that contain
691        // unauthenticated notes for which we want to prove inclusion.
692        let required_blocks = tx_reference_blocks.into_iter().chain(
693            unauthenticated_note_proofs
694                .values()
695                .map(|note_proof| note_proof.location().block_num()),
696        );
697
698        let (batch_reference_block, partial_block_chain) =
699            self.latest_selective_partial_blockchain(required_blocks)?;
700
701        Ok((batch_reference_block, partial_block_chain, unauthenticated_note_proofs))
702    }
703
704    /// Gets foreign account inputs to execute FPI transactions.
705    pub fn get_foreign_account_inputs(
706        &self,
707        account_id: AccountId,
708    ) -> anyhow::Result<AccountInputs> {
709        let account = self.committed_account(account_id)?;
710
711        let account_witness = self.account_tree().open(account_id);
712        assert_eq!(account_witness.state_commitment(), account.commitment());
713
714        let mut storage_map_proofs = vec![];
715        for slot in account.storage().slots() {
716            // if there are storage maps, we populate the merkle store and advice map
717            if let StorageSlot::Map(map) = slot {
718                let proofs: Vec<SmtProof> = map.entries().map(|(key, _)| map.open(key)).collect();
719                storage_map_proofs.extend(proofs);
720            }
721        }
722
723        Ok(AccountInputs::new(account.into(), account_witness))
724    }
725
726    /// Gets the inputs for a block for the provided batches.
727    pub fn get_block_inputs<'batch, I>(
728        &self,
729        batch_iter: impl IntoIterator<Item = &'batch ProvenBatch, IntoIter = I>,
730    ) -> anyhow::Result<BlockInputs>
731    where
732        I: Iterator<Item = &'batch ProvenBatch> + Clone,
733    {
734        let batch_iterator = batch_iter.into_iter();
735
736        let unauthenticated_note_proofs =
737            self.unauthenticated_note_proofs(batch_iterator.clone().flat_map(|batch| {
738                batch.input_notes().iter().filter_map(|note| note.header().map(NoteHeader::id))
739            }));
740
741        let (block_reference_block, partial_blockchain) = self
742            .latest_selective_partial_blockchain(
743                batch_iterator.clone().map(ProvenBatch::reference_block_num).chain(
744                    unauthenticated_note_proofs.values().map(|proof| proof.location().block_num()),
745                ),
746            )?;
747
748        let account_witnesses =
749            self.account_witnesses(batch_iterator.clone().flat_map(ProvenBatch::updated_accounts));
750
751        let nullifier_proofs =
752            self.nullifier_witnesses(batch_iterator.flat_map(ProvenBatch::created_nullifiers));
753
754        Ok(BlockInputs::new(
755            block_reference_block,
756            partial_blockchain,
757            account_witnesses,
758            nullifier_proofs,
759            unauthenticated_note_proofs,
760        ))
761    }
762
763    // PUBLIC MUTATORS
764    // ----------------------------------------------------------------------------------------
765
766    /// Creates the next block in the mock chain.
767    ///
768    /// This will make all the objects currently pending available for use.
769    pub fn prove_next_block(&mut self) -> anyhow::Result<ProvenBlock> {
770        self.prove_block_inner(None)
771    }
772
773    /// Proves the next block in the mock chain at the given timestamp.
774    pub fn prove_next_block_at(&mut self, timestamp: u32) -> anyhow::Result<ProvenBlock> {
775        self.prove_block_inner(Some(timestamp))
776    }
777
778    /// Proves new blocks until the block with the given target block number has been created.
779    ///
780    /// For example, if the latest block is `5` and this function is called with `10`, then blocks
781    /// `6..=10` will be created and block 10 will be returned.
782    ///
783    /// # Panics
784    ///
785    /// Panics if:
786    /// - the given block number is smaller or equal to the number of the latest block in the chain.
787    pub fn prove_until_block(
788        &mut self,
789        target_block_num: impl Into<BlockNumber>,
790    ) -> anyhow::Result<ProvenBlock> {
791        let target_block_num = target_block_num.into();
792        let latest_block_num = self.latest_block_header().block_num();
793        assert!(
794            target_block_num > latest_block_num,
795            "target block number must be greater than the number of the latest block in the chain"
796        );
797
798        let mut last_block = None;
799        for _ in latest_block_num.as_usize()..target_block_num.as_usize() {
800            last_block = Some(self.prove_next_block()?);
801        }
802
803        Ok(last_block.expect("at least one block should have been created"))
804    }
805
806    /// Sets the seed for the internal RNG.
807    pub fn set_rng_seed(&mut self, seed: [u8; 32]) {
808        self.rng = ChaCha20Rng::from_seed(seed);
809    }
810
811    // PUBLIC MUTATORS (PENDING APIS)
812    // ----------------------------------------------------------------------------------------
813
814    /// Adds the given [`ExecutedTransaction`] to the list of pending transactions.
815    ///
816    /// A block has to be created to apply the transaction effects to the chain state, e.g. using
817    /// [`MockChain::prove_next_block`].
818    ///
819    /// Returns the resulting state of the executing account after executing the transaction.
820    pub fn add_pending_executed_transaction(
821        &mut self,
822        transaction: &ExecutedTransaction,
823    ) -> anyhow::Result<Account> {
824        let mut account = transaction.initial_account().clone();
825        account.apply_delta(transaction.account_delta())?;
826
827        // This essentially transforms an executed tx into a proven tx with a dummy proof.
828        let proven_tx = ProvenTransaction::from_executed_transaction_mocked(transaction.clone());
829
830        self.pending_transactions.push(proven_tx);
831
832        Ok(account)
833    }
834
835    /// Adds the given [`ProvenTransaction`] to the list of pending transactions.
836    ///
837    /// A block has to be created to apply the transaction effects to the chain state, e.g. using
838    /// [`MockChain::prove_next_block`].
839    pub fn add_pending_proven_transaction(&mut self, transaction: ProvenTransaction) {
840        self.pending_transactions.push(transaction);
841    }
842
843    /// Adds the given [`OutputNote`] to the list of pending notes.
844    ///
845    /// A block has to be created to add the note to that block and make it available in the chain
846    /// state, e.g. using [`MockChain::prove_next_block`].
847    pub fn add_pending_note(&mut self, note: OutputNote) {
848        self.pending_objects.output_notes.push(note);
849    }
850
851    /// Adds a plain P2ID [`OutputNote`] to the list of pending notes.
852    ///
853    /// The note is immediately spendable by `target_account_id` and carries no
854    /// additional reclaim or timelock conditions.
855    pub fn add_pending_p2id_note(
856        &mut self,
857        sender_account_id: AccountId,
858        target_account_id: AccountId,
859        asset: &[Asset],
860        note_type: NoteType,
861    ) -> Result<Note, NoteError> {
862        let mut rng = RpoRandomCoin::new(Word::default());
863
864        let note = create_p2id_note(
865            sender_account_id,
866            target_account_id,
867            asset.to_vec(),
868            note_type,
869            Default::default(),
870            &mut rng,
871        )?;
872
873        self.add_pending_note(OutputNote::Full(note.clone()));
874        Ok(note)
875    }
876
877    /// Adds a P2IDE [`OutputNote`] (pay‑to‑ID‑escape) to the list of pending notes.
878    ///
879    /// A P2IDE note can include an optional `timelock_height` and/or an optional
880    /// `reclaim_height` after which the `sender_account_id` may reclaim the
881    /// funds.
882    pub fn add_pending_p2ide_note(
883        &mut self,
884        sender_account_id: AccountId,
885        target_account_id: AccountId,
886        asset: &[Asset],
887        note_type: NoteType,
888        reclaim_height: Option<BlockNumber>,
889        timelock_height: Option<BlockNumber>,
890    ) -> Result<Note, NoteError> {
891        let mut rng = RpoRandomCoin::new(Word::default());
892
893        let note = create_p2ide_note(
894            sender_account_id,
895            target_account_id,
896            asset.to_vec(),
897            reclaim_height,
898            timelock_height,
899            note_type,
900            Default::default(),
901            &mut rng,
902        )?;
903
904        self.add_pending_note(OutputNote::Full(note.clone()));
905        Ok(note)
906    }
907
908    /// Adds the [`Nullifier`] to the list of pending nullifiers.
909    ///
910    /// A block has to be created to add the nullifier to the nullifier tree as part of that block,
911    /// e.g. using [`MockChain::prove_next_block`].
912    pub fn add_pending_nullifier(&mut self, nullifier: Nullifier) {
913        self.pending_objects.created_nullifiers.push(nullifier);
914    }
915
916    /// Adds a new public [`BasicWallet`] account to the list of pending accounts.
917    ///
918    /// A block has to be created to add the account to the chain state as part of that block,
919    /// e.g. using [`MockChain::prove_next_block`].
920    pub fn add_pending_new_wallet(&mut self, auth_method: Auth) -> Account {
921        let account_builder = AccountBuilder::new(self.rng.random())
922            .storage_mode(AccountStorageMode::Public)
923            .with_component(BasicWallet);
924
925        self.add_pending_account_from_builder(auth_method, account_builder, AccountState::New)
926            .expect("failed to add pending account from builder")
927    }
928
929    /// Adds an existing public [`BasicWallet`] account with nonce `1` to the list of pending
930    /// accounts.
931    ///
932    /// A block has to be created to add the account to the chain state as part of that block,
933    /// e.g. using [`MockChain::prove_next_block`].
934    pub fn add_pending_existing_wallet(
935        &mut self,
936        auth_method: Auth,
937        assets: Vec<Asset>,
938    ) -> Account {
939        let account_builder = Account::builder(self.rng.random())
940            .storage_mode(AccountStorageMode::Public)
941            .with_component(BasicWallet)
942            .with_assets(assets);
943
944        self.add_pending_account_from_builder(auth_method, account_builder, AccountState::Exists)
945            .expect("failed to add pending account from builder")
946    }
947
948    /// Adds a new public [`BasicFungibleFaucet`] account with the specified authentication method
949    /// and the given token metadata to the list of pending accounts.
950    ///
951    /// A block has to be created to add the account to the chain state as part of that block,
952    /// e.g. using [`MockChain::prove_next_block`].
953    pub fn add_pending_new_faucet(
954        &mut self,
955        auth_method: Auth,
956        token_symbol: &str,
957        max_supply: u64,
958    ) -> anyhow::Result<MockFungibleFaucet> {
959        let token_symbol = TokenSymbol::new(token_symbol)
960            .with_context(|| format!("invalid token symbol: {token_symbol}"))?;
961        let max_supply_felt = max_supply.try_into().map_err(|_| {
962            anyhow::anyhow!("max supply value cannot be converted to Felt: {max_supply}")
963        })?;
964        let basic_faucet = BasicFungibleFaucet::new(token_symbol, 10, max_supply_felt)
965            .context("failed to create BasicFungibleFaucet")?;
966
967        let account_builder = AccountBuilder::new(self.rng.random())
968            .storage_mode(AccountStorageMode::Public)
969            .account_type(AccountType::FungibleFaucet)
970            .with_component(basic_faucet);
971
972        let account =
973            self.add_pending_account_from_builder(auth_method, account_builder, AccountState::New)?;
974
975        Ok(MockFungibleFaucet::new(account))
976    }
977
978    /// Adds an existing [`BasicFungibleFaucet`] account with the specified authentication method
979    /// and the given token metadata to the list of pending accounts.
980    ///
981    /// A block has to be created to add the account to the chain state as part of that block,
982    /// e.g. using [`MockChain::prove_next_block`].
983    pub fn add_pending_existing_faucet(
984        &mut self,
985        auth_method: Auth,
986        token_symbol: &str,
987        max_supply: u64,
988        total_issuance: Option<u64>,
989    ) -> anyhow::Result<MockFungibleFaucet> {
990        let token_symbol = TokenSymbol::new(token_symbol).context("invalid argument")?;
991        let basic_faucet = BasicFungibleFaucet::new(token_symbol, 10u8, Felt::new(max_supply))
992            .context("invalid argument")?;
993
994        let mut account_builder = AccountBuilder::new(self.rng.random())
995            .storage_mode(AccountStorageMode::Public)
996            .with_component(basic_faucet)
997            .account_type(AccountType::FungibleFaucet);
998
999        let (auth_component, authenticator) = auth_method.build_component();
1000        account_builder = account_builder.with_auth_component(auth_component);
1001        let mut account = account_builder
1002            .build_existing()
1003            .context("failed to build account from builder")?;
1004
1005        // The faucet's reserved slot is initialized to an empty word by default.
1006        // If total_issuance is set, overwrite it.
1007        if let Some(issuance) = total_issuance {
1008            account
1009                .storage_mut()
1010                .set_item(memory::FAUCET_STORAGE_DATA_SLOT, [ZERO, ZERO, ZERO, Felt::new(issuance)])
1011                .context("failed to set faucet storage")?;
1012        }
1013
1014        // We have to insert these into the committed accounts so the authenticator is available.
1015        // Without this, the account couldn't be authenticated.
1016        self.committed_accounts
1017            .insert(account.id(), MockAccount::new(account.clone(), None, authenticator));
1018        self.add_pending_account(account.clone());
1019
1020        Ok(MockFungibleFaucet::new(account))
1021    }
1022
1023    /// Adds the [`AccountComponent`](miden_objects::account::AccountComponent) corresponding to
1024    /// `auth_method` to the account in the builder and builds a new or existing account
1025    /// depending on `account_state`.
1026    ///
1027    /// The account is added to the list of committed accounts _and_, if [`AccountState::Exists`] is
1028    /// passed, is also added to the list of pending accounts. Adding it to committed accounts
1029    /// makes the account seed and authenticator available for account creation and
1030    /// authentication, respectively. If the account exists, then the next block that is created
1031    /// will add the pending accounts to the chain state.
1032    pub fn add_pending_account_from_builder(
1033        &mut self,
1034        auth_method: Auth,
1035        mut account_builder: AccountBuilder,
1036        account_state: AccountState,
1037    ) -> anyhow::Result<Account> {
1038        let (auth_component, authenticator) = auth_method.build_component();
1039        account_builder = account_builder.with_auth_component(auth_component);
1040
1041        let (account, seed) = if let AccountState::New = account_state {
1042            let (account, seed) =
1043                account_builder.build().context("failed to build account from builder")?;
1044            (account, Some(seed))
1045        } else {
1046            let account = account_builder
1047                .build_existing()
1048                .context("failed to build account from builder")?;
1049            (account, None)
1050        };
1051
1052        // Add account to the committed accounts so transaction inputs can be retrieved via the mock
1053        // chain APIs.
1054        // We also have to insert these into the committed accounts so the account seed and
1055        // authenticator are available. Without this, the account couldn't be created or
1056        // authenticated.
1057        self.committed_accounts
1058            .insert(account.id(), MockAccount::new(account.clone(), seed, authenticator));
1059
1060        // Do not add new accounts to the pending accounts. Usually, new accounts are added in tests
1061        // to create transactions that create these new accounts in the chain. If we add them to the
1062        // pending accounts and the test calls prove_next_block (which typically happens to add all
1063        // pending objects to the chain state as part of the test setup), the account will be added
1064        // to the chain which means the account-creating transaction fails because the
1065        // account already exists. So new accounts are added only to the committed accounts
1066        // so the transaction context APIs work as expected.
1067        if let AccountState::Exists = account_state {
1068            self.add_pending_account(account.clone());
1069        }
1070
1071        Ok(account)
1072    }
1073
1074    /// Adds a new `Account` to the list of pending objects.
1075    ///
1076    /// A block has to be created to finalize the new entity.
1077    pub fn add_pending_account(&mut self, account: Account) {
1078        let account_id = account.id();
1079        let account_commitment = account.commitment();
1080        let update_details = match account.is_private() {
1081            true => AccountUpdateDetails::Private,
1082            false => AccountUpdateDetails::New(account),
1083        };
1084
1085        self.pending_objects.updated_accounts.insert(
1086            account_id,
1087            BlockAccountUpdate::new(account_id, account_commitment, update_details),
1088        );
1089    }
1090
1091    // PRIVATE HELPERS
1092    // ----------------------------------------------------------------------------------------
1093
1094    /// Inserts the given block's account updates and created nullifiers into the account tree and
1095    /// nullifier tree, respectively.
1096    fn apply_block_tree_updates(&mut self, proven_block: &ProvenBlock) -> anyhow::Result<()> {
1097        for account_update in proven_block.updated_accounts() {
1098            self.account_tree
1099                .insert(account_update.account_id(), account_update.final_state_commitment())
1100                .context("failed to insert account update into account tree")?;
1101        }
1102
1103        for nullifier in proven_block.created_nullifiers() {
1104            self.nullifier_tree
1105                .mark_spent(*nullifier, proven_block.header().block_num())
1106                .context("failed to mark block nullifier as spent")?;
1107
1108            // TODO: Remove from self.committed_notes. This is not critical to have for now. It is
1109            // not straightforward, because committed_notes are indexed by note IDs rather than
1110            // nullifiers, so we'll have to create a second index to do this.
1111        }
1112
1113        Ok(())
1114    }
1115
1116    /// Applies the given block to the chain state, which means:
1117    ///
1118    /// - Updated accounts from the block are updated in the committed accounts.
1119    /// - Created notes are inserted into the committed notes.
1120    /// - Consumed notes are removed from the committed notes.
1121    /// - The block is appended to the [`BlockChain`] and the list of proven blocks.
1122    fn apply_block(&mut self, proven_block: ProvenBlock) -> anyhow::Result<()> {
1123        for account_update in proven_block.updated_accounts() {
1124            match account_update.details() {
1125                AccountUpdateDetails::New(account) => {
1126                    let committed_account =
1127                        self.committed_accounts.get(&account_update.account_id());
1128                    let authenticator =
1129                        committed_account.and_then(|account| account.authenticator());
1130                    let seed = committed_account.and_then(|account| account.seed());
1131
1132                    self.committed_accounts.insert(
1133                        account.id(),
1134                        MockAccount::new(account.clone(), seed.cloned(), authenticator.cloned()),
1135                    );
1136                },
1137                AccountUpdateDetails::Delta(account_delta) => {
1138                    let committed_account =
1139                        self.committed_accounts.get_mut(&account_update.account_id()).ok_or_else(
1140                            || anyhow::anyhow!("account delta in block for non-existent account"),
1141                        )?;
1142                    committed_account
1143                        .apply_delta(account_delta)
1144                        .context("failed to apply account delta")?;
1145                },
1146                // No state to keep for private accounts other than the commitment on the account
1147                // tree
1148                AccountUpdateDetails::Private => {},
1149            }
1150        }
1151
1152        let notes_tree = proven_block.build_output_note_tree();
1153        for (block_note_index, created_note) in proven_block.output_notes() {
1154            let note_path = notes_tree.get_note_path(block_note_index);
1155            let note_inclusion_proof = NoteInclusionProof::new(
1156                proven_block.header().block_num(),
1157                block_note_index.leaf_index_value(),
1158                note_path,
1159            )
1160            .context("failed to construct note inclusion proof")?;
1161
1162            if let OutputNote::Full(note) = created_note {
1163                self.committed_notes
1164                    .insert(note.id(), MockChainNote::Public(note.clone(), note_inclusion_proof));
1165            } else {
1166                self.committed_notes.insert(
1167                    created_note.id(),
1168                    MockChainNote::Private(
1169                        created_note.id(),
1170                        *created_note.metadata(),
1171                        note_inclusion_proof,
1172                    ),
1173                );
1174            }
1175        }
1176
1177        debug_assert_eq!(
1178            self.chain.commitment(),
1179            proven_block.header().chain_commitment(),
1180            "current mock chain commitment and new block's chain commitment should match"
1181        );
1182        debug_assert_eq!(
1183            BlockNumber::from(self.chain.as_mmr().forest() as u32),
1184            proven_block.header().block_num(),
1185            "current mock chain length and new block's number should match"
1186        );
1187
1188        self.chain.push(proven_block.header().commitment());
1189        self.blocks.push(proven_block);
1190
1191        Ok(())
1192    }
1193
1194    fn pending_transactions_to_batches(&mut self) -> anyhow::Result<Vec<ProvenBatch>> {
1195        // Batches must contain at least one transaction, so if there are no pending transactions,
1196        // return early.
1197        if self.pending_transactions.is_empty() {
1198            return Ok(vec![]);
1199        }
1200
1201        let pending_transactions = core::mem::take(&mut self.pending_transactions);
1202
1203        // TODO: Distribute the transactions into multiple batches if the transactions would not fit
1204        // into a single batch (according to max input notes, max output notes and max accounts).
1205        let proposed_batch = self.propose_transaction_batch(pending_transactions)?;
1206        let proven_batch = self.prove_transaction_batch(proposed_batch)?;
1207
1208        Ok(vec![proven_batch])
1209    }
1210
1211    fn apply_pending_objects_to_block(
1212        &mut self,
1213        proven_block: &mut ProvenBlock,
1214    ) -> anyhow::Result<()> {
1215        // Add pending accounts to block.
1216        let pending_account_updates = core::mem::take(&mut self.pending_objects.updated_accounts);
1217
1218        let updated_accounts_block: BTreeSet<AccountId> = proven_block
1219            .updated_accounts()
1220            .iter()
1221            .map(|update| update.account_id())
1222            .collect();
1223
1224        for (id, account_update) in pending_account_updates {
1225            if updated_accounts_block.contains(&id) {
1226                return Err(anyhow::anyhow!(
1227                    "account {id} is already modified in block through transactions",
1228                ));
1229            }
1230
1231            self.account_tree
1232                .insert(id, account_update.final_state_commitment())
1233                .context("failed to insert pending account into tree")?;
1234
1235            proven_block.updated_accounts_mut().push(account_update);
1236        }
1237
1238        // Add pending nullifiers to block.
1239        let pending_created_nullifiers =
1240            core::mem::take(&mut self.pending_objects.created_nullifiers);
1241
1242        let created_nullifiers_block: BTreeSet<Nullifier> =
1243            proven_block.created_nullifiers().iter().copied().collect();
1244
1245        for nullifier in pending_created_nullifiers {
1246            if created_nullifiers_block.contains(&nullifier) {
1247                return Err(anyhow::anyhow!(
1248                    "nullifier {nullifier} is already created in block through transactions",
1249                ));
1250            }
1251
1252            self.nullifier_tree
1253                .mark_spent(nullifier, proven_block.header().block_num())
1254                .context("failed to insert pending nullifier into tree")?;
1255
1256            proven_block.created_nullifiers_mut().push(nullifier);
1257        }
1258
1259        // Add pending output notes to block.
1260        let output_notes_block: BTreeSet<NoteId> =
1261            proven_block.output_notes().map(|(_, output_note)| output_note.id()).collect();
1262
1263        // We could distribute notes over multiple batches (if space is available), but most likely
1264        // one is sufficient.
1265        if self.pending_objects.output_notes.len() > MAX_OUTPUT_NOTES_PER_BATCH {
1266            return Err(anyhow::anyhow!(
1267                "too many pending output notes: {}, max allowed: {MAX_OUTPUT_NOTES_PER_BATCH}",
1268                self.pending_objects.output_notes.len(),
1269            ));
1270        }
1271
1272        let mut pending_note_batch = Vec::with_capacity(self.pending_objects.output_notes.len());
1273        let pending_output_notes = core::mem::take(&mut self.pending_objects.output_notes);
1274        for (note_idx, output_note) in pending_output_notes.into_iter().enumerate() {
1275            if output_notes_block.contains(&output_note.id()) {
1276                return Err(anyhow::anyhow!(
1277                    "output note {} is already created in block through transactions",
1278                    output_note.id()
1279                ));
1280            }
1281
1282            pending_note_batch.push((note_idx, output_note));
1283        }
1284
1285        if (proven_block.output_note_batches().len() + 1) > MAX_BATCHES_PER_BLOCK {
1286            return Err(anyhow::anyhow!(
1287                "too many batches in block: cannot add more pending notes".to_string(),
1288            ));
1289        }
1290
1291        proven_block.output_note_batches_mut().push(pending_note_batch);
1292
1293        let updated_block_note_tree = proven_block.build_output_note_tree().root();
1294
1295        // Update account tree and nullifier tree root in the block.
1296        let block_header = proven_block.header();
1297        let updated_header = BlockHeader::new(
1298            block_header.version(),
1299            block_header.prev_block_commitment(),
1300            block_header.block_num(),
1301            block_header.chain_commitment(),
1302            self.account_tree.root(),
1303            self.nullifier_tree.root(),
1304            updated_block_note_tree,
1305            block_header.tx_commitment(),
1306            block_header.tx_kernel_commitment(),
1307            block_header.proof_commitment(),
1308            block_header.timestamp(),
1309        );
1310        proven_block.set_block_header(updated_header);
1311
1312        Ok(())
1313    }
1314
1315    /// Creates a new block in the mock chain.
1316    ///
1317    /// This will make all the objects currently pending available for use.
1318    ///
1319    /// If a `timestamp` is provided, it will be set on the block.
1320    ///
1321    /// Block building is divided into a few steps:
1322    ///
1323    /// 1. Build batches from pending transactions and a block from those batches. This results in a
1324    ///    block.
1325    /// 2. Take that block and apply only its account/nullifier tree updates to the chain.
1326    /// 3. Then take the pending objects and insert them directly into the proven block. This means
1327    ///    we have to update the header of the block as well, with the newly inserted pending
1328    ///    accounts/nullifiers/notes. This is why we already did step 2, so that we can insert the
1329    ///    pending objects directly into the account/nullifier tree to get the latest correct state
1330    ///    of those trees. Then take the root of the trees and update them in the header of the
1331    ///    block. This should be pretty efficient because we don't have to do any tree insertions
1332    ///    multiple times (which would be slow).
1333    /// 4. Finally, now the block contains both the updates from the regular transactions/batches as
1334    ///    well as the pending objects. Now insert all the remaining updates into the chain state.
1335    fn prove_block_inner(&mut self, timestamp: Option<u32>) -> anyhow::Result<ProvenBlock> {
1336        // Create batches from pending transactions.
1337        // ----------------------------------------------------------------------------------------
1338
1339        let batches = self.pending_transactions_to_batches()?;
1340
1341        // Create block.
1342        // ----------------------------------------------------------------------------------------
1343
1344        let block_timestamp =
1345            timestamp.unwrap_or(self.latest_block_header().timestamp() + Self::TIMESTAMP_STEP_SECS);
1346
1347        let proposed_block = self
1348            .propose_block_at(batches, block_timestamp)
1349            .context("failed to create proposed block")?;
1350        let mut proven_block = self.prove_block(proposed_block).context("failed to prove block")?;
1351
1352        // We apply the block tree updates here, so that apply_pending_objects_to_block can easily
1353        // update the block header of this block with the pending accounts and nullifiers.
1354        self.apply_block_tree_updates(&proven_block)
1355            .context("failed to apply block tree updates")?;
1356
1357        if !self.pending_objects.is_empty() {
1358            self.apply_pending_objects_to_block(&mut proven_block)?;
1359        }
1360
1361        self.apply_block(proven_block.clone()).context("failed to apply block")?;
1362
1363        Ok(proven_block)
1364    }
1365}
1366
1367/// Creates the genesis state, consisting of a block containing the provided account updates and an
1368/// account tree with those accounts.
1369fn create_genesis_state(
1370    accounts: impl IntoIterator<Item = Account>,
1371) -> anyhow::Result<(ProvenBlock, AccountTree)> {
1372    let block_account_updates: Vec<BlockAccountUpdate> = accounts
1373        .into_iter()
1374        .map(|account| {
1375            BlockAccountUpdate::new(
1376                account.id(),
1377                account.commitment(),
1378                AccountUpdateDetails::New(account),
1379            )
1380        })
1381        .collect();
1382
1383    let account_tree = AccountTree::with_entries(
1384        block_account_updates
1385            .iter()
1386            .map(|account| (account.account_id(), account.final_state_commitment())),
1387    )
1388    .context("failed to create genesis account tree")?;
1389
1390    let output_note_batches = Vec::new();
1391    let created_nullifiers = Vec::new();
1392    let transactions = OrderedTransactionHeaders::new_unchecked(Vec::new());
1393
1394    let version = 0;
1395    let prev_block_commitment = Digest::default();
1396    let block_num = BlockNumber::from(0u32);
1397    let chain_commitment = Blockchain::new().commitment();
1398    let account_root = account_tree.root();
1399    let nullifier_root = NullifierTree::new().root();
1400    let note_root = BlockNoteTree::empty().root();
1401    let tx_commitment = transactions.commitment();
1402    let tx_kernel_commitment = TransactionKernel::kernel_commitment();
1403    let proof_commitment = Digest::default();
1404    let timestamp = MockChain::TIMESTAMP_START_SECS;
1405
1406    let header = BlockHeader::new(
1407        version,
1408        prev_block_commitment,
1409        block_num,
1410        chain_commitment,
1411        account_root,
1412        nullifier_root,
1413        note_root,
1414        tx_commitment,
1415        tx_kernel_commitment,
1416        proof_commitment,
1417        timestamp,
1418    );
1419
1420    Ok((
1421        ProvenBlock::new_unchecked(
1422            header,
1423            block_account_updates,
1424            output_note_batches,
1425            created_nullifiers,
1426            transactions,
1427        ),
1428        account_tree,
1429    ))
1430}
1431
1432impl Default for MockChain {
1433    fn default() -> Self {
1434        MockChain::new()
1435    }
1436}
1437
1438// PENDING OBJECTS
1439// ================================================================================================
1440
1441/// Aggregates all entities that were added using the _pending_ APIs of the [`MockChain`].
1442#[derive(Default, Debug, Clone)]
1443struct PendingObjects {
1444    /// Account updates for the block.
1445    updated_accounts: BTreeMap<AccountId, BlockAccountUpdate>,
1446
1447    /// Note batches created in transactions in the block.
1448    output_notes: Vec<OutputNote>,
1449
1450    /// Nullifiers produced in transactions in the block.
1451    created_nullifiers: Vec<Nullifier>,
1452}
1453
1454impl PendingObjects {
1455    pub fn new() -> PendingObjects {
1456        PendingObjects {
1457            updated_accounts: BTreeMap::new(),
1458            output_notes: vec![],
1459            created_nullifiers: vec![],
1460        }
1461    }
1462
1463    /// Returns `true` if there are no pending objects, `false` otherwise.
1464    pub fn is_empty(&self) -> bool {
1465        self.updated_accounts.is_empty()
1466            && self.output_notes.is_empty()
1467            && self.created_nullifiers.is_empty()
1468    }
1469}
1470
1471// ACCOUNT STATE
1472// ================================================================================================
1473
1474/// Helper type for increased readability at call-sites. Indicates whether to build a new (nonce =
1475/// ZERO) or existing account (nonce = ONE).
1476pub enum AccountState {
1477    New,
1478    Exists,
1479}
1480
1481// TX CONTEXT INPUT
1482// ================================================================================================
1483
1484/// Helper type to abstract over the inputs to [`MockChain::build_tx_context`]. See that method's
1485/// docs for details.
1486#[derive(Debug, Clone)]
1487pub enum TxContextInput {
1488    AccountId(AccountId),
1489    Account(Account),
1490    ExecutedTransaction(Box<ExecutedTransaction>),
1491}
1492
1493impl From<AccountId> for TxContextInput {
1494    fn from(account: AccountId) -> Self {
1495        Self::AccountId(account)
1496    }
1497}
1498
1499impl From<Account> for TxContextInput {
1500    fn from(account: Account) -> Self {
1501        Self::Account(account)
1502    }
1503}
1504
1505impl From<ExecutedTransaction> for TxContextInput {
1506    fn from(tx: ExecutedTransaction) -> Self {
1507        Self::ExecutedTransaction(Box::new(tx))
1508    }
1509}
1510
1511// TESTS
1512// ================================================================================================
1513
1514#[cfg(test)]
1515mod tests {
1516    use miden_objects::{
1517        account::{AccountStorage, AccountStorageMode},
1518        asset::FungibleAsset,
1519        testing::{
1520            account_component::AccountMockComponent,
1521            account_id::{ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, ACCOUNT_ID_SENDER},
1522        },
1523    };
1524
1525    use super::*;
1526
1527    #[test]
1528    fn with_accounts() -> anyhow::Result<()> {
1529        let account = AccountBuilder::new([4; 32])
1530            .storage_mode(AccountStorageMode::Public)
1531            .with_auth_component(Auth::IncrNonce)
1532            .with_component(
1533                AccountMockComponent::new_with_slots(
1534                    TransactionKernel::testing_assembler(),
1535                    vec![AccountStorage::mock_item_2().slot],
1536                )
1537                .unwrap(),
1538            )
1539            .build_existing()?;
1540
1541        let mock_chain = MockChain::with_accounts(&[account.clone()])?;
1542
1543        assert_eq!(mock_chain.committed_account(account.id())?, &account);
1544
1545        // Check that transaction inputs retrieved from the chain are against the block header with
1546        // the current account tree root.
1547        let tx_context = mock_chain.build_tx_context(account.id(), &[], &[])?.build()?;
1548        assert_eq!(tx_context.tx_inputs().block_header().block_num(), BlockNumber::from(0u32));
1549        assert_eq!(
1550            tx_context.tx_inputs().block_header().account_root(),
1551            mock_chain.account_tree.root()
1552        );
1553
1554        Ok(())
1555    }
1556
1557    #[test]
1558    fn prove_until_block() -> anyhow::Result<()> {
1559        let mut chain = MockChain::new();
1560        let block = chain.prove_until_block(5)?;
1561        assert_eq!(block.header().block_num(), 5u32.into());
1562        assert_eq!(chain.proven_blocks().len(), 6);
1563
1564        Ok(())
1565    }
1566
1567    #[test]
1568    fn private_account_state_update() -> anyhow::Result<()> {
1569        let faucet_id = ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into()?;
1570        let account_builder = AccountBuilder::new([4; 32])
1571            .storage_mode(AccountStorageMode::Private)
1572            .with_component(BasicWallet);
1573
1574        let mut mock_chain = MockChain::new();
1575        let account = mock_chain.add_pending_account_from_builder(
1576            Auth::BasicAuth,
1577            account_builder,
1578            AccountState::New,
1579        )?;
1580        let account_id = account.id();
1581        assert_eq!(account.nonce().as_int(), 0);
1582
1583        let note_1 = mock_chain.add_pending_p2id_note(
1584            ACCOUNT_ID_SENDER.try_into().unwrap(),
1585            account.id(),
1586            &[Asset::Fungible(FungibleAsset::new(faucet_id, 1000u64).unwrap())],
1587            NoteType::Private,
1588        )?;
1589
1590        mock_chain.prove_next_block()?;
1591
1592        let tx = mock_chain
1593            .build_tx_context(TxContextInput::Account(account), &[], &[note_1])?
1594            .build()?
1595            .execute()?;
1596
1597        mock_chain.add_pending_executed_transaction(&tx)?;
1598        mock_chain.prove_next_block()?;
1599
1600        assert!(tx.final_account().nonce().as_int() > 0);
1601        assert_eq!(
1602            tx.final_account().commitment(),
1603            mock_chain.account_tree.open(account_id).state_commitment()
1604        );
1605
1606        Ok(())
1607    }
1608}