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::Word;
8use miden_protocol::account::auth::{AuthSecretKey, PublicKey};
9use miden_protocol::account::{Account, AccountId, AccountUpdateDetails, PartialAccount};
10use miden_protocol::batch::{ProposedBatch, ProvenBatch};
11use miden_protocol::block::account_tree::{AccountTree, AccountWitness};
12use miden_protocol::block::nullifier_tree::{NullifierTree, NullifierWitness};
13use miden_protocol::block::{
14    BlockAccountUpdate,
15    BlockBody,
16    BlockHeader,
17    BlockInputs,
18    BlockNumber,
19    BlockSignatures,
20    Blockchain,
21    OutputNoteBatch,
22    ProposedBlock,
23    ProvenBlock,
24    ValidatorConfig,
25};
26use miden_protocol::crypto::dsa::ecdsa_k256_keccak::SigningKey;
27use miden_protocol::note::{Note, NoteHeader, NoteId, NoteInclusionProof, Nullifier};
28use miden_protocol::protocol_config::ProtocolConfig;
29use miden_protocol::transaction::{
30    ExecutedTransaction,
31    InputNote,
32    InputNotes,
33    OrderedTransactionHeaders,
34    OutputNote,
35    PartialBlockchain,
36    ProvenTransaction,
37    TransactionInputs,
38};
39use miden_protocol::vm::ExecutionProof;
40use miden_tx::LocalTransactionProver;
41use miden_tx::auth::BasicAuthenticator;
42use miden_tx::utils::serde::{ByteReader, ByteWriter, Deserializable, Serializable};
43use miden_tx_batch::LocalBatchProver;
44
45use super::note::MockChainNote;
46use crate::{MockChainBuilder, MockTransactionBuilder};
47
48// MOCK CHAIN
49// ================================================================================================
50
51/// The [`MockChain`] simulates a simplified blockchain environment for testing purposes.
52///
53/// The typical usage of a mock chain is:
54/// - Creating it using a [`MockChainBuilder`], which allows adding accounts and notes to the
55///   genesis state.
56/// - Creating transactions against the chain state and executing them.
57/// - Adding executed or proven transactions to the set of pending transactions (the "mempool"),
58///   e.g. using [`MockChain::add_pending_executed_transaction`].
59/// - Proving a block, which adds all pending transactions to the chain state, e.g. using
60///   [`MockChain::prove_next_block`].
61///
62/// The mock chain uses the batch and block provers underneath to process pending transactions, so
63/// the generated blocks are realistic and indistinguishable from a real node. The only caveat is
64/// that no real ZK proofs are generated or validated as part of transaction, batch or block
65/// building.
66///
67/// # Examples
68///
69/// ## Executing a simple transaction
70/// ```
71/// # use anyhow::Result;
72/// # use miden_protocol::{
73/// #    account::auth::AuthScheme,
74/// #    asset::{Asset, FungibleAsset},
75/// #    note::NoteType,
76/// # };
77/// # use miden_testing::{Auth, MockChain};
78/// #
79/// # #[tokio::main(flavor = "current_thread")]
80/// # async fn main() -> Result<()> {
81/// // Build a genesis state for a mock chain using a MockChainBuilder.
82/// // --------------------------------------------------------------------------------------------
83///
84/// let mut builder = MockChain::builder();
85///
86/// // Add a recipient wallet with basic authentication.
87/// // Use either ECDSA K256 Keccak (scheme_id: 1) or Falcon512Poseidon2 (scheme_id: 2) auth scheme.
88/// let receiver = builder.add_existing_wallet(Auth::BasicAuth {
89///     auth_scheme: AuthScheme::Falcon512Poseidon2,
90/// })?;
91///
92/// // Add a wallet with assets.
93/// let sender = builder.add_existing_wallet(Auth::IncrNonce)?;
94///
95/// let fungible_asset = FungibleAsset::mock(10).unwrap_fungible();
96/// // Add a P2ID note with a fungible asset to the chain.
97/// let note = builder.add_p2id_note(
98///     sender.id(),
99///     receiver.id(),
100///     &[Asset::from(fungible_asset)],
101///     NoteType::Public,
102/// )?;
103///
104/// let mut mock_chain: MockChain = builder.build()?;
105///
106/// // Create a transaction against the receiver account consuming the note.
107/// // --------------------------------------------------------------------------------------------
108///
109/// let transaction = mock_chain
110///     .build_transaction(receiver.id())
111///     .authenticated_input_note(note.id())
112///     .build()?
113///     .execute()
114///     .await?;
115///
116/// // Add the transaction to the chain state.
117/// // --------------------------------------------------------------------------------------------
118///
119/// // Add the transaction to the mock chain's "mempool" of pending transactions.
120/// mock_chain.add_pending_executed_transaction(&transaction)?;
121///
122/// // Prove the next block to include the transaction in the chain state.
123/// mock_chain.prove_next_block()?;
124///
125/// // The receiver account should now have the asset in its account vault.
126/// assert_eq!(
127///     mock_chain
128///         .committed_account(receiver.id())?
129///         .vault()
130///         .get_balance(fungible_asset.id())?,
131///     fungible_asset.amount()
132/// );
133/// # Ok(())
134/// # }
135/// ```
136///
137/// ## Create mock objects and build a mock transaction
138///
139/// ```
140/// # use anyhow::Result;
141/// # use miden_protocol::{
142/// #    Felt,
143/// #    account::auth::AuthScheme,
144/// #    asset::{Asset, FungibleAsset},
145/// #    note::NoteType
146/// # };
147/// # use miden_testing::{Auth, MockChain};
148/// #
149/// # #[tokio::main(flavor = "current_thread")]
150/// # async fn main() -> Result<()> {
151/// let mut builder = MockChain::builder();
152///
153/// let faucet = builder.create_new_faucet(
154///     Auth::BasicAuth {
155///         auth_scheme: AuthScheme::Falcon512Poseidon2,
156///     },
157///     "USDT",
158///     100_000,
159/// )?;
160/// let asset = Asset::from(FungibleAsset::new(faucet.id(), 10)?);
161///
162/// let sender = builder.create_new_wallet(Auth::BasicAuth {
163///     auth_scheme: AuthScheme::Falcon512Poseidon2,
164/// })?;
165/// let target = builder.create_new_wallet(Auth::BasicAuth {
166///     auth_scheme: AuthScheme::Falcon512Poseidon2,
167/// })?;
168///
169/// let note = builder.add_p2id_note(faucet.id(), target.id(), &[asset], NoteType::Public)?;
170///
171/// let mock_chain = builder.build()?;
172///
173/// // The target account is a new account so we move it into the transaction builder, since the
174/// // chain's committed accounts do not yet contain it.
175/// let mock_tx = mock_chain
176///     .build_transaction(target)
177///     .authenticated_input_note(note.id())
178///     .build()?;
179/// let executed_transaction = mock_tx.execute().await?;
180/// # Ok(())
181/// # }
182/// ```
183#[derive(Debug, Clone)]
184pub struct MockChain {
185    /// An append-only structure used to represent the history of blocks produced for this chain.
186    chain: Blockchain,
187
188    /// History of produced blocks.
189    blocks: Vec<ProvenBlock>,
190
191    /// Tree containing all nullifiers.
192    nullifier_tree: NullifierTree,
193
194    /// Tree containing the state commitments of all accounts.
195    account_tree: AccountTree,
196
197    /// Transactions that have been submitted to the chain but have not yet been included in a
198    /// block.
199    pending_transactions: Vec<ProvenTransaction>,
200
201    /// Batches that have been submitted to the chain but have not yet been included in a block.
202    pending_batches: Vec<ProvenBatch>,
203
204    /// NoteID |-> MockChainNote mapping to simplify note retrieval.
205    committed_notes: BTreeMap<NoteId, MockChainNote>,
206
207    /// AccountId |-> Account mapping to simplify transaction creation.
208    ///
209    /// The map holds the latest known state for public accounts. Private accounts are represented
210    /// only by their commitments in the [`AccountTree`] and are not retained here.
211    committed_accounts: BTreeMap<AccountId, Account>,
212
213    /// AccountId |-> AccountAuthenticator mapping to store the authenticator for accounts to
214    /// simplify transaction creation.
215    account_authenticators: BTreeMap<AccountId, AccountAuthenticator>,
216
217    /// Validator secret keys used for signing blocks. All of them must sign each block.
218    validator_secret_keys: Vec<SigningKey>,
219
220    /// The chain's protocol configuration, which every block header commits to.
221    protocol_config: ProtocolConfig,
222}
223
224impl MockChain {
225    // CONSTANTS
226    // ----------------------------------------------------------------------------------------
227
228    /// The timestamp of the genesis block of the chain. Chosen as an easily readable number.
229    pub const TIMESTAMP_START_SECS: u32 = 1700000000;
230
231    /// The number of seconds by which a block's timestamp increases over the previous block's
232    /// timestamp, unless overwritten when calling [`Self::prove_next_block_at`].
233    pub const TIMESTAMP_STEP_SECS: u32 = 10;
234
235    // CONSTRUCTORS
236    // ----------------------------------------------------------------------------------------
237
238    /// Creates a new `MockChain` with an empty genesis block.
239    pub fn new() -> Self {
240        Self::builder().build().expect("empty chain should be valid")
241    }
242
243    /// Returns a new, empty [`MockChainBuilder`].
244    pub fn builder() -> MockChainBuilder {
245        MockChainBuilder::new()
246    }
247
248    /// Creates a new `MockChain` with the provided genesis block and account tree.
249    pub(super) fn from_genesis_block(
250        genesis_block: ProvenBlock,
251        account_tree: AccountTree,
252        account_authenticators: BTreeMap<AccountId, AccountAuthenticator>,
253        secret_keys: Vec<SigningKey>,
254        protocol_config: ProtocolConfig,
255        genesis_notes: Vec<Note>,
256    ) -> anyhow::Result<Self> {
257        let mut chain = MockChain {
258            chain: Blockchain::default(),
259            blocks: vec![],
260            nullifier_tree: NullifierTree::default(),
261            account_tree,
262            pending_transactions: Vec::new(),
263            pending_batches: Vec::new(),
264            committed_notes: BTreeMap::new(),
265            committed_accounts: BTreeMap::new(),
266            account_authenticators,
267            validator_secret_keys: secret_keys,
268            protocol_config,
269        };
270
271        // We do not have to apply the tree changes, because the account tree is already initialized
272        // and the nullifier tree is empty at genesis.
273        chain
274            .apply_block(genesis_block)
275            .context("failed to build account from builder")?;
276
277        // Update committed_notes with full note details for genesis notes.
278        // This is needed because apply_block only stores headers for private notes,
279        // but tests need full note details to create input notes.
280        for note in genesis_notes {
281            if let Some(MockChainNote::Private(_, _, _, inclusion_proof)) =
282                chain.committed_notes.get(&note.id())
283            {
284                chain.committed_notes.insert(
285                    note.id(),
286                    MockChainNote::Public(note.clone(), inclusion_proof.clone()),
287                );
288            }
289        }
290
291        debug_assert_eq!(chain.blocks.len(), 1);
292
293        Ok(chain)
294    }
295
296    // PUBLIC ACCESSORS
297    // ----------------------------------------------------------------------------------------
298
299    /// Returns a reference to the current [`Blockchain`].
300    pub fn blockchain(&self) -> &Blockchain {
301        &self.chain
302    }
303
304    /// Returns a [`PartialBlockchain`] instantiated from the current [`Blockchain`] and with
305    /// authentication paths for all all blocks in the chain.
306    pub fn latest_partial_blockchain(&self) -> PartialBlockchain {
307        // We have to exclude the latest block because we need to fetch the state of the chain at
308        // that latest block, which does not include itself.
309        let block_headers =
310            self.blocks.iter().map(|b| b.header()).take(self.blocks.len() - 1).cloned();
311
312        PartialBlockchain::from_blockchain(&self.chain, block_headers)
313            .expect("blockchain should be valid by construction")
314    }
315
316    /// Creates a new [`PartialBlockchain`] with all reference blocks in the given iterator except
317    /// for the latest block header in the chain and returns that latest block header.
318    ///
319    /// The intended use for the latest block header is to become the reference block of a new
320    /// transaction batch or block.
321    pub fn latest_selective_partial_blockchain(
322        &self,
323        reference_blocks: impl IntoIterator<Item = BlockNumber>,
324    ) -> anyhow::Result<(BlockHeader, PartialBlockchain)> {
325        let latest_block_header = self.latest_block_header();
326
327        self.selective_partial_blockchain(latest_block_header.block_num(), reference_blocks)
328    }
329
330    /// Creates a new [`PartialBlockchain`] with all reference blocks in the given iterator except
331    /// for the reference block header in the chain and returns that reference block header.
332    ///
333    /// The intended use for the reference block header is to become the reference block of a new
334    /// transaction batch or block.
335    pub fn selective_partial_blockchain(
336        &self,
337        reference_block: BlockNumber,
338        reference_blocks: impl IntoIterator<Item = BlockNumber>,
339    ) -> anyhow::Result<(BlockHeader, PartialBlockchain)> {
340        let reference_block_header = self.block_header(reference_block.as_usize());
341        // Deduplicate block numbers so each header will be included just once. This is required so
342        // PartialBlockchain::from_blockchain does not panic.
343        let reference_blocks: BTreeSet<_> = reference_blocks.into_iter().collect();
344
345        // Include all block headers except the reference block itself.
346        let mut block_headers = Vec::new();
347
348        for block_ref_num in &reference_blocks {
349            let block_index = block_ref_num.as_usize();
350            let block = self
351                .blocks
352                .get(block_index)
353                .ok_or_else(|| anyhow::anyhow!("block {} not found in chain", block_ref_num))?;
354            let block_header = block.header().clone();
355            // Exclude the reference block header.
356            if block_header.commitment() != reference_block_header.commitment() {
357                block_headers.push(block_header);
358            }
359        }
360
361        let partial_blockchain =
362            PartialBlockchain::from_blockchain_at(&self.chain, reference_block, block_headers)?;
363
364        Ok((reference_block_header, partial_blockchain))
365    }
366
367    /// Returns a map of [`AccountWitness`]es for the requested account IDs from the current
368    /// [`AccountTree`] in the chain.
369    pub fn account_witnesses(
370        &self,
371        account_ids: impl IntoIterator<Item = AccountId>,
372    ) -> BTreeMap<AccountId, AccountWitness> {
373        let mut account_witnesses = BTreeMap::new();
374
375        for account_id in account_ids {
376            let witness = self.account_tree.open(account_id);
377            account_witnesses.insert(account_id, witness);
378        }
379
380        account_witnesses
381    }
382
383    /// Returns a map of [`NullifierWitness`]es for the requested nullifiers from the current
384    /// [`NullifierTree`] in the chain.
385    pub fn nullifier_witnesses(
386        &self,
387        nullifiers: impl IntoIterator<Item = Nullifier>,
388    ) -> BTreeMap<Nullifier, NullifierWitness> {
389        let mut nullifier_proofs = BTreeMap::new();
390
391        for nullifier in nullifiers {
392            let witness = self.nullifier_tree.open(&nullifier);
393            nullifier_proofs.insert(nullifier, witness);
394        }
395
396        nullifier_proofs
397    }
398
399    /// Returns all note inclusion proofs for the requested note IDs, **if they are available for
400    /// consumption**. Therefore, not all of the requested notes will be guaranteed to have an entry
401    /// in the returned map.
402    pub fn unauthenticated_note_proofs(
403        &self,
404        notes: impl IntoIterator<Item = NoteId>,
405    ) -> BTreeMap<NoteId, NoteInclusionProof> {
406        let mut proofs = BTreeMap::default();
407        for note in notes {
408            if let Some(input_note) = self.committed_notes.get(&note) {
409                proofs.insert(note, input_note.inclusion_proof().clone());
410            }
411        }
412
413        proofs
414    }
415
416    /// Returns the genesis [`BlockHeader`] of the chain.
417    pub fn genesis_block_header(&self) -> BlockHeader {
418        self.block_header(BlockNumber::GENESIS.as_usize())
419    }
420
421    /// Returns the latest [`BlockHeader`] in the chain.
422    pub fn latest_block_header(&self) -> BlockHeader {
423        let chain_tip =
424            self.chain.chain_tip().expect("chain should contain at least the genesis block");
425        self.blocks[chain_tip.as_usize()].header().clone()
426    }
427
428    /// Returns the validator configuration that signs the next block produced by this chain.
429    pub fn validator_config(&self) -> ValidatorConfig {
430        ValidatorConfig::from_signers(&self.validator_secret_keys)
431    }
432
433    /// Signs `commitment` with every validator secret key, ordering the resulting signatures to
434    /// align positionally with [`Self::validator_config`].
435    fn sign_block(&self, commitment: Word) -> BlockSignatures {
436        let signatures = self
437            .validator_config()
438            .keys()
439            .iter()
440            .map(|key| {
441                let signer = self
442                    .validator_secret_keys
443                    .iter()
444                    .find(|sk| &sk.public_key() == key)
445                    .expect("a signer should exist for every validator key");
446                signer.sign(commitment)
447            })
448            .collect();
449        BlockSignatures::new(signatures).expect("signature count same as validator key count")
450    }
451
452    /// Returns the latest [`ProvenBlock`] in the chain.
453    pub fn latest_block(&self) -> ProvenBlock {
454        let chain_tip =
455            self.chain.chain_tip().expect("chain should contain at least the genesis block");
456        self.blocks[chain_tip.as_usize()].clone()
457    }
458
459    /// Returns the [`BlockHeader`] with the specified `block_number`.
460    ///
461    /// # Panics
462    ///
463    /// - If the block number does not exist in the chain.
464    pub fn block_header(&self, block_number: usize) -> BlockHeader {
465        self.blocks[block_number].header().clone()
466    }
467
468    /// Returns a reference to slice of all created proven blocks.
469    pub fn proven_blocks(&self) -> &[ProvenBlock] {
470        &self.blocks
471    }
472
473    /// Returns the chain's [`ProtocolConfig`], which every block header commits to.
474    pub fn protocol_config(&self) -> &ProtocolConfig {
475        &self.protocol_config
476    }
477
478    /// Returns the [`AccountId`] of the faucet whose assets are accepted for fee payments in the
479    /// transaction kernel, or in other words, the fee faucet of the blockchain.
480    pub fn fee_faucet_id(&self) -> AccountId {
481        self.protocol_config.fee_asset_id().faucet_id()
482    }
483
484    /// Returns a reference to the nullifier tree.
485    pub fn nullifier_tree(&self) -> &NullifierTree {
486        &self.nullifier_tree
487    }
488
489    /// Returns the map of note IDs to committed notes.
490    ///
491    /// These notes are committed for authenticated consumption.
492    pub fn committed_notes(&self) -> &BTreeMap<NoteId, MockChainNote> {
493        &self.committed_notes
494    }
495
496    /// Returns `true` if a note with the given ID is recorded in committed notes.
497    pub fn is_note_committed(&self, note_id: &NoteId) -> bool {
498        self.committed_notes.contains_key(note_id)
499    }
500
501    /// Returns `true` if the nullifier has been recorded on-chain (note was consumed).
502    pub fn is_note_consumed(&self, nullifier: &Nullifier) -> bool {
503        self.nullifier_tree.get_block_num(nullifier).is_some()
504    }
505
506    /// Returns `true` if the nullifier is not yet on-chain.
507    ///
508    /// A nullifier can be unspent without the chain having seen the underlying note. Pair with
509    /// [`Self::is_note_committed`] when both conditions matter.
510    pub fn is_note_unspent(&self, nullifier: &Nullifier) -> bool {
511        !self.is_note_consumed(nullifier)
512    }
513
514    /// Returns an [`InputNote`] for the given note ID. If the note does not exist or is not
515    /// public, `None` is returned.
516    pub fn get_public_note(&self, note_id: &NoteId) -> Option<InputNote> {
517        let note = self.committed_notes.get(note_id)?;
518        note.clone().try_into().ok()
519    }
520
521    /// Returns a reference to the public account identified by the given account ID.
522    ///
523    /// The account is retrieved with the latest state known to the [`MockChain`]. Private account
524    /// states are not retained by the chain.
525    pub fn committed_account(&self, account_id: AccountId) -> anyhow::Result<&Account> {
526        self.committed_accounts
527            .get(&account_id)
528            .with_context(|| format!("account {account_id} not found in committed accounts"))
529    }
530
531    /// Returns a reference to the [`AccountTree`] of the chain.
532    pub fn account_tree(&self) -> &AccountTree {
533        &self.account_tree
534    }
535
536    // BATCH APIS
537    // ----------------------------------------------------------------------------------------
538
539    /// Proposes a new transaction batch from the provided transactions and returns it.
540    ///
541    /// This method does not modify the chain state.
542    pub fn propose_transaction_batch<I>(
543        &self,
544        txs: impl IntoIterator<Item = ProvenTransaction, IntoIter = I>,
545    ) -> anyhow::Result<ProposedBatch>
546    where
547        I: Iterator<Item = ProvenTransaction> + Clone,
548    {
549        let transactions: Vec<_> = txs.into_iter().map(alloc::sync::Arc::new).collect();
550
551        let (batch_reference_block, partial_blockchain, unauthenticated_note_proofs) = self
552            .get_batch_inputs(
553                transactions.iter().map(|tx| tx.ref_block_num()),
554                transactions
555                    .iter()
556                    .flat_map(|tx| tx.unauthenticated_notes().map(NoteHeader::id)),
557            )?;
558
559        Ok(ProposedBatch::new_unverified(
560            transactions,
561            batch_reference_block,
562            partial_blockchain,
563            unauthenticated_note_proofs,
564        )?)
565    }
566
567    /// Mock-proves a proposed transaction batch from the provided [`ProposedBatch`] and returns it.
568    ///
569    /// This method does not modify the chain state.
570    pub fn prove_transaction_batch(
571        &self,
572        proposed_batch: ProposedBatch,
573    ) -> anyhow::Result<ProvenBatch> {
574        let batch_prover = LocalBatchProver::default();
575        Ok(batch_prover.prove_dummy(proposed_batch)?)
576    }
577
578    // BLOCK APIS
579    // ----------------------------------------------------------------------------------------
580
581    /// Proposes a new block from the provided batches with the given timestamp and returns it.
582    ///
583    /// This method does not modify the chain state.
584    pub fn propose_block_at<I>(
585        &self,
586        batches: impl IntoIterator<Item = ProvenBatch, IntoIter = I>,
587        timestamp: u32,
588    ) -> anyhow::Result<ProposedBlock>
589    where
590        I: Iterator<Item = ProvenBatch> + Clone,
591    {
592        let batches: Vec<_> = batches.into_iter().collect();
593
594        let block_inputs = self
595            .get_block_inputs(batches.iter())
596            .context("could not retrieve block inputs")?;
597
598        let proposed_block = ProposedBlock::new_at(block_inputs, batches, timestamp)
599            .context("failed to create proposed block")?;
600
601        Ok(proposed_block)
602    }
603
604    /// Proposes a new block from the provided batches and returns it.
605    ///
606    /// This method does not modify the chain state.
607    pub fn propose_block<I>(
608        &self,
609        batches: impl IntoIterator<Item = ProvenBatch, IntoIter = I>,
610    ) -> anyhow::Result<ProposedBlock>
611    where
612        I: Iterator<Item = ProvenBatch> + Clone,
613    {
614        // We can't access system time because we are in a no-std environment, so we use the
615        // minimally correct next timestamp.
616        let timestamp = self.latest_block_header().timestamp() + 1;
617
618        self.propose_block_at(batches, timestamp)
619    }
620
621    // TRANSACTION APIS
622    // ----------------------------------------------------------------------------------------
623
624    /// Returns a [`MockTransactionBuilder`] for executing a transaction against this chain.
625    ///
626    /// This is the public entry point for creating and executing transactions against a concrete
627    /// [`MockChain`]. Input notes are added explicitly on the returned builder, and the transaction
628    /// inputs are only resolved against the chain once all input notes are known. See
629    /// [`MockTransactionBuilder`] for details.
630    ///
631    /// Depending on the provided `input`, the builder is initialized differently:
632    /// - [`MockTransactionInput::AccountId`]: The transaction inputs are resolved against the
633    ///   public account committed to the chain under that ID.
634    /// - [`MockTransactionInput::Account`]: The account is passed as-is to the transaction inputs.
635    ///   This can be used to build a chain of transactions against the same account that build on
636    ///   top of each other. For example, transaction A modifies an account from state 0 to 1, and
637    ///   transaction B modifies it from state 1 to 2.
638    ///
639    /// In both cases, if the chain holds an authenticator for the account, it is set on the
640    /// builder.
641    pub fn build_transaction(
642        &self,
643        input: impl Into<MockTransactionInput>,
644    ) -> MockTransactionBuilder<'_> {
645        MockTransactionBuilder::new(self, input)
646    }
647
648    /// Resolves the account referenced by `input` into a concrete [`Account`].
649    ///
650    /// For [`MockTransactionInput::AccountId`], the public account committed to the chain is
651    /// returned. For [`MockTransactionInput::Account`], the account is returned as-is.
652    pub(crate) fn resolve_tx_account(
653        &self,
654        input: MockTransactionInput,
655    ) -> anyhow::Result<Account> {
656        match input {
657            MockTransactionInput::AccountId(account_id) => {
658                anyhow::ensure!(
659                    !account_id.is_private(),
660                    "mock transactions for private accounts should be created with MockTransactionInput::Account"
661                );
662
663                self.committed_account(account_id).cloned()
664            },
665            MockTransactionInput::Account(account) => Ok(account),
666        }
667    }
668
669    /// Returns the authenticator the chain holds for the given account, if any.
670    pub(crate) fn account_authenticator(
671        &self,
672        account_id: AccountId,
673    ) -> Option<BasicAuthenticator> {
674        self.account_authenticators
675            .get(&account_id)
676            .and_then(|authenticator| authenticator.authenticator().cloned())
677    }
678
679    // INPUTS APIS
680    // ----------------------------------------------------------------------------------------
681
682    /// Returns a valid [`TransactionInputs`] for the specified entities, executing against
683    /// a specific block number.
684    ///
685    /// The returned partial blockchain tracks the blocks the input notes were created in, plus the
686    /// blocks in `required_blocks`. The latter are for blocks whose commitment the executed code
687    /// reads without an input note requiring them, e.g. the older block a multisig transaction
688    /// summary binds. Blocks at or after the reference block are ignored.
689    pub fn get_transaction_inputs_at(
690        &self,
691        reference_block: BlockNumber,
692        account: impl Into<PartialAccount>,
693        notes: &[NoteId],
694        unauthenticated_notes: &[Note],
695        required_blocks: impl IntoIterator<Item = BlockNumber>,
696    ) -> anyhow::Result<TransactionInputs> {
697        let ref_block = self.block_header(reference_block.as_usize());
698
699        let mut input_notes = vec![];
700        let mut block_headers_map: BTreeMap<BlockNumber, BlockHeader> = BTreeMap::new();
701
702        for block_num in required_blocks {
703            if block_num < ref_block.block_num() {
704                let block_header = self
705                    .blocks
706                    .get(block_num.as_usize())
707                    .with_context(|| format!("block {block_num} not found in chain"))?
708                    .header()
709                    .clone();
710                block_headers_map.insert(block_num, block_header);
711            }
712        }
713
714        for note in notes {
715            let input_note: InputNote = self
716                .committed_notes
717                .get(note)
718                .with_context(|| format!("note with id {note} not found"))?
719                .clone()
720                .try_into()
721                .with_context(|| {
722                    format!("failed to convert mock chain note with id {note} into input note")
723                })?;
724
725            let note_block_num = input_note
726                .location()
727                .with_context(|| format!("note location not available: {note}"))?
728                .block_num();
729
730            if note_block_num > ref_block.block_num() {
731                anyhow::bail!(
732                    "note with ID {note} was created in block {note_block_num} which is larger than the reference block number {}",
733                    ref_block.block_num()
734                )
735            }
736
737            if note_block_num != ref_block.block_num() {
738                let block_header = self
739                    .blocks
740                    .get(note_block_num.as_usize())
741                    .with_context(|| format!("block {note_block_num} not found in chain"))?
742                    .header()
743                    .clone();
744                block_headers_map.insert(note_block_num, block_header);
745            }
746
747            input_notes.push(input_note);
748        }
749
750        for note in unauthenticated_notes {
751            input_notes.push(InputNote::Unauthenticated { note: note.clone() })
752        }
753
754        let block_headers = block_headers_map.values();
755        let (_, partial_blockchain) = self.selective_partial_blockchain(
756            reference_block,
757            block_headers.map(BlockHeader::block_num),
758        )?;
759
760        let input_notes = InputNotes::new(input_notes)?;
761
762        Ok(TransactionInputs::new(
763            account.into(),
764            ref_block.clone(),
765            self.protocol_config.clone(),
766            partial_blockchain,
767            input_notes,
768        )?)
769    }
770
771    /// Returns a valid [`TransactionInputs`] for the specified entities.
772    pub fn get_transaction_inputs(
773        &self,
774        account: impl Into<PartialAccount>,
775        notes: &[NoteId],
776        unauthenticated_notes: &[Note],
777    ) -> anyhow::Result<TransactionInputs> {
778        let latest_block_num = self.latest_block_header().block_num();
779        self.get_transaction_inputs_at(latest_block_num, account, notes, unauthenticated_notes, [])
780    }
781
782    /// Returns inputs for a transaction batch for all the reference blocks of the provided
783    /// transactions.
784    pub fn get_batch_inputs(
785        &self,
786        tx_reference_blocks: impl IntoIterator<Item = BlockNumber>,
787        unauthenticated_notes: impl Iterator<Item = NoteId>,
788    ) -> anyhow::Result<(BlockHeader, PartialBlockchain, BTreeMap<NoteId, NoteInclusionProof>)>
789    {
790        // Fetch note proofs for notes that exist in the chain.
791        let unauthenticated_note_proofs = self.unauthenticated_note_proofs(unauthenticated_notes);
792
793        // We also need to fetch block inclusion proofs for any of the blocks that contain
794        // unauthenticated notes for which we want to prove inclusion.
795        let required_blocks = tx_reference_blocks.into_iter().chain(
796            unauthenticated_note_proofs
797                .values()
798                .map(|note_proof| note_proof.location().block_num()),
799        );
800
801        let (batch_reference_block, partial_block_chain) =
802            self.latest_selective_partial_blockchain(required_blocks)?;
803
804        Ok((batch_reference_block, partial_block_chain, unauthenticated_note_proofs))
805    }
806
807    /// Gets foreign account inputs to execute FPI transactions.
808    ///
809    /// Pass an [`AccountId`] to resolve a public account from the chain, or pass an [`Account`]
810    /// directly for a private account whose state is retained by the caller.
811    pub fn get_foreign_account_inputs(
812        &self,
813        input: impl Into<MockTransactionInput>,
814    ) -> anyhow::Result<(Account, AccountWitness)> {
815        let account = self.resolve_tx_account(input.into())?;
816        let account_id = account.id();
817
818        let account_witness = self.account_tree().open(account_id);
819        anyhow::ensure!(
820            account_witness.state_commitment() == account.to_commitment(),
821            "account {account_id} does not match its witness"
822        );
823
824        Ok((account, account_witness))
825    }
826
827    /// Gets the inputs for a block for the provided batches.
828    pub fn get_block_inputs<'batch, I>(
829        &self,
830        batch_iter: impl IntoIterator<Item = &'batch ProvenBatch, IntoIter = I>,
831    ) -> anyhow::Result<BlockInputs>
832    where
833        I: Iterator<Item = &'batch ProvenBatch> + Clone,
834    {
835        let batch_iterator = batch_iter.into_iter();
836
837        let unauthenticated_note_proofs =
838            self.unauthenticated_note_proofs(batch_iterator.clone().flat_map(|batch| {
839                batch.input_notes().iter().filter_map(|note| note.header().map(NoteHeader::id))
840            }));
841
842        let (block_reference_block, partial_blockchain) = self
843            .latest_selective_partial_blockchain(
844                batch_iterator.clone().map(ProvenBatch::reference_block_num).chain(
845                    unauthenticated_note_proofs.values().map(|proof| proof.location().block_num()),
846                ),
847            )?;
848
849        let account_witnesses =
850            self.account_witnesses(batch_iterator.clone().flat_map(ProvenBatch::updated_accounts));
851
852        let nullifier_proofs =
853            self.nullifier_witnesses(batch_iterator.flat_map(ProvenBatch::created_nullifiers));
854
855        Ok(BlockInputs::new(
856            block_reference_block,
857            partial_blockchain,
858            account_witnesses,
859            nullifier_proofs,
860            unauthenticated_note_proofs,
861        ))
862    }
863
864    // PUBLIC MUTATORS
865    // ----------------------------------------------------------------------------------------
866
867    /// Proves the next block in the mock chain.
868    ///
869    /// This will commit all the currently pending transactions into the chain state.
870    pub fn prove_next_block(&mut self) -> anyhow::Result<ProvenBlock> {
871        self.prove_and_apply_block(None, None)
872    }
873
874    /// Proves the next block in the mock chain, rotating the validator key set.
875    ///
876    /// The produced block is still signed by the current validator keys (the ones committed to by
877    /// the previous block) but commits the public keys of `new_secret_keys` as the validator set
878    /// authorized to sign the *following* block. After this block is applied, the chain signs
879    /// subsequent blocks with `new_secret_keys`.
880    ///
881    /// This commits all currently pending transactions into the chain state.
882    pub fn prove_next_block_with_validator_config_rotation(
883        &mut self,
884        new_secret_keys: Vec<SigningKey>,
885    ) -> anyhow::Result<ProvenBlock> {
886        let next_config = ValidatorConfig::from_signers(&new_secret_keys);
887        let block = self.prove_and_apply_block(None, Some(next_config))?;
888        self.validator_secret_keys = new_secret_keys;
889        Ok(block)
890    }
891
892    /// Proves the next block in the mock chain at the given timestamp.
893    ///
894    /// This will commit all the currently pending transactions into the chain state.
895    pub fn prove_next_block_at(&mut self, timestamp: u32) -> anyhow::Result<ProvenBlock> {
896        self.prove_and_apply_block(Some(timestamp), None)
897    }
898
899    /// Proves new blocks until the block with the given target block number has been created.
900    ///
901    /// For example, if the latest block is `5` and this function is called with `10`, then blocks
902    /// `6..=10` will be created and block 10 will be returned.
903    ///
904    /// # Panics
905    ///
906    /// Panics if:
907    /// - the given block number is smaller or equal to the number of the latest block in the chain.
908    pub fn prove_until_block(
909        &mut self,
910        target_block_num: impl Into<BlockNumber>,
911    ) -> anyhow::Result<ProvenBlock> {
912        let target_block_num = target_block_num.into();
913        let latest_block_num = self.latest_block_header().block_num();
914        assert!(
915            target_block_num > latest_block_num,
916            "target block number must be greater than the number of the latest block in the chain"
917        );
918
919        let mut last_block = None;
920        for _ in latest_block_num.as_usize()..target_block_num.as_usize() {
921            last_block = Some(self.prove_next_block()?);
922        }
923
924        Ok(last_block.expect("at least one block should have been created"))
925    }
926
927    // PUBLIC MUTATORS (PENDING APIS)
928    // ----------------------------------------------------------------------------------------
929
930    /// Adds the given [`ExecutedTransaction`] to the list of pending transactions.
931    ///
932    /// A block has to be created to apply the transaction effects to the chain state, e.g. using
933    /// [`MockChain::prove_next_block`].
934    pub fn add_pending_executed_transaction(
935        &mut self,
936        transaction: &ExecutedTransaction,
937    ) -> anyhow::Result<()> {
938        // Transform the executed tx into a proven tx with a dummy proof.
939        let proven_tx = LocalTransactionProver::default()
940            .prove_dummy(transaction.clone())
941            .context("failed to dummy-prove executed transaction into proven transaction")?;
942
943        self.pending_transactions.push(proven_tx);
944
945        Ok(())
946    }
947
948    /// Adds the given [`ProvenTransaction`] to the list of pending transactions.
949    ///
950    /// A block has to be created to apply the transaction effects to the chain state, e.g. using
951    /// [`MockChain::prove_next_block`].
952    pub fn add_pending_proven_transaction(&mut self, transaction: ProvenTransaction) {
953        self.pending_transactions.push(transaction);
954    }
955
956    /// Adds the given [`ProvenBatch`] to the list of pending batches.
957    ///
958    /// A block has to be created to apply the batch effects to the chain state, e.g. using
959    /// [`MockChain::prove_next_block`].
960    pub fn add_pending_batch(&mut self, batch: ProvenBatch) {
961        self.pending_batches.push(batch);
962    }
963
964    // PRIVATE HELPERS
965    // ----------------------------------------------------------------------------------------
966
967    /// Applies the given block to the chain state, which means:
968    ///
969    /// - Insert account and nullifiers into the respective trees.
970    /// - Updated accounts from the block are updated in the committed accounts.
971    /// - Created notes are inserted into the committed notes.
972    /// - Consumed notes are removed from the committed notes.
973    /// - The block is appended to the [`BlockChain`] and the list of proven blocks.
974    fn apply_block(&mut self, proven_block: ProvenBlock) -> anyhow::Result<()> {
975        // Verify the block is correctly linked to and authorized by its parent. Genesis is the
976        // trust root and has no parent to anchor against, so it is skipped.
977        if proven_block.header().block_num() != BlockNumber::GENESIS {
978            let parent = self.latest_block_header();
979            proven_block
980                .validate(Some(&parent))
981                .context("block failed validation against its parent")?;
982        }
983
984        for account_update in proven_block.body().updated_accounts() {
985            self.account_tree
986                .insert(account_update.account_id(), account_update.final_state_commitment())
987                .context("failed to insert account update into account tree")?;
988        }
989
990        for nullifier in proven_block.body().created_nullifiers() {
991            self.nullifier_tree
992                .mark_spent(*nullifier, proven_block.header().block_num())
993                .context("failed to mark block nullifier as spent")?;
994
995            // TODO: Remove from self.committed_notes. This is not critical to have for now. It is
996            // not straightforward, because committed_notes are indexed by note IDs rather than
997            // nullifiers, so we'll have to create a second index to do this.
998        }
999
1000        for account_update in proven_block.body().updated_accounts() {
1001            match account_update.details() {
1002                AccountUpdateDetails::Public(account_patch) => {
1003                    // The mock chain holds every committed public account, so a patch for an
1004                    // unknown account must create it.
1005                    match self.committed_accounts.get_mut(&account_update.account_id()) {
1006                        Some(committed_account) => committed_account
1007                            .apply_patch(account_patch)
1008                            .context("failed to apply account patch")?,
1009                        None => {
1010                            let account = account_patch
1011                                .try_to_new_account()
1012                                .context("failed to convert creation patch into account")?;
1013                            self.committed_accounts.insert(account.id(), account);
1014                        },
1015                    }
1016                },
1017                // No state to keep for private accounts other than the commitment on the account
1018                // tree
1019                AccountUpdateDetails::Private => {},
1020            }
1021        }
1022
1023        let notes_tree = proven_block.body().compute_block_note_tree();
1024        for (block_note_index, created_note) in proven_block.body().output_notes() {
1025            let note_path = notes_tree.open(block_note_index);
1026            let note_inclusion_proof = NoteInclusionProof::new(
1027                proven_block.header().block_num(),
1028                block_note_index.leaf_index_value(),
1029                note_path,
1030            )
1031            .context("failed to create inclusion proof for output note")?;
1032
1033            match created_note {
1034                OutputNote::Public(public_note) => {
1035                    self.committed_notes.insert(
1036                        public_note.id(),
1037                        MockChainNote::Public(public_note.as_note().clone(), note_inclusion_proof),
1038                    );
1039                },
1040                OutputNote::Private(private_note) => {
1041                    self.committed_notes.insert(
1042                        private_note.id(),
1043                        MockChainNote::Private(
1044                            private_note.id(),
1045                            *private_note.metadata(),
1046                            private_note.attachments().clone(),
1047                            note_inclusion_proof,
1048                        ),
1049                    );
1050                },
1051            }
1052        }
1053
1054        debug_assert_eq!(
1055            self.chain.commitment(),
1056            proven_block.header().chain_commitment(),
1057            "current mock chain commitment and new block's chain commitment should match"
1058        );
1059        debug_assert_eq!(
1060            BlockNumber::from(self.chain.as_mmr().forest().num_leaves() as u32),
1061            proven_block.header().block_num(),
1062            "current mock chain length and new block's number should match"
1063        );
1064
1065        self.chain.push(proven_block.header().commitment());
1066        self.blocks.push(proven_block);
1067
1068        Ok(())
1069    }
1070
1071    fn pending_transactions_to_batches(&mut self) -> anyhow::Result<Vec<ProvenBatch>> {
1072        // Batches must contain at least one transaction, so if there are no pending transactions,
1073        // return early.
1074        if self.pending_transactions.is_empty() {
1075            return Ok(vec![]);
1076        }
1077
1078        let pending_transactions = core::mem::take(&mut self.pending_transactions);
1079
1080        // TODO: Distribute the transactions into multiple batches if the transactions would not fit
1081        // into a single batch (according to max input notes, max output notes and max accounts).
1082        let proposed_batch = self.propose_transaction_batch(pending_transactions)?;
1083        let proven_batch = self.prove_transaction_batch(proposed_batch)?;
1084
1085        Ok(vec![proven_batch])
1086    }
1087
1088    /// Creates a new block in the mock chain.
1089    ///
1090    /// Block building is divided into two steps:
1091    ///
1092    /// 1. Build batches from pending transactions and a block from those batches. This results in a
1093    ///    block.
1094    /// 2. Insert all the account updates, nullifiers and notes from the block into the chain state.
1095    ///
1096    /// If a `timestamp` is provided, it will be set on the block.
1097    fn prove_and_apply_block(
1098        &mut self,
1099        timestamp: Option<u32>,
1100        next_validator_config: Option<ValidatorConfig>,
1101    ) -> anyhow::Result<ProvenBlock> {
1102        // Create batches from pending transactions.
1103        // ----------------------------------------------------------------------------------------
1104
1105        let mut batches = self.pending_transactions_to_batches()?;
1106        batches.extend(core::mem::take(&mut self.pending_batches));
1107
1108        // Create block.
1109        // ----------------------------------------------------------------------------------------
1110
1111        let block_timestamp =
1112            timestamp.unwrap_or(self.latest_block_header().timestamp() + Self::TIMESTAMP_STEP_SECS);
1113
1114        let mut proposed_block = self
1115            .propose_block_at(batches.clone(), block_timestamp)
1116            .context("failed to create proposed block")?;
1117
1118        // Commit to a rotated validator key set for the next block, if requested.
1119        if let Some(next_validator_config) = next_validator_config {
1120            proposed_block = proposed_block.with_next_validator_config(next_validator_config);
1121        }
1122
1123        let proven_block = self.prove_block(proposed_block.clone())?;
1124
1125        // Apply block.
1126        // ----------------------------------------------------------------------------------------
1127
1128        self.apply_block(proven_block.clone()).context("failed to apply block")?;
1129
1130        Ok(proven_block)
1131    }
1132
1133    /// Proves proposed block alongside a corresponding list of batches.
1134    pub fn prove_block(&self, proposed_block: ProposedBlock) -> anyhow::Result<ProvenBlock> {
1135        let (header, body) = proposed_block.into_header_and_body()?;
1136        let block_proof = LocalBlockProver::default().prove_dummy();
1137        let signatures = self.sign_block(header.commitment());
1138        Ok(ProvenBlock::new_unchecked(header, body, signatures, block_proof))
1139    }
1140}
1141
1142impl Default for MockChain {
1143    fn default() -> Self {
1144        MockChain::new()
1145    }
1146}
1147
1148// SERIALIZATION
1149// ================================================================================================
1150
1151fn read_blocks_with_unchecked_genesis<R: ByteReader>(
1152    source: &mut R,
1153) -> Result<Vec<ProvenBlock>, DeserializationError> {
1154    let block_count = source.read_usize()?;
1155    if block_count == 0 {
1156        return Ok(Vec::new());
1157    }
1158
1159    let mut blocks = vec![read_genesis_block_unchecked(source)?];
1160    let remaining_blocks = source
1161        .read_many_iter(block_count - 1)?
1162        .collect::<Result<Vec<ProvenBlock>, _>>()?;
1163    blocks.extend(remaining_blocks);
1164
1165    Ok(blocks)
1166}
1167
1168fn read_genesis_block_unchecked<R: ByteReader>(
1169    source: &mut R,
1170) -> Result<ProvenBlock, DeserializationError> {
1171    let header = BlockHeader::read_from(source)?;
1172    if header.block_num() != BlockNumber::GENESIS {
1173        return Err(DeserializationError::InvalidValue(format!(
1174            "first mock chain block must be genesis, got block {}",
1175            header.block_num()
1176        )));
1177    }
1178
1179    let body = BlockBody::new_unchecked(
1180        Vec::<BlockAccountUpdate>::read_from(source)?,
1181        Vec::<OutputNoteBatch>::read_from(source)?,
1182        Vec::<Nullifier>::read_from(source)?,
1183        OrderedTransactionHeaders::read_from(source)?,
1184    );
1185    let signatures = BlockSignatures::read_from(source)?;
1186    let proof = ExecutionProof::read_from(source)?;
1187
1188    Ok(ProvenBlock::new_unchecked(header, body, signatures, proof))
1189}
1190
1191impl Serializable for MockChain {
1192    fn write_into<W: ByteWriter>(&self, target: &mut W) {
1193        self.chain.write_into(target);
1194        self.blocks.write_into(target);
1195        self.nullifier_tree.write_into(target);
1196        self.account_tree.write_into(target);
1197        self.pending_transactions.write_into(target);
1198        self.committed_accounts.write_into(target);
1199        self.committed_notes.write_into(target);
1200        self.account_authenticators.write_into(target);
1201        self.validator_secret_keys.write_into(target);
1202        self.protocol_config.write_into(target);
1203    }
1204}
1205
1206impl Deserializable for MockChain {
1207    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
1208        let chain = Blockchain::read_from(source)?;
1209        let blocks = read_blocks_with_unchecked_genesis(source)?;
1210        let nullifier_tree = NullifierTree::read_from(source)?;
1211        let account_tree = AccountTree::read_from(source)?;
1212        let pending_transactions = Vec::<ProvenTransaction>::read_from(source)?;
1213        let committed_accounts = BTreeMap::<AccountId, Account>::read_from(source)?;
1214        let committed_notes = BTreeMap::<NoteId, MockChainNote>::read_from(source)?;
1215        let account_authenticators =
1216            BTreeMap::<AccountId, AccountAuthenticator>::read_from(source)?;
1217        let secret_keys = Vec::<SigningKey>::read_from(source)?;
1218        let protocol_config = ProtocolConfig::read_from(source)?;
1219
1220        Ok(Self {
1221            chain,
1222            blocks,
1223            nullifier_tree,
1224            account_tree,
1225            pending_transactions,
1226            pending_batches: Vec::new(),
1227            committed_notes,
1228            committed_accounts,
1229            account_authenticators,
1230            validator_secret_keys: secret_keys,
1231            protocol_config,
1232        })
1233    }
1234}
1235
1236// ACCOUNT STATE
1237// ================================================================================================
1238
1239/// Helper type for increased readability at call-sites. Indicates whether to build a new (nonce =
1240/// ZERO) or existing account (nonce = ONE).
1241pub enum AccountState {
1242    New,
1243    Exists,
1244}
1245
1246// ACCOUNT AUTHENTICATOR
1247// ================================================================================================
1248
1249/// A wrapper around the authenticator of an account.
1250#[derive(Debug, Clone)]
1251pub(super) struct AccountAuthenticator {
1252    authenticator: Option<BasicAuthenticator>,
1253}
1254
1255impl AccountAuthenticator {
1256    pub fn new(authenticator: Option<BasicAuthenticator>) -> Self {
1257        Self { authenticator }
1258    }
1259
1260    pub fn authenticator(&self) -> Option<&BasicAuthenticator> {
1261        self.authenticator.as_ref()
1262    }
1263}
1264
1265impl PartialEq for AccountAuthenticator {
1266    fn eq(&self, other: &Self) -> bool {
1267        match (&self.authenticator, &other.authenticator) {
1268            (Some(a), Some(b)) => {
1269                a.keys().keys().zip(b.keys().keys()).all(|(a_key, b_key)| a_key == b_key)
1270            },
1271            (None, None) => true,
1272            _ => false,
1273        }
1274    }
1275}
1276
1277// SERIALIZATION
1278// ================================================================================================
1279
1280impl Serializable for AccountAuthenticator {
1281    fn write_into<W: ByteWriter>(&self, target: &mut W) {
1282        self.authenticator
1283            .as_ref()
1284            .map(|auth| {
1285                auth.keys()
1286                    .values()
1287                    .map(|(secret_key, public_key)| (secret_key, public_key.as_ref().clone()))
1288                    .collect::<Vec<_>>()
1289            })
1290            .write_into(target);
1291    }
1292}
1293
1294impl Deserializable for AccountAuthenticator {
1295    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
1296        let authenticator = Option::<Vec<(AuthSecretKey, PublicKey)>>::read_from(source)?;
1297
1298        let authenticator = authenticator.map(|keys| BasicAuthenticator::from_key_pairs(&keys));
1299
1300        Ok(Self { authenticator })
1301    }
1302}
1303
1304// MOCK TRANSACTION INPUT
1305// ================================================================================================
1306
1307/// Account input accepted by [`MockChain::build_transaction`] and
1308/// [`MockChain::get_foreign_account_inputs`].
1309///
1310/// [`MockTransactionInput::AccountId`] resolves a committed public account from the chain, while
1311/// [`MockTransactionInput::Account`] supplies caller-owned account state directly, including the
1312/// state of a private account.
1313#[allow(clippy::large_enum_variant)]
1314#[derive(Debug, Clone)]
1315pub enum MockTransactionInput {
1316    AccountId(AccountId),
1317    Account(Account),
1318}
1319
1320impl MockTransactionInput {
1321    /// Returns the account ID that this input references.
1322    pub(crate) fn id(&self) -> AccountId {
1323        match self {
1324            MockTransactionInput::AccountId(account_id) => *account_id,
1325            MockTransactionInput::Account(account) => account.id(),
1326        }
1327    }
1328}
1329
1330impl From<AccountId> for MockTransactionInput {
1331    fn from(account: AccountId) -> Self {
1332        Self::AccountId(account)
1333    }
1334}
1335
1336impl From<Account> for MockTransactionInput {
1337    fn from(account: Account) -> Self {
1338        Self::Account(account)
1339    }
1340}
1341
1342// TESTS
1343// ================================================================================================
1344
1345#[cfg(test)]
1346mod tests {
1347    use miden_protocol::account::auth::AuthScheme;
1348    use miden_protocol::account::{AccountBuilder, AccountType};
1349    use miden_protocol::asset::{Asset, FungibleAsset};
1350    use miden_protocol::errors::ValidatorConfigError;
1351    use miden_protocol::note::NoteType;
1352    use miden_protocol::testing::account_id::{
1353        ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
1354        ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
1355        ACCOUNT_ID_SENDER,
1356    };
1357    use miden_protocol::testing::random_secret_key::random_secret_key;
1358    use miden_standards::account::wallets::BasicWallet;
1359    use miden_tx::utils::serde::SliceReader;
1360
1361    use super::*;
1362    use crate::Auth;
1363
1364    #[test]
1365    fn prove_until_block() -> anyhow::Result<()> {
1366        let mut chain = MockChain::new();
1367        let block = chain.prove_until_block(5)?;
1368        assert_eq!(block.header().block_num(), 5u32.into());
1369        assert_eq!(chain.proven_blocks().len(), 6);
1370
1371        Ok(())
1372    }
1373
1374    #[test]
1375    fn genesis_private_account_is_not_retained() -> anyhow::Result<()> {
1376        let account_builder = AccountBuilder::new([5; 32])
1377            .account_type(AccountType::Private)
1378            .with_component(BasicWallet);
1379        let mut builder = MockChain::builder();
1380        let account = builder.add_account_from_builder(
1381            Auth::BasicAuth { auth_scheme: AuthScheme::EcdsaK256Keccak },
1382            account_builder,
1383            AccountState::Exists,
1384        )?;
1385
1386        let chain = builder.build()?;
1387        let update = &chain.blocks[0].body().updated_accounts()[0];
1388
1389        assert!(update.details().is_private());
1390        assert!(chain.committed_account(account.id()).is_err());
1391
1392        Ok(())
1393    }
1394
1395    #[test]
1396    fn unchecked_genesis_deserialization_rejects_non_genesis_block() -> anyhow::Result<()> {
1397        let mut chain = MockChain::new();
1398        let block = chain.prove_next_block()?;
1399        let bytes = vec![block].to_bytes();
1400
1401        let error = read_blocks_with_unchecked_genesis(&mut SliceReader::new(&bytes)).unwrap_err();
1402
1403        assert!(matches!(
1404            error,
1405            DeserializationError::InvalidValue(message)
1406                if message == "first mock chain block must be genesis, got block 1"
1407        ));
1408
1409        Ok(())
1410    }
1411
1412    #[rstest::rstest]
1413    #[case::default(None)]
1414    #[case::supplied(Some(1))]
1415    fn validator_config_rotation_across_blocks(
1416        #[case] key_count: Option<usize>,
1417    ) -> anyhow::Result<()> {
1418        let mut builder = MockChain::builder();
1419        if let Some(key_count) = key_count {
1420            builder = builder
1421                .validator_signing_keys((0..key_count).map(|_| random_secret_key()).collect());
1422        }
1423        let mut chain = builder.build()?;
1424        let original_keys = chain.validator_config();
1425
1426        // Build normal blocks. The parent-linkage and signatures are verified inside `apply_block`,
1427        // so these calls succeeding proves the chain validates against the previous block's keys.
1428        chain.prove_next_block()?;
1429        chain.prove_next_block()?;
1430        assert_eq!(chain.validator_config(), original_keys);
1431
1432        // Rotate to a new, larger validator key set.
1433        let new_signers: Vec<SigningKey> = (0..4).map(|_| random_secret_key()).collect();
1434        let new_keys = ValidatorConfig::from_signers(&new_signers);
1435        let rotation_block = chain.prove_next_block_with_validator_config_rotation(new_signers)?;
1436
1437        // The rotation block is still signed by (and validates against) the original keys, but
1438        // commits the new set as the signer authorized for the next block.
1439        assert_eq!(rotation_block.header().validator_config(), &new_keys);
1440        assert_eq!(chain.validator_config(), new_keys);
1441        rotation_block
1442            .signatures()
1443            .verify_against(rotation_block.header().commitment(), &original_keys)?;
1444
1445        // The next block is signed by the rotated keys and must validate against the rotation
1446        // block's committed set; `apply_block` would error otherwise.
1447        chain.prove_next_block()?;
1448        assert_eq!(chain.validator_config(), new_keys);
1449
1450        Ok(())
1451    }
1452
1453    #[rstest::rstest]
1454    #[case::single(1)]
1455    #[case::multiple(3)]
1456    #[case::maximum(ValidatorConfig::MAX_VALIDATORS)]
1457    fn supplied_validator_signing_keys(#[case] key_count: usize) -> anyhow::Result<()> {
1458        let mut keys: Vec<SigningKey> = (0..key_count).map(|_| random_secret_key()).collect();
1459        // Reverse canonical order to exercise positional signature ordering.
1460        keys.sort_by_key(|key| core::cmp::Reverse(key.public_key().to_bytes()));
1461        let expected_config = ValidatorConfig::from_signers(&keys);
1462        let mut chain = MockChain::builder().validator_signing_keys(keys.clone()).build()?;
1463
1464        assert_eq!(chain.validator_config(), expected_config);
1465        assert_eq!(chain.genesis_block_header().validator_config(), &expected_config);
1466        assert_eq!(expected_config.len(), key_count);
1467        assert_eq!(usize::from(expected_config.quorum()), key_count);
1468
1469        let genesis = chain.latest_block();
1470        genesis
1471            .signatures()
1472            .verify_against(genesis.header().commitment(), &expected_config)?;
1473
1474        // The same keys produce the same genesis header regardless of input order.
1475        keys.reverse();
1476        let reordered = MockChain::builder().validator_signing_keys(keys).build()?;
1477        assert_eq!(chain.genesis_block_header(), reordered.genesis_block_header());
1478
1479        for _ in 0..2 {
1480            let block = chain.prove_next_block()?;
1481            assert_eq!(block.header().validator_config(), &expected_config);
1482            block
1483                .signatures()
1484                .verify_against(block.header().commitment(), &expected_config)?;
1485        }
1486
1487        Ok(())
1488    }
1489
1490    #[test]
1491    fn empty_validator_signing_keys_are_rejected() {
1492        let error = MockChain::builder().validator_signing_keys(Vec::new()).build().unwrap_err();
1493        assert!(matches!(
1494            error.downcast_ref::<ValidatorConfigError>(),
1495            Some(ValidatorConfigError::EmptySet)
1496        ));
1497    }
1498
1499    #[test]
1500    fn duplicate_validator_signing_keys_are_rejected() {
1501        let key = random_secret_key();
1502        let error = MockChain::builder()
1503            .validator_signing_keys(vec![key.clone(), key])
1504            .build()
1505            .unwrap_err();
1506        assert!(matches!(
1507            error.downcast_ref::<ValidatorConfigError>(),
1508            Some(ValidatorConfigError::DuplicateKey)
1509        ));
1510    }
1511
1512    #[test]
1513    fn too_many_validator_signing_keys_are_rejected() {
1514        let count = ValidatorConfig::MAX_VALIDATORS + 1;
1515        let keys = (0..count).map(|_| random_secret_key()).collect();
1516        let error = MockChain::builder().validator_signing_keys(keys).build().unwrap_err();
1517        assert!(matches!(
1518            error.downcast_ref::<ValidatorConfigError>(),
1519            Some(ValidatorConfigError::TooManyKeys { count: actual }) if *actual == count
1520        ));
1521    }
1522
1523    #[test]
1524    fn validator_signing_key_count_overflow_is_rejected() {
1525        let keys = vec![random_secret_key(); usize::from(u16::MAX) + 1];
1526        assert!(MockChain::builder().validator_signing_keys(keys).build().is_err());
1527    }
1528
1529    #[test]
1530    fn proposed_block_serialization_round_trip() -> anyhow::Result<()> {
1531        let chain = MockChain::new();
1532        let timestamp = chain.latest_block_header().timestamp() + 1;
1533        let next_keys = ValidatorConfig::from_signers(&[random_secret_key()]);
1534        let proposed = chain
1535            .propose_block_at(Vec::<ProvenBatch>::new(), timestamp)?
1536            .with_next_validator_config(next_keys.clone());
1537
1538        let bytes = proposed.to_bytes();
1539        let deserialized = ProposedBlock::read_from_bytes(&bytes).unwrap();
1540
1541        // `ProposedBlock` does not implement `PartialEq`, so compare via re-serialization and the
1542        // round-tripped `next_validator_config` field added by this change.
1543        assert_eq!(deserialized.to_bytes(), bytes);
1544        assert_eq!(deserialized.next_validator_config(), &next_keys);
1545
1546        Ok(())
1547    }
1548
1549    #[tokio::test]
1550    async fn private_account_state_update() -> anyhow::Result<()> {
1551        let faucet_id = ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into()?;
1552        let account_builder = AccountBuilder::new([4; 32])
1553            .account_type(AccountType::Private)
1554            .with_component(BasicWallet);
1555
1556        let mut builder = MockChain::builder();
1557        let auth_scheme = AuthScheme::EcdsaK256Keccak;
1558        let account = builder.add_account_from_builder(
1559            Auth::BasicAuth { auth_scheme },
1560            account_builder,
1561            AccountState::New,
1562        )?;
1563
1564        let account_id = account.id();
1565        assert_eq!(account.nonce().as_canonical_u64(), 0);
1566
1567        let note_1 = builder.add_p2id_note(
1568            ACCOUNT_ID_SENDER.try_into().unwrap(),
1569            account.id(),
1570            &[Asset::from(FungibleAsset::new(faucet_id, 1000u64).unwrap())],
1571            NoteType::Private,
1572        )?;
1573
1574        let mut mock_chain = builder.build()?;
1575        mock_chain.prove_next_block()?;
1576
1577        let tx = mock_chain
1578            .build_transaction(account)
1579            .unauthenticated_input_note(note_1)
1580            .build()?
1581            .execute()
1582            .await?;
1583
1584        mock_chain.add_pending_executed_transaction(&tx)?;
1585        mock_chain.prove_next_block()?;
1586
1587        assert!(tx.final_account().nonce().as_canonical_u64() > 0);
1588        assert_eq!(
1589            tx.final_account().to_commitment(),
1590            mock_chain.account_tree.open(account_id).state_commitment()
1591        );
1592
1593        Ok(())
1594    }
1595
1596    #[tokio::test]
1597    async fn mock_chain_serialization() {
1598        let mut builder = MockChain::builder();
1599
1600        let mut notes = vec![];
1601        for i in 0..10 {
1602            let account = builder
1603                .add_account_from_builder(
1604                    Auth::BasicAuth {
1605                        auth_scheme: AuthScheme::Falcon512Poseidon2,
1606                    },
1607                    AccountBuilder::new([i; 32]).with_component(BasicWallet),
1608                    AccountState::New,
1609                )
1610                .unwrap();
1611            let note = builder
1612                .add_p2id_note(
1613                    ACCOUNT_ID_SENDER.try_into().unwrap(),
1614                    account.id(),
1615                    &[Asset::from(
1616                        FungibleAsset::new(
1617                            ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET.try_into().unwrap(),
1618                            1000u64,
1619                        )
1620                        .unwrap(),
1621                    )],
1622                    NoteType::Private,
1623                )
1624                .unwrap();
1625            notes.push((account, note));
1626        }
1627
1628        let mut chain = builder.build().unwrap();
1629        for (account, note) in notes {
1630            let tx = chain
1631                .build_transaction(account)
1632                .unauthenticated_input_note(note)
1633                .build()
1634                .unwrap()
1635                .execute()
1636                .await
1637                .unwrap();
1638            chain.add_pending_executed_transaction(&tx).unwrap();
1639            chain.prove_next_block().unwrap();
1640        }
1641
1642        let bytes = chain.to_bytes();
1643
1644        let deserialized = MockChain::read_from_bytes(&bytes).unwrap();
1645
1646        assert_eq!(chain.chain.as_mmr().peaks(), deserialized.chain.as_mmr().peaks());
1647        assert_eq!(chain.blocks, deserialized.blocks);
1648        assert_eq!(chain.nullifier_tree, deserialized.nullifier_tree);
1649        assert_eq!(chain.account_tree, deserialized.account_tree);
1650        assert_eq!(chain.pending_transactions, deserialized.pending_transactions);
1651        assert_eq!(chain.committed_accounts, deserialized.committed_accounts);
1652        assert_eq!(chain.committed_notes, deserialized.committed_notes);
1653        assert_eq!(chain.account_authenticators, deserialized.account_authenticators);
1654    }
1655
1656    #[test]
1657    fn mock_chain_block_signatures() -> anyhow::Result<()> {
1658        let mut builder = MockChain::builder();
1659        builder.add_existing_mock_account(Auth::IncrNonce)?;
1660        let mut chain = builder.build()?;
1661
1662        // The genesis block is the trust root: it is self-signed by the validator set it commits
1663        // as the signer of block 1.
1664        let genesis_block = chain.latest_block();
1665        let genesis_validator_config = genesis_block.header().validator_config().clone();
1666        assert_eq!(genesis_validator_config.len(), 3);
1667        assert_eq!(genesis_validator_config.quorum(), 3);
1668        genesis_block
1669            .signatures()
1670            .verify_against(genesis_block.header().commitment(), &genesis_validator_config)
1671            .unwrap();
1672
1673        // Add another block.
1674        chain.prove_next_block()?;
1675
1676        // The next block's signatures must verify against the validator keys committed to by its
1677        // parent (the genesis block), not the keys in its own header.
1678        let next_block = chain.latest_block();
1679        next_block
1680            .signatures()
1681            .verify_against(next_block.header().commitment(), &genesis_validator_config)
1682            .unwrap();
1683
1684        // Without rotation, the validator keys are carried through from the genesis header to the
1685        // next.
1686        assert_eq!(next_block.header().validator_config(), &genesis_validator_config);
1687
1688        Ok(())
1689    }
1690
1691    #[tokio::test]
1692    async fn add_pending_batch() -> anyhow::Result<()> {
1693        let mut builder = MockChain::builder();
1694        let account = builder.add_existing_mock_account(Auth::IncrNonce)?;
1695        let mut chain = builder.build()?;
1696
1697        // Execute a noop transaction and create a batch from it.
1698        let tx = chain.build_transaction(account.id()).build()?.execute().await?;
1699        let proven_tx = LocalTransactionProver::default().prove_dummy(tx)?;
1700        let proposed_batch = chain.propose_transaction_batch(vec![proven_tx])?;
1701        let proven_batch = chain.prove_transaction_batch(proposed_batch)?;
1702
1703        // Submit the batch directly and prove the block.
1704        let num_blocks_before = chain.proven_blocks().len();
1705        chain.add_pending_batch(proven_batch);
1706        chain.prove_next_block()?;
1707
1708        assert_eq!(chain.proven_blocks().len(), num_blocks_before + 1);
1709
1710        Ok(())
1711    }
1712}