Skip to content
Draft
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
4 changes: 4 additions & 0 deletions bitcoin-core-sv2/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ bitcoin_capnp_types_v30 = { package = "bitcoin-capnp-types", version = "0.1.2" }
# Bitcoin Core v31.x IPC dependencies
bitcoin_capnp_types_v31 = { package = "bitcoin-capnp-types", version = "0.2.1" }

# todo: fetch from crates.io
# Bitcoin Core v32.x IPC dependencies
bitcoin_capnp_types_v32 = { package = "bitcoin-capnp-types", git = "https://github.com/2140-dev/bitcoin-capnp-types.git", rev = "6e4c99834df947df3a0f733d2a2dc6e12100e441" }

# fetching from github enables synchronizing development workflows across sv2-apps and stratum repos
# it MUST be changed before bitcoin-core-sv2 is published to crates.io
# with the proper version of stratum-core being fetched from crates.io as well
Expand Down
178 changes: 178 additions & 0 deletions bitcoin-core-sv2/examples/tdp_logger_v32x.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
//! A simple example of how to use `BitcoinCoreSv2TDP`.
//!
//! This example demonstrates the pattern used in applications where `BitcoinCoreSv2TDP` is
//! spawned in a dedicated thread with its own Tokio runtime and `LocalSet`. This allows the
//! main application to run in a separate async context while `BitcoinCoreSv2TDP` runs in its
//! own isolated thread.
//!
//! We connect to the Bitcoin Core UNIX socket, and log the received Sv2 Template Distribution
//! Protocol messages.
//!
//! We send a `CoinbaseOutputConstraints` message to the `BitcoinCoreSv2TDP` instance once at
//! startup.
//!
//! `BitcoinCoreSv2TDP` will not start distributing new templates until it receives the first
//! `CoinbaseOutputConstraints` message.

use bitcoin_core_sv2::unix_capnp::v32x::template_distribution_protocol::BitcoinCoreSv2TDP;
use std::path::Path;

use async_channel::unbounded;
use stratum_core::{
parsers_sv2::TemplateDistribution,
template_distribution_sv2::{CoinbaseOutputConstraints, RequestTransactionData},
};
use tokio_util::sync::CancellationToken;
use tracing::{error, info};

#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();

// the user must provide the path to the Bitcoin Core UNIX socket
let args: Vec<String> = std::env::args().collect();
if args.len() != 2 {
eprintln!("Usage: {} <bitcoin_core_unix_socket_path>", args[0]);
eprintln!("Example: {} /path/to/bitcoin/regtest/node.sock", args[0]);
std::process::exit(1);
}

let bitcoin_core_unix_socket_path = Path::new(&args[1]);

// `BitcoinCoreSv2TDP` uses this to cancel internally spawned tasks
let cancellation_token = CancellationToken::new();

// get new templates whenever the mempool has changed by more than 100 sats
let fee_threshold = 100;

// the minimum interval between template updates in seconds
let min_interval = 5;

// these messages are sent into the `BitcoinCoreSv2TDP` instance
let (msg_sender_into_bitcoin_core_sv2, msg_receiver_into_bitcoin_core_sv2) = unbounded();
// these messages are received from the `BitcoinCoreSv2TDP` instance
let (msg_sender_from_bitcoin_core_sv2, msg_receiver_from_bitcoin_core_sv2) = unbounded();

// clone so we can move it into the thread
let cancellation_token_clone = cancellation_token.clone();
let bitcoin_core_unix_socket_path_clone = bitcoin_core_unix_socket_path.to_path_buf();

// spawn a dedicated thread to run the BitcoinCoreSv2TDP instance
// because we're limited to tokio::task::LocalSet
//
// please note that it's important to keep a reference to the join handle so we can wait for it
// to finish shutdown this will no longer be a pre-requisite once https://github.com/bitcoin/bitcoin/pull/33676 lands in a release
// see https://github.com/stratum-mining/sv2-apps/issues/81 for more details
let join_handle = std::thread::spawn(move || {
// we need a dedicated runtime so we can spawn an async task inside the LocalSet
let rt = match tokio::runtime::Runtime::new() {
Ok(rt) => rt,
Err(e) => {
error!("Failed to create Tokio runtime: {:?}", e);
cancellation_token_clone.cancel();
return;
}
};
let tokio_local_set = tokio::task::LocalSet::new();

tokio_local_set.block_on(&rt, async move {
// create a new `BitcoinCoreSv2TDP` instance
let mut sv2_bitcoin_core = match BitcoinCoreSv2TDP::new(
&bitcoin_core_unix_socket_path_clone,
fee_threshold,
min_interval,
msg_receiver_into_bitcoin_core_sv2,
msg_sender_from_bitcoin_core_sv2,
cancellation_token_clone.clone(),
)
.await
{
Ok(sv2_bitcoin_core) => sv2_bitcoin_core,
Err(e) => {
error!("Failed to create BitcoinCoreToSv2: {:?}", e);
cancellation_token_clone.cancel();
return;
}
};

// run the `BitcoinCoreSv2TDP` instance, which will block until the cancellation token
// is activated
sv2_bitcoin_core.run().await;
});
});

// clone so we can move it
let cancellation_token_clone = cancellation_token.clone();

// clone so we can move it
let msg_sender_into_bitcoin_core_sv2_clone = msg_sender_into_bitcoin_core_sv2.clone();

// a task to consume and log the received Sv2 Template Distribution Protocol messages
tokio::spawn(async move {
loop {
tokio::select! {
// monitor for Ctrl+C, activating the cancellation token and exiting the loop
_ = tokio::signal::ctrl_c() => {
info!("Ctrl+C received");
cancellation_token_clone.cancel();
return;
}
// monitor potential internal activations of the cancellation token for exiting the loop
_ = cancellation_token_clone.cancelled() => {
info!("Cancellation token activated");
return;
}
// monitor for Sv2 Template Distribution Protocol messages
// coming from `BitcoinCoreSv2TDP`
Ok(template_distribution_message) = msg_receiver_from_bitcoin_core_sv2.recv() => {
// log the message
info!("Message received: {}", template_distribution_message);

// send a RequestTransactionData every time a NewTemplate message is received
if let TemplateDistribution::NewTemplate(new_template) = template_distribution_message {
let template_id = new_template.template_id;
let request_transaction_data = TemplateDistribution::RequestTransactionData(RequestTransactionData {
template_id,
});

match msg_sender_into_bitcoin_core_sv2_clone.send(request_transaction_data).await {
Ok(_) => (),
Err(e) => {
error!("Failed to send request transaction data: {}", e);
cancellation_token_clone.cancel();
return;
}
}
}
}
}
}
});

// send CoinbaseOutputConstraints once at startup
//
// `BitcoinCoreSv2TDP` will not start distributing new templates until it receives the first
// `CoinbaseOutputConstraints` message.
let new_coinbase_output_constraints =
TemplateDistribution::CoinbaseOutputConstraints(CoinbaseOutputConstraints {
coinbase_output_max_additional_size: 2,
coinbase_output_max_additional_sigops: 2,
});

if let Err(e) = msg_sender_into_bitcoin_core_sv2
.send(new_coinbase_output_constraints)
.await
{
error!("Failed to send coinbase output constraints: {}", e);
cancellation_token.cancel();
}
info!("Sent CoinbaseOutputConstraints");

// wait for the cancellation token to be activated
cancellation_token.cancelled().await;
info!("Shutting down...");

// wait for the dedicated thread to finish shutdown
join_handle.join().unwrap();
info!("BitcoinCoreSv2TDP dedicated thread shutdown complete.");
}
1 change: 1 addition & 0 deletions bitcoin-core-sv2/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
//! protocols.
//! - [`unix_capnp::v30x`] contains the Bitcoin Core v30.x IPC implementation.
//! - [`unix_capnp::v31x`] contains the Bitcoin Core v31.x IPC implementation.
//! - [`unix_capnp::v32x`] contains the Bitcoin Core v32.x IPC implementation.
//!
//! ## Flavor direction
//!
Expand Down
28 changes: 13 additions & 15 deletions bitcoin-core-sv2/src/runtime_api/job_declaration_protocol/io.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
//! Request / response types exchanged between `jd-server` and the Bitcoin Core IPC thread.

use stratum_core::{
bitcoin::{BlockHash, CompactTarget, Transaction, Txid, Wtxid, block::Version},
job_declaration_sv2::PushSolution,
};
use stratum_core::bitcoin::{Block, BlockHash, CompactTarget, Transaction, Wtxid, block::Version};
use tokio::sync::oneshot;

/// Snapshot of the template parameters used by the validator at decision time.
///
/// This lets callers distinguish stale-tip races from other validation failures.
/// Stale-chain-tip detection relies solely on [`prev_hash`](Self::prev_hash)
/// drift; nbits is used for validation of SetCustomMiningJob.
///
/// Please check <https://github.com/stratum-mining/sv2-apps/issues/364>
/// for more details on the regression that motivated this field.
#[derive(Debug, Clone, Copy)]
pub struct ValidationContext {
pub prev_hash: BlockHash,
pub nbits: CompactTarget,
pub min_ntime: u32,
}

/// A request sent from `jd-server` to the [`BitcoinCoreSv2JDP`](super::BitcoinCoreSv2JDP) IPC
Expand All @@ -33,10 +30,11 @@ pub enum JdRequest {
missing_txs: Vec<Transaction>,
response_tx: oneshot::Sender<JdResponse>,
},
/// Submit a mining solution to Bitcoin Core (fire-and-forget).
PushSolution {
push_solution: PushSolution<'static>,
},
/// Submit a fully assembled block to Bitcoin Core (fire-and-forget).
///
/// This request is currently stubbed as a warning-only no-op by the v30.x and v31.x
/// backends.
PushSolution { block: Block },
Comment on lines +33 to +37

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note to self:

we cannot do JdRequest::PushSolution take a Block

#609 (comment)

@plebhash plebhash Jul 19, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another reason to pivot away from this approach:

I'm noticing a lot of JDS log lines like this on my signet e2e tests:

2026-07-19T18:07:28.604132Z  WARN bitcoin_core_sv2::unix_capnp::v32x::job_declaration_protocol::handlers: Bitcoin Core rejected block via submitBlock reason=O
k("high-hash") debug=Ok("proof of work failed")

it doesn't happen 100% of the time, because propagation on JDC side is always successful, and I also see JDS log lines like this:

2026-07-19T18:12:07.353926Z  WARN bitcoin_core_sv2::unix_capnp::v32x::job_declaration_protocol::handlers: Bitcoin Core rejected block via submitBlock reason=O
k("duplicate") debug=Ok("")

I haven't been able to trace the root cause of the submitBlock failures with 100% certainty, but my suspicion is that the solution is being applied to the wrong custom job, due to DeclareMiningJob vs PushSolution races, which is a direct consequence of the KISS approach of only processing PushSolution against the "latest ACKd DeclareMiningJob"

}

/// The result of trying to handle a DeclareMiningJob request.
Expand All @@ -45,11 +43,11 @@ pub enum JdResponse {
Success {
prev_hash: BlockHash,
nbits: CompactTarget,
min_ntime: u32,
/// Txids for all transactions (excluding coinbase), in the same order as the declared
/// wtxid_list. Enables the caller to build the txid merkle tree for validating
/// SetCustomMiningJob.merkle_path.
txid_list: Vec<Txid>,
/// Full non-coinbase transaction list in declaration order.
///
/// This is used by `jd-server` both to reconstruct solved blocks when handling
/// `PushSolution` and to derive txids for merkle-root/merkle-path validation.
txdata: Vec<Transaction>,
},
Error {
error_code: &'static str,
Expand Down
17 changes: 15 additions & 2 deletions bitcoin-core-sv2/src/runtime_api/job_declaration_protocol/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
//!
//! The request channel covers the two base JDP flows:
//! - `DeclareMiningJob`
//! - `PushSolution`
//! - `PushSolution` (currently stubbed as a warning-only no-op on v30.x and v31.x backends)
//!
//! Token lifecycle and higher-level protocol state remain the caller responsibility (for example,
//! associating `AllocateMiningJobToken`/`DeclareMiningJob`/`SetCustomMiningJob` state).
Expand All @@ -14,7 +14,7 @@ pub mod io;

use crate::{
runtime_api::{BitcoinCoreSv2Error, BitcoinCoreSv2Protocol, BitcoinCoreVersion},
unix_capnp::{v30x, v31x},
unix_capnp::{v30x, v31x, v32x},
};
use async_channel::Receiver;
use io::JdRequest;
Expand All @@ -28,13 +28,15 @@ pub use tokio_util::sync::CancellationToken;
pub enum BitcoinCoreSv2JDP {
V30X(v30x::job_declaration_protocol::BitcoinCoreSv2JDP),
V31X(v31x::job_declaration_protocol::BitcoinCoreSv2JDP),
V32X(v32x::job_declaration_protocol::BitcoinCoreSv2JDP),
}

impl BitcoinCoreSv2JDP {
pub async fn run(&self) {
match self {
Self::V30X(runtime) => runtime.run().await,
Self::V31X(runtime) => runtime.run().await,
Self::V32X(runtime) => runtime.run().await,
}
}
}
Expand Down Expand Up @@ -75,5 +77,16 @@ where
.map_err(|error| {
BitcoinCoreSv2JDPError::from_debug(version, BitcoinCoreSv2Protocol::JDP, error)
}),
BitcoinCoreVersion::V32X => v32x::job_declaration_protocol::BitcoinCoreSv2JDP::new(
bitcoin_core_unix_socket_path,
incoming_requests,
cancellation_token,
ready_tx,
)
.await
.map(BitcoinCoreSv2JDP::V32X)
.map_err(|error| {
BitcoinCoreSv2JDPError::from_debug(version, BitcoinCoreSv2Protocol::JDP, error)
}),
}
}
3 changes: 3 additions & 0 deletions bitcoin-core-sv2/src/runtime_api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ use std::fmt;
pub enum BitcoinCoreVersion {
V30X,
V31X,
V32X,
}

impl BitcoinCoreVersion {
pub const fn as_major(self) -> u8 {
match self {
Self::V30X => 30,
Self::V31X => 31,
Self::V32X => 32,
}
}
}
Expand All @@ -28,6 +30,7 @@ impl TryFrom<u8> for BitcoinCoreVersion {
match value {
30 => Ok(Self::V30X),
31 => Ok(Self::V31X),
32 => Ok(Self::V32X),
_ => Err(value),
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

use crate::{
runtime_api::{BitcoinCoreSv2Error, BitcoinCoreSv2Protocol, BitcoinCoreVersion},
unix_capnp::{v30x, v31x},
unix_capnp::{v30x, v31x, v32x},
};
use async_channel::{Receiver, Sender};
use std::path::Path;
Expand All @@ -28,13 +28,15 @@ pub use tokio_util::sync::CancellationToken;
pub enum BitcoinCoreSv2TDP {
V30X(v30x::template_distribution_protocol::BitcoinCoreSv2TDP),
V31X(v31x::template_distribution_protocol::BitcoinCoreSv2TDP),
V32X(v32x::template_distribution_protocol::BitcoinCoreSv2TDP),
}

impl BitcoinCoreSv2TDP {
pub async fn run(&mut self) {
match self {
Self::V30X(runtime) => runtime.run().await,
Self::V31X(runtime) => runtime.run().await,
Self::V32X(runtime) => runtime.run().await,
}
}
}
Expand Down Expand Up @@ -82,5 +84,18 @@ where
.map_err(|error| {
BitcoinCoreSv2TDPError::from_debug(version, BitcoinCoreSv2Protocol::TDP, error)
}),
BitcoinCoreVersion::V32X => v32x::template_distribution_protocol::BitcoinCoreSv2TDP::new(
bitcoin_core_unix_socket_path,
fee_threshold,
min_interval,
incoming_messages,
outgoing_messages,
global_cancellation_token,
)
.await
.map(BitcoinCoreSv2TDP::V32X)
.map_err(|error| {
BitcoinCoreSv2TDPError::from_debug(version, BitcoinCoreSv2Protocol::TDP, error)
}),
}
}
4 changes: 3 additions & 1 deletion bitcoin-core-sv2/src/unix_capnp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

pub mod v30x;
pub mod v31x;
pub mod v32x;

// Shared cross-version implementation modules are internal plumbing, not stable public API.
pub(crate) mod v31x_v30x;
pub(crate) mod v32x_v31x;
pub(crate) mod v32x_v31x_v30x;
Loading
Loading