diff --git a/zebra-chain/src/parameters/network.rs b/zebra-chain/src/parameters/network.rs index e346e28e6..94fd30150 100644 --- a/zebra-chain/src/parameters/network.rs +++ b/zebra-chain/src/parameters/network.rs @@ -11,6 +11,8 @@ use crate::{ parameters::NetworkUpgrade, }; +use self::testnet::ConfiguredActivationHeights; + pub mod testnet; #[cfg(test)] @@ -142,8 +144,6 @@ impl<'a> From<&'a Network> for &'a str { fn from(network: &'a Network) -> &'a str { match network { Network::Mainnet => "Mainnet", - // TODO: - // - zcashd calls the Regtest cache dir 'regtest' (#8327). Network::Testnet(params) => params.network_name(), } } @@ -166,6 +166,11 @@ impl Network { Self::Testnet(Arc::new(params)) } + /// Creates a new [`Network::Testnet`] with `Regtest` parameters and the provided network upgrade activation heights. + pub fn new_regtest(activation_heights: ConfiguredActivationHeights) -> Self { + Self::new_configured_testnet(testnet::Parameters::new_regtest(activation_heights)) + } + /// Returns true if the network is the default Testnet, or false otherwise. pub fn is_default_testnet(&self) -> bool { if let Self::Testnet(params) = self { @@ -175,6 +180,15 @@ impl Network { } } + /// Returns true if the network is Regtest, or false otherwise. + pub fn is_regtest(&self) -> bool { + if let Self::Testnet(params) = self { + params.is_regtest() + } else { + false + } + } + /// Returns the [`NetworkKind`] for this network. pub fn kind(&self) -> NetworkKind { match self { diff --git a/zebra-chain/src/parameters/network/testnet.rs b/zebra-chain/src/parameters/network/testnet.rs index 7cc936927..295f51813 100644 --- a/zebra-chain/src/parameters/network/testnet.rs +++ b/zebra-chain/src/parameters/network/testnet.rs @@ -24,6 +24,9 @@ pub const RESERVED_NETWORK_NAMES: [&str; 6] = [ /// Maximum length for a configured network name. pub const MAX_NETWORK_NAME_LENGTH: usize = 30; +/// Maximum length for a configured human-readable prefix. +pub const MAX_HRP_LENGTH: usize = 30; + /// Configurable activation heights for Regtest and configured Testnets. #[derive(Deserialize, Default)] #[serde(rename_all = "PascalCase")] @@ -67,13 +70,7 @@ impl Default for ParametersBuilder { // # Correctness // // `Genesis` network upgrade activation height must always be 0 - activation_heights: [ - (Height(0), NetworkUpgrade::Genesis), - // TODO: Find out if `BeforeOverwinter` must always be active at Height(1), remove it here if it's not required. - (Height(1), NetworkUpgrade::BeforeOverwinter), - ] - .into_iter() - .collect(), + 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: @@ -109,6 +106,44 @@ impl ParametersBuilder { self } + /// Checks that the provided Sapling human-readable prefixes (HRPs) are valid and unique, then + /// sets the Sapling HRPs to be used in the [`Parameters`] being built. + pub fn with_sapling_hrps( + mut self, + hrp_sapling_extended_spending_key: impl fmt::Display, + hrp_sapling_extended_full_viewing_key: impl fmt::Display, + hrp_sapling_payment_address: impl fmt::Display, + ) -> Self { + self.hrp_sapling_extended_spending_key = hrp_sapling_extended_spending_key.to_string(); + self.hrp_sapling_extended_full_viewing_key = + hrp_sapling_extended_full_viewing_key.to_string(); + self.hrp_sapling_payment_address = hrp_sapling_payment_address.to_string(); + + let sapling_hrps = [ + &self.hrp_sapling_extended_spending_key, + &self.hrp_sapling_extended_full_viewing_key, + &self.hrp_sapling_payment_address, + ]; + + for sapling_hrp in sapling_hrps { + assert!(sapling_hrp.len() <= MAX_HRP_LENGTH, "Sapling human-readable prefix {sapling_hrp} is too long, must be {MAX_HRP_LENGTH} characters or less"); + assert!( + sapling_hrp.chars().all(|c| c.is_ascii_lowercase() || c == '-'), + "human-readable prefixes should contain only lowercase ASCII characters and dashes, hrp: {sapling_hrp}" + ); + assert_eq!( + sapling_hrps + .iter() + .filter(|&&hrp| hrp == sapling_hrp) + .count(), + 1, + "Sapling human-readable prefixes must be unique, repeated Sapling HRP: {sapling_hrp}" + ); + } + + 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 with_activation_heights( @@ -166,6 +201,7 @@ impl ParametersBuilder { // # Correctness // // Height(0) must be reserved for the `NetworkUpgrade::Genesis`. + // TODO: Find out if `BeforeOverwinter` must always be active at Height(1), remove it here if it's not required. self.activation_heights.split_off(&Height(2)); self.activation_heights.extend(activation_heights); @@ -220,7 +256,6 @@ impl Default for Parameters { fn default() -> Self { Self { network_name: "Testnet".to_string(), - activation_heights: TESTNET_ACTIVATION_HEIGHTS.iter().cloned().collect(), ..Self::build().finish() } } @@ -232,11 +267,46 @@ impl Parameters { ParametersBuilder::default() } + /// Accepts a [`ConfiguredActivationHeights`]. + /// + /// Creates an instance of [`Parameters`] with `Regtest` values. + pub fn new_regtest(activation_heights: ConfiguredActivationHeights) -> Self { + Self { + network_name: "Regtest".to_string(), + ..Self::build() + .with_sapling_hrps( + zp_constants::regtest::HRP_SAPLING_EXTENDED_SPENDING_KEY, + zp_constants::regtest::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, + zp_constants::regtest::HRP_SAPLING_PAYMENT_ADDRESS, + ) + // Removes default Testnet activation heights if not configured, + // most network upgrades are disabled by default for Regtest in zcashd + .with_activation_heights(activation_heights) + .finish() + } + } + /// Returns true if the instance of [`Parameters`] represents the default public Testnet. pub fn is_default_testnet(&self) -> bool { self == &Self::default() } + /// Returns true if the instance of [`Parameters`] represents Regtest. + pub fn is_regtest(&self) -> bool { + let Self { + network_name, + hrp_sapling_extended_spending_key, + hrp_sapling_extended_full_viewing_key, + hrp_sapling_payment_address, + .. + } = Self::new_regtest(ConfiguredActivationHeights::default()); + + self.network_name == network_name + && self.hrp_sapling_extended_spending_key == hrp_sapling_extended_spending_key + && self.hrp_sapling_extended_full_viewing_key == hrp_sapling_extended_full_viewing_key + && self.hrp_sapling_payment_address == hrp_sapling_payment_address + } + /// Returns the network name pub fn network_name(&self) -> &str { &self.network_name diff --git a/zebra-chain/src/parameters/network/tests/vectors.rs b/zebra-chain/src/parameters/network/tests/vectors.rs index 1d619aa5e..d794b0471 100644 --- a/zebra-chain/src/parameters/network/tests/vectors.rs +++ b/zebra-chain/src/parameters/network/tests/vectors.rs @@ -1,14 +1,19 @@ //! Fixed test vectors for the network consensus parameters. -use zcash_primitives::consensus::{self as zp_consensus, Parameters}; +use zcash_primitives::{ + consensus::{self as zp_consensus, Parameters}, + constants as zp_constants, +}; use crate::{ block::Height, parameters::{ testnet::{ - self, ConfiguredActivationHeights, MAX_NETWORK_NAME_LENGTH, RESERVED_NETWORK_NAMES, + self, ConfiguredActivationHeights, MAX_HRP_LENGTH, MAX_NETWORK_NAME_LENGTH, + RESERVED_NETWORK_NAMES, }, - Network, NetworkUpgrade, NETWORK_UPGRADES_IN_ORDER, + Network, NetworkUpgrade, MAINNET_ACTIVATION_HEIGHTS, NETWORK_UPGRADES_IN_ORDER, + TESTNET_ACTIVATION_HEIGHTS, }, }; @@ -93,8 +98,9 @@ fn check_parameters_impl() { } /// Checks that `NetworkUpgrade::activation_height()` returns the activation height of the next -/// network upgrade if it doesn't find an activation height for a prior network upgrade, and that the -/// `Genesis` upgrade is always at `Height(0)`. +/// network upgrade if it doesn't find an activation height for a prior network upgrade, that the +/// `Genesis` upgrade is always at `Height(0)`, and that the default Mainnet/Testnet/Regtest activation +/// heights are what's expected. #[test] fn activates_network_upgrades_correctly() { let expected_activation_height = 1; @@ -126,6 +132,164 @@ fn activates_network_upgrades_correctly() { should match NU5 activation height, network_upgrade: {nu}, activation_height: {activation_height:?}" ); } + + let expected_default_regtest_activation_heights = &[ + (Height(0), NetworkUpgrade::Genesis), + (Height(1), NetworkUpgrade::BeforeOverwinter), + ]; + + for (network, expected_activation_heights) in [ + (Network::Mainnet, MAINNET_ACTIVATION_HEIGHTS), + (Network::new_default_testnet(), TESTNET_ACTIVATION_HEIGHTS), + ( + Network::new_regtest(Default::default()), + expected_default_regtest_activation_heights, + ), + ] { + assert_eq!( + network.activation_list(), + expected_activation_heights.iter().cloned().collect(), + "network activation list should match expected activation heights" + ); + } +} + +/// Checks that configured testnet names are validated and used correctly. +#[test] +fn check_configured_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 '_')"); + } + + drop(std::panic::take_hook()); + + // 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'" + ); + assert_eq!( + Network::new_regtest(Default::default()).to_string(), + "Regtest", + "Regtest should be displayed as '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" + ); +} + +/// Checks that configured Sapling human-readable prefixes (HRPs) are validated and used correctly. +#[test] +fn check_configured_sapling_hrps() { + // Sets a no-op panic hook to avoid long output. + std::panic::set_hook(Box::new(|_| {})); + + // Check that configured Sapling HRPs must be unique. + std::panic::catch_unwind(|| { + testnet::Parameters::build().with_sapling_hrps("", "", ""); + }) + .expect_err("should panic when setting non-unique Sapling HRPs"); + + // Check that max length is enforced, and that network names may only contain lowecase ASCII characters and dashes. + for invalid_hrp in [ + "a".repeat(MAX_NETWORK_NAME_LENGTH + 1), + "!!!!non-alphabetical-name".to_string(), + "A".to_string(), + ] { + std::panic::catch_unwind(|| { + testnet::Parameters::build().with_sapling_hrps(invalid_hrp, "dummy-hrp-a", "dummy-hrp-b"); + }) + .expect_err("should panic when setting Sapling HRPs that are too long or contain non-alphanumeric characters (except '-')"); + } + + drop(std::panic::take_hook()); + + // Check that Sapling HRPs can contain lowercase ascii characters and dashes. + let expected_hrp_sapling_extended_spending_key = "sapling-hrp-a"; + let expected_hrp_sapling_extended_full_viewing_key = "sapling-hrp-b"; + let expected_hrp_sapling_payment_address = "sapling-hrp-c"; + + let network = testnet::Parameters::build() + // Check that Sapling HRPs can contain `MAX_HRP_LENGTH` characters + .with_sapling_hrps("a".repeat(MAX_HRP_LENGTH), "dummy-hrp-a", "dummy-hrp-b") + .with_sapling_hrps( + expected_hrp_sapling_extended_spending_key, + expected_hrp_sapling_extended_full_viewing_key, + expected_hrp_sapling_payment_address, + ) + .to_network(); + + // Check that configured Sapling HRPs are returned by `Parameters` trait methods + assert_eq!( + network.hrp_sapling_extended_spending_key(), + expected_hrp_sapling_extended_spending_key, + "should return expected Sapling extended spending key HRP" + ); + assert_eq!( + network.hrp_sapling_extended_full_viewing_key(), + expected_hrp_sapling_extended_full_viewing_key, + "should return expected Sapling EFVK HRP" + ); + assert_eq!( + network.hrp_sapling_payment_address(), + expected_hrp_sapling_payment_address, + "should return expected Sapling payment address HRP" + ); + + // Check that default Mainnet, Testnet, and Regtest HRPs are valid, these calls will panic + // if any of the values fail validation. + testnet::Parameters::build() + .with_sapling_hrps( + zp_constants::mainnet::HRP_SAPLING_EXTENDED_SPENDING_KEY, + zp_constants::mainnet::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, + zp_constants::mainnet::HRP_SAPLING_PAYMENT_ADDRESS, + ) + .with_sapling_hrps( + zp_constants::testnet::HRP_SAPLING_EXTENDED_SPENDING_KEY, + zp_constants::testnet::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, + zp_constants::testnet::HRP_SAPLING_PAYMENT_ADDRESS, + ) + .with_sapling_hrps( + zp_constants::regtest::HRP_SAPLING_EXTENDED_SPENDING_KEY, + zp_constants::regtest::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, + zp_constants::regtest::HRP_SAPLING_PAYMENT_ADDRESS, + ); } /// Checks that configured testnet names are validated and used correctly. @@ -153,6 +317,8 @@ fn check_network_name() { .expect_err("should panic when setting network name that's too long or contains non-alphanumeric characters (except '_')"); } + drop(std::panic::take_hook()); + // Checks that network names are displayed correctly assert_eq!( Network::new_default_testnet().to_string(), diff --git a/zebra-network/src/config.rs b/zebra-network/src/config.rs index e32f6c138..b630c30c2 100644 --- a/zebra-network/src/config.rs +++ b/zebra-network/src/config.rs @@ -630,8 +630,7 @@ impl<'de> Deserialize<'de> for Config { #[derive(Deserialize)] struct DTestnetParameters { network_name: Option, - #[serde(default)] - activation_heights: ConfiguredActivationHeights, + activation_heights: Option, } #[derive(Deserialize)] @@ -640,6 +639,7 @@ impl<'de> Deserialize<'de> for Config { listen_addr: String, network: NetworkKind, testnet_parameters: Option, + regtest_activation_heights: ConfiguredActivationHeights, initial_mainnet_peers: IndexSet, initial_testnet_peers: IndexSet, cache_dir: CacheDir, @@ -656,6 +656,7 @@ impl<'de> Deserialize<'de> for Config { listen_addr: "0.0.0.0".to_string(), network: Default::default(), testnet_parameters: None, + regtest_activation_heights: ConfiguredActivationHeights::default(), initial_mainnet_peers: config.initial_mainnet_peers, initial_testnet_peers: config.initial_testnet_peers, cache_dir: config.cache_dir, @@ -670,6 +671,7 @@ impl<'de> Deserialize<'de> for Config { listen_addr, network: network_kind, testnet_parameters, + regtest_activation_heights, initial_mainnet_peers, initial_testnet_peers, cache_dir, @@ -678,33 +680,55 @@ impl<'de> Deserialize<'de> for Config { max_connections_per_ip, } = DConfig::deserialize(deserializer)?; - // TODO: Panic here if the initial testnet peers are the default initial testnet peers. - let network = if let Some(DTestnetParameters { - network_name, - activation_heights, - }) = testnet_parameters - { - assert_eq!( - network_kind, + /// Accepts an [`IndexSet`] of initial peers, + /// + /// Returns true if any of them are the default Testnet or Mainnet initial peers. + fn contains_default_initial_peers(initial_peers: &IndexSet) -> bool { + let Config { + initial_mainnet_peers: mut default_initial_peers, + initial_testnet_peers: default_initial_testnet_peers, + .. + } = Config::default(); + default_initial_peers.extend(default_initial_testnet_peers); + + initial_peers + .intersection(&default_initial_peers) + .next() + .is_some() + } + + let network = match (network_kind, testnet_parameters) { + (NetworkKind::Mainnet, _) => Network::Mainnet, + (NetworkKind::Testnet, None) => Network::new_default_testnet(), + (NetworkKind::Regtest, _) => Network::new_regtest(regtest_activation_heights), + ( NetworkKind::Testnet, - "set network to 'Testnet' to use configured testnet parameters" - ); + Some(DTestnetParameters { + network_name, + activation_heights, + }), + ) => { + let mut params_builder = testnet::Parameters::build(); - let mut params_builder = testnet::Parameters::build(); + if let Some(network_name) = network_name { + params_builder = params_builder.with_network_name(network_name) + } - if let Some(network_name) = network_name { - params_builder = params_builder.with_network_name(network_name) - } + // Retain default Testnet activation heights unless there's an empty [testnet_parameters.activation_heights] section. + if let Some(activation_heights) = activation_heights { + // Return an error if the initial testnet peers includes any of the default initial Mainnet or Testnet + // peers while activation heights are configured. + // TODO: Check that the network magic is different from the default Mainnet/Testnet network magic too? + if contains_default_initial_peers(&initial_testnet_peers) { + return Err(de::Error::custom( + "cannot use default initial testnet peers with configured activation heights", + )); + } - params_builder - .with_activation_heights(activation_heights) - .to_network() - } else { - // Convert to default `Network` for a `NetworkKind` if there are no testnet parameters. - match network_kind { - NetworkKind::Mainnet => Network::Mainnet, - NetworkKind::Testnet => Network::new_default_testnet(), - NetworkKind::Regtest => unimplemented!("Regtest is not yet implemented in Zebra"), + params_builder = params_builder.with_activation_heights(activation_heights) + } + + params_builder.to_network() } }; diff --git a/zebrad/tests/common/configs/v1.7.0.toml b/zebrad/tests/common/configs/v1.7.0.toml index 00438213d..f29e39378 100644 --- a/zebrad/tests/common/configs/v1.7.0.toml +++ b/zebrad/tests/common/configs/v1.7.0.toml @@ -57,7 +57,7 @@ initial_testnet_peers = [ ] listen_addr = "0.0.0.0:8233" max_connections_per_ip = 1 -network = "Testnet" +network = "Regtest" peerset_initial_target_size = 25 [network.testnet_parameters] @@ -72,6 +72,15 @@ Heartwood = 903_800 Canopy = 1_028_500 NU5 = 1_842_420 +[network.regtest_activation_heights] +BeforeOverwinter = 1 +Overwinter = 207_500 +Sapling = 280_000 +Blossom = 584_000 +Heartwood = 903_800 +Canopy = 1_028_500 +NU5 = 1_842_420 + [rpc] debug_force_finished_sync = false parallel_cpu_threads = 0