diff --git a/zebra-chain/src/chain_sync_status.rs b/zebra-chain/src/chain_sync_status.rs new file mode 100644 index 00000000..7a85c896 --- /dev/null +++ b/zebra-chain/src/chain_sync_status.rs @@ -0,0 +1,32 @@ +//! Defines method signatures for checking if the synchronizer is likely close to the network chain tip. + +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; + +/// An interface for checking if the synchronization is likely close to the network chain tip. +pub trait ChainSyncStatus { + /// Check if the synchronization is likely close to the network chain tip. + fn is_close_to_tip(&self) -> bool; +} + +/// A mock [`ChainSyncStatus`] implementation that allows setting the status externally. +#[derive(Clone, Default)] +pub struct MockSyncStatus { + is_close_to_tip: Arc, +} + +impl MockSyncStatus { + /// Sets mock sync status determining the return value of `is_close_to_tip()` + pub fn set_is_close_to_tip(&mut self, is_close_to_tip: bool) { + self.is_close_to_tip + .store(is_close_to_tip, Ordering::SeqCst); + } +} + +impl ChainSyncStatus for MockSyncStatus { + fn is_close_to_tip(&self) -> bool { + self.is_close_to_tip.load(Ordering::SeqCst) + } +} diff --git a/zebra-chain/src/lib.rs b/zebra-chain/src/lib.rs index a9d20dc2..57c3e544 100644 --- a/zebra-chain/src/lib.rs +++ b/zebra-chain/src/lib.rs @@ -20,6 +20,7 @@ extern crate tracing; pub mod amount; pub mod block; +pub mod chain_sync_status; pub mod chain_tip; pub mod diagnostic; pub mod fmt; diff --git a/zebra-rpc/src/methods/get_block_template_rpcs.rs b/zebra-rpc/src/methods/get_block_template_rpcs.rs index b4fa4948..c7ddda99 100644 --- a/zebra-rpc/src/methods/get_block_template_rpcs.rs +++ b/zebra-rpc/src/methods/get_block_template_rpcs.rs @@ -15,6 +15,7 @@ use zebra_chain::{ merkle::{self, AuthDataRoot}, Block, MAX_BLOCK_BYTES, ZCASH_BLOCK_VERSION, }, + chain_sync_status::ChainSyncStatus, chain_tip::ChainTip, parameters::Network, serialization::ZcashDeserializeInto, @@ -44,10 +45,19 @@ pub mod constants; pub(crate) mod types; pub(crate) mod zip317; -/// The max estimated distance to the chain tip for the getblocktemplate method -// Set to 30 in case the local time is a little ahead. -// TODO: Replace this with SyncStatus -const MAX_ESTIMATED_DISTANCE_TO_NETWORK_CHAIN_TIP: i32 = 30; +/// The max estimated distance to the chain tip for the getblocktemplate method. +/// +/// Allows the same clock skew as the Zcash network, which is 100 blocks, based on the standard rule: +/// > A full validator MUST NOT accept blocks with nTime more than two hours in the future +/// > according to its clock. This is not strictly a consensus rule because it is nondeterministic, +/// > and clock time varies between nodes. +const MAX_ESTIMATED_DISTANCE_TO_NETWORK_CHAIN_TIP: i32 = 100; + +/// The RPC error code used by `zcashd` for when it's still downloading initial blocks. +/// +/// `s-nomp` mining pool expects error code `-10` when the node is not synced: +/// +pub const NOT_SYNCED_ERROR_CODE: ErrorCode = ErrorCode::ServerError(-10); /// getblocktemplate RPC method signatures. #[rpc(server)] @@ -123,7 +133,7 @@ pub trait GetBlockTemplateRpc { } /// RPC method implementations. -pub struct GetBlockTemplateRpcImpl +pub struct GetBlockTemplateRpcImpl where Mempool: Service< mempool::Request, @@ -140,6 +150,7 @@ where + Send + Sync + 'static, + SyncStatus: ChainSyncStatus + Clone + Send + Sync + 'static, { // TODO: Add the other fields from the [`Rpc`] struct as-needed @@ -166,9 +177,13 @@ where /// The chain verifier, used for submitting blocks. chain_verifier: ChainVerifier, + + /// The chain sync status, used for checking if Zebra is likely close to the network chain tip. + sync_status: SyncStatus, } -impl GetBlockTemplateRpcImpl +impl + GetBlockTemplateRpcImpl where Mempool: Service< mempool::Request, @@ -189,6 +204,7 @@ where + Send + Sync + 'static, + SyncStatus: ChainSyncStatus + Clone + Send + Sync + 'static, { /// Create a new instance of the handler for getblocktemplate RPCs. pub fn new( @@ -198,6 +214,7 @@ where state: State, latest_chain_tip: Tip, chain_verifier: ChainVerifier, + sync_status: SyncStatus, ) -> Self { Self { network, @@ -206,12 +223,13 @@ where state, latest_chain_tip, chain_verifier, + sync_status, } } } -impl GetBlockTemplateRpc - for GetBlockTemplateRpcImpl +impl GetBlockTemplateRpc + for GetBlockTemplateRpcImpl where Mempool: Service< mempool::Request, @@ -235,6 +253,7 @@ where + Sync + 'static, >>::Future: Send, + SyncStatus: ChainSyncStatus + Clone + Send + Sync + 'static, { fn get_block_count(&self) -> Result { best_chain_tip_height(&self.latest_chain_tip).map(|height| height.0) @@ -279,6 +298,7 @@ where let mempool = self.mempool.clone(); let latest_chain_tip = self.latest_chain_tip.clone(); + let sync_status = self.sync_status.clone(); let mut state = self.state.clone(); // Since this is a very large RPC, we use separate functions for each group of fields. @@ -301,7 +321,7 @@ where data: None, })?; - if estimated_distance_to_chain_tip > MAX_ESTIMATED_DISTANCE_TO_NETWORK_CHAIN_TIP { + if !sync_status.is_close_to_tip() || estimated_distance_to_chain_tip > MAX_ESTIMATED_DISTANCE_TO_NETWORK_CHAIN_TIP { tracing::info!( estimated_distance_to_chain_tip, ?tip_height, @@ -309,9 +329,7 @@ where ); return Err(Error { - // Return error code -10 (https://github.com/s-nomp/node-stratum-pool/blob/d86ae73f8ff968d9355bb61aac05e0ebef36ccb5/lib/pool.js#L140) - // TODO: Confirm that this is the expected error code for !synced - code: ErrorCode::ServerError(-10), + code: NOT_SYNCED_ERROR_CODE, message: format!("Zebra has not synced to the chain tip, estimated distance: {estimated_distance_to_chain_tip}"), data: None, }); diff --git a/zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs b/zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs index 58a61704..df67bcc7 100644 --- a/zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs +++ b/zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs @@ -12,6 +12,7 @@ use tower::{buffer::Buffer, Service}; use zebra_chain::{ block::Hash, + chain_sync_status::MockSyncStatus, chain_tip::mock::MockChainTip, parameters::{Network, NetworkUpgrade}, serialization::ZcashDeserializeInto, @@ -77,6 +78,9 @@ pub async fn test_responses( ) .await; + let mut mock_sync_status = MockSyncStatus::default(); + mock_sync_status.set_is_close_to_tip(true); + let mining_config = get_block_template_rpcs::config::Config { miner_address: Some(transparent::Address::from_script_hash(network, [0xad; 20])), }; @@ -107,6 +111,7 @@ pub async fn test_responses( read_state, mock_chain_tip.clone(), chain_verifier.clone(), + mock_sync_status.clone(), ); // `getblockcount` @@ -138,6 +143,7 @@ pub async fn test_responses( new_read_state.clone(), mock_chain_tip, chain_verifier, + mock_sync_status, ); // `getblocktemplate` diff --git a/zebra-rpc/src/methods/tests/vectors.rs b/zebra-rpc/src/methods/tests/vectors.rs index 7f25cfad..9c28085b 100644 --- a/zebra-rpc/src/methods/tests/vectors.rs +++ b/zebra-rpc/src/methods/tests/vectors.rs @@ -21,6 +21,9 @@ use zebra_test::mock_service::MockService; use super::super::*; +#[cfg(feature = "getblocktemplate-rpcs")] +use zebra_chain::chain_sync_status::MockSyncStatus; + #[tokio::test(flavor = "multi_thread")] async fn rpc_getinfo() { let _init_guard = zebra_test::init(); @@ -658,6 +661,7 @@ async fn rpc_getblockcount() { read_state, latest_chain_tip.clone(), chain_verifier, + MockSyncStatus::default(), ); // Get the tip height using RPC method `get_block_count` @@ -703,6 +707,7 @@ async fn rpc_getblockcount_empty_state() { read_state, latest_chain_tip.clone(), chain_verifier, + MockSyncStatus::default(), ); // Get the tip height using RPC method `get_block_count @@ -754,6 +759,7 @@ async fn rpc_getblockhash() { read_state, latest_chain_tip.clone(), tower::ServiceBuilder::new().service(chain_verifier), + MockSyncStatus::default(), ); // Query the hashes using positive indexes @@ -809,6 +815,9 @@ async fn rpc_getblocktemplate() { let read_state = MockService::build().for_unit_tests(); let chain_verifier = MockService::build().for_unit_tests(); + let mut mock_sync_status = MockSyncStatus::default(); + mock_sync_status.set_is_close_to_tip(true); + let mining_config = get_block_template_rpcs::config::Config { miner_address: Some(transparent::Address::from_script_hash(Mainnet, [0x7e; 20])), }; @@ -837,7 +846,8 @@ async fn rpc_getblocktemplate() { Buffer::new(mempool.clone(), 1), read_state.clone(), mock_chain_tip, - tower::ServiceBuilder::new().service(chain_verifier), + chain_verifier, + mock_sync_status.clone(), ); // Fake the ChainInfo response @@ -909,7 +919,7 @@ async fn rpc_getblocktemplate() { mempool.expect_no_requests().await; - mock_chain_tip_sender.send_estimated_distance_to_network_chain_tip(Some(100)); + mock_chain_tip_sender.send_estimated_distance_to_network_chain_tip(Some(200)); let get_block_template_sync_error = get_block_template_rpc .get_block_template() .await @@ -919,6 +929,30 @@ async fn rpc_getblocktemplate() { get_block_template_sync_error.code, ErrorCode::ServerError(-10) ); + + mock_sync_status.set_is_close_to_tip(false); + + mock_chain_tip_sender.send_estimated_distance_to_network_chain_tip(Some(0)); + let get_block_template_sync_error = get_block_template_rpc + .get_block_template() + .await + .expect_err("needs an error when syncer is not close to tip"); + + assert_eq!( + get_block_template_sync_error.code, + ErrorCode::ServerError(-10) + ); + + mock_chain_tip_sender.send_estimated_distance_to_network_chain_tip(Some(200)); + let get_block_template_sync_error = get_block_template_rpc + .get_block_template() + .await + .expect_err("needs an error when syncer is not close to tip or estimated distance to network chain tip is far"); + + assert_eq!( + get_block_template_sync_error.code, + ErrorCode::ServerError(-10) + ); } #[cfg(feature = "getblocktemplate-rpcs")] @@ -958,7 +992,8 @@ async fn rpc_submitblock_errors() { Buffer::new(mempool.clone(), 1), read_state, latest_chain_tip.clone(), - tower::ServiceBuilder::new().service(chain_verifier), + chain_verifier, + MockSyncStatus::default(), ); // Try to submit pre-populated blocks and assert that it responds with duplicate. diff --git a/zebra-rpc/src/server.rs b/zebra-rpc/src/server.rs index 3caf6f56..9df46003 100644 --- a/zebra-rpc/src/server.rs +++ b/zebra-rpc/src/server.rs @@ -18,6 +18,7 @@ use tracing::{Instrument, *}; use zebra_chain::{ block::{self, Block}, + chain_sync_status::ChainSyncStatus, chain_tip::ChainTip, parameters::Network, }; @@ -71,7 +72,7 @@ impl RpcServer { // // TODO: put some of the configs or services in their own struct? #[allow(clippy::too_many_arguments)] - pub fn spawn( + pub fn spawn( config: Config, #[cfg(feature = "getblocktemplate-rpcs")] mining_config: get_block_template_rpcs::config::Config, @@ -83,6 +84,8 @@ impl RpcServer { state: State, #[cfg_attr(not(feature = "getblocktemplate-rpcs"), allow(unused_variables))] chain_verifier: ChainVerifier, + #[cfg_attr(not(feature = "getblocktemplate-rpcs"), allow(unused_variables))] + sync_status: SyncStatus, latest_chain_tip: Tip, network: Network, ) -> (JoinHandle<()>, JoinHandle<()>, Option) @@ -110,6 +113,7 @@ impl RpcServer { + Sync + 'static, >>::Future: Send, + SyncStatus: ChainSyncStatus + Clone + Send + Sync + 'static, { if let Some(listen_addr) = config.listen_addr { info!("Trying to open RPC endpoint at {}...", listen_addr,); @@ -144,6 +148,7 @@ impl RpcServer { state.clone(), latest_chain_tip.clone(), chain_verifier, + sync_status, ); io.extend_with(get_block_template_rpc_impl.to_delegate()); diff --git a/zebra-rpc/src/server/tests/vectors.rs b/zebra-rpc/src/server/tests/vectors.rs index 1588c8d8..33699460 100644 --- a/zebra-rpc/src/server/tests/vectors.rs +++ b/zebra-rpc/src/server/tests/vectors.rs @@ -8,7 +8,9 @@ use std::{ use futures::FutureExt; use tower::buffer::Buffer; -use zebra_chain::{chain_tip::NoChainTip, parameters::Network::*}; +use zebra_chain::{ + chain_sync_status::MockSyncStatus, chain_tip::NoChainTip, parameters::Network::*, +}; use zebra_node_services::BoxError; use zebra_test::mock_service::MockService; @@ -58,6 +60,7 @@ fn rpc_server_spawn(parallel_cpu_threads: bool) { Buffer::new(mempool.clone(), 1), Buffer::new(state.clone(), 1), Buffer::new(chain_verifier.clone(), 1), + MockSyncStatus::default(), NoChainTip, Mainnet, ); @@ -142,6 +145,7 @@ fn rpc_server_spawn_unallocated_port(parallel_cpu_threads: bool, do_shutdown: bo Buffer::new(mempool.clone(), 1), Buffer::new(state.clone(), 1), Buffer::new(chain_verifier.clone(), 1), + MockSyncStatus::default(), NoChainTip, Mainnet, ); @@ -220,6 +224,7 @@ fn rpc_server_spawn_port_conflict() { Buffer::new(mempool.clone(), 1), Buffer::new(state.clone(), 1), Buffer::new(chain_verifier.clone(), 1), + MockSyncStatus::default(), NoChainTip, Mainnet, ); @@ -235,6 +240,7 @@ fn rpc_server_spawn_port_conflict() { Buffer::new(mempool.clone(), 1), Buffer::new(state.clone(), 1), Buffer::new(chain_verifier.clone(), 1), + MockSyncStatus::default(), NoChainTip, Mainnet, ); @@ -324,6 +330,7 @@ fn rpc_server_spawn_port_conflict_parallel_auto() { Buffer::new(mempool.clone(), 1), Buffer::new(state.clone(), 1), Buffer::new(chain_verifier.clone(), 1), + MockSyncStatus::default(), NoChainTip, Mainnet, ); @@ -339,6 +346,7 @@ fn rpc_server_spawn_port_conflict_parallel_auto() { Buffer::new(mempool.clone(), 1), Buffer::new(state.clone(), 1), Buffer::new(chain_verifier.clone(), 1), + MockSyncStatus::default(), NoChainTip, Mainnet, ); diff --git a/zebrad/src/commands/start.rs b/zebrad/src/commands/start.rs index a524ef71..9c47ece8 100644 --- a/zebrad/src/commands/start.rs +++ b/zebrad/src/commands/start.rs @@ -185,6 +185,7 @@ impl StartCmd { mempool.clone(), read_only_state_service, chain_verifier.clone(), + sync_status.clone(), latest_chain_tip.clone(), config.network.network, ); diff --git a/zebrad/src/components/mempool.rs b/zebrad/src/components/mempool.rs index a53895f1..d8821cba 100644 --- a/zebrad/src/components/mempool.rs +++ b/zebrad/src/components/mempool.rs @@ -30,7 +30,10 @@ use futures::{future::FutureExt, stream::Stream}; use tokio::sync::watch; use tower::{buffer::Buffer, timeout::Timeout, util::BoxService, Service}; -use zebra_chain::{block::Height, chain_tip::ChainTip, transaction::UnminedTxId}; +use zebra_chain::{ + block::Height, chain_sync_status::ChainSyncStatus, chain_tip::ChainTip, + transaction::UnminedTxId, +}; use zebra_consensus::{error::TransactionError, transaction}; use zebra_network as zn; use zebra_node_services::mempool::{Request, Response}; diff --git a/zebrad/src/components/mempool/crawler/tests/prop.rs b/zebrad/src/components/mempool/crawler/tests/prop.rs index 0f4de35a..f14ce04d 100644 --- a/zebrad/src/components/mempool/crawler/tests/prop.rs +++ b/zebrad/src/components/mempool/crawler/tests/prop.rs @@ -8,7 +8,9 @@ use proptest::{ }; use tokio::time; -use zebra_chain::{parameters::Network, transaction::UnminedTxId}; +use zebra_chain::{ + chain_sync_status::ChainSyncStatus, parameters::Network, transaction::UnminedTxId, +}; use zebra_network as zn; use zebra_node_services::mempool::Gossip; use zebra_state::ChainTipSender; diff --git a/zebrad/src/components/sync/progress.rs b/zebrad/src/components/sync/progress.rs index 91ed3c26..991d7e60 100644 --- a/zebrad/src/components/sync/progress.rs +++ b/zebrad/src/components/sync/progress.rs @@ -7,6 +7,7 @@ use num_integer::div_ceil; use zebra_chain::{ block::Height, + chain_sync_status::ChainSyncStatus, chain_tip::ChainTip, fmt::humantime_seconds, parameters::{Network, NetworkUpgrade, POST_BLOSSOM_POW_TARGET_SPACING}, diff --git a/zebrad/src/components/sync/status.rs b/zebrad/src/components/sync/status.rs index ae1f414f..01ac3853 100644 --- a/zebrad/src/components/sync/status.rs +++ b/zebrad/src/components/sync/status.rs @@ -1,4 +1,5 @@ use tokio::sync::watch; +use zebra_chain::chain_sync_status::ChainSyncStatus; use super::RecentSyncLengths; @@ -43,8 +44,28 @@ impl SyncStatus { Ok(()) } + /// Feed the given [`RecentSyncLengths`] it order to make the matching + /// [`SyncStatus`] report that it's close to the tip. + #[cfg(test)] + pub(crate) fn sync_close_to_tip(recent_syncs: &mut RecentSyncLengths) { + for _ in 0..RecentSyncLengths::MAX_RECENT_LENGTHS { + recent_syncs.push_extend_tips_length(1); + } + } + + /// Feed the given [`RecentSyncLengths`] it order to make the matching + /// [`SyncStatus`] report that it's not close to the tip. + #[cfg(test)] + pub(crate) fn sync_far_from_tip(recent_syncs: &mut RecentSyncLengths) { + for _ in 0..RecentSyncLengths::MAX_RECENT_LENGTHS { + recent_syncs.push_extend_tips_length(Self::MIN_DIST_FROM_TIP * 10); + } + } +} + +impl ChainSyncStatus for SyncStatus { /// Check if the synchronization is likely close to the chain tip. - pub fn is_close_to_tip(&self) -> bool { + fn is_close_to_tip(&self) -> bool { let sync_lengths = self.latest_sync_length.borrow(); // Return early if sync_lengths is empty. @@ -66,22 +87,4 @@ impl SyncStatus { // average sync length falls below the threshold. avg < Self::MIN_DIST_FROM_TIP } - - /// Feed the given [`RecentSyncLengths`] it order to make the matching - /// [`SyncStatus`] report that it's close to the tip. - #[cfg(test)] - pub(crate) fn sync_close_to_tip(recent_syncs: &mut RecentSyncLengths) { - for _ in 0..RecentSyncLengths::MAX_RECENT_LENGTHS { - recent_syncs.push_extend_tips_length(1); - } - } - - /// Feed the given [`RecentSyncLengths`] it order to make the matching - /// [`SyncStatus`] report that it's not close to the tip. - #[cfg(test)] - pub(crate) fn sync_far_from_tip(recent_syncs: &mut RecentSyncLengths) { - for _ in 0..RecentSyncLengths::MAX_RECENT_LENGTHS { - recent_syncs.push_extend_tips_length(Self::MIN_DIST_FROM_TIP * 10); - } - } } diff --git a/zebrad/src/components/sync/status/tests.rs b/zebrad/src/components/sync/status/tests.rs index 8402e924..31c300a4 100644 --- a/zebrad/src/components/sync/status/tests.rs +++ b/zebrad/src/components/sync/status/tests.rs @@ -3,6 +3,7 @@ use std::{env, sync::Arc, time::Duration}; use futures::{select, FutureExt}; use proptest::prelude::*; use tokio::{sync::Semaphore, time::timeout}; +use zebra_chain::chain_sync_status::ChainSyncStatus; use super::{super::RecentSyncLengths, SyncStatus};