use crate::job_declaration_protocol::{
BitcoinCoreSv2JDP,
io::{JdResponse, ValidationContext},
mempool::decode_bip34_height_from_coinbase_script_sig,
};
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 {
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 declared_bip34_height = coinbase_tx
.input
.first()
.and_then(|input| {
decode_bip34_height_from_coinbase_script_sig(input.script_sig.as_bytes())
})
.unwrap_or_else(|| coinbase_tx.lock_time.to_consensus_u32());
let (initial_validation_context, initial_bip34_height, txdata) = {
let mut mempool_mirror = self.mempool_mirror.borrow_mut();
mempool_mirror.add_transactions(missing_txs);
let prev_hash = 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 initial_validation_context = ValidationContext {
prev_hash,
nbits,
min_ntime,
};
let initial_bip34_height = mempool_mirror
.get_current_bip34_height()
.expect("current_bip34_height must be set");
let missing_wtxids = mempool_mirror.verify(&wtxid_list);
if !missing_wtxids.is_empty() {
let _ = response_tx.send(JdResponse::MissingTransactions {
missing_wtxids,
validation_context: initial_validation_context,
});
return;
}
let txdata = mempool_mirror.get_txdata(&wtxid_list);
tracing::info!(
"Using prevhash: {:?}, nbits: {:?}, min_ntime: {}, bip34_height: {} from mempool mirror",
initial_validation_context.prev_hash,
initial_validation_context.nbits,
initial_validation_context.min_ntime,
initial_bip34_height
);
(initial_validation_context, initial_bip34_height, txdata)
};
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();
let block_time = initial_validation_context.min_ntime;
let header = Header {
version,
prev_blockhash: initial_validation_context.prev_hash,
merkle_root: TxMerkleNode::all_zeros(), time: block_time,
bits: initial_validation_context.nbits,
nonce: 0, };
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();
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}");
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_request.get().get_options() {
Ok(options) => options,
Err(e) => {
tracing::error!("Failed to get check block options: {e}");
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;
}
};
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}");
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;
}
};
let check_block_result = match check_block_response.get() {
Ok(result) => result,
Err(e) => {
tracing::error!("Failed to get check block result: {e}");
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;
}
};
let result = check_block_result.get_result();
let check_block_reason = check_block_result.get_reason();
let check_block_debug = check_block_result.get_debug();
tracing::debug!("checkBlock returned: {}", result);
if !result {
tracing::error!(
reason = ?check_block_reason,
debug = ?check_block_debug,
"Bitcoin Core rejected the block via checkBlock"
);
tracing::debug!(
"Block details - version: {:?}, prev_blockhash: {:?}, bits: {:?}, num_txs: {}",
version,
initial_validation_context.prev_hash,
initial_validation_context.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
};
if !valid_job {
if let Err(e) = self.force_update_mempool_mirror().await {
tracing::debug!(
error = ?e,
"Failed to force-refresh template/mempool mirror after checkBlock failure; continuing with current validation context"
);
}
}
let (latest_validation_context, latest_bip34_height) = {
let mempool_mirror = self.mempool_mirror.borrow();
let latest_validation_context = ValidationContext {
prev_hash: mempool_mirror
.get_current_prev_hash()
.expect("current_prev_hash must be set"),
nbits: mempool_mirror
.get_current_nbits()
.expect("current_nbits must be set"),
min_ntime: mempool_mirror
.get_current_min_ntime()
.expect("current_min_ntime must be set"),
};
let latest_bip34_height = mempool_mirror
.get_current_bip34_height()
.expect("current_bip34_height must be set");
(latest_validation_context, latest_bip34_height)
};
let response = if valid_job {
JdResponse::Success {
prev_hash: initial_validation_context.prev_hash,
nbits: initial_validation_context.nbits,
min_ntime: initial_validation_context.min_ntime,
txid_list,
}
} else {
let stale_at_arrival_by_bip34 = declared_bip34_height != latest_bip34_height;
let context_drifted = initial_validation_context.prev_hash
!= latest_validation_context.prev_hash
|| initial_validation_context.nbits != latest_validation_context.nbits
|| initial_validation_context.min_ntime != latest_validation_context.min_ntime
|| initial_bip34_height != latest_bip34_height
|| stale_at_arrival_by_bip34;
let error_code = if context_drifted {
tracing::debug!(
initial_prev_hash = ?initial_validation_context.prev_hash,
initial_nbits = ?initial_validation_context.nbits,
initial_min_ntime = initial_validation_context.min_ntime,
initial_bip34_height,
declared_bip34_height,
latest_prev_hash = ?latest_validation_context.prev_hash,
latest_nbits = ?latest_validation_context.nbits,
latest_min_ntime = latest_validation_context.min_ntime,
latest_bip34_height,
stale_at_arrival_by_bip34,
"Detected stale chain tip during DeclareMiningJob validation; classifying error as stale-chain-tip"
);
"stale-chain-tip".to_string()
} else {
"invalid-job".to_string()
};
JdResponse::Error {
error_code,
validation_context: latest_validation_context,
}
};
let _ = response_tx.send(response);
}
pub(crate) async fn handle_push_solution(&self, _push_solution: PushSolution<'_>) {
}
}