@@ -12,11 +12,13 @@ use crate::error::{Result, StratumTranslationError};
1212use bitcoin:: Target ;
1313use channels_sv2:: bip141:: try_strip_bip141;
1414use mining_sv2:: { NewExtendedMiningJob , SetNewPrevHash , SetTarget } ;
15+ use serde_json:: Value ;
1516use tracing:: debug;
1617use v1:: {
1718 json_rpc, server_to_client,
1819 utils:: { HexU32Be , MerkleNode , PrevHash } ,
1920} ;
21+
2022/// Builds an SV1 `mining.notify` message from SV2 messages.
2123///
2224/// This function attempts to strip BIP141 (SegWit) data from the coinbase transaction
@@ -124,17 +126,95 @@ pub fn build_sv1_set_difficulty_from_sv2_target(target: Target) -> Result<json_r
124126 Ok ( set_target. into ( ) )
125127}
126128
129+ /// Builds an SV1 `mining.set_difficulty` message using integer power-of-two rounding above a
130+ /// configurable minimum difficulty.
131+ ///
132+ /// The difficulty is computed from the SV2 target using Bitcoin's standard difficulty-1 target.
133+ /// Difficulties below `minimum_difficulty_for_integer_power_of_two_rounding` are emitted unchanged
134+ /// as JSON numbers built from `f64`, preserving fractional values.
135+ ///
136+ /// Difficulties at or above the threshold are rounded up to the next integer power of two and
137+ /// emitted as JSON numbers built from `u64`.
138+ pub fn build_sv1_set_difficulty_from_sv2_target_with_integer_power_of_two_rounding (
139+ target : Target ,
140+ minimum_difficulty_for_integer_power_of_two_rounding : f64 ,
141+ ) -> Result < json_rpc:: Message > {
142+ let value = integer_power_of_two_sv1_difficulty_value_from_difficulty (
143+ target. difficulty_float ( ) ,
144+ minimum_difficulty_for_integer_power_of_two_rounding,
145+ ) ?;
146+ Ok ( build_sv1_set_difficulty_notification ( value) )
147+ }
148+
149+ fn integer_power_of_two_sv1_difficulty_value_from_difficulty (
150+ difficulty : f64 ,
151+ minimum_difficulty_for_integer_power_of_two_rounding : f64 ,
152+ ) -> Result < Value > {
153+ if !difficulty. is_finite ( ) || difficulty <= 0.0 {
154+ return Err ( StratumTranslationError :: InvalidSv1Difficulty ( difficulty) ) ;
155+ }
156+
157+ if !minimum_difficulty_for_integer_power_of_two_rounding. is_finite ( )
158+ || minimum_difficulty_for_integer_power_of_two_rounding <= 0.0
159+ {
160+ return Err (
161+ StratumTranslationError :: InvalidSv1IntegerPowerOfTwoRoundingThreshold (
162+ minimum_difficulty_for_integer_power_of_two_rounding,
163+ ) ,
164+ ) ;
165+ }
166+
167+ if difficulty < minimum_difficulty_for_integer_power_of_two_rounding {
168+ let value = serde_json:: Number :: from_f64 ( difficulty)
169+ . ok_or ( StratumTranslationError :: InvalidSv1Difficulty ( difficulty) ) ?;
170+ return Ok ( Value :: Number ( value) ) ;
171+ }
172+
173+ let integer_difficulty = difficulty. ceil ( ) ;
174+ if integer_difficulty > u64:: MAX as f64 {
175+ return Err ( StratumTranslationError :: Sv1DifficultyOverflow ( difficulty) ) ;
176+ }
177+
178+ let power_of_two = ( integer_difficulty as u64 )
179+ . checked_next_power_of_two ( )
180+ . ok_or ( StratumTranslationError :: Sv1DifficultyOverflow ( difficulty) ) ?;
181+
182+ Ok ( Value :: from ( power_of_two) )
183+ }
184+
185+ fn build_sv1_set_difficulty_notification ( value : Value ) -> json_rpc:: Message {
186+ json_rpc:: Message :: Notification ( json_rpc:: Notification {
187+ method : "mining.set_difficulty" . to_string ( ) ,
188+ params : Value :: Array ( vec ! [ value] ) ,
189+ } )
190+ }
191+
127192#[ cfg( test) ]
128193mod tests {
129194 use super :: * ;
130195 use binary_sv2:: { Seq0255 , Sv2Option , U256 } ;
131- use bitcoin:: Target ;
196+ use bitcoin:: { CompactTarget , Target } ;
132197 use mining_sv2:: { NewExtendedMiningJob , SetNewPrevHash , SetTarget as Sv2SetTarget } ;
133198
134199 fn dummy_target ( ) -> Target {
135200 Target :: from_le_bytes ( [ 0xffu8 ; 32 ] )
136201 }
137202
203+ fn set_difficulty_value ( msg : & json_rpc:: Message ) -> & Value {
204+ match msg {
205+ json_rpc:: Message :: Notification ( notif) => {
206+ assert_eq ! ( notif. method, "mining.set_difficulty" ) ;
207+ notif
208+ . params
209+ . as_array ( )
210+ . expect ( "params must be an array" )
211+ . first ( )
212+ . expect ( "params must contain difficulty" )
213+ }
214+ _ => panic ! ( "Expected mining.set_difficulty notification" ) ,
215+ }
216+ }
217+
138218 #[ test]
139219 fn test_build_sv1_set_difficulty_from_sv2_target ( ) {
140220 let msg = build_sv1_set_difficulty_from_sv2_target ( dummy_target ( ) )
@@ -169,6 +249,95 @@ mod tests {
169249 }
170250 }
171251
252+ #[ test]
253+ fn test_integer_power_of_two_difficulty_keeps_value_below_threshold ( ) {
254+ let value = integer_power_of_two_sv1_difficulty_value_from_difficulty ( 0.25 , 1.0 )
255+ . expect ( "valid value" ) ;
256+
257+ assert_eq ! ( value. as_f64( ) , Some ( 0.25 ) ) ;
258+ assert_eq ! ( value. as_u64( ) , None ) ;
259+ }
260+
261+ #[ test]
262+ fn test_integer_power_of_two_difficulty_can_round_sub_one_when_threshold_is_lower ( ) {
263+ let value = integer_power_of_two_sv1_difficulty_value_from_difficulty ( 0.25 , 0.1 )
264+ . expect ( "valid value" ) ;
265+
266+ assert_eq ! ( value. as_u64( ) , Some ( 1 ) ) ;
267+ }
268+
269+ #[ test]
270+ fn test_integer_power_of_two_difficulty_rounds_up ( ) {
271+ let cases = [
272+ ( 1.0 , 1 ) ,
273+ ( 2.0 , 2 ) ,
274+ ( 2.1 , 4 ) ,
275+ ( 10_000.0 , 16_384 ) ,
276+ ( 12_500.0 , 16_384 ) ,
277+ ( 20_000.0 , 32_768 ) ,
278+ ( 50_000.0 , 65_536 ) ,
279+ ( 100_000.0 , 131_072 ) ,
280+ ] ;
281+
282+ for ( difficulty, expected) in cases {
283+ let value = integer_power_of_two_sv1_difficulty_value_from_difficulty ( difficulty, 1.0 )
284+ . expect ( "valid value" ) ;
285+
286+ assert_eq ! ( value. as_u64( ) , Some ( expected) , "difficulty={difficulty}" ) ;
287+ }
288+ }
289+
290+ #[ test]
291+ fn test_integer_power_of_two_set_difficulty_uses_configurable_rounding_threshold ( ) {
292+ let target = Target :: from_compact ( CompactTarget :: from_consensus ( 0x1b00ffff ) ) ;
293+
294+ let default_msg =
295+ build_sv1_set_difficulty_from_sv2_target_with_integer_power_of_two_rounding (
296+ target, 1.0 ,
297+ )
298+ . expect ( "valid default threshold" ) ;
299+ let high_threshold_msg =
300+ build_sv1_set_difficulty_from_sv2_target_with_integer_power_of_two_rounding (
301+ target, 100_000.0 ,
302+ )
303+ . expect ( "valid custom threshold" ) ;
304+
305+ assert_eq ! ( set_difficulty_value( & default_msg) . as_u64( ) , Some ( 65_536 ) ) ;
306+ assert_eq ! (
307+ set_difficulty_value( & high_threshold_msg) . as_f64( ) ,
308+ Some ( 65_536.0 )
309+ ) ;
310+ assert_eq ! ( set_difficulty_value( & high_threshold_msg) . as_u64( ) , None ) ;
311+ }
312+
313+ #[ test]
314+ fn test_integer_power_of_two_difficulty_rejects_invalid_values ( ) {
315+ for difficulty in [ 0.0 , -1.0 , f64:: NAN , f64:: INFINITY ] {
316+ assert ! ( matches!(
317+ integer_power_of_two_sv1_difficulty_value_from_difficulty( difficulty, 1.0 ) ,
318+ Err ( StratumTranslationError :: InvalidSv1Difficulty ( _) )
319+ ) ) ;
320+ }
321+ }
322+
323+ #[ test]
324+ fn test_integer_power_of_two_difficulty_rejects_invalid_rounding_threshold ( ) {
325+ for rounding_threshold in [ 0.0 , -1.0 , f64:: NAN , f64:: INFINITY ] {
326+ assert ! ( matches!(
327+ integer_power_of_two_sv1_difficulty_value_from_difficulty( 1.0 , rounding_threshold) ,
328+ Err ( StratumTranslationError :: InvalidSv1IntegerPowerOfTwoRoundingThreshold ( _) )
329+ ) ) ;
330+ }
331+ }
332+
333+ #[ test]
334+ fn test_integer_power_of_two_difficulty_rejects_overflow ( ) {
335+ assert ! ( matches!(
336+ integer_power_of_two_sv1_difficulty_value_from_difficulty( u64 :: MAX as f64 , 1.0 ) ,
337+ Err ( StratumTranslationError :: Sv1DifficultyOverflow ( _) )
338+ ) ) ;
339+ }
340+
172341 #[ test]
173342 fn test_build_sv1_notify_from_sv2_with_future_job ( ) {
174343 // Test with a future job using realistic data from existing tests
0 commit comments