nano-rspow-python 0.8.0__tar.gz → 0.10.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.8.0 → nano_rspow_python-0.10.0}/Cargo.lock +5 -5
- {nano_rspow_python-0.8.0 → nano_rspow_python-0.10.0}/Cargo.toml +1 -1
- {nano_rspow_python-0.8.0 → nano_rspow_python-0.10.0}/PKG-INFO +1 -1
- {nano_rspow_python-0.8.0 → nano_rspow_python-0.10.0}/nano-rspow/README.md +14 -11
- nano_rspow_python-0.10.0/nano-rspow/src/cpu.rs +376 -0
- nano_rspow_python-0.8.0/nano-rspow/src/cpu.rs +0 -196
- {nano_rspow_python-0.8.0 → nano_rspow_python-0.10.0}/nano-rspow/Cargo.toml +0 -0
- {nano_rspow_python-0.8.0 → nano_rspow_python-0.10.0}/nano-rspow/benches/pow_benchmark.rs +0 -0
- {nano_rspow_python-0.8.0 → nano_rspow_python-0.10.0}/nano-rspow/src/difficulty.rs +0 -0
- {nano_rspow_python-0.8.0 → nano_rspow_python-0.10.0}/nano-rspow/src/lib.rs +0 -0
- {nano_rspow_python-0.8.0 → nano_rspow_python-0.10.0}/nano-rspow/src/opencl_backend/mod.rs +0 -0
- {nano_rspow_python-0.8.0 → nano_rspow_python-0.10.0}/nano-rspow/src/opencl_backend/pow.cl +0 -0
- {nano_rspow_python-0.8.0 → nano_rspow_python-0.10.0}/nano-rspow/src/thresholds.rs +0 -0
- {nano_rspow_python-0.8.0 → nano_rspow_python-0.10.0}/nano-rspow/src/types.rs +0 -0
- {nano_rspow_python-0.8.0 → nano_rspow_python-0.10.0}/nano-rspow/src/wgpu_backend/mod.rs +0 -0
- {nano_rspow_python-0.8.0 → nano_rspow_python-0.10.0}/nano-rspow/src/wgpu_backend/pow.wgsl +0 -0
- {nano_rspow_python-0.8.0 → nano_rspow_python-0.10.0}/nano-rspow/src/wgpu_shared.rs +0 -0
- {nano_rspow_python-0.8.0 → nano_rspow_python-0.10.0}/nano-rspow-python/Cargo.toml +0 -0
- {nano_rspow_python-0.8.0 → nano_rspow_python-0.10.0}/nano-rspow-python/build.rs +0 -0
- {nano_rspow_python-0.8.0 → nano_rspow_python-0.10.0}/nano-rspow-python/nano_rspow.pyi +0 -0
- {nano_rspow_python-0.8.0 → nano_rspow_python-0.10.0}/nano-rspow-python/src/lib.rs +0 -0
- {nano_rspow_python-0.8.0 → nano_rspow_python-0.10.0}/nano-rspow-python/tests/test_pow.py +0 -0
- {nano_rspow_python-0.8.0 → nano_rspow_python-0.10.0}/pyproject.toml +0 -0
|
@@ -739,7 +739,7 @@ dependencies = [
|
|
|
739
739
|
|
|
740
740
|
[[package]]
|
|
741
741
|
name = "nano-rspow"
|
|
742
|
-
version = "0.
|
|
742
|
+
version = "0.10.0"
|
|
743
743
|
dependencies = [
|
|
744
744
|
"blake2b_simd",
|
|
745
745
|
"bytemuck",
|
|
@@ -755,7 +755,7 @@ dependencies = [
|
|
|
755
755
|
|
|
756
756
|
[[package]]
|
|
757
757
|
name = "nano-rspow-cli"
|
|
758
|
-
version = "0.
|
|
758
|
+
version = "0.10.0"
|
|
759
759
|
dependencies = [
|
|
760
760
|
"clap",
|
|
761
761
|
"hex",
|
|
@@ -766,7 +766,7 @@ dependencies = [
|
|
|
766
766
|
|
|
767
767
|
[[package]]
|
|
768
768
|
name = "nano-rspow-node"
|
|
769
|
-
version = "0.
|
|
769
|
+
version = "0.10.0"
|
|
770
770
|
dependencies = [
|
|
771
771
|
"hex",
|
|
772
772
|
"nano-rspow",
|
|
@@ -778,7 +778,7 @@ dependencies = [
|
|
|
778
778
|
|
|
779
779
|
[[package]]
|
|
780
780
|
name = "nano-rspow-python"
|
|
781
|
-
version = "0.
|
|
781
|
+
version = "0.10.0"
|
|
782
782
|
dependencies = [
|
|
783
783
|
"hex",
|
|
784
784
|
"nano-rspow",
|
|
@@ -787,7 +787,7 @@ dependencies = [
|
|
|
787
787
|
|
|
788
788
|
[[package]]
|
|
789
789
|
name = "nano-rspow-web"
|
|
790
|
-
version = "0.
|
|
790
|
+
version = "0.10.0"
|
|
791
791
|
dependencies = [
|
|
792
792
|
"bytemuck",
|
|
793
793
|
"getrandom",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# nano-rspow
|
|
2
2
|
|
|
3
|
-
nano-rspow is a fast, zero-configuration hybrid CPU/GPU proof-of-work generator
|
|
3
|
+
nano-rspow is a fast, zero-configuration hybrid CPU/GPU proof-of-work generator for [Nano](https://www.nano.org).
|
|
4
4
|
|
|
5
5
|
Developed for high-throughput node operations, client-side web integrations, and native app developers, this repository packages an optimized Blake2b hashing engine under a transparent, auto-detecting **hybrid race architecture**. It seamlessly runs multi-threaded CPU solvers (powered by `rayon` and Web Workers) or GPU pipelines (`wgpu`, `OpenCL`, and WebGPU) depending on target hardware availability. No boilerplate, no device selection headaches—just instant, maximum-performance PoW generation everywhere.
|
|
6
6
|
|
|
@@ -8,7 +8,7 @@ Developed for high-throughput node operations, client-side web integrations, and
|
|
|
8
8
|
|
|
9
9
|
## 🎯 Who is this for?
|
|
10
10
|
|
|
11
|
-
* **Exchange & Wallet Integrators** wanting low-latency, high-volume block generation
|
|
11
|
+
* **Exchange & Wallet Integrators** wanting low-latency, high-volume block generation in-process / without the complexity of adding a another external service.
|
|
12
12
|
* **Server-side Developers** using Node.js or Python who need native bindings running at C-level execution speed.
|
|
13
13
|
* **Frontend Web Developers** building sleek wallets or dApps requiring non-blocking WASM and hardware-accelerated WebGPU directly in user browsers.
|
|
14
14
|
* **Power Users & Node Operators** looking for an ultra-fast benchmarking tool to tune threshold multipliers.
|
|
@@ -18,21 +18,24 @@ Developed for high-throughput node operations, client-side web integrations, and
|
|
|
18
18
|
## ⚡ Quick Start
|
|
19
19
|
|
|
20
20
|
### 1. Standalone CLI
|
|
21
|
-
|
|
21
|
+
Install from crates.io (requires a Rust toolchain):
|
|
22
|
+
```bash
|
|
23
|
+
cargo install nano-rspow-cli
|
|
24
|
+
nano-rspow-cli benchmark --count 10
|
|
25
|
+
```
|
|
26
|
+
Alternatively, build from this repository:
|
|
22
27
|
```bash
|
|
23
|
-
# Build binary in release mode (output at target/release/nano-rspow-cli)
|
|
24
28
|
cargo build -p nano-rspow-cli --release
|
|
25
|
-
|
|
26
|
-
# Run the compiled binary
|
|
27
29
|
./target/release/nano-rspow-cli benchmark --count 10
|
|
28
30
|
```
|
|
29
|
-
|
|
31
|
+
Or use `cargo run -p nano-rspow-cli -- <args>` to build and execute in one step.
|
|
30
32
|
|
|
31
|
-
### 2. Node.js
|
|
32
|
-
|
|
33
|
+
### 2. Node.js (Programmatic API)
|
|
34
|
+
Use the native bindings for in-process PoW generation:
|
|
33
35
|
```bash
|
|
34
|
-
|
|
36
|
+
npm install nano-rspow-node
|
|
35
37
|
```
|
|
38
|
+
See [nano-rspow-node/README.md](nano-rspow-node/README.md) for API usage.
|
|
36
39
|
|
|
37
40
|
### 3. Interactive Web Dashboard
|
|
38
41
|
Build and open the self-contained HTML5 benchmarking tool with real-time performance stats, non-blocking Web Worker fallback, WebGPU execution, and customizable cellular-signal difficulty thresholds:
|
|
@@ -52,7 +55,7 @@ Below is the directory mapping for each target, along with their primary release
|
|
|
52
55
|
| **Node.js & TS** | [nano-rspow-node/README.md](nano-rspow-node/README.md) | [Public NPM package](https://www.npmjs.com/package/nano-rspow-node) |
|
|
53
56
|
| **Python** | [nano-rspow-python/](nano-rspow-python/) | [PyPI (pip)](https://pypi.org/project/nano-rspow-python/) |
|
|
54
57
|
| **Web (WASM / WebGPU)** | [nano-rspow-web/README.md](nano-rspow-web/README.md) | [Public NPM package](https://www.npmjs.com/package/nano-rspow-web) + [🎛️ Demo](https://csi.ninzin.net/nano-rspow/) |
|
|
55
|
-
| **CLI Tool** | [nano-rspow-cli/](nano-rspow-cli/) | [GitHub Releases](https://github.com/CasualSecurityInc/nano-rspow/releases) |
|
|
58
|
+
| **CLI Tool** | [nano-rspow-cli/](nano-rspow-cli/) | [crates.io](https://crates.io/crates/nano-rspow-cli) · [GitHub Releases](https://github.com/CasualSecurityInc/nano-rspow/releases) |
|
|
56
59
|
|
|
57
60
|
---
|
|
58
61
|
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
//! CPU backend: multi-threaded Blake2b PoW generation.
|
|
2
|
+
//!
|
|
3
|
+
//! Uses pre-spun native threads with per-thread mpsc command channels.
|
|
4
|
+
//! Each worker thread independently searches random nonces using
|
|
5
|
+
//! XorShift1024* — the same algorithm as the reference Nano node.
|
|
6
|
+
|
|
7
|
+
use std::sync::{Arc, atomic::Ordering};
|
|
8
|
+
|
|
9
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
10
|
+
use std::sync::atomic::AtomicBool;
|
|
11
|
+
|
|
12
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
13
|
+
use std::sync::mpsc::{self, RecvTimeoutError};
|
|
14
|
+
|
|
15
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
16
|
+
use std::thread::{self, JoinHandle};
|
|
17
|
+
|
|
18
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
19
|
+
use std::time::Duration;
|
|
20
|
+
|
|
21
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
22
|
+
use rand::Rng;
|
|
23
|
+
|
|
24
|
+
use crate::{Backend, CancelToken, GeneratorDiagnostics, difficulty};
|
|
25
|
+
|
|
26
|
+
/// XorShift1024* PRNG — same algorithm as the reference Nano node's `XorShift1024Star`.
|
|
27
|
+
struct XorShift1024Star {
|
|
28
|
+
state: [u64; 16],
|
|
29
|
+
p: usize,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
impl XorShift1024Star {
|
|
33
|
+
fn new(seed: u64) -> Self {
|
|
34
|
+
// Splitmix64 to populate state from a single seed
|
|
35
|
+
let mut s = seed;
|
|
36
|
+
let mut state = [0u64; 16];
|
|
37
|
+
for x in state.iter_mut() {
|
|
38
|
+
s = s.wrapping_add(0x9e3779b97f4a7c15);
|
|
39
|
+
let mut z = s;
|
|
40
|
+
z = (z ^ (z >> 30)).wrapping_mul(0xbf58476d1ce4e5b9);
|
|
41
|
+
z = (z ^ (z >> 27)).wrapping_mul(0x94d049bb133111eb);
|
|
42
|
+
*x = z ^ (z >> 31);
|
|
43
|
+
}
|
|
44
|
+
Self { state, p: 0 }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
48
|
+
fn new_from_system() -> Self {
|
|
49
|
+
Self::new(rand::rng().random())
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
#[inline]
|
|
53
|
+
fn next(&mut self) -> u64 {
|
|
54
|
+
let s0 = self.state[self.p];
|
|
55
|
+
self.p = (self.p + 1) & 15;
|
|
56
|
+
let mut s1 = self.state[self.p];
|
|
57
|
+
s1 ^= s1 << 31;
|
|
58
|
+
self.state[self.p] = s1 ^ s0 ^ (s1 >> 11) ^ (s0 >> 30);
|
|
59
|
+
self.state[self.p].wrapping_mul(1181783497276652981)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
64
|
+
enum WorkCommand {
|
|
65
|
+
Search {
|
|
66
|
+
hash: [u8; 32],
|
|
67
|
+
threshold: u64,
|
|
68
|
+
done: Arc<AtomicBool>,
|
|
69
|
+
cancel: Arc<AtomicBool>,
|
|
70
|
+
result_tx: mpsc::Sender<u64>,
|
|
71
|
+
},
|
|
72
|
+
Stop,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
76
|
+
pub(crate) struct CpuBackend {
|
|
77
|
+
threads: Vec<JoinHandle<()>>,
|
|
78
|
+
senders: Vec<mpsc::Sender<WorkCommand>>,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
#[cfg(target_arch = "wasm32")]
|
|
82
|
+
pub(crate) struct CpuBackend;
|
|
83
|
+
|
|
84
|
+
impl CpuBackend {
|
|
85
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
86
|
+
pub fn new() -> Self {
|
|
87
|
+
let thread_count = std::thread::available_parallelism()
|
|
88
|
+
.map(|n| n.get())
|
|
89
|
+
.unwrap_or(4);
|
|
90
|
+
|
|
91
|
+
let mut threads = Vec::new();
|
|
92
|
+
let mut senders = Vec::new();
|
|
93
|
+
|
|
94
|
+
for _ in 0..thread_count {
|
|
95
|
+
let (cmd_tx, cmd_rx) = mpsc::channel::<WorkCommand>();
|
|
96
|
+
senders.push(cmd_tx);
|
|
97
|
+
|
|
98
|
+
threads.push(thread::spawn(move || {
|
|
99
|
+
let mut rng = XorShift1024Star::new_from_system();
|
|
100
|
+
|
|
101
|
+
loop {
|
|
102
|
+
let cmd = match cmd_rx.recv() {
|
|
103
|
+
Ok(cmd) => cmd,
|
|
104
|
+
Err(_) => return,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
match cmd {
|
|
108
|
+
WorkCommand::Stop => return,
|
|
109
|
+
WorkCommand::Search { hash, threshold, done, cancel, result_tx } => {
|
|
110
|
+
const BATCH: usize = 256;
|
|
111
|
+
|
|
112
|
+
loop {
|
|
113
|
+
if done.load(Ordering::Relaxed) || cancel.load(Ordering::Relaxed) {
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for _ in 0..BATCH {
|
|
118
|
+
let nonce = rng.next();
|
|
119
|
+
if difficulty::compute(&hash, nonce) >= threshold {
|
|
120
|
+
if !done.swap(true, Ordering::AcqRel) {
|
|
121
|
+
let _ = result_tx.send(nonce);
|
|
122
|
+
}
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
Self { threads, senders }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
#[cfg(target_arch = "wasm32")]
|
|
137
|
+
pub fn new() -> Self {
|
|
138
|
+
Self
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
143
|
+
impl Drop for CpuBackend {
|
|
144
|
+
fn drop(&mut self) {
|
|
145
|
+
for sender in &self.senders {
|
|
146
|
+
let _ = sender.send(WorkCommand::Stop);
|
|
147
|
+
}
|
|
148
|
+
for thread in self.threads.drain(..) {
|
|
149
|
+
let _ = thread.join();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
impl Backend for CpuBackend {
|
|
155
|
+
fn name(&self) -> &'static str {
|
|
156
|
+
"cpu"
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
160
|
+
fn generate(&self, hash: &[u8; 32], threshold: u64, cancel: &CancelToken) -> Option<u64> {
|
|
161
|
+
let done = Arc::new(AtomicBool::new(false));
|
|
162
|
+
let (result_tx, result_rx) = mpsc::channel();
|
|
163
|
+
|
|
164
|
+
for sender in &self.senders {
|
|
165
|
+
let _ = sender.send(WorkCommand::Search {
|
|
166
|
+
hash: *hash,
|
|
167
|
+
threshold,
|
|
168
|
+
done: Arc::clone(&done),
|
|
169
|
+
cancel: Arc::clone(&cancel.flag),
|
|
170
|
+
result_tx: result_tx.clone(),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
drop(result_tx);
|
|
174
|
+
|
|
175
|
+
loop {
|
|
176
|
+
match result_rx.recv_timeout(Duration::from_millis(10)) {
|
|
177
|
+
Ok(nonce) => return Some(nonce),
|
|
178
|
+
Err(RecvTimeoutError::Timeout) => {
|
|
179
|
+
if cancel.is_cancelled() {
|
|
180
|
+
done.store(true, Ordering::Relaxed);
|
|
181
|
+
return None;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
Err(RecvTimeoutError::Disconnected) => return None,
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
#[cfg(target_arch = "wasm32")]
|
|
190
|
+
fn generate(&self, hash: &[u8; 32], threshold: u64, cancel: &CancelToken) -> Option<u64> {
|
|
191
|
+
let cancelled = Arc::clone(&cancel.flag);
|
|
192
|
+
let hash = *hash;
|
|
193
|
+
|
|
194
|
+
let mut rng = XorShift1024Star::new(rand::random());
|
|
195
|
+
const BATCH: usize = 256;
|
|
196
|
+
|
|
197
|
+
while !cancelled.load(Ordering::Relaxed) {
|
|
198
|
+
for _ in 0..BATCH {
|
|
199
|
+
let nonce = rng.next();
|
|
200
|
+
if difficulty::compute(&hash, nonce) >= threshold {
|
|
201
|
+
return Some(nonce);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
None
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
fn diagnostics(&self) -> GeneratorDiagnostics {
|
|
209
|
+
GeneratorDiagnostics {
|
|
210
|
+
backend: "cpu".to_string(),
|
|
211
|
+
gpu: None,
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
#[cfg(test)]
|
|
217
|
+
mod tests {
|
|
218
|
+
use super::*;
|
|
219
|
+
use crate::thresholds;
|
|
220
|
+
|
|
221
|
+
#[test]
|
|
222
|
+
fn xorshift_is_deterministic() {
|
|
223
|
+
let mut a = XorShift1024Star::new(42);
|
|
224
|
+
let mut b = XorShift1024Star::new(42);
|
|
225
|
+
for _ in 0..1000 {
|
|
226
|
+
assert_eq!(a.next(), b.next());
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
#[test]
|
|
231
|
+
fn xorshift_distinct_seeds_differ() {
|
|
232
|
+
let mut a = XorShift1024Star::new(1);
|
|
233
|
+
let mut b = XorShift1024Star::new(2);
|
|
234
|
+
// Should produce different sequences
|
|
235
|
+
let va: Vec<u64> = (0..10).map(|_| a.next()).collect();
|
|
236
|
+
let vb: Vec<u64> = (0..10).map(|_| b.next()).collect();
|
|
237
|
+
assert_ne!(va, vb);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
#[test]
|
|
241
|
+
fn cpu_generates_valid_work_dev_threshold() {
|
|
242
|
+
let hash = [0u8; 32];
|
|
243
|
+
let backend = CpuBackend::new();
|
|
244
|
+
let cancel = CancelToken::new();
|
|
245
|
+
let nonce = backend.generate(&hash, thresholds::DEV, &cancel).unwrap();
|
|
246
|
+
let diff = difficulty::compute(&hash, nonce);
|
|
247
|
+
assert!(
|
|
248
|
+
diff >= thresholds::DEV,
|
|
249
|
+
"nonce {nonce:#018x} produced difficulty {diff:#018x} < threshold {:#018x}",
|
|
250
|
+
thresholds::DEV
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
#[test]
|
|
255
|
+
fn cpu_respects_cancellation() {
|
|
256
|
+
let hash = [0u8; 32];
|
|
257
|
+
let backend = CpuBackend::new();
|
|
258
|
+
let cancel = CancelToken::new();
|
|
259
|
+
// Cancel immediately — should return None
|
|
260
|
+
cancel.cancel();
|
|
261
|
+
let result = backend.generate(&hash, u64::MAX, &cancel);
|
|
262
|
+
assert!(result.is_none());
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/// Round-trip: generate then validate using the same known vectors
|
|
266
|
+
/// from difficulty.rs.
|
|
267
|
+
#[test]
|
|
268
|
+
fn roundtrip_work_validate() {
|
|
269
|
+
use crate::difficulty;
|
|
270
|
+
|
|
271
|
+
// Official known-good test vector hash from the nano-node core implementation
|
|
272
|
+
let hash = hex::decode("718CC2121C3E641059BC1C2CFC45666C99E8AE922F7A807B7D07B62C995D79E2")
|
|
273
|
+
.unwrap();
|
|
274
|
+
let hash: [u8; 32] = hash.try_into().unwrap();
|
|
275
|
+
|
|
276
|
+
let backend = CpuBackend::new();
|
|
277
|
+
let cancel = CancelToken::new();
|
|
278
|
+
let nonce = backend.generate(&hash, thresholds::DEV, &cancel).unwrap();
|
|
279
|
+
let diff = difficulty::compute(&hash, nonce);
|
|
280
|
+
assert!(diff >= thresholds::DEV);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
#[test]
|
|
284
|
+
fn cpu_reuses_threads_across_generate_calls() {
|
|
285
|
+
let hash = [0u8; 32];
|
|
286
|
+
let backend = CpuBackend::new();
|
|
287
|
+
let cancel = CancelToken::new();
|
|
288
|
+
|
|
289
|
+
// Multiple calls on the same backend should all produce valid work
|
|
290
|
+
for _ in 0..5 {
|
|
291
|
+
let nonce = backend.generate(&hash, thresholds::DEV, &cancel).unwrap();
|
|
292
|
+
let diff = difficulty::compute(&hash, nonce);
|
|
293
|
+
assert!(
|
|
294
|
+
diff >= thresholds::DEV,
|
|
295
|
+
"nonce {nonce:#018x} produced difficulty {diff:#018x} < threshold {:#018x}",
|
|
296
|
+
thresholds::DEV
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
#[test]
|
|
302
|
+
fn cpu_concurrent_generates_produce_unique_work() {
|
|
303
|
+
use std::sync::Arc;
|
|
304
|
+
use std::thread;
|
|
305
|
+
|
|
306
|
+
let backend = Arc::new(CpuBackend::new());
|
|
307
|
+
let hash = [0u8; 32];
|
|
308
|
+
let cancel = CancelToken::new();
|
|
309
|
+
|
|
310
|
+
// Spawn several threads that all call generate() concurrently
|
|
311
|
+
let handles: Vec<_> = (0..4)
|
|
312
|
+
.map(|_| {
|
|
313
|
+
let backend = Arc::clone(&backend);
|
|
314
|
+
let cancel = cancel.clone();
|
|
315
|
+
thread::spawn(move || {
|
|
316
|
+
backend.generate(&hash, thresholds::DEV, &cancel)
|
|
317
|
+
})
|
|
318
|
+
})
|
|
319
|
+
.collect();
|
|
320
|
+
|
|
321
|
+
let mut nonces = Vec::new();
|
|
322
|
+
for h in handles {
|
|
323
|
+
if let Some(nonce) = h.join().unwrap() {
|
|
324
|
+
nonces.push(nonce);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// All nonces should be valid
|
|
329
|
+
for &nonce in &nonces {
|
|
330
|
+
let diff = difficulty::compute(&hash, nonce);
|
|
331
|
+
assert!(diff >= thresholds::DEV);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/// Verifies that drop() cleanly joins threads even when workers were
|
|
336
|
+
/// mid-search. We spawn generate on a thread, cancel after a short delay,
|
|
337
|
+
/// drop the backend, and verify the thread returns cleanly.
|
|
338
|
+
#[test]
|
|
339
|
+
fn cpu_drop_after_generate_does_not_panic() {
|
|
340
|
+
use std::thread;
|
|
341
|
+
|
|
342
|
+
let hash = [0u8; 32];
|
|
343
|
+
let backend = CpuBackend::new();
|
|
344
|
+
let cancel = CancelToken::new();
|
|
345
|
+
|
|
346
|
+
let cancel_clone = cancel.clone();
|
|
347
|
+
let handle = thread::spawn(move || {
|
|
348
|
+
backend.generate(&hash, u64::MAX, &cancel_clone)
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
thread::sleep(std::time::Duration::from_millis(100));
|
|
352
|
+
cancel.cancel();
|
|
353
|
+
|
|
354
|
+
// Thread should return None or Some (shouldn't hang)
|
|
355
|
+
let result = handle.join().unwrap();
|
|
356
|
+
assert!(result.is_none());
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/// Each worker thread uses system-entropy RNG seeds, so two threads
|
|
360
|
+
/// searching the same hash should produce different nonce sequences
|
|
361
|
+
/// and (almost certainly) different winning nonces.
|
|
362
|
+
#[test]
|
|
363
|
+
fn cpu_workers_produce_different_nonces() {
|
|
364
|
+
let hash = [0u8; 32];
|
|
365
|
+
let backend = CpuBackend::new();
|
|
366
|
+
let cancel = CancelToken::new();
|
|
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();
|
|
371
|
+
|
|
372
|
+
// Three nonces from a seeded RNG with 2^64 space should all be distinct.
|
|
373
|
+
assert_ne!(nonce1, nonce2, "same nonce on consecutive generate calls");
|
|
374
|
+
assert_ne!(nonce2, nonce3, "same nonce on consecutive generate calls");
|
|
375
|
+
}
|
|
376
|
+
}
|
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
//! CPU backend: multi-threaded Blake2b PoW generation.
|
|
2
|
-
//!
|
|
3
|
-
//! Uses rayon for parallelism. Each worker thread independently searches
|
|
4
|
-
//! random nonces using XorShift1024* (same RNG as rsnano-node).
|
|
5
|
-
|
|
6
|
-
use std::sync::{Arc, atomic::Ordering};
|
|
7
|
-
|
|
8
|
-
#[cfg(not(target_arch = "wasm32"))]
|
|
9
|
-
use std::sync::atomic::{AtomicBool, AtomicU64};
|
|
10
|
-
|
|
11
|
-
#[cfg(not(target_arch = "wasm32"))]
|
|
12
|
-
use rayon::prelude::*;
|
|
13
|
-
|
|
14
|
-
use crate::{Backend, CancelToken, GeneratorDiagnostics, difficulty};
|
|
15
|
-
|
|
16
|
-
/// XorShift1024* PRNG — same algorithm as rsnano-node's `XorShift1024Star`.
|
|
17
|
-
/// Fast, non-cryptographic, good for PoW nonce exploration.
|
|
18
|
-
struct XorShift1024Star {
|
|
19
|
-
state: [u64; 16],
|
|
20
|
-
p: usize,
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
impl XorShift1024Star {
|
|
24
|
-
fn new(seed: u64) -> Self {
|
|
25
|
-
// Splitmix64 to populate state from a single seed
|
|
26
|
-
let mut s = seed;
|
|
27
|
-
let mut state = [0u64; 16];
|
|
28
|
-
for x in state.iter_mut() {
|
|
29
|
-
s = s.wrapping_add(0x9e3779b97f4a7c15);
|
|
30
|
-
let mut z = s;
|
|
31
|
-
z = (z ^ (z >> 30)).wrapping_mul(0xbf58476d1ce4e5b9);
|
|
32
|
-
z = (z ^ (z >> 27)).wrapping_mul(0x94d049bb133111eb);
|
|
33
|
-
*x = z ^ (z >> 31);
|
|
34
|
-
}
|
|
35
|
-
Self { state, p: 0 }
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
#[inline]
|
|
39
|
-
fn next(&mut self) -> u64 {
|
|
40
|
-
let s0 = self.state[self.p];
|
|
41
|
-
self.p = (self.p + 1) & 15;
|
|
42
|
-
let mut s1 = self.state[self.p];
|
|
43
|
-
s1 ^= s1 << 31;
|
|
44
|
-
self.state[self.p] = s1 ^ s0 ^ (s1 >> 11) ^ (s0 >> 30);
|
|
45
|
-
self.state[self.p].wrapping_mul(1181783497276652981)
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
pub(crate) struct CpuBackend;
|
|
50
|
-
|
|
51
|
-
impl CpuBackend {
|
|
52
|
-
pub fn new() -> Self {
|
|
53
|
-
Self
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
impl Backend for CpuBackend {
|
|
58
|
-
fn name(&self) -> &'static str {
|
|
59
|
-
"cpu"
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
fn generate(&self, hash: &[u8; 32], threshold: u64, cancel: &CancelToken) -> Option<u64> {
|
|
63
|
-
let cancelled = Arc::clone(&cancel.flag);
|
|
64
|
-
let hash = *hash;
|
|
65
|
-
|
|
66
|
-
#[cfg(not(target_arch = "wasm32"))]
|
|
67
|
-
{
|
|
68
|
-
let found = Arc::new(AtomicBool::new(false));
|
|
69
|
-
let result = Arc::new(AtomicU64::new(0));
|
|
70
|
-
|
|
71
|
-
// Use rayon's thread pool — one worker per available CPU core.
|
|
72
|
-
// Each worker starts from a different random seed and searches in batches.
|
|
73
|
-
let thread_count = rayon::current_num_threads();
|
|
74
|
-
|
|
75
|
-
(0..thread_count).into_par_iter().for_each(|thread_idx| {
|
|
76
|
-
let mut rng = XorShift1024Star::new(
|
|
77
|
-
// Distinct seed per thread using splitmix on thread index
|
|
78
|
-
thread_idx as u64 ^ 0xdeadbeef_cafebabe,
|
|
79
|
-
);
|
|
80
|
-
|
|
81
|
-
const BATCH: usize = 256;
|
|
82
|
-
|
|
83
|
-
while !cancelled.load(Ordering::Relaxed) && !found.load(Ordering::Relaxed) {
|
|
84
|
-
for _ in 0..BATCH {
|
|
85
|
-
let nonce = rng.next();
|
|
86
|
-
if difficulty::compute(&hash, nonce) >= threshold {
|
|
87
|
-
// Atomically claim the result
|
|
88
|
-
if !found.swap(true, Ordering::AcqRel) {
|
|
89
|
-
result.store(nonce, Ordering::Release);
|
|
90
|
-
}
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
if found.load(Ordering::Acquire) {
|
|
98
|
-
Some(result.load(Ordering::Acquire))
|
|
99
|
-
} else {
|
|
100
|
-
None
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
#[cfg(target_arch = "wasm32")]
|
|
105
|
-
{
|
|
106
|
-
// WASM single-threaded fallback uses a random seed
|
|
107
|
-
let mut rng = XorShift1024Star::new(rand::random());
|
|
108
|
-
const BATCH: usize = 256;
|
|
109
|
-
|
|
110
|
-
while !cancelled.load(Ordering::Relaxed) {
|
|
111
|
-
for _ in 0..BATCH {
|
|
112
|
-
let nonce = rng.next();
|
|
113
|
-
if difficulty::compute(&hash, nonce) >= threshold {
|
|
114
|
-
return Some(nonce);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
None
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
fn diagnostics(&self) -> GeneratorDiagnostics {
|
|
123
|
-
GeneratorDiagnostics {
|
|
124
|
-
backend: "cpu".to_string(),
|
|
125
|
-
gpu: None,
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
#[cfg(test)]
|
|
131
|
-
mod tests {
|
|
132
|
-
use super::*;
|
|
133
|
-
use crate::thresholds;
|
|
134
|
-
|
|
135
|
-
#[test]
|
|
136
|
-
fn xorshift_is_deterministic() {
|
|
137
|
-
let mut a = XorShift1024Star::new(42);
|
|
138
|
-
let mut b = XorShift1024Star::new(42);
|
|
139
|
-
for _ in 0..1000 {
|
|
140
|
-
assert_eq!(a.next(), b.next());
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
#[test]
|
|
145
|
-
fn xorshift_distinct_seeds_differ() {
|
|
146
|
-
let mut a = XorShift1024Star::new(1);
|
|
147
|
-
let mut b = XorShift1024Star::new(2);
|
|
148
|
-
// Should produce different sequences
|
|
149
|
-
let va: Vec<u64> = (0..10).map(|_| a.next()).collect();
|
|
150
|
-
let vb: Vec<u64> = (0..10).map(|_| b.next()).collect();
|
|
151
|
-
assert_ne!(va, vb);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
#[test]
|
|
155
|
-
fn cpu_generates_valid_work_dev_threshold() {
|
|
156
|
-
let hash = [0u8; 32];
|
|
157
|
-
let backend = CpuBackend::new();
|
|
158
|
-
let cancel = CancelToken::new();
|
|
159
|
-
let nonce = backend.generate(&hash, thresholds::DEV, &cancel).unwrap();
|
|
160
|
-
let diff = difficulty::compute(&hash, nonce);
|
|
161
|
-
assert!(
|
|
162
|
-
diff >= thresholds::DEV,
|
|
163
|
-
"nonce {nonce:#018x} produced difficulty {diff:#018x} < threshold {:#018x}",
|
|
164
|
-
thresholds::DEV
|
|
165
|
-
);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
#[test]
|
|
169
|
-
fn cpu_respects_cancellation() {
|
|
170
|
-
let hash = [0u8; 32];
|
|
171
|
-
let backend = CpuBackend::new();
|
|
172
|
-
let cancel = CancelToken::new();
|
|
173
|
-
// Cancel immediately — should return None
|
|
174
|
-
cancel.cancel();
|
|
175
|
-
let result = backend.generate(&hash, u64::MAX, &cancel);
|
|
176
|
-
assert!(result.is_none());
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/// Round-trip: generate then validate using the same known vectors
|
|
180
|
-
/// from difficulty.rs.
|
|
181
|
-
#[test]
|
|
182
|
-
fn roundtrip_work_validate() {
|
|
183
|
-
use crate::difficulty;
|
|
184
|
-
|
|
185
|
-
// Official known-good test vector hash from the nano-node core implementation
|
|
186
|
-
let hash = hex::decode("718CC2121C3E641059BC1C2CFC45666C99E8AE922F7A807B7D07B62C995D79E2")
|
|
187
|
-
.unwrap();
|
|
188
|
-
let hash: [u8; 32] = hash.try_into().unwrap();
|
|
189
|
-
|
|
190
|
-
let backend = CpuBackend::new();
|
|
191
|
-
let cancel = CancelToken::new();
|
|
192
|
-
let nonce = backend.generate(&hash, thresholds::DEV, &cancel).unwrap();
|
|
193
|
-
let diff = difficulty::compute(&hash, nonce);
|
|
194
|
-
assert!(diff >= thresholds::DEV);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|