soroban_env_host/
e2e_invoke.rs

1/// This module contains functionality to invoke host functions in embedder
2/// environments using a clean host instance.
3/// Also contains helpers for processing the ledger changes caused by these
4/// host functions.
5use std::{cmp::max, rc::Rc};
6
7#[cfg(any(test, feature = "recording_mode"))]
8use crate::{
9    auth::RecordedAuthPayload,
10    storage::is_persistent_key,
11    xdr::{ContractEvent, ReadXdr, ScVal, SorobanAddressCredentials, SorobanCredentials, WriteXdr},
12    DEFAULT_XDR_RW_LIMITS,
13};
14use crate::{
15    budget::{AsBudget, Budget},
16    crypto::sha256_hash_from_bytes,
17    events::Events,
18    fees::LedgerEntryRentChange,
19    host::{
20        metered_clone::{MeteredAlloc, MeteredClone, MeteredContainer, MeteredIterator},
21        metered_xdr::{metered_from_xdr_with_budget, metered_write_xdr},
22        TraceHook,
23    },
24    storage::{AccessType, Footprint, FootprintMap, SnapshotSource, Storage, StorageMap},
25    xdr::{
26        AccountId, ContractDataDurability, ContractEventType, DiagnosticEvent, HostFunction,
27        LedgerEntry, LedgerEntryData, LedgerEntryType, LedgerFootprint, LedgerKey,
28        LedgerKeyAccount, LedgerKeyContractCode, LedgerKeyContractData, LedgerKeyTrustLine,
29        ScErrorCode, ScErrorType, SorobanAuthorizationEntry, SorobanResources, TtlEntry,
30    },
31    DiagnosticLevel, Error, Host, HostError, LedgerInfo, MeteredOrdMap,
32};
33use crate::{ledger_info::get_key_durability, ModuleCache};
34use crate::{storage::EntryWithLiveUntil, vm::wasm_module_memory_cost};
35#[cfg(any(test, feature = "recording_mode"))]
36use sha2::{Digest, Sha256};
37
38type TtlEntryMap = MeteredOrdMap<Rc<LedgerKey>, Rc<TtlEntry>, Budget>;
39type RestoredKeySet = MeteredOrdMap<Rc<LedgerKey>, (), Budget>;
40
41/// Result of invoking a single host function prepared for embedder consumption.
42pub struct InvokeHostFunctionResult {
43    /// Result value of the function, encoded `ScVal` XDR on success, or error.
44    pub encoded_invoke_result: Result<Vec<u8>, HostError>,
45    /// All the ledger changes caused by this invocation, including no-ops.
46    /// This contains an entry for *every* item in the input footprint, even if
47    /// it wasn't modified at all.
48    ///
49    /// Read-only entry can only have their live until ledger increased.
50    /// Read-write entries can be modified arbitrarily or removed.
51    ///
52    /// Empty when invocation fails.
53    pub ledger_changes: Vec<LedgerEntryChange>,
54    /// All the events that contracts emitted during invocation, encoded as
55    /// `ContractEvent` XDR.
56    ///
57    /// Empty when invocation fails.
58    pub encoded_contract_events: Vec<Vec<u8>>,
59}
60
61/// Result of invoking a single host function prepared for embedder consumption.
62#[cfg(any(test, feature = "recording_mode"))]
63pub struct InvokeHostFunctionRecordingModeResult {
64    /// Result value of the invoked function or error returned for invocation.
65    pub invoke_result: Result<ScVal, HostError>,
66    /// Resources recorded during the invocation, including the footprint.
67    pub resources: SorobanResources,
68    /// Indices of the entries in read-write footprint that are to be
69    /// auto-restored when transaction is executed.
70    ///
71    /// Specifically, these are indices of ledger entry keys in
72    /// `resources.footprint.read_write` vector. Thus additional care should
73    /// be taken if the read-write footprint is re-arranged in any way (that
74    /// shouldn't normally be necessary though).
75    pub restored_rw_entry_indices: Vec<u32>,
76    /// Authorization data, either passed through from the call (when provided),
77    /// or recorded during the invocation.
78    pub auth: Vec<SorobanAuthorizationEntry>,
79    /// All the ledger changes caused by this invocation, including no-ops.
80    ///
81    /// Read-only entry can only have their live until ledger increased.
82    /// Read-write entries can be modified arbitrarily or removed.
83    ///
84    /// Empty when invocation fails.
85    pub ledger_changes: Vec<LedgerEntryChange>,
86    /// All the events that contracts emitted during invocation.
87    ///
88    /// Empty when invocation fails.
89    pub contract_events: Vec<ContractEvent>,
90    /// Size of the encoded contract events and the return value.
91    /// Non-zero only when invocation has succeeded.
92    pub contract_events_and_return_value_size: u32,
93}
94
95/// Represents a change of the ledger entry from 'old' value to the 'new' one.
96/// Only contains the final value of the entry (if any) and some minimal
97/// information about the old entry for convenience.
98#[derive(Default)]
99pub struct LedgerEntryChange {
100    /// Whether the ledger entry is read-only, as defined by the footprint.
101    pub read_only: bool,
102    /// Entry key encoded as `LedgerKey` XDR.
103    pub encoded_key: Vec<u8>,
104    /// Size of the 'old' entry to use in the rent computations.
105    /// This is the size of the encoded entry XDR for all of the entries besides
106    /// contract code, for which the module in-memory size is used instead.
107    pub old_entry_size_bytes_for_rent: u32,
108    /// New value of the ledger entry encoded as `LedgerEntry` XDR.
109    /// Only set for non-removed, non-readonly values, otherwise `None`.
110    pub encoded_new_value: Option<Vec<u8>>,
111    /// Size of the 'new' entry to use in the rent computations.
112    /// This is the size of the encoded entry XDR (i.e. length of `encoded_new_value`)
113    /// for all of the entries besides contract code, for which the module
114    /// in-memory size is used instead.
115    pub new_entry_size_bytes_for_rent: u32,
116    /// Change of the live until state of the entry.
117    /// Only set for entries that have a TTL, otherwise `None`.
118    pub ttl_change: Option<LedgerEntryLiveUntilChange>,
119}
120/// Represents the live until-related state of the entry.
121#[derive(Debug, Eq, PartialEq, Clone)]
122pub struct LedgerEntryLiveUntilChange {
123    /// Hash of the LedgerKey for the entry that this live until ledger change is tied to
124    pub key_hash: Vec<u8>,
125    /// Durability of the entry.    
126    pub durability: ContractDataDurability,
127    /// Type of the entry.
128    pub entry_type: LedgerEntryType,
129    /// Live until ledger of the old entry.
130    pub old_live_until_ledger: u32,
131    /// Live until ledger of the new entry. Guaranteed to always be greater than
132    /// or equal to `old_live_until_ledger`.
133    pub new_live_until_ledger: u32,
134}
135
136// Builds a set for metered lookups of keys for entries that were restored from
137// the archived state.
138// This returns an `Option` instead of an empty set because most of the
139// invocations won't have this populated and it makes sense to not run
140// unnecessary metered work for them (even empty map lookups have some cost
141// charged).
142fn build_restored_key_set(
143    budget: &Budget,
144    resources: &SorobanResources,
145    restored_rw_entry_indices: &[u32],
146) -> Result<Option<RestoredKeySet>, HostError> {
147    if restored_rw_entry_indices.is_empty() {
148        return Ok(None);
149    }
150    let rw_footprint = &resources.footprint.read_write;
151    let mut key_set = RestoredKeySet::default();
152    for e in restored_rw_entry_indices {
153        key_set = key_set.insert(
154            Rc::new(
155                rw_footprint
156                    .get(*e as usize)
157                    .ok_or_else(|| {
158                        HostError::from(Error::from_type_and_code(
159                            ScErrorType::Storage,
160                            ScErrorCode::InternalError,
161                        ))
162                    })?
163                    .metered_clone(budget)?,
164            ),
165            (),
166            budget,
167        )?;
168    }
169    Ok(Some(key_set))
170}
171
172/// Returns the difference between the `storage` and its initial snapshot as
173/// `LedgerEntryChanges`.
174/// Returns an entry for every item in `storage` footprint.
175fn get_ledger_changes(
176    budget: &Budget,
177    storage: &Storage,
178    init_storage_snapshot: &(impl SnapshotSource + ?Sized),
179    init_ttl_entries: TtlEntryMap,
180    min_live_until_ledger: u32,
181    restored_keys: &Option<RestoredKeySet>,
182    #[cfg(any(test, feature = "recording_mode"))] current_ledger_seq: u32,
183) -> Result<Vec<LedgerEntryChange>, HostError> {
184    // Skip allocation metering for this for the sake of simplicity - the
185    // bounding factor here is XDR decoding which is metered.
186    let mut changes = Vec::with_capacity(storage.map.len());
187
188    let footprint_map = &storage.footprint.0;
189    // We return any invariant errors here as internal errors, as they would
190    // typically mean inconsistency between storage and snapshot that shouldn't
191    // happen in embedder environments, or simply fundamental invariant bugs.
192    let internal_error = || {
193        HostError::from(Error::from_type_and_code(
194            ScErrorType::Storage,
195            ScErrorCode::InternalError,
196        ))
197    };
198    for (key, entry_with_live_until_ledger) in storage.map.iter(budget)? {
199        let mut entry_change = LedgerEntryChange::default();
200        metered_write_xdr(budget, key.as_ref(), &mut entry_change.encoded_key)?;
201        let durability = get_key_durability(key);
202
203        if let Some(durability) = durability {
204            let key_hash = match init_ttl_entries.get::<Rc<LedgerKey>>(key, budget)? {
205                Some(ttl_entry) => ttl_entry.key_hash.0.to_vec(),
206                None => sha256_hash_from_bytes(entry_change.encoded_key.as_slice(), budget)?,
207            };
208
209            entry_change.ttl_change = Some(LedgerEntryLiveUntilChange {
210                key_hash,
211                entry_type: key.discriminant(),
212                durability,
213                old_live_until_ledger: 0,
214                new_live_until_ledger: 0,
215            });
216        }
217        let entry_with_live_until = init_storage_snapshot.get(key)?;
218        if let Some((old_entry, old_live_until_ledger)) = entry_with_live_until {
219            let mut buf = vec![];
220            metered_write_xdr(budget, old_entry.as_ref(), &mut buf)?;
221
222            entry_change.old_entry_size_bytes_for_rent =
223                entry_size_for_rent(budget, &old_entry, buf.len() as u32)?;
224
225            if let Some(ref mut ttl_change) = &mut entry_change.ttl_change {
226                ttl_change.old_live_until_ledger =
227                    old_live_until_ledger.ok_or_else(internal_error)?;
228                // In recording mode we might encounter ledger changes that have an expired 'old'
229                // entry. In that case we should treat it as non-existent instead.
230                // Note, that this should only be necessary for the temporary
231                // entries, the auto-restored persistent entries are handled below
232                // via `restored_keys` check.
233                #[cfg(any(test, feature = "recording_mode"))]
234                if ttl_change.old_live_until_ledger < current_ledger_seq {
235                    ttl_change.old_live_until_ledger = 0;
236                    entry_change.old_entry_size_bytes_for_rent = 0;
237                }
238            }
239        }
240        if let Some((_, new_live_until_ledger)) = entry_with_live_until_ledger {
241            if let Some(ref mut ttl_change) = &mut entry_change.ttl_change {
242                // Never reduce the final live until ledger.
243                ttl_change.new_live_until_ledger = max(
244                    new_live_until_ledger.ok_or_else(internal_error)?,
245                    ttl_change.old_live_until_ledger,
246                );
247            }
248        }
249        let maybe_access_type: Option<AccessType> =
250            footprint_map.get::<Rc<LedgerKey>>(key, budget)?.copied();
251        match maybe_access_type {
252            Some(AccessType::ReadOnly) => {
253                entry_change.read_only = true;
254            }
255            Some(AccessType::ReadWrite) => {
256                if let Some((entry, _)) = entry_with_live_until_ledger {
257                    let mut entry_buf = vec![];
258                    metered_write_xdr(budget, entry.as_ref(), &mut entry_buf)?;
259                    entry_change.new_entry_size_bytes_for_rent =
260                        entry_size_for_rent(budget, &entry, entry_buf.len() as u32)?;
261                    entry_change.encoded_new_value = Some(entry_buf);
262
263                    if let Some(restored_keys) = &restored_keys {
264                        if restored_keys.contains_key::<LedgerKey>(key, budget)? {
265                            entry_change.old_entry_size_bytes_for_rent = 0;
266                            if let Some(ref mut ttl_change) = &mut entry_change.ttl_change {
267                                ttl_change.old_live_until_ledger = 0;
268                                ttl_change.new_live_until_ledger =
269                                    max(ttl_change.new_live_until_ledger, min_live_until_ledger);
270                            }
271                        }
272                    }
273                }
274            }
275            None => {
276                return Err(internal_error());
277            }
278        }
279        changes.push(entry_change);
280    }
281    Ok(changes)
282}
283
284/// Creates ledger changes for entries that don't exist in the storage.
285///
286/// In recording mode it's possible to have discrepancies between the storage
287/// and the footprint. Specifically, if an entry is only accessed from a
288/// function that has failed and had its failure handled gracefully (via
289/// `try_call`), then the storage map will get rolled back and the access will
290/// only be recorded in the footprint. However, we still need to account for
291/// these in the ledger entry changes, as downstream consumers (simulation) rely
292/// on that to determine the fees.
293#[cfg(any(test, feature = "recording_mode"))]
294fn add_footprint_only_ledger_changes(
295    budget: &Budget,
296    storage: &Storage,
297    changes: &mut Vec<LedgerEntryChange>,
298) -> Result<(), HostError> {
299    for (key, access_type) in storage.footprint.0.iter(budget)? {
300        // We have to check if the entry exists in the internal storage map
301        // because `has` check on storage affects the footprint.
302        if storage.map.contains_key::<Rc<LedgerKey>>(key, budget)? {
303            continue;
304        }
305        let mut entry_change = LedgerEntryChange::default();
306        metered_write_xdr(budget, key.as_ref(), &mut entry_change.encoded_key)?;
307        entry_change.read_only = matches!(*access_type, AccessType::ReadOnly);
308        changes.push(entry_change);
309    }
310    Ok(())
311}
312
313/// Extracts the rent-related changes from the provided ledger changes.
314///
315/// Only meaningful changes are returned (i.e. no-op changes are skipped).
316///
317/// Extracted changes can be used to compute the rent fee via `fees::compute_rent_fee`.
318pub fn extract_rent_changes(ledger_changes: &[LedgerEntryChange]) -> Vec<LedgerEntryRentChange> {
319    ledger_changes
320        .iter()
321        .filter_map(|entry_change| {
322            // Rent changes are only relevant to non-removed entries with
323            // a ttl.
324            if let (Some(ttl_change), optional_encoded_new_value) =
325                (&entry_change.ttl_change, &entry_change.encoded_new_value)
326            {
327                let new_size_bytes_for_rent = if optional_encoded_new_value.is_some() {
328                    entry_change.new_entry_size_bytes_for_rent
329                } else {
330                    entry_change.old_entry_size_bytes_for_rent
331                };
332
333                // Skip the entry if 1. it is not extended and 2. the entry size has not increased
334                if ttl_change.old_live_until_ledger >= ttl_change.new_live_until_ledger
335                    && entry_change.old_entry_size_bytes_for_rent >= new_size_bytes_for_rent
336                {
337                    return None;
338                }
339                Some(LedgerEntryRentChange {
340                    is_persistent: matches!(
341                        ttl_change.durability,
342                        ContractDataDurability::Persistent
343                    ),
344                    is_code_entry: matches!(ttl_change.entry_type, LedgerEntryType::ContractCode),
345                    old_size_bytes: entry_change.old_entry_size_bytes_for_rent,
346                    new_size_bytes: new_size_bytes_for_rent,
347                    old_live_until_ledger: ttl_change.old_live_until_ledger,
348                    new_live_until_ledger: ttl_change.new_live_until_ledger,
349                })
350            } else {
351                None
352            }
353        })
354        .collect()
355}
356
357/// Helper for computing the size of the ledger entry to be used in rent
358/// computations.
359///
360/// This returns the size of the Wasm module in memory for the contract code
361/// entries and the provided XDR size of the entry otherwise.
362///
363/// Note, that this doesn't compute the XDR size because the recomputation
364/// might be costly.
365pub fn entry_size_for_rent(
366    budget: &Budget,
367    entry: &LedgerEntry,
368    entry_xdr_size: u32,
369) -> Result<u32, HostError> {
370    Ok(match &entry.data {
371        LedgerEntryData::ContractCode(contract_code_entry) => entry_xdr_size.saturating_add(
372            wasm_module_memory_cost(budget, contract_code_entry)?.min(u32::MAX as u64) as u32,
373        ),
374        _ => entry_xdr_size,
375    })
376}
377
378/// Invokes a host function within a fresh host instance.
379///
380/// This collects the necessary inputs as encoded XDR and returns the outputs
381/// as encoded XDR as well. This is supposed to encapsulate all the metered
382/// operations needed to invoke a host function, including the input/output
383/// decoding/encoding.
384///
385/// In order to get clean budget metering data, a clean budget has to be
386/// provided as an input. It can then be examined immediately after execution in
387/// order to get the precise metering data. Budget is not reset in case of
388/// errors.
389///
390/// This may only fail when budget is exceeded or if there is an internal error.
391/// Host function invocation errors are stored within
392///  `Ok(InvokeHostFunctionResult)`.
393///
394/// When diagnostics are enabled, we try to populate `diagnostic_events`
395/// even if the `InvokeHostFunctionResult` fails for any reason.
396#[allow(clippy::too_many_arguments)]
397pub fn invoke_host_function<T: AsRef<[u8]>, I: ExactSizeIterator<Item = T>>(
398    budget: &Budget,
399    enable_diagnostics: bool,
400    encoded_host_fn: T,
401    encoded_resources: T,
402    restored_rw_entry_indices: &[u32],
403    encoded_source_account: T,
404    encoded_auth_entries: I,
405    ledger_info: LedgerInfo,
406    encoded_ledger_entries: I,
407    encoded_ttl_entries: I,
408    base_prng_seed: T,
409    diagnostic_events: &mut Vec<DiagnosticEvent>,
410    trace_hook: Option<TraceHook>,
411    module_cache: Option<ModuleCache>,
412) -> Result<InvokeHostFunctionResult, HostError> {
413    let _span0 = tracy_span!("invoke_host_function");
414
415    let resources: SorobanResources =
416        metered_from_xdr_with_budget(encoded_resources.as_ref(), &budget)?;
417    let restored_keys = build_restored_key_set(&budget, &resources, &restored_rw_entry_indices)?;
418    let footprint = build_storage_footprint_from_xdr(&budget, resources.footprint)?;
419    let current_ledger_seq = ledger_info.sequence_number;
420    let min_live_until_ledger = ledger_info
421        .min_live_until_ledger_checked(ContractDataDurability::Persistent)
422        .ok_or_else(|| {
423            HostError::from(Error::from_type_and_code(
424                ScErrorType::Context,
425                ScErrorCode::InternalError,
426            ))
427        })?;
428    let (storage_map, init_ttl_map) = build_storage_map_from_xdr_ledger_entries(
429        &budget,
430        &footprint,
431        encoded_ledger_entries,
432        encoded_ttl_entries,
433        current_ledger_seq,
434        #[cfg(any(test, feature = "recording_mode"))]
435        false,
436    )?;
437
438    let init_storage_map = storage_map.metered_clone(budget)?;
439
440    let storage = Storage::with_enforcing_footprint_and_map(footprint, storage_map);
441    let host = Host::with_storage_and_budget(storage, budget.clone());
442    let have_trace_hook = trace_hook.is_some();
443    if let Some(th) = trace_hook {
444        host.set_trace_hook(Some(th))?;
445    }
446    let auth_entries = host.build_auth_entries_from_xdr(encoded_auth_entries)?;
447    let host_function: HostFunction = host.metered_from_xdr(encoded_host_fn.as_ref())?;
448    let source_account: AccountId = host.metered_from_xdr(encoded_source_account.as_ref())?;
449    host.set_source_account(source_account)?;
450    host.set_ledger_info(ledger_info)?;
451    host.set_authorization_entries(auth_entries)?;
452    let seed32: [u8; 32] = base_prng_seed.as_ref().try_into().map_err(|_| {
453        host.err(
454            ScErrorType::Context,
455            ScErrorCode::InternalError,
456            "base PRNG seed is not 32-bytes long",
457            &[],
458        )
459    })?;
460    host.set_base_prng_seed(seed32)?;
461    if enable_diagnostics {
462        host.set_diagnostic_level(DiagnosticLevel::Debug)?;
463    }
464    if let Some(module_cache) = module_cache {
465        host.set_module_cache(module_cache)?;
466    }
467    let result = {
468        let _span1 = tracy_span!("Host::invoke_function");
469        host.invoke_function(host_function)
470    };
471    if have_trace_hook {
472        host.set_trace_hook(None)?;
473    }
474    let (storage, events) = host.try_finish()?;
475    if enable_diagnostics {
476        extract_diagnostic_events(&events, diagnostic_events);
477    }
478    let encoded_invoke_result = result.and_then(|res| {
479        let mut encoded_result_sc_val = vec![];
480        metered_write_xdr(&budget, &res, &mut encoded_result_sc_val).map(|_| encoded_result_sc_val)
481    });
482    if encoded_invoke_result.is_ok() {
483        let init_storage_snapshot = StorageMapSnapshotSource {
484            budget: &budget,
485            map: &init_storage_map,
486        };
487        let ledger_changes = get_ledger_changes(
488            &budget,
489            &storage,
490            &init_storage_snapshot,
491            init_ttl_map,
492            min_live_until_ledger,
493            &restored_keys,
494            #[cfg(any(test, feature = "recording_mode"))]
495            current_ledger_seq,
496        )?;
497        let encoded_contract_events = encode_contract_events(budget, &events)?;
498        Ok(InvokeHostFunctionResult {
499            encoded_invoke_result,
500            ledger_changes,
501            encoded_contract_events,
502        })
503    } else {
504        Ok(InvokeHostFunctionResult {
505            encoded_invoke_result,
506            ledger_changes: vec![],
507            encoded_contract_events: vec![],
508        })
509    }
510}
511
512#[cfg(any(test, feature = "recording_mode"))]
513impl Host {
514    fn to_xdr_non_metered(&self, v: &impl WriteXdr) -> Result<Vec<u8>, HostError> {
515        v.to_xdr(DEFAULT_XDR_RW_LIMITS).map_err(|_| {
516            self.err(
517                ScErrorType::Value,
518                ScErrorCode::InvalidInput,
519                "could not convert XDR struct to bytes - the input is too deep or too large",
520                &[],
521            )
522        })
523    }
524
525    fn xdr_roundtrip<T>(&self, v: &T) -> Result<T, HostError>
526    where
527        T: WriteXdr + ReadXdr,
528    {
529        self.metered_from_xdr(self.to_xdr_non_metered(v)?.as_slice())
530    }
531}
532
533#[cfg(any(test, feature = "recording_mode"))]
534fn storage_footprint_to_ledger_footprint(
535    footprint: &Footprint,
536) -> Result<LedgerFootprint, HostError> {
537    let mut read_only: Vec<LedgerKey> = Vec::with_capacity(footprint.0.len());
538    let mut read_write: Vec<LedgerKey> = Vec::with_capacity(footprint.0.len());
539    for (key, access_type) in &footprint.0 {
540        match access_type {
541            AccessType::ReadOnly => read_only.push((**key).clone()),
542            AccessType::ReadWrite => read_write.push((**key).clone()),
543        }
544    }
545    Ok(LedgerFootprint {
546        read_only: read_only
547            .try_into()
548            .map_err(|_| HostError::from((ScErrorType::Storage, ScErrorCode::InternalError)))?,
549        read_write: read_write
550            .try_into()
551            .map_err(|_| HostError::from((ScErrorType::Storage, ScErrorCode::InternalError)))?,
552    })
553}
554
555#[cfg(any(test, feature = "recording_mode"))]
556impl RecordedAuthPayload {
557    fn into_auth_entry_with_emulated_signature(
558        self,
559    ) -> Result<SorobanAuthorizationEntry, HostError> {
560        const EMULATED_SIGNATURE_SIZE: usize = 512;
561
562        match (self.address, self.nonce) {
563            (Some(address), Some(nonce)) => Ok(SorobanAuthorizationEntry {
564                credentials: SorobanCredentials::Address(SorobanAddressCredentials {
565                    address,
566                    nonce,
567                    signature_expiration_ledger: 0,
568                    signature: ScVal::Bytes(
569                        vec![0_u8; EMULATED_SIGNATURE_SIZE].try_into().unwrap(),
570                    ),
571                }),
572                root_invocation: self.invocation,
573            }),
574            (None, None) => Ok(SorobanAuthorizationEntry {
575                credentials: SorobanCredentials::SourceAccount,
576                root_invocation: self.invocation,
577            }),
578            (_, _) => Err((ScErrorType::Auth, ScErrorCode::InternalError).into()),
579        }
580    }
581}
582
583#[cfg(any(test, feature = "recording_mode"))]
584fn clear_signature(auth_entry: &mut SorobanAuthorizationEntry) {
585    match &mut auth_entry.credentials {
586        SorobanCredentials::Address(address_creds) => {
587            address_creds.signature = ScVal::Void;
588        }
589        SorobanCredentials::SourceAccount => {}
590    }
591}
592
593#[cfg(any(test, feature = "recording_mode"))]
594/// Defines the authorization mode for the `invoke_host_function_in_recording_mode`.
595pub enum RecordingInvocationAuthMode {
596    /// Use enforcing auth and pass the signed authorization entries to be used.
597    Enforcing(Vec<SorobanAuthorizationEntry>),
598    /// Use recording auth and determine whether non-root authorization is
599    /// disabled (i.e. non-root auth is not allowed when `true` is passed to
600    /// the enum).
601    Recording(bool),
602}
603
604/// Invokes a host function within a fresh host instance in 'recording' mode.
605///
606/// The purpose of recording mode is to measure the resources necessary for
607/// the invocation to succeed in the 'enforcing' mode (i.e. via
608/// `invoke_host_function`). The following information is recorded:
609///
610/// - Footprint - this is based on the ledger entries accessed.
611/// - Read/write bytes - this is based on the sizes of ledger entries read
612/// from the provided `ledger_snapshot`
613/// - Authorization mode - when the input `auth_mode` is `None`, Host
614/// switches to recording auth mode and fills the recorded data in the output.
615/// When `auth_mode` is not `None`, the authorization is performed in
616/// enforcing mode and entries from `auth_mode` are passed through to the
617/// output.
618/// - Instructions - this simply measures the instructions measured by the
619/// provided `budget`. While this function makes the best effort to emulate
620/// the work performed by `invoke_host_function`, the measured value might
621/// still be slightly lower than the actual value used during the enforcing
622/// call. Typically this difference should be within 1% from the correct
623/// value, but in scenarios where recording auth is used it might be
624/// significantly higher (e.g. if the user uses multisig with classic
625/// accounts or custom accounts - there is currently no way to emulate that
626/// in recording auth mode).
627///
628/// The input `Budget` should normally be configured to match the network
629/// limits. Exceeding the budget is the only error condition for this
630/// function, otherwise we try to populate
631/// `InvokeHostFunctionRecordingModeResult` as much as possible (e.g.
632/// if host function invocation fails, we would still populate the resources).
633///
634/// When diagnostics are enabled, we try to populate `diagnostic_events`
635/// even if the `InvokeHostFunctionResult` fails for any reason.
636#[cfg(any(test, feature = "recording_mode"))]
637#[allow(clippy::too_many_arguments)]
638pub fn invoke_host_function_in_recording_mode(
639    budget: &Budget,
640    enable_diagnostics: bool,
641    host_fn: &HostFunction,
642    source_account: &AccountId,
643    auth_mode: RecordingInvocationAuthMode,
644    ledger_info: LedgerInfo,
645    ledger_snapshot: Rc<dyn SnapshotSource>,
646    base_prng_seed: [u8; 32],
647    diagnostic_events: &mut Vec<DiagnosticEvent>,
648) -> Result<InvokeHostFunctionRecordingModeResult, HostError> {
649    let storage = Storage::with_recording_footprint(ledger_snapshot.clone());
650    let host = Host::with_storage_and_budget(storage, budget.clone());
651    let is_recording_auth = matches!(auth_mode, RecordingInvocationAuthMode::Recording(_));
652    let ledger_seq = ledger_info.sequence_number;
653    let min_live_until_ledger = ledger_info
654        .min_live_until_ledger_checked(ContractDataDurability::Persistent)
655        .ok_or_else(|| {
656            HostError::from(Error::from_type_and_code(
657                ScErrorType::Context,
658                ScErrorCode::InternalError,
659            ))
660        })?;
661    let host_function = host.xdr_roundtrip(host_fn)?;
662    let source_account: AccountId = host.xdr_roundtrip(source_account)?;
663    host.set_source_account(source_account)?;
664    host.set_ledger_info(ledger_info)?;
665    host.set_base_prng_seed(base_prng_seed)?;
666
667    match &auth_mode {
668        RecordingInvocationAuthMode::Enforcing(auth_entries) => {
669            host.set_authorization_entries(auth_entries.clone())?;
670        }
671        RecordingInvocationAuthMode::Recording(disable_non_root_auth) => {
672            host.switch_to_recording_auth(*disable_non_root_auth)?;
673        }
674    }
675
676    if enable_diagnostics {
677        host.set_diagnostic_level(DiagnosticLevel::Debug)?;
678    }
679    let invoke_result = host.invoke_function(host_function);
680    let mut contract_events_and_return_value_size = 0_u32;
681    if let Ok(res) = &invoke_result {
682        let mut encoded_result_sc_val = vec![];
683        metered_write_xdr(&budget, res, &mut encoded_result_sc_val)?;
684        contract_events_and_return_value_size = contract_events_and_return_value_size
685            .saturating_add(encoded_result_sc_val.len() as u32);
686    }
687
688    let mut output_auth = if let RecordingInvocationAuthMode::Enforcing(auth_entries) = auth_mode {
689        auth_entries
690    } else {
691        let recorded_auth = host.get_recorded_auth_payloads()?;
692        recorded_auth
693            .into_iter()
694            .map(|a| a.into_auth_entry_with_emulated_signature())
695            .collect::<Result<Vec<SorobanAuthorizationEntry>, HostError>>()?
696    };
697
698    let encoded_auth_entries = output_auth
699        .iter()
700        .map(|e| host.to_xdr_non_metered(e))
701        .collect::<Result<Vec<Vec<u8>>, HostError>>()?;
702    let decoded_auth_entries = host.build_auth_entries_from_xdr(encoded_auth_entries.iter())?;
703    if is_recording_auth {
704        host.set_authorization_entries(decoded_auth_entries)?;
705        for auth_entry in &mut output_auth {
706            clear_signature(auth_entry);
707        }
708    }
709
710    let (footprint, disk_read_bytes, init_ttl_map, restored_rw_entry_ids, restored_keys) = host
711        .with_mut_storage(|storage| {
712            let footprint = storage_footprint_to_ledger_footprint(&storage.footprint)?;
713            let _footprint_from_xdr = build_storage_footprint_from_xdr(&budget, footprint.clone())?;
714
715            let mut encoded_ledger_entries = Vec::with_capacity(storage.footprint.0.len());
716            let mut encoded_ttl_entries = Vec::with_capacity(storage.footprint.0.len());
717            let mut disk_read_bytes = 0_u32;
718            let mut current_rw_id = 0;
719            let mut restored_rw_entry_ids = vec![];
720            let mut restored_keys = RestoredKeySet::default();
721
722            for (lk, access_type) in &storage.footprint.0 {
723                let entry_with_live_until = ledger_snapshot.get(lk)?;
724                if let Some((le, live_until)) = entry_with_live_until {
725                    let encoded_le = host.to_xdr_non_metered(&*le)?;
726                    match &le.data {
727                        LedgerEntryData::ContractData(_) | LedgerEntryData::ContractCode(_) => {
728                            if let Some(live_until) = live_until {
729                                // Check if entry has been auto-restored (only persistent entries
730                                // can be auto-restored)
731                                if live_until < ledger_seq && is_persistent_key(lk.as_ref()) {
732                                    // Auto-restored entries are expected to be in RW footprint.
733                                    if !matches!(*access_type, AccessType::ReadWrite) {
734                                        return Err(HostError::from(Error::from_type_and_code(
735                                            ScErrorType::Storage,
736                                            ScErrorCode::InternalError,
737                                        )));
738                                    }
739                                    // Auto-restored entries are counted towards disk read bytes.
740                                    disk_read_bytes =
741                                        disk_read_bytes.saturating_add(encoded_le.len() as u32);
742                                    restored_rw_entry_ids.push(current_rw_id);
743                                    restored_keys =
744                                        restored_keys.insert(lk.clone(), (), &budget)?;
745                                }
746                            }
747                        }
748                        _ => {
749                            // Non-Soroban entries are counted towards disk read bytes.
750                            disk_read_bytes =
751                                disk_read_bytes.saturating_add(encoded_le.len() as u32);
752                        }
753                    }
754
755                    encoded_ledger_entries.push(encoded_le);
756                    if let Some(live_until_ledger) = live_until {
757                        let key_xdr = host.to_xdr_non_metered(lk.as_ref())?;
758                        let key_hash: [u8; 32] = Sha256::digest(&key_xdr).into();
759                        let ttl_entry = TtlEntry {
760                            key_hash: key_hash.try_into().map_err(|_| {
761                                HostError::from((ScErrorType::Context, ScErrorCode::InternalError))
762                            })?,
763                            live_until_ledger_seq: live_until_ledger,
764                        };
765                        encoded_ttl_entries.push(host.to_xdr_non_metered(&ttl_entry)?);
766                    } else {
767                        encoded_ttl_entries.push(vec![]);
768                    }
769                }
770                if matches!(*access_type, AccessType::ReadWrite) {
771                    current_rw_id += 1;
772                }
773            }
774            let (init_storage, init_ttl_map) = build_storage_map_from_xdr_ledger_entries(
775                &budget,
776                &storage.footprint,
777                encoded_ledger_entries.iter(),
778                encoded_ttl_entries.iter(),
779                ledger_seq,
780                true,
781            )?;
782            let _init_storage_clone = init_storage.metered_clone(budget)?;
783            Ok((
784                footprint,
785                disk_read_bytes,
786                init_ttl_map,
787                restored_rw_entry_ids,
788                restored_keys,
789            ))
790        })?;
791    let mut resources = SorobanResources {
792        footprint,
793        instructions: 0,
794        disk_read_bytes,
795        write_bytes: 0,
796    };
797    let _resources_roundtrip: SorobanResources =
798        host.metered_from_xdr(host.to_xdr_non_metered(&resources)?.as_slice())?;
799    let (storage, events) = host.try_finish()?;
800    if enable_diagnostics {
801        extract_diagnostic_events(&events, diagnostic_events);
802    }
803    let restored_keys = if restored_keys.map.is_empty() {
804        None
805    } else {
806        Some(restored_keys)
807    };
808    let (ledger_changes, contract_events) = if invoke_result.is_ok() {
809        let mut ledger_changes = get_ledger_changes(
810            &budget,
811            &storage,
812            &*ledger_snapshot,
813            init_ttl_map,
814            min_live_until_ledger,
815            &restored_keys,
816            ledger_seq,
817        )?;
818        // Add the keys that only exist in the footprint, but not in the
819        // storage. This doesn't resemble anything in the enforcing mode, so use
820        // the shadow budget for this.
821        budget.with_shadow_mode(|| {
822            add_footprint_only_ledger_changes(budget, &storage, &mut ledger_changes)
823        });
824
825        let encoded_contract_events = encode_contract_events(budget, &events)?;
826        for e in &encoded_contract_events {
827            contract_events_and_return_value_size =
828                contract_events_and_return_value_size.saturating_add(e.len() as u32);
829        }
830        let contract_events: Vec<ContractEvent> = events
831            .0
832            .into_iter()
833            .filter(|e| !e.failed_call && e.event.type_ != ContractEventType::Diagnostic)
834            .map(|e| e.event)
835            .collect();
836
837        (ledger_changes, contract_events)
838    } else {
839        (vec![], vec![])
840    };
841    resources.instructions = budget.get_cpu_insns_consumed()? as u32;
842    for ledger_change in &ledger_changes {
843        if !ledger_change.read_only {
844            if let Some(new_entry) = &ledger_change.encoded_new_value {
845                resources.write_bytes =
846                    resources.write_bytes.saturating_add(new_entry.len() as u32);
847            }
848        }
849    }
850
851    Ok(InvokeHostFunctionRecordingModeResult {
852        invoke_result,
853        resources,
854        restored_rw_entry_indices: restored_rw_entry_ids,
855        auth: output_auth,
856        ledger_changes,
857        contract_events,
858        contract_events_and_return_value_size,
859    })
860}
861
862/// Encodes host events as `ContractEvent` XDR.
863pub fn encode_contract_events(budget: &Budget, events: &Events) -> Result<Vec<Vec<u8>>, HostError> {
864    let ce = events
865        .0
866        .iter()
867        .filter(|e| !e.failed_call && e.event.type_ != ContractEventType::Diagnostic)
868        .map(|e| {
869            let mut buf = vec![];
870            metered_write_xdr(budget, &e.event, &mut buf)?;
871            Ok(buf)
872        })
873        .collect::<Result<Vec<Vec<u8>>, HostError>>()?;
874    // Here we collect first then charge, so that the input size excludes the diagnostic events.
875    // This means we may temporarily go over the budget limit but should be okay.
876    Vec::<Vec<u8>>::charge_bulk_init_cpy(ce.len() as u64, budget)?;
877    Ok(ce)
878}
879
880fn extract_diagnostic_events(events: &Events, diagnostic_events: &mut Vec<DiagnosticEvent>) {
881    // Important: diagnostic events should be non-metered and not fallible in
882    // order to not cause unitentional change in transaction result.
883    for event in &events.0 {
884        diagnostic_events.push(DiagnosticEvent {
885            in_successful_contract_call: !event.failed_call,
886            event: event.event.clone(),
887        });
888    }
889}
890
891pub(crate) fn ledger_entry_to_ledger_key(
892    le: &LedgerEntry,
893    budget: &Budget,
894) -> Result<LedgerKey, HostError> {
895    match &le.data {
896        LedgerEntryData::Account(a) => Ok(LedgerKey::Account(LedgerKeyAccount {
897            account_id: a.account_id.metered_clone(budget)?,
898        })),
899        LedgerEntryData::Trustline(tl) => Ok(LedgerKey::Trustline(LedgerKeyTrustLine {
900            account_id: tl.account_id.metered_clone(budget)?,
901            asset: tl.asset.metered_clone(budget)?,
902        })),
903        LedgerEntryData::ContractData(cd) => Ok(LedgerKey::ContractData(LedgerKeyContractData {
904            contract: cd.contract.metered_clone(budget)?,
905            key: cd.key.metered_clone(budget)?,
906            durability: cd.durability,
907        })),
908        LedgerEntryData::ContractCode(code) => Ok(LedgerKey::ContractCode(LedgerKeyContractCode {
909            hash: code.hash.metered_clone(budget)?,
910        })),
911        _ => {
912            return Err(Error::from_type_and_code(
913                ScErrorType::Storage,
914                ScErrorCode::InternalError,
915            )
916            .into());
917        }
918    }
919}
920
921fn build_storage_footprint_from_xdr(
922    budget: &Budget,
923    footprint: LedgerFootprint,
924) -> Result<Footprint, HostError> {
925    let mut footprint_map = FootprintMap::new();
926
927    for key in footprint.read_write.as_vec() {
928        Storage::check_supported_ledger_key_type(&key)?;
929        footprint_map = footprint_map.insert(
930            Rc::metered_new(key.metered_clone(budget)?, budget)?,
931            AccessType::ReadWrite,
932            budget,
933        )?;
934    }
935
936    for key in footprint.read_only.as_vec() {
937        Storage::check_supported_ledger_key_type(&key)?;
938        footprint_map = footprint_map.insert(
939            Rc::metered_new(key.metered_clone(budget)?, budget)?,
940            AccessType::ReadOnly,
941            budget,
942        )?;
943    }
944    Ok(Footprint(footprint_map))
945}
946
947fn build_storage_map_from_xdr_ledger_entries<T: AsRef<[u8]>, I: ExactSizeIterator<Item = T>>(
948    budget: &Budget,
949    footprint: &Footprint,
950    encoded_ledger_entries: I,
951    encoded_ttl_entries: I,
952    ledger_num: u32,
953    #[cfg(any(test, feature = "recording_mode"))] is_recording_mode: bool,
954) -> Result<(StorageMap, TtlEntryMap), HostError> {
955    let mut storage_map = StorageMap::new();
956    let mut ttl_map = TtlEntryMap::new();
957
958    if encoded_ledger_entries.len() != encoded_ttl_entries.len() {
959        return Err(
960            Error::from_type_and_code(ScErrorType::Storage, ScErrorCode::InternalError).into(),
961        );
962    }
963
964    for (entry_buf, ttl_buf) in encoded_ledger_entries.zip(encoded_ttl_entries) {
965        let mut live_until_ledger: Option<u32> = None;
966
967        let le = Rc::metered_new(
968            metered_from_xdr_with_budget::<LedgerEntry>(entry_buf.as_ref(), budget)?,
969            budget,
970        )?;
971        let key = Rc::metered_new(ledger_entry_to_ledger_key(&le, budget)?, budget)?;
972        if !ttl_buf.as_ref().is_empty() {
973            let ttl_entry = Rc::metered_new(
974                metered_from_xdr_with_budget::<TtlEntry>(ttl_buf.as_ref(), budget)?,
975                budget,
976            )?;
977            // In the default host flow (i.e. enforcing storage only) we don't
978            // expect expired entries to ever appear in the storage map, so
979            // that's always an internal error.
980            #[cfg(not(any(test, feature = "recording_mode")))]
981            if ttl_entry.live_until_ledger_seq < ledger_num {
982                #[cfg(any(test, feature = "recording_mode"))]
983                if !is_recording_mode {
984                    return Err(Error::from_type_and_code(
985                        ScErrorType::Storage,
986                        ScErrorCode::InternalError,
987                    )
988                    .into());
989                }
990            }
991            // In the recording mode we still compile both recording and
992            // enforcing functions, and we do allow expired entries in the
993            // recording mode when allow_expired_entries is true.
994            #[cfg(any(test, feature = "recording_mode"))]
995            if ttl_entry.live_until_ledger_seq < ledger_num {
996                #[cfg(any(test, feature = "recording_mode"))]
997                if !is_recording_mode {
998                    return Err(Error::from_type_and_code(
999                        ScErrorType::Storage,
1000                        ScErrorCode::InternalError,
1001                    )
1002                    .into());
1003                }
1004                // Skip expired temp entries, as these can't actually appear in
1005                // storage.
1006                if !crate::storage::is_persistent_key(key.as_ref()) {
1007                    continue;
1008                }
1009            }
1010
1011            live_until_ledger = Some(ttl_entry.live_until_ledger_seq);
1012
1013            ttl_map = ttl_map.insert(key.clone(), ttl_entry, budget)?;
1014        } else if matches!(le.as_ref().data, LedgerEntryData::ContractData(_))
1015            || matches!(le.as_ref().data, LedgerEntryData::ContractCode(_))
1016        {
1017            return Err(Error::from_type_and_code(
1018                ScErrorType::Storage,
1019                ScErrorCode::InternalError,
1020            )
1021            .into());
1022        }
1023
1024        if !footprint.0.contains_key::<LedgerKey>(&key, budget)? {
1025            return Err(Error::from_type_and_code(
1026                ScErrorType::Storage,
1027                ScErrorCode::InternalError,
1028            )
1029            .into());
1030        }
1031        storage_map = storage_map.insert(key, Some((le, live_until_ledger)), budget)?;
1032    }
1033
1034    // Add non-existing entries from the footprint to the storage.
1035    for k in footprint.0.keys(budget)? {
1036        if !storage_map.contains_key::<LedgerKey>(k, budget)? {
1037            storage_map = storage_map.insert(Rc::clone(k), None, budget)?;
1038        }
1039    }
1040    Ok((storage_map, ttl_map))
1041}
1042
1043impl Host {
1044    fn build_auth_entries_from_xdr<T: AsRef<[u8]>, I: ExactSizeIterator<Item = T>>(
1045        &self,
1046        encoded_contract_auth_entries: I,
1047    ) -> Result<Vec<SorobanAuthorizationEntry>, HostError> {
1048        encoded_contract_auth_entries
1049            .map(|buf| self.metered_from_xdr::<SorobanAuthorizationEntry>(buf.as_ref()))
1050            .metered_collect::<Result<Vec<SorobanAuthorizationEntry>, HostError>>(
1051                self.as_budget(),
1052            )?
1053    }
1054}
1055
1056struct StorageMapSnapshotSource<'a> {
1057    budget: &'a Budget,
1058    map: &'a StorageMap,
1059}
1060
1061impl SnapshotSource for StorageMapSnapshotSource<'_> {
1062    fn get(&self, key: &Rc<LedgerKey>) -> Result<Option<EntryWithLiveUntil>, HostError> {
1063        if let Some(Some((entry, live_until_ledger))) =
1064            self.map.get::<Rc<LedgerKey>>(key, self.budget)?
1065        {
1066            Ok(Some((Rc::clone(entry), *live_until_ledger)))
1067        } else {
1068            Ok(None)
1069        }
1070    }
1071}