diff --git a/zebra-consensus/src/chain.rs b/zebra-consensus/src/chain.rs index 2f9775a1..3e4a6587 100644 --- a/zebra-consensus/src/chain.rs +++ b/zebra-consensus/src/chain.rs @@ -15,6 +15,7 @@ mod tests; use crate::checkpoint::{CheckpointList, CheckpointVerifier}; +use crate::Config; use futures_util::FutureExt; use std::{ @@ -199,7 +200,8 @@ fn is_higher_than_max_checkpoint( } } -/// Return a chain verification service, using `network` and `state_service`. +/// Return a chain verification service, using `config`, `network` and +/// `state_service`. /// /// Gets the initial tip from the state service, and uses it to create a block /// verifier and checkpoint verifier (if needed). @@ -212,6 +214,7 @@ fn is_higher_than_max_checkpoint( // mempool transactions. We might want to share the BlockVerifier, and we // might not want to add generated blocks to the state. pub async fn init( + config: Config, network: Network, state_service: S, ) -> impl Service< @@ -234,7 +237,10 @@ where .expect("State service poll_ready is Ok"); let block_verifier = crate::block::init(state_service.clone()); - let checkpoint_list = CheckpointList::new(network); + let checkpoint_list = match config.checkpoint_sync { + true => CheckpointList::new(network), + false => CheckpointList::new_up_to(network, Sapling), + }; init_from_verifiers( network, @@ -263,7 +269,7 @@ where /// /// Panics: /// -/// Panics if the `checkpoint_verifier` is None, and the `initial_tip_height` is +/// Panics if the `checkpoint_list` is None, and the `initial_tip_height` is /// below the Sapling network upgrade for `network`. (The `block_verifier` can't /// verify all the constraints on pre-Sapling blocks, so they require /// checkpoints.) diff --git a/zebra-consensus/src/chain/tests.rs b/zebra-consensus/src/chain/tests.rs index b43216d0..94c16436 100644 --- a/zebra-consensus/src/chain/tests.rs +++ b/zebra-consensus/src/chain/tests.rs @@ -18,6 +18,7 @@ use zebra_chain::{ use zebra_test::transcript::{TransError, Transcript}; use crate::checkpoint::CheckpointList; +use crate::Config; use super::*; @@ -225,19 +226,29 @@ async fn verify_block() -> Result<(), Report> { #[tokio::test] async fn verify_checkpoint_test() -> Result<(), Report> { - verify_checkpoint().await + verify_checkpoint(true).await?; + verify_checkpoint(false).await?; + + Ok(()) } /// Test that checkpoint verifies work. /// /// Also tests the `chain::init` function. #[spandoc::spandoc] -async fn verify_checkpoint() -> Result<(), Report> { +async fn verify_checkpoint(checkpoint_sync: bool) -> Result<(), Report> { zebra_test::init(); + let config = Config { checkpoint_sync }; + // Test that the chain::init function works. Most of the other tests use // init_from_verifiers. - let chain_verifier = super::init(Network::Mainnet, zebra_state::in_memory::init()).await; + let chain_verifier = super::init( + config.clone(), + Network::Mainnet, + zebra_state::in_memory::init(), + ) + .await; // Add a timeout layer let chain_verifier = diff --git a/zebra-consensus/src/checkpoint/list.rs b/zebra-consensus/src/checkpoint/list.rs index e453bcd2..2e0b8079 100644 --- a/zebra-consensus/src/checkpoint/list.rs +++ b/zebra-consensus/src/checkpoint/list.rs @@ -18,7 +18,7 @@ use std::{ }; use zebra_chain::block; -use zebra_chain::parameters::Network; +use zebra_chain::parameters::{Network, NetworkUpgrade, NetworkUpgrade::*}; const MAINNET_CHECKPOINTS: &str = include_str!("main-checkpoints.txt"); const TESTNET_CHECKPOINTS: &str = include_str!("test-checkpoints.txt"); @@ -86,6 +86,43 @@ impl CheckpointList { } } + /// Returns the hard-coded checkpoint list for `network`, up to and + /// including the first checkpoint after the activation of the `limit` + /// network upgrade. + pub fn new_up_to(network: Network, limit: NetworkUpgrade) -> Self { + let full_list = Self::new(network); + + match limit { + Genesis | BeforeOverwinter | Overwinter => unreachable!("Caller passed a pre-Sapling network upgrade: Zebra must checkpoint up to Sapling activation"), + _ => {}, + }; + + let activation = match limit.activation_height(network) { + Some(height) => height, + // If it's a future upgrade, it can't possibly limit our past checkpoints + None => return full_list, + }; + + let last_checkpoint = match full_list.min_height_in_range(activation..) { + Some(height) => height, + // If the full list has no checkpoints after limit, then all checkpoints + // are already under the limit + None => return full_list, + }; + + let limited_list = full_list + .0 + .range(..=last_checkpoint) + .map(|(hash, height)| (*hash, *height)); + + match Self::from_list(limited_list) { + Ok(list) => list, + Err(_) => unreachable!( + "Unexpected invalid list: a non-empty prefix of a valid list should also be valid" + ), + } + } + /// Create a new checkpoint list for `network` from `checkpoint_list`. /// /// Assumes that the provided genesis checkpoint is correct. @@ -165,6 +202,14 @@ impl CheckpointList { .expect("checkpoint lists must have at least one checkpoint") } + /// Return the block height of the lowest checkpoint in a sub-range. + pub fn min_height_in_range(&self, range: R) -> Option + where + R: RangeBounds, + { + self.0.range(range).map(|(height, _)| *height).next() + } + /// Return the block height of the highest checkpoint in a sub-range. pub fn max_height_in_range(&self, range: R) -> Option where diff --git a/zebra-consensus/src/checkpoint/list/tests.rs b/zebra-consensus/src/checkpoint/list/tests.rs index 724cadbd..87dc9455 100644 --- a/zebra-consensus/src/checkpoint/list/tests.rs +++ b/zebra-consensus/src/checkpoint/list/tests.rs @@ -2,9 +2,10 @@ use super::*; +use std::ops::Bound::*; use std::sync::Arc; -use zebra_chain::parameters::{Network, NetworkUpgrade::Sapling}; +use zebra_chain::parameters::{Network, Network::*, NetworkUpgrade, NetworkUpgrade::*}; use zebra_chain::{ block::{self, Block}, serialization::ZcashDeserialize, @@ -234,20 +235,20 @@ fn checkpoint_list_load_hard_coded() -> Result<(), Error> { .parse() .expect("hard-coded Testnet checkpoint list should parse"); - let _ = CheckpointList::new(Network::Mainnet); - let _ = CheckpointList::new(Network::Testnet); + let _ = CheckpointList::new(Mainnet); + let _ = CheckpointList::new(Testnet); Ok(()) } #[test] fn checkpoint_list_hard_coded_sapling_mainnet() -> Result<(), Error> { - checkpoint_list_hard_coded_sapling(Network::Mainnet) + checkpoint_list_hard_coded_sapling(Mainnet) } #[test] fn checkpoint_list_hard_coded_sapling_testnet() -> Result<(), Error> { - checkpoint_list_hard_coded_sapling(Network::Testnet) + checkpoint_list_hard_coded_sapling(Testnet) } /// Check that the hard-coded lists cover the Sapling network upgrade @@ -267,3 +268,84 @@ fn checkpoint_list_hard_coded_sapling(network: Network) -> Result<(), Error> { Ok(()) } + +#[test] +fn checkpoint_list_up_to_mainnet() -> Result<(), Error> { + checkpoint_list_up_to(Mainnet, Sapling)?; + checkpoint_list_up_to(Mainnet, Blossom)?; + checkpoint_list_up_to(Mainnet, Heartwood)?; + checkpoint_list_up_to(Mainnet, Canopy)?; + + Ok(()) +} + +#[test] +fn checkpoint_list_up_to_testnet() -> Result<(), Error> { + checkpoint_list_up_to(Testnet, Sapling)?; + checkpoint_list_up_to(Testnet, Blossom)?; + checkpoint_list_up_to(Testnet, Heartwood)?; + checkpoint_list_up_to(Testnet, Canopy)?; + + Ok(()) +} + +/// Check that CheckpointList::new_up_to works +fn checkpoint_list_up_to(network: Network, limit: NetworkUpgrade) -> Result<(), Error> { + zebra_test::init(); + + let sapling_activation = Sapling + .activation_height(network) + .expect("Unexpected network upgrade info: Sapling must have an activation height"); + + let limited_list = CheckpointList::new_up_to(network, limit); + let full_list = CheckpointList::new(network); + + assert!( + limited_list.max_height() >= sapling_activation, + "Pre-Sapling blocks must be verified by checkpoints" + ); + + if let Some(limit_activation) = limit.activation_height(network) { + if limit_activation <= full_list.max_height() { + assert!( + limited_list.max_height() >= limit_activation, + "The 'limit' network upgrade must be verified by checkpoints" + ); + + let next_checkpoint_after_limit = limited_list + .min_height_in_range((Included(limit_activation), Unbounded)) + .expect("There must be a checkpoint at or after the limit"); + + assert_eq!( + limited_list + .min_height_in_range((Excluded(next_checkpoint_after_limit), Unbounded)), + None, + "There must not be multiple checkpoints after the limit" + ); + + let next_activation = NetworkUpgrade::next(network, limit_activation) + .map(|nu| nu.activation_height(network)) + .flatten(); + if let Some(next_activation) = next_activation { + // We expect that checkpoints happen much more often than network upgrades + assert!( + limited_list.max_height() < next_activation, + "The next network upgrade after 'limit' must not be verified by checkpoints" + ); + } + + // We have an effective limit, so skip the "no limit" test + return Ok(()); + } + } + + // Either the activation height is unspecified, or it is above the maximum + // checkpoint height (in the full checkpoint list) + assert_eq!( + limited_list.max_height(), + full_list.max_height(), + "Future network upgrades must not limit checkpoints" + ); + + Ok(()) +} diff --git a/zebrad/src/commands/start.rs b/zebrad/src/commands/start.rs index 5c416963..d31f88ee 100644 --- a/zebrad/src/commands/start.rs +++ b/zebrad/src/commands/start.rs @@ -43,7 +43,12 @@ impl StartCmd { let config = app_config(); let state = zebra_state::on_disk::init(config.state.clone(), config.network.network); - let verifier = zebra_consensus::chain::init(config.network.network, state.clone()).await; + let verifier = zebra_consensus::chain::init( + config.consensus.clone(), + config.network.network, + state.clone(), + ) + .await; // The service that our node uses to respond to requests by peers let node = Buffer::new(