diff --git a/zebra-rpc/src/methods/get_block_template_rpcs.rs b/zebra-rpc/src/methods/get_block_template_rpcs.rs index 9ef9c581e..62aa4a9b8 100644 --- a/zebra-rpc/src/methods/get_block_template_rpcs.rs +++ b/zebra-rpc/src/methods/get_block_template_rpcs.rs @@ -638,7 +638,10 @@ where // // Optional TODO: // - add `async changed()` method to ChainSyncStatus (like `ChainTip`) - check_synced_to_tip(&network, latest_chain_tip.clone(), sync_status.clone())?; + // TODO: Add a `disable_peers` field to `Network` to check instead of `disable_pow()` (#8361) + if !network.disable_pow() { + check_synced_to_tip(&network, latest_chain_tip.clone(), sync_status.clone())?; + } // TODO: return an error if we have no peers, like `zcashd` does, // and add a developer config that mines regardless of how many peers we have. diff --git a/zebra-state/src/service.rs b/zebra-state/src/service.rs index 423ef50a7..d2f061398 100644 --- a/zebra-state/src/service.rs +++ b/zebra-state/src/service.rs @@ -395,7 +395,7 @@ impl StateService { - HeightDiff::try_from(checkpoint_verify_concurrency_limit) .expect("fits in HeightDiff"); let full_verifier_utxo_lookahead = - full_verifier_utxo_lookahead.expect("unexpected negative height"); + full_verifier_utxo_lookahead.unwrap_or(block::Height::MIN); let non_finalized_state_queued_blocks = QueuedBlocks::default(); let pending_utxos = PendingUtxos::default(); diff --git a/zebrad/src/commands/start.rs b/zebrad/src/commands/start.rs index dbe2368f3..99d6ec72c 100644 --- a/zebrad/src/commands/start.rs +++ b/zebrad/src/commands/start.rs @@ -78,14 +78,17 @@ //! //! Some of the diagnostic features are optional, and need to be enabled at compile-time. +use std::sync::Arc; + use abscissa_core::{config, Command, FrameworkError}; use color_eyre::eyre::{eyre, Report}; use futures::FutureExt; use tokio::{pin, select, sync::oneshot}; -use tower::{builder::ServiceBuilder, util::BoxService}; +use tower::{builder::ServiceBuilder, util::BoxService, ServiceExt}; use tracing_futures::Instrument; -use zebra_consensus::router::BackgroundTaskHandles; +use zebra_chain::block::genesis::regtest_genesis_block; +use zebra_consensus::{router::BackgroundTaskHandles, ParameterCheckpoint}; use zebra_rpc::server::RpcServer; use crate::{ @@ -110,9 +113,8 @@ pub struct StartCmd { } impl StartCmd { - async fn start(&self) -> Result<(), Report> { - let config = APPLICATION.config(); - + /// Starts `zebrad` with the provided config. + pub async fn start(&self, config: Arc) -> Result<(), Report> { info!("initializing node state"); let (_, max_checkpoint_height) = zebra_consensus::router::init_checkpoint_list( config.consensus.clone(), @@ -135,7 +137,7 @@ impl StartCmd { read_only_state_service.log_db_metrics(); let state = ServiceBuilder::new() - .buffer(Self::state_buffer_bound()) + .buffer(Self::state_buffer_bound(config.clone())) .service(state_service); info!("initializing network"); @@ -177,7 +179,7 @@ impl StartCmd { .await; info!("initializing syncer"); - let (syncer, sync_status) = ChainSync::new( + let (mut syncer, sync_status) = ChainSync::new( &config, max_checkpoint_height, peer_set.clone(), @@ -300,7 +302,28 @@ impl StartCmd { ); info!("spawning syncer task"); - let syncer_task_handle = tokio::spawn(syncer.sync().in_current_span()); + let syncer_task_handle = if config.network.network.is_regtest() { + if !syncer + .state_contains(config.network.network.genesis_hash()) + .await? + { + let genesis_hash = block_verifier_router + .clone() + .oneshot(zebra_consensus::Request::Commit(regtest_genesis_block())) + .await + .expect("should validate Regtest genesis block"); + + assert_eq!( + genesis_hash, + config.network.network.genesis_hash(), + "validated block hash should match network genesis hash" + ) + } + + tokio::spawn(std::future::pending().in_current_span()) + } else { + tokio::spawn(syncer.sync().in_current_span()) + }; #[cfg(feature = "shielded-scan")] // Spawn never ending scan task only if we have keys to scan for. @@ -505,9 +528,7 @@ impl StartCmd { /// Returns the bound for the state service buffer, /// based on the configurations of the services that use the state concurrently. - fn state_buffer_bound() -> usize { - let config = APPLICATION.config(); - + fn state_buffer_bound(config: Arc) -> usize { // Ignore the checkpoint verify limit, because it is very large. // // TODO: do we also need to account for concurrent use across services? @@ -536,8 +557,10 @@ impl Runnable for StartCmd { .rt .take(); + let config = APPLICATION.config(); + rt.expect("runtime should not already be taken") - .run(self.start()); + .run(self.start(config)); info!("stopping zebrad"); } diff --git a/zebrad/src/components/sync.rs b/zebrad/src/components/sync.rs index cbfd5e7b6..e51ee06f0 100644 --- a/zebrad/src/components/sync.rs +++ b/zebrad/src/components/sync.rs @@ -1136,7 +1136,7 @@ where /// Returns `true` if the hash is present in the state, and `false` /// if the hash is not present in the state. - async fn state_contains(&mut self, hash: block::Hash) -> Result { + pub(crate) async fn state_contains(&mut self, hash: block::Hash) -> Result { match self .state .ready() diff --git a/zebrad/tests/acceptance.rs b/zebrad/tests/acceptance.rs index 5605d3956..5e6342c6b 100644 --- a/zebrad/tests/acceptance.rs +++ b/zebrad/tests/acceptance.rs @@ -3126,3 +3126,15 @@ async fn validate_regtest_genesis_block() { "validated block hash should match network genesis hash" ) } + +/// Test successful `getblocktemplate` and `submitblock` RPC calls on Regtest on Canopy and NU5. +/// +/// See [`common::regtest::submit_blocks`] for more information. +#[tokio::test] +#[cfg(feature = "getblocktemplate-rpcs")] +async fn regtest_submit_blocks() -> Result<()> { + for nu5_activation_height in [None, Some(1), Some(1_000)] { + common::regtest::submit_blocks::run(nu5_activation_height).await?; + } + Ok(()) +} diff --git a/zebrad/tests/common/mod.rs b/zebrad/tests/common/mod.rs index 703831248..3ff3f63db 100644 --- a/zebrad/tests/common/mod.rs +++ b/zebrad/tests/common/mod.rs @@ -24,5 +24,8 @@ pub mod checkpoints; #[cfg(feature = "getblocktemplate-rpcs")] pub mod get_block_template_rpcs; +#[cfg(feature = "getblocktemplate-rpcs")] +pub mod regtest; + #[cfg(feature = "shielded-scan")] pub mod shielded_scan; diff --git a/zebrad/tests/common/regtest.rs b/zebrad/tests/common/regtest.rs new file mode 100644 index 000000000..03dad0b72 --- /dev/null +++ b/zebrad/tests/common/regtest.rs @@ -0,0 +1,3 @@ +//! Acceptance tests for Regtest in Zebra. + +pub(crate) mod submit_blocks; diff --git a/zebrad/tests/common/regtest/submit_blocks.rs b/zebrad/tests/common/regtest/submit_blocks.rs new file mode 100644 index 000000000..914ffd0a0 --- /dev/null +++ b/zebrad/tests/common/regtest/submit_blocks.rs @@ -0,0 +1,88 @@ +//! Test submitblock RPC method on Regtest. +//! +//! This test will get block templates via the `getblocktemplate` RPC method and submit them as new blocks +//! via the `submitblock` RPC method on Regtest. + +use std::{net::SocketAddr, sync::Arc, time::Duration}; + +use color_eyre::eyre::Result; +use tokio::task::JoinHandle; +use tracing::*; + +use zebra_chain::{parameters::Network, serialization::ZcashSerialize}; +use zebra_node_services::rpc_client::RpcRequestClient; +use zebra_rpc::methods::get_block_template_rpcs::get_block_template::{ + proposal::TimeSource, proposal_block_from_template, GetBlockTemplate, +}; + +use crate::common::config::random_known_rpc_port_config; +use zebrad::commands::StartCmd; + +/// Number of blocks that should be submitted before the test is considered successful. +const NUM_BLOCKS_TO_SUBMIT: usize = 2_000; + +/// - Starts Zebrad +/// - Calls `getblocktemplate` RPC method and submits blocks with empty equihash solutions +/// - Checks that blocks were validated and committed successfully +// TODO: Implement custom serialization for `zebra_network::Config` and spawn a zebrad child process instead? +pub(crate) async fn run(nu5_activation_height: Option) -> Result<()> { + let _init_guard = zebra_test::init(); + info!("starting regtest submit_blocks test"); + + let network = Network::new_regtest(nu5_activation_height); + let config = random_known_rpc_port_config(false, &network)?; + let rpc_address = config.rpc.listen_addr.unwrap(); + + info!("starting zebrad"); + let _zebrad_start_task: JoinHandle<_> = tokio::task::spawn_blocking(|| { + let rt = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .expect("runtime building should not fail"); + let start_cmd = StartCmd::default(); + + let _ = rt.block_on(start_cmd.start(Arc::new(config))); + }); + + info!("waiting for zebrad to start"); + + tokio::time::sleep(Duration::from_secs(30)).await; + + info!("attempting to submit blocks"); + submit_blocks(rpc_address).await?; + + Ok(()) +} + +/// Get block templates and submit blocks +async fn submit_blocks(rpc_address: SocketAddr) -> Result<()> { + let client = RpcRequestClient::new(rpc_address); + + for _ in 0..NUM_BLOCKS_TO_SUBMIT { + let block_template: GetBlockTemplate = client + .json_result_from_call("getblocktemplate", "[]".to_string()) + .await + .expect("response should be success output with a serialized `GetBlockTemplate`"); + + let block_data = hex::encode( + proposal_block_from_template(&block_template, TimeSource::default())? + .zcash_serialize_to_vec()?, + ); + + let submit_block_response = client + .text_from_call("submitblock", format!(r#"["{block_data}"]"#)) + .await?; + + let was_submission_successful = submit_block_response.contains(r#""result":null"#); + + info!(was_submission_successful, "submitted block"); + + // Check that the block was validated and committed. + assert!( + submit_block_response.contains(r#""result":null"#), + "unexpected response from submitblock RPC, should be null, was: {submit_block_response}" + ); + } + + Ok(()) +}