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