soroban_env_host/
storage.rs

1//! This module contains the [Storage] type and its supporting types, which
2//! provide the [Host](crate::Host) with access to durable ledger entries.
3//!
4//! For more details, see the [Env](crate::Env) data access functions:
5//!   - [Env::has_contract_data](crate::Env::has_contract_data)
6//!   - [Env::get_contract_data](crate::Env::get_contract_data)
7//!   - [Env::put_contract_data](crate::Env::put_contract_data)
8//!   - [Env::del_contract_data](crate::Env::del_contract_data)
9
10use std::rc::Rc;
11
12use crate::budget::AsBudget;
13use crate::host::metered_clone::MeteredClone;
14use crate::{
15    budget::Budget,
16    host::metered_map::MeteredOrdMap,
17    ledger_info::get_key_durability,
18    xdr::{ContractDataDurability, LedgerEntry, LedgerKey, ScErrorCode, ScErrorType, ScVal},
19    Env, Error, Host, HostError, Val,
20};
21
22pub type FootprintMap = MeteredOrdMap<Rc<LedgerKey>, AccessType, Budget>;
23pub type EntryWithLiveUntil = (Rc<LedgerEntry>, Option<u32>);
24pub type StorageMap = MeteredOrdMap<Rc<LedgerKey>, Option<EntryWithLiveUntil>, Budget>;
25
26/// The in-memory instance storage of the current running contract. Initially
27/// contains entries from the `ScMap` of the corresponding `ScContractInstance`
28/// contract data entry.
29#[derive(Clone, Hash)]
30pub(crate) struct InstanceStorageMap {
31    pub(crate) map: MeteredOrdMap<Val, Val, Host>,
32    pub(crate) is_modified: bool,
33}
34
35impl InstanceStorageMap {
36    pub(crate) fn from_map(map: Vec<(Val, Val)>, host: &Host) -> Result<Self, HostError> {
37        Ok(Self {
38            map: MeteredOrdMap::from_map(map, host)?,
39            is_modified: false,
40        })
41    }
42}
43
44/// A helper type used by [Footprint] to designate which ways
45/// a given [LedgerKey] is accessed, or is allowed to be accessed,
46/// in a given transaction.
47#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
48pub enum AccessType {
49    /// When in [FootprintMode::Recording], indicates that the [LedgerKey] is only read.
50    /// When in [FootprintMode::Enforcing], indicates that the [LedgerKey] is only _allowed_ to be read.
51    ReadOnly,
52    /// When in [FootprintMode::Recording], indicates that the [LedgerKey] is written (and also possibly read)
53    /// When in [FootprintMode::Enforcing], indicates that the [LedgerKey] is _allowed_ to be written (and also allowed to be read).
54    ReadWrite,
55}
56
57/// A helper type used by [FootprintMode::Recording] to provide access
58/// to a stable read-snapshot of a ledger.
59/// The snapshot is expected to only return live ledger entries.
60pub trait SnapshotSource {
61    /// Returns the ledger entry for the key and its live_until ledger if entry
62    /// exists, or `None` otherwise.
63    fn get(&self, key: &Rc<LedgerKey>) -> Result<Option<EntryWithLiveUntil>, HostError>;
64}
65
66/// Describes the total set of [LedgerKey]s that a given transaction
67/// will access, as well as the [AccessType] governing each key.
68///
69/// A [Footprint] must be provided in order to run a transaction that
70/// accesses any [LedgerKey]s in [FootprintMode::Enforcing]. If a
71/// transaction has an unknown [Footprint] it can be calculated by
72/// running a "preflight" execution in [FootprintMode::Recording],
73/// against a suitably fresh [SnapshotSource].
74// Notes on metering: covered by the underneath `MeteredOrdMap`.
75#[derive(Clone, Default, Hash)]
76pub struct Footprint(pub FootprintMap);
77
78impl Footprint {
79    pub fn record_access(
80        &mut self,
81        key: &Rc<LedgerKey>,
82        ty: AccessType,
83        budget: &Budget,
84    ) -> Result<(), HostError> {
85        if let Some(existing) = self.0.get::<Rc<LedgerKey>>(key, budget)? {
86            match (existing, ty) {
87                (AccessType::ReadOnly, AccessType::ReadOnly) => Ok(()),
88                (AccessType::ReadOnly, AccessType::ReadWrite) => {
89                    // The only interesting case is an upgrade
90                    // from previously-read-only to read-write.
91                    self.0 = self.0.insert(Rc::clone(key), ty, budget)?;
92                    Ok(())
93                }
94                (AccessType::ReadWrite, AccessType::ReadOnly) => Ok(()),
95                (AccessType::ReadWrite, AccessType::ReadWrite) => Ok(()),
96            }
97        } else {
98            self.0 = self.0.insert(Rc::clone(key), ty, budget)?;
99            Ok(())
100        }
101    }
102
103    pub fn enforce_access(
104        &mut self,
105        key: &Rc<LedgerKey>,
106        ty: AccessType,
107        budget: &Budget,
108    ) -> Result<(), HostError> {
109        // `ExceededLimit` is not the most precise term here, but footprint has
110        // to be externally supplied in a similar fashion to budget and it's
111        // also representing an execution resource limit (number of ledger
112        // entries to access), so it might be considered 'exceeded'.
113        // This also helps distinguish access errors from the values simply
114        // being  missing from storage (but with a valid footprint).
115        if let Some(existing) = self.0.get::<Rc<LedgerKey>>(key, budget)? {
116            match (existing, ty) {
117                (AccessType::ReadOnly, AccessType::ReadOnly) => Ok(()),
118                (AccessType::ReadOnly, AccessType::ReadWrite) => {
119                    Err((ScErrorType::Storage, ScErrorCode::ExceededLimit).into())
120                }
121                (AccessType::ReadWrite, AccessType::ReadOnly) => Ok(()),
122                (AccessType::ReadWrite, AccessType::ReadWrite) => Ok(()),
123            }
124        } else {
125            Err((ScErrorType::Storage, ScErrorCode::ExceededLimit).into())
126        }
127    }
128}
129
130#[derive(Clone, Default)]
131pub enum FootprintMode {
132    Recording(Rc<dyn SnapshotSource>),
133    #[default]
134    Enforcing,
135}
136
137/// A special-purpose map from [LedgerKey]s to [LedgerEntry]s. Represents a
138/// transactional batch of contract IO from and to durable storage, while
139/// partitioning that IO between concurrently executing groups of contracts
140/// through the use of IO [Footprint]s.
141///
142/// Specifically: access to each [LedgerKey] is mediated by the [Footprint],
143/// which may be in either [FootprintMode::Recording] or
144/// [FootprintMode::Enforcing] mode.
145///
146/// [FootprintMode::Recording] mode is used to calculate [Footprint]s during
147/// "preflight" execution of a contract. Once calculated, a recorded [Footprint]
148/// can be provided to "real" execution, which always runs in
149/// [FootprintMode::Enforcing] mode and enforces partitioned access.
150#[derive(Clone, Default)]
151pub struct Storage {
152    pub footprint: Footprint,
153    pub mode: FootprintMode,
154    pub map: StorageMap,
155}
156
157// Notes on metering: all storage operations: `put`, `get`, `del`, `has` are
158// covered by the underlying [MeteredOrdMap] and the [Footprint]'s own map.
159impl Storage {
160    /// Only a subset of Stellar's XDR ledger key or entry types are supported
161    /// by Soroban: accounts, trustlines, contract code and data. The rest are
162    /// never used by stellar-core when interacting with the Soroban host, nor
163    /// does the Soroban host ever generate any. Therefore the storage system
164    /// will reject them with [ScErrorCode::InternalError] if they ever occur.
165    pub fn check_supported_ledger_entry_type(le: &LedgerEntry) -> Result<(), HostError> {
166        use crate::xdr::LedgerEntryData::*;
167        match le.data {
168            Account(_) | Trustline(_) | ContractData(_) | ContractCode(_) => Ok(()),
169            Offer(_) | Data(_) | ClaimableBalance(_) | LiquidityPool(_) | ConfigSetting(_)
170            | Ttl(_) => Err((ScErrorType::Storage, ScErrorCode::InternalError).into()),
171        }
172    }
173
174    /// Only a subset of Stellar's XDR ledger key or entry types are supported
175    /// by Soroban: accounts, trustlines, contract code and data. The rest are
176    /// never used by stellar-core when interacting with the Soroban host, nor
177    /// does the Soroban host ever generate any. Therefore the storage system
178    /// will reject them with [ScErrorCode::InternalError] if they ever occur.
179    pub fn check_supported_ledger_key_type(lk: &LedgerKey) -> Result<(), HostError> {
180        use LedgerKey::*;
181        match lk {
182            Account(_) | Trustline(_) | ContractData(_) | ContractCode(_) => Ok(()),
183            Offer(_) | Data(_) | ClaimableBalance(_) | LiquidityPool(_) | ConfigSetting(_)
184            | Ttl(_) => Err((ScErrorType::Storage, ScErrorCode::InternalError).into()),
185        }
186    }
187
188    /// Constructs a new [Storage] in [FootprintMode::Enforcing] using a
189    /// given [Footprint] and a storage map populated with all the keys
190    /// listed in the [Footprint].
191    pub fn with_enforcing_footprint_and_map(footprint: Footprint, map: StorageMap) -> Self {
192        Self {
193            mode: FootprintMode::Enforcing,
194            footprint,
195            map,
196        }
197    }
198
199    /// Constructs a new [Storage] in [FootprintMode::Recording] using a
200    /// given [SnapshotSource].
201    pub fn with_recording_footprint(src: Rc<dyn SnapshotSource>) -> Self {
202        Self {
203            mode: FootprintMode::Recording(src),
204            footprint: Footprint::default(),
205            map: Default::default(),
206        }
207    }
208
209    // Helper function the next 3 `get`-variants funnel into.
210    fn try_get_full(
211        &mut self,
212        key: &Rc<LedgerKey>,
213        budget: &Budget,
214    ) -> Result<Option<EntryWithLiveUntil>, HostError> {
215        let _span = tracy_span!("storage get");
216        Self::check_supported_ledger_key_type(key)?;
217        self.prepare_read_only_access(key, budget)?;
218        match self.map.get::<Rc<LedgerKey>>(key, budget)? {
219            // Key has to be in the storage map at this point due to
220            // `prepare_read_only_access`.
221            None => Err((ScErrorType::Storage, ScErrorCode::InternalError).into()),
222            Some(pair_option) => Ok(pair_option.clone()),
223        }
224    }
225
226    pub(crate) fn try_get_full_with_host(
227        &mut self,
228        key: &Rc<LedgerKey>,
229        host: &Host,
230        key_val: Option<Val>,
231    ) -> Result<Option<EntryWithLiveUntil>, HostError> {
232        let res = self
233            .try_get_full(key, host.as_budget())
234            .map_err(|e| host.decorate_storage_error(e, key.as_ref(), key_val))?;
235
236        #[cfg(any(test, feature = "testutils"))]
237        if !host.check_if_entry_is_live(key.as_ref(), &res, key_val)? {
238            return Ok(None);
239        }
240
241        Ok(res)
242    }
243
244    /// Attempts to retrieve the [LedgerEntry] associated with a given
245    /// [LedgerKey] in the [Storage], returning an error if the key is not
246    /// found.
247    ///
248    /// In [FootprintMode::Recording] mode, records the read [LedgerKey] in the
249    /// [Footprint] as [AccessType::ReadOnly] (unless already recorded as
250    /// [AccessType::ReadWrite]) and reads through to the underlying
251    /// [SnapshotSource], if the [LedgerKey] has not yet been loaded.
252    ///
253    /// In [FootprintMode::Enforcing] mode, succeeds only if the read
254    /// [LedgerKey] has been declared in the [Footprint].
255    pub fn get(
256        &mut self,
257        key: &Rc<LedgerKey>,
258        budget: &Budget,
259    ) -> Result<Rc<LedgerEntry>, HostError> {
260        self.try_get_full(key, budget)?
261            .ok_or_else(|| (ScErrorType::Storage, ScErrorCode::MissingValue).into())
262            .map(|e| e.0)
263    }
264    pub(crate) fn get_with_host(
265        &mut self,
266        key: &Rc<LedgerKey>,
267        host: &Host,
268        key_val: Option<Val>,
269    ) -> Result<Rc<LedgerEntry>, HostError> {
270        self.try_get_full_with_host(key, host, key_val)?
271            .ok_or_else(|| (ScErrorType::Storage, ScErrorCode::MissingValue).into())
272            .map(|e| e.0)
273            .map_err(|e| host.decorate_storage_error(e, key.as_ref(), key_val))
274    }
275
276    // Like `get_with_host`, but distinguishes between missing values (return `Ok(None)`)
277    // and out-of-footprint values or errors (`Err(...)`).
278    pub(crate) fn try_get(
279        &mut self,
280        key: &Rc<LedgerKey>,
281        host: &Host,
282        key_val: Option<Val>,
283    ) -> Result<Option<Rc<LedgerEntry>>, HostError> {
284        self.try_get_full_with_host(key, host, key_val)
285            .map(|ok| ok.map(|pair| pair.0))
286    }
287
288    /// Attempts to retrieve the [LedgerEntry] associated with a given
289    /// [LedgerKey] and its live until ledger (if applicable) in the [Storage],
290    /// returning an error if the key is not found.
291    ///
292    /// Live until ledgers only exist for `ContractData` and `ContractCode`
293    /// ledger entries and are `None` for all the other entry kinds.
294    ///
295    /// In [FootprintMode::Recording] mode, records the read [LedgerKey] in the
296    /// [Footprint] as [AccessType::ReadOnly] (unless already recorded as
297    /// [AccessType::ReadWrite]) and reads through to the underlying
298    /// [SnapshotSource], if the [LedgerKey] has not yet been loaded.
299    ///
300    /// In [FootprintMode::Enforcing] mode, succeeds only if the read
301    /// [LedgerKey] has been declared in the [Footprint].
302    pub(crate) fn get_with_live_until_ledger(
303        &mut self,
304        key: &Rc<LedgerKey>,
305        host: &Host,
306        key_val: Option<Val>,
307    ) -> Result<EntryWithLiveUntil, HostError> {
308        self.try_get_full_with_host(key, host, key_val)
309            .and_then(|maybe_entry| {
310                maybe_entry.ok_or_else(|| (ScErrorType::Storage, ScErrorCode::MissingValue).into())
311            })
312            .map_err(|e| host.decorate_storage_error(e, key.as_ref(), key_val))
313    }
314
315    // Helper function `put` and `del` funnel into.
316    fn put_opt(
317        &mut self,
318        key: &Rc<LedgerKey>,
319        val: Option<EntryWithLiveUntil>,
320        budget: &Budget,
321    ) -> Result<(), HostError> {
322        Self::check_supported_ledger_key_type(key)?;
323        if let Some(le) = &val {
324            Self::check_supported_ledger_entry_type(&le.0)?;
325        }
326        let ty = AccessType::ReadWrite;
327        match self.mode {
328            FootprintMode::Recording(_) => {
329                self.footprint.record_access(key, ty, budget)?;
330            }
331            FootprintMode::Enforcing => {
332                self.footprint.enforce_access(key, ty, budget)?;
333            }
334        };
335        self.map = self.map.insert(Rc::clone(key), val, budget)?;
336        Ok(())
337    }
338
339    fn put_opt_with_host(
340        &mut self,
341        key: &Rc<LedgerKey>,
342        val: Option<EntryWithLiveUntil>,
343        host: &Host,
344        key_val: Option<Val>,
345    ) -> Result<(), HostError> {
346        let prev = host.as_budget().get_cpu_insns_consumed().unwrap();
347        #[cfg(any(test, feature = "testutils"))]
348        let _ = host.as_budget().with_observable_shadow_mode(|| {
349            let Ok(Some(entry_with_live_until)) =
350                self.map.get::<Rc<LedgerKey>>(key, host.as_budget())
351            else {
352                return Ok(());
353            };
354            let _ = host.check_if_entry_is_live(key.as_ref(), &entry_with_live_until, key_val)?;
355            Ok(())
356        })?;
357        let curr = host.as_budget().get_cpu_insns_consumed().unwrap();
358        assert_eq!(prev, curr);
359        self.put_opt(key, val, host.as_budget())
360            .map_err(|e| host.decorate_storage_error(e, key.as_ref(), key_val))
361    }
362
363    /// Attempts to write to the [LedgerEntry] associated with a given
364    /// [LedgerKey] in the [Storage].
365    ///
366    /// In [FootprintMode::Recording] mode, records the written [LedgerKey] in
367    /// the [Footprint] as [AccessType::ReadWrite].
368    ///
369    /// In [FootprintMode::Enforcing] mode, succeeds only if the written
370    /// [LedgerKey] has been declared in the [Footprint] as
371    /// [AccessType::ReadWrite].
372    pub fn put(
373        &mut self,
374        key: &Rc<LedgerKey>,
375        val: &Rc<LedgerEntry>,
376        live_until_ledger: Option<u32>,
377        budget: &Budget,
378    ) -> Result<(), HostError> {
379        let _span = tracy_span!("storage put");
380        self.put_opt(key, Some((val.clone(), live_until_ledger)), budget)
381    }
382
383    pub(crate) fn put_with_host(
384        &mut self,
385        key: &Rc<LedgerKey>,
386        val: &Rc<LedgerEntry>,
387        live_until_ledger: Option<u32>,
388        host: &Host,
389        key_val: Option<Val>,
390    ) -> Result<(), HostError> {
391        let _span = tracy_span!("storage put");
392        self.put_opt_with_host(key, Some((val.clone(), live_until_ledger)), host, key_val)
393    }
394
395    /// Attempts to delete the [LedgerEntry] associated with a given [LedgerKey]
396    /// in the [Storage].
397    ///
398    /// In [FootprintMode::Recording] mode, records the deleted [LedgerKey] in
399    /// the [Footprint] as [AccessType::ReadWrite].
400    ///
401    /// In [FootprintMode::Enforcing] mode, succeeds only if the deleted
402    /// [LedgerKey] has been declared in the [Footprint] as
403    /// [AccessType::ReadWrite].
404    pub fn del(&mut self, key: &Rc<LedgerKey>, budget: &Budget) -> Result<(), HostError> {
405        let _span = tracy_span!("storage del");
406        self.put_opt(key, None, budget)
407    }
408
409    pub(crate) fn del_with_host(
410        &mut self,
411        key: &Rc<LedgerKey>,
412        host: &Host,
413        key_val: Option<Val>,
414    ) -> Result<(), HostError> {
415        let _span = tracy_span!("storage del");
416        self.put_opt_with_host(key, None, host, key_val)
417            .map_err(|e| host.decorate_storage_error(e, key.as_ref(), key_val))
418    }
419
420    /// Attempts to determine the presence of a [LedgerEntry] associated with a
421    /// given [LedgerKey] in the [Storage], returning `Ok(true)` if an entry
422    /// with the key exists and `Ok(false)` if it does not.
423    ///
424    /// In [FootprintMode::Recording] mode, records the access and reads-through
425    /// to the underlying [SnapshotSource].
426    ///
427    /// In [FootprintMode::Enforcing] mode, succeeds only if the access has been
428    /// declared in the [Footprint].
429    pub fn has(&mut self, key: &Rc<LedgerKey>, budget: &Budget) -> Result<bool, HostError> {
430        let _span = tracy_span!("storage has");
431        Self::check_supported_ledger_key_type(key)?;
432        self.prepare_read_only_access(key, budget)?;
433        Ok(self
434            .map
435            .get::<Rc<LedgerKey>>(key, budget)?
436            // Key has to be present in storage at this point, so not having it
437            // would be an internal error.
438            .ok_or_else(|| HostError::from((ScErrorType::Storage, ScErrorCode::InternalError)))?
439            .is_some())
440    }
441
442    pub(crate) fn has_with_host(
443        &mut self,
444        key: &Rc<LedgerKey>,
445        host: &Host,
446        key_val: Option<Val>,
447    ) -> Result<bool, HostError> {
448        let _span = tracy_span!("storage has");
449        Ok(self.try_get_full_with_host(key, host, key_val)?.is_some())
450    }
451
452    /// Extends `key` to live `extend_to` ledgers from now (not counting the
453    /// current ledger) if the current `live_until_ledger_seq` for the entry is
454    /// `threshold` ledgers or less away from the current ledger.
455    ///
456    /// If attempting to extend an entry past `Host::max_live_until_ledger()`
457    /// - if the entry is `Persistent`, the entries's new
458    ///   `live_until_ledger_seq` is clamped to it.
459    /// - if the entry is `Temporary`, returns error.
460    ///
461    /// This operation is only defined within a host as it relies on ledger
462    /// state.
463    ///
464    /// This operation does not modify any ledger entries, but does change the
465    /// internal storage
466    pub(crate) fn extend_ttl(
467        &mut self,
468        host: &Host,
469        key: Rc<LedgerKey>,
470        threshold: u32,
471        extend_to: u32,
472        key_val: Option<Val>,
473    ) -> Result<(), HostError> {
474        let _span = tracy_span!("extend key");
475        Self::check_supported_ledger_key_type(&key)?;
476
477        if threshold > extend_to {
478            return Err(host.err(
479                ScErrorType::Storage,
480                ScErrorCode::InvalidInput,
481                "threshold must be <= extend_to",
482                &[threshold.into(), extend_to.into()],
483            ));
484        }
485
486        // Extending deleted/non-existing/out-of-footprint entries will result in
487        // an error.
488        let (entry, old_live_until) = self.get_with_live_until_ledger(&key, &host, key_val)?;
489        let old_live_until = old_live_until.ok_or_else(|| {
490            host.err(
491                ScErrorType::Storage,
492                ScErrorCode::InternalError,
493                "trying to extend invalid entry",
494                &[],
495            )
496        })?;
497
498        let ledger_seq: u32 = host.get_ledger_sequence()?.into();
499        if old_live_until < ledger_seq {
500            return Err(host.err(
501                ScErrorType::Storage,
502                ScErrorCode::InternalError,
503                "accessing no-longer-live entry",
504                &[old_live_until.into(), ledger_seq.into()],
505            ));
506        }
507
508        let mut new_live_until = host.with_ledger_info(|li| {
509            li.sequence_number.checked_add(extend_to).ok_or_else(|| {
510                // overflowing here means a misconfiguration of the network (the
511                // ttl is too large), in which case we immediately flag it as an
512                // unrecoverable `InternalError`, even though the source is
513                // external to the host.
514                HostError::from(Error::from_type_and_code(
515                    ScErrorType::Context,
516                    ScErrorCode::InternalError,
517                ))
518            })
519        })?;
520
521        if new_live_until > host.max_live_until_ledger()? {
522            if let Some(durability) = get_key_durability(&key) {
523                if matches!(durability, ContractDataDurability::Persistent) {
524                    new_live_until = host.max_live_until_ledger()?;
525                } else {
526                    //  for `Temporary` entries TTL has to be exact - most of
527                    //  the time entry has to live until the exact specified
528                    //  ledger, or else something bad would happen (e.g. nonce
529                    //  expiring before the corresponding signature, thus
530                    //  allowing replay and double spend).
531                    return Err(host.err(
532                        ScErrorType::Storage,
533                        ScErrorCode::InvalidAction,
534                        "trying to extend past max live_until ledger",
535                        &[new_live_until.into()],
536                    ));
537                }
538            } else {
539                return Err(host.err(
540                    ScErrorType::Storage,
541                    ScErrorCode::InternalError,
542                    "durability is missing",
543                    &[],
544                ));
545            }
546        }
547
548        if new_live_until > old_live_until && old_live_until.saturating_sub(ledger_seq) <= threshold
549        {
550            self.map = self.map.insert(
551                key,
552                Some((entry.clone(), Some(new_live_until))),
553                host.budget_ref(),
554            )?;
555        }
556        Ok(())
557    }
558
559    #[cfg(any(test, feature = "recording_mode"))]
560    pub(crate) fn get_snapshot_value(
561        &self,
562        host: &Host,
563        key: &Rc<LedgerKey>,
564    ) -> Result<Option<EntryWithLiveUntil>, HostError> {
565        match &self.mode {
566            FootprintMode::Recording(snapshot) => snapshot.get(key),
567            FootprintMode::Enforcing => Err(host.err(
568                ScErrorType::Storage,
569                ScErrorCode::InternalError,
570                "trying to get snapshot value in enforcing mode",
571                &[],
572            )),
573        }
574    }
575
576    fn prepare_read_only_access(
577        &mut self,
578        key: &Rc<LedgerKey>,
579        budget: &Budget,
580    ) -> Result<(), HostError> {
581        let ty = AccessType::ReadOnly;
582        match self.mode {
583            FootprintMode::Recording(ref src) => {
584                self.footprint.record_access(key, ty, budget)?;
585                // In recording mode we treat the map as a cache
586                // that misses read-through to the underlying src.
587                if !self.map.contains_key::<Rc<LedgerKey>>(key, budget)? {
588                    let value = src.get(&key)?;
589                    self.map = self.map.insert(key.clone(), value, budget)?;
590                }
591            }
592            FootprintMode::Enforcing => {
593                self.footprint.enforce_access(key, ty, budget)?;
594            }
595        };
596        Ok(())
597    }
598
599    #[cfg(any(test, feature = "testutils"))]
600    pub(crate) fn reset_footprint(&mut self) {
601        self.footprint = Footprint::default();
602    }
603}
604
605fn get_key_type_string_for_error(lk: &LedgerKey) -> &str {
606    match lk {
607        LedgerKey::ContractData(cd) => match cd.key {
608            ScVal::LedgerKeyContractInstance => "contract instance",
609            ScVal::LedgerKeyNonce(_) => "nonce",
610            _ => "contract data key",
611        },
612        LedgerKey::ContractCode(_) => "contract code",
613        LedgerKey::Account(_) => "account",
614        LedgerKey::Trustline(_) => "account trustline",
615        // This shouldn't normally trigger, but it's safer to just return
616        // a safe default instead of an error in case if new key types are
617        // accessed.
618        _ => "ledger key",
619    }
620}
621
622impl Host {
623    fn decorate_storage_error(
624        &self,
625        err: HostError,
626        lk: &LedgerKey,
627        key_val: Option<Val>,
628    ) -> HostError {
629        let mut err = err;
630        self.with_debug_mode(|| {
631            if !err.error.is_type(ScErrorType::Storage) {
632                return Ok(());
633            }
634            if !err.error.is_code(ScErrorCode::ExceededLimit)
635                && !err.error.is_code(ScErrorCode::MissingValue)
636            {
637                return Ok(());
638            }
639
640            let key_type_str = get_key_type_string_for_error(lk);
641            // Accessing an entry outside of the footprint is a non-recoverable error, thus
642            // there is no way to observe the object pool being changed (host will continue
643            // propagating an error until there are no frames left and control is never
644            // returned to guest). This allows us to build a nicer error message.
645            // For the missing values we unfortunately can only safely use the existing `Val`s
646            // to enhance errors.
647            let can_create_new_objects = err.error.is_code(ScErrorCode::ExceededLimit);
648            let args = self
649                .get_args_for_error(lk, key_val, can_create_new_objects)
650                .unwrap_or_else(|_| vec![]);
651            if err.error.is_code(ScErrorCode::ExceededLimit) {
652                err = self.err(
653                    ScErrorType::Storage,
654                    ScErrorCode::ExceededLimit,
655                    format!("trying to access {} outside of the footprint", key_type_str).as_str(),
656                    args.as_slice(),
657                );
658            } else if err.error.is_code(ScErrorCode::MissingValue) {
659                err = self.err(
660                    ScErrorType::Storage,
661                    ScErrorCode::MissingValue,
662                    format!("trying to get non-existing value for {}", key_type_str).as_str(),
663                    args.as_slice(),
664                );
665            }
666
667            Ok(())
668        });
669        err
670    }
671
672    fn get_args_for_error(
673        &self,
674        lk: &LedgerKey,
675        key_val: Option<Val>,
676        can_create_new_objects: bool,
677    ) -> Result<Vec<Val>, HostError> {
678        let mut res = vec![];
679        match lk {
680            LedgerKey::ContractData(cd) => {
681                if can_create_new_objects {
682                    let address_val = self
683                        .add_host_object(cd.contract.metered_clone(self.as_budget())?)?
684                        .into();
685                    res.push(address_val);
686                }
687                match &cd.key {
688                    ScVal::LedgerKeyContractInstance => (),
689                    ScVal::LedgerKeyNonce(n) => {
690                        if can_create_new_objects {
691                            res.push(self.add_host_object(n.nonce)?.into());
692                        }
693                    }
694                    _ => {
695                        if let Some(key) = key_val {
696                            res.push(key);
697                        }
698                    }
699                }
700            }
701            LedgerKey::ContractCode(c) => {
702                if can_create_new_objects {
703                    res.push(
704                        self.add_host_object(self.scbytes_from_hash(&c.hash)?)?
705                            .into(),
706                    );
707                }
708            }
709            LedgerKey::Account(_) | LedgerKey::Trustline(_) => {
710                if can_create_new_objects {
711                    res.push(self.account_address_from_key(lk)?)
712                }
713            }
714            // This shouldn't normally trigger, but it's safer to just return
715            // a safe default instead of an error in case if new key types are
716            // accessed.
717            _ => (),
718        };
719        Ok(res)
720    }
721
722    #[cfg(any(test, feature = "testutils"))]
723    fn check_if_entry_is_live(
724        &self,
725        key: &LedgerKey,
726        entry_with_live_until: &Option<EntryWithLiveUntil>,
727        key_val: Option<Val>,
728    ) -> Result<bool, HostError> {
729        let Some((_, Some(live_until_ledger))) = &entry_with_live_until else {
730            return Ok(true);
731        };
732        let ledger_seq = self
733            .with_ledger_info(|li| Ok(li.sequence_number))
734            .unwrap_or_default();
735        if *live_until_ledger >= ledger_seq {
736            return Ok(true);
737        }
738        match get_key_durability(key) {
739            Some(ContractDataDurability::Temporary) => Ok(false),
740            Some(ContractDataDurability::Persistent) => {
741                let key_type_str = get_key_type_string_for_error(key);
742                let args = self
743                    .as_budget()
744                    .with_observable_shadow_mode(|| self.get_args_for_error(key, key_val, true))
745                    .unwrap_or_else(|_| vec![]);
746                let msg = format!("[testing-only] Accessed {} key that has been archived. Important: this error may only appear in tests; in the real network contracts aren't called at all if any archived entry is accessed.", key_type_str);
747                Err(self.err(
748                    ScErrorType::Storage,
749                    ScErrorCode::InternalError,
750                    msg.as_str(),
751                    args.as_slice(),
752                ))
753            }
754            None => Ok(true),
755        }
756    }
757}