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