From 092d82103f6fd03fcf53e0290c7307bb8d9ab67c Mon Sep 17 00:00:00 2001 From: pool Date: Sun, 4 Oct 2026 22:52:15 +0000 Subject: [PATCH] Accept decimal suggest_difficulty and keep it across authorize --- .../message_handlers/authorize_response.rs | 65 ++++++++++++++++++- .../message_handlers/suggest_difficulty.rs | 18 +++++ p2poolv2_lib/src/stratum/messages.rs | 26 +++++++- 3 files changed, 106 insertions(+), 3 deletions(-) diff --git a/p2poolv2_lib/src/stratum/message_handlers/authorize_response.rs b/p2poolv2_lib/src/stratum/message_handlers/authorize_response.rs index be1f640..c9ca6bf 100644 --- a/p2poolv2_lib/src/stratum/message_handlers/authorize_response.rs +++ b/p2poolv2_lib/src/stratum/message_handlers/authorize_response.rs @@ -113,6 +113,9 @@ pub(crate) async fn handle_authorize<'a, D: DifficultyAdjusterTrait>( } }; + // Keep a difficulty the miner suggested before authorizing (already + // constrained by handle_suggest_difficulty) instead of resetting it. + let default_difficulty = session.suggested_difficulty.unwrap_or(ctx.start_difficulty); let start_difficulty = match &session.password { Some(password) => { let parsed = parse_password(password); @@ -128,10 +131,10 @@ pub(crate) async fn handle_authorize<'a, D: DifficultyAdjusterTrait>( ); constrained } - None => ctx.start_difficulty, + None => default_difficulty, } } - None => ctx.start_difficulty, + None => default_difficulty, }; session @@ -612,6 +615,64 @@ mod tests { } } + #[tokio::test] + async fn test_handle_authorize_keeps_suggested_difficulty() { + let mut session = Session::::new(1, 1, None, 0x1fffe000); + session.suggested_difficulty = Some(1); + let request = SimpleRequest::new_authorize( + 12345, + "tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx".to_string(), + Some("x".to_string()), + ); + let (notify_tx, _notify_rx) = mpsc::channel(1); + let (emissions_tx, _emissions_rx) = mpsc::channel(10); + let (_mock_rpc_server, bitcoinrpc_config) = setup_mock_bitcoin_rpc().await; + let stats_dir = tempfile::tempdir().unwrap(); + let metrics_handle = metrics::start_metrics(stats_dir.path().to_str().unwrap().to_string()) + .await + .unwrap(); + let (chain_store_handle, _temp_dir) = setup_test_chain_store_handle(true).await; + let tracker_handle = start_tracker_actor(); + + let ctx = StratumContext { + notify_tx, + tracker_handle, + bitcoindrpc_client: BitcoindRpcClient::new( + &bitcoinrpc_config.url, + &bitcoinrpc_config.username, + &bitcoinrpc_config.password, + ) + .unwrap(), + start_difficulty: 1000, + minimum_difficulty: 1, + maximum_difficulty: None, + ignore_difficulty: false, + validate_addresses: true, + emissions_tx, + network: bitcoin::network::Network::Testnet, + metrics: metrics_handle, + chain_store_handle, + mode: PoolMode::P2poolv2, + }; + + let messages = handle_authorize(request, &mut session, ctx).await.unwrap(); + + assert_eq!( + session.difficulty_adjuster.get_current_difficulty(), + 1, + "Suggested difficulty sent before authorize should be kept" + ); + + if let Message::SetDifficulty(notification) = &messages[1] { + assert_eq!( + notification.params[0], 1, + "SetDifficulty notification should use suggested difficulty" + ); + } else { + panic!("Expected SetDifficulty message"); + } + } + #[tokio::test] async fn test_handle_authorize_invalid_username_second_attempt() { // Setup - session with auth_failed_once already true diff --git a/p2poolv2_lib/src/stratum/message_handlers/suggest_difficulty.rs b/p2poolv2_lib/src/stratum/message_handlers/suggest_difficulty.rs index 45a1e4b..38b9a73 100644 --- a/p2poolv2_lib/src/stratum/message_handlers/suggest_difficulty.rs +++ b/p2poolv2_lib/src/stratum/message_handlers/suggest_difficulty.rs @@ -52,6 +52,24 @@ pub async fn handle_suggest_difficulty<'a, D: DifficultyAdjusterTrait>( #[cfg(test)] mod tests { use super::*; + + #[test] + fn test_parse_decimal_suggest_difficulty() { + use crate::stratum::messages::{Message, Request}; + let line = r#"{"id": 3, "method": "mining.suggest_difficulty", "params": [0.00015]}"#; + let msg: Request = serde_json::from_str(line).expect("decimal difficulty should parse"); + match msg { + Request::SuggestDifficultyRequest(r) => assert_eq!(r.params[0], 1), + other => panic!("unexpected {:?}", other), + } + let line = r#"{"id": 4, "method": "mining.suggest_difficulty", "params": [512]}"#; + let msg: Request = serde_json::from_str(line).expect("integer difficulty should parse"); + match msg { + Request::SuggestDifficultyRequest(r) => assert_eq!(r.params[0], 512), + other => panic!("unexpected {:?}", other), + } + let _ = std::mem::size_of::(); + } use crate::stratum::difficulty_adjuster::DifficultyAdjuster; use crate::stratum::messages::Id; use std::borrow::Cow; diff --git a/p2poolv2_lib/src/stratum/messages.rs b/p2poolv2_lib/src/stratum/messages.rs index 103abed..be33f52 100644 --- a/p2poolv2_lib/src/stratum/messages.rs +++ b/p2poolv2_lib/src/stratum/messages.rs @@ -126,10 +126,34 @@ pub struct SuggestDifficulty<'a> { pub id: Option, #[serde(borrow)] pub method: Cow<'a, str>, - #[serde(borrow, default)] + #[serde(default, deserialize_with = "deserialize_difficulty_params")] pub params: Cow<'a, Vec>, } +/// Accept integer or decimal difficulties in mining.suggest_difficulty. +/// Some firmwares (NerdMiner, etc.) send values like 0.00015. Decimals are +/// rounded up; non-positive or non-finite values become 0. The difficulty +/// adjuster then raises the value to the pool minimum. +fn deserialize_difficulty_params<'de, 'a, D>(deserializer: D) -> Result>, D::Error> +where + D: serde::Deserializer<'de>, +{ + let values: Vec = Vec::deserialize(deserializer)?; + values + .into_iter() + .map(|v| { + let f = match &v { + Value::Number(n) => n.as_f64(), + Value::String(s) => s.trim().parse::().ok(), + _ => None, + } + .ok_or_else(|| serde::de::Error::custom("difficulty must be a number"))?; + Ok(if f.is_finite() && f > 0.0 { f.ceil() as u64 } else { 0 }) + }) + .collect::, D::Error>>() + .map(Cow::Owned) +} + /// Struct for mining.configure messages /// These messages have mixed types in params and those are defined in MiningConfigureParams. /// -- 2.43.0