diff --git a/zebra-chain/src/parameters/network.rs b/zebra-chain/src/parameters/network.rs index 47bcf68e8..e346e28e6 100644 --- a/zebra-chain/src/parameters/network.rs +++ b/zebra-chain/src/parameters/network.rs @@ -138,15 +138,13 @@ impl fmt::Display for NetworkKind { } } -impl From<&Network> for &'static str { - fn from(network: &Network) -> &'static str { +impl<'a> From<&'a Network> for &'a str { + fn from(network: &'a Network) -> &'a str { match network { Network::Mainnet => "Mainnet", // TODO: - // - Add a `name` field to use here instead of checking `is_default_testnet()` // - zcashd calls the Regtest cache dir 'regtest' (#8327). - Network::Testnet(params) if params.is_default_testnet() => "Testnet", - Network::Testnet(_params) => "UnknownTestnet", + Network::Testnet(params) => params.network_name(), } } } diff --git a/zebra-chain/src/parameters/network/testnet.rs b/zebra-chain/src/parameters/network/testnet.rs index 1a1b47001..7cc936927 100644 --- a/zebra-chain/src/parameters/network/testnet.rs +++ b/zebra-chain/src/parameters/network/testnet.rs @@ -1,5 +1,5 @@ //! Types and implementation for Testnet consensus parameters -use std::collections::BTreeMap; +use std::{collections::BTreeMap, fmt}; use zcash_primitives::constants as zp_constants; @@ -11,6 +11,19 @@ use crate::{ }, }; +/// Reserved network names that should not be allowed for configured Testnets. +pub const RESERVED_NETWORK_NAMES: [&str; 6] = [ + "Mainnet", + "Testnet", + "Regtest", + "MainnetKind", + "TestnetKind", + "RegtestKind", +]; + +/// Maximum length for a configured network name. +pub const MAX_NETWORK_NAME_LENGTH: usize = 30; + /// Configurable activation heights for Regtest and configured Testnets. #[derive(Deserialize, Default)] #[serde(rename_all = "PascalCase")] @@ -35,6 +48,8 @@ pub struct ConfiguredActivationHeights { /// Builder for the [`Parameters`] struct. #[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)] pub struct ParametersBuilder { + /// The name of this network to be used by the `Display` trait impl. + network_name: String, /// The network upgrade activation heights for this network, see [`Parameters::activation_heights`] for more details. activation_heights: BTreeMap, /// Sapling extended spending key human-readable prefix for this network @@ -48,6 +63,7 @@ pub struct ParametersBuilder { impl Default for ParametersBuilder { fn default() -> Self { Self { + network_name: "UnknownTestnet".to_string(), // # Correctness // // `Genesis` network upgrade activation height must always be 0 @@ -69,9 +85,33 @@ impl Default for ParametersBuilder { } impl ParametersBuilder { + /// Sets the network name to be used in the [`Parameters`] being built. + pub fn with_network_name(mut self, network_name: impl fmt::Display) -> Self { + self.network_name = network_name.to_string(); + + assert!( + !RESERVED_NETWORK_NAMES.contains(&self.network_name.as_str()), + "cannot use reserved network name '{network_name}' as configured Testnet name, reserved names: {RESERVED_NETWORK_NAMES:?}" + ); + + assert!( + self.network_name.len() <= MAX_NETWORK_NAME_LENGTH, + "network name {network_name} is too long, must be {MAX_NETWORK_NAME_LENGTH} characters or less" + ); + + assert!( + self.network_name + .chars() + .all(|x| x.is_alphanumeric() || x == '_'), + "network name must include only alphanumeric characters or '_'" + ); + + self + } + /// Checks that the provided network upgrade activation heights are in the correct order, then /// sets them as the new network upgrade activation heights. - pub fn activation_heights( + pub fn with_activation_heights( mut self, ConfiguredActivationHeights { // TODO: Find out if `BeforeOverwinter` is required at Height(1), allow for @@ -99,7 +139,6 @@ impl ParametersBuilder { .chain(heartwood.into_iter().map(|h| (h, Heartwood))) .chain(canopy.into_iter().map(|h| (h, Canopy))) .chain(nu5.into_iter().map(|h| (h, Nu5))) - .filter(|&(_, nu)| nu != NetworkUpgrade::BeforeOverwinter) .map(|(h, nu)| (h.try_into().expect("activation height must be valid"), nu)) .collect(); @@ -136,12 +175,14 @@ impl ParametersBuilder { /// Converts the builder to a [`Parameters`] struct pub fn finish(self) -> Parameters { let Self { + network_name, activation_heights, hrp_sapling_extended_spending_key, hrp_sapling_extended_full_viewing_key, hrp_sapling_payment_address, } = self; Parameters { + network_name, activation_heights, hrp_sapling_extended_spending_key, hrp_sapling_extended_full_viewing_key, @@ -158,6 +199,8 @@ impl ParametersBuilder { /// Network consensus parameters for test networks such as Regtest and the default Testnet. #[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)] pub struct Parameters { + /// The name of this network to be used by the `Display` trait impl. + network_name: String, /// The network upgrade activation heights for this network. /// /// Note: This value is ignored by `Network::activation_list()` when `zebra-chain` is @@ -176,13 +219,9 @@ impl Default for Parameters { /// Returns an instance of the default public testnet [`Parameters`]. fn default() -> Self { Self { + network_name: "Testnet".to_string(), activation_heights: TESTNET_ACTIVATION_HEIGHTS.iter().cloned().collect(), - hrp_sapling_extended_spending_key: - zp_constants::testnet::HRP_SAPLING_EXTENDED_SPENDING_KEY.to_string(), - hrp_sapling_extended_full_viewing_key: - zp_constants::testnet::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY.to_string(), - hrp_sapling_payment_address: zp_constants::testnet::HRP_SAPLING_PAYMENT_ADDRESS - .to_string(), + ..Self::build().finish() } } } @@ -198,6 +237,11 @@ impl Parameters { self == &Self::default() } + /// Returns the network name + pub fn network_name(&self) -> &str { + &self.network_name + } + /// Returns the network upgrade activation heights pub fn activation_heights(&self) -> &BTreeMap { &self.activation_heights diff --git a/zebra-chain/src/parameters/network/tests/vectors.rs b/zebra-chain/src/parameters/network/tests/vectors.rs index b6fca27c2..1d619aa5e 100644 --- a/zebra-chain/src/parameters/network/tests/vectors.rs +++ b/zebra-chain/src/parameters/network/tests/vectors.rs @@ -5,7 +5,9 @@ use zcash_primitives::consensus::{self as zp_consensus, Parameters}; use crate::{ block::Height, parameters::{ - testnet::{self, ConfiguredActivationHeights}, + testnet::{ + self, ConfiguredActivationHeights, MAX_NETWORK_NAME_LENGTH, RESERVED_NETWORK_NAMES, + }, Network, NetworkUpgrade, NETWORK_UPGRADES_IN_ORDER, }, }; @@ -97,7 +99,7 @@ fn check_parameters_impl() { fn activates_network_upgrades_correctly() { let expected_activation_height = 1; let network = testnet::Parameters::build() - .activation_heights(ConfiguredActivationHeights { + .with_activation_heights(ConfiguredActivationHeights { nu5: Some(expected_activation_height), ..Default::default() }) @@ -125,3 +127,58 @@ fn activates_network_upgrades_correctly() { ); } } + +/// Checks that configured testnet names are validated and used correctly. +#[test] +fn check_network_name() { + // Sets a no-op panic hook to avoid long output. + std::panic::set_hook(Box::new(|_| {})); + + // Checks that reserved network names cannot be used for configured testnets. + for reserved_network_name in RESERVED_NETWORK_NAMES { + std::panic::catch_unwind(|| { + testnet::Parameters::build().with_network_name(reserved_network_name) + }) + .expect_err("should panic when attempting to set network name as a reserved name"); + } + + // Check that max length is enforced, and that network names may only contain alphanumeric characters and '_'. + for invalid_network_name in [ + "a".repeat(MAX_NETWORK_NAME_LENGTH + 1), + "!!!!non-alphanumeric-name".to_string(), + ] { + std::panic::catch_unwind(|| { + testnet::Parameters::build().with_network_name(invalid_network_name) + }) + .expect_err("should panic when setting network name that's too long or contains non-alphanumeric characters (except '_')"); + } + + // Checks that network names are displayed correctly + assert_eq!( + Network::new_default_testnet().to_string(), + "Testnet", + "default testnet should be displayed as 'Testnet'" + ); + assert_eq!( + Network::Mainnet.to_string(), + "Mainnet", + "Mainnet should be displayed as 'Mainnet'" + ); + + // TODO: Check Regtest + + // Check that network name can contain alphanumeric characters and '_'. + let expected_name = "ConfiguredTestnet_1"; + let network = testnet::Parameters::build() + // Check that network name can contain `MAX_NETWORK_NAME_LENGTH` characters + .with_network_name("a".repeat(MAX_NETWORK_NAME_LENGTH)) + .with_network_name(expected_name) + .to_network(); + + // Check that configured network name is displayed + assert_eq!( + network.to_string(), + expected_name, + "network must be displayed as configured network name" + ); +} diff --git a/zebra-network/src/config.rs b/zebra-network/src/config.rs index fa1e20568..e32f6c138 100644 --- a/zebra-network/src/config.rs +++ b/zebra-network/src/config.rs @@ -629,8 +629,9 @@ impl<'de> Deserialize<'de> for Config { { #[derive(Deserialize)] struct DTestnetParameters { + network_name: Option, #[serde(default)] - pub(super) activation_heights: ConfiguredActivationHeights, + activation_heights: ConfiguredActivationHeights, } #[derive(Deserialize)] @@ -678,15 +679,25 @@ impl<'de> Deserialize<'de> for Config { } = DConfig::deserialize(deserializer)?; // TODO: Panic here if the initial testnet peers are the default initial testnet peers. - let network = if let Some(DTestnetParameters { activation_heights }) = testnet_parameters { + let network = if let Some(DTestnetParameters { + network_name, + activation_heights, + }) = testnet_parameters + { assert_eq!( network_kind, NetworkKind::Testnet, "set network to 'Testnet' to use configured testnet parameters" ); - testnet::Parameters::build() - .activation_heights(activation_heights) + let mut params_builder = testnet::Parameters::build(); + + if let Some(network_name) = network_name { + params_builder = params_builder.with_network_name(network_name) + } + + params_builder + .with_activation_heights(activation_heights) .to_network() } else { // Convert to default `Network` for a `NetworkKind` if there are no testnet parameters. diff --git a/zebrad/tests/common/configs/v1.7.0.toml b/zebrad/tests/common/configs/v1.7.0.toml index 58aed232f..00438213d 100644 --- a/zebrad/tests/common/configs/v1.7.0.toml +++ b/zebrad/tests/common/configs/v1.7.0.toml @@ -60,6 +60,9 @@ max_connections_per_ip = 1 network = "Testnet" peerset_initial_target_size = 25 +[network.testnet_parameters] +network_name = "ConfiguredTestnet_1" + [network.testnet_parameters.activation_heights] BeforeOverwinter = 1 Overwinter = 207_500