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    /// Returns `true` if the key exists in the snapshot and is live w.r.t
503    /// the current ledger sequence.
504    pub(crate) fn is_key_live_in_snapshot(
505        &self,
506        host: &Host,
507        key: &Rc<LedgerKey>,
508    ) -> Result<bool, HostError> {
509        match &self.mode {
510            FootprintMode::Recording(snapshot) => {
511                let snapshot_value = snapshot.get(key)?;
512                if let Some((_, live_until_ledger)) = snapshot_value {
513                    if let Some(live_until_ledger) = live_until_ledger {
514                        let current_ledger_sequence =
515                            host.with_ledger_info(|li| Ok(li.sequence_number))?;
516                        Ok(live_until_ledger >= current_ledger_sequence)
517                    } else {
518                        // Non-Soroban entries are always live.
519                        Ok(true)
520                    }
521                } else {
522                    // Key is not in the snapshot.
523                    Ok(false)
524                }
525            }
526            FootprintMode::Enforcing => Err(host.err(
527                ScErrorType::Storage,
528                ScErrorCode::InternalError,
529                "trying to get snapshot value in enforcing mode",
530                &[],
531            )),
532        }
533    }
534
535    // Test-only helper for getting the value directly from the storage map,
536    // without the footprint management and autorestoration.
537    #[cfg(any(test, feature = "testutils"))]
538    pub(crate) fn get_from_map(
539        &self,
540        key: &Rc<LedgerKey>,
541        host: &Host,
542    ) -> Result<Option<EntryWithLiveUntil>, HostError> {
543        match self.map.get::<Rc<LedgerKey>>(key, host.budget_ref())? {
544            Some(pair_option) => Ok(pair_option.clone()),
545            None => Ok(None),
546        }
547    }
548
549    fn prepare_read_only_access(
550        &mut self,
551        key: &Rc<LedgerKey>,
552        host: &Host,
553    ) -> Result<(), HostError> {
554        let ty = AccessType::ReadOnly;
555        match self.mode {
556            #[cfg(any(test, feature = "recording_mode"))]
557            FootprintMode::Recording(ref src) => {
558                self.footprint.record_access(key, ty, host.budget_ref())?;
559                // In recording mode we treat the map as a cache
560                // that misses read-through to the underlying src.
561                if !self
562                    .map
563                    .contains_key::<Rc<LedgerKey>>(key, host.budget_ref())?
564                {
565                    let value = src.get(&key)?;
566                    self.map = self.map.insert(key.clone(), value, host.budget_ref())?;
567                }
568                self.footprint.record_access(key, ty, host.budget_ref())?;
569                self.handle_maybe_expired_entry(key, host)?;
570            }
571            FootprintMode::Enforcing => {
572                self.footprint.enforce_access(key, ty, host.budget_ref())?;
573            }
574        };
575        Ok(())
576    }
577
578    #[cfg(any(test, feature = "recording_mode"))]
579    fn handle_maybe_expired_entry(
580        &mut self,
581        key: &Rc<LedgerKey>,
582        host: &Host,
583    ) -> Result<(), HostError> {
584        host.with_ledger_info(|li| {
585            let budget = host.budget_ref();
586            if let Some(Some((entry, live_until))) =
587                self.map.get::<Rc<LedgerKey>>(key, host.budget_ref())?
588            {
589                if let Some(durability) = get_key_durability(key.as_ref()) {
590                    let live_until = live_until.ok_or_else(|| {
591                        host.err(
592                            ScErrorType::Storage,
593                            ScErrorCode::InternalError,
594                            "unexpected contract entry without TTL",
595                            &[],
596                        )
597                    })?;
598                    if live_until < li.sequence_number {
599                        match durability {
600                            ContractDataDurability::Temporary => {
601                                self.map = self.map.insert(key.clone(), None, budget)?;
602                            }
603                            ContractDataDurability::Persistent => {
604                                self.footprint
605                                    .record_access(key, AccessType::ReadWrite, budget)?;
606                                let new_live_until = li
607                                    .min_live_until_ledger_checked(
608                                        ContractDataDurability::Persistent,
609                                    )
610                                    .ok_or_else(|| {
611                                        host.err(ScErrorType::Storage,
612                                        ScErrorCode::InternalError,
613                                        "persistent entry TTL overflow, ledger is mis-configured",
614                                        &[],)
615                                    })?;
616                                self.map = self.map.insert(
617                                    key.clone(),
618                                    Some((entry.clone(), Some(new_live_until))),
619                                    budget,
620                                )?;
621                            }
622                        };
623                    }
624                }
625            }
626            Ok(())
627        })
628    }
629
630    #[cfg(any(test, feature = "testutils"))]
631    pub(crate) fn reset_footprint(&mut self) {
632        self.footprint = Footprint::default();
633    }
634}
635
636fn get_key_type_string_for_error(lk: &LedgerKey) -> &str {
637    match lk {
638        LedgerKey::ContractData(cd) => match cd.key {
639            ScVal::LedgerKeyContractInstance => "contract instance",
640            ScVal::LedgerKeyNonce(_) => "nonce",
641            _ => "contract data key",
642        },
643        LedgerKey::ContractCode(_) => "contract code",
644        LedgerKey::Account(_) => "account",
645        LedgerKey::Trustline(_) => "account trustline",
646        // This shouldn't normally trigger, but it's safer to just return
647        // a safe default instead of an error in case if new key types are
648        // accessed.
649        _ => "ledger key",
650    }
651}
652
653impl Host {
654    fn decorate_storage_error(
655        &self,
656        err: HostError,
657        lk: &LedgerKey,
658        key_val: Option<Val>,
659    ) -> HostError {
660        let mut err = err;
661        self.with_debug_mode_allowing_new_objects(
662            || {
663                if !err.error.is_type(ScErrorType::Storage) {
664                    return Ok(());
665                }
666                if !err.error.is_code(ScErrorCode::ExceededLimit)
667                    && !err.error.is_code(ScErrorCode::MissingValue)
668                {
669                    return Ok(());
670                }
671
672                let key_type_str = get_key_type_string_for_error(lk);
673                // Accessing an entry outside of the footprint is a non-recoverable error, thus
674                // there is no way to observe the object pool being changed (host will continue
675                // propagating an error until there are no frames left and control is never
676                // returned to guest). This allows us to build a nicer error message.
677                // For the missing values we unfortunately can only safely use the existing `Val`s
678                // to enhance errors.
679                let can_create_new_objects = err.error.is_code(ScErrorCode::ExceededLimit);
680                let args = self
681                    .get_args_for_error(lk, key_val, can_create_new_objects)
682                    .unwrap_or_else(|_| vec![]);
683                if err.error.is_code(ScErrorCode::ExceededLimit) {
684                    err = self.err(
685                        ScErrorType::Storage,
686                        ScErrorCode::ExceededLimit,
687                        format!("trying to access {} outside of the footprint", key_type_str)
688                            .as_str(),
689                        args.as_slice(),
690                    );
691                } else if err.error.is_code(ScErrorCode::MissingValue) {
692                    err = self.err(
693                        ScErrorType::Storage,
694                        ScErrorCode::MissingValue,
695                        format!("trying to get non-existing value for {}", key_type_str).as_str(),
696                        args.as_slice(),
697                    );
698                }
699
700                Ok(())
701            },
702            true,
703        );
704        err
705    }
706
707    fn get_args_for_error(
708        &self,
709        lk: &LedgerKey,
710        key_val: Option<Val>,
711        can_create_new_objects: bool,
712    ) -> Result<Vec<Val>, HostError> {
713        let mut res = vec![];
714        match lk {
715            LedgerKey::ContractData(cd) => {
716                if can_create_new_objects {
717                    let address_val = self
718                        .add_host_object(cd.contract.metered_clone(self.as_budget())?)?
719                        .into();
720                    res.push(address_val);
721                }
722                match &cd.key {
723                    ScVal::LedgerKeyContractInstance => (),
724                    ScVal::LedgerKeyNonce(n) => {
725                        if can_create_new_objects {
726                            res.push(self.add_host_object(n.nonce)?.into());
727                        }
728                    }
729                    _ => {
730                        if let Some(key) = key_val {
731                            res.push(key);
732                        }
733                    }
734                }
735            }
736            LedgerKey::ContractCode(c) => {
737                if can_create_new_objects {
738                    res.push(
739                        self.add_host_object(self.scbytes_from_hash(&c.hash)?)?
740                            .into(),
741                    );
742                }
743            }
744            LedgerKey::Account(_) | LedgerKey::Trustline(_) => {
745                if can_create_new_objects {
746                    res.push(self.account_address_from_key(lk)?)
747                }
748            }
749            // This shouldn't normally trigger, but it's safer to just return
750            // a safe default instead of an error in case if new key types are
751            // accessed.
752            _ => (),
753        };
754        Ok(res)
755    }
756}
757
758#[cfg(any(test, feature = "recording_mode"))]
759pub(crate) fn is_persistent_key(key: &LedgerKey) -> bool {
760    match key {
761        LedgerKey::ContractData(k) => {
762            matches!(k.durability, ContractDataDurability::Persistent)
763        }
764        LedgerKey::ContractCode(_) => true,
765        _ => false,
766    }
767}