unc_parameters/
config_store.rs

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