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                refund_gas_price_changes: params.get(Parameter::RefundGasPriceChanges)?,
302                gas_refund_penalty: params.get(Parameter::GasRefundPenalty)?,
303                min_gas_refund_penalty: params.get(Parameter::MinGasRefundPenalty)?,
304                storage_usage_config: StorageUsageConfig {
305                    storage_amount_per_byte: params.get(Parameter::StorageAmountPerByte)?,
306                    num_bytes_account: params.get(Parameter::StorageNumBytesAccount)?,
307                    num_extra_bytes_record: params.get(Parameter::StorageNumExtraBytesRecord)?,
308                    global_contract_storage_amount_per_byte: params
309                        .get(Parameter::GlobalContractStorageAmountPerByte)?,
310                },
311            }),
312            wasm_config: Arc::new(Config {
313                ext_costs: ExtCostsConfig {
314                    costs: enum_map::enum_map! {
315                        cost => params.get(cost.param())?
316                    },
317                },
318                vm_kind: params.get(Parameter::VmKind)?,
319                grow_mem_cost: params.get(Parameter::WasmGrowMemCost)?,
320                regular_op_cost: params.get(Parameter::WasmRegularOpCost)?,
321                discard_custom_sections: params.get(Parameter::DiscardCustomSections)?,
322                saturating_float_to_int: params.get(Parameter::SaturatingFloatToInt)?,
323                limit_config: serde_yaml::from_value(params.yaml_map(Parameter::vm_limits()))
324                    .map_err(InvalidConfigError::InvalidYaml)?,
325                fix_contract_loading_cost: params.get(Parameter::FixContractLoadingCost)?,
326                storage_get_mode: match params.get(Parameter::FlatStorageReads)? {
327                    true => StorageGetMode::FlatStorage,
328                    false => StorageGetMode::Trie,
329                },
330                implicit_account_creation: params.get(Parameter::ImplicitAccountCreation)?,
331                eth_implicit_accounts: params.get(Parameter::EthImplicitAccounts)?,
332                global_contract_host_fns: params.get(Parameter::GlobalContractHostFns)?,
333            }),
334            account_creation_config: AccountCreationConfig {
335                min_allowed_top_level_account_length: params
336                    .get(Parameter::MinAllowedTopLevelAccountLength)?,
337                registrar_account_id: params.get(Parameter::RegistrarAccountId)?,
338            },
339            congestion_control_config: get_congestion_control_config(params)?,
340            witness_config: WitnessConfig {
341                main_storage_proof_size_soft_limit: params
342                    .get(Parameter::MainStorageProofSizeSoftLimit)?,
343                combined_transactions_size_limit: params
344                    .get(Parameter::CombinedTransactionsSizeLimit)?,
345                new_transactions_validation_state_size_soft_limit: params
346                    .get(Parameter::NewTransactionsValidationStateSizeSoftLimit)?,
347            },
348            bandwidth_scheduler_config: BandwidthSchedulerConfig {
349                max_shard_bandwidth: params.get(Parameter::MaxShardBandwidth)?,
350                max_single_grant: params.get(Parameter::MaxSingleGrant)?,
351                max_allowance: params.get(Parameter::MaxAllowance)?,
352                max_base_bandwidth: params.get(Parameter::MaxBaseBandwidth)?,
353            },
354            use_state_stored_receipt: params.get(Parameter::UseStateStoredReceipt)?,
355        })
356    }
357}
358
359fn get_congestion_control_config(
360    params: &ParameterTable,
361) -> Result<CongestionControlConfig, <RuntimeConfig as TryFrom<&ParameterTable>>::Error> {
362    let congestion_control_config = CongestionControlConfig {
363        max_congestion_incoming_gas: params.get(Parameter::MaxCongestionIncomingGas)?,
364        max_congestion_outgoing_gas: params.get(Parameter::MaxCongestionOutgoingGas)?,
365        max_congestion_memory_consumption: params.get(Parameter::MaxCongestionMemoryConsumption)?,
366        max_congestion_missed_chunks: params.get(Parameter::MaxCongestionMissedChunks)?,
367        max_outgoing_gas: params.get(Parameter::MaxOutgoingGas)?,
368        min_outgoing_gas: params.get(Parameter::MinOutgoingGas)?,
369        allowed_shard_outgoing_gas: params.get(Parameter::AllowedShardOutgoingGas)?,
370        max_tx_gas: params.get(Parameter::MaxTxGas)?,
371        min_tx_gas: params.get(Parameter::MinTxGas)?,
372        reject_tx_congestion_threshold: {
373            let rational: Rational32 = params.get(Parameter::RejectTxCongestionThreshold)?;
374            *rational.numer() as f64 / *rational.denom() as f64
375        },
376        outgoing_receipts_usual_size_limit: params
377            .get(Parameter::OutgoingReceiptsUsualSizeLimit)?,
378        outgoing_receipts_big_size_limit: params.get(Parameter::OutgoingReceiptsBigSizeLimit)?,
379    };
380    Ok(congestion_control_config)
381}
382
383impl ParameterTable {
384    pub(crate) fn apply_diff(
385        &mut self,
386        diff: ParameterTableDiff,
387    ) -> Result<(), InvalidConfigError> {
388        for (key, (before, after)) in diff.parameters {
389            let old_value = self.parameters.get(&key);
390            if old_value != before.as_ref() {
391                if old_value.is_none() {
392                    return Err(InvalidConfigError::NoOldValueExists(key, before.unwrap()));
393                }
394                if before.is_none() {
395                    return Err(InvalidConfigError::OldValueExists(
396                        key,
397                        old_value.unwrap().clone(),
398                    ));
399                }
400                return Err(InvalidConfigError::WrongOldValue(
401                    key,
402                    old_value.unwrap().clone(),
403                    before.unwrap(),
404                ));
405            }
406
407            if let Some(new_value) = after {
408                self.parameters.insert(key, new_value);
409            } else {
410                self.parameters.remove(&key);
411            }
412        }
413        Ok(())
414    }
415
416    fn yaml_map(&self, params: impl Iterator<Item = &'static Parameter>) -> serde_yaml::Value {
417        // All parameter values can be serialized as YAML, so we don't ever expect this to fail.
418        serde_yaml::to_value(
419            params
420                .filter_map(|param| Some((param.to_string(), self.parameters.get(param)?)))
421                .collect::<BTreeMap<_, _>>(),
422        )
423        .expect("failed to convert parameter values to YAML")
424    }
425
426    /// Read and parse a typed parameter from the `ParameterTable`.
427    fn get<'a, T>(&'a self, key: Parameter) -> Result<T, InvalidConfigError>
428    where
429        T: TryFrom<&'a ParameterValue, Error = ValueConversionError>,
430    {
431        let value = self.parameters.get(&key).ok_or(InvalidConfigError::MissingParameter(key))?;
432        value.try_into().map_err(|err| InvalidConfigError::ValueConversionError(err, key))
433    }
434
435    /// Access action fee by `ActionCosts`.
436    fn get_fee(&self, cost: ActionCosts) -> Result<Fee, InvalidConfigError> {
437        let key: Parameter = format!("{}", FeeParameter::from(cost)).parse().unwrap();
438        self.get(key)
439    }
440}
441
442/// Represents values supported by parameter diff config.
443#[derive(serde::Deserialize, Clone, Debug)]
444struct ParameterDiffConfigValue {
445    old: Option<serde_yaml::Value>,
446    new: Option<serde_yaml::Value>,
447}
448
449impl std::str::FromStr for ParameterTableDiff {
450    type Err = InvalidConfigError;
451    fn from_str(arg: &str) -> Result<ParameterTableDiff, InvalidConfigError> {
452        let yaml_map: BTreeMap<String, ParameterDiffConfigValue> =
453            serde_yaml::from_str(arg).map_err(|err| InvalidConfigError::InvalidYaml(err))?;
454
455        let parameters = yaml_map
456            .iter()
457            .map(|(key, value)| {
458                let typed_key: Parameter = key
459                    .parse()
460                    .map_err(|err| InvalidConfigError::UnknownParameter(err, key.to_owned()))?;
461
462                let old_value =
463                    if let Some(s) = &value.old { Some(parse_parameter_value(s)?) } else { None };
464
465                let new_value =
466                    if let Some(s) = &value.new { Some(parse_parameter_value(s)?) } else { None };
467
468                Ok((typed_key, (old_value, new_value)))
469            })
470            .collect::<Result<BTreeMap<_, _>, _>>()?;
471        Ok(ParameterTableDiff { parameters })
472    }
473}
474
475/// Parses a value from YAML to a more restricted type of parameter values.
476fn parse_parameter_value(value: &serde_yaml::Value) -> Result<ParameterValue, InvalidConfigError> {
477    Ok(serde_yaml::from_value(canonicalize_yaml_value(value)?)
478        .map_err(|err| InvalidConfigError::InvalidYaml(err))?)
479}
480
481/// Recursively canonicalizes values inside of the YAML structure.
482fn canonicalize_yaml_value(
483    value: &serde_yaml::Value,
484) -> Result<serde_yaml::Value, InvalidConfigError> {
485    Ok(match value {
486        serde_yaml::Value::String(s) => canonicalize_yaml_string(s)?,
487        serde_yaml::Value::Mapping(m) => serde_yaml::Value::Mapping(
488            m.iter()
489                .map(|(key, value)| {
490                    let canonical_value = canonicalize_yaml_value(value)?;
491                    Ok((key.clone(), canonical_value))
492                })
493                .collect::<Result<_, _>>()?,
494        ),
495        _ => value.clone(),
496    })
497}
498
499/// Parses a value from the custom format for runtime parameter definitions.
500///
501/// A value can be a positive integer or a string, with or without quotes.
502/// Integers can use underlines as separators (for readability).
503///
504/// The main purpose of this function is to add support for integers with underscore digit
505/// separators which we use in the config but are not supported in YAML.
506fn canonicalize_yaml_string(value: &str) -> Result<serde_yaml::Value, InvalidConfigError> {
507    if value.is_empty() {
508        return Ok(serde_yaml::Value::Null);
509    }
510    if value.bytes().all(|c| c.is_ascii_digit() || c == '_' as u8) {
511        let mut raw_number = value.to_owned();
512        raw_number.retain(char::is_numeric);
513        // We do not have "arbitrary_precision" serde feature enabled, thus we
514        // can only store up to `u64::MAX`, which is `18446744073709551615` and
515        // has 20 characters.
516        if raw_number.len() < 20 {
517            serde_yaml::from_str(&raw_number)
518                .map_err(|err| InvalidConfigError::ValueParseError(err, value.to_owned()))
519        } else {
520            Ok(serde_yaml::Value::String(raw_number))
521        }
522    } else {
523        Ok(serde_yaml::Value::String(value.to_owned()))
524    }
525}
526
527#[cfg(test)]
528mod tests {
529    use super::{
530        InvalidConfigError, ParameterTable, ParameterTableDiff, ParameterValue,
531        parse_parameter_value,
532    };
533    use crate::Parameter;
534    use assert_matches::assert_matches;
535    use std::collections::BTreeMap;
536
537    #[track_caller]
538    fn check_parameter_table(
539        base_config: &str,
540        diffs: &[&str],
541        expected: impl IntoIterator<Item = (Parameter, &'static str)>,
542    ) {
543        let mut params: ParameterTable = base_config.parse().unwrap();
544        for diff in diffs {
545            let diff: ParameterTableDiff = diff.parse().unwrap();
546            params.apply_diff(diff).unwrap();
547        }
548
549        let expected_map = BTreeMap::from_iter(expected.into_iter().map(|(param, value)| {
550            (param, {
551                assert!(!value.is_empty(), "omit the parameter in the test instead");
552                parse_parameter_value(
553                    &serde_yaml::from_str(value).expect("Test data has invalid YAML"),
554                )
555                .unwrap()
556            })
557        }));
558
559        assert_eq!(params.parameters, expected_map);
560    }
561
562    #[track_caller]
563    fn check_invalid_parameter_table(base_config: &str, diffs: &[&str]) -> InvalidConfigError {
564        let params = base_config.parse();
565
566        let result = params.and_then(|params: ParameterTable| {
567            diffs.iter().try_fold(params, |mut params, diff| {
568                params.apply_diff(diff.parse()?)?;
569                Ok(params)
570            })
571        });
572
573        match result {
574            Ok(_) => panic!("Input should have parser error"),
575            Err(err) => err,
576        }
577    }
578
579    static BASE_0: &str = include_str!("fixture_base_0.yml");
580    static BASE_1: &str = include_str!("fixture_base_1.yml");
581    static DIFF_0: &str = include_str!("fixture_diff_0.yml");
582    static DIFF_1: &str = include_str!("fixture_diff_1.yml");
583
584    // Tests synthetic small example configurations. For tests with "real"
585    // input data, we already have
586    // `test_old_and_new_runtime_config_format_match` in `configs_store.rs`.
587
588    /// Check empty input
589    #[test]
590    fn test_empty_parameter_table() {
591        check_parameter_table("", &[], []);
592    }
593
594    /// Reading a normally formatted base parameter file with no diffs
595    #[test]
596    fn test_basic_parameter_table() {
597        check_parameter_table(
598            BASE_0,
599            &[],
600            [
601                (Parameter::RegistrarAccountId, "\"registrar\""),
602                (Parameter::MinAllowedTopLevelAccountLength, "32"),
603                (Parameter::StorageAmountPerByte, "\"100000000000000000000\""),
604                (Parameter::StorageNumBytesAccount, "100"),
605                (Parameter::StorageNumExtraBytesRecord, "40"),
606                (Parameter::BurntGasReward, "{ numerator: 1_000_000, denominator: 300 }"),
607                (
608                    Parameter::WasmStorageReadBase,
609                    "{ gas: 50_000_000_000, compute: 100_000_000_000 }",
610                ),
611            ],
612        );
613    }
614
615    /// Reading a slightly funky formatted base parameter file with no diffs
616    #[test]
617    fn test_basic_parameter_table_weird_syntax() {
618        check_parameter_table(
619            BASE_1,
620            &[],
621            [
622                (Parameter::RegistrarAccountId, "\"registrar\""),
623                (Parameter::MinAllowedTopLevelAccountLength, "32"),
624                (Parameter::StorageAmountPerByte, "\"100000000000000000000\""),
625                (Parameter::StorageNumBytesAccount, "100"),
626                (Parameter::StorageNumExtraBytesRecord, "40"),
627            ],
628        );
629    }
630
631    /// Apply one diff
632    #[test]
633    fn test_parameter_table_with_diff() {
634        check_parameter_table(
635            BASE_0,
636            &[DIFF_0],
637            [
638                (Parameter::RegistrarAccountId, "\"near\""),
639                (Parameter::MinAllowedTopLevelAccountLength, "32000"),
640                (Parameter::StorageAmountPerByte, "\"100000000000000000000\""),
641                (Parameter::StorageNumBytesAccount, "100"),
642                (Parameter::StorageNumExtraBytesRecord, "40"),
643                (Parameter::WasmRegularOpCost, "3856371"),
644                (Parameter::BurntGasReward, "{ numerator: 2_000_000, denominator: 500 }"),
645                (
646                    Parameter::WasmStorageReadBase,
647                    "{ gas: 50_000_000_000, compute: 200_000_000_000 }",
648                ),
649            ],
650        );
651    }
652
653    /// Apply two diffs
654    #[test]
655    fn test_parameter_table_with_diffs() {
656        check_parameter_table(
657            BASE_0,
658            &[DIFF_0, DIFF_1],
659            [
660                (Parameter::RegistrarAccountId, "\"registrar\""),
661                (Parameter::MinAllowedTopLevelAccountLength, "32000"),
662                (Parameter::StorageAmountPerByte, "\"100000000000000000000\""),
663                (Parameter::StorageNumBytesAccount, "100"),
664                (Parameter::StorageNumExtraBytesRecord, "77"),
665                (Parameter::WasmRegularOpCost, "0"),
666                (Parameter::MaxMemoryPages, "512"),
667                (Parameter::BurntGasReward, "{ numerator: 3_000_000, denominator: 800 }"),
668                (
669                    Parameter::WasmStorageReadBase,
670                    "{ gas: 50_000_000_000, compute: 200_000_000_000 }",
671                ),
672            ],
673        );
674    }
675
676    #[test]
677    fn test_parameter_table_with_empty_value() {
678        let diff_with_empty_value = "min_allowed_top_level_account_length: { old: 32 }";
679        check_parameter_table(
680            BASE_0,
681            &[diff_with_empty_value],
682            [
683                (Parameter::RegistrarAccountId, "\"registrar\""),
684                (Parameter::StorageAmountPerByte, "\"100000000000000000000\""),
685                (Parameter::StorageNumBytesAccount, "100"),
686                (Parameter::StorageNumExtraBytesRecord, "40"),
687                (Parameter::BurntGasReward, "{ numerator: 1_000_000, denominator: 300 }"),
688                (
689                    Parameter::WasmStorageReadBase,
690                    "{ gas: 50_000_000_000, compute: 100_000_000_000 }",
691                ),
692            ],
693        );
694    }
695
696    #[test]
697    fn test_parameter_table_invalid_key() {
698        // Key that is not a `Parameter`
699        assert_matches!(
700            check_invalid_parameter_table("invalid_key: 100", &[]),
701            InvalidConfigError::UnknownParameter(_, _)
702        );
703    }
704
705    #[test]
706    fn test_parameter_table_invalid_key_in_diff() {
707        assert_matches!(
708            check_invalid_parameter_table(
709                "wasm_regular_op_cost: 100",
710                &["invalid_key: { new: 100 }"]
711            ),
712            InvalidConfigError::UnknownParameter(_, _)
713        );
714    }
715
716    #[test]
717    fn test_parameter_table_no_key() {
718        assert_matches!(
719            check_invalid_parameter_table(": 100", &[]),
720            InvalidConfigError::InvalidYaml(_)
721        );
722    }
723
724    #[test]
725    fn test_parameter_table_no_key_in_diff() {
726        assert_matches!(
727            check_invalid_parameter_table("wasm_regular_op_cost: 100", &[": 100"]),
728            InvalidConfigError::InvalidYaml(_)
729        );
730    }
731
732    #[test]
733    fn test_parameter_table_wrong_separator() {
734        assert_matches!(
735            check_invalid_parameter_table("wasm_regular_op_cost=100", &[]),
736            InvalidConfigError::InvalidYaml(_)
737        );
738    }
739
740    #[test]
741    fn test_parameter_table_wrong_separator_in_diff() {
742        assert_matches!(
743            check_invalid_parameter_table(
744                "wasm_regular_op_cost: 100",
745                &["wasm_regular_op_cost=100"]
746            ),
747            InvalidConfigError::InvalidYaml(_)
748        );
749    }
750
751    #[test]
752    fn test_parameter_table_wrong_old_value() {
753        assert_matches!(
754            check_invalid_parameter_table(
755                "min_allowed_top_level_account_length: 3_200_000_000",
756                &["min_allowed_top_level_account_length: { old: 3_200_000, new: 1_600_000 }"]
757            ),
758            InvalidConfigError::WrongOldValue(
759                Parameter::MinAllowedTopLevelAccountLength,
760                expected,
761                found
762            ) => {
763                assert_eq!(expected, ParameterValue::U64(3200000000));
764                assert_eq!(found, ParameterValue::U64(3200000));
765            }
766        );
767    }
768
769    #[test]
770    fn test_parameter_table_no_old_value() {
771        assert_matches!(
772            check_invalid_parameter_table(
773                "min_allowed_top_level_account_length: 3_200_000_000",
774                &["min_allowed_top_level_account_length: { new: 1_600_000 }"]
775            ),
776            InvalidConfigError::OldValueExists(Parameter::MinAllowedTopLevelAccountLength, expected) => {
777                assert_eq!(expected, ParameterValue::U64(3200000000));
778            }
779        );
780    }
781
782    #[test]
783    fn test_parameter_table_old_parameter_undefined() {
784        assert_matches!(
785            check_invalid_parameter_table(
786                "min_allowed_top_level_account_length: 3_200_000_000",
787                &["wasm_regular_op_cost: { old: 3_200_000, new: 1_600_000 }"]
788            ),
789            InvalidConfigError::NoOldValueExists(Parameter::WasmRegularOpCost, found) => {
790                assert_eq!(found, ParameterValue::U64(3200000));
791            }
792        );
793    }
794
795    #[test]
796    fn test_parameter_table_yaml_map() {
797        let params: ParameterTable = BASE_0.parse().unwrap();
798        let yaml = params.yaml_map(
799            [
800                Parameter::RegistrarAccountId,
801                Parameter::MinAllowedTopLevelAccountLength,
802                Parameter::StorageAmountPerByte,
803                Parameter::StorageNumBytesAccount,
804                Parameter::StorageNumExtraBytesRecord,
805                Parameter::BurntGasReward,
806                Parameter::WasmStorageReadBase,
807            ]
808            .iter(),
809        );
810        assert_eq!(
811            yaml,
812            serde_yaml::to_value(
813                params
814                    .parameters
815                    .iter()
816                    .map(|(key, value)| (key.to_string(), value))
817                    .collect::<BTreeMap<_, _>>()
818            )
819            .unwrap()
820        );
821    }
822}