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.
- {nano_rspow_python-0.16.0 → nano_rspow_python-0.17.0}/Cargo.lock +6 -6
- {nano_rspow_python-0.16.0 → nano_rspow_python-0.17.0}/Cargo.toml +1 -1
- {nano_rspow_python-0.16.0 → nano_rspow_python-0.17.0}/PKG-INFO +29 -6
- {nano_rspow_python-0.16.0/nano-rspow-python → nano_rspow_python-0.17.0}/README.md +28 -5
- {nano_rspow_python-0.16.0 → nano_rspow_python-0.17.0}/nano-rspow/README.md +10 -0
- {nano_rspow_python-0.16.0 → nano_rspow_python-0.17.0}/nano-rspow/benches/pow_benchmark.rs +11 -11
- {nano_rspow_python-0.16.0 → nano_rspow_python-0.17.0}/nano-rspow/src/cpu.rs +13 -13
- {nano_rspow_python-0.16.0 → nano_rspow_python-0.17.0}/nano-rspow/src/difficulty.rs +4 -4
- {nano_rspow_python-0.16.0 → nano_rspow_python-0.17.0}/nano-rspow/src/lib.rs +4 -4
- {nano_rspow_python-0.16.0 → nano_rspow_python-0.17.0}/nano-rspow/src/opencl_backend/mod.rs +5 -3
- nano_rspow_python-0.17.0/nano-rspow/src/thresholds.rs +75 -0
- {nano_rspow_python-0.16.0 → nano_rspow_python-0.17.0}/nano-rspow/src/types.rs +3 -3
- {nano_rspow_python-0.16.0 → nano_rspow_python-0.17.0}/nano-rspow/src/wgpu_backend/mod.rs +4 -4
- {nano_rspow_python-0.16.0 → nano_rspow_python-0.17.0/nano-rspow-python}/README.md +28 -5
- {nano_rspow_python-0.16.0 → nano_rspow_python-0.17.0}/nano-rspow-python/nano_rspow.pyi +18 -32
- {nano_rspow_python-0.16.0 → nano_rspow_python-0.17.0}/nano-rspow-python/src/lib.rs +55 -32
- {nano_rspow_python-0.16.0 → nano_rspow_python-0.17.0}/nano-rspow-python/tests/test_pow.py +28 -23
- nano_rspow_python-0.16.0/nano-rspow/src/thresholds.rs +0 -79
- {nano_rspow_python-0.16.0 → nano_rspow_python-0.17.0}/nano-rspow/Cargo.toml +0 -0
- {nano_rspow_python-0.16.0 → nano_rspow_python-0.17.0}/nano-rspow/src/opencl_backend/pow.cl +0 -0
- {nano_rspow_python-0.16.0 → nano_rspow_python-0.17.0}/nano-rspow/src/wgpu_backend/pow.wgsl +0 -0
- {nano_rspow_python-0.16.0 → nano_rspow_python-0.17.0}/nano-rspow/src/wgpu_shared.rs +0 -0
- {nano_rspow_python-0.16.0 → nano_rspow_python-0.17.0}/nano-rspow-python/Cargo.toml +0 -0
- {nano_rspow_python-0.16.0 → nano_rspow_python-0.17.0}/nano-rspow-python/build.rs +0 -0
- {nano_rspow_python-0.16.0 → nano_rspow_python-0.17.0}/nano-rspow-python/uv.lock +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
816
|
+
version = "0.17.0"
|
|
817
817
|
dependencies = [
|
|
818
818
|
"bytemuck",
|
|
819
819
|
"getrandom",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nano-rspow-python
|
|
3
|
-
Version: 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` |
|
|
85
|
-
| `Receive` |
|
|
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
|
|
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` |
|
|
61
|
-
| `Receive` |
|
|
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
|
|
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
|
-
//!
|
|
7
|
+
//! receive 0xfffffe0000000000 (current receive/open/epoch)
|
|
8
8
|
//! legacy_epoch1 0xffffffc000000000 (legacy-only)
|
|
9
|
-
//!
|
|
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
|
-
//! `
|
|
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
|
|
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("
|
|
71
|
+
group.bench_function("receive", |b| {
|
|
72
72
|
b.iter(|| {
|
|
73
73
|
generator
|
|
74
|
-
.generate(&BENCH_HASH, thresholds::
|
|
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::
|
|
91
|
+
b.iter(|| generator.generate(&BENCH_HASH, thresholds::legacy::EPOCH1).unwrap());
|
|
92
92
|
});
|
|
93
93
|
|
|
94
|
-
group.bench_function("
|
|
94
|
+
group.bench_function("send", |b| {
|
|
95
95
|
b.iter(|| {
|
|
96
96
|
generator
|
|
97
|
-
.generate(&BENCH_HASH, thresholds::
|
|
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::
|
|
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::
|
|
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::
|
|
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::
|
|
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
|
|
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(
|
|
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 (
|
|
87
|
-
|
|
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
|
|
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::
|
|
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::
|
|
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 (
|
|
439
|
-
|
|
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 (
|
|
442
|
-
|
|
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` |
|
|
61
|
-
| `Receive` |
|
|
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
|
|
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:
|
|
13
|
-
Receive:
|
|
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
|
-
"""
|
|
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
|
-
|
|
135
|
-
|
|
115
|
+
class current:
|
|
116
|
+
"""Current Nano mainnet threshold presets."""
|
|
117
|
+
SEND: int
|
|
118
|
+
RECEIVE: int
|
|
136
119
|
|
|
137
|
-
|
|
138
|
-
|
|
120
|
+
class legacy:
|
|
121
|
+
"""Historical Epoch 1 threshold presets."""
|
|
122
|
+
EPOCH1: int
|
|
123
|
+
BETA_EPOCH1: int
|
|
139
124
|
|
|
140
|
-
|
|
141
|
-
|
|
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
|
|
21
|
-
//! valid = nano_rspow.
|
|
20
|
+
//! # Validate historical work with an explicit threshold
|
|
21
|
+
//! valid = nano_rspow.validate_work_with_threshold(
|
|
22
22
|
//! "718CC2121C3E641059BC1C2CFC45666C99E8AE922F7A807B7D07B62C995D79E2",
|
|
23
23
|
//! "2bf29ef00786a6bc",
|
|
24
|
-
//!
|
|
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
|
|
53
|
-
/// - `Send` →
|
|
54
|
-
/// - `Receive` → epoch
|
|
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::
|
|
70
|
-
WorkType::Receive => thresholds::
|
|
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 =
|
|
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\
|
|
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::
|
|
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
|
|
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
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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(¤t)?;
|
|
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", ¤t)?;
|
|
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
|
-
|
|
29
|
-
|
|
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.
|
|
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.
|
|
83
|
-
VECTOR_HASH, "0000000000000000",
|
|
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.
|
|
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
|
|
114
|
-
"""Generate
|
|
115
|
-
|
|
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.
|
|
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
|
|
140
|
-
assert WorkType
|
|
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
|
|
152
|
-
|
|
153
|
-
assert
|
|
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
|
|
156
|
-
assert nano_rspow.thresholds.
|
|
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.
|
|
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 ==
|
|
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
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|