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