Skip to main content

thruput_native_exec/
network_config.rs

1use crate::test_environment::{
2    DBWrapper, mock_genesis_root_contract, test_network_hash,
3};
4use std::cmp::Ordering;
5use std::collections::BTreeMap;
6use std::sync::OnceLock;
7use thruput_xdr::network_config::{
8    ConfigParam, ConfigParamID, NetworkConfig, NetworkConfigV0,
9};
10
11/// The configuration a database takes when none is supplied: the stock
12/// genesis values of the hogd build this crate is linked against.
13///
14/// Read from hogd rather than restated here, so it cannot drift from the
15/// binary actually executing the tests. The first call creates (and frees) a
16/// throwaway database to ask; the answer is cached for the process.
17///
18/// This is the base [`NetworkConfigBuilder`] overrides are applied to, and
19/// what a [`TestEnvironment`](crate::test_environment::TestEnvironment)
20/// built without configuration runs under.
21pub fn stock_network_config() -> NetworkConfig {
22    static STOCK: OnceLock<NetworkConfig> = OnceLock::new();
23    STOCK
24        .get_or_init(|| {
25            let db = DBWrapper::new(
26                None,
27                &test_network_hash(),
28                &mock_genesis_root_contract(),
29                None,
30            );
31            db.get_network_config()
32        })
33        .clone()
34}
35
36/// The parameters `config` states, in id order.
37///
38/// The list is a prefix of the id space: a parameter past its end is not
39/// stated by this configuration and takes the value the protocol defines
40/// for it.
41pub fn config_params(config: &NetworkConfig) -> &[ConfigParam] {
42    match config {
43        NetworkConfig::_0 { v0 } => &v0.params.0,
44        other => panic!(
45            "unsupported network configuration version: {other:?}\n\
46             This build of the SDK only knows NetworkConfig::_0."
47        ),
48    }
49}
50
51/// The value `config` states for `param`, or `None` when it states none.
52///
53/// ```no_run
54/// use thruput_native_exec::network_config;
55/// use thruput_xdr::network_config::{ConfigParam, ConfigParamID};
56///
57/// let config = network_config::stock_network_config();
58/// let cap = match network_config::find_config_param(
59///     &config,
60///     ConfigParamID::kConfigParamMaxSHA256Buffers,
61/// ) {
62///     Some(ConfigParam::kConfigParamMaxSHA256Buffers {
63///         max_sha256_buffers,
64///     }) => *max_sha256_buffers,
65///     _ => panic!("this chain states no SHA-256 buffer cap"),
66/// };
67/// assert!(cap > 0);
68/// ```
69pub fn find_config_param(
70    config: &NetworkConfig,
71    param: ConfigParamID,
72) -> Option<&ConfigParam> {
73    config_params(config).iter().find(|p| p.param() == param)
74}
75
76/// Builder for a [`NetworkConfig`] stating non-default execution
77/// parameters.
78///
79/// Overrides are applied to a base configuration -- [`stock_network_config`]
80/// unless [`with_base`](Self::with_base) says otherwise -- so setting one
81/// parameter leaves every other one at the value the chain would have used:
82///
83/// ```no_run
84/// use thruput_native_exec::network_config::NetworkConfigBuilder;
85///
86/// let config = NetworkConfigBuilder::new()
87///     .with_max_sha256_buffers(2)
88///     .build();
89/// ```
90///
91/// Most tests never name this type: the same `with_*` setters are on
92/// [`TestEnvironmentBuilder`](crate::test_environment_builder::TestEnvironmentBuilder),
93/// which builds the configuration and the environment together.
94///
95/// A setter is only usable against a hogd build that knows its parameter.
96/// The stock configuration states what that build's genesis states, and
97/// [`build`](Self::build) refuses to leave a gap below a newer parameter,
98/// so the ones protocol 0.35 introduced
99/// ([`with_max_contract_size_bytes`](Self::with_max_contract_size_bytes)
100/// and later) need a hogd built from that protocol or newer.
101#[derive(Clone, Debug, Default)]
102#[must_use = "a NetworkConfigBuilder does nothing until `build` is called"]
103pub struct NetworkConfigBuilder {
104    base: Option<NetworkConfig>,
105    overrides: BTreeMap<ConfigParamID, ConfigParam>,
106}
107
108impl NetworkConfigBuilder {
109    /// A builder with no overrides, over the stock configuration.
110    pub fn new() -> Self {
111        Self::default()
112    }
113
114    /// Apply the overrides to `base` instead of to
115    /// [`stock_network_config`].
116    ///
117    /// Use this to start from a configuration a test already has in hand
118    /// (e.g. one read back off another environment). `base` must itself
119    /// state a gapless prefix of the id space, as any configuration hogd
120    /// accepts does.
121    pub fn with_base(mut self, base: NetworkConfig) -> Self {
122        self.base = Some(base);
123        self
124    }
125
126    /// State `param`, replacing whatever the base says for it.
127    ///
128    /// The typed setters below cover the parameters this SDK release knows;
129    /// this is the escape hatch for one it does not.
130    pub fn with_config_param(mut self, param: ConfigParam) -> Self {
131        self.overrides.insert(param.param(), param);
132        self
133    }
134
135    /// Concurrently open incremental SHA-256 buffers permitted per invocation
136    /// frame (the cap `thruput_sdk::Hasher` allocates against).
137    pub fn with_max_sha256_buffers(self, max_sha256_buffers: u32) -> Self {
138        self.with_config_param(ConfigParam::kConfigParamMaxSHA256Buffers {
139            max_sha256_buffers,
140        })
141    }
142
143    /// Invocation frames one transaction may have open at once.
144    ///
145    /// The harness opens its frames directly, so one the executor refuses is
146    /// an unrecoverable panic rather than the error a contract would observe.
147    /// To assert on the cap, have the frame call through the Invoke syscall
148    /// (`thruput_sdk::call::try_call_raw_with`), which reports it as
149    /// `RawCallError::DepthExceeded`.
150    pub fn with_max_cross_invoke_depth(
151        self,
152        max_cross_invoke_depth: u32,
153    ) -> Self {
154        self.with_config_param(ConfigParam::kConfigParamMaxCrossInvokeDepth {
155            max_cross_invoke_depth,
156        })
157    }
158
159    /// Contracts one transaction may attach in `contracts_to_deploy`.
160    pub fn with_max_new_contracts_per_tx(
161        self,
162        max_new_contracts_per_tx: u32,
163    ) -> Self {
164        self.with_config_param(ConfigParam::kConfigParamMaxNewContractsPerTx {
165            max_new_contracts_per_tx,
166        })
167    }
168
169    /// Bytes one bytestring storage item may hold.
170    pub fn with_max_bytestring_len(self, max_bytestring_len: u32) -> Self {
171        self.with_config_param(ConfigParam::kConfigParamMaxBytestringLen {
172            max_bytestring_len,
173        })
174    }
175
176    /// Bytes one attached contract's code blob (`ContractBlob.data`) may
177    /// hold, checked before any attached contract is verified or executed.
178    pub fn with_max_contract_size_bytes(
179        self,
180        max_contract_size_bytes: u32,
181    ) -> Self {
182        self.with_config_param(ConfigParam::kConfigParamMaxContractSizeBytes {
183            max_contract_size_bytes,
184        })
185    }
186
187    /// Storage savepoints one transaction may have open at once: one per
188    /// `TryInvoke` frame still executing, plus the one the root commit
189    /// takes.
190    pub fn with_max_savepoint_depth(self, max_savepoint_depth: u32) -> Self {
191        self.with_config_param(ConfigParam::kConfigParamMaxSavepointDepth {
192            max_savepoint_depth,
193        })
194    }
195
196    /// Bytes one incremental SHA-256 buffer may absorb between resets, the
197    /// per-buffer companion of
198    /// [`with_max_sha256_buffers`](Self::with_max_sha256_buffers).
199    pub fn with_max_sha256_buffer_bytes(
200        self,
201        max_sha256_buffer_bytes: u32,
202    ) -> Self {
203        self.with_config_param(ConfigParam::kConfigParamMaxSHA256BufferBytes {
204            max_sha256_buffer_bytes,
205        })
206    }
207
208    /// Levels of `AuthTokenNode` nesting an auth token may carry, counted
209    /// from its root.
210    pub fn with_max_auth_token_depth(self, max_auth_token_depth: u32) -> Self {
211        self.with_config_param(ConfigParam::kConfigParamMaxAuthTokenDepth {
212            max_auth_token_depth,
213        })
214    }
215
216    /// Bytes of code (`code_size` in the artifact header) an RV32 artifact
217    /// may carry to be attached as a contract.
218    pub fn with_max_rv32_code_bytes(self, max_rv32_code_bytes: u32) -> Self {
219        self.with_config_param(ConfigParam::kConfigParamMaxRv32CodeBytes {
220            max_rv32_code_bytes,
221        })
222    }
223
224    /// Bytes of guest data memory (`memory_bytes` in the artifact header)
225    /// an RV32 artifact may declare to be attached as a contract.
226    pub fn with_max_rv32_memory_bytes(
227        self,
228        max_rv32_memory_bytes: u32,
229    ) -> Self {
230        self.with_config_param(ConfigParam::kConfigParamMaxRv32MemoryBytes {
231            max_rv32_memory_bytes,
232        })
233    }
234
235    /// Whether this builder would produce exactly the stock configuration:
236    /// no base of its own and nothing overridden.
237    pub fn is_stock(&self) -> bool {
238        self.base.is_none() && self.overrides.is_empty()
239    }
240
241    /// The configuration: the base with every override applied.
242    ///
243    /// Panics rather than hand hogd a list it would reject: the parameters
244    /// must come out a gapless prefix of the id space (ids 0, 1, 2, ...),
245    /// which is what gives one configuration one encoding, and hence one
246    /// config hash.
247    pub fn build(self) -> NetworkConfig {
248        let base = self.base.unwrap_or_else(stock_network_config);
249        let mut params = config_params(&base).to_vec();
250
251        // Ascending id order (BTreeMap), so an override that extends the
252        // prefix by one lands right after the parameters below it.
253        for (param_id, param) in self.overrides {
254            let id = param_id.0;
255            let index = usize::try_from(id).unwrap_or_else(|_| {
256                panic!("network configuration parameter id {id} is negative")
257            });
258            match index.cmp(&params.len()) {
259                Ordering::Less => params[index] = param,
260                Ordering::Equal => params.push(param),
261                Ordering::Greater => panic!(
262                    "cannot state {} (id {id}): the configuration states \
263                     {} parameter(s), so this would leave ids {}..{id} \
264                     unstated, and the list must be a gapless prefix of \
265                     the id space. State the parameters in between too -- \
266                     or, if this hogd build does not know id {id} at all, \
267                     drop the override.",
268                    param.param(),
269                    params.len(),
270                    params.len(),
271                ),
272            }
273        }
274
275        for (index, param) in params.iter().enumerate() {
276            assert_eq!(
277                param.param().0,
278                index as i32,
279                "network configuration parameters must be a gapless prefix \
280                 of the id space in id order, but position {index} states \
281                 {}. hogd rejects any other arrangement (fatally, from \
282                 inside the FFI).",
283                param.param(),
284            );
285        }
286
287        NetworkConfig::_0 {
288            v0: NetworkConfigV0 {
289                params: params.try_into().expect(
290                    "the parameter list cannot exceed the XDR length bound",
291                ),
292            },
293        }
294    }
295}
296
297#[cfg(test)]
298mod tests {
299    use super::*;
300
301    fn config_of(params: Vec<ConfigParam>) -> NetworkConfig {
302        NetworkConfig::_0 {
303            v0: NetworkConfigV0 {
304                params: params.try_into().unwrap(),
305            },
306        }
307    }
308
309    #[test]
310    fn the_stock_config_states_a_gapless_prefix_of_the_id_space() {
311        let stock = stock_network_config();
312        let params = config_params(&stock);
313
314        assert!(!params.is_empty(), "a chain states its configuration");
315        for (index, param) in params.iter().enumerate() {
316            assert_eq!(param.param().0, index as i32);
317        }
318    }
319
320    #[test]
321    fn a_builder_with_no_overrides_is_the_stock_config() {
322        let builder = NetworkConfigBuilder::new();
323        assert!(builder.is_stock());
324        assert_eq!(builder.build(), stock_network_config());
325    }
326
327    #[test]
328    fn setting_a_parameter_leaves_the_others_at_their_stock_values() {
329        let stock = stock_network_config();
330        let built = NetworkConfigBuilder::new()
331            .with_max_sha256_buffers(1)
332            .build();
333
334        assert_eq!(
335            find_config_param(
336                &built,
337                ConfigParamID::kConfigParamMaxSHA256Buffers
338            ),
339            Some(&ConfigParam::kConfigParamMaxSHA256Buffers {
340                max_sha256_buffers: 1
341            }),
342        );
343        assert_eq!(&config_params(&built)[1..], &config_params(&stock)[1..],);
344    }
345
346    #[test]
347    fn the_last_setter_for_a_parameter_wins() {
348        let built = NetworkConfigBuilder::new()
349            .with_max_sha256_buffers(1)
350            .with_max_sha256_buffers(4)
351            .build();
352
353        assert_eq!(
354            find_config_param(
355                &built,
356                ConfigParamID::kConfigParamMaxSHA256Buffers
357            ),
358            Some(&ConfigParam::kConfigParamMaxSHA256Buffers {
359                max_sha256_buffers: 4
360            }),
361        );
362    }
363
364    #[test]
365    fn overrides_apply_to_an_explicit_base() {
366        let base = config_of(vec![
367            ConfigParam::kConfigParamMaxSHA256Buffers {
368                max_sha256_buffers: 7,
369            },
370            ConfigParam::kConfigParamMaxCrossInvokeDepth {
371                max_cross_invoke_depth: 9,
372            },
373        ]);
374        let built = NetworkConfigBuilder::new()
375            .with_base(base)
376            .with_max_cross_invoke_depth(3)
377            .build();
378
379        assert_eq!(
380            config_params(&built),
381            [
382                ConfigParam::kConfigParamMaxSHA256Buffers {
383                    max_sha256_buffers: 7
384                },
385                ConfigParam::kConfigParamMaxCrossInvokeDepth {
386                    max_cross_invoke_depth: 3
387                },
388            ],
389        );
390    }
391
392    #[test]
393    fn every_typed_setter_states_its_own_parameter() {
394        // Applied in id order from an empty base, so each setter extends
395        // the prefix by exactly one; the result names every parameter this
396        // release knows, in the order the protocol numbers them.
397        let built = NetworkConfigBuilder::new()
398            .with_base(config_of(vec![]))
399            .with_max_sha256_buffers(1)
400            .with_max_cross_invoke_depth(2)
401            .with_max_new_contracts_per_tx(3)
402            .with_max_bytestring_len(4)
403            .with_max_contract_size_bytes(5)
404            .with_max_savepoint_depth(6)
405            .with_max_sha256_buffer_bytes(7)
406            .with_max_auth_token_depth(8)
407            .with_max_rv32_code_bytes(9)
408            .with_max_rv32_memory_bytes(10)
409            .build();
410
411        assert_eq!(
412            config_params(&built),
413            [
414                ConfigParam::kConfigParamMaxSHA256Buffers {
415                    max_sha256_buffers: 1
416                },
417                ConfigParam::kConfigParamMaxCrossInvokeDepth {
418                    max_cross_invoke_depth: 2
419                },
420                ConfigParam::kConfigParamMaxNewContractsPerTx {
421                    max_new_contracts_per_tx: 3
422                },
423                ConfigParam::kConfigParamMaxBytestringLen {
424                    max_bytestring_len: 4
425                },
426                ConfigParam::kConfigParamMaxContractSizeBytes {
427                    max_contract_size_bytes: 5
428                },
429                ConfigParam::kConfigParamMaxSavepointDepth {
430                    max_savepoint_depth: 6
431                },
432                ConfigParam::kConfigParamMaxSHA256BufferBytes {
433                    max_sha256_buffer_bytes: 7
434                },
435                ConfigParam::kConfigParamMaxAuthTokenDepth {
436                    max_auth_token_depth: 8
437                },
438                ConfigParam::kConfigParamMaxRv32CodeBytes {
439                    max_rv32_code_bytes: 9
440                },
441                ConfigParam::kConfigParamMaxRv32MemoryBytes {
442                    max_rv32_memory_bytes: 10
443                },
444            ],
445        );
446    }
447
448    #[test]
449    fn an_override_may_extend_the_prefix_by_one() {
450        let base =
451            config_of(vec![ConfigParam::kConfigParamMaxSHA256Buffers {
452                max_sha256_buffers: 7,
453            }]);
454        let built = NetworkConfigBuilder::new()
455            .with_base(base)
456            .with_max_cross_invoke_depth(3)
457            .build();
458
459        assert_eq!(config_params(&built).len(), 2);
460    }
461
462    // hogd fails a gapped list from inside the FFI, taking the whole test
463    // binary with it. Refuse to build one in the first place.
464    #[test]
465    #[should_panic(expected = "gapless prefix")]
466    fn an_override_that_would_leave_a_gap_is_refused() {
467        let base = config_of(vec![]);
468        let _ = NetworkConfigBuilder::new()
469            .with_base(base)
470            .with_max_cross_invoke_depth(3)
471            .build();
472    }
473
474    #[test]
475    #[should_panic(expected = "gapless prefix")]
476    fn a_base_that_is_not_a_prefix_is_refused() {
477        let base =
478            config_of(vec![ConfigParam::kConfigParamMaxCrossInvokeDepth {
479                max_cross_invoke_depth: 9,
480            }]);
481        let _ = NetworkConfigBuilder::new().with_base(base).build();
482    }
483
484    #[test]
485    fn a_builder_with_a_base_is_not_stock() {
486        assert!(
487            !NetworkConfigBuilder::new()
488                .with_base(stock_network_config())
489                .is_stock()
490        );
491    }
492}