Skip to main content

thruput_native_exec/
test_environment_builder.rs

1use crate::network_config::NetworkConfigBuilder;
2use crate::test_environment::{
3    TestEnvironment, TestEnvironmentHandle, mock_genesis_root_contract,
4    test_network_hash,
5};
6use thruput_storage_cost::StorageCostModel;
7use thruput_xdr::contract::Contract;
8use thruput_xdr::network_config::{ConfigParam, NetworkConfig};
9use thruput_xdr::types::NetworkHash;
10
11/// Builder for a [`TestEnvironment`] that needs something other than the
12/// stock chain settings.
13///
14/// The plain constructors ([`TestEnvironment::new`],
15/// [`no_cost`](TestEnvironment::no_cost), and the rest) cover the ordinary
16/// case and are unaffected by this type. Reach for the builder when a test
17/// wants to drive contract behavior that a network config parameter
18/// governs -- what a contract does as it approaches a cap the chain sets,
19/// which is otherwise only observable on a chain configured that way:
20///
21/// ```no_run
22/// use thruput_native_exec::test_environment::TestEnvironment;
23///
24/// let env = TestEnvironment::builder()
25///     .no_cost()
26///     .with_max_sha256_buffers(2)
27///     .build();
28/// ```
29///
30/// Every setter is optional and independent; an unset one keeps what
31/// [`TestEnvironment::new`] would have used, and unstated config
32/// parameters keep their stock values (see
33/// [`stock_network_config`](crate::network_config::stock_network_config)).
34/// A builder with no configuration overrides at all creates its database
35/// exactly as the plain constructors do. A parameter setter needs a hogd
36/// build that knows the parameter; see [`NetworkConfigBuilder`].
37#[must_use = "a TestEnvironmentBuilder does nothing until `build` is called"]
38pub struct TestEnvironmentBuilder {
39    cost_model: Option<StorageCostModel>,
40    network_hash: NetworkHash,
41    genesis_root_contract: Contract,
42    network_config: NetworkConfigBuilder,
43}
44
45impl Default for TestEnvironmentBuilder {
46    fn default() -> Self {
47        Self {
48            cost_model: None,
49            network_hash: test_network_hash(),
50            genesis_root_contract: mock_genesis_root_contract(),
51            network_config: NetworkConfigBuilder::new(),
52        }
53    }
54}
55
56impl TestEnvironmentBuilder {
57    /// A builder that would produce exactly `TestEnvironment::new(None)`.
58    pub fn new() -> Self {
59        Self::default()
60    }
61
62    /// Charge storage against `cost_model` instead of the chain default.
63    pub fn with_cost_model(mut self, cost_model: StorageCostModel) -> Self {
64        self.cost_model = Some(cost_model);
65        self
66    }
67
68    /// Make every storage operation free, as
69    /// [`TestEnvironment::no_cost`] does.
70    pub fn no_cost(self) -> Self {
71        self.with_cost_model(StorageCostModel::no_cost_model())
72    }
73
74    /// Run on `network_hash` instead of [`test_network_hash`].
75    ///
76    /// The network hash is part of every transaction signature preimage, so
77    /// pin it when a test's signatures were produced for a specific
78    /// network.
79    pub fn with_network_hash(mut self, network_hash: NetworkHash) -> Self {
80        self.network_hash = network_hash;
81        self
82    }
83
84    /// Install `contract` as the genesis root deployment instead of
85    /// [`mock_genesis_root_contract`].
86    pub fn with_genesis_root_contract(mut self, contract: Contract) -> Self {
87        self.genesis_root_contract = contract;
88        self
89    }
90
91    /// Apply the parameter setters below to `config` rather than to the
92    /// stock configuration.
93    ///
94    /// This is the whole-configuration escape hatch: pass one read back off
95    /// another environment, or one built by hand. Anything the setters do
96    /// not restate is taken from it verbatim.
97    pub fn with_network_config(mut self, config: NetworkConfig) -> Self {
98        self.network_config = self.network_config.with_base(config);
99        self
100    }
101
102    /// State one config parameter the typed setters below do not cover.
103    pub fn with_config_param(mut self, param: ConfigParam) -> Self {
104        self.network_config = self.network_config.with_config_param(param);
105        self
106    }
107
108    /// Concurrently open incremental SHA-256 buffers permitted per invocation
109    /// frame (the cap `thruput_sdk::Hasher` allocates against).
110    pub fn with_max_sha256_buffers(mut self, max_sha256_buffers: u32) -> Self {
111        self.network_config = self
112            .network_config
113            .with_max_sha256_buffers(max_sha256_buffers);
114        self
115    }
116
117    /// Invocation frames one transaction may have open at once.
118    ///
119    /// The harness opens its frames directly, so one the executor refuses is
120    /// an unrecoverable panic rather than the error a contract would observe.
121    /// To assert on the cap, have the frame call through the Invoke syscall
122    /// (`thruput_sdk::call::try_call_raw_with`), which reports it as
123    /// `RawCallError::DepthExceeded`.
124    pub fn with_max_cross_invoke_depth(
125        mut self,
126        max_cross_invoke_depth: u32,
127    ) -> Self {
128        self.network_config = self
129            .network_config
130            .with_max_cross_invoke_depth(max_cross_invoke_depth);
131        self
132    }
133
134    /// Contracts one transaction may attach in `contracts_to_deploy`.
135    pub fn with_max_new_contracts_per_tx(
136        mut self,
137        max_new_contracts_per_tx: u32,
138    ) -> Self {
139        self.network_config = self
140            .network_config
141            .with_max_new_contracts_per_tx(max_new_contracts_per_tx);
142        self
143    }
144
145    /// Bytes one bytestring storage item may hold.
146    pub fn with_max_bytestring_len(mut self, max_bytestring_len: u32) -> Self {
147        self.network_config = self
148            .network_config
149            .with_max_bytestring_len(max_bytestring_len);
150        self
151    }
152
153    /// Bytes one attached contract's code blob (`ContractBlob.data`) may
154    /// hold, checked before any attached contract is verified or executed.
155    pub fn with_max_contract_size_bytes(
156        mut self,
157        max_contract_size_bytes: u32,
158    ) -> Self {
159        self.network_config = self
160            .network_config
161            .with_max_contract_size_bytes(max_contract_size_bytes);
162        self
163    }
164
165    /// Storage savepoints one transaction may have open at once: one per
166    /// `TryInvoke` frame still executing, plus the one the root commit
167    /// takes.
168    pub fn with_max_savepoint_depth(
169        mut self,
170        max_savepoint_depth: u32,
171    ) -> Self {
172        self.network_config = self
173            .network_config
174            .with_max_savepoint_depth(max_savepoint_depth);
175        self
176    }
177
178    /// Bytes one incremental SHA-256 buffer may absorb between resets, the
179    /// per-buffer companion of
180    /// [`with_max_sha256_buffers`](Self::with_max_sha256_buffers).
181    pub fn with_max_sha256_buffer_bytes(
182        mut self,
183        max_sha256_buffer_bytes: u32,
184    ) -> Self {
185        self.network_config = self
186            .network_config
187            .with_max_sha256_buffer_bytes(max_sha256_buffer_bytes);
188        self
189    }
190
191    /// Levels of `AuthTokenNode` nesting an auth token may carry, counted
192    /// from its root.
193    pub fn with_max_auth_token_depth(
194        mut self,
195        max_auth_token_depth: u32,
196    ) -> Self {
197        self.network_config = self
198            .network_config
199            .with_max_auth_token_depth(max_auth_token_depth);
200        self
201    }
202
203    /// Bytes of code (`code_size` in the artifact header) an RV32 artifact
204    /// may carry to be attached as a contract.
205    pub fn with_max_rv32_code_bytes(
206        mut self,
207        max_rv32_code_bytes: u32,
208    ) -> Self {
209        self.network_config = self
210            .network_config
211            .with_max_rv32_code_bytes(max_rv32_code_bytes);
212        self
213    }
214
215    /// Bytes of guest data memory (`memory_bytes` in the artifact header)
216    /// an RV32 artifact may declare to be attached as a contract.
217    pub fn with_max_rv32_memory_bytes(
218        mut self,
219        max_rv32_memory_bytes: u32,
220    ) -> Self {
221        self.network_config = self
222            .network_config
223            .with_max_rv32_memory_bytes(max_rv32_memory_bytes);
224        self
225    }
226
227    /// Create the configured environment.
228    pub fn build(self) -> TestEnvironmentHandle {
229        // Nothing to configure: create the database the way the plain
230        // constructors do, rather than restating hogd's own defaults back
231        // at it (which would also cost a probe for them).
232        let network_config = if self.network_config.is_stock() {
233            None
234        } else {
235            Some(self.network_config.build())
236        };
237
238        TestEnvironment::from_parts(
239            self.cost_model.as_ref(),
240            &self.network_hash,
241            &self.genesis_root_contract,
242            network_config.as_ref(),
243        )
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250    use crate::network_config::{
251        config_params, find_config_param, stock_network_config,
252    };
253    use crate::test_helpers::Named;
254    use thruput_xdr::network_config::ConfigParamID;
255    use thruput_xdr::syscalls::{raw_BytestringSet, raw_SHA256Update};
256    use thruput_xdr::types::{ContractAddress, StorageKeyContractData};
257
258    fn max_sha256_buffers(config: &NetworkConfig) -> u32 {
259        match find_config_param(
260            config,
261            ConfigParamID::kConfigParamMaxSHA256Buffers,
262        ) {
263            Some(ConfigParam::kConfigParamMaxSHA256Buffers {
264                max_sha256_buffers,
265            }) => *max_sha256_buffers,
266            other => panic!("no SHA-256 buffer cap in the config: {other:?}"),
267        }
268    }
269
270    #[test]
271    fn a_builder_with_no_overrides_matches_the_plain_constructor() {
272        let built = TestEnvironment::builder().build();
273        let plain = TestEnvironment::new(None);
274
275        assert_eq!(built.get_network_config(), plain.get_network_config());
276        assert_eq!(built.get_network_config(), stock_network_config());
277    }
278
279    #[test]
280    fn no_cost_builds_the_free_cost_model() {
281        assert_eq!(
282            TestEnvironment::builder()
283                .no_cost()
284                .build()
285                .get_current_cost_model()
286                .bytestring_cost(32),
287            0,
288        );
289        assert!(
290            TestEnvironment::builder()
291                .build()
292                .get_current_cost_model()
293                .bytestring_cost(32)
294                > 0,
295            "the chain default charges for storage",
296        );
297    }
298
299    #[test]
300    fn an_overridden_parameter_is_the_only_one_that_changes() {
301        let stock = stock_network_config();
302        let env = TestEnvironment::builder()
303            .with_max_sha256_buffers(max_sha256_buffers(&stock) + 1)
304            .build();
305        let config = env.get_network_config();
306
307        assert_eq!(
308            max_sha256_buffers(&config),
309            max_sha256_buffers(&stock) + 1,
310        );
311        assert_eq!(config_params(&config).len(), config_params(&stock).len());
312        assert_eq!(
313            &config_params(&config)[1..],
314            &config_params(&stock)[1..],
315            "the parameters left alone keep their stock values",
316        );
317    }
318
319    // The point of the whole builder: a parameter the chain sets, set
320    // differently, actually governs execution. Every live incremental
321    // SHA-256 stream costs one buffer, so the cap is what the VM refuses
322    // the next one against.
323    #[test]
324    fn the_sha256_buffer_cap_the_builder_sets_is_the_one_the_vm_enforces() {
325        let env = TestEnvironment::builder()
326            .no_cost()
327            .with_max_sha256_buffers(2)
328            .build();
329
330        env.transact(|tx| {
331            tx.call(&ContractAddress::named("sha256_cap")).run(|| {
332                assert!(raw_SHA256Update(0, &[], true).is_ok());
333                assert!(raw_SHA256Update(1, &[], true).is_ok());
334                assert!(
335                    raw_SHA256Update(2, &[], true).is_err(),
336                    "a third live stream must exceed the cap of 2",
337                );
338            })
339        })
340        .assert_success();
341    }
342
343    #[test]
344    fn the_stock_cap_leaves_room_for_more_streams_than_that() {
345        let env = TestEnvironment::builder().no_cost().build();
346        let cap = max_sha256_buffers(&env.get_network_config());
347        assert!(cap > 2, "this test needs a stock cap above the one above");
348
349        env.transact(|tx| {
350            tx.call(&ContractAddress::named("sha256_stock_cap")).run(
351                move || {
352                    for index in 0..cap {
353                        assert!(raw_SHA256Update(index, &[], true).is_ok());
354                    }
355                    assert!(raw_SHA256Update(cap, &[], true).is_err());
356                },
357            )
358        })
359        .assert_success();
360    }
361
362    #[test]
363    fn the_max_bytestring_len_parameter_is_settable() {
364        let env = TestEnvironment::builder()
365            .no_cost()
366            .with_max_bytestring_len(64)
367            .build();
368
369        assert_eq!(
370            find_config_param(
371                &env.get_network_config(),
372                ConfigParamID::kConfigParamMaxBytestringLen,
373            ),
374            Some(&ConfigParam::kConfigParamMaxBytestringLen {
375                max_bytestring_len: 64
376            }),
377        );
378    }
379
380    // As with the SHA-256 cap: the parameter the builder sets is the one
381    // the write is measured against. It is checked where the bytestring is
382    // written, so that is where the boundary shows.
383    #[test]
384    fn the_bytestring_cap_the_builder_sets_is_the_one_the_vm_enforces() {
385        let env = TestEnvironment::builder()
386            .no_cost()
387            .with_max_bytestring_len(64)
388            .build();
389        let key = StorageKeyContractData::new([0x77; 32]);
390
391        env.transact(|tx| {
392            tx.call(&ContractAddress::named("bytestring_cap"))
393                .run(move || {
394                    assert!(
395                        raw_BytestringSet(&key, &[0u8; 64]).is_ok(),
396                        "a bytestring at the cap must still be admitted",
397                    );
398                    assert!(
399                        raw_BytestringSet(&key, &[0u8; 65])
400                            .unwrap_err()
401                            .is_invalid_input(),
402                        "one byte over must not be",
403                    );
404                })
405        })
406        .assert_success();
407    }
408
409    #[test]
410    fn an_explicit_network_config_is_taken_verbatim() {
411        let stock = stock_network_config();
412        let env = TestEnvironment::builder()
413            .with_network_config(stock.clone())
414            .build();
415
416        assert_eq!(env.get_network_config(), stock);
417    }
418}