diff --git a/Cargo.lock b/Cargo.lock index c5fbc16a86..cc38e2834c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1246,7 +1246,7 @@ dependencies = [ [[package]] name = "stratum_translation" -version = "0.4.0" +version = "0.4.1" dependencies = [ "binary_sv2", "bitcoin", diff --git a/stratum-core/stratum-translation/Cargo.toml b/stratum-core/stratum-translation/Cargo.toml index 8ba7ba3f9c..dcc7c5add8 100644 --- a/stratum-core/stratum-translation/Cargo.toml +++ b/stratum-core/stratum-translation/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "stratum_translation" -version = "0.4.0" +version = "0.4.1" edition = "2021" license = "MIT OR Apache-2.0" description = "Stratum V1 ↔ Stratum V2 translation utilities for reuse across proxies, apps, and firmware" diff --git a/stratum-core/stratum-translation/src/sv2_to_sv1.rs b/stratum-core/stratum-translation/src/sv2_to_sv1.rs index 4103b38e28..eec51e1791 100644 --- a/stratum-core/stratum-translation/src/sv2_to_sv1.rs +++ b/stratum-core/stratum-translation/src/sv2_to_sv1.rs @@ -133,8 +133,13 @@ pub fn build_sv1_set_difficulty_from_sv2_target(target: Target) -> Result Result { +) -> Result { if !difficulty.is_finite() || difficulty <= 0.0 { return Err(StratumTranslationError::InvalidSv1Difficulty(difficulty)); } if !minimum_difficulty_for_integer_power_of_two_rounding.is_finite() - || minimum_difficulty_for_integer_power_of_two_rounding <= 0.0 + || minimum_difficulty_for_integer_power_of_two_rounding < 1.0 { return Err( StratumTranslationError::InvalidSv1IntegerPowerOfTwoRoundingThreshold( @@ -165,21 +173,82 @@ fn integer_power_of_two_sv1_difficulty_value_from_difficulty( } if difficulty < minimum_difficulty_for_integer_power_of_two_rounding { - let value = serde_json::Number::from_f64(difficulty) - .ok_or(StratumTranslationError::InvalidSv1Difficulty(difficulty))?; - return Ok(Value::Number(value)); + return Ok(difficulty); } - let integer_difficulty = difficulty.ceil(); - if integer_difficulty > u64::MAX as f64 { + let integer_difficulty = difficulty.floor(); + if integer_difficulty >= u64::MAX as f64 { return Err(StratumTranslationError::Sv1DifficultyOverflow(difficulty)); } - let power_of_two = (integer_difficulty as u64) - .checked_next_power_of_two() - .ok_or(StratumTranslationError::Sv1DifficultyOverflow(difficulty))?; + let integer_difficulty = integer_difficulty as u64; + let power_of_two = if integer_difficulty == 0 { + 1 + } else { + 1u64 << (63 - integer_difficulty.leading_zeros()) + }; + + Ok(power_of_two as f64) +} + +/// Returns the target corresponding to the SV1 difficulty that +/// [`build_sv1_set_difficulty_from_sv2_target_with_integer_power_of_two_rounding`] advertises for +/// `target`. +/// +/// Callers validating SV1 shares should use this target for downstream validation, then use the +/// original upstream target to decide whether an accepted downstream share is strong enough to +/// forward upstream. +pub fn sv1_advertised_target_from_sv2_target( + target: Target, + minimum_difficulty_for_integer_power_of_two_rounding: f64, +) -> Result { + let advertised = advertised_sv1_difficulty_from_difficulty( + target.difficulty_float(), + minimum_difficulty_for_integer_power_of_two_rounding, + )?; + + if advertised < minimum_difficulty_for_integer_power_of_two_rounding { + return Ok(target); + } + + Ok(target_from_power_of_two_difficulty(advertised as u64)) +} + +fn target_from_power_of_two_difficulty(difficulty: u64) -> Target { + let shift = difficulty.trailing_zeros() as usize; + let bytes = Target::MAX.to_be_bytes(); + let byte_shift = shift / 8; + let bit_shift = shift % 8; + let mut out = [0u8; 32]; + + for i in (byte_shift..32).rev() { + let src = i - byte_shift; + let mut byte = bytes[src] >> bit_shift; + if bit_shift > 0 && src > 0 { + byte |= bytes[src - 1] << (8 - bit_shift); + } + out[i] = byte; + } + + Target::from_be_bytes(out) +} + +fn integer_power_of_two_sv1_difficulty_value_from_difficulty( + difficulty: f64, + minimum_difficulty_for_integer_power_of_two_rounding: f64, +) -> Result { + let advertised = advertised_sv1_difficulty_from_difficulty( + difficulty, + minimum_difficulty_for_integer_power_of_two_rounding, + )?; + + if advertised < minimum_difficulty_for_integer_power_of_two_rounding { + let value = serde_json::Number::from_f64(advertised) + .ok_or(StratumTranslationError::InvalidSv1Difficulty(advertised))?; + return Ok(Value::Number(value)); + } - Ok(Value::from(power_of_two)) + Ok(Value::from(advertised as u64)) } fn build_sv1_set_difficulty_notification(value: Value) -> json_rpc::Message { @@ -259,24 +328,26 @@ mod tests { } #[test] - fn test_integer_power_of_two_difficulty_can_round_sub_one_when_threshold_is_lower() { - let value = integer_power_of_two_sv1_difficulty_value_from_difficulty(0.25, 0.1) - .expect("valid value"); - - assert_eq!(value.as_u64(), Some(1)); + fn test_integer_power_of_two_difficulty_rejects_threshold_below_one() { + assert!(matches!( + integer_power_of_two_sv1_difficulty_value_from_difficulty(0.25, 0.1), + Err(StratumTranslationError::InvalidSv1IntegerPowerOfTwoRoundingThreshold(_)) + )); } #[test] - fn test_integer_power_of_two_difficulty_rounds_up() { + fn test_integer_power_of_two_difficulty_rounds_down() { let cases = [ (1.0, 1), (2.0, 2), - (2.1, 4), - (10_000.0, 16_384), - (12_500.0, 16_384), - (20_000.0, 32_768), - (50_000.0, 65_536), - (100_000.0, 131_072), + (2.1, 2), + (3.9, 2), + (10_000.0, 8_192), + (12_500.0, 8_192), + (20_000.0, 16_384), + (50_000.0, 32_768), + (65_536.0, 65_536), + (100_000.0, 65_536), ]; for (difficulty, expected) in cases { @@ -310,6 +381,42 @@ mod tests { assert_eq!(set_difficulty_value(&high_threshold_msg).as_u64(), None); } + #[test] + fn test_sv1_advertised_target_keeps_target_below_threshold() { + let target = dummy_target(); + + assert_eq!( + sv1_advertised_target_from_sv2_target(target, 1.0).expect("valid target"), + target + ); + } + + #[test] + fn test_sv1_advertised_target_matches_power_of_two_difficulty() { + let target = Target::from_compact(CompactTarget::from_consensus(0x1b00ffff)); + + assert_eq!( + sv1_advertised_target_from_sv2_target(target, 1.0).expect("valid target"), + target_from_power_of_two_difficulty(65_536) + ); + } + + #[test] + fn test_sv1_advertised_target_rounds_down_to_power_of_two_target() { + let expected = target_from_power_of_two_difficulty(65_536); + let mut bytes = expected.to_be_bytes(); + bytes[6] = 0xaa; + bytes[7] = 0xaa; + let target = Target::from_be_bytes(bytes); + + assert!(target.difficulty_float() > 65_536.0); + assert!(target.difficulty_float() < 131_072.0); + assert_eq!( + sv1_advertised_target_from_sv2_target(target, 1.0).expect("valid target"), + expected + ); + } + #[test] fn test_integer_power_of_two_difficulty_rejects_invalid_values() { for difficulty in [0.0, -1.0, f64::NAN, f64::INFINITY] { @@ -322,7 +429,7 @@ mod tests { #[test] fn test_integer_power_of_two_difficulty_rejects_invalid_rounding_threshold() { - for rounding_threshold in [0.0, -1.0, f64::NAN, f64::INFINITY] { + for rounding_threshold in [0.0, 0.5, -1.0, f64::NAN, f64::INFINITY] { assert!(matches!( integer_power_of_two_sv1_difficulty_value_from_difficulty(1.0, rounding_threshold), Err(StratumTranslationError::InvalidSv1IntegerPowerOfTwoRoundingThreshold(_))