near_parameters/
config_store.rs

1use crate::config::{
2    BandwidthSchedulerConfig, CongestionControlConfig, RuntimeConfig, WitnessConfig,
3};
4use crate::parameter_table::{ParameterTable, ParameterTableDiff};
5use crate::vm;
6use near_primitives_core::types::ProtocolVersion;
7use near_primitives_core::version::{PROTOCOL_VERSION, ProtocolFeature};
8use std::collections::BTreeMap;
9use std::ops::Bound;
10use std::sync::Arc;
11
12macro_rules! include_config {
13    ($file:expr) => {
14        include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/res/runtime_configs/", $file))
15    };
16}
17
18/// The base config file with all initial parameter values defined.
19/// Later version are calculated by applying diffs to this base.
20static BASE_CONFIG: &str = include_config!("parameters.yaml");
21
22/// Stores pairs of protocol versions for which runtime config was updated and
23/// the file containing the diffs in bytes.
24static CONFIG_DIFFS: &[(ProtocolVersion, &str)] = &[
25    (35, include_config!("35.yaml")),
26    (42, include_config!("42.yaml")),
27    (46, include_config!("46.yaml")),
28    (48, include_config!("48.yaml")),
29    (49, include_config!("49.yaml")),
30    (50, include_config!("50.yaml")),
31    // max_gas_burnt increased to 300 TGas
32    (52, include_config!("52.yaml")),
33    // Increased deployment costs, increased wasmer2 stack_limit, added limiting of contract locals,
34    // set read_cached_trie_node cost, decrease storage key limit
35    (53, include_config!("53.yaml")),
36    (55, include_config!("55.yaml")),
37    (57, include_config!("57.yaml")),
38    // Introduce Zero Balance Account and increase account creation cost to 7.7Tgas
39    (59, include_config!("59.yaml")),
40    (61, include_config!("61.yaml")),
41    (62, include_config!("62.yaml")),
42    (63, include_config!("63.yaml")),
43    (64, include_config!("64.yaml")),
44    (66, include_config!("66.yaml")),
45    (67, include_config!("67.yaml")),
46    // Congestion Control.
47    (68, include_config!("68.yaml")),
48    // Stateless Validation.
49    (69, include_config!("69.yaml")),
50    // Introduce ETH-implicit accounts.
51    (70, include_config!("70.yaml")),
52    // Increase main_storage_proof_size_soft_limit and introduces StateStoredReceipt
53    (72, include_config!("72.yaml")),
54    // Fix wasm_yield_resume_byte and relax congestion control.
55    (73, include_config!("73.yaml")),
56    (74, include_config!("74.yaml")),
57    (77, include_config!("77.yaml")),
58    (78, include_config!("78.yaml")),
59    (79, include_config!("79.yaml")),
60    (129, include_config!("129.yaml")),
61];
62
63/// Testnet parameters for versions <= 29, which (incorrectly) differed from mainnet parameters
64pub static INITIAL_TESTNET_CONFIG: &str = include_config!("parameters_testnet.yaml");
65
66/// Stores runtime config for each protocol version where it was updated.
67#[derive(Clone, Debug)]
68pub struct RuntimeConfigStore {
69    /// Maps protocol version to the config.
70    store: BTreeMap<ProtocolVersion, Arc<RuntimeConfig>>,
71}
72
73impl RuntimeConfigStore {
74    /// Constructs a new store.
75    ///
76    /// If genesis_runtime_config is Some, configs for protocol versions 0 and 42 are overridden by
77    /// this config and config with lowered storage cost, respectively.
78    /// This is done to preserve compatibility with previous implementation, where we updated
79    /// runtime config by sequential modifications to the genesis runtime config.
80    /// calimero_zero_storage flag sets all storages fees to zero by setting
81    /// storage_amount_per_byte to zero, to keep calimero private shards compatible with future
82    /// protocol upgrades this is done for all protocol versions
83    /// TODO #4775: introduce new protocol version to have the same runtime config for all chains
84    pub fn new(genesis_runtime_config: Option<&RuntimeConfig>) -> Self {
85        let mut params: ParameterTable =
86            BASE_CONFIG.parse().expect("Failed parsing base parameter file.");
87
88        let mut store = BTreeMap::new();
89        #[cfg(not(feature = "calimero_zero_storage"))]
90        {
91            let initial_config = RuntimeConfig::new(&params).unwrap_or_else(|err| {
92                panic!(
93                    "Failed generating `RuntimeConfig` from parameters for base parameter file. \
94                     Error: {err:?}"
95                )
96            });
97            store.insert(0, Arc::new(initial_config));
98        }
99        #[cfg(feature = "calimero_zero_storage")]
100        {
101            let mut initial_config = RuntimeConfig::new(&params).unwrap_or_else(|err| {
102                panic!(
103                    "Failed generating `RuntimeConfig` from parameters for base parameter file. \
104                     Error: {err:?}"
105                )
106            });
107            let fees = Arc::make_mut(&mut initial_config.fees);
108            fees.storage_usage_config.storage_amount_per_byte = 0;
109            store.insert(0, Arc::new(initial_config));
110        }
111
112        for (protocol_version, diff_bytes) in CONFIG_DIFFS {
113            let diff: ParameterTableDiff = diff_bytes.parse().unwrap_or_else(|err| {
114                panic!(
115                    "Failed parsing runtime parameters diff for version {protocol_version}. \
116                     Error: {err:?}"
117                )
118            });
119            params.apply_diff(diff).unwrap_or_else(|err| {
120                panic!(
121                    "Failed applying diff to `RuntimeConfig` for version {protocol_version}. \
122                     Error: {err}"
123                )
124            });
125            #[cfg(not(feature = "calimero_zero_storage"))]
126            store.insert(
127                *protocol_version,
128                Arc::new(RuntimeConfig::new(&params).unwrap_or_else(|err| {
129                    panic!(
130                        "Failed generating `RuntimeConfig` from parameters for \
131                         version {protocol_version}. Error: {err:?}"
132                    )
133                })),
134            );
135            #[cfg(feature = "calimero_zero_storage")]
136            {
137                let mut runtime_config = RuntimeConfig::new(&params).unwrap_or_else(|err| {
138                    panic!(
139                        "Failed generating `RuntimeConfig` from parameters for \
140                         version {protocol_version}. Error: {err:?}"
141                    )
142                });
143                let fees = Arc::make_mut(&mut runtime_config.fees);
144                fees.storage_usage_config.storage_amount_per_byte = 0;
145                store.insert(*protocol_version, Arc::new(runtime_config));
146            }
147        }
148
149        if let Some(runtime_config) = genesis_runtime_config {
150            let mut fees = crate::RuntimeFeesConfig::clone(&runtime_config.fees);
151            fees.storage_usage_config.storage_amount_per_byte = 10u128.pow(19);
152            store.insert(
153                42,
154                Arc::new(RuntimeConfig {
155                    fees: Arc::new(fees),
156                    wasm_config: Arc::clone(&runtime_config.wasm_config),
157                    account_creation_config: runtime_config.account_creation_config.clone(),
158                    congestion_control_config: runtime_config.congestion_control_config,
159                    witness_config: runtime_config.witness_config,
160                    bandwidth_scheduler_config: runtime_config.bandwidth_scheduler_config,
161                    use_state_stored_receipt: runtime_config.use_state_stored_receipt,
162                }),
163            );
164            store.insert(0, Arc::new(runtime_config.clone()));
165        }
166
167        Self { store }
168    }
169
170    /// Create store of runtime configs for the given chain id.
171    ///
172    /// For mainnet and other chains except testnet we don't need to override runtime config for
173    /// first protocol versions.
174    /// For testnet, runtime config for genesis block was (incorrectly) different, that's why we
175    /// need to override it specifically to preserve compatibility.
176    /// In benchmarknet, we are measuring the peak throughput that the NEAR network can handle while still being stable.
177    /// This requires increasing the limits below that are set too conservatively.
178    pub fn for_chain_id(chain_id: &str) -> Self {
179        match chain_id {
180            near_primitives_core::chains::TESTNET => {
181                let genesis_runtime_config = RuntimeConfig::initial_testnet_config();
182                Self::new(Some(&genesis_runtime_config))
183            }
184            near_primitives_core::chains::BENCHMARKNET => {
185                let mut config_store = Self::new(None);
186                let mut config = RuntimeConfig::clone(config_store.get_config(PROTOCOL_VERSION));
187                config.congestion_control_config = CongestionControlConfig::test_disabled();
188                config.bandwidth_scheduler_config = BandwidthSchedulerConfig::test_disabled();
189                config.witness_config = WitnessConfig::test_disabled();
190                let mut wasm_config = vm::Config::clone(&config.wasm_config);
191                wasm_config.limit_config.per_receipt_storage_proof_size_limit = usize::max_value();
192                config.wasm_config = Arc::new(wasm_config);
193                config_store.store.insert(PROTOCOL_VERSION, Arc::new(config));
194                config_store
195            }
196            near_primitives_core::chains::CONGESTION_CONTROL_TEST => {
197                let mut config_store = Self::new(None);
198
199                // TODO(limited_replayability): Move tests to use config from latest protocol version.
200                // Get the original congestion control config. The nayduck tests are tuned to this config.
201                #[allow(deprecated)]
202                let source_protocol_version =
203                    ProtocolFeature::_DeprecatedCongestionControl.protocol_version();
204                let source_runtime_config = config_store.get_config(source_protocol_version);
205
206                let mut config = RuntimeConfig::clone(config_store.get_config(PROTOCOL_VERSION));
207                config.congestion_control_config = source_runtime_config.congestion_control_config;
208
209                config_store.store.insert(PROTOCOL_VERSION, Arc::new(config));
210                config_store
211            }
212            _ => Self::new(None),
213        }
214    }
215
216    /// Constructs test store.
217    pub fn with_one_config(runtime_config: RuntimeConfig) -> Self {
218        Self { store: BTreeMap::from_iter([(0, Arc::new(runtime_config))].iter().cloned()) }
219    }
220
221    /// Constructs store with custom configs. This should only be used for testing.
222    pub fn new_custom(store: BTreeMap<ProtocolVersion, Arc<RuntimeConfig>>) -> Self {
223        Self { store }
224    }
225
226    /// Constructs test store.
227    pub fn test() -> Self {
228        Self::with_one_config(RuntimeConfig::test())
229    }
230
231    /// Constructs test store.
232    pub fn test_congestion_control_disabled() -> Self {
233        let mut config = RuntimeConfig::test();
234        config.congestion_control_config = CongestionControlConfig::test_disabled();
235
236        Self::with_one_config(config)
237    }
238
239    /// Constructs store with a single config with zero costs.
240    pub fn free() -> Self {
241        Self::with_one_config(RuntimeConfig::free())
242    }
243
244    /// Returns a `RuntimeConfig` for the corresponding protocol version.
245    pub fn get_config(&self, protocol_version: ProtocolVersion) -> &Arc<RuntimeConfig> {
246        self.store
247            .range((Bound::Unbounded, Bound::Included(protocol_version)))
248            .next_back()
249            .unwrap_or_else(|| {
250                panic!("Not found RuntimeConfig for protocol version {}", protocol_version)
251            })
252            .1
253    }
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259    use crate::cost::ActionCosts;
260    use std::collections::HashSet;
261
262    const GENESIS_PROTOCOL_VERSION: ProtocolVersion = 29;
263
264    #[test]
265    fn all_configs_are_specified() {
266        let file_versions =
267            std::fs::read_dir(concat!(env!("CARGO_MANIFEST_DIR"), "/res/runtime_configs/"))
268                .expect("can open config directory");
269        let mut files = file_versions
270            .into_iter()
271            .map(|de| {
272                de.expect("dir entry should read successfully")
273                    .path()
274                    .file_name()
275                    .expect("dir entry should have a filename")
276                    .to_string_lossy()
277                    .into_owned()
278            })
279            .collect::<HashSet<_>>();
280
281        for (ver, _) in super::CONFIG_DIFFS {
282            assert!(files.remove(&format!("{ver}.yaml")), "{ver}.yaml file is missing?");
283        }
284
285        for file in files {
286            let Some((name, "yaml")) = file.rsplit_once(".") else { continue };
287            let Ok(version_num) = name.parse::<u32>() else { continue };
288            panic!("CONFIG_DIFFS does not contain reference to the {version_num}.yaml file!");
289        }
290    }
291
292    #[test]
293    fn test_override_account_length() {
294        // Check that default value is 32.
295        let base_store = RuntimeConfigStore::new(None);
296        let base_cfg = base_store.get_config(GENESIS_PROTOCOL_VERSION);
297        assert_eq!(base_cfg.account_creation_config.min_allowed_top_level_account_length, 32);
298
299        let mut cfg = base_cfg.as_ref().clone();
300        cfg.account_creation_config.min_allowed_top_level_account_length = 0;
301
302        // Check that length was changed.
303        let new_store = RuntimeConfigStore::new(Some(&cfg));
304        let new_cfg = new_store.get_config(GENESIS_PROTOCOL_VERSION);
305        assert_eq!(new_cfg.account_creation_config.min_allowed_top_level_account_length, 0);
306    }
307
308    #[test]
309    fn test_parameter_merging() {
310        let mut base_params: ParameterTable = BASE_CONFIG.parse().unwrap();
311        let base_config = RuntimeConfig::new(&base_params).unwrap();
312
313        let mock_diff_str = r#"
314        max_length_storage_key: { old: 4_194_304, new: 42 }
315        action_receipt_creation: {
316          old: {
317            send_sir: 108_059_500_000,
318            send_not_sir: 108_059_500_000,
319            execution: 108_059_500_000,
320          },
321          new: {
322            send_sir: 100000000,
323            send_not_sir: 108_059_500_000,
324            execution: 108_059_500_000,
325          },
326        }
327        "#;
328
329        let mock_diff: ParameterTableDiff = mock_diff_str.parse().unwrap();
330
331        base_params.apply_diff(mock_diff).unwrap();
332        let modified_config = RuntimeConfig::new(&base_params).unwrap();
333
334        assert_eq!(modified_config.wasm_config.limit_config.max_length_storage_key, 42);
335        assert_eq!(modified_config.fees.fee(ActionCosts::new_action_receipt).send_sir, 100000000);
336
337        assert_eq!(
338            base_config.storage_amount_per_byte(),
339            modified_config.storage_amount_per_byte()
340        );
341    }
342
343    /// Use snapshot testing to check that the JSON representation of the configurations of each version is unchanged.
344    /// If tests fail after an intended change, follow the steps below to update the config files:
345    /// 1) Run the following to run tests with cargo insta so it generates all the file differences:
346    ///    cargo insta test  -p near-parameters -- tests::test_json_unchanged
347    /// 2) Run the following to examine the diffs at each file and see if the changes make sense:
348    ///    cargo insta review
349    ///    If the changes make sense, accept the changes per file (by responding to the prompts from the command).
350    /// Alternatively, add --accept to the first command so that it automatically does step 2.
351    #[test]
352    #[cfg(not(feature = "nightly"))]
353    #[cfg(not(feature = "calimero_zero_storage"))]
354    fn test_json_unchanged() {
355        use crate::view::RuntimeConfigView;
356        use near_primitives_core::version::PROTOCOL_VERSION;
357
358        let store = RuntimeConfigStore::new(None);
359        let mut any_failure = false;
360
361        for version in store.store.keys() {
362            let snapshot_name = format!("{version}.json");
363            let config_view = RuntimeConfigView::from(store.get_config(*version).as_ref().clone());
364            any_failure |= std::panic::catch_unwind(|| {
365                insta::assert_json_snapshot!(snapshot_name, config_view, { ".wasm_config.vm_kind" => "<REDACTED>"});
366            })
367            .is_err();
368        }
369
370        // Store the latest values of parameters in a human-readable snapshot.
371        {
372            let mut params: ParameterTable = BASE_CONFIG.parse().unwrap();
373            for (_, diff_bytes) in
374                CONFIG_DIFFS.iter().filter(|(version, _)| *version <= PROTOCOL_VERSION)
375            {
376                params.apply_diff(diff_bytes.parse().unwrap()).unwrap();
377            }
378            insta::with_settings!({
379                snapshot_path => "../res/runtime_configs",
380                prepend_module_to_snapshot => false,
381                description => "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.",
382                omit_expression => true,
383            }, {
384                any_failure |= std::panic::catch_unwind(|| {
385                    insta::assert_snapshot!("parameters", params);
386                }).is_err();
387            });
388        }
389
390        // Testnet initial config for old version was different, thus needs separate testing
391        let params = INITIAL_TESTNET_CONFIG.parse().unwrap();
392        let new_genesis_runtime_config = RuntimeConfig::new(&params).unwrap();
393        let testnet_store = RuntimeConfigStore::new(Some(&new_genesis_runtime_config));
394
395        for version in testnet_store.store.keys() {
396            let snapshot_name = format!("testnet_{version}.json");
397            let config_view = RuntimeConfigView::from(store.get_config(*version).as_ref().clone());
398            any_failure |= std::panic::catch_unwind(|| {
399                insta::assert_json_snapshot!(snapshot_name, config_view, { ".wasm_config.vm_kind" => "<REDACTED>"});
400            })
401            .is_err();
402        }
403        if any_failure {
404            panic!("some snapshot assertions failed");
405        }
406    }
407
408    #[test]
409    #[cfg(feature = "calimero_zero_storage")]
410    fn test_calimero_storage_costs_zero() {
411        let store = RuntimeConfigStore::new(None);
412        for (_, config) in &store.store {
413            assert_eq!(config.storage_amount_per_byte(), 0u128);
414        }
415    }
416
417    #[test]
418    fn test_benchmarknet_config() {
419        let store = RuntimeConfigStore::for_chain_id(near_primitives_core::chains::BENCHMARKNET);
420        let config = store.get_config(PROTOCOL_VERSION);
421        assert_eq!(config.witness_config.main_storage_proof_size_soft_limit, usize::MAX);
422    }
423}