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