use std::{cmp::min, sync::Arc, thread::available_parallelism, time::Duration};
use color_eyre::Report;
use futures::{stream::FuturesUnordered, StreamExt};
use thread_priority::{ThreadBuilder, ThreadPriority};
use tokio::{select, sync::watch, task::JoinHandle, time::sleep};
use tower::Service;
use tracing::{Instrument, Span};
use zakura_chain::{
block::{self, Block},
chain_sync_status::ChainSyncStatus,
chain_tip::ChainTip,
diagnostic::task::WaitForPanics,
serialization::{AtLeastOne, ZcashSerialize},
shutdown::is_shutting_down,
work::equihash::{Solution, SolverCancelled},
};
use zakura_network::AddressBookPeers;
use zakura_node_services::mempool;
use zakura_rpc::{
client::{
BlockTemplateTimeSource,
GetBlockTemplateCapability::{CoinbaseTxn, LongPoll},
GetBlockTemplateParameters,
GetBlockTemplateRequestMode::Template,
HexData,
},
methods::{RpcImpl, RpcServer},
proposal_block_from_template,
};
use zakura_state::WatchReceiver;
use crate::components::metrics::Config;
pub const BLOCK_TEMPLATE_WAIT_TIME: Duration = Duration::from_secs(20);
pub const BLOCK_TEMPLATE_REFRESH_LIMIT: Duration = Duration::from_secs(2);
pub const BLOCK_MINING_WAIT_TIME: Duration = Duration::from_secs(3);
fn should_replace_mining_template(
current_header: Option<block::Header>,
new_header: block::Header,
submit_old: Option<bool>,
) -> bool {
current_header != Some(new_header) && (current_header.is_none() || submit_old != Some(true))
}
fn cancel_if_mining_template_changed(
template_receiver: &mut WatchReceiver<Option<Arc<Block>>>,
old_header: block::Header,
) -> Result<(), SolverCancelled> {
match template_receiver.has_changed() {
Ok(has_changed) => {
template_receiver.mark_as_seen();
if has_changed
&& Some(old_header) != template_receiver.cloned_watch_data().map(|b| *b.header)
{
Err(SolverCancelled)
} else {
Ok(())
}
}
Err(_sender_dropped) => Err(SolverCancelled),
}
}
pub fn spawn_init<Mempool, State, ReadState, Tip, AddressBook, BlockVerifierRouter, SyncStatus>(
config: &Config,
rpc: RpcImpl<Mempool, State, ReadState, Tip, AddressBook, BlockVerifierRouter, SyncStatus>,
) -> JoinHandle<Result<(), Report>>
where
Mempool: Service<
mempool::Request,
Response = mempool::Response,
Error = zakura_node_services::BoxError,
> + Clone
+ Send
+ Sync
+ 'static,
Mempool::Future: Send,
State: Service<
zakura_state::Request,
Response = zakura_state::Response,
Error = zakura_state::BoxError,
> + Clone
+ Send
+ Sync
+ 'static,
<State as Service<zakura_state::Request>>::Future: Send,
ReadState: Service<
zakura_state::ReadRequest,
Response = zakura_state::ReadResponse,
Error = zakura_state::BoxError,
> + Clone
+ Send
+ Sync
+ 'static,
<ReadState as Service<zakura_state::ReadRequest>>::Future: Send,
Tip: ChainTip + Clone + Send + Sync + 'static,
BlockVerifierRouter: Service<
zakura_consensus::Request,
Response = block::Hash,
Error = zakura_consensus::BoxError,
> + Clone
+ Send
+ Sync
+ 'static,
<BlockVerifierRouter as Service<zakura_consensus::Request>>::Future: Send,
SyncStatus: ChainSyncStatus + Clone + Send + Sync + 'static,
AddressBook: AddressBookPeers + Clone + Send + Sync + 'static,
{
tokio::spawn(init(config.clone(), rpc).in_current_span())
}
pub async fn init<Mempool, State, ReadState, Tip, BlockVerifierRouter, SyncStatus, AddressBook>(
_config: Config,
rpc: RpcImpl<Mempool, State, ReadState, Tip, AddressBook, BlockVerifierRouter, SyncStatus>,
) -> Result<(), Report>
where
Mempool: Service<
mempool::Request,
Response = mempool::Response,
Error = zakura_node_services::BoxError,
> + Clone
+ Send
+ Sync
+ 'static,
Mempool::Future: Send,
State: Service<
zakura_state::Request,
Response = zakura_state::Response,
Error = zakura_state::BoxError,
> + Clone
+ Send
+ Sync
+ 'static,
<State as Service<zakura_state::Request>>::Future: Send,
ReadState: Service<
zakura_state::ReadRequest,
Response = zakura_state::ReadResponse,
Error = zakura_state::BoxError,
> + Clone
+ Send
+ Sync
+ 'static,
<ReadState as Service<zakura_state::ReadRequest>>::Future: Send,
Tip: ChainTip + Clone + Send + Sync + 'static,
BlockVerifierRouter: Service<
zakura_consensus::Request,
Response = block::Hash,
Error = zakura_consensus::BoxError,
> + Clone
+ Send
+ Sync
+ 'static,
<BlockVerifierRouter as Service<zakura_consensus::Request>>::Future: Send,
SyncStatus: ChainSyncStatus + Clone + Send + Sync + 'static,
AddressBook: AddressBookPeers + Clone + Send + Sync + 'static,
{
let configured_threads = 1;
let available_threads = available_parallelism()
.map(usize::from)
.unwrap_or(configured_threads);
let solver_count = min(configured_threads, available_threads);
info!(
?solver_count,
"launching mining tasks with parallel solvers"
);
let (template_sender, template_receiver) = watch::channel(None);
let template_receiver = WatchReceiver::new(template_receiver);
let mut abort_handles = Vec::new();
let template_generator = tokio::task::spawn(
generate_block_templates(rpc.clone(), template_sender).in_current_span(),
);
abort_handles.push(template_generator.abort_handle());
let template_generator = template_generator.wait_for_panics();
let mut mining_solvers = FuturesUnordered::new();
for solver_id in 0..solver_count {
let solver_id = min(solver_id, usize::from(u8::MAX))
.try_into()
.expect("just limited to u8::MAX");
let solver = tokio::task::spawn(
run_mining_solver(solver_id, template_receiver.clone(), rpc.clone()).in_current_span(),
);
abort_handles.push(solver.abort_handle());
mining_solvers.push(solver.wait_for_panics());
}
let first_result;
select! {
result = template_generator => { first_result = result; }
result = mining_solvers.next() => {
first_result = result
.expect("stream never terminates because there is at least one solver task");
}
}
for abort_handle in abort_handles {
abort_handle.abort();
}
first_result
}
#[instrument(skip(rpc, template_sender))]
pub async fn generate_block_templates<
Mempool,
State,
ReadState,
Tip,
BlockVerifierRouter,
SyncStatus,
AddressBook,
>(
rpc: RpcImpl<Mempool, State, ReadState, Tip, AddressBook, BlockVerifierRouter, SyncStatus>,
template_sender: watch::Sender<Option<Arc<Block>>>,
) -> Result<(), Report>
where
Mempool: Service<
mempool::Request,
Response = mempool::Response,
Error = zakura_node_services::BoxError,
> + Clone
+ Send
+ Sync
+ 'static,
Mempool::Future: Send,
State: Service<
zakura_state::Request,
Response = zakura_state::Response,
Error = zakura_state::BoxError,
> + Clone
+ Send
+ Sync
+ 'static,
<State as Service<zakura_state::Request>>::Future: Send,
ReadState: Service<
zakura_state::ReadRequest,
Response = zakura_state::ReadResponse,
Error = zakura_state::BoxError,
> + Clone
+ Send
+ Sync
+ 'static,
<ReadState as Service<zakura_state::ReadRequest>>::Future: Send,
Tip: ChainTip + Clone + Send + Sync + 'static,
BlockVerifierRouter: Service<
zakura_consensus::Request,
Response = block::Hash,
Error = zakura_consensus::BoxError,
> + Clone
+ Send
+ Sync
+ 'static,
<BlockVerifierRouter as Service<zakura_consensus::Request>>::Future: Send,
SyncStatus: ChainSyncStatus + Clone + Send + Sync + 'static,
AddressBook: AddressBookPeers + Clone + Send + Sync + 'static,
{
let mut parameters =
GetBlockTemplateParameters::new(Template, None, vec![LongPoll, CoinbaseTxn], None, None);
while !template_sender.is_closed() && !is_shutting_down() {
let template: Result<_, _> = rpc.get_block_template(Some(parameters.clone())).await;
let Ok(template) = template else {
let active_template_invalidated = template_sender.send_if_modified(|template| {
if template.is_none() {
return false;
}
*template = None;
true
});
warn!(
?BLOCK_TEMPLATE_WAIT_TIME,
?template,
?active_template_invalidated,
"waiting for a valid block template",
);
if !is_shutting_down() {
sleep(BLOCK_TEMPLATE_WAIT_TIME).await;
}
continue;
};
let template = template
.try_into_template()
.expect("invalid RPC response: proposal in response to a template request");
let height = template.height();
let transaction_count = template.transactions().len();
let submit_old = template.submit_old();
parameters = GetBlockTemplateParameters::new(
Template,
None,
vec![LongPoll, CoinbaseTxn],
Some(template.long_poll_id()),
None,
);
let block = proposal_block_from_template(
&template,
BlockTemplateTimeSource::CurTime,
rpc.network(),
)?;
let template_replaced = template_sender.send_if_modified(|old_block| {
if !should_replace_mining_template(
old_block.as_ref().map(|block| *block.header),
*block.header,
submit_old,
) {
return false;
}
*old_block = Some(Arc::new(block));
true
});
if template_replaced {
info!(
?height,
transactions = ?transaction_count,
"mining with an updated block template",
);
} else {
debug!(
?height,
transactions = ?transaction_count,
?submit_old,
"keeping valid work across a block template update",
);
}
if !template_sender.is_closed() && !is_shutting_down() {
sleep(BLOCK_TEMPLATE_REFRESH_LIMIT).await;
}
}
Ok(())
}
#[instrument(skip(template_receiver, rpc))]
pub async fn run_mining_solver<
Mempool,
State,
ReadState,
Tip,
BlockVerifierRouter,
SyncStatus,
AddressBook,
>(
solver_id: u8,
mut template_receiver: WatchReceiver<Option<Arc<Block>>>,
rpc: RpcImpl<Mempool, State, ReadState, Tip, AddressBook, BlockVerifierRouter, SyncStatus>,
) -> Result<(), Report>
where
Mempool: Service<
mempool::Request,
Response = mempool::Response,
Error = zakura_node_services::BoxError,
> + Clone
+ Send
+ Sync
+ 'static,
Mempool::Future: Send,
State: Service<
zakura_state::Request,
Response = zakura_state::Response,
Error = zakura_state::BoxError,
> + Clone
+ Send
+ Sync
+ 'static,
<State as Service<zakura_state::Request>>::Future: Send,
ReadState: Service<
zakura_state::ReadRequest,
Response = zakura_state::ReadResponse,
Error = zakura_state::BoxError,
> + Clone
+ Send
+ Sync
+ 'static,
<ReadState as Service<zakura_state::ReadRequest>>::Future: Send,
Tip: ChainTip + Clone + Send + Sync + 'static,
BlockVerifierRouter: Service<
zakura_consensus::Request,
Response = block::Hash,
Error = zakura_consensus::BoxError,
> + Clone
+ Send
+ Sync
+ 'static,
<BlockVerifierRouter as Service<zakura_consensus::Request>>::Future: Send,
SyncStatus: ChainSyncStatus + Clone + Send + Sync + 'static,
AddressBook: AddressBookPeers + Clone + Send + Sync + 'static,
{
while template_receiver.has_changed().is_ok() && !is_shutting_down() {
template_receiver.mark_as_seen();
let template = template_receiver.cloned_watch_data();
let Some(template) = template else {
if solver_id == 0 {
info!(
?solver_id,
?BLOCK_TEMPLATE_WAIT_TIME,
"solver waiting for initial block template"
);
} else {
debug!(
?solver_id,
?BLOCK_TEMPLATE_WAIT_TIME,
"solver waiting for initial block template"
);
}
if !is_shutting_down() {
sleep(BLOCK_TEMPLATE_WAIT_TIME).await;
}
continue;
};
let height = template.coinbase_height().expect("template is valid");
let mut cancel_receiver = template_receiver.clone();
let old_header = *template.header;
let cancel_fn = move || cancel_if_mining_template_changed(&mut cancel_receiver, old_header);
let Ok(blocks) = mine_a_block(solver_id, template, cancel_fn).await else {
if solver_id == 0 {
info!(
?height,
?solver_id,
new_template = ?template_receiver.has_changed(),
shutting_down = ?is_shutting_down(),
"solver cancelled: getting a new block template or shutting down"
);
} else {
debug!(
?height,
?solver_id,
new_template = ?template_receiver.has_changed(),
shutting_down = ?is_shutting_down(),
"solver cancelled: getting a new block template or shutting down"
);
}
if template_receiver.has_changed().is_ok() && !is_shutting_down() {
sleep(BLOCK_TEMPLATE_REFRESH_LIMIT).await;
}
continue;
};
let mut any_success = false;
for block in blocks {
let data = block
.zcash_serialize_to_vec()
.expect("serializing to Vec never fails");
match rpc.submit_block(HexData(data), None).await {
Ok(success) => {
info!(
?height,
hash = ?block.hash(),
?solver_id,
?success,
"successfully mined a new block",
);
any_success = true;
}
Err(error) => info!(
?height,
hash = ?block.hash(),
?solver_id,
?error,
"validating a newly mined block failed, trying again",
),
}
}
if !any_success {
if template_receiver.has_changed().is_ok() && !is_shutting_down() {
sleep(BLOCK_TEMPLATE_REFRESH_LIMIT).await;
}
continue;
}
tokio::select! {
shutdown_result = template_receiver.changed() => shutdown_result?,
_ = sleep(BLOCK_MINING_WAIT_TIME) => {}
}
}
Ok(())
}
pub async fn mine_a_block<F>(
solver_id: u8,
template: Arc<Block>,
cancel_fn: F,
) -> Result<AtLeastOne<Block>, SolverCancelled>
where
F: FnMut() -> Result<(), SolverCancelled> + Send + Sync + 'static,
{
let mut header = *template.header;
*header.nonce.first_mut().unwrap() = solver_id;
*header.nonce.last_mut().unwrap() = solver_id;
let span = Span::current();
let solved_headers =
tokio::task::spawn_blocking(move || span.in_scope(move || {
let miner_thread_handle = ThreadBuilder::default().name("zakura-miner").priority(ThreadPriority::Min).spawn(move |priority_result| {
if let Err(error) = priority_result {
info!(?error, "could not set miner to run at a low priority: running at default priority");
}
Solution::solve(header, cancel_fn)
}).expect("unable to spawn miner thread");
miner_thread_handle.wait_for_panics()
}))
.wait_for_panics()
.await?;
let block = (*template).clone();
let solved_blocks: Vec<Block> = solved_headers
.into_iter()
.map(|header| {
let mut block = block.clone();
block.header = Arc::new(header);
block
})
.collect();
Ok(solved_blocks
.try_into()
.expect("a 1:1 mapping of AtLeastOne produces at least one block"))
}
#[cfg(test)]
mod tests {
use tower::buffer::Buffer;
use zakura_chain::{
chain_sync_status::MockSyncStatus, chain_tip::mock::MockChainTip, parameters::Network,
serialization::ZcashDeserializeInto,
};
use zakura_network::address_book_peers::MockAddressBookPeers;
use zakura_rpc::config::mining::{default_miner_address, MinerAddressType};
use zakura_test::mock_service::MockService;
use super::*;
#[test]
fn mining_template_updates_respect_submit_old() {
let block = zakura_test::vectors::BLOCK_MAINNET_1_BYTES
.zcash_deserialize_into::<Arc<Block>>()
.expect("block 1 deserializes");
let current_header = *block.header;
let mut changed_header = current_header;
changed_header.nonce.0[0] ^= 1;
assert!(should_replace_mining_template(None, current_header, None));
assert!(!should_replace_mining_template(
Some(current_header),
current_header,
Some(false),
));
assert!(!should_replace_mining_template(
Some(current_header),
changed_header,
Some(true),
));
assert!(should_replace_mining_template(
Some(current_header),
changed_header,
Some(false),
));
assert!(should_replace_mining_template(
Some(current_header),
changed_header,
None,
));
}
#[test]
fn unavailable_mining_template_cancels_current_work() {
let block = zakura_test::vectors::BLOCK_MAINNET_1_BYTES
.zcash_deserialize_into::<Arc<Block>>()
.expect("block 1 deserializes");
let old_header = *block.header;
let (template_sender, template_receiver) = watch::channel(Some(block.clone()));
let mut template_receiver = WatchReceiver::new(template_receiver);
template_sender
.send(Some(block))
.expect("template receiver remains open");
assert!(cancel_if_mining_template_changed(&mut template_receiver, old_header).is_ok());
template_sender
.send(None)
.expect("template receiver remains open");
assert!(matches!(
cancel_if_mining_template_changed(&mut template_receiver, old_header),
Err(SolverCancelled)
));
}
#[tokio::test]
async fn template_generation_failure_invalidates_current_template() {
let network = Network::Mainnet;
let block = zakura_test::vectors::BLOCK_MAINNET_1_BYTES
.zcash_deserialize_into::<Arc<Block>>()
.expect("block 1 deserializes");
let (template_sender, mut template_receiver) = watch::channel(Some(block.clone()));
let mining_config = zakura_rpc::config::mining::Config {
miner_address: Some(
default_miner_address(network.kind(), &MinerAddressType::Transparent)
.parse()
.expect("default Mainnet miner address is valid"),
),
internal_miner: true,
..Default::default()
};
let (chain_tip, chain_tip_sender) = MockChainTip::new();
chain_tip_sender.send_best_tip_height(block::Height(1));
chain_tip_sender.send_best_tip_hash(block.hash());
chain_tip_sender.send_estimated_distance_to_network_chain_tip(Some(0));
let mut sync_status = MockSyncStatus::default();
sync_status.set_is_close_to_tip(false);
let mempool = Buffer::new(
MockService::build().for_unit_tests::<
mempool::Request,
mempool::Response,
zakura_node_services::BoxError,
>(),
1,
);
let state = Buffer::new(
MockService::build().for_unit_tests::<
zakura_state::Request,
zakura_state::Response,
zakura_state::BoxError,
>(),
1,
);
let read_state = Buffer::new(
MockService::build().for_unit_tests::<
zakura_state::ReadRequest,
zakura_state::ReadResponse,
zakura_state::BoxError,
>(),
1,
);
let block_verifier = Buffer::new(
MockService::build().for_unit_tests::<
zakura_consensus::Request,
block::Hash,
zakura_consensus::BoxError,
>(),
1,
);
let (_last_log_sender, last_log_receiver) = watch::channel(None);
let (rpc, rpc_queue_task) = RpcImpl::new(
network,
mining_config,
false,
"0.0.1",
"miner test",
mempool,
state,
read_state,
block_verifier,
sync_status,
chain_tip,
MockAddressBookPeers::default(),
last_log_receiver,
None,
);
let template_generator = tokio::spawn(generate_block_templates(rpc, template_sender));
tokio::time::timeout(Duration::from_secs(1), template_receiver.changed())
.await
.expect("template generation failure invalidates promptly")
.expect("template sender remains open");
assert!(
template_receiver.borrow().is_none(),
"an unsynced node must not retain its last valid mining template"
);
template_generator.abort();
rpc_queue_task.abort();
}
}