soroban_env_host/host/
invocation_metering.rs

1use soroban_env_common::Env;
2
3use crate::{
4    builtin_contracts::account_contract::ACCOUNT_CONTRACT_CHECK_AUTH_FN_NAME,
5    e2e_invoke::{encode_contract_events, entry_size_for_rent},
6    fees::{FeeConfiguration, DATA_SIZE_1KB_INCREMENT, INSTRUCTIONS_INCREMENT, TTL_ENTRY_SIZE},
7    ledger_info::get_key_durability,
8    storage::{is_persistent_key, AccessType, Storage},
9    xdr::{
10        ContractDataDurability, ContractId, HostFunction, LedgerKey, ScAddress, ScErrorCode,
11        ScErrorType, ScSymbol,
12    },
13    AddressObject, Symbol, SymbolStr, TryFromVal,
14};
15
16use super::{metered_xdr::metered_write_xdr, Host, HostError};
17
18/// Represents the resources measured during an invocation.
19///
20/// This resembles the resources necessary to build a Soroban transaction and
21/// compute its fee with a few exceptions (specifically, the transaction size
22/// and the return value size).
23///
24/// This is almost the same as `InvocationResources`, but uses signed types
25/// everywhere because sub-invocations may actually have 'negative' resource
26/// consumption (e.g. if a contract call reduces the size of the entry that has
27/// previously been modified to a larger size).
28#[derive(Default, Clone, Debug, Eq, PartialEq)]
29pub struct InvocationResources {
30    /// Number of modelled CPU instructions.
31    pub instructions: i64,
32    /// Size of modelled memory in bytes.
33    ///
34    /// Note, that the used memory does not affect the fees. It only has an
35    /// upper bound.
36    pub mem_bytes: i64,
37    /// Number of entries that need to be read from the disk.
38    ///
39    /// This is the total number of restored Soroban ledger entries and
40    /// non-Soroban entries (such as 'classic' account balances).
41    ///
42    /// Live Soroban state is stored in-memory and most of the time this
43    /// is going to be 0 or almost 0.
44    pub disk_read_entries: u32,
45    /// Number of in-memory ledger entries accessed by the invocation.
46    ///
47    /// This includes all the live Soroban entries, i.e. most of the entries
48    /// that a contract interacts with.
49    ///
50    /// Note, that this value does not affect the fees. It only has an upper
51    /// bound.
52    pub memory_read_entries: u32,
53    /// Number of entries that need to be written to the ledger due to
54    /// modification.
55    pub write_entries: u32,
56    /// Total number of bytes that need to be read from disk.
57    ///
58    /// This is the total size of restored Soroban ledger entries and
59    /// non-Soroban entries (such as 'classic' account balances).
60    ///
61    /// Live Soroban state is stored in-memory and most of the time this
62    /// is going to be 0 or almost 0.
63    pub disk_read_bytes: u32,
64    /// Total number of bytes that need to be written to the ledger.
65    pub write_bytes: u32,
66    /// Total size of the contract events emitted.
67    pub contract_events_size_bytes: u32,
68    /// Cumulative rent bump of all the persistent entries in 'ledger-bytes'.
69    /// 'Ledger-byte' is a rent bump of 1 byte for 1 ledger. Rent fee is
70    /// proportional to the total amount of 'ledger-bytes'.
71    pub persistent_rent_ledger_bytes: i64,
72    /// Number of persistent entries that had their rent bumped.
73    pub persistent_entry_rent_bumps: u32,
74    /// Cumulative rent bump of all the temporary entries in 'ledger-bytes'.
75    /// 'Ledger-byte' is a rent bump of 1 byte for 1 ledger. Rent fee is
76    /// proportional to the total amount of 'ledger-bytes'.    
77    pub temporary_rent_ledger_bytes: i64,
78    /// Number of temporary entries that had their rent bumped.
79    pub temporary_entry_rent_bumps: u32,
80}
81
82/// Represents the resources measured during an invocation.
83///
84/// This resembles the resources necessary to build a Soroban transaction and
85/// compute its fee with a few exceptions (specifically, the transaction size
86/// and the return value size).
87#[derive(Default, Clone, Debug, Eq, PartialEq)]
88pub struct SubInvocationResources {
89    /// Number of modelled CPU instructions.
90    pub instructions: i64,
91    /// Size of modelled memory in bytes.
92    ///
93    /// Note, that the used memory does not affect the fees. It only has an
94    /// upper bound.
95    pub mem_bytes: i64,
96    /// Number of entries that need to be read from the disk.
97    ///
98    /// This is the total number of restored Soroban ledger entries and
99    /// non-Soroban entries (such as 'classic' account balances).
100    ///
101    /// Live Soroban state is stored in-memory and most of the time this
102    /// is going to be 0 or almost 0.
103    pub disk_read_entries: i32,
104    /// Number of in-memory ledger entries accessed by the invocation.
105    ///
106    /// This includes all the live Soroban entries, i.e. most of the entries
107    /// that a contract interacts with.
108    ///
109    /// Note, that this value does not affect the fees. It only has an upper
110    /// bound.
111    pub memory_read_entries: i32,
112    /// Number of entries that need to be written to the ledger due to
113    /// modification.
114    pub write_entries: i32,
115    /// Total number of bytes that need to be read from disk.
116    ///
117    /// This is the total size of restored Soroban ledger entries and
118    /// non-Soroban entries (such as 'classic' account balances).
119    ///
120    /// Live Soroban state is stored in-memory and most of the time this
121    /// is going to be 0 or almost 0.
122    pub disk_read_bytes: i32,
123    /// Total number of bytes that need to be written to the ledger.
124    pub write_bytes: i32,
125    /// Total size of the contract events emitted.
126    pub contract_events_size_bytes: i32,
127    /// Cumulative rent bump of all the persistent entries in 'ledger-bytes'.
128    /// 'Ledger-byte' is a rent bump of 1 byte for 1 ledger. Rent fee is
129    /// proportional to the total amount of 'ledger-bytes'.
130    pub persistent_rent_ledger_bytes: i64,
131    /// Number of persistent entries that had their rent bumped.
132    pub persistent_entry_rent_bumps: i32,
133    /// Cumulative rent bump of all the temporary entries in 'ledger-bytes'.
134    /// 'Ledger-byte' is a rent bump of 1 byte for 1 ledger. Rent fee is
135    /// proportional to the total amount of 'ledger-bytes'.    
136    pub temporary_rent_ledger_bytes: i64,
137    /// Number of temporary entries that had their rent bumped.
138    pub temporary_entry_rent_bumps: i32,
139}
140
141impl From<SubInvocationResources> for InvocationResources {
142    fn from(sub: SubInvocationResources) -> Self {
143        Self {
144            instructions: sub.instructions,
145            mem_bytes: sub.mem_bytes,
146            disk_read_entries: sub.disk_read_entries.max(0) as u32,
147            memory_read_entries: sub.memory_read_entries.max(0) as u32,
148            write_entries: sub.write_entries.max(0) as u32,
149            disk_read_bytes: sub.disk_read_bytes.max(0) as u32,
150            write_bytes: sub.write_bytes.max(0) as u32,
151            contract_events_size_bytes: sub.contract_events_size_bytes.max(0) as u32,
152            persistent_rent_ledger_bytes: sub.persistent_rent_ledger_bytes,
153            persistent_entry_rent_bumps: sub.persistent_entry_rent_bumps.max(0) as u32,
154            temporary_rent_ledger_bytes: sub.temporary_rent_ledger_bytes,
155            temporary_entry_rent_bumps: sub.temporary_entry_rent_bumps.max(0) as u32,
156        }
157    }
158}
159
160impl SubInvocationResources {
161    fn subtract(mut self, other: &SubInvocationResources) -> Self {
162        self.instructions = self.instructions.saturating_sub(other.instructions);
163        self.mem_bytes = self.mem_bytes.saturating_sub(other.mem_bytes);
164        self.disk_read_entries = self
165            .disk_read_entries
166            .saturating_sub(other.disk_read_entries);
167        self.memory_read_entries = self
168            .memory_read_entries
169            .saturating_sub(other.memory_read_entries);
170        self.write_entries = self.write_entries.saturating_sub(other.write_entries);
171        self.disk_read_bytes = self.disk_read_bytes.saturating_sub(other.disk_read_bytes);
172        self.write_bytes = self.write_bytes.saturating_sub(other.write_bytes);
173        self.contract_events_size_bytes = self
174            .contract_events_size_bytes
175            .saturating_sub(other.contract_events_size_bytes);
176        self.persistent_rent_ledger_bytes = self
177            .persistent_rent_ledger_bytes
178            .saturating_sub(other.persistent_rent_ledger_bytes);
179        self.persistent_entry_rent_bumps = self
180            .persistent_entry_rent_bumps
181            .saturating_sub(other.persistent_entry_rent_bumps);
182        self.temporary_rent_ledger_bytes = self
183            .temporary_rent_ledger_bytes
184            .saturating_sub(other.temporary_rent_ledger_bytes);
185        self.temporary_entry_rent_bumps = self
186            .temporary_entry_rent_bumps
187            .saturating_sub(other.temporary_entry_rent_bumps);
188        self
189    }
190}
191
192#[derive(Clone, Debug, Eq, PartialEq)]
193pub struct DetailedInvocationResources {
194    /// The type of the invocation (e.g. contract call, Wasm upload).
195    pub invocation: MeteringInvocation,
196    /// Resources measured during the invocation.
197    pub resources: SubInvocationResources,
198    /// Resources for sub-calls made during the invocation, if any.
199    /// Note, that not all the resources commute, i.e. the sum of the resources
200    pub sub_call_resources: Vec<DetailedInvocationResources>,
201}
202
203/// Detailed estimate of the transaction fees in stroops based on the
204/// `InvocationResources`.
205///
206/// Since `InvocationResources` don't account for certain metered resources,
207/// these are omitted from the estimate as well.
208#[derive(Default, Clone, Debug, Eq, PartialEq)]
209pub struct FeeEstimate {
210    /// Total fee (sum of all the remaining fields).
211    pub total: i64,
212    /// Fee for instructions.
213    pub instructions: i64,
214    /// Fee for ledger entry reads.
215    pub disk_read_entries: i64,
216    /// Fee for ledger entry writes.
217    pub write_entries: i64,
218    /// Fee for the overall size of ledger disk reads.
219    pub disk_read_bytes: i64,
220    /// Fee for the overall size of ledger writes.
221    pub write_bytes: i64,
222    /// Fee for the contract events emitted.
223    pub contract_events: i64,
224    /// Rent fee for the persistent entries.
225    pub persistent_entry_rent: i64,
226    /// Rent fee for the temporary entries.
227    pub temporary_entry_rent: i64,
228}
229
230#[derive(Clone, Debug, Eq, PartialEq)]
231pub struct DetailedFeeEstimate {
232    /// The type of the invocation (e.g. contract call, Wasm upload).
233    pub invocation: MeteringInvocation,
234    /// Fee estimate for the invocation.
235    pub fee_estimate: FeeEstimate,
236    /// Fee estimates for sub-calls made during the invocation, if any.
237    pub sub_call_fee_estimates: Vec<DetailedFeeEstimate>,
238}
239
240impl InvocationResources {
241    /// Estimates the fees necessary for the current resources based on the
242    /// provided fee configuration.
243    ///
244    /// This is only an estimate and it can't be used for the actual transaction
245    /// submission (simulation using the Soroban RPC should be used instead).
246    ///
247    /// The quality of the estimate depends on the provided fee configuration,
248    /// so it must resemble the target network as close as possible.
249    pub fn estimate_fees(
250        &self,
251        fee_config: &FeeConfiguration,
252        fee_per_rent_1kb: i64,
253        persistent_rent_rate_denominator: i64,
254        temporary_rent_rate_denominator: i64,
255    ) -> FeeEstimate {
256        let instructions = compute_fee_per_increment(
257            self.instructions,
258            fee_config.fee_per_instruction_increment,
259            INSTRUCTIONS_INCREMENT,
260        );
261        let disk_read_entries = fee_config.fee_per_disk_read_entry.saturating_mul(
262            self.disk_read_entries
263                .saturating_add(self.write_entries)
264                .into(),
265        );
266        let write_entries = fee_config
267            .fee_per_write_entry
268            .saturating_mul(self.write_entries.into());
269        let disk_read_bytes = compute_fee_per_increment(
270            self.disk_read_bytes.into(),
271            fee_config.fee_per_disk_read_1kb,
272            DATA_SIZE_1KB_INCREMENT,
273        );
274        let write_bytes = compute_fee_per_increment(
275            self.write_bytes.into(),
276            fee_config.fee_per_write_1kb,
277            DATA_SIZE_1KB_INCREMENT,
278        );
279        let contract_events = compute_fee_per_increment(
280            self.contract_events_size_bytes.into(),
281            fee_config.fee_per_contract_event_1kb,
282            DATA_SIZE_1KB_INCREMENT,
283        );
284
285        let mut persistent_entry_ttl_entry_writes = fee_config
286            .fee_per_write_entry
287            .saturating_mul(self.persistent_entry_rent_bumps.into());
288        persistent_entry_ttl_entry_writes =
289            persistent_entry_ttl_entry_writes.saturating_add(compute_fee_per_increment(
290                (TTL_ENTRY_SIZE as i64).saturating_mul(self.persistent_entry_rent_bumps.into()),
291                fee_config.fee_per_write_1kb,
292                DATA_SIZE_1KB_INCREMENT,
293            ));
294        let mut temp_entry_ttl_entry_writes = fee_config
295            .fee_per_write_entry
296            .saturating_mul(self.temporary_entry_rent_bumps.into());
297        temp_entry_ttl_entry_writes =
298            temp_entry_ttl_entry_writes.saturating_add(compute_fee_per_increment(
299                (TTL_ENTRY_SIZE as i64).saturating_mul(self.temporary_entry_rent_bumps.into()),
300                fee_config.fee_per_write_1kb,
301                DATA_SIZE_1KB_INCREMENT,
302            ));
303
304        let persistent_entry_rent = compute_fee_per_increment(
305            self.persistent_rent_ledger_bytes,
306            fee_per_rent_1kb,
307            DATA_SIZE_1KB_INCREMENT.saturating_mul(persistent_rent_rate_denominator),
308        )
309        .saturating_add(persistent_entry_ttl_entry_writes);
310        let temporary_entry_rent = compute_fee_per_increment(
311            self.temporary_rent_ledger_bytes,
312            fee_per_rent_1kb,
313            DATA_SIZE_1KB_INCREMENT.saturating_mul(temporary_rent_rate_denominator),
314        )
315        .saturating_add(temp_entry_ttl_entry_writes);
316        let total = instructions
317            .saturating_add(disk_read_entries)
318            .saturating_add(write_entries)
319            .saturating_add(disk_read_bytes)
320            .saturating_add(write_bytes)
321            .saturating_add(contract_events)
322            .saturating_add(persistent_entry_rent)
323            .saturating_add(temporary_entry_rent);
324        FeeEstimate {
325            total,
326            instructions,
327            disk_read_entries,
328            write_entries,
329            disk_read_bytes,
330            write_bytes,
331            contract_events,
332            persistent_entry_rent,
333            temporary_entry_rent,
334        }
335    }
336}
337
338impl DetailedInvocationResources {
339    /// Estimates the fees necessary for the resources for this invocation, as
340    /// well as its sub-invocations, based on the provided fee configuration.
341    ///
342    /// This is only an estimate and it can't be used for the actual transaction
343    /// submission (simulation using the Soroban RPC should be used instead).
344    ///
345    /// The quality of the estimate depends on the provided fee configuration,
346    /// so it must resemble the target network as close as possible.
347    pub fn estimate_fees(
348        &self,
349        fee_config: &FeeConfiguration,
350        fee_per_rent_1kb: i64,
351        persistent_rent_rate_denominator: i64,
352        temporary_rent_rate_denominator: i64,
353    ) -> DetailedFeeEstimate {
354        let resources: InvocationResources = self.resources.clone().into();
355        let fee_estimate = resources.estimate_fees(
356            fee_config,
357            fee_per_rent_1kb,
358            persistent_rent_rate_denominator,
359            temporary_rent_rate_denominator,
360        );
361        let sub_call_fee_estimates = self
362            .sub_call_resources
363            .iter()
364            .map(|r| {
365                r.estimate_fees(
366                    fee_config,
367                    fee_per_rent_1kb,
368                    persistent_rent_rate_denominator,
369                    temporary_rent_rate_denominator,
370                )
371            })
372            .collect();
373        DetailedFeeEstimate {
374            invocation: self.invocation.clone(),
375            fee_estimate,
376            sub_call_fee_estimates,
377        }
378    }
379}
380
381/// A helper for metering the resources only within a logical host invocation
382/// without finalizing the host.
383///
384/// The 'logical' invocations are the typical entry points for the unit tests,
385/// such as invocations based on `HostFunction` XDR, lifecycle operations
386/// (registering Wasm, creating a contract instance), direct contract calls
387/// etc.
388#[derive(Clone, Default)]
389pub(crate) struct InvocationMeter {
390    enabled: bool,
391    stack_depth: u32,
392    storage_snapshot: Storage,
393    detailed_invocation_resources: Option<DetailedInvocationResources>,
394}
395
396/// Identifies the type of invocation being metered.
397#[derive(Clone, Eq, PartialEq, Debug)]
398pub enum MeteringInvocation {
399    /// A contract function call. Contains the contract address and the name
400    /// of the function being called.
401    InvokeContract(ScAddress, ScSymbol),
402    /// Wasm upload that happened as a top-level invocation (i.e. this will not
403    /// be recorded for Wasm uploads initiated from within a contract).
404    /// The top-level Wasm uploads may only happen in transactions or in test
405    /// setup functions.
406    WasmUploadEntryPoint,
407    /// Contract creation that happened as a top-level invocation (i.e. this
408    /// will not be recorded for contract creation initiated from within a contract).
409    CreateContractEntryPoint,
410}
411
412impl MeteringInvocation {
413    pub(crate) fn from_host_function(hf: &HostFunction) -> Self {
414        match hf {
415            HostFunction::InvokeContract(invoke_args) => MeteringInvocation::InvokeContract(
416                invoke_args.contract_address.clone(),
417                invoke_args.function_name.clone(),
418            ),
419            HostFunction::UploadContractWasm(_) => MeteringInvocation::WasmUploadEntryPoint,
420            HostFunction::CreateContract(_) | HostFunction::CreateContractV2(_) => {
421                MeteringInvocation::CreateContractEntryPoint
422            }
423        }
424    }
425
426    pub(crate) fn contract_invocation_with_address_obj(
427        host: &Host,
428        address: AddressObject,
429        function_name: Symbol,
430    ) -> Self {
431        let mut address_xdr = ScAddress::Contract(Default::default());
432        let mut function_name_xdr = ScSymbol::default();
433        host.with_debug_mode(|| {
434            address_xdr = host.visit_obj(address, |a: &ScAddress| Ok(a.clone()))?;
435            function_name_xdr = SymbolStr::try_from_val(host, &function_name)?
436                .to_string()
437                .as_str()
438                .try_into()
439                .map_err(|_| {
440                    host.err(
441                        ScErrorType::Value,
442                        ScErrorCode::InternalError,
443                        "can't convert Symbol to ScSymbol",
444                        &[],
445                    )
446                })?;
447            Ok(())
448        });
449        MeteringInvocation::InvokeContract(address_xdr, function_name_xdr)
450    }
451
452    pub(crate) fn contract_invocation(
453        host: &Host,
454        contract_id: &ContractId,
455        function_name: Symbol,
456    ) -> Self {
457        let address = ScAddress::Contract(contract_id.clone());
458        let mut function_name_xdr = ScSymbol::default();
459        host.with_debug_mode(|| {
460            function_name_xdr = SymbolStr::try_from_val(host, &function_name)?
461                .to_string()
462                .as_str()
463                .try_into()
464                .map_err(|_| {
465                    host.err(
466                        ScErrorType::Value,
467                        ScErrorCode::InternalError,
468                        "can't convert Symbol to ScSymbol",
469                        &[],
470                    )
471                })?;
472            Ok(())
473        });
474        MeteringInvocation::InvokeContract(address, function_name_xdr)
475    }
476
477    pub(crate) fn check_auth_invocation(host: &Host, address: AddressObject) -> Self {
478        let mut address_xdr = ScAddress::Contract(Default::default());
479        let function_name = ACCOUNT_CONTRACT_CHECK_AUTH_FN_NAME
480            .try_into()
481            .unwrap_or_default();
482        host.with_debug_mode(|| {
483            address_xdr = host.visit_obj(address, |a: &ScAddress| Ok(a.clone()))?;
484            Ok(())
485        });
486        MeteringInvocation::InvokeContract(address_xdr, function_name)
487    }
488}
489/// Scope guard for `InvocationMeter` that automatically finishes the metered
490/// invocation when it goes out of scope.
491pub(crate) struct InvocationMeterScope<'a> {
492    host: &'a Host,
493}
494
495impl<'a> InvocationMeterScope<'a> {
496    fn new(host: &'a Host) -> Self {
497        Self { host }
498    }
499}
500impl Drop for InvocationMeterScope<'_> {
501    fn drop(&mut self) {
502        if let Ok(mut meter) = self.host.try_borrow_invocation_meter_mut() {
503            let _res = meter.pop_invocation(self.host);
504            _res.unwrap();
505        }
506    }
507}
508
509impl InvocationMeter {
510    // Gets the metered resources for the last metered invocation.
511    pub(crate) fn get_root_invocation_resources(&self) -> Option<InvocationResources> {
512        self.detailed_invocation_resources
513            .as_ref()
514            .map(|r| r.resources.clone().into())
515    }
516
517    // Gets the detailed, per-invocation metered resources for the last
518    // top-level invocation.
519    pub(crate) fn get_detailed_invocation_resources(&self) -> Option<DetailedInvocationResources> {
520        self.detailed_invocation_resources.clone()
521    }
522
523    fn push_invocation<'a>(
524        &mut self,
525        host: &'a Host,
526        invocation: MeteringInvocation,
527    ) -> Result<Option<InvocationMeterScope<'a>>, HostError> {
528        if !self.enabled {
529            return Ok(None);
530        }
531        if self.stack_depth == 0 {
532            // Reset all the state relevant to the invocation resource metering.
533            host.budget_ref().reset()?;
534            host.try_borrow_events_mut()?.clear();
535            // Note, that the storage itself shouldn't be reset, as it's treated
536            // as the ledger state before invocation.
537            host.try_borrow_storage_mut()?.reset_footprint();
538            self.storage_snapshot = host.try_borrow_storage()?.clone();
539            self.stack_depth = 1;
540            self.detailed_invocation_resources = Some(DetailedInvocationResources {
541                invocation,
542                resources: host.snapshot_current_resources(&self.storage_snapshot),
543                sub_call_resources: vec![],
544            });
545            return Ok(Some(InvocationMeterScope::new(host)));
546        }
547        let mut parent_invocation_resources =
548            self.detailed_invocation_resources.as_mut().ok_or_else(|| {
549                host.err(
550                    ScErrorType::Context,
551                    ScErrorCode::InternalError,
552                    "missing invocation resources for non-root invocation",
553                    &[],
554                )
555            })?;
556
557        for _ in 0..(self.stack_depth - 1) {
558            parent_invocation_resources = parent_invocation_resources
559                .sub_call_resources
560                .last_mut()
561                .ok_or_else(|| {
562                    host.err(
563                        ScErrorType::Context,
564                        ScErrorCode::InternalError,
565                        "incorrect stack depth for invocation metering",
566                        &[],
567                    )
568                })?;
569        }
570        // In tests we sometimes end up with multiple invocations that do
571        // effectively the same thing, e.g. a test helper that registers a
572        // contract will call a host function that creates contract, both can
573        // be an entry point for the invocation metering. In these cases we
574        // don't start a new invocation scope.
575        if parent_invocation_resources.invocation == invocation {
576            return Ok(None);
577        }
578
579        // We only meter cross-contract calls after the top-level invocation, so
580        // inner calls to upload Wasm or create contract from within a contract
581        // should not spawn a new invocation scope.
582        if !matches!(invocation, MeteringInvocation::InvokeContract(_, _)) {
583            return Ok(None);
584        }
585        parent_invocation_resources
586            .sub_call_resources
587            .push(DetailedInvocationResources {
588                invocation: invocation.clone(),
589                resources: host.snapshot_current_resources(&self.storage_snapshot),
590                sub_call_resources: vec![],
591            });
592
593        self.stack_depth += 1;
594        return Ok(Some(InvocationMeterScope::new(host)));
595    }
596
597    fn pop_invocation(&mut self, host: &Host) -> Result<(), HostError> {
598        if self.stack_depth == 0 {
599            return Ok(());
600        }
601        let mut current_invocation_resources =
602            self.detailed_invocation_resources.as_mut().ok_or_else(|| {
603                host.err(
604                    ScErrorType::Context,
605                    ScErrorCode::InternalError,
606                    "missing invocation resources for non-root invocation",
607                    &[],
608                )
609            })?;
610
611        for _ in 0..(self.stack_depth - 1) {
612            current_invocation_resources = current_invocation_resources
613                .sub_call_resources
614                .last_mut()
615                .ok_or_else(|| {
616                    host.err(
617                        ScErrorType::Context,
618                        ScErrorCode::InternalError,
619                        "incorrect stack depth for invocation metering",
620                        &[],
621                    )
622                })?;
623        }
624        current_invocation_resources.resources = host
625            .snapshot_current_resources(&self.storage_snapshot)
626            .subtract(&current_invocation_resources.resources);
627
628        // If we're popping the root invocation in test environment, we need to
629        // emulate the write-back to the module cache (typically done by the
630        // embedding environment) of any new contracts added during the
631        // invocation.
632        #[cfg(any(test, feature = "testutils"))]
633        if self.stack_depth == 1 {
634            // Subtle: we use `with_shadow_mode` instead of `with_debug_mode`
635            // here because this needs to happen even if debug mode is off and
636            // also because we need to have some side effects that are not
637            // desired in debug mode.
638            host.budget_ref()
639                .with_shadow_mode(|| host.ensure_module_cache_contains_host_storage_contracts());
640        }
641
642        self.stack_depth -= 1;
643        Ok(())
644    }
645}
646
647impl Host {
648    /// Tries to start a metered invocation, when invocation metering is enabled.
649    ///
650    /// The returned object has to stay alive while the invocation is active.
651    ///
652    /// If there is already an invocation active, returns `None`.
653    pub(crate) fn maybe_meter_invocation(
654        &self,
655        invocation: MeteringInvocation,
656    ) -> Option<InvocationMeterScope<'_>> {
657        // Note: we're using the standard `try_borrow_mut` instead of a helper
658        // generated with `impl_checked_borrow_helpers` in order to not spam
659        // the logs with failures. It is expected for metering_scope to be
660        // borrowed.
661        if let Ok(mut scope) = self.0.invocation_meter.try_borrow_mut() {
662            let res = scope.push_invocation(self, invocation);
663            if let Ok(maybe_scope) = res {
664                maybe_scope
665            } else {
666                #[cfg(any(test, feature = "testutils"))]
667                {
668                    res.unwrap();
669                }
670                None
671            }
672        } else {
673            None
674        }
675    }
676
677    /// Enables invocation metering (it's disabled by default).
678    pub fn enable_invocation_metering(&self) {
679        // This only works when debug mode is enabled. SDK enables debug mode
680        // by default, so this should be no-op for most of the current users.
681        self.enable_debug().unwrap();
682        if let Ok(mut meter) = self.0.invocation_meter.try_borrow_mut() {
683            meter.enabled = true;
684        }
685    }
686
687    fn snapshot_current_resources(
688        &self,
689        init_storage_snapshot: &Storage,
690    ) -> SubInvocationResources {
691        let mut invocation_resources = SubInvocationResources::default();
692        let budget = self.budget_ref();
693        invocation_resources.instructions =
694            budget.get_cpu_insns_consumed().unwrap_or_default() as i64;
695        invocation_resources.mem_bytes = budget.get_mem_bytes_consumed().unwrap_or_default() as i64;
696
697        // Resource measurement is best-effort, though we don't expect this to
698        // ever fail.
699        self.with_debug_mode(|| {
700            let _res = self.try_snapshot_storage_and_event_resources(
701                init_storage_snapshot,
702                &mut invocation_resources,
703            );
704            #[cfg(test)]
705            _res.unwrap();
706            Ok(())
707        });
708
709        invocation_resources
710    }
711
712    fn try_snapshot_storage_and_event_resources(
713        &self,
714        init_storage_snapshot: &Storage,
715        invocation_resources: &mut SubInvocationResources,
716    ) -> Result<(), HostError> {
717        let mut curr_storage = self.try_borrow_storage_mut()?;
718        let curr_footprint = curr_storage.footprint.clone();
719
720        let curr_ledger_seq: u32 = self.get_ledger_sequence()?.into();
721        for (key, curr_access_type) in curr_footprint.0.iter(self.budget_ref())? {
722            let maybe_init_entry = init_storage_snapshot.get_from_map(key, self)?;
723            let mut init_entry_size_for_rent = 0;
724            let mut init_live_until_ledger = curr_ledger_seq;
725            let mut is_disk_read = match key.as_ref() {
726                LedgerKey::ContractData(_) | LedgerKey::ContractCode(_) => false,
727                _ => true,
728            };
729            if let Some((init_entry, init_entry_live_until)) = maybe_init_entry {
730                if let Some(live_until) = init_entry_live_until {
731                    if live_until >= curr_ledger_seq {
732                        // Only bump `init_live_until_ledger` to a value higher than the current
733                        // ledger in order to get the appropriate rent bump amount.
734                        init_live_until_ledger = live_until;
735                    } else {
736                        // If the entry is persistent and it has expired, then
737                        // we deal with the autorestore and thus need to mark
738                        // the entry as disk read.
739                        is_disk_read = is_persistent_key(key.as_ref());
740                    }
741                }
742
743                let mut buf = Vec::<u8>::new();
744                metered_write_xdr(self.budget_ref(), init_entry.as_ref(), &mut buf)?;
745                if is_disk_read {
746                    invocation_resources.disk_read_bytes += buf.len() as i32;
747                }
748                init_entry_size_for_rent =
749                    entry_size_for_rent(self.budget_ref(), &init_entry, buf.len() as u32)?;
750            }
751            let mut entry_size = 0;
752            let mut new_entry_size_for_rent = 0;
753            let mut entry_live_until_ledger = None;
754            let maybe_entry = curr_storage.try_get_full(key, self, None)?;
755            if let Some((entry, entry_live_until)) = maybe_entry {
756                let mut buf = Vec::<u8>::new();
757                metered_write_xdr(self.budget_ref(), entry.as_ref(), &mut buf)?;
758                entry_size = buf.len() as u32;
759                new_entry_size_for_rent =
760                    entry_size_for_rent(self.budget_ref(), &entry, entry_size)?;
761                entry_live_until_ledger = entry_live_until;
762            }
763            if is_disk_read {
764                invocation_resources.disk_read_entries += 1;
765            } else {
766                invocation_resources.memory_read_entries += 1;
767            }
768            if matches!(curr_access_type, AccessType::ReadWrite) {
769                invocation_resources.write_entries += 1;
770                invocation_resources.write_bytes += entry_size as i32;
771            }
772
773            if let Some(new_live_until) = entry_live_until_ledger {
774                let extension_ledgers = (new_live_until - init_live_until_ledger) as i64;
775                let rent_size_delta = if new_entry_size_for_rent > init_entry_size_for_rent {
776                    (new_entry_size_for_rent - init_entry_size_for_rent) as i64
777                } else {
778                    0
779                };
780                let existing_ledgers = (init_live_until_ledger - curr_ledger_seq) as i64;
781                let rent_ledger_bytes = existing_ledgers * rent_size_delta
782                    + extension_ledgers * (new_entry_size_for_rent as i64);
783                if rent_ledger_bytes > 0 {
784                    match get_key_durability(key.as_ref()) {
785                        Some(ContractDataDurability::Temporary) => {
786                            invocation_resources.temporary_rent_ledger_bytes += rent_ledger_bytes;
787                            invocation_resources.temporary_entry_rent_bumps += 1;
788                        }
789                        Some(ContractDataDurability::Persistent) => {
790                            invocation_resources.persistent_rent_ledger_bytes += rent_ledger_bytes;
791                            invocation_resources.persistent_entry_rent_bumps += 1;
792                        }
793                        None => (),
794                    }
795                }
796            }
797        }
798        let events = self.try_borrow_events()?.externalize(self)?;
799        let encoded_contract_events = encode_contract_events(self.budget_ref(), &events)?;
800        for event in &encoded_contract_events {
801            invocation_resources.contract_events_size_bytes += event.len() as i32;
802        }
803        Ok(())
804    }
805}
806
807fn compute_fee_per_increment(resource_value: i64, fee_rate: i64, increment: i64) -> i64 {
808    num_integer::div_ceil(resource_value.saturating_mul(fee_rate), increment.max(1))
809}
810
811#[cfg(test)]
812mod test {
813    use super::*;
814    use crate::{
815        xdr::{ContractId, Hash},
816        Symbol, TryFromVal, TryIntoVal,
817    };
818    use expect_test::expect;
819    use soroban_test_wasms::CONTRACT_STORAGE;
820
821    fn assert_resources_equal_to_budget(host: &Host) {
822        assert_eq!(
823            host.get_last_invocation_resources().unwrap().instructions as u64,
824            host.budget_ref().get_cpu_insns_consumed().unwrap()
825        );
826        assert_eq!(
827            host.get_last_invocation_resources().unwrap().mem_bytes as u64,
828            host.budget_ref().get_mem_bytes_consumed().unwrap()
829        );
830    }
831
832    // run `UPDATE_EXPECT=true cargo test` to update this test.
833    // The exact values don't matter too much here (unless the diffs are
834    // produced without a protocol upgrade), but the presence/absence of certain
835    // resources is important (comments clarify which ones).
836    #[test]
837    fn test_invocation_resource_metering() {
838        let host = Host::test_host_with_recording_footprint();
839        host.enable_invocation_metering();
840        host.enable_debug().unwrap();
841        host.with_mut_ledger_info(|li| {
842            li.sequence_number = 100;
843            li.max_entry_ttl = 10000;
844            li.min_persistent_entry_ttl = 1000;
845            li.min_temp_entry_ttl = 16;
846        })
847        .unwrap();
848
849        let contract_id = host.register_test_contract_wasm(CONTRACT_STORAGE);
850        // We meter the whole registration procedure here (upload + create
851        // contract), so 2 writes/bumps are expected.
852        expect![[r#"
853            InvocationResources {
854                instructions: 4199640,
855                mem_bytes: 2863204,
856                disk_read_entries: 0,
857                memory_read_entries: 2,
858                write_entries: 2,
859                disk_read_bytes: 0,
860                write_bytes: 3132,
861                contract_events_size_bytes: 0,
862                persistent_rent_ledger_bytes: 80531388,
863                persistent_entry_rent_bumps: 2,
864                temporary_rent_ledger_bytes: 0,
865                temporary_entry_rent_bumps: 0,
866            }"#]]
867        .assert_eq(format!("{:#?}", host.get_last_invocation_resources().unwrap()).as_str());
868        expect![[r#"
869            DetailedInvocationResources {
870                invocation: CreateContractEntryPoint,
871                resources: SubInvocationResources {
872                    instructions: 4199640,
873                    mem_bytes: 2863204,
874                    disk_read_entries: 0,
875                    memory_read_entries: 2,
876                    write_entries: 2,
877                    disk_read_bytes: 0,
878                    write_bytes: 3132,
879                    contract_events_size_bytes: 0,
880                    persistent_rent_ledger_bytes: 80531388,
881                    persistent_entry_rent_bumps: 2,
882                    temporary_rent_ledger_bytes: 0,
883                    temporary_entry_rent_bumps: 0,
884                },
885                sub_call_resources: [],
886            }"#]]
887        .assert_eq(
888            format!(
889                "{:#?}",
890                host.get_detailed_last_invocation_resources().unwrap()
891            )
892            .as_str(),
893        );
894        assert_resources_equal_to_budget(&host);
895
896        let key = Symbol::try_from_small_str("key_1").unwrap();
897
898        // Has with no entries - no writes/rent bumps expected.
899        let _ = &host
900            .call(
901                contract_id,
902                Symbol::try_from_val(&host, &"has_persistent").unwrap(),
903                test_vec![&host, key].into(),
904            )
905            .unwrap();
906        expect![[r#"
907            InvocationResources {
908                instructions: 316637,
909                mem_bytes: 1134859,
910                disk_read_entries: 0,
911                memory_read_entries: 3,
912                write_entries: 0,
913                disk_read_bytes: 0,
914                write_bytes: 0,
915                contract_events_size_bytes: 0,
916                persistent_rent_ledger_bytes: 0,
917                persistent_entry_rent_bumps: 0,
918                temporary_rent_ledger_bytes: 0,
919                temporary_entry_rent_bumps: 0,
920            }"#]]
921        .assert_eq(format!("{:#?}", host.get_last_invocation_resources().unwrap()).as_str());
922        expect![[r#"
923            DetailedInvocationResources {
924                invocation: InvokeContract(
925                    Contract(
926                        ContractId(
927                            Hash(ba863dea340f907c97f640ecbe669125e9f8f3b63ed1f4ed0f30073b869e5441),
928                        ),
929                    ),
930                    ScSymbol(
931                        StringM(has_persistent),
932                    ),
933                ),
934                resources: SubInvocationResources {
935                    instructions: 316637,
936                    mem_bytes: 1134859,
937                    disk_read_entries: 0,
938                    memory_read_entries: 3,
939                    write_entries: 0,
940                    disk_read_bytes: 0,
941                    write_bytes: 0,
942                    contract_events_size_bytes: 0,
943                    persistent_rent_ledger_bytes: 0,
944                    persistent_entry_rent_bumps: 0,
945                    temporary_rent_ledger_bytes: 0,
946                    temporary_entry_rent_bumps: 0,
947                },
948                sub_call_resources: [],
949            }"#]]
950        .assert_eq(
951            format!(
952                "{:#?}",
953                host.get_detailed_last_invocation_resources().unwrap()
954            )
955            .as_str(),
956        );
957        assert_resources_equal_to_budget(&host);
958
959        // 1 persistent write together with the respective initial rent bump.
960        let _ = &host
961            .try_call(
962                contract_id,
963                Symbol::try_from_val(&host, &"put_persistent").unwrap(),
964                test_vec![&host, key, 1234_u64].into(),
965            )
966            .unwrap();
967        expect![[r#"
968            InvocationResources {
969                instructions: 320246,
970                mem_bytes: 1135322,
971                disk_read_entries: 0,
972                memory_read_entries: 3,
973                write_entries: 1,
974                disk_read_bytes: 0,
975                write_bytes: 84,
976                contract_events_size_bytes: 0,
977                persistent_rent_ledger_bytes: 83916,
978                persistent_entry_rent_bumps: 1,
979                temporary_rent_ledger_bytes: 0,
980                temporary_entry_rent_bumps: 0,
981            }"#]]
982        .assert_eq(format!("{:#?}", host.get_last_invocation_resources().unwrap()).as_str());
983        assert_resources_equal_to_budget(&host);
984
985        // Another has check, should have more data read than the first one.
986        let _ = &host
987            .call(
988                contract_id,
989                Symbol::try_from_val(&host, &"has_persistent").unwrap(),
990                test_vec![&host, key].into(),
991            )
992            .unwrap();
993        expect![[r#"
994            InvocationResources {
995                instructions: 315936,
996                mem_bytes: 1134707,
997                disk_read_entries: 0,
998                memory_read_entries: 3,
999                write_entries: 0,
1000                disk_read_bytes: 0,
1001                write_bytes: 0,
1002                contract_events_size_bytes: 0,
1003                persistent_rent_ledger_bytes: 0,
1004                persistent_entry_rent_bumps: 0,
1005                temporary_rent_ledger_bytes: 0,
1006                temporary_entry_rent_bumps: 0,
1007            }"#]]
1008        .assert_eq(format!("{:#?}", host.get_last_invocation_resources().unwrap()).as_str());
1009        assert_resources_equal_to_budget(&host);
1010
1011        // 1 temporary entry write with the initial rent bump.
1012        let _ = &host
1013            .try_call(
1014                contract_id,
1015                Symbol::try_from_val(&host, &"put_temporary").unwrap(),
1016                test_vec![&host, key, 1234_u64].into(),
1017            )
1018            .unwrap();
1019        expect![[r#"
1020            InvocationResources {
1021                instructions: 322157,
1022                mem_bytes: 1135678,
1023                disk_read_entries: 0,
1024                memory_read_entries: 3,
1025                write_entries: 1,
1026                disk_read_bytes: 0,
1027                write_bytes: 84,
1028                contract_events_size_bytes: 0,
1029                persistent_rent_ledger_bytes: 0,
1030                persistent_entry_rent_bumps: 0,
1031                temporary_rent_ledger_bytes: 1260,
1032                temporary_entry_rent_bumps: 1,
1033            }"#]]
1034        .assert_eq(format!("{:#?}", host.get_last_invocation_resources().unwrap()).as_str());
1035        assert_resources_equal_to_budget(&host);
1036
1037        // Has check, same amount of data is read as for persistent has check.
1038        let _ = &host
1039            .try_call(
1040                contract_id,
1041                Symbol::try_from_val(&host, &"has_temporary").unwrap(),
1042                test_vec![&host, key].into(),
1043            )
1044            .unwrap();
1045        expect![[r#"
1046            InvocationResources {
1047                instructions: 316476,
1048                mem_bytes: 1134775,
1049                disk_read_entries: 0,
1050                memory_read_entries: 3,
1051                write_entries: 0,
1052                disk_read_bytes: 0,
1053                write_bytes: 0,
1054                contract_events_size_bytes: 0,
1055                persistent_rent_ledger_bytes: 0,
1056                persistent_entry_rent_bumps: 0,
1057                temporary_rent_ledger_bytes: 0,
1058                temporary_entry_rent_bumps: 0,
1059            }"#]]
1060        .assert_eq(format!("{:#?}", host.get_last_invocation_resources().unwrap()).as_str());
1061        assert_resources_equal_to_budget(&host);
1062
1063        // Extend persistent entry, 1 persistent extension is expected.
1064        let _ = &host
1065            .call(
1066                contract_id,
1067                Symbol::try_from_val(&host, &"extend_persistent").unwrap(),
1068                test_vec![&host, key, &5000_u32, &5000_u32].into(),
1069            )
1070            .unwrap();
1071        expect![[r#"
1072            InvocationResources {
1073                instructions: 317701,
1074                mem_bytes: 1135127,
1075                disk_read_entries: 0,
1076                memory_read_entries: 3,
1077                write_entries: 0,
1078                disk_read_bytes: 0,
1079                write_bytes: 0,
1080                contract_events_size_bytes: 0,
1081                persistent_rent_ledger_bytes: 336084,
1082                persistent_entry_rent_bumps: 1,
1083                temporary_rent_ledger_bytes: 0,
1084                temporary_entry_rent_bumps: 0,
1085            }"#]]
1086        .assert_eq(format!("{:#?}", host.get_last_invocation_resources().unwrap()).as_str());
1087        assert_resources_equal_to_budget(&host);
1088
1089        // Extend temp entry, 1 persistent extension is expected.
1090        let _ = &host
1091            .call(
1092                contract_id,
1093                Symbol::try_from_val(&host, &"extend_temporary").unwrap(),
1094                test_vec![&host, key, &3000_u32, &3000_u32].into(),
1095            )
1096            .unwrap();
1097        expect![[r#"
1098            InvocationResources {
1099                instructions: 318103,
1100                mem_bytes: 1135127,
1101                disk_read_entries: 0,
1102                memory_read_entries: 3,
1103                write_entries: 0,
1104                disk_read_bytes: 0,
1105                write_bytes: 0,
1106                contract_events_size_bytes: 0,
1107                persistent_rent_ledger_bytes: 0,
1108                persistent_entry_rent_bumps: 0,
1109                temporary_rent_ledger_bytes: 250740,
1110                temporary_entry_rent_bumps: 1,
1111            }"#]]
1112        .assert_eq(format!("{:#?}", host.get_last_invocation_resources().unwrap()).as_str());
1113        assert_resources_equal_to_budget(&host);
1114
1115        // Try extending entry for a non-existent key, this should fail.
1116        let non_existent_key = Symbol::try_from_small_str("non_exist").unwrap();
1117        let res = &host.call(
1118            contract_id,
1119            Symbol::try_from_val(&host, &"extend_persistent").unwrap(),
1120            test_vec![&host, non_existent_key, &5000_u32, &5000_u32].into(),
1121        );
1122        assert!(res.is_err());
1123        expect![[r#"
1124            InvocationResources {
1125                instructions: 317540,
1126                mem_bytes: 1135195,
1127                disk_read_entries: 0,
1128                memory_read_entries: 3,
1129                write_entries: 0,
1130                disk_read_bytes: 0,
1131                write_bytes: 0,
1132                contract_events_size_bytes: 0,
1133                persistent_rent_ledger_bytes: 0,
1134                persistent_entry_rent_bumps: 0,
1135                temporary_rent_ledger_bytes: 0,
1136                temporary_entry_rent_bumps: 0,
1137            }"#]]
1138        .assert_eq(format!("{:#?}", host.get_last_invocation_resources().unwrap()).as_str());
1139        assert_resources_equal_to_budget(&host);
1140
1141        // Advance the ledger sequence to get the contract instance and Wasm
1142        // to expire.
1143        host.with_mut_ledger_info(|li| {
1144            li.sequence_number += li.min_persistent_entry_ttl;
1145        })
1146        .unwrap();
1147        // `has_persistent`` check has to trigger auto-restore for 2 entries (
1148        // the contract instance/code).
1149        let _ = &host
1150            .call(
1151                contract_id,
1152                Symbol::try_from_val(&host, &"has_persistent").unwrap(),
1153                test_vec![&host, key].into(),
1154            )
1155            .unwrap();
1156        expect![[r#"
1157            InvocationResources {
1158                instructions: 320711,
1159                mem_bytes: 1135662,
1160                disk_read_entries: 2,
1161                memory_read_entries: 1,
1162                write_entries: 2,
1163                disk_read_bytes: 3132,
1164                write_bytes: 3132,
1165                contract_events_size_bytes: 0,
1166                persistent_rent_ledger_bytes: 80531388,
1167                persistent_entry_rent_bumps: 2,
1168                temporary_rent_ledger_bytes: 0,
1169                temporary_entry_rent_bumps: 0,
1170            }"#]]
1171        .assert_eq(format!("{:#?}", host.get_last_invocation_resources().unwrap()).as_str());
1172        assert_resources_equal_to_budget(&host);
1173
1174        // Advance the ledger further to make the persistent key to expire as
1175        // well.
1176        host.with_mut_ledger_info(|li| {
1177            li.sequence_number += 5000 - li.min_persistent_entry_ttl + 1;
1178        })
1179        .unwrap();
1180        // 3 entries will be autorestored now.
1181        let _ = &host
1182            .call(
1183                contract_id,
1184                Symbol::try_from_val(&host, &"has_persistent").unwrap(),
1185                test_vec![&host, key].into(),
1186            )
1187            .unwrap();
1188        expect![[r#"
1189            InvocationResources {
1190                instructions: 323248,
1191                mem_bytes: 1136109,
1192                disk_read_entries: 3,
1193                memory_read_entries: 0,
1194                write_entries: 3,
1195                disk_read_bytes: 3216,
1196                write_bytes: 3216,
1197                contract_events_size_bytes: 0,
1198                persistent_rent_ledger_bytes: 80615304,
1199                persistent_entry_rent_bumps: 3,
1200                temporary_rent_ledger_bytes: 0,
1201                temporary_entry_rent_bumps: 0,
1202            }"#]]
1203        .assert_eq(format!("{:#?}", host.get_last_invocation_resources().unwrap()).as_str());
1204        assert_resources_equal_to_budget(&host);
1205    }
1206
1207    #[test]
1208    fn test_resource_fee_estimation() {
1209        // No resources
1210        assert_eq!(
1211            InvocationResources {
1212                instructions: 0,
1213                mem_bytes: 100_000,
1214                disk_read_entries: 0,
1215                memory_read_entries: 100,
1216                write_entries: 0,
1217                disk_read_bytes: 0,
1218                write_bytes: 0,
1219                contract_events_size_bytes: 0,
1220                persistent_rent_ledger_bytes: 0,
1221                persistent_entry_rent_bumps: 0,
1222                temporary_rent_ledger_bytes: 0,
1223                temporary_entry_rent_bumps: 0,
1224            }
1225            .estimate_fees(
1226                &FeeConfiguration {
1227                    fee_per_instruction_increment: 100,
1228                    fee_per_disk_read_entry: 100,
1229                    fee_per_write_entry: 100,
1230                    fee_per_disk_read_1kb: 100,
1231                    fee_per_write_1kb: 100,
1232                    fee_per_historical_1kb: 100,
1233                    fee_per_contract_event_1kb: 100,
1234                    fee_per_transaction_size_1kb: 100,
1235                },
1236                100,
1237                1,
1238                1
1239            ),
1240            FeeEstimate {
1241                total: 0,
1242                instructions: 0,
1243                disk_read_entries: 0,
1244                write_entries: 0,
1245                disk_read_bytes: 0,
1246                write_bytes: 0,
1247                contract_events: 0,
1248                persistent_entry_rent: 0,
1249                temporary_entry_rent: 0,
1250            }
1251        );
1252
1253        // Minimal resources
1254        assert_eq!(
1255            InvocationResources {
1256                instructions: 1,
1257                mem_bytes: 100_000,
1258                disk_read_entries: 1,
1259                memory_read_entries: 100,
1260                write_entries: 1,
1261                disk_read_bytes: 1,
1262                write_bytes: 1,
1263                contract_events_size_bytes: 1,
1264                persistent_rent_ledger_bytes: 1,
1265                persistent_entry_rent_bumps: 1,
1266                temporary_rent_ledger_bytes: 1,
1267                temporary_entry_rent_bumps: 1
1268            }
1269            .estimate_fees(
1270                &FeeConfiguration {
1271                    fee_per_instruction_increment: 100,
1272                    fee_per_disk_read_entry: 100,
1273                    fee_per_write_entry: 100,
1274                    fee_per_disk_read_1kb: 100,
1275                    fee_per_write_1kb: 100,
1276                    fee_per_historical_1kb: 100,
1277                    fee_per_contract_event_1kb: 100,
1278                    fee_per_transaction_size_1kb: 100,
1279                },
1280                100,
1281                1,
1282                1
1283            ),
1284            FeeEstimate {
1285                total: 516,
1286                instructions: 1,
1287                disk_read_entries: 200,
1288                write_entries: 100,
1289                disk_read_bytes: 1,
1290                write_bytes: 1,
1291                contract_events: 1,
1292                persistent_entry_rent: 106,
1293                temporary_entry_rent: 106
1294            }
1295        );
1296
1297        // Different resource/fee values, based on the values from
1298        // fees::resource_fee_computation test.
1299        assert_eq!(
1300            InvocationResources {
1301                instructions: 10_123_456,
1302                mem_bytes: 100_000,
1303                disk_read_entries: 30,
1304                memory_read_entries: 100,
1305                write_entries: 10,
1306                disk_read_bytes: 25_600,
1307                write_bytes: 10_340,
1308                contract_events_size_bytes: 321_654,
1309                persistent_rent_ledger_bytes: 1_000_000_000,
1310                persistent_entry_rent_bumps: 3,
1311                temporary_rent_ledger_bytes: 4_000_000_000,
1312                temporary_entry_rent_bumps: 6
1313            }
1314            .estimate_fees(
1315                &FeeConfiguration {
1316                    fee_per_instruction_increment: 1000,
1317                    fee_per_disk_read_entry: 2000,
1318                    fee_per_write_1kb: 3000,
1319                    fee_per_write_entry: 4000,
1320                    fee_per_disk_read_1kb: 1500,
1321                    fee_per_historical_1kb: 300,
1322                    fee_per_contract_event_1kb: 200,
1323                    fee_per_transaction_size_1kb: 900,
1324                },
1325                6000,
1326                1000,
1327                2000
1328            ),
1329            FeeEstimate {
1330                // 1_200_139 + event fees + rent fees
1331                total: 18_878_354,
1332                instructions: 1_012_346,
1333                disk_read_entries: 80000,
1334                write_entries: 40000,
1335                disk_read_bytes: 37500,
1336                write_bytes: 30293,
1337                contract_events: 62824,
1338                persistent_entry_rent: 5871797,
1339                temporary_entry_rent: 11743594
1340            }
1341        );
1342
1343        // Integer limits
1344        assert_eq!(
1345            InvocationResources {
1346                instructions: i64::MAX,
1347                mem_bytes: i64::MAX,
1348                disk_read_entries: u32::MAX,
1349                memory_read_entries: 100,
1350                write_entries: u32::MAX,
1351                disk_read_bytes: u32::MAX,
1352                write_bytes: u32::MAX,
1353                contract_events_size_bytes: u32::MAX,
1354                persistent_rent_ledger_bytes: i64::MAX,
1355                persistent_entry_rent_bumps: u32::MAX,
1356                temporary_rent_ledger_bytes: i64::MAX,
1357                temporary_entry_rent_bumps: u32::MAX
1358            }
1359            .estimate_fees(
1360                &FeeConfiguration {
1361                    fee_per_instruction_increment: i64::MAX,
1362                    fee_per_disk_read_entry: i64::MAX,
1363                    fee_per_write_entry: i64::MAX,
1364                    fee_per_disk_read_1kb: i64::MAX,
1365                    fee_per_write_1kb: i64::MAX,
1366                    fee_per_historical_1kb: i64::MAX,
1367                    fee_per_contract_event_1kb: i64::MAX,
1368                    fee_per_transaction_size_1kb: i64::MAX,
1369                },
1370                i64::MAX,
1371                i64::MAX,
1372                i64::MAX
1373            ),
1374            FeeEstimate {
1375                total: i64::MAX,
1376                instructions: 922337203685478,
1377                disk_read_entries: i64::MAX,
1378                write_entries: i64::MAX,
1379                disk_read_bytes: 9007199254740992,
1380                write_bytes: 9007199254740992,
1381                contract_events: 9007199254740992,
1382                persistent_entry_rent: i64::MAX,
1383                temporary_entry_rent: i64::MAX
1384            }
1385        );
1386    }
1387
1388    #[test]
1389    fn test_estimate_detailed_fees() {
1390        let resources = DetailedInvocationResources {
1391            invocation: MeteringInvocation::InvokeContract(
1392                ScAddress::Contract(ContractId(Hash([1; 32]))),
1393                "foo".try_into().unwrap(),
1394            ),
1395            resources: SubInvocationResources {
1396                instructions: 10_123_456,
1397                mem_bytes: 100_000,
1398                disk_read_entries: 30,
1399                memory_read_entries: 100,
1400                write_entries: 10,
1401                disk_read_bytes: 25_600,
1402                write_bytes: 10_340,
1403                contract_events_size_bytes: 321_654,
1404                persistent_rent_ledger_bytes: 1_000_000_000,
1405                persistent_entry_rent_bumps: 3,
1406                temporary_rent_ledger_bytes: 4_000_000_000,
1407                temporary_entry_rent_bumps: 6,
1408            },
1409            sub_call_resources: vec![
1410                DetailedInvocationResources {
1411                    invocation: MeteringInvocation::WasmUploadEntryPoint,
1412                    resources: SubInvocationResources {
1413                        instructions: 1,
1414                        mem_bytes: 200_000,
1415                        disk_read_entries: 1,
1416                        memory_read_entries: 100,
1417                        write_entries: 1,
1418                        disk_read_bytes: 1,
1419                        write_bytes: 1,
1420                        contract_events_size_bytes: 1,
1421                        persistent_rent_ledger_bytes: 1,
1422                        persistent_entry_rent_bumps: 1,
1423                        temporary_rent_ledger_bytes: 1,
1424                        temporary_entry_rent_bumps: 1,
1425                    },
1426                    sub_call_resources: vec![DetailedInvocationResources {
1427                        invocation: MeteringInvocation::CreateContractEntryPoint,
1428                        resources: SubInvocationResources {
1429                            instructions: 0,
1430                            mem_bytes: 300_000,
1431                            disk_read_entries: 0,
1432                            memory_read_entries: 100,
1433                            write_entries: 0,
1434                            disk_read_bytes: 0,
1435                            write_bytes: 0,
1436                            contract_events_size_bytes: 0,
1437                            persistent_rent_ledger_bytes: 0,
1438                            persistent_entry_rent_bumps: 0,
1439                            temporary_rent_ledger_bytes: 0,
1440                            temporary_entry_rent_bumps: 0,
1441                        },
1442                        sub_call_resources: vec![],
1443                    }],
1444                },
1445                DetailedInvocationResources {
1446                    invocation: MeteringInvocation::InvokeContract(
1447                        ScAddress::Contract(ContractId(Hash([2; 32]))),
1448                        "bar".try_into().unwrap(),
1449                    ),
1450                    resources: SubInvocationResources {
1451                        instructions: 10_000,
1452                        mem_bytes: 500_000,
1453                        contract_events_size_bytes: 100,
1454                        // All the storage metrics may be negative in case if
1455                        // a contract call deletes previously modified entries.
1456                        // We just treat them as zeroes for fee estimation.
1457                        disk_read_entries: -1,
1458                        memory_read_entries: -2,
1459                        write_entries: -3,
1460                        disk_read_bytes: -4,
1461                        write_bytes: -5,
1462                        persistent_rent_ledger_bytes: -6,
1463                        persistent_entry_rent_bumps: -7,
1464                        temporary_rent_ledger_bytes: -8,
1465                        temporary_entry_rent_bumps: -9,
1466                    },
1467                    sub_call_resources: vec![],
1468                },
1469            ],
1470        };
1471
1472        let fee_estimate = resources.estimate_fees(
1473            &FeeConfiguration {
1474                fee_per_instruction_increment: 1000,
1475                fee_per_disk_read_entry: 2000,
1476                fee_per_write_1kb: 3000,
1477                fee_per_write_entry: 4000,
1478                fee_per_disk_read_1kb: 1500,
1479                fee_per_historical_1kb: 300,
1480                fee_per_contract_event_1kb: 200,
1481                fee_per_transaction_size_1kb: 900,
1482            },
1483            6000,
1484            1000,
1485            2000,
1486        );
1487        expect![[r#"
1488            DetailedFeeEstimate {
1489                invocation: InvokeContract(
1490                    Contract(
1491                        ContractId(
1492                            Hash(0101010101010101010101010101010101010101010101010101010101010101),
1493                        ),
1494                    ),
1495                    ScSymbol(
1496                        StringM(foo),
1497                    ),
1498                ),
1499                fee_estimate: FeeEstimate {
1500                    total: 18878354,
1501                    instructions: 1012346,
1502                    disk_read_entries: 80000,
1503                    write_entries: 40000,
1504                    disk_read_bytes: 37500,
1505                    write_bytes: 30293,
1506                    contract_events: 62824,
1507                    persistent_entry_rent: 5871797,
1508                    temporary_entry_rent: 11743594,
1509                },
1510                sub_call_fee_estimates: [
1511                    DetailedFeeEstimate {
1512                        invocation: WasmUploadEntryPoint,
1513                        fee_estimate: FeeEstimate {
1514                            total: 16291,
1515                            instructions: 1,
1516                            disk_read_entries: 4000,
1517                            write_entries: 4000,
1518                            disk_read_bytes: 2,
1519                            write_bytes: 3,
1520                            contract_events: 1,
1521                            persistent_entry_rent: 4142,
1522                            temporary_entry_rent: 4142,
1523                        },
1524                        sub_call_fee_estimates: [
1525                            DetailedFeeEstimate {
1526                                invocation: CreateContractEntryPoint,
1527                                fee_estimate: FeeEstimate {
1528                                    total: 0,
1529                                    instructions: 0,
1530                                    disk_read_entries: 0,
1531                                    write_entries: 0,
1532                                    disk_read_bytes: 0,
1533                                    write_bytes: 0,
1534                                    contract_events: 0,
1535                                    persistent_entry_rent: 0,
1536                                    temporary_entry_rent: 0,
1537                                },
1538                                sub_call_fee_estimates: [],
1539                            },
1540                        ],
1541                    },
1542                    DetailedFeeEstimate {
1543                        invocation: InvokeContract(
1544                            Contract(
1545                                ContractId(
1546                                    Hash(0202020202020202020202020202020202020202020202020202020202020202),
1547                                ),
1548                            ),
1549                            ScSymbol(
1550                                StringM(bar),
1551                            ),
1552                        ),
1553                        fee_estimate: FeeEstimate {
1554                            total: 1020,
1555                            instructions: 1000,
1556                            disk_read_entries: 0,
1557                            write_entries: 0,
1558                            disk_read_bytes: 0,
1559                            write_bytes: 0,
1560                            contract_events: 20,
1561                            persistent_entry_rent: 0,
1562                            temporary_entry_rent: 0,
1563                        },
1564                        sub_call_fee_estimates: [],
1565                    },
1566                ],
1567            }"#]]
1568        .assert_eq(format!("{:#?}", fee_estimate).as_str());
1569    }
1570}