Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion .github/workflows/docker-release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:

strategy:
matrix:
app: [pool_sv2, jd_server, jd_client_sv2, translator_sv2]
app: [pool_sv2, jd_client_sv2, translator_sv2]

steps:
- name: Checkout code
Expand Down
2 changes: 1 addition & 1 deletion bitcoin-core-sv2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ brew install capnproto

### `LocalSet` Requirement

Due to limitations in the `capnp-rpc` dependency (where some abstractions do not implement the `Send` trait), `BitcoinCoreSv2` must be run within a [`tokio::task::LocalSet`](https://docs.rs/tokio/latest/tokio/task/struct.LocalSet.html). The crate examples demonstrate the proper setup pattern.
Due to limitations in the `capnp-rpc` dependency (where some abstractions do not implement the `Send` trait), `BitcoinCoreSv2TDP` must be run within a [`tokio::task::LocalSet`](https://docs.rs/tokio/latest/tokio/task/struct.LocalSet.html). The crate examples demonstrate the proper setup pattern.

### Fee Threshold

Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
//! A simple example of how to use `BitcoinCoreSv2`.
//! A simple example of how to use `BitcoinCoreSv2TDP`.
//!
//! This example demonstrates the pattern used in applications where `BitcoinCoreSv2` is
//! 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 `BitcoinCoreSv2` runs in its
//! 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 `BitcoinCoreSv2` instance once at startup.
//! We send a `CoinbaseOutputConstraints` message to the `BitcoinCoreSv2TDP` instance once at
//! startup.
//!
//! `BitcoinCoreSv2` will not start distributing new templates until it receives the first
//! `BitcoinCoreSv2TDP` will not start distributing new templates until it receives the first
//! `CoinbaseOutputConstraints` message.

use bitcoin_core_sv2::BitcoinCoreSv2;
use bitcoin_core_sv2::template_distribution_protocol::BitcoinCoreSv2TDP;
use std::path::Path;

use async_channel::unbounded;
Expand All @@ -38,7 +39,7 @@ async fn main() {

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

// `BitcoinCoreSv2` uses this to cancel internally spawned tasks
// `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
Expand All @@ -47,16 +48,16 @@ async fn main() {
// the minimum interval between template updates in seconds
let min_interval = 5;

// these messages are sent into the `BitcoinCoreSv2` instance
// 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 `BitcoinCoreSv2` instance
// 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 BitcoinCoreSv2 instance
// 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
Expand All @@ -75,8 +76,8 @@ async fn main() {
let tokio_local_set = tokio::task::LocalSet::new();

tokio_local_set.block_on(&rt, async move {
// create a new `BitcoinCoreSv2` instance
let mut sv2_bitcoin_core = match BitcoinCoreSv2::new(
// create a new `BitcoinCoreSv2TDP` instance
let mut sv2_bitcoin_core = match BitcoinCoreSv2TDP::new(
&bitcoin_core_unix_socket_path_clone,
fee_threshold,
min_interval,
Expand All @@ -94,8 +95,8 @@ async fn main() {
}
};

// run the `BitcoinCoreSv2` instance, which will block until the cancellation token is
// activated
// run the `BitcoinCoreSv2TDP` instance, which will block until the cancellation token
// is activated
sv2_bitcoin_core.run().await;
});
});
Expand All @@ -122,7 +123,7 @@ async fn main() {
return;
}
// monitor for Sv2 Template Distribution Protocol messages
// coming from `BitcoinCoreSv2`
// coming from `BitcoinCoreSv2TDP`
Ok(template_distribution_message) = msg_receiver_from_bitcoin_core_sv2.recv() => {
// log the message
info!("Message received: {}", template_distribution_message);
Expand Down Expand Up @@ -150,7 +151,7 @@ async fn main() {

// send CoinbaseOutputConstraints once at startup
//
// `BitcoinCoreSv2` will not start distributing new templates until it receives the first
// `BitcoinCoreSv2TDP` will not start distributing new templates until it receives the first
// `CoinbaseOutputConstraints` message.
let new_coinbase_output_constraints =
TemplateDistribution::CoinbaseOutputConstraints(CoinbaseOutputConstraints {
Expand All @@ -173,5 +174,5 @@ async fn main() {

// wait for the dedicated thread to finish shutdown
join_handle.join().unwrap();
info!("BitcoinCoreSv2 dedicated thread shutdown complete.");
info!("BitcoinCoreSv2TDP dedicated thread shutdown complete.");
}
25 changes: 25 additions & 0 deletions bitcoin-core-sv2/src/job_declaration_protocol/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//! Error types for the Bitcoin Core IPC integration.

use std::path::Path;
use stratum_core::bitcoin::consensus;

/// Errors from the [`crate::job_declaration_protocol::BitcoinCoreSv2JDP`] layer.
#[derive(Debug)]
pub enum BitcoinCoreSv2JDPError {
/// Cap'n Proto RPC error.
CapnpError(capnp::Error),
/// Failed to connect to the Bitcoin Core Unix socket.
CannotConnectToUnixSocket(Box<Path>, String),
/// Failed to deserialize a block from the IPC response.
FailedToDeserializeBlock(consensus::encode::Error),
/// Failed to deserialize a block header from the IPC response.
FailedToDeserializeBlockHeader(consensus::encode::Error),
/// Readiness signal receiver was dropped before bootstrap completed.
ReadinessSignalFailed,
}

impl From<capnp::Error> for BitcoinCoreSv2JDPError {
fn from(error: capnp::Error) -> Self {
BitcoinCoreSv2JDPError::CapnpError(error)
}
}
204 changes: 204 additions & 0 deletions bitcoin-core-sv2/src/job_declaration_protocol/handlers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
//! Handlers for Job Declaration Protocol messages.

use crate::job_declaration_protocol::{BitcoinCoreSv2JDP, io::JdResponse};
use stratum_core::{
bitcoin::{
Block, Transaction, TxMerkleNode, Txid, Wtxid,
block::{Header, Version},
consensus::serialize,
hashes::Hash,
},
job_declaration_sv2::PushSolution,
};
use tokio::sync::oneshot;

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.
pub(crate) async fn handle_declare_mining_job(
&self,
version: Version,
coinbase_tx: Transaction,
wtxid_list: Vec<Wtxid>,
missing_txs: Vec<Transaction>,
response_tx: oneshot::Sender<JdResponse>,
) {
tracing::info!(
"Validating DeclareMiningJob - version: {:?}, coinbase inputs: {}, outputs: {}, locktime: {}",
version,
coinbase_tx.input.len(),
coinbase_tx.output.len(),
coinbase_tx.lock_time.to_consensus_u32()
);
tracing::debug!(
"Declared coinbase scriptSig: {:?}",
coinbase_tx.input[0].script_sig
);

let (prevhash, nbits, min_ntime, txdata) = {
let mut mempool_mirror = self.mempool_mirror.borrow_mut();

// Add the missing transactions to the mempool mirror
mempool_mirror.add_transactions(&missing_txs);

// Now verify that all wtxids from the declared job are available
let missing_wtxids = mempool_mirror.verify(&wtxid_list);
if !missing_wtxids.is_empty() {
// deliberately ignore potential errors
// we don't care if the receiver dropped the channel
let _ = response_tx.send(JdResponse::MissingTransactions(missing_wtxids));
return;
}

let prevhash = mempool_mirror
.get_current_prev_hash()
.expect("current_prev_hash must be set");
let nbits = mempool_mirror
.get_current_nbits()
.expect("current_nbits must be set");
let min_ntime = mempool_mirror
.get_current_min_ntime()
.expect("current_min_ntime must be set");
let txdata = mempool_mirror.get_txdata(&wtxid_list);

tracing::info!(
"Using prevhash: {:?}, nbits: {:?}, min_ntime: {} from mempool mirror",
prevhash,
nbits,
min_ntime
);

(prevhash, nbits, min_ntime, txdata)
}; // mempool_mirror dropped here, we don't want to hold it across await points

let txid_list: Vec<Txid> = txdata.iter().map(|tx| tx.compute_txid()).collect();

let valid_job = {
let mut all_transactions = Vec::with_capacity(1 + txdata.len());
all_transactions.push(coinbase_tx.clone());
all_transactions.extend(txdata);

let num_transactions = all_transactions.len();

// Use the min_ntime from the template as the block timestamp
// This ensures we meet Bitcoin Core's timestamp validation rules
let block_time = min_ntime;

let header = Header {
version,
prev_blockhash: prevhash,
merkle_root: TxMerkleNode::all_zeros(), // doesn't matter
time: block_time,
bits: nbits,
nonce: 0, // doesn't matter
};

let block = Block {
header,
txdata: all_transactions,
};

let block_bytes: Vec<u8> = serialize(&block);

tracing::debug!(
"Assembled block for checkBlock: {} bytes, {} transactions",
block_bytes.len(),
num_transactions
);

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);

let mut options = match check_block_params.get_options() {
Ok(options) => options,
Err(e) => {
tracing::error!("Failed to get check block options: {e}");
// send error response to the client
// deliberately ignore potential send errors
let _ = response_tx.send(JdResponse::Error("internal-error".to_string()));
tracing::warn!("Terminating Sv2 Bitcoin Core IPC Connection");
self.cancellation_token.cancel();
return;
}
};
options.set_check_merkle_root(false);
options.set_check_pow(false);

let check_block_response = match check_block_request.send().promise.await {
Ok(response) => response,
Err(e) => {
tracing::error!("Failed to send check block request: {e}");
// send error response to the client
// deliberately ignore potential send errors
let _ = response_tx.send(JdResponse::Error("internal-error".to_string()));
tracing::warn!("Terminating Sv2 Bitcoin Core IPC Connection");
self.cancellation_token.cancel();
return;
}
};
let check_block_result = match check_block_response.get() {
Ok(result) => result,
Err(e) => {
tracing::error!("Failed to get check block result: {e}");
// send error response to the client
// deliberately ignore potential send errors
let _ = response_tx.send(JdResponse::Error("internal-error".to_string()));
tracing::warn!("Terminating Sv2 Bitcoin Core IPC Connection");
self.cancellation_token.cancel();
return;
}
};

let result = check_block_result.get_result();
tracing::debug!("checkBlock returned: {}", result);
if !result {
tracing::error!("Bitcoin Core rejected the block via checkBlock");
tracing::debug!(
"Block details - version: {:?}, prev_blockhash: {:?}, bits: {:?}, num_txs: {}",
version,
prevhash,
nbits,
num_transactions
);
tracing::debug!(
"Coinbase tx inputs: {}, outputs: {}",
coinbase_tx.input.len(),
coinbase_tx.output.len()
);
tracing::debug!(
"Block header time: {}, merkle_root: {:?}",
header.time,
header.merkle_root
);
}
result
};

let response = if valid_job {
JdResponse::Success {
prev_hash: prevhash,
nbits,
txid_list,
}
} else {
JdResponse::Error("invalid-job".to_string())
};

// deliberately ignore potential send errors
// we don't care if the receiver dropped the channel
let _ = response_tx.send(response);
}

/// Submits a mining solution to Bitcoin Core.
///
/// Not yet implemented — deliberately left as a stub for future work.
pub(crate) async fn handle_push_solution(&self, _push_solution: PushSolution<'_>) {
// todo
}
}
42 changes: 42 additions & 0 deletions bitcoin-core-sv2/src/job_declaration_protocol/io.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//! 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 tokio::sync::oneshot;

/// A request sent from `jd-server` to the [`BitcoinCoreSv2JDP`](super::BitcoinCoreSv2JDP) IPC
/// thread.
///
/// Built from a `DeclareMiningJob` (plus an optional `ProvideMissingTransactionsSuccess`)
/// or a `PushSolution`.
pub enum JdRequest {
/// Validate a declared mining job via Bitcoin Core's `checkBlock`.
DeclareMiningJob {
version: Version,
coinbase_tx: Transaction,
wtxid_list: Vec<Wtxid>,
missing_txs: Vec<Transaction>,
response_tx: oneshot::Sender<JdResponse>,
},
/// Submit a mining solution to Bitcoin Core (fire-and-forget).
PushSolution {
push_solution: PushSolution<'static>,
},
}

/// The result of trying to handle a DeclareMiningJob request.
#[derive(Debug, Clone)]
pub enum JdResponse {
Success {
prev_hash: BlockHash,
nbits: CompactTarget,
/// 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>,
},
Error(String), // error_code string
MissingTransactions(Vec<Wtxid>),
}
Loading
Loading