13 Commits

Author SHA1 Message Date
Nikolaus Heger
aa9e7ca5f2 Merge pull request #23 from Quantus-Network/release/v1.0.0
Release v1.0.0

CUDA is failing but we don't support GPU mining in this release anyway.
2025-11-07 14:52:59 +08:00
n13
6902c650a9 bump version to v1.0.0 2025-11-07 06:46:28 +00:00
Nikolaus Heger
e0f7beef74 make major update actually work
remove weird special case for 0.x.x
2025-11-07 14:44:55 +08:00
Nikolaus Heger
e92c4f0769 update comment 2025-11-07 14:40:13 +08:00
Nikolaus Heger
06ddee497d remove montgomery from CI 2025-11-07 14:34:28 +08:00
illuzen
63d7e114c2 Support new pow algo on cpu, not gpu (#22)
* support new pow algo, remove montgomery

* format

* remove redundant check

* use local poseidon until we release

* back to release

* remove unnecessary fxn

* comment out cuda for now

* comment out open-cl

* fix more tests

* remove cuda jobs
2025-11-03 16:23:18 +08:00
Nikolaus Heger
57f69b6aae Merge pull request #21 from Quantus-Network/illuzen/use-qpow-math
use qpow-math package
2025-10-16 16:33:21 +08:00
Nikolaus Heger
f35660c9e9 clippy 2025-10-16 16:03:25 +08:00
Nikolaus Heger
3466eff8b0 fmt 2025-10-16 16:01:10 +08:00
illuzen
c0bfca592d added benchmarks 2025-10-13 23:10:41 +08:00
illuzen
6912470e84 removed unused files 2025-10-13 22:00:38 +08:00
illuzen
534b3ac2f8 qpow-math from git 2025-10-13 21:53:11 +08:00
illuzen
ff3e65361a remove qpow reimplementation 2025-10-13 21:36:44 +08:00
21 changed files with 2177 additions and 3534 deletions

View File

@@ -1,83 +1,83 @@
name: Cuda Docker Image Builder
# name: Cuda Docker Image Builder
on:
workflow_dispatch:
push:
branches:
- main
paths:
- cuda-builder.cf
pull_request:
branches:
- main
paths:
- cuda-builder.cf
# on:
# workflow_dispatch:
# push:
# branches:
# - main
# paths:
# - cuda-builder.cf
# pull_request:
# branches:
# - main
# paths:
# - cuda-builder.cf
permissions:
contents: read
packages: write
# permissions:
# contents: read
# packages: write
jobs:
build-and-push:
name: cuda-builder:${{ matrix.cuda_tag }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
cuda_tag:
- 12.9.0
- 13.0.0
steps:
- shell: bash
run: |
echo "IMAGE_NAME=ghcr.io/${GITHUB_REPOSITORY_OWNER,,}/cuda-builder" >> "$GITHUB_ENV"
# jobs:
# build-and-push:
# name: cuda-builder:${{ matrix.cuda_tag }}
# runs-on: ubuntu-latest
# strategy:
# fail-fast: false
# matrix:
# cuda_tag:
# - 12.9.0
# - 13.0.0
# steps:
# - shell: bash
# run: |
# echo "IMAGE_NAME=ghcr.io/${GITHUB_REPOSITORY_OWNER,,}/cuda-builder" >> "$GITHUB_ENV"
- name: Checkout repository
uses: actions/checkout@v4
# - name: Checkout repository
# uses: actions/checkout@v4
- uses: ./.github/actions/disk
# - uses: ./.github/actions/disk
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# - name: Set up Docker Buildx
# uses: docker/setup-buildx-action@v3
- name: Prepare Buildx cache
uses: actions/cache@v4
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-cuda-builder-${{ matrix.cuda_tag }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-cuda-builder-${{ matrix.cuda_tag }}-
# - name: Prepare Buildx cache
# uses: actions/cache@v4
# with:
# path: /tmp/.buildx-cache
# key: ${{ runner.os }}-buildx-cuda-builder-${{ matrix.cuda_tag }}-${{ github.sha }}
# restore-keys: |
# ${{ runner.os }}-buildx-cuda-builder-${{ matrix.cuda_tag }}-
- name: Log in to GHCR
if: github.event_name == 'push'
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
# - name: Log in to GHCR
# if: github.event_name == 'push'
# uses: docker/login-action@v3
# with:
# registry: ghcr.io
# username: ${{ github.repository_owner }}
# password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and (conditionally) push cuda-builder image
uses: docker/build-push-action@v5
with:
context: .
file: cuda-builder.cf
build-args: |
CUDA_TAG=${{ matrix.cuda_tag }}
pull: true
push: ${{ github.event_name == 'push' }}
tags: |
${{ env.IMAGE_NAME }}:${{ matrix.cuda_tag }}
labels: |
org.opencontainers.image.source=${{ github.repository }}
org.opencontainers.image.description=Pristine CUDA builder image (CUDA ${{ matrix.cuda_tag }})
org.opencontainers.image.licenses=Apache-2.0
org.opencontainers.image.title=cuda-builder
org.opencontainers.image.vendor=Quantus Network
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
provenance: false
# - name: Build and (conditionally) push cuda-builder image
# uses: docker/build-push-action@v5
# with:
# context: .
# file: cuda-builder.cf
# build-args: |
# CUDA_TAG=${{ matrix.cuda_tag }}
# pull: true
# push: ${{ github.event_name == 'push' }}
# tags: |
# ${{ env.IMAGE_NAME }}:${{ matrix.cuda_tag }}
# labels: |
# org.opencontainers.image.source=${{ github.repository }}
# org.opencontainers.image.description=Pristine CUDA builder image (CUDA ${{ matrix.cuda_tag }})
# org.opencontainers.image.licenses=Apache-2.0
# org.opencontainers.image.title=cuda-builder
# org.opencontainers.image.vendor=Quantus Network
# cache-from: type=local,src=/tmp/.buildx-cache
# cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
# provenance: false
- name: Move Buildx cache
run: |
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
# - name: Move Buildx cache
# run: |
# rm -rf /tmp/.buildx-cache
# mv /tmp/.buildx-cache-new /tmp/.buildx-cache

View File

@@ -96,15 +96,9 @@ jobs:
# Increment based on type
if [[ "$VERSION_TYPE" == "major" ]]; then
if [[ "$major" == "0" ]]; then # Handle 0.x.y -> 0.(x+1).0
major=$major
minor=$((minor + 1))
patch=0
else
major=$((major + 1))
minor=0
patch=0
fi
major=$((major + 1))
minor=0
patch=0
elif [[ "$VERSION_TYPE" == "minor" ]]; then
minor=$((minor + 1))
patch=0
@@ -166,7 +160,6 @@ jobs:
cargo update -p engine-cpu --precise "$new_cargo_version"
cargo update -p engine-gpu-cuda --precise "$new_cargo_version"
cargo update -p engine-gpu-opencl --precise "$new_cargo_version"
cargo update -p engine-montgomery --precise "$new_cargo_version"
# Verify everything compiles correctly
cargo check --workspace

273
Cargo.lock generated
View File

@@ -26,6 +26,12 @@ dependencies = [
"memchr",
]
[[package]]
name = "anes"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
[[package]]
name = "anstream"
version = "0.6.20"
@@ -187,6 +193,12 @@ version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
[[package]]
name = "cast"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
name = "cc"
version = "1.2.38"
@@ -203,6 +215,33 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
[[package]]
name = "ciborium"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
dependencies = [
"ciborium-io",
"ciborium-ll",
"serde",
]
[[package]]
name = "ciborium-io"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
[[package]]
name = "ciborium-ll"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
dependencies = [
"ciborium-io",
"half",
]
[[package]]
name = "cl-sys"
version = "0.4.3"
@@ -287,6 +326,42 @@ dependencies = [
"libc",
]
[[package]]
name = "criterion"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f"
dependencies = [
"anes",
"cast",
"ciborium",
"clap",
"criterion-plot",
"is-terminal",
"itertools 0.10.5",
"num-traits 0.2.19",
"once_cell",
"oorandom",
"plotters",
"rayon",
"regex",
"serde",
"serde_derive",
"serde_json",
"tinytemplate",
"walkdir",
]
[[package]]
name = "criterion-plot"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
dependencies = [
"cast",
"itertools 0.10.5",
]
[[package]]
name = "crossbeam"
version = "0.8.4"
@@ -349,16 +424,6 @@ version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]]
name = "crypto-bigint"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
dependencies = [
"rand_core 0.6.4",
"subtle",
]
[[package]]
name = "crypto-common"
version = "0.1.6"
@@ -477,19 +542,21 @@ dependencies = [
[[package]]
name = "engine-cpu"
version = "0.3.0"
version = "1.0.0"
dependencies = [
"anyhow",
"criterion",
"hex",
"log",
"pow-core",
"primitive-types",
"rand 0.8.5",
"thiserror",
]
[[package]]
name = "engine-gpu-cuda"
version = "0.3.0"
version = "1.0.0"
dependencies = [
"anyhow",
"cust",
@@ -508,7 +575,7 @@ dependencies = [
[[package]]
name = "engine-gpu-opencl"
version = "0.3.0"
version = "1.0.0"
dependencies = [
"anyhow",
"log",
@@ -518,19 +585,6 @@ dependencies = [
"thiserror",
]
[[package]]
name = "engine-montgomery"
version = "0.3.0"
dependencies = [
"crypto-bigint",
"engine-cpu",
"log",
"metrics",
"num-traits 0.2.19",
"pow-core",
"primitive-types",
]
[[package]]
name = "enum_primitive"
version = "0.1.1"
@@ -775,6 +829,17 @@ dependencies = [
"tracing",
]
[[package]]
name = "half"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e54c115d4f30f52c67202f079c5f9d8b49db4691f460fdb0b4c2e838261b2ba5"
dependencies = [
"cfg-if",
"crunchy",
"zerocopy",
]
[[package]]
name = "hashbrown"
version = "0.16.0"
@@ -1063,6 +1128,15 @@ version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "itertools"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.14.0"
@@ -1136,7 +1210,7 @@ checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
[[package]]
name = "metrics"
version = "0.3.0"
version = "1.0.0"
dependencies = [
"anyhow",
"log",
@@ -1166,7 +1240,7 @@ dependencies = [
[[package]]
name = "miner-cli"
version = "0.3.0"
version = "1.0.0"
dependencies = [
"clap",
"env_logger",
@@ -1177,14 +1251,13 @@ dependencies = [
[[package]]
name = "miner-service"
version = "0.3.0"
version = "1.0.0"
dependencies = [
"anyhow",
"crossbeam-channel",
"engine-cpu",
"engine-gpu-cuda",
"engine-gpu-opencl",
"engine-montgomery",
"hex",
"log",
"metrics",
@@ -1202,7 +1275,7 @@ dependencies = [
[[package]]
name = "miner-telemetry"
version = "0.3.0"
version = "1.0.0"
dependencies = [
"anyhow",
"futures 0.3.31",
@@ -1383,13 +1456,19 @@ version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
[[package]]
name = "oorandom"
version = "11.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
[[package]]
name = "p3-dft"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3b2764a3982d22d62aa933c8de6f9d71d8a474c9110b69e675dea1887bdeffc"
dependencies = [
"itertools",
"itertools 0.14.0",
"p3-field",
"p3-matrix",
"p3-maybe-rayon",
@@ -1403,7 +1482,7 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc13a73509fe09c67b339951ca8d4cc6e61c9bf08c130dbc90dda52452918cc2"
dependencies = [
"itertools",
"itertools 0.14.0",
"num-bigint",
"p3-maybe-rayon",
"p3-util",
@@ -1437,7 +1516,7 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8e1e9f69c2fe15768b3ceb2915edb88c47398aa22c485d8163deab2a47fe194"
dependencies = [
"itertools",
"itertools 0.14.0",
"p3-field",
"p3-maybe-rayon",
"p3-util",
@@ -1485,7 +1564,7 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72d5db8f05a26d706dfd8aaf7aa4272ca4f3e7a075db897ec7108f24fad78759"
dependencies = [
"itertools",
"itertools 0.14.0",
"p3-field",
"serde",
]
@@ -1594,6 +1673,34 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "plotters"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747"
dependencies = [
"num-traits 0.2.19",
"plotters-backend",
"plotters-svg",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "plotters-backend"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a"
[[package]]
name = "plotters-svg"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670"
dependencies = [
"plotters-backend",
]
[[package]]
name = "potential_utf"
version = "0.1.3"
@@ -1605,7 +1712,7 @@ dependencies = [
[[package]]
name = "pow-core"
version = "0.3.0"
version = "1.0.0"
dependencies = [
"anyhow",
"hex",
@@ -1614,6 +1721,7 @@ dependencies = [
"num-traits 0.2.19",
"primitive-types",
"qp-poseidon-core",
"qpow-math",
"thiserror",
]
@@ -1670,19 +1778,45 @@ dependencies = [
]
[[package]]
name = "qp-poseidon-core"
version = "0.9.5"
name = "qp-poseidon-constants"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec326fc2631a929de09a38af2613a3db5230882c12a2f68205693ec632751e8b"
checksum = "4d56b56652e9f44a43de9593e75d7c3e0c3a352e10675cf3024e5b3175711cd3"
dependencies = [
"p3-field",
"p3-goldilocks",
"p3-poseidon2",
"rand 0.9.2",
"rand_chacha 0.9.0",
]
[[package]]
name = "qp-poseidon-core"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e658a373a7fb22babeda9ffcc8af0a894e6e3c008272ed735509eccb7769ead3"
dependencies = [
"p3-field",
"p3-goldilocks",
"p3-poseidon2",
"p3-symmetric",
"rand 0.9.2",
"qp-poseidon-constants",
"rand_chacha 0.9.0",
]
[[package]]
name = "qpow-math"
version = "0.1.0"
source = "git+https://github.com/Quantus-Network/chain.git#a5b9e4cc6900641884171a9972e0276ebb73a3ab"
dependencies = [
"hex",
"log",
"num-bigint",
"num-traits 0.2.19",
"primitive-types",
"qp-poseidon-core",
]
[[package]]
name = "quantus-miner-api"
version = "0.0.3"
@@ -1782,6 +1916,26 @@ dependencies = [
"getrandom 0.3.3",
]
[[package]]
name = "rayon"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
]
[[package]]
name = "redox_syscall"
version = "0.5.17"
@@ -1929,6 +2083,15 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "scoped-tls"
version = "1.0.1"
@@ -2174,6 +2337,16 @@ dependencies = [
"zerovec",
]
[[package]]
name = "tinytemplate"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
dependencies = [
"serde",
"serde_json",
]
[[package]]
name = "tokio"
version = "1.47.1"
@@ -2481,6 +2654,16 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "want"
version = "0.3.1"
@@ -2602,6 +2785,16 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "web-sys"
version = "0.3.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbe734895e869dc429d78c4b433f8d17d95f8d05317440b4fad5ab2d33e596dc"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "webpki-roots"
version = "0.26.11"

View File

@@ -3,7 +3,6 @@ members = [
"crates/engine-cpu",
"crates/engine-gpu-cuda",
"crates/engine-gpu-opencl",
"crates/engine-montgomery",
"crates/metrics",
"crates/miner-cli",
"crates/miner-service",
@@ -17,7 +16,7 @@ resolver = "2"
edition = "2021"
authors = ["Quantus Network"]
description = "Quantus External Miner Workspace"
version = "0.3.0"
version = "1.0.0"
[workspace.dependencies]
anyhow = "1"
@@ -31,7 +30,8 @@ num-bigint = { version = "0.4", features = ["rand"] }
num-traits = "0.2"
num_cpus = "1.16"
primitive-types = { version = "0.13.1", default-features = false }
qp-poseidon-core = { version = "0.9.5", default-features = false }
qp-poseidon-core = { version = "1.0.1", default-features = false }
qpow-math = { git = "https://github.com/Quantus-Network/chain.git", package = "qpow-math", default-features = false }
quantus-miner-api = { version = "0.0.3", default-features = false }
rand = { version = "0.8.5", default-features = false }
serde = { version = "1.0", features = ["derive"] }
@@ -39,8 +39,7 @@ serde_json = { version = "1.0.132", 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.
# pow-core now delegates to qpow-math for core functionality while providing optimized mining scaffolding.
# GPU backend dependencies are declared in individual engine crates (no optional deps in workspace)

View File

@@ -27,3 +27,9 @@ anyhow = { workspace = true }
[dev-dependencies]
hex = { workspace = true }
criterion = "0.5"
rand = { workspace = true, features = ["std", "std_rng"] }
[[bench]]
name = "cpu_engine_bench"
harness = false

View File

@@ -0,0 +1,56 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use engine_cpu::{FastCpuEngine, MinerEngine, Range};
use pow_core::{hash_from_nonce, JobContext};
use primitive_types::U512;
use rand::RngCore;
use std::sync::atomic::AtomicBool;
fn bench_cpu_fast_engine(c: &mut Criterion) {
// Create the engine
let engine = FastCpuEngine::new();
let cancel_flag = AtomicBool::new(false);
let large_range = Range {
start: U512::from(1000u64),
end: U512::from(101000u64), // 100,000 nonces
};
c.bench_function("cpu_fast_large_range", |b| {
b.iter(|| {
let mut header = [0u8; 32];
rand::thread_rng().fill_bytes(&mut header);
let difficulty = U512::from(1000u64);
let ctx = JobContext::new(header, difficulty);
let result = engine.search_range(
black_box(&ctx),
black_box(large_range.clone()),
black_box(&cancel_flag),
);
black_box(result)
})
});
}
fn bench_hash_from_nonce(c: &mut Criterion) {
// Create a test job context
let mut header = [0u8; 32];
rand::thread_rng().fill_bytes(&mut header);
let difficulty = U512::from(1000u64);
let ctx = JobContext::new(header, difficulty);
// Create some test nonce values
let test_nonce_values: Vec<U512> = (0..100).map(|i| U512::from(1000u64 + i)).collect();
c.bench_function("hash_from_nonce_single", |b| {
let mut i = 0;
b.iter(|| {
let nonce = test_nonce_values[i % test_nonce_values.len()];
i += 1;
let hash = hash_from_nonce(black_box(&ctx), black_box(nonce));
black_box(hash)
})
});
}
criterion_group!(benches, bench_cpu_fast_engine, bench_hash_from_nonce);
criterion_main!(benches);

View File

@@ -14,7 +14,7 @@
use core::cmp::Ordering;
use pow_core::{distance_for_nonce, is_valid_distance, JobContext};
use pow_core::{is_valid_nonce, JobContext};
use primitive_types::U512;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering as AtomicOrdering};
use std::time::Duration;
@@ -31,7 +31,7 @@ pub struct Range {
pub struct Candidate {
pub nonce: U512,
pub work: [u8; 64], // big-endian representation of nonce
pub distance: U512, // achieved distance for this nonce
pub hash: U512, // output hash for this nonce
}
/// Origin of a found candidate.
@@ -78,8 +78,8 @@ pub trait MinerEngine: Send + Sync {
/// Human-readable engine name (for logs/metrics).
fn name(&self) -> &'static str;
/// Prepare a precomputed context for a job (header + threshold).
fn prepare_context(&self, header_hash: [u8; 32], threshold: U512) -> JobContext;
/// Prepare a precomputed context for a job (header + difficulty).
fn prepare_context(&self, header_hash: [u8; 32], difficulty: U512) -> JobContext;
/// Search an inclusive nonce range with cancellation support.
///
@@ -109,8 +109,8 @@ impl MinerEngine for BaselineCpuEngine {
"cpu-baseline"
}
fn prepare_context(&self, header_hash: [u8; 32], threshold: U512) -> JobContext {
JobContext::new(header_hash, threshold)
fn prepare_context(&self, header_hash: [u8; 32], difficulty: U512) -> JobContext {
JobContext::new(header_hash, difficulty)
}
fn search_range(&self, ctx: &JobContext, range: Range, cancel: &AtomicBool) -> EngineStatus {
@@ -128,17 +128,18 @@ impl MinerEngine for BaselineCpuEngine {
return EngineStatus::Cancelled { hash_count };
}
// Compute distance for this nonce using the context.
let distance = distance_for_nonce(ctx, current);
// Compute hash for this nonce using Bitcoin-style double Poseidon2
let (is_valid, hash) =
is_valid_nonce(ctx.header, current.to_big_endian(), ctx.difficulty);
hash_count = hash_count.saturating_add(1);
// Check if it's valid under threshold.
if is_valid_distance(ctx, distance) {
// Check if it meets difficulty target
if is_valid {
let work = current.to_big_endian();
let candidate = Candidate {
nonce: current,
work,
distance,
hash,
};
return EngineStatus::Found {
candidate,
@@ -178,12 +179,12 @@ impl MinerEngine for FastCpuEngine {
"cpu-fast"
}
fn prepare_context(&self, header_hash: [u8; 32], threshold: U512) -> JobContext {
JobContext::new(header_hash, threshold)
fn prepare_context(&self, header_hash: [u8; 32], difficulty: U512) -> JobContext {
JobContext::new(header_hash, difficulty)
}
fn search_range(&self, ctx: &JobContext, range: Range, cancel: &AtomicBool) -> EngineStatus {
use pow_core::{distance_from_y, init_worker_y0, is_valid_distance, step_mul};
use pow_core::{hash_from_nonce, is_valid_hash, step_nonce};
// Ensure start <= end (inclusive range). If not, treat as exhausted.
if range.start > range.end {
@@ -191,7 +192,6 @@ impl MinerEngine for FastCpuEngine {
}
let mut current = range.start;
let mut y = init_worker_y0(ctx, current);
let mut hash_count: u64 = 0;
loop {
@@ -200,17 +200,17 @@ impl MinerEngine for FastCpuEngine {
return EngineStatus::Cancelled { hash_count };
}
// Compute distance from current accumulator
let distance = distance_from_y(ctx, y);
// Compute hash using Bitcoin-style double Poseidon2
let hash = hash_from_nonce(ctx, current);
hash_count = hash_count.saturating_add(1);
if is_valid_distance(ctx, distance) {
if is_valid_hash(ctx, hash) {
let work = current.to_big_endian();
return EngineStatus::Found {
candidate: Candidate {
nonce: current,
work,
distance,
hash,
},
hash_count,
origin: FoundOrigin::Cpu,
@@ -218,13 +218,14 @@ impl MinerEngine for FastCpuEngine {
}
if current == range.end {
break EngineStatus::Exhausted { hash_count };
break;
}
// Advance to next nonce: y <- y * m (mod n), current <- current + 1
y = step_mul(ctx, y);
current = current.saturating_add(U512::one());
// Advance to next nonce
current = step_nonce(current);
}
EngineStatus::Exhausted { hash_count }
}
}
@@ -259,9 +260,9 @@ impl MinerEngine for ChainManipulatorEngine {
"cpu-chain-manipulator"
}
fn prepare_context(&self, header_hash: [u8; 32], threshold: U512) -> JobContext {
fn prepare_context(&self, header_hash: [u8; 32], difficulty: U512) -> JobContext {
// Per-block throttling: do NOT increment here. We increment on Found (i.e., when a block is solved).
let ctx = JobContext::new(header_hash, threshold);
let ctx = JobContext::new(header_hash, difficulty);
// Debug: log current throttle state at job start
log::debug!(
target: "miner",
@@ -274,16 +275,14 @@ impl MinerEngine for ChainManipulatorEngine {
}
fn search_range(&self, ctx: &JobContext, range: Range, cancel: &AtomicBool) -> EngineStatus {
use pow_core::{distance_from_y, init_worker_y0, is_valid_distance, step_mul};
use pow_core::{hash_from_nonce, is_valid_hash, step_nonce};
if range.start > range.end {
return EngineStatus::Exhausted { hash_count: 0 };
}
// Fast incremental path (same as cpu-fast) to start with high hashrate:
// y0 = m^(h + start_nonce) mod n, then y = y * m (mod n) each step.
// Bitcoin-style hashing path
let mut current = range.start;
let mut y = init_worker_y0(ctx, current);
let mut hash_count: u64 = 0;
let mut batch_counter: u64 = 0;
@@ -302,8 +301,8 @@ impl MinerEngine for ChainManipulatorEngine {
return EngineStatus::Cancelled { hash_count };
}
// Compute distance from current accumulator (incremental path).
let distance = distance_from_y(ctx, y);
// Compute hash using Bitcoin-style double Poseidon2
let hash = hash_from_nonce(ctx, current);
hash_count = hash_count.saturating_add(1);
batch_counter = batch_counter.saturating_add(1);
@@ -311,7 +310,7 @@ impl MinerEngine for ChainManipulatorEngine {
#[allow(unused_variables)]
let _dbg_batch = batch_counter;
if is_valid_distance(ctx, distance) {
if is_valid_hash(ctx, hash) {
let work = current.to_big_endian();
// Increment solved-block counter so the NEXT block throttles more.
let _new_idx = self.job_index.fetch_add(1, AtomicOrdering::Relaxed) + 1;
@@ -333,7 +332,7 @@ impl MinerEngine for ChainManipulatorEngine {
candidate: Candidate {
nonce: current,
work,
distance,
hash,
},
hash_count,
origin: FoundOrigin::Cpu,
@@ -348,9 +347,7 @@ impl MinerEngine for ChainManipulatorEngine {
// Advance
if current < range.end {
// y <- y * m (mod n); current <- current + 1
y = step_mul(ctx, y);
current = current.saturating_add(U512::one());
current = step_nonce(current);
} else {
break EngineStatus::Exhausted { hash_count };
}
@@ -371,60 +368,16 @@ mod tests {
fn make_ctx() -> JobContext {
let header = [1u8; 32];
let threshold = U512::MAX; // permissive threshold for "found" parity test
JobContext::new(header, threshold)
}
#[test]
fn baseline_and_fast_engines_find_same_candidate_on_small_range() {
let ctx = make_ctx();
let range = Range {
start: U512::from(0u64),
end: U512::from(100u64),
};
let cancel = AtomicBool::new(false);
let baseline = BaselineCpuEngine::new();
let fast = FastCpuEngine::new();
let b_status = baseline.search_range(&ctx, range.clone(), &cancel);
let f_status = fast.search_range(&ctx, range.clone(), &cancel);
match (b_status, f_status) {
(
EngineStatus::Found {
candidate: b_cand,
hash_count: b_hashes,
origin: _,
},
EngineStatus::Found {
candidate: f_cand,
hash_count: f_hashes,
origin: _,
},
) => {
assert_eq!(
b_cand.nonce, f_cand.nonce,
"engines disagreed on winning nonce"
);
assert_eq!(
b_cand.distance, f_cand.distance,
"engines disagreed on distance"
);
assert_eq!(b_hashes, f_hashes, "engines disagreed on hash_count");
}
(b, f) => panic!("expected Found/Found, got baseline={b:?}, fast={f:?}"),
}
let difficulty = U512::from(1u64); // easy difficulty for "found" parity test
JobContext::new(header, difficulty)
}
#[test]
fn engine_returns_exhausted_when_no_solution_in_range() {
// Use a very strict threshold to make solutions effectively impossible in a tiny range.
// Use a very hard difficulty to make solutions effectively impossible in a tiny range.
let header = [2u8; 32];
let threshold = U512::zero();
let ctx = JobContext::new(header, threshold);
let difficulty = U512::MAX;
let ctx = JobContext::new(header, difficulty);
let range = Range {
start: U512::from(1u64),

File diff suppressed because it is too large Load Diff

View File

@@ -1,101 +1,101 @@
#![deny(rust_2018_idioms)]
#![forbid(unsafe_code)]
// #![deny(rust_2018_idioms)]
// #![forbid(unsafe_code)]
//! OpenCL-based GPU mining engine (placeholder)
//!
//! This crate is a scaffold for a future OpenCL backend that will implement the
//! mining engine interface used by the service layer. It currently provides:
//! - An `OpenClEngine` type with a constructor and basic helpers.
//! - Documentation of the intended integration points.
//!
//! Planned responsibilities (non-exhaustive):
//! - Accept a prepared `JobContext` (from `pow-core`) per job.
//! - Partition nonce ranges into GPU work assignments.
//! - Run an OpenCL kernel that performs, per nonce in the range:
//! - y <- y * m (mod n) using Montgomery multiplication (in Montgomery domain)
//! - nonce_element <- SHA3_512(y) in the normal domain
//! - distance <- target XOR nonce_element
//! - if distance <= threshold: report solution and signal early-cancel
//! - Coordinate early-exit via device-global flags and host polling.
//!
//! Notes:
//! - This crate deliberately does NOT implement the `MinerEngine` trait yet,
//! because the engine trait currently lives in `engine-cpu`. Once the trait
//! is promoted to a shared crate (or re-exported for engines), this crate
//! will implement it and become selectable at runtime via the service config.
//! - OpenCL bindings (e.g., via the `ocl` crate) and kernels will be added
//! behind feature flags (e.g., `opencl`). For now, we only offer placeholders
//! so the workspace compiles cleanly and the integration points are clear.
// //! OpenCL-based GPU mining engine (placeholder)
// //!
// //! This crate is a scaffold for a future OpenCL backend that will implement the
// //! mining engine interface used by the service layer. It currently provides:
// //! - An `OpenClEngine` type with a constructor and basic helpers.
// //! - Documentation of the intended integration points.
// //!
// //! Planned responsibilities (non-exhaustive):
// //! - Accept a prepared `JobContext` (from `pow-core`) per job.
// //! - Partition nonce ranges into GPU work assignments.
// //! - Run an OpenCL kernel that performs, per nonce in the range:
// //! - y <- y * m (mod n) using Montgomery multiplication (in Montgomery domain)
// //! - nonce_element <- SHA3_512(y) in the normal domain
// //! - distance <- target XOR nonce_element
// //! - if distance <= threshold: report solution and signal early-cancel
// //! - Coordinate early-exit via device-global flags and host polling.
// //!
// //! Notes:
// //! - This crate deliberately does NOT implement the `MinerEngine` trait yet,
// //! because the engine trait currently lives in `engine-cpu`. Once the trait
// //! is promoted to a shared crate (or re-exported for engines), this crate
// //! will implement it and become selectable at runtime via the service config.
// //! - OpenCL bindings (e.g., via the `ocl` crate) and kernels will be added
// //! behind feature flags (e.g., `opencl`). For now, we only offer placeholders
// //! so the workspace compiles cleanly and the integration points are clear.
use pow_core::JobContext;
use primitive_types::U512;
// use pow_core::JobContext;
// use primitive_types::U512;
/// Placeholder type for the OpenCL engine.
///
/// When fully implemented, this engine will manage OpenCL platform/device
/// discovery, context/queue creation, kernel compilation, memory transfers,
/// and kernel launches. It will expose the same search-range semantics as
/// the CPU engine(s) but backed by the GPU.
#[derive(Default, Debug)]
pub struct OpenClEngine {
// Future fields (examples):
// platform_id: usize,
// device_id: usize,
// context: ocl::Context,
// queue: ocl::Queue,
// program: ocl::Program,
// kernel: ocl::Kernel,
}
// /// Placeholder type for the OpenCL engine.
// ///
// /// When fully implemented, this engine will manage OpenCL platform/device
// /// discovery, context/queue creation, kernel compilation, memory transfers,
// /// and kernel launches. It will expose the same search-range semantics as
// /// the CPU engine(s) but backed by the GPU.
// #[derive(Default, Debug)]
// pub struct OpenClEngine {
// // Future fields (examples):
// // platform_id: usize,
// // device_id: usize,
// // context: ocl::Context,
// // queue: ocl::Queue,
// // program: ocl::Program,
// // kernel: ocl::Kernel,
// }
impl OpenClEngine {
/// Construct a new OpenCL engine placeholder.
///
/// Future versions may accept configuration (e.g., platform/device index).
pub fn new() -> Self {
Self::default()
}
// impl OpenClEngine {
// /// Construct a new OpenCL engine placeholder.
// ///
// /// Future versions may accept configuration (e.g., platform/device index).
// pub fn new() -> Self {
// Self::default()
// }
/// Human-readable name for logs/metrics.
pub fn name(&self) -> &'static str {
"gpu-opencl (placeholder)"
}
// /// Human-readable name for logs/metrics.
// pub fn name(&self) -> &'static str {
// "gpu-opencl (placeholder)"
// }
/// Prepare a precomputed job context for a given header and threshold.
///
/// This defers to `pow-core` to derive (m, n) and `target` from the header.
/// In a full OpenCL implementation, this context will be uploaded to device
/// constant buffers or passed as kernel arguments.
pub fn prepare_context(&self, header_hash: [u8; 32], threshold: U512) -> JobContext {
JobContext::new(header_hash, threshold)
}
// /// Prepare a precomputed job context for a given header and threshold.
// ///
// /// This defers to `pow-core` to derive (m, n) and `target` from the header.
// /// In a full OpenCL implementation, this context will be uploaded to device
// /// constant buffers or passed as kernel arguments.
// pub fn prepare_context(&self, header_hash: [u8; 32], threshold: U512) -> JobContext {
// JobContext::new(header_hash, threshold)
// }
/// Returns whether this build has OpenCL support compiled in.
///
/// When actual OpenCL integration is added behind a feature flag, this will
/// return true only if that feature is enabled.
pub fn opencl_available(&self) -> bool {
// Adjust once actual OpenCL integration is implemented behind a feature:
// cfg!(feature = "opencl")
false
}
}
// /// Returns whether this build has OpenCL support compiled in.
// ///
// /// When actual OpenCL integration is added behind a feature flag, this will
// /// return true only if that feature is enabled.
// pub fn opencl_available(&self) -> bool {
// // Adjust once actual OpenCL integration is implemented behind a feature:
// // cfg!(feature = "opencl")
// false
// }
// }
#[cfg(test)]
mod tests {
use super::*;
use primitive_types::U512;
// #[cfg(test)]
// mod tests {
// use super::*;
// use primitive_types::U512;
#[test]
fn placeholder_engine_basics() {
let eng = OpenClEngine::new();
assert_eq!(eng.name(), "gpu-opencl (placeholder)");
// #[test]
// fn placeholder_engine_basics() {
// let eng = OpenClEngine::new();
// assert_eq!(eng.name(), "gpu-opencl (placeholder)");
// Ensure context creation works and is deterministic in shape.
let header = [1u8; 32];
let threshold = U512::from(12345u64);
let ctx = eng.prepare_context(header, threshold);
// // Ensure context creation works and is deterministic in shape.
// let header = [1u8; 32];
// let threshold = U512::from(12345u64);
// let ctx = eng.prepare_context(header, threshold);
assert_eq!(ctx.header, header);
assert_eq!(ctx.threshold, threshold);
}
}
// assert_eq!(ctx.header, header);
// assert_eq!(ctx.threshold, threshold);
// }
// }

View File

@@ -1,29 +0,0 @@
[package]
name = "engine-montgomery"
version.workspace = true
edition.workspace = true
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 }
num-traits = { workspace = true }
metrics = { path = "../metrics", optional = true }
# Fixed-width big-integer backend for 512-bit Montgomery ops
crypto-bigint = "0.5"

View File

@@ -1,210 +0,0 @@
# 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 Poseidon2 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 Poseidon2_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 NewtonRaphson).
- `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 Poseidon2, we convert the residue `ŷ` back to the normal domain via `mont_mul(ŷ, 1)` and serialize as big-endian 64 bytes.
- We reuse a single `Poseidon2Core` hasher per search call and `hash_512()` 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-cores 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 Poseidon2 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.52.0×), with end-to-end uplift bounded by Poseidon2 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., 30005000) 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 Poseidon2 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.

View File

@@ -1,974 +0,0 @@
#![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 Poseidon2-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,
origin: engine_cpu::FoundOrigin::Cpu,
};
}
// Advance or finish
match current.cmp(&range.end) {
Ordering::Less => {
// Incremental step in Montgomery domain: y_hat <- y_hat * m_hat (mod n)
y_hat = mont.mul(&y_hat, &m_hat);
current = current.saturating_add(U512::one());
}
_ => {
break EngineStatus::Exhausted { hash_count };
}
}
}
}
}
/// Montgomery scaffolding with crypto-bigint.
/// This module provides conversions and parameter containers that we can use
/// to wire up a fixed-width limb backend for 512-bit operations.
mod mont_portable {
use super::*;
// Montgomery context with portable CIOS 8x64 implementation (u128 intermediates).
// Limbs are stored little-endian (limb 0 is least significant).
type MulFn = fn(&[u64; 8], &[u64; 8], &[u64; 8], u64) -> [u64; 8];
#[derive(Clone)]
pub struct MontCtx {
n: [u64; 8],
n0_inv: u64, // -n^{-1} mod 2^64
r2: [u64; 8], // R^2 mod n
pub m_hat: [u64; 8],
mul_fn: MulFn,
}
impl MontCtx {
pub fn from_ctx(ctx: &JobContext) -> Self {
let n = u512_to_le(ctx.n);
let n0_inv = mont_n0_inv(n[0]);
let r2_u512 = compat::mod_pow(&U512::from(2u32), &U512::from(1024u32), &ctx.n);
let r2 = u512_to_le(r2_u512);
let m = u512_to_le(ctx.m);
let (mul_fn, backend) = select_backend();
log::info!(target: "miner", "cpu-montgomery backend selected: {backend}");
#[cfg(feature = "metrics")]
{
metrics::set_engine_backend("cpu-montgomery", backend);
}
let m_hat = mul_fn(&m, &r2, &n, n0_inv);
MontCtx {
n,
n0_inv,
r2,
m_hat,
mul_fn,
}
}
pub fn to_mont_u512(&self, x: &U512) -> [u64; 8] {
let xl = u512_to_le(*x);
(self.mul_fn)(&xl, &self.r2, &self.n, self.n0_inv)
}
#[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 NewtonRaphson (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] {
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 = ((ai as u128) * (b[j] as u128)) >> 64;
let sum = acc[j] + (lo as u128) + carry;
acc[j] = sum & MASK;
carry = (sum >> 64) + hi;
}
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 lo2 = m.wrapping_mul(n[j]);
let hi2 = ((m as u128) * (n[j] as u128)) >> 64;
let sum2 = acc[j] + (lo2 as u128) + carry2;
acc[j] = sum2 & MASK;
carry2 = (sum2 >> 64) + hi2;
}
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,
origin: _,
},
EngineStatus::Found {
candidate: cf,
hash_count: hf,
origin: _,
},
) => {
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,
origin: _,
},
EngineStatus::Found {
candidate: f_cand,
hash_count: f_hashes,
origin: _,
},
) => {
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:?}"),
}
}
}

View File

@@ -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, cpu-montgomery, gpu-cuda, gpu-opencl
/// Options: cpu-baseline, cpu-fast, cpu-chain-manipulator, 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,
@@ -96,8 +96,6 @@ 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)
@@ -111,7 +109,6 @@ 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,

View File

@@ -6,21 +6,18 @@ publish = false
description = "Service layer: HTTP API, job orchestration, and engine abstraction for the Quantus External Miner"
[features]
default = ["cpu", "metrics", "montgomery"]
default = ["cpu", "metrics"]
# Enable CPU engine by default.
cpu = ["engine-cpu"]
# Optional metrics/observability (Prometheus endpoint).
metrics = [
"dep:metrics",
"engine-gpu-cuda?/metrics",
"engine-montgomery?/metrics",
"metrics/http-exporter",
]
# Optional GPU backends (off by default).
cuda = ["dep:engine-gpu-cuda", "engine-gpu-cuda/cuda"]
opencl = ["engine-gpu-opencl"]
# Optional Montgomery-optimized CPU backend
montgomery = ["engine-montgomery"]
[dependencies]
# Workspace-shared deps
@@ -42,7 +39,6 @@ quantus-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, features = ["cuda"] }
engine-gpu-opencl = { path = "../engine-gpu-opencl", optional = true }
metrics = { path = "../metrics", optional = true }

View File

@@ -3,7 +3,6 @@
use crossbeam_channel::{bounded, Receiver, Sender};
use engine_cpu::{EngineCandidate, EngineRange, MinerEngine};
use pow_core::compat;
use primitive_types::U512;
use quantus_miner_api::*;
use std::collections::HashMap;
@@ -45,7 +44,6 @@ pub struct ServiceConfig {
pub enum EngineSelection {
CpuBaseline,
CpuFast,
CpuMontgomery,
CpuChainManipulator,
GpuCuda,
GpuOpenCl,
@@ -72,7 +70,6 @@ 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",
@@ -281,14 +278,14 @@ pub enum JobStatus {
pub struct MiningJobResult {
pub nonce: U512,
pub work: [u8; 64],
pub distance: U512,
pub hash: U512,
}
/// Mining job data structure stored in the service.
#[derive(Debug)]
pub struct MiningJob {
pub header_hash: [u8; 32],
pub distance_threshold: U512,
pub difficulty: U512,
pub nonce_start: U512,
pub nonce_end: U512,
@@ -313,7 +310,7 @@ impl Clone for MiningJob {
fn clone(&self) -> Self {
MiningJob {
header_hash: self.header_hash,
distance_threshold: self.distance_threshold,
difficulty: self.difficulty,
nonce_start: self.nonce_start,
nonce_end: self.nonce_end,
@@ -349,13 +346,13 @@ pub struct ThreadResult {
impl MiningJob {
pub fn new(
header_hash: [u8; 32],
distance_threshold: U512,
difficulty: U512,
nonce_start: U512,
nonce_end: U512,
) -> Self {
MiningJob {
header_hash,
distance_threshold,
difficulty,
nonce_start,
nonce_end,
status: JobStatus::Running,
@@ -401,7 +398,7 @@ impl MiningJob {
);
// Prepare shared job context once per job.
let ctx = engine.prepare_context(self.header_hash, self.distance_threshold);
let ctx = engine.prepare_context(self.header_hash, self.difficulty);
for (thread_id, (start, end)) in partitions.ranges.into_iter().enumerate() {
let cancel_flag = self.cancel_flag.clone();
@@ -537,20 +534,20 @@ impl MiningJob {
let is_better = self
.best_result
.as_ref()
.is_none_or(|current_best| result.distance < current_best.distance);
.is_none_or(|current_best| result.hash < current_best.hash);
if is_better {
log::debug!(target: "miner",
"Found better result from thread {}: distance = {}, nonce = {}",
thread_result.thread_id,
result.distance,
result.hash,
result.nonce
);
self.best_result = Some(result.clone());
self.cancel_flag.store(true, Ordering::Relaxed);
// Result is now ready to be fetched via /result
log::info!(target: "miner", "Result ready: engine={}, nonce={}, distance={}",
self.engine_name, result.nonce, result.distance);
self.engine_name, result.nonce, result.hash);
#[cfg(feature = "metrics")]
{
// reuse existing http metric bucket for visibility until dedicated counters exist
@@ -689,23 +686,14 @@ fn mine_range_with_engine(
match status {
engine_cpu::EngineStatus::Found {
candidate:
EngineCandidate {
nonce,
work,
distance,
},
candidate: EngineCandidate { nonce, work, hash },
hash_count,
origin,
} => {
// Send final result with found candidate and the hashes covered in this subrange
let final_result = ThreadResult {
thread_id,
result: Some(MiningJobResult {
nonce,
work,
distance,
}),
result: Some(MiningJobResult { nonce, work, hash }),
hash_count,
origin: Some(origin),
completed: true,
@@ -845,11 +833,11 @@ pub async fn handle_mine_request(
.unwrap()
.try_into()
.expect("Validated hex string is 32 bytes");
let distance_threshold = U512::from_dec_str(&request.distance_threshold).unwrap();
let difficulty = U512::from_dec_str(&request.distance_threshold).unwrap();
let nonce_start = U512::from_str_radix(&request.nonce_start, 16).unwrap();
let nonce_end = U512::from_str_radix(&request.nonce_end, 16).unwrap();
let job = MiningJob::new(header_hash, distance_threshold, nonce_start, nonce_end);
let job = MiningJob::new(header_hash, difficulty, nonce_start, nonce_end);
match state.add_job(request.job_id.clone(), job).await {
Ok(_) => {
@@ -941,16 +929,15 @@ pub async fn handle_result_request(
// Inline re-verify using the exact nonce bytes we will return
if let Some(result) = &job.best_result {
let nonce_be = result.nonce.to_big_endian();
let d2 = compat::get_nonce_distance(job.header_hash, nonce_be);
let ok = d2 <= job.distance_threshold;
let (ok, hash_result) = pow_core::is_valid_nonce(job.header_hash, nonce_be, job.difficulty);
log::info!(
target: "miner",
"Serving result: job_id={}, engine={}, ok={}, host_distance={}, threshold={}",
"Serving result: job_id={}, engine={}, ok={}, hash={}, difficulty={}",
job_id,
job.engine_name,
ok,
d2,
job.distance_threshold
hash_result,
job.difficulty
);
#[cfg(feature = "metrics")]
{
@@ -1185,17 +1172,6 @@ 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.
@@ -1437,13 +1413,13 @@ mod tests {
let state = MiningService::new(2, engine, 2000);
state.start_mining_loop().await;
// Impossible threshold with a nonce range that excludes 0
// Impossible difficulty with a nonce range that excludes 0
let header_hash = [1u8; 32];
let distance_threshold = U512::zero();
let difficulty = U512::MAX;
let nonce_start = U512::from(1);
let nonce_end = U512::from(100);
let job = MiningJob::new(header_hash, distance_threshold, nonce_start, nonce_end);
let job = MiningJob::new(header_hash, difficulty, nonce_start, nonce_end);
state.add_job("fail_job".to_string(), job).await.unwrap();
let mut finished_job = None;
@@ -1535,13 +1511,13 @@ mod tests {
let state = MiningService::new(2, engine, 2000);
state.start_mining_loop().await;
// Easy threshold
// Easy difficulty
let header_hash = [1u8; 32];
let distance_threshold = U512::MAX; // Easiest difficulty
let difficulty = U512::from(1u64); // Easiest difficulty
let nonce_start = U512::from(0);
let nonce_end = U512::from(10000);
let job = MiningJob::new(header_hash, distance_threshold, nonce_start, nonce_end);
let job = MiningJob::new(header_hash, difficulty, nonce_start, nonce_end);
state.add_job("success_job".to_string(), job).await.unwrap();
let mut finished_job = None;
@@ -1599,7 +1575,7 @@ mod tests {
assert!(e.contains("mining_hash must be valid hex"));
}
// 4) Bad distance_threshold decimal
// 4) Bad difficulty decimal
{
let mut r = valid_req();
r.distance_threshold = "not-a-decimal".to_string();
@@ -1681,9 +1657,9 @@ mod tests {
// 3) POST /mine valid -> 200 Accepted, duplicate -> 409
let req = quantus_miner_api::MiningRequest {
job_id: "job-http-1".to_string(),
mining_hash: "11".repeat(32), // 64 hex chars
distance_threshold: "0".to_string(), // strict, likely to fail later; OK for accept flow
nonce_start: "00".repeat(64), // 128 hex chars
mining_hash: "11".repeat(32), // 64 hex chars
distance_threshold: "99999999999999".to_string(), // hard, likely to fail later; OK for accept flow
nonce_start: "00".repeat(64), // 128 hex chars
nonce_end: format!("{:0128x}", 1u8),
};
@@ -1709,11 +1685,11 @@ mod tests {
use crossbeam_channel::bounded;
use std::sync::atomic::AtomicBool;
// Baseline engine, strict threshold to force Exhausted path for the sub-range
// Baseline engine, hard difficulty to force Exhausted path for the sub-range
let engine = engine_cpu::BaselineCpuEngine::new();
let header = [3u8; 32];
let threshold = U512::zero();
let ctx = engine.prepare_context(header, threshold);
let difficulty = U512::MAX;
let ctx = engine.prepare_context(header, difficulty);
// Small range; chunking derives a large chunk size, so it will be a single chunk,
// which still exercises the Exhausted -> progress update and final completion paths.

View File

@@ -39,6 +39,7 @@ primitive-types = { workspace = true }
num-bigint = { workspace = true }
num-traits = { workspace = true }
qp-poseidon-core = { workspace = true }
qpow-math = { workspace = true }
thiserror = { workspace = true }
anyhow = { workspace = true }

View File

@@ -1,423 +1,192 @@
#![cfg_attr(not(feature = "std"), no_std)]
// Local QPoW math core with a compatibility API mirroring the original `qpow-math` crate,
// plus new scaffolding for an optimized path (precompute + incremental evaluation).
//
// This crate intentionally provides:
// - `compat` API: Drop-in functions like `is_valid_nonce` and `get_nonce_distance`
// - `JobContext`: Precomputed constants (m, n, target, threshold) for a given header
// - Incremental helpers: `init_worker_y0`, `step_mul`, and `distance_from_y`
// to enable replacing per-nonce exponentiation with a single modular multiplication.
//
// Notes:
// - Current implementation mirrors the reference algorithm using BigUint-based modular arithmetic.
// - Future work will gate accelerated paths (e.g., Montgomery, SIMD Poseidon2) behind features.
extern crate alloc;
use core::ops::BitXor;
use primitive_types::U512;
use qp_poseidon_core::Poseidon2Core;
#[cfg(feature = "std")]
use log::{debug, error};
pub use qpow_math::{get_nonce_hash, is_valid_nonce, mine_range};
pub mod compat {
//! Compatibility layer that mirrors the original `qpow-math` crate API.
use super::*;
/// Check QPoW validity for a given `header` and `nonce` against `threshold`.
///
/// Returns a boolean indicating validity. If you also need the computed distance,
/// use `is_valid_nonce_with_distance`.
pub fn is_valid_nonce(header: [u8; 32], nonce: [u8; 64], threshold: U512) -> bool {
let (ok, _) = is_valid_nonce_with_distance(header, nonce, threshold);
ok
}
/// Same as `is_valid_nonce`, but also returns the computed distance (U512).
pub fn is_valid_nonce_with_distance(
header: [u8; 32],
nonce: [u8; 64],
threshold: U512,
) -> (bool, U512) {
if nonce == [0u8; 64] {
#[cfg(feature = "std")]
error!(
"is_valid_nonce should not be called with 0 nonce, but was for header: {header:?}"
);
return (false, U512::zero());
}
let distance_achieved = get_nonce_distance(header, nonce);
#[cfg(feature = "std")]
debug!(target: "pow-core", "distance = {distance_achieved}..., threshold = {threshold}...");
(distance_achieved <= threshold, distance_achieved)
}
/// Compute the QPoW distance for (header, nonce).
///
/// distance = target XOR H(m^(h + nonce) mod n)
/// where (m, n) are derived deterministically from the header, and H is Poseidon2-512.
pub fn get_nonce_distance(header: [u8; 32], nonce: [u8; 64]) -> U512 {
super::get_nonce_distance_impl(header, nonce)
}
/// Generate a pair (m, n) deterministically from the header.
pub fn get_random_rsa(header: &[u8; 32]) -> (U512, U512) {
super::get_random_rsa_impl(header)
}
/// Check if two numbers are coprime using the Euclidean algorithm.
pub fn is_coprime(a: &U512, b: &U512) -> bool {
super::is_coprime_impl(a, b)
}
/// MillerRabin primality test used by `get_random_rsa`.
pub fn is_prime(n: &U512) -> bool {
super::is_prime_impl(n)
}
/// Apply the reference "hash-to-group" function then Poseidon2-512.
pub fn hash_to_group_bigint_sha(h: &U512, m: &U512, n: &U512, solution: &U512) -> U512 {
super::hash_to_group_bigint_sha_impl(h, m, n, solution)
}
/// Reference hash-to-group function: computes m^(h + solution) mod n.
pub fn hash_to_group_bigint(h: &U512, m: &U512, n: &U512, solution: &U512) -> U512 {
super::hash_to_group_bigint_impl(h, m, n, solution)
}
/// Reference modular exponentiation via BigUint.
pub fn mod_pow(base: &U512, exponent: &U512, modulus: &U512) -> U512 {
super::mod_pow_impl(base, exponent, modulus)
}
/// Poseidon2-512 over the big-endian bytes of input U512.
pub fn poseidon2_512(input: U512) -> U512 {
super::poseidon2_512_impl(input)
}
}
/// Precomputed context for a single mining job (header + threshold).
///
/// This enables an optimized path:
/// - Precompute (m, n) and target = H(m^(h + 0) mod n) once per job.
/// - For each worker, compute `y0 = m^(h + start_nonce) mod n` once.
/// - For each subsequent nonce, update y = (y * m) mod n (O(1) per step).
/// - Distance at a step is `target XOR H(y)`.
#[derive(Clone, Debug)]
/// Job context for Bitcoin-style PoW mining with double Poseidon2 hashing
#[derive(Debug, Clone)]
pub struct JobContext {
pub header: [u8; 32],
pub header_int: U512,
pub threshold: U512,
pub m: U512,
pub n: U512,
pub difficulty: U512,
pub target: U512,
}
impl JobContext {
/// Build a new context by deriving (m, n) and target from the header.
pub fn new(header: [u8; 32], threshold: U512) -> Self {
let header_int = U512::from_big_endian(&header);
let (m, n) = get_random_rsa_impl(&header);
let target = hash_to_group_bigint_sha_impl(&header_int, &m, &n, &U512::zero());
/// Build a new context from header and difficulty
pub fn new(header: [u8; 32], difficulty: U512) -> Self {
// In Bitcoin-style PoW, target = max_target / difficulty
let max_target = U512::MAX;
let target = max_target / difficulty;
JobContext {
header,
header_int,
threshold,
m,
n,
difficulty,
target,
}
}
}
/// Compute y0 = m^(h + start_nonce) mod n for a worker's starting nonce.
///
/// This is the one-time exponentiation cost per worker/thread. Subsequent steps can
/// use `step_mul` to advance y with a single modular multiplication.
pub fn init_worker_y0(ctx: &JobContext, start_nonce: U512) -> U512 {
let sum = ctx.header_int.saturating_add(start_nonce);
mod_pow_impl(&ctx.m, &sum, &ctx.n)
/// Initialize a worker with starting nonce (no special initialization needed for Bitcoin-style)
pub fn init_worker_nonce(start_nonce: U512) -> U512 {
start_nonce
}
/// Advance y by one nonce: y <- y * m (mod n).
pub fn step_mul(ctx: &JobContext, y: U512) -> U512 {
mod_mul_impl(&y, &ctx.m, &ctx.n)
/// Advance nonce by one (simple increment for Bitcoin-style)
pub fn step_nonce(nonce: U512) -> U512 {
nonce.saturating_add(U512::from(1u64))
}
/// Compute distance for the current y:
/// distance = target XOR Poseidon2_512(y)
pub fn distance_from_y(ctx: &JobContext, y: U512) -> U512 {
let hashed = poseidon2_512_impl(y);
ctx.target.bitxor(hashed)
/// Compute hash for the current nonce using Bitcoin-style double Poseidon2
pub fn hash_from_nonce(ctx: &JobContext, nonce: U512) -> U512 {
let nonce_bytes = nonce.to_big_endian();
qpow_math::get_nonce_hash(ctx.header, nonce_bytes)
}
/// Convenience: compute distance for an arbitrary nonce using the context.
pub fn distance_for_nonce(ctx: &JobContext, nonce: U512) -> U512 {
let y = mod_pow_impl(&ctx.m, &ctx.header_int.saturating_add(nonce), &ctx.n);
distance_from_y(ctx, y)
/// Check if hash meets difficulty target
pub fn is_valid_hash(ctx: &JobContext, hash: U512) -> bool {
hash < ctx.target
}
/// Convenience: check if a distance is valid under the context's threshold.
pub fn is_valid_distance(ctx: &JobContext, distance: U512) -> bool {
distance <= ctx.threshold
}
/// Mine a range of nonces starting from start_nonce
pub fn mine_nonce_range(ctx: &JobContext, start_nonce: U512, steps: u64) -> Option<(U512, U512)> {
let start_nonce_bytes = start_nonce.to_big_endian();
/// Reference distance computation used by the compat layer.
fn get_nonce_distance_impl(header: [u8; 32], nonce: [u8; 64]) -> U512 {
if nonce == [0u8; 64] {
#[cfg(feature = "std")]
debug!(target: "pow-core", "zero nonce");
return U512::zero();
if let Some((nonce_bytes, hash)) =
mine_range(ctx.header, start_nonce_bytes, steps, ctx.difficulty)
{
let nonce = U512::from_big_endian(&nonce_bytes);
Some((nonce, hash))
} else {
None
}
let (m, n) = get_random_rsa_impl(&header);
let header_int = U512::from_big_endian(&header);
let nonce_int = U512::from_big_endian(&nonce);
let target = hash_to_group_bigint_sha_impl(&header_int, &m, &n, &U512::zero());
let nonce_element = hash_to_group_bigint_sha_impl(&header_int, &m, &n, &nonce_int);
let distance = target.bitxor(nonce_element);
#[cfg(feature = "std")]
debug!(target: "pow-core", "distance = {distance}");
distance
}
/// Generates a pair of RSA-style numbers (m, n) deterministically from input header.
///
/// - m: 256-bit derived via Poseidon2-256(header)
/// - n: 512-bit derived via Poseidon2-512(header), iteratively rehashed until valid:
/// (odd, composite, coprime with m, and n > m)
fn get_random_rsa_impl(header: &[u8; 32]) -> (U512, U512) {
let poseidon = Poseidon2Core::new();
// m from Poseidon2-256
let m_bytes = poseidon.hash_no_pad_bytes(header);
let m = U512::from_big_endian(&m_bytes);
// initial n from Poseidon2-512
let mut n_bytes = poseidon.hash_512(&m_bytes);
let mut n = U512::from_big_endian(&n_bytes);
// Keep hashing until n satisfies constraints
while n % 2u32 == U512::zero() || n <= m || !is_coprime_impl(&m, &n) || is_prime_impl(&n) {
n_bytes = poseidon.hash_512(&n_bytes);
n = U512::from_big_endian(&n_bytes);
}
(m, n)
}
/// Check if two numbers are coprime using the Euclidean algorithm.
fn is_coprime_impl(a: &U512, b: &U512) -> bool {
let mut x = *a;
let mut y = *b;
while y != U512::zero() {
let tmp = y;
y = x % y;
x = tmp;
}
x == U512::one()
}
/// Hash-to-group then Poseidon2-512.
///
/// Note: The reference calls `hash_to_group_bigint` followed by an additional Poseidon2-512.
fn hash_to_group_bigint_sha_impl(h: &U512, m: &U512, n: &U512, solution: &U512) -> U512 {
let result = hash_to_group_bigint_impl(h, m, n, solution);
poseidon2_512_impl(result)
}
/// Reference hash-to-group big-integer function (no chunk splitting).
/// Computes sum = h + solution; then y = m^sum mod n.
fn hash_to_group_bigint_impl(h: &U512, m: &U512, n: &U512, solution: &U512) -> U512 {
let sum = h.saturating_add(*solution);
mod_pow_impl(m, &sum, n)
}
/// Reference modular exponentiation using BigUint square-and-multiply.
fn mod_pow_impl(base: &U512, exponent: &U512, modulus: &U512) -> U512 {
if *modulus == U512::zero() {
panic!("Modulus cannot be zero");
}
use num_bigint::BigUint;
use num_traits::{One, Zero};
// Convert inputs to BigUint
let mut base = BigUint::from_bytes_be(&base.to_big_endian());
let mut exp = BigUint::from_bytes_be(&exponent.to_big_endian());
let modulus = BigUint::from_bytes_be(&modulus.to_big_endian());
// Initialize result as 1
let mut result = BigUint::one();
// Square-and-multiply algorithm
while !exp.is_zero() {
if exp.bit(0) {
result = (result * &base) % &modulus;
}
base = (&base * &base) % &modulus;
exp >>= 1;
}
U512::from_big_endian(&result.to_bytes_be())
}
/// Reference modular multiplication using BigUint, i.e., (a * b) mod n.
fn mod_mul_impl(a: &U512, b: &U512, modulus: &U512) -> U512 {
use num_bigint::BigUint;
if *modulus == U512::zero() {
panic!("Modulus cannot be zero");
}
let a_bi = BigUint::from_bytes_be(&a.to_big_endian());
let b_bi = BigUint::from_bytes_be(&b.to_big_endian());
let n_bi = BigUint::from_bytes_be(&modulus.to_big_endian());
let prod = (a_bi * b_bi) % n_bi;
U512::from_big_endian(&prod.to_bytes_be())
}
/// MillerRabin primality test.
///
/// Deterministically selects k=32 bases hashed from `n` using Poseidon2-512 to
/// bound false-positive probability to ~1/2^64 for composites.
fn is_prime_impl(n: &U512) -> bool {
if *n <= U512::one() {
return false;
}
if *n == U512::from(2u32) || *n == U512::from(3u32) {
return true;
}
if *n % U512::from(2u32) == U512::zero() {
return false;
}
// write n-1 as d * 2^r
let mut d = *n - U512::one();
let mut r = 0u32;
while d % U512::from(2u32) == U512::zero() {
d /= U512::from(2u32);
r += 1;
}
// Generate test bases deterministically from n using Poseidon2
let mut bases = [U512::zero(); 32];
let mut base_count = 0;
let poseidon = Poseidon2Core::new();
let mut counter = U512::zero();
while base_count < 32 {
// Hash n concatenated with counter
let mut bytes = [0u8; 128];
let n_bytes = n.to_big_endian();
let counter_bytes = counter.to_big_endian();
bytes[..64].copy_from_slice(&n_bytes);
bytes[64..128].copy_from_slice(&counter_bytes);
let poseidon_bytes = poseidon.hash_512(&bytes);
// Use the hash to generate a base in [2, n-2]
let hash = U512::from_big_endian(&poseidon_bytes);
let base = (hash % (*n - U512::from(4u32))) + U512::from(2u32);
bases[base_count] = base;
base_count += 1;
counter += U512::one();
}
'witness: for base in bases {
let mut x = mod_pow_impl(&base, &d, n);
if x == U512::one() || x == *n - U512::one() {
continue 'witness;
}
// Square r-1 times
for _ in 0..r - 1 {
x = mod_pow_impl(&x, &U512::from(2u32), n);
if x == *n - U512::one() {
continue 'witness;
}
if x == U512::one() {
return false;
}
}
return false;
}
true
}
/// Poseidon2-512 over the big-endian bytes of input `U512`.
fn poseidon2_512_impl(input: U512) -> U512 {
let poseidon = Poseidon2Core::new();
let bytes = input.to_big_endian();
U512::from_big_endian(&poseidon.hash_512(&bytes))
}
#[cfg(test)]
mod tests {
use super::*;
// removed unused helper u512_from_hex
#[test]
fn compat_distance_matches_context_distance() {
// Synthetic header and nonce
fn test_job_context_creation() {
let header = [1u8; 32];
let nonce = [2u8; 64];
let difficulty = U512::from(1000u64);
let dist_compat = compat::get_nonce_distance(header, nonce);
let ctx = JobContext::new(header, difficulty);
let ctx = JobContext::new(header, U512::from(123u64));
let nonce_int = U512::from_big_endian(&nonce);
let dist_ctx = distance_for_nonce(&ctx, nonce_int);
assert_eq!(dist_compat, dist_ctx);
assert_eq!(ctx.header, header);
assert_eq!(ctx.difficulty, difficulty);
assert_eq!(ctx.target, U512::MAX / difficulty);
}
#[test]
fn incremental_step_matches_pow_plus_one() {
fn test_nonce_stepping() {
let start = U512::from(100u64);
let next = step_nonce(start);
assert_eq!(next, U512::from(101u64));
}
#[test]
fn test_hash_computation() {
let header = [1u8; 32];
let difficulty = U512::from(1u64);
let ctx = JobContext::new(header, difficulty);
let nonce = U512::from(123u64);
let hash1 = hash_from_nonce(&ctx, nonce);
let hash2 = hash_from_nonce(&ctx, nonce);
// Same input should produce same hash
assert_eq!(hash1, hash2);
// Hash should not be zero for non-zero nonce
assert_ne!(hash1, U512::zero());
}
#[test]
fn test_different_nonces_different_hashes() {
let header = [2u8; 32];
let difficulty = U512::from(1u64);
let ctx = JobContext::new(header, difficulty);
let nonce1 = U512::from(100u64);
let nonce2 = U512::from(101u64);
let hash1 = hash_from_nonce(&ctx, nonce1);
let hash2 = hash_from_nonce(&ctx, nonce2);
assert_ne!(hash1, hash2);
}
#[test]
fn test_validity_check() {
let header = [3u8; 32];
let threshold = U512::from(99999u64);
let ctx = JobContext::new(header, threshold);
let easy_difficulty = U512::from(1u64);
let ctx = JobContext::new(header, easy_difficulty);
let start = U512::from(1000u64);
let y0 = init_worker_y0(&ctx, start);
let nonce = U512::from(1u64);
let (is_valid, hash) = is_valid_nonce(ctx.header, nonce.to_big_endian(), ctx.difficulty);
// Distance for start+1 computed two ways should match:
// 1) Incremental: step once from y0
// 2) Direct exponentiation with nonce = start+1
let y1_inc = step_mul(&ctx, y0);
let dist_inc = distance_from_y(&ctx, y1_inc);
// With very easy difficulty, should be valid
assert!(is_valid);
assert_ne!(hash, U512::zero());
let direct = mod_pow_impl(
&ctx.m,
&ctx.header_int.saturating_add(start + U512::one()),
&ctx.n,
);
let dist_direct = distance_from_y(&ctx, direct);
assert_eq!(dist_inc, dist_direct);
// Verify hash is actually below target
assert!(hash < ctx.target);
}
#[test]
fn zero_nonce_is_zero_distance() {
let header = [0xABu8; 32];
let nonce = [0u8; 64];
let d = compat::get_nonce_distance(header, nonce);
assert_eq!(d, U512::zero());
fn test_target_calculation() {
let header = [4u8; 32];
let difficulty = U512::from(256u64);
let ctx = JobContext::new(header, difficulty);
let expected_target = U512::MAX / U512::from(256u64);
assert_eq!(ctx.target, expected_target);
}
#[test]
fn test_hash_matches_qpow_math() {
// Test that our JobContext produces the same results as qpow_math directly
let header = [1u8; 32];
let nonce = U512::from(123u64);
let difficulty = U512::from(1000u64);
let ctx = JobContext::new(header, difficulty);
let hash_ctx = hash_from_nonce(&ctx, nonce);
let nonce_bytes = nonce.to_big_endian();
let hash_direct = qpow_math::get_nonce_hash(header, nonce_bytes);
assert_eq!(hash_ctx, hash_direct);
}
#[test]
fn test_mine_range_functionality() {
let header = [5u8; 32];
let difficulty = U512::from(1u64); // Very easy
let ctx = JobContext::new(header, difficulty);
let start_nonce = U512::from(1u64);
let result = mine_nonce_range(&ctx, start_nonce, 10);
// With very easy difficulty, should find a solution quickly
if let Some((found_nonce, found_hash)) = result {
assert!(found_nonce >= start_nonce);
assert!(found_nonce < start_nonce + U512::from(10u64));
assert!(found_hash < ctx.target);
}
// If no solution found, that's also valid behavior
}
#[test]
fn test_hard_difficulty_no_solution() {
let header = [6u8; 32];
let very_hard_difficulty = U512::MAX; // Impossible difficulty
let ctx = JobContext::new(header, very_hard_difficulty);
let start_nonce = U512::from(1u64);
let result = mine_nonce_range(&ctx, start_nonce, 5);
// With impossible difficulty, should not find solution
assert!(result.is_none());
}
}

View File

@@ -28,7 +28,7 @@
Description=Quantus External Miner Service
Wants=network-online.target
After=network-online.target
Documentation=https://github.com/Quantus-Network/quantus-external-miner
Documentation=https://github.com/Quantus-Network/quantus-miner
[Service]
Type=simple

View File

@@ -126,4 +126,4 @@ Operational tips
- Use RUST_LOG=info,miner=debug temporarily to verify startup detection (cpuset mask, effective CPUs) and to observe mining loop behavior; then turn back down to reduce log volume.
Support
- Repository: https://github.com/Quantus-Network/quantus-external-miner
- Repository: https://github.com/Quantus-Network/quantus-miner

View File

@@ -1,48 +0,0 @@
/*!
Deprecated root library entrypoint.
This repository has been restructured into a Cargo workspace to provide clear
separation of concerns and to enable aggressive optimization of the mining core.
Use the new workspace crates instead of the old root library:
- crates/miner-cli
- The CLI binary for running the external miner service.
- Examples:
- cargo run -p miner-cli -- --port 9833
- cargo run -p miner-cli -- --port 9833 --metrics-port 9900
- cargo run -p miner-cli -- --workers 4
- crates/miner-service
- The service layer: HTTP API (compatible with the node), job orchestration,
and engine abstraction (CPU/GPU backends).
- crates/pow-core
- The QPoW math core (local fork/superset of qpow-math) with a compatibility
API and new optimized paths for future refactoring (e.g., precomputation,
incremental evaluation, Montgomery multiplication).
- crates/engine-cpu
- CPU mining engine(s) implementing the unified engine trait. Contains the
baseline/reference engine and will host the optimized incremental/Montgomery
engine.
- crates/engine-gpu-cuda (optional, scaffold)
- Placeholder for a CUDA-based GPU engine.
- crates/engine-gpu-opencl (optional, scaffold)
- Placeholder for an OpenCL-based GPU engine.
- crates/metrics (optional)
- Prometheus metrics registry and optional HTTP exporter.
- Metrics are toggled by the presence of the `--metrics-port` CLI parameter
(when omitted, metrics are disabled entirely).
Notes:
- The public HTTP API remains compatible with the node.
- The old root library is intentionally left without exports to avoid conflicts.
- Build the binary via the new CLI package:
cargo build -p miner-cli --release
- Run the service:
cargo run -p miner-cli -- --port 9833 [--metrics-port 9900] [--workers N]
*/

View File

@@ -1,33 +0,0 @@
/*!
Deprecated root binary entrypoint.
This repository has been restructured into a Cargo workspace.
Run the external miner using the new CLI binary:
- cargo run -p miner-cli -- [args...]
- cargo build -p miner-cli --release
Examples:
- cargo run -p miner-cli -- --port 9833
- cargo run -p miner-cli -- --port 9833 --metrics-port 9900
- cargo run -p miner-cli -- --workers 4
See crates/miner-cli for the active entrypoint and crates/miner-service for the service layer.
*/
fn main() {
eprintln!(
"\n[DEPRECATED] This entrypoint has moved.\n\
Use the new CLI binary in the workspace instead:\n\
\n\
- cargo run -p miner-cli -- [args...]\n\
- cargo build -p miner-cli --release\n\
\n\
Examples:\n\
- cargo run -p miner-cli -- --port 9833\n\
- cargo run -p miner-cli -- --port 9833 --metrics-port 9900\n\
- cargo run -p miner-cli -- --workers 4\n"
);
std::process::exit(1);
}