near_parameters/
config_store.rs

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