nano-rspow-python 0.16.0__tar.gz → 0.17.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. {nano_rspow_python-0.16.0 → nano_rspow_python-0.17.0}/Cargo.lock +6 -6
  2. {nano_rspow_python-0.16.0 → nano_rspow_python-0.17.0}/Cargo.toml +1 -1
  3. {nano_rspow_python-0.16.0 → nano_rspow_python-0.17.0}/PKG-INFO +29 -6
  4. {nano_rspow_python-0.16.0/nano-rspow-python → nano_rspow_python-0.17.0}/README.md +28 -5
  5. {nano_rspow_python-0.16.0 → nano_rspow_python-0.17.0}/nano-rspow/README.md +10 -0
  6. {nano_rspow_python-0.16.0 → nano_rspow_python-0.17.0}/nano-rspow/benches/pow_benchmark.rs +11 -11
  7. {nano_rspow_python-0.16.0 → nano_rspow_python-0.17.0}/nano-rspow/src/cpu.rs +13 -13
  8. {nano_rspow_python-0.16.0 → nano_rspow_python-0.17.0}/nano-rspow/src/difficulty.rs +4 -4
  9. {nano_rspow_python-0.16.0 → nano_rspow_python-0.17.0}/nano-rspow/src/lib.rs +4 -4
  10. {nano_rspow_python-0.16.0 → nano_rspow_python-0.17.0}/nano-rspow/src/opencl_backend/mod.rs +5 -3
  11. nano_rspow_python-0.17.0/nano-rspow/src/thresholds.rs +75 -0
  12. {nano_rspow_python-0.16.0 → nano_rspow_python-0.17.0}/nano-rspow/src/types.rs +3 -3
  13. {nano_rspow_python-0.16.0 → nano_rspow_python-0.17.0}/nano-rspow/src/wgpu_backend/mod.rs +4 -4
  14. {nano_rspow_python-0.16.0 → nano_rspow_python-0.17.0/nano-rspow-python}/README.md +28 -5
  15. {nano_rspow_python-0.16.0 → nano_rspow_python-0.17.0}/nano-rspow-python/nano_rspow.pyi +18 -32
  16. {nano_rspow_python-0.16.0 → nano_rspow_python-0.17.0}/nano-rspow-python/src/lib.rs +55 -32
  17. {nano_rspow_python-0.16.0 → nano_rspow_python-0.17.0}/nano-rspow-python/tests/test_pow.py +28 -23
  18. nano_rspow_python-0.16.0/nano-rspow/src/thresholds.rs +0 -79
  19. {nano_rspow_python-0.16.0 → nano_rspow_python-0.17.0}/nano-rspow/Cargo.toml +0 -0
  20. {nano_rspow_python-0.16.0 → nano_rspow_python-0.17.0}/nano-rspow/src/opencl_backend/pow.cl +0 -0
  21. {nano_rspow_python-0.16.0 → nano_rspow_python-0.17.0}/nano-rspow/src/wgpu_backend/pow.wgsl +0 -0
  22. {nano_rspow_python-0.16.0 → nano_rspow_python-0.17.0}/nano-rspow/src/wgpu_shared.rs +0 -0
  23. {nano_rspow_python-0.16.0 → nano_rspow_python-0.17.0}/nano-rspow-python/Cargo.toml +0 -0
  24. {nano_rspow_python-0.16.0 → nano_rspow_python-0.17.0}/nano-rspow-python/build.rs +0 -0
  25. {nano_rspow_python-0.16.0 → nano_rspow_python-0.17.0}/nano-rspow-python/uv.lock +0 -0
  26. {nano_rspow_python-0.16.0 → nano_rspow_python-0.17.0}/pyproject.toml +0 -0
@@ -757,7 +757,7 @@ dependencies = [
757
757
 
758
758
  [[package]]
759
759
  name = "nano-rspow"
760
- version = "0.16.0"
760
+ version = "0.17.0"
761
761
  dependencies = [
762
762
  "blake2b_simd",
763
763
  "bytemuck",
@@ -773,7 +773,7 @@ dependencies = [
773
773
 
774
774
  [[package]]
775
775
  name = "nano-rspow-cli"
776
- version = "0.16.0"
776
+ version = "0.17.0"
777
777
  dependencies = [
778
778
  "clap",
779
779
  "hex",
@@ -785,14 +785,14 @@ dependencies = [
785
785
 
786
786
  [[package]]
787
787
  name = "nano-rspow-ffi"
788
- version = "0.16.0"
788
+ version = "0.17.0"
789
789
  dependencies = [
790
790
  "nano-rspow",
791
791
  ]
792
792
 
793
793
  [[package]]
794
794
  name = "nano-rspow-node"
795
- version = "0.16.0"
795
+ version = "0.17.0"
796
796
  dependencies = [
797
797
  "hex",
798
798
  "nano-rspow",
@@ -804,7 +804,7 @@ dependencies = [
804
804
 
805
805
  [[package]]
806
806
  name = "nano-rspow-python"
807
- version = "0.16.0"
807
+ version = "0.17.0"
808
808
  dependencies = [
809
809
  "hex",
810
810
  "nano-rspow",
@@ -813,7 +813,7 @@ dependencies = [
813
813
 
814
814
  [[package]]
815
815
  name = "nano-rspow-web"
816
- version = "0.16.0"
816
+ version = "0.17.0"
817
817
  dependencies = [
818
818
  "bytemuck",
819
819
  "getrandom",
@@ -3,7 +3,7 @@ members = ["nano-rspow", "nano-rspow-python"]
3
3
  resolver = "2"
4
4
 
5
5
  [workspace.package]
6
- version = "0.16.0"
6
+ version = "0.17.0"
7
7
  edition = "2024"
8
8
  license = "MIT"
9
9
  repository = "https://github.com/CasualSecurityInc/nano-rspow"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nano-rspow-python
3
- Version: 0.16.0
3
+ Version: 0.17.0
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: 3 :: Only
6
6
  Classifier: Programming Language :: Python :: 3.9
@@ -50,6 +50,7 @@ line protocol as `nano-rspow generate --stream`:
50
50
  ```python
51
51
  import nano_rspow
52
52
  from nano_rspow import WorkType
53
+ from nano_rspow.thresholds import current
53
54
 
54
55
  root = "718CC2121C3E641059BC1C2CFC45666C99E8AE922F7A807B7D07B62C995D79E2"
55
56
  result = nano_rspow.generate_work(root, WorkType.Send)
@@ -57,6 +58,12 @@ result = nano_rspow.generate_work(root, WorkType.Send)
57
58
  print(result.nonce_hex)
58
59
  assert result.is_valid
59
60
  assert nano_rspow.validate_work(root, result.nonce_hex, WorkType.Send)
61
+ assert current.SEND == 0xfffffff800000000
62
+
63
+ # Use an arbitrary threshold when a node requires more than the public floor.
64
+ threshold = "fffffff900000000"
65
+ result = nano_rspow.generate_work_with_threshold(root, threshold)
66
+ assert nano_rspow.validate_work_with_threshold(root, result.nonce_hex, threshold)
60
67
  ```
61
68
 
62
69
  ## API reference
@@ -69,6 +76,15 @@ Generates work for a 32-byte hexadecimal block root. Raises `ValueError` for inv
69
76
 
70
77
  Tests a hexadecimal nonce against a block root and the threshold selected by `work_type`. Raises `ValueError` for invalid input.
71
78
 
79
+ ### `generate_work_with_threshold(hash_hex, threshold_hex) -> WorkResult`
80
+
81
+ Generates work for any hexadecimal threshold. Use it when a node requires a
82
+ stricter threshold than the public Nano floor or when handling historical data.
83
+
84
+ ### `validate_work_with_threshold(hash_hex, work_hex, threshold_hex) -> bool`
85
+
86
+ Validates a nonce against any hexadecimal threshold.
87
+
72
88
  ### `compute_difficulty(hash_hex, nonce_hex) -> str`
73
89
 
74
90
  Returns the raw difficulty as a 16-character lowercase hexadecimal string. Raises `ValueError` for invalid input.
@@ -81,10 +97,8 @@ Returns the selected backend name, such as `cpu`, `wgpu`, or `opencl`.
81
97
 
82
98
  | Value | Use |
83
99
  | --- | --- |
84
- | `Send` | Epoch 2 send and change blocks |
85
- | `Receive` | Epoch 2 receive blocks |
86
- | `LegacyEpoch1` | Historical epoch-1 validation only |
87
- | `Epoch1` | Compatibility alias for `LegacyEpoch1` |
100
+ | `Send` | Current send and change blocks |
101
+ | `Receive` | Current receive, open, and epoch blocks |
88
102
 
89
103
  ### `WorkResult`
90
104
 
@@ -92,5 +106,14 @@ Returns the selected backend name, such as `cpu`, `wgpu`, or `opencl`.
92
106
 
93
107
  ### `thresholds`
94
108
 
95
- The `nano_rspow.thresholds` submodule provides `EPOCH2_SEND`, `EPOCH2_RECEIVE`, `LEGACY_EPOCH1`, `EPOCH1`, `BETA_LEGACY_EPOCH1`, `BETA_EPOCH1`, `DEV`, and `BASE`.
109
+ The `nano_rspow.thresholds` submodule separates values by applicability:
110
+
111
+ - `thresholds.current.SEND` and `.RECEIVE` are the current public mainnet floors.
112
+ - `thresholds.legacy.EPOCH1` and `.BETA_EPOCH1` are historical values.
113
+ - `thresholds.testing.DEV` is for tests only.
114
+
115
+ Nano nodes may require a stricter arbitrary threshold. Use the explicit
116
+ custom-threshold APIs for that case. See Nano's
117
+ [Work Generation guide](https://docs.nano.org/integration-guides/work-generation/)
118
+ for current network requirements.
96
119
 
@@ -26,6 +26,7 @@ line protocol as `nano-rspow generate --stream`:
26
26
  ```python
27
27
  import nano_rspow
28
28
  from nano_rspow import WorkType
29
+ from nano_rspow.thresholds import current
29
30
 
30
31
  root = "718CC2121C3E641059BC1C2CFC45666C99E8AE922F7A807B7D07B62C995D79E2"
31
32
  result = nano_rspow.generate_work(root, WorkType.Send)
@@ -33,6 +34,12 @@ result = nano_rspow.generate_work(root, WorkType.Send)
33
34
  print(result.nonce_hex)
34
35
  assert result.is_valid
35
36
  assert nano_rspow.validate_work(root, result.nonce_hex, WorkType.Send)
37
+ assert current.SEND == 0xfffffff800000000
38
+
39
+ # Use an arbitrary threshold when a node requires more than the public floor.
40
+ threshold = "fffffff900000000"
41
+ result = nano_rspow.generate_work_with_threshold(root, threshold)
42
+ assert nano_rspow.validate_work_with_threshold(root, result.nonce_hex, threshold)
36
43
  ```
37
44
 
38
45
  ## API reference
@@ -45,6 +52,15 @@ Generates work for a 32-byte hexadecimal block root. Raises `ValueError` for inv
45
52
 
46
53
  Tests a hexadecimal nonce against a block root and the threshold selected by `work_type`. Raises `ValueError` for invalid input.
47
54
 
55
+ ### `generate_work_with_threshold(hash_hex, threshold_hex) -> WorkResult`
56
+
57
+ Generates work for any hexadecimal threshold. Use it when a node requires a
58
+ stricter threshold than the public Nano floor or when handling historical data.
59
+
60
+ ### `validate_work_with_threshold(hash_hex, work_hex, threshold_hex) -> bool`
61
+
62
+ Validates a nonce against any hexadecimal threshold.
63
+
48
64
  ### `compute_difficulty(hash_hex, nonce_hex) -> str`
49
65
 
50
66
  Returns the raw difficulty as a 16-character lowercase hexadecimal string. Raises `ValueError` for invalid input.
@@ -57,10 +73,8 @@ Returns the selected backend name, such as `cpu`, `wgpu`, or `opencl`.
57
73
 
58
74
  | Value | Use |
59
75
  | --- | --- |
60
- | `Send` | Epoch 2 send and change blocks |
61
- | `Receive` | Epoch 2 receive blocks |
62
- | `LegacyEpoch1` | Historical epoch-1 validation only |
63
- | `Epoch1` | Compatibility alias for `LegacyEpoch1` |
76
+ | `Send` | Current send and change blocks |
77
+ | `Receive` | Current receive, open, and epoch blocks |
64
78
 
65
79
  ### `WorkResult`
66
80
 
@@ -68,4 +82,13 @@ Returns the selected backend name, such as `cpu`, `wgpu`, or `opencl`.
68
82
 
69
83
  ### `thresholds`
70
84
 
71
- The `nano_rspow.thresholds` submodule provides `EPOCH2_SEND`, `EPOCH2_RECEIVE`, `LEGACY_EPOCH1`, `EPOCH1`, `BETA_LEGACY_EPOCH1`, `BETA_EPOCH1`, `DEV`, and `BASE`.
85
+ The `nano_rspow.thresholds` submodule separates values by applicability:
86
+
87
+ - `thresholds.current.SEND` and `.RECEIVE` are the current public mainnet floors.
88
+ - `thresholds.legacy.EPOCH1` and `.BETA_EPOCH1` are historical values.
89
+ - `thresholds.testing.DEV` is for tests only.
90
+
91
+ Nano nodes may require a stricter arbitrary threshold. Use the explicit
92
+ custom-threshold APIs for that case. See Nano's
93
+ [Work Generation guide](https://docs.nano.org/integration-guides/work-generation/)
94
+ for current network requirements.
@@ -4,6 +4,16 @@ nano-rspow generates and validates [Nano](https://www.nano.org) proof of work on
4
4
 
5
5
  The native core selects an available GPU backend when possible and otherwise uses its multi-threaded CPU backend. The browser package tries WebGPU, then falls back to single-threaded WebAssembly CPU execution.
6
6
 
7
+ ## Current Nano thresholds
8
+
9
+ For current Nano mainnet blocks, use the send/change floor
10
+ `fffffff800000000` or the receive/open/epoch floor `fffffe0000000000`.
11
+ These are convenience presets, not an upper limit: every binding also accepts
12
+ an arbitrary threshold when the node that will publish the block requires a
13
+ stricter value. Historical Epoch 1 thresholds are isolated in explicitly named
14
+ legacy APIs. See Nano's [Work Generation guide](https://docs.nano.org/integration-guides/work-generation/)
15
+ for the network requirements.
16
+
7
17
  ---
8
18
 
9
19
  ## 🎯 Who is this for?
@@ -4,13 +4,13 @@
4
4
  //!
5
5
  //! Tiers mirror the CLI `benchmark` command:
6
6
  //! dev 0xfe00000000000000 (very low — fast iteration)
7
- //! ep2_recv 0xfffffe0000000000 (epoch-2 receive — common real-world)
7
+ //! receive 0xfffffe0000000000 (current receive/open/epoch)
8
8
  //! legacy_epoch1 0xffffffc000000000 (legacy-only)
9
- //! ep2_send 0xfffffff800000000 (epoch-2 send/change — hardest)
9
+ //! send 0xfffffff800000000 (current send/change)
10
10
  //!
11
11
  //! CPU benchmarks run at `dev` threshold only (higher tiers take 1-2 s each,
12
12
  //! making Criterion wall-time impractical). GPU benchmarks run at `dev` and
13
- //! `ep2_recv`; `legacy_epoch1` and `ep2_send` are included but marked slow so
13
+ //! `receive`; `legacy_epoch1` and `send` are included but marked slow so
14
14
  //! they only run when specifically requested (`cargo bench -- legacy_epoch1`).
15
15
 
16
16
  use criterion::{Criterion, SamplingMode, Throughput, criterion_group, criterion_main};
@@ -25,7 +25,7 @@ const BENCH_HASH: [u8; 32] = [
25
25
  // ── difficulty_compute ────────────────────────────────────────────────────────
26
26
 
27
27
  fn bench_difficulty(c: &mut Criterion) {
28
- // Official known-good nonce for BENCH_HASH at epoch-2-send threshold.
28
+ // Official known-good nonce for BENCH_HASH at the current send threshold.
29
29
  let nonce = 0x2bf29ef00786a6bc_u64;
30
30
  c.bench_function("difficulty_compute", |b| {
31
31
  b.iter(|| difficulty::compute(&BENCH_HASH, std::hint::black_box(nonce)));
@@ -41,7 +41,7 @@ fn bench_cpu_generation(c: &mut Criterion) {
41
41
  // Only dev threshold: higher tiers take 1-2 s per sample.
42
42
  group.bench_function("dev", |b| {
43
43
  let generator = nano_rspow::WorkGenerator::cpu();
44
- b.iter(|| generator.generate(&BENCH_HASH, thresholds::DEV).unwrap());
44
+ b.iter(|| generator.generate(&BENCH_HASH, thresholds::testing::DEV).unwrap());
45
45
  });
46
46
 
47
47
  group.finish();
@@ -65,13 +65,13 @@ fn bench_gpu_generation(c: &mut Criterion) {
65
65
  group.throughput(Throughput::Elements(1));
66
66
 
67
67
  group.bench_function("dev", |b| {
68
- b.iter(|| generator.generate(&BENCH_HASH, thresholds::DEV).unwrap());
68
+ b.iter(|| generator.generate(&BENCH_HASH, thresholds::testing::DEV).unwrap());
69
69
  });
70
70
 
71
- group.bench_function("ep2_recv", |b| {
71
+ group.bench_function("receive", |b| {
72
72
  b.iter(|| {
73
73
  generator
74
- .generate(&BENCH_HASH, thresholds::EPOCH2_RECEIVE)
74
+ .generate(&BENCH_HASH, thresholds::current::RECEIVE)
75
75
  .unwrap()
76
76
  });
77
77
  });
@@ -88,13 +88,13 @@ fn bench_gpu_generation(c: &mut Criterion) {
88
88
  group.sampling_mode(SamplingMode::Flat);
89
89
 
90
90
  group.bench_function("legacy_epoch1", |b| {
91
- b.iter(|| generator.generate(&BENCH_HASH, thresholds::LEGACY_EPOCH1).unwrap());
91
+ b.iter(|| generator.generate(&BENCH_HASH, thresholds::legacy::EPOCH1).unwrap());
92
92
  });
93
93
 
94
- group.bench_function("ep2_send", |b| {
94
+ group.bench_function("send", |b| {
95
95
  b.iter(|| {
96
96
  generator
97
- .generate(&BENCH_HASH, thresholds::EPOCH2_SEND)
97
+ .generate(&BENCH_HASH, thresholds::current::SEND)
98
98
  .unwrap()
99
99
  });
100
100
  });
@@ -242,12 +242,12 @@ mod tests {
242
242
  let hash = [0u8; 32];
243
243
  let backend = CpuBackend::new();
244
244
  let cancel = CancelToken::new();
245
- let nonce = backend.generate(&hash, thresholds::DEV, &cancel).unwrap();
245
+ let nonce = backend.generate(&hash, thresholds::testing::DEV, &cancel).unwrap();
246
246
  let diff = difficulty::compute(&hash, nonce);
247
247
  assert!(
248
- diff >= thresholds::DEV,
248
+ diff >= thresholds::testing::DEV,
249
249
  "nonce {nonce:#018x} produced difficulty {diff:#018x} < threshold {:#018x}",
250
- thresholds::DEV
250
+ thresholds::testing::DEV
251
251
  );
252
252
  }
253
253
 
@@ -275,9 +275,9 @@ mod tests {
275
275
 
276
276
  let backend = CpuBackend::new();
277
277
  let cancel = CancelToken::new();
278
- let nonce = backend.generate(&hash, thresholds::DEV, &cancel).unwrap();
278
+ let nonce = backend.generate(&hash, thresholds::testing::DEV, &cancel).unwrap();
279
279
  let diff = difficulty::compute(&hash, nonce);
280
- assert!(diff >= thresholds::DEV);
280
+ assert!(diff >= thresholds::testing::DEV);
281
281
  }
282
282
 
283
283
  #[test]
@@ -288,12 +288,12 @@ mod tests {
288
288
 
289
289
  // Multiple calls on the same backend should all produce valid work
290
290
  for _ in 0..5 {
291
- let nonce = backend.generate(&hash, thresholds::DEV, &cancel).unwrap();
291
+ let nonce = backend.generate(&hash, thresholds::testing::DEV, &cancel).unwrap();
292
292
  let diff = difficulty::compute(&hash, nonce);
293
293
  assert!(
294
- diff >= thresholds::DEV,
294
+ diff >= thresholds::testing::DEV,
295
295
  "nonce {nonce:#018x} produced difficulty {diff:#018x} < threshold {:#018x}",
296
- thresholds::DEV
296
+ thresholds::testing::DEV
297
297
  );
298
298
  }
299
299
  }
@@ -313,7 +313,7 @@ mod tests {
313
313
  let backend = Arc::clone(&backend);
314
314
  let cancel = cancel.clone();
315
315
  thread::spawn(move || {
316
- backend.generate(&hash, thresholds::DEV, &cancel)
316
+ backend.generate(&hash, thresholds::testing::DEV, &cancel)
317
317
  })
318
318
  })
319
319
  .collect();
@@ -328,7 +328,7 @@ mod tests {
328
328
  // All nonces should be valid
329
329
  for &nonce in &nonces {
330
330
  let diff = difficulty::compute(&hash, nonce);
331
- assert!(diff >= thresholds::DEV);
331
+ assert!(diff >= thresholds::testing::DEV);
332
332
  }
333
333
  }
334
334
 
@@ -365,9 +365,9 @@ mod tests {
365
365
  let backend = CpuBackend::new();
366
366
  let cancel = CancelToken::new();
367
367
 
368
- let nonce1 = backend.generate(&hash, thresholds::DEV, &cancel).unwrap();
369
- let nonce2 = backend.generate(&hash, thresholds::DEV, &cancel).unwrap();
370
- let nonce3 = backend.generate(&hash, thresholds::DEV, &cancel).unwrap();
368
+ let nonce1 = backend.generate(&hash, thresholds::testing::DEV, &cancel).unwrap();
369
+ let nonce2 = backend.generate(&hash, thresholds::testing::DEV, &cancel).unwrap();
370
+ let nonce3 = backend.generate(&hash, thresholds::testing::DEV, &cancel).unwrap();
371
371
 
372
372
  // Three nonces from a seeded RNG with 2^64 space should all be distinct.
373
373
  assert_ne!(nonce1, nonce2, "same nonce on consecutive generate calls");
@@ -73,7 +73,7 @@ mod tests {
73
73
  "difficulty mismatch: got {actual:#018x}, expected {expected_difficulty:#018x}"
74
74
  );
75
75
  assert!(
76
- actual >= thresholds::LEGACY_EPOCH1,
76
+ actual >= thresholds::legacy::EPOCH1,
77
77
  "work must meet legacy epoch1 threshold"
78
78
  );
79
79
  }
@@ -87,7 +87,7 @@ mod tests {
87
87
 
88
88
  let actual = compute(&hash, work);
89
89
  assert_eq!(actual, expected_difficulty);
90
- assert!(actual >= thresholds::LEGACY_EPOCH1);
90
+ assert!(actual >= thresholds::legacy::EPOCH1);
91
91
  }
92
92
 
93
93
  /// Vector 3 — boundary: work exactly at the legacy epoch-1 threshold should be valid.
@@ -96,7 +96,7 @@ mod tests {
96
96
  // We brute-force a nonce that's right at the boundary using CPU search
97
97
  // (very low dev threshold for speed)
98
98
  let hash = [0u8; 32];
99
- let threshold = thresholds::DEV; // Very low — tests finish fast
99
+ let threshold = thresholds::testing::DEV; // Very low — tests finish fast
100
100
 
101
101
  // Search for a valid nonce
102
102
  let nonce = (0u64..).find(|&n| compute(&hash, n) >= threshold).unwrap();
@@ -110,7 +110,7 @@ mod tests {
110
110
  // work = 0 almost certainly produces difficulty below any real threshold
111
111
  let hash = hex_to_array("718CC2121C3E641059BC1C2CFC45666C99E8AE922F7A807B7D07B62C995D79E2");
112
112
  let work = 0u64;
113
- assert!(!is_valid(&hash, work, thresholds::LEGACY_EPOCH1));
113
+ assert!(!is_valid(&hash, work, thresholds::legacy::EPOCH1));
114
114
  }
115
115
 
116
116
  /// Vector 5 — determinism: same inputs always produce same output.
@@ -16,7 +16,7 @@
16
16
  //!
17
17
  //! // Validate a known-good work value (nonce) matching the above test vector
18
18
  //! let work = u64::from_str_radix("2bf29ef00786a6bc", 16).unwrap();
19
- //! let result = nano_rspow::work_validate(&hash, work, thresholds::LEGACY_EPOCH1);
19
+ //! let result = nano_rspow::work_validate(&hash, work, thresholds::current::RECEIVE);
20
20
  //! assert!(result.is_valid());
21
21
  //! ```
22
22
 
@@ -221,7 +221,7 @@ pub fn recommend_local_pow() -> bool {
221
221
 
222
222
  let estimated_total_hps = hashes_per_sec * core_count;
223
223
 
224
- // Target: ~15 MH/s minimum for reasonable EPOCH2_SEND speed
224
+ // Target: ~15 MH/s minimum for reasonable current send-threshold speed.
225
225
  estimated_total_hps >= 15_000_000.0
226
226
  };
227
227
 
@@ -260,7 +260,7 @@ mod tests {
260
260
  fn assert_repeated_generate_valid(generator: &WorkGenerator) {
261
261
  let hash = test_hash();
262
262
  for _ in 0..3 {
263
- let result = generator.generate(&hash, thresholds::DEV).unwrap();
263
+ let result = generator.generate(&hash, thresholds::testing::DEV).unwrap();
264
264
  assert!(result.is_valid());
265
265
  }
266
266
  }
@@ -272,7 +272,7 @@ mod tests {
272
272
  for _ in 0..4 {
273
273
  let g = Arc::clone(&g);
274
274
  handles.push(thread::spawn(move || {
275
- let result = g.generate(&hash, thresholds::DEV).unwrap();
275
+ let result = g.generate(&hash, thresholds::testing::DEV).unwrap();
276
276
  assert!(result.is_valid());
277
277
  }));
278
278
  }
@@ -27,7 +27,9 @@ impl OpenClBackend {
27
27
  pub fn new(_config: OpenClConfig) -> Result<Self, WorkError> {
28
28
  let platforms = ocl::core::get_platform_ids();
29
29
  if platforms.is_err() || platforms.as_ref().map(|p| p.is_empty()).unwrap_or(true) {
30
- return Err(WorkError::GpuInit("No OpenCL platforms available".to_string()));
30
+ return Err(WorkError::GpuInit(
31
+ "No OpenCL platforms available".to_string(),
32
+ ));
31
33
  }
32
34
 
33
35
  let pro_que = ProQue::builder()
@@ -83,8 +85,8 @@ impl Backend for OpenClBackend {
83
85
 
84
86
  // Convert the 32-byte hash into 4 x u64 (little-endian)
85
87
  let mut h = [0u64; 4];
86
- for (i, chunk) in hash.chunks_exact(8).enumerate() {
87
- h[i] = u64::from_le_bytes(chunk.try_into().unwrap());
88
+ for (output, chunk) in h.iter_mut().zip(hash.as_chunks::<8>().0) {
89
+ *output = u64::from_le_bytes(*chunk);
88
90
  }
89
91
 
90
92
  let mut base_nonce: u64 = rand::random();
@@ -0,0 +1,75 @@
1
+ //! Nano PoW threshold presets.
2
+ //!
3
+ //! Use [`current`] for blocks published to current Nano mainnet nodes. The
4
+ //! [`legacy`] and [`testing`] modules are deliberately separate so historical
5
+ //! and test-only values are not mistaken for current network requirements.
6
+ //!
7
+ //! Source: rsnano-node `work/src/work_thresholds.rs` and the original
8
+ //! C++ nano-node `nano/lib/work.cpp`.
9
+
10
+ /// Current Nano mainnet minimum thresholds.
11
+ pub mod current {
12
+ /// Minimum threshold for send and change blocks.
13
+ pub const SEND: u64 = 0xfffffff800000000;
14
+
15
+ /// Minimum threshold for receive, open, and epoch blocks.
16
+ pub const RECEIVE: u64 = 0xfffffe0000000000;
17
+ }
18
+
19
+ /// Historical thresholds for validating or benchmarking Epoch 1 blocks.
20
+ pub mod legacy {
21
+ /// Epoch 1 mainnet threshold.
22
+ ///
23
+ /// This value is not required when publishing a current mainnet block.
24
+ pub const EPOCH1: u64 = 0xffffffc000000000;
25
+
26
+ /// Epoch 1 beta-network threshold.
27
+ pub const BETA_EPOCH1: u64 = 0xfffff00000000000;
28
+ }
29
+
30
+ /// Non-mainnet thresholds intended only for development and tests.
31
+ pub mod testing {
32
+ /// Low-difficulty development threshold.
33
+ pub const DEV: u64 = 0xfe00000000000000;
34
+ }
35
+
36
+ /// Compute a difficulty multiplier relative to a base threshold.
37
+ ///
38
+ /// A multiplier of 1.0 means exactly at base difficulty. >1.0 means harder.
39
+ pub fn to_multiplier(difficulty: u64, base: u64) -> f64 {
40
+ debug_assert!(base > 0);
41
+ let max = u64::MAX as f64;
42
+ (max - base as f64) / (max - difficulty as f64)
43
+ }
44
+
45
+ /// Compute the difficulty from a multiplier and base threshold.
46
+ pub fn from_multiplier(multiplier: f64, base: u64) -> u64 {
47
+ debug_assert!(multiplier >= 1.0);
48
+ let max = u64::MAX as f64;
49
+ u64::MAX - ((max - base as f64) / multiplier) as u64
50
+ }
51
+
52
+ #[cfg(test)]
53
+ mod tests {
54
+ use super::*;
55
+
56
+ #[test]
57
+ fn current_send_is_eight_times_legacy_epoch1() {
58
+ let m = to_multiplier(current::SEND, legacy::EPOCH1);
59
+ assert!((m - 8.0).abs() < 0.01, "expected 8.0, got {m}");
60
+ }
61
+
62
+ #[test]
63
+ fn current_receive_is_one_eighth_of_legacy_epoch1() {
64
+ let m = to_multiplier(current::RECEIVE, legacy::EPOCH1);
65
+ assert!((m - 0.125).abs() < 0.001, "expected 0.125, got {m}");
66
+ }
67
+
68
+ #[test]
69
+ fn roundtrip_multiplier() {
70
+ let m = 2.5_f64;
71
+ let d = from_multiplier(m, current::SEND);
72
+ let m2 = to_multiplier(d, current::SEND);
73
+ assert!((m - m2).abs() < 0.001);
74
+ }
75
+ }
@@ -26,7 +26,7 @@ impl WorkResult {
26
26
  self.difficulty >= self.threshold
27
27
  }
28
28
 
29
- /// Returns the multiplier relative to the epoch 2 base threshold.
29
+ /// Returns the multiplier relative to this result's required threshold.
30
30
  pub fn multiplier(&self) -> f64 {
31
31
  thresholds::to_multiplier(self.difficulty, self.threshold)
32
32
  }
@@ -128,7 +128,7 @@ mod tests {
128
128
  let r = WorkResult {
129
129
  nonce: 0x2bf29ef00786a6bc,
130
130
  difficulty: 0xffffffd21c3933f4,
131
- threshold: thresholds::LEGACY_EPOCH1,
131
+ threshold: thresholds::legacy::EPOCH1,
132
132
  };
133
133
  assert!(r.is_valid());
134
134
  assert_eq!(r.nonce_hex(), "2bf29ef00786a6bc");
@@ -139,7 +139,7 @@ mod tests {
139
139
  let r = WorkResult {
140
140
  nonce: 0,
141
141
  difficulty: 0,
142
- threshold: thresholds::LEGACY_EPOCH1,
142
+ threshold: thresholds::legacy::EPOCH1,
143
143
  };
144
144
  assert!(!r.is_valid());
145
145
  }
@@ -435,11 +435,11 @@ impl Backend for WgpuBackend {
435
435
 
436
436
  let mut hash0 = [0u32; 4];
437
437
  let mut hash1 = [0u32; 4];
438
- for (i, chunk) in hash[..16].chunks_exact(4).enumerate() {
439
- hash0[i] = u32::from_le_bytes(chunk.try_into().unwrap());
438
+ for (output, chunk) in hash0.iter_mut().zip(hash[..16].as_chunks::<4>().0) {
439
+ *output = u32::from_le_bytes(*chunk);
440
440
  }
441
- for (i, chunk) in hash[16..].chunks_exact(4).enumerate() {
442
- hash1[i] = u32::from_le_bytes(chunk.try_into().unwrap());
441
+ for (output, chunk) in hash1.iter_mut().zip(hash[16..].as_chunks::<4>().0) {
442
+ *output = u32::from_le_bytes(*chunk);
443
443
  }
444
444
 
445
445
  let threshold_lo = threshold as u32;
@@ -26,6 +26,7 @@ line protocol as `nano-rspow generate --stream`:
26
26
  ```python
27
27
  import nano_rspow
28
28
  from nano_rspow import WorkType
29
+ from nano_rspow.thresholds import current
29
30
 
30
31
  root = "718CC2121C3E641059BC1C2CFC45666C99E8AE922F7A807B7D07B62C995D79E2"
31
32
  result = nano_rspow.generate_work(root, WorkType.Send)
@@ -33,6 +34,12 @@ result = nano_rspow.generate_work(root, WorkType.Send)
33
34
  print(result.nonce_hex)
34
35
  assert result.is_valid
35
36
  assert nano_rspow.validate_work(root, result.nonce_hex, WorkType.Send)
37
+ assert current.SEND == 0xfffffff800000000
38
+
39
+ # Use an arbitrary threshold when a node requires more than the public floor.
40
+ threshold = "fffffff900000000"
41
+ result = nano_rspow.generate_work_with_threshold(root, threshold)
42
+ assert nano_rspow.validate_work_with_threshold(root, result.nonce_hex, threshold)
36
43
  ```
37
44
 
38
45
  ## API reference
@@ -45,6 +52,15 @@ Generates work for a 32-byte hexadecimal block root. Raises `ValueError` for inv
45
52
 
46
53
  Tests a hexadecimal nonce against a block root and the threshold selected by `work_type`. Raises `ValueError` for invalid input.
47
54
 
55
+ ### `generate_work_with_threshold(hash_hex, threshold_hex) -> WorkResult`
56
+
57
+ Generates work for any hexadecimal threshold. Use it when a node requires a
58
+ stricter threshold than the public Nano floor or when handling historical data.
59
+
60
+ ### `validate_work_with_threshold(hash_hex, work_hex, threshold_hex) -> bool`
61
+
62
+ Validates a nonce against any hexadecimal threshold.
63
+
48
64
  ### `compute_difficulty(hash_hex, nonce_hex) -> str`
49
65
 
50
66
  Returns the raw difficulty as a 16-character lowercase hexadecimal string. Raises `ValueError` for invalid input.
@@ -57,10 +73,8 @@ Returns the selected backend name, such as `cpu`, `wgpu`, or `opencl`.
57
73
 
58
74
  | Value | Use |
59
75
  | --- | --- |
60
- | `Send` | Epoch 2 send and change blocks |
61
- | `Receive` | Epoch 2 receive blocks |
62
- | `LegacyEpoch1` | Historical epoch-1 validation only |
63
- | `Epoch1` | Compatibility alias for `LegacyEpoch1` |
76
+ | `Send` | Current send and change blocks |
77
+ | `Receive` | Current receive, open, and epoch blocks |
64
78
 
65
79
  ### `WorkResult`
66
80
 
@@ -68,4 +82,13 @@ Returns the selected backend name, such as `cpu`, `wgpu`, or `opencl`.
68
82
 
69
83
  ### `thresholds`
70
84
 
71
- The `nano_rspow.thresholds` submodule provides `EPOCH2_SEND`, `EPOCH2_RECEIVE`, `LEGACY_EPOCH1`, `EPOCH1`, `BETA_LEGACY_EPOCH1`, `BETA_EPOCH1`, `DEV`, and `BASE`.
85
+ The `nano_rspow.thresholds` submodule separates values by applicability:
86
+
87
+ - `thresholds.current.SEND` and `.RECEIVE` are the current public mainnet floors.
88
+ - `thresholds.legacy.EPOCH1` and `.BETA_EPOCH1` are historical values.
89
+ - `thresholds.testing.DEV` is for tests only.
90
+
91
+ Nano nodes may require a stricter arbitrary threshold. Use the explicit
92
+ custom-threshold APIs for that case. See Nano's
93
+ [Work Generation guide](https://docs.nano.org/integration-guides/work-generation/)
94
+ for current network requirements.
@@ -9,16 +9,12 @@ class WorkType(IntEnum):
9
9
  """Nano network work type — determines the difficulty threshold.
10
10
 
11
11
  Values:
12
- Send: Epoch 2 send/change threshold (0xfffffff800000000)
13
- Receive: Epoch 2 receive threshold (0xfffffe0000000000)
14
- LegacyEpoch1: Legacy threshold only (0xffffffc000000000)
15
- Epoch1: LegacyEpoch1 compatibility alias (deprecated)
12
+ Send: Current send/change threshold (0xfffffff800000000)
13
+ Receive: Current receive/open/epoch threshold (0xfffffe0000000000)
16
14
  """
17
15
 
18
16
  Send = 0
19
17
  Receive = 1
20
- Epoch1 = 2
21
- LegacyEpoch1 = 3
22
18
 
23
19
  class WorkResult:
24
20
  """The result of a PoW generation or validation.
@@ -83,6 +79,10 @@ def validate_work(hash_hex: str, work_hex: str, work_type: WorkType) -> bool:
83
79
  """
84
80
  ...
85
81
 
82
+ def validate_work_with_threshold(hash_hex: str, work_hex: str, threshold_hex: str) -> bool:
83
+ """Validate a nonce against an arbitrary hexadecimal threshold."""
84
+ ...
85
+
86
86
  def compute_difficulty(hash_hex: str, nonce_hex: str) -> str:
87
87
  """Compute the raw PoW difficulty for a hash+nonce pair.
88
88
 
@@ -110,32 +110,18 @@ def cli_main() -> int:
110
110
  ...
111
111
 
112
112
  class thresholds:
113
- """Nano PoW threshold constants for all network epochs.
114
-
115
- Constants are sourced from the Rust core library and match
116
- the values used by rsnano-node and the C++ nano-node.
117
- """
118
-
119
- EPOCH2_SEND: int
120
- """Epoch 2 send/change threshold (current live network default for sends)."""
121
-
122
- EPOCH2_RECEIVE: int
123
- """Epoch 2 receive threshold."""
124
-
125
- LEGACY_EPOCH1: int
126
- """Legacy epoch 1 threshold; not for current mainnet epoch blocks."""
127
-
128
- EPOCH1: int
129
- """Deprecated compatibility alias for LEGACY_EPOCH1."""
130
-
131
- BETA_LEGACY_EPOCH1: int
132
- """Beta network legacy epoch 1 threshold."""
113
+ """Grouped threshold presets from the Rust core library."""
133
114
 
134
- BETA_EPOCH1: int
135
- """Deprecated compatibility alias for BETA_LEGACY_EPOCH1."""
115
+ class current:
116
+ """Current Nano mainnet threshold presets."""
117
+ SEND: int
118
+ RECEIVE: int
136
119
 
137
- DEV: int
138
- """Dev network threshold (very low, for testing)."""
120
+ class legacy:
121
+ """Historical Epoch 1 threshold presets."""
122
+ EPOCH1: int
123
+ BETA_EPOCH1: int
139
124
 
140
- BASE: int
141
- """The highest threshold — used as the base for multiplier calculations."""
125
+ class testing:
126
+ """Test-only threshold presets."""
127
+ DEV: int
@@ -17,11 +17,11 @@
17
17
  //! print(result.nonce_hex)
18
18
  //! print(result.is_valid)
19
19
  //!
20
- //! # Validate existing work
21
- //! valid = nano_rspow.validate_work(
20
+ //! # Validate historical work with an explicit threshold
21
+ //! valid = nano_rspow.validate_work_with_threshold(
22
22
  //! "718CC2121C3E641059BC1C2CFC45666C99E8AE922F7A807B7D07B62C995D79E2",
23
23
  //! "2bf29ef00786a6bc",
24
- //! WorkType.LegacyEpoch1,
24
+ //! "ffffffc000000000",
25
25
  //! )
26
26
  //! ```
27
27
 
@@ -49,26 +49,21 @@ fn get_generator() -> &'static WorkGenerator {
49
49
 
50
50
  /// Nano network work type — determines the difficulty threshold.
51
51
  ///
52
- /// Values mirror the thresholds defined in `nano-rspow/src/thresholds.rs`:
53
- /// - `Send` → epoch 2 send/change (0xfffffff800000000)
54
- /// - `Receive` → epoch 2 receive (0xfffffe0000000000)
55
- /// - `LegacyEpoch1` → legacy threshold only (0xffffffc000000000)
56
- /// - `Epoch1` → deprecated compatibility alias for `LegacyEpoch1`
52
+ /// Values mirror the current mainnet presets in `nano-rspow/src/thresholds.rs`:
53
+ /// - `Send` → send/change (0xfffffff800000000)
54
+ /// - `Receive` → receive/open/epoch (0xfffffe0000000000)
57
55
  #[pyclass(eq, eq_int, from_py_object)]
58
56
  #[derive(Clone, Copy, PartialEq)]
59
57
  enum WorkType {
60
58
  Send = 0,
61
59
  Receive = 1,
62
- Epoch1 = 2,
63
- LegacyEpoch1 = 3,
64
60
  }
65
61
 
66
62
  impl WorkType {
67
63
  fn threshold(self) -> u64 {
68
64
  match self {
69
- WorkType::Send => thresholds::EPOCH2_SEND,
70
- WorkType::Receive => thresholds::EPOCH2_RECEIVE,
71
- WorkType::Epoch1 | WorkType::LegacyEpoch1 => thresholds::LEGACY_EPOCH1,
65
+ WorkType::Send => thresholds::current::SEND,
66
+ WorkType::Receive => thresholds::current::RECEIVE,
72
67
  }
73
68
  }
74
69
  }
@@ -146,6 +141,11 @@ fn parse_hash(hash_hex: &str) -> PyResult<[u8; 32]> {
146
141
  Ok(hash)
147
142
  }
148
143
 
144
+ fn parse_threshold(threshold_hex: &str) -> PyResult<u64> {
145
+ u64::from_str_radix(threshold_hex.trim().trim_start_matches("0x"), 16)
146
+ .map_err(|e| PyValueError::new_err(format!("Invalid threshold hex: {e}")))
147
+ }
148
+
149
149
  // ---------------------------------------------------------------------------
150
150
  // Public API — module-level functions
151
151
  // ---------------------------------------------------------------------------
@@ -196,11 +196,7 @@ fn generate_work_with_threshold(
196
196
  threshold_hex: &str,
197
197
  ) -> PyResult<WorkResult> {
198
198
  let hash = parse_hash(hash_hex)?;
199
- let threshold = u64::from_str_radix(
200
- threshold_hex.trim().trim_start_matches("0x"),
201
- 16,
202
- )
203
- .map_err(|e| PyValueError::new_err(format!("Invalid threshold hex: {e}")))?;
199
+ let threshold = parse_threshold(threshold_hex)?;
204
200
  let generator = get_generator();
205
201
 
206
202
  let result = py.detach(move || generator.generate(&hash, threshold));
@@ -240,6 +236,24 @@ fn validate_work(hash_hex: &str, work_hex: &str, work_type: WorkType) -> PyResul
240
236
  Ok(result.is_valid())
241
237
  }
242
238
 
239
+ /// Validate a work nonce against an arbitrary hexadecimal threshold.
240
+ ///
241
+ /// Use this function when a node requires a threshold stricter than the
242
+ /// current mainnet presets, or when validating historical work.
243
+ #[pyfunction]
244
+ fn validate_work_with_threshold(
245
+ hash_hex: &str,
246
+ work_hex: &str,
247
+ threshold_hex: &str,
248
+ ) -> PyResult<bool> {
249
+ let hash = parse_hash(hash_hex)?;
250
+ let nonce = u64::from_str_radix(work_hex.trim().trim_start_matches("0x"), 16)
251
+ .map_err(|e| PyValueError::new_err(format!("Invalid work hex: {e}")))?;
252
+ let threshold = parse_threshold(threshold_hex)?;
253
+
254
+ Ok(::nano_rspow::work_validate(&hash, nonce, threshold).is_valid())
255
+ }
256
+
243
257
  /// Compute the raw PoW difficulty for a hash+nonce pair.
244
258
  ///
245
259
  /// This is a low-level function useful for validation tooling
@@ -272,7 +286,7 @@ fn backend_name() -> &'static str {
272
286
  get_generator().backend_name()
273
287
  }
274
288
 
275
- const CLI_HELP: &str = "nano-rspow-python generates Nano proof of work from stdin.\n\nUsage:\n nano-rspow-python [--help]\n\nWith no arguments, read one request per line and write one result per line.\nEmpty lines are ignored. The only accepted argument is --help.\n\nInput:\n <hash_hex>\n <hash_hex>:<threshold_hex>\n\nThe optional threshold must use a 0x prefix. Without one, the current\nepoch-2 send/change threshold is used.\n\nOutput:\n <hash_hex>:0x<threshold_hex>:<work_hex>\n";
289
+ const CLI_HELP: &str = "nano-rspow-python generates Nano proof of work from stdin.\n\nUsage:\n nano-rspow-python [--help]\n\nWith no arguments, read one request per line and write one result per line.\nEmpty lines are ignored. The only accepted argument is --help.\n\nInput:\n <hash_hex>\n <hash_hex>:<threshold_hex>\n\nThe optional threshold must use a 0x prefix. Without one, the current\nsend/change threshold is used.\n\nOutput:\n <hash_hex>:0x<threshold_hex>:<work_hex>\n";
276
290
 
277
291
  fn parse_cli_request(line: &str) -> Result<(String, [u8; 32], u64), String> {
278
292
  let parts: Vec<&str> = line.split(':').collect();
@@ -291,7 +305,7 @@ fn parse_cli_request(line: &str) -> Result<(String, [u8; 32], u64), String> {
291
305
  u64::from_str_radix(&threshold_text[2..], 16)
292
306
  .map_err(|error| format!("invalid threshold {threshold_text:?}: {error}"))?
293
307
  } else {
294
- thresholds::EPOCH2_SEND
308
+ thresholds::current::SEND
295
309
  };
296
310
 
297
311
  Ok((hash_text, hash, threshold))
@@ -361,21 +375,29 @@ fn cli_main(py: Python<'_>) -> PyResult<i32> {
361
375
  // Threshold constants submodule
362
376
  // ---------------------------------------------------------------------------
363
377
 
364
- /// Register the ``thresholds`` submodule with Nano PoW threshold constants.
365
- ///
366
- /// Constants are sourced from ``nano-rspow/src/thresholds.rs`` and match
367
- /// the values used by rsnano-node and the C++ nano-node.
378
+ /// Register grouped Nano PoW threshold preset submodules.
368
379
  fn register_thresholds(parent: &Bound<'_, PyModule>) -> PyResult<()> {
369
380
  let sub = PyModule::new(parent.py(), "thresholds")?;
370
- sub.add("EPOCH2_SEND", thresholds::EPOCH2_SEND)?;
371
- sub.add("EPOCH2_RECEIVE", thresholds::EPOCH2_RECEIVE)?;
372
- sub.add("LEGACY_EPOCH1", thresholds::LEGACY_EPOCH1)?;
373
- sub.add("EPOCH1", thresholds::LEGACY_EPOCH1)?;
374
- sub.add("BETA_LEGACY_EPOCH1", thresholds::BETA_LEGACY_EPOCH1)?;
375
- sub.add("BETA_EPOCH1", thresholds::BETA_LEGACY_EPOCH1)?;
376
- sub.add("DEV", thresholds::DEV)?;
377
- sub.add("BASE", thresholds::BASE)?;
381
+ let current = PyModule::new(parent.py(), "current")?;
382
+ current.add("SEND", thresholds::current::SEND)?;
383
+ current.add("RECEIVE", thresholds::current::RECEIVE)?;
384
+ let legacy = PyModule::new(parent.py(), "legacy")?;
385
+ legacy.add("EPOCH1", thresholds::legacy::EPOCH1)?;
386
+ legacy.add("BETA_EPOCH1", thresholds::legacy::BETA_EPOCH1)?;
387
+ let testing = PyModule::new(parent.py(), "testing")?;
388
+ testing.add("DEV", thresholds::testing::DEV)?;
389
+ sub.add_submodule(&current)?;
390
+ sub.add_submodule(&legacy)?;
391
+ sub.add_submodule(&testing)?;
378
392
  parent.add_submodule(&sub)?;
393
+
394
+ // Register each level so `from nano_rspow.thresholds import current` works
395
+ // in addition to attribute access on the extension module.
396
+ let modules = parent.py().import("sys")?.getattr("modules")?;
397
+ modules.set_item("nano_rspow.thresholds", &sub)?;
398
+ modules.set_item("nano_rspow.thresholds.current", &current)?;
399
+ modules.set_item("nano_rspow.thresholds.legacy", &legacy)?;
400
+ modules.set_item("nano_rspow.thresholds.testing", &testing)?;
379
401
  Ok(())
380
402
  }
381
403
 
@@ -394,6 +416,7 @@ fn nano_rspow(m: &Bound<'_, PyModule>) -> PyResult<()> {
394
416
  m.add_function(wrap_pyfunction!(generate_work, m)?)?;
395
417
  m.add_function(wrap_pyfunction!(generate_work_with_threshold, m)?)?;
396
418
  m.add_function(wrap_pyfunction!(validate_work, m)?)?;
419
+ m.add_function(wrap_pyfunction!(validate_work_with_threshold, m)?)?;
397
420
  m.add_function(wrap_pyfunction!(compute_difficulty, m)?)?;
398
421
  m.add_function(wrap_pyfunction!(backend_name, m)?)?;
399
422
  m.add_function(wrap_pyfunction!(cli_main, m)?)?;
@@ -7,6 +7,7 @@ Test vectors are sourced from the same data used in the Rust core library:
7
7
 
8
8
  import nano_rspow
9
9
  from nano_rspow import WorkType, WorkResult
10
+ from nano_rspow.thresholds import current, legacy, testing
10
11
 
11
12
 
12
13
  # ---------------------------------------------------------------------------
@@ -25,8 +26,8 @@ RSNANO_WORK = "3c82cc724905ee95"
25
26
  RSNANO_DIFFICULTY_INT = 18446743921403126366
26
27
 
27
28
  # Threshold constants from thresholds.rs
28
- EPOCH2_SEND = 0xfffffff800000000
29
- EPOCH2_RECEIVE = 0xFFFFFE0000000000
29
+ CURRENT_SEND = 0xfffffff800000000
30
+ CURRENT_RECEIVE = 0xFFFFFE0000000000
30
31
  LEGACY_EPOCH1 = 0xFFFFFFc000000000
31
32
  DEV = 0xFE00000000000000
32
33
 
@@ -75,18 +76,22 @@ class TestValidateWork:
75
76
 
76
77
  def test_known_valid_legacy_epoch1(self):
77
78
  """Known-good work from nano-work-server validates at legacy epoch-1 difficulty."""
78
- assert nano_rspow.validate_work(VECTOR_HASH, VECTOR_WORK, WorkType.LegacyEpoch1)
79
+ assert nano_rspow.validate_work_with_threshold(
80
+ VECTOR_HASH, VECTOR_WORK, f"{LEGACY_EPOCH1:016x}"
81
+ )
79
82
 
80
83
  def test_known_invalid(self):
81
84
  """Work = 0 should not meet any real threshold."""
82
- assert not nano_rspow.validate_work(
83
- VECTOR_HASH, "0000000000000000", WorkType.LegacyEpoch1
85
+ assert not nano_rspow.validate_work_with_threshold(
86
+ VECTOR_HASH, "0000000000000000", f"{LEGACY_EPOCH1:016x}"
84
87
  )
85
88
 
86
89
  def test_invalid_hash_hex_raises(self):
87
90
  """Non-hex hash should raise ValueError."""
88
91
  try:
89
- nano_rspow.validate_work("zzzz", VECTOR_WORK, WorkType.LegacyEpoch1)
92
+ nano_rspow.validate_work_with_threshold(
93
+ "zzzz", VECTOR_WORK, f"{LEGACY_EPOCH1:016x}"
94
+ )
90
95
  assert False, "Expected ValueError"
91
96
  except ValueError:
92
97
  pass
@@ -110,11 +115,12 @@ class TestGenerateWork:
110
115
  # Round-trip validation
111
116
  assert nano_rspow.validate_work(zero_hash, result.nonce_hex, WorkType.Receive)
112
117
 
113
- def test_generate_legacy_epoch1_known_hash(self):
114
- """Generate work for a known hash at legacy epoch-1 difficulty."""
115
- result = nano_rspow.generate_work(VECTOR_HASH, WorkType.LegacyEpoch1)
118
+ def test_generate_custom_threshold_roundtrip(self):
119
+ """Generate and validate work at a caller-supplied historical threshold."""
120
+ threshold = f"{LEGACY_EPOCH1:016x}"
121
+ result = nano_rspow.generate_work_with_threshold(VECTOR_HASH, threshold)
116
122
  assert result.is_valid
117
- assert nano_rspow.validate_work(VECTOR_HASH, result.nonce_hex, WorkType.LegacyEpoch1)
123
+ assert nano_rspow.validate_work_with_threshold(VECTOR_HASH, result.nonce_hex, threshold)
118
124
 
119
125
  def test_str_returns_nonce(self):
120
126
  """str(result) should return the nonce hex."""
@@ -136,8 +142,8 @@ class TestWorkType:
136
142
  """Enum integer values should be stable."""
137
143
  assert WorkType.Send == 0
138
144
  assert WorkType.Receive == 1
139
- assert WorkType.Epoch1 == 2
140
- assert WorkType.LegacyEpoch1 == 3
145
+ assert not hasattr(WorkType, "LegacyEpoch1")
146
+ assert not hasattr(WorkType, "Dev")
141
147
 
142
148
  def test_equality(self):
143
149
  """Enum equality should work."""
@@ -148,22 +154,21 @@ class TestWorkType:
148
154
  class TestThresholds:
149
155
  """Tests for the threshold constants submodule."""
150
156
 
151
- def test_epoch2_send(self):
152
- """EPOCH2_SEND must match the Rust constant."""
153
- assert nano_rspow.thresholds.EPOCH2_SEND == 0xfffffff800000000
157
+ def test_current_send(self):
158
+ assert nano_rspow.thresholds.current.SEND == CURRENT_SEND
159
+ assert current.SEND == CURRENT_SEND
160
+ assert not hasattr(nano_rspow.thresholds, "LEGACY_EPOCH1")
154
161
 
155
- def test_epoch2_receive(self):
156
- assert nano_rspow.thresholds.EPOCH2_RECEIVE == 0xFFFFFE0000000000
162
+ def test_current_receive(self):
163
+ assert nano_rspow.thresholds.current.RECEIVE == CURRENT_RECEIVE
157
164
 
158
165
  def test_legacy_epoch1(self):
159
- assert nano_rspow.thresholds.LEGACY_EPOCH1 == 0xFFFFFFc000000000
166
+ assert nano_rspow.thresholds.legacy.EPOCH1 == LEGACY_EPOCH1
167
+ assert legacy.EPOCH1 == LEGACY_EPOCH1
160
168
 
161
169
  def test_dev(self):
162
- assert nano_rspow.thresholds.DEV == 0xFE00000000000000
163
-
164
- def test_base_equals_epoch2_send(self):
165
- """BASE should equal EPOCH2_SEND (the hardest threshold)."""
166
- assert nano_rspow.thresholds.BASE == nano_rspow.thresholds.EPOCH2_SEND
170
+ assert nano_rspow.thresholds.testing.DEV == DEV
171
+ assert testing.DEV == DEV
167
172
 
168
173
 
169
174
  class TestBackendName:
@@ -1,79 +0,0 @@
1
- //! Nano PoW threshold constants for all network epochs.
2
- //!
3
- //! Source: rsnano-node `work/src/work_thresholds.rs` and the original
4
- //! C++ nano-node `nano/lib/work.cpp`.
5
-
6
- /// Epoch 2 send/change threshold (current live network default for sends).
7
- /// 8x harder than LEGACY_EPOCH1.
8
- pub const EPOCH2_SEND: u64 = 0xfffffff800000000;
9
-
10
- /// Epoch 2 receive threshold.
11
- /// 8x easier than LEGACY_EPOCH1.
12
- pub const EPOCH2_RECEIVE: u64 = 0xfffffe0000000000;
13
-
14
- /// Legacy epoch 1 threshold.
15
- ///
16
- /// This is retained for historical block validation and compatibility only.
17
- /// It is not the threshold required for current mainnet epoch blocks.
18
- pub const LEGACY_EPOCH1: u64 = 0xffffffc000000000;
19
-
20
- /// Deprecated alias for [`LEGACY_EPOCH1`].
21
- #[deprecated(note = "use LEGACY_EPOCH1; this threshold is legacy-only")]
22
- pub const EPOCH1: u64 = LEGACY_EPOCH1;
23
-
24
- /// Beta network epoch 1 threshold (64x lower than live epoch 1).
25
- pub const BETA_LEGACY_EPOCH1: u64 = 0xfffff00000000000;
26
-
27
- /// Deprecated alias for [`BETA_LEGACY_EPOCH1`].
28
- #[deprecated(note = "use BETA_LEGACY_EPOCH1; this threshold is legacy-only")]
29
- pub const BETA_EPOCH1: u64 = BETA_LEGACY_EPOCH1;
30
-
31
- /// Dev network threshold (very low, for testing).
32
- pub const DEV: u64 = 0xfe00000000000000;
33
-
34
- /// The highest threshold across all epochs — used as the base for multiplier
35
- /// calculations.
36
- pub const BASE: u64 = EPOCH2_SEND;
37
-
38
- /// Compute a difficulty multiplier relative to a base threshold.
39
- ///
40
- /// A multiplier of 1.0 means exactly at base difficulty. >1.0 means harder.
41
- pub fn to_multiplier(difficulty: u64, base: u64) -> f64 {
42
- debug_assert!(base > 0);
43
- let max = u64::MAX as f64;
44
- (max - base as f64) / (max - difficulty as f64)
45
- }
46
-
47
- /// Compute the difficulty from a multiplier and base threshold.
48
- pub fn from_multiplier(multiplier: f64, base: u64) -> u64 {
49
- debug_assert!(multiplier >= 1.0);
50
- let max = u64::MAX as f64;
51
- u64::MAX - ((max - base as f64) / multiplier) as u64
52
- }
53
-
54
- #[cfg(test)]
55
- mod tests {
56
- use super::*;
57
-
58
- #[test]
59
- fn multiplier_epoch2_vs_legacy_epoch1() {
60
- // rsnano confirms live epoch_2 is 8x epoch_1
61
- let m = to_multiplier(EPOCH2_SEND, LEGACY_EPOCH1);
62
- assert!((m - 8.0).abs() < 0.01, "expected 8.0, got {m}");
63
- }
64
-
65
- #[test]
66
- fn multiplier_epoch2_receive_vs_legacy_epoch1() {
67
- // epoch_2_receive is 1/8 of epoch_1
68
- let m = to_multiplier(EPOCH2_RECEIVE, LEGACY_EPOCH1);
69
- assert!((m - 0.125).abs() < 0.001, "expected 0.125, got {m}");
70
- }
71
-
72
- #[test]
73
- fn roundtrip_multiplier() {
74
- let m = 2.5_f64;
75
- let d = from_multiplier(m, LEGACY_EPOCH1);
76
- let m2 = to_multiplier(d, LEGACY_EPOCH1);
77
- assert!((m - m2).abs() < 0.001);
78
- }
79
- }