soroban_env_host/host/
invocation_metering.rs

1use std::cell::RefMut;
2
3use soroban_env_common::Env;
4
5use crate::{
6    e2e_invoke::{encode_contract_events, entry_size_for_rent},
7    fees::{FeeConfiguration, DATA_SIZE_1KB_INCREMENT, INSTRUCTIONS_INCREMENT, TTL_ENTRY_SIZE},
8    ledger_info::get_key_durability,
9    storage::{is_persistent_key, AccessType, Storage},
10    xdr::{ContractDataDurability, LedgerKey, ScErrorCode, ScErrorType},
11};
12
13use super::{metered_xdr::metered_write_xdr, Host, HostError};
14
15/// Represents the resources measured during an invocation.
16///
17/// This resembles the resources necessary to build a Soroban transaction and
18/// compute its fee with a few exceptions (specifically, the transaction size
19/// and the return value size).
20#[derive(Default, Clone, Debug, Eq, PartialEq)]
21pub struct InvocationResources {
22    /// Number of modelled CPU instructions.
23    pub instructions: i64,
24    /// Size of modelled memory in bytes.
25    ///
26    /// Note, that the used memory does not affect the fees. It only has an
27    /// upper bound.
28    pub mem_bytes: i64,
29    /// Number of entries that need to be read from the disk.
30    ///
31    /// This is the total number of restored Soroban ledger entries and
32    /// non-Soroban entries (such as 'classic' account balances).
33    ///
34    /// Live Soroban state is stored in-memory and most of the time this
35    /// is going to be 0 or almost 0.
36    pub disk_read_entries: u32,
37    /// Number of in-memory ledger entries accessed by the invocation.
38    ///
39    /// This includes all the live Soroban entries, i.e. most of the entries
40    /// that a contract interacts with.
41    ///
42    /// Note, that this value does not affect the fees. It only has an upper
43    /// bound.
44    pub memory_read_entries: u32,
45    /// Number of entries that need to be written to the ledger due to
46    /// modification.
47    pub write_entries: u32,
48    /// Total number of bytes that need to be read from disk.
49    ///
50    /// This is the total size of restored Soroban ledger entries and
51    /// non-Soroban entries (such as 'classic' account balances).
52    ///
53    /// Live Soroban state is stored in-memory and most of the time this
54    /// is going to be 0 or almost 0.
55    pub disk_read_bytes: u32,
56    /// Total number of bytes that need to be written to the ledger.
57    pub write_bytes: u32,
58    /// Total size of the contract events emitted.
59    pub contract_events_size_bytes: u32,
60    /// Cumulative rent bump of all the persistent entries in 'ledger-bytes'.
61    /// 'Ledger-byte' is a rent bump of 1 byte for 1 ledger. Rent fee is
62    /// proportional to the total amount of 'ledger-bytes'.
63    pub persistent_rent_ledger_bytes: i64,
64    /// Number of persistent entries that had their rent bumped.
65    pub persistent_entry_rent_bumps: u32,
66    /// Cumulative rent bump of all the temporary entries in 'ledger-bytes'.
67    /// 'Ledger-byte' is a rent bump of 1 byte for 1 ledger. Rent fee is
68    /// proportional to the total amount of 'ledger-bytes'.    
69    pub temporary_rent_ledger_bytes: i64,
70    /// Number of temporary entries that had their rent bumped.
71    pub temporary_entry_rent_bumps: u32,
72}
73
74/// Detailed estimate of the transaction fees in stroops based on the
75/// `InvocationResources`.
76///
77/// Since `InvocationResources` don't account for certain metered resources,
78/// these are omitted from the estimate as well.
79#[derive(Default, Clone, Debug, Eq, PartialEq)]
80pub struct FeeEstimate {
81    /// Total fee (sum of all the remaining fields).
82    pub total: i64,
83    /// Fee for instructions.
84    pub instructions: i64,
85    /// Fee for ledger entry reads.
86    pub disk_read_entries: i64,
87    /// Fee for ledger entry writes.
88    pub write_entries: i64,
89    /// Fee for the overall size of ledger disk reads.
90    pub disk_read_bytes: i64,
91    /// Fee for the overall size of ledger writes.
92    pub write_bytes: i64,
93    /// Fee for the contract events emitted.
94    pub contract_events: i64,
95    /// Rent fee for the persistent entries.
96    pub persistent_entry_rent: i64,
97    /// Rent fee for the temporary entries.
98    pub temporary_entry_rent: i64,
99}
100
101impl InvocationResources {
102    /// Estimates the fees necessary for the current resources based on the
103    /// provided fee configuration.
104    ///
105    /// This is only an estimate and it can't be used for the actual transaction
106    /// submission (simulation using the Soroban RPC should be used instead).
107    ///
108    /// The quality of the estimate depends on the provided fee configuration,
109    /// so it must resemble the target network as close as possible.
110    pub fn estimate_fees(
111        &self,
112        fee_config: &FeeConfiguration,
113        fee_per_rent_1kb: i64,
114        persistent_rent_rate_denominator: i64,
115        temporary_rent_rate_denominator: i64,
116    ) -> FeeEstimate {
117        let instructions = compute_fee_per_increment(
118            self.instructions,
119            fee_config.fee_per_instruction_increment,
120            INSTRUCTIONS_INCREMENT,
121        );
122        let disk_read_entries = fee_config.fee_per_disk_read_entry.saturating_mul(
123            self.disk_read_entries
124                .saturating_add(self.write_entries)
125                .into(),
126        );
127        let write_entries = fee_config
128            .fee_per_write_entry
129            .saturating_mul(self.write_entries.into());
130        let disk_read_bytes = compute_fee_per_increment(
131            self.disk_read_bytes.into(),
132            fee_config.fee_per_disk_read_1kb,
133            DATA_SIZE_1KB_INCREMENT,
134        );
135        let write_bytes = compute_fee_per_increment(
136            self.write_bytes.into(),
137            fee_config.fee_per_write_1kb,
138            DATA_SIZE_1KB_INCREMENT,
139        );
140        let contract_events = compute_fee_per_increment(
141            self.contract_events_size_bytes.into(),
142            fee_config.fee_per_contract_event_1kb,
143            DATA_SIZE_1KB_INCREMENT,
144        );
145
146        let mut persistent_entry_ttl_entry_writes = fee_config
147            .fee_per_write_entry
148            .saturating_mul(self.persistent_entry_rent_bumps.into());
149        persistent_entry_ttl_entry_writes =
150            persistent_entry_ttl_entry_writes.saturating_add(compute_fee_per_increment(
151                (TTL_ENTRY_SIZE as i64).saturating_mul(self.persistent_entry_rent_bumps.into()),
152                fee_config.fee_per_write_1kb,
153                DATA_SIZE_1KB_INCREMENT,
154            ));
155        let mut temp_entry_ttl_entry_writes = fee_config
156            .fee_per_write_entry
157            .saturating_mul(self.temporary_entry_rent_bumps.into());
158        temp_entry_ttl_entry_writes =
159            temp_entry_ttl_entry_writes.saturating_add(compute_fee_per_increment(
160                (TTL_ENTRY_SIZE as i64).saturating_mul(self.temporary_entry_rent_bumps.into()),
161                fee_config.fee_per_write_1kb,
162                DATA_SIZE_1KB_INCREMENT,
163            ));
164
165        let persistent_entry_rent = compute_fee_per_increment(
166            self.persistent_rent_ledger_bytes,
167            fee_per_rent_1kb,
168            DATA_SIZE_1KB_INCREMENT.saturating_mul(persistent_rent_rate_denominator),
169        )
170        .saturating_add(persistent_entry_ttl_entry_writes);
171        let temporary_entry_rent = compute_fee_per_increment(
172            self.temporary_rent_ledger_bytes,
173            fee_per_rent_1kb,
174            DATA_SIZE_1KB_INCREMENT.saturating_mul(temporary_rent_rate_denominator),
175        )
176        .saturating_add(temp_entry_ttl_entry_writes);
177        let total = instructions
178            .saturating_add(disk_read_entries)
179            .saturating_add(write_entries)
180            .saturating_add(disk_read_bytes)
181            .saturating_add(write_bytes)
182            .saturating_add(contract_events)
183            .saturating_add(persistent_entry_rent)
184            .saturating_add(temporary_entry_rent);
185        FeeEstimate {
186            total,
187            instructions,
188            disk_read_entries,
189            write_entries,
190            disk_read_bytes,
191            write_bytes,
192            contract_events,
193            persistent_entry_rent,
194            temporary_entry_rent,
195        }
196    }
197}
198
199/// A helper for metering the resources only within a logical host invocation
200/// without finalizing the host.
201///
202/// The 'logical' invocations are the typical entry points for the unit tests,
203/// such as invocations based on `HostFunction` XDR, lifecycle operations
204/// (registering Wasm, creating a contract instance), direct contract calls
205/// etc.
206#[derive(Default, Clone)]
207pub(crate) struct InvocationMeter {
208    active: bool,
209    enabled: bool,
210    storage_snapshot: Option<Storage>,
211    invocation_resources: Option<InvocationResources>,
212}
213
214/// Scope guard for `InvocationMeter` that automatically finishes the metered
215/// invocation when it goes out of scope.
216pub(crate) struct InvocationMeterScope<'a> {
217    meter: RefMut<'a, InvocationMeter>,
218    host: &'a Host,
219}
220
221impl Drop for InvocationMeterScope<'_> {
222    fn drop(&mut self) {
223        self.meter.finish_invocation(self.host);
224    }
225}
226
227impl InvocationMeter {
228    /// Gets the metered resources for the last metered invocation (if any).
229    pub(crate) fn get_invocation_resources(&self) -> Option<InvocationResources> {
230        self.invocation_resources.clone()
231    }
232
233    fn start_invocation<'a>(
234        mut scope: RefMut<'a, InvocationMeter>,
235        host: &'a Host,
236    ) -> Result<Option<InvocationMeterScope<'a>>, HostError> {
237        if scope.active || !scope.enabled {
238            return Ok(None);
239        }
240        scope.storage_snapshot = Some(host.try_borrow_storage()?.clone());
241        // Reset all the state relevant to the invocation resources. Note, that
242        // the storage itself shouldn't be reset, as it's treated as the ledger
243        // state before invocation.
244        host.try_borrow_storage_mut()?.reset_footprint();
245        host.try_borrow_events_mut()?.clear();
246        host.budget_ref().reset()?;
247        Ok(Some(InvocationMeterScope { meter: scope, host }))
248    }
249
250    fn finish_invocation(&mut self, host: &Host) -> () {
251        self.active = false;
252        let mut invocation_resources = InvocationResources::default();
253        let budget = host.budget_ref();
254        invocation_resources.instructions =
255            budget.get_cpu_insns_consumed().unwrap_or_default() as i64;
256        invocation_resources.mem_bytes = budget.get_mem_bytes_consumed().unwrap_or_default() as i64;
257
258        let measure_res = budget.with_observable_shadow_mode(|| {
259            self.try_measure_resources(&mut invocation_resources, host)
260        });
261
262        if measure_res.is_ok() {
263            self.invocation_resources = Some(invocation_resources);
264        } else {
265            self.invocation_resources = None;
266        }
267
268        // Emulate the write-back to the module cache (typically done by the
269        // embedding environment) of any new contracts added during the
270        // invocation.
271        budget.with_shadow_mode(|| host.ensure_module_cache_contains_host_storage_contracts());
272
273        self.storage_snapshot = None;
274    }
275
276    fn try_measure_resources(
277        &mut self,
278        invocation_resources: &mut InvocationResources,
279        host: &Host,
280    ) -> Result<(), HostError> {
281        let prev_storage = self.storage_snapshot.as_mut().ok_or_else(|| {
282            host.err(
283                ScErrorType::Context,
284                ScErrorCode::InternalError,
285                "missing a storage snapshot in metering scope, `open` must be called before `close`",
286                &[],
287            )
288        })?;
289
290        let mut curr_storage = host.try_borrow_storage_mut()?;
291        let footprint = curr_storage.footprint.clone();
292        let curr_ledger_seq: u32 = host.get_ledger_sequence()?.into();
293        for (key, access_type) in footprint.0.iter(host.budget_ref())? {
294            let maybe_init_entry = prev_storage.get_from_map(key, host)?;
295            let mut init_entry_size_for_rent = 0;
296            let mut init_live_until_ledger = curr_ledger_seq;
297            let mut is_disk_read = match key.as_ref() {
298                LedgerKey::ContractData(_) | LedgerKey::ContractCode(_) => false,
299                _ => true,
300            };
301            if let Some((init_entry, init_entry_live_until)) = maybe_init_entry {
302                if let Some(live_until) = init_entry_live_until {
303                    if live_until >= curr_ledger_seq {
304                        // Only bump `init_live_until_ledger` to a value higher than the current
305                        // ledger in order to get the appropriate rent bump amount.
306                        init_live_until_ledger = live_until;
307                    } else {
308                        // If the entry is persistent and it has expired, then
309                        // we deal with the autorestore and thus need to mark
310                        // the entry as disk read.
311                        is_disk_read = is_persistent_key(key.as_ref());
312                    }
313                }
314
315                let mut buf = Vec::<u8>::new();
316                metered_write_xdr(host.budget_ref(), init_entry.as_ref(), &mut buf)?;
317                if is_disk_read {
318                    invocation_resources.disk_read_bytes += buf.len() as u32;
319                }
320                init_entry_size_for_rent =
321                    entry_size_for_rent(host.budget_ref(), &init_entry, buf.len() as u32)?;
322            }
323            let mut entry_size = 0;
324            let mut new_entry_size_for_rent = 0;
325            let mut entry_live_until_ledger = None;
326            let maybe_entry = curr_storage.try_get_full(key, host, None)?;
327            if let Some((entry, entry_live_until)) = maybe_entry {
328                let mut buf = Vec::<u8>::new();
329                metered_write_xdr(host.budget_ref(), entry.as_ref(), &mut buf)?;
330                entry_size = buf.len() as u32;
331                new_entry_size_for_rent =
332                    entry_size_for_rent(host.budget_ref(), &entry, entry_size)?;
333                entry_live_until_ledger = entry_live_until;
334            }
335            if is_disk_read {
336                invocation_resources.disk_read_entries += 1;
337            } else {
338                invocation_resources.memory_read_entries += 1;
339            }
340            if matches!(access_type, AccessType::ReadWrite) {
341                invocation_resources.write_entries += 1;
342                invocation_resources.write_bytes += entry_size;
343            }
344
345            if let Some(new_live_until) = entry_live_until_ledger {
346                let extension_ledgers = (new_live_until - init_live_until_ledger) as i64;
347                let rent_size_delta = if new_entry_size_for_rent > init_entry_size_for_rent {
348                    (new_entry_size_for_rent - init_entry_size_for_rent) as i64
349                } else {
350                    0
351                };
352                let existing_ledgers = (init_live_until_ledger - curr_ledger_seq) as i64;
353                let rent_ledger_bytes = existing_ledgers * rent_size_delta
354                    + extension_ledgers * (new_entry_size_for_rent as i64);
355                if rent_ledger_bytes > 0 {
356                    match get_key_durability(key.as_ref()) {
357                        Some(ContractDataDurability::Temporary) => {
358                            invocation_resources.temporary_rent_ledger_bytes += rent_ledger_bytes;
359                            invocation_resources.temporary_entry_rent_bumps += 1;
360                        }
361                        Some(ContractDataDurability::Persistent) => {
362                            invocation_resources.persistent_rent_ledger_bytes += rent_ledger_bytes;
363                            invocation_resources.persistent_entry_rent_bumps += 1;
364                        }
365                        None => (),
366                    }
367                }
368            }
369        }
370        let events = host.try_borrow_events()?.externalize(&host)?;
371        let encoded_contract_events = encode_contract_events(host.budget_ref(), &events)?;
372        for event in &encoded_contract_events {
373            invocation_resources.contract_events_size_bytes += event.len() as u32;
374        }
375        Ok(())
376    }
377}
378
379impl Host {
380    /// Tries to start a metered invocation, when invocation metering is enabled.
381    ///
382    /// The returned object has to stay alive while the invocation is active.
383    ///
384    /// If there is already an invocation active, returns `None`.
385    pub(crate) fn maybe_meter_invocation(
386        &self,
387    ) -> Result<Option<InvocationMeterScope<'_>>, HostError> {
388        // Note: we're using the standard `try_borrow_mut` instead of a helper
389        // generated with `impl_checked_borrow_helpers` in order to not spam
390        // the logs with failures. It is expected for metering_scope to be
391        // borrowed.
392        if let Ok(scope) = self.0.invocation_meter.try_borrow_mut() {
393            InvocationMeter::start_invocation(scope, self)
394        } else {
395            Ok(None)
396        }
397    }
398
399    /// Enables invocation metering (it's disabled by default).
400    pub fn enable_invocation_metering(&self) {
401        if let Ok(mut meter) = self.0.invocation_meter.try_borrow_mut() {
402            meter.enabled = true;
403        }
404    }
405}
406
407fn compute_fee_per_increment(resource_value: i64, fee_rate: i64, increment: i64) -> i64 {
408    num_integer::div_ceil(resource_value.saturating_mul(fee_rate), increment.max(1))
409}
410
411#[cfg(test)]
412mod test {
413    use super::*;
414    use crate::{Symbol, TryFromVal, TryIntoVal};
415    use expect_test::expect;
416    use soroban_test_wasms::CONTRACT_STORAGE;
417
418    fn assert_resources_equal_to_budget(host: &Host) {
419        assert_eq!(
420            host.get_last_invocation_resources().unwrap().instructions as u64,
421            host.budget_ref().get_cpu_insns_consumed().unwrap()
422        );
423        assert_eq!(
424            host.get_last_invocation_resources().unwrap().mem_bytes as u64,
425            host.budget_ref().get_mem_bytes_consumed().unwrap()
426        );
427    }
428
429    // run `UPDATE_EXPECT=true cargo test` to update this test.
430    // The exact values don't matter too much here (unless the diffs are
431    // produced without a protocol upgrade), but the presence/absence of certain
432    // resources is important (comments clarify which ones).
433    #[test]
434    fn test_invocation_resource_metering() {
435        let host = Host::test_host_with_recording_footprint();
436        host.enable_invocation_metering();
437        host.enable_debug().unwrap();
438        host.with_mut_ledger_info(|li| {
439            li.sequence_number = 100;
440            li.max_entry_ttl = 10000;
441            li.min_persistent_entry_ttl = 1000;
442            li.min_temp_entry_ttl = 16;
443        })
444        .unwrap();
445
446        let contract_id = host.register_test_contract_wasm(CONTRACT_STORAGE);
447        // We meter the whole registration procedure here (upload + create
448        // contract), so 2 writes/bumps are expected.
449        expect![[r#"
450            InvocationResources {
451                instructions: 4199640,
452                mem_bytes: 2863204,
453                disk_read_entries: 0,
454                memory_read_entries: 2,
455                write_entries: 2,
456                disk_read_bytes: 0,
457                write_bytes: 3132,
458                contract_events_size_bytes: 0,
459                persistent_rent_ledger_bytes: 80531388,
460                persistent_entry_rent_bumps: 2,
461                temporary_rent_ledger_bytes: 0,
462                temporary_entry_rent_bumps: 0,
463            }"#]]
464        .assert_eq(format!("{:#?}", host.get_last_invocation_resources().unwrap()).as_str());
465        assert_resources_equal_to_budget(&host);
466
467        let key = Symbol::try_from_small_str("key_1").unwrap();
468
469        // Has with no entries - no writes/rent bumps expected.
470        let _ = &host
471            .call(
472                contract_id,
473                Symbol::try_from_val(&host, &"has_persistent").unwrap(),
474                test_vec![&host, key].into(),
475            )
476            .unwrap();
477        expect![[r#"
478            InvocationResources {
479                instructions: 316637,
480                mem_bytes: 1134859,
481                disk_read_entries: 0,
482                memory_read_entries: 3,
483                write_entries: 0,
484                disk_read_bytes: 0,
485                write_bytes: 0,
486                contract_events_size_bytes: 0,
487                persistent_rent_ledger_bytes: 0,
488                persistent_entry_rent_bumps: 0,
489                temporary_rent_ledger_bytes: 0,
490                temporary_entry_rent_bumps: 0,
491            }"#]]
492        .assert_eq(format!("{:#?}", host.get_last_invocation_resources().unwrap()).as_str());
493        assert_resources_equal_to_budget(&host);
494
495        // 1 persistent write together with the respective initial rent bump.
496        let _ = &host
497            .try_call(
498                contract_id,
499                Symbol::try_from_val(&host, &"put_persistent").unwrap(),
500                test_vec![&host, key, 1234_u64].into(),
501            )
502            .unwrap();
503        expect![[r#"
504            InvocationResources {
505                instructions: 320246,
506                mem_bytes: 1135322,
507                disk_read_entries: 0,
508                memory_read_entries: 3,
509                write_entries: 1,
510                disk_read_bytes: 0,
511                write_bytes: 84,
512                contract_events_size_bytes: 0,
513                persistent_rent_ledger_bytes: 83916,
514                persistent_entry_rent_bumps: 1,
515                temporary_rent_ledger_bytes: 0,
516                temporary_entry_rent_bumps: 0,
517            }"#]]
518        .assert_eq(format!("{:#?}", host.get_last_invocation_resources().unwrap()).as_str());
519        assert_resources_equal_to_budget(&host);
520
521        // Another has check, should have more data read than the first one.
522        let _ = &host
523            .call(
524                contract_id,
525                Symbol::try_from_val(&host, &"has_persistent").unwrap(),
526                test_vec![&host, key].into(),
527            )
528            .unwrap();
529        expect![[r#"
530            InvocationResources {
531                instructions: 315936,
532                mem_bytes: 1134707,
533                disk_read_entries: 0,
534                memory_read_entries: 3,
535                write_entries: 0,
536                disk_read_bytes: 0,
537                write_bytes: 0,
538                contract_events_size_bytes: 0,
539                persistent_rent_ledger_bytes: 0,
540                persistent_entry_rent_bumps: 0,
541                temporary_rent_ledger_bytes: 0,
542                temporary_entry_rent_bumps: 0,
543            }"#]]
544        .assert_eq(format!("{:#?}", host.get_last_invocation_resources().unwrap()).as_str());
545        assert_resources_equal_to_budget(&host);
546
547        // 1 temporary entry write with the initial rent bump.
548        let _ = &host
549            .try_call(
550                contract_id,
551                Symbol::try_from_val(&host, &"put_temporary").unwrap(),
552                test_vec![&host, key, 1234_u64].into(),
553            )
554            .unwrap();
555        expect![[r#"
556            InvocationResources {
557                instructions: 322157,
558                mem_bytes: 1135678,
559                disk_read_entries: 0,
560                memory_read_entries: 3,
561                write_entries: 1,
562                disk_read_bytes: 0,
563                write_bytes: 84,
564                contract_events_size_bytes: 0,
565                persistent_rent_ledger_bytes: 0,
566                persistent_entry_rent_bumps: 0,
567                temporary_rent_ledger_bytes: 1260,
568                temporary_entry_rent_bumps: 1,
569            }"#]]
570        .assert_eq(format!("{:#?}", host.get_last_invocation_resources().unwrap()).as_str());
571        assert_resources_equal_to_budget(&host);
572
573        // Has check, same amount of data is read as for persistent has check.
574        let _ = &host
575            .try_call(
576                contract_id,
577                Symbol::try_from_val(&host, &"has_temporary").unwrap(),
578                test_vec![&host, key].into(),
579            )
580            .unwrap();
581        expect![[r#"
582            InvocationResources {
583                instructions: 316476,
584                mem_bytes: 1134775,
585                disk_read_entries: 0,
586                memory_read_entries: 3,
587                write_entries: 0,
588                disk_read_bytes: 0,
589                write_bytes: 0,
590                contract_events_size_bytes: 0,
591                persistent_rent_ledger_bytes: 0,
592                persistent_entry_rent_bumps: 0,
593                temporary_rent_ledger_bytes: 0,
594                temporary_entry_rent_bumps: 0,
595            }"#]]
596        .assert_eq(format!("{:#?}", host.get_last_invocation_resources().unwrap()).as_str());
597        assert_resources_equal_to_budget(&host);
598
599        // Extend persistent entry, 1 persistent extension is expected.
600        let _ = &host
601            .call(
602                contract_id,
603                Symbol::try_from_val(&host, &"extend_persistent").unwrap(),
604                test_vec![&host, key, &5000_u32, &5000_u32].into(),
605            )
606            .unwrap();
607        expect![[r#"
608            InvocationResources {
609                instructions: 317701,
610                mem_bytes: 1135127,
611                disk_read_entries: 0,
612                memory_read_entries: 3,
613                write_entries: 0,
614                disk_read_bytes: 0,
615                write_bytes: 0,
616                contract_events_size_bytes: 0,
617                persistent_rent_ledger_bytes: 336084,
618                persistent_entry_rent_bumps: 1,
619                temporary_rent_ledger_bytes: 0,
620                temporary_entry_rent_bumps: 0,
621            }"#]]
622        .assert_eq(format!("{:#?}", host.get_last_invocation_resources().unwrap()).as_str());
623        assert_resources_equal_to_budget(&host);
624
625        // Extend temp entry, 1 persistent extension is expected.
626        let _ = &host
627            .call(
628                contract_id,
629                Symbol::try_from_val(&host, &"extend_temporary").unwrap(),
630                test_vec![&host, key, &3000_u32, &3000_u32].into(),
631            )
632            .unwrap();
633        expect![[r#"
634            InvocationResources {
635                instructions: 318103,
636                mem_bytes: 1135127,
637                disk_read_entries: 0,
638                memory_read_entries: 3,
639                write_entries: 0,
640                disk_read_bytes: 0,
641                write_bytes: 0,
642                contract_events_size_bytes: 0,
643                persistent_rent_ledger_bytes: 0,
644                persistent_entry_rent_bumps: 0,
645                temporary_rent_ledger_bytes: 250740,
646                temporary_entry_rent_bumps: 1,
647            }"#]]
648        .assert_eq(format!("{:#?}", host.get_last_invocation_resources().unwrap()).as_str());
649        assert_resources_equal_to_budget(&host);
650
651        // Try extending entry for a non-existent key, this should fail.
652        let non_existent_key = Symbol::try_from_small_str("non_exist").unwrap();
653        let res = &host.call(
654            contract_id,
655            Symbol::try_from_val(&host, &"extend_persistent").unwrap(),
656            test_vec![&host, non_existent_key, &5000_u32, &5000_u32].into(),
657        );
658        assert!(res.is_err());
659        expect![[r#"
660            InvocationResources {
661                instructions: 317540,
662                mem_bytes: 1135195,
663                disk_read_entries: 0,
664                memory_read_entries: 3,
665                write_entries: 0,
666                disk_read_bytes: 0,
667                write_bytes: 0,
668                contract_events_size_bytes: 0,
669                persistent_rent_ledger_bytes: 0,
670                persistent_entry_rent_bumps: 0,
671                temporary_rent_ledger_bytes: 0,
672                temporary_entry_rent_bumps: 0,
673            }"#]]
674        .assert_eq(format!("{:#?}", host.get_last_invocation_resources().unwrap()).as_str());
675        assert_resources_equal_to_budget(&host);
676
677        // Advance the ledger sequence to get the contract instance and Wasm
678        // to expire.
679        host.with_mut_ledger_info(|li| {
680            li.sequence_number += li.min_persistent_entry_ttl;
681        })
682        .unwrap();
683        // `has_persistent`` check has to trigger auto-restore for 2 entries (
684        // the contract instance/code).
685        let _ = &host
686            .call(
687                contract_id,
688                Symbol::try_from_val(&host, &"has_persistent").unwrap(),
689                test_vec![&host, key].into(),
690            )
691            .unwrap();
692        expect![[r#"
693            InvocationResources {
694                instructions: 320711,
695                mem_bytes: 1135662,
696                disk_read_entries: 2,
697                memory_read_entries: 1,
698                write_entries: 2,
699                disk_read_bytes: 3132,
700                write_bytes: 3132,
701                contract_events_size_bytes: 0,
702                persistent_rent_ledger_bytes: 80531388,
703                persistent_entry_rent_bumps: 2,
704                temporary_rent_ledger_bytes: 0,
705                temporary_entry_rent_bumps: 0,
706            }"#]]
707        .assert_eq(format!("{:#?}", host.get_last_invocation_resources().unwrap()).as_str());
708        assert_resources_equal_to_budget(&host);
709
710        // Advance the ledger further to make the persistent key to expire as
711        // well.
712        host.with_mut_ledger_info(|li| {
713            li.sequence_number += 5000 - li.min_persistent_entry_ttl + 1;
714        })
715        .unwrap();
716        // 3 entries will be autorestored now.
717        let _ = &host
718            .call(
719                contract_id,
720                Symbol::try_from_val(&host, &"has_persistent").unwrap(),
721                test_vec![&host, key].into(),
722            )
723            .unwrap();
724        expect![[r#"
725            InvocationResources {
726                instructions: 323248,
727                mem_bytes: 1136109,
728                disk_read_entries: 3,
729                memory_read_entries: 0,
730                write_entries: 3,
731                disk_read_bytes: 3216,
732                write_bytes: 3216,
733                contract_events_size_bytes: 0,
734                persistent_rent_ledger_bytes: 80615304,
735                persistent_entry_rent_bumps: 3,
736                temporary_rent_ledger_bytes: 0,
737                temporary_entry_rent_bumps: 0,
738            }"#]]
739        .assert_eq(format!("{:#?}", host.get_last_invocation_resources().unwrap()).as_str());
740        assert_resources_equal_to_budget(&host);
741    }
742
743    #[test]
744    fn test_resource_fee_estimation() {
745        // No resources
746        assert_eq!(
747            InvocationResources {
748                instructions: 0,
749                mem_bytes: 100_000,
750                disk_read_entries: 0,
751                memory_read_entries: 100,
752                write_entries: 0,
753                disk_read_bytes: 0,
754                write_bytes: 0,
755                contract_events_size_bytes: 0,
756                persistent_rent_ledger_bytes: 0,
757                persistent_entry_rent_bumps: 0,
758                temporary_rent_ledger_bytes: 0,
759                temporary_entry_rent_bumps: 0,
760            }
761            .estimate_fees(
762                &FeeConfiguration {
763                    fee_per_instruction_increment: 100,
764                    fee_per_disk_read_entry: 100,
765                    fee_per_write_entry: 100,
766                    fee_per_disk_read_1kb: 100,
767                    fee_per_write_1kb: 100,
768                    fee_per_historical_1kb: 100,
769                    fee_per_contract_event_1kb: 100,
770                    fee_per_transaction_size_1kb: 100,
771                },
772                100,
773                1,
774                1
775            ),
776            FeeEstimate {
777                total: 0,
778                instructions: 0,
779                disk_read_entries: 0,
780                write_entries: 0,
781                disk_read_bytes: 0,
782                write_bytes: 0,
783                contract_events: 0,
784                persistent_entry_rent: 0,
785                temporary_entry_rent: 0,
786            }
787        );
788
789        // Minimal resources
790        assert_eq!(
791            InvocationResources {
792                instructions: 1,
793                mem_bytes: 100_000,
794                disk_read_entries: 1,
795                memory_read_entries: 100,
796                write_entries: 1,
797                disk_read_bytes: 1,
798                write_bytes: 1,
799                contract_events_size_bytes: 1,
800                persistent_rent_ledger_bytes: 1,
801                persistent_entry_rent_bumps: 1,
802                temporary_rent_ledger_bytes: 1,
803                temporary_entry_rent_bumps: 1
804            }
805            .estimate_fees(
806                &FeeConfiguration {
807                    fee_per_instruction_increment: 100,
808                    fee_per_disk_read_entry: 100,
809                    fee_per_write_entry: 100,
810                    fee_per_disk_read_1kb: 100,
811                    fee_per_write_1kb: 100,
812                    fee_per_historical_1kb: 100,
813                    fee_per_contract_event_1kb: 100,
814                    fee_per_transaction_size_1kb: 100,
815                },
816                100,
817                1,
818                1
819            ),
820            FeeEstimate {
821                total: 516,
822                instructions: 1,
823                disk_read_entries: 200,
824                write_entries: 100,
825                disk_read_bytes: 1,
826                write_bytes: 1,
827                contract_events: 1,
828                persistent_entry_rent: 106,
829                temporary_entry_rent: 106
830            }
831        );
832
833        // Different resource/fee values, based on the values from
834        // fees::resource_fee_computation test.
835        assert_eq!(
836            InvocationResources {
837                instructions: 10_123_456,
838                mem_bytes: 100_000,
839                disk_read_entries: 30,
840                memory_read_entries: 100,
841                write_entries: 10,
842                disk_read_bytes: 25_600,
843                write_bytes: 10_340,
844                contract_events_size_bytes: 321_654,
845                persistent_rent_ledger_bytes: 1_000_000_000,
846                persistent_entry_rent_bumps: 3,
847                temporary_rent_ledger_bytes: 4_000_000_000,
848                temporary_entry_rent_bumps: 6
849            }
850            .estimate_fees(
851                &FeeConfiguration {
852                    fee_per_instruction_increment: 1000,
853                    fee_per_disk_read_entry: 2000,
854                    fee_per_write_1kb: 3000,
855                    fee_per_write_entry: 4000,
856                    fee_per_disk_read_1kb: 1500,
857                    fee_per_historical_1kb: 300,
858                    fee_per_contract_event_1kb: 200,
859                    fee_per_transaction_size_1kb: 900,
860                },
861                6000,
862                1000,
863                2000
864            ),
865            FeeEstimate {
866                // 1_200_139 + event fees + rent fees
867                total: 18_878_354,
868                instructions: 1_012_346,
869                disk_read_entries: 80000,
870                write_entries: 40000,
871                disk_read_bytes: 37500,
872                write_bytes: 30293,
873                contract_events: 62824,
874                persistent_entry_rent: 5871797,
875                temporary_entry_rent: 11743594
876            }
877        );
878
879        // Integer limits
880        assert_eq!(
881            InvocationResources {
882                instructions: i64::MAX,
883                mem_bytes: i64::MAX,
884                disk_read_entries: u32::MAX,
885                memory_read_entries: 100,
886                write_entries: u32::MAX,
887                disk_read_bytes: u32::MAX,
888                write_bytes: u32::MAX,
889                contract_events_size_bytes: u32::MAX,
890                persistent_rent_ledger_bytes: i64::MAX,
891                persistent_entry_rent_bumps: u32::MAX,
892                temporary_rent_ledger_bytes: i64::MAX,
893                temporary_entry_rent_bumps: u32::MAX
894            }
895            .estimate_fees(
896                &FeeConfiguration {
897                    fee_per_instruction_increment: i64::MAX,
898                    fee_per_disk_read_entry: i64::MAX,
899                    fee_per_write_entry: i64::MAX,
900                    fee_per_disk_read_1kb: i64::MAX,
901                    fee_per_write_1kb: i64::MAX,
902                    fee_per_historical_1kb: i64::MAX,
903                    fee_per_contract_event_1kb: i64::MAX,
904                    fee_per_transaction_size_1kb: i64::MAX,
905                },
906                i64::MAX,
907                i64::MAX,
908                i64::MAX
909            ),
910            FeeEstimate {
911                total: i64::MAX,
912                instructions: 922337203685478,
913                disk_read_entries: i64::MAX,
914                write_entries: i64::MAX,
915                disk_read_bytes: 9007199254740992,
916                write_bytes: 9007199254740992,
917                contract_events: 9007199254740992,
918                persistent_entry_rent: i64::MAX,
919                temporary_entry_rent: i64::MAX
920            }
921        );
922    }
923}