diff --git a/integration-tests/tests/jd_integration.rs b/integration-tests/tests/jd_integration.rs index 251d38b33..d05097a1e 100644 --- a/integration-tests/tests/jd_integration.rs +++ b/integration-tests/tests/jd_integration.rs @@ -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 diff --git a/integration-tests/tests/translator_integration.rs b/integration-tests/tests/translator_integration.rs index c3bc5efa9..42fb5e32f 100644 --- a/integration-tests/tests/translator_integration.rs +++ b/integration-tests/tests/translator_integration.rs @@ -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); +} diff --git a/miner-apps/jd-client/src/lib/channel_manager/downstream_message_handler.rs b/miner-apps/jd-client/src/lib/channel_manager/downstream_message_handler.rs index 951622998..dd6e48661 100644 --- a/miner-apps/jd-client/src/lib/channel_manager/downstream_message_handler.rs +++ b/miner-apps/jd-client/src/lib/channel_manager/downstream_message_handler.rs @@ -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}, }; @@ -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. @@ -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 diff --git a/miner-apps/jd-client/src/lib/channel_manager/mod.rs b/miner-apps/jd-client/src/lib/channel_manager/mod.rs index ba1464ef7..f69c9b8fa 100644 --- a/miner-apps/jd-client/src/lib/channel_manager/mod.rs +++ b/miner-apps/jd-client/src/lib/channel_manager/mod.rs @@ -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, @@ -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)] @@ -345,6 +347,7 @@ impl ChannelManager { coinbase_outputs: Vec, supported_extensions: Vec, required_extensions: Vec, + mode: JDMode, ) -> JDCResult { // Start with a solo-mining allocator (no upstream prefix). Once the // upstream channel is opened in `handle_open_extended_mining_channel_success` @@ -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) diff --git a/miner-apps/jd-client/src/lib/channel_manager/template_message_handler.rs b/miner-apps/jd-client/src/lib/channel_manager/template_message_handler.rs index e0968b25c..a59d0f21d 100644 --- a/miner-apps/jd-client/src/lib/channel_manager/template_message_handler.rs +++ b/miner-apps/jd-client/src/lib/channel_manager/template_message_handler.rs @@ -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)] @@ -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, @@ -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() @@ -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); @@ -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() { diff --git a/miner-apps/jd-client/src/lib/channel_manager/upstream_message_handler.rs b/miner-apps/jd-client/src/lib/channel_manager/upstream_message_handler.rs index b642baf95..d6c2799d7 100644 --- a/miner-apps/jd-client/src/lib/channel_manager/upstream_message_handler.rs +++ b/miner-apps/jd-client/src/lib/channel_manager/upstream_message_handler.rs @@ -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}, }; @@ -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() @@ -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 { @@ -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) diff --git a/miner-apps/jd-client/src/lib/config.rs b/miner-apps/jd-client/src/lib/config.rs index 81739a1dd..8108a6bb2 100644 --- a/miner-apps/jd-client/src/lib/config.rs +++ b/miner-apps/jd-client/src/lib/config.rs @@ -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] diff --git a/miner-apps/jd-client/src/lib/jd_mode.rs b/miner-apps/jd-client/src/lib/jd_mode.rs index 0533afc71..6c1761378 100644 --- a/miner-apps/jd-client/src/lib/jd_mode.rs +++ b/miner-apps/jd-client/src/lib/jd_mode.rs @@ -3,59 +3,77 @@ //! This module defines different operating modes for the Job Declarator //! and provides atomic accessors for setting and retrieving the current mode. //! -//! Modes are stored in a global [`AtomicU8`] to allow safe concurrent access +//! Mode state is stored in `JDMode::inner` (`Arc`) to allow safe concurrent access //! across threads. -use std::sync::atomic::{AtomicU8, Ordering}; - -/// Operating modes for the Job Declarator. -#[repr(u8)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum JdMode { - /// Runs in Coinbase only mode. - CoinbaseOnly = 0, - /// Runs in Full template mode, - FullTemplate = 1, - /// Runs in solo mining mode, - SoloMining = 2, +use std::sync::{ + atomic::{AtomicU8, Ordering}, + Arc, +}; + +use crate::config::ConfigJDCMode; + +#[derive(Clone, Debug)] +pub struct JDMode { + inner: Arc, + // Currently, how JDC works is the mode in config + // only gets activated once an upstream connection + // is made. + config_mode: ConfigJDCMode, } -impl From for JdMode { - fn from(val: u8) -> Self { - match val { - 0 => JdMode::CoinbaseOnly, - 1 => JdMode::FullTemplate, - 2 => JdMode::SoloMining, - _ => JdMode::SoloMining, +impl JDMode { + pub fn new(config_mode: ConfigJDCMode) -> JDMode { + JDMode { + inner: Arc::new(AtomicU8::new(ConfigJDCMode::SoloMining as u8)), + config_mode, } } -} -impl From for JdMode { - fn from(val: u32) -> Self { - match val { - 0 => JdMode::CoinbaseOnly, - 1 => JdMode::FullTemplate, - 2 => JdMode::SoloMining, - _ => JdMode::SoloMining, + /// This activates mode based on config file once + /// upstream connection is made. + pub fn activate(&self) { + match self.config_mode { + ConfigJDCMode::CoinbaseOnly => self.set_coinbase_only(), + ConfigJDCMode::FullTemplate => self.set_full_template(), + ConfigJDCMode::SoloMining => self.set_solo_mining(), } } -} -impl From for u8 { - fn from(mode: JdMode) -> Self { - mode as u8 + pub fn set_solo_mining(&self) { + self.inner + .store(ConfigJDCMode::SoloMining as u8, Ordering::Relaxed); } -} -/// Global atomic variable storing the current JD mode. -pub static JD_MODE: AtomicU8 = AtomicU8::new(JdMode::SoloMining as u8); + fn set_full_template(&self) { + self.inner + .store(ConfigJDCMode::FullTemplate as u8, Ordering::Relaxed); + } -/// Updates the global JD mode. -pub fn set_jd_mode(mode: JdMode) { - JD_MODE.store(mode as u8, Ordering::SeqCst); -} + fn set_coinbase_only(&self) { + self.inner + .store(ConfigJDCMode::CoinbaseOnly as u8, Ordering::Relaxed); + } -/// Returns the current global JD mode. -pub fn get_jd_mode() -> JdMode { - JD_MODE.load(Ordering::SeqCst).into() + pub fn is_solo_mining(&self) -> bool { + let mode = self.inner.load(Ordering::Relaxed); + mode == ConfigJDCMode::SoloMining as u8 + } + + pub fn is_full_template(&self) -> bool { + let mode = self.inner.load(Ordering::Relaxed); + mode == ConfigJDCMode::FullTemplate as u8 + } + + pub fn is_coinbase_only(&self) -> bool { + let mode = self.inner.load(Ordering::Relaxed); + mode == ConfigJDCMode::CoinbaseOnly as u8 + } + + pub fn is_config_full_template(&self) -> bool { + self.config_mode == ConfigJDCMode::FullTemplate + } + + pub fn is_config_coinbase_only(&self) -> bool { + self.config_mode == ConfigJDCMode::CoinbaseOnly + } } diff --git a/miner-apps/jd-client/src/lib/job_declarator/message_handler.rs b/miner-apps/jd-client/src/lib/job_declarator/message_handler.rs index 57ee0ab02..a8a7948c0 100644 --- a/miner-apps/jd-client/src/lib/job_declarator/message_handler.rs +++ b/miner-apps/jd-client/src/lib/job_declarator/message_handler.rs @@ -8,9 +8,7 @@ use stratum_apps::stratum_core::{ use tracing::{info, warn}; use crate::{ - config::ConfigJDCMode, error::{self, JDCError, JDCErrorKind}, - jd_mode::{set_jd_mode, JdMode}, job_declarator::JobDeclarator, }; @@ -32,16 +30,7 @@ impl HandleCommonMessagesFromServerAsync for JobDeclarator { _tlv_fields: Option<&[Tlv]>, ) -> Result<(), Self::Error> { info!("Received: {}", msg); - // Setting up JDMode from config, upon - // successful handshake. - let jd_mode = match self.mode { - ConfigJDCMode::CoinbaseOnly => JdMode::CoinbaseOnly, - ConfigJDCMode::FullTemplate => JdMode::FullTemplate, - ConfigJDCMode::SoloMining => JdMode::SoloMining, - }; - - set_jd_mode(jd_mode); - + self.mode.activate(); Ok(()) } diff --git a/miner-apps/jd-client/src/lib/job_declarator/mod.rs b/miner-apps/jd-client/src/lib/job_declarator/mod.rs index 7704a3ec7..da37b57de 100644 --- a/miner-apps/jd-client/src/lib/job_declarator/mod.rs +++ b/miner-apps/jd-client/src/lib/job_declarator/mod.rs @@ -21,9 +21,9 @@ use tokio::net::TcpStream; use tracing::{debug, error, info, warn}; use crate::{ - config::ConfigJDCMode, error::{self, Action, JDCError, JDCErrorKind, JDCResult, LoopControl}, io_task::spawn_io_tasks, + jd_mode::JDMode, utils::{get_setup_connection_message_jds, UpstreamEntry}, }; @@ -52,7 +52,7 @@ pub struct JobDeclarator { /// Socket address of the Job Declarator server. socket_address: SocketAddr, /// Config JDC mode - mode: ConfigJDCMode, + mode: JDMode, } #[cfg_attr(not(test), hotpath::measure_all)] @@ -110,7 +110,7 @@ impl JobDeclarator { channel_manager_receiver: Receiver>, cancellation_token: CancellationToken, fallback_coordinator: FallbackCoordinator, - mode: ConfigJDCMode, + mode: JDMode, task_manager: Arc, ) -> JDCResult { let addr = resolve_host(&upstream_entry.jds_host, upstream_entry.jds_port) diff --git a/miner-apps/jd-client/src/lib/mod.rs b/miner-apps/jd-client/src/lib/mod.rs index ef3265ce8..e5689557d 100644 --- a/miner-apps/jd-client/src/lib/mod.rs +++ b/miner-apps/jd-client/src/lib/mod.rs @@ -21,9 +21,9 @@ use tracing::{debug, error, info, warn}; use crate::{ channel_manager::ChannelManager, - config::{ConfigJDCMode, JobDeclaratorClientConfig}, + config::JobDeclaratorClientConfig, error::JDCErrorKind, - jd_mode::{set_jd_mode, JdMode}, + jd_mode::JDMode, job_declarator::JobDeclarator, template_receiver::{ bitcoin_core::{connect_to_bitcoin_core, BitcoinCoreSv2TDPConfig}, @@ -76,6 +76,7 @@ impl JobDeclaratorClient { let miner_coinbase_outputs = vec![self.config.get_txout()]; let mut encoded_outputs = vec![]; + let mode = JDMode::new(self.config.mode); miner_coinbase_outputs .consensus_encode(&mut encoded_outputs) @@ -112,6 +113,7 @@ impl JobDeclaratorClient { encoded_outputs.clone(), self.config.supported_extensions().to_vec(), self.config.required_extensions().to_vec(), + mode.clone(), ) .await .unwrap(); @@ -266,7 +268,7 @@ impl JobDeclaratorClient { ); } info!("Starting in solo mining mode"); - set_jd_mode(jd_mode::JdMode::SoloMining); + mode.set_solo_mining(); } else if upstream_addresses.is_empty() { error!( "No upstreams configured for {:?} mode - at least one upstream is required", @@ -285,7 +287,7 @@ impl JobDeclaratorClient { jd_to_channel_manager_sender.clone(), self.cancellation_token.clone(), fallback_coordinator.clone(), - self.config.mode.clone(), + mode.clone(), task_manager.clone(), ) .await @@ -316,7 +318,7 @@ impl JobDeclaratorClient { } Err(e) => { tracing::error!("Failed to initialize upstream: {:?}", e); - set_jd_mode(jd_mode::JdMode::SoloMining); + mode.set_solo_mining(); } }; } @@ -357,7 +359,7 @@ impl JobDeclaratorClient { fallback_coordinator.trigger_fallback_and_wait().await; info!("All components finished fallback cleanup"); - set_jd_mode(JdMode::SoloMining); + mode.set_solo_mining(); info!("Existing Upstream or JD instance taken out. Preparing fallback."); // Create a fresh FallbackCoordinator for the reconnection attempt @@ -388,6 +390,7 @@ impl JobDeclaratorClient { encoded_outputs.clone(), self.config.supported_extensions().to_vec(), self.config.required_extensions().to_vec(), + mode.clone() ) .await .unwrap(); @@ -414,7 +417,7 @@ impl JobDeclaratorClient { jd_to_channel_manager_sender_new.clone(), self.cancellation_token.clone(), fallback_coordinator.clone(), - self.config.mode.clone(), + mode.clone(), task_manager.clone(), ) .await @@ -449,7 +452,7 @@ impl JobDeclaratorClient { channel_manager_clone .upstream_state .set(UpstreamState::SoloMining); - set_jd_mode(jd_mode::JdMode::SoloMining); + mode.set_solo_mining(); info!("Fallback to solo mining mode"); } }; @@ -578,7 +581,7 @@ impl JobDeclaratorClient { jd_to_channel_manager_sender: Sender>, cancellation_token: CancellationToken, fallback_coordinator: FallbackCoordinator, - mode: ConfigJDCMode, + mode: JDMode, task_manager: Arc, ) -> Result<(Upstream, JobDeclarator), JDCErrorKind> { const MAX_RETRIES: usize = 3; @@ -689,7 +692,7 @@ async fn try_initialize_single( channel_manager_to_jd_receiver: Receiver>, cancellation_token: CancellationToken, fallback_coordinator: FallbackCoordinator, - mode: ConfigJDCMode, + mode: JDMode, task_manager: Arc, config: &JobDeclaratorClientConfig, ) -> Result<(Upstream, JobDeclarator), JDCErrorKind> { diff --git a/miner-apps/jd-client/src/lib/utils.rs b/miner-apps/jd-client/src/lib/utils.rs index dd44340ba..f68104ef1 100644 --- a/miner-apps/jd-client/src/lib/utils.rs +++ b/miner-apps/jd-client/src/lib/utils.rs @@ -40,8 +40,8 @@ use tracing::{debug, info}; use crate::{ channel_manager::{downstream_message_handler::RouteMessageTo, ChannelManagerData}, - config::ConfigJDCMode, error::JDCErrorKind, + jd_mode::JDMode, }; pub(crate) type DownstreamMessage = (Mining<'static>, Option>); @@ -89,7 +89,7 @@ pub fn get_setup_connection_message( /// Constructs a `SetupConnection` message for the Job Declarator (JDS). pub fn get_setup_connection_message_jds( proxy_address: &SocketAddr, - mode: &ConfigJDCMode, + mode: &JDMode, ) -> SetupConnection<'static> { let endpoint_host = proxy_address .ip() @@ -114,7 +114,7 @@ pub fn get_setup_connection_message_jds( device_id, }; - if matches!(mode, ConfigJDCMode::FullTemplate) { + if mode.is_config_full_template() { setup_connection.allow_full_template_mode(); } diff --git a/miner-apps/translator/src/lib/mod.rs b/miner-apps/translator/src/lib/mod.rs index aa65bf50d..09af0e217 100644 --- a/miner-apps/translator/src/lib/mod.rs +++ b/miner-apps/translator/src/lib/mod.rs @@ -16,7 +16,7 @@ use std::{ net::SocketAddr, sync::{ atomic::{AtomicBool, Ordering}, - Arc, OnceLock, + Arc, }, time::Duration, }; @@ -38,7 +38,7 @@ use crate::{ status::{State, Status}, sv1::sv1_server::sv1_server::Sv1Server, sv2::{ChannelManager, Upstream}, - utils::UpstreamEntry, + utils::{TproxyMode, UpstreamEntry}, }; pub mod config; @@ -83,16 +83,10 @@ impl TranslatorSv2 { /// protocol translation, job management, and status reporting. pub async fn start(self) { info!("Starting Translator Proxy..."); - // only initialized once - TPROXY_MODE - .set(self.config.aggregate_channels.into()) - .expect("TPROXY_MODE initialized more than once"); - VARDIFF_ENABLED - .set(self.config.downstream_difficulty_config.enable_vardiff) - .expect("VARDIFF_ENABLED initialized more than once"); let cancellation_token = self.cancellation_token.clone(); let mut fallback_coordinator = FallbackCoordinator::new(); + let tproxy_mode = TproxyMode::from(self.config.aggregate_channels); let task_manager = Arc::new(TaskManager::new()); let (status_sender, status_receiver) = async_channel::unbounded::(); @@ -130,6 +124,7 @@ impl TranslatorSv2 { channel_manager_to_sv1_server_receiver, sv1_server_to_channel_manager_sender, self.config.clone(), + tproxy_mode, )); info!("Initializing upstream connection..."); @@ -162,6 +157,9 @@ impl TranslatorSv2 { status_sender.clone(), self.config.supported_extensions.clone(), self.config.required_extensions.clone(), + tproxy_mode, + #[cfg(feature = "monitoring")] + self.config.downstream_difficulty_config.enable_vardiff, )); info!("Launching ChannelManager tasks..."); @@ -283,6 +281,7 @@ impl TranslatorSv2 { channel_manager_to_sv1_server_receiver, sv1_server_to_channel_manager_sender, self.config.clone(), + tproxy_mode )); if let Err(e) = self.initialize_upstream( @@ -309,6 +308,9 @@ impl TranslatorSv2 { status_sender.clone(), self.config.supported_extensions.clone(), self.config.required_extensions.clone(), + tproxy_mode, + #[cfg(feature = "monitoring")] + self.config.downstream_difficulty_config.enable_vardiff )); info!("Launching ChannelManager tasks..."); @@ -548,71 +550,6 @@ async fn try_initialize_upstream( Ok(()) } -/// Defines the operational mode for Translator Proxy. -/// -/// It can operate in two different modes that affect how Sv1 -/// downstream connections are mapped to the upstream Sv2 channels. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum TproxyMode { - /// All Sv1 downstream connections share a single extended Sv2 channel. - /// This mode uses extranonce_prefix allocation to distinguish between - /// different downstream miners while presenting them as a single entity - /// to the upstream server. This is more efficient for pools with many - /// miners. - Aggregated, - /// Each Sv1 downstream connection gets its own dedicated extended Sv2 channel. - /// This mode provides complete isolation between downstream connections - /// but may be less efficient for large numbers of miners. - NonAggregated, -} - -impl From for TproxyMode { - fn from(aggregate: bool) -> Self { - if aggregate { - return TproxyMode::Aggregated; - } - - TproxyMode::NonAggregated - } -} - -static TPROXY_MODE: OnceLock = OnceLock::new(); -static VARDIFF_ENABLED: OnceLock = OnceLock::new(); - -#[cfg(not(test))] -pub fn tproxy_mode() -> TproxyMode { - *TPROXY_MODE.get().expect("TPROXY_MODE has to exist") -} - -// We don’t initialize `TPROXY_MODE` in tests, so any test that -// depends on it will panic if the mode is undefined. -// This `cfg` wrapper ensures `tproxy_mode` does not panic in -// an undefined state by providing a default value when needed. -#[cfg(test)] -pub fn tproxy_mode() -> TproxyMode { - *TPROXY_MODE.get_or_init(|| TproxyMode::Aggregated) -} - -#[inline] -pub fn is_aggregated() -> bool { - matches!(tproxy_mode(), TproxyMode::Aggregated) -} - -#[inline] -pub fn is_non_aggregated() -> bool { - matches!(tproxy_mode(), TproxyMode::NonAggregated) -} - -#[cfg(not(test))] -pub fn vardiff_enabled() -> bool { - *VARDIFF_ENABLED.get().expect("VARDIFF_ENABLED has to exist") -} - -#[cfg(test)] -pub fn vardiff_enabled() -> bool { - *VARDIFF_ENABLED.get_or_init(|| true) -} - impl Drop for TranslatorSv2 { fn drop(&mut self) { info!("TranslatorSv2 dropped"); diff --git a/miner-apps/translator/src/lib/monitoring.rs b/miner-apps/translator/src/lib/monitoring.rs index 9c5e3d42a..09957409c 100644 --- a/miner-apps/translator/src/lib/monitoring.rs +++ b/miner-apps/translator/src/lib/monitoring.rs @@ -6,18 +6,15 @@ use stratum_apps::monitoring::server::{ServerExtendedChannelInfo, ServerInfo, ServerMonitoring}; -use crate::{ - sv2::channel_manager::ChannelManager, tproxy_mode, utils::AGGREGATED_CHANNEL_ID, - vardiff_enabled, TproxyMode, -}; +use crate::{sv2::channel_manager::ChannelManager, utils::AGGREGATED_CHANNEL_ID, TproxyMode}; impl ServerMonitoring for ChannelManager { fn get_server(&self) -> ServerInfo { let mut extended_channels = Vec::new(); let standard_channels = Vec::new(); // tProxy only uses extended channels - let report_hashrate = vardiff_enabled(); + let report_hashrate = self.report_hashrate; - match tproxy_mode() { + match self.mode { TproxyMode::Aggregated => { // In Aggregated mode: one shared channel to the server // stored under AGGREGATED_CHANNEL_ID diff --git a/miner-apps/translator/src/lib/sv1/sv1_server/difficulty_manager.rs b/miner-apps/translator/src/lib/sv1/sv1_server/difficulty_manager.rs index 3b93a3fed..11971491d 100644 --- a/miner-apps/translator/src/lib/sv1/sv1_server/difficulty_manager.rs +++ b/miner-apps/translator/src/lib/sv1/sv1_server/difficulty_manager.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use crate::{is_aggregated, is_non_aggregated, sv1::sv1_server::sv1_server::PendingTargetUpdate}; +use crate::sv1::sv1_server::sv1_server::PendingTargetUpdate; use stratum_apps::{ stratum_core::{ @@ -189,7 +189,7 @@ impl Sv1Server { * new_target, * new_hashrate) */ ) { - if is_aggregated() { + if self.mode.is_aggregated() { // Aggregated mode: Send single UpdateChannel with minimum target and total hashrate of // ALL downstreams self.send_aggregated_update_channel(all_updates).await; @@ -303,7 +303,7 @@ impl Sv1Server { set_target.channel_id, new_upstream_target ); - if is_aggregated() { + if self.mode.is_aggregated() { return self .handle_aggregated_set_target(new_upstream_target, set_target.channel_id) .await; @@ -458,7 +458,7 @@ impl Sv1Server { /// (e.g., disconnect). Calculates total hashrate and minimum target among all remaining /// downstreams. pub async fn send_update_channel_on_downstream_state_change(&self) { - if is_non_aggregated() { + if self.mode.is_non_aggregated() { return; } diff --git a/miner-apps/translator/src/lib/sv1/sv1_server/downstream_message_handler.rs b/miner-apps/translator/src/lib/sv1/sv1_server/downstream_message_handler.rs index dad60feba..f3d4bf584 100644 --- a/miner-apps/translator/src/lib/sv1/sv1_server/downstream_message_handler.rs +++ b/miner-apps/translator/src/lib/sv1/sv1_server/downstream_message_handler.rs @@ -9,7 +9,7 @@ use stratum_apps::stratum_core::sv1_api::{ use tracing::{debug, info, warn}; use crate::{ - error, is_aggregated, + error, sv1::{downstream::SubmitShareWithChannelId, sv1_server::tlv_compatible_username, Sv1Server}, utils::{validate_sv1_share, AGGREGATED_CHANNEL_ID}, }; @@ -111,7 +111,7 @@ impl IsServer<'static> for Sv1Server { return Ok(false); }; - let channel_id = if is_aggregated() { + let channel_id = if self.mode.is_aggregated() { AGGREGATED_CHANNEL_ID } else { channel_id diff --git a/miner-apps/translator/src/lib/sv1/sv1_server/sv1_server.rs b/miner-apps/translator/src/lib/sv1/sv1_server/sv1_server.rs index 7c6ffafd6..33fb90c22 100644 --- a/miner-apps/translator/src/lib/sv1/sv1_server/sv1_server.rs +++ b/miner-apps/translator/src/lib/sv1/sv1_server/sv1_server.rs @@ -1,7 +1,6 @@ use crate::{ config::TranslatorConfig, error::{self, TproxyError, TproxyErrorKind, TproxyResult}, - is_aggregated, is_non_aggregated, status::{handle_error, Status, StatusSender}, sv1::{ downstream::{downstream::Downstream, SubmitShareWithChannelId}, @@ -9,7 +8,7 @@ use crate::{ channel::Sv1ServerChannelState, is_mining_authorize, KEEPALIVE_JOB_ID_DELIMITER, }, }, - utils::AGGREGATED_CHANNEL_ID, + utils::{TproxyMode, AGGREGATED_CHANNEL_ID}, }; use async_channel::{Receiver, Sender}; use dashmap::DashMap; @@ -85,6 +84,7 @@ pub struct Sv1Server { /// Valid Sv1 jobs storage, containing only a single shared entry (AGGREGATED_CHANNEL_ID) in /// case of channels aggregation (aggregated mode) pub(crate) valid_sv1_jobs: Arc>>>, + pub(crate) mode: TproxyMode, } #[cfg_attr(not(test), hotpath::measure_all)] @@ -171,6 +171,7 @@ impl Sv1Server { channel_manager_receiver: Receiver<(Mining<'static>, Option>)>, channel_manager_sender: Sender<(Mining<'static>, Option>)>, config: TranslatorConfig, + mode: TproxyMode, ) -> Self { let shares_per_minute = config.downstream_difficulty_config.shares_per_minute; let sv1_server_channel_state = @@ -192,6 +193,7 @@ impl Sv1Server { prevhashes: Arc::new(DashMap::new()), pending_target_updates: Arc::new(Mutex::new(Vec::new())), valid_sv1_jobs: Arc::new(DashMap::new()), + mode, } } @@ -510,7 +512,7 @@ impl Sv1Server { .map_err(|_| TproxyError::shutdown(TproxyErrorKind::SV1Error))?; // Only add TLV fields with user identity in non-aggregated mode - let tlv_fields = if is_non_aggregated() { + let tlv_fields = if self.mode.is_non_aggregated() { let Some(downstream) = self .downstreams .get(&message.downstream_id) @@ -749,7 +751,7 @@ impl Sv1Server { // Update job storage based on the configured mode let notify_parsed = notify.clone(); - let job_channel_id = if is_non_aggregated() { + let job_channel_id = if self.mode.is_non_aggregated() { m.channel_id } else { AGGREGATED_CHANNEL_ID @@ -984,7 +986,7 @@ impl Sv1Server { } }; - if is_aggregated() { + if self.mode.is_aggregated() { // Aggregated mode: send set_difficulty to ALL downstreams and update hashrate return self .send_set_difficulty_to_all_downstreams(new_target, derived_hashrate) @@ -1234,7 +1236,7 @@ impl Sv1Server { keepalive_notify.time = HexU32Be(new_time); // Add the keepalive job to valid jobs so shares can be validated - let job_channel_id = if is_aggregated() { + let job_channel_id = if self.mode.is_aggregated() { Some(AGGREGATED_CHANNEL_ID) } else { channel_id @@ -1307,7 +1309,7 @@ impl Sv1Server { &self, channel_id: Option, ) -> Option> { - let channel_id = if is_aggregated() { + let channel_id = if self.mode.is_aggregated() { AGGREGATED_CHANNEL_ID } else { channel_id? @@ -1325,7 +1327,7 @@ impl Sv1Server { job_id: &str, channel_id: Option, ) -> Option> { - let channel_id = if is_aggregated() { + let channel_id = if self.mode.is_aggregated() { AGGREGATED_CHANNEL_ID } else { channel_id? @@ -1383,8 +1385,8 @@ mod tests { let (_downstream_sender, cm_receiver) = unbounded(); let config = create_test_config(); let addr = "127.0.0.1:3333".parse().unwrap(); - - Sv1Server::new(addr, cm_receiver, cm_sender, config) + let tproxy_mode = TproxyMode::from(config.aggregate_channels); + Sv1Server::new(addr, cm_receiver, cm_sender, config, tproxy_mode) } #[test] @@ -1405,8 +1407,8 @@ mod tests { let (cm_sender, _cm_receiver) = unbounded(); let (_downstream_sender, cm_receiver) = unbounded(); let addr = "127.0.0.1:3333".parse().unwrap(); - - let server = Sv1Server::new(addr, cm_receiver, cm_sender, config); + let tproxy_mode = TproxyMode::from(config.aggregate_channels); + let server = Sv1Server::new(addr, cm_receiver, cm_sender, config, tproxy_mode); assert!(server.config.downstream_difficulty_config.enable_vardiff); } @@ -1446,8 +1448,9 @@ mod tests { let (cm_sender, _cm_receiver) = unbounded(); let (_downstream_sender, cm_receiver) = unbounded(); let addr = "127.0.0.1:3333".parse().unwrap(); + let tproxy_mode = TproxyMode::from(config.aggregate_channels); - let server = Sv1Server::new(addr, cm_receiver, cm_sender, config); + let server = Sv1Server::new(addr, cm_receiver, cm_sender, config, tproxy_mode); let target: Target = hash_rate_to_target(200.0, 5.0).unwrap(); let set_target = SetTarget { @@ -1467,8 +1470,8 @@ mod tests { let (cm_sender, _cm_receiver) = unbounded(); let (_downstream_sender, cm_receiver) = unbounded(); let addr = "127.0.0.1:3333".parse().unwrap(); - - let server = Sv1Server::new(addr, cm_receiver, cm_sender, config); + let tproxy_mode = TproxyMode::from(config.aggregate_channels); + let server = Sv1Server::new(addr, cm_receiver, cm_sender, config, tproxy_mode); let target: Target = hash_rate_to_target(200.0, 5.0).unwrap(); let set_target = SetTarget { diff --git a/miner-apps/translator/src/lib/sv2/channel_manager/channel_manager.rs b/miner-apps/translator/src/lib/sv2/channel_manager/channel_manager.rs index e9a53eca8..64ecab810 100644 --- a/miner-apps/translator/src/lib/sv2/channel_manager/channel_manager.rs +++ b/miner-apps/translator/src/lib/sv2/channel_manager/channel_manager.rs @@ -1,9 +1,9 @@ use crate::{ error::{self, TproxyError, TproxyErrorKind, TproxyResult}, - is_aggregated, status::{handle_error, Status, StatusSender}, sv2::channel_manager::channel::ChannelState, utils::{AggregatedState, AtomicAggregatedState, AGGREGATED_CHANNEL_ID}, + TproxyMode, }; use async_channel::{Receiver, Sender}; use dashmap::DashMap; @@ -129,6 +129,11 @@ pub struct ChannelManager { /// Tracks whether the single upstream channel in aggregated mode is absent, /// being established, or connected. pub aggregated_channel_state: AtomicAggregatedState, + /// Current mode Tproxy is operating in. + pub(crate) mode: TproxyMode, + /// Required to show or not show hashrate on monitoring. + #[cfg(feature = "monitoring")] + pub(crate) report_hashrate: bool, } #[cfg_attr(not(test), hotpath::measure_all)] @@ -157,6 +162,8 @@ impl ChannelManager { status_sender: Sender, supported_extensions: Vec, required_extensions: Vec, + tproxy_mode: TproxyMode, + #[cfg(feature = "monitoring")] report_hashrate: bool, ) -> Self { let channel_state = ChannelState::new( upstream_sender, @@ -177,6 +184,9 @@ impl ChannelManager { negotiated_extensions: Arc::new(Mutex::new(Vec::new())), aggregated_extranonce_allocator: Arc::new(Mutex::new(None)), aggregated_channel_state: AtomicAggregatedState::new(AggregatedState::NoChannel), + mode: tproxy_mode, + #[cfg(feature = "monitoring")] + report_hashrate, } } @@ -347,7 +357,7 @@ impl ChannelManager { let hashrate = m.nominal_hash_rate; let min_extranonce_size = m.min_extranonce_size as usize; - if is_aggregated() { + if self.mode.is_aggregated() { match self.aggregated_channel_state.get() { AggregatedState::Connected => { return self @@ -397,7 +407,7 @@ impl ChannelManager { // In non-aggregated mode there is nothing to multiplex (1 // upstream ↔ 1 downstream), so we request `min_extranonce_size` verbatim. Any slack // upstream may grant on top is absorbed later as allocator padding. - let upstream_min_extranonce_size = if is_aggregated() { + let upstream_min_extranonce_size = if self.mode.is_aggregated() { min_extranonce_size + AGGREGATED_TPROXY_LOCAL_PREFIX_BYTES as usize } else { min_extranonce_size @@ -410,7 +420,7 @@ impl ChannelManager { // used in the `OpenExtendedMiningChannel.Success` handler. // In aggregated mode it was already inserted in the `AggregatedState::NoChannel` // match arm above. - if !is_aggregated() { + if !self.mode.is_aggregated() { self.pending_downstream_channels.insert( open_channel_msg.request_id as DownstreamId, (user_identity, hashrate, min_extranonce_size), @@ -446,7 +456,7 @@ impl ChannelManager { ) }); if let Some((Ok(_result), _share_accounting)) = value { - if is_aggregated() + if self.mode.is_aggregated() && self.extended_channels.contains_key(&AGGREGATED_CHANNEL_ID) { let upstream_extended_channel_id = self @@ -606,7 +616,7 @@ impl ChannelManager { Mining::UpdateChannel(mut m) => { debug!("Received UpdateChannel from SV1Server: {}", m); - if is_aggregated() { + if self.mode.is_aggregated() { // Update the aggregated channel's nominal hashrate so // that monitoring reports a value consistent with the // downstream vardiff estimate. @@ -649,7 +659,7 @@ impl ChannelManager { // here. `AGGREGATED_CHANNEL_ID` represents the single shared // upstream channel in aggregated mode and must only be torn // down via fallback/shutdown. - if is_aggregated() && m.channel_id == AGGREGATED_CHANNEL_ID { + if self.mode.is_aggregated() && m.channel_id == AGGREGATED_CHANNEL_ID { warn!("Ignoring CloseChannel from Sv1Server targeting AGGREGATED_CHANNEL_ID"); return Ok(()); } @@ -686,7 +696,7 @@ impl ChannelManager { // mode. In aggregated mode the upstream channel is shared // across all SV1 miners and must stay open when any one of // them disconnects. - if !is_aggregated() { + if !self.mode.is_aggregated() { let message = Mining::CloseChannel(m); let sv2_frame: Sv2Frame = AnyMessage::Mining(message) .try_into() @@ -896,6 +906,9 @@ mod tests { status_sender, vec![], vec![], + TproxyMode::from(true), + #[cfg(feature = "monitoring")] + true, ) } diff --git a/miner-apps/translator/src/lib/sv2/channel_manager/mining_message_handler.rs b/miner-apps/translator/src/lib/sv2/channel_manager/mining_message_handler.rs index 1604650f3..43bb40f9b 100644 --- a/miner-apps/translator/src/lib/sv2/channel_manager/mining_message_handler.rs +++ b/miner-apps/translator/src/lib/sv2/channel_manager/mining_message_handler.rs @@ -1,6 +1,5 @@ use crate::{ error::{self, TproxyError, TproxyErrorKind}, - is_aggregated, sv2::{ channel_manager::channel_manager::{ AGGREGATED_TPROXY_LOCAL_PREFIX_BYTES, AGGREGATED_TPROXY_MAX_CHANNELS, @@ -123,7 +122,7 @@ impl HandleMiningMessagesFromServerAsync for ChannelManager { let target = Target::from_le_bytes(m.target.clone().inner_as_ref().try_into().unwrap()); let version_rolling = true; // we assume this is always true on extended channels - if is_aggregated() { + if self.mode.is_aggregated() { // Aggregated: we asked upstream for `downstream_extranonce_len // + AGGREGATED_TPROXY_LOCAL_PREFIX_BYTES` so the allocator's // `local_index` has room to uniquely address each multiplexed @@ -375,7 +374,7 @@ impl HandleMiningMessagesFromServerAsync for ChannelManager { // In aggregated mode, serve any downstream requests that were buffered in // pending_channels while the upstream channel was being established (Pending state). - if is_aggregated() { + if self.mode.is_aggregated() { let pending_requests: Vec<(u32, String, Hashrate, usize)> = self .pending_downstream_channels .iter() @@ -434,7 +433,7 @@ impl HandleMiningMessagesFromServerAsync for ChannelManager { ) -> Result<(), Self::Error> { info!("Received: {}", m); // are we working in aggregated mode? - if is_aggregated() { + if self.mode.is_aggregated() { // even if aggregated channel_id != m.channel_id, we should trigger fallback // because why would a sane server send a CloseChannel message to a different // channel? @@ -496,7 +495,7 @@ impl HandleMiningMessagesFromServerAsync for ChannelManager { // In aggregated mode, the Pool responds with the upstream channel ID, but the // channel is stored under AGGREGATED_CHANNEL_ID in the DashMap. // In non-aggregated mode, m.channel_id matches the DashMap key directly. - let key = if is_aggregated() { + let key = if self.mode.is_aggregated() { AGGREGATED_CHANNEL_ID } else { m.channel_id @@ -518,7 +517,7 @@ impl HandleMiningMessagesFromServerAsync for ChannelManager { ) -> Result<(), Self::Error> { warn!("Received: {} ❌", m); - let key = if is_aggregated() { + let key = if self.mode.is_aggregated() { AGGREGATED_CHANNEL_ID } else { m.channel_id @@ -558,7 +557,7 @@ impl HandleMiningMessagesFromServerAsync for ChannelManager { let mut new_extended_mining_job_messages = Vec::new(); // are we in aggregated mode? - if is_aggregated() { + if self.mode.is_aggregated() { // Validate that the message is for the aggregated channel or its group let aggregated_channel_id = self .extended_channels @@ -700,7 +699,7 @@ impl HandleMiningMessagesFromServerAsync for ChannelManager { let mut set_new_prev_hash_messages = Vec::new(); let mut new_extended_mining_job_messages = Vec::new(); - if is_aggregated() { + if self.mode.is_aggregated() { // Validate that the message is for the aggregated channel or its group let aggregated_channel_id = self .extended_channels @@ -911,7 +910,7 @@ impl HandleMiningMessagesFromServerAsync for ChannelManager { let mut set_target_messages = Vec::new(); // are in aggregated mode? - if is_aggregated() { + if self.mode.is_aggregated() { let aggregated_channel_id = self .extended_channels .get(&AGGREGATED_CHANNEL_ID) diff --git a/miner-apps/translator/src/lib/utils.rs b/miner-apps/translator/src/lib/utils.rs index eb45c7ece..a154d8de3 100644 --- a/miner-apps/translator/src/lib/utils.rs +++ b/miner-apps/translator/src/lib/utils.rs @@ -169,3 +169,40 @@ pub struct UpstreamEntry { pub authority_pubkey: Secp256k1PublicKey, pub tried_or_flagged: bool, } + +/// Defines the operational mode for Translator Proxy. +/// +/// It can operate in two different modes that affect how Sv1 +/// downstream connections are mapped to the upstream Sv2 channels. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TproxyMode { + /// All Sv1 downstream connections share a single extended Sv2 channel. + /// This mode uses extranonce_prefix allocation to distinguish between + /// different downstream miners while presenting them as a single entity + /// to the upstream server. This is more efficient for pools with many + /// miners. + Aggregated, + /// Each Sv1 downstream connection gets its own dedicated extended Sv2 channel. + /// This mode provides complete isolation between downstream connections + /// but may be less efficient for large numbers of miners. + NonAggregated, +} + +impl From for TproxyMode { + fn from(value: bool) -> Self { + if value { + return TproxyMode::Aggregated; + } + TproxyMode::NonAggregated + } +} + +impl TproxyMode { + pub(crate) fn is_aggregated(self) -> bool { + TproxyMode::Aggregated == self + } + + pub(crate) fn is_non_aggregated(self) -> bool { + TproxyMode::NonAggregated == self + } +}