Merge pull request #3 from Quantus-Network/feat/montgomery
fast modular arithmetic (montgomery multiplication)
This commit is contained in:
+36
-37
@@ -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
|
||||
|
||||
Generated
+31
@@ -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"
|
||||
|
||||
+9
-8
@@ -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)
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -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="<name>"} = 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.
|
||||
@@ -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<HashMap<([u8; 64], [u8; 64]), Arc<mont_portable::MontCtx>>>,
|
||||
}
|
||||
|
||||
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:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,21 @@ static ACTIVE_JOBS: Lazy<IntGauge> = Lazy::new(|| {
|
||||
g
|
||||
});
|
||||
|
||||
static ENGINE_BACKEND_INFO: Lazy<GaugeVec> = 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<IntGauge> = 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();
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ struct Args {
|
||||
manip_throttle_cap: Option<u64>,
|
||||
|
||||
/// 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<EngineCli> 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:?}");
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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<dyn MinerEngine> = 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);
|
||||
|
||||
Reference in New Issue
Block a user