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
50pub struct ClientDataStore {
55 store: alloc::sync::Arc<dyn Store>,
57 cache: DataStoreCache,
59 rpc_api: Arc<dyn NodeRpcClient>,
61 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 #[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 #[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 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 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 pub fn register_note_scripts(&self, note_scripts: impl IntoIterator<Item = NoteScript>) {
130 self.cache.insert_note_scripts(note_scripts);
131 }
132
133 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 if let Some(witness) = self.cache.get_storage_map_witness(map_root, map_key) {
524 return Ok(witness);
525 }
526
527 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 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 async fn get_foreign_account_inputs(
565 &self,
566 foreign_account_id: AccountId,
567 ref_block: BlockNumber,
568 ) -> Result<AccountInputs, DataStoreError> {
569 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 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 if let Some(note_script) = registered_script {
591 return Ok(Some(note_script));
592 }
593
594 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 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 if let Err(err) = store.upsert_note_scripts(core::slice::from_ref(¬e_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
628impl MastForestStore for ClientDataStore {
632 fn get(&self, procedure_hash: &Word) -> Option<LoadedMastForest> {
633 self.cache.mast_store.get(procedure_hash)
634 }
635}
636
637enum WitnessResolution {
643 Witness(StorageMapWitness),
644 FetchParams(StorageSlotName, AccountCode),
647}
648
649fn 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
679pub(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
714pub(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
748async 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
791async 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 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 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
837fn 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}