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::SecretKey;
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, 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.faucet_id())?,
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: SecretKey,
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: SecretKey,
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 native asset 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 native_asset_id(&self) -> AccountId {
442        self.genesis_block_header().fee_parameters().native_asset_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 an [`InputNote`] for the given note ID. If the note does not exist or is not
458    /// public, `None` is returned.
459    pub fn get_public_note(&self, note_id: &NoteId) -> Option<InputNote> {
460        let note = self.committed_notes.get(note_id)?;
461        note.clone().try_into().ok()
462    }
463
464    /// Returns a reference to the account identified by the given account ID.
465    ///
466    /// The account is retrieved with the latest state known to the [`MockChain`].
467    pub fn committed_account(&self, account_id: AccountId) -> anyhow::Result<&Account> {
468        self.committed_accounts
469            .get(&account_id)
470            .with_context(|| format!("account {account_id} not found in committed accounts"))
471    }
472
473    /// Returns a reference to the [`AccountTree`] of the chain.
474    pub fn account_tree(&self) -> &AccountTree {
475        &self.account_tree
476    }
477
478    // BATCH APIS
479    // ----------------------------------------------------------------------------------------
480
481    /// Proposes a new transaction batch from the provided transactions and returns it.
482    ///
483    /// This method does not modify the chain state.
484    pub fn propose_transaction_batch<I>(
485        &self,
486        txs: impl IntoIterator<Item = ProvenTransaction, IntoIter = I>,
487    ) -> anyhow::Result<ProposedBatch>
488    where
489        I: Iterator<Item = ProvenTransaction> + Clone,
490    {
491        let transactions: Vec<_> = txs.into_iter().map(alloc::sync::Arc::new).collect();
492
493        let (batch_reference_block, partial_blockchain, unauthenticated_note_proofs) = self
494            .get_batch_inputs(
495                transactions.iter().map(|tx| tx.ref_block_num()),
496                transactions
497                    .iter()
498                    .flat_map(|tx| tx.unauthenticated_notes().map(NoteHeader::id)),
499            )?;
500
501        Ok(ProposedBatch::new(
502            transactions,
503            batch_reference_block,
504            partial_blockchain,
505            unauthenticated_note_proofs,
506        )?)
507    }
508
509    /// Mock-proves a proposed transaction batch from the provided [`ProposedBatch`] and returns it.
510    ///
511    /// This method does not modify the chain state.
512    pub fn prove_transaction_batch(
513        &self,
514        proposed_batch: ProposedBatch,
515    ) -> anyhow::Result<ProvenBatch> {
516        let batch_prover = LocalBatchProver::new(0);
517        Ok(batch_prover.prove_dummy(proposed_batch)?)
518    }
519
520    // BLOCK APIS
521    // ----------------------------------------------------------------------------------------
522
523    /// Proposes a new block from the provided batches with the given timestamp and returns it.
524    ///
525    /// This method does not modify the chain state.
526    pub fn propose_block_at<I>(
527        &self,
528        batches: impl IntoIterator<Item = ProvenBatch, IntoIter = I>,
529        timestamp: u32,
530    ) -> anyhow::Result<ProposedBlock>
531    where
532        I: Iterator<Item = ProvenBatch> + Clone,
533    {
534        let batches: Vec<_> = batches.into_iter().collect();
535
536        let block_inputs = self
537            .get_block_inputs(batches.iter())
538            .context("could not retrieve block inputs")?;
539
540        let proposed_block = ProposedBlock::new_at(block_inputs, batches, timestamp)
541            .context("failed to create proposed block")?;
542
543        Ok(proposed_block)
544    }
545
546    /// Proposes a new block from the provided batches and returns it.
547    ///
548    /// This method does not modify the chain state.
549    pub fn propose_block<I>(
550        &self,
551        batches: impl IntoIterator<Item = ProvenBatch, IntoIter = I>,
552    ) -> anyhow::Result<ProposedBlock>
553    where
554        I: Iterator<Item = ProvenBatch> + Clone,
555    {
556        // We can't access system time because we are in a no-std environment, so we use the
557        // minimally correct next timestamp.
558        let timestamp = self.latest_block_header().timestamp() + 1;
559
560        self.propose_block_at(batches, timestamp)
561    }
562
563    // TRANSACTION APIS
564    // ----------------------------------------------------------------------------------------
565
566    /// Initializes a [`TransactionContextBuilder`] for executing against a specific block number.
567    ///
568    /// Depending on the provided `input`, the builder is initialized differently:
569    /// - [`TxContextInput::AccountId`]: Initialize the builder with [`TransactionInputs`] fetched
570    ///   from the chain for the public account identified by the ID.
571    /// - [`TxContextInput::Account`]: Initialize the builder with [`TransactionInputs`] where the
572    ///   account is passed as-is to the inputs.
573    ///
574    /// In all cases, if the chain contains authenticator for the account, they are added to the
575    /// builder.
576    ///
577    /// [`TxContextInput::Account`] can be used to build a chain of transactions against the same
578    /// account that build on top of each other. For example, transaction A modifies an account
579    /// from state 0 to 1, and transaction B modifies it from state 1 to 2.
580    pub fn build_tx_context_at(
581        &self,
582        reference_block: impl Into<BlockNumber>,
583        input: impl Into<TxContextInput>,
584        note_ids: &[NoteId],
585        unauthenticated_notes: &[Note],
586    ) -> anyhow::Result<TransactionContextBuilder> {
587        let input = input.into();
588        let reference_block = reference_block.into();
589
590        let authenticator = self.account_authenticators.get(&input.id());
591        let authenticator =
592            authenticator.and_then(|authenticator| authenticator.authenticator().cloned());
593
594        anyhow::ensure!(
595            reference_block.as_usize() < self.blocks.len(),
596            "reference block {reference_block} is out of range (latest {})",
597            self.latest_block_header().block_num()
598        );
599
600        let account = match input {
601            TxContextInput::AccountId(account_id) => {
602                if account_id.is_private() {
603                    return Err(anyhow::anyhow!(
604                        "transaction contexts for private accounts should be created with TxContextInput::Account"
605                    ));
606                }
607
608                self.committed_accounts
609                    .get(&account_id)
610                    .with_context(|| {
611                        format!("account {account_id} not found in committed accounts")
612                    })?
613                    .clone()
614            },
615            TxContextInput::Account(account) => account,
616        };
617
618        let tx_inputs = self
619            .get_transaction_inputs_at(reference_block, &account, note_ids, unauthenticated_notes)
620            .context("failed to gather transaction inputs")?;
621
622        let tx_context_builder = TransactionContextBuilder::new(account)
623            .authenticator(authenticator)
624            .tx_inputs(tx_inputs);
625
626        Ok(tx_context_builder)
627    }
628
629    /// Initializes a [`TransactionContextBuilder`] for executing against the last block header.
630    ///
631    /// This is a wrapper around [`Self::build_tx_context_at`] which uses the latest block as the
632    /// reference block. See that function's docs for details.
633    pub fn build_tx_context(
634        &self,
635        input: impl Into<TxContextInput>,
636        note_ids: &[NoteId],
637        unauthenticated_notes: &[Note],
638    ) -> anyhow::Result<TransactionContextBuilder> {
639        let reference_block = self.latest_block_header().block_num();
640        self.build_tx_context_at(reference_block, input, note_ids, unauthenticated_notes)
641    }
642
643    // INPUTS APIS
644    // ----------------------------------------------------------------------------------------
645
646    /// Returns a valid [`TransactionInputs`] for the specified entities, executing against
647    /// a specific block number.
648    pub fn get_transaction_inputs_at(
649        &self,
650        reference_block: BlockNumber,
651        account: impl Into<PartialAccount>,
652        notes: &[NoteId],
653        unauthenticated_notes: &[Note],
654    ) -> anyhow::Result<TransactionInputs> {
655        let ref_block = self.block_header(reference_block.as_usize());
656
657        let mut input_notes = vec![];
658        let mut block_headers_map: BTreeMap<BlockNumber, BlockHeader> = BTreeMap::new();
659        for note in notes {
660            let input_note: InputNote = self
661                .committed_notes
662                .get(note)
663                .with_context(|| format!("note with id {note} not found"))?
664                .clone()
665                .try_into()
666                .with_context(|| {
667                    format!("failed to convert mock chain note with id {note} into input note")
668                })?;
669
670            let note_block_num = input_note
671                .location()
672                .with_context(|| format!("note location not available: {note}"))?
673                .block_num();
674
675            if note_block_num > ref_block.block_num() {
676                anyhow::bail!(
677                    "note with ID {note} was created in block {note_block_num} which is larger than the reference block number {}",
678                    ref_block.block_num()
679                )
680            }
681
682            if note_block_num != ref_block.block_num() {
683                let block_header = self
684                    .blocks
685                    .get(note_block_num.as_usize())
686                    .with_context(|| format!("block {note_block_num} not found in chain"))?
687                    .header()
688                    .clone();
689                block_headers_map.insert(note_block_num, block_header);
690            }
691
692            input_notes.push(input_note);
693        }
694
695        for note in unauthenticated_notes {
696            input_notes.push(InputNote::Unauthenticated { note: note.clone() })
697        }
698
699        let block_headers = block_headers_map.values();
700        let (_, partial_blockchain) = self.selective_partial_blockchain(
701            reference_block,
702            block_headers.map(BlockHeader::block_num),
703        )?;
704
705        let input_notes = InputNotes::new(input_notes)?;
706
707        Ok(TransactionInputs::new(
708            account.into(),
709            ref_block.clone(),
710            partial_blockchain,
711            input_notes,
712        )?)
713    }
714
715    /// Returns a valid [`TransactionInputs`] for the specified entities.
716    pub fn get_transaction_inputs(
717        &self,
718        account: impl Into<PartialAccount>,
719        notes: &[NoteId],
720        unauthenticated_notes: &[Note],
721    ) -> anyhow::Result<TransactionInputs> {
722        let latest_block_num = self.latest_block_header().block_num();
723        self.get_transaction_inputs_at(latest_block_num, account, notes, unauthenticated_notes)
724    }
725
726    /// Returns inputs for a transaction batch for all the reference blocks of the provided
727    /// transactions.
728    pub fn get_batch_inputs(
729        &self,
730        tx_reference_blocks: impl IntoIterator<Item = BlockNumber>,
731        unauthenticated_notes: impl Iterator<Item = NoteId>,
732    ) -> anyhow::Result<(BlockHeader, PartialBlockchain, BTreeMap<NoteId, NoteInclusionProof>)>
733    {
734        // Fetch note proofs for notes that exist in the chain.
735        let unauthenticated_note_proofs = self.unauthenticated_note_proofs(unauthenticated_notes);
736
737        // We also need to fetch block inclusion proofs for any of the blocks that contain
738        // unauthenticated notes for which we want to prove inclusion.
739        let required_blocks = tx_reference_blocks.into_iter().chain(
740            unauthenticated_note_proofs
741                .values()
742                .map(|note_proof| note_proof.location().block_num()),
743        );
744
745        let (batch_reference_block, partial_block_chain) =
746            self.latest_selective_partial_blockchain(required_blocks)?;
747
748        Ok((batch_reference_block, partial_block_chain, unauthenticated_note_proofs))
749    }
750
751    /// Gets foreign account inputs to execute FPI transactions.
752    ///
753    /// Used in tests to get foreign account inputs for FPI calls.
754    pub fn get_foreign_account_inputs(
755        &self,
756        account_id: AccountId,
757    ) -> anyhow::Result<(Account, AccountWitness)> {
758        let account = self.committed_account(account_id)?.clone();
759
760        let account_witness = self.account_tree().open(account_id);
761        assert_eq!(account_witness.state_commitment(), account.to_commitment());
762
763        Ok((account, account_witness))
764    }
765
766    /// Gets the inputs for a block for the provided batches.
767    pub fn get_block_inputs<'batch, I>(
768        &self,
769        batch_iter: impl IntoIterator<Item = &'batch ProvenBatch, IntoIter = I>,
770    ) -> anyhow::Result<BlockInputs>
771    where
772        I: Iterator<Item = &'batch ProvenBatch> + Clone,
773    {
774        let batch_iterator = batch_iter.into_iter();
775
776        let unauthenticated_note_proofs =
777            self.unauthenticated_note_proofs(batch_iterator.clone().flat_map(|batch| {
778                batch.input_notes().iter().filter_map(|note| note.header().map(NoteHeader::id))
779            }));
780
781        let (block_reference_block, partial_blockchain) = self
782            .latest_selective_partial_blockchain(
783                batch_iterator.clone().map(ProvenBatch::reference_block_num).chain(
784                    unauthenticated_note_proofs.values().map(|proof| proof.location().block_num()),
785                ),
786            )?;
787
788        let account_witnesses =
789            self.account_witnesses(batch_iterator.clone().flat_map(ProvenBatch::updated_accounts));
790
791        let nullifier_proofs =
792            self.nullifier_witnesses(batch_iterator.flat_map(ProvenBatch::created_nullifiers));
793
794        Ok(BlockInputs::new(
795            block_reference_block,
796            partial_blockchain,
797            account_witnesses,
798            nullifier_proofs,
799            unauthenticated_note_proofs,
800        ))
801    }
802
803    // PUBLIC MUTATORS
804    // ----------------------------------------------------------------------------------------
805
806    /// Proves the next block in the mock chain.
807    ///
808    /// This will commit all the currently pending transactions into the chain state.
809    pub fn prove_next_block(&mut self) -> anyhow::Result<ProvenBlock> {
810        self.prove_and_apply_block(None)
811    }
812
813    /// Proves the next block in the mock chain at the given timestamp.
814    ///
815    /// This will commit all the currently pending transactions into the chain state.
816    pub fn prove_next_block_at(&mut self, timestamp: u32) -> anyhow::Result<ProvenBlock> {
817        self.prove_and_apply_block(Some(timestamp))
818    }
819
820    /// Proves new blocks until the block with the given target block number has been created.
821    ///
822    /// For example, if the latest block is `5` and this function is called with `10`, then blocks
823    /// `6..=10` will be created and block 10 will be returned.
824    ///
825    /// # Panics
826    ///
827    /// Panics if:
828    /// - the given block number is smaller or equal to the number of the latest block in the chain.
829    pub fn prove_until_block(
830        &mut self,
831        target_block_num: impl Into<BlockNumber>,
832    ) -> anyhow::Result<ProvenBlock> {
833        let target_block_num = target_block_num.into();
834        let latest_block_num = self.latest_block_header().block_num();
835        assert!(
836            target_block_num > latest_block_num,
837            "target block number must be greater than the number of the latest block in the chain"
838        );
839
840        let mut last_block = None;
841        for _ in latest_block_num.as_usize()..target_block_num.as_usize() {
842            last_block = Some(self.prove_next_block()?);
843        }
844
845        Ok(last_block.expect("at least one block should have been created"))
846    }
847
848    // PUBLIC MUTATORS (PENDING APIS)
849    // ----------------------------------------------------------------------------------------
850
851    /// Adds the given [`ExecutedTransaction`] to the list of pending transactions.
852    ///
853    /// A block has to be created to apply the transaction effects to the chain state, e.g. using
854    /// [`MockChain::prove_next_block`].
855    pub fn add_pending_executed_transaction(
856        &mut self,
857        transaction: &ExecutedTransaction,
858    ) -> anyhow::Result<()> {
859        // Transform the executed tx into a proven tx with a dummy proof.
860        let proven_tx = LocalTransactionProver::default()
861            .prove_dummy(transaction.clone())
862            .context("failed to dummy-prove executed transaction into proven transaction")?;
863
864        self.pending_transactions.push(proven_tx);
865
866        Ok(())
867    }
868
869    /// Adds the given [`ProvenTransaction`] 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_proven_transaction(&mut self, transaction: ProvenTransaction) {
874        self.pending_transactions.push(transaction);
875    }
876
877    /// Adds the given [`ProvenBatch`] to the list of pending batches.
878    ///
879    /// A block has to be created to apply the batch effects to the chain state, e.g. using
880    /// [`MockChain::prove_next_block`].
881    pub fn add_pending_batch(&mut self, batch: ProvenBatch) {
882        self.pending_batches.push(batch);
883    }
884
885    // PRIVATE HELPERS
886    // ----------------------------------------------------------------------------------------
887
888    /// Applies the given block to the chain state, which means:
889    ///
890    /// - Insert account and nullifiers into the respective trees.
891    /// - Updated accounts from the block are updated in the committed accounts.
892    /// - Created notes are inserted into the committed notes.
893    /// - Consumed notes are removed from the committed notes.
894    /// - The block is appended to the [`BlockChain`] and the list of proven blocks.
895    fn apply_block(&mut self, proven_block: ProvenBlock) -> anyhow::Result<()> {
896        for account_update in proven_block.body().updated_accounts() {
897            self.account_tree
898                .insert(account_update.account_id(), account_update.final_state_commitment())
899                .context("failed to insert account update into account tree")?;
900        }
901
902        for nullifier in proven_block.body().created_nullifiers() {
903            self.nullifier_tree
904                .mark_spent(*nullifier, proven_block.header().block_num())
905                .context("failed to mark block nullifier as spent")?;
906
907            // TODO: Remove from self.committed_notes. This is not critical to have for now. It is
908            // not straightforward, because committed_notes are indexed by note IDs rather than
909            // nullifiers, so we'll have to create a second index to do this.
910        }
911
912        for account_update in proven_block.body().updated_accounts() {
913            match account_update.details() {
914                AccountUpdateDetails::Delta(account_delta) => {
915                    if account_delta.is_full_state() {
916                        let account = Account::try_from(account_delta)
917                            .context("failed to convert full state delta into full account")?;
918                        self.committed_accounts.insert(account.id(), account.clone());
919                    } else {
920                        let committed_account = self
921                            .committed_accounts
922                            .get_mut(&account_update.account_id())
923                            .ok_or_else(|| {
924                                anyhow::anyhow!("account delta in block for non-existent account")
925                            })?;
926                        committed_account
927                            .apply_delta(account_delta)
928                            .context("failed to apply account delta")?;
929                    }
930                },
931                // No state to keep for private accounts other than the commitment on the account
932                // tree
933                AccountUpdateDetails::Private => {},
934            }
935        }
936
937        let notes_tree = proven_block.body().compute_block_note_tree();
938        for (block_note_index, created_note) in proven_block.body().output_notes() {
939            let note_path = notes_tree.open(block_note_index);
940            let note_inclusion_proof = NoteInclusionProof::new(
941                proven_block.header().block_num(),
942                block_note_index.leaf_index_value(),
943                note_path,
944            )
945            .context("failed to create inclusion proof for output note")?;
946
947            if let OutputNote::Public(public_note) = created_note {
948                self.committed_notes.insert(
949                    public_note.id(),
950                    MockChainNote::Public(public_note.as_note().clone(), note_inclusion_proof),
951                );
952            } else {
953                self.committed_notes.insert(
954                    created_note.id(),
955                    MockChainNote::Private(
956                        created_note.id(),
957                        created_note.metadata().clone(),
958                        note_inclusion_proof,
959                    ),
960                );
961            }
962        }
963
964        debug_assert_eq!(
965            self.chain.commitment(),
966            proven_block.header().chain_commitment(),
967            "current mock chain commitment and new block's chain commitment should match"
968        );
969        debug_assert_eq!(
970            BlockNumber::from(self.chain.as_mmr().forest().num_leaves() as u32),
971            proven_block.header().block_num(),
972            "current mock chain length and new block's number should match"
973        );
974
975        self.chain.push(proven_block.header().commitment());
976        self.blocks.push(proven_block);
977
978        Ok(())
979    }
980
981    fn pending_transactions_to_batches(&mut self) -> anyhow::Result<Vec<ProvenBatch>> {
982        // Batches must contain at least one transaction, so if there are no pending transactions,
983        // return early.
984        if self.pending_transactions.is_empty() {
985            return Ok(vec![]);
986        }
987
988        let pending_transactions = core::mem::take(&mut self.pending_transactions);
989
990        // TODO: Distribute the transactions into multiple batches if the transactions would not fit
991        // into a single batch (according to max input notes, max output notes and max accounts).
992        let proposed_batch = self.propose_transaction_batch(pending_transactions)?;
993        let proven_batch = self.prove_transaction_batch(proposed_batch)?;
994
995        Ok(vec![proven_batch])
996    }
997
998    /// Creates a new block in the mock chain.
999    ///
1000    /// Block building is divided into two steps:
1001    ///
1002    /// 1. Build batches from pending transactions and a block from those batches. This results in a
1003    ///    block.
1004    /// 2. Insert all the account updates, nullifiers and notes from the block into the chain state.
1005    ///
1006    /// If a `timestamp` is provided, it will be set on the block.
1007    fn prove_and_apply_block(&mut self, timestamp: Option<u32>) -> anyhow::Result<ProvenBlock> {
1008        // Create batches from pending transactions.
1009        // ----------------------------------------------------------------------------------------
1010
1011        let mut batches = self.pending_transactions_to_batches()?;
1012        batches.extend(core::mem::take(&mut self.pending_batches));
1013
1014        // Create block.
1015        // ----------------------------------------------------------------------------------------
1016
1017        let block_timestamp =
1018            timestamp.unwrap_or(self.latest_block_header().timestamp() + Self::TIMESTAMP_STEP_SECS);
1019
1020        let proposed_block = self
1021            .propose_block_at(batches.clone(), block_timestamp)
1022            .context("failed to create proposed block")?;
1023        let proven_block = self.prove_block(proposed_block.clone())?;
1024
1025        // Apply block.
1026        // ----------------------------------------------------------------------------------------
1027
1028        self.apply_block(proven_block.clone()).context("failed to apply block")?;
1029
1030        Ok(proven_block)
1031    }
1032
1033    /// Proves proposed block alongside a corresponding list of batches.
1034    pub fn prove_block(&self, proposed_block: ProposedBlock) -> anyhow::Result<ProvenBlock> {
1035        let (header, body) = proposed_block.clone().into_header_and_body()?;
1036        let inputs = self.get_block_inputs(proposed_block.batches().as_slice())?;
1037        let block_proof = LocalBlockProver::new(MIN_PROOF_SECURITY_LEVEL).prove_dummy(
1038            proposed_block.batches().clone(),
1039            header.clone(),
1040            inputs,
1041        )?;
1042        let signature = self.validator_secret_key.sign(header.commitment());
1043        Ok(ProvenBlock::new_unchecked(header, body, signature, block_proof))
1044    }
1045}
1046
1047impl Default for MockChain {
1048    fn default() -> Self {
1049        MockChain::new()
1050    }
1051}
1052
1053// SERIALIZATION
1054// ================================================================================================
1055
1056impl Serializable for MockChain {
1057    fn write_into<W: ByteWriter>(&self, target: &mut W) {
1058        self.chain.write_into(target);
1059        self.blocks.write_into(target);
1060        self.nullifier_tree.write_into(target);
1061        self.account_tree.write_into(target);
1062        self.pending_transactions.write_into(target);
1063        self.committed_accounts.write_into(target);
1064        self.committed_notes.write_into(target);
1065        self.account_authenticators.write_into(target);
1066        self.validator_secret_key.write_into(target);
1067    }
1068}
1069
1070impl Deserializable for MockChain {
1071    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
1072        let chain = Blockchain::read_from(source)?;
1073        let blocks = Vec::<ProvenBlock>::read_from(source)?;
1074        let nullifier_tree = NullifierTree::read_from(source)?;
1075        let account_tree = AccountTree::read_from(source)?;
1076        let pending_transactions = Vec::<ProvenTransaction>::read_from(source)?;
1077        let committed_accounts = BTreeMap::<AccountId, Account>::read_from(source)?;
1078        let committed_notes = BTreeMap::<NoteId, MockChainNote>::read_from(source)?;
1079        let account_authenticators =
1080            BTreeMap::<AccountId, AccountAuthenticator>::read_from(source)?;
1081        let secret_key = SecretKey::read_from(source)?;
1082
1083        Ok(Self {
1084            chain,
1085            blocks,
1086            nullifier_tree,
1087            account_tree,
1088            pending_transactions,
1089            pending_batches: Vec::new(),
1090            committed_notes,
1091            committed_accounts,
1092            account_authenticators,
1093            validator_secret_key: secret_key,
1094        })
1095    }
1096}
1097
1098// ACCOUNT STATE
1099// ================================================================================================
1100
1101/// Helper type for increased readability at call-sites. Indicates whether to build a new (nonce =
1102/// ZERO) or existing account (nonce = ONE).
1103pub enum AccountState {
1104    New,
1105    Exists,
1106}
1107
1108// ACCOUNT AUTHENTICATOR
1109// ================================================================================================
1110
1111/// A wrapper around the authenticator of an account.
1112#[derive(Debug, Clone)]
1113pub(super) struct AccountAuthenticator {
1114    authenticator: Option<BasicAuthenticator>,
1115}
1116
1117impl AccountAuthenticator {
1118    pub fn new(authenticator: Option<BasicAuthenticator>) -> Self {
1119        Self { authenticator }
1120    }
1121
1122    pub fn authenticator(&self) -> Option<&BasicAuthenticator> {
1123        self.authenticator.as_ref()
1124    }
1125}
1126
1127impl PartialEq for AccountAuthenticator {
1128    fn eq(&self, other: &Self) -> bool {
1129        match (&self.authenticator, &other.authenticator) {
1130            (Some(a), Some(b)) => {
1131                a.keys().keys().zip(b.keys().keys()).all(|(a_key, b_key)| a_key == b_key)
1132            },
1133            (None, None) => true,
1134            _ => false,
1135        }
1136    }
1137}
1138
1139// SERIALIZATION
1140// ================================================================================================
1141
1142impl Serializable for AccountAuthenticator {
1143    fn write_into<W: ByteWriter>(&self, target: &mut W) {
1144        self.authenticator
1145            .as_ref()
1146            .map(|auth| {
1147                auth.keys()
1148                    .values()
1149                    .map(|(secret_key, public_key)| (secret_key, public_key.as_ref().clone()))
1150                    .collect::<Vec<_>>()
1151            })
1152            .write_into(target);
1153    }
1154}
1155
1156impl Deserializable for AccountAuthenticator {
1157    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
1158        let authenticator = Option::<Vec<(AuthSecretKey, PublicKey)>>::read_from(source)?;
1159
1160        let authenticator = authenticator.map(|keys| BasicAuthenticator::from_key_pairs(&keys));
1161
1162        Ok(Self { authenticator })
1163    }
1164}
1165
1166// TX CONTEXT INPUT
1167// ================================================================================================
1168
1169/// Helper type to abstract over the inputs to [`MockChain::build_tx_context`]. See that method's
1170/// docs for details.
1171#[allow(clippy::large_enum_variant)]
1172#[derive(Debug, Clone)]
1173pub enum TxContextInput {
1174    AccountId(AccountId),
1175    Account(Account),
1176}
1177
1178impl TxContextInput {
1179    /// Returns the account ID that this input references.
1180    fn id(&self) -> AccountId {
1181        match self {
1182            TxContextInput::AccountId(account_id) => *account_id,
1183            TxContextInput::Account(account) => account.id(),
1184        }
1185    }
1186}
1187
1188impl From<AccountId> for TxContextInput {
1189    fn from(account: AccountId) -> Self {
1190        Self::AccountId(account)
1191    }
1192}
1193
1194impl From<Account> for TxContextInput {
1195    fn from(account: Account) -> Self {
1196        Self::Account(account)
1197    }
1198}
1199
1200// TESTS
1201// ================================================================================================
1202
1203#[cfg(test)]
1204mod tests {
1205    use miden_protocol::account::auth::AuthScheme;
1206    use miden_protocol::account::{AccountBuilder, AccountStorageMode};
1207    use miden_protocol::asset::{Asset, FungibleAsset};
1208    use miden_protocol::note::NoteType;
1209    use miden_protocol::testing::account_id::{
1210        ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
1211        ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
1212        ACCOUNT_ID_SENDER,
1213    };
1214    use miden_standards::account::wallets::BasicWallet;
1215
1216    use super::*;
1217    use crate::Auth;
1218
1219    #[test]
1220    fn prove_until_block() -> anyhow::Result<()> {
1221        let mut chain = MockChain::new();
1222        let block = chain.prove_until_block(5)?;
1223        assert_eq!(block.header().block_num(), 5u32.into());
1224        assert_eq!(chain.proven_blocks().len(), 6);
1225
1226        Ok(())
1227    }
1228
1229    #[tokio::test]
1230    async fn private_account_state_update() -> anyhow::Result<()> {
1231        let faucet_id = ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into()?;
1232        let account_builder = AccountBuilder::new([4; 32])
1233            .storage_mode(AccountStorageMode::Private)
1234            .with_component(BasicWallet);
1235
1236        let mut builder = MockChain::builder();
1237        let auth_scheme = AuthScheme::EcdsaK256Keccak;
1238        let account = builder.add_account_from_builder(
1239            Auth::BasicAuth { auth_scheme },
1240            account_builder,
1241            AccountState::New,
1242        )?;
1243
1244        let account_id = account.id();
1245        assert_eq!(account.nonce().as_canonical_u64(), 0);
1246
1247        let note_1 = builder.add_p2id_note(
1248            ACCOUNT_ID_SENDER.try_into().unwrap(),
1249            account.id(),
1250            &[Asset::Fungible(FungibleAsset::new(faucet_id, 1000u64).unwrap())],
1251            NoteType::Private,
1252        )?;
1253
1254        let mut mock_chain = builder.build()?;
1255        mock_chain.prove_next_block()?;
1256
1257        let tx = mock_chain
1258            .build_tx_context(TxContextInput::Account(account), &[], &[note_1])?
1259            .build()?
1260            .execute()
1261            .await?;
1262
1263        mock_chain.add_pending_executed_transaction(&tx)?;
1264        mock_chain.prove_next_block()?;
1265
1266        assert!(tx.final_account().nonce().as_canonical_u64() > 0);
1267        assert_eq!(
1268            tx.final_account().to_commitment(),
1269            mock_chain.account_tree.open(account_id).state_commitment()
1270        );
1271
1272        Ok(())
1273    }
1274
1275    #[tokio::test]
1276    async fn mock_chain_serialization() {
1277        let mut builder = MockChain::builder();
1278
1279        let mut notes = vec![];
1280        for i in 0..10 {
1281            let account = builder
1282                .add_account_from_builder(
1283                    Auth::BasicAuth {
1284                        auth_scheme: AuthScheme::Falcon512Poseidon2,
1285                    },
1286                    AccountBuilder::new([i; 32]).with_component(BasicWallet),
1287                    AccountState::New,
1288                )
1289                .unwrap();
1290            let note = builder
1291                .add_p2id_note(
1292                    ACCOUNT_ID_SENDER.try_into().unwrap(),
1293                    account.id(),
1294                    &[Asset::Fungible(
1295                        FungibleAsset::new(
1296                            ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET.try_into().unwrap(),
1297                            1000u64,
1298                        )
1299                        .unwrap(),
1300                    )],
1301                    NoteType::Private,
1302                )
1303                .unwrap();
1304            notes.push((account, note));
1305        }
1306
1307        let mut chain = builder.build().unwrap();
1308        for (account, note) in notes {
1309            let tx = chain
1310                .build_tx_context(TxContextInput::Account(account), &[], &[note])
1311                .unwrap()
1312                .build()
1313                .unwrap()
1314                .execute()
1315                .await
1316                .unwrap();
1317            chain.add_pending_executed_transaction(&tx).unwrap();
1318            chain.prove_next_block().unwrap();
1319        }
1320
1321        let bytes = chain.to_bytes();
1322
1323        let deserialized = MockChain::read_from_bytes(&bytes).unwrap();
1324
1325        assert_eq!(chain.chain.as_mmr().peaks(), deserialized.chain.as_mmr().peaks());
1326        assert_eq!(chain.blocks, deserialized.blocks);
1327        assert_eq!(chain.nullifier_tree, deserialized.nullifier_tree);
1328        assert_eq!(chain.account_tree, deserialized.account_tree);
1329        assert_eq!(chain.pending_transactions, deserialized.pending_transactions);
1330        assert_eq!(chain.committed_accounts, deserialized.committed_accounts);
1331        assert_eq!(chain.committed_notes, deserialized.committed_notes);
1332        assert_eq!(chain.account_authenticators, deserialized.account_authenticators);
1333    }
1334
1335    #[test]
1336    fn mock_chain_block_signature() -> anyhow::Result<()> {
1337        let mut builder = MockChain::builder();
1338        builder.add_existing_mock_account(Auth::IncrNonce)?;
1339        let mut chain = builder.build()?;
1340
1341        // Verify the genesis block signature.
1342        let genesis_block = chain.latest_block();
1343        assert!(
1344            genesis_block.signature().verify(
1345                genesis_block.header().commitment(),
1346                genesis_block.header().validator_key()
1347            )
1348        );
1349
1350        // Add another block.
1351        chain.prove_next_block()?;
1352
1353        // Verify the next block signature.
1354        let next_block = chain.latest_block();
1355        assert!(
1356            next_block
1357                .signature()
1358                .verify(next_block.header().commitment(), next_block.header().validator_key())
1359        );
1360
1361        // Public keys should be carried through from the genesis header to the next.
1362        assert_eq!(next_block.header().validator_key(), next_block.header().validator_key());
1363
1364        Ok(())
1365    }
1366
1367    #[tokio::test]
1368    async fn add_pending_batch() -> anyhow::Result<()> {
1369        let mut builder = MockChain::builder();
1370        let account = builder.add_existing_mock_account(Auth::IncrNonce)?;
1371        let mut chain = builder.build()?;
1372
1373        // Execute a noop transaction and create a batch from it.
1374        let tx = chain.build_tx_context(account.id(), &[], &[])?.build()?.execute().await?;
1375        let proven_tx = LocalTransactionProver::default().prove_dummy(tx)?;
1376        let proposed_batch = chain.propose_transaction_batch(vec![proven_tx])?;
1377        let proven_batch = chain.prove_transaction_batch(proposed_batch)?;
1378
1379        // Submit the batch directly and prove the block.
1380        let num_blocks_before = chain.proven_blocks().len();
1381        chain.add_pending_batch(proven_batch);
1382        chain.prove_next_block()?;
1383
1384        assert_eq!(chain.proven_blocks().len(), num_blocks_before + 1);
1385
1386        Ok(())
1387    }
1388}