diff --git a/zebra-chain/src/parameters/network/testnet.rs b/zebra-chain/src/parameters/network/testnet.rs index bb6acaf9e..c9c8cd483 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")] @@ -103,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( @@ -233,7 +274,13 @@ impl Parameters { Self { network_name: "Regtest".to_string(), ..Self::build() - // Removes default Testnet activation heights, most network upgrades are disabled by default for Regtest + .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() } diff --git a/zebra-chain/src/parameters/network/tests/vectors.rs b/zebra-chain/src/parameters/network/tests/vectors.rs index 3adfa3706..843122d31 100644 --- a/zebra-chain/src/parameters/network/tests/vectors.rs +++ b/zebra-chain/src/parameters/network/tests/vectors.rs @@ -151,7 +151,7 @@ fn activates_network_upgrades_correctly() { /// Checks that configured testnet names are validated and used correctly. #[test] -fn check_network_name() { +fn check_configured_network_name() { // Sets a no-op panic hook to avoid long output. std::panic::set_hook(Box::new(|_| {})); @@ -206,3 +206,64 @@ fn check_network_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 '-')"); + } + + // 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_NETWORK_NAME_LENGTH` characters + .with_sapling_hrps( + "a".repeat(MAX_NETWORK_NAME_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" + ); +}