Skip to main content

miden_client/test_utils/
mock.rs

1use alloc::boxed::Box;
2use alloc::collections::{BTreeMap, BTreeSet};
3use alloc::sync::Arc;
4use alloc::vec::Vec;
5
6use miden_protocol::Word;
7use miden_protocol::account::delta::AccountUpdateDetails;
8use miden_protocol::account::{AccountCode, AccountId, StorageSlot, StorageSlotContent};
9use miden_protocol::address::NetworkId;
10use miden_protocol::block::{BlockHeader, BlockNumber, ProvenBlock};
11use miden_protocol::crypto::merkle::mmr::{Forest, Mmr, MmrProof};
12use miden_protocol::crypto::merkle::smt::SmtProof;
13use miden_protocol::note::{NoteHeader, NoteId, NoteScript, NoteTag, Nullifier};
14use miden_protocol::transaction::{ProvenTransaction, TransactionInputs};
15use miden_testing::{MockChain, MockChainNote};
16use miden_tx::utils::sync::RwLock;
17
18use crate::Client;
19use crate::rpc::domain::account::{
20    AccountDetails,
21    AccountProof,
22    AccountStorageDetails,
23    AccountStorageMapDetails,
24    AccountStorageRequirements,
25    AccountUpdateSummary,
26    AccountVaultDetails,
27    FetchedAccount,
28    StorageMapEntries,
29    StorageMapEntry,
30};
31use crate::rpc::domain::account_vault::{AccountVaultInfo, AccountVaultUpdate};
32use crate::rpc::domain::note::{
33    CommittedNote,
34    CommittedNoteMetadata,
35    FetchedNote,
36    NoteSyncBlock,
37    NoteSyncInfo,
38};
39use crate::rpc::domain::nullifier::NullifierUpdate;
40use crate::rpc::domain::storage_map::{StorageMapInfo, StorageMapUpdate};
41use crate::rpc::domain::sync::ChainMmrInfo;
42use crate::rpc::domain::transaction::{TransactionRecord, TransactionsInfo};
43use crate::rpc::{AccountStateAt, NodeRpcClient, RpcError, RpcStatusInfo};
44
45pub type MockClient<AUTH> = Client<AUTH>;
46
47/// Mock RPC API
48///
49/// This struct implements the RPC API used by the client to communicate with the node. It simulates
50/// most of the functionality of the actual node, with some small differences:
51/// - It uses a [`MockChain`] to simulate the blockchain state.
52/// - Blocks are not automatically created after time passes, but rather new blocks are created when
53///   calling the `prove_block` method.
54/// - Network account and transactions aren't supported in the current version.
55/// - Account update block numbers aren't tracked, so any endpoint that returns when certain account
56///   updates were made will return the chain tip block number instead.
57#[derive(Clone)]
58pub struct MockRpcApi {
59    account_commitment_updates: Arc<RwLock<BTreeMap<BlockNumber, BTreeMap<AccountId, Word>>>>,
60    pub mock_chain: Arc<RwLock<MockChain>>,
61}
62
63impl Default for MockRpcApi {
64    fn default() -> Self {
65        Self::new(MockChain::new())
66    }
67}
68
69impl MockRpcApi {
70    // Constant to use in mocked pagination.
71    const PAGINATION_BLOCK_LIMIT: u32 = 5;
72
73    /// Creates a new [`MockRpcApi`] instance with the state of the provided [`MockChain`].
74    pub fn new(mock_chain: MockChain) -> Self {
75        Self {
76            account_commitment_updates: Arc::new(RwLock::new(build_account_updates(&mock_chain))),
77            mock_chain: Arc::new(RwLock::new(mock_chain)),
78        }
79    }
80
81    /// Returns the current MMR of the blockchain.
82    pub fn get_mmr(&self) -> Mmr {
83        self.mock_chain.read().blockchain().as_mmr().clone()
84    }
85
86    /// Returns the chain tip block number.
87    pub fn get_chain_tip_block_num(&self) -> BlockNumber {
88        self.mock_chain.read().latest_block_header().block_num()
89    }
90
91    /// Advances the mock chain by proving the next block, committing all pending objects to the
92    /// chain in the process.
93    pub fn prove_block(&self) {
94        let proven_block = self.mock_chain.write().prove_next_block().unwrap();
95        let mut account_commitment_updates = self.account_commitment_updates.write();
96        let block_num = proven_block.header().block_num();
97        let updates: BTreeMap<AccountId, Word> = proven_block
98            .body()
99            .updated_accounts()
100            .iter()
101            .map(|update| (update.account_id(), update.final_state_commitment()))
102            .collect();
103
104        if !updates.is_empty() {
105            account_commitment_updates.insert(block_num, updates);
106        }
107    }
108
109    /// Retrieves a block by its block number.
110    fn get_block_by_num(&self, block_num: BlockNumber) -> BlockHeader {
111        self.mock_chain.read().block_header(block_num.as_usize())
112    }
113
114    /// Retrieves account vault updates in a given block range.
115    /// This method tries to simulate pagination by limiting the number of blocks processed per
116    /// request.
117    fn get_sync_account_vault_request(
118        &self,
119        block_from: BlockNumber,
120        block_to: Option<BlockNumber>,
121        account_id: AccountId,
122    ) -> AccountVaultInfo {
123        let chain_tip = self.get_chain_tip_block_num();
124        let target_block = block_to.unwrap_or(chain_tip).min(chain_tip);
125
126        let page_end_block: BlockNumber = (block_from.as_u32() + Self::PAGINATION_BLOCK_LIMIT)
127            .min(target_block.as_u32())
128            .into();
129
130        let mut updates = vec![];
131        for block in self.mock_chain.read().proven_blocks() {
132            let block_number = block.header().block_num();
133            // Only include blocks in range (block_from, page_end_block]
134            if block_number <= block_from || block_number > page_end_block {
135                continue;
136            }
137
138            for update in block
139                .body()
140                .updated_accounts()
141                .iter()
142                .filter(|block_acc_update| block_acc_update.account_id() == account_id)
143            {
144                let AccountUpdateDetails::Delta(account_delta) = update.details().clone() else {
145                    continue;
146                };
147
148                let vault_delta = account_delta.vault();
149
150                for asset in vault_delta.added_assets() {
151                    let account_vault_update = AccountVaultUpdate {
152                        block_num: block_number,
153                        asset: Some(asset),
154                        vault_key: asset.vault_key(),
155                    };
156                    updates.push(account_vault_update);
157                }
158            }
159        }
160
161        AccountVaultInfo {
162            chain_tip,
163            block_number: page_end_block,
164            updates,
165        }
166    }
167
168    /// Retrieves transactions in a given block range that match the provided account IDs
169    fn get_sync_transactions_request(
170        &self,
171        block_from: BlockNumber,
172        block_to: Option<BlockNumber>,
173        account_ids: &[AccountId],
174    ) -> TransactionsInfo {
175        let chain_tip = self.get_chain_tip_block_num();
176        let block_to = match block_to {
177            Some(block_to) => block_to,
178            None => chain_tip,
179        };
180
181        let mut transaction_records = vec![];
182        for block in self.mock_chain.read().proven_blocks() {
183            let block_number = block.header().block_num();
184            if block_number <= block_from || block_number > block_to {
185                continue;
186            }
187
188            for transaction_header in block.body().transactions().as_slice() {
189                if !account_ids.contains(&transaction_header.account_id()) {
190                    continue;
191                }
192
193                transaction_records.push(TransactionRecord {
194                    block_num: block_number,
195                    transaction_header: transaction_header.clone(),
196                    output_notes: vec![],
197                });
198            }
199        }
200
201        TransactionsInfo {
202            chain_tip,
203            block_num: block_to,
204            transaction_records,
205        }
206    }
207
208    /// Retrieves storage map updates in a given block range.
209    ///
210    /// This method tries to simulate pagination of the real node.
211    fn get_sync_storage_maps_request(
212        &self,
213        block_from: BlockNumber,
214        block_to: Option<BlockNumber>,
215        account_id: AccountId,
216    ) -> StorageMapInfo {
217        let chain_tip = self.get_chain_tip_block_num();
218        let target_block = block_to.unwrap_or(chain_tip).min(chain_tip);
219
220        let page_end_block: BlockNumber = (block_from.as_u32() + Self::PAGINATION_BLOCK_LIMIT)
221            .min(target_block.as_u32())
222            .into();
223
224        let mut updates = vec![];
225        for block in self.mock_chain.read().proven_blocks() {
226            let block_number = block.header().block_num();
227            if block_number <= block_from || block_number > page_end_block {
228                continue;
229            }
230
231            for update in block
232                .body()
233                .updated_accounts()
234                .iter()
235                .filter(|block_acc_update| block_acc_update.account_id() == account_id)
236            {
237                let AccountUpdateDetails::Delta(account_delta) = update.details().clone() else {
238                    continue;
239                };
240
241                let storage_delta = account_delta.storage();
242
243                for (slot_name, map_delta) in storage_delta.maps() {
244                    for (key, value) in map_delta.entries() {
245                        let storage_map_info = StorageMapUpdate {
246                            block_num: block_number,
247                            slot_name: slot_name.clone(),
248                            key: *key,
249                            value: *value,
250                        };
251                        updates.push(storage_map_info);
252                    }
253                }
254            }
255        }
256
257        StorageMapInfo {
258            chain_tip,
259            block_number: page_end_block,
260            updates,
261        }
262    }
263
264    pub fn get_available_notes(&self) -> Vec<MockChainNote> {
265        self.mock_chain.read().committed_notes().values().cloned().collect()
266    }
267
268    pub fn get_public_available_notes(&self) -> Vec<MockChainNote> {
269        self.mock_chain
270            .read()
271            .committed_notes()
272            .values()
273            .filter(|n| matches!(n, MockChainNote::Public(_, _)))
274            .cloned()
275            .collect()
276    }
277
278    pub fn get_private_available_notes(&self) -> Vec<MockChainNote> {
279        self.mock_chain
280            .read()
281            .committed_notes()
282            .values()
283            .filter(|n| matches!(n, MockChainNote::Private(_, _, _)))
284            .cloned()
285            .collect()
286    }
287
288    pub fn advance_blocks(&self, num_blocks: u32) {
289        let current_height = self.get_chain_tip_block_num();
290        let mut mock_chain = self.mock_chain.write();
291        mock_chain.prove_until_block(current_height + num_blocks).unwrap();
292    }
293}
294#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
295#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
296impl NodeRpcClient for MockRpcApi {
297    fn has_genesis_commitment(&self) -> Option<Word> {
298        None
299    }
300
301    async fn set_genesis_commitment(&self, _commitment: Word) -> Result<(), RpcError> {
302        // The mock client doesn't use accept headers, so we don't need to do anything here.
303        Ok(())
304    }
305
306    /// Returns note updates after the specified block number. Only notes that match the
307    /// provided tags will be returned, grouped by block.
308    async fn sync_notes(
309        &self,
310        block_num: BlockNumber,
311        block_to: Option<BlockNumber>,
312        note_tags: &BTreeSet<NoteTag>,
313    ) -> Result<NoteSyncInfo, RpcError> {
314        let chain_tip = self.get_chain_tip_block_num();
315        let upper_bound = block_to.unwrap_or(chain_tip);
316
317        // Collect all blocks with matching notes in the range (block_num, upper_bound]
318        let mut blocks_with_notes: BTreeMap<BlockNumber, BTreeMap<NoteId, CommittedNote>> =
319            BTreeMap::new();
320        for note in self.mock_chain.read().committed_notes().values() {
321            let note_block = note.inclusion_proof().location().block_num();
322            if note_tags.contains(&note.metadata().tag())
323                && note_block > block_num
324                && note_block <= upper_bound
325            {
326                let committed = CommittedNote::new(
327                    note.id(),
328                    CommittedNoteMetadata::Full(note.metadata().clone()),
329                    note.inclusion_proof().clone(),
330                );
331                blocks_with_notes.entry(note_block).or_default().insert(note.id(), committed);
332            }
333        }
334
335        // Always include the upper_bound block (with empty notes if needed), matching the
336        // node behavior where the range-end block is always present when the scan completes.
337        blocks_with_notes.entry(upper_bound).or_default();
338
339        let blocks: Vec<NoteSyncBlock> = blocks_with_notes
340            .into_iter()
341            .map(|(bn, notes)| {
342                let block_header = self.get_block_by_num(bn);
343                let mmr_path = self.get_mmr().open(bn.as_usize()).unwrap().merkle_path().clone();
344                NoteSyncBlock { block_header, mmr_path, notes }
345            })
346            .collect();
347
348        Ok(NoteSyncInfo { chain_tip, block_to: upper_bound, blocks })
349    }
350
351    async fn sync_chain_mmr(
352        &self,
353        block_from: BlockNumber,
354        block_to: Option<BlockNumber>,
355    ) -> Result<ChainMmrInfo, RpcError> {
356        let chain_tip = self.get_chain_tip_block_num();
357        let target_block = block_to.unwrap_or(chain_tip).min(chain_tip);
358
359        let from_forest = if block_from == chain_tip {
360            target_block.as_usize()
361        } else {
362            block_from.as_u32() as usize + 1
363        };
364
365        let mmr_delta = self
366            .get_mmr()
367            .get_delta(Forest::new(from_forest), Forest::new(target_block.as_usize()))
368            .unwrap();
369
370        let block_header = self.get_block_by_num(target_block);
371
372        Ok(ChainMmrInfo {
373            block_from,
374            block_to: target_block,
375            mmr_delta,
376            block_header,
377        })
378    }
379
380    /// Retrieves the block header for the specified block number. If the block number is not
381    /// provided, the chain tip block header will be returned.
382    async fn get_block_header_by_number(
383        &self,
384        block_num: Option<BlockNumber>,
385        include_mmr_proof: bool,
386    ) -> Result<(BlockHeader, Option<MmrProof>), RpcError> {
387        let block = if let Some(block_num) = block_num {
388            self.mock_chain.read().block_header(block_num.as_usize())
389        } else {
390            self.mock_chain.read().latest_block_header()
391        };
392
393        let mmr_proof = if include_mmr_proof {
394            Some(self.get_mmr().open(block_num.unwrap().as_usize()).unwrap())
395        } else {
396            None
397        };
398
399        Ok((block, mmr_proof))
400    }
401
402    /// Returns the node's tracked notes that match the provided note IDs.
403    async fn get_notes_by_id(&self, note_ids: &[NoteId]) -> Result<Vec<FetchedNote>, RpcError> {
404        // assume all public notes for now
405        let notes = self.mock_chain.read().committed_notes().clone();
406
407        let hit_notes = note_ids.iter().filter_map(|id| notes.get(id));
408        let mut return_notes = vec![];
409        for note in hit_notes {
410            let fetched_note = match note {
411                MockChainNote::Private(note_id, note_metadata, note_inclusion_proof) => {
412                    let note_header = NoteHeader::new(*note_id, note_metadata.clone());
413                    FetchedNote::Private(note_header, note_inclusion_proof.clone())
414                },
415                MockChainNote::Public(note, note_inclusion_proof) => {
416                    FetchedNote::Public(note.clone(), note_inclusion_proof.clone())
417                },
418            };
419            return_notes.push(fetched_note);
420        }
421        Ok(return_notes)
422    }
423
424    /// Simulates the submission of a proven transaction to the node. This will create a new block
425    /// just for the new transaction and return the block number of the newly created block.
426    async fn submit_proven_transaction(
427        &self,
428        proven_transaction: ProvenTransaction,
429        _tx_inputs: TransactionInputs, // Unnecessary for testing client itself.
430    ) -> Result<BlockNumber, RpcError> {
431        // TODO: add some basic validations to test error cases
432
433        {
434            let mut mock_chain = self.mock_chain.write();
435            mock_chain.add_pending_proven_transaction(proven_transaction.clone());
436        };
437
438        let block_num = self.get_chain_tip_block_num();
439
440        Ok(block_num)
441    }
442
443    /// Returns the node's tracked account details for the specified account ID.
444    async fn get_account_details(&self, account_id: AccountId) -> Result<FetchedAccount, RpcError> {
445        let summary =
446            self.account_commitment_updates
447                .read()
448                .iter()
449                .rev()
450                .find_map(|(block_num, updates)| {
451                    updates.get(&account_id).map(|commitment| AccountUpdateSummary {
452                        commitment: *commitment,
453                        last_block_num: *block_num,
454                    })
455                });
456
457        if let Ok(account) = self.mock_chain.read().committed_account(account_id) {
458            let summary = summary.unwrap_or_else(|| AccountUpdateSummary {
459                commitment: account.to_commitment(),
460                last_block_num: BlockNumber::GENESIS,
461            });
462            Ok(FetchedAccount::new_public(account.clone(), summary))
463        } else if let Some(summary) = summary {
464            Ok(FetchedAccount::new_private(account_id, summary))
465        } else {
466            Err(RpcError::ExpectedDataMissing(format!(
467                "account {account_id} not found in mock commitment updates or mock chain"
468            )))
469        }
470    }
471
472    /// Returns the account proof for the specified account. The `known_account_code` parameter
473    /// is ignored in the mock implementation and the latest account code is always returned.
474    async fn get_account_proof(
475        &self,
476        account_id: AccountId,
477        account_storage_requirements: AccountStorageRequirements,
478        account_state: AccountStateAt,
479        _known_account_code: Option<AccountCode>,
480        _known_vault_commitment: Option<Word>,
481    ) -> Result<(BlockNumber, AccountProof), RpcError> {
482        let mock_chain = self.mock_chain.read();
483
484        let block_number = match account_state {
485            AccountStateAt::Block(number) => number,
486            AccountStateAt::ChainTip => mock_chain.latest_block_header().block_num(),
487        };
488
489        let headers = if account_id.has_public_state() {
490            let account = mock_chain.committed_account(account_id).unwrap();
491
492            let mut map_details = vec![];
493            for slot_name in account_storage_requirements.inner().keys() {
494                if let Some(StorageSlotContent::Map(storage_map)) =
495                    account.storage().get(slot_name).map(StorageSlot::content)
496                {
497                    let entries: Vec<StorageMapEntry> = storage_map
498                        .entries()
499                        .map(|(key, value)| StorageMapEntry { key: *key, value: *value })
500                        .collect();
501
502                    let too_many_entries = entries.len() > 1000;
503                    let account_storage_map_detail = AccountStorageMapDetails {
504                        slot_name: slot_name.clone(),
505                        too_many_entries,
506                        entries: StorageMapEntries::AllEntries(entries),
507                    };
508
509                    map_details.push(account_storage_map_detail);
510                } else {
511                    panic!("Storage slot {slot_name} is not a map");
512                }
513            }
514
515            let storage_details = AccountStorageDetails {
516                header: account.storage().to_header(),
517                map_details,
518            };
519
520            let mut assets = vec![];
521            for asset in account.vault().assets() {
522                assets.push(asset);
523            }
524            let vault_details = AccountVaultDetails {
525                too_many_assets: assets.len() > 1000,
526                assets,
527            };
528
529            Some(AccountDetails {
530                header: account.into(),
531                storage_details,
532                code: account.code().clone(),
533                vault_details,
534            })
535        } else {
536            None
537        };
538
539        let witness = mock_chain.account_tree().open(account_id);
540
541        let proof = AccountProof::new(witness, headers).unwrap();
542
543        Ok((block_number, proof))
544    }
545
546    /// Returns the nullifiers created after the specified block number that match the provided
547    /// prefixes.
548    async fn sync_nullifiers(
549        &self,
550        prefixes: &[u16],
551        from_block_num: BlockNumber,
552        block_to: Option<BlockNumber>,
553    ) -> Result<Vec<NullifierUpdate>, RpcError> {
554        let nullifiers = self
555            .mock_chain
556            .read()
557            .nullifier_tree()
558            .entries()
559            .filter_map(|(nullifier, block_num)| {
560                let within_range = if let Some(to_block) = block_to {
561                    block_num >= from_block_num && block_num <= to_block
562                } else {
563                    block_num >= from_block_num
564                };
565
566                if prefixes.contains(&nullifier.prefix()) && within_range {
567                    Some(NullifierUpdate { nullifier, block_num })
568                } else {
569                    None
570                }
571            })
572            .collect::<Vec<_>>();
573
574        Ok(nullifiers)
575    }
576
577    /// Returns proofs for all the provided nullifiers.
578    async fn check_nullifiers(&self, nullifiers: &[Nullifier]) -> Result<Vec<SmtProof>, RpcError> {
579        Ok(nullifiers
580            .iter()
581            .map(|nullifier| self.mock_chain.read().nullifier_tree().open(nullifier).into_proof())
582            .collect())
583    }
584
585    async fn get_block_by_number(&self, block_num: BlockNumber) -> Result<ProvenBlock, RpcError> {
586        let block = self
587            .mock_chain
588            .read()
589            .proven_blocks()
590            .iter()
591            .find(|b| b.header().block_num() == block_num)
592            .unwrap()
593            .clone();
594
595        Ok(block)
596    }
597
598    async fn get_note_script_by_root(&self, root: Word) -> Result<NoteScript, RpcError> {
599        let note = self
600            .get_available_notes()
601            .iter()
602            .find(|note| note.note().is_some_and(|n| n.script().root() == root))
603            .unwrap()
604            .clone();
605
606        Ok(note.note().unwrap().script().clone())
607    }
608
609    async fn sync_storage_maps(
610        &self,
611        block_from: BlockNumber,
612        block_to: Option<BlockNumber>,
613        account_id: AccountId,
614    ) -> Result<StorageMapInfo, RpcError> {
615        let mut all_updates = Vec::new();
616        let mut current_block_from = block_from;
617        let chain_tip = self.get_chain_tip_block_num();
618        let target_block = block_to.unwrap_or(chain_tip).min(chain_tip);
619
620        loop {
621            let response =
622                self.get_sync_storage_maps_request(current_block_from, block_to, account_id);
623            all_updates.extend(response.updates);
624
625            if response.block_number >= target_block {
626                return Ok(StorageMapInfo {
627                    chain_tip: response.chain_tip,
628                    block_number: response.block_number,
629                    updates: all_updates,
630                });
631            }
632
633            current_block_from = (response.block_number.as_u32() + 1).into();
634        }
635    }
636
637    async fn sync_account_vault(
638        &self,
639        block_from: BlockNumber,
640        block_to: Option<BlockNumber>,
641        account_id: AccountId,
642    ) -> Result<AccountVaultInfo, RpcError> {
643        let mut all_updates = Vec::new();
644        let mut current_block_from = block_from;
645        let chain_tip = self.get_chain_tip_block_num();
646        let target_block = block_to.unwrap_or(chain_tip).min(chain_tip);
647
648        loop {
649            let response =
650                self.get_sync_account_vault_request(current_block_from, block_to, account_id);
651            all_updates.extend(response.updates);
652
653            if response.block_number >= target_block {
654                return Ok(AccountVaultInfo {
655                    chain_tip: response.chain_tip,
656                    block_number: response.block_number,
657                    updates: all_updates,
658                });
659            }
660
661            current_block_from = (response.block_number.as_u32() + 1).into();
662        }
663    }
664
665    async fn sync_transactions(
666        &self,
667        block_from: BlockNumber,
668        block_to: Option<BlockNumber>,
669        account_ids: Vec<AccountId>,
670    ) -> Result<TransactionsInfo, RpcError> {
671        let response = self.get_sync_transactions_request(block_from, block_to, &account_ids);
672        Ok(response)
673    }
674
675    async fn get_network_id(&self) -> Result<NetworkId, RpcError> {
676        Ok(NetworkId::Testnet)
677    }
678
679    async fn get_rpc_limits(&self) -> Result<crate::rpc::RpcLimits, RpcError> {
680        Ok(crate::rpc::RpcLimits::default())
681    }
682
683    fn has_rpc_limits(&self) -> Option<crate::rpc::RpcLimits> {
684        None
685    }
686
687    async fn set_rpc_limits(&self, _limits: crate::rpc::RpcLimits) {
688        // No-op for mock client
689    }
690
691    async fn get_status_unversioned(&self) -> Result<RpcStatusInfo, RpcError> {
692        Ok(RpcStatusInfo {
693            version: env!("CARGO_PKG_VERSION").into(),
694            genesis_commitment: None,
695            store: None,
696            block_producer: None,
697        })
698    }
699}
700
701// CONVERSIONS
702// ================================================================================================
703
704impl From<MockChain> for MockRpcApi {
705    fn from(mock_chain: MockChain) -> Self {
706        MockRpcApi::new(mock_chain)
707    }
708}
709
710// HELPERS
711// ================================================================================================
712
713fn build_account_updates(
714    mock_chain: &MockChain,
715) -> BTreeMap<BlockNumber, BTreeMap<AccountId, Word>> {
716    let mut account_commitment_updates = BTreeMap::new();
717    for block in mock_chain.proven_blocks() {
718        let block_num = block.header().block_num();
719        let mut updates = BTreeMap::new();
720
721        for update in block.body().updated_accounts() {
722            updates.insert(update.account_id(), update.final_state_commitment());
723        }
724
725        if updates.is_empty() {
726            continue;
727        }
728
729        account_commitment_updates.insert(block_num, updates);
730    }
731    account_commitment_updates
732}