unc_parameters/
parameter_table.rs

1use super::config::{AccountCreationConfig, RuntimeConfig};
2use crate::cost::{
3    ActionCosts, ExtCostsConfig, Fee, ParameterCost, RuntimeFeesConfig, StorageUsageConfig,
4};
5use crate::parameter::{FeeParameter, Parameter};
6use crate::vm::VMKind;
7use crate::vm::{Config, StorageGetMode};
8use num_rational::Rational32;
9use std::collections::BTreeMap;
10use unc_primitives_core::account::id::ParseAccountError;
11use unc_primitives_core::types::AccountId;
12
13/// Represents values supported by parameter config.
14#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq)]
15#[serde(untagged)]
16pub(crate) enum ParameterValue {
17    U64(u64),
18    Rational { numerator: i32, denominator: i32 },
19    ParameterCost { gas: u64, compute: u64 },
20    Fee { send_sir: u64, send_not_sir: u64, execution: u64 },
21    // Can be used to store either a string or u128. Ideally, we would use a dedicated enum member
22    // for u128, but this is currently impossible to express in YAML (see
23    // `canonicalize_yaml_string`).
24    String(String),
25    Flag(bool),
26}
27
28#[derive(thiserror::Error, Debug)]
29pub(crate) enum ValueConversionError {
30    #[error("expected a value of type `{0}`, but could not parse it from `{1:?}`")]
31    ParseType(&'static str, ParameterValue),
32
33    #[error("expected an integer of type `{1}` but could not parse it from `{2:?}`")]
34    ParseInt(#[source] std::num::ParseIntError, &'static str, ParameterValue),
35
36    #[error("expected an integer of type `{1}` but could not parse it from `{2:?}`")]
37    TryFromInt(#[source] std::num::TryFromIntError, &'static str, ParameterValue),
38
39    #[error("expected an account id, but could not parse it from `{1}`")]
40    ParseAccountId(#[source] ParseAccountError, String),
41
42    #[error("expected a VM kind, but could not parse it from `{1}`")]
43    ParseVmKind(#[source] strum::ParseError, String),
44}
45
46macro_rules! implement_conversion_to {
47    ($($ty: ty),*) => {
48        $(impl TryFrom<&ParameterValue> for $ty {
49            type Error = ValueConversionError;
50            fn try_from(value: &ParameterValue) -> Result<Self, Self::Error> {
51                match value {
52                    ParameterValue::U64(v) => <$ty>::try_from(*v).map_err(|err| {
53                        ValueConversionError::TryFromInt(
54                            err.into(),
55                            std::any::type_name::<$ty>(),
56                            value.clone(),
57                        )
58                    }),
59                    _ => Err(ValueConversionError::ParseType(
60                            std::any::type_name::<$ty>(), value.clone()
61                    )),
62                }
63            }
64        })*
65    }
66}
67
68implement_conversion_to!(u64, u32, u16, u8, i64, i32, i16, i8, usize, isize);
69
70impl TryFrom<&ParameterValue> for u128 {
71    type Error = ValueConversionError;
72
73    fn try_from(value: &ParameterValue) -> Result<Self, Self::Error> {
74        match value {
75            ParameterValue::U64(v) => Ok(u128::from(*v)),
76            ParameterValue::String(s) => s.parse().map_err(|err| {
77                ValueConversionError::ParseInt(err, std::any::type_name::<u128>(), value.clone())
78            }),
79            _ => Err(ValueConversionError::ParseType(std::any::type_name::<u128>(), value.clone())),
80        }
81    }
82}
83
84impl TryFrom<&ParameterValue> for bool {
85    type Error = ValueConversionError;
86
87    fn try_from(value: &ParameterValue) -> Result<Self, Self::Error> {
88        match value {
89            ParameterValue::Flag(b) => Ok(*b),
90            ParameterValue::String(s) => match &**s {
91                "true" => Ok(true),
92                "false" => Ok(false),
93                _ => Err(ValueConversionError::ParseType("bool", value.clone())),
94            },
95            _ => Err(ValueConversionError::ParseType("bool", value.clone())),
96        }
97    }
98}
99
100impl TryFrom<&ParameterValue> for Rational32 {
101    type Error = ValueConversionError;
102
103    fn try_from(value: &ParameterValue) -> Result<Self, Self::Error> {
104        match value {
105            &ParameterValue::Rational { numerator, denominator } => {
106                Ok(Rational32::new(numerator, denominator))
107            }
108            _ => Err(ValueConversionError::ParseType(
109                std::any::type_name::<Rational32>(),
110                value.clone(),
111            )),
112        }
113    }
114}
115
116impl TryFrom<&ParameterValue> for ParameterCost {
117    type Error = ValueConversionError;
118
119    fn try_from(value: &ParameterValue) -> Result<Self, Self::Error> {
120        match value {
121            ParameterValue::ParameterCost { gas, compute } => {
122                Ok(ParameterCost { gas: *gas, compute: *compute })
123            }
124            // If not specified, compute costs default to gas costs.
125            &ParameterValue::U64(v) => Ok(ParameterCost { gas: v, compute: v }),
126            _ => Err(ValueConversionError::ParseType(
127                std::any::type_name::<ParameterCost>(),
128                value.clone(),
129            )),
130        }
131    }
132}
133
134impl TryFrom<&ParameterValue> for Fee {
135    type Error = ValueConversionError;
136
137    fn try_from(value: &ParameterValue) -> Result<Self, Self::Error> {
138        match value {
139            &ParameterValue::Fee { send_sir, send_not_sir, execution } => {
140                Ok(Fee { send_sir, send_not_sir, execution })
141            }
142            _ => Err(ValueConversionError::ParseType(std::any::type_name::<Fee>(), value.clone())),
143        }
144    }
145}
146
147impl<'a> TryFrom<&'a ParameterValue> for &'a str {
148    type Error = ValueConversionError;
149
150    fn try_from(value: &'a ParameterValue) -> Result<Self, Self::Error> {
151        match value {
152            ParameterValue::String(v) => Ok(v),
153            _ => {
154                Err(ValueConversionError::ParseType(std::any::type_name::<String>(), value.clone()))
155            }
156        }
157    }
158}
159
160impl TryFrom<&ParameterValue> for AccountId {
161    type Error = ValueConversionError;
162
163    fn try_from(value: &ParameterValue) -> Result<Self, Self::Error> {
164        let value: &str = value.try_into()?;
165        value.parse().map_err(|err| ValueConversionError::ParseAccountId(err, value.to_string()))
166    }
167}
168
169impl TryFrom<&ParameterValue> for VMKind {
170    type Error = ValueConversionError;
171
172    fn try_from(value: &ParameterValue) -> Result<Self, Self::Error> {
173        match value {
174            ParameterValue::String(v) => v
175                .parse()
176                .map(|v: VMKind| v.replace_with_wasmtime_if_unsupported())
177                .map_err(|e| ValueConversionError::ParseVmKind(e, value.to_string())),
178            _ => {
179                Err(ValueConversionError::ParseType(std::any::type_name::<VMKind>(), value.clone()))
180            }
181        }
182    }
183}
184
185fn format_number(mut n: u64) -> String {
186    let mut parts = Vec::new();
187    while n >= 1000 {
188        parts.push(format!("{:03?}", n % 1000));
189        n /= 1000;
190    }
191    parts.push(n.to_string());
192    parts.reverse();
193    parts.join("_")
194}
195
196impl core::fmt::Display for ParameterValue {
197    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
198        match self {
199            ParameterValue::U64(v) => write!(f, "{:>20}", format_number(*v)),
200            ParameterValue::Rational { numerator, denominator } => {
201                write!(f, "{numerator} / {denominator}")
202            }
203            ParameterValue::ParameterCost { gas, compute } => {
204                write!(f, "{:>20}, compute: {:>20}", format_number(*gas), format_number(*compute))
205            }
206            ParameterValue::Fee { send_sir, send_not_sir, execution } => {
207                write!(
208                    f,
209                    r#"
210- send_sir:     {:>20}
211- send_not_sir: {:>20}
212- execution:    {:>20}"#,
213                    format_number(*send_sir),
214                    format_number(*send_not_sir),
215                    format_number(*execution)
216                )
217            }
218            ParameterValue::String(v) => write!(f, "{v}"),
219            ParameterValue::Flag(b) => write!(f, "{b:?}"),
220        }
221    }
222}
223
224pub(crate) struct ParameterTable {
225    parameters: BTreeMap<Parameter, ParameterValue>,
226}
227
228/// Formats `ParameterTable` in human-readable format which is a subject to change and is not
229/// intended to be parsed back.
230impl core::fmt::Display for ParameterTable {
231    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
232        for (key, value) in &self.parameters {
233            write!(f, "{key:40}{value}\n")?
234        }
235        Ok(())
236    }
237}
238
239/// Changes made to parameters between versions.
240pub(crate) struct ParameterTableDiff {
241    parameters: BTreeMap<Parameter, (Option<ParameterValue>, Option<ParameterValue>)>,
242}
243
244/// Error returned by ParameterTable::from_str() that parses a runtime configuration YAML file.
245#[derive(thiserror::Error, Debug)]
246pub(crate) enum InvalidConfigError {
247    #[error("could not parse `{1}` as a parameter")]
248    UnknownParameter(#[source] strum::ParseError, String),
249    #[error("could not parse `{1}` as a value")]
250    ValueParseError(#[source] serde_yaml::Error, String),
251    #[error("could not parse YAML that defines the structure of the config")]
252    InvalidYaml(#[source] serde_yaml::Error),
253    #[error("config diff expected to contain old value `{1:?}` for parameter `{0}`")]
254    OldValueExists(Parameter, ParameterValue),
255    #[error(
256        "unexpected old value `{1:?}` for parameter `{0}` in config diff, previous version does not have such a value"
257    )]
258    NoOldValueExists(Parameter, ParameterValue),
259    #[error("expected old value `{1:?}` but found `{2:?}` for parameter `{0}` in config diff")]
260    WrongOldValue(Parameter, ParameterValue, ParameterValue),
261    #[error("expected a value for `{0}` but found none")]
262    MissingParameter(Parameter),
263    #[error("failed to convert a value for `{1}`")]
264    ValueConversionError(#[source] ValueConversionError, Parameter),
265}
266
267impl std::str::FromStr for ParameterTable {
268    type Err = InvalidConfigError;
269    fn from_str(arg: &str) -> Result<ParameterTable, InvalidConfigError> {
270        let yaml_map: BTreeMap<String, serde_yaml::Value> =
271            serde_yaml::from_str(arg).map_err(|err| InvalidConfigError::InvalidYaml(err))?;
272
273        let parameters = yaml_map
274            .iter()
275            .map(|(key, value)| {
276                let typed_key: Parameter = key
277                    .parse()
278                    .map_err(|err| InvalidConfigError::UnknownParameter(err, key.to_owned()))?;
279                Ok((typed_key, parse_parameter_value(value)?))
280            })
281            .collect::<Result<BTreeMap<_, _>, _>>()?;
282
283        Ok(ParameterTable { parameters })
284    }
285}
286
287impl TryFrom<&ParameterTable> for RuntimeConfig {
288    type Error = InvalidConfigError;
289
290    fn try_from(params: &ParameterTable) -> Result<Self, Self::Error> {
291        Ok(RuntimeConfig {
292            fees: RuntimeFeesConfig {
293                action_fees: enum_map::enum_map! {
294                    action_cost => params.get_fee(action_cost)?
295                },
296                burnt_gas_reward: params.get(Parameter::BurntGasReward)?,
297                pessimistic_gas_price_inflation_ratio: params
298                    .get(Parameter::PessimisticGasPriceInflation)?,
299                storage_usage_config: StorageUsageConfig {
300                    storage_amount_per_byte: params.get(Parameter::StorageAmountPerByte)?,
301                    num_bytes_account: params.get(Parameter::StorageNumBytesAccount)?,
302                    num_extra_bytes_record: params.get(Parameter::StorageNumExtraBytesRecord)?,
303                },
304            },
305            wasm_config: Config {
306                ext_costs: ExtCostsConfig {
307                    costs: enum_map::enum_map! {
308                        cost => params.get(cost.param())?
309                    },
310                },
311                vm_kind: params.get(Parameter::VmKind)?,
312                grow_mem_cost: params.get(Parameter::WasmGrowMemCost)?,
313                regular_op_cost: params.get(Parameter::WasmRegularOpCost)?,
314                disable_9393_fix: params.get(Parameter::Disable9393Fix)?,
315                limit_config: serde_yaml::from_value(params.yaml_map(Parameter::vm_limits()))
316                    .map_err(InvalidConfigError::InvalidYaml)?,
317                fix_contract_loading_cost: params.get(Parameter::FixContractLoadingCost)?,
318                storage_get_mode: match params.get(Parameter::FlatStorageReads)? {
319                    true => StorageGetMode::FlatStorage,
320                    false => StorageGetMode::Trie,
321                },
322                implicit_account_creation: params.get(Parameter::ImplicitAccountCreation)?,
323                math_extension: params.get(Parameter::MathExtension)?,
324                ed25519_verify: params.get(Parameter::Ed25519Verify)?,
325                alt_bn128: params.get(Parameter::AltBn128)?,
326                function_call_weight: params.get(Parameter::FunctionCallWeight)?,
327                eth_accounts: params.get(Parameter::EthAccounts)?,
328            },
329            account_creation_config: AccountCreationConfig {
330                min_allowed_top_level_account_length: params
331                    .get(Parameter::MinAllowedTopLevelAccountLength)?,
332                registrar_account_id: params.get(Parameter::RegistrarAccountId)?,
333            },
334        })
335    }
336}
337
338impl ParameterTable {
339    pub(crate) fn apply_diff(
340        &mut self,
341        diff: ParameterTableDiff,
342    ) -> Result<(), InvalidConfigError> {
343        for (key, (before, after)) in diff.parameters {
344            let old_value = self.parameters.get(&key);
345            if old_value != before.as_ref() {
346                if old_value.is_none() {
347                    return Err(InvalidConfigError::NoOldValueExists(key, before.unwrap()));
348                }
349                if before.is_none() {
350                    return Err(InvalidConfigError::OldValueExists(
351                        key,
352                        old_value.unwrap().clone(),
353                    ));
354                }
355                return Err(InvalidConfigError::WrongOldValue(
356                    key,
357                    old_value.unwrap().clone(),
358                    before.unwrap(),
359                ));
360            }
361
362            if let Some(new_value) = after {
363                self.parameters.insert(key, new_value);
364            } else {
365                self.parameters.remove(&key);
366            }
367        }
368        Ok(())
369    }
370
371    fn yaml_map(&self, params: impl Iterator<Item = &'static Parameter>) -> serde_yaml::Value {
372        // All parameter values can be serialized as YAML, so we don't ever expect this to fail.
373        serde_yaml::to_value(
374            params
375                .filter_map(|param| Some((param.to_string(), self.parameters.get(param)?)))
376                .collect::<BTreeMap<_, _>>(),
377        )
378        .expect("failed to convert parameter values to YAML")
379    }
380
381    /// Read and parse a typed parameter from the `ParameterTable`.
382    fn get<'a, T>(&'a self, key: Parameter) -> Result<T, InvalidConfigError>
383    where
384        T: TryFrom<&'a ParameterValue, Error = ValueConversionError>,
385    {
386        let value = self.parameters.get(&key).ok_or(InvalidConfigError::MissingParameter(key))?;
387        value.try_into().map_err(|err| InvalidConfigError::ValueConversionError(err, key))
388    }
389
390    /// Access action fee by `ActionCosts`.
391    fn get_fee(&self, cost: ActionCosts) -> Result<Fee, InvalidConfigError> {
392        let key: Parameter = format!("{}", FeeParameter::from(cost)).parse().unwrap();
393        self.get(key)
394    }
395}
396
397/// Represents values supported by parameter diff config.
398#[derive(serde::Deserialize, Clone, Debug)]
399struct ParameterDiffConfigValue {
400    old: Option<serde_yaml::Value>,
401    new: Option<serde_yaml::Value>,
402}
403
404impl std::str::FromStr for ParameterTableDiff {
405    type Err = InvalidConfigError;
406    fn from_str(arg: &str) -> Result<ParameterTableDiff, InvalidConfigError> {
407        let yaml_map: BTreeMap<String, ParameterDiffConfigValue> =
408            serde_yaml::from_str(arg).map_err(|err| InvalidConfigError::InvalidYaml(err))?;
409
410        let parameters = yaml_map
411            .iter()
412            .map(|(key, value)| {
413                let typed_key: Parameter = key
414                    .parse()
415                    .map_err(|err| InvalidConfigError::UnknownParameter(err, key.to_owned()))?;
416
417                let old_value =
418                    if let Some(s) = &value.old { Some(parse_parameter_value(s)?) } else { None };
419
420                let new_value =
421                    if let Some(s) = &value.new { Some(parse_parameter_value(s)?) } else { None };
422
423                Ok((typed_key, (old_value, new_value)))
424            })
425            .collect::<Result<BTreeMap<_, _>, _>>()?;
426        Ok(ParameterTableDiff { parameters })
427    }
428}
429
430/// Parses a value from YAML to a more restricted type of parameter values.
431fn parse_parameter_value(value: &serde_yaml::Value) -> Result<ParameterValue, InvalidConfigError> {
432    Ok(serde_yaml::from_value(canonicalize_yaml_value(value)?)
433        .map_err(|err| InvalidConfigError::InvalidYaml(err))?)
434}
435
436/// Recursively canonicalizes values inside of the YAML structure.
437fn canonicalize_yaml_value(
438    value: &serde_yaml::Value,
439) -> Result<serde_yaml::Value, InvalidConfigError> {
440    Ok(match value {
441        serde_yaml::Value::String(s) => canonicalize_yaml_string(s)?,
442        serde_yaml::Value::Mapping(m) => serde_yaml::Value::Mapping(
443            m.iter()
444                .map(|(key, value)| {
445                    let canonical_value = canonicalize_yaml_value(value)?;
446                    Ok((key.clone(), canonical_value))
447                })
448                .collect::<Result<_, _>>()?,
449        ),
450        _ => value.clone(),
451    })
452}
453
454/// Parses a value from the custom format for runtime parameter definitions.
455///
456/// A value can be a positive integer or a string, with or without quotes.
457/// Integers can use underlines as separators (for readability).
458///
459/// The main purpose of this function is to add support for integers with underscore digit
460/// separators which we use in the config but are not supported in YAML.
461fn canonicalize_yaml_string(value: &str) -> Result<serde_yaml::Value, InvalidConfigError> {
462    if value.is_empty() {
463        return Ok(serde_yaml::Value::Null);
464    }
465    if value.bytes().all(|c| c.is_ascii_digit() || c == '_' as u8) {
466        let mut raw_number = value.to_owned();
467        raw_number.retain(char::is_numeric);
468        // We do not have "arbitrary_precision" serde feature enabled, thus we
469        // can only store up to `u64::MAX`, which is `18446744073709551615` and
470        // has 20 characters.
471        if raw_number.len() < 20 {
472            serde_yaml::from_str(&raw_number)
473                .map_err(|err| InvalidConfigError::ValueParseError(err, value.to_owned()))
474        } else {
475            Ok(serde_yaml::Value::String(raw_number))
476        }
477    } else {
478        Ok(serde_yaml::Value::String(value.to_owned()))
479    }
480}
481
482#[cfg(test)]
483mod tests {
484    use super::{
485        parse_parameter_value, InvalidConfigError, ParameterTable, ParameterTableDiff,
486        ParameterValue,
487    };
488    use crate::Parameter;
489    use assert_matches::assert_matches;
490    use std::collections::BTreeMap;
491
492    #[track_caller]
493    fn check_parameter_table(
494        base_config: &str,
495        diffs: &[&str],
496        expected: impl IntoIterator<Item = (Parameter, &'static str)>,
497    ) {
498        let mut params: ParameterTable = base_config.parse().unwrap();
499        for diff in diffs {
500            let diff: ParameterTableDiff = diff.parse().unwrap();
501            params.apply_diff(diff).unwrap();
502        }
503
504        let expected_map = BTreeMap::from_iter(expected.into_iter().map(|(param, value)| {
505            (param, {
506                assert!(!value.is_empty(), "omit the parameter in the test instead");
507                parse_parameter_value(
508                    &serde_yaml::from_str(value).expect("Test data has invalid YAML"),
509                )
510                .unwrap()
511            })
512        }));
513
514        assert_eq!(params.parameters, expected_map);
515    }
516
517    #[track_caller]
518    fn check_invalid_parameter_table(base_config: &str, diffs: &[&str]) -> InvalidConfigError {
519        let params = base_config.parse();
520
521        let result = params.and_then(|params: ParameterTable| {
522            diffs.iter().try_fold(params, |mut params, diff| {
523                params.apply_diff(diff.parse()?)?;
524                Ok(params)
525            })
526        });
527
528        match result {
529            Ok(_) => panic!("Input should have parser error"),
530            Err(err) => err,
531        }
532    }
533
534    static BASE_0: &str = r#"
535# Comment line
536registrar_account_id: registrar
537min_allowed_top_level_account_length: 0
538storage_amount_per_byte: 100_000_000_000_000_000_000
539storage_num_bytes_account: 100
540storage_num_extra_bytes_record: 40
541burnt_gas_reward: {
542  numerator: 1_000_000,
543  denominator: 300,
544}
545wasm_storage_read_base: { gas: 50_000_000_000, compute: 100_000_000_000 }
546"#;
547
548    static BASE_1: &str = r#"
549registrar_account_id: registrar
550# Comment line
551min_allowed_top_level_account_length: 0
552
553# Comment line with trailing whitespace # 
554
555# Note the quotes here, they are necessary as otherwise the value can't be parsed by `serde_yaml`
556# due to not fitting into u64 type.
557storage_amount_per_byte: "100000000000000000000"
558storage_num_bytes_account: 100
559storage_num_extra_bytes_record   :   40  
560
561"#;
562
563    static DIFF_0: &str = r#"
564# Comment line
565registrar_account_id: { old: "registrar", new: "unc" }
566min_allowed_top_level_account_length: { old: 32, new: 32_000 }
567wasm_regular_op_cost: { new: 3_856_371 }
568burnt_gas_reward: {
569    old: { numerator: 1_000_000, denominator: 300 },
570    new: { numerator: 2_000_000, denominator: 500 },
571}
572wasm_storage_read_base: {
573    old: { gas: 50_000_000_000, compute: 100_000_000_000 },
574    new: { gas: 50_000_000_000, compute: 200_000_000_000 },
575}
576"#;
577
578    static DIFF_1: &str = r#"
579# Comment line
580registrar_account_id: { old: "unc", new: "registrar" }
581storage_num_extra_bytes_record: { old: 40, new: 77 }
582wasm_regular_op_cost: { old: 3_856_371, new: 0 }
583max_memory_pages: { new: 512 }
584burnt_gas_reward: {
585    old: { numerator: 2_000_000, denominator: 500 },
586    new: { numerator: 3_000_000, denominator: 800 },
587}
588"#;
589
590    // Tests synthetic small example configurations. For tests with "real"
591    // input data, we already have
592    // `test_old_and_new_runtime_config_format_match` in `configs_store.rs`.
593
594    /// Check empty input
595    #[test]
596    fn test_empty_parameter_table() {
597        check_parameter_table("", &[], []);
598    }
599
600    /// Reading reading a normally formatted base parameter file with no diffs
601    #[test]
602    fn test_basic_parameter_table() {
603        check_parameter_table(
604            BASE_0,
605            &[],
606            [
607                (Parameter::RegistrarAccountId, "\"registrar\""),
608                (Parameter::MinAllowedTopLevelAccountLength, "32"),
609                (Parameter::StorageAmountPerByte, "\"100000000000000000000\""),
610                (Parameter::StorageNumBytesAccount, "100"),
611                (Parameter::StorageNumExtraBytesRecord, "40"),
612                (Parameter::BurntGasReward, "{ numerator: 1_000_000, denominator: 300 }"),
613                (
614                    Parameter::WasmStorageReadBase,
615                    "{ gas: 50_000_000_000, compute: 100_000_000_000 }",
616                ),
617            ],
618        );
619    }
620
621    /// Reading reading a slightly funky formatted base parameter file with no diffs
622    #[test]
623    fn test_basic_parameter_table_weird_syntax() {
624        check_parameter_table(
625            BASE_1,
626            &[],
627            [
628                (Parameter::RegistrarAccountId, "\"registrar\""),
629                (Parameter::MinAllowedTopLevelAccountLength, "32"),
630                (Parameter::StorageAmountPerByte, "\"100000000000000000000\""),
631                (Parameter::StorageNumBytesAccount, "100"),
632                (Parameter::StorageNumExtraBytesRecord, "40"),
633            ],
634        );
635    }
636
637    /// Apply one diff
638    #[test]
639    fn test_parameter_table_with_diff() {
640        check_parameter_table(
641            BASE_0,
642            &[DIFF_0],
643            [
644                (Parameter::RegistrarAccountId, "\"unc\""),
645                (Parameter::MinAllowedTopLevelAccountLength, "32000"),
646                (Parameter::StorageAmountPerByte, "\"100000000000000000000\""),
647                (Parameter::StorageNumBytesAccount, "100"),
648                (Parameter::StorageNumExtraBytesRecord, "40"),
649                (Parameter::WasmRegularOpCost, "3856371"),
650                (Parameter::BurntGasReward, "{ numerator: 2_000_000, denominator: 500 }"),
651                (
652                    Parameter::WasmStorageReadBase,
653                    "{ gas: 50_000_000_000, compute: 200_000_000_000 }",
654                ),
655            ],
656        );
657    }
658
659    /// Apply two diffs
660    #[test]
661    fn test_parameter_table_with_diffs() {
662        check_parameter_table(
663            BASE_0,
664            &[DIFF_0, DIFF_1],
665            [
666                (Parameter::RegistrarAccountId, "\"registrar\""),
667                (Parameter::MinAllowedTopLevelAccountLength, "32000"),
668                (Parameter::StorageAmountPerByte, "\"100000000000000000000\""),
669                (Parameter::StorageNumBytesAccount, "100"),
670                (Parameter::StorageNumExtraBytesRecord, "77"),
671                (Parameter::WasmRegularOpCost, "0"),
672                (Parameter::MaxMemoryPages, "512"),
673                (Parameter::BurntGasReward, "{ numerator: 3_000_000, denominator: 800 }"),
674                (
675                    Parameter::WasmStorageReadBase,
676                    "{ gas: 50_000_000_000, compute: 200_000_000_000 }",
677                ),
678            ],
679        );
680    }
681
682    #[test]
683    fn test_parameter_table_with_empty_value() {
684        let diff_with_empty_value = "min_allowed_top_level_account_length: { old: 32 }";
685        check_parameter_table(
686            BASE_0,
687            &[diff_with_empty_value],
688            [
689                (Parameter::RegistrarAccountId, "\"registrar\""),
690                (Parameter::StorageAmountPerByte, "\"100000000000000000000\""),
691                (Parameter::StorageNumBytesAccount, "100"),
692                (Parameter::StorageNumExtraBytesRecord, "40"),
693                (Parameter::BurntGasReward, "{ numerator: 1_000_000, denominator: 300 }"),
694                (
695                    Parameter::WasmStorageReadBase,
696                    "{ gas: 50_000_000_000, compute: 100_000_000_000 }",
697                ),
698            ],
699        );
700    }
701
702    #[test]
703    fn test_parameter_table_invalid_key() {
704        // Key that is not a `Parameter`
705        assert_matches!(
706            check_invalid_parameter_table("invalid_key: 100", &[]),
707            InvalidConfigError::UnknownParameter(_, _)
708        );
709    }
710
711    #[test]
712    fn test_parameter_table_invalid_key_in_diff() {
713        assert_matches!(
714            check_invalid_parameter_table(
715                "wasm_regular_op_cost: 100",
716                &["invalid_key: { new: 100 }"]
717            ),
718            InvalidConfigError::UnknownParameter(_, _)
719        );
720    }
721
722    #[test]
723    fn test_parameter_table_no_key() {
724        assert_matches!(
725            check_invalid_parameter_table(": 100", &[]),
726            InvalidConfigError::InvalidYaml(_)
727        );
728    }
729
730    #[test]
731    fn test_parameter_table_no_key_in_diff() {
732        assert_matches!(
733            check_invalid_parameter_table("wasm_regular_op_cost: 100", &[": 100"]),
734            InvalidConfigError::InvalidYaml(_)
735        );
736    }
737
738    #[test]
739    fn test_parameter_table_wrong_separator() {
740        assert_matches!(
741            check_invalid_parameter_table("wasm_regular_op_cost=100", &[]),
742            InvalidConfigError::InvalidYaml(_)
743        );
744    }
745
746    #[test]
747    fn test_parameter_table_wrong_separator_in_diff() {
748        assert_matches!(
749            check_invalid_parameter_table(
750                "wasm_regular_op_cost: 100",
751                &["wasm_regular_op_cost=100"]
752            ),
753            InvalidConfigError::InvalidYaml(_)
754        );
755    }
756
757    #[test]
758    fn test_parameter_table_wrong_old_value() {
759        assert_matches!(
760            check_invalid_parameter_table(
761                "min_allowed_top_level_account_length: 3_200_000_000",
762                &["min_allowed_top_level_account_length: { old: 3_200_000, new: 1_600_000 }"]
763            ),
764            InvalidConfigError::WrongOldValue(
765                Parameter::MinAllowedTopLevelAccountLength,
766                expected,
767                found
768            ) => {
769                assert_eq!(expected, ParameterValue::U64(3200000000));
770                assert_eq!(found, ParameterValue::U64(3200000));
771            }
772        );
773    }
774
775    #[test]
776    fn test_parameter_table_no_old_value() {
777        assert_matches!(
778            check_invalid_parameter_table(
779                "min_allowed_top_level_account_length: 3_200_000_000",
780                &["min_allowed_top_level_account_length: { new: 1_600_000 }"]
781            ),
782            InvalidConfigError::OldValueExists(Parameter::MinAllowedTopLevelAccountLength, expected) => {
783                assert_eq!(expected, ParameterValue::U64(3200000000));
784            }
785        );
786    }
787
788    #[test]
789    fn test_parameter_table_old_parameter_undefined() {
790        assert_matches!(
791            check_invalid_parameter_table(
792                "min_allowed_top_level_account_length: 3_200_000_000",
793                &["wasm_regular_op_cost: { old: 3_200_000, new: 1_600_000 }"]
794            ),
795            InvalidConfigError::NoOldValueExists(Parameter::WasmRegularOpCost, found) => {
796                assert_eq!(found, ParameterValue::U64(3200000));
797            }
798        );
799    }
800
801    #[test]
802    fn test_parameter_table_yaml_map() {
803        let params: ParameterTable = BASE_0.parse().unwrap();
804        let yaml = params.yaml_map(
805            [
806                Parameter::RegistrarAccountId,
807                Parameter::MinAllowedTopLevelAccountLength,
808                Parameter::StorageAmountPerByte,
809                Parameter::StorageNumBytesAccount,
810                Parameter::StorageNumExtraBytesRecord,
811                Parameter::BurntGasReward,
812                Parameter::WasmStorageReadBase,
813            ]
814            .iter(),
815        );
816        assert_eq!(
817            yaml,
818            serde_yaml::to_value(
819                params
820                    .parameters
821                    .iter()
822                    .map(|(key, value)| (key.to_string(), value))
823                    .collect::<BTreeMap<_, _>>()
824            )
825            .unwrap()
826        );
827    }
828}