diff --git a/api/src/foreign.rs b/api/src/foreign.rs index 70b056e2e2..4a5f618ce1 100644 --- a/api/src/foreign.rs +++ b/api/src/foreign.rs @@ -21,6 +21,7 @@ use crate::handlers::blocks_api::{BlockHandler, HeaderHandler}; use crate::handlers::chain_api::{ChainHandler, KernelHandler, OutputHandler}; use crate::handlers::pool_api::PoolHandler; use crate::handlers::transactions_api::TxHashSetHandler; +use crate::handlers::utils::ensure_not_syncing; use crate::handlers::version_api::VersionHandler; use crate::pool::{self, BlockChain, PoolAdapter, PoolEntry}; use crate::types::{ @@ -236,6 +237,7 @@ where include_proof: Option, _include_merkle_proof: Option, ) -> Result, Error> { + ensure_not_syncing(&self.sync_state)?; let output_handler = OutputHandler { chain: self.chain.clone(), }; @@ -267,6 +269,7 @@ where max: u64, include_proof: Option, ) -> Result { + ensure_not_syncing(&self.sync_state)?; let output_handler = OutputHandler { chain: self.chain.clone(), }; @@ -288,6 +291,8 @@ where start_block_height: u64, end_block_height: Option, ) -> Result { + // Wallet scan/info hits this early; return a clear signal while syncing (#3546). + ensure_not_syncing(&self.sync_state)?; let txhashset_handler = TxHashSetHandler { chain: self.chain.clone(), }; diff --git a/api/src/handlers/transactions_api.rs b/api/src/handlers/transactions_api.rs index 9da0a3d515..a15db90cec 100644 --- a/api/src/handlers/transactions_api.rs +++ b/api/src/handlers/transactions_api.rs @@ -106,7 +106,13 @@ impl TxHashSetHandler { let chain = w(&self.chain)?; let range = chain .block_height_range_to_pmmr_indices(start_block_height, end_block_height) - .map_err(|_| Error::NotFound)?; + .map_err(|e| { + // Prefer a descriptive error over empty NotFound (wallet misreports null). + Error::Argument(format!( + "cannot map block heights {}..{:?} to PMMR indices: {}", + start_block_height, end_block_height, e + )) + })?; let out = OutputListing { last_retrieved_index: range.0, highest_index: range.1, diff --git a/api/src/handlers/utils.rs b/api/src/handlers/utils.rs index 85b2b39ddb..fa336351b2 100644 --- a/api/src/handlers/utils.rs +++ b/api/src/handlers/utils.rs @@ -14,6 +14,7 @@ use crate::chain; use crate::chain::types::CommitPos; +use crate::chain::SyncState; use crate::core::core::OutputIdentifier; use crate::rest::*; use crate::types::*; @@ -29,6 +30,18 @@ pub fn w(weak: &Weak) -> Result, Error> { .ok_or_else(|| Error::Internal("failed to upgrade weak reference".to_owned())) } +/// Fail wallet-oriented queries while the node is still syncing so clients get a +/// clear "try again later" signal instead of a misleading NotFound (#3546). +pub fn ensure_not_syncing(sync_state: &Weak) -> Result<(), Error> { + let sync = w(sync_state)?; + if sync.is_syncing() { + return Err(Error::Unavailable( + "node is still syncing; wait until sync completes and retry".into(), + )); + } + Ok(()) +} + /// Internal function to retrieves an output by a given commitment fn get_unspent( chain: &Arc, @@ -88,3 +101,31 @@ pub fn get_output_v2( Ok(Some((output_printable, out))) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::chain::types::SyncStatus; + use crate::chain::SyncState; + use std::sync::Arc; + + #[test] + fn ensure_not_syncing_ok_when_no_sync() { + let sync = Arc::new(SyncState::new()); + sync.update(SyncStatus::NoSync); + let weak = Arc::downgrade(&sync); + assert!(ensure_not_syncing(&weak).is_ok()); + } + + #[test] + fn ensure_not_syncing_unavailable_while_syncing() { + let sync = Arc::new(SyncState::new()); + // Default SyncState is Initial → is_syncing() true + let weak = Arc::downgrade(&sync); + let err = ensure_not_syncing(&weak).unwrap_err(); + match err { + Error::Unavailable(msg) => assert!(msg.contains("syncing")), + other => panic!("expected Unavailable, got {:?}", other), + } + } +} diff --git a/api/src/rest.rs b/api/src/rest.rs index b0b151874e..c104b524e6 100644 --- a/api/src/rest.rs +++ b/api/src/rest.rs @@ -49,6 +49,10 @@ pub enum Error { Argument(String), #[error("Not found.")] NotFound, + /// Node is not ready yet (e.g. still syncing). Prefer this over [`NotFound`] + /// so clients can distinguish "missing data" from "try again later" (#3546). + #[error("Service unavailable: {0}")] + Unavailable(String), #[error("Request error: {0}")] RequestError(String), #[error("ResponseError error: {0}")] diff --git a/api/src/web.rs b/api/src/web.rs index 4795108d58..34682fc61a 100644 --- a/api/src/web.rs +++ b/api/src/web.rs @@ -52,6 +52,7 @@ where Error::Argument(msg) => response(StatusCode::BAD_REQUEST, msg), Error::RequestError(msg) => response(StatusCode::BAD_REQUEST, msg), Error::NotFound => response(StatusCode::NOT_FOUND, "".into()), + Error::Unavailable(msg) => response(StatusCode::SERVICE_UNAVAILABLE, msg), Error::Internal(msg) => response(StatusCode::INTERNAL_SERVER_ERROR, msg), Error::ResponseError(msg) => response(StatusCode::INTERNAL_SERVER_ERROR, msg), Error::Router { .. } => response(StatusCode::INTERNAL_SERVER_ERROR, "".into()), diff --git a/servers/src/mining/stratumserver.rs b/servers/src/mining/stratumserver.rs index d39312a560..75547d7611 100644 --- a/servers/src/mining/stratumserver.rs +++ b/servers/src/mining/stratumserver.rs @@ -878,11 +878,23 @@ impl StratumServer { let handler = Arc::new(Handler::from_stratum(&self)); let h = handler.clone(); + // Wait until the node has finished syncing before accepting miner connections + // (#3546). Previously we listened early and returned confusing "syncing" / + // failed job responses while the chain was still catching up. + info!( + "(Server ID: {}) Stratum waiting for node sync before listening on {}", + self.id, + self.config.stratum_server_addr.clone().unwrap() + ); + while self.sync_state.is_syncing() { + thread::sleep(Duration::from_millis(50)); + } + let _listener_th = thread::spawn(move || { accept_connections(listen_addr, h); }); - // We have started + // We have started (only after sync so is_running matches actual readiness). { let mut stratum_stats = self.stratum_stats.write(); stratum_stats.is_running = true; @@ -895,11 +907,6 @@ impl StratumServer { self.config.stratum_server_addr.clone().unwrap() ); - // Initial Loop. Waiting node complete syncing - while self.sync_state.is_syncing() { - thread::sleep(Duration::from_millis(50)); - } - handler.run(&self.config, &self.tx_pool); } // fn run_loop() } // StratumServer