moves all ordered network upgrades to a constant, adds a no_duplicates test, moves struct with activation heights outside deserialization impl and accepts it in `ParametersBuilder::activation_heights()` instead of a Vec

This commit is contained in:
Arya 2024-04-12 17:19:34 -04:00
parent f724c0188f
commit a37510a67b
4 changed files with 154 additions and 58 deletions

View File

@ -3,9 +3,33 @@ use std::collections::BTreeMap;
use crate::{
block::Height,
parameters::{network_upgrade::TESTNET_ACTIVATION_HEIGHTS, NetworkUpgrade},
parameters::{
network_upgrade::TESTNET_ACTIVATION_HEIGHTS, Network, NetworkUpgrade,
NETWORK_UPGRADES_IN_ORDER,
},
};
/// Configurable activation heights for Regtest and configured Testnets.
#[derive(Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct ConfiguredActivationHeights {
/// Activation height for `BeforeOverwinter` network upgrade.
pub before_overwinter: Option<u32>,
/// Activation height for `Overwinter` network upgrade.
pub overwinter: Option<u32>,
/// Activation height for `Sapling` network upgrade.
pub sapling: Option<u32>,
/// Activation height for `Blossom` network upgrade.
pub blossom: Option<u32>,
/// Activation height for `Heartwood` network upgrade.
pub heartwood: Option<u32>,
/// Activation height for `Canopy` network upgrade.
pub canopy: Option<u32>,
/// Activation height for `NU5` network upgrade.
#[serde(rename = "NU5")]
pub nu5: Option<u32>,
}
/// Builder for the [`Parameters`] struct.
#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub struct ParametersBuilder {
@ -33,23 +57,48 @@ impl Default for ParametersBuilder {
impl ParametersBuilder {
/// 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(mut self, activation_heights: Vec<(Height, NetworkUpgrade)>) -> Self {
let activation_heights: BTreeMap<_, _> = activation_heights
pub fn activation_heights(
mut self,
ConfiguredActivationHeights {
before_overwinter,
overwinter,
sapling,
blossom,
heartwood,
canopy,
nu5,
}: ConfiguredActivationHeights,
) -> Self {
use NetworkUpgrade::*;
// # Correctness
//
// These must be in order so that later network upgrades overwrite prior ones
// if multiple network upgrades are configured with the same activation height.
let activation_heights: BTreeMap<_, _> = before_overwinter
.into_iter()
// TODO: Find out if `BeforeOverwinter` is required at Height(1), remove this filter if it's not required to be at Height(1)
.map(|h| (h, BeforeOverwinter))
.chain(overwinter.into_iter().map(|h| (h, Overwinter)))
.chain(sapling.into_iter().map(|h| (h, Sapling)))
.chain(blossom.into_iter().map(|h| (h, Blossom)))
.chain(heartwood.into_iter().map(|h| (h, Heartwood)))
.chain(canopy.into_iter().map(|h| (h, Canopy)))
.chain(nu5.into_iter().map(|h| (h, Nu5))) // TODO: Find out if `BeforeOverwinter` is required at Height(1), remove this filter if it's not required to be at Height(1)
.filter(|&(_, nu)| nu != NetworkUpgrade::BeforeOverwinter)
.map(|(h, nu)| (h.try_into().expect("activation height must be valid"), nu))
.collect();
// Check that the provided network upgrade activation heights are in the same order by height as the default testnet activation heights
let network_upgrades: Vec<_> = activation_heights.iter().map(|(_h, &nu)| nu).collect();
// Check that the provided network upgrade activation heights are in the same order by height as the default testnet activation heights
let mut activation_heights_iter = activation_heights.iter();
for expected_network_upgrade in TESTNET_ACTIVATION_HEIGHTS.iter().map(|&(_, nu)| nu) {
for expected_network_upgrade in NETWORK_UPGRADES_IN_ORDER {
if !network_upgrades.contains(&expected_network_upgrade) {
continue;
} else if let Some((_h, &network_upgrade)) = activation_heights_iter.next() {
assert!(
network_upgrade == expected_network_upgrade,
"network upgrades must be in order"
"network upgrades must be activated in order, the correct order is {NETWORK_UPGRADES_IN_ORDER:?}"
);
}
}
@ -68,6 +117,11 @@ impl ParametersBuilder {
let Self { activation_heights } = self;
Parameters { activation_heights }
}
/// Converts the builder to a configured [`Network::Testnet`]
pub fn to_network(self) -> Network {
Network::new_configured_testnet(self.finish())
}
}
/// Network consensus parameters for test networks such as Regtest and the default Testnet.
@ -84,9 +138,9 @@ pub struct Parameters {
impl Default for Parameters {
/// Returns an instance of the default public testnet [`Parameters`].
fn default() -> Self {
Self::build()
.activation_heights(TESTNET_ACTIVATION_HEIGHTS.to_vec())
.finish()
Self {
activation_heights: TESTNET_ACTIVATION_HEIGHTS.iter().cloned().collect(),
}
}
}

View File

@ -2,8 +2,16 @@
use zcash_primitives::consensus::{self as zp_consensus, Parameters};
use crate::parameters::Network;
use crate::{
block::Height,
parameters::{
testnet::{self, ConfiguredActivationHeights},
Network, NetworkUpgrade, NETWORK_UPGRADES_IN_ORDER,
},
};
/// Checks that every method in the `Parameters` impl for `zebra_chain::Network` has the same output
/// as the Parameters impl for `zcash_primitives::consensus::Network` on Mainnet and the default Testnet.
#[test]
fn check_parameters_impl() {
let zp_network_upgrades = [
@ -81,3 +89,63 @@ 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)`.
#[test]
fn activates_network_upgrades_correctly() {
let expected_activation_height = 1;
let network = testnet::Parameters::build()
.activation_heights(ConfiguredActivationHeights {
nu5: Some(expected_activation_height),
..Default::default()
})
.to_network();
let genesis_activation_height = NetworkUpgrade::Genesis
.activation_height(&network)
.expect("must return an activation height");
assert_eq!(
genesis_activation_height,
Height(0),
"activation height for all networks after Genesis and BeforeOverwinter should match NU5 activation height"
);
for nu in NETWORK_UPGRADES_IN_ORDER.into_iter().skip(1) {
let activation_height = nu
.activation_height(&network)
.expect("must return an activation height");
assert_eq!(
activation_height, Height(expected_activation_height),
"activation height for all networks after Genesis and BeforeOverwinter \
should match NU5 activation height, network_upgrade: {nu}, activation_height: {activation_height:?}"
);
}
}
/// Checks that there are no duplicate activation heights when using configured activation heights.
// TODO: Convert this to a proptest.
#[test]
fn no_duplicate_activation_heights() {
let network = testnet::Parameters::build()
.activation_heights(ConfiguredActivationHeights {
overwinter: Some(2),
..Default::default()
})
.to_network();
for target_nu in NETWORK_UPGRADES_IN_ORDER.into_iter().skip(1) {
assert!(
network
.activation_list()
.into_iter()
.filter(|&(_h, nu)| nu == target_nu)
.count()
<= 1,
"there should be at most 1 activation height per possible network upgrade"
);
}
}

View File

@ -14,6 +14,18 @@ use hex::{FromHex, ToHex};
#[cfg(any(test, feature = "proptest-impl"))]
use proptest_derive::Arbitrary;
/// A list of network upgrades in the order that they must be activated.
pub const NETWORK_UPGRADES_IN_ORDER: [NetworkUpgrade; 8] = [
Genesis,
BeforeOverwinter,
Overwinter,
Sapling,
Blossom,
Heartwood,
Canopy,
Nu5,
];
/// A Zcash network upgrade.
///
/// Network upgrades can change the Zcash network protocol or consensus rules in

View File

@ -15,7 +15,10 @@ use tempfile::NamedTempFile;
use tokio::{fs, io::AsyncWriteExt};
use tracing::Span;
use zebra_chain::parameters::{testnet, Network, NetworkKind, NetworkUpgrade};
use zebra_chain::parameters::{
testnet::{self, ConfiguredActivationHeights},
Network, NetworkKind,
};
use crate::{
constants::{
@ -625,23 +628,10 @@ impl<'de> Deserialize<'de> for Config {
where
D: Deserializer<'de>,
{
#[derive(Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
struct DNetworkUpgradeActivationHeights {
before_overwinter: Option<u32>,
overwinter: Option<u32>,
sapling: Option<u32>,
blossom: Option<u32>,
heartwood: Option<u32>,
canopy: Option<u32>,
#[serde(rename = "NU5")]
nu5: Option<u32>,
}
#[derive(Deserialize)]
struct DTestnetParameters {
#[serde(default)]
pub(super) activation_heights: DNetworkUpgradeActivationHeights,
pub(super) activation_heights: ConfiguredActivationHeights,
}
#[derive(Deserialize)]
@ -688,45 +678,17 @@ impl<'de> Deserialize<'de> for Config {
max_connections_per_ip,
} = DConfig::deserialize(deserializer)?;
let network = if let Some(DTestnetParameters {
activation_heights:
DNetworkUpgradeActivationHeights {
before_overwinter,
overwinter,
sapling,
blossom,
heartwood,
canopy,
nu5,
},
}) = testnet_parameters
{
use NetworkUpgrade::*;
// TODO: Panic here if the initial testnet peers are the default initial testnet peers.
// TODO: Panic here if the initial testnet peers are the default initial testnet peers.
let network = if let Some(DTestnetParameters { activation_heights }) = testnet_parameters {
assert_eq!(
network_kind,
NetworkKind::Testnet,
"set network to 'Testnet' to use configured testnet parameters"
);
let activation_heights = before_overwinter
.into_iter()
.map(|h| (h, BeforeOverwinter))
.chain(overwinter.into_iter().map(|h| (h, Overwinter)))
.chain(sapling.into_iter().map(|h| (h, Sapling)))
.chain(blossom.into_iter().map(|h| (h, Blossom)))
.chain(heartwood.into_iter().map(|h| (h, Heartwood)))
.chain(canopy.into_iter().map(|h| (h, Canopy)))
.chain(nu5.into_iter().map(|h| (h, Nu5)))
.map(|(h, nu)| (h.try_into().expect("activation height must be valid"), nu))
.collect();
let testnet_parameters = testnet::Parameters::build()
testnet::Parameters::build()
.activation_heights(activation_heights)
.finish();
Network::new_configured_testnet(testnet_parameters)
.to_network()
} else {
// Convert to default `Network` for a `NetworkKind` if there are no testnet parameters.
match network_kind {