use std::sync::{
atomic::{AtomicBool, AtomicU32},
Arc,
};
use async_channel::{unbounded, Receiver, Sender};
use stratum_apps::{
bitcoin_core_sv2::common::template_distribution_protocol::CancellationToken,
channel_utils::ReceiverCleanup,
network_helpers::noise_stream::NoiseTcpStream,
stratum_core::{
channels_sv2::server::{
extended::ExtendedChannel, group::GroupChannel, standard::StandardChannel,
},
common_messages_sv2::MESSAGE_TYPE_SETUP_CONNECTION,
framing_sv2,
handlers_sv2::{HandleCommonMessagesFromClientAsync, HandleExtensionsFromClientAsync},
parsers_sv2::{parse_message_frame_with_tlvs, AnyMessage, Mining, Tlv},
},
sync::{SharedLock, SharedMap},
task_manager::TaskManager,
utils::{
protocol_message_type::{protocol_message_type, MessageType},
types::{ChannelId, DownstreamId, Message, Sv2Frame},
},
};
use tracing::{debug, error, warn};
use crate::{
error::{self, Action, LoopControl, PoolError, PoolErrorKind, PoolResult},
io_task::spawn_io_tasks,
utils::PayoutMode,
};
mod common_message_handler;
mod extensions_message_handler;
#[derive(Clone)]
pub struct DownstreamIo {
channel_manager_sender: Sender<(DownstreamId, Mining<'static>, Option<Vec<Tlv>>)>,
channel_manager_receiver: Receiver<(Mining<'static>, Option<Vec<Tlv>>)>,
downstream_sender: Sender<Sv2Frame>,
downstream_receiver: Receiver<Sv2Frame>,
}
impl DownstreamIo {
fn close(&self) {
self.downstream_sender.close();
self.channel_manager_receiver.close_and_drain();
self.downstream_receiver.close_and_drain();
}
}
#[derive(Clone)]
pub struct Downstream {
pub group_channel: SharedLock<GroupChannel<'static>>,
pub extended_channels: SharedMap<ChannelId, ExtendedChannel<'static>>,
pub standard_channels: SharedMap<ChannelId, StandardChannel<'static>>,
pub channel_id_factory: Arc<AtomicU32>,
pub negotiated_extensions: SharedLock<Vec<u16>>,
pub payout_mode: SharedLock<Option<PayoutMode>>,
downstream_io: DownstreamIo,
pub downstream_id: usize,
pub requires_standard_jobs: Arc<AtomicBool>,
pub requires_custom_work: Arc<AtomicBool>,
pub supported_extensions: Vec<u16>,
pub required_extensions: Vec<u16>,
downstream_connection_token: CancellationToken,
}
#[cfg_attr(not(test), hotpath::measure_all)]
impl Downstream {
fn handle_error_action(
&self,
context: &str,
e: &PoolError<error::Downstream>,
cancellation_token: &CancellationToken,
) -> LoopControl {
if cancellation_token.is_cancelled() {
debug!(
error_kind = ?e.kind,
"{context} returned an error after shutdown was requested"
);
return LoopControl::Continue;
}
match e.action {
Action::Log => {
warn!(
downstream_id = self.downstream_id,
error_kind = ?e.kind,
"{context} returned a log-only error"
);
LoopControl::Continue
}
Action::Disconnect(_) => {
warn!(
downstream_id = self.downstream_id,
error_kind = ?e.kind,
"{context} requested disconnect; cancelling downstream token"
);
self.downstream_connection_token.cancel();
LoopControl::Break
}
Action::Shutdown => {
warn!(
downstream_id = self.downstream_id,
error_kind = ?e.kind,
"{context} requested shutdown; cancelling global token"
);
cancellation_token.cancel();
LoopControl::Break
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn new(
downstream_id: DownstreamId,
channel_id_factory: AtomicU32,
group_channel: GroupChannel<'static>,
channel_manager_sender: Sender<(DownstreamId, Mining<'static>, Option<Vec<Tlv>>)>,
channel_manager_receiver: Receiver<(Mining<'static>, Option<Vec<Tlv>>)>,
noise_stream: NoiseTcpStream<Message>,
cancellation_token: CancellationToken,
task_manager: Arc<TaskManager>,
supported_extensions: Vec<u16>,
required_extensions: Vec<u16>,
) -> Self {
let (noise_stream_reader, noise_stream_writer) = noise_stream.into_split();
let (inbound_tx, inbound_rx) = unbounded::<Sv2Frame>();
let (outbound_tx, outbound_rx) = unbounded::<Sv2Frame>();
let downstream_connection_token = cancellation_token.child_token();
spawn_io_tasks(
task_manager,
noise_stream_reader,
noise_stream_writer,
outbound_rx,
inbound_tx,
downstream_connection_token.clone(),
);
let downstream_io = DownstreamIo {
channel_manager_receiver,
channel_manager_sender,
downstream_sender: outbound_tx,
downstream_receiver: inbound_rx,
};
Downstream {
downstream_io,
group_channel: SharedLock::new(group_channel),
extended_channels: SharedMap::new(),
standard_channels: SharedMap::new(),
channel_id_factory: Arc::new(channel_id_factory),
negotiated_extensions: SharedLock::new(Vec::new()),
payout_mode: SharedLock::new(None),
downstream_id,
requires_standard_jobs: Arc::new(AtomicBool::new(false)),
requires_custom_work: Arc::new(AtomicBool::new(false)),
supported_extensions,
required_extensions,
downstream_connection_token,
}
}
pub async fn start(
mut self,
cancellation_token: CancellationToken,
task_manager: Arc<TaskManager>,
on_disconnect: impl FnOnce(DownstreamId) + Send + 'static,
) {
if let Err(e) = self.setup_connection_with_downstream().await {
error!(?e, "Failed to set up downstream connection");
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
_ = self.handle_error_action(
"Downstream::setup_connection_with_downstream",
&e,
&cancellation_token,
);
on_disconnect(self.downstream_id);
self.downstream_io.close();
return;
}
task_manager.spawn(async move {
loop {
let mut self_clone_1 = self.clone();
let downstream_id = self_clone_1.downstream_id;
let self_clone_2 = self.clone();
tokio::select! {
biased;
_ = cancellation_token.cancelled() => {
debug!("Downstream {downstream_id}: received shutdown signal");
break;
}
res = self_clone_1.handle_downstream_message() => {
if let Err(e) = res {
error!(?e, "Error handling downstream message for {downstream_id}");
if let LoopControl::Break = self.handle_error_action(
"Downstream::handle_downstream_message",
&e,
&cancellation_token,
) {
break;
}
}
}
res = self_clone_2.handle_channel_manager_message() => {
if let Err(e) = res {
error!(?e, "Error handling channel manager message for {downstream_id}");
if let LoopControl::Break = self.handle_error_action(
"Downstream::handle_channel_manager_message",
&e,
&cancellation_token,
) {
break;
}
}
}
}
}
self.downstream_connection_token.cancel();
self.downstream_io.close();
on_disconnect(self.downstream_id);
warn!("Downstream: unified message loop exited.");
});
}
async fn setup_connection_with_downstream(&mut self) -> PoolResult<(), error::Downstream> {
let mut frame = self
.downstream_io
.downstream_receiver
.recv()
.await
.map_err(|error| PoolError::disconnect(error, self.downstream_id))?;
let Some(header) = frame.get_header() else {
error!("SV2 frame missing header");
return Err(PoolError::disconnect(
framing_sv2::Error::MissingHeader,
self.downstream_id,
));
};
if header.msg_type() == MESSAGE_TYPE_SETUP_CONNECTION {
self.handle_common_message_frame_from_client(
Some(self.downstream_id),
header,
frame.payload(),
)
.await?;
return Ok(());
}
Err(PoolError::disconnect(
PoolErrorKind::UnexpectedMessage(
header.ext_type_without_channel_msg(),
header.msg_type(),
),
self.downstream_id,
))
}
async fn handle_channel_manager_message(self) -> PoolResult<(), error::Downstream> {
let (msg, _tlv_fields) = match self.downstream_io.channel_manager_receiver.recv().await {
Ok(msg) => msg,
Err(e) => {
warn!(
?e,
"Channel manager receiver closed - disconnecting downstream"
);
return Err(PoolError::disconnect(
PoolErrorKind::ChannelRecv(e),
self.downstream_id,
));
}
};
let message = AnyMessage::Mining(msg);
let std_frame: Sv2Frame = message.try_into().map_err(PoolError::shutdown)?;
self.downstream_io
.downstream_sender
.send(std_frame)
.await
.map_err(|e| {
error!(?e, "Downstream send failed");
PoolError::disconnect(PoolErrorKind::ChannelErrorSender, self.downstream_id)
})?;
Ok(())
}
async fn handle_downstream_message(&mut self) -> PoolResult<(), error::Downstream> {
let mut sv2_frame = self
.downstream_io
.downstream_receiver
.recv()
.await
.map_err(|error| PoolError::disconnect(error, self.downstream_id))?;
let Some(header) = sv2_frame.get_header() else {
error!("SV2 frame missing header");
return Err(PoolError::disconnect(
framing_sv2::Error::MissingHeader,
self.downstream_id,
));
};
match protocol_message_type(header.ext_type(), header.msg_type()) {
MessageType::Mining => {
debug!("Received mining SV2 frame from downstream.");
let negotiated_extensions = self
.negotiated_extensions
.get()
.map_err(PoolError::shutdown)?;
let (any_message, tlv_fields) = parse_message_frame_with_tlvs(
header,
sv2_frame.payload(),
&negotiated_extensions,
)
.map_err(|error| PoolError::disconnect(error, self.downstream_id))?;
let mining_message = match any_message {
AnyMessage::Mining(msg) => msg,
_ => {
error!("Expected Mining message but got different type");
return Err(PoolError::disconnect(
PoolErrorKind::UnexpectedMessage(
header.ext_type_without_channel_msg(),
header.msg_type(),
),
self.downstream_id,
));
}
};
self.downstream_io
.channel_manager_sender
.send((self.downstream_id, mining_message, tlv_fields))
.await
.map_err(|e| {
error!(?e, "Failed to send mining message to channel manager.");
PoolError::shutdown(e)
})?;
}
MessageType::Extensions => {
self.handle_extensions_message_frame_from_client(None, header, sv2_frame.payload())
.await?;
}
MessageType::Common
| MessageType::JobDeclaration
| MessageType::TemplateDistribution => {
warn!(
ext_type = ?header.ext_type(),
msg_type = ?header.msg_type(),
"Received unexpected message from downstream."
);
}
MessageType::Unknown => {
warn!(
ext_type = ?header.ext_type(),
msg_type = ?header.msg_type(),
"Received unknown message from downstream."
);
}
}
Ok(())
}
}