diff --git a/integration-tests/lib/mod.rs b/integration-tests/lib/mod.rs index 360250be9..ffed1db13 100644 --- a/integration-tests/lib/mod.rs +++ b/integration-tests/lib/mod.rs @@ -283,6 +283,7 @@ pub async fn start_pool_with_jds( supported_extensions: Vec, required_extensions: Vec, enable_monitoring: bool, + full_template_mode_required: bool, ) -> (PoolSv2, SocketAddr, SocketAddr, Option) { use pool_sv2::config::{AuthorityConfig, ConnectionConfig, JDSPartialConfig, PoolConfig}; @@ -329,7 +330,11 @@ pub async fn start_pool_with_jds( required_extensions.clone(), monitoring_address, monitoring_cache_refresh_secs, - Some(JDSPartialConfig::new(jds_address)), + Some({ + let mut jds_partial = JDSPartialConfig::new(jds_address); + jds_partial.set_full_template_mode_required(full_template_mode_required); + jds_partial + }), ); let pool = PoolSv2::new(config); diff --git a/integration-tests/tests/bitcoin_core_ipc_integration.rs b/integration-tests/tests/bitcoin_core_ipc_integration.rs index 0813f9847..11fce55a6 100644 --- a/integration-tests/tests/bitcoin_core_ipc_integration.rs +++ b/integration-tests/tests/bitcoin_core_ipc_integration.rs @@ -52,7 +52,7 @@ async fn jdc_propagates_block_with_bitcoin_core_ipc() { let (tp, _tp_addr) = start_template_provider(None, DifficultyLevel::Low); let current_block_hash = tp.get_best_block_hash().unwrap(); let (pool, pool_addr, jds_addr, _) = - start_pool_with_jds(tp.bitcoin_core(), vec![], vec![], false).await; + start_pool_with_jds(tp.bitcoin_core(), vec![], vec![], false, true).await; let ignore_push_solution = IgnoreMessage::new(MessageDirection::ToUpstream, MESSAGE_TYPE_PUSH_SOLUTION); let (sniffer, sniffer_addr) = start_sniffer( diff --git a/integration-tests/tests/jd_full_template_mode.rs b/integration-tests/tests/jd_full_template_mode.rs new file mode 100644 index 000000000..54051458c --- /dev/null +++ b/integration-tests/tests/jd_full_template_mode.rs @@ -0,0 +1,66 @@ +use integration_tests_sv2::{interceptor::MessageDirection, template_provider::DifficultyLevel, *}; +use stratum_apps::stratum_core::{ + common_messages_sv2::*, job_declaration_sv2::*, template_distribution_sv2::*, +}; + +// JDC in FullTemplate mode (default) exchanges DeclareMiningJob with JDS +// and propagates blocks via both SubmitSolution (to TP) and PushSolution (to JDS) +#[tokio::test] +async fn jd_full_template_mode_declare_mining_job_exchanged() { + start_tracing(); + let (tp, tp_addr) = start_template_provider(None, DifficultyLevel::Low); + let current_block_hash = tp.get_best_block_hash().unwrap(); + let (pool, pool_addr, jds_addr, _) = + start_pool_with_jds(tp.bitcoin_core(), vec![], vec![], false, true).await; + let (jdc_jds_sniffer, jdc_jds_sniffer_addr) = + start_sniffer("jdc-jds", jds_addr, false, vec![], None); + let (jdc_tp_sniffer, jdc_tp_sniffer_addr) = + start_sniffer("jdc-tp", tp_addr, false, vec![], None); + let (jdc, jdc_addr, _) = start_jdc( + &[(pool_addr, jdc_jds_sniffer_addr)], + sv2_tp_config(jdc_tp_sniffer_addr), + vec![], + vec![], + false, + None, + ); + jdc_jds_sniffer + .wait_for_message_type(MessageDirection::ToUpstream, MESSAGE_TYPE_SETUP_CONNECTION) + .await; + jdc_jds_sniffer + .wait_for_message_type( + MessageDirection::ToDownstream, + MESSAGE_TYPE_SETUP_CONNECTION_SUCCESS, + ) + .await; + + let (translator, tproxy_addr, _) = + start_sv2_translator(&[jdc_addr], false, vec![], vec![], None, false).await; + let (_minerd, _) = start_minerd(tproxy_addr, None, None, false).await; + + // DeclareMiningJob exchanged in both directions + jdc_jds_sniffer + .wait_for_message_type( + MessageDirection::ToUpstream, + MESSAGE_TYPE_DECLARE_MINING_JOB, + ) + .await; + jdc_jds_sniffer + .wait_for_message_type( + MessageDirection::ToDownstream, + MESSAGE_TYPE_DECLARE_MINING_JOB_SUCCESS, + ) + .await; + + // Cross-socket arrival order is not guaranteed; wait for each independently. + jdc_jds_sniffer + .wait_for_message_type(MessageDirection::ToUpstream, MESSAGE_TYPE_PUSH_SOLUTION) + .await; + jdc_tp_sniffer + .wait_for_message_type(MessageDirection::ToUpstream, MESSAGE_TYPE_SUBMIT_SOLUTION) + .await; + let new_block_hash = tp.get_best_block_hash().unwrap(); + assert_ne!(current_block_hash, new_block_hash); + + shutdown_all!(pool, jdc, translator); +} diff --git a/integration-tests/tests/jd_integration.rs b/integration-tests/tests/jd_integration.rs index d05097a1e..139e2bc56 100644 --- a/integration-tests/tests/jd_integration.rs +++ b/integration-tests/tests/jd_integration.rs @@ -23,7 +23,7 @@ async fn jds_should_not_panic_if_jdc_shutsdown() { start_tracing(); let (tp, tp_addr) = start_template_provider(None, DifficultyLevel::Low); let (pool, pool_addr, jds_addr, _) = - start_pool_with_jds(tp.bitcoin_core(), vec![], vec![], false).await; + start_pool_with_jds(tp.bitcoin_core(), vec![], vec![], false, true).await; let (sniffer_a, sniffer_addr_a) = start_sniffer("0", jds_addr, false, vec![], None); let (jdc, jdc_addr, _) = start_jdc( &[(pool_addr, sniffer_addr_a)], @@ -69,7 +69,7 @@ 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; + start_pool_with_jds(tp.bitcoin_core(), vec![], vec![], false, true).await; let (solo_tp_sniffer, solo_tp_sniffer_addr) = start_sniffer("solo-jdc-tp", tp_addr, false, vec![], None); @@ -150,7 +150,7 @@ async fn jdc_tp_success_setup() { start_tracing(); let (tp, tp_addr) = start_template_provider(None, DifficultyLevel::Low); let (pool, pool_addr, jds_addr, _) = - start_pool_with_jds(tp.bitcoin_core(), vec![], vec![], false).await; + start_pool_with_jds(tp.bitcoin_core(), vec![], vec![], false, true).await; let (tp_jdc_sniffer, tp_jdc_sniffer_addr) = start_sniffer("0", tp_addr, false, vec![], None); let (jdc, jdc_addr, _) = start_jdc( &[(pool_addr, jds_addr)], @@ -182,7 +182,7 @@ async fn jds_reject_setup_connection_with_non_job_declaration_protocol() { start_tracing(); let (tp, _tp_addr) = start_template_provider(None, DifficultyLevel::Low); let (pool, _pool_addr, jds_addr, _) = - start_pool_with_jds(tp.bitcoin_core(), vec![], vec![], false).await; + start_pool_with_jds(tp.bitcoin_core(), vec![], vec![], false, true).await; let (sniffer, sniffer_addr) = start_sniffer("mock-jds", jds_addr, false, vec![], None); let _mock_downstream = MockDownstream::new( sniffer_addr, @@ -217,13 +217,14 @@ async fn jds_reject_setup_connection_with_non_job_declaration_protocol() { shutdown_all!(pool); } -// This test verifies that JDS rejects SetupConnection without DECLARE_TX_DATA flag. +// This test verifies that JDS rejects SetupConnection from a JDC that does not advertise +// full template mode when `full_template_mode_required` is enabled. #[tokio::test] -async fn jds_reject_setup_connection_without_declare_tx_data_flag() { +async fn jds_reject_setup_connection_when_full_template_mode_required() { start_tracing(); let (tp, _tp_addr) = start_template_provider(None, DifficultyLevel::Low); let (pool, _pool_addr, jds_addr, _) = - start_pool_with_jds(tp.bitcoin_core(), vec![], vec![], false).await; + start_pool_with_jds(tp.bitcoin_core(), vec![], vec![], false, true).await; let (sniffer, sniffer_addr) = start_sniffer("mock-jds", jds_addr, false, vec![], None); let _mock_downstream = MockDownstream::new( sniffer_addr, @@ -251,8 +252,8 @@ async fn jds_reject_setup_connection_without_declare_tx_data_flag() { }; assert_eq!( setup_connection_error.error_code.as_utf8_or_hex(), - "missing-declare-tx-data-flag", - "SetupConnectionError message error code should be missing-declare-tx-data-flag" + "requires-full-template-mode", + "SetupConnectionError message error code should be requires-full-template-mode" ); shutdown_all!(pool); @@ -264,7 +265,7 @@ async fn jds_reject_declare_mining_job_with_invalid_mining_job_token() { start_tracing(); let (tp, _tp_addr) = start_template_provider(None, DifficultyLevel::Low); let (pool, _pool_addr, jds_addr, _) = - start_pool_with_jds(tp.bitcoin_core(), vec![], vec![], false).await; + start_pool_with_jds(tp.bitcoin_core(), vec![], vec![], false, true).await; let (sniffer, sniffer_addr) = start_sniffer("mock-jds", jds_addr, false, vec![], None); let send_to_jds = MockDownstream::new( sniffer_addr, @@ -388,7 +389,7 @@ async fn pool_rejects_reused_set_custom_mining_job_token() { start_tracing(); let (tp, tp_addr) = start_template_provider(None, DifficultyLevel::Low); let (pool, pool_addr, jds_addr, _) = - start_pool_with_jds(tp.bitcoin_core(), vec![], vec![], false).await; + start_pool_with_jds(tp.bitcoin_core(), vec![], vec![], false, true).await; // First, run the regular JDC flow and capture one valid SetCustomMiningJob. let (jdc_pool_sniffer, jdc_pool_sniffer_addr) = @@ -495,7 +496,7 @@ async fn jds_receive_solution_while_processing_declared_job_test() { let (tp_1, _tp_addr_1) = start_template_provider(None, DifficultyLevel::Low); let (tp_2, tp_addr_2) = start_template_provider(None, DifficultyLevel::Low); let (pool, pool_addr, jds_addr, _) = - start_pool_with_jds(tp_1.bitcoin_core(), vec![], vec![], false).await; + start_pool_with_jds(tp_1.bitcoin_core(), vec![], vec![], false, true).await; let prev_hash = U256::Owned(vec![ 184, 103, 138, 88, 153, 105, 236, 29, 123, 246, 107, 203, 1, 33, 10, 122, 188, 139, 218, @@ -596,7 +597,7 @@ async fn jds_wont_exit_upon_receiving_unexpected_txids_in_provide_missing_transa assert!(tp_2.create_mempool_transaction().is_ok()); let (pool, pool_addr, jds_addr, _) = - start_pool_with_jds(tp_1.bitcoin_core(), vec![], vec![], false).await; + start_pool_with_jds(tp_1.bitcoin_core(), vec![], vec![], false, true).await; let provide_missing_transaction_success_replace = ReplaceMessage::new( MessageDirection::ToUpstream, @@ -686,7 +687,7 @@ async fn jdc_group_extended_channels() { let (tp, tp_addr) = start_template_provider(sv2_interval, DifficultyLevel::Low); tp.fund_wallet().unwrap(); let (pool, pool_addr, jds_addr, _) = - start_pool_with_jds(tp.bitcoin_core(), vec![], vec![], false).await; + start_pool_with_jds(tp.bitcoin_core(), vec![], vec![], false, true).await; let (jdc, jdc_addr, _) = start_jdc( &[(pool_addr, jds_addr)], @@ -871,7 +872,7 @@ async fn jdc_group_standard_channels() { let (tp, tp_addr) = start_template_provider(sv2_interval, DifficultyLevel::Low); tp.fund_wallet().unwrap(); let (pool, pool_addr, jds_addr, _) = - start_pool_with_jds(tp.bitcoin_core(), vec![], vec![], false).await; + start_pool_with_jds(tp.bitcoin_core(), vec![], vec![], false, true).await; let (jdc, jdc_addr, _) = start_jdc( &[(pool_addr, jds_addr)], @@ -1065,7 +1066,7 @@ async fn jdc_require_standard_jobs_set_does_not_group_standard_channels() { let (tp, tp_addr) = start_template_provider(sv2_interval, DifficultyLevel::Low); tp.fund_wallet().unwrap(); let (pool, pool_addr, jds_addr, _) = - start_pool_with_jds(tp.bitcoin_core(), vec![], vec![], true).await; + start_pool_with_jds(tp.bitcoin_core(), vec![], vec![], true, true).await; let (jdc, jdc_addr, _) = start_jdc( &[(pool_addr, jds_addr)], diff --git a/integration-tests/tests/jd_mining_modes.rs b/integration-tests/tests/jd_mining_modes.rs new file mode 100644 index 000000000..0d3c62f17 --- /dev/null +++ b/integration-tests/tests/jd_mining_modes.rs @@ -0,0 +1,105 @@ +use integration_tests_sv2::{interceptor::MessageDirection, template_provider::DifficultyLevel, *}; +use jd_client_sv2::config::ConfigJDCMode; +use stratum_apps::stratum_core::{ + common_messages_sv2::*, job_declaration_sv2::*, parsers_sv2, parsers_sv2::AnyMessage, + template_distribution_sv2::*, +}; + +#[tokio::test] +async fn jd_mode_mismatch_setup_connection_fails() { + start_tracing(); + let (tp, _tp_addr) = start_template_provider(None, DifficultyLevel::Low); + let (pool, pool_addr, jds_addr, _) = + start_pool_with_jds(tp.bitcoin_core(), vec![], vec![], false, true).await; + let (sniffer, sniffer_addr) = start_sniffer("jdc-jds", jds_addr, false, vec![], None); + let (jdc, _jdc_addr, _) = start_jdc( + &[(pool_addr, sniffer_addr)], + ipc_config( + tp.bitcoin_core().data_dir().clone(), + tp.bitcoin_core().is_signet(), + None, + ), + vec![], + vec![], + false, + Some(ConfigJDCMode::CoinbaseOnly), + ); + sniffer + .wait_for_message_type(MessageDirection::ToUpstream, MESSAGE_TYPE_SETUP_CONNECTION) + .await; + sniffer + .wait_for_message_type( + MessageDirection::ToDownstream, + MESSAGE_TYPE_SETUP_CONNECTION_ERROR, + ) + .await; + + let setup_connection_error = sniffer.next_message_from_upstream(); + let setup_connection_error = match setup_connection_error { + Some((_, AnyMessage::Common(parsers_sv2::CommonMessages::SetupConnectionError(msg)))) => { + msg + } + msg => panic!("Expected SetupConnectionError message, found: {:?}", msg), + }; + assert_eq!( + setup_connection_error.error_code.as_utf8_or_hex(), + "requires-full-template-mode", + "SetupConnectionError error code should be requires-full-template-mode" + ); + + shutdown_all!(pool, jdc); +} + +#[tokio::test] +async fn jd_coinbase_only_mode_skips_declare_mining_job() { + start_tracing(); + let (tp, tp_addr) = start_template_provider(None, DifficultyLevel::Low); + let current_block_hash = tp.get_best_block_hash().unwrap(); + let (pool, pool_addr, jds_addr, _) = + start_pool_with_jds(tp.bitcoin_core(), vec![], vec![], false, false).await; + let (jdc_jds_sniffer, jdc_jds_sniffer_addr) = + start_sniffer("jdc-jds", jds_addr, false, vec![], None); + let (jdc_tp_sniffer, jdc_tp_sniffer_addr) = + start_sniffer("jdc-tp", tp_addr, false, vec![], None); + let (jdc, jdc_addr, _) = start_jdc( + &[(pool_addr, jdc_jds_sniffer_addr)], + sv2_tp_config(jdc_tp_sniffer_addr), + vec![], + vec![], + false, + Some(ConfigJDCMode::CoinbaseOnly), + ); + + jdc_jds_sniffer + .wait_for_message_type(MessageDirection::ToUpstream, MESSAGE_TYPE_SETUP_CONNECTION) + .await; + jdc_jds_sniffer + .wait_for_message_type( + MessageDirection::ToDownstream, + MESSAGE_TYPE_SETUP_CONNECTION_SUCCESS, + ) + .await; + + let (translator, tproxy_addr, _) = + start_sv2_translator(&[jdc_addr], false, vec![], vec![], None, false).await; + let (_minerd, _) = start_minerd(tproxy_addr, None, None, false).await; + + jdc_tp_sniffer + .wait_for_message_type(MessageDirection::ToUpstream, MESSAGE_TYPE_SUBMIT_SOLUTION) + .await; + let new_block_hash = tp.get_best_block_hash().unwrap(); + assert_ne!(current_block_hash, new_block_hash); + + assert!( + jdc_jds_sniffer + .assert_message_not_present( + MessageDirection::ToUpstream, + MESSAGE_TYPE_DECLARE_MINING_JOB, + std::time::Duration::from_secs(2), + ) + .await, + "DeclareMiningJob must not be exchanged in CoinbaseOnly mode" + ); + + shutdown_all!(pool, jdc, translator); +} diff --git a/integration-tests/tests/jd_provide_missing_transaction.rs b/integration-tests/tests/jd_provide_missing_transaction.rs index 5236330f6..8cd045d02 100644 --- a/integration-tests/tests/jd_provide_missing_transaction.rs +++ b/integration-tests/tests/jd_provide_missing_transaction.rs @@ -7,7 +7,7 @@ async fn jds_ask_for_missing_transactions() { let (tp_1, _tp_addr_1) = start_template_provider(None, DifficultyLevel::Low); let (tp_2, tp_addr_2) = start_template_provider(None, DifficultyLevel::Low); let (pool, pool_addr, jds_addr, _) = - start_pool_with_jds(tp_1.bitcoin_core(), vec![], vec![], false).await; + start_pool_with_jds(tp_1.bitcoin_core(), vec![], vec![], false, true).await; let (sniffer, sniffer_addr) = start_sniffer("A", jds_addr, false, vec![], None); let (jdc, jdc_addr, _) = start_jdc( &[(pool_addr, sniffer_addr)], diff --git a/integration-tests/tests/jd_tproxy_integration.rs b/integration-tests/tests/jd_tproxy_integration.rs index 9555408e2..f2556f54b 100644 --- a/integration-tests/tests/jd_tproxy_integration.rs +++ b/integration-tests/tests/jd_tproxy_integration.rs @@ -6,7 +6,7 @@ async fn jd_non_aggregated_tproxy_integration() { start_tracing(); let (tp, _tp_addr) = start_template_provider(None, DifficultyLevel::Low); let (pool, pool_addr, jds_addr, _) = - start_pool_with_jds(tp.bitcoin_core(), vec![], vec![], false).await; + start_pool_with_jds(tp.bitcoin_core(), vec![], vec![], false, true).await; let (jdc_pool_sniffer, jdc_pool_sniffer_addr) = start_sniffer("0", pool_addr, false, vec![], None); let (jdc, jdc_addr, _) = start_jdc( @@ -95,7 +95,7 @@ async fn jd_aggregated_tproxy_integration() { start_tracing(); let (tp, _tp_addr) = start_template_provider(None, DifficultyLevel::Low); let (pool, pool_addr, jds_addr, _) = - start_pool_with_jds(tp.bitcoin_core(), vec![], vec![], false).await; + start_pool_with_jds(tp.bitcoin_core(), vec![], vec![], false, true).await; let (jdc_pool_sniffer, jdc_pool_sniffer_addr) = start_sniffer("0", pool_addr, false, vec![], None); let (jdc, jdc_addr, _) = start_jdc( diff --git a/integration-tests/tests/jdc_block_propagation.rs b/integration-tests/tests/jdc_block_propagation.rs index 89b8240e4..7cdd00beb 100644 --- a/integration-tests/tests/jdc_block_propagation.rs +++ b/integration-tests/tests/jdc_block_propagation.rs @@ -12,7 +12,7 @@ async fn propagated_from_jdc_to_tp() { let (tp, tp_addr) = start_template_provider(None, DifficultyLevel::Low); let current_block_hash = tp.get_best_block_hash().unwrap(); let (pool, pool_addr, jds_addr, _) = - start_pool_with_jds(tp.bitcoin_core(), vec![], vec![], false).await; + start_pool_with_jds(tp.bitcoin_core(), vec![], vec![], false, true).await; let ignore_push_solution = IgnoreMessage::new(MessageDirection::ToUpstream, MESSAGE_TYPE_PUSH_SOLUTION); let (jdc_jds_sniffer, jdc_jds_sniffer_addr) = start_sniffer( diff --git a/integration-tests/tests/jdc_cached_shares.rs b/integration-tests/tests/jdc_cached_shares.rs index ce16980ca..937dd76ca 100644 --- a/integration-tests/tests/jdc_cached_shares.rs +++ b/integration-tests/tests/jdc_cached_shares.rs @@ -27,7 +27,7 @@ async fn jdc_cached_shares_relayed_on_set_custom_job_success() { // causing `SetCustomMiningJob.Success` to fail. let (tp, tp_addr) = start_template_provider(None, DifficultyLevel::High); let (_pool, _pool_addr, jds_addr, _) = - start_pool_with_jds(tp.bitcoin_core(), vec![], vec![], false).await; + start_pool_with_jds(tp.bitcoin_core(), vec![], vec![], false, true).await; let mock_pool_addr = get_available_address(); let mock_pool = MockUpstream::new( diff --git a/integration-tests/tests/jdc_fallback_to_solo.rs b/integration-tests/tests/jdc_fallback_to_solo.rs index ef3f244ff..0f39c0144 100644 --- a/integration-tests/tests/jdc_fallback_to_solo.rs +++ b/integration-tests/tests/jdc_fallback_to_solo.rs @@ -11,7 +11,7 @@ async fn jdc_fallback_to_solo_mines_block_with_bitcoin_core_ipc() { let current_block_hash = bitcoin_core.get_best_block_hash().unwrap(); let (pool, pool_addr, jds_addr, _) = - start_pool_with_jds(&bitcoin_core, vec![], vec![], false).await; + start_pool_with_jds(&bitcoin_core, vec![], vec![], false, true).await; let (jdc_jds_sniffer, jdc_jds_sniffer_addr) = start_sniffer( "jdc-fallback-bitcoin-core-jds", jds_addr, @@ -102,7 +102,7 @@ async fn jdc_fallback_to_solo_mines_block_with_template_provider() { let current_block_hash = tp.get_best_block_hash().unwrap(); let (pool, pool_addr, jds_addr, _) = - start_pool_with_jds(tp.bitcoin_core(), vec![], vec![], false).await; + start_pool_with_jds(tp.bitcoin_core(), vec![], vec![], false, true).await; let (jdc_jds_sniffer, jdc_jds_sniffer_addr) = start_sniffer("jdc-fallback-sv2-tp-jds", jds_addr, false, vec![], None); let (jdc_tp_sniffer, jdc_tp_sniffer_addr) = diff --git a/integration-tests/tests/jdc_receives_submit_shares_success.rs b/integration-tests/tests/jdc_receives_submit_shares_success.rs index 9bd04d3ae..13ae9149c 100644 --- a/integration-tests/tests/jdc_receives_submit_shares_success.rs +++ b/integration-tests/tests/jdc_receives_submit_shares_success.rs @@ -6,7 +6,7 @@ async fn jdc_submit_shares_success() { start_tracing(); let (tp, tp_addr) = start_template_provider(None, DifficultyLevel::Low); let (pool, pool_addr, jds_addr, _) = - start_pool_with_jds(tp.bitcoin_core(), vec![], vec![], false).await; + start_pool_with_jds(tp.bitcoin_core(), vec![], vec![], false, true).await; let (sniffer, sniffer_addr) = start_sniffer("0", pool_addr, false, vec![], None); let (jdc, jdc_addr, _) = start_jdc( &[(sniffer_addr, jds_addr)], diff --git a/integration-tests/tests/jds_block_propagation.rs b/integration-tests/tests/jds_block_propagation.rs index c2fb4bee7..4a8e3fbe8 100644 --- a/integration-tests/tests/jds_block_propagation.rs +++ b/integration-tests/tests/jds_block_propagation.rs @@ -14,7 +14,7 @@ async fn propagated_from_jds_to_tp() { let (tp, tp_addr) = start_template_provider(None, DifficultyLevel::Low); let current_block_hash = tp.get_best_block_hash().unwrap(); let (pool, pool_addr, jds_addr, _) = - start_pool_with_jds(tp.bitcoin_core(), vec![], vec![], false).await; + start_pool_with_jds(tp.bitcoin_core(), vec![], vec![], false, true).await; let (jdc_jds_sniffer, jdc_jds_sniffer_addr) = start_sniffer("0", jds_addr, false, vec![], None); let ignore_submit_solution = IgnoreMessage::new(MessageDirection::ToUpstream, MESSAGE_TYPE_SUBMIT_SOLUTION); diff --git a/integration-tests/tests/monitoring_integration.rs b/integration-tests/tests/monitoring_integration.rs index 9936b6b00..0de9b4da1 100644 --- a/integration-tests/tests/monitoring_integration.rs +++ b/integration-tests/tests/monitoring_integration.rs @@ -177,7 +177,7 @@ async fn jd_aggregated_topology_monitoring() { start_tracing(); let (tp, tp_addr) = start_template_provider(None, DifficultyLevel::Low); let (pool, pool_addr, jds_addr, pool_monitoring) = - start_pool_with_jds(tp.bitcoin_core(), vec![], vec![], true).await; + start_pool_with_jds(tp.bitcoin_core(), vec![], vec![], true, true).await; let (jdc_pool_sniffer, jdc_pool_sniffer_addr) = start_sniffer("0", pool_addr, false, vec![], None); let (jdc, jdc_addr, _jdc_monitoring) = start_jdc( @@ -278,7 +278,7 @@ async fn block_found_detected_in_pool_metrics() { start_tracing(); let (tp, tp_addr) = start_template_provider(None, DifficultyLevel::Low); let (pool, pool_addr, jds_addr, pool_monitoring) = - start_pool_with_jds(tp.bitcoin_core(), vec![], vec![], true).await; + start_pool_with_jds(tp.bitcoin_core(), vec![], vec![], true, true).await; let (_jdc_jds_sniffer, jdc_jds_sniffer_addr) = start_sniffer("0", jds_addr, false, vec![], None); diff --git a/integration-tests/tests/pool_integration.rs b/integration-tests/tests/pool_integration.rs index c87edf407..5d90e98cb 100644 --- a/integration-tests/tests/pool_integration.rs +++ b/integration-tests/tests/pool_integration.rs @@ -251,7 +251,7 @@ async fn pool_does_not_send_jobs_to_jdc() { let (tp, tp_addr) = start_template_provider(sv2_interval, DifficultyLevel::Low); tp.fund_wallet().unwrap(); let (pool, pool_addr, jds_addr, _) = - start_pool_with_jds(tp.bitcoin_core(), vec![], vec![], false).await; + start_pool_with_jds(tp.bitcoin_core(), vec![], vec![], false, true).await; let (pool_jdc_sniffer, pool_jdc_sniffer_addr) = start_sniffer("pool_jdc", pool_addr, false, vec![], None); let (jdc, jdc_addr, _) = start_jdc( diff --git a/pool-apps/jd-server/src/lib/config.rs b/pool-apps/jd-server/src/lib/config.rs index 557e881f6..a0caf22ea 100644 --- a/pool-apps/jd-server/src/lib/config.rs +++ b/pool-apps/jd-server/src/lib/config.rs @@ -25,6 +25,8 @@ pub struct JDSPartialConfig { supported_extensions: Vec, #[serde(default)] required_extensions: Vec, + #[serde(default = "default_true")] + full_template_mode_required: bool, } /// Complete JDS configuration with all required fields populated. @@ -40,6 +42,7 @@ pub struct JDSConfig { coinbase_reward_script: CoinbaseRewardScript, supported_extensions: Vec, required_extensions: Vec, + full_template_mode_required: bool, } impl JDSPartialConfig { @@ -52,8 +55,13 @@ impl JDSPartialConfig { listen_address, supported_extensions: Vec::new(), required_extensions: Vec::new(), + full_template_mode_required: true, } } + + pub fn set_full_template_mode_required(&mut self, required: bool) { + self.full_template_mode_required = required; + } } #[cfg_attr(not(test), hotpath::measure_all)] @@ -77,6 +85,7 @@ impl JDSConfig { coinbase_reward_script, supported_extensions, required_extensions, + full_template_mode_required: true, } } @@ -100,9 +109,14 @@ impl JDSConfig { coinbase_reward_script, supported_extensions: partial.supported_extensions, required_extensions: partial.required_extensions, + full_template_mode_required: partial.full_template_mode_required, } } + pub fn full_template_mode_required(&self) -> bool { + self.full_template_mode_required + } + /// Address the JDS downstream server listens on (Noise-encrypted JDP). pub fn listen_address(&self) -> &SocketAddr { &self.listen_address @@ -138,3 +152,7 @@ impl JDSConfig { &self.required_extensions } } + +fn default_true() -> bool { + true +} diff --git a/pool-apps/jd-server/src/lib/job_declarator/downstream/common_message_handler.rs b/pool-apps/jd-server/src/lib/job_declarator/downstream/common_message_handler.rs index 05c566916..223f93404 100644 --- a/pool-apps/jd-server/src/lib/job_declarator/downstream/common_message_handler.rs +++ b/pool-apps/jd-server/src/lib/job_declarator/downstream/common_message_handler.rs @@ -15,6 +15,10 @@ use stratum_apps::{ }; use tracing::info; +// Bit 0 of `SetupConnection.flags` for the Job Declaration protocol. Tracks +// stratum-mining/stratum#2117 (named constant pending in `common_messages_sv2`). +const FULL_TEMPLATE_MODE_FLAG: u32 = 1; + #[cfg_attr(not(test), hotpath::measure_all)] impl HandleCommonMessagesFromClientAsync for Downstream { type Error = JDSError; @@ -65,19 +69,13 @@ impl HandleCommonMessagesFromClientAsync for Downstream { )); } - // todo: add this to `common_messages_sv2` - // see https://github.com/stratum-mining/stratum/issues/2117 - let has_declare_tx_data = { - let flags = msg.flags.reverse_bits(); - let flag = flags >> 31; - flag != 0 - }; + let has_full_template_mode = msg.flags & FULL_TEMPLATE_MODE_FLAG != 0; - if !has_declare_tx_data { - info!("Rejecting connection from {downstream_id}: SetupConnection missing DECLARE_TX_DATA flag."); + if self.full_template_mode_required && !has_full_template_mode { + info!("Rejecting connection from {downstream_id}: JDS requires full template mode."); let response = SetupConnectionError { flags: 0, - error_code: "missing-declare-tx-data-flag" + error_code: "requires-full-template-mode" .to_string() .try_into() .expect("error code must be valid string"), @@ -89,7 +87,7 @@ impl HandleCommonMessagesFromClientAsync for Downstream { .to_downstream_sender .send(frame) .await - .map_err(|e| JDSError::disconnect(e, self.downstream_id))?; + .map_err(|e| JDSError::disconnect(e, downstream_id))?; return Err(JDSError::disconnect( JDSErrorKind::UnsupportedConnectionFlags, diff --git a/pool-apps/jd-server/src/lib/job_declarator/downstream/mod.rs b/pool-apps/jd-server/src/lib/job_declarator/downstream/mod.rs index b7bef17e9..8b09d7973 100644 --- a/pool-apps/jd-server/src/lib/job_declarator/downstream/mod.rs +++ b/pool-apps/jd-server/src/lib/job_declarator/downstream/mod.rs @@ -69,6 +69,8 @@ pub struct Downstream { /// Extensions that JDS requires #[allow(unused)] pub required_extensions: Vec, + /// Whether the JDS requires full template mode from this downstream. + pub full_template_mode_required: bool, /// Per-downstream cancellation token (child of the global token). /// Cancelling this stops IO tasks, the pending jobs janitor, and the downstream loop /// without affecting other downstreams or the server. @@ -119,6 +121,7 @@ impl Downstream { from_job_declarator_receiver: Receiver, supported_extensions: Vec, required_extensions: Vec, + full_template_mode_required: bool, task_manager: Arc, global_cancellation_token: CancellationToken, ) -> Self { @@ -154,6 +157,7 @@ impl Downstream { downstream_id, supported_extensions, required_extensions, + full_template_mode_required, downstream_cancellation_token, } } diff --git a/pool-apps/jd-server/src/lib/job_declarator/mod.rs b/pool-apps/jd-server/src/lib/job_declarator/mod.rs index bb259c7ef..457f81aae 100644 --- a/pool-apps/jd-server/src/lib/job_declarator/mod.rs +++ b/pool-apps/jd-server/src/lib/job_declarator/mod.rs @@ -186,6 +186,7 @@ impl JobDeclarator { cancellation_token: CancellationToken, supported_extensions: Vec, required_extensions: Vec, + full_template_mode_required: bool, ) -> JDSResult<(), error::JobDeclarator> { info!("Starting downstream server at {listening_address}"); let server = TcpListener::bind(listening_address) @@ -215,6 +216,7 @@ impl JobDeclarator { let task_manager_inner = task_manager_clone.clone(); let supported_extensions_inner = supported_extensions.clone(); let required_extensions_inner = required_extensions.clone(); + let full_template_mode_required_inner = full_template_mode_required; task_manager_clone.spawn(async move { let noise_stream = tokio::select! { @@ -254,6 +256,7 @@ impl JobDeclarator { to_downstream_receiver, supported_extensions_inner, required_extensions_inner, + full_template_mode_required_inner, task_manager_inner.clone(), cancellation_token_inner.clone(), ); diff --git a/pool-apps/pool/src/lib/mod.rs b/pool-apps/pool/src/lib/mod.rs index 8a950bb2d..95210e437 100644 --- a/pool-apps/pool/src/lib/mod.rs +++ b/pool-apps/pool/src/lib/mod.rs @@ -133,6 +133,7 @@ impl PoolSv2 { cancellation_token.clone(), jds_config.supported_extensions().to_vec(), jds_config.required_extensions().to_vec(), + jds_config.full_template_mode_required(), ) .await .map_err(|e| PoolErrorKind::Jds(e.into()))?;