Skip to main content

miden_client/test_utils/
mock.rs

1use alloc::boxed::Box;
2use alloc::collections::{BTreeMap, BTreeSet};
3use alloc::string::String;
4use alloc::sync::Arc;
5use alloc::vec::Vec;
6use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
7
8use miden_protocol::Word;
9use miden_protocol::account::{
10    AccountId,
11    AccountUpdateDetails,
12    AccountVaultPatch,
13    StorageMapKey,
14    StorageMapPatchEntries,
15    StorageSlot,
16    StorageSlotContent,
17    StorageSlotName,
18    StorageSlotType,
19};
20use miden_protocol::address::NetworkId;
21use miden_protocol::asset::{AssetVault, PartialVault};
22use miden_protocol::batch::{ProposedBatch, ProvenBatch};
23use miden_protocol::block::{BlockHeader, BlockNumber, SignedBlock};
24use miden_protocol::crypto::merkle::MerklePath;
25use miden_protocol::crypto::merkle::mmr::{Forest, Mmr, MmrProof};
26use miden_protocol::crypto::merkle::smt::PartialSmt;
27use miden_protocol::note::{NoteAttachments, NoteHeader, NoteId, NoteScript, NoteTag};
28use miden_protocol::protocol_config::ProtocolConfig;
29use miden_protocol::transaction::{ExecutedTransaction, OutputNote, ProvenTransaction};
30use miden_protocol::vm::ExecutionProof;
31use miden_testing::{MockChain, MockChainNote};
32use miden_tx::utils::sync::RwLock;
33
34use crate::Client;
35use crate::rpc::domain::account::{
36    AccountDetails,
37    AccountProof,
38    AccountStorageDetails,
39    AccountStorageMapDetails,
40    AccountVaultDetails,
41    GetAccountRequest,
42    StorageMapEntries,
43    StorageMapEntry,
44    StorageMapFetch,
45    VaultFetch,
46};
47use crate::rpc::domain::account_vault::AccountVaultInfo;
48use crate::rpc::domain::note::{CommittedNote, FetchedNote, SyncNotesBlock};
49use crate::rpc::domain::nullifier::NullifierUpdate;
50use crate::rpc::domain::status::NetworkNoteStatusInfo;
51use crate::rpc::domain::storage_map::StorageMapInfo;
52use crate::rpc::domain::sync::{ChainMmrInfo, SyncTarget};
53use crate::rpc::domain::transaction::TransactionRecord;
54use crate::rpc::encryption::{AttestedTransactionEncryptionKey, SealedTransactionInputs};
55use crate::rpc::{AccountStateAt, NodeRpcClient, RpcEndpoint, RpcError, RpcStatusInfo};
56
57pub type MockClient<AUTH> = Client<AUTH>;
58
59#[derive(Clone, Copy)]
60struct BlockHeaderRequest {
61    block_num: Option<BlockNumber>,
62    include_mmr_proof: bool,
63}
64
65/// Mock RPC API
66///
67/// This struct implements the RPC API used by the client to communicate with the node. It simulates
68/// most of the functionality of the actual node, with some small differences:
69/// - It uses a [`MockChain`] to simulate the blockchain state.
70/// - Blocks are not automatically created after time passes, but rather new blocks are created when
71///   calling the `prove_block` method.
72/// - Network account and transactions aren't supported in the current version.
73/// - Account update block numbers aren't tracked, so any endpoint that returns when certain account
74///   updates were made will return the chain tip block number instead.
75#[derive(Clone)]
76pub struct MockRpcApi {
77    account_commitment_updates: Arc<RwLock<BTreeMap<BlockNumber, BTreeMap<AccountId, Word>>>>,
78    pub mock_chain: Arc<RwLock<MockChain>>,
79    /// Chain snapshots used to answer block-pinned account queries.
80    historical_chains: Arc<RwLock<BTreeMap<BlockNumber, Arc<MockChain>>>>,
81    oversize_threshold: usize,
82    /// Note headers to report as erased in sync transaction responses.
83    erased_notes: Arc<RwLock<Vec<NoteHeader>>>,
84    /// Attachment content `get_notes_by_id` serves for private notes, populated by
85    /// `submit_proven_transaction` and by `register_private_note_attachments`. A note absent here
86    /// is served with empty attachments, which is how a test simulates a withholding node.
87    private_note_attachments: Arc<RwLock<BTreeMap<NoteId, NoteAttachments>>>,
88    /// Test overrides for the MMR paths returned by `sync_notes`, keyed by block number.
89    sync_notes_mmr_path_overrides: Arc<RwLock<BTreeMap<BlockNumber, MerklePath>>>,
90    /// Number of `get_notes_by_id` requests served, so a test can assert that a flow avoided the
91    /// round trip.
92    get_notes_by_id_calls: Arc<AtomicUsize>,
93    /// Block header requests, recorded with the requested block and proof flag.
94    block_header_requests: Arc<RwLock<Vec<BlockHeaderRequest>>>,
95    /// Number of `get_account` requests served, so a test can assert that a flow avoided the round
96    /// trip.
97    get_account_calls: Arc<AtomicUsize>,
98    /// Number of `sync_account_vault` requests served, so a test can assert that a flow avoided the
99    /// round trip.
100    sync_account_vault_calls: Arc<AtomicUsize>,
101    /// Failures to serve instead of answering, keyed by [`RpcEndpoint::proto_name`] and set by
102    /// [`MockRpcApi::fail_next_call`]. An entry is removed when served, so the call after it
103    /// answers normally and a test can exercise a retry.
104    next_call_failures: Arc<RwLock<BTreeMap<&'static str, RpcError>>>,
105    /// Invitation code each account was registered with, recorded by `register_account`.
106    registered_accounts: Arc<RwLock<BTreeMap<AccountId, String>>>,
107    /// Invitation codes that tests have made available for registration.
108    invitation_codes: Arc<RwLock<BTreeSet<String>>>,
109    /// Whether `is_account_allowed` consults `registered_accounts`. A node that does not enforce
110    /// the allowlist answers `true` for every account, which is the default here so that tests
111    /// which deploy accounts need no registration.
112    allowlist_enforced: Arc<AtomicBool>,
113    /// Number of `is_account_allowed` requests served, so a test can assert that a flow avoided the
114    /// round trip.
115    is_account_allowed_calls: Arc<AtomicUsize>,
116    /// Sealed inputs handed to `submit_proven_batch`, one entry per call and recorded before any
117    /// staged failure is served, so a test can assert that a resubmission sealed again instead of
118    /// reusing a cached ciphertext.
119    submitted_batch_sealed_inputs: Arc<RwLock<Vec<Vec<SealedTransactionInputs>>>>,
120}
121
122impl Default for MockRpcApi {
123    fn default() -> Self {
124        Self::new(MockChain::new())
125    }
126}
127
128impl MockRpcApi {
129    // Constant to use in mocked pagination.
130    const PAGINATION_BLOCK_LIMIT: u32 = 5;
131
132    /// Creates a new [`MockRpcApi`] instance with the state of the provided [`MockChain`].
133    pub fn new(mock_chain: MockChain) -> Self {
134        Self {
135            account_commitment_updates: Arc::new(RwLock::new(build_account_updates(&mock_chain))),
136            mock_chain: Arc::new(RwLock::new(mock_chain)),
137            historical_chains: Arc::new(RwLock::new(BTreeMap::new())),
138            oversize_threshold: 1000,
139            erased_notes: Arc::new(RwLock::new(Vec::new())),
140            private_note_attachments: Arc::new(RwLock::new(BTreeMap::new())),
141            sync_notes_mmr_path_overrides: Arc::new(RwLock::new(BTreeMap::new())),
142            get_notes_by_id_calls: Arc::new(AtomicUsize::new(0)),
143            block_header_requests: Arc::new(RwLock::new(Vec::new())),
144            get_account_calls: Arc::new(AtomicUsize::new(0)),
145            sync_account_vault_calls: Arc::new(AtomicUsize::new(0)),
146            next_call_failures: Arc::new(RwLock::new(BTreeMap::new())),
147            registered_accounts: Arc::new(RwLock::new(BTreeMap::new())),
148            invitation_codes: Arc::new(RwLock::new(BTreeSet::new())),
149            allowlist_enforced: Arc::new(AtomicBool::new(false)),
150            is_account_allowed_calls: Arc::new(AtomicUsize::new(0)),
151            submitted_batch_sealed_inputs: Arc::new(RwLock::new(Vec::new())),
152        }
153    }
154
155    /// Makes `is_account_allowed` answer from the recorded registrations, modelling a node that
156    /// enforces the account allowlist. Without this the mock answers `true` for every account.
157    pub fn enforce_account_allowlist(&self) {
158        self.allowlist_enforced.store(true, Ordering::SeqCst);
159    }
160
161    /// Adds an invitation code for validation queries.
162    pub fn add_invitation_code(&self, invitation_code: &str) {
163        self.invitation_codes.write().insert(String::from(invitation_code));
164    }
165
166    /// Id of the first account updated in the mock chain's proven blocks, in block then
167    /// within-block order. Tests use it to get hold of an account the chain already knows.
168    ///
169    /// Panics if the chain has no account updates.
170    pub fn first_account_id(&self) -> AccountId {
171        self.mock_chain
172            .read()
173            .proven_blocks()
174            .iter()
175            .flat_map(|block| block.body().updated_accounts())
176            .next()
177            .expect("the mock chain must have at least one account update")
178            .account_id()
179    }
180
181    /// Sealed inputs recorded by `submit_proven_batch`, one entry per call, including calls that
182    /// went on to be served a staged failure. Within an entry the order matches the batch's
183    /// transaction order.
184    pub fn submitted_batch_sealed_inputs(&self) -> Vec<Vec<SealedTransactionInputs>> {
185        self.submitted_batch_sealed_inputs.read().clone()
186    }
187
188    /// Makes the next call to `endpoint` fail with `error` instead of answering. The failure is
189    /// consumed, so the call after it answers normally and a test can exercise a retry.
190    ///
191    /// Staging a failure for an endpoint whose mock implementation does not look for one is a
192    /// silent no-op.
193    pub fn fail_next_call(&self, endpoint: RpcEndpoint, error: RpcError) {
194        self.next_call_failures.write().insert(endpoint.proto_name(), error);
195    }
196
197    /// Returns the failure staged for `endpoint`, removing it so it is served once.
198    fn take_failure(&self, endpoint: RpcEndpoint) -> Option<RpcError> {
199        self.next_call_failures.write().remove(endpoint.proto_name())
200    }
201
202    /// Returns the invitation code `account_id` was registered with, or `None` if this API served
203    /// no registration for it.
204    pub fn registered_invitation_code(&self, account_id: AccountId) -> Option<String> {
205        self.registered_accounts.read().get(&account_id).cloned()
206    }
207
208    /// Registers the attachment content for a private note so that subsequent `get_notes_by_id`
209    /// responses include it, mirroring a node that stores private-note attachments on-chain.
210    pub fn register_private_note_attachments(&self, note_id: NoteId, attachments: NoteAttachments) {
211        self.private_note_attachments.write().insert(note_id, attachments);
212    }
213
214    /// Returns how many `get_notes_by_id` requests this API has served.
215    pub fn get_notes_by_id_call_count(&self) -> usize {
216        self.get_notes_by_id_calls.load(Ordering::Relaxed)
217    }
218
219    /// Returns the proof flags of requests for `block_num`.
220    pub fn block_header_requests(&self, block_num: BlockNumber) -> Vec<bool> {
221        self.block_header_requests
222            .read()
223            .iter()
224            .filter_map(|request| {
225                (request.block_num == Some(block_num)).then_some(request.include_mmr_proof)
226            })
227            .collect()
228    }
229
230    /// Returns how many `get_account` requests this API has served.
231    pub fn get_account_call_count(&self) -> usize {
232        self.get_account_calls.load(Ordering::Relaxed)
233    }
234
235    /// Returns how many `sync_account_vault` requests this API has served.
236    pub fn sync_account_vault_call_count(&self) -> usize {
237        self.sync_account_vault_calls.load(Ordering::Relaxed)
238    }
239
240    /// Returns how many `is_account_allowed` requests this API has served.
241    pub fn is_account_allowed_call_count(&self) -> usize {
242        self.is_account_allowed_calls.load(Ordering::Relaxed)
243    }
244
245    /// Overrides the MMR path returned by `sync_notes` for the specified block.
246    pub fn set_sync_notes_mmr_path(&self, block_num: BlockNumber, path: MerklePath) {
247        self.sync_notes_mmr_path_overrides.write().insert(block_num, path);
248    }
249
250    /// Sets the oversize threshold for `get_account`. A storage map whose entries were requested in
251    /// full comes back as `StorageMapEntries::LimitExceeded` past this threshold, and a vault with
252    /// more assets than it comes back as `AccountVaultDetails::TooManyAssets`.
253    #[must_use]
254    pub fn with_oversize_threshold(mut self, threshold: usize) -> Self {
255        self.oversize_threshold = threshold;
256        self
257    }
258
259    /// Returns the `get_account` vault details for `vault` under the `request` policy.
260    ///
261    /// Mirrors the node: `Skip` sends no assets, `IfChangedFrom` omits them when the vault root
262    /// already equals the sent commitment, and `Assets` sends proofs for the requested assets for
263    /// any vault size. The node sends no asset list for a vault above `oversize_threshold`.
264    fn vault_details(&self, vault: &AssetVault, request: &VaultFetch) -> AccountVaultDetails {
265        match request {
266            VaultFetch::Skip => AccountVaultDetails::Assets(Vec::new()),
267            VaultFetch::IfChangedFrom(root) if *root == vault.root() => {
268                AccountVaultDetails::Assets(Vec::new())
269            },
270            VaultFetch::Always | VaultFetch::IfChangedFrom(_) => {
271                if vault.num_assets() > self.oversize_threshold {
272                    AccountVaultDetails::TooManyAssets
273                } else {
274                    AccountVaultDetails::Assets(vault.assets().collect())
275                }
276            },
277            VaultFetch::Assets(asset_ids) if asset_ids.is_empty() => {
278                AccountVaultDetails::Partial(PartialVault::new(vault.root()))
279            },
280            VaultFetch::Assets(asset_ids) => {
281                let witnesses = asset_ids.iter().map(|asset_id| vault.open(*asset_id));
282                let partial_vault = PartialVault::with_witnesses(witnesses)
283                    .expect("witnesses from one vault share a root");
284                AccountVaultDetails::Partial(partial_vault)
285            },
286        }
287    }
288
289    /// Registers a note header to be reported as erased in subsequent sync transaction responses.
290    pub fn mark_note_as_erased(&self, header: NoteHeader) {
291        self.erased_notes.write().push(header);
292    }
293
294    /// Returns the current MMR of the blockchain.
295    pub fn get_mmr(&self) -> Mmr {
296        self.mock_chain.read().blockchain().as_mmr().clone()
297    }
298
299    /// Returns the protocol configuration the mock chain commits to.
300    pub fn protocol_config(&self) -> ProtocolConfig {
301        self.mock_chain.read().protocol_config().clone()
302    }
303
304    /// Returns the chain tip block number.
305    pub fn get_chain_tip_block_num(&self) -> BlockNumber {
306        self.mock_chain.read().latest_block_header().block_num()
307    }
308
309    /// Adds an executed transaction to the pending transactions of the mock chain with a dummy
310    /// proof. The next [`Self::prove_block`] call commits it.
311    ///
312    /// Tests use this method to put a transaction on chain without the cost of a real proof.
313    pub fn add_pending_executed_transaction(&self, executed_transaction: &ExecutedTransaction) {
314        self.mock_chain
315            .write()
316            .add_pending_executed_transaction(executed_transaction)
317            .expect("mock chain should accept the executed transaction");
318    }
319
320    /// Advances the mock chain by proving the next block, committing all pending objects to the
321    /// chain in the process.
322    pub fn prove_block(&self) {
323        let proven_block = {
324            let mut mock_chain = self.mock_chain.write();
325            let historical_block_num = mock_chain.latest_block_header().block_num();
326            let snapshot = Arc::new(mock_chain.clone());
327            let proven_block = mock_chain.prove_next_block().unwrap();
328            self.historical_chains.write().insert(historical_block_num, snapshot);
329            proven_block
330        };
331        let block_num = proven_block.header().block_num();
332        let mut account_commitment_updates = self.account_commitment_updates.write();
333        let updates: BTreeMap<AccountId, Word> = proven_block
334            .body()
335            .updated_accounts()
336            .iter()
337            .map(|update| (update.account_id(), update.final_state_commitment()))
338            .collect();
339
340        if !updates.is_empty() {
341            account_commitment_updates.insert(block_num, updates);
342        }
343    }
344
345    /// Removes the account-state snapshot for the specified block.
346    ///
347    /// Tests use this method to model a node that pruned historical account state.
348    pub fn prune_account_state_at(&self, block_num: BlockNumber) {
349        self.historical_chains.write().remove(&block_num);
350    }
351
352    /// Retrieves a block by its block number.
353    fn get_block_by_num(&self, block_num: BlockNumber) -> BlockHeader {
354        self.mock_chain.read().block_header(block_num.as_usize())
355    }
356
357    /// Retrieves account vault updates in a given block range. This method tries to simulate
358    /// pagination by limiting the number of blocks processed per request.
359    fn get_sync_account_vault_request(
360        &self,
361        block_from: BlockNumber,
362        block_to: BlockNumber,
363        account_id: AccountId,
364    ) -> (BlockNumber, BlockNumber, AccountVaultPatch) {
365        let chain_tip = self.get_chain_tip_block_num();
366        let target_block = block_to.min(chain_tip);
367
368        let page_end_block: BlockNumber = (block_from.as_u32() + Self::PAGINATION_BLOCK_LIMIT)
369            .min(target_block.as_u32())
370            .into();
371
372        // Blocks are iterated in ascending order, so later blocks win per asset ID.
373        let mut vault_patch = AccountVaultPatch::default();
374        for block in self.mock_chain.read().proven_blocks() {
375            let block_number = block.header().block_num();
376            // Only include blocks in range [block_from, page_end_block]
377            if block_number < block_from || block_number > page_end_block {
378                continue;
379            }
380
381            for update in block
382                .body()
383                .updated_accounts()
384                .iter()
385                .filter(|block_acc_update| block_acc_update.account_id() == account_id)
386            {
387                let AccountUpdateDetails::Public(patch) = update.details().clone() else {
388                    continue;
389                };
390
391                vault_patch.merge(patch.vault().clone());
392            }
393        }
394
395        (chain_tip, page_end_block, vault_patch)
396    }
397
398    /// Retrieves transactions in a given block range that match the provided account IDs
399    fn get_sync_transactions_request(
400        &self,
401        block_from: BlockNumber,
402        block_to: BlockNumber,
403        account_ids: &[AccountId],
404    ) -> Vec<TransactionRecord> {
405        let mut transactions = Vec::new();
406        for block in self.mock_chain.read().proven_blocks() {
407            let block_number = block.header().block_num();
408            if block_number < block_from || block_number > block_to {
409                continue;
410            }
411
412            for transaction_header in block.body().transactions().as_slice() {
413                if !account_ids.contains(&transaction_header.account_id()) {
414                    continue;
415                }
416
417                let erased_output_notes = self.erased_notes.read().clone();
418
419                transactions.push(TransactionRecord {
420                    block_num: block_number,
421                    transaction_header: transaction_header.clone(),
422                    output_notes: vec![],
423                    erased_output_notes,
424                    consumed_note_refs: vec![],
425                });
426            }
427        }
428
429        transactions
430    }
431
432    /// Retrieves storage map updates in a given block range.
433    ///
434    /// This method tries to simulate pagination of the real node.
435    fn get_sync_storage_maps_request(
436        &self,
437        block_from: BlockNumber,
438        block_to: BlockNumber,
439        account_id: AccountId,
440    ) -> (BlockNumber, BlockNumber, BTreeMap<StorageSlotName, StorageMapPatchEntries>) {
441        let chain_tip = self.get_chain_tip_block_num();
442        let target_block = block_to.min(chain_tip);
443
444        let page_end_block: BlockNumber = (block_from.as_u32() + Self::PAGINATION_BLOCK_LIMIT)
445            .min(target_block.as_u32())
446            .into();
447
448        // Blocks are iterated in ascending order, so later blocks win per `(slot, key)`.
449        let mut map_entries: BTreeMap<StorageSlotName, StorageMapPatchEntries> = BTreeMap::new();
450        for block in self.mock_chain.read().proven_blocks() {
451            let block_number = block.header().block_num();
452            // Only include blocks in range [block_from, page_end_block]
453            if block_number < block_from || block_number > page_end_block {
454                continue;
455            }
456
457            for update in block
458                .body()
459                .updated_accounts()
460                .iter()
461                .filter(|block_acc_update| block_acc_update.account_id() == account_id)
462            {
463                let AccountUpdateDetails::Public(patch) = update.details().clone() else {
464                    continue;
465                };
466
467                for (slot_name, map_patch) in patch.storage().maps() {
468                    if let Some(entries) = map_patch.entries() {
469                        map_entries
470                            .entry(slot_name.clone())
471                            .or_default()
472                            .as_map_mut()
473                            .extend(entries.as_map().clone());
474                    }
475                }
476            }
477        }
478
479        (chain_tip, page_end_block, map_entries)
480    }
481
482    pub fn get_available_notes(&self) -> Vec<MockChainNote> {
483        self.mock_chain.read().committed_notes().values().cloned().collect()
484    }
485
486    pub fn get_public_available_notes(&self) -> Vec<MockChainNote> {
487        self.mock_chain
488            .read()
489            .committed_notes()
490            .values()
491            .filter(|n| matches!(n, MockChainNote::Public(_, _)))
492            .cloned()
493            .collect()
494    }
495
496    pub fn advance_blocks(&self, num_blocks: u32) {
497        let mut mock_chain = self.mock_chain.write();
498        let block_num = mock_chain.latest_block_header().block_num();
499        let snapshot = Arc::new(mock_chain.clone());
500        mock_chain.prove_until_block(block_num + num_blocks).unwrap();
501        self.historical_chains.write().insert(block_num, snapshot);
502    }
503}
504#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
505#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
506impl NodeRpcClient for MockRpcApi {
507    /// Always reports the commitment as unset, unlike a real client.
508    ///
509    /// A real client's RPC connection is its own, so whoever set the commitment also stored the
510    /// header. Tests share one mock across clients with separate stores, where a commitment set by
511    /// the first would stop every later client from storing genesis at all.
512    fn has_genesis_commitment(&self) -> Option<Word> {
513        None
514    }
515
516    async fn set_genesis_commitment(&self, _commitment: Word) -> Result<(), RpcError> {
517        // The mock sends no request headers, so there is nothing to pin the commitment to.
518        Ok(())
519    }
520
521    /// Returns note updates in the inclusive block range `[block_from, block_to]`. Only notes that
522    /// match the provided tags will be returned, grouped by block.
523    async fn sync_notes(
524        &self,
525        block_from: BlockNumber,
526        block_to: BlockNumber,
527        note_tags: &BTreeSet<NoteTag>,
528    ) -> Result<Vec<SyncNotesBlock>, RpcError> {
529        let mut blocks_with_notes: BTreeMap<BlockNumber, BTreeMap<NoteId, CommittedNote>> =
530            BTreeMap::new();
531        for note in self.mock_chain.read().committed_notes().values() {
532            let note_block = note.inclusion_proof().location().block_num();
533            if note_tags.contains(&note.metadata().tag())
534                && note_block >= block_from
535                && note_block <= block_to
536            {
537                let mut committed =
538                    CommittedNote::new(note.id(), *note.metadata(), note.inclusion_proof().clone());
539                // Mirror the node: a single-word attachment is sent verbatim and the record is
540                // complete. A larger one is sent as a commitment only.
541                let attachments = note.attachments();
542                if attachments.iter().all(|attachment| attachment.num_words() == 1) {
543                    committed = committed
544                        .with_attachments(attachments.clone())
545                        .expect("the note's own attachments match its commitment");
546                }
547                blocks_with_notes.entry(note_block).or_default().insert(note.id(), committed);
548            }
549        }
550
551        Ok(blocks_with_notes
552            .into_iter()
553            .map(|(bn, notes)| {
554                let block_header = self.get_block_by_num(bn);
555                let mmr_path =
556                    self.sync_notes_mmr_path_overrides.read().get(&bn).cloned().unwrap_or_else(
557                        || self.get_mmr().open(bn.as_usize()).unwrap().merkle_path().clone(),
558                    );
559                SyncNotesBlock { block_header, mmr_path, notes }
560            })
561            .collect())
562    }
563
564    async fn sync_chain_mmr(
565        &self,
566        current_block_height: BlockNumber,
567        upper_bound: SyncTarget,
568    ) -> Result<ChainMmrInfo, RpcError> {
569        let chain_tip = self.get_chain_tip_block_num();
570        // The mock chain doesn't distinguish committed vs proven tips.
571        let target_block = match upper_bound {
572            SyncTarget::CommittedChainTip | SyncTarget::ProvenChainTip => chain_tip,
573        };
574
575        let from_forest = if current_block_height == target_block {
576            target_block.as_usize()
577        } else {
578            current_block_height.as_u32() as usize + 1
579        };
580
581        let mmr_delta = self
582            .get_mmr()
583            .get_delta(
584                Forest::new(from_forest).unwrap(),
585                Forest::new(target_block.as_usize()).unwrap(),
586            )
587            .unwrap();
588
589        let block_header = self.get_block_by_num(target_block);
590        let block_signatures = self
591            .mock_chain
592            .read()
593            .proven_blocks()
594            .iter()
595            .find(|block| block.header().block_num() == target_block)
596            .expect("the mock chain contains the target block")
597            .signatures()
598            .clone();
599
600        // Mirrors the node: send the configuration when the caller starts at genesis, or when the
601        // commitment changed over the range. A caller already at the target gets nothing.
602        let protocol_config = if current_block_height == BlockNumber::GENESIS {
603            Some(self.protocol_config())
604        } else if current_block_height == target_block {
605            None
606        } else {
607            let commitment_at_start =
608                self.get_block_by_num(current_block_height).protocol_config_commitment();
609            (commitment_at_start != block_header.protocol_config_commitment())
610                .then(|| self.protocol_config())
611        };
612
613        Ok(ChainMmrInfo {
614            block_from: current_block_height,
615            block_to: target_block,
616            mmr_delta,
617            block_header,
618            protocol_config,
619            block_signatures,
620        })
621    }
622
623    /// Retrieves the block header for the specified block number. If the block number is not
624    /// provided, the chain tip block header will be returned.
625    async fn get_block_header_by_number(
626        &self,
627        block_num: Option<BlockNumber>,
628        include_mmr_proof: bool,
629    ) -> Result<(BlockHeader, Option<MmrProof>), RpcError> {
630        self.block_header_requests
631            .write()
632            .push(BlockHeaderRequest { block_num, include_mmr_proof });
633
634        let block = if let Some(block_num) = block_num {
635            self.mock_chain.read().block_header(block_num.as_usize())
636        } else {
637            self.mock_chain.read().latest_block_header()
638        };
639
640        let mmr_proof = if include_mmr_proof {
641            Some(self.get_mmr().open(block_num.unwrap().as_usize()).unwrap())
642        } else {
643            None
644        };
645
646        Ok((block, mmr_proof))
647    }
648
649    /// Returns the node's tracked notes that match the provided note IDs.
650    async fn get_notes_by_id(&self, note_ids: &[NoteId]) -> Result<Vec<FetchedNote>, RpcError> {
651        self.get_notes_by_id_calls.fetch_add(1, Ordering::Relaxed);
652
653        let notes = self.mock_chain.read().committed_notes().clone();
654
655        let hit_notes = note_ids.iter().filter_map(|id| notes.get(id));
656        let mut return_notes = vec![];
657        for note in hit_notes {
658            let fetched_note = match note {
659                MockChainNote::Private(note_id, note_metadata, _, note_inclusion_proof) => {
660                    let attachments = self
661                        .private_note_attachments
662                        .read()
663                        .get(note_id)
664                        .cloned()
665                        .unwrap_or_else(NoteAttachments::empty);
666                    FetchedNote::Private(
667                        *note_id,
668                        *note_metadata,
669                        attachments,
670                        note_inclusion_proof.clone(),
671                    )
672                },
673                MockChainNote::Public(note, note_inclusion_proof) => {
674                    FetchedNote::Public(note.clone(), note_inclusion_proof.clone())
675                },
676            };
677            return_notes.push(fetched_note);
678        }
679        Ok(return_notes)
680    }
681
682    /// The mock does not serve the encryption key. Verifying an attestation needs a validator
683    /// signature the mock chain cannot produce, so tests that submit transactions seed the key
684    /// directly through `Client::seed_transaction_encryption_key` instead.
685    async fn get_transaction_encryption_key(
686        &self,
687    ) -> Result<AttestedTransactionEncryptionKey, RpcError> {
688        Err(RpcError::TransactionEncryptionKeyRejected(
689            "the mock RPC client does not serve a transaction encryption key".into(),
690        ))
691    }
692
693    /// Simulates the submission of a proven transaction to the node. This will create a new block
694    /// just for the new transaction and return the block number of the newly created block.
695    async fn submit_proven_transaction(
696        &self,
697        proven_transaction: &ProvenTransaction,
698        _sealed_transaction_inputs: SealedTransactionInputs, /* Unnecessary for testing client
699                                                              * itself. */
700    ) -> Result<BlockNumber, RpcError> {
701        if let Some(error) = self.take_failure(RpcEndpoint::SubmitProvenTx) {
702            return Err(error);
703        }
704
705        // Record private-note attachment content the way a real node does: attachments are stored
706        // on-chain even for private notes, so `get_notes_by_id` must be able to serve them. The
707        // mock chain itself only keeps private note headers.
708        for note in proven_transaction.output_notes().iter() {
709            if let OutputNote::Private(private_note) = note
710                && !private_note.attachments().is_empty()
711            {
712                self.private_note_attachments
713                    .write()
714                    .insert(private_note.id(), private_note.attachments().clone());
715            }
716        }
717
718        {
719            let mut mock_chain = self.mock_chain.write();
720            mock_chain.add_pending_proven_transaction(proven_transaction.clone());
721        };
722
723        let block_num = self.get_chain_tip_block_num();
724
725        Ok(block_num)
726    }
727
728    /// Simulates the submission of a proven batch to the node by adding it to the mock chain's
729    /// pending batches. The `proposed_batch` argument is accepted to match the trait signature but
730    /// is unused: the mock relies on the `ProvenBatch` alone. The sealed inputs are recorded rather
731    /// than decrypted, so a test can inspect what each attempt sent.
732    async fn submit_proven_batch(
733        &self,
734        proven_batch: &ProvenBatch,
735        _proposed_batch: &ProposedBatch,
736        sealed_transaction_inputs: Vec<SealedTransactionInputs>,
737    ) -> Result<BlockNumber, RpcError> {
738        // Recorded before the staged failure is served: a submission whose response is lost still
739        // reached the node, so a test can compare what that attempt sent against the retry.
740        self.submitted_batch_sealed_inputs.write().push(sealed_transaction_inputs);
741
742        if let Some(error) = self.take_failure(RpcEndpoint::SubmitProvenBatch) {
743            return Err(error);
744        }
745
746        let mut mock_chain = self.mock_chain.write();
747        mock_chain.add_pending_batch(proven_batch.clone());
748        drop(mock_chain);
749
750        let block_num = self.get_chain_tip_block_num();
751
752        Ok(block_num)
753    }
754
755    /// Returns the account proof for the specified account. The `known_code` field is ignored, so
756    /// the response always contains the account code. Storage maps and vaults above
757    /// `oversize_threshold` come back with truncation flags set.
758    async fn get_account(
759        &self,
760        request: GetAccountRequest,
761    ) -> Result<(BlockNumber, AccountProof), RpcError> {
762        let account_id = request.account_id;
763        self.get_account_calls.fetch_add(1, Ordering::Relaxed);
764
765        if let Some(error) = self.take_failure(RpcEndpoint::GetAccount) {
766            return Err(error);
767        }
768
769        let current_chain = self.mock_chain.read();
770        let current_block_number = current_chain.latest_block_header().block_num();
771        let block_number = match request.at {
772            AccountStateAt::Block(number) => number,
773            AccountStateAt::ChainTip => current_block_number,
774        };
775        let historical_chain = match request.at {
776            AccountStateAt::Block(_) if block_number != current_block_number => Some(
777                self.historical_chains.read().get(&block_number).cloned().ok_or_else(|| {
778                    RpcError::InvalidResponse(alloc::format!(
779                        "no mock chain snapshot at block {block_number}"
780                    ))
781                })?,
782            ),
783            AccountStateAt::ChainTip | AccountStateAt::Block(_) => None,
784        };
785        let mock_chain = historical_chain.as_deref().unwrap_or(&*current_chain);
786
787        let headers = if account_id.is_public() {
788            let account = mock_chain.committed_account(account_id).unwrap();
789
790            // `All` enumerates the account's map slots directly — the mock can introspect the
791            // account, so it simulates the (not-yet-on-the-wire) "all storage maps" request. A slot
792            // maps to the keys requested for it, empty meaning "every entry".
793            let requested_slots: Vec<(StorageSlotName, Vec<StorageMapKey>)> = match request
794                .fetch
795                .storage()
796            {
797                StorageMapFetch::Skip => Vec::new(),
798                StorageMapFetch::Slots(reqs) => {
799                    reqs.inner().iter().map(|(name, keys)| (name.clone(), keys.clone())).collect()
800                },
801                StorageMapFetch::All => account
802                    .storage()
803                    .to_header()
804                    .slots()
805                    .filter(|slot| slot.slot_type() == StorageSlotType::Map)
806                    .map(|slot| (slot.name().clone(), Vec::new()))
807                    .collect(),
808            };
809
810            let mut map_details = vec![];
811            for (slot_name, requested_keys) in &requested_slots {
812                if let Some(StorageSlotContent::Map(storage_map)) =
813                    account.storage().get(slot_name).map(StorageSlot::content)
814                {
815                    // Mirror the node: named keys come back as one partial SMT covering them, and
816                    // an empty key list comes back as the whole map, or as `LimitExceeded` once it
817                    // grows past the threshold.
818                    let entries = if requested_keys.is_empty() {
819                        let entries: Vec<StorageMapEntry> = storage_map
820                            .entries()
821                            .map(|(key, value)| StorageMapEntry { key: *key, value: *value })
822                            .collect();
823
824                        if entries.len() > self.oversize_threshold {
825                            StorageMapEntries::LimitExceeded
826                        } else {
827                            StorageMapEntries::AllEntries(entries)
828                        }
829                    } else {
830                        let partial_smt = PartialSmt::from_proofs(
831                            requested_keys.iter().map(|key| storage_map.open(key).into()),
832                        )
833                        .expect("proofs from one map share a root");
834
835                        StorageMapEntries::PartialMap {
836                            map_keys: requested_keys.clone(),
837                            partial_smt,
838                        }
839                    };
840
841                    map_details
842                        .push(AccountStorageMapDetails { slot_name: slot_name.clone(), entries });
843                } else {
844                    panic!("Storage slot {slot_name} is not a map");
845                }
846            }
847
848            let storage_details = AccountStorageDetails {
849                header: account.storage().to_header(),
850                map_details,
851            };
852
853            let vault_details = self.vault_details(account.vault(), request.fetch.vault());
854
855            Some(AccountDetails {
856                header: account.into(),
857                storage_details,
858                code: account.code().clone(),
859                vault_details,
860            })
861        } else {
862            None
863        };
864
865        let witness = mock_chain.account_tree().open(account_id);
866
867        let proof = AccountProof::new(witness, headers).unwrap();
868
869        Ok((block_number, proof))
870    }
871
872    async fn register_account(
873        &self,
874        invitation_code: &str,
875        account_id: AccountId,
876    ) -> Result<(), RpcError> {
877        if let Some(error) = self.take_failure(RpcEndpoint::RegisterAccount) {
878            return Err(error);
879        }
880
881        // Registration accepts any code. Use `fail_next_call` to test a rejection.
882        self.registered_accounts
883            .write()
884            .insert(account_id, String::from(invitation_code));
885
886        Ok(())
887    }
888
889    async fn is_account_allowed(&self, account_id: AccountId) -> Result<bool, RpcError> {
890        self.is_account_allowed_calls.fetch_add(1, Ordering::Relaxed);
891
892        if let Some(error) = self.take_failure(RpcEndpoint::IsAccountAllowed) {
893            return Err(error);
894        }
895
896        // A node that does not enforce the allowlist allows every account. Call
897        // `enforce_account_allowlist` to answer from the recorded registrations instead.
898        if !self.allowlist_enforced.load(Ordering::SeqCst) {
899            return Ok(true);
900        }
901
902        Ok(self.registered_accounts.read().contains_key(&account_id))
903    }
904
905    async fn is_invitation_code_valid(&self, invitation_code: &str) -> Result<bool, RpcError> {
906        if let Some(error) = self.take_failure(RpcEndpoint::IsInvitationCodeValid) {
907            return Err(error);
908        }
909
910        if !self.allowlist_enforced.load(Ordering::SeqCst) {
911            return Ok(true);
912        }
913
914        if invitation_code.is_empty() {
915            return Err(RpcError::RequestError {
916                endpoint: RpcEndpoint::IsInvitationCodeValid,
917                error_kind: crate::rpc::GrpcError::InvalidArgument,
918                endpoint_error: None,
919                source: None,
920            });
921        }
922
923        Ok(self.invitation_codes.read().contains(invitation_code)
924            && !self.registered_accounts.read().values().any(|code| code == invitation_code))
925    }
926
927    /// Returns the nullifiers created after the specified block number that match the provided
928    /// prefixes.
929    async fn sync_nullifiers(
930        &self,
931        prefixes: &[u16],
932        block_from: BlockNumber,
933        block_to: BlockNumber,
934    ) -> Result<Vec<NullifierUpdate>, RpcError> {
935        let nullifiers = self
936            .mock_chain
937            .read()
938            .nullifier_tree()
939            .entries()
940            .filter_map(|(nullifier, block_num)| {
941                let within_range = block_num >= block_from && block_num <= block_to;
942
943                if prefixes.contains(&nullifier.prefix()) && within_range {
944                    Some(NullifierUpdate { nullifier, block_num })
945                } else {
946                    None
947                }
948            })
949            .collect::<Vec<_>>();
950
951        Ok(nullifiers)
952    }
953
954    async fn get_block_by_number(
955        &self,
956        block_num: BlockNumber,
957        include_proof: bool,
958    ) -> Result<(SignedBlock, Option<ExecutionProof>), RpcError> {
959        let block = self
960            .mock_chain
961            .read()
962            .proven_blocks()
963            .iter()
964            .find(|b| b.header().block_num() == block_num)
965            .unwrap()
966            .clone();
967        let (header, body, signatures, proof) = block.into_parts();
968
969        Ok((
970            SignedBlock::new_unchecked(header, body, signatures),
971            include_proof.then_some(proof),
972        ))
973    }
974
975    async fn get_note_script_by_root(&self, root: Word) -> Result<Option<NoteScript>, RpcError> {
976        let script = self
977            .get_available_notes()
978            .iter()
979            .filter_map(|note| note.note())
980            .find(|n| Word::from(n.script().root()) == root)
981            .map(|n| n.script().clone());
982
983        Ok(script)
984    }
985
986    async fn sync_storage_maps(
987        &self,
988        block_from: BlockNumber,
989        block_to: BlockNumber,
990        account_id: AccountId,
991    ) -> Result<StorageMapInfo, RpcError> {
992        let mut map_entries: BTreeMap<StorageSlotName, StorageMapPatchEntries> = BTreeMap::new();
993        let mut current_block_from = block_from;
994        let chain_tip = self.get_chain_tip_block_num();
995        let target_block = block_to.min(chain_tip);
996
997        loop {
998            let (page_chain_tip, page_block_number, page_entries) =
999                self.get_sync_storage_maps_request(current_block_from, block_to, account_id);
1000            for (slot_name, entries) in page_entries {
1001                map_entries
1002                    .entry(slot_name)
1003                    .or_default()
1004                    .as_map_mut()
1005                    .extend(entries.into_map());
1006            }
1007
1008            if page_block_number >= target_block {
1009                return Ok(StorageMapInfo {
1010                    chain_tip: page_chain_tip,
1011                    block_number: page_block_number,
1012                    map_entries,
1013                });
1014            }
1015
1016            current_block_from = (page_block_number.as_u32() + 1).into();
1017        }
1018    }
1019
1020    async fn sync_account_vault(
1021        &self,
1022        block_from: BlockNumber,
1023        block_to: BlockNumber,
1024        account_id: AccountId,
1025    ) -> Result<AccountVaultInfo, RpcError> {
1026        self.sync_account_vault_calls.fetch_add(1, Ordering::Relaxed);
1027
1028        let mut vault_patch = AccountVaultPatch::default();
1029        let mut current_block_from = block_from;
1030        let chain_tip = self.get_chain_tip_block_num();
1031        let target_block = block_to.min(chain_tip);
1032
1033        loop {
1034            let (page_chain_tip, page_block_number, page_patch) =
1035                self.get_sync_account_vault_request(current_block_from, block_to, account_id);
1036            vault_patch.merge(page_patch);
1037
1038            if page_block_number >= target_block {
1039                return Ok(AccountVaultInfo {
1040                    chain_tip: page_chain_tip,
1041                    block_number: page_block_number,
1042                    vault_patch,
1043                });
1044            }
1045
1046            current_block_from = (page_block_number.as_u32() + 1).into();
1047        }
1048    }
1049
1050    async fn sync_transactions(
1051        &self,
1052        block_from: BlockNumber,
1053        block_to: BlockNumber,
1054        account_ids: Vec<AccountId>,
1055    ) -> Result<Vec<TransactionRecord>, RpcError> {
1056        Ok(self.get_sync_transactions_request(block_from, block_to, &account_ids))
1057    }
1058
1059    async fn get_network_id(&self) -> Result<NetworkId, RpcError> {
1060        Ok(NetworkId::Testnet)
1061    }
1062
1063    async fn get_rpc_limits(&self) -> Result<crate::rpc::RpcLimits, RpcError> {
1064        Ok(crate::rpc::RpcLimits::default())
1065    }
1066
1067    fn has_rpc_limits(&self) -> Option<crate::rpc::RpcLimits> {
1068        None
1069    }
1070
1071    async fn set_rpc_limits(&self, _limits: crate::rpc::RpcLimits) {
1072        // No-op for mock client
1073    }
1074
1075    async fn get_status_unversioned(&self) -> Result<RpcStatusInfo, RpcError> {
1076        Ok(RpcStatusInfo {
1077            version: env!("CARGO_PKG_VERSION").into(),
1078            genesis_commitment: None,
1079            chain_tip: 0,
1080            block_producer: None,
1081        })
1082    }
1083
1084    async fn get_network_note_status(
1085        &self,
1086        _note_id: NoteId,
1087    ) -> Result<NetworkNoteStatusInfo, RpcError> {
1088        todo!("We need to check if we want to implement this for the mockchain");
1089    }
1090}
1091
1092// CONVERSIONS
1093// ================================================================================================
1094
1095impl From<MockChain> for MockRpcApi {
1096    fn from(mock_chain: MockChain) -> Self {
1097        MockRpcApi::new(mock_chain)
1098    }
1099}
1100
1101// HELPERS
1102// ================================================================================================
1103
1104fn build_account_updates(
1105    mock_chain: &MockChain,
1106) -> BTreeMap<BlockNumber, BTreeMap<AccountId, Word>> {
1107    let mut account_commitment_updates = BTreeMap::new();
1108    for block in mock_chain.proven_blocks() {
1109        let block_num = block.header().block_num();
1110        let mut updates = BTreeMap::new();
1111
1112        for update in block.body().updated_accounts() {
1113            updates.insert(update.account_id(), update.final_state_commitment());
1114        }
1115
1116        if updates.is_empty() {
1117            continue;
1118        }
1119
1120        account_commitment_updates.insert(block_num, updates);
1121    }
1122    account_commitment_updates
1123}