Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

change(network): Add configurable network parameters to Network::Testnet #7924

Closed
wants to merge 21 commits into from

Conversation

arya2
Copy link
Contributor

@arya2 arya2 commented Nov 8, 2023

Motivation

This PR is a step towards adding the Regtest network to Zebra, adding optional cache_name, genesis_hash, network_id, default_port, and activation_heights fields to Network::Testnet.

For testing ZSAs, it's not yet clear what other fields will be needed or whether the scope of this approach will be viable.

Part of #7845.

PR Author Checklist

Check before marking the PR as ready for review:

  • Will the PR name make sense to users?
  • Does the PR have a priority label?
  • Have you added or updated tests?
  • Is the documentation up to date?
For significant changes:
  • Is there a summary in the CHANGELOG?
  • Can these changes be split into multiple PRs?

Solution

  • Adds new fields to TestnetParameters in Testnet
  • Uses values in TestnetParameters, if any, instead of hard-coded defaults
  • Updates references

Review

Anyone can review.

Reviewer Checklist

Check before approving the PR:

  • Does the PR scope match the ticket?
  • Are there enough tests to make sure it works? Do the tests cover the PR motivation?
  • Are all the PR blockers dealt with?
    PR blockers can be dealt with in new tickets or PRs.

And check the PR Author checklist is complete.

Follow Up Work

@arya2 arya2 added A-consensus Area: Consensus rule updates C-enhancement Category: This is an improvement I-usability Zebra is hard to understand or use A-network Area: Network protocol updates or fixes C-testing Category: These are tests C-feature Category: New features labels Nov 8, 2023
@arya2 arya2 self-assigned this Nov 8, 2023
@arya2 arya2 requested review from a team as code owners November 8, 2023 00:47
@arya2 arya2 requested review from teor2345 and removed request for a team November 8, 2023 00:47
@github-actions github-actions bot added the C-trivial Category: A trivial change that is not worth mentioning in the CHANGELOG label Nov 8, 2023
@teor2345

This comment was marked as resolved.

Comment on lines 101 to 107
&*vectors::MAINNET_FINAL_SPROUT_ROOTS,
MAINNET_FIRST_JOINSPLIT_HEIGHT,
),
Network::Testnet => (
Network::Testnet(_) => (
&*vectors::TESTNET_BLOCKS,
&*vectors::TESTNET_FINAL_SPROUT_ROOTS,
TESTNET_FIRST_JOINSPLIT_HEIGHT,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like this code has been copied around a lot, maybe we should replace it with methods in test::vectors that take is_mainnet: bool?

This can happen in an earlier PR, and then all this test code doesn't need to change at all in this PR.

@@ -95,7 +95,7 @@ fn funding_stream_address_period(height: Height, network: Network) -> u32 {
fn funding_stream_address_index(height: Height, network: Network) -> usize {
let num_addresses = match network {
Network::Mainnet => FUNDING_STREAMS_NUM_ADDRESSES_MAINNET,
Network::Testnet => FUNDING_STREAMS_NUM_ADDRESSES_TESTNET,
Network::Testnet(_) => FUNDING_STREAMS_NUM_ADDRESSES_TESTNET,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's check what zcashd does here.

@@ -60,7 +60,7 @@ pub fn height_for_first_halving(network: Network) -> Height {
Network::Mainnet => Canopy
.activation_height(network)
.expect("canopy activation height should be available"),
Network::Testnet => FIRST_HALVING_TESTNET,
Network::Testnet(_) => FIRST_HALVING_TESTNET,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's check what zcashd does here.

Comment on lines +64 to +75
Network::Testnet(_) => Self::from_str(TESTNET_CHECKPOINTS, network)
.expect("Hard-coded Testnet checkpoint list parses and validates"),
};

match checkpoint_list.hash(block::Height(0)) {
Some(hash) if hash == genesis_hash(network) => checkpoint_list,
Some(_) => {
panic!("The hard-coded genesis checkpoint does not match the network genesis hash")
}
None => unreachable!("Parser should have checked for a missing genesis checkpoint"),
}
}
Copy link
Contributor

@teor2345 teor2345 Nov 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only correct if network == Network::new_testnet(). If it's not, then the list should consist of only the checkpoints supplied by the custom network. Currently that's a single item list containing the genesis block hash.

If we implement it that way, then there's no need to check the genesis checkpoint in production code, and that check can be moved to a unit test.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If changing genesis is out of scope, then this should be the hard-coded testnet genesis block hash.

Comment on lines 117 to 124
// Check that the list starts with the correct genesis block
match checkpoints.iter().next() {
Some((block::Height(0), hash))
if (hash == &genesis_hash(Network::Mainnet)
|| hash == &genesis_hash(Network::Testnet)) => {}
Some((block::Height(0), hash)) if hash == &genesis_hash(network) => {}
Some((block::Height(0), _)) => {
Err("the genesis checkpoint does not match the Mainnet or Testnet genesis hash")?
Err("the genesis checkpoint does not match the network genesis hash")?
}
Some(_) => Err("checkpoints must start at the genesis block height 0")?,
None => Err("there must be at least one checkpoint, for the genesis block")?,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than complicating this code by adding a network parameter, let's move the checks that depend on network into new(). (And replace the similar check already in new().)

@@ -225,7 +225,7 @@ impl Config {
pub fn initial_peer_hostnames(&self) -> &IndexSet<String> {
match self.network {
Network::Mainnet => &self.initial_mainnet_peers,
Network::Testnet => &self.initial_testnet_peers,
Network::Testnet(_) => &self.initial_testnet_peers,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is incorrect and could cause issues with the reliability of the public testnet. A custom testnet needs to supply its own peers if it changes certain parameters.

Currently those are the genesis hash, the activation heights, and the network magic. Let's write code that checks if these parameters match the default testnet parameters?

There's a specific way to do that so that each newly added parameter field causes a compile error, and we have to add it to the check.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the specific check that we want here is:
If the network is an incompatible custom testnet (that is, it changes any incompatible parameters from the default testnet values), and the configured initial testnet peers contains any of the default testnet peers, then panic.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A simpler check would be to just check against the default testnet for now.

Comment on lines 429 to 432
let mut blocks_size_to_dump = match self.network {
Network::Mainnet => MAINNET_AWAY_FROM_TIP_BULK_SIZE,
Network::Testnet => TESTNET_AWAY_FROM_TIP_BULK_SIZE,
Network::Testnet(_) => TESTNET_AWAY_FROM_TIP_BULK_SIZE,
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is incorrect, and will fail if the public or custom testnet has a significant number of 2MB blocks. Instead, the value of MAINNET_AWAY_FROM_TIP_BULK_SIZE should be used for all networks.

Comment on lines +507 to +508
(Testnet(_), PayToPublicKeyHash { .. }) => 2,
(Testnet(_), PayToScriptHash { .. }) => 3,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be incorrect for custom testnets if they change their address formats. What does zcashd do here?

Copy link
Contributor Author

@arya2 arya2 Nov 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does zcashd do here?

I'm not sure, I imagine it may be storing the entire prefix, I'll ask.

I think this could be fine as long as we use always different cache directories, but it would be safer to use different values for Regtest or custom testnets (without differentiating between different kinds of custom testnets).

@@ -107,7 +107,7 @@ lazy_static! {
pub static ref FUNDING_STREAM_HEIGHT_RANGES: HashMap<Network, std::ops::Range<Height>> = {
let mut hash_map = HashMap::new();
hash_map.insert(Network::Mainnet, Height(1_046_400)..Height(2_726_400));
hash_map.insert(Network::Testnet, Height(1_028_500)..Height(2_796_000));
hash_map.insert(Network::new_testnet(), Height(1_028_500)..Height(2_796_000));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will cause a panic with a cryptic error message for custom testnets:

let range = FUNDING_STREAM_HEIGHT_RANGES.get(&network).unwrap();

@@ -127,7 +127,7 @@ lazy_static! {
testnet_addresses.insert(FundingStreamReceiver::Ecc, FUNDING_STREAM_ECC_ADDRESSES_TESTNET.iter().map(|a| a.to_string()).collect());
testnet_addresses.insert(FundingStreamReceiver::ZcashFoundation, FUNDING_STREAM_ZF_ADDRESSES_TESTNET.iter().map(|a| a.to_string()).collect());
testnet_addresses.insert(FundingStreamReceiver::MajorGrants, FUNDING_STREAM_MG_ADDRESSES_TESTNET.iter().map(|a| a.to_string()).collect());
addresses_by_network.insert(Network::Testnet, testnet_addresses);
addresses_by_network.insert(Network::new_testnet(), testnet_addresses);
Copy link
Contributor

@teor2345 teor2345 Nov 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will cause a panic here for custom testnets:

let address = &FUNDING_STREAM_ADDRESSES
.get(&network)
.expect("there is always another hash map as value for a given valid network")

@@ -85,11 +170,26 @@ impl fmt::Display for Network {
}

impl Network {
/// Creates a new `Testnet` with no parameters.
pub fn new_testnet() -> Self {
Copy link
Contributor

@teor2345 teor2345 Nov 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like we're using this method for both:

  • the canonical public testnet used in tests
  • the custom testnet parameters used by zebrad when configured with Testnet

They can be the same, but we also want to make it easy for them to be different. And make the code easy to maintain.

So here's what I suggest to make the code clearer and easier to maintain:

  • use a PUBLIC_TESTNET constant in tests, and make it test-only/proptest-impl using #cfg
  • use a new_configured_testnet() method in production code, it should only be called once when the network config is loaded, so it can be pub(crate)
  • make all the other methods that can create a testnet into test-only/proptest-impl using #cfg

After that change, anything that doesn't compile is an incorrect use of the public testnet in production code, or an incorrect use of the configured testnet in test code.

We'll also need tests for custom parameter changes, so we can't make those methods private, they need to be called from tests.

Suggested change
pub fn new_testnet() -> Self {
#[cfg(any(test, feature = "proptest-impl"))]
pub(crate) fn new_public_testnet() -> Self {

}

/// Creates a new `Testnet` with the provided parameters.
pub fn new_testnet_with_parameters(parameters: impl Into<TestnetParameters>) -> Self {
Copy link
Contributor

@teor2345 teor2345 Nov 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Except I think this one needs to be private so it can be called from both new_public_testnet() and new_configured_testnet(). (But not anything else.)

Suggested change
pub fn new_testnet_with_parameters(parameters: impl Into<TestnetParameters>) -> Self {
fn new_testnet_with_parameters(parameters: impl Into<TestnetParameters>) -> Self {

@teor2345 teor2345 added P-Low ❄️ and removed C-trivial Category: A trivial change that is not worth mentioning in the CHANGELOG do-not-merge Tells Mergify not to merge this PR labels Nov 16, 2023
Copy link
Contributor

@teor2345 teor2345 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Draft review, I'll resolve most of it.

zebra-chain/src/parameters/network.rs Show resolved Hide resolved
zebra-chain/src/parameters/network.rs Show resolved Hide resolved
zebra-chain/src/parameters/network.rs Show resolved Hide resolved

impl TestnetParameters {
/// Returns `network_id` in network parameters, if any.
pub fn network_id(self) -> Option<[u8; 4]> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Taking self by value only works while this is a Copy type. Instead, take &self (by reference) to make a future transition to a non-Copy type easier.

zebra-chain/src/parameters/network.rs Show resolved Hide resolved
zebra-chain/src/work/difficulty.rs Show resolved Hide resolved
Comment on lines +64 to +75
Network::Testnet(_) => Self::from_str(TESTNET_CHECKPOINTS, network)
.expect("Hard-coded Testnet checkpoint list parses and validates"),
};

match checkpoint_list.hash(block::Height(0)) {
Some(hash) if hash == genesis_hash(network) => checkpoint_list,
Some(_) => {
panic!("The hard-coded genesis checkpoint does not match the network genesis hash")
}
None => unreachable!("Parser should have checked for a missing genesis checkpoint"),
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If changing genesis is out of scope, then this should be the hard-coded testnet genesis block hash.

@@ -225,7 +225,7 @@ impl Config {
pub fn initial_peer_hostnames(&self) -> &IndexSet<String> {
match self.network {
Network::Mainnet => &self.initial_mainnet_peers,
Network::Testnet => &self.initial_testnet_peers,
Network::Testnet(_) => &self.initial_testnet_peers,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A simpler check would be to just check against the default testnet for now.


/// Configures network parameters to use instead of default values.
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash, Serialize)]
pub struct NetworkParameters {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the first useful parameter to add is activation_heights, let's open a ticket?
In another ticket it would be useful to add an NU6 variant and a random consensus branch ID.

@arya2
Copy link
Contributor Author

arya2 commented Nov 23, 2023

The changes in this PR and the requirements of the issue it was intended to close have diverged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-consensus Area: Consensus rule updates A-network Area: Network protocol updates or fixes C-feature Category: New features C-testing Category: These are tests I-usability Zebra is hard to understand or use
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants