Skip to content

Commit 18bf77c

Browse files
authored
Merge pull request #2210 from GitGab19/expand-stratum-translation
add integer power-of-two Sv1 difficulty conversion in `stratum-translation`
2 parents b019ba3 + 4532d61 commit 18bf77c

4 files changed

Lines changed: 175 additions & 1 deletion

File tree

‎Cargo.lock‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎stratum-core/stratum-translation/Cargo.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ channels_sv2 = { path = "../../sv2/channels-sv2", version = "^7.0.0" }
1616
v1 = { path = "../../sv1", package = "sv1_api", version = "^5.0.0" }
1717
tracing = { workspace = true }
1818
bitcoin = { workspace = true }
19+
serde_json = { workspace = true }

‎stratum-core/stratum-translation/src/error.rs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ pub enum StratumTranslationError {
1010
// SV2 -> SV1
1111
FailedToTryToStripBip141(StripBip141Error),
1212
FailedToSerializeToB064K,
13+
InvalidSv1Difficulty(f64),
14+
InvalidSv1IntegerPowerOfTwoRoundingThreshold(f64),
15+
Sv1DifficultyOverflow(f64),
1316
}
1417

1518
pub type Result<T> = core::result::Result<T, StratumTranslationError>;

‎stratum-core/stratum-translation/src/sv2_to_sv1.rs‎

Lines changed: 170 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@ use crate::error::{Result, StratumTranslationError};
1212
use bitcoin::Target;
1313
use channels_sv2::bip141::try_strip_bip141;
1414
use mining_sv2::{NewExtendedMiningJob, SetNewPrevHash, SetTarget};
15+
use serde_json::Value;
1516
use tracing::debug;
1617
use 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)]
128193
mod 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

Comments
 (0)