use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
use std::sync::Mutex;
use std::thread;
use rayon::prelude::*;
use triton_vm::prelude::*;
pub use crate::types::{HEADER_PATH_LEN, HEIGHT, KERNEL_PATH_LEN, POW_PATH_LEN};
pub const NUM_LEAFS: usize = 1 << HEIGHT;
const NUM_BUD_LAYERS: usize = 5;
const NUM_INDEX_REPETITIONS: u32 = 63;
#[derive(Clone, Debug, Default)]
pub struct PowMastPaths {
pub pow: [Digest; POW_PATH_LEN],
pub header: [Digest; HEADER_PATH_LEN],
pub kernel: [Digest; KERNEL_PATH_LEN],
}
#[derive(Clone, Debug)]
pub struct NeptunePow {
pub root: Digest,
pub path_a: [Digest; HEIGHT],
pub path_b: [Digest; HEIGHT],
pub nonce: Digest,
}
impl PowMastPaths {
pub fn commit(&self) -> Digest {
let fields: Vec<BFieldElement> = self
.pow
.iter()
.chain(self.header.iter())
.chain(self.kernel.iter())
.flat_map(|d| d.values())
.collect();
Tip5::hash_varlen(&fields)
}
pub fn fast_mast_hash(&self, pow: &NeptunePow) -> Digest {
let mut encoded = [0u64; 300];
pow.encode_into_u64(&mut encoded);
let header_hash = acpu::field::tip5_hash_pair(
acpu::field::tip5_hash_varlen(&encoded),
digest_to_raw(&self.pow[0]),
);
let header_hash =
acpu::field::tip5_hash_pair(header_hash, digest_to_raw(&self.pow[1]));
let header_hash =
acpu::field::tip5_hash_pair(digest_to_raw(&self.pow[2]), header_hash);
let kernel_hash = acpu::field::tip5_hash_pair(
acpu::field::tip5_hash_varlen(&header_hash),
digest_to_raw(&self.header[0]),
);
let kernel_hash =
acpu::field::tip5_hash_pair(kernel_hash, digest_to_raw(&self.header[1]));
raw_to_digest(acpu::field::tip5_hash_pair(
acpu::field::tip5_hash_varlen(&kernel_hash),
digest_to_raw(&self.kernel[0]),
))
}
}
#[inline(always)]
fn digest_to_raw(d: &Digest) -> [u64; 5] {
let v = d.values();
[
v[0].raw_u64(),
v[1].raw_u64(),
v[2].raw_u64(),
v[3].raw_u64(),
v[4].raw_u64(),
]
}
#[inline(always)]
fn raw_to_digest(arr: [u64; 5]) -> Digest {
Digest::new([
BFieldElement::from_raw_u64(arr[0]),
BFieldElement::from_raw_u64(arr[1]),
BFieldElement::from_raw_u64(arr[2]),
BFieldElement::from_raw_u64(arr[3]),
BFieldElement::from_raw_u64(arr[4]),
])
}
impl NeptunePow {
pub fn encode(&self) -> Vec<BFieldElement> {
let mut out = Vec::with_capacity(300);
out.extend_from_slice(&self.nonce.values());
for d in &self.path_b {
out.extend_from_slice(&d.values());
}
for d in &self.path_a {
out.extend_from_slice(&d.values());
}
out.extend_from_slice(&self.root.values());
out
}
#[inline(always)]
pub fn encode_into_u64(&self, out: &mut [u64; 300]) {
let n = self.nonce.values();
for i in 0..5 {
out[i] = n[i].raw_u64();
}
for (di, d) in self.path_b.iter().enumerate() {
let off = 5 + di * 5;
let v = d.values();
for j in 0..5 {
out[off + j] = v[j].raw_u64();
}
}
for (di, d) in self.path_a.iter().enumerate() {
let off = 5 + 145 + di * 5;
let v = d.values();
for j in 0..5 {
out[off + j] = v[j].raw_u64();
}
}
let r = self.root.values();
for i in 0..5 {
out[295 + i] = r[i].raw_u64();
}
}
}
pub struct GuesserBuffer {
pub leafs: Vec<Digest>,
pub internal_nodes: Vec<Digest>, pub prev_block_digest: Digest,
}
impl GuesserBuffer {
pub fn build(prev_block_digest: Digest) -> Self {
eprintln!("GuesserBuffer: generating {NUM_LEAFS} leaves...");
let t0 = std::time::Instant::now();
let leafs = Self::compute_leafs(prev_block_digest);
eprintln!(" leaves: {:.1}s", t0.elapsed().as_secs_f64());
eprintln!("GuesserBuffer: building partial internal nodes (top 28 levels)...");
let t1 = std::time::Instant::now();
let internal_nodes = Self::build_internal_nodes(&leafs);
eprintln!(
" internal: {:.1}s โ root: {}",
t1.elapsed().as_secs_f64(),
internal_nodes[1]
);
Self { leafs, internal_nodes, prev_block_digest }
}
pub fn root(&self) -> Digest {
self.internal_nodes[1]
}
pub fn path(&self, idx: usize) -> [Digest; HEIGHT] {
let mut path = [Digest::default(); HEIGHT];
path[0] = self.leafs[idx ^ 1];
let mut running = (idx + NUM_LEAFS) >> 1;
let sib1 = running ^ 1;
let j = sib1 - (NUM_LEAFS >> 1);
path[1] = Tip5::hash_pair(self.leafs[2 * j], self.leafs[2 * j + 1]);
for k in 2..HEIGHT {
running >>= 1; path[k] = self.internal_nodes[running ^ 1];
}
path
}
fn bud(commitment: Digest, index: u64) -> Digest {
Tip5::hash_pair(
commitment,
Digest::new([
BFieldElement::new(index),
BFieldElement::new(0),
BFieldElement::new(0),
BFieldElement::new(0),
BFieldElement::new(0),
]),
)
}
fn bitreverse(mut n: u32, bits: u32) -> u32 {
n = ((n & 0x5555_5555) << 1) | ((n & 0xaaaa_aaaa) >> 1);
n = ((n & 0x3333_3333) << 2) | ((n & 0xcccc_cccc) >> 2);
n = ((n & 0x0f0f_0f0f) << 4) | ((n & 0xf0f0_f0f0) >> 4);
n = ((n & 0x00ff_00ff) << 8) | ((n & 0xff00_ff00) >> 8);
n = n.rotate_right(16);
n >> ((32 - bits) & 0x1f)
}
fn compute_leafs(prev_block_digest: Digest) -> Vec<Digest> {
let mut buf0: Vec<Digest> = (0u64..NUM_LEAFS as u64)
.into_par_iter()
.map(|i| Self::bud(prev_block_digest, i))
.collect();
let mut buf1 = buf0.clone();
for round in 0..NUM_BUD_LAYERS {
let stride = 1usize << round;
buf1.par_iter_mut().enumerate().for_each(|(k, out)| {
*out = Tip5::hash_pair(buf0[k], buf0[(k + stride) % NUM_LEAFS]);
});
std::mem::swap(&mut buf0, &mut buf1);
}
drop(buf1);
let bits = NUM_LEAFS.ilog2();
for k in 0..NUM_LEAFS {
let r = Self::bitreverse(k as u32, bits) as usize;
if r > k {
buf0.swap(k, r);
}
}
buf0
}
fn build_internal_nodes(leafs: &[Digest]) -> Vec<Digest> {
let size = NUM_LEAFS >> 1; let mut nodes = vec![Digest::default(); size];
let lv27 = size >> 1; nodes[lv27..].par_iter_mut().enumerate().for_each(|(o, node)| {
let b = o * 4;
let left = Tip5::hash_pair(leafs[b], leafs[b + 1]);
let right = Tip5::hash_pair(leafs[b + 2], leafs[b + 3]);
*node = Tip5::hash_pair(left, right);
});
let mut child_start = lv27;
while child_start > 1 {
let parent_start = child_start >> 1;
let (parent_half, child_half) = nodes.split_at_mut(child_start);
parent_half[parent_start..].par_iter_mut().enumerate().for_each(|(o, node)| {
*node = Tip5::hash_pair(child_half[o * 2], child_half[o * 2 + 1]);
});
child_start = parent_start;
}
nodes
}
}
fn indices(index_preimage: Digest, nonce: Digest) -> (usize, usize) {
let mut h = Tip5::hash_pair(index_preimage, nonce);
for _ in 1..NUM_INDEX_REPETITIONS {
h = Tip5::hash_pair(h, Digest::default());
}
let a = h.values()[0].value() as usize % NUM_LEAFS;
let b = h.values()[1].value() as usize % NUM_LEAFS;
(a, b)
}
#[inline(always)]
fn random_nonce(attempt: u64, tid: u64) -> Digest {
let left = [
BFieldElement::new(attempt).raw_u64(),
BFieldElement::new(tid).raw_u64(),
BFieldElement::new(attempt.wrapping_mul(6_364_136_223_846_793_005)).raw_u64(),
BFieldElement::new(attempt.rotate_left(32) ^ tid).raw_u64(),
BFieldElement::new(attempt.wrapping_add(tid.wrapping_mul(1_442_695_040_888_963_407)))
.raw_u64(),
];
raw_to_digest(acpu::field::tip5_hash_pair(left, [0u64; 5]))
}
pub fn mine_hardfork_beta(
path_a: [Digest; HEIGHT],
mast_paths: &PowMastPaths,
target: Digest,
max_attempts: u64,
) -> Option<NeptunePow> {
let root = Digest::default();
let path_b = [Digest::default(); HEIGHT];
let num_threads = thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(12);
eprintln!(
"Neptune HardforkBeta mining: {num_threads} P-core threads, target={target}"
);
let t0 = std::time::Instant::now();
let counter = AtomicU64::new(0);
let found = AtomicU32::new(0);
let result: Mutex<Option<NeptunePow>> = Mutex::new(None);
thread::scope(|s| {
for tid in 0..num_threads as u64 {
let counter = &counter;
let found = &found;
let result = &result;
s.spawn(move || {
let _ = acpu::sync::affinity::pin_p_core();
loop {
if found.load(Ordering::Relaxed) != 0 {
return;
}
let attempt = counter.fetch_add(1, Ordering::Relaxed);
if attempt >= max_attempts {
return;
}
let nonce = random_nonce(attempt, tid);
let pow = NeptunePow {
root,
path_a,
path_b,
nonce,
};
if mast_paths.fast_mast_hash(&pow) <= target
&& found
.compare_exchange(0, 1, Ordering::SeqCst, Ordering::Relaxed)
.is_ok()
{
*result.lock().unwrap() = Some(pow);
return;
}
}
});
}
});
let total = counter.load(Ordering::Relaxed);
let result = result.into_inner().unwrap();
if result.is_some() {
let elapsed = t0.elapsed().as_secs_f64();
eprintln!(
"Found nonce in {:.1}s ({:.0} H/s)",
elapsed,
total as f64 / elapsed
);
}
result
}
pub fn benchmark_hardfork_beta(duration_secs: f64) {
let root = Digest::default();
let path_a = [Digest::default(); HEIGHT];
let path_b = [Digest::default(); HEIGHT];
let mast_paths = PowMastPaths::default();
let num_threads = thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(12);
eprintln!(
"Benchmarking HardforkBeta PoW ({num_threads} P-core threads, {duration_secs:.0}s)..."
);
let counter = AtomicU64::new(0);
let stop = AtomicU32::new(0);
let t0 = std::time::Instant::now();
thread::scope(|s| {
for tid in 0..num_threads as u64 {
let counter = &counter;
let stop = &stop;
let mast_paths = &mast_paths;
s.spawn(move || {
let _ = acpu::sync::affinity::pin_p_core();
while stop.load(Ordering::Relaxed) == 0 {
let attempt = counter.fetch_add(1, Ordering::Relaxed);
let nonce = random_nonce(attempt, tid);
let pow = NeptunePow {
root,
path_a,
path_b,
nonce,
};
let _ = mast_paths.fast_mast_hash(&pow);
}
});
}
thread::sleep(std::time::Duration::from_secs_f64(duration_secs));
stop.store(1, Ordering::Release);
});
let total = counter.load(Ordering::Relaxed);
let elapsed = t0.elapsed().as_secs_f64();
let hps = total as f64 / elapsed;
eprintln!(
"Result: {} attempts in {:.2}s = {:.0} H/s ({:.2} MH/s)",
total,
elapsed,
hps,
hps / 1e6
);
}
#[cfg(feature = "gpu")]
pub fn mine_hardfork_beta_gpu(
path_a: [Digest; HEIGHT],
mast_paths: &PowMastPaths,
target: Digest,
max_attempts: u64,
) -> Option<NeptunePow> {
use crate::aruminium_mine::{AruMine, THREADS_TOTAL};
let miner = AruMine::try_new()?;
miner.set_template(&path_a, mast_paths, &target);
let root = Digest::default();
let path_b = [Digest::default(); HEIGHT];
let total_threads = thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(12);
let cpu_threads = total_threads.saturating_sub(1).max(1);
eprintln!(
"Neptune HardforkBeta unified mining: {cpu_threads} CPU threads + GPU ({THREADS_TOTAL} nonces/launch)"
);
let t0 = std::time::Instant::now();
let counter = AtomicU64::new(0);
let cpu_attempts = AtomicU64::new(0);
let gpu_attempts = AtomicU64::new(0);
let result: Mutex<Option<NeptunePow>> = Mutex::new(None);
thread::scope(|s| {
for tid in 0..cpu_threads as u64 {
let counter = &counter;
let cpu_attempts = &cpu_attempts;
let miner = &miner;
let result = &result;
s.spawn(move || {
let _ = acpu::sync::affinity::pin_p_core();
let state = miner.state();
loop {
if state.found.load(Ordering::Acquire) != 0 {
return;
}
let attempt = counter.fetch_add(1, Ordering::Relaxed);
if attempt >= max_attempts {
return;
}
cpu_attempts.fetch_add(1, Ordering::Relaxed);
let nonce = random_nonce(attempt, tid);
let pow = NeptunePow {
root,
path_a,
path_b,
nonce,
};
if mast_paths.fast_mast_hash(&pow) <= target
&& state
.found
.compare_exchange(0, 1, Ordering::SeqCst, Ordering::Relaxed)
.is_ok()
{
*result.lock().unwrap() = Some(pow);
return;
}
}
});
}
let counter_g = &counter;
let gpu_attempts_g = &gpu_attempts;
let miner_g = &miner;
let result_g = &result;
s.spawn(move || {
let state = miner_g.state();
loop {
if state.found.load(Ordering::Acquire) != 0 {
return;
}
let base = counter_g.fetch_add(THREADS_TOTAL as u64, Ordering::Relaxed);
if base >= max_attempts {
return;
}
miner_g.dispatch_batch(base);
gpu_attempts_g.fetch_add(THREADS_TOTAL as u64, Ordering::Relaxed);
if state.found.load(Ordering::Acquire) == 0 {
continue;
}
let nonce = Digest::new(state.winning_nonce.map(BFieldElement::from_raw_u64));
let pow = NeptunePow {
root,
path_a,
path_b,
nonce,
};
if mast_paths.fast_mast_hash(&pow) <= target {
*result_g.lock().unwrap() = Some(pow);
return;
}
eprintln!(
"AruMine: GPU false positive at attempt={}, resetting",
state.winning_attempt
);
state.found.store(0, Ordering::Release);
}
});
});
let cpu_total = cpu_attempts.load(Ordering::Relaxed);
let gpu_total = gpu_attempts.load(Ordering::Relaxed);
let result = result.into_inner().unwrap();
if result.is_some() {
let elapsed = t0.elapsed().as_secs_f64();
let total = cpu_total + gpu_total;
eprintln!(
"Found nonce in {:.1}s โ CPU {} H, GPU {} H, total {:.2} MH/s",
elapsed,
cpu_total,
gpu_total,
total as f64 / elapsed / 1e6
);
}
result
}
#[cfg(feature = "gpu")]
pub fn benchmark_gpu(duration_secs: f64) {
use crate::aruminium_mine::{AruMine, THREADS_TOTAL};
let miner = match AruMine::try_new() {
Some(m) => m,
None => { eprintln!("AruMine: no Metal GPU available"); return; }
};
let path_a = [Digest::default(); HEIGHT];
let mast = PowMastPaths::default();
let target = Digest::new([BFieldElement::new(0); 5]);
miner.set_template(&path_a, &mast, &target);
eprintln!("Benchmarking GPU (AruMine) for {duration_secs:.0}s...");
let t0 = std::time::Instant::now();
let deadline = t0 + std::time::Duration::from_secs_f64(duration_secs);
let mut total = 0u64;
let mut batch = 0u64;
loop {
if std::time::Instant::now() >= deadline { break; }
miner.dispatch_batch(batch * THREADS_TOTAL as u64);
total += THREADS_TOTAL as u64;
batch += 1;
}
let elapsed = t0.elapsed().as_secs_f64();
let hps = total as f64 / elapsed;
eprintln!("GPU: {total} attempts in {elapsed:.2}s = {hps:.0} H/s ({:.3} MH/s)", hps / 1e6);
}
pub fn mine(
buffer: &GuesserBuffer,
mast_paths: &PowMastPaths,
target: Digest,
max_attempts: u64,
) -> Option<NeptunePow> {
let root = buffer.root();
let index_preimage = Tip5::hash_pair(root, mast_paths.commit());
let threads = rayon::current_num_threads();
eprintln!("Neptune memory-hard mining: {threads} threads, target={target}");
let t0 = std::time::Instant::now();
let result = (0u64..max_attempts).into_par_iter().find_map_any(|attempt| {
let tid = rayon::current_thread_index().unwrap_or(0) as u64;
let nonce = random_nonce(attempt, tid);
let (ia, ib) = indices(index_preimage, nonce);
let path_a = buffer.path(ia);
let path_b = buffer.path(ib);
let pow = NeptunePow { root, path_a, path_b, nonce };
if mast_paths.fast_mast_hash(&pow) <= target {
Some(pow)
} else {
None
}
});
if result.is_some() {
let elapsed = t0.elapsed().as_secs_f64();
eprintln!(
"Found nonce in {:.1}s ({:.0} H/s)",
elapsed,
max_attempts as f64 / elapsed
);
}
result
}