Skip to main content

miden_client/store/data_store/
mod.rs

1use alloc::boxed::Box;
2use alloc::collections::BTreeSet;
3use alloc::sync::Arc;
4use alloc::vec::Vec;
5
6use miden_protocol::account::{
7    Account,
8    AccountCode,
9    AccountId,
10    PartialAccount,
11    StorageMapKey,
12    StorageMapWitness,
13    StorageSlot,
14    StorageSlotContent,
15    StorageSlotName,
16};
17use miden_protocol::asset::{AssetId, AssetVault, AssetWitness};
18use miden_protocol::block::{BlockHeader, BlockNumber};
19use miden_protocol::crypto::merkle::MerklePath;
20use miden_protocol::crypto::merkle::mmr::{InOrderIndex, MmrPeaks, PartialMmr};
21use miden_protocol::note::{NoteScript, NoteScriptRoot};
22use miden_protocol::transaction::{AccountInputs, PartialBlockchain};
23use miden_protocol::vm::FutureMaybeSend;
24use miden_protocol::{Word, ZERO};
25use miden_tx::{
26    DataStore,
27    DataStoreError,
28    LoadedMastForest,
29    MastForestStore,
30    TransactionMastStore,
31};
32
33use super::{AccountStorageFilter, PartialBlockchainFilter, Store};
34use crate::rpc::domain::account::{
35    AccountStorageRequirements,
36    GetAccountRequest,
37    StorageMapEntries,
38    StorageMapFetch,
39    VaultFetch,
40};
41use crate::rpc::{AccountStateAt, NodeRpcClient};
42use crate::store::StoreError;
43use crate::transaction::{ChainAnchor, ChainAnchorError, fetch_public_account_inputs};
44
45mod cache;
46use cache::DataStoreCache;
47
48// DATA STORE
49// ================================================================================================
50
51/// Wrapper structure that implements [`DataStore`] over any [`Store`].
52pub struct ClientDataStore {
53    /// Local database containing information about the accounts managed by this client.
54    store: alloc::sync::Arc<dyn Store>,
55    /// In-memory state served to the executor for the duration of the execution session.
56    cache: DataStoreCache,
57    /// RPC client used to lazy-load foreign account data on cache miss.
58    rpc_api: Arc<dyn NodeRpcClient>,
59    /// When set, chain data (reference block header and partial blockchain) is served from this
60    /// anchor instead of being rebuilt at the store's sync height. Boxed to keep the data store
61    /// small: it is held inline by every execution future.
62    anchor: Option<Box<ChainAnchor>>,
63}
64
65impl ClientDataStore {
66    pub fn new(store: alloc::sync::Arc<dyn Store>, rpc_api: Arc<dyn NodeRpcClient>) -> Self {
67        Self {
68            store,
69            cache: DataStoreCache::new(),
70            rpc_api,
71            anchor: None,
72        }
73    }
74
75    /// Serves chain data from the provided [`ChainAnchor`] instead of rebuilding it at the
76    /// store's sync height, pinning execution to the anchor's reference block.
77    ///
78    /// The store's account data is still used as-is: only the reference block header and the
79    /// partial blockchain come from the anchor. Any authenticated input note must have been
80    /// created in a block tracked by the anchor's partial blockchain, otherwise
81    /// `get_transaction_inputs` fails.
82    #[must_use]
83    pub fn with_chain_anchor(mut self, anchor: ChainAnchor) -> Self {
84        self.anchor = Some(Box::new(anchor));
85        self
86    }
87
88    /// Enables memoization of `get_transaction_inputs` and `get_vault_asset_witnesses` for the
89    /// lifetime of this data store.
90    ///
91    /// This is only correct when the account state served to the executor does not change between
92    /// executions, as is the case while the [`crate::note::NoteScreener`] runs trial executions
93    /// against the same accounts and reference block. It must stay disabled for real transaction
94    /// execution, where account state evolves between executions.
95    #[must_use]
96    pub fn with_execution_input_cache(mut self) -> Self {
97        self.cache.enable_execution_input_cache();
98        self
99    }
100
101    pub fn mast_store(&self) -> Arc<TransactionMastStore> {
102        self.cache.mast_store.clone()
103    }
104
105    /// Stores the provided foreign account inputs so they can be served to the executor upon
106    /// request.
107    pub fn register_foreign_account_inputs(
108        &self,
109        foreign_accounts: impl IntoIterator<Item = AccountInputs>,
110    ) {
111        self.cache.replace_foreign_account_inputs(foreign_accounts);
112    }
113
114    /// Registers note scripts so they can be served to the executor upon request.
115    ///
116    /// Scripts accumulate across calls (they are not cleared) so that a data store reused for
117    /// several executions — e.g. by [`crate::transaction::BatchBuilder`] — keeps serving the
118    /// scripts registered for earlier transactions.
119    pub fn register_note_scripts(&self, note_scripts: impl IntoIterator<Item = NoteScript>) {
120        self.cache.insert_note_scripts(note_scripts);
121    }
122
123    /// Attempts to resolve a storage map witness from the local store.
124    ///
125    /// This covers any account present in the store (local or foreign) as well as any
126    /// foreign account previously cached in `foreign_account_inputs`.
127    ///
128    /// Returns `Ok(None)` when the map is not found locally.
129    async fn get_local_storage_map_witness(
130        &self,
131        account_id: AccountId,
132        map_root: Word,
133        map_key: StorageMapKey,
134    ) -> Result<Option<StorageMapWitness>, DataStoreError> {
135        match self
136            .store
137            .get_account_storage(account_id, AccountStorageFilter::Root(map_root))
138            .await
139        {
140            Ok(account_storage) => {
141                match account_storage.slots().first().map(StorageSlot::content) {
142                    Some(StorageSlotContent::Map(map)) => Ok(Some(map.open(&map_key))),
143                    Some(StorageSlotContent::Value(value)) => Err(DataStoreError::other(format!(
144                        "found StorageSlotContent::Value with {value} as its value."
145                    ))),
146                    _ => Ok(None),
147                }
148            },
149            Err(err) => {
150                tracing::debug!(
151                    %account_id,
152                    %err,
153                    "storage map not found locally, will try remote fetch"
154                );
155                Ok(None)
156            },
157        }
158    }
159
160    /// Lazily fetches a foreign account's inputs from the network, loads its code into the MAST
161    /// store, and caches the result in [`Self::foreign_account_inputs`].
162    async fn fetch_and_cache_foreign_account(
163        &self,
164        account_id: AccountId,
165        account_state_at: AccountStateAt,
166    ) -> Result<AccountInputs, DataStoreError> {
167        let account_inputs = fetch_public_account_inputs(
168            &self.store,
169            &self.rpc_api,
170            account_id,
171            AccountStorageRequirements::default(),
172            account_state_at,
173        )
174        .await
175        .map_err(|err| {
176            DataStoreError::other_with_source("failed to fetch foreign account inputs", err)
177        })?;
178
179        self.cache.mast_store.load_account_code(account_inputs.code());
180        self.cache.insert_foreign_account_inputs(account_inputs.clone());
181
182        Ok(account_inputs)
183    }
184
185    /// Fetches a storage map witness for a specific key from the network via RPC and caches it.
186    ///
187    /// Anchored at the transaction reference block: a chain-tip query would return proofs for a
188    /// newer map root whenever the account changed after the caller's last sync.
189    async fn fetch_and_cache_storage_map_witness(
190        &self,
191        account_id: AccountId,
192        map_root: Word,
193        slot_name: StorageSlotName,
194        map_key: StorageMapKey,
195        known_code: AccountCode,
196    ) -> Result<StorageMapWitness, DataStoreError> {
197        let account_state_at =
198            self.cache.ref_block().map_or(AccountStateAt::ChainTip, AccountStateAt::Block);
199
200        let storage_requirements = AccountStorageRequirements::new([(slot_name, &[map_key])]);
201        let (_, account_proof): (BlockNumber, _) = self
202            .rpc_api
203            .get_account(
204                account_id,
205                GetAccountRequest::new()
206                    .with_storage(StorageMapFetch::Slots(storage_requirements))
207                    .with_known_code(Some(known_code))
208                    .at(account_state_at),
209            )
210            .await
211            .map_err(|err| {
212                DataStoreError::other_with_source("failed to fetch storage map via RPC", err)
213            })?;
214
215        let (_, account_details) = account_proof.into_parts();
216        let details = account_details.ok_or_else(|| {
217            DataStoreError::other(format!(
218                "RPC returned no account details for account {account_id}"
219            ))
220        })?;
221
222        let map_detail =
223            details.storage_details.map_details.into_iter().next().ok_or_else(|| {
224                DataStoreError::other(format!(
225                    "RPC returned no storage map details for account {account_id}"
226                ))
227            })?;
228
229        let StorageMapEntries::PartialMap { partial_smt, .. } = map_detail.entries else {
230            return Err(DataStoreError::other(
231                "expected a partial storage map in response to a specific-key request",
232            ));
233        };
234
235        // Reject a wrong-root response here rather than as an opaque merkle error inside the VM.
236        // The whole tree shares one root, so this covers every opening taken from it.
237        let map_detail_root = partial_smt.root();
238        if map_detail_root != map_root {
239            return Err(DataStoreError::other(format!(
240                "storage map fetched for account {account_id} verifies against root \
241                 {map_detail_root} but the executor requires root {map_root}"
242            )));
243        }
244
245        let proof = partial_smt.open(&map_key.hash().as_word()).map_err(|err| {
246            DataStoreError::other_with_source("failed to open the requested storage map key", err)
247        })?;
248
249        let witness = StorageMapWitness::new(proof, [map_key]).map_err(|err| {
250            DataStoreError::other_with_source("failed to create storage map witness", err)
251        })?;
252        self.cache.insert_storage_map_witness(map_root, map_key, witness.clone());
253        Ok(witness)
254    }
255
256    /// Fetches an account's full vault via RPC — anchored at the transaction reference block —
257    /// and verifies it against the vault root the executor requires. Fallback for vault reads
258    /// the local store cannot serve, typically foreign accounts whose [`AccountInputs`] carry
259    /// only their vault root.
260    async fn fetch_vault_via_rpc(
261        &self,
262        account_id: AccountId,
263        vault_root: Word,
264    ) -> Result<AssetVault, DataStoreError> {
265        let account_state_at =
266            self.cache.ref_block().map_or(AccountStateAt::ChainTip, AccountStateAt::Block);
267
268        // The cached foreign inputs hold the code, letting the node omit it from the response.
269        let known_code = self
270            .cache
271            .with_foreign_account_inputs(account_id, |inputs| inputs.code().clone());
272
273        let (block_num, mut account_proof) = self
274            .rpc_api
275            .get_account(
276                account_id,
277                GetAccountRequest::new()
278                    .at(account_state_at)
279                    .with_known_code(known_code)
280                    .with_vault(VaultFetch::Always),
281            )
282            .await
283            .map_err(|err| {
284                DataStoreError::other_with_source("failed to fetch account vault via RPC", err)
285            })?;
286
287        let details = account_proof.details_mut().ok_or_else(|| {
288            DataStoreError::other(format!(
289                "RPC returned no account details for account {account_id}"
290            ))
291        })?;
292
293        self.rpc_api
294            .resolve_oversize_vault(account_id, block_num, details)
295            .await
296            .map_err(|err| {
297                DataStoreError::other_with_source("failed to resolve oversize vault via RPC", err)
298            })?;
299
300        let vault = AssetVault::new(&details.vault_details.assets).map_err(|err| {
301            DataStoreError::other_with_source("failed to build the fetched vault", err)
302        })?;
303
304        if vault.root() != vault_root {
305            return Err(DataStoreError::other(format!(
306                "vault fetched for account {account_id} has root {} but the executor requires \
307                 root {vault_root}",
308                vault.root()
309            )));
310        }
311
312        Ok(vault)
313    }
314}
315
316impl DataStore for ClientDataStore {
317    async fn get_transaction_inputs(
318        &self,
319        account_id: AccountId,
320        mut block_refs: BTreeSet<BlockNumber>,
321    ) -> Result<(PartialAccount, BlockHeader, PartialBlockchain), DataStoreError> {
322        // Last block is used as reference (it does not need to be authenticated manually)
323        let ref_block = *block_refs.last().ok_or(DataStoreError::other("block set is empty"))?;
324
325        // Cache the reference block so lazy-loading methods can use it
326        self.cache.set_ref_block(ref_block);
327
328        let partial_account =
329            if let Some(partial_account) = self.cache.get_partial_account(account_id) {
330                partial_account
331            } else {
332                let partial_account_record = self
333                    .store
334                    .get_minimal_partial_account(account_id)
335                    .await?
336                    .ok_or(DataStoreError::AccountNotFound(account_id))?;
337
338                // New accounts (nonce == 0) need full storage maps as advice inputs for the
339                // kernel to validate during account creation. For these, fetch the full account
340                // and convert to PartialAccount (which includes full storage for new accounts).
341                // Existing accounts use the minimal partial record directly.
342                let partial_account: PartialAccount = if partial_account_record.nonce() == ZERO {
343                    let full_record = self
344                        .store
345                        .get_account(account_id)
346                        .await?
347                        .ok_or(DataStoreError::AccountNotFound(account_id))?;
348                    let account: Account = full_record
349                        .try_into()
350                        .map_err(|_| DataStoreError::AccountNotFound(account_id))?;
351                    PartialAccount::from(&account)
352                } else {
353                    partial_account_record
354                        .try_into()
355                        .map_err(|_| DataStoreError::AccountNotFound(account_id))?
356                };
357
358                self.cache.insert_partial_account(&partial_account);
359                partial_account
360            };
361
362        let (block_header, partial_blockchain) = if let Some(anchor) = &self.anchor {
363            // Anchored execution: serve the pinned chain data. The executor-derived reference
364            // block must match the anchor, and every other block in the set (input note creation
365            // blocks) must already be tracked by the anchor's partial blockchain.
366            if ref_block != anchor.block_num() {
367                return Err(DataStoreError::other_with_source(
368                    "anchored data store cannot serve the requested reference block",
369                    ChainAnchorError::ReferenceBlockMismatch {
370                        requested: ref_block,
371                        anchor: anchor.block_num(),
372                    },
373                ));
374            }
375
376            for block_num in block_refs.iter().filter(|block_num| **block_num != ref_block) {
377                if !anchor.partial_blockchain().contains_block(*block_num) {
378                    return Err(DataStoreError::other_with_source(
379                        "anchored data store cannot serve an untracked block",
380                        ChainAnchorError::BlockNotTracked { block_num: *block_num },
381                    ));
382                }
383            }
384
385            (anchor.header().clone(), anchor.partial_blockchain().clone())
386        } else if let Some((block_header, partial_blockchain)) =
387            self.cache.get_blockchain(&block_refs)
388        {
389            (block_header, partial_blockchain)
390        } else {
391            // The full set identifies the served blockchain, so keep it as the cache key before
392            // the reference block is removed from it below.
393            let cache_key = block_refs.clone();
394            block_refs.remove(&ref_block);
395
396            let current_peaks = self.store.get_current_blockchain_peaks().await?;
397
398            // Get header data
399            let (block_header, _had_notes) = self
400                .store
401                .get_block_header_by_num(ref_block)
402                .await?
403                .ok_or(DataStoreError::BlockNotFound(ref_block))?;
404
405            let block_headers: Vec<BlockHeader> = self
406                .store
407                .get_block_headers(&block_refs)
408                .await?
409                .into_iter()
410                .map(|(header, _has_notes)| header)
411                .collect();
412
413            // TODO: the client stores only the peaks of the MMR at the current sync height, so we
414            // are not actually following the block_ref here. If the block_ref !=
415            // current_sync_height, this would return an invalid partial blockchain.
416            let partial_mmr =
417                build_partial_mmr_with_paths(&self.store, current_peaks, &block_headers).await?;
418
419            let partial_blockchain =
420                PartialBlockchain::new(partial_mmr, block_headers).map_err(|err| {
421                    DataStoreError::other_with_source(
422                        "error creating PartialBlockchain from internal data",
423                        err,
424                    )
425                })?;
426
427            self.cache.insert_blockchain(cache_key, &block_header, &partial_blockchain);
428            (block_header, partial_blockchain)
429        };
430
431        Ok((partial_account, block_header, partial_blockchain))
432    }
433
434    /// Retrieves witnesses for the requested assets from the local store, falling back to a
435    /// single RPC vault fetch when the store cannot serve the requested root.
436    ///
437    /// Assets absent from the vault are served too: the store returns an emptiness proof for
438    /// them, which the executor needs when an asset is being added to the vault.
439    async fn get_vault_asset_witnesses(
440        &self,
441        account_id: AccountId,
442        vault_root: Word,
443        asset_ids: BTreeSet<AssetId>,
444    ) -> Result<Vec<AssetWitness>, DataStoreError> {
445        if let Some(witnesses) = self.cache.get_vault_asset_witnesses(vault_root, &asset_ids) {
446            return Ok(witnesses);
447        }
448
449        let asset_witnesses = match self
450            .store
451            .get_vault_asset_witnesses(account_id, vault_root, asset_ids.clone())
452            .await
453        {
454            Ok(witnesses) => witnesses,
455            Err(err) => {
456                tracing::debug!(
457                    %account_id,
458                    requested_root = %vault_root,
459                    %err,
460                    "local store cannot serve the requested vault root, will fetch it via RPC"
461                );
462                let vault = self.fetch_vault_via_rpc(account_id, vault_root).await?;
463                asset_ids.iter().copied().map(|asset_id| vault.open(asset_id)).collect()
464            },
465        };
466
467        self.cache
468            .insert_vault_asset_witnesses(vault_root, &asset_ids, &asset_witnesses);
469        Ok(asset_witnesses)
470    }
471
472    /// Retrieves the [`StorageMapWitness`] requested from the store. Alternatively fetching it
473    /// from the RPC if not available locally. Witnesses fetched via RPC are cached in memory so
474    /// that repeated accesses to the same map entry within a transaction avoid additional RPC
475    /// calls.
476    async fn get_storage_map_witness(
477        &self,
478        account_id: AccountId,
479        map_root: Word,
480        map_key: StorageMapKey,
481    ) -> Result<StorageMapWitness, DataStoreError> {
482        // Check the in-memory witness cache first.
483        if let Some(witness) = self.cache.get_storage_map_witness(map_root, map_key) {
484            return Ok(witness);
485        }
486
487        // Try the local store.
488        if let Some(witness) =
489            self.get_local_storage_map_witness(account_id, map_root, map_key).await?
490        {
491            return Ok(witness);
492        }
493
494        // Resolve against the cached account inputs (without cloning them), fetching and caching
495        // the account first if it isn't cached yet.
496        let resolution = if let Some(resolution) =
497            self.cache.with_foreign_account_inputs(account_id, |inputs| {
498                resolve_witness_from_inputs(inputs, map_root, map_key)
499            }) {
500            resolution?
501        } else {
502            let account_state_at = self
503                .cache
504                .ref_block()
505                .map(AccountStateAt::Block)
506                .expect("reference block should be set");
507            let inputs = self.fetch_and_cache_foreign_account(account_id, account_state_at).await?;
508            resolve_witness_from_inputs(&inputs, map_root, map_key)?
509        };
510
511        match resolution {
512            WitnessResolution::Witness(witness) => Ok(witness),
513            WitnessResolution::FetchParams(slot_name, known_code) => {
514                self.fetch_and_cache_storage_map_witness(
515                    account_id, map_root, slot_name, map_key, known_code,
516                )
517                .await
518            },
519        }
520    }
521
522    /// Returns the [`AccountInputs`] for the given foreign account from the cache or alternatively
523    /// fetching them from the RPC if not available locally.
524    async fn get_foreign_account_inputs(
525        &self,
526        foreign_account_id: AccountId,
527        ref_block: BlockNumber,
528    ) -> Result<AccountInputs, DataStoreError> {
529        // Fast path: check the cache first.
530        if let Some(inputs) = self.cache.get_foreign_account_inputs(foreign_account_id) {
531            return Ok(inputs);
532        }
533
534        self.fetch_and_cache_foreign_account(foreign_account_id, AccountStateAt::Block(ref_block))
535            .await
536    }
537
538    /// Returns the [`NoteScript`] for the given script root from the registered session scripts,
539    /// the store, or alternatively fetching it from the RPC if not available locally.
540    fn get_note_script(
541        &self,
542        script_root: NoteScriptRoot,
543    ) -> impl FutureMaybeSend<Result<Option<NoteScript>, DataStoreError>> {
544        let registered_script = self.cache.get_note_script(script_root.into());
545        let store = self.store.clone();
546        let rpc_api = self.rpc_api.clone();
547
548        async move {
549            // Fastest path: scripts registered for the in-flight transaction request.
550            if let Some(note_script) = registered_script {
551                return Ok(Some(note_script));
552            }
553
554            // Fast path: check the local store first.
555            match store.get_note_script(script_root.into()).await {
556                Ok(note_script) => return Ok(Some(note_script)),
557                Err(StoreError::NoteScriptNotFound(_)) => {},
558                Err(err) => {
559                    return Err(DataStoreError::other_with_source(
560                        format!("failed to get note script {script_root} from store"),
561                        err,
562                    ));
563                },
564            }
565
566            // Store miss, fetch from the network via RPC.
567            let Some(note_script) =
568                rpc_api.get_note_script_by_root(script_root.into()).await.map_err(|err| {
569                    DataStoreError::other_with_source("failed to fetch note script via RPC", err)
570                })?
571            else {
572                return Ok(None);
573            };
574
575            // Persist for future lookups.
576            if let Err(err) = store.upsert_note_scripts(core::slice::from_ref(&note_script)).await {
577                tracing::warn!(
578                    %err,
579                    "Failed to persist fetched note script to store"
580                );
581            }
582
583            Ok(Some(note_script))
584        }
585    }
586}
587
588// MAST FOREST STORE
589// ================================================================================================
590
591impl MastForestStore for ClientDataStore {
592    fn get(&self, procedure_hash: &Word) -> Option<LoadedMastForest> {
593        self.cache.mast_store.get(procedure_hash)
594    }
595}
596
597// HELPER FUNCTIONS
598// ================================================================================================
599
600/// Outcome of resolving a storage map witness against an account's inputs: either the witness
601/// itself, or the parameters needed to fetch it via RPC.
602enum WitnessResolution {
603    Witness(StorageMapWitness),
604    /// The [`AccountCode`] is not needed to build the witness: it is only sent along with the
605    /// RPC request so the node can omit the account code from its response.
606    FetchParams(StorageSlotName, AccountCode),
607}
608
609/// Tries to open the witness from the inputs' partial storage maps (this can miss if the
610/// account's storage is too big); on a miss, resolves the slot name and account code needed to
611/// fetch the witness via RPC.
612fn resolve_witness_from_inputs(
613    inputs: &AccountInputs,
614    map_root: Word,
615    map_key: StorageMapKey,
616) -> Result<WitnessResolution, DataStoreError> {
617    if let Some(partial_map) = inputs.storage().maps().find(|m| m.root() == map_root)
618        && let Ok(witness) = partial_map.open(&map_key)
619    {
620        return Ok(WitnessResolution::Witness(witness));
621    }
622
623    let account_id = inputs.id();
624    let slot_name = inputs
625        .storage()
626        .header()
627        .slots()
628        .find(|slot| slot.slot_type().is_map() && slot.value() == map_root)
629        .map(|slot| slot.name().clone())
630        .ok_or_else(|| {
631            DataStoreError::other(format!(
632                "did not find map slot with root {map_root} for foreign account {account_id}"
633            ))
634        })?;
635
636    Ok(WitnessResolution::FetchParams(slot_name, inputs.code().clone()))
637}
638
639/// Builds a [`PartialMmr`] from the given peaks and a list of blocks that should be
640/// authenticated against them.
641///
642/// `authenticated_blocks` must not contain the block whose forest matches `peaks`. For that
643/// block the kernel extends the MMR itself, so an authentication path is not needed.
644pub(crate) async fn build_partial_mmr_with_paths(
645    store: &alloc::sync::Arc<dyn Store>,
646    peaks: MmrPeaks,
647    authenticated_blocks: &[BlockHeader],
648) -> Result<PartialMmr, DataStoreError> {
649    let mut partial_mmr: PartialMmr = PartialMmr::from_peaks(peaks);
650
651    let block_nums: Vec<BlockNumber> =
652        authenticated_blocks.iter().map(BlockHeader::block_num).collect();
653
654    let authentication_paths =
655        get_authentication_path_for_blocks(store, &block_nums, partial_mmr.forest().num_leaves())
656            .await?;
657
658    for (header, path) in authenticated_blocks.iter().zip(authentication_paths.iter()) {
659        partial_mmr
660            .track(header.block_num().as_usize(), header.commitment(), path)
661            .map_err(|err| DataStoreError::other(format!("error constructing MMR: {err}")))?;
662    }
663
664    Ok(partial_mmr)
665}
666
667/// Retrieves all Partial Blockchain nodes required for authenticating the set of blocks, and then
668/// constructs the path for each of them.
669///
670/// This function assumes `block_nums` doesn't contain values above or equal to `forest`.
671/// If there are any such values, the function will panic when calling `mmr_merkle_path_len()`.
672async fn get_authentication_path_for_blocks(
673    store: &alloc::sync::Arc<dyn Store>,
674    block_nums: &[BlockNumber],
675    forest: usize,
676) -> Result<Vec<MerklePath>, StoreError> {
677    let mut node_indices = BTreeSet::new();
678
679    // Calculate all needed nodes indices for generating the paths
680    for block_num in block_nums {
681        let path_depth = mmr_merkle_path_len(block_num.as_usize(), forest);
682
683        let mut idx = InOrderIndex::from_leaf_pos(block_num.as_usize());
684
685        for _ in 0..path_depth {
686            node_indices.insert(idx.sibling());
687            idx = idx.parent();
688        }
689    }
690
691    // Get all MMR nodes based on collected indices
692    let node_indices: Vec<InOrderIndex> = node_indices.into_iter().collect();
693
694    let filter = PartialBlockchainFilter::List(node_indices);
695    let mmr_nodes = store.get_partial_blockchain_nodes(filter).await?;
696
697    // Construct authentication paths
698    let mut authentication_paths = vec![];
699    for block_num in block_nums {
700        let mut merkle_nodes = vec![];
701        let mut idx = InOrderIndex::from_leaf_pos(block_num.as_usize());
702
703        while let Some(node) = mmr_nodes.get(&idx.sibling()) {
704            merkle_nodes.push(*node);
705            idx = idx.parent();
706        }
707        let path = MerklePath::new(merkle_nodes);
708        authentication_paths.push(path);
709    }
710
711    Ok(authentication_paths)
712}
713
714/// Calculates the merkle path length for an MMR of a specific forest and a leaf index
715/// `leaf_index` is a 0-indexed leaf number and `forest` is the total amount of leaves
716/// in the MMR at this point.
717fn mmr_merkle_path_len(leaf_index: usize, forest: usize) -> usize {
718    let before: usize = forest & leaf_index;
719    let after = forest ^ before;
720
721    after.ilog2() as usize
722}