Skip to content
Merged
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
82 changes: 82 additions & 0 deletions integration-tests/tests/jd_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,88 @@ async fn jds_should_not_panic_if_jdc_shutsdown() {
shutdown_all!(jdc_1, pool);
}

// This test verifies that mode state is isolated per JDC instance.
//
// We start one JDC in solo mining mode (no upstreams) and then start another
// JDC in full template mode (with upstream). The solo instance must not start
// behaving like full-template mode after the second instance activates.
#[tokio::test]
async fn multiple_jdc_sessions() {
start_tracing();
let (tp, tp_addr) = start_template_provider(Some(1), DifficultyLevel::Low);
let (pool, pool_addr, jds_addr, _) =
start_pool_with_jds(tp.bitcoin_core(), vec![], vec![], false).await;

let (solo_tp_sniffer, solo_tp_sniffer_addr) =
start_sniffer("solo-jdc-tp", tp_addr, false, vec![], None);
let (solo_jdc, solo_jdc_addr, _) = start_jdc(
&[],
sv2_tp_config(solo_tp_sniffer_addr),
vec![],
vec![],
false,
Some(jd_client_sv2::config::ConfigJDCMode::SoloMining),
);
let _solo_downstream = MockDownstream::new(
solo_jdc_addr,
WithSetup::yes_with_defaults(Protocol::MiningProtocol, 0),
)
.start()
.await;

solo_tp_sniffer
.wait_for_message_type(MessageDirection::ToUpstream, MESSAGE_TYPE_SETUP_CONNECTION)
.await;
solo_tp_sniffer
.wait_for_message_type(
MessageDirection::ToDownstream,
MESSAGE_TYPE_SETUP_CONNECTION_SUCCESS,
)
.await;
solo_tp_sniffer.clean_queue(MessageDirection::ToUpstream);
solo_tp_sniffer.clean_queue(MessageDirection::ToDownstream);

let (full_jds_sniffer, full_jds_sniffer_addr) =
start_sniffer("full-jdc-jds", jds_addr, false, vec![], None);
let (full_jdc, _full_jdc_addr, _) = start_jdc(
&[(pool_addr, full_jds_sniffer_addr)],
sv2_tp_config(tp_addr),
vec![],
vec![],
false,
Some(jd_client_sv2::config::ConfigJDCMode::FullTemplate),
);

full_jds_sniffer
.wait_for_message_type(MessageDirection::ToUpstream, MESSAGE_TYPE_SETUP_CONNECTION)
.await;
full_jds_sniffer
.wait_for_message_type(
MessageDirection::ToDownstream,
MESSAGE_TYPE_SETUP_CONNECTION_SUCCESS,
)
.await;

// Trigger post-start template updates; using two blocks reduces timing flakiness.
tp.generate_blocks(1);
tp.generate_blocks(1);

// RequestTransactionData is FullTemplate-only. If mode leaked process-wide,
// the solo JDC would emit this after the full-template JDC activates.
assert!(
solo_tp_sniffer
.assert_message_not_present(
MessageDirection::ToUpstream,
MESSAGE_TYPE_REQUEST_TRANSACTION_DATA,
std::time::Duration::from_secs(2),
)
.await,
"Solo-mode JDC should not request transaction data after another JDC activates full-template mode"
);

shutdown_all!(solo_jdc, full_jdc, pool);
}

// This test verifies that jd-client exchange SetupConnection messages with a Template Provider.
//
// Note that jd-client starts to exchange messages with the Template Provider after it has accepted
Expand Down
57 changes: 57 additions & 0 deletions integration-tests/tests/translator_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1950,3 +1950,60 @@ async fn tproxy_sends_single_open_extended_mining_channel_in_aggregated_mode() {

shutdown_all!(pool, tproxy);
}

// This test verifies whether we can spawn multiple tproxy in the
// same process.
//
// More info here: https://github.com/stratum-mining/sv2-apps/issues/430
#[tokio::test]
async fn multiple_tproxy_sessions() {
start_tracing();
let (_tp, tp_addr) = start_template_provider(None, DifficultyLevel::High);
let (pool, pool_addr, _) = start_pool(sv2_tp_config(tp_addr), vec![], vec![], false).await;

let (pool_translator_sniffer_1, pool_translator_sniffer_addr_1) =
start_sniffer("0", pool_addr, false, vec![], None);
let (tproxy_1, _, _) = start_sv2_translator(
&[pool_translator_sniffer_addr_1],
true,
vec![],
vec![],
None,
false,
)
.await;

let (pool_translator_sniffer_2, pool_translator_sniffer_addr_2) =
start_sniffer("0", pool_addr, false, vec![], None);
let (tproxy_2, _, _) = start_sv2_translator(
&[pool_translator_sniffer_addr_2],
true,
vec![],
vec![],
None,
false,
)
.await;

pool_translator_sniffer_1
.wait_for_message_type(MessageDirection::ToUpstream, MESSAGE_TYPE_SETUP_CONNECTION)
.await;
pool_translator_sniffer_1
.wait_for_message_type(
MessageDirection::ToDownstream,
MESSAGE_TYPE_SETUP_CONNECTION_SUCCESS,
)
.await;

pool_translator_sniffer_2
.wait_for_message_type(MessageDirection::ToUpstream, MESSAGE_TYPE_SETUP_CONNECTION)
.await;
pool_translator_sniffer_2
.wait_for_message_type(
MessageDirection::ToDownstream,
MESSAGE_TYPE_SETUP_CONNECTION_SUCCESS,
)
.await;

shutdown_all!(pool, tproxy_1, tproxy_2);
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ use crate::{
ChannelManager, ChannelManagerChannel, SharesOrderedByDiff, SOLO_FULL_EXTRANONCE_SIZE,
},
error::{self, JDCError, JDCErrorKind},
jd_mode::{get_jd_mode, JdMode},
utils::{add_share_to_cache, create_close_channel_msg},
};

Expand Down Expand Up @@ -100,8 +99,8 @@ impl RouteMessageTo<'_> {
/// The routing is handled as follows:
/// - [`RouteMessageTo::Downstream`] → Sends the mining message to the specified downstream
/// client.
/// - [`RouteMessageTo::Upstream`] → Sends the mining message upstream, unless in
/// [`JdMode::SoloMining`].
/// - [`RouteMessageTo::Upstream`] → Forwards mining message upstream. In solo mode,
/// upstream-directed messages should not be produced.
/// - [`RouteMessageTo::JobDeclarator`] → Sends the job declaration message to the JDS.
/// - [`RouteMessageTo::TemplateProvider`] → Sends the template distribution message to the
/// template provider.
Expand All @@ -121,14 +120,12 @@ impl RouteMessageTo<'_> {
}
}
RouteMessageTo::Upstream(message) => {
if get_jd_mode() != JdMode::SoloMining {
let message_static = message.into_static();
let sv2_frame: Sv2Frame = AnyMessage::Mining(message_static).try_into()?;
channel_manager_channel
.upstream_sender
.send(sv2_frame)
.await?;
}
let message_static = message.into_static();
let sv2_frame: Sv2Frame = AnyMessage::Mining(message_static).try_into()?;
channel_manager_channel
.upstream_sender
.send(sv2_frame)
.await?;
}
RouteMessageTo::JobDeclarator(message) => {
channel_manager_channel
Expand Down
4 changes: 4 additions & 0 deletions miner-apps/jd-client/src/lib/channel_manager/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ use crate::{
config::JobDeclaratorClientConfig,
downstream::Downstream,
error::{self, Action, JDCError, JDCErrorKind, JDCResult, LoopControl},
jd_mode::JDMode,
utils::{
AtomicUpstreamState, DownstreamChannelJobId, DownstreamMessage, PendingChannelRequest,
SharesOrderedByDiff, UpstreamState,
Expand Down Expand Up @@ -284,6 +285,7 @@ pub struct ChannelManager {
/// 3. Connected: An upstream channel is successfully established.
/// 4. SoloMining: No upstream is available; the JDC operates in solo mining mode. case.
pub upstream_state: AtomicUpstreamState,
pub mode: JDMode,
}

#[cfg_attr(not(test), hotpath::measure_all)]
Expand Down Expand Up @@ -345,6 +347,7 @@ impl ChannelManager {
coinbase_outputs: Vec<u8>,
supported_extensions: Vec<u16>,
required_extensions: Vec<u16>,
mode: JDMode,
) -> JDCResult<Self, error::ChannelManager> {
// Start with a solo-mining allocator (no upstream prefix). Once the
// upstream channel is opened in `handle_open_extended_mining_channel_success`
Expand Down Expand Up @@ -399,6 +402,7 @@ impl ChannelManager {
reserved_downstream_rollable_extranonce_size: config
.reserved_downstream_rollable_extranonce_size(),
upstream_state: AtomicUpstreamState::new(UpstreamState::SoloMining),
mode,
};

Ok(channel_manager)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ use tracing::{error, info, warn};
use crate::{
channel_manager::{downstream_message_handler::RouteMessageTo, ChannelManager, DeclaredJob},
error::{self, JDCError, JDCErrorKind},
jd_mode::{get_jd_mode, JdMode},
};

#[cfg_attr(not(test), hotpath::measure_all)]
Expand Down Expand Up @@ -63,7 +62,7 @@ impl HandleTemplateDistributionMessagesFromServerAsync for ChannelManager {
let mut coinbase_outputs = deserialize_outputs(coinbase_outputs)
.map_err(|_| JDCError::shutdown(JDCErrorKind::ChannelManagerHasBadCoinbaseOutputs))?;

if get_jd_mode() == JdMode::FullTemplate {
if self.mode.is_full_template() {
let tx_data_request =
TemplateDistribution::RequestTransactionData(RequestTransactionData {
template_id: msg.template_id,
Expand All @@ -81,7 +80,7 @@ impl HandleTemplateDistributionMessagesFromServerAsync for ChannelManager {
coinbase_outputs[0].value = Amount::from_sat(msg.coinbase_tx_value_remaining);

let coinbase_only_token = if !msg.future_template
&& get_jd_mode() == JdMode::CoinbaseOnly
&& self.mode.is_coinbase_only()
&& channel_manager_data.upstream_channel.is_some()
&& channel_manager_data.last_new_prev_hash.is_some()
&& channel_manager_data.job_factory.is_some()
Expand Down Expand Up @@ -440,7 +439,7 @@ impl HandleTemplateDistributionMessagesFromServerAsync for ChannelManager {
(data.last_future_template.clone(), declare_job)
});

if get_jd_mode() == JdMode::FullTemplate {
if self.mode.is_full_template() {
if let Some(Some(job)) = declare_job {
let message = JobDeclaration::DeclareMiningJob(job);

Expand All @@ -467,7 +466,7 @@ impl HandleTemplateDistributionMessagesFromServerAsync for ChannelManager {
if let Some(ref mut upstream_channel) = channel_manager_data.upstream_channel {
_ = upstream_channel.on_chain_tip_update(msg.clone().into());

if get_jd_mode() == JdMode::CoinbaseOnly
if self.mode.is_coinbase_only()
&& channel_manager_data.job_factory.is_some()
&& future_template.is_some()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ use crate::{
JDC_LOCAL_PREFIX_BYTES, JDC_MAX_CHANNELS,
},
error::{self, JDCError, JDCErrorKind},
jd_mode::{get_jd_mode, JdMode},
utils::{create_close_channel_msg, validate_cached_share, UpstreamState},
};

Expand Down Expand Up @@ -179,7 +178,7 @@ impl HandleMiningMessagesFromServerAsync for ChannelManager {
debug!("Applied last_new_prev_hash to new extended channel");
}

let set_custom_job = if get_jd_mode() == JdMode::CoinbaseOnly
let set_custom_job = if self.mode.is_coinbase_only()
&& data.job_factory.is_some()
&& data.last_future_template.is_some()
&& data.last_new_prev_hash.is_some()
Expand Down Expand Up @@ -254,7 +253,7 @@ impl HandleMiningMessagesFromServerAsync for ChannelManager {
});

if channel_state == UpstreamState::Connected {
if get_jd_mode() == JdMode::FullTemplate {
if self.mode.is_full_template() {
if let Some(template) = template {
let tx_data_request =
TemplateDistribution::RequestTransactionData(RequestTransactionData {
Expand All @@ -268,7 +267,7 @@ impl HandleMiningMessagesFromServerAsync for ChannelManager {
}
}

if get_jd_mode() == JdMode::CoinbaseOnly {
if self.mode.is_coinbase_only() {
if let Some(custom_job) = custom_job {
let set_custom_job = Mining::SetCustomMiningJob(custom_job);
let sv2_frame: Sv2Frame = AnyMessage::Mining(set_custom_job)
Expand Down
2 changes: 1 addition & 1 deletion miner-apps/jd-client/src/lib/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ impl JobDeclaratorClientConfig {
}
}

#[derive(Debug, Deserialize, Clone, Default, PartialEq)]
#[derive(Debug, Deserialize, Clone, Copy, Default, PartialEq)]
#[serde(rename_all = "UPPERCASE")]
pub enum ConfigJDCMode {
#[default]
Expand Down
Loading
Loading