near_parameters/
parameter_table.rs

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