Skip to content

Commit a3cefc1

Browse files
dmkozhCopilotleighmcculloch
authored
Add test resource limit enforcement functionality. (#1677)
### What Add test resource limit enforcement functionality. Also enable mainnet limit enforcement by default. These shouldn't be disruptive to most of the existing users (as they're likely able to run their contracts in testnet or mainnet), and for those in the experimentation stage this might be useful for early discovery of the issues. The limits enforced can be customized or enforcement can be disabled altogether. ### Why Resource limit enforcement provides early feedback for the contracts that might be too heavy to be usable after deployment. The resource estimation is not perfect, but usually the test environment will have under-estimated resources (e.g. due to not using the compiled Wasms), i.e. there shouldn't be annoying 'false positives' for this. ### Known limitations N/A --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Leigh <351529+leighmcculloch@users.noreply.github.com>
1 parent c02538a commit a3cefc1

File tree

8 files changed

+253
-34
lines changed

8 files changed

+253
-34
lines changed

Cargo.lock

Lines changed: 10 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,17 @@ soroban-token-spec = { version = "25.0.0", path = "soroban-token-spec" }
3030
stellar-asset-spec = { version = "25.0.0", path = "stellar-asset-spec" }
3131

3232
[workspace.dependencies.soroban-env-common]
33-
version = "=25.0.0"
33+
version = "=25.0.1"
3434
# git = "https://github.com/stellar/rs-soroban-env"
3535
# rev = "cf58d535ab05d02802a5e804a95524650f8c62c7"
3636

3737
[workspace.dependencies.soroban-env-guest]
38-
version = "=25.0.0"
38+
version = "=25.0.1"
3939
# git = "https://github.com/stellar/rs-soroban-env"
4040
# rev = "cf58d535ab05d02802a5e804a95524650f8c62c7"
4141

4242
[workspace.dependencies.soroban-env-host]
43-
version = "=25.0.0"
43+
version = "=25.0.1"
4444
# git = "https://github.com/stellar/rs-soroban-env"
4545
# rev = "cf58d535ab05d02802a5e804a95524650f8c62c7"
4646

soroban-sdk/src/_migrating.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,14 @@
1515
//! Define traits with default implementations using `#[contracttrait]`, then implement them
1616
//! in contracts using `#[contractimpl(contracttrait)]`.
1717
//!
18+
//! 5. [Resource limit enforcement enabled by default in tests][v25_resource_limits].
19+
//! `Env::default()` now enforces Mainnet resource limits for contract invocations.
20+
//! Tests will fail if limits are exceeded. This provides early warning of contracts that
21+
//! may be too resource-heavy for Mainnet. If you see test failures after upgrading,
22+
//! use `env.cost_estimate().disable_resource_limits()` to opt-out while optimizing.
23+
//!
1824
//! [v25_contracttrait]: v25_contracttrait
25+
//! [v25_resource_limits]: v25_resource_limits
1926
//!
2027
//! # Migrating from v22 to v23
2128
//!
@@ -272,3 +279,4 @@ pub mod v25_bn254;
272279
pub mod v25_contracttrait;
273280
pub mod v25_event_testing;
274281
pub mod v25_poseidon;
282+
pub mod v25_resource_limits;
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
//! Resource limit enforcement in tests.
2+
//!
3+
//! ## Breaking Change: Tests May Fail Due to Resource Limits
4+
//!
5+
//! By default, [`Env::default()`] now enforces mainnet resource limits for contract invocations in
6+
//! tests. **If your contract exceeds any resource limit, your tests will panic** with details
7+
//! about which limits were exceeded.
8+
//!
9+
//! This provides an early warning that a contract might be too resource-heavy to run on mainnet.
10+
//!
11+
//! **If you see test failures after upgrading**, and you wish to test without mainnet limits
12+
//! (e.g., while experimenting or optimizing), see [Disabling Resource
13+
//! Limits](#disabling-resource-limits) below.
14+
//!
15+
//! ## New Default Behavior
16+
//!
17+
//! When creating a new `Env` with [`Env::default()`], mainnet resource limits are automatically
18+
//! enforced. No changes to existing test code are required to benefit from this protection.
19+
//!
20+
//! ```
21+
//! use soroban_sdk::{contract, contractimpl, Env};
22+
//!
23+
//! #[contract]
24+
//! pub struct Contract;
25+
//!
26+
//! #[contractimpl]
27+
//! impl Contract {
28+
//! pub fn execute() {
29+
//! // ... code
30+
//! }
31+
//! }
32+
//!
33+
//! #[test]
34+
//! fn test() {
35+
//! # }
36+
//! # #[cfg(feature = "testutils")]
37+
//! # fn main() {
38+
//! let env = Env::default(); // Mainnet limits enforced automatically
39+
//! let contract_id = env.register(Contract, ());
40+
//! let client = ContractClient::new(&env, &contract_id);
41+
//! client.execute(); // Will panic if resource limit exceeded
42+
//! }
43+
//! # #[cfg(not(feature = "testutils"))]
44+
//! # fn main() { }
45+
//! ```
46+
//!
47+
//! ## Disabling Resource Limits
48+
//!
49+
//! For experimental contracts that are still being optimized, resource limit enforcement can be
50+
//! disabled using [`CostEstimate::disable_resource_limits()`]:
51+
//!
52+
//! ```
53+
//! use soroban_sdk::{contract, contractimpl, Env};
54+
//!
55+
//! #[contract]
56+
//! pub struct Contract;
57+
//!
58+
//! #[contractimpl]
59+
//! impl Contract {
60+
//! pub fn execute() {
61+
//! // ... resource-heavy code
62+
//! }
63+
//! }
64+
//!
65+
//! #[test]
66+
//! fn test() {
67+
//! # }
68+
//! # #[cfg(feature = "testutils")]
69+
//! # fn main() {
70+
//! let env = Env::default();
71+
//! env.cost_estimate().disable_resource_limits(); // Disable resource limit
72+
//!
73+
//! let contract_id = env.register(Contract, ());
74+
//! let client = ContractClient::new(&env, &contract_id);
75+
//! client.execute(); // Won't panic even if limits exceeded
76+
//! }
77+
//! # #[cfg(not(feature = "testutils"))]
78+
//! # fn main() { }
79+
//! ```
80+
//!
81+
//! ## Custom Resource Limits
82+
//!
83+
//! Custom resource limits can be enforced using [`CostEstimate::enforce_resource_limits()`]:
84+
//!
85+
//! ```
86+
//! use soroban_sdk::{contract, contractimpl, Env};
87+
//! use soroban_sdk::testutils::cost_estimate::NetworkInvocationResourceLimits;
88+
//! use soroban_env_host::InvocationResourceLimits;
89+
//!
90+
//! #[contract]
91+
//! pub struct Contract;
92+
//!
93+
//! #[contractimpl]
94+
//! impl Contract {
95+
//! pub fn execute() {
96+
//! // ... code
97+
//! }
98+
//! }
99+
//!
100+
//! #[test]
101+
//! fn test() {
102+
//! # }
103+
//! # #[cfg(feature = "testutils")]
104+
//! # fn main() {
105+
//! let env = Env::default();
106+
//!
107+
//! // Use custom limits (this example uses mainnet limits as a base)
108+
//! let mut limits = InvocationResourceLimits::mainnet();
109+
//! limits.instructions = 100_000_000; // Reduce instruction limit
110+
//! env.cost_estimate().enforce_resource_limits(limits);
111+
//!
112+
//! let contract_id = env.register(Contract, ());
113+
//! let client = ContractClient::new(&env, &contract_id);
114+
//! client.execute(); // Uses the custom limits
115+
//! }
116+
//! # #[cfg(not(feature = "testutils"))]
117+
//! # fn main() { }
118+
//! ```
119+
//!
120+
//! ## Mainnet Resource Limits
121+
//!
122+
//! The [`NetworkInvocationResourceLimits`] trait provides the `mainnet()` method on
123+
//! [`InvocationResourceLimits`] to get the current mainnet limits:
124+
//!
125+
//! - Instructions: 600,000,000
126+
//! - Memory: 41,943,040 bytes
127+
//! - Disk read entries: 100
128+
//! - Write entries: 50
129+
//! - Ledger entries: 100
130+
//! - Disk read bytes: 200,000
131+
//! - Write bytes: 132,096
132+
//! - Contract events size: 16,384 bytes
133+
//! - Max contract data key size: 250 bytes
134+
//! - Max contract data entry size: 65,536 bytes
135+
//! - Max contract code entry size: 131,072 bytes
136+
//!
137+
//! Note: These values are not pulled dynamically. The SDK will be updated from time-to-time to
138+
//! pick up changes to mainnet limits. These changes may occur in any major, minor, or patch
139+
//! release.
140+
//!
141+
//! [`Env::default()`]: crate::Env::default
142+
//! [`CostEstimate::disable_resource_limits()`]: crate::testutils::cost_estimate::CostEstimate::disable_resource_limits
143+
//! [`CostEstimate::enforce_resource_limits()`]: crate::testutils::cost_estimate::CostEstimate::enforce_resource_limits
144+
//! [`InvocationResourceLimits`]: soroban_env_host::InvocationResourceLimits
145+
//! [`NetworkInvocationResourceLimits`]: crate::testutils::cost_estimate::NetworkInvocationResourceLimits

soroban-sdk/src/env.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -477,16 +477,17 @@ use crate::testutils::cost_estimate::CostEstimate;
477477
use crate::{
478478
auth,
479479
testutils::{
480-
budget::Budget, default_ledger_info, Address as _, AuthSnapshot, AuthorizedInvocation,
481-
ContractFunctionSet, EventsSnapshot, Generators, Ledger as _, MockAuth, MockAuthContract,
482-
Register, Snapshot, SnapshotSourceInput, StellarAssetContract, StellarAssetIssuer,
480+
budget::Budget, cost_estimate::NetworkInvocationResourceLimits, default_ledger_info,
481+
Address as _, AuthSnapshot, AuthorizedInvocation, ContractFunctionSet, EventsSnapshot,
482+
Generators, Ledger as _, MockAuth, MockAuthContract, Register, Snapshot,
483+
SnapshotSourceInput, StellarAssetContract, StellarAssetIssuer,
483484
},
484485
Bytes, BytesN, ConstructorArgs,
485486
};
486487
#[cfg(any(test, feature = "testutils"))]
487488
use core::{cell::RefCell, cell::RefMut};
488489
#[cfg(any(test, feature = "testutils"))]
489-
use internal::ContractInvocationEvent;
490+
use internal::{ContractInvocationEvent, InvocationResourceLimits};
490491
#[cfg(any(test, feature = "testutils"))]
491492
use soroban_ledger_snapshot::LedgerSnapshot;
492493
#[cfg(any(test, feature = "testutils"))]
@@ -547,6 +548,7 @@ impl Env {
547548
// Store in the Env the name of the test it is for, and a number so that within a test
548549
// where one or more Env's have been created they can be uniquely identified relative to
549550
// each other.
551+
550552
let test_name = match std::thread::current().name() {
551553
// When doc tests are running they're all run with the thread name main. There's no way
552554
// to detect which doc test is being run.
@@ -608,6 +610,9 @@ impl Env {
608610
})))
609611
.unwrap();
610612
env_impl.enable_invocation_metering();
613+
env_impl
614+
.set_invocation_resource_limits(Some(InvocationResourceLimits::mainnet()))
615+
.unwrap();
611616

612617
let env = Env {
613618
env_impl,

soroban-sdk/src/tests/cost_estimate.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -161,17 +161,17 @@ fn test_cost_estimate_budget() {
161161
// Budget breakdown corresponds to the last invocation only.
162162
expect![[r#"
163163
===============================================================================================================================================================================
164-
Cpu limit: 100000000; used: 242479
165-
Mem limit: 41943040; used: 1126705
164+
Cpu limit: 100000000; used: 257503
165+
Mem limit: 41943040; used: 1145211
166166
===============================================================================================================================================================================
167167
CostType iterations input cpu_insns mem_bytes const_term_cpu lin_term_cpu const_term_mem lin_term_mem
168168
WasmInsnExec 284 None 1136 0 4 0 0 0
169169
MemAlloc 23 Some(1051337) 141397 1051705 434 16 16 128
170-
MemCpy 97 Some(9494) 5246 0 42 16 0 0
171-
MemCmp 60 Some(1806) 2854 0 44 16 0 0
170+
MemCpy 104 Some(10589) 5675 0 42 16 0 0
171+
MemCmp 67 Some(1996) 3184 0 44 16 0 0
172172
DispatchHostFunction 1 None 310 0 310 0 0 0
173173
VisitObject 2 None 122 0 61 0 0 0
174-
ValSer 0 Some(0) 0 0 230 29 242 384
174+
ValSer 61 Some(1248) 14265 18506 230 29 242 384
175175
ValDeser 0 Some(0) 0 0 59052 4001 0 384
176176
ComputeSha256Hash 1 Some(0) 3738 0 3738 7012 0 0
177177
ComputeEd25519PubKey 0 None 0 0 40253 0 0 0
@@ -252,7 +252,7 @@ fn test_cost_estimate_budget() {
252252
Bn254FrInv 0 None 0 0 33151 0 0 0
253253
===============================================================================================================================================================================
254254
Internal details (diagnostics info, does not affect fees)
255-
Total # times meter was called: 197
255+
Total # times meter was called: 272
256256
Shadow cpu limit: 100000000; used: 32431
257257
Shadow mem limit: 41943040; used: 27108
258258
===============================================================================================================================================================================

soroban-sdk/src/testutils/cost_estimate.rs

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
use soroban_env_host::{fees::FeeConfiguration, FeeEstimate, InvocationResources};
1+
use soroban_env_host::{
2+
fees::FeeConfiguration, FeeEstimate, InvocationResourceLimits, InvocationResources,
3+
};
24

35
use crate::{testutils::budget::Budget, Env};
46

@@ -89,4 +91,63 @@ impl CostEstimate {
8991
pub fn budget(&self) -> Budget {
9092
Budget::new(self.env.host().budget_cloned())
9193
}
94+
95+
/// Enforces custom resource limits for contract invocations in tests.
96+
///
97+
/// When limit enforcement is enabled, for every contract invocation the
98+
/// resource usage is checked against the provided limits, and if any of the
99+
/// limits is exceeded, the contract invocation will result in a panic
100+
/// that indicates which limits were exceeded.
101+
///
102+
/// Limit enforcement is meant to provide an early warning sign that a
103+
/// contract might be too resource heavy to run on a real network. If the
104+
/// high resource usage is intentional and expected (e.g. for
105+
/// experimentation), disable the enforcement via
106+
/// `disable_resource_limits()`.
107+
///
108+
/// By default, `InvocationResourceLimits::mainnet()` limits are enforced.
109+
pub fn enforce_resource_limits(&self, limits: InvocationResourceLimits) {
110+
self.env
111+
.host()
112+
.set_invocation_resource_limits(Some(limits))
113+
.unwrap();
114+
}
115+
116+
/// Disables resource limit enforcement for contract invocations in tests.
117+
///
118+
/// This may be useful for the experimental contracts that are still being
119+
/// optimized.
120+
pub fn disable_resource_limits(&self) {
121+
self.env
122+
.host()
123+
.set_invocation_resource_limits(None)
124+
.unwrap();
125+
}
126+
}
127+
128+
/// Predefined network invocation resource limits.
129+
pub trait NetworkInvocationResourceLimits {
130+
fn mainnet() -> Self;
131+
}
132+
133+
impl NetworkInvocationResourceLimits for InvocationResourceLimits {
134+
/// Returns the invocation resource limits used on Stellar Mainnet.
135+
///
136+
/// This is not pulling the values dynamically, so updating the SDK is
137+
/// necessary to pick up the most recent values.
138+
fn mainnet() -> Self {
139+
InvocationResourceLimits {
140+
instructions: 600_000_000,
141+
mem_bytes: 41943040,
142+
disk_read_entries: 100,
143+
write_entries: 50,
144+
ledger_entries: 100,
145+
disk_read_bytes: 200000,
146+
write_bytes: 132096,
147+
contract_events_size_bytes: 16384,
148+
max_contract_data_key_size_bytes: 250,
149+
max_contract_data_entry_size_bytes: 65536,
150+
max_contract_code_entry_size_bytes: 131072,
151+
}
152+
}
92153
}

0 commit comments

Comments
 (0)