diff --git a/bitcoin-core-sv2/Cargo.toml b/bitcoin-core-sv2/Cargo.toml index 5b4757322..98bad552e 100644 --- a/bitcoin-core-sv2/Cargo.toml +++ b/bitcoin-core-sv2/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "bitcoin_core_sv2" description = "A library to get Stratum V2 Template Distribution Protocol from Bitcoin Core over IPC" -version = "0.1.1" +version = "0.2.0" edition = "2024" license = "MIT OR Apache-2.0" authors = ["The Stratum V2 Developers"] @@ -12,8 +12,8 @@ keywords = ["stratum", "mining", "bitcoin", "bitcoin-core"] [dependencies] tracing = "0.1.41" tracing-subscriber = "0.3.19" -capnp = "0.21.4" -capnp-rpc = "0.21.0" +capnp = "0.25.0" +capnp-rpc = "0.25.0" tokio = { version = "1.44.1", features = ["full"] } tokio-util = { version = "0.7.10", features = ["codec", "compat"] } async-channel = "1.5.1" @@ -23,4 +23,4 @@ async-channel = "1.5.1" # with the proper version of stratum-core being fetched from crates.io as well stratum-core = { git = "https://github.com/stratum-mining/stratum", branch = "main" } -bitcoin-capnp-types = "0.1.0" +bitcoin-capnp-types = "0.2.0" diff --git a/bitcoin-core-sv2/README.md b/bitcoin-core-sv2/README.md index 2d003bdfc..b81a699d6 100644 --- a/bitcoin-core-sv2/README.md +++ b/bitcoin-core-sv2/README.md @@ -36,9 +36,17 @@ The `fee_threshold` parameter (in satoshis) determines when a new template is di The `min_interval` parameter (in seconds) determines the minimum amount of time between two consecutive `NewTemplate` messages (with exception to Chain Tip updates, which are always sent immediately, followed by `SetNewPrevHash`). +## Version Compatibility + +| `bitcoin_core_sv2` | Bitcoin Core | +|--------------------|--------------| +| v0.1.0 | v30.2 | +| v0.1.1 | v30.2 | +| v0.2.0 | v31.0 | + ## License Licensed under either of: - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) -- MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) \ No newline at end of file +- MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) diff --git a/bitcoin-core-sv2/src/job_declaration_protocol/error.rs b/bitcoin-core-sv2/src/job_declaration_protocol/error.rs index 9c2f4b62b..385e202fd 100644 --- a/bitcoin-core-sv2/src/job_declaration_protocol/error.rs +++ b/bitcoin-core-sv2/src/job_declaration_protocol/error.rs @@ -8,6 +8,8 @@ use stratum_core::bitcoin::consensus; pub enum BitcoinCoreSv2JDPError { /// Cap'n Proto RPC error. CapnpError(capnp::Error), + /// Failed to create a dedicated thread IPC client, capturing the underlying context. + FailedToCreateThreadIpcClient(String), /// Failed to connect to the Bitcoin Core Unix socket. CannotConnectToUnixSocket(PathBuf, String), /// Failed to deserialize a block from the IPC response. diff --git a/bitcoin-core-sv2/src/job_declaration_protocol/handlers.rs b/bitcoin-core-sv2/src/job_declaration_protocol/handlers.rs index fe4bb776a..323412a50 100644 --- a/bitcoin-core-sv2/src/job_declaration_protocol/handlers.rs +++ b/bitcoin-core-sv2/src/job_declaration_protocol/handlers.rs @@ -20,9 +20,9 @@ impl BitcoinCoreSv2JDP { /// Validates a declared mining job by checking transaction availability and block structure. /// /// Adds missing transactions to the mempool mirror, verifies all transactions are available, - /// assembles a test block, and uses Bitcoin Core's `checkBlock` to validate the block - /// structure. Returns success with current template parameters or an error if validation - /// fails. + /// assembles a test block, sets IPC thread context, and uses Bitcoin Core's `checkBlock` to + /// validate the block structure. Returns success with current template parameters or an error + /// if validation fails. pub(crate) async fn handle_declare_mining_job( &self, version: Version, @@ -142,11 +142,26 @@ impl BitcoinCoreSv2JDP { ); let mut check_block_request = self.mining_ipc_client.check_block_request(); - let mut check_block_params = check_block_request.get(); - check_block_params.set_block(&block_bytes); + match check_block_request.get().get_context() { + Ok(mut context) => context.set_thread(self.thread_ipc_client.clone()), + Err(e) => { + tracing::error!("Failed to set check block request thread context: {e}"); + // send error response to the client + // deliberately ignore potential send errors + let _ = response_tx.send(JdResponse::Error { + error_code: "internal-error".to_string(), + validation_context: initial_validation_context, + }); + tracing::warn!("Terminating Sv2 Bitcoin Core IPC Connection"); + self.cancellation_token.cancel(); + return; + } + } + + check_block_request.get().set_block(&block_bytes); - let mut options = match check_block_params.get_options() { + let mut options = match check_block_request.get().get_options() { Ok(options) => options, Err(e) => { tracing::error!("Failed to get check block options: {e}"); diff --git a/bitcoin-core-sv2/src/job_declaration_protocol/mod.rs b/bitcoin-core-sv2/src/job_declaration_protocol/mod.rs index 3ea97445c..305127d83 100644 --- a/bitcoin-core-sv2/src/job_declaration_protocol/mod.rs +++ b/bitcoin-core-sv2/src/job_declaration_protocol/mod.rs @@ -48,6 +48,7 @@ mod monitors; /// Incoming [`PushSolution`] requests are used to submit mining solutions to Bitcoin Core. #[derive(Clone)] pub struct BitcoinCoreSv2JDP { + thread_map: ThreadMapIpcClient, thread_ipc_client: ThreadIpcClient, mining_ipc_client: MiningIpcClient, current_template_ipc_client: Rc>, @@ -120,6 +121,10 @@ impl BitcoinCoreSv2JDP { let mining_ipc_client: MiningIpcClient = mining_client_response.get()?.get_result()?; let mut template_ipc_client_request = mining_ipc_client.create_new_block_request(); + template_ipc_client_request + .get() + .get_context()? + .set_thread(thread_ipc_client.clone()); let mut template_ipc_client_request_options = template_ipc_client_request .get() .get_options() @@ -130,14 +135,25 @@ impl BitcoinCoreSv2JDP { template_ipc_client_request_options.set_use_mempool(true); tracing::debug!("Sending createNewBlock request to Bitcoin Core"); - let template_ipc_client_response = template_ipc_client_request - .send() - .promise - .await - .map_err(|e| { - tracing::error!("Failed to send template IPC client request: {}", e); - e - })?; + let create_new_block_promise = template_ipc_client_request.send().promise; + // During IBD this startup call can block for a long time, so shutdown must interrupt the + // in-flight request instead of only abandoning the outer wait loop. + let template_ipc_client_response = tokio::select! { + template_ipc_client_response = create_new_block_promise => { + template_ipc_client_response.map_err(|e| { + tracing::error!("Failed to send template IPC client request: {}", e); + e + })? + } + _ = cancellation_token.cancelled() => { + tracing::debug!("Interrupting initial createNewBlock request"); + Self::interrupt_create_new_block_request(&mining_ipc_client).await?; + return Err(capnp::Error::failed( + "createNewBlock request interrupted during shutdown".to_string(), + ) + .into()); + } + }; let template_ipc_client_result = template_ipc_client_response.get().map_err(|e| { tracing::error!("Failed to get template IPC client result: {}", e); @@ -152,6 +168,7 @@ impl BitcoinCoreSv2JDP { info!("IPC JDP client successfully created."); let self_ = Self { + thread_map, thread_ipc_client, mining_ipc_client, current_template_ipc_client: Rc::new(RefCell::new(template_ipc_client)), @@ -178,6 +195,45 @@ impl BitcoinCoreSv2JDP { Ok(self_) } + /// Creates a new dedicated thread IPC client. + async fn new_thread_ipc_client(&self) -> Result { + let thread_request = self.thread_map.make_thread_request(); + let thread_response = thread_request.send().promise.await.map_err(|e| { + let details = format!("Failed to send make_thread request: {}", e); + tracing::error!("{}", details); + BitcoinCoreSv2JDPError::FailedToCreateThreadIpcClient(details) + })?; + + let thread_ipc_client = thread_response + .get() + .map_err(|e| { + let details = format!("Failed to read make_thread response: {}", e); + tracing::error!("{}", details); + BitcoinCoreSv2JDPError::FailedToCreateThreadIpcClient(details) + })? + .get_result() + .map_err(|e| { + let details = format!("Failed to get thread IPC client: {}", e); + tracing::error!("{}", details); + BitcoinCoreSv2JDPError::FailedToCreateThreadIpcClient(details) + })?; + + Ok(thread_ipc_client) + } + + /// Interrupts an in-flight `createNewBlock` request during startup shutdown. + async fn interrupt_create_new_block_request( + mining_ipc_client: &MiningIpcClient, + ) -> Result<(), BitcoinCoreSv2JDPError> { + let interrupt_request = mining_ipc_client.interrupt_request(); + if let Err(e) = interrupt_request.send().promise.await { + tracing::error!("Failed to send interrupt createNewBlock request: {}", e); + return Err(BitcoinCoreSv2JDPError::CapnpError(e)); + } + + Ok(()) + } + /// Main event loop - runs in a LocalSet on dedicated thread. /// /// Spawns the monitor task and processes incoming job declaration requests until shutdown. @@ -194,14 +250,10 @@ impl BitcoinCoreSv2JDP { break; } - // Process incoming requests - // Note: requests are processed sequentially for two reasons: - // 1. This loop awaits each request before reading the next one - // 2. On the Bitcoin Core side, `checkBlock` lacks a `context :Proxy.Context` - // parameter in its capnp definition (mining.capnp), so it runs synchronously - // on the Cap'n Proto event loop thread, blocking all other IPC operations on - // this connection until it completes - // Pending requests are unboundedly buffered in the async_channel + // Process incoming requests. + // Requests are handled sequentially because this loop awaits each request before + // reading the next one. + // Pending requests are unboundedly buffered in the async_channel. request = self.incoming_requests.recv() => { match request { Ok(request) => { @@ -280,6 +332,15 @@ impl BitcoinCoreSv2JDP { let mut create_new_block_request = self.mining_ipc_client.create_new_block_request(); + create_new_block_request + .get() + .get_context() + .map_err(|e| { + tracing::error!("Failed to get template IPC client request context: {e}"); + e + })? + .set_thread(self.thread_ipc_client.clone()); + let mut create_new_block_options = create_new_block_request.get().get_options().map_err(|e| { tracing::error!("Failed to get createNewBlock options: {e}"); diff --git a/bitcoin-core-sv2/src/job_declaration_protocol/monitors.rs b/bitcoin-core-sv2/src/job_declaration_protocol/monitors.rs index 55495f055..90649a4f3 100644 --- a/bitcoin-core-sv2/src/job_declaration_protocol/monitors.rs +++ b/bitcoin-core-sv2/src/job_declaration_protocol/monitors.rs @@ -12,6 +12,16 @@ impl BitcoinCoreSv2JDP { tokio::task::spawn_local(async move { tracing::debug!("monitor_mempool_mirror() task started"); + tracing::debug!("Creating dedicated blocking_thread_ipc_client for waitNext requests"); + let blocking_thread_ipc_client = match self_clone.new_thread_ipc_client().await { + Ok(blocking_thread_ipc_client) => blocking_thread_ipc_client, + Err(e) => { + tracing::error!("Failed to create blocking thread IPC client: {:?}", e); + tracing::warn!("Terminating Sv2 Bitcoin Core IPC Connection"); + self_clone.cancellation_token.cancel(); + return; + } + }; tracing::debug!("monitor_mempool_mirror() entering main loop"); loop { @@ -22,7 +32,7 @@ impl BitcoinCoreSv2JDP { .wait_next_request(); match wait_next_request.get().get_context() { - Ok(mut context) => context.set_thread(self_clone.thread_ipc_client.clone()), + Ok(mut context) => context.set_thread(blocking_thread_ipc_client.clone()), Err(e) => { tracing::error!("Failed to set thread: {}", e); self_clone.cancellation_token.cancel(); @@ -39,13 +49,19 @@ impl BitcoinCoreSv2JDP { } }; - // 0 sat fee threshold (accept all mempool transactions) + // Rebuild aggressively instead of waiting only for tip changes. + // Bitcoin Core reevaluates fee growth on a 1s tick, and with + // fee_threshold = 0 it returns any candidate whose total fees + // are not lower than the current template. In steady state this + // usually produces a new BlockTemplate about once per second. wait_next_request_options.set_fee_threshold(0); - // 10 seconds timeout for waitNext requests - // please note that this is NOT how often we expect to get new templates - // it's just the max time we'll wait for the current waitNext request to complete - wait_next_request_options.set_timeout(10_000.0); + // Bound how long a single waitNext call can stay attached to + // one BlockTemplate before the loop recreates it from the + // latest current_template_ipc_client when Bitcoin Core does not + // produce a returnable candidate. This is a fallback, not the + // expected cadence of template updates. + wait_next_request_options.set_timeout(3_000.0); tokio::select! { _ = self_clone.cancellation_token.cancelled() => { diff --git a/bitcoin-core-sv2/src/template_distribution_protocol/error.rs b/bitcoin-core-sv2/src/template_distribution_protocol/error.rs index 1f5041977..047ba2a8c 100644 --- a/bitcoin-core-sv2/src/template_distribution_protocol/error.rs +++ b/bitcoin-core-sv2/src/template_distribution_protocol/error.rs @@ -23,9 +23,12 @@ pub enum BitcoinCoreSv2TDPError { FailedToSubmitSolution, FailedToSetThread, FailedToGetWaitNextRequestOptions, + CreateNewBlockRequestInterrupted, + FailedToSendInterruptCreateNewBlockRequest, FailedToSendInterruptWaitRequest, FailedToWaitForMonitorIpcTemplatesTask, FailedToCreateSolutionDir, + InvalidBlockRewardRemaining(i64), } impl From for BitcoinCoreSv2TDPError { @@ -53,6 +56,7 @@ pub enum TemplateDataError { CapnpError(capnp::Error), FailedIpcSubmitSolution, FailedToSerializeEmptyCoinbaseOutputs, + FailedToSerializeCoinbaseOutputs, FailedToConvertMerklePathHashToU256, FailedToCreateMerklePathSeq, BitcoinCoreSv2TDPError(BitcoinCoreSv2TDPError), @@ -95,6 +99,9 @@ impl std::fmt::Display for TemplateDataError { TemplateDataError::FailedToSerializeEmptyCoinbaseOutputs => { write!(f, "Failed to serialize empty coinbase outputs") } + TemplateDataError::FailedToSerializeCoinbaseOutputs => { + write!(f, "Failed to serialize coinbase outputs") + } TemplateDataError::FailedToSumCoinbaseOutputs => { write!(f, "Failed to sum coinbase outputs") } diff --git a/bitcoin-core-sv2/src/template_distribution_protocol/mod.rs b/bitcoin-core-sv2/src/template_distribution_protocol/mod.rs index b8857ccd6..b9c53fe08 100644 --- a/bitcoin-core-sv2/src/template_distribution_protocol/mod.rs +++ b/bitcoin-core-sv2/src/template_distribution_protocol/mod.rs @@ -9,6 +9,7 @@ use bitcoin_capnp_types::{ Client as BlockTemplateIpcClient, wait_next_params::Owned as WaitNextParams, wait_next_results::Owned as WaitNextResults, }, + coinbase_tx, mining::Client as MiningIpcClient, }, proxy_capnp::{thread::Client as ThreadIpcClient, thread_map::Client as ThreadMapIpcClient}, @@ -26,7 +27,13 @@ use std::{ }; use stratum_core::{ binary_sv2::U256, - bitcoin::{Transaction, block::Header, consensus::deserialize}, + bitcoin::{ + OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Witness, + absolute::LockTime, + block::Header, + consensus::{Decodable, deserialize}, + transaction::Version as TransactionVersion, + }, parsers_sv2::TemplateDistribution, template_distribution_sv2::CoinbaseOutputConstraints, }; @@ -225,6 +232,12 @@ impl BitcoinCoreSv2TDP { ); break; } + Err(BitcoinCoreSv2TDPError::CreateNewBlockRequestInterrupted) => { + tracing::debug!( + "Initial createNewBlock request interrupted during shutdown" + ); + return; + } Err(e) => { tracing::error!( "Failed to bootstrap initial template IPC client: {:?}", @@ -321,21 +334,14 @@ impl BitcoinCoreSv2TDP { .get_context()? .set_thread(thread_ipc_client.clone()); - let coinbase_tx_bytes = coinbase_tx_request - .send() - .promise - .await? - .get()? - .get_result()? - .to_vec(); - - // Deserialize the coinbase tx from Bitcoin Core's serialization format + let coinbase_tx_response = coinbase_tx_request.send().promise.await?; + let coinbase_tx_result = coinbase_tx_response.get()?; + let coinbase_tx_reader = coinbase_tx_result.get_result()?; + let (coinbase_tx, block_reward_remaining) = coinbase_tx_from_ipc(coinbase_tx_reader)?; tracing::debug!( - "Deserializing coinbase tx ({} bytes)", - coinbase_tx_bytes.len() + "Coinbase tx built from getCoinbaseTx result: {:?}", + coinbase_tx ); - let coinbase_tx: Transaction = deserialize(&coinbase_tx_bytes)?; - tracing::debug!("Coinbase tx deserialized: {:?}", coinbase_tx); let mut merkle_path_request = template_ipc_client.get_coinbase_merkle_path_request(); merkle_path_request @@ -358,6 +364,7 @@ impl BitcoinCoreSv2TDP { template_id, header, coinbase_tx, + block_reward_remaining, merkle_path, template_ipc_client, ); @@ -510,6 +517,16 @@ impl BitcoinCoreSv2TDP { ); let mut template_ipc_client_request = self.mining_ipc_client.create_new_block_request(); + + template_ipc_client_request + .get() + .get_context() + .map_err(|e| { + tracing::error!("Failed to get template IPC client request context: {e}"); + e + })? + .set_thread(self.thread_ipc_client.clone()); + let mut template_ipc_client_request_options = template_ipc_client_request .get() .get_options() @@ -529,14 +546,20 @@ impl BitcoinCoreSv2TDP { template_ipc_client_request_options.set_use_mempool(true); tracing::debug!("Sending createNewBlock request to Bitcoin Core"); - let template_ipc_client_response = template_ipc_client_request - .send() - .promise - .await - .map_err(|e| { - tracing::error!("Failed to send template IPC client request: {}", e); - e - })?; + let create_new_block_promise = template_ipc_client_request.send().promise; + let template_ipc_client_response = tokio::select! { + template_ipc_client_response = create_new_block_promise => { + template_ipc_client_response.map_err(|e| { + tracing::error!("Failed to send template IPC client request: {}", e); + e + })? + } + _ = self.global_cancellation_token.cancelled() => { + tracing::debug!("Interrupting createNewBlock request"); + self.interrupt_create_new_block_request().await?; + return Err(BitcoinCoreSv2TDPError::CreateNewBlockRequestInterrupted); + } + }; let template_ipc_client_result = template_ipc_client_response.get().map_err(|e| { tracing::error!("Failed to get template IPC client result: {}", e); @@ -577,6 +600,16 @@ impl BitcoinCoreSv2TDP { Ok(()) } + async fn interrupt_create_new_block_request(&self) -> Result<(), BitcoinCoreSv2TDPError> { + let interrupt_request = self.mining_ipc_client.interrupt_request(); + if let Err(e) = interrupt_request.send().promise.await { + tracing::error!("Failed to send interrupt createNewBlock request: {}", e); + return Err(BitcoinCoreSv2TDPError::FailedToSendInterruptCreateNewBlockRequest); + } + + Ok(()) + } + async fn new_wait_next_request( &self, template_ipc_client: &BlockTemplateIpcClient, @@ -692,3 +725,92 @@ impl BitcoinCoreSv2TDP { }); } } + +fn coinbase_tx_from_ipc( + coinbase_tx: coinbase_tx::Reader<'_>, +) -> Result<(Transaction, u64), BitcoinCoreSv2TDPError> { + let block_reward_remaining: i64 = coinbase_tx.get_block_reward_remaining(); + let block_reward_remaining: u64 = block_reward_remaining + .try_into() + .map_err(|_| BitcoinCoreSv2TDPError::InvalidBlockRewardRemaining(block_reward_remaining))?; + + let witness = { + let witness_bytes = coinbase_tx.get_witness()?; + let mut witness = Witness::new(); + if !witness_bytes.is_empty() { + witness.push(witness_bytes); + } + witness + }; + + let mut required_outputs = Vec::new(); + for output_bytes in coinbase_tx.get_required_outputs()?.iter() { + let output_bytes = output_bytes?; + required_outputs.push(TxOut::consensus_decode(&mut &output_bytes[..])?); + } + + let transaction = Transaction { + version: TransactionVersion::non_standard(coinbase_tx.get_version() as i32), + lock_time: LockTime::from_consensus(coinbase_tx.get_lock_time()), + input: vec![TxIn { + previous_output: OutPoint::null(), + script_sig: ScriptBuf::from_bytes(coinbase_tx.get_script_sig_prefix()?.to_vec()), + sequence: Sequence::from_consensus(coinbase_tx.get_sequence()), + witness, + }], + output: required_outputs, + }; + + Ok((transaction, block_reward_remaining)) +} + +#[cfg(test)] +mod tests { + use super::*; + use stratum_core::bitcoin::{Amount, consensus::serialize}; + + #[test] + fn coinbase_tx_from_ipc_builds_transaction_from_struct_fields() { + let required_output = TxOut { + value: Amount::ZERO, + script_pubkey: ScriptBuf::from_bytes(vec![0x6a, 0x24]), + }; + let required_output_bytes = serialize(&required_output); + + let mut message = capnp::message::Builder::new_default(); + let mut coinbase_tx_builder: coinbase_tx::Builder<'_> = message.init_root(); + coinbase_tx_builder.set_version(2); + coinbase_tx_builder.set_sequence(0xffff_fffe); + coinbase_tx_builder.set_script_sig_prefix(&[0x03, 0xaa, 0xbb, 0xcc]); + coinbase_tx_builder.set_witness(&[0x42; 32]); + coinbase_tx_builder.set_block_reward_remaining(5_000_000_000); + coinbase_tx_builder.set_lock_time(840_000); + { + let mut required_outputs = coinbase_tx_builder.reborrow().init_required_outputs(1); + required_outputs.set(0, &required_output_bytes); + } + + let coinbase_tx_reader = coinbase_tx_builder.into_reader(); + let (coinbase_tx, value_remaining) = + coinbase_tx_from_ipc(coinbase_tx_reader).expect("coinbase tx should convert"); + + println!("coinbase_tx: {:?}", coinbase_tx); + + assert_eq!(value_remaining, 5_000_000_000); + assert_eq!(coinbase_tx.version, TransactionVersion::TWO); + assert_eq!(coinbase_tx.lock_time.to_consensus_u32(), 840_000); + assert_eq!(coinbase_tx.input.len(), 1); + assert_eq!(coinbase_tx.input[0].previous_output, OutPoint::null()); + assert_eq!( + coinbase_tx.input[0].sequence, + Sequence::from_consensus(0xffff_fffe) + ); + assert_eq!( + coinbase_tx.input[0].script_sig.as_bytes(), + &[0x03, 0xaa, 0xbb, 0xcc] + ); + assert_eq!(coinbase_tx.input[0].witness.len(), 1); + assert_eq!(&coinbase_tx.input[0].witness[0], &[0x42; 32]); + assert_eq!(coinbase_tx.output, vec![required_output]); + } +} diff --git a/bitcoin-core-sv2/src/template_distribution_protocol/template_data.rs b/bitcoin-core-sv2/src/template_distribution_protocol/template_data.rs index 238371645..63cae2341 100644 --- a/bitcoin-core-sv2/src/template_distribution_protocol/template_data.rs +++ b/bitcoin-core-sv2/src/template_distribution_protocol/template_data.rs @@ -7,7 +7,6 @@ use bitcoin_capnp_types::{ use std::{fs::File, io::Write, path::Path}; use stratum_core::bitcoin::{ Target, Transaction, TxOut, - amount::{Amount, CheckedSum}, block::{Block, Header, Version}, consensus::{deserialize, serialize}, hashes::{Hash, HashEngine, sha256d}, @@ -25,6 +24,7 @@ pub struct TemplateData { template_id: u64, header: Header, coinbase_tx: Transaction, + block_reward_remaining: u64, merkle_path: Vec>, template_ipc_client: BlockTemplateIpcClient, } @@ -35,6 +35,7 @@ impl TemplateData { template_id: u64, header: Header, coinbase_tx: Transaction, + block_reward_remaining: u64, merkle_path: Vec>, template_ipc_client: BlockTemplateIpcClient, ) -> Self { @@ -42,6 +43,7 @@ impl TemplateData { template_id, header, coinbase_tx, + block_reward_remaining, merkle_path, template_ipc_client, } @@ -80,9 +82,9 @@ impl TemplateData { coinbase_tx_version: self.get_coinbase_tx_version()?, coinbase_prefix: self.get_coinbase_script_sig()?, coinbase_tx_input_sequence: self.get_coinbase_input_sequence(), - coinbase_tx_value_remaining: self.get_coinbase_tx_value_remaining()?, - coinbase_tx_outputs_count: self.get_empty_coinbase_outputs().len() as u32, - coinbase_tx_outputs: self.get_serialized_empty_coinbase_outputs()?, + coinbase_tx_value_remaining: self.block_reward_remaining, + coinbase_tx_outputs_count: self.get_required_coinbase_outputs().len() as u32, + coinbase_tx_outputs: self.get_serialized_required_coinbase_outputs()?, coinbase_tx_locktime: self.get_coinbase_tx_lock_time(), merkle_path: self.get_merkle_path()?, }; @@ -327,36 +329,19 @@ impl TemplateData { self.coinbase_tx.input[0].sequence.to_consensus_u32() } - fn get_empty_coinbase_outputs(&self) -> Vec { - self.coinbase_tx - .output - .iter() - .filter(|output| output.value == Amount::from_sat(0)) - .cloned() - .collect() + fn get_required_coinbase_outputs(&self) -> &[TxOut] { + &self.coinbase_tx.output } - fn get_serialized_empty_coinbase_outputs(&self) -> Result, TemplateDataError> { - let empty_coinbase_outputs = self.get_empty_coinbase_outputs(); - let mut serialized_empty_coinbase_outputs = Vec::new(); - for output in empty_coinbase_outputs { - serialized_empty_coinbase_outputs.extend_from_slice(&serialize(&output)); + fn get_serialized_required_coinbase_outputs(&self) -> Result, TemplateDataError> { + let mut serialized_required_coinbase_outputs = Vec::new(); + for output in self.get_required_coinbase_outputs() { + serialized_required_coinbase_outputs.extend_from_slice(&serialize(output)); } - let serialized_empty_coinbase_outputs: B064K = serialized_empty_coinbase_outputs + let serialized_required_coinbase_outputs: B064K = serialized_required_coinbase_outputs .try_into() - .map_err(|_| TemplateDataError::FailedToSerializeEmptyCoinbaseOutputs)?; - Ok(serialized_empty_coinbase_outputs) - } - - fn get_coinbase_tx_value_remaining(&self) -> Result { - Ok(self - .coinbase_tx - .output - .iter() - .map(|output| output.value) - .checked_sum() - .ok_or(TemplateDataError::FailedToSumCoinbaseOutputs)? - .to_sat()) + .map_err(|_| TemplateDataError::FailedToSerializeCoinbaseOutputs)?; + Ok(serialized_required_coinbase_outputs) } fn get_coinbase_tx_lock_time(&self) -> u32 { diff --git a/integration-tests/Cargo.lock b/integration-tests/Cargo.lock index 30681895f..24e7d8267 100644 --- a/integration-tests/Cargo.lock +++ b/integration-tests/Cargo.lock @@ -492,9 +492,9 @@ dependencies = [ [[package]] name = "bitcoin-capnp-types" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e20759e30b46af17a13f2e34c9e090c3672938b5a0c22358cba971d1a8f5d492" +checksum = "a58af19b421a85566a1d46617f40b18eea77d565a0271bdff2deabf0d27c075c" dependencies = [ "capnp", "capnpc", @@ -533,7 +533,7 @@ dependencies = [ [[package]] name = "bitcoin_core_sv2" -version = "0.1.1" +version = "0.2.0" dependencies = [ "async-channel 1.9.0", "bitcoin-capnp-types", @@ -677,18 +677,18 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "capnp" -version = "0.21.7" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e92edec8974fcd7ece90bb021db782abe14a61c10c817f197f700fef7430eb8" +checksum = "c982cc37b8f646c753f3b0a24d4d40ca2eac8a9c2b9ea6fff524be67ddc184cb" dependencies = [ "embedded-io", ] [[package]] name = "capnp-futures" -version = "0.21.0" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d04478adeb234836f886ec554a0d96e3af3a939ba7b3962af5addddf7ab71231" +checksum = "73b69dfddccc57844f9a90f9d72b44b97c326914851ea94fb7da40ef9cad6e8d" dependencies = [ "capnp", "futures-channel", @@ -697,9 +697,9 @@ dependencies = [ [[package]] name = "capnp-rpc" -version = "0.21.0" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85e9c19ef52ff1b9c9822fb21bfa68a72bc58711676295ff06eb88e64c7877f7" +checksum = "07ccca6d26009f4d6c12b741994f33b421da613b5dcf461508e236b53ef862f1" dependencies = [ "capnp", "capnp-futures", @@ -708,9 +708,9 @@ dependencies = [ [[package]] name = "capnpc" -version = "0.21.4" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6da96dcb0a0e0c526daf42bac55e1550f18ad973df9ef9ba75204f332c80ad16" +checksum = "d6cdfa6b0df161a71201367910265b97180541ecdb48bd08e05ef8694c295d1f" dependencies = [ "capnp", ] @@ -1202,9 +1202,9 @@ checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "embedded-io" -version = "0.6.1" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" +checksum = "9eb1aa714776b75c7e67e1da744b81a129b3ff919c8712b5e1b32252c1f07cc7" [[package]] name = "encoding_rs" @@ -1862,7 +1862,7 @@ dependencies = [ [[package]] name = "integration_tests_sv2" -version = "0.1.1" +version = "0.2.0" dependencies = [ "async-channel 1.9.0", "clap", @@ -1932,7 +1932,7 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jd_client_sv2" -version = "0.1.4" +version = "0.2.0" dependencies = [ "async-channel 1.9.0", "bitcoin_core_sv2", @@ -1948,7 +1948,7 @@ dependencies = [ [[package]] name = "jd_server_sv2" -version = "0.1.2" +version = "0.2.0" dependencies = [ "async-channel 1.9.0", "async-trait", @@ -2475,7 +2475,7 @@ dependencies = [ [[package]] name = "pool_sv2" -version = "0.2.2" +version = "0.3.0" dependencies = [ "async-channel 1.9.0", "bitcoin_core_sv2", diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml index fd7558b25..68c8979b0 100644 --- a/integration-tests/Cargo.toml +++ b/integration-tests/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "integration_tests_sv2" -version = "0.1.1" +version = "0.2.0" authors = ["The Stratum V2 Developers"] edition = "2021" description = "Sv2 Integration Tests Framework" @@ -15,8 +15,8 @@ exclude = ["resources/high_diff_chain.tar.gz"] [dependencies] stratum-apps = { version = "0.4.0", path = "../stratum-apps", features = ["network", "config"] } -jd_client_sv2 = { version = "0.1.3", path = "../miner-apps/jd-client" } -pool_sv2 = { version = "0.2.1", path = "../pool-apps/pool" } +jd_client_sv2 = { version = "0.2.0", path = "../miner-apps/jd-client" } +pool_sv2 = { version = "0.3.0", path = "../pool-apps/pool" } translator_sv2 = { version = "0.2.1", path = "../miner-apps/translator" } async-channel = { version = "1.5.1", default-features = false } corepc-node = { version = "0.7.0", default-features = false, features = ["28_0"] } diff --git a/integration-tests/README.md b/integration-tests/README.md index bbce5322f..6be141110 100644 --- a/integration-tests/README.md +++ b/integration-tests/README.md @@ -14,7 +14,7 @@ other tests in the `tests` folder. All tests run in either regtest or signet network. -Bitcoin Core v30.2 binaries are downloaded from https://bitcoincore.org/bin/bitcoin-core-30.2/ and the +Bitcoin Core binaries are downloaded from https://bitcoincore.org/bin/ and the Template provider (sv2-tp) binaries from https://github.com/stratum-mining/sv2-tp/releases. Bitcoin Core runs via IPC, and sv2-tp provides Stratum V2 template distribution. These are the only diff --git a/integration-tests/lib/mod.rs b/integration-tests/lib/mod.rs index 492b42242..360250be9 100644 --- a/integration-tests/lib/mod.rs +++ b/integration-tests/lib/mod.rs @@ -179,15 +179,24 @@ pub fn start_template_provider( ) -> (TemplateProvider, SocketAddr) { let address = get_available_address(); let sv2_interval = sv2_interval.unwrap_or(20); - let template_provider = TemplateProvider::start(address.port(), sv2_interval, difficulty_level); - template_provider.generate_blocks(1); + let template_provider = + TemplateProvider::start(address.port(), sv2_interval, difficulty_level.clone()); + if difficulty_level == DifficultyLevel::Low { + // template_provider.generate_blocks(1); + // generate 16 blocks as a workaround for https://github.com/bitcoin/bitcoin/issues/35126 + template_provider.generate_blocks(16); + } (template_provider, address) } pub fn start_bitcoin_core(difficulty_level: DifficultyLevel) -> BitcoinCore { let address = get_available_address(); - let bitcoin_core = BitcoinCore::start(address.port(), difficulty_level); - bitcoin_core.generate_blocks(1); + let bitcoin_core = BitcoinCore::start(address.port(), difficulty_level.clone()); + if difficulty_level == DifficultyLevel::Low { + // template_provider.generate_blocks(1); + // generate 16 blocks as a workaround for https://github.com/bitcoin/bitcoin/issues/35126 + bitcoin_core.generate_blocks(16); + } bitcoin_core } diff --git a/integration-tests/lib/template_provider.rs b/integration-tests/lib/template_provider.rs index f58c425c0..712f86356 100644 --- a/integration-tests/lib/template_provider.rs +++ b/integration-tests/lib/template_provider.rs @@ -10,8 +10,19 @@ use tracing::warn; use crate::utils::{fs_utils, http, tarball}; -const VERSION_SV2_TP: &str = "1.0.3"; -const VERSION_BITCOIN_CORE: &str = "30.2"; +const VERSION_SV2_TP: &str = "1.1.0"; +const VERSION_BITCOIN_CORE: &str = "31.0"; +/// Allow static signet fixtures to leave IBD without freezing Bitcoin Core's +/// clock, so mined blocks still use wall-clock timestamps. +/// +/// Since Bitcoin Core v31, IPC `createNewBlock` waits while IBD is active, so +/// `bitcoin_core_sv2` does not send templates before IBD is over. +/// See https://github.com/bitcoin/bitcoin/issues/33994. +/// +/// 100 years is intentionally far above the fixture age so these static chains +/// remain usable without periodic timestamp refreshes. This only relaxes the +/// stale-tip IBD threshold; it does not change Bitcoin Core's clock. +const SIGNET_FIXTURE_MAX_TIP_AGE_SECS: u64 = 100 * 365 * 24 * 60 * 60; fn get_sv2_tp_filename(os: &str, arch: &str) -> String { match (os, arch) { @@ -52,13 +63,14 @@ fn get_bitcoin_core_filename(os: &str, arch: &str) -> String { /// (most of the time, a CPU should take a REALLY long time to find a block) /// /// Note: signet mode has signetchallenge=51, which means no signature is needed on the coinbase. +#[derive(PartialEq, Clone)] pub enum DifficultyLevel { Low, Mid, High, } -/// Represents a Bitcoin Core v30.2+ node with IPC enabled. +/// Represents a Bitcoin Core node with IPC enabled. #[derive(Debug)] pub struct BitcoinCore { bitcoind: Node, @@ -85,6 +97,7 @@ impl BitcoinCore { let staticdir = format!(".bitcoin-{port}"); conf.staticdir = Some(data_dir.join(staticdir.clone())); + let max_tip_age_arg = format!("-maxtipage={SIGNET_FIXTURE_MAX_TIP_AGE_SECS}"); match difficulty_level { DifficultyLevel::Low => { // use default corepc-node settings, which means regtest mode @@ -94,14 +107,24 @@ impl BitcoinCore { // use signet mode with genesis difficulty // (signetchallenge=51, no signature needed on the coinbase) // most of the time, a CPU should find a block in a minute or less - conf.args = vec!["-signet", "-fallbackfee=0.0001", "-signetchallenge=51"]; + conf.args = vec![ + "-signet", + "-fallbackfee=0.0001", + "-signetchallenge=51", + max_tip_age_arg.as_str(), + ]; conf.network = "signet"; } DifficultyLevel::High => { // use signet mode with premined blocks raising difficulty to 77761.11 // (signetchallenge=51, no signature needed on the coinbase) // most of the time, a CPU should take a REALLY long time to find a block - conf.args = vec!["-signet", "-fallbackfee=0.0001", "-signetchallenge=51"]; + conf.args = vec![ + "-signet", + "-fallbackfee=0.0001", + "-signetchallenge=51", + max_tip_age_arg.as_str(), + ]; conf.network = "signet"; // Create signet datadir @@ -132,7 +155,7 @@ impl BitcoinCore { } } - // Download and setup Bitcoin Core v30.2 with IPC support + // Download and setup Bitcoin Core with IPC support let os = env::consts::OS; let arch = env::consts::ARCH; let bitcoin_filename = get_bitcoin_core_filename(os, arch); @@ -147,7 +170,7 @@ impl BitcoinCore { warn!("Downloading Bitcoin Core {} for the testing session. This could take a while...", VERSION_BITCOIN_CORE); let download_endpoint = env::var("BITCOIN_CORE_DOWNLOAD_ENDPOINT") .unwrap_or_else(|_| { - "https://bitcoincore.org/bin/bitcoin-core-30.2".to_owned() + "https://bitcoincore.org/bin/bitcoin-core-31.0".to_owned() }); let url = format!("{download_endpoint}/{bitcoin_filename}"); http::make_get_request(&url, 5) @@ -212,16 +235,25 @@ impl BitcoinCore { } /// Mine `n` blocks. - pub fn generate_blocks(&self, n: u64) { + pub fn generate_blocks(&self, n: usize) { let mining_address = self .bitcoind .client .new_address() .expect("Failed to get mining address"); - self.bitcoind + let generated_blocks = self + .bitcoind .client - .generate_to_address(n as usize, &mining_address) + .generate_to_address(n, &mining_address) .expect("Failed to generate blocks"); + // Bitcoin Core's generatetoaddress returns Ok(block_hashes) with an array of hashes of the + // generated blocks. + assert_eq!( + generated_blocks.0.len(), + n, + "Bitcoin Core generated {} of {n} requested blocks", + generated_blocks.0.len() + ); } /// Return the node's RPC info. @@ -285,10 +317,10 @@ impl BitcoinCore { } } -/// Represents a template provider using Bitcoin Core v30.2+ with IPC and standalone sv2-tp. +/// Represents a template provider using Bitcoin Core with IPC and standalone sv2-tp. /// /// This implementation launches two separate processes: -/// 1. Bitcoin Core v30.2+ (bitcoin-node) with IPC enabled +/// 1. Bitcoin Core (bitcoin-node) with IPC enabled /// 2. Standalone sv2-tp binary that connects to Bitcoin Core via IPC #[derive(Debug)] pub struct TemplateProvider { @@ -298,7 +330,7 @@ pub struct TemplateProvider { } impl TemplateProvider { - /// Start a new [`TemplateProvider`] instance with Bitcoin Core v30.2+ and standalone sv2-tp. + /// Start a new [`TemplateProvider`] instance with Bitcoin Core and standalone sv2-tp. pub fn start(port: u16, sv2_interval: u32, difficulty_level: DifficultyLevel) -> Self { let bitcoin_core = BitcoinCore::start(port, difficulty_level); @@ -355,7 +387,7 @@ impl TemplateProvider { .arg(network) .arg(format!("-datadir={}", datadir.display())) .arg(format!("-sv2port={}", port)) - .arg(format!("-sv2interval={}", sv2_interval)) + .arg(format!("-templateinterval={}", sv2_interval)) .arg("-sv2feedelta=0") .arg("-debug=sv2") .arg("-loglevel=sv2:trace") @@ -375,7 +407,7 @@ impl TemplateProvider { } /// Mine `n` blocks. - pub fn generate_blocks(&self, n: u64) { + pub fn generate_blocks(&self, n: usize) { self.bitcoin_core.generate_blocks(n); } diff --git a/integration-tests/tests/translator_integration.rs b/integration-tests/tests/translator_integration.rs index 42fb5e32f..67971eee1 100644 --- a/integration-tests/tests/translator_integration.rs +++ b/integration-tests/tests/translator_integration.rs @@ -1840,7 +1840,21 @@ async fn pool_does_not_hang_on_no_handshake() { start_tracing(); let (_tp, tp_addr) = start_template_provider(None, DifficultyLevel::Low); let (pool, pool_addr, _) = start_pool(sv2_tp_config(tp_addr), vec![], vec![], false).await; - let ephemeral_stream = TcpStream::connect(pool_addr).await.unwrap(); + let ephemeral_stream = tokio::time::timeout(Duration::from_secs(5), async { + loop { + match TcpStream::connect(pool_addr).await { + Ok(stream) => break stream, + Err(e) => { + if e.kind() != std::io::ErrorKind::ConnectionRefused { + panic!("failed to connect to {pool_addr}: {e}"); + } + tokio::time::sleep(Duration::from_millis(50)).await; + } + } + } + }) + .await + .expect("pool downstream listener did not start"); tokio::time::sleep(Duration::from_secs(1)).await; let (pool_translator_sniffer, pool_translator_sniffer_addr) = diff --git a/miner-apps/Cargo.lock b/miner-apps/Cargo.lock index d3a60e030..467e27617 100644 --- a/miner-apps/Cargo.lock +++ b/miner-apps/Cargo.lock @@ -298,9 +298,9 @@ dependencies = [ [[package]] name = "bitcoin-capnp-types" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e20759e30b46af17a13f2e34c9e090c3672938b5a0c22358cba971d1a8f5d492" +checksum = "a58af19b421a85566a1d46617f40b18eea77d565a0271bdff2deabf0d27c075c" dependencies = [ "capnp", "capnpc", @@ -335,7 +335,7 @@ dependencies = [ [[package]] name = "bitcoin_core_sv2" -version = "0.1.1" +version = "0.2.0" dependencies = [ "async-channel", "bitcoin-capnp-types", @@ -459,18 +459,18 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "capnp" -version = "0.21.7" +version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e92edec8974fcd7ece90bb021db782abe14a61c10c817f197f700fef7430eb8" +checksum = "63da65e5e9ffc3b8f993d4ad222a548152549351a643f6b850a7773cb6ff2809" dependencies = [ "embedded-io", ] [[package]] name = "capnp-futures" -version = "0.21.0" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d04478adeb234836f886ec554a0d96e3af3a939ba7b3962af5addddf7ab71231" +checksum = "73b69dfddccc57844f9a90f9d72b44b97c326914851ea94fb7da40ef9cad6e8d" dependencies = [ "capnp", "futures-channel", @@ -479,9 +479,9 @@ dependencies = [ [[package]] name = "capnp-rpc" -version = "0.21.0" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85e9c19ef52ff1b9c9822fb21bfa68a72bc58711676295ff06eb88e64c7877f7" +checksum = "07ccca6d26009f4d6c12b741994f33b421da613b5dcf461508e236b53ef862f1" dependencies = [ "capnp", "capnp-futures", @@ -490,9 +490,9 @@ dependencies = [ [[package]] name = "capnpc" -version = "0.21.4" +version = "0.25.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6da96dcb0a0e0c526daf42bac55e1550f18ad973df9ef9ba75204f332c80ad16" +checksum = "fca02be865c8c5a78bfc24b9819006ab6b59bef238467203928e26459557af93" dependencies = [ "capnp", ] @@ -891,9 +891,9 @@ dependencies = [ [[package]] name = "embedded-io" -version = "0.6.1" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" +checksum = "9eb1aa714776b75c7e67e1da744b81a129b3ff919c8712b5e1b32252c1f07cc7" [[package]] name = "encode_unicode" @@ -1517,7 +1517,7 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jd_client_sv2" -version = "0.1.4" +version = "0.2.0" dependencies = [ "async-channel", "bitcoin_core_sv2", diff --git a/miner-apps/jd-client/Cargo.toml b/miner-apps/jd-client/Cargo.toml index ffb4bc21b..301d88dad 100644 --- a/miner-apps/jd-client/Cargo.toml +++ b/miner-apps/jd-client/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jd_client_sv2" -version = "0.1.4" +version = "0.2.0" authors = ["The Stratum V2 Developers"] edition = "2021" description = "Job Declarator Client (JDC) role" @@ -23,7 +23,7 @@ tokio = { version = "1.44.1", features = ["full"] } ext-config = { version = "0.14.0", features = ["toml"], package = "config" } tracing = { version = "0.1" } clap = { version = "4.5.39", features = ["derive"] } -bitcoin_core_sv2 = { version = "0.1.0", path = "../../bitcoin-core-sv2" } +bitcoin_core_sv2 = { version = "0.2.0", path = "../../bitcoin-core-sv2" } hex = "0.4.3" hotpath = "0.14.0" diff --git a/miner-apps/jd-client/src/lib/channel_manager/mod.rs b/miner-apps/jd-client/src/lib/channel_manager/mod.rs index 011b2f7ff..6c3fd8faf 100644 --- a/miner-apps/jd-client/src/lib/channel_manager/mod.rs +++ b/miner-apps/jd-client/src/lib/channel_manager/mod.rs @@ -528,6 +528,7 @@ impl ChannelManager { } warn!("Waiting for initial template and prevhash from Template Provider..."); + warn!("Is the Bitcoin node undergoing IBD?"); select! { _ = cancellation_token.cancelled() => { info!("Channel Manager: received shutdown while waiting for templates"); @@ -537,7 +538,7 @@ impl ChannelManager { info!("Channel Manager: received fallback while waiting for templates"); return Ok(()); } - _ = tokio::time::sleep(std::time::Duration::from_millis(100)) => {} + _ = tokio::time::sleep(std::time::Duration::from_secs(1)) => {} } } diff --git a/pool-apps/Cargo.lock b/pool-apps/Cargo.lock index 45ce83e55..485939ad7 100644 --- a/pool-apps/Cargo.lock +++ b/pool-apps/Cargo.lock @@ -298,9 +298,9 @@ dependencies = [ [[package]] name = "bitcoin-capnp-types" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e20759e30b46af17a13f2e34c9e090c3672938b5a0c22358cba971d1a8f5d492" +checksum = "a58af19b421a85566a1d46617f40b18eea77d565a0271bdff2deabf0d27c075c" dependencies = [ "capnp", "capnpc", @@ -335,7 +335,7 @@ dependencies = [ [[package]] name = "bitcoin_core_sv2" -version = "0.1.1" +version = "0.2.0" dependencies = [ "async-channel", "bitcoin-capnp-types", @@ -450,18 +450,18 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "capnp" -version = "0.21.7" +version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e92edec8974fcd7ece90bb021db782abe14a61c10c817f197f700fef7430eb8" +checksum = "63da65e5e9ffc3b8f993d4ad222a548152549351a643f6b850a7773cb6ff2809" dependencies = [ "embedded-io", ] [[package]] name = "capnp-futures" -version = "0.21.0" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d04478adeb234836f886ec554a0d96e3af3a939ba7b3962af5addddf7ab71231" +checksum = "73b69dfddccc57844f9a90f9d72b44b97c326914851ea94fb7da40ef9cad6e8d" dependencies = [ "capnp", "futures-channel", @@ -470,9 +470,9 @@ dependencies = [ [[package]] name = "capnp-rpc" -version = "0.21.0" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85e9c19ef52ff1b9c9822fb21bfa68a72bc58711676295ff06eb88e64c7877f7" +checksum = "07ccca6d26009f4d6c12b741994f33b421da613b5dcf461508e236b53ef862f1" dependencies = [ "capnp", "capnp-futures", @@ -481,9 +481,9 @@ dependencies = [ [[package]] name = "capnpc" -version = "0.21.4" +version = "0.25.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6da96dcb0a0e0c526daf42bac55e1550f18ad973df9ef9ba75204f332c80ad16" +checksum = "fca02be865c8c5a78bfc24b9819006ab6b59bef238467203928e26459557af93" dependencies = [ "capnp", ] @@ -882,9 +882,9 @@ dependencies = [ [[package]] name = "embedded-io" -version = "0.6.1" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" +checksum = "9eb1aa714776b75c7e67e1da744b81a129b3ff919c8712b5e1b32252c1f07cc7" [[package]] name = "encode_unicode" @@ -1508,7 +1508,7 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jd_server_sv2" -version = "0.1.2" +version = "0.2.0" dependencies = [ "async-channel", "async-trait", @@ -1911,7 +1911,7 @@ dependencies = [ [[package]] name = "pool_sv2" -version = "0.2.2" +version = "0.3.0" dependencies = [ "async-channel", "bitcoin_core_sv2", diff --git a/pool-apps/jd-server/Cargo.toml b/pool-apps/jd-server/Cargo.toml index c756b6ad8..69bd3e34f 100644 --- a/pool-apps/jd-server/Cargo.toml +++ b/pool-apps/jd-server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jd_server_sv2" -version = "0.1.2" +version = "0.2.0" authors = ["The Stratum V2 Developers"] edition = "2021" description = "Sv2 Job Declaration Server" @@ -19,7 +19,7 @@ path = "src/lib/mod.rs" stratum-apps = { version = "0.4.0", path = "../../stratum-apps", features = ["jd_server"] } async-channel = "1.5.1" -bitcoin_core_sv2 = { version = "0.1.0", path = "../../bitcoin-core-sv2" } +bitcoin_core_sv2 = { version = "0.2.0", path = "../../bitcoin-core-sv2" } serde = { version = "1.0.89", features = ["derive", "alloc"], default-features = false } tracing = { version = "0.1" } tokio = { version = "1.44.1", features = ["full"] } diff --git a/pool-apps/jd-server/src/lib/job_declarator/job_validation/bitcoin_core_ipc.rs b/pool-apps/jd-server/src/lib/job_declarator/job_validation/bitcoin_core_ipc.rs index 7b65cbbca..7df7e1ebf 100644 --- a/pool-apps/jd-server/src/lib/job_declarator/job_validation/bitcoin_core_ipc.rs +++ b/pool-apps/jd-server/src/lib/job_declarator/job_validation/bitcoin_core_ipc.rs @@ -264,14 +264,16 @@ impl BitcoinCoreIPCEngine { let bitcoin_core_sv2_jdp = match BitcoinCoreSv2JDP::new( unix_socket_path, request_receiver, - cancellation_token_clone, + cancellation_token_clone.clone(), ready_tx, ) .await { Ok(client) => client, Err(e) => { - tracing::error!("Failed to create BitcoinCoreSv2JDP: {:?}", e); + if !cancellation_token_clone.is_cancelled() { + tracing::error!("Failed to create BitcoinCoreSv2JDP: {:?}", e); + } // ready_tx dropped here, signaling failure to ready_rx return; } @@ -283,10 +285,42 @@ impl BitcoinCoreIPCEngine { }); }); - // Wait for BitcoinCoreSv2JDP to complete mempool bootstrap - ready_rx - .await - .map_err(|_| JDSErrorKind::BitcoinCoreIPC("Mempool bootstrap failed".to_string()))?; + // Wait for BitcoinCoreSv2JDP to complete mempool bootstrap, mirroring the + // pool's Template Provider startup behavior during IBD. + // Until `new()` succeeds, this function is still the only owner of the spawned JDP + // thread handle, so cancellation/bootstrap failure must join here rather than detach it. + let mut ready_rx = ready_rx; + loop { + tokio::select! { + res = &mut ready_rx => { + match res { + Ok(()) => break, + Err(_) => { + if let Err(e) = jdp_thread_handle.join() { + tracing::warn!("BitcoinCoreSv2JDP thread join failed: {e:?}"); + } + + return Err(JDSErrorKind::BitcoinCoreIPC( + "Mempool bootstrap did not complete".to_string(), + )); + } + } + } + _ = cancellation_token.cancelled() => { + tracing::info!("BitcoinCoreIPCEngine stopped before mempool bootstrap completed"); + if let Err(e) = jdp_thread_handle.join() { + tracing::warn!("BitcoinCoreSv2JDP thread join failed during startup cancellation: {e:?}"); + } + return Err(JDSErrorKind::BitcoinCoreIPC( + "Mempool bootstrap did not complete".to_string(), + )); + } + _ = tokio::time::sleep(Duration::from_secs(1)) => { + tracing::warn!("Waiting for initial template and prevhash from Template Provider..."); + tracing::warn!("Is the Bitcoin node undergoing IBD?"); + } + } + } let allocated_token_to_request_id = Arc::new(DashMap::::new()); diff --git a/pool-apps/pool/Cargo.toml b/pool-apps/pool/Cargo.toml index eeceef8a2..bc694aa55 100644 --- a/pool-apps/pool/Cargo.toml +++ b/pool-apps/pool/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pool_sv2" -version = "0.2.2" +version = "0.3.0" authors = ["The Stratum V2 Developers"] edition = "2021" description = "SV2 pool role" @@ -24,8 +24,8 @@ tokio = { version = "1.44.1", features = ["full"] } ext-config = { version = "0.14.0", features = ["toml"], package = "config" } tracing = { version = "0.1" } clap = { version = "4.5.39", features = ["derive"] } -bitcoin_core_sv2 = { version = "0.1.0", path = "../../bitcoin-core-sv2" } -jd_server_sv2 = { version = "0.1.0", path = "../jd-server" } +bitcoin_core_sv2 = { version = "0.2.0", path = "../../bitcoin-core-sv2" } +jd_server_sv2 = { version = "0.2.0", path = "../jd-server" } hex = "0.4.3" hotpath = "0.14.0" diff --git a/pool-apps/pool/src/lib/channel_manager/mod.rs b/pool-apps/pool/src/lib/channel_manager/mod.rs index aa3197b82..9506bf4a4 100644 --- a/pool-apps/pool/src/lib/channel_manager/mod.rs +++ b/pool-apps/pool/src/lib/channel_manager/mod.rs @@ -273,12 +273,13 @@ impl ChannelManager { } warn!("Waiting for initial template and prevhash from Template Provider..."); + warn!("Is the Bitcoin node undergoing IBD?"); select! { _ = cancellation_token.cancelled() => { info!("Channel Manager: received shutdown while waiting for templates"); return Ok(()); } - _ = tokio::time::sleep(std::time::Duration::from_millis(100)) => {} + _ = tokio::time::sleep(std::time::Duration::from_secs(1)) => {} } }