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/Cargo.lock b/Cargo.lock index b992786..2ec95e8 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,20 @@ dependencies = [ "thiserror", ] +[[package]] +name = "engine-montgomery" +version = "0.1.0" +dependencies = [ + "crypto-bigint", + "engine-cpu", + "log", + "metrics", + "num-traits 0.2.19", + "pow-core", + "primitive-types", + "sha3", +] + [[package]] name = "enum_primitive" version = "0.1.1" @@ -1047,6 +1071,7 @@ dependencies = [ "engine-cpu", "engine-gpu-cuda", "engine-gpu-opencl", + "engine-montgomery", "hex", "log", "metrics", @@ -1706,6 +1731,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..c69714f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ members = [ "crates/engine-cpu", "crates/engine-gpu-cuda", "crates/engine-gpu-opencl", + "crates/engine-montgomery", "crates/metrics", "crates/miner-cli", "crates/miner-service", @@ -17,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/engine-montgomery/Cargo.toml b/crates/engine-montgomery/Cargo.toml new file mode 100644 index 0000000..02de853 --- /dev/null +++ b/crates/engine-montgomery/Cargo.toml @@ -0,0 +1,29 @@ +[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" + +[features] +default = [] +metrics = ["dep:metrics"] + +[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 } +metrics = { path = "../metrics", optional = 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..d9e7102 --- /dev/null +++ b/crates/engine-montgomery/readme.md @@ -0,0 +1,210 @@ +# 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 and BMI2+ADX; aarch64 UMULH). +- 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: + - `mont_mul_bmi2_adx`: implemented using MULX + ADCX/ADOX dual carry chains for higher ILP. + +- aarch64 + - UMULH/ADCS: + - `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|umulh` + +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`. + +- 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 and on aarch64 to cover all optimized code paths. + +--- + +## Safety + +- The crate uses `#![deny(unsafe_code)]`. +- `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. + +--- + +## 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 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. + +--- + +## 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 + - `MINER_MONT_BACKEND=umulh` on aarch64 + - 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 +- [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 + +--- + +## 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..a590847 --- /dev/null +++ b/crates/engine-montgomery/src/lib.rs @@ -0,0 +1,972 @@ +#![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 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 std::collections::HashMap; +use std::sync::atomic::{AtomicBool, Ordering as AtomicOrdering}; +use std::sync::{Arc, Mutex}; + +/// Montgomery engine for CPU. +pub struct MontgomeryCpuEngine { + #[allow(clippy::type_complexity)] + 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; + + 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: 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) { + 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::*; + #[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))] + use std::arch::asm; + + // 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) + } + + #[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 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 or CPU features missing. + let (mul_fn, _backend): (MulFn, &'static str) = match tag { + "x86_64-bmi2-adx" | "bmi2-adx" => { + #[cfg(target_arch = "x86_64")] + { + 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"))] + { + (mont_mul_portable, "portable") + } + } + "x86_64-bmi2" | "bmi2" => { + #[cfg(target_arch = "x86_64")] + { + if std::is_x86_feature_detected!("bmi2") { + (mont_mul_bmi2, "x86_64-bmi2") + } else { + (mont_mul_portable, "portable") + } + } + #[cfg(not(target_arch = "x86_64"))] + { + (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"), + }; + + 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)] + #[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]; + 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]; + // 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[i * 8..(i + 1) * 8]); + limbs[i] = u64::from_be_bytes(bytes); + } + limbs.reverse(); + 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 &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] += 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] += 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 '{other}' is not recognized on x86_64; using auto-detect"); + } + } + } + + #[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"); + + if bmi2 { + (mont_mul_bmi2, "x86_64-bmi2") + } else { + (mont_mul_portable, "x86_64-generic") + } + } + #[cfg(target_arch = "aarch64")] + { + (mont_mul_aarch64, "aarch64-umulh") + } + #[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 &ai in a.iter().take(8) { + // acc += ai * b + 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] += carry; + + // m = (acc[0] * n0_inv) mod 2^64 + let m = (acc[0] as u64).wrapping_mul(n0_inv); + + // 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] += 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)] + #[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: + // - 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 &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 + 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(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 &ai in a.iter().take(8) { + // acc += ai * 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::*; + 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"); + } + } + + #[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). + // 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(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 + // 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"); + } + } + } +} + +#[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..bc9c3f7 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, @@ -98,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/Cargo.toml b/crates/miner-service/Cargo.toml index c66b29f..07d216c 100644 --- a/crates/miner-service/Cargo.toml +++ b/crates/miner-service/Cargo.toml @@ -6,14 +6,16 @@ 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). -metrics = ["dep:metrics", "metrics/http-exporter"] +metrics = ["dep:metrics", "engine-montgomery?/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..bc6f1a4 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", @@ -988,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"); } @@ -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. @@ -1080,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);