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.
Files changed (23) hide show
  1. {nano_rspow_python-0.8.0 → nano_rspow_python-0.10.0}/Cargo.lock +5 -5
  2. {nano_rspow_python-0.8.0 → nano_rspow_python-0.10.0}/Cargo.toml +1 -1
  3. {nano_rspow_python-0.8.0 → nano_rspow_python-0.10.0}/PKG-INFO +1 -1
  4. {nano_rspow_python-0.8.0 → nano_rspow_python-0.10.0}/nano-rspow/README.md +14 -11
  5. nano_rspow_python-0.10.0/nano-rspow/src/cpu.rs +376 -0
  6. nano_rspow_python-0.8.0/nano-rspow/src/cpu.rs +0 -196
  7. {nano_rspow_python-0.8.0 → nano_rspow_python-0.10.0}/nano-rspow/Cargo.toml +0 -0
  8. {nano_rspow_python-0.8.0 → nano_rspow_python-0.10.0}/nano-rspow/benches/pow_benchmark.rs +0 -0
  9. {nano_rspow_python-0.8.0 → nano_rspow_python-0.10.0}/nano-rspow/src/difficulty.rs +0 -0
  10. {nano_rspow_python-0.8.0 → nano_rspow_python-0.10.0}/nano-rspow/src/lib.rs +0 -0
  11. {nano_rspow_python-0.8.0 → nano_rspow_python-0.10.0}/nano-rspow/src/opencl_backend/mod.rs +0 -0
  12. {nano_rspow_python-0.8.0 → nano_rspow_python-0.10.0}/nano-rspow/src/opencl_backend/pow.cl +0 -0
  13. {nano_rspow_python-0.8.0 → nano_rspow_python-0.10.0}/nano-rspow/src/thresholds.rs +0 -0
  14. {nano_rspow_python-0.8.0 → nano_rspow_python-0.10.0}/nano-rspow/src/types.rs +0 -0
  15. {nano_rspow_python-0.8.0 → nano_rspow_python-0.10.0}/nano-rspow/src/wgpu_backend/mod.rs +0 -0
  16. {nano_rspow_python-0.8.0 → nano_rspow_python-0.10.0}/nano-rspow/src/wgpu_backend/pow.wgsl +0 -0
  17. {nano_rspow_python-0.8.0 → nano_rspow_python-0.10.0}/nano-rspow/src/wgpu_shared.rs +0 -0
  18. {nano_rspow_python-0.8.0 → nano_rspow_python-0.10.0}/nano-rspow-python/Cargo.toml +0 -0
  19. {nano_rspow_python-0.8.0 → nano_rspow_python-0.10.0}/nano-rspow-python/build.rs +0 -0
  20. {nano_rspow_python-0.8.0 → nano_rspow_python-0.10.0}/nano-rspow-python/nano_rspow.pyi +0 -0
  21. {nano_rspow_python-0.8.0 → nano_rspow_python-0.10.0}/nano-rspow-python/src/lib.rs +0 -0
  22. {nano_rspow_python-0.8.0 → nano_rspow_python-0.10.0}/nano-rspow-python/tests/test_pow.py +0 -0
  23. {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.8.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.8.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.8.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.8.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.8.0"
790
+ version = "0.10.0"
791
791
  dependencies = [
792
792
  "bytemuck",
793
793
  "getrandom",
@@ -3,7 +3,7 @@ members = ["nano-rspow", "nano-rspow-python"]
3
3
  resolver = "2"
4
4
 
5
5
  [workspace.package]
6
- version = "0.8.0"
6
+ version = "0.10.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.8.0
3
+ Version: 0.10.0
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -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 and block signing library for [Nano](https://www.nano.org).
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 and local signing.
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
- Compile and run the native generator directly on your machine:
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
- Alternatively, you can use `cargo run -p nano-rspow-cli -- <args>` to automatically build and execute in one step.
31
+ Or use `cargo run -p nano-rspow-cli -- <args>` to build and execute in one step.
30
32
 
31
- ### 2. Node.js & CLI (Zero-Install)
32
- Execute the high-performance CLI instantly using precompiled native binaries:
33
+ ### 2. Node.js (Programmatic API)
34
+ Use the native bindings for in-process PoW generation:
33
35
  ```bash
34
- npx -p nano-rspow-node nano-rspow generate 718CC2121C3E641059BC1C2CFC45666C99E8AE922F7A807B7D07B62C995D79E2 --backend cpu
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
- }