use std::cell::RefMut;
use soroban_env_common::Env;
use crate::{
e2e_invoke::encode_contract_events,
fees::{FeeConfiguration, DATA_SIZE_1KB_INCREMENT, INSTRUCTIONS_INCREMENT, TTL_ENTRY_SIZE},
ledger_info::get_key_durability,
storage::{AccessType, Storage},
xdr::{ContractDataDurability, ScErrorCode, ScErrorType},
};
use super::{metered_xdr::metered_write_xdr, Host, HostError};
#[derive(Default, Clone, Debug, Eq, PartialEq)]
pub struct InvocationResources {
pub instructions: i64,
pub mem_bytes: i64,
pub read_entries: u32,
pub write_entries: u32,
pub read_bytes: u32,
pub write_bytes: u32,
pub contract_events_size_bytes: u32,
pub persistent_rent_ledger_bytes: i64,
pub persistent_entry_rent_bumps: u32,
pub temporary_rent_ledger_bytes: i64,
pub temporary_entry_rent_bumps: u32,
}
#[derive(Default, Clone, Debug, Eq, PartialEq)]
pub struct FeeEstimate {
pub total: i64,
pub instructions: i64,
pub read_entries: i64,
pub write_entries: i64,
pub read_bytes: i64,
pub write_bytes: i64,
pub contract_events: i64,
pub persistent_entry_rent: i64,
pub temporary_entry_rent: i64,
}
impl InvocationResources {
pub fn estimate_fees(
&self,
fee_config: &FeeConfiguration,
persistent_rent_rate_denominator: i64,
temporary_rent_rate_denominator: i64,
) -> FeeEstimate {
let instructions = compute_fee_per_increment(
self.instructions,
fee_config.fee_per_instruction_increment,
INSTRUCTIONS_INCREMENT,
);
let read_entries = fee_config
.fee_per_read_entry
.saturating_mul(self.read_entries.saturating_add(self.write_entries).into());
let write_entries = fee_config
.fee_per_write_entry
.saturating_mul(self.write_entries.into());
let read_bytes = compute_fee_per_increment(
self.read_bytes.into(),
fee_config.fee_per_read_1kb,
DATA_SIZE_1KB_INCREMENT,
);
let write_bytes = compute_fee_per_increment(
self.write_bytes.into(),
fee_config.fee_per_write_1kb,
DATA_SIZE_1KB_INCREMENT,
);
let contract_events = compute_fee_per_increment(
self.contract_events_size_bytes.into(),
fee_config.fee_per_contract_event_1kb,
DATA_SIZE_1KB_INCREMENT,
);
let mut persistent_entry_ttl_entry_writes = fee_config
.fee_per_write_entry
.saturating_mul(self.persistent_entry_rent_bumps.into());
persistent_entry_ttl_entry_writes =
persistent_entry_ttl_entry_writes.saturating_add(compute_fee_per_increment(
(TTL_ENTRY_SIZE as i64).saturating_mul(self.persistent_entry_rent_bumps.into()),
fee_config.fee_per_write_1kb,
DATA_SIZE_1KB_INCREMENT,
));
let mut temp_entry_ttl_entry_writes = fee_config
.fee_per_write_entry
.saturating_mul(self.temporary_entry_rent_bumps.into());
temp_entry_ttl_entry_writes =
temp_entry_ttl_entry_writes.saturating_add(compute_fee_per_increment(
(TTL_ENTRY_SIZE as i64).saturating_mul(self.temporary_entry_rent_bumps.into()),
fee_config.fee_per_write_1kb,
DATA_SIZE_1KB_INCREMENT,
));
let persistent_entry_rent = compute_fee_per_increment(
self.persistent_rent_ledger_bytes,
fee_config.fee_per_write_1kb,
DATA_SIZE_1KB_INCREMENT.saturating_mul(persistent_rent_rate_denominator),
)
.saturating_add(persistent_entry_ttl_entry_writes);
let temporary_entry_rent = compute_fee_per_increment(
self.temporary_rent_ledger_bytes,
fee_config.fee_per_write_1kb,
DATA_SIZE_1KB_INCREMENT.saturating_mul(temporary_rent_rate_denominator),
)
.saturating_add(temp_entry_ttl_entry_writes);
let total = instructions
.saturating_add(read_entries)
.saturating_add(write_entries)
.saturating_add(read_bytes)
.saturating_add(write_bytes)
.saturating_add(contract_events)
.saturating_add(persistent_entry_rent)
.saturating_add(temporary_entry_rent);
FeeEstimate {
total,
instructions,
read_entries,
write_entries,
read_bytes,
write_bytes,
contract_events,
persistent_entry_rent,
temporary_entry_rent,
}
}
}
#[derive(Default, Clone)]
pub(crate) struct InvocationMeter {
active: bool,
enabled: bool,
storage_snapshot: Option<Storage>,
invocation_resources: Option<InvocationResources>,
}
pub(crate) struct InvocationMeterScope<'a> {
meter: RefMut<'a, InvocationMeter>,
host: &'a Host,
}
impl Drop for InvocationMeterScope<'_> {
fn drop(&mut self) {
self.meter.finish_invocation(self.host);
}
}
impl InvocationMeter {
pub(crate) fn get_invocation_resources(&self) -> Option<InvocationResources> {
self.invocation_resources.clone()
}
fn start_invocation<'a>(
mut scope: RefMut<'a, InvocationMeter>,
host: &'a Host,
) -> Result<Option<InvocationMeterScope<'a>>, HostError> {
if scope.active || !scope.enabled {
return Ok(None);
}
scope.storage_snapshot = Some(host.try_borrow_storage()?.clone());
host.try_borrow_storage_mut()?.reset_footprint();
host.try_borrow_events_mut()?.clear();
host.budget_ref().reset()?;
Ok(Some(InvocationMeterScope { meter: scope, host }))
}
fn finish_invocation(&mut self, host: &Host) -> () {
self.active = false;
let mut invocation_resources = InvocationResources::default();
let budget = host.budget_ref();
invocation_resources.instructions =
budget.get_cpu_insns_consumed().unwrap_or_default() as i64;
invocation_resources.mem_bytes = budget.get_mem_bytes_consumed().unwrap_or_default() as i64;
let measure_res = budget.with_observable_shadow_mode(|| {
self.try_measure_resources(&mut invocation_resources, host)
});
if measure_res.is_ok() {
self.invocation_resources = Some(invocation_resources);
} else {
self.invocation_resources = None;
}
self.storage_snapshot = None;
}
fn try_measure_resources(
&mut self,
invocation_resources: &mut InvocationResources,
host: &Host,
) -> Result<(), HostError> {
let prev_storage = self.storage_snapshot.as_mut().ok_or_else(|| {
host.err(
ScErrorType::Context,
ScErrorCode::InternalError,
"missing a storage snapshot in metering scope, `open` must be called before `close`",
&[],
)
})?;
let mut curr_storage = host.try_borrow_storage_mut()?;
let footprint = curr_storage.footprint.clone();
let curr_ledger_seq: u32 = host.get_ledger_sequence()?.into();
for (key, access_type) in footprint.0.iter(host.budget_ref())? {
let maybe_init_entry = prev_storage.try_get_full_with_host(key, host, None)?;
let mut init_entry_size = 0;
let mut init_live_until_ledger = curr_ledger_seq;
if let Some((init_entry, init_entry_live_until)) = maybe_init_entry {
let mut buf = Vec::<u8>::new();
metered_write_xdr(host.budget_ref(), init_entry.as_ref(), &mut buf)?;
init_entry_size = buf.len() as u32;
invocation_resources.read_bytes += init_entry_size;
if let Some(live_until) = init_entry_live_until {
init_live_until_ledger = live_until;
}
}
let mut entry_size = 0;
let mut entry_live_until_ledger = None;
let maybe_entry = curr_storage.try_get_full_with_host(key, host, None)?;
if let Some((entry, entry_live_until)) = maybe_entry {
let mut buf = Vec::<u8>::new();
metered_write_xdr(host.budget_ref(), entry.as_ref(), &mut buf)?;
entry_size = buf.len() as u32;
entry_live_until_ledger = entry_live_until;
}
match access_type {
AccessType::ReadOnly => {
invocation_resources.read_entries += 1;
}
AccessType::ReadWrite => {
invocation_resources.write_entries += 1;
invocation_resources.write_bytes += entry_size;
}
}
if let Some(new_live_until) = entry_live_until_ledger {
let extension_ledgers = (new_live_until - init_live_until_ledger) as i64;
let size_delta = if entry_size > init_entry_size {
(entry_size - init_entry_size) as i64
} else {
0
};
let existing_ledgers = (init_live_until_ledger - curr_ledger_seq) as i64;
let rent_ledger_bytes =
existing_ledgers * size_delta + extension_ledgers * (entry_size as i64);
if rent_ledger_bytes > 0 {
match get_key_durability(key.as_ref()) {
Some(ContractDataDurability::Temporary) => {
invocation_resources.temporary_rent_ledger_bytes += rent_ledger_bytes;
invocation_resources.temporary_entry_rent_bumps += 1;
}
Some(ContractDataDurability::Persistent) => {
invocation_resources.persistent_rent_ledger_bytes += rent_ledger_bytes;
invocation_resources.persistent_entry_rent_bumps += 1;
}
None => (),
}
}
}
}
let events = host.try_borrow_events()?.externalize(&host)?;
let encoded_contract_events = encode_contract_events(host.budget_ref(), &events)?;
for event in &encoded_contract_events {
invocation_resources.contract_events_size_bytes += event.len() as u32;
}
Ok(())
}
}
impl Host {
pub(crate) fn maybe_meter_invocation(
&self,
) -> Result<Option<InvocationMeterScope<'_>>, HostError> {
if let Ok(scope) = self.0.invocation_meter.try_borrow_mut() {
InvocationMeter::start_invocation(scope, self)
} else {
Ok(None)
}
}
pub fn enable_invocation_metering(&self) {
if let Ok(mut meter) = self.0.invocation_meter.try_borrow_mut() {
meter.enabled = true;
}
}
}
fn compute_fee_per_increment(resource_value: i64, fee_rate: i64, increment: i64) -> i64 {
num_integer::div_ceil(resource_value.saturating_mul(fee_rate), increment.max(1))
}
#[cfg(test)]
mod test {
use super::*;
use crate::{Symbol, TryFromVal, TryIntoVal};
use expect_test::expect;
use soroban_test_wasms::CONTRACT_STORAGE;
fn assert_resources_equal_to_budget(host: &Host) {
assert_eq!(
host.get_last_invocation_resources().unwrap().instructions as u64,
host.budget_ref().get_cpu_insns_consumed().unwrap()
);
assert_eq!(
host.get_last_invocation_resources().unwrap().mem_bytes as u64,
host.budget_ref().get_mem_bytes_consumed().unwrap()
);
}
#[test]
fn test_invocation_resource_metering() {
let host = Host::test_host_with_recording_footprint();
host.enable_invocation_metering();
host.enable_debug().unwrap();
host.with_mut_ledger_info(|li| {
li.sequence_number = 100;
li.max_entry_ttl = 10000;
li.min_persistent_entry_ttl = 1000;
li.min_temp_entry_ttl = 16;
})
.unwrap();
let contract_id = host.register_test_contract_wasm(CONTRACT_STORAGE);
expect![[r#"
InvocationResources {
instructions: 4196698,
mem_bytes: 2863076,
read_entries: 0,
write_entries: 2,
read_bytes: 0,
write_bytes: 3132,
contract_events_size_bytes: 0,
persistent_rent_ledger_bytes: 3128868,
persistent_entry_rent_bumps: 2,
temporary_rent_ledger_bytes: 0,
temporary_entry_rent_bumps: 0,
}"#]]
.assert_eq(format!("{:#?}", host.get_last_invocation_resources().unwrap()).as_str());
assert_resources_equal_to_budget(&host);
let key = Symbol::try_from_small_str("key_1").unwrap();
let _ = &host
.call(
contract_id,
Symbol::try_from_val(&host, &"has_persistent").unwrap(),
test_vec![&host, key].into(),
)
.unwrap();
expect![[r#"
InvocationResources {
instructions: 846137,
mem_bytes: 1215737,
read_entries: 3,
write_entries: 0,
read_bytes: 3132,
write_bytes: 0,
contract_events_size_bytes: 0,
persistent_rent_ledger_bytes: 0,
persistent_entry_rent_bumps: 0,
temporary_rent_ledger_bytes: 0,
temporary_entry_rent_bumps: 0,
}"#]]
.assert_eq(format!("{:#?}", host.get_last_invocation_resources().unwrap()).as_str());
assert_resources_equal_to_budget(&host);
let _ = &host
.try_call(
contract_id,
Symbol::try_from_val(&host, &"put_persistent").unwrap(),
test_vec![&host, key, 1234_u64].into(),
)
.unwrap();
expect![[r#"
InvocationResources {
instructions: 850395,
mem_bytes: 1216336,
read_entries: 2,
write_entries: 1,
read_bytes: 3132,
write_bytes: 84,
contract_events_size_bytes: 0,
persistent_rent_ledger_bytes: 83916,
persistent_entry_rent_bumps: 1,
temporary_rent_ledger_bytes: 0,
temporary_entry_rent_bumps: 0,
}"#]]
.assert_eq(format!("{:#?}", host.get_last_invocation_resources().unwrap()).as_str());
assert_resources_equal_to_budget(&host);
let _ = &host
.call(
contract_id,
Symbol::try_from_val(&host, &"has_persistent").unwrap(),
test_vec![&host, key].into(),
)
.unwrap();
expect![[r#"
InvocationResources {
instructions: 846406,
mem_bytes: 1215721,
read_entries: 3,
write_entries: 0,
read_bytes: 3216,
write_bytes: 0,
contract_events_size_bytes: 0,
persistent_rent_ledger_bytes: 0,
persistent_entry_rent_bumps: 0,
temporary_rent_ledger_bytes: 0,
temporary_entry_rent_bumps: 0,
}"#]]
.assert_eq(format!("{:#?}", host.get_last_invocation_resources().unwrap()).as_str());
assert_resources_equal_to_budget(&host);
let _ = &host
.try_call(
contract_id,
Symbol::try_from_val(&host, &"put_temporary").unwrap(),
test_vec![&host, key, 1234_u64].into(),
)
.unwrap();
expect![[r#"
InvocationResources {
instructions: 852007,
mem_bytes: 1216692,
read_entries: 2,
write_entries: 1,
read_bytes: 3132,
write_bytes: 84,
contract_events_size_bytes: 0,
persistent_rent_ledger_bytes: 0,
persistent_entry_rent_bumps: 0,
temporary_rent_ledger_bytes: 1260,
temporary_entry_rent_bumps: 1,
}"#]]
.assert_eq(format!("{:#?}", host.get_last_invocation_resources().unwrap()).as_str());
assert_resources_equal_to_budget(&host);
let _ = &host
.try_call(
contract_id,
Symbol::try_from_val(&host, &"has_temporary").unwrap(),
test_vec![&host, key].into(),
)
.unwrap();
expect![[r#"
InvocationResources {
instructions: 846795,
mem_bytes: 1215789,
read_entries: 3,
write_entries: 0,
read_bytes: 3216,
write_bytes: 0,
contract_events_size_bytes: 0,
persistent_rent_ledger_bytes: 0,
persistent_entry_rent_bumps: 0,
temporary_rent_ledger_bytes: 0,
temporary_entry_rent_bumps: 0,
}"#]]
.assert_eq(format!("{:#?}", host.get_last_invocation_resources().unwrap()).as_str());
assert_resources_equal_to_budget(&host);
let _ = &host
.call(
contract_id,
Symbol::try_from_val(&host, &"extend_persistent").unwrap(),
test_vec![&host, key, &5000_u32, &5000_u32].into(),
)
.unwrap();
expect![[r#"
InvocationResources {
instructions: 847840,
mem_bytes: 1216141,
read_entries: 3,
write_entries: 0,
read_bytes: 3216,
write_bytes: 0,
contract_events_size_bytes: 0,
persistent_rent_ledger_bytes: 336084,
persistent_entry_rent_bumps: 1,
temporary_rent_ledger_bytes: 0,
temporary_entry_rent_bumps: 0,
}"#]]
.assert_eq(format!("{:#?}", host.get_last_invocation_resources().unwrap()).as_str());
assert_resources_equal_to_budget(&host);
let _ = &host
.call(
contract_id,
Symbol::try_from_val(&host, &"extend_temporary").unwrap(),
test_vec![&host, key, &3000_u32, &3000_u32].into(),
)
.unwrap();
expect![[r#"
InvocationResources {
instructions: 847960,
mem_bytes: 1216141,
read_entries: 3,
write_entries: 0,
read_bytes: 3216,
write_bytes: 0,
contract_events_size_bytes: 0,
persistent_rent_ledger_bytes: 0,
persistent_entry_rent_bumps: 0,
temporary_rent_ledger_bytes: 250740,
temporary_entry_rent_bumps: 1,
}"#]]
.assert_eq(format!("{:#?}", host.get_last_invocation_resources().unwrap()).as_str());
assert_resources_equal_to_budget(&host);
let non_existent_key = Symbol::try_from_small_str("non_exist").unwrap();
let res = &host.call(
contract_id,
Symbol::try_from_val(&host, &"extend_persistent").unwrap(),
test_vec![&host, non_existent_key, &5000_u32, &5000_u32].into(),
);
assert!(res.is_err());
expect![[r#"
InvocationResources {
instructions: 847829,
mem_bytes: 1216209,
read_entries: 3,
write_entries: 0,
read_bytes: 3132,
write_bytes: 0,
contract_events_size_bytes: 0,
persistent_rent_ledger_bytes: 0,
persistent_entry_rent_bumps: 0,
temporary_rent_ledger_bytes: 0,
temporary_entry_rent_bumps: 0,
}"#]]
.assert_eq(format!("{:#?}", host.get_last_invocation_resources().unwrap()).as_str());
assert_resources_equal_to_budget(&host);
}
#[test]
fn test_resource_fee_estimation() {
assert_eq!(
InvocationResources {
instructions: 0,
mem_bytes: 100_000,
read_entries: 0,
write_entries: 0,
read_bytes: 0,
write_bytes: 0,
contract_events_size_bytes: 0,
persistent_rent_ledger_bytes: 0,
persistent_entry_rent_bumps: 0,
temporary_rent_ledger_bytes: 0,
temporary_entry_rent_bumps: 0
}
.estimate_fees(
&FeeConfiguration {
fee_per_instruction_increment: 100,
fee_per_read_entry: 100,
fee_per_write_entry: 100,
fee_per_read_1kb: 100,
fee_per_write_1kb: 100,
fee_per_historical_1kb: 100,
fee_per_contract_event_1kb: 100,
fee_per_transaction_size_1kb: 100,
},
1,
1
),
FeeEstimate {
total: 0,
instructions: 0,
read_entries: 0,
write_entries: 0,
read_bytes: 0,
write_bytes: 0,
contract_events: 0,
persistent_entry_rent: 0,
temporary_entry_rent: 0,
}
);
assert_eq!(
InvocationResources {
instructions: 1,
mem_bytes: 100_000,
read_entries: 1,
write_entries: 1,
read_bytes: 1,
write_bytes: 1,
contract_events_size_bytes: 1,
persistent_rent_ledger_bytes: 1,
persistent_entry_rent_bumps: 1,
temporary_rent_ledger_bytes: 1,
temporary_entry_rent_bumps: 1
}
.estimate_fees(
&FeeConfiguration {
fee_per_instruction_increment: 100,
fee_per_read_entry: 100,
fee_per_write_entry: 100,
fee_per_read_1kb: 100,
fee_per_write_1kb: 100,
fee_per_historical_1kb: 100,
fee_per_contract_event_1kb: 100,
fee_per_transaction_size_1kb: 100,
},
1,
1
),
FeeEstimate {
total: 516,
instructions: 1,
read_entries: 200,
write_entries: 100,
read_bytes: 1,
write_bytes: 1,
contract_events: 1,
persistent_entry_rent: 106,
temporary_entry_rent: 106
}
);
assert_eq!(
InvocationResources {
instructions: 10_123_456,
mem_bytes: 100_000,
read_entries: 30,
write_entries: 10,
read_bytes: 25_600,
write_bytes: 10_340,
contract_events_size_bytes: 321_654,
persistent_rent_ledger_bytes: 1_000_000_000,
persistent_entry_rent_bumps: 3,
temporary_rent_ledger_bytes: 4_000_000_000,
temporary_entry_rent_bumps: 6
}
.estimate_fees(
&FeeConfiguration {
fee_per_instruction_increment: 1000,
fee_per_read_entry: 2000,
fee_per_write_entry: 4000,
fee_per_read_1kb: 1500,
fee_per_write_1kb: 3000,
fee_per_historical_1kb: 300,
fee_per_contract_event_1kb: 200,
fee_per_transaction_size_1kb: 900,
},
1000,
2000
),
FeeEstimate {
total: 10_089_292,
instructions: 1_012_346,
read_entries: 80000,
write_entries: 40000,
read_bytes: 37500,
write_bytes: 30293,
contract_events: 62824,
persistent_entry_rent: 2942110,
temporary_entry_rent: 5884219
}
);
assert_eq!(
InvocationResources {
instructions: i64::MAX,
mem_bytes: i64::MAX,
read_entries: u32::MAX,
write_entries: u32::MAX,
read_bytes: u32::MAX,
write_bytes: u32::MAX,
contract_events_size_bytes: u32::MAX,
persistent_rent_ledger_bytes: i64::MAX,
persistent_entry_rent_bumps: u32::MAX,
temporary_rent_ledger_bytes: i64::MAX,
temporary_entry_rent_bumps: u32::MAX
}
.estimate_fees(
&FeeConfiguration {
fee_per_instruction_increment: i64::MAX,
fee_per_read_entry: i64::MAX,
fee_per_write_entry: i64::MAX,
fee_per_read_1kb: i64::MAX,
fee_per_write_1kb: i64::MAX,
fee_per_historical_1kb: i64::MAX,
fee_per_contract_event_1kb: i64::MAX,
fee_per_transaction_size_1kb: i64::MAX,
},
i64::MAX,
i64::MAX
),
FeeEstimate {
total: i64::MAX,
instructions: 922337203685478,
read_entries: i64::MAX,
write_entries: i64::MAX,
read_bytes: 9007199254740992,
write_bytes: 9007199254740992,
contract_events: 9007199254740992,
persistent_entry_rent: i64::MAX,
temporary_entry_rent: i64::MAX
}
);
}
}