Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions bitcoin-core-sv2/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"]
Expand All @@ -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"
Expand All @@ -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"
10 changes: 9 additions & 1 deletion bitcoin-core-sv2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
- MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
2 changes: 2 additions & 0 deletions bitcoin-core-sv2/src/job_declaration_protocol/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
27 changes: 21 additions & 6 deletions bitcoin-core-sv2/src/job_declaration_protocol/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Comment thread
GitGab19 marked this conversation as resolved.

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}");
Expand Down
93 changes: 77 additions & 16 deletions bitcoin-core-sv2/src/job_declaration_protocol/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<RefCell<BlockTemplateIpcClient>>,
Expand Down Expand Up @@ -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()
Expand All @@ -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);
Expand All @@ -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)),
Expand All @@ -178,6 +195,45 @@ impl BitcoinCoreSv2JDP {
Ok(self_)
}

/// Creates a new dedicated thread IPC client.
async fn new_thread_ipc_client(&self) -> Result<ThreadIpcClient, BitcoinCoreSv2JDPError> {
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.
Expand All @@ -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) => {
Expand Down Expand Up @@ -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}");
Expand Down
28 changes: 22 additions & 6 deletions bitcoin-core-sv2/src/job_declaration_protocol/monitors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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()),
Comment thread
GitGab19 marked this conversation as resolved.
Err(e) => {
tracing::error!("Failed to set thread: {}", e);
self_clone.cancellation_token.cancel();
Expand All @@ -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() => {
Expand Down
7 changes: 7 additions & 0 deletions bitcoin-core-sv2/src/template_distribution_protocol/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,12 @@ pub enum BitcoinCoreSv2TDPError {
FailedToSubmitSolution,
FailedToSetThread,
FailedToGetWaitNextRequestOptions,
CreateNewBlockRequestInterrupted,
FailedToSendInterruptCreateNewBlockRequest,
FailedToSendInterruptWaitRequest,
FailedToWaitForMonitorIpcTemplatesTask,
FailedToCreateSolutionDir,
InvalidBlockRewardRemaining(i64),
}

impl From<capnp::Error> for BitcoinCoreSv2TDPError {
Expand Down Expand Up @@ -53,6 +56,7 @@ pub enum TemplateDataError {
CapnpError(capnp::Error),
FailedIpcSubmitSolution,
FailedToSerializeEmptyCoinbaseOutputs,
FailedToSerializeCoinbaseOutputs,
FailedToConvertMerklePathHashToU256,
FailedToCreateMerklePathSeq,
BitcoinCoreSv2TDPError(BitcoinCoreSv2TDPError),
Expand Down Expand Up @@ -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")
}
Expand Down
Loading
Loading