Skip to main content

miden_testing/mock_chain/
chain.rs

1use alloc::collections::{BTreeMap, BTreeSet};
2use alloc::vec::Vec;
3
4use anyhow::Context;
5use miden_block_prover::LocalBlockProver;
6use miden_processor::serde::DeserializationError;
7use miden_protocol::MIN_PROOF_SECURITY_LEVEL;
8use miden_protocol::account::auth::{AuthSecretKey, PublicKey};
9use miden_protocol::account::delta::AccountUpdateDetails;
10use miden_protocol::account::{Account, AccountId, PartialAccount};
11use miden_protocol::batch::{ProposedBatch, ProvenBatch};
12use miden_protocol::block::account_tree::{AccountTree, AccountWitness};
13use miden_protocol::block::nullifier_tree::{NullifierTree, NullifierWitness};
14use miden_protocol::block::{
15    BlockHeader,
16    BlockInputs,
17    BlockNumber,
18    Blockchain,
19    ProposedBlock,
20    ProvenBlock,
21};
22use miden_protocol::crypto::dsa::ecdsa_k256_keccak::SigningKey;
23use miden_protocol::note::{Note, NoteHeader, NoteId, NoteInclusionProof, Nullifier};
24use miden_protocol::transaction::{
25    ExecutedTransaction,
26    InputNote,
27    InputNotes,
28    OutputNote,
29    PartialBlockchain,
30    ProvenTransaction,
31    TransactionInputs,
32};
33use miden_tx::LocalTransactionProver;
34use miden_tx::auth::BasicAuthenticator;
35use miden_tx::utils::serde::{ByteReader, ByteWriter, Deserializable, Serializable};
36use miden_tx_batch_prover::LocalBatchProver;
37
38use super::note::MockChainNote;
39use crate::{MockChainBuilder, TransactionContextBuilder};
40
41// MOCK CHAIN
42// ================================================================================================
43
44/// The [`MockChain`] simulates a simplified blockchain environment for testing purposes.
45///
46/// The typical usage of a mock chain is:
47/// - Creating it using a [`MockChainBuilder`], which allows adding accounts and notes to the
48///   genesis state.
49/// - Creating transactions against the chain state and executing them.
50/// - Adding executed or proven transactions to the set of pending transactions (the "mempool"),
51///   e.g. using [`MockChain::add_pending_executed_transaction`].
52/// - Proving a block, which adds all pending transactions to the chain state, e.g. using
53///   [`MockChain::prove_next_block`].
54///
55/// The mock chain uses the batch and block provers underneath to process pending transactions, so
56/// the generated blocks are realistic and indistinguishable from a real node. The only caveat is
57/// that no real ZK proofs are generated or validated as part of transaction, batch or block
58/// building.
59///
60/// # Examples
61///
62/// ## Executing a simple transaction
63/// ```
64/// # use anyhow::Result;
65/// # use miden_protocol::{
66/// #    account::auth::AuthScheme,
67/// #    asset::{Asset, AssetCallbackFlag, FungibleAsset},
68/// #    note::NoteType,
69/// # };
70/// # use miden_testing::{Auth, MockChain};
71/// #
72/// # #[tokio::main(flavor = "current_thread")]
73/// # async fn main() -> Result<()> {
74/// // Build a genesis state for a mock chain using a MockChainBuilder.
75/// // --------------------------------------------------------------------------------------------
76///
77/// let mut builder = MockChain::builder();
78///
79/// // Add a recipient wallet with basic authentication.
80/// // Use either ECDSA K256 Keccak (scheme_id: 1) or Falcon512Poseidon2 (scheme_id: 2) auth scheme.
81/// let receiver = builder.add_existing_wallet(Auth::BasicAuth {
82///     auth_scheme: AuthScheme::Falcon512Poseidon2,
83/// })?;
84///
85/// // Add a wallet with assets.
86/// let sender = builder.add_existing_wallet(Auth::IncrNonce)?;
87///
88/// let fungible_asset = FungibleAsset::mock(10).unwrap_fungible();
89/// // Add a P2ID note with a fungible asset to the chain.
90/// let note = builder.add_p2id_note(
91///     sender.id(),
92///     receiver.id(),
93///     &[Asset::Fungible(fungible_asset)],
94///     NoteType::Public,
95/// )?;
96///
97/// let mut mock_chain: MockChain = builder.build()?;
98///
99/// // Create a transaction against the receiver account consuming the note.
100/// // --------------------------------------------------------------------------------------------
101///
102/// let transaction = mock_chain
103///     .build_tx_context(receiver.id(), &[note.id()], &[])?
104///     .build()?
105///     .execute()
106///     .await?;
107///
108/// // Add the transaction to the chain state.
109/// // --------------------------------------------------------------------------------------------
110///
111/// // Add the transaction to the mock chain's "mempool" of pending transactions.
112/// mock_chain.add_pending_executed_transaction(&transaction)?;
113///
114/// // Prove the next block to include the transaction in the chain state.
115/// mock_chain.prove_next_block()?;
116///
117/// // The receiver account should now have the asset in its account vault.
118/// assert_eq!(
119///     mock_chain
120///         .committed_account(receiver.id())?
121///         .vault()
122///         .get_balance(fungible_asset.vault_key())?,
123///     fungible_asset.amount()
124/// );
125/// # Ok(())
126/// # }
127/// ```
128///
129/// ## Create mock objects and build a transaction context
130///
131/// ```
132/// # use anyhow::Result;
133/// # use miden_protocol::{
134/// #    Felt,
135/// #    account::auth::AuthScheme,
136/// #    asset::{Asset, FungibleAsset},
137/// #    note::NoteType
138/// # };
139/// # use miden_testing::{Auth, MockChain, TransactionContextBuilder};
140/// #
141/// # #[tokio::main(flavor = "current_thread")]
142/// # async fn main() -> Result<()> {
143/// let mut builder = MockChain::builder();
144///
145/// let faucet = builder.create_new_faucet(
146///     Auth::BasicAuth {
147///         auth_scheme: AuthScheme::Falcon512Poseidon2,
148///     },
149///     "USDT",
150///     100_000,
151/// )?;
152/// let asset = Asset::from(FungibleAsset::new(faucet.id(), 10)?);
153///
154/// let sender = builder.create_new_wallet(Auth::BasicAuth {
155///     auth_scheme: AuthScheme::Falcon512Poseidon2,
156/// })?;
157/// let target = builder.create_new_wallet(Auth::BasicAuth {
158///     auth_scheme: AuthScheme::Falcon512Poseidon2,
159/// })?;
160///
161/// let note = builder.add_p2id_note(faucet.id(), target.id(), &[asset], NoteType::Public)?;
162///
163/// let mock_chain = builder.build()?;
164///
165/// // The target account is a new account so we move it into the build_tx_context, since the
166/// // chain's committed accounts do not yet contain it.
167/// let tx_context = mock_chain.build_tx_context(target, &[note.id()], &[])?.build()?;
168/// let executed_transaction = tx_context.execute().await?;
169/// # Ok(())
170/// # }
171/// ```
172#[derive(Debug, Clone)]
173pub struct MockChain {
174    /// An append-only structure used to represent the history of blocks produced for this chain.
175    chain: Blockchain,
176
177    /// History of produced blocks.
178    blocks: Vec<ProvenBlock>,
179
180    /// Tree containing all nullifiers.
181    nullifier_tree: NullifierTree,
182
183    /// Tree containing the state commitments of all accounts.
184    account_tree: AccountTree,
185
186    /// Transactions that have been submitted to the chain but have not yet been included in a
187    /// block.
188    pending_transactions: Vec<ProvenTransaction>,
189
190    /// Batches that have been submitted to the chain but have not yet been included in a block.
191    pending_batches: Vec<ProvenBatch>,
192
193    /// NoteID |-> MockChainNote mapping to simplify note retrieval.
194    committed_notes: BTreeMap<NoteId, MockChainNote>,
195
196    /// AccountId |-> Account mapping to simplify transaction creation. Latest known account
197    /// state is maintained for each account here.
198    ///
199    /// The map always holds the most recent *public* state known for every account. For private
200    /// accounts, however, transactions do not emit the post-transaction state, so their entries
201    /// remain at the last observed state.
202    committed_accounts: BTreeMap<AccountId, Account>,
203
204    /// AccountId |-> AccountAuthenticator mapping to store the authenticator for accounts to
205    /// simplify transaction creation.
206    account_authenticators: BTreeMap<AccountId, AccountAuthenticator>,
207
208    /// Validator secret key used for signing blocks.
209    validator_secret_key: SigningKey,
210}
211
212impl MockChain {
213    // CONSTANTS
214    // ----------------------------------------------------------------------------------------
215
216    /// The timestamp of the genesis block of the chain. Chosen as an easily readable number.
217    pub const TIMESTAMP_START_SECS: u32 = 1700000000;
218
219    /// The number of seconds by which a block's timestamp increases over the previous block's
220    /// timestamp, unless overwritten when calling [`Self::prove_next_block_at`].
221    pub const TIMESTAMP_STEP_SECS: u32 = 10;
222
223    // CONSTRUCTORS
224    // ----------------------------------------------------------------------------------------
225
226    /// Creates a new `MockChain` with an empty genesis block.
227    pub fn new() -> Self {
228        Self::builder().build().expect("empty chain should be valid")
229    }
230
231    /// Returns a new, empty [`MockChainBuilder`].
232    pub fn builder() -> MockChainBuilder {
233        MockChainBuilder::new()
234    }
235
236    /// Creates a new `MockChain` with the provided genesis block and account tree.
237    pub(super) fn from_genesis_block(
238        genesis_block: ProvenBlock,
239        account_tree: AccountTree,
240        account_authenticators: BTreeMap<AccountId, AccountAuthenticator>,
241        secret_key: SigningKey,
242        genesis_notes: Vec<Note>,
243    ) -> anyhow::Result<Self> {
244        let mut chain = MockChain {
245            chain: Blockchain::default(),
246            blocks: vec![],
247            nullifier_tree: NullifierTree::default(),
248            account_tree,
249            pending_transactions: Vec::new(),
250            pending_batches: Vec::new(),
251            committed_notes: BTreeMap::new(),
252            committed_accounts: BTreeMap::new(),
253            account_authenticators,
254            validator_secret_key: secret_key,
255        };
256
257        // We do not have to apply the tree changes, because the account tree is already initialized
258        // and the nullifier tree is empty at genesis.
259        chain
260            .apply_block(genesis_block)
261            .context("failed to build account from builder")?;
262
263        // Update committed_notes with full note details for genesis notes.
264        // This is needed because apply_block only stores headers for private notes,
265        // but tests need full note details to create input notes.
266        for note in genesis_notes {
267            if let Some(MockChainNote::Private(_, _, inclusion_proof)) =
268                chain.committed_notes.get(&note.id())
269            {
270                chain.committed_notes.insert(
271                    note.id(),
272                    MockChainNote::Public(note.clone(), inclusion_proof.clone()),
273                );
274            }
275        }
276
277        debug_assert_eq!(chain.blocks.len(), 1);
278        debug_assert_eq!(chain.committed_accounts.len(), chain.account_tree.num_accounts());
279
280        Ok(chain)
281    }
282
283    // PUBLIC ACCESSORS
284    // ----------------------------------------------------------------------------------------
285
286    /// Returns a reference to the current [`Blockchain`].
287    pub fn blockchain(&self) -> &Blockchain {
288        &self.chain
289    }
290
291    /// Returns a [`PartialBlockchain`] instantiated from the current [`Blockchain`] and with
292    /// authentication paths for all all blocks in the chain.
293    pub fn latest_partial_blockchain(&self) -> PartialBlockchain {
294        // We have to exclude the latest block because we need to fetch the state of the chain at
295        // that latest block, which does not include itself.
296        let block_headers =
297            self.blocks.iter().map(|b| b.header()).take(self.blocks.len() - 1).cloned();
298
299        PartialBlockchain::from_blockchain(&self.chain, block_headers)
300            .expect("blockchain should be valid by construction")
301    }
302
303    /// Creates a new [`PartialBlockchain`] with all reference blocks in the given iterator except
304    /// for the latest block header in the chain and returns that latest block header.
305    ///
306    /// The intended use for the latest block header is to become the reference block of a new
307    /// transaction batch or block.
308    pub fn latest_selective_partial_blockchain(
309        &self,
310        reference_blocks: impl IntoIterator<Item = BlockNumber>,
311    ) -> anyhow::Result<(BlockHeader, PartialBlockchain)> {
312        let latest_block_header = self.latest_block_header();
313
314        self.selective_partial_blockchain(latest_block_header.block_num(), reference_blocks)
315    }
316
317    /// Creates a new [`PartialBlockchain`] with all reference blocks in the given iterator except
318    /// for the reference block header in the chain and returns that reference block header.
319    ///
320    /// The intended use for the reference block header is to become the reference block of a new
321    /// transaction batch or block.
322    pub fn selective_partial_blockchain(
323        &self,
324        reference_block: BlockNumber,
325        reference_blocks: impl IntoIterator<Item = BlockNumber>,
326    ) -> anyhow::Result<(BlockHeader, PartialBlockchain)> {
327        let reference_block_header = self.block_header(reference_block.as_usize());
328        // Deduplicate block numbers so each header will be included just once. This is required so
329        // PartialBlockchain::from_blockchain does not panic.
330        let reference_blocks: BTreeSet<_> = reference_blocks.into_iter().collect();
331
332        // Include all block headers except the reference block itself.
333        let mut block_headers = Vec::new();
334
335        for block_ref_num in &reference_blocks {
336            let block_index = block_ref_num.as_usize();
337            let block = self
338                .blocks
339                .get(block_index)
340                .ok_or_else(|| anyhow::anyhow!("block {} not found in chain", block_ref_num))?;
341            let block_header = block.header().clone();
342            // Exclude the reference block header.
343            if block_header.commitment() != reference_block_header.commitment() {
344                block_headers.push(block_header);
345            }
346        }
347
348        let partial_blockchain =
349            PartialBlockchain::from_blockchain_at(&self.chain, reference_block, block_headers)?;
350
351        Ok((reference_block_header, partial_blockchain))
352    }
353
354    /// Returns a map of [`AccountWitness`]es for the requested account IDs from the current
355    /// [`AccountTree`] in the chain.
356    pub fn account_witnesses(
357        &self,
358        account_ids: impl IntoIterator<Item = AccountId>,
359    ) -> BTreeMap<AccountId, AccountWitness> {
360        let mut account_witnesses = BTreeMap::new();
361
362        for account_id in account_ids {
363            let witness = self.account_tree.open(account_id);
364            account_witnesses.insert(account_id, witness);
365        }
366
367        account_witnesses
368    }
369
370    /// Returns a map of [`NullifierWitness`]es for the requested nullifiers from the current
371    /// [`NullifierTree`] in the chain.
372    pub fn nullifier_witnesses(
373        &self,
374        nullifiers: impl IntoIterator<Item = Nullifier>,
375    ) -> BTreeMap<Nullifier, NullifierWitness> {
376        let mut nullifier_proofs = BTreeMap::new();
377
378        for nullifier in nullifiers {
379            let witness = self.nullifier_tree.open(&nullifier);
380            nullifier_proofs.insert(nullifier, witness);
381        }
382
383        nullifier_proofs
384    }
385
386    /// Returns all note inclusion proofs for the requested note IDs, **if they are available for
387    /// consumption**. Therefore, not all of the requested notes will be guaranteed to have an entry
388    /// in the returned map.
389    pub fn unauthenticated_note_proofs(
390        &self,
391        notes: impl IntoIterator<Item = NoteId>,
392    ) -> BTreeMap<NoteId, NoteInclusionProof> {
393        let mut proofs = BTreeMap::default();
394        for note in notes {
395            if let Some(input_note) = self.committed_notes.get(&note) {
396                proofs.insert(note, input_note.inclusion_proof().clone());
397            }
398        }
399
400        proofs
401    }
402
403    /// Returns the genesis [`BlockHeader`] of the chain.
404    pub fn genesis_block_header(&self) -> BlockHeader {
405        self.block_header(BlockNumber::GENESIS.as_usize())
406    }
407
408    /// Returns the latest [`BlockHeader`] in the chain.
409    pub fn latest_block_header(&self) -> BlockHeader {
410        let chain_tip =
411            self.chain.chain_tip().expect("chain should contain at least the genesis block");
412        self.blocks[chain_tip.as_usize()].header().clone()
413    }
414
415    /// Returns the latest [`ProvenBlock`] in the chain.
416    pub fn latest_block(&self) -> ProvenBlock {
417        let chain_tip =
418            self.chain.chain_tip().expect("chain should contain at least the genesis block");
419        self.blocks[chain_tip.as_usize()].clone()
420    }
421
422    /// Returns the [`BlockHeader`] with the specified `block_number`.
423    ///
424    /// # Panics
425    ///
426    /// - If the block number does not exist in the chain.
427    pub fn block_header(&self, block_number: usize) -> BlockHeader {
428        self.blocks[block_number].header().clone()
429    }
430
431    /// Returns a reference to slice of all created proven blocks.
432    pub fn proven_blocks(&self) -> &[ProvenBlock] {
433        &self.blocks
434    }
435
436    /// Returns the [`AccountId`] of the faucet whose assets are accepted for fee payments in the
437    /// transaction kernel, or in other words, the fee faucet of the blockchain.
438    ///
439    /// This value is taken from the genesis block because it is assumed not to change throughout
440    /// the chain's lifecycle.
441    pub fn fee_faucet_id(&self) -> AccountId {
442        self.genesis_block_header().fee_parameters().fee_faucet_id()
443    }
444
445    /// Returns a reference to the nullifier tree.
446    pub fn nullifier_tree(&self) -> &NullifierTree {
447        &self.nullifier_tree
448    }
449
450    /// Returns the map of note IDs to committed notes.
451    ///
452    /// These notes are committed for authenticated consumption.
453    pub fn committed_notes(&self) -> &BTreeMap<NoteId, MockChainNote> {
454        &self.committed_notes
455    }
456
457    /// Returns `true` if a note with the given ID is recorded in committed notes.
458    pub fn is_note_committed(&self, note_id: &NoteId) -> bool {
459        self.committed_notes.contains_key(note_id)
460    }
461
462    /// Returns `true` if the nullifier has been recorded on-chain (note was consumed).
463    pub fn is_note_consumed(&self, nullifier: &Nullifier) -> bool {
464        self.nullifier_tree.get_block_num(nullifier).is_some()
465    }
466
467    /// Returns `true` if the nullifier is not yet on-chain.
468    ///
469    /// A nullifier can be unspent without the chain having seen the underlying note. Pair with
470    /// [`Self::is_note_committed`] when both conditions matter.
471    pub fn is_note_unspent(&self, nullifier: &Nullifier) -> bool {
472        !self.is_note_consumed(nullifier)
473    }
474
475    /// Returns an [`InputNote`] for the given note ID. If the note does not exist or is not
476    /// public, `None` is returned.
477    pub fn get_public_note(&self, note_id: &NoteId) -> Option<InputNote> {
478        let note = self.committed_notes.get(note_id)?;
479        note.clone().try_into().ok()
480    }
481
482    /// Returns a reference to the account identified by the given account ID.
483    ///
484    /// The account is retrieved with the latest state known to the [`MockChain`].
485    pub fn committed_account(&self, account_id: AccountId) -> anyhow::Result<&Account> {
486        self.committed_accounts
487            .get(&account_id)
488            .with_context(|| format!("account {account_id} not found in committed accounts"))
489    }
490
491    /// Returns a reference to the [`AccountTree`] of the chain.
492    pub fn account_tree(&self) -> &AccountTree {
493        &self.account_tree
494    }
495
496    // BATCH APIS
497    // ----------------------------------------------------------------------------------------
498
499    /// Proposes a new transaction batch from the provided transactions and returns it.
500    ///
501    /// This method does not modify the chain state.
502    pub fn propose_transaction_batch<I>(
503        &self,
504        txs: impl IntoIterator<Item = ProvenTransaction, IntoIter = I>,
505    ) -> anyhow::Result<ProposedBatch>
506    where
507        I: Iterator<Item = ProvenTransaction> + Clone,
508    {
509        let transactions: Vec<_> = txs.into_iter().map(alloc::sync::Arc::new).collect();
510
511        let (batch_reference_block, partial_blockchain, unauthenticated_note_proofs) = self
512            .get_batch_inputs(
513                transactions.iter().map(|tx| tx.ref_block_num()),
514                transactions
515                    .iter()
516                    .flat_map(|tx| tx.unauthenticated_notes().map(NoteHeader::id)),
517            )?;
518
519        Ok(ProposedBatch::new(
520            transactions,
521            batch_reference_block,
522            partial_blockchain,
523            unauthenticated_note_proofs,
524        )?)
525    }
526
527    /// Mock-proves a proposed transaction batch from the provided [`ProposedBatch`] and returns it.
528    ///
529    /// This method does not modify the chain state.
530    pub fn prove_transaction_batch(
531        &self,
532        proposed_batch: ProposedBatch,
533    ) -> anyhow::Result<ProvenBatch> {
534        let batch_prover = LocalBatchProver::new(0);
535        Ok(batch_prover.prove_dummy(proposed_batch)?)
536    }
537
538    // BLOCK APIS
539    // ----------------------------------------------------------------------------------------
540
541    /// Proposes a new block from the provided batches with the given timestamp and returns it.
542    ///
543    /// This method does not modify the chain state.
544    pub fn propose_block_at<I>(
545        &self,
546        batches: impl IntoIterator<Item = ProvenBatch, IntoIter = I>,
547        timestamp: u32,
548    ) -> anyhow::Result<ProposedBlock>
549    where
550        I: Iterator<Item = ProvenBatch> + Clone,
551    {
552        let batches: Vec<_> = batches.into_iter().collect();
553
554        let block_inputs = self
555            .get_block_inputs(batches.iter())
556            .context("could not retrieve block inputs")?;
557
558        let proposed_block = ProposedBlock::new_at(block_inputs, batches, timestamp)
559            .context("failed to create proposed block")?;
560
561        Ok(proposed_block)
562    }
563
564    /// Proposes a new block from the provided batches and returns it.
565    ///
566    /// This method does not modify the chain state.
567    pub fn propose_block<I>(
568        &self,
569        batches: impl IntoIterator<Item = ProvenBatch, IntoIter = I>,
570    ) -> anyhow::Result<ProposedBlock>
571    where
572        I: Iterator<Item = ProvenBatch> + Clone,
573    {
574        // We can't access system time because we are in a no-std environment, so we use the
575        // minimally correct next timestamp.
576        let timestamp = self.latest_block_header().timestamp() + 1;
577
578        self.propose_block_at(batches, timestamp)
579    }
580
581    // TRANSACTION APIS
582    // ----------------------------------------------------------------------------------------
583
584    /// Initializes a [`TransactionContextBuilder`] for executing against a specific block number.
585    ///
586    /// Depending on the provided `input`, the builder is initialized differently:
587    /// - [`TxContextInput::AccountId`]: Initialize the builder with [`TransactionInputs`] fetched
588    ///   from the chain for the public account identified by the ID.
589    /// - [`TxContextInput::Account`]: Initialize the builder with [`TransactionInputs`] where the
590    ///   account is passed as-is to the inputs.
591    ///
592    /// In all cases, if the chain contains authenticator for the account, they are added to the
593    /// builder.
594    ///
595    /// [`TxContextInput::Account`] can be used to build a chain of transactions against the same
596    /// account that build on top of each other. For example, transaction A modifies an account
597    /// from state 0 to 1, and transaction B modifies it from state 1 to 2.
598    pub fn build_tx_context_at(
599        &self,
600        reference_block: impl Into<BlockNumber>,
601        input: impl Into<TxContextInput>,
602        note_ids: &[NoteId],
603        unauthenticated_notes: &[Note],
604    ) -> anyhow::Result<TransactionContextBuilder> {
605        let input = input.into();
606        let reference_block = reference_block.into();
607
608        let authenticator = self.account_authenticators.get(&input.id());
609        let authenticator =
610            authenticator.and_then(|authenticator| authenticator.authenticator().cloned());
611
612        anyhow::ensure!(
613            reference_block.as_usize() < self.blocks.len(),
614            "reference block {reference_block} is out of range (latest {})",
615            self.latest_block_header().block_num()
616        );
617
618        let account = match input {
619            TxContextInput::AccountId(account_id) => {
620                if account_id.is_private() {
621                    return Err(anyhow::anyhow!(
622                        "transaction contexts for private accounts should be created with TxContextInput::Account"
623                    ));
624                }
625
626                self.committed_accounts
627                    .get(&account_id)
628                    .with_context(|| {
629                        format!("account {account_id} not found in committed accounts")
630                    })?
631                    .clone()
632            },
633            TxContextInput::Account(account) => account,
634        };
635
636        let tx_inputs = self
637            .get_transaction_inputs_at(reference_block, &account, note_ids, unauthenticated_notes)
638            .context("failed to gather transaction inputs")?;
639
640        let tx_context_builder = TransactionContextBuilder::new(account)
641            .authenticator(authenticator)
642            .tx_inputs(tx_inputs);
643
644        Ok(tx_context_builder)
645    }
646
647    /// Initializes a [`TransactionContextBuilder`] for executing against the last block header.
648    ///
649    /// This is a wrapper around [`Self::build_tx_context_at`] which uses the latest block as the
650    /// reference block. See that function's docs for details.
651    pub fn build_tx_context(
652        &self,
653        input: impl Into<TxContextInput>,
654        note_ids: &[NoteId],
655        unauthenticated_notes: &[Note],
656    ) -> anyhow::Result<TransactionContextBuilder> {
657        let reference_block = self.latest_block_header().block_num();
658        self.build_tx_context_at(reference_block, input, note_ids, unauthenticated_notes)
659    }
660
661    // INPUTS APIS
662    // ----------------------------------------------------------------------------------------
663
664    /// Returns a valid [`TransactionInputs`] for the specified entities, executing against
665    /// a specific block number.
666    pub fn get_transaction_inputs_at(
667        &self,
668        reference_block: BlockNumber,
669        account: impl Into<PartialAccount>,
670        notes: &[NoteId],
671        unauthenticated_notes: &[Note],
672    ) -> anyhow::Result<TransactionInputs> {
673        let ref_block = self.block_header(reference_block.as_usize());
674
675        let mut input_notes = vec![];
676        let mut block_headers_map: BTreeMap<BlockNumber, BlockHeader> = BTreeMap::new();
677        for note in notes {
678            let input_note: InputNote = self
679                .committed_notes
680                .get(note)
681                .with_context(|| format!("note with id {note} not found"))?
682                .clone()
683                .try_into()
684                .with_context(|| {
685                    format!("failed to convert mock chain note with id {note} into input note")
686                })?;
687
688            let note_block_num = input_note
689                .location()
690                .with_context(|| format!("note location not available: {note}"))?
691                .block_num();
692
693            if note_block_num > ref_block.block_num() {
694                anyhow::bail!(
695                    "note with ID {note} was created in block {note_block_num} which is larger than the reference block number {}",
696                    ref_block.block_num()
697                )
698            }
699
700            if note_block_num != ref_block.block_num() {
701                let block_header = self
702                    .blocks
703                    .get(note_block_num.as_usize())
704                    .with_context(|| format!("block {note_block_num} not found in chain"))?
705                    .header()
706                    .clone();
707                block_headers_map.insert(note_block_num, block_header);
708            }
709
710            input_notes.push(input_note);
711        }
712
713        for note in unauthenticated_notes {
714            input_notes.push(InputNote::Unauthenticated { note: note.clone() })
715        }
716
717        let block_headers = block_headers_map.values();
718        let (_, partial_blockchain) = self.selective_partial_blockchain(
719            reference_block,
720            block_headers.map(BlockHeader::block_num),
721        )?;
722
723        let input_notes = InputNotes::new(input_notes)?;
724
725        Ok(TransactionInputs::new(
726            account.into(),
727            ref_block.clone(),
728            partial_blockchain,
729            input_notes,
730        )?)
731    }
732
733    /// Returns a valid [`TransactionInputs`] for the specified entities.
734    pub fn get_transaction_inputs(
735        &self,
736        account: impl Into<PartialAccount>,
737        notes: &[NoteId],
738        unauthenticated_notes: &[Note],
739    ) -> anyhow::Result<TransactionInputs> {
740        let latest_block_num = self.latest_block_header().block_num();
741        self.get_transaction_inputs_at(latest_block_num, account, notes, unauthenticated_notes)
742    }
743
744    /// Returns inputs for a transaction batch for all the reference blocks of the provided
745    /// transactions.
746    pub fn get_batch_inputs(
747        &self,
748        tx_reference_blocks: impl IntoIterator<Item = BlockNumber>,
749        unauthenticated_notes: impl Iterator<Item = NoteId>,
750    ) -> anyhow::Result<(BlockHeader, PartialBlockchain, BTreeMap<NoteId, NoteInclusionProof>)>
751    {
752        // Fetch note proofs for notes that exist in the chain.
753        let unauthenticated_note_proofs = self.unauthenticated_note_proofs(unauthenticated_notes);
754
755        // We also need to fetch block inclusion proofs for any of the blocks that contain
756        // unauthenticated notes for which we want to prove inclusion.
757        let required_blocks = tx_reference_blocks.into_iter().chain(
758            unauthenticated_note_proofs
759                .values()
760                .map(|note_proof| note_proof.location().block_num()),
761        );
762
763        let (batch_reference_block, partial_block_chain) =
764            self.latest_selective_partial_blockchain(required_blocks)?;
765
766        Ok((batch_reference_block, partial_block_chain, unauthenticated_note_proofs))
767    }
768
769    /// Gets foreign account inputs to execute FPI transactions.
770    ///
771    /// Used in tests to get foreign account inputs for FPI calls.
772    pub fn get_foreign_account_inputs(
773        &self,
774        account_id: AccountId,
775    ) -> anyhow::Result<(Account, AccountWitness)> {
776        let account = self.committed_account(account_id)?.clone();
777
778        let account_witness = self.account_tree().open(account_id);
779        assert_eq!(account_witness.state_commitment(), account.to_commitment());
780
781        Ok((account, account_witness))
782    }
783
784    /// Gets the inputs for a block for the provided batches.
785    pub fn get_block_inputs<'batch, I>(
786        &self,
787        batch_iter: impl IntoIterator<Item = &'batch ProvenBatch, IntoIter = I>,
788    ) -> anyhow::Result<BlockInputs>
789    where
790        I: Iterator<Item = &'batch ProvenBatch> + Clone,
791    {
792        let batch_iterator = batch_iter.into_iter();
793
794        let unauthenticated_note_proofs =
795            self.unauthenticated_note_proofs(batch_iterator.clone().flat_map(|batch| {
796                batch.input_notes().iter().filter_map(|note| note.header().map(NoteHeader::id))
797            }));
798
799        let (block_reference_block, partial_blockchain) = self
800            .latest_selective_partial_blockchain(
801                batch_iterator.clone().map(ProvenBatch::reference_block_num).chain(
802                    unauthenticated_note_proofs.values().map(|proof| proof.location().block_num()),
803                ),
804            )?;
805
806        let account_witnesses =
807            self.account_witnesses(batch_iterator.clone().flat_map(ProvenBatch::updated_accounts));
808
809        let nullifier_proofs =
810            self.nullifier_witnesses(batch_iterator.flat_map(ProvenBatch::created_nullifiers));
811
812        Ok(BlockInputs::new(
813            block_reference_block,
814            partial_blockchain,
815            account_witnesses,
816            nullifier_proofs,
817            unauthenticated_note_proofs,
818        ))
819    }
820
821    // PUBLIC MUTATORS
822    // ----------------------------------------------------------------------------------------
823
824    /// Proves the next block in the mock chain.
825    ///
826    /// This will commit all the currently pending transactions into the chain state.
827    pub fn prove_next_block(&mut self) -> anyhow::Result<ProvenBlock> {
828        self.prove_and_apply_block(None)
829    }
830
831    /// Proves the next block in the mock chain at the given timestamp.
832    ///
833    /// This will commit all the currently pending transactions into the chain state.
834    pub fn prove_next_block_at(&mut self, timestamp: u32) -> anyhow::Result<ProvenBlock> {
835        self.prove_and_apply_block(Some(timestamp))
836    }
837
838    /// Proves new blocks until the block with the given target block number has been created.
839    ///
840    /// For example, if the latest block is `5` and this function is called with `10`, then blocks
841    /// `6..=10` will be created and block 10 will be returned.
842    ///
843    /// # Panics
844    ///
845    /// Panics if:
846    /// - the given block number is smaller or equal to the number of the latest block in the chain.
847    pub fn prove_until_block(
848        &mut self,
849        target_block_num: impl Into<BlockNumber>,
850    ) -> anyhow::Result<ProvenBlock> {
851        let target_block_num = target_block_num.into();
852        let latest_block_num = self.latest_block_header().block_num();
853        assert!(
854            target_block_num > latest_block_num,
855            "target block number must be greater than the number of the latest block in the chain"
856        );
857
858        let mut last_block = None;
859        for _ in latest_block_num.as_usize()..target_block_num.as_usize() {
860            last_block = Some(self.prove_next_block()?);
861        }
862
863        Ok(last_block.expect("at least one block should have been created"))
864    }
865
866    // PUBLIC MUTATORS (PENDING APIS)
867    // ----------------------------------------------------------------------------------------
868
869    /// Adds the given [`ExecutedTransaction`] to the list of pending transactions.
870    ///
871    /// A block has to be created to apply the transaction effects to the chain state, e.g. using
872    /// [`MockChain::prove_next_block`].
873    pub fn add_pending_executed_transaction(
874        &mut self,
875        transaction: &ExecutedTransaction,
876    ) -> anyhow::Result<()> {
877        // Transform the executed tx into a proven tx with a dummy proof.
878        let proven_tx = LocalTransactionProver::default()
879            .prove_dummy(transaction.clone())
880            .context("failed to dummy-prove executed transaction into proven transaction")?;
881
882        self.pending_transactions.push(proven_tx);
883
884        Ok(())
885    }
886
887    /// Adds the given [`ProvenTransaction`] to the list of pending transactions.
888    ///
889    /// A block has to be created to apply the transaction effects to the chain state, e.g. using
890    /// [`MockChain::prove_next_block`].
891    pub fn add_pending_proven_transaction(&mut self, transaction: ProvenTransaction) {
892        self.pending_transactions.push(transaction);
893    }
894
895    /// Adds the given [`ProvenBatch`] to the list of pending batches.
896    ///
897    /// A block has to be created to apply the batch effects to the chain state, e.g. using
898    /// [`MockChain::prove_next_block`].
899    pub fn add_pending_batch(&mut self, batch: ProvenBatch) {
900        self.pending_batches.push(batch);
901    }
902
903    // PRIVATE HELPERS
904    // ----------------------------------------------------------------------------------------
905
906    /// Applies the given block to the chain state, which means:
907    ///
908    /// - Insert account and nullifiers into the respective trees.
909    /// - Updated accounts from the block are updated in the committed accounts.
910    /// - Created notes are inserted into the committed notes.
911    /// - Consumed notes are removed from the committed notes.
912    /// - The block is appended to the [`BlockChain`] and the list of proven blocks.
913    fn apply_block(&mut self, proven_block: ProvenBlock) -> anyhow::Result<()> {
914        for account_update in proven_block.body().updated_accounts() {
915            self.account_tree
916                .insert(account_update.account_id(), account_update.final_state_commitment())
917                .context("failed to insert account update into account tree")?;
918        }
919
920        for nullifier in proven_block.body().created_nullifiers() {
921            self.nullifier_tree
922                .mark_spent(*nullifier, proven_block.header().block_num())
923                .context("failed to mark block nullifier as spent")?;
924
925            // TODO: Remove from self.committed_notes. This is not critical to have for now. It is
926            // not straightforward, because committed_notes are indexed by note IDs rather than
927            // nullifiers, so we'll have to create a second index to do this.
928        }
929
930        for account_update in proven_block.body().updated_accounts() {
931            match account_update.details() {
932                AccountUpdateDetails::Delta(account_delta) => {
933                    if account_delta.is_full_state() {
934                        let account = Account::try_from(account_delta)
935                            .context("failed to convert full state delta into full account")?;
936                        self.committed_accounts.insert(account.id(), account.clone());
937                    } else {
938                        let committed_account = self
939                            .committed_accounts
940                            .get_mut(&account_update.account_id())
941                            .ok_or_else(|| {
942                                anyhow::anyhow!("account delta in block for non-existent account")
943                            })?;
944                        committed_account
945                            .apply_delta(account_delta)
946                            .context("failed to apply account delta")?;
947                    }
948                },
949                // No state to keep for private accounts other than the commitment on the account
950                // tree
951                AccountUpdateDetails::Private => {},
952            }
953        }
954
955        let notes_tree = proven_block.body().compute_block_note_tree();
956        for (block_note_index, created_note) in proven_block.body().output_notes() {
957            let note_path = notes_tree.open(block_note_index);
958            let note_inclusion_proof = NoteInclusionProof::new(
959                proven_block.header().block_num(),
960                block_note_index.leaf_index_value(),
961                note_path,
962            )
963            .context("failed to create inclusion proof for output note")?;
964
965            if let OutputNote::Public(public_note) = created_note {
966                self.committed_notes.insert(
967                    public_note.id(),
968                    MockChainNote::Public(public_note.as_note().clone(), note_inclusion_proof),
969                );
970            } else {
971                self.committed_notes.insert(
972                    created_note.id(),
973                    MockChainNote::Private(
974                        created_note.id(),
975                        *created_note.metadata(),
976                        note_inclusion_proof,
977                    ),
978                );
979            }
980        }
981
982        debug_assert_eq!(
983            self.chain.commitment(),
984            proven_block.header().chain_commitment(),
985            "current mock chain commitment and new block's chain commitment should match"
986        );
987        debug_assert_eq!(
988            BlockNumber::from(self.chain.as_mmr().forest().num_leaves() as u32),
989            proven_block.header().block_num(),
990            "current mock chain length and new block's number should match"
991        );
992
993        self.chain.push(proven_block.header().commitment());
994        self.blocks.push(proven_block);
995
996        Ok(())
997    }
998
999    fn pending_transactions_to_batches(&mut self) -> anyhow::Result<Vec<ProvenBatch>> {
1000        // Batches must contain at least one transaction, so if there are no pending transactions,
1001        // return early.
1002        if self.pending_transactions.is_empty() {
1003            return Ok(vec![]);
1004        }
1005
1006        let pending_transactions = core::mem::take(&mut self.pending_transactions);
1007
1008        // TODO: Distribute the transactions into multiple batches if the transactions would not fit
1009        // into a single batch (according to max input notes, max output notes and max accounts).
1010        let proposed_batch = self.propose_transaction_batch(pending_transactions)?;
1011        let proven_batch = self.prove_transaction_batch(proposed_batch)?;
1012
1013        Ok(vec![proven_batch])
1014    }
1015
1016    /// Creates a new block in the mock chain.
1017    ///
1018    /// Block building is divided into two steps:
1019    ///
1020    /// 1. Build batches from pending transactions and a block from those batches. This results in a
1021    ///    block.
1022    /// 2. Insert all the account updates, nullifiers and notes from the block into the chain state.
1023    ///
1024    /// If a `timestamp` is provided, it will be set on the block.
1025    fn prove_and_apply_block(&mut self, timestamp: Option<u32>) -> anyhow::Result<ProvenBlock> {
1026        // Create batches from pending transactions.
1027        // ----------------------------------------------------------------------------------------
1028
1029        let mut batches = self.pending_transactions_to_batches()?;
1030        batches.extend(core::mem::take(&mut self.pending_batches));
1031
1032        // Create block.
1033        // ----------------------------------------------------------------------------------------
1034
1035        let block_timestamp =
1036            timestamp.unwrap_or(self.latest_block_header().timestamp() + Self::TIMESTAMP_STEP_SECS);
1037
1038        let proposed_block = self
1039            .propose_block_at(batches.clone(), block_timestamp)
1040            .context("failed to create proposed block")?;
1041        let proven_block = self.prove_block(proposed_block.clone())?;
1042
1043        // Apply block.
1044        // ----------------------------------------------------------------------------------------
1045
1046        self.apply_block(proven_block.clone()).context("failed to apply block")?;
1047
1048        Ok(proven_block)
1049    }
1050
1051    /// Proves proposed block alongside a corresponding list of batches.
1052    pub fn prove_block(&self, proposed_block: ProposedBlock) -> anyhow::Result<ProvenBlock> {
1053        let (header, body) = proposed_block.clone().into_header_and_body()?;
1054        let inputs = self.get_block_inputs(proposed_block.batches().as_slice())?;
1055        let block_proof = LocalBlockProver::new(MIN_PROOF_SECURITY_LEVEL).prove_dummy(
1056            proposed_block.batches().clone(),
1057            header.clone(),
1058            inputs,
1059        )?;
1060        let signature = self.validator_secret_key.sign(header.commitment());
1061        Ok(ProvenBlock::new_unchecked(header, body, signature, block_proof))
1062    }
1063}
1064
1065impl Default for MockChain {
1066    fn default() -> Self {
1067        MockChain::new()
1068    }
1069}
1070
1071// SERIALIZATION
1072// ================================================================================================
1073
1074impl Serializable for MockChain {
1075    fn write_into<W: ByteWriter>(&self, target: &mut W) {
1076        self.chain.write_into(target);
1077        self.blocks.write_into(target);
1078        self.nullifier_tree.write_into(target);
1079        self.account_tree.write_into(target);
1080        self.pending_transactions.write_into(target);
1081        self.committed_accounts.write_into(target);
1082        self.committed_notes.write_into(target);
1083        self.account_authenticators.write_into(target);
1084        self.validator_secret_key.write_into(target);
1085    }
1086}
1087
1088impl Deserializable for MockChain {
1089    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
1090        let chain = Blockchain::read_from(source)?;
1091        let blocks = Vec::<ProvenBlock>::read_from(source)?;
1092        let nullifier_tree = NullifierTree::read_from(source)?;
1093        let account_tree = AccountTree::read_from(source)?;
1094        let pending_transactions = Vec::<ProvenTransaction>::read_from(source)?;
1095        let committed_accounts = BTreeMap::<AccountId, Account>::read_from(source)?;
1096        let committed_notes = BTreeMap::<NoteId, MockChainNote>::read_from(source)?;
1097        let account_authenticators =
1098            BTreeMap::<AccountId, AccountAuthenticator>::read_from(source)?;
1099        let secret_key = SigningKey::read_from(source)?;
1100
1101        Ok(Self {
1102            chain,
1103            blocks,
1104            nullifier_tree,
1105            account_tree,
1106            pending_transactions,
1107            pending_batches: Vec::new(),
1108            committed_notes,
1109            committed_accounts,
1110            account_authenticators,
1111            validator_secret_key: secret_key,
1112        })
1113    }
1114}
1115
1116// ACCOUNT STATE
1117// ================================================================================================
1118
1119/// Helper type for increased readability at call-sites. Indicates whether to build a new (nonce =
1120/// ZERO) or existing account (nonce = ONE).
1121pub enum AccountState {
1122    New,
1123    Exists,
1124}
1125
1126// ACCOUNT AUTHENTICATOR
1127// ================================================================================================
1128
1129/// A wrapper around the authenticator of an account.
1130#[derive(Debug, Clone)]
1131pub(super) struct AccountAuthenticator {
1132    authenticator: Option<BasicAuthenticator>,
1133}
1134
1135impl AccountAuthenticator {
1136    pub fn new(authenticator: Option<BasicAuthenticator>) -> Self {
1137        Self { authenticator }
1138    }
1139
1140    pub fn authenticator(&self) -> Option<&BasicAuthenticator> {
1141        self.authenticator.as_ref()
1142    }
1143}
1144
1145impl PartialEq for AccountAuthenticator {
1146    fn eq(&self, other: &Self) -> bool {
1147        match (&self.authenticator, &other.authenticator) {
1148            (Some(a), Some(b)) => {
1149                a.keys().keys().zip(b.keys().keys()).all(|(a_key, b_key)| a_key == b_key)
1150            },
1151            (None, None) => true,
1152            _ => false,
1153        }
1154    }
1155}
1156
1157// SERIALIZATION
1158// ================================================================================================
1159
1160impl Serializable for AccountAuthenticator {
1161    fn write_into<W: ByteWriter>(&self, target: &mut W) {
1162        self.authenticator
1163            .as_ref()
1164            .map(|auth| {
1165                auth.keys()
1166                    .values()
1167                    .map(|(secret_key, public_key)| (secret_key, public_key.as_ref().clone()))
1168                    .collect::<Vec<_>>()
1169            })
1170            .write_into(target);
1171    }
1172}
1173
1174impl Deserializable for AccountAuthenticator {
1175    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
1176        let authenticator = Option::<Vec<(AuthSecretKey, PublicKey)>>::read_from(source)?;
1177
1178        let authenticator = authenticator.map(|keys| BasicAuthenticator::from_key_pairs(&keys));
1179
1180        Ok(Self { authenticator })
1181    }
1182}
1183
1184// TX CONTEXT INPUT
1185// ================================================================================================
1186
1187/// Helper type to abstract over the inputs to [`MockChain::build_tx_context`]. See that method's
1188/// docs for details.
1189#[allow(clippy::large_enum_variant)]
1190#[derive(Debug, Clone)]
1191pub enum TxContextInput {
1192    AccountId(AccountId),
1193    Account(Account),
1194}
1195
1196impl TxContextInput {
1197    /// Returns the account ID that this input references.
1198    fn id(&self) -> AccountId {
1199        match self {
1200            TxContextInput::AccountId(account_id) => *account_id,
1201            TxContextInput::Account(account) => account.id(),
1202        }
1203    }
1204}
1205
1206impl From<AccountId> for TxContextInput {
1207    fn from(account: AccountId) -> Self {
1208        Self::AccountId(account)
1209    }
1210}
1211
1212impl From<Account> for TxContextInput {
1213    fn from(account: Account) -> Self {
1214        Self::Account(account)
1215    }
1216}
1217
1218// TESTS
1219// ================================================================================================
1220
1221#[cfg(test)]
1222mod tests {
1223    use miden_protocol::account::auth::AuthScheme;
1224    use miden_protocol::account::{AccountBuilder, AccountType};
1225    use miden_protocol::asset::{Asset, FungibleAsset};
1226    use miden_protocol::note::NoteType;
1227    use miden_protocol::testing::account_id::{
1228        ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
1229        ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
1230        ACCOUNT_ID_SENDER,
1231    };
1232    use miden_standards::account::wallets::BasicWallet;
1233
1234    use super::*;
1235    use crate::Auth;
1236
1237    #[test]
1238    fn prove_until_block() -> anyhow::Result<()> {
1239        let mut chain = MockChain::new();
1240        let block = chain.prove_until_block(5)?;
1241        assert_eq!(block.header().block_num(), 5u32.into());
1242        assert_eq!(chain.proven_blocks().len(), 6);
1243
1244        Ok(())
1245    }
1246
1247    #[tokio::test]
1248    async fn private_account_state_update() -> anyhow::Result<()> {
1249        let faucet_id = ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into()?;
1250        let account_builder = AccountBuilder::new([4; 32])
1251            .account_type(AccountType::Private)
1252            .with_component(BasicWallet);
1253
1254        let mut builder = MockChain::builder();
1255        let auth_scheme = AuthScheme::EcdsaK256Keccak;
1256        let account = builder.add_account_from_builder(
1257            Auth::BasicAuth { auth_scheme },
1258            account_builder,
1259            AccountState::New,
1260        )?;
1261
1262        let account_id = account.id();
1263        assert_eq!(account.nonce().as_canonical_u64(), 0);
1264
1265        let note_1 = builder.add_p2id_note(
1266            ACCOUNT_ID_SENDER.try_into().unwrap(),
1267            account.id(),
1268            &[Asset::Fungible(FungibleAsset::new(faucet_id, 1000u64).unwrap())],
1269            NoteType::Private,
1270        )?;
1271
1272        let mut mock_chain = builder.build()?;
1273        mock_chain.prove_next_block()?;
1274
1275        let tx = mock_chain
1276            .build_tx_context(TxContextInput::Account(account), &[], &[note_1])?
1277            .build()?
1278            .execute()
1279            .await?;
1280
1281        mock_chain.add_pending_executed_transaction(&tx)?;
1282        mock_chain.prove_next_block()?;
1283
1284        assert!(tx.final_account().nonce().as_canonical_u64() > 0);
1285        assert_eq!(
1286            tx.final_account().to_commitment(),
1287            mock_chain.account_tree.open(account_id).state_commitment()
1288        );
1289
1290        Ok(())
1291    }
1292
1293    #[tokio::test]
1294    async fn mock_chain_serialization() {
1295        let mut builder = MockChain::builder();
1296
1297        let mut notes = vec![];
1298        for i in 0..10 {
1299            let account = builder
1300                .add_account_from_builder(
1301                    Auth::BasicAuth {
1302                        auth_scheme: AuthScheme::Falcon512Poseidon2,
1303                    },
1304                    AccountBuilder::new([i; 32]).with_component(BasicWallet),
1305                    AccountState::New,
1306                )
1307                .unwrap();
1308            let note = builder
1309                .add_p2id_note(
1310                    ACCOUNT_ID_SENDER.try_into().unwrap(),
1311                    account.id(),
1312                    &[Asset::Fungible(
1313                        FungibleAsset::new(
1314                            ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET.try_into().unwrap(),
1315                            1000u64,
1316                        )
1317                        .unwrap(),
1318                    )],
1319                    NoteType::Private,
1320                )
1321                .unwrap();
1322            notes.push((account, note));
1323        }
1324
1325        let mut chain = builder.build().unwrap();
1326        for (account, note) in notes {
1327            let tx = chain
1328                .build_tx_context(TxContextInput::Account(account), &[], &[note])
1329                .unwrap()
1330                .build()
1331                .unwrap()
1332                .execute()
1333                .await
1334                .unwrap();
1335            chain.add_pending_executed_transaction(&tx).unwrap();
1336            chain.prove_next_block().unwrap();
1337        }
1338
1339        let bytes = chain.to_bytes();
1340
1341        let deserialized = MockChain::read_from_bytes(&bytes).unwrap();
1342
1343        assert_eq!(chain.chain.as_mmr().peaks(), deserialized.chain.as_mmr().peaks());
1344        assert_eq!(chain.blocks, deserialized.blocks);
1345        assert_eq!(chain.nullifier_tree, deserialized.nullifier_tree);
1346        assert_eq!(chain.account_tree, deserialized.account_tree);
1347        assert_eq!(chain.pending_transactions, deserialized.pending_transactions);
1348        assert_eq!(chain.committed_accounts, deserialized.committed_accounts);
1349        assert_eq!(chain.committed_notes, deserialized.committed_notes);
1350        assert_eq!(chain.account_authenticators, deserialized.account_authenticators);
1351    }
1352
1353    #[test]
1354    fn mock_chain_block_signature() -> anyhow::Result<()> {
1355        let mut builder = MockChain::builder();
1356        builder.add_existing_mock_account(Auth::IncrNonce)?;
1357        let mut chain = builder.build()?;
1358
1359        // Verify the genesis block signature.
1360        let genesis_block = chain.latest_block();
1361        assert!(
1362            genesis_block.signature().verify(
1363                genesis_block.header().commitment(),
1364                genesis_block.header().validator_key()
1365            )
1366        );
1367
1368        // Add another block.
1369        chain.prove_next_block()?;
1370
1371        // Verify the next block signature.
1372        let next_block = chain.latest_block();
1373        assert!(
1374            next_block
1375                .signature()
1376                .verify(next_block.header().commitment(), next_block.header().validator_key())
1377        );
1378
1379        // Public keys should be carried through from the genesis header to the next.
1380        assert_eq!(next_block.header().validator_key(), next_block.header().validator_key());
1381
1382        Ok(())
1383    }
1384
1385    #[tokio::test]
1386    async fn add_pending_batch() -> anyhow::Result<()> {
1387        let mut builder = MockChain::builder();
1388        let account = builder.add_existing_mock_account(Auth::IncrNonce)?;
1389        let mut chain = builder.build()?;
1390
1391        // Execute a noop transaction and create a batch from it.
1392        let tx = chain.build_tx_context(account.id(), &[], &[])?.build()?.execute().await?;
1393        let proven_tx = LocalTransactionProver::default().prove_dummy(tx)?;
1394        let proposed_batch = chain.propose_transaction_batch(vec![proven_tx])?;
1395        let proven_batch = chain.prove_transaction_batch(proposed_batch)?;
1396
1397        // Submit the batch directly and prove the block.
1398        let num_blocks_before = chain.proven_blocks().len();
1399        chain.add_pending_batch(proven_batch);
1400        chain.prove_next_block()?;
1401
1402        assert_eq!(chain.proven_blocks().len(), num_blocks_before + 1);
1403
1404        Ok(())
1405    }
1406}