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