Skip to content

Commit

Permalink
Expose invocation metering in the SDK.
Browse files Browse the repository at this point in the history
  • Loading branch information
dmkozh committed Dec 13, 2024
1 parent 5a3ca3a commit 5b226bd
Show file tree
Hide file tree
Showing 5 changed files with 401 additions and 0 deletions.
1 change: 1 addition & 0 deletions soroban-sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ derive_arbitrary = { version = "~1.3.0" }
proptest = "1.2.0"
proptest-arbitrary-interop = "0.1.0"
libfuzzer-sys = "0.4.7"
expect-test = "1.4.1"

[features]
alloc = []
Expand Down
87 changes: 87 additions & 0 deletions soroban-sdk/src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,8 @@ use core::{cell::RefCell, cell::RefMut};
#[cfg(any(test, feature = "testutils"))]
use internal::ContractInvocationEvent;
#[cfg(any(test, feature = "testutils"))]
pub use soroban_env_host::{fees::FeeConfiguration, FeeEstimate, InvocationResources};
#[cfg(any(test, feature = "testutils"))]
use soroban_ledger_snapshot::LedgerSnapshot;
#[cfg(any(test, feature = "testutils"))]
use std::{path::Path, rc::Rc};
Expand Down Expand Up @@ -576,6 +578,91 @@ impl Env {
env
}

/// Enables detailed metering of contract invocations.
///
/// The top-level contract invocations and lifecycle operations (such as
/// `register` or `env.deployer()` operations) will be metered and the
/// budget will reset in-between them. Metering will not be reset while
/// inside the call (e.g. if a contract calls or creates another contract,
/// that won't reset metering).
///
/// The metered resources for the last invocation can be retrieved with
/// `get_last_invocation_resources()` and the estimated fee corresponding
/// to these resources can be estimated with
/// `estimate_last_invocation_fee()`.
///
/// While the resource metering may be useful for contract optimization,
/// keep in mind that resource and fee estimation may be imprecise. Use
/// simulation with RPC in order to get the exact resources for submitting
/// the transactions to the network.
pub fn enable_invocation_metering(&self) {
self.env_impl.enable_invocation_metering();
}

/// Returns the resources metered during the last top level contract
/// invocation.
///
/// In order to get non-`None` results, `enable_invocation_metering` has to
/// be called and at least one invocation has to happen after that.
///
/// Take the return value with a grain of salt. The returned resources mostly
/// correspond only to the operations that have happened during the host
/// invocation, i.e. this won't try to simulate the work that happens in
/// production scenarios (e.g. certain XDR rountrips). This also doesn't try
/// to model resources related to the transaction size.
///
/// The returned value is as useful as the preceding setup, e.g. if a test
/// contract is used instead of a Wasm contract, all the costs related to
/// VM instantiation and execution, as well as Wasm reads/rent bumps will be
/// missed.
pub fn get_last_invocation_resources(&self) -> Option<InvocationResources> {
self.env_impl.get_last_invocation_resources()
}

/// Estimates the fee for the last invocation's resources, i.e. the
/// resources returned by `get_last_invocation_resources`.
///
/// In order to get non-`None` results, `enable_invocation_metering` has to
/// be called and at least one invocation has to happen after that.
///
/// The fees are computed using the snapshot of the Stellar Pubnet fees made
/// on 2024-12-11.
///
/// Take the return value with a grain of salt as both the resource estimate
/// and the fee rates may be imprecise.
///
/// The returned value is as useful as the preceding setup, e.g. if a test
/// contract is used instead of a Wasm contract, all the costs related to
/// VM instantiation and execution, as well as Wasm reads/rent bumps will be
/// missed.
pub fn estimate_last_invocation_fee(&self) -> Option<FeeEstimate> {
// This is a snapshot of the fees as of 2024-12-11.
let pubnet_fee_config = FeeConfiguration {
fee_per_instruction_increment: 25,
fee_per_read_entry: 6250,
fee_per_write_entry: 10000,
fee_per_read_1kb: 1786,
// This is a bit higher than the current network fee, it's an
// overestimate for the sake of providing a bit more conservative
// results in case if the state grows.
fee_per_write_1kb: 12000,
fee_per_historical_1kb: 16235,
fee_per_contract_event_1kb: 10000,
fee_per_transaction_size_1kb: 1624,
};
let pubnet_persistent_rent_rate_denominator = 2103;
let pubnet_temp_rent_rate_denominator = 4206;
if let Some(resources) = self.get_last_invocation_resources() {
Some(resources.estimate_fees(
&pubnet_fee_config,
pubnet_persistent_rent_rate_denominator,
pubnet_temp_rent_rate_denominator,
))
} else {
None
}
}

/// Register a contract with the [Env] for testing.
///
/// Pass the contract type when the contract is defined in the current crate
Expand Down
1 change: 1 addition & 0 deletions soroban-sdk/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ mod crypto_secp256k1;
mod crypto_secp256r1;
mod crypto_sha256;
mod env;
mod invocation_metering;
mod max_ttl;
mod prng;
mod proptest_scval_cmp;
Expand Down
184 changes: 184 additions & 0 deletions soroban-sdk/src/tests/invocation_metering.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
use crate as soroban_sdk;
use expect_test::expect;
use soroban_sdk::Env;
use soroban_sdk_macros::symbol_short;

mod contract_data {
use crate as soroban_sdk;
soroban_sdk::contractimport!(
file = "../target/wasm32-unknown-unknown/release/test_contract_data.wasm"
);
}

// Update the test data in this test via running it with `UPDATE_EXPECT=1`.
#[test]
fn test_invocation_metering_with_storage() {
let e = Env::default();
e.enable_invocation_metering();

let contract_id = e.register(contract_data::WASM, ());

// Register operations does both Wasm upload and creating the contract
// instance. The latter is the last invocation, i.e. the following resources
// were metered for creating a contract instance.
expect![[r#"
InvocationResources {
instructions: 918961,
mem_bytes: 2325733,
read_entries: 2,
write_entries: 2,
read_bytes: 928,
write_bytes: 176,
contract_events_size_bytes: 0,
persistent_rent_ledger_bytes: 425880,
persistent_entry_rent_bumps: 1,
temporary_rent_ledger_bytes: 454463928,
temporary_entry_rent_bumps: 1,
}"#]]
.assert_eq(format!("{:#?}", e.get_last_invocation_resources().unwrap()).as_str());
// Note, that the fee mostly comes from bumping the temporary nonce entry
// to the maximum possible TTL, which is the worst possible case for the
// signatures that don't expire for as long as possible.
expect![[r#"
FeeEstimate {
total: 1340707,
instructions: 2298,
read_entries: 25000,
write_entries: 20000,
read_bytes: 1619,
write_bytes: 2063,
contract_events: 0,
persistent_entry_rent: 12937,
temporary_entry_rent: 1276790,
}"#]]
.assert_eq(format!("{:#?}", e.estimate_last_invocation_fee().unwrap()).as_str());

let client = contract_data::Client::new(&e, &contract_id);

// Write a single new entry to the storage.
client.put(&symbol_short!("k1"), &symbol_short!("v1"));
expect![[r#"
InvocationResources {
instructions: 455853,
mem_bytes: 1162245,
read_entries: 2,
write_entries: 1,
read_bytes: 1032,
write_bytes: 80,
contract_events_size_bytes: 0,
persistent_rent_ledger_bytes: 327600,
persistent_entry_rent_bumps: 1,
temporary_rent_ledger_bytes: 0,
temporary_entry_rent_bumps: 0,
}"#]]
.assert_eq(format!("{:#?}", e.get_last_invocation_resources().unwrap()).as_str());
expect![[r#"
FeeEstimate {
total: 45017,
instructions: 1140,
read_entries: 18750,
write_entries: 10000,
read_bytes: 1800,
write_bytes: 938,
contract_events: 0,
persistent_entry_rent: 12389,
temporary_entry_rent: 0,
}"#]]
.assert_eq(format!("{:#?}", e.estimate_last_invocation_fee().unwrap()).as_str());

// Read an entry from the storage. Now there are no write-related resources
// and fees consumed.
assert_eq!(client.get(&symbol_short!("k1")), Some(symbol_short!("v1")));
expect![[r#"
InvocationResources {
instructions: 454080,
mem_bytes: 1161342,
read_entries: 3,
write_entries: 0,
read_bytes: 1112,
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!("{:#?}", e.get_last_invocation_resources().unwrap()).as_str());
expect![[r#"
FeeEstimate {
total: 21826,
instructions: 1136,
read_entries: 18750,
write_entries: 0,
read_bytes: 1940,
write_bytes: 0,
contract_events: 0,
persistent_entry_rent: 0,
temporary_entry_rent: 0,
}"#]]
.assert_eq(format!("{:#?}", e.estimate_last_invocation_fee().unwrap()).as_str());

// Delete the entry. There is 1 write_entry, but 0 write_bytes and no rent
// as this is deletion.
client.del(&symbol_short!("k1"));
expect![[r#"
InvocationResources {
instructions: 452458,
mem_bytes: 1161562,
read_entries: 2,
write_entries: 1,
read_bytes: 1112,
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!("{:#?}", e.get_last_invocation_resources().unwrap()).as_str());
expect![[r#"
FeeEstimate {
total: 31822,
instructions: 1132,
read_entries: 18750,
write_entries: 10000,
read_bytes: 1940,
write_bytes: 0,
contract_events: 0,
persistent_entry_rent: 0,
temporary_entry_rent: 0,
}"#]]
.assert_eq(format!("{:#?}", e.estimate_last_invocation_fee().unwrap()).as_str());

// Read an entry again, now it no longer exists, so there is less read_bytes
// than in the case when the entry is present.
assert_eq!(client.get(&symbol_short!("k1")), None);
expect![[r#"
InvocationResources {
instructions: 452445,
mem_bytes: 1161206,
read_entries: 3,
write_entries: 0,
read_bytes: 1032,
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!("{:#?}", e.get_last_invocation_resources().unwrap()).as_str());
expect![[r#"
FeeEstimate {
total: 21682,
instructions: 1132,
read_entries: 18750,
write_entries: 0,
read_bytes: 1800,
write_bytes: 0,
contract_events: 0,
persistent_entry_rent: 0,
temporary_entry_rent: 0,
}"#]]
.assert_eq(format!("{:#?}", e.estimate_last_invocation_fee().unwrap()).as_str());
}
Loading

0 comments on commit 5b226bd

Please sign in to comment.