Skip to main content

miden_client/store/data_store/
mod.rs

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