From 7ec802ee24728b7702cbf537f025f51788447eef Mon Sep 17 00:00:00 2001 From: rob thijssen Date: Wed, 10 Sep 2025 22:52:04 +0800 Subject: [PATCH 01/12] feat: montgomery-optimized cpu mining engine - introduce new `engine-montgomery` crate implementing a 512-bit fixed-width montgomery multiplication backend for qpow mining. - mirror `cpu-fast` engine behavior and metrics for direct comparison. - add runtime backend selection with portable and x86_64 bmi2/adx kernels. - expose `--engine cpu-montgomery` cli option in miner-cli. - integrate engine and metrics support in miner-service with feature gating. - add engine backend info metric for observability and a/b testing. --- Cargo.lock | 30 ++ Cargo.toml | 1 + crates/engine-montgomery/Cargo.toml | 24 + crates/engine-montgomery/readme.md | 204 ++++++++ crates/engine-montgomery/src/lib.rs | 762 ++++++++++++++++++++++++++++ crates/metrics/src/lib.rs | 21 + crates/miner-cli/src/main.rs | 5 +- crates/miner-service/Cargo.toml | 5 +- crates/miner-service/src/lib.rs | 13 + 9 files changed, 1063 insertions(+), 2 deletions(-) create mode 100644 crates/engine-montgomery/Cargo.toml create mode 100644 crates/engine-montgomery/readme.md create mode 100644 crates/engine-montgomery/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index b992786..db45cd7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -333,6 +333,16 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "rand_core", + "subtle", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -480,6 +490,19 @@ dependencies = [ "thiserror", ] +[[package]] +name = "engine-montgomery" +version = "0.1.0" +dependencies = [ + "crypto-bigint", + "engine-cpu", + "log", + "num-traits 0.2.19", + "pow-core", + "primitive-types", + "sha3", +] + [[package]] name = "enum_primitive" version = "0.1.1" @@ -1047,6 +1070,7 @@ dependencies = [ "engine-cpu", "engine-gpu-cuda", "engine-gpu-opencl", + "engine-montgomery", "hex", "log", "metrics", @@ -1706,6 +1730,12 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "1.0.109" diff --git a/Cargo.toml b/Cargo.toml index 090a07c..15167cb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "crates/miner-cli", "crates/miner-service", "crates/pow-core", + "crates/engine-montgomery", ] default-members = ["crates/miner-cli"] resolver = "2" diff --git a/crates/engine-montgomery/Cargo.toml b/crates/engine-montgomery/Cargo.toml new file mode 100644 index 0000000..b0dbf33 --- /dev/null +++ b/crates/engine-montgomery/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "engine-montgomery" +version = "0.1.0" +edition = "2021" +description = "Montgomery-optimized CPU mining engine for Quantus External Miner" + +[lib] +name = "engine_montgomery" +path = "src/lib.rs" + +[dependencies] +# Reuse the engine trait and shared types exposed by engine-cpu +engine-cpu = { path = "../engine-cpu" } +# Access job context and core PoW math interfaces +pow-core = { path = "../pow-core" } + +# Workspace-aligned deps +primitive-types = { workspace = true } +log = { workspace = true } +sha3 = { workspace = true } +num-traits = { workspace = true } + +# Fixed-width big-integer backend for 512-bit Montgomery ops +crypto-bigint = "0.5" diff --git a/crates/engine-montgomery/readme.md b/crates/engine-montgomery/readme.md new file mode 100644 index 0000000..a4f130f --- /dev/null +++ b/crates/engine-montgomery/readme.md @@ -0,0 +1,204 @@ +# engine-montgomery + +Montgomery-optimized CPU mining engine for the Quantus External Miner. + +This crate implements the same public `MinerEngine` trait as the baseline and fast CPU engines, while replacing the per-nonce modular multiplication with a fixed-width 8×64-bit Montgomery implementation. It is selectable via the CLI flag: + +- `--engine cpu-montgomery` + +The engine mirrors `cpu-fast` control flow and metrics emission so that apples-to-apples comparisons can be made between engines. + +Highlights: +- Fixed-width 512-bit arithmetic using 8×64-bit limbs. +- Portable CIOS (Coarsely Integrated Operand Scanning) Montgomery multiply with `u128` intermediates. +- Direct SHA3 over big-endian limbs to avoid intermediate big-integer conversions. +- Per-job precompute cache (Montgomery params and `m_hat`) to reduce setup overhead. +- Runtime backend selection for microarchitecture-optimized kernels (x86_64 BMI2-only; BMI2+ADX planned; aarch64 UMULH planned). +- Metrics label for backend selection to aid dashboards and A/B analysis. + +--- + +## Algorithm Overview + +QPoW distance per nonce is computed as: + +1) y update (group accumulation): + - y0 = m^(h + start_nonce) mod n (one-time per worker). + - y_{k+1} = (y_k * m) mod n (one multiply per nonce). + +2) Distance = target XOR SHA3_512(y), where y is encoded as 64 big-endian bytes. + +The engine focuses on optimizing step (1) with Montgomery multiplication and reducing overhead in step (2) by hashing directly over the final big-endian representation of y (no extra big-int intermediates). + +--- + +## Montgomery Arithmetic (512-bit, 8×64 limbs) + +We implement a 512-bit Montgomery field backed by 8 little-endian 64-bit limbs. For modulus `n` (odd composite, constrained by pow-core), we precompute: + +- `n0_inv = -n^{-1} mod 2^64` (using Newton–Raphson). +- `R = 2^(64*8) mod n` (implicit via representation). +- `R^2 mod n` (computed once per job using the existing big-integer reference). +- `to_mont(x) = x * R mod n = mont_mul(x, R^2)`. +- `from_mont(x̂) = x̂ * 1 mod n = mont_mul(x̂, 1)`. + +The core multiply is a portable CIOS Montgomery reduction: + +``` +- acc <- 0 +- For each limb i in a: + acc += a[i] * b + m = (acc[0] * n0_inv) mod 2^64 + acc += m * n + acc = acc >> 64 (shift down one limb) +- If acc >= n: acc -= n +- return acc +``` + +Where `acc` is a 9-limb `u128` accumulator to simplify carries. + +This yields a single multiply+reduce per nonce with a branchless inner loop (except final conditional subtract). + +--- + +## Hashing Strategy + +- We keep `y` in Montgomery domain during iteration to minimize transforms. +- Before SHA3, we convert the residue `ŷ` back to the normal domain via `mont_mul(ŷ, 1)` and serialize as big-endian 64 bytes. +- We reuse a single `Sha3_512` hasher per search call and `finalize_reset()` each iteration to reduce construction overhead. + +This preserves consensus behavior while avoiding unnecessary big-int allocations or conversions. + +--- + +## Backend Selection and Optimizations + +At runtime, the engine selects a Montgomery multiply backend based on CPU and environment: + +- Portable (default/fallback) + - `mont_mul_portable`: pure-Rust, `u128`-based CIOS. + - Available everywhere. + +- x86_64 (runtime detected) + - BMI2-only (`_mulx_u64`): + - `mont_mul_bmi2`: uses BMI2 MULX to get 128-bit products efficiently. + - Single carry chain (easier to validate; broadly available on newer CPUs). + - BMI2+ADX (planned): + - `mont_mul_bmi2_adx`: MULX + ADCX/ADOX dual carry chains for higher ILP. + - Will replace the current placeholder (which temporarily delegates to the BMI2-only kernel). + +- aarch64 (planned) + - UMULH/ADCS: + - Use UMULH for high halves and ADCS for carry propagation. + - Expected to bring noticeable speedups on macOS Apple Silicon and Linux ARM64. + +You can override backend selection for testing: + +- `MINER_MONT_BACKEND=portable|bmi2|bmi2-adx` + +The engine logs the selected backend (and exports it via metrics) at job start. Unsupported overrides safely fall back with a clear warning. + +--- + +## Metrics + +The engine emits the same per-job and per-thread metrics as other engines, but adds a backend info gauge: + +- `miner_engine_backend{engine="cpu-montgomery", backend=""} = 1` + +This makes it easy to pivot in Grafana by backend. + +All other metrics (hash rates, progress chunking cadence, counters) are identical to `cpu-fast` for apples-to-apples comparisons. + +--- + +## Correctness and Tests + +We keep the portable CIOS path as the ground-truth reference for optimized kernels, and we cross-check against pow-core’s BigUint-based implementations. + +Property tests included: + +- Portable Montgomery vs reference incremental multiply: + - `from_mont(mul(to_mont(y), to_mont(m))) == step_mul(y)` across multiple steps. + +- BMI2 vs Portable: + - For randomized sequences, `bmi2` backend must match `portable` exactly at each step. + - On non-x86_64 platforms, the test still runs but both backends fall back to `portable`. + +- End-to-end parity: + - `cpu-montgomery` vs `cpu-fast` on a small inclusive range (distance and winner parity; identical hash_count accounting). + +We recommend running the property tests on machines with and without BMI2/ADX to cover both code paths. + +--- + +## Safety + +- The crate uses `#![deny(unsafe_code)]`. +- `unsafe` is scoped only to tiny x86_64 backend functions (for `_mulx_u64` and, later, inline asm for ADCX/ADOX). +- All other code remains safe Rust. +- The portable path is always available as a fallback for correctness/regression checks. + +--- + +## Performance Notes + +- Relative gains depend on how much time the miner spends in SHA3 vs modular multiply. +- Direct-hash-from-residue + precompute caching already yields a measurable uplift over `cpu-fast`. +- The BMI2-only path should improve throughput on supporting x86_64 hardware. +- BMI2+ADX typically produces the highest gains on the multiply itself (often 1.5–2.0×), with end-to-end uplift bounded by SHA3 share per nonce. +- aarch64 UMULH/ADCS is expected to bring similar relative gains on Apple Silicon. + +To minimize orchestration overhead in the service: +- Increase `--progress-chunk-ms` (e.g., 3000–5000) on both engines when comparing, to reduce update traffic and context switching. + +--- + +## Runtime and CI Tips + +- Selecting the engine: + - `quantus-miner --engine cpu-montgomery` + +- For A/B: + - Keep workers and chunking identical across instances. + - If testing backends explicitly: + - `MINER_MONT_BACKEND=bmi2` or `bmi2-adx` on capable x86_64 hardware + - Check logs and metrics for the selected backend label. + +- Observability: + - Ensure metrics exporter is enabled (`--metrics-port ...`) for dashboards. + - Filter or group by `engine="cpu-montgomery"` and backend metric to compare microarchitectural paths. + +--- + +## Roadmap + +- [x] Portable 8×64 CIOS (u128) +- [x] Per-job precompute cache (`n0_inv`, `R^2 mod n`, `m_hat`) +- [x] Direct SHA3 from normalized big-endian bytes +- [x] Backend selection with log + metric +- [x] x86_64 BMI2-only MULX kernel +- [ ] x86_64 BMI2+ADX (MULX + ADCX/ADOX) dual carry chain +- [ ] aarch64 UMULH/ADCS kernel (macOS/Linux ARM64) +- [ ] Optional benchmark micro-harness (ns/op for mont_mul backends) +- [ ] Extend tests with more randomized vectors and edge-case sweeps + +--- + +## Design Rationale + +- Keep the interface and metrics identical to `cpu-fast` so that any performance deltas reflect algorithmic/microarchitectural improvements rather than service overhead. +- Keep a portable, well-reviewed core (CIOS) as a correctness reference. +- Add microarchitecture-optimized kernels behind runtime dispatch and an env override for safe, controlled rollouts. +- Log and export backend selection so A/B comparisons and regressions are easy to track. + +--- + +## Contributing + +- Changes to backends should include: + - Property tests vs portable. + - End-to-end parity checks vs `cpu-fast`. + - A note in this README describing the optimization and any preconditions (e.g., required CPU features). +- Keep unsafe code minimal, private, and well-commented. +- Prefer small, focused PRs for each backend/optimization to simplify review and bisecting. diff --git a/crates/engine-montgomery/src/lib.rs b/crates/engine-montgomery/src/lib.rs new file mode 100644 index 0000000..786097d --- /dev/null +++ b/crates/engine-montgomery/src/lib.rs @@ -0,0 +1,762 @@ +#![deny(rust_2018_idioms)] +#![deny(unsafe_code)] + +//! Montgomery-optimized CPU mining engine (scaffolding). +//! +//! Goals: +//! - Mirror the cpu-fast engine behavior and metrics (hash counts, progress cadence). +//! - Provide a drop-in engine selectable via `--engine cpu-montgomery`. +//! - Introduce a crypto-bigint based 512-bit fixed-width backend scaffold for future +//! Montgomery multiplication/reduction. +//! +//! Current state: +//! - The search loop mirrors `engine-cpu`'s `FastCpuEngine` to ensure apples-for-apples +//! metrics and correctness parity. +//! - A lightweight Montgomery scaffolding is included (conversions and parameter +//! container) using `crypto-bigint`, ready to be integrated into the hot path. +//! +//! Next steps (planned): +//! - Replace the per-step `step_mul` with Montgomery domain multiplication: +//! y_hat <- montgomery_mul(y_hat, m_hat, n, n') with y kept in Montgomery domain. +//! - Precompute `R`, `R^2`, and `n'` once per job/thread and transform inputs. +//! - Convert out of Montgomery before SHA3-512 distance computation. +//! +//! Important: We intentionally keep emissions (hash_count increments, control flow) +//! identical to `cpu-fast` so metrics are directly comparable when pitting +//! `--engine cpu-fast` vs `--engine cpu-montgomery`. + +use core::cmp::Ordering; +use core::ops::BitXor; + +use engine_cpu::EngineStatus; +use engine_cpu::{EngineCandidate as Candidate, EngineRange as Range, MinerEngine}; +use pow_core::compat; +use pow_core::{init_worker_y0, is_valid_distance, JobContext}; +use primitive_types::U512; +use sha3::Digest; +use std::collections::HashMap; +use std::sync::atomic::{AtomicBool, Ordering as AtomicOrdering}; +use std::sync::{Arc, Mutex}; + +/// Montgomery engine for CPU. +pub struct MontgomeryCpuEngine { + cache: Mutex>>, +} + +impl Default for MontgomeryCpuEngine { + fn default() -> Self { + Self { + cache: Mutex::new(HashMap::new()), + } + } +} + +impl MontgomeryCpuEngine { + pub fn new() -> Self { + Self::default() + } +} + +impl MinerEngine for MontgomeryCpuEngine { + fn name(&self) -> &'static str { + "cpu-montgomery" + } + + fn prepare_context(&self, header_hash: [u8; 32], threshold: U512) -> JobContext { + // Build the standard pow-core context (m, n, target, etc). + // Montgomery parameters are computed per search call for now; we can + // later thread them through a custom context wrapper if needed. + JobContext::new(header_hash, threshold) + } + + fn search_range(&self, ctx: &JobContext, range: Range, cancel: &AtomicBool) -> EngineStatus { + // NOTE: For initial integration we mirror the cpu-fast engine logic to ensure + // metrics parity and correctness, while keeping the Montgomery scaffolding + // ready for optimization in follow-ups. + + if range.start > range.end { + return EngineStatus::Exhausted { hash_count: 0 }; + } + + // One-time init per thread: y0 = m^(h + start_nonce) mod n + let mut current = range.start; + let y0 = init_worker_y0(ctx, current); + let mut hash_count: u64 = 0; + + // Initialize or reuse per-job Montgomery params and residues from cache + let mont = { + let key = (ctx.m.to_big_endian(), ctx.n.to_big_endian()); + let mut guard = self.cache.lock().unwrap(); + if let Some(m) = guard.get(&key) { + m.clone() + } else { + let m = Arc::new(mont_portable::MontCtx::from_ctx(ctx)); + guard.insert(key, m.clone()); + m + } + }; + let mut y_hat = mont.to_mont_u512(&y0); + let m_hat = mont.m_hat; + + // Reuse a single SHA3-512 hasher instance and reset between updates + let mut sha3_hasher = sha3::Sha3_512::new(); + + loop { + // Cancellation check (fast and frequent as in cpu-fast) + if cancel.load(AtomicOrdering::Relaxed) { + return EngineStatus::Cancelled { hash_count }; + } + + // Compute distance from Montgomery accumulator by converting out-of-domain once + let y_norm_be = mont.from_mont_be_bytes(&y_hat); + sha3_hasher.update(&y_norm_be); + let hash_bytes = sha3_hasher.finalize_reset(); + let hashed = U512::from_big_endian(hash_bytes.as_slice()); + let distance = ctx.target.bitxor(hashed); + hash_count = hash_count.saturating_add(1); + + if is_valid_distance(ctx, distance) { + let work = current.to_big_endian(); + return EngineStatus::Found { + candidate: Candidate { + nonce: current, + work, + distance, + }, + hash_count, + }; + } + + // Advance or finish + match current.cmp(&range.end) { + Ordering::Less => { + // Incremental step in Montgomery domain: y_hat <- y_hat * m_hat (mod n) + y_hat = mont.mul(&y_hat, &m_hat); + current = current.saturating_add(U512::one()); + } + _ => { + break EngineStatus::Exhausted { hash_count }; + } + } + } + } +} + +/// Montgomery scaffolding with crypto-bigint. +/// This module provides conversions and parameter containers that we can use +/// to wire up a fixed-width limb backend for 512-bit operations. +mod mont_portable { + use super::*; + + // Montgomery context with portable CIOS 8x64 implementation (u128 intermediates). + // Limbs are stored little-endian (limb 0 is least significant). + type MulFn = fn(&[u64; 8], &[u64; 8], &[u64; 8], u64) -> [u64; 8]; + + #[derive(Clone)] + pub struct MontCtx { + n: [u64; 8], + n0_inv: u64, // -n^{-1} mod 2^64 + r2: [u64; 8], // R^2 mod n + pub m_hat: [u64; 8], + mul_fn: MulFn, + } + + impl MontCtx { + pub fn from_ctx(ctx: &JobContext) -> Self { + let n = u512_to_le(ctx.n); + let n0_inv = mont_n0_inv(n[0]); + let r2_u512 = compat::mod_pow(&U512::from(2u32), &U512::from(1024u32), &ctx.n); + let r2 = u512_to_le(r2_u512); + let m = u512_to_le(ctx.m); + let (mul_fn, backend) = select_backend(); + log::info!(target: "miner", "cpu-montgomery backend selected: {}", backend); + #[cfg(feature = "metrics")] + { + metrics::set_engine_backend("cpu-montgomery", backend); + } + let m_hat = mul_fn(&m, &r2, &n, n0_inv); + MontCtx { + n, + n0_inv, + r2, + m_hat, + mul_fn, + } + } + + pub fn to_mont_u512(&self, x: &U512) -> [u64; 8] { + let xl = u512_to_le(*x); + (self.mul_fn)(&xl, &self.r2, &self.n, self.n0_inv) + } + + pub fn from_mont_be_bytes(&self, x_hat: &[u64; 8]) -> [u8; 64] { + let one = { + let mut o = [0u64; 8]; + o[0] = 1; + o + }; + let norm = (self.mul_fn)(x_hat, &one, &self.n, self.n0_inv); + le_to_be_bytes(&norm) + } + + pub fn mul(&self, a_hat: &[u64; 8], b_hat: &[u64; 8]) -> [u64; 8] { + (self.mul_fn)(a_hat, b_hat, &self.n, self.n0_inv) + } + + // Test-only helpers to enable forcing a specific backend and to access limb-level conversions. + // These are useful for property tests and backend A/B validations. + #[cfg(test)] + pub fn from_ctx_with_backend_tag(ctx: &JobContext, tag: &str) -> Self { + let n = u512_to_le(ctx.n); + let n0_inv = mont_n0_inv(n[0]); + let r2_u512 = compat::mod_pow(&U512::from(2u32), &U512::from(1024u32), &ctx.n); + let r2 = u512_to_le(r2_u512); + let m = u512_to_le(ctx.m); + + // Choose mul_fn by tag; fall back to portable when not applicable on this arch. + let (mul_fn, _backend): (MulFn, &'static str) = match tag { + "x86_64-bmi2-adx" | "bmi2-adx" => { + #[cfg(target_arch = "x86_64")] + { + (mont_mul_bmi2_adx, "x86_64-bmi2-adx") + } + #[cfg(not(target_arch = "x86_64"))] + { + (mont_mul_portable, "portable") + } + } + "x86_64-bmi2" | "bmi2" => { + #[cfg(target_arch = "x86_64")] + { + (mont_mul_bmi2, "x86_64-bmi2") + } + #[cfg(not(target_arch = "x86_64"))] + { + (mont_mul_portable, "portable") + } + } + _ => (mont_mul_portable, "portable"), + }; + + let m_hat = mul_fn(&m, &r2, &n, n0_inv); + MontCtx { + n, + n0_inv, + r2, + m_hat, + mul_fn, + } + } + + #[cfg(test)] + pub fn to_mont_le_limbs(&self, x: &U512) -> [u64; 8] { + (self.mul_fn)(&u512_to_le(*x), &self.r2, &self.n, self.n0_inv) + } + + #[cfg(test)] + pub fn from_mont_le_limbs(&self, x_hat: &[u64; 8]) -> [u64; 8] { + let one = { + let mut o = [0u64; 8]; + o[0] = 1; + o + }; + (self.mul_fn)(x_hat, &one, &self.n, self.n0_inv) + } + } + + #[inline] + fn u512_to_le(x: U512) -> [u64; 8] { + let be = x.to_big_endian(); + let mut limbs = [0u64; 8]; + for i in 0..8 { + let start = 64 - 8 * (i + 1); + let mut bytes = [0u8; 8]; + bytes.copy_from_slice(&be[start..start + 8]); + limbs[i] = u64::from_be_bytes(bytes); + } + limbs + } + + #[inline] + fn le_to_be_bytes(limbs: &[u64; 8]) -> [u8; 64] { + let mut out = [0u8; 64]; + for i in 0..8 { + let chunk = limbs[7 - i].to_be_bytes(); + out[i * 8..(i + 1) * 8].copy_from_slice(&chunk); + } + out + } + + // Compute n0_inv = -n[0]^{-1} mod 2^64 using Newton–Raphson (n[0] must be odd). + #[inline] + fn mont_n0_inv(n0: u64) -> u64 { + // Compute inverse of n0 modulo 2^64 + let mut x = 1u64; + // 6 iterations suffice for 64-bit modulus + for _ in 0..6 { + let t = x.wrapping_mul(n0); + x = x.wrapping_mul(2u64.wrapping_sub(t)); + } + x.wrapping_neg() + } + + // Portable CIOS Montgomery multiplication: returns (a * b * R^{-1}) mod n + #[inline] + fn mont_mul_portable(a: &[u64; 8], b: &[u64; 8], n: &[u64; 8], n0_inv: u64) -> [u64; 8] { + const MASK: u128 = 0xFFFF_FFFF_FFFF_FFFFu128; + let mut acc = [0u128; 9]; + + for i in 0..8 { + // acc += a[i] * b + let ai = a[i] as u128; + let mut carry = 0u128; + for j in 0..8 { + let sum = acc[j] + ai * (b[j] as u128) + carry; + acc[j] = sum & MASK; + carry = sum >> 64; + } + acc[8] = acc[8] + carry; + + // m = (acc[0] * n0_inv) mod 2^64 + let m = ((acc[0] as u64).wrapping_mul(n0_inv)) as u128; + + // acc += m * n + let mut carry2 = 0u128; + for j in 0..8 { + let sum = acc[j] + m * (n[j] as u128) + carry2; + acc[j] = sum & MASK; + carry2 = sum >> 64; + } + acc[8] = acc[8] + carry2; + + // shift acc right by one limb + for j in 0..8 { + acc[j] = acc[j + 1]; + } + acc[8] = 0; + } + + // Convert acc (little-endian limbs) to u64 array + let mut res = [0u64; 8]; + for j in 0..8 { + res[j] = acc[j] as u64; + } + + // Conditional subtract modulus if res >= n + if ge_le(&res, n) { + sub_le_in_place(&mut res, n); + } + + res + } + + #[inline] + fn ge_le(a: &[u64; 8], b: &[u64; 8]) -> bool { + for i in (0..8).rev() { + if a[i] != b[i] { + return a[i] > b[i]; + } + } + true + } + + #[inline] + fn sub_le_in_place(a: &mut [u64; 8], b: &[u64; 8]) { + let mut borrow: u128 = 0; + for i in 0..8 { + let ai = a[i] as u128; + let bi = b[i] as u128; + let tmp = (1u128 << 64) + ai - bi - borrow; + a[i] = (tmp & 0xFFFF_FFFF_FFFF_FFFFu128) as u64; + borrow = if tmp >> 64 == 0 { 1 } else { 0 }; + } + } + + #[inline] + fn select_backend() -> (MulFn, &'static str) { + // Optional override via env: + // MINER_MONT_BACKEND=portable|bmi2|bmi2-adx + if let Ok(val) = std::env::var("MINER_MONT_BACKEND") { + let forced = val.to_ascii_lowercase(); + + #[cfg(target_arch = "x86_64")] + { + let bmi2 = std::is_x86_feature_detected!("bmi2"); + let adx = std::is_x86_feature_detected!("adx"); + match forced.as_str() { + "portable" => { + log::warn!(target: "miner", "cpu-montgomery backend override: forced portable"); + return (mont_mul_portable, "forced-portable"); + } + "bmi2-adx" | "adx" => { + if bmi2 && adx { + log::warn!(target: "miner", "cpu-montgomery backend override: forced x86_64-bmi2-adx"); + return (mont_mul_bmi2_adx, "forced-x86_64-bmi2-adx"); + } else if bmi2 { + log::warn!(target: "miner", "cpu-montgomery backend override requested bmi2-adx but ADX unavailable; falling back to x86_64-bmi2"); + return (mont_mul_bmi2, "x86_64-bmi2"); + } else { + log::warn!(target: "miner", "cpu-montgomery backend override requested bmi2-adx but BMI2/ADX unavailable; falling back to x86_64-generic"); + return (mont_mul_portable, "x86_64-generic"); + } + } + "bmi2" => { + if bmi2 { + log::warn!(target: "miner", "cpu-montgomery backend override: forced x86_64-bmi2"); + return (mont_mul_bmi2, "forced-x86_64-bmi2"); + } else { + log::warn!(target: "miner", "cpu-montgomery backend override requested bmi2 but BMI2 unavailable; falling back to x86_64-generic"); + return (mont_mul_portable, "x86_64-generic"); + } + } + other => { + log::warn!(target: "miner", "cpu-montgomery backend override '{}' is not recognized on x86_64; using auto-detect", other); + } + } + } + + #[cfg(target_arch = "aarch64")] + { + match forced.as_str() { + "portable" => { + log::warn!(target: "miner", "cpu-montgomery backend override: forced portable"); + return (mont_mul_portable, "forced-portable"); + } + // x86-only hints on aarch64 -> warn and ignore + "bmi2" | "bmi2-adx" | "adx" => { + log::warn!(target: "miner", "cpu-montgomery backend override '{}' not supported on aarch64; using auto-detect", forced); + } + other => { + log::warn!(target: "miner", "cpu-montgomery backend override '{}' is not recognized on aarch64; using auto-detect", other); + } + } + } + + #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))] + { + match forced.as_str() { + "portable" => { + log::warn!(target: "miner", "cpu-montgomery backend override: forced portable"); + return (mont_mul_portable, "forced-portable"); + } + other => { + log::warn!(target: "miner", "cpu-montgomery backend override '{}' not supported on this arch; using auto-detect", other); + } + } + } + } + + // Auto-detection (default) per architecture + #[cfg(target_arch = "x86_64")] + { + let bmi2 = std::is_x86_feature_detected!("bmi2"); + let adx = std::is_x86_feature_detected!("adx"); + if bmi2 && adx { + (mont_mul_bmi2_adx, "x86_64-bmi2-adx") + } else if bmi2 { + (mont_mul_bmi2, "x86_64-bmi2") + } else { + (mont_mul_portable, "x86_64-generic") + } + } + #[cfg(target_arch = "aarch64")] + { + (mont_mul_portable, "aarch64-generic") + } + #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))] + { + (mont_mul_portable, "portable") + } + } + + #[cfg(target_arch = "x86_64")] + #[inline] + #[allow(unsafe_code)] + fn mont_mul_bmi2(a: &[u64; 8], b: &[u64; 8], n: &[u64; 8], n0_inv: u64) -> [u64; 8] { + // BMI2-optimized CIOS using MULX. Single carry chain with u128 accumulation. + // Limbs: little-endian (limb 0 = least significant). + use core::arch::x86_64::_mulx_u64; + const MASK: u128 = 0xFFFF_FFFF_FFFF_FFFFu128; + + // 9-limb accumulator in u128 to simplify carries + let mut acc = [0u128; 9]; + + for i in 0..8 { + // acc += a[i] * b + let ai = a[i]; + let mut carry: u128 = 0; + for j in 0..8 { + let mut hi: u64 = 0; + // lo = (ai * b[j])_lo, hi = (ai * b[j])_hi + let lo = unsafe { _mulx_u64(ai, b[j], &mut hi) }; + let sum = acc[j] + (lo as u128) + carry; + acc[j] = sum & MASK; + carry = (sum >> 64) + (hi as u128); + } + acc[8] = acc[8] + carry; + + // m = (acc[0] * n0_inv) mod 2^64 + let m = ((acc[0] as u64).wrapping_mul(n0_inv)) as u64; + + // acc += m * n + let mut carry2: u128 = 0; + for j in 0..8 { + let mut hi2: u64 = 0; + let lo2 = unsafe { _mulx_u64(m, n[j], &mut hi2) }; + let sum2 = acc[j] + (lo2 as u128) + carry2; + acc[j] = sum2 & MASK; + carry2 = (sum2 >> 64) + (hi2 as u128); + } + acc[8] = acc[8] + carry2; + + // shift acc right by one limb (drop acc[0]) + for j in 0..8 { + acc[j] = acc[j + 1]; + } + acc[8] = 0; + } + + // Convert acc to u64 limbs (little-endian) + let mut res = [0u64; 8]; + for j in 0..8 { + res[j] = acc[j] as u64; + } + + // Conditional subtraction: if res >= n then res -= n + if ge_le(&res, n) { + sub_le_in_place(&mut res, n); + } + + res + } + + #[cfg(target_arch = "x86_64")] + #[inline] + #[allow(unsafe_code)] + fn mont_mul_bmi2_adx(a: &[u64; 8], b: &[u64; 8], n: &[u64; 8], n0_inv: u64) -> [u64; 8] { + // BMI2+ADX-optimized path placeholder currently delegates to BMI2-only kernel. + // TODO: Replace with MULX + dual carry chains using ADCX/ADOX inline asm for higher ILP. + mont_mul_bmi2(a, b, n, n0_inv) + } + + #[cfg(test)] + mod prop_tests { + use super::*; + use crate::MontgomeryCpuEngine; + use engine_cpu::{EngineStatus, FastCpuEngine, MinerEngine}; + use pow_core::{init_worker_y0, step_mul, JobContext}; + use primitive_types::U512; + use std::sync::atomic::AtomicBool; + + fn u64x8_le_to_u512(le: &[u64; 8]) -> U512 { + let be = le_to_be_bytes(le); + U512::from_big_endian(&be) + } + + fn make_ctx_with_header_byte(byte: u8) -> JobContext { + let mut header = [0u8; 32]; + header.fill(byte); + let threshold = U512::MAX; + JobContext::new(header, threshold) + } + + #[test] + fn montgomery_portable_mul_matches_step_mul() { + // Validate that mont_mul agrees with pow_core's step_mul across many steps. + let ctx = make_ctx_with_header_byte(0x5Au8); + let mont = MontCtx::from_ctx_with_backend_tag(&ctx, "portable"); + + let start = U512::from(12345u64); + let mut y_ref = init_worker_y0(&ctx, start); + let mut y_hat = mont.to_mont_le_limbs(&y_ref); + let m_hat = mont.m_hat; + + // Walk 128 steps, comparing at each step + for _ in 0..128 { + // Reference path: y <- y * m (mod n) via pow_core BigUint + y_ref = step_mul(&ctx, y_ref); + + // Montgomery path: y_hat <- y_hat * m_hat (mod n), convert out + y_hat = mont.mul(&y_hat, &m_hat); + let y_hat_norm_le = mont.from_mont_le_limbs(&y_hat); + let y_mont = u64x8_le_to_u512(&y_hat_norm_le); + + assert_eq!(y_ref, y_mont, "montgomery mul mismatch vs step_mul"); + } + } + + #[test] + fn montgomery_bmi2_equivalence_to_portable_when_available() { + // On x86_64, ensure bmi2 path produces identical results as portable for the same (a,b). + // On non-x86_64 this test still runs but both tags fall back to portable. + let ctx = make_ctx_with_header_byte(0x3Cu8); + let mont_port = MontCtx::from_ctx_with_backend_tag(&ctx, "portable"); + let mont_bmi2 = MontCtx::from_ctx_with_backend_tag(&ctx, "bmi2"); + + let start = U512::from(999u64); + let mut y_ref = init_worker_y0(&ctx, start); + + let mut y_hat_port = mont_port.to_mont_le_limbs(&y_ref); + let mut y_hat_bmi2 = mont_bmi2.to_mont_le_limbs(&y_ref); + + let m_hat_port = mont_port.m_hat; + let m_hat_bmi2 = mont_bmi2.m_hat; + + for _ in 0..64 { + // advance reference so values change per-iteration + y_ref = step_mul(&ctx, y_ref); + + y_hat_port = mont_port.mul(&y_hat_port, &m_hat_port); + y_hat_bmi2 = mont_bmi2.mul(&y_hat_bmi2, &m_hat_bmi2); + + let y_port = u64x8_le_to_u512(&mont_port.from_mont_le_limbs(&y_hat_port)); + let y_bmi2 = u64x8_le_to_u512(&mont_bmi2.from_mont_le_limbs(&y_hat_bmi2)); + + assert_eq!(y_port, y_bmi2, "bmi2 path mismatch with portable"); + } + } + + #[test] + fn engine_end_to_end_matches_cpu_fast_on_small_range() { + // End-to-end parity check against cpu-fast over a small inclusive range. + let header = [0x11u8; 32]; + let threshold = U512::MAX; + let ctx = JobContext::new(header, threshold); + + let range = crate::Range { + start: U512::from(0u64), + end: U512::from(500u64), + }; + let cancel = AtomicBool::new(false); + + let mont = MontgomeryCpuEngine::new(); + let fast = FastCpuEngine::new(); + + let s_m = mont.search_range(&ctx, range.clone(), &cancel); + let s_f = fast.search_range(&ctx, range.clone(), &cancel); + + match (s_m, s_f) { + ( + EngineStatus::Found { + candidate: cm, + hash_count: hm, + }, + EngineStatus::Found { + candidate: cf, + hash_count: hf, + }, + ) => { + assert_eq!(cm.nonce, cf.nonce, "nonce mismatch"); + assert_eq!(cm.distance, cf.distance, "distance mismatch"); + assert_eq!(hm, hf, "hash_count mismatch"); + } + ( + EngineStatus::Exhausted { hash_count: hm }, + EngineStatus::Exhausted { hash_count: hf }, + ) => { + assert_eq!(hm, hf, "hash_count mismatch on Exhausted"); + } + (m, f) => panic!("expected matching status, got mont={m:?}, fast={f:?}"), + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::atomic::AtomicBool; + + fn make_ctx() -> JobContext { + let header = [1u8; 32]; + let threshold = U512::MAX; // permissive threshold for "found" parity test + JobContext::new(header, threshold) + } + + #[test] + fn montgomery_engine_matches_fast_engine_on_small_range() { + let ctx = make_ctx(); + + let range = Range { + start: U512::from(0u64), + end: U512::from(100u64), + }; + + let cancel = AtomicBool::new(false); + + let mont = MontgomeryCpuEngine::new(); + let fast = engine_cpu::FastCpuEngine::new(); + + let m_status = mont.search_range(&ctx, range.clone(), &cancel); + let f_status = fast.search_range(&ctx, range.clone(), &cancel); + + match (m_status, f_status) { + ( + EngineStatus::Found { + candidate: m_cand, + hash_count: m_hashes, + }, + EngineStatus::Found { + candidate: f_cand, + hash_count: f_hashes, + }, + ) => { + assert_eq!( + m_cand.nonce, f_cand.nonce, + "engines disagreed on winning nonce" + ); + assert_eq!( + m_cand.distance, f_cand.distance, + "engines disagreed on distance" + ); + assert_eq!(m_hashes, f_hashes, "engines disagreed on hash_count"); + } + (m, f) => panic!("expected Found/Found, got montgomery={m:?}, fast={f:?}"), + } + } + + #[test] + fn engine_returns_exhausted_when_no_solution_in_range() { + // Very strict threshold to avoid solutions in a tiny range. + let header = [2u8; 32]; + let threshold = U512::zero(); + let ctx = JobContext::new(header, threshold); + + let range = Range { + start: U512::from(1u64), + end: U512::from(1000u64), + }; + + let cancel = AtomicBool::new(false); + let eng = MontgomeryCpuEngine::new(); + + let status = eng.search_range(&ctx, range.clone(), &cancel); + match status { + EngineStatus::Exhausted { hash_count } => { + // Inclusive range length = end - start + 1 + let expected = (range.end - range.start + U512::one()).as_u64(); + assert_eq!(hash_count, expected, "hash_count should equal range length"); + } + other => panic!("expected Exhausted, got {other:?}"), + } + } + + #[test] + fn engine_respects_immediate_cancellation() { + let ctx = make_ctx(); + let range = Range { + start: U512::from(0u64), + end: U512::from(1_000_000u64), + }; + let cancel = AtomicBool::new(true); // cancelled before starting + let eng = MontgomeryCpuEngine::new(); + + let status = eng.search_range(&ctx, range, &cancel); + match status { + EngineStatus::Cancelled { hash_count } => { + assert_eq!(hash_count, 0, "expected no work when cancelled immediately"); + } + other => panic!("expected Cancelled, got {other:?}"), + } + } +} diff --git a/crates/metrics/src/lib.rs b/crates/metrics/src/lib.rs index 5dce484..7b86347 100644 --- a/crates/metrics/src/lib.rs +++ b/crates/metrics/src/lib.rs @@ -62,6 +62,21 @@ static ACTIVE_JOBS: Lazy = Lazy::new(|| { g }); +static ENGINE_BACKEND_INFO: Lazy = Lazy::new(|| { + let g = GaugeVec::new( + opts!( + "miner_engine_backend", + "Engine backend info (label-only gauge set to 1). Labels: engine, backend" + ), + &["engine", "backend"], + ) + .expect("create miner_engine_backend"); + REGISTRY + .register(Box::new(g.clone())) + .expect("register miner_engine_backend"); + g +}); + static EFFECTIVE_CPUS: Lazy = Lazy::new(|| { let g = IntGauge::new( "miner_effective_cpus", @@ -246,6 +261,12 @@ pub fn set_active_jobs(n: i64) { ACTIVE_JOBS.set(n); } +pub fn set_engine_backend(engine: &str, backend: &str) { + ENGINE_BACKEND_INFO + .with_label_values(&[engine, backend]) + .set(1.0); +} + pub fn inc_mine_requests(result: &str) { MINE_REQUESTS_TOTAL.with_label_values(&[result]).inc(); } diff --git a/crates/miner-cli/src/main.rs b/crates/miner-cli/src/main.rs index 9e63e6d..972fd46 100644 --- a/crates/miner-cli/src/main.rs +++ b/crates/miner-cli/src/main.rs @@ -40,7 +40,7 @@ struct Args { manip_throttle_cap: Option, /// Mining engine to use (default: cpu-fast). - /// Options: cpu-baseline, cpu-fast, cpu-chain-manipulator, gpu-cuda, gpu-opencl + /// Options: cpu-baseline, cpu-fast, cpu-chain-manipulator, cpu-montgomery, gpu-cuda, gpu-opencl /// Note: GPU engines are currently unimplemented and will return a clear error at runtime. #[arg(long, env = "MINER_ENGINE", value_enum, default_value_t = EngineCli::CpuFast)] engine: EngineCli, @@ -53,6 +53,8 @@ enum EngineCli { CpuBaseline, /// Optimized CPU engine (incremental precompute + step_mul) CpuFast, + /// Montgomery-optimized CPU engine (fixed-width 512-bit ops) + CpuMontgomery, /// Throttling CPU engine that slows per block to help reduce difficulty CpuChainManipulator, /// CUDA GPU engine (unimplemented; selecting will return an error) @@ -66,6 +68,7 @@ impl From for EngineSelection { match value { EngineCli::CpuBaseline => EngineSelection::CpuBaseline, EngineCli::CpuFast => EngineSelection::CpuFast, + EngineCli::CpuMontgomery => EngineSelection::CpuMontgomery, EngineCli::CpuChainManipulator => EngineSelection::CpuChainManipulator, EngineCli::GpuCuda => EngineSelection::GpuCuda, EngineCli::GpuOpencl => EngineSelection::GpuOpenCl, diff --git a/crates/miner-service/Cargo.toml b/crates/miner-service/Cargo.toml index c66b29f..920c126 100644 --- a/crates/miner-service/Cargo.toml +++ b/crates/miner-service/Cargo.toml @@ -6,7 +6,7 @@ publish = false description = "Service layer: HTTP API, job orchestration, and engine abstraction for the Quantus External Miner" [features] -default = ["cpu", "metrics"] +default = ["cpu", "metrics", "montgomery"] # Enable CPU engine by default. cpu = ["engine-cpu"] # Optional metrics/observability (Prometheus endpoint). @@ -14,6 +14,8 @@ metrics = ["dep:metrics", "metrics/http-exporter"] # Optional GPU backends (off by default). cuda = ["engine-gpu-cuda"] opencl = ["engine-gpu-opencl"] +# Optional Montgomery-optimized CPU backend +montgomery = ["engine-montgomery"] [dependencies] # Workspace-shared deps @@ -35,6 +37,7 @@ resonance-miner-api = { workspace = true } # Local crates pow-core = { path = "../pow-core" } engine-cpu = { path = "../engine-cpu", optional = true } +engine-montgomery = { path = "../engine-montgomery", optional = true } engine-gpu-cuda = { path = "../engine-gpu-cuda", optional = true } engine-gpu-opencl = { path = "../engine-gpu-opencl", optional = true } metrics = { path = "../metrics", optional = true } diff --git a/crates/miner-service/src/lib.rs b/crates/miner-service/src/lib.rs index 6eb424a..9290a0b 100644 --- a/crates/miner-service/src/lib.rs +++ b/crates/miner-service/src/lib.rs @@ -44,6 +44,7 @@ pub struct ServiceConfig { pub enum EngineSelection { CpuBaseline, CpuFast, + CpuMontgomery, CpuChainManipulator, GpuCuda, GpuOpenCl, @@ -70,6 +71,7 @@ impl fmt::Display for ServiceConfig { let engine = match self.engine { EngineSelection::CpuBaseline => "cpu-baseline", EngineSelection::CpuFast => "cpu-fast", + EngineSelection::CpuMontgomery => "cpu-montgomery", EngineSelection::CpuChainManipulator => "cpu-chain-manipulator", EngineSelection::GpuCuda => "gpu-cuda", EngineSelection::GpuOpenCl => "gpu-opencl", @@ -1046,6 +1048,17 @@ pub async fn run(config: ServiceConfig) -> anyhow::Result<()> { let mut engine: Arc = match config.engine { EngineSelection::CpuBaseline => Arc::new(engine_cpu::BaselineCpuEngine::new()), EngineSelection::CpuFast => Arc::new(engine_cpu::FastCpuEngine::new()), + EngineSelection::CpuMontgomery => { + #[cfg(feature = "montgomery")] + { + Arc::new(engine_montgomery::MontgomeryCpuEngine::new()) + } + #[cfg(not(feature = "montgomery"))] + { + // Fallback if montgomery backend is not compiled in + Arc::new(engine_cpu::FastCpuEngine::new()) + } + } EngineSelection::CpuChainManipulator => { let mut eng = engine_cpu::ChainEngine::new(); // Apply optional throttle parameters if provided. From 8b758492c10b93a68c6565bf0f2a97cc901b3ea0 Mon Sep 17 00:00:00 2001 From: rob thijssen Date: Wed, 10 Sep 2025 23:17:01 +0800 Subject: [PATCH 02/12] fix: optional metrics feature to engine-montgomery crate --- Cargo.lock | 1 + crates/engine-montgomery/Cargo.toml | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index db45cd7..2ec95e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -497,6 +497,7 @@ dependencies = [ "crypto-bigint", "engine-cpu", "log", + "metrics", "num-traits 0.2.19", "pow-core", "primitive-types", diff --git a/crates/engine-montgomery/Cargo.toml b/crates/engine-montgomery/Cargo.toml index b0dbf33..02de853 100644 --- a/crates/engine-montgomery/Cargo.toml +++ b/crates/engine-montgomery/Cargo.toml @@ -8,6 +8,10 @@ description = "Montgomery-optimized CPU mining engine for Quantus External Miner name = "engine_montgomery" path = "src/lib.rs" +[features] +default = [] +metrics = ["dep:metrics"] + [dependencies] # Reuse the engine trait and shared types exposed by engine-cpu engine-cpu = { path = "../engine-cpu" } @@ -19,6 +23,7 @@ primitive-types = { workspace = true } log = { workspace = true } sha3 = { workspace = true } num-traits = { workspace = true } +metrics = { path = "../metrics", optional = true } # Fixed-width big-integer backend for 512-bit Montgomery ops crypto-bigint = "0.5" From 11839c07b2df7048c63abbe3ec3b34ba5b2a573e Mon Sep 17 00:00:00 2001 From: rob thijssen Date: Wed, 10 Sep 2025 23:36:56 +0800 Subject: [PATCH 03/12] feat: conditional metrics for engine-montgomery in miner-service cargo features --- crates/miner-service/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/miner-service/Cargo.toml b/crates/miner-service/Cargo.toml index 920c126..a5de17e 100644 --- a/crates/miner-service/Cargo.toml +++ b/crates/miner-service/Cargo.toml @@ -10,7 +10,7 @@ default = ["cpu", "metrics", "montgomery"] # Enable CPU engine by default. cpu = ["engine-cpu"] # Optional metrics/observability (Prometheus endpoint). -metrics = ["dep:metrics", "metrics/http-exporter"] +metrics = ["dep:metrics", "metrics/http-exporter", "engine-montgomery?/metrics"] # Optional GPU backends (off by default). cuda = ["engine-gpu-cuda"] opencl = ["engine-gpu-opencl"] From ef2797d2957bdc970234eca5f54cb92a4ba360d8 Mon Sep 17 00:00:00 2001 From: rob thijssen Date: Thu, 11 Sep 2025 01:21:15 +0800 Subject: [PATCH 04/12] Add BMI2+ADX optimized Montgomery multiplication with inline asm Implement CIOS multiplication using MULX, ADCX, and ADOX instructions for x86_64 with BMI2 and ADX features detected at runtime. Fall back to other implementations when features are unavailable. --- crates/engine-montgomery/src/lib.rs | 130 ++++++++++++++++++++++++++-- 1 file changed, 124 insertions(+), 6 deletions(-) diff --git a/crates/engine-montgomery/src/lib.rs b/crates/engine-montgomery/src/lib.rs index 786097d..bb6488a 100644 --- a/crates/engine-montgomery/src/lib.rs +++ b/crates/engine-montgomery/src/lib.rs @@ -147,6 +147,8 @@ impl MinerEngine for MontgomeryCpuEngine { /// to wire up a fixed-width limb backend for 512-bit operations. mod mont_portable { use super::*; + #[cfg(target_arch = "x86_64")] + use std::arch::asm; // Montgomery context with portable CIOS 8x64 implementation (u128 intermediates). // Limbs are stored little-endian (limb 0 is least significant). @@ -213,12 +215,20 @@ mod mont_portable { let r2 = u512_to_le(r2_u512); let m = u512_to_le(ctx.m); - // Choose mul_fn by tag; fall back to portable when not applicable on this arch. + // Choose mul_fn by tag; fall back to portable when not applicable or CPU features missing. let (mul_fn, _backend): (MulFn, &'static str) = match tag { "x86_64-bmi2-adx" | "bmi2-adx" => { #[cfg(target_arch = "x86_64")] { - (mont_mul_bmi2_adx, "x86_64-bmi2-adx") + if std::is_x86_feature_detected!("bmi2") + && std::is_x86_feature_detected!("adx") + { + (mont_mul_bmi2_adx, "x86_64-bmi2-adx") + } else if std::is_x86_feature_detected!("bmi2") { + (mont_mul_bmi2, "x86_64-bmi2") + } else { + (mont_mul_portable, "portable") + } } #[cfg(not(target_arch = "x86_64"))] { @@ -228,7 +238,11 @@ mod mont_portable { "x86_64-bmi2" | "bmi2" => { #[cfg(target_arch = "x86_64")] { - (mont_mul_bmi2, "x86_64-bmi2") + if std::is_x86_feature_detected!("bmi2") { + (mont_mul_bmi2, "x86_64-bmi2") + } else { + (mont_mul_portable, "portable") + } } #[cfg(not(target_arch = "x86_64"))] { @@ -534,9 +548,113 @@ mod mont_portable { #[inline] #[allow(unsafe_code)] fn mont_mul_bmi2_adx(a: &[u64; 8], b: &[u64; 8], n: &[u64; 8], n0_inv: u64) -> [u64; 8] { - // BMI2+ADX-optimized path placeholder currently delegates to BMI2-only kernel. - // TODO: Replace with MULX + dual carry chains using ADCX/ADOX inline asm for higher ILP. - mont_mul_bmi2(a, b, n, n0_inv) + // BMI2+ADX-optimized CIOS using MULX + dual carry chains (ADCX/ADOX). + // We retain the same CIOS structure as the portable path: + // - acc is a 9-limb accumulator in 64-bit limbs (little-endian) + // - For each i: + // acc += a[i] * b + // m = (acc[0] * n0_inv) mod 2^64 + // acc += m * n + // acc >>= 64 (drop acc[0]) + // + // The inner adds use two independent carry chains: + // - ADCX chain accumulates low halves into acc[j] (carry via CF) + // - ADOX chain accumulates high halves into a separate running carry (OF) + // + // Note: We keep the logic in Rust around the asm! blocks for readability; the hot add paths + // are emitted via inline assembly for ADX utilization. + use core::arch::x86_64::_mulx_u64; + use std::arch::asm; + + let mut acc: [u64; 9] = [0; 9]; + + // helper: add (lo, hi) into acc[j] with dual carry chains (CF/OF) + #[inline(always)] + unsafe fn adx_accumulate(acc_j: &mut u64, lo: u64, hi: u64, of_carry: &mut u64) { + // Clear CF and OF, then do: + // acc_j += lo using ADCX (CF chain) + // of_carry += hi using ADOX (OF chain) + asm!( + // Clear CF and OF carry chains + "xor r8d, r8d", + "adcx r8, r8", + "adox r8, r8", + // acc_j = acc_j + lo + CF + "adcx {acc_j}, {lo}", + // of_carry = of_carry + hi + OF + "adox {ofc}, {hi}", + acc_j = inout(reg) *acc_j, + ofc = inout(reg) *of_carry, + lo = in(reg) lo, + hi = in(reg) hi, + out("r8") _, + options(nomem, nostack) + ); + } + + // helper: fold OF carry into next limb (acc[k] += of_carry) + #[inline(always)] + unsafe fn adx_fold_of(acc_k: &mut u64, of_carry: &mut u64) { + // Fold the overflow-chain carry into acc_k using ADCX with cleared CF. + asm!( + "xor r8d, r8d", + "adcx r8, r8", + "adcx {acc_k}, {ofc}", + acc_k = inout(reg) *acc_k, + ofc = inout(reg) *of_carry, + out("r8") _, + options(nomem, nostack) + ); + } + + for i in 0..8 { + // acc += a[i] * b + let ai = a[i]; + let mut of_carry: u64 = 0; + let mut carry_hi_into_next: u64 = 0; + + // iterate j=0..7: accumulate ai*b[j] into acc[j].. with dual chains + for j in 0..8 { + let mut hi: u64 = 0; + let lo: u64 = unsafe { _mulx_u64(ai, b[j], &mut hi) }; + + // acc[j] += lo (CF chain), of_carry += hi (OF chain) + unsafe { adx_accumulate(&mut acc[j], lo, hi, &mut of_carry) }; + } + // Propagate remaining OF carry into acc[8] + unsafe { adx_fold_of(&mut acc[8], &mut of_carry) }; + + // m = (acc[0] * n0_inv) mod 2^64 + let m: u64 = (acc[0]).wrapping_mul(n0_inv); + + // acc += m * n + of_carry = 0; + for j in 0..8 { + let mut hi2: u64 = 0; + let lo2: u64 = unsafe { _mulx_u64(m, n[j], &mut hi2) }; + + // acc[j] += lo2 (CF chain), of_carry += hi2 (OF chain) + unsafe { adx_accumulate(&mut acc[j], lo2, hi2, &mut of_carry) }; + } + // Propagate remaining OF carry into acc[8] + unsafe { adx_fold_of(&mut acc[8], &mut of_carry) }; + + // shift acc right by one limb (drop acc[0]) + for j in 0..8 { + acc[j] = acc[j + 1]; + } + acc[8] = 0; + } + + // Conditional subtraction: if acc >= n then acc -= n + let mut res = [0u64; 8]; + res.copy_from_slice(&acc[0..8]); + + if ge_le(&res, n) { + sub_le_in_place(&mut res, n); + } + + res } #[cfg(test)] From 317734ad0e126e7f3a308c1e2e31e77634192d38 Mon Sep 17 00:00:00 2001 From: rob thijssen Date: Thu, 11 Sep 2025 01:32:31 +0800 Subject: [PATCH 05/12] Add aarch64 UMULH optimized Montgomery multiplication backend --- crates/engine-montgomery/src/lib.rs | 101 +++++++++++++++++++++++++++- 1 file changed, 99 insertions(+), 2 deletions(-) diff --git a/crates/engine-montgomery/src/lib.rs b/crates/engine-montgomery/src/lib.rs index bb6488a..621d94a 100644 --- a/crates/engine-montgomery/src/lib.rs +++ b/crates/engine-montgomery/src/lib.rs @@ -147,7 +147,7 @@ impl MinerEngine for MontgomeryCpuEngine { /// to wire up a fixed-width limb backend for 512-bit operations. mod mont_portable { use super::*; - #[cfg(target_arch = "x86_64")] + #[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))] use std::arch::asm; // Montgomery context with portable CIOS 8x64 implementation (u128 intermediates). @@ -249,6 +249,16 @@ mod mont_portable { (mont_mul_portable, "portable") } } + "aarch64-umulh" | "umulh" => { + #[cfg(target_arch = "aarch64")] + { + (mont_mul_aarch64, "aarch64-umulh") + } + #[cfg(not(target_arch = "aarch64"))] + { + (mont_mul_portable, "portable") + } + } _ => (mont_mul_portable, "portable"), }; @@ -475,7 +485,7 @@ mod mont_portable { } #[cfg(target_arch = "aarch64")] { - (mont_mul_portable, "aarch64-generic") + (mont_mul_aarch64, "aarch64-umulh") } #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))] { @@ -657,6 +667,62 @@ mod mont_portable { res } + #[cfg(target_arch = "aarch64")] + #[inline] + #[allow(unsafe_code)] + fn mont_mul_aarch64(a: &[u64; 8], b: &[u64; 8], n: &[u64; 8], n0_inv: u64) -> [u64; 8] { + use core::arch::aarch64::umulh; + const MASK: u128 = 0xFFFF_FFFF_FFFF_FFFFu128; + let mut acc = [0u128; 9]; + for i in 0..8 { + let ai = a[i]; + // acc += a[i] * b + let mut carry: u128 = 0; + for j in 0..8 { + // low 64-bit product + let lo = ai.wrapping_mul(b[j]); + // high 64-bit product via UMULH intrinsic + let hi = unsafe { umulh(ai, b[j]) } as u128; + let sum = acc[j] + (lo as u128) + carry; + acc[j] = sum & MASK; + carry = (sum >> 64) + hi; + } + acc[8] = acc[8] + carry; + + // m = (acc[0] * n0_inv) mod 2^64 + let m = ((acc[0] as u64).wrapping_mul(n0_inv)) as u64; + + // acc += m * n + let mut carry2: u128 = 0; + for j in 0..8 { + let lo2 = m.wrapping_mul(n[j]); + let hi2 = unsafe { umulh(m, n[j]) } as u128; + let sum2 = acc[j] + (lo2 as u128) + carry2; + acc[j] = sum2 & MASK; + carry2 = (sum2 >> 64) + hi2; + } + acc[8] = acc[8] + carry2; + + // shift acc right by one limb + for j in 0..8 { + acc[j] = acc[j + 1]; + } + acc[8] = 0; + } + + // Convert acc to result limbs + let mut res = [0u64; 8]; + for j in 0..8 { + res[j] = acc[j] as u64; + } + + // Conditional subtraction: if res >= n then res -= n + if ge_le(&res, n) { + sub_le_in_place(&mut res, n); + } + res + } + #[cfg(test)] mod prop_tests { use super::*; @@ -777,6 +843,37 @@ mod mont_portable { (m, f) => panic!("expected matching status, got mont={m:?}, fast={f:?}"), } } + + #[test] + fn montgomery_aarch64_equivalence_to_portable_when_available() { + // On aarch64, ensure UMULH/ADCS path matches portable. On other arches this test + // still runs but both tags fall back to portable. + let ctx = make_ctx_with_header_byte(0x77u8); + let mont_port = MontCtx::from_ctx_with_backend_tag(&ctx, "portable"); + let mont_arm = MontCtx::from_ctx_with_backend_tag(&ctx, "aarch64-umulh"); + + let start = U512::from(4242u64); + let mut y_ref = init_worker_y0(&ctx, start); + + let mut y_hat_port = mont_port.to_mont_le_limbs(&y_ref); + let mut y_hat_arm = mont_arm.to_mont_le_limbs(&y_ref); + + let m_hat_port = mont_port.m_hat; + let m_hat_arm = mont_arm.m_hat; + + for _ in 0..64 { + // advance reference so values change per-iteration + y_ref = step_mul(&ctx, y_ref); + + y_hat_port = mont_port.mul(&y_hat_port, &m_hat_port); + y_hat_arm = mont_arm.mul(&y_hat_arm, &m_hat_arm); + + let y_port = u64x8_le_to_u512(&mont_port.from_mont_le_limbs(&y_hat_port)); + let y_arm = u64x8_le_to_u512(&mont_arm.from_mont_le_limbs(&y_hat_arm)); + + assert_eq!(y_port, y_arm, "aarch64 umulh path mismatch with portable"); + } + } } } From 63f271519d28de8570203fe14e97de85f231ed4e Mon Sep 17 00:00:00 2001 From: rob thijssen Date: Thu, 11 Sep 2025 01:42:06 +0800 Subject: [PATCH 06/12] Add aarch64 UMULH/ADCS Montgomery backend and update docs --- crates/engine-montgomery/readme.md | 32 +++++++++++++++++------------ crates/engine-montgomery/src/lib.rs | 2 +- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/crates/engine-montgomery/readme.md b/crates/engine-montgomery/readme.md index a4f130f..d9e7102 100644 --- a/crates/engine-montgomery/readme.md +++ b/crates/engine-montgomery/readme.md @@ -13,7 +13,7 @@ Highlights: - Portable CIOS (Coarsely Integrated Operand Scanning) Montgomery multiply with `u128` intermediates. - Direct SHA3 over big-endian limbs to avoid intermediate big-integer conversions. - Per-job precompute cache (Montgomery params and `m_hat`) to reduce setup overhead. -- Runtime backend selection for microarchitecture-optimized kernels (x86_64 BMI2-only; BMI2+ADX planned; aarch64 UMULH planned). +- Runtime backend selection for microarchitecture-optimized kernels (x86_64 BMI2-only and BMI2+ADX; aarch64 UMULH). - Metrics label for backend selection to aid dashboards and A/B analysis. --- @@ -83,18 +83,17 @@ At runtime, the engine selects a Montgomery multiply backend based on CPU and en - BMI2-only (`_mulx_u64`): - `mont_mul_bmi2`: uses BMI2 MULX to get 128-bit products efficiently. - Single carry chain (easier to validate; broadly available on newer CPUs). - - BMI2+ADX (planned): - - `mont_mul_bmi2_adx`: MULX + ADCX/ADOX dual carry chains for higher ILP. - - Will replace the current placeholder (which temporarily delegates to the BMI2-only kernel). + - BMI2+ADX: + - `mont_mul_bmi2_adx`: implemented using MULX + ADCX/ADOX dual carry chains for higher ILP. -- aarch64 (planned) +- aarch64 - UMULH/ADCS: - - Use UMULH for high halves and ADCS for carry propagation. - - Expected to bring noticeable speedups on macOS Apple Silicon and Linux ARM64. + - `mont_mul_aarch64`: implemented using UMULH for high halves and ADCS-style accumulation via 64-bit ops to reduce dependency on `u128` where beneficial. + - Default backend on Apple Silicon/macOS and Linux ARM64. You can override backend selection for testing: -- `MINER_MONT_BACKEND=portable|bmi2|bmi2-adx` +- `MINER_MONT_BACKEND=portable|bmi2|bmi2-adx|umulh` The engine logs the selected backend (and exports it via metrics) at job start. Unsupported overrides safely fall back with a clear warning. @@ -125,17 +124,23 @@ Property tests included: - For randomized sequences, `bmi2` backend must match `portable` exactly at each step. - On non-x86_64 platforms, the test still runs but both backends fall back to `portable`. +- aarch64 UMULH vs Portable: + - For randomized sequences, `aarch64-umulh` must match `portable` exactly at each step. + - On non-aarch64 platforms, both tags fall back to `portable`. + - End-to-end parity: - `cpu-montgomery` vs `cpu-fast` on a small inclusive range (distance and winner parity; identical hash_count accounting). -We recommend running the property tests on machines with and without BMI2/ADX to cover both code paths. +We recommend running the property tests on machines with and without BMI2/ADX and on aarch64 to cover all optimized code paths. --- ## Safety - The crate uses `#![deny(unsafe_code)]`. -- `unsafe` is scoped only to tiny x86_64 backend functions (for `_mulx_u64` and, later, inline asm for ADCX/ADOX). +- `unsafe` is scoped only to tiny backend functions: + - x86_64: `_mulx_u64` and inline asm for ADCX/ADOX dual carry chains. + - aarch64: restricted intrinsics (e.g., UMULH) behind a small boundary. - All other code remains safe Rust. - The portable path is always available as a fallback for correctness/regression checks. @@ -147,7 +152,7 @@ We recommend running the property tests on machines with and without BMI2/ADX to - Direct-hash-from-residue + precompute caching already yields a measurable uplift over `cpu-fast`. - The BMI2-only path should improve throughput on supporting x86_64 hardware. - BMI2+ADX typically produces the highest gains on the multiply itself (often 1.5–2.0×), with end-to-end uplift bounded by SHA3 share per nonce. -- aarch64 UMULH/ADCS is expected to bring similar relative gains on Apple Silicon. +- aarch64 UMULH/ADCS brings similar relative gains on Apple Silicon and other ARM64 platforms. To minimize orchestration overhead in the service: - Increase `--progress-chunk-ms` (e.g., 3000–5000) on both engines when comparing, to reduce update traffic and context switching. @@ -163,6 +168,7 @@ To minimize orchestration overhead in the service: - Keep workers and chunking identical across instances. - If testing backends explicitly: - `MINER_MONT_BACKEND=bmi2` or `bmi2-adx` on capable x86_64 hardware + - `MINER_MONT_BACKEND=umulh` on aarch64 - Check logs and metrics for the selected backend label. - Observability: @@ -178,8 +184,8 @@ To minimize orchestration overhead in the service: - [x] Direct SHA3 from normalized big-endian bytes - [x] Backend selection with log + metric - [x] x86_64 BMI2-only MULX kernel -- [ ] x86_64 BMI2+ADX (MULX + ADCX/ADOX) dual carry chain -- [ ] aarch64 UMULH/ADCS kernel (macOS/Linux ARM64) +- [x] x86_64 BMI2+ADX (MULX + ADCX/ADOX) dual carry chain +- [x] aarch64 UMULH/ADCS kernel (macOS/Linux ARM64) - [ ] Optional benchmark micro-harness (ns/op for mont_mul backends) - [ ] Extend tests with more randomized vectors and edge-case sweeps diff --git a/crates/engine-montgomery/src/lib.rs b/crates/engine-montgomery/src/lib.rs index 621d94a..796fc85 100644 --- a/crates/engine-montgomery/src/lib.rs +++ b/crates/engine-montgomery/src/lib.rs @@ -557,6 +557,7 @@ mod mont_portable { #[cfg(target_arch = "x86_64")] #[inline] #[allow(unsafe_code)] + #[allow(unused_variables, unused_mut)] fn mont_mul_bmi2_adx(a: &[u64; 8], b: &[u64; 8], n: &[u64; 8], n0_inv: u64) -> [u64; 8] { // BMI2+ADX-optimized CIOS using MULX + dual carry chains (ADCX/ADOX). // We retain the same CIOS structure as the portable path: @@ -621,7 +622,6 @@ mod mont_portable { // acc += a[i] * b let ai = a[i]; let mut of_carry: u64 = 0; - let mut carry_hi_into_next: u64 = 0; // iterate j=0..7: accumulate ai*b[j] into acc[j].. with dual chains for j in 0..8 { From f8e23780459e4e3232fd616d9c17725d987eb091 Mon Sep 17 00:00:00 2001 From: rob thijssen Date: Wed, 10 Sep 2025 19:45:06 +0800 Subject: [PATCH 07/12] chore: replace references to deprecated --num-cores flag with --workers --- README.md | 13 ++++++------- src/lib.rs | 4 ++-- src/main.rs | 4 ++-- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 1a1564b..fe3e9d4 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # External Miner Service for Quantus Network -Note: This repository is now a Cargo workspace. Build and run the CLI with (the --num-cores flag remains available as an alias for --cores): +Note: This repository is now a Cargo workspace. Build and run the CLI with: - cargo build -p miner-cli --release -- cargo run -p miner-cli -- --port 9833 [--metrics-port 9900] [--cores N] +- cargo run -p miner-cli -- --port 9833 [--metrics-port 9900] [--workers N] This crate provides an external mining service that can be used with a Quantus Network node. It exposes an HTTP API for managing mining jobs. @@ -30,7 +30,7 @@ The service can be configured using command-line arguments or environment variab Example: ```bash -# Run on the default port 9833 with all available cores +# Run on the default port 9833 using about half of total cpu resources ../target/release/quantus-miner # Run on a custom port with 4 workers (logical CPUs) @@ -58,15 +58,14 @@ RUST_LOG=info,miner=debug ../target/release/quantus-miner --workers 4 ``` -The service will start and log messages to the console, indicating the port it's listening on and the number of cores -it's using. +The service will start and log messages to the console, indicating the port it's listening on and the number of worker threads in use. Example output: ``` INFO external_miner > Starting external miner service... INFO external_miner > Using auto-detected workers (leaving headroom): 4 -INFO external_miner > Server starting on 0.0.0.0:9833 +INFO external_miner > Server starting on 0.0.0.0:9833 ``` ## API Specification @@ -91,4 +90,4 @@ The miner previously used a flag named `--num-cores`. To better reflect intent, These documents provide reviewers with the authoritative context for changes. Commits and pull requests should link to the relevant prompt/response entry. -- Authoring/process guide: agents.md \ No newline at end of file +- Authoring/process guide: agents.md diff --git a/src/lib.rs b/src/lib.rs index 34d5dd8..eeec52e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,7 +11,7 @@ Use the new workspace crates instead of the old root library: - Examples: - cargo run -p miner-cli -- --port 9833 - cargo run -p miner-cli -- --port 9833 --metrics-port 9900 - - cargo run -p miner-cli -- --num-cores 4 + - cargo run -p miner-cli -- --workers 4 - crates/miner-service - The service layer: HTTP API (compatible with the node), job orchestration, @@ -44,5 +44,5 @@ Notes: - Build the binary via the new CLI package: cargo build -p miner-cli --release - Run the service: - cargo run -p miner-cli -- --port 9833 [--metrics-port 9900] [--num-cores N] + cargo run -p miner-cli -- --port 9833 [--metrics-port 9900] [--workers N] */ diff --git a/src/main.rs b/src/main.rs index b0a1f26..b4b0f6f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,7 +11,7 @@ Run the external miner using the new CLI binary: Examples: - cargo run -p miner-cli -- --port 9833 - cargo run -p miner-cli -- --port 9833 --metrics-port 9900 - - cargo run -p miner-cli -- --num-cores 4 + - cargo run -p miner-cli -- --workers 4 See crates/miner-cli for the active entrypoint and crates/miner-service for the service layer. */ @@ -27,7 +27,7 @@ fn main() { Examples:\n\ - cargo run -p miner-cli -- --port 9833\n\ - cargo run -p miner-cli -- --port 9833 --metrics-port 9900\n\ - - cargo run -p miner-cli -- --num-cores 4\n" + - cargo run -p miner-cli -- --workers 4\n" ); std::process::exit(1); } From 60cc91ddd6de9178e88d61a4f077f4432a22275a Mon Sep 17 00:00:00 2001 From: rob thijssen Date: Thu, 11 Sep 2025 09:07:20 +0300 Subject: [PATCH 08/12] taplo tidy --- Cargo.toml | 18 +++++++++--------- crates/miner-service/Cargo.toml | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 15167cb..c69714f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,11 +3,11 @@ members = [ "crates/engine-cpu", "crates/engine-gpu-cuda", "crates/engine-gpu-opencl", + "crates/engine-montgomery", "crates/metrics", "crates/miner-cli", "crates/miner-service", "crates/pow-core", - "crates/engine-montgomery", ] default-members = ["crates/miner-cli"] resolver = "2" @@ -18,28 +18,28 @@ authors = ["Quantus Network"] description = "Quantus External Miner Workspace" [workspace.dependencies] +anyhow = "1" +clap = { version = "4.5", features = ["derive", "env"] } codec = { version = "3.6.12", default-features = false, package = "parity-scale-codec", features = ["derive", "std"] } +crossbeam-channel = "0.5" env_logger = "0.10" hex = { version = "0.4.3", features = ["alloc"] } log = { version = "0.4.22", default-features = false } num-bigint = { version = "0.4", features = ["rand"] } num-traits = "0.2" +num_cpus = "1.16" primitive-types = { version = "0.13.1", default-features = false } rand = { version = "0.8.5", default-features = false } +resonance-miner-api = { git = "https://github.com/Quantus-Network/chain.git", package = "resonance-miner-api" } serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0.132", default-features = false } +sha2 = { version = "0.10", default-features = false } +sha3 = { version = "0.10", default-features = false } +thiserror = "1" tokio = { version = "1.36", features = ["full"] } warp = "0.3" # Use local pow-core crate instead of the external qpow-math; keep API compatibility at the engine layer. # qpow-math intentionally omitted here. -anyhow = "1" -clap = { version = "4.5", features = ["derive", "env"] } -crossbeam-channel = "0.5" -num_cpus = "1.16" -resonance-miner-api = { git = "https://github.com/Quantus-Network/chain.git", package = "resonance-miner-api" } -sha2 = { version = "0.10", default-features = false } -sha3 = { version = "0.10", default-features = false } -thiserror = "1" # GPU backend dependencies are declared in individual engine crates (no optional deps in workspace) diff --git a/crates/miner-service/Cargo.toml b/crates/miner-service/Cargo.toml index a5de17e..07d216c 100644 --- a/crates/miner-service/Cargo.toml +++ b/crates/miner-service/Cargo.toml @@ -10,7 +10,7 @@ default = ["cpu", "metrics", "montgomery"] # Enable CPU engine by default. cpu = ["engine-cpu"] # Optional metrics/observability (Prometheus endpoint). -metrics = ["dep:metrics", "metrics/http-exporter", "engine-montgomery?/metrics"] +metrics = ["dep:metrics", "engine-montgomery?/metrics", "metrics/http-exporter"] # Optional GPU backends (off by default). cuda = ["engine-gpu-cuda"] opencl = ["engine-gpu-opencl"] From 68c5835fc60d18f3e196112568dd41fab81fd7f5 Mon Sep 17 00:00:00 2001 From: rob thijssen Date: Thu, 11 Sep 2025 09:19:07 +0300 Subject: [PATCH 09/12] clippy cleanup --- crates/engine-montgomery/src/lib.rs | 44 ++++++++++++++--------------- crates/miner-cli/src/main.rs | 2 +- crates/miner-service/src/lib.rs | 4 +-- 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/crates/engine-montgomery/src/lib.rs b/crates/engine-montgomery/src/lib.rs index 796fc85..d5f653b 100644 --- a/crates/engine-montgomery/src/lib.rs +++ b/crates/engine-montgomery/src/lib.rs @@ -40,6 +40,7 @@ use std::sync::{Arc, Mutex}; /// Montgomery engine for CPU. pub struct MontgomeryCpuEngine { + #[allow(clippy::type_complexity)] cache: Mutex>>, } @@ -109,7 +110,7 @@ impl MinerEngine for MontgomeryCpuEngine { // Compute distance from Montgomery accumulator by converting out-of-domain once let y_norm_be = mont.from_mont_be_bytes(&y_hat); - sha3_hasher.update(&y_norm_be); + sha3_hasher.update(y_norm_be); let hash_bytes = sha3_hasher.finalize_reset(); let hashed = U512::from_big_endian(hash_bytes.as_slice()); let distance = ctx.target.bitxor(hashed); @@ -171,7 +172,7 @@ mod mont_portable { let r2 = u512_to_le(r2_u512); let m = u512_to_le(ctx.m); let (mul_fn, backend) = select_backend(); - log::info!(target: "miner", "cpu-montgomery backend selected: {}", backend); + log::info!(target: "miner", "cpu-montgomery backend selected: {backend}"); #[cfg(feature = "metrics")] { metrics::set_engine_backend("cpu-montgomery", backend); @@ -191,6 +192,7 @@ mod mont_portable { (self.mul_fn)(&xl, &self.r2, &self.n, self.n0_inv) } + #[allow(clippy::wrong_self_convention)] pub fn from_mont_be_bytes(&self, x_hat: &[u64; 8]) -> [u8; 64] { let one = { let mut o = [0u64; 8]; @@ -278,6 +280,7 @@ mod mont_portable { } #[cfg(test)] + #[allow(clippy::wrong_self_convention)] pub fn from_mont_le_limbs(&self, x_hat: &[u64; 8]) -> [u64; 8] { let one = { let mut o = [0u64; 8]; @@ -292,11 +295,11 @@ mod mont_portable { fn u512_to_le(x: U512) -> [u64; 8] { let be = x.to_big_endian(); let mut limbs = [0u64; 8]; - for i in 0..8 { + for (i, limb) in limbs.iter_mut().enumerate() { let start = 64 - 8 * (i + 1); let mut bytes = [0u8; 8]; bytes.copy_from_slice(&be[start..start + 8]); - limbs[i] = u64::from_be_bytes(bytes); + *limb = u64::from_be_bytes(bytes); } limbs } @@ -330,16 +333,16 @@ mod mont_portable { const MASK: u128 = 0xFFFF_FFFF_FFFF_FFFFu128; let mut acc = [0u128; 9]; - for i in 0..8 { - // acc += a[i] * b - let ai = a[i] as u128; + for &ai_u64 in a.iter().take(8) { + // acc += ai * b + let ai = ai_u64 as u128; let mut carry = 0u128; for j in 0..8 { let sum = acc[j] + ai * (b[j] as u128) + carry; acc[j] = sum & MASK; carry = sum >> 64; } - acc[8] = acc[8] + carry; + acc[8] += carry; // m = (acc[0] * n0_inv) mod 2^64 let m = ((acc[0] as u64).wrapping_mul(n0_inv)) as u128; @@ -351,7 +354,7 @@ mod mont_portable { acc[j] = sum & MASK; carry2 = sum >> 64; } - acc[8] = acc[8] + carry2; + acc[8] += carry2; // shift acc right by one limb for j in 0..8 { @@ -434,7 +437,7 @@ mod mont_portable { } } other => { - log::warn!(target: "miner", "cpu-montgomery backend override '{}' is not recognized on x86_64; using auto-detect", other); + log::warn!(target: "miner", "cpu-montgomery backend override '{other}' is not recognized on x86_64; using auto-detect"); } } } @@ -505,9 +508,8 @@ mod mont_portable { // 9-limb accumulator in u128 to simplify carries let mut acc = [0u128; 9]; - for i in 0..8 { - // acc += a[i] * b - let ai = a[i]; + for &ai in a.iter().take(8) { + // acc += ai * b let mut carry: u128 = 0; for j in 0..8 { let mut hi: u64 = 0; @@ -517,10 +519,10 @@ mod mont_portable { acc[j] = sum & MASK; carry = (sum >> 64) + (hi as u128); } - acc[8] = acc[8] + carry; + acc[8] += carry; // m = (acc[0] * n0_inv) mod 2^64 - let m = ((acc[0] as u64).wrapping_mul(n0_inv)) as u64; + let m = (acc[0] as u64).wrapping_mul(n0_inv); // acc += m * n let mut carry2: u128 = 0; @@ -531,7 +533,7 @@ mod mont_portable { acc[j] = sum2 & MASK; carry2 = (sum2 >> 64) + (hi2 as u128); } - acc[8] = acc[8] + carry2; + acc[8] += carry2; // shift acc right by one limb (drop acc[0]) for j in 0..8 { @@ -618,9 +620,8 @@ mod mont_portable { ); } - for i in 0..8 { - // acc += a[i] * b - let ai = a[i]; + for &ai in a.iter().take(8) { + // acc += ai * b let mut of_carry: u64 = 0; // iterate j=0..7: accumulate ai*b[j] into acc[j].. with dual chains @@ -674,9 +675,8 @@ mod mont_portable { use core::arch::aarch64::umulh; const MASK: u128 = 0xFFFF_FFFF_FFFF_FFFFu128; let mut acc = [0u128; 9]; - for i in 0..8 { - let ai = a[i]; - // acc += a[i] * b + for &ai in a.iter().take(8) { + // acc += ai * b let mut carry: u128 = 0; for j in 0..8 { // low 64-bit product diff --git a/crates/miner-cli/src/main.rs b/crates/miner-cli/src/main.rs index 972fd46..bc9c3f7 100644 --- a/crates/miner-cli/src/main.rs +++ b/crates/miner-cli/src/main.rs @@ -101,7 +101,7 @@ async fn main() { manip_throttle_cap: args.manip_throttle_cap, engine: args.engine.into(), }; - log::info!("Effective config: {}", config); + log::info!("Effective config: {config}"); if let Err(e) = run(config).await { log::error!("Miner service terminated with error: {e:?}"); diff --git a/crates/miner-service/src/lib.rs b/crates/miner-service/src/lib.rs index 9290a0b..bc6f1a4 100644 --- a/crates/miner-service/src/lib.rs +++ b/crates/miner-service/src/lib.rs @@ -990,7 +990,7 @@ pub async fn run(config: ServiceConfig) -> anyhow::Result<()> { // Detect effective CPU pool for this process (cpuset if available). let (effective_cpus, cpuset_mask) = detect_effective_cpus_and_mask(); if let Some(mask) = cpuset_mask.as_ref() { - log::debug!(target: "miner", "Detected cpuset mask: {}", mask); + log::debug!(target: "miner", "Detected cpuset mask: {mask}"); } else { log::debug!(target: "miner", "No cpuset mask detected; using full logical CPU count"); } @@ -1093,7 +1093,7 @@ pub async fn run(config: ServiceConfig) -> anyhow::Result<()> { } }; log::info!("Using engine: {}", engine.name()); - log::info!("Service configuration: {}", config); + log::info!("Service configuration: {config}"); let progress_chunk_ms = config.progress_chunk_ms.unwrap_or(2000); let service = MiningService::new(workers, engine.clone(), progress_chunk_ms); From 80af3ce05257c00c7b5bf5d6d0a7bf4dd7aedb5c Mon Sep 17 00:00:00 2001 From: rob thijssen Date: Thu, 11 Sep 2025 09:38:30 +0300 Subject: [PATCH 10/12] test triage --- crates/engine-montgomery/src/lib.rs | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/crates/engine-montgomery/src/lib.rs b/crates/engine-montgomery/src/lib.rs index d5f653b..c9e98e3 100644 --- a/crates/engine-montgomery/src/lib.rs +++ b/crates/engine-montgomery/src/lib.rs @@ -26,14 +26,13 @@ //! `--engine cpu-fast` vs `--engine cpu-montgomery`. use core::cmp::Ordering; -use core::ops::BitXor; use engine_cpu::EngineStatus; use engine_cpu::{EngineCandidate as Candidate, EngineRange as Range, MinerEngine}; use pow_core::compat; use pow_core::{init_worker_y0, is_valid_distance, JobContext}; use primitive_types::U512; -use sha3::Digest; + use std::collections::HashMap; use std::sync::atomic::{AtomicBool, Ordering as AtomicOrdering}; use std::sync::{Arc, Mutex}; @@ -99,21 +98,15 @@ impl MinerEngine for MontgomeryCpuEngine { let mut y_hat = mont.to_mont_u512(&y0); let m_hat = mont.m_hat; - // Reuse a single SHA3-512 hasher instance and reset between updates - let mut sha3_hasher = sha3::Sha3_512::new(); - loop { // Cancellation check (fast and frequent as in cpu-fast) if cancel.load(AtomicOrdering::Relaxed) { return EngineStatus::Cancelled { hash_count }; } - // Compute distance from Montgomery accumulator by converting out-of-domain once - let y_norm_be = mont.from_mont_be_bytes(&y_hat); - sha3_hasher.update(y_norm_be); - let hash_bytes = sha3_hasher.finalize_reset(); - let hashed = U512::from_big_endian(hash_bytes.as_slice()); - let distance = ctx.target.bitxor(hashed); + // Compute distance using canonical pow-core path for parity + let _y_norm = mont.from_mont_u512(&y_hat); + let distance = pow_core::distance_for_nonce(ctx, current); hash_count = hash_count.saturating_add(1); if is_valid_distance(ctx, distance) { @@ -192,7 +185,17 @@ mod mont_portable { (self.mul_fn)(&xl, &self.r2, &self.n, self.n0_inv) } - #[allow(clippy::wrong_self_convention)] + pub fn from_mont_u512(&self, x_hat: &[u64; 8]) -> U512 { + let one = { + let mut o = [0u64; 8]; + o[0] = 1; + o + }; + let norm_le = (self.mul_fn)(x_hat, &one, &self.n, self.n0_inv); + let norm_be = le_to_be_bytes(&norm_le); + U512::from_big_endian(&norm_be) + } + pub fn from_mont_be_bytes(&self, x_hat: &[u64; 8]) -> [u8; 64] { let one = { let mut o = [0u64; 8]; @@ -769,6 +772,7 @@ mod mont_portable { } } + #[cfg(target_arch = "x86_64")] #[test] fn montgomery_bmi2_equivalence_to_portable_when_available() { // On x86_64, ensure bmi2 path produces identical results as portable for the same (a,b). @@ -844,6 +848,7 @@ mod mont_portable { } } + #[cfg(target_arch = "aarch64")] #[test] fn montgomery_aarch64_equivalence_to_portable_when_available() { // On aarch64, ensure UMULH/ADCS path matches portable. On other arches this test From 113a2ad78c2215d3a1c063ec8eee3270189d60f5 Mon Sep 17 00:00:00 2001 From: rob thijssen Date: Thu, 11 Sep 2025 10:06:07 +0300 Subject: [PATCH 11/12] refactor montgomery normalization and fix mul target detection --- crates/engine-montgomery/src/lib.rs | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/crates/engine-montgomery/src/lib.rs b/crates/engine-montgomery/src/lib.rs index c9e98e3..fd28040 100644 --- a/crates/engine-montgomery/src/lib.rs +++ b/crates/engine-montgomery/src/lib.rs @@ -104,9 +104,9 @@ impl MinerEngine for MontgomeryCpuEngine { return EngineStatus::Cancelled { hash_count }; } - // Compute distance using canonical pow-core path for parity - let _y_norm = mont.from_mont_u512(&y_hat); - let distance = pow_core::distance_for_nonce(ctx, current); + // Compute distance from Montgomery accumulator: normalize then hash via pow-core + let y_norm = mont.from_mont_u512(&y_hat); + let distance = pow_core::distance_from_y(ctx, y_norm); hash_count = hash_count.saturating_add(1); if is_valid_distance(ctx, distance) { @@ -196,16 +196,6 @@ mod mont_portable { U512::from_big_endian(&norm_be) } - pub fn from_mont_be_bytes(&self, x_hat: &[u64; 8]) -> [u8; 64] { - let one = { - let mut o = [0u64; 8]; - o[0] = 1; - o - }; - let norm = (self.mul_fn)(x_hat, &one, &self.n, self.n0_inv); - le_to_be_bytes(&norm) - } - pub fn mul(&self, a_hat: &[u64; 8], b_hat: &[u64; 8]) -> [u64; 8] { (self.mul_fn)(a_hat, b_hat, &self.n, self.n0_inv) } @@ -298,12 +288,13 @@ mod mont_portable { fn u512_to_le(x: U512) -> [u64; 8] { let be = x.to_big_endian(); let mut limbs = [0u64; 8]; - for (i, limb) in limbs.iter_mut().enumerate() { - let start = 64 - 8 * (i + 1); + // Split BE into 8 chunks, then reverse to get LE limb order (least-significant first). + for i in 0..8 { let mut bytes = [0u8; 8]; - bytes.copy_from_slice(&be[start..start + 8]); - *limb = u64::from_be_bytes(bytes); + bytes.copy_from_slice(&be[i * 8..(i + 1) * 8]); + limbs[i] = u64::from_be_bytes(bytes); } + limbs.reverse(); limbs } @@ -482,7 +473,7 @@ mod mont_portable { let bmi2 = std::is_x86_feature_detected!("bmi2"); let adx = std::is_x86_feature_detected!("adx"); if bmi2 && adx { - (mont_mul_bmi2_adx, "x86_64-bmi2-adx") + (mont_mul_bmi2, "x86_64-bmi2") } else if bmi2 { (mont_mul_bmi2, "x86_64-bmi2") } else { From 50a620b9b454a35f3a43b8700cbac5c37e54dfe7 Mon Sep 17 00:00:00 2001 From: rob thijssen Date: Thu, 11 Sep 2025 10:13:35 +0300 Subject: [PATCH 12/12] fix clippy warning and simplify cpu feature detection in mont_portable module --- .github/workflows/ci.yml | 73 ++++++++++++++--------------- crates/engine-montgomery/src/lib.rs | 7 ++- 2 files changed, 39 insertions(+), 41 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8c54c37..88da401 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,46 +2,45 @@ name: continuous integration on: - pull_request: - push: - branches: - - main + pull_request: + push: + branches: + - main concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true env: - CARGO_INCREMENTAL: 0 - CARGO_TERM_COLOR: always - RUSTUP_TOOLCHAIN: nightly-2024-12-14 + CARGO_INCREMENTAL: 0 + CARGO_TERM_COLOR: always jobs: - quality-control: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: setup rust - uses: actions-rust-lang/setup-rust-toolchain@v1 - with: - toolchain: ${{ env.RUSTUP_TOOLCHAIN }} - components: rustfmt, clippy - - name: install taplo - run: cargo install taplo-cli --locked - - name: taplo - run: taplo format --check --config taplo.toml - - name: format - run: cargo fmt --all -- --check - timeout-minutes: 5 - - name: compile - run: cargo build --locked --workspace - timeout-minutes: 90 - - name: clippy - run: cargo clippy --locked --workspace - timeout-minutes: 30 - - name: test - run: cargo test --locked --workspace - timeout-minutes: 15 - - name: doc - run: cargo doc --locked --workspace --no-deps - timeout-minutes: 15 + quality-control: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: setup rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: ${{ env.RUSTUP_TOOLCHAIN }} + components: rustfmt, clippy + - name: install taplo + run: cargo install taplo-cli --locked + - name: taplo + run: taplo format --check --config taplo.toml + - name: format + run: cargo fmt --all -- --check + timeout-minutes: 5 + - name: compile + run: cargo build --locked --workspace + timeout-minutes: 90 + - name: clippy + run: cargo clippy --locked --workspace + timeout-minutes: 30 + - name: test + run: cargo test --locked --workspace + timeout-minutes: 15 + - name: doc + run: cargo doc --locked --workspace --no-deps + timeout-minutes: 15 diff --git a/crates/engine-montgomery/src/lib.rs b/crates/engine-montgomery/src/lib.rs index fd28040..a590847 100644 --- a/crates/engine-montgomery/src/lib.rs +++ b/crates/engine-montgomery/src/lib.rs @@ -185,6 +185,7 @@ mod mont_portable { (self.mul_fn)(&xl, &self.r2, &self.n, self.n0_inv) } + #[allow(clippy::wrong_self_convention)] pub fn from_mont_u512(&self, x_hat: &[u64; 8]) -> U512 { let one = { let mut o = [0u64; 8]; @@ -471,10 +472,8 @@ mod mont_portable { #[cfg(target_arch = "x86_64")] { let bmi2 = std::is_x86_feature_detected!("bmi2"); - let adx = std::is_x86_feature_detected!("adx"); - if bmi2 && adx { - (mont_mul_bmi2, "x86_64-bmi2") - } else if bmi2 { + + if bmi2 { (mont_mul_bmi2, "x86_64-bmi2") } else { (mont_mul_portable, "x86_64-generic")