17 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
Quantus Network CI
a6660c5e13 bump version to v0.3.0 (#18)
Co-authored-by: czareko <czareko@users.noreply.github.com>
2025-09-26 09:32:02 +08:00
illuzen
85006dd3a7 Merge pull request #17 from Quantus-Network/illuzen/poseidon-pow
switch to poseidon-pow
2025-09-25 22:36:00 +08:00
illuzen
0a96d2f6b7 taplooooo 2025-09-25 22:31:21 +08:00
illuzen
aa1204bab9 switch to poseidon
pow-core updated, checked that it is compatible with chain/main
2025-09-25 22:27:17 +08:00
23 changed files with 2373 additions and 3593 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

472
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"
@@ -460,6 +525,12 @@ dependencies = [
"syn 2.0.106",
]
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "encoding_rs"
version = "0.8.35"
@@ -471,19 +542,21 @@ dependencies = [
[[package]]
name = "engine-cpu"
version = "0.2.1"
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.2.1"
version = "1.0.0"
dependencies = [
"anyhow",
"cust",
@@ -495,14 +568,14 @@ dependencies = [
"num-traits 0.2.19",
"pow-core",
"primitive-types",
"qp-poseidon-core",
"rustacuda",
"sha3",
"thiserror",
]
[[package]]
name = "engine-gpu-opencl"
version = "0.2.1"
version = "1.0.0"
dependencies = [
"anyhow",
"log",
@@ -512,20 +585,6 @@ dependencies = [
"thiserror",
]
[[package]]
name = "engine-montgomery"
version = "0.2.1"
dependencies = [
"crypto-bigint",
"engine-cpu",
"log",
"metrics",
"num-traits 0.2.19",
"pow-core",
"primitive-types",
"sha3",
]
[[package]]
name = "enum_primitive"
version = "0.1.1"
@@ -770,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"
@@ -1058,6 +1128,24 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.15"
@@ -1074,15 +1162,6 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "keccak"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654"
dependencies = [
"cpufeatures",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
@@ -1131,7 +1210,7 @@ checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
[[package]]
name = "metrics"
version = "0.2.1"
version = "1.0.0"
dependencies = [
"anyhow",
"log",
@@ -1161,7 +1240,7 @@ dependencies = [
[[package]]
name = "miner-cli"
version = "0.2.1"
version = "1.0.0"
dependencies = [
"clap",
"env_logger",
@@ -1172,14 +1251,13 @@ dependencies = [
[[package]]
name = "miner-service"
version = "0.2.1"
version = "1.0.0"
dependencies = [
"anyhow",
"crossbeam-channel",
"engine-cpu",
"engine-gpu-cuda",
"engine-gpu-opencl",
"engine-montgomery",
"hex",
"log",
"metrics",
@@ -1197,7 +1275,7 @@ dependencies = [
[[package]]
name = "miner-telemetry"
version = "0.2.1"
version = "1.0.0"
dependencies = [
"anyhow",
"futures 0.3.31",
@@ -1378,6 +1456,128 @@ 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 0.14.0",
"p3-field",
"p3-matrix",
"p3-maybe-rayon",
"p3-util",
"tracing",
]
[[package]]
name = "p3-field"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc13a73509fe09c67b339951ca8d4cc6e61c9bf08c130dbc90dda52452918cc2"
dependencies = [
"itertools 0.14.0",
"num-bigint",
"p3-maybe-rayon",
"p3-util",
"paste",
"rand 0.9.2",
"serde",
"tracing",
]
[[package]]
name = "p3-goldilocks"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "552849f6309ffde34af0d31aa9a2d0a549cb0ec138d9792bfbf4a17800742362"
dependencies = [
"num-bigint",
"p3-dft",
"p3-field",
"p3-mds",
"p3-poseidon2",
"p3-symmetric",
"p3-util",
"paste",
"rand 0.9.2",
"serde",
]
[[package]]
name = "p3-matrix"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8e1e9f69c2fe15768b3ceb2915edb88c47398aa22c485d8163deab2a47fe194"
dependencies = [
"itertools 0.14.0",
"p3-field",
"p3-maybe-rayon",
"p3-util",
"rand 0.9.2",
"serde",
"tracing",
"transpose",
]
[[package]]
name = "p3-maybe-rayon"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33f765046b763d046728b3246b690f81dfa7ccd7523b7a1582c74f616fbce6a0"
[[package]]
name = "p3-mds"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c90541c6056712daf2ee69ec328db8b5605ae8dbafe60226c8eb75eaac0e1f9"
dependencies = [
"p3-dft",
"p3-field",
"p3-symmetric",
"p3-util",
"rand 0.9.2",
]
[[package]]
name = "p3-poseidon2"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88e9f053f120a78ad27e9c1991a0ea547777328ca24025c42364d6ee2667d59a"
dependencies = [
"p3-field",
"p3-mds",
"p3-symmetric",
"p3-util",
"rand 0.9.2",
]
[[package]]
name = "p3-symmetric"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72d5db8f05a26d706dfd8aaf7aa4272ca4f3e7a075db897ec7108f24fad78759"
dependencies = [
"itertools 0.14.0",
"p3-field",
"serde",
]
[[package]]
name = "p3-util"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dfee67245d9ce78a15176728da2280032f0a84b5819a39a953e7ec03cfd9bd7"
dependencies = [
"serde",
]
[[package]]
name = "parity-scale-codec"
version = "3.7.5"
@@ -1429,6 +1629,12 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "percent-encoding"
version = "2.3.2"
@@ -1467,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"
@@ -1478,7 +1712,7 @@ dependencies = [
[[package]]
name = "pow-core"
version = "0.2.1"
version = "1.0.0"
dependencies = [
"anyhow",
"hex",
@@ -1486,8 +1720,8 @@ dependencies = [
"num-bigint",
"num-traits 0.2.19",
"primitive-types",
"sha2",
"sha3",
"qp-poseidon-core",
"qpow-math",
"thiserror",
]
@@ -1543,6 +1777,46 @@ dependencies = [
"thiserror",
]
[[package]]
name = "qp-poseidon-constants"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
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",
"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"
@@ -1642,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"
@@ -1789,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"
@@ -1873,27 +2176,6 @@ dependencies = [
"digest",
]
[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "sha3"
version = "0.10.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60"
dependencies = [
"digest",
"keccak",
]
[[package]]
name = "shlex"
version = "1.3.0"
@@ -1959,6 +2241,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "strength_reduce"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82"
[[package]]
name = "strsim"
version = "0.11.1"
@@ -2049,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"
@@ -2175,9 +2473,21 @@ checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
dependencies = [
"log",
"pin-project-lite",
"tracing-attributes",
"tracing-core",
]
[[package]]
name = "tracing-attributes"
version = "0.1.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
]
[[package]]
name = "tracing-core"
version = "0.1.34"
@@ -2187,6 +2497,16 @@ dependencies = [
"once_cell",
]
[[package]]
name = "transpose"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e"
dependencies = [
"num-integer",
"strength_reduce",
]
[[package]]
name = "try-lock"
version = "0.2.5"
@@ -2334,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"
@@ -2455,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.2.1"
version = "1.0.0"
[workspace.dependencies]
anyhow = "1"
@@ -31,17 +30,16 @@ 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 = "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"] }
serde_json = { version = "1.0.132", default-features = false }
sha2 = { version = "0.10", default-features = false }
sha3 = { version = "0.10", default-features = false }
thiserror = "1"
tokio = { version = "1.36", features = ["full"] }
warp = "0.3"
# Use local pow-core crate instead of the external qpow-math; keep API compatibility at the engine layer.
# qpow-math intentionally omitted here.
# 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

@@ -16,7 +16,7 @@ default = ["baseline"]
# Map engine features to pow-core features so engine-cpu consumers can toggle them.
baseline = ["pow-core/baseline"]
montgomery = ["pow-core/montgomery"]
simd-sha3 = ["pow-core/simd-sha3"]
simd-poseidon2 = ["pow-core/simd-poseidon2"]
[dependencies]
pow-core = { path = "../pow-core" }
@@ -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),

View File

@@ -32,6 +32,6 @@ cust = { version = "0.3", optional = true }
rustacuda = { version = "0.1", optional = true }
# Host-side hashing and big-int precompute for GPU constants
sha3 = { workspace = true }
qp-poseidon-core = { workspace = true }
num-bigint = "0.4"
num-traits = { workspace = true }

View File

@@ -1,6 +1,6 @@
# engine-gpu-cuda (CUDA backend) G1 bringup
This crate provides the CUDA GPU backend for the Quantus External Miner. It currently implements G1 bringup: the pernonce modular multiply loop runs on the GPU (512bit Montgomery CIOS), while SHA3512 and the threshold check run on the host CPU. This allows correctness and plumbing to be validated before we move SHA3 and earlyexit onto the device in G2.
This crate provides the CUDA GPU backend for the Quantus External Miner. It currently implements "G1" bringup: the pernonce modular multiply loop runs on the GPU (512bit Montgomery CIOS), while Poseidon2512 and the threshold check run on the host CPU. This allows correctness and plumbing to be validated before we move Poseidon2 and earlyexit onto the device in G2.
The backend is featuregated. When built with `--features cuda`, the crates build script compiles the CUDA kernel and embeds device images into the binary (CUBIN preferred, PTX as fallback). At runtime the engine selects an embedded image and launches the kernel to produce normalized `y` values per iteration.
@@ -10,16 +10,16 @@ The backend is featuregated. When built with `--features cuda`, the crates
- G1 (current):
- Device: 512bit Montgomery multiply (CIOS using 64×64→128 via `__umul64hi`), maintaining `ŷ` in Montgomery domain and converting to normal domain for output.
- Host: SHA3512 and threshold compare (and orchestration).
- Host: Poseidon2512 and threshold compare (and orchestration).
- Correctness: parity against CPU smallrange tests.
- Performance: primarily limited by PCIe copyback and host SHA3. See Tuning below.
- Performance: primarily limited by PCIe copyback and host Poseidon2. See "Tuning" below.
- G2 (next):
- Device: SHA3512 (Keccakf[1600], 24 rounds) optimized for 64byte input.
- Device: Poseidon2512 optimized for 64byte input.
- Device: threshold compare + global earlyexit flag (atomic) + tiny candidate write.
- Device: move constants to `__constant__` memory.
- Host: poll earlyexit; no large copybacks (only candidate or counters).
- Result: removes PCIe and hostSHA3 bottlenecks; enables real GPUbound throughput. Selection will be enabled via `MINER_CUDA_MODE=g2` once available.
- Result: removes PCIe and hostPoseidon2 bottlenecks; enables real GPUbound throughput. Selection will be enabled via `MINER_CUDA_MODE=g2` once available.
---
@@ -43,7 +43,7 @@ Notes:
## Runtime selection and embeds
At startup, the engine prefers the embedded CUBIN; if absent it falls back to the embedded PTX. You can override with `MINER_CUDA_IMAGE=cubin|ptx`. To attempt the G2 path (device SHA3 + early-exit), set `MINER_CUDA_MODE=g2`; if the G2 kernel isnt embedded/available for the current device, the engine will fall back to G1 automatically. Youll see logs like:
At startup, the engine prefers the embedded CUBIN; if absent it falls back to the embedded PTX. You can override with `MINER_CUDA_IMAGE=cubin|ptx`. To attempt the G2 path (device Poseidon2 + early-exit), set `MINER_CUDA_MODE=g2`; if the G2 kernel isn't embedded/available for the current device, the engine will fall back to G1 automatically. You'll see logs like:
- `CUDA: using CUBIN (embedded)`
- `CUDA: using PTX source = embedded`
- (If neither exists, the engine logs the absence and delegates to CPU fast engine.)
@@ -58,26 +58,26 @@ When a job runs, the engine prints its launch configuration and perlaunch out
## Env knobs runtime (G1)
These knobs affect GPU launch shape and how much work is returned to the host (and thus how much SHA3 the CPU must perform per launch).
These knobs affect GPU launch shape and how much work is returned to the host (and thus how much Poseidon2 the CPU must perform per launch).
- `MINER_CUDA_BLOCK_DIM` (default `256`)
- Threads per block (`blockDim.x`). Use a multiple of 32 (warp size). 256 is a good default.
- `MINER_CUDA_THREADS`
- Total threads (grid workload). Grid dimension is `grid_dim = ceil(threads / block_dim)`. Target at least “#SMs × 12 blocks” for decent occupancy (e.g., RTX 3060 has 28 SMs → 28 or 32 blocks).
- `MINER_CUDA_ITERS`
- Iterations per thread. Higher values produce larger output buffers and more host SHA3 work per launch.
- Iterations per thread. Higher values produce larger output buffers and more host Poseidon2 work per launch.
- `MINER_CUDA_IMAGE` = `cubin` | `ptx` (optional)
- Overrides the embedded image choice (debugging/testing). Default is to prefer CUBIN.
- `MINER_CUDA_HASH_THREADS` (optional)
- Number of host SHA3 worker threads to use to consume GPU output. Defaults to available parallelism.
- Number of host Poseidon2 worker threads to use to consume GPU output. Defaults to available parallelism.
- `MINER_CUDA_PINNED` = `1|true` (optional)
- Use pinned (page-locked) host buffers and asynchronous device-to-host copies for G1 copy-back to reduce PCIe latency.
- `MINER_CUDA_MODE` = `g2` (optional)
- Attempt G2 kernel (device SHA3-512 + threshold compare + early-exit). Falls back to G1 if the G2 kernel is not available for the current device image.
- Attempt G2 kernel (device Poseidon2-512 + threshold compare + early-exit). Falls back to G1 if the G2 kernel is not available for the current device image.
How much data per launch?
- y_out bytes = `threads × iters × 64`.
- Keep this around 64128 MB in G1 to avoid PCIe and host SHA3 dominating.
- Keep this around 64128 MB in G1 to avoid PCIe and host Poseidon2 dominating.
Example configs (RTX 3060, SM 86):
- ~64 MB per launch:
@@ -115,12 +115,12 @@ The build fails with clear messages if:
## Tuning guide (G1)
Goal in G1: balance kernel time (GPU) against copy-back time (PCIe) and host SHA3 time (CPU) to avoid starving the GPU or overwhelming the host. Practical steps:
Goal in G1: balance kernel time (GPU) against copy-back time (PCIe) and host Poseidon2 time (CPU) to avoid starving the GPU or overwhelming the host. Practical steps:
1) Size the output buffer:
- Start with 64128 MB per launch: `bytes ≈ threads × iters × 64`.
- Increase `threads` to raise occupancy (more blocks). Start with `block_dim=256`.
- Increase `iters` only while host SHA3 still keeps up.
- Increase `iters` only while host Poseidon2 still keeps up.
2) Watch timings:
- The engine logs `kernel_ms` and `copy_ms`.
@@ -159,7 +159,7 @@ Goal in G1: balance kernel time (GPU) against copy-back time (PCIe) and host SHA
## Roadmap to G2
- Device SHA3512 (Keccakf[1600], 24 rounds) tuned for 64B input.
- Device Poseidon2512 tuned for 64B input.
- Ondevice threshold compare and earlyexit flag (atomic).
- Host polling and tiny candidate copyback.
- Constants in `__constant__` memory.
@@ -185,9 +185,9 @@ Runtime:
- `MINER_CUDA_THREADS` — total threads (increase for more blocks).
- `MINER_CUDA_ITERS` — iterations per thread (controls y_out size).
- `MINER_CUDA_IMAGE` = `cubin|ptx` — force embedded image selection (optional).
- `MINER_CUDA_HASH_THREADS` — parallel host SHA3 workers (optional).
- `MINER_CUDA_HASH_THREADS` — parallel host Poseidon2 workers (optional).
- `MINER_CUDA_PINNED` = `1|true` — use pinned host buffers + async D2H copy (G1 optimization).
- `MINER_CUDA_MODE` = `g2` — try device SHA3 + early-exit; falls back to G1 if G2 kernel isnt available.
- `MINER_CUDA_MODE` = `g2` — try device Poseidon2 + early-exit; falls back to G1 if G2 kernel isn't available.
Build-time:
- `CUDA_ARCH` = `sm_86|sm_89|sm_120|…` — SM target for device images (normalized internally).
@@ -209,4 +209,4 @@ Presets are provided under `examples/.env` and follow a “lower” (≈1× SMs
- RTX A5000: `cuda-miner-a5000-lower.env`, `cuda-miner-a5000-upper.env`
- RTX A6000: `cuda-miner-a6000-lower.env`, `cuda-miner-a6000-upper.env`
Each preset uses `MINER_CUDA_MODE=g2` (device SHA3 + early-exit) and `MINER_CUDA_BLOCK_DIM=256`, and sizes `MINER_CUDA_THREADS` as `blocks × 256`. Adjust `MINER_CUDA_ITERS` to tune kernel dwell time vs early-exit responsiveness. If a G2 kernel image isnt embedded for your device, the engine falls back to G1 automatically.
Each preset uses `MINER_CUDA_MODE=g2` (device Poseidon2 + early-exit) and `MINER_CUDA_BLOCK_DIM=256`, and sizes `MINER_CUDA_THREADS` as `blocks × 256`. Adjust `MINER_CUDA_ITERS` to tune kernel dwell time vs early-exit responsiveness. If a G2 kernel image isn't embedded for your device, the engine falls back to G1 automatically.

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 }
sha3 = { workspace = true }
num-traits = { workspace = true }
metrics = { path = "../metrics", optional = true }
# Fixed-width big-integer backend for 512-bit Montgomery ops
crypto-bigint = "0.5"

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 SHA3 over big-endian limbs to avoid intermediate big-integer conversions.
- Per-job precompute cache (Montgomery params and `m_hat`) to reduce setup overhead.
- Runtime backend selection for microarchitecture-optimized kernels (x86_64 BMI2-only and BMI2+ADX; aarch64 UMULH).
- Metrics label for backend selection to aid dashboards and A/B analysis.
---
## Algorithm Overview
QPoW distance per nonce is computed as:
1) y update (group accumulation):
- y0 = m^(h + start_nonce) mod n (one-time per worker).
- y_{k+1} = (y_k * m) mod n (one multiply per nonce).
2) Distance = target XOR SHA3_512(y), where y is encoded as 64 big-endian bytes.
The engine focuses on optimizing step (1) with Montgomery multiplication and reducing overhead in step (2) by hashing directly over the final big-endian representation of y (no extra big-int intermediates).
---
## Montgomery Arithmetic (512-bit, 8×64 limbs)
We implement a 512-bit Montgomery field backed by 8 little-endian 64-bit limbs. For modulus `n` (odd composite, constrained by pow-core), we precompute:
- `n0_inv = -n^{-1} mod 2^64` (using 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 SHA3, we convert the residue `ŷ` back to the normal domain via `mont_mul(ŷ, 1)` and serialize as big-endian 64 bytes.
- We reuse a single `Sha3_512` hasher per search call and `finalize_reset()` each iteration to reduce construction overhead.
This preserves consensus behavior while avoiding unnecessary big-int allocations or conversions.
---
## Backend Selection and Optimizations
At runtime, the engine selects a Montgomery multiply backend based on CPU and environment:
- Portable (default/fallback)
- `mont_mul_portable`: pure-Rust, `u128`-based CIOS.
- Available everywhere.
- x86_64 (runtime detected)
- BMI2-only (`_mulx_u64`):
- `mont_mul_bmi2`: uses BMI2 MULX to get 128-bit products efficiently.
- Single carry chain (easier to validate; broadly available on newer CPUs).
- BMI2+ADX:
- `mont_mul_bmi2_adx`: implemented using MULX + ADCX/ADOX dual carry chains for higher ILP.
- aarch64
- UMULH/ADCS:
- `mont_mul_aarch64`: implemented using UMULH for high halves and ADCS-style accumulation via 64-bit ops to reduce dependency on `u128` where beneficial.
- Default backend on Apple Silicon/macOS and Linux ARM64.
You can override backend selection for testing:
- `MINER_MONT_BACKEND=portable|bmi2|bmi2-adx|umulh`
The engine logs the selected backend (and exports it via metrics) at job start. Unsupported overrides safely fall back with a clear warning.
---
## Metrics
The engine emits the same per-job and per-thread metrics as other engines, but adds a backend info gauge:
- `miner_engine_backend{engine="cpu-montgomery", backend="<name>"} = 1`
This makes it easy to pivot in Grafana by backend.
All other metrics (hash rates, progress chunking cadence, counters) are identical to `cpu-fast` for apples-to-apples comparisons.
---
## Correctness and Tests
We keep the portable CIOS path as the ground-truth reference for optimized kernels, and we cross-check against pow-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 SHA3 vs modular multiply.
- Direct-hash-from-residue + precompute caching already yields a measurable uplift over `cpu-fast`.
- The BMI2-only path should improve throughput on supporting x86_64 hardware.
- BMI2+ADX typically produces the highest gains on the multiply itself (often 1.52.0×), with end-to-end uplift bounded by SHA3 share per nonce.
- aarch64 UMULH/ADCS brings similar relative gains on Apple Silicon and other ARM64 platforms.
To minimize orchestration overhead in the service:
- Increase `--progress-chunk-ms` (e.g., 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 SHA3 from normalized big-endian bytes
- [x] Backend selection with log + metric
- [x] x86_64 BMI2-only MULX kernel
- [x] x86_64 BMI2+ADX (MULX + ADCX/ADOX) dual carry chain
- [x] aarch64 UMULH/ADCS kernel (macOS/Linux ARM64)
- [ ] Optional benchmark micro-harness (ns/op for mont_mul backends)
- [ ] Extend tests with more randomized vectors and edge-case sweeps
---
## Design Rationale
- Keep the interface and metrics identical to `cpu-fast` so that any performance deltas reflect algorithmic/microarchitectural improvements rather than service overhead.
- Keep a portable, well-reviewed core (CIOS) as a correctness reference.
- Add microarchitecture-optimized kernels behind runtime dispatch and an env override for safe, controlled rollouts.
- Log and export backend selection so A/B comparisons and regressions are easy to track.
---
## Contributing
- Changes to backends should include:
- Property tests vs portable.
- End-to-end parity checks vs `cpu-fast`.
- A note in this README describing the optimization and any preconditions (e.g., required CPU features).
- Keep unsafe code minimal, private, and well-commented.
- Prefer small, focused PRs for each backend/optimization to simplify review and bisecting.

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 SHA3-512 distance computation.
//!
//! Important: We intentionally keep emissions (hash_count increments, control flow)
//! identical to `cpu-fast` so metrics are directly comparable when pitting
//! `--engine cpu-fast` vs `--engine cpu-montgomery`.
use core::cmp::Ordering;
use engine_cpu::EngineStatus;
use engine_cpu::{EngineCandidate as Candidate, EngineRange as Range, MinerEngine};
use pow_core::compat;
use pow_core::{init_worker_y0, is_valid_distance, JobContext};
use primitive_types::U512;
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering as AtomicOrdering};
use std::sync::{Arc, Mutex};
/// Montgomery engine for CPU.
pub struct MontgomeryCpuEngine {
#[allow(clippy::type_complexity)]
cache: Mutex<HashMap<([u8; 64], [u8; 64]), Arc<mont_portable::MontCtx>>>,
}
impl Default for MontgomeryCpuEngine {
fn default() -> Self {
Self {
cache: Mutex::new(HashMap::new()),
}
}
}
impl MontgomeryCpuEngine {
pub fn new() -> Self {
Self::default()
}
}
impl MinerEngine for MontgomeryCpuEngine {
fn name(&self) -> &'static str {
"cpu-montgomery"
}
fn prepare_context(&self, header_hash: [u8; 32], threshold: U512) -> JobContext {
// Build the standard pow-core context (m, n, target, etc).
// Montgomery parameters are computed per search call for now; we can
// later thread them through a custom context wrapper if needed.
JobContext::new(header_hash, threshold)
}
fn search_range(&self, ctx: &JobContext, range: Range, cancel: &AtomicBool) -> EngineStatus {
// NOTE: For initial integration we mirror the cpu-fast engine logic to ensure
// metrics parity and correctness, while keeping the Montgomery scaffolding
// ready for optimization in follow-ups.
if range.start > range.end {
return EngineStatus::Exhausted { hash_count: 0 };
}
// One-time init per thread: y0 = m^(h + start_nonce) mod n
let mut current = range.start;
let y0 = init_worker_y0(ctx, current);
let mut hash_count: u64 = 0;
// Initialize or reuse per-job Montgomery params and residues from cache
let mont = {
let key = (ctx.m.to_big_endian(), ctx.n.to_big_endian());
let mut guard = self.cache.lock().unwrap();
if let Some(m) = guard.get(&key) {
m.clone()
} else {
let m = Arc::new(mont_portable::MontCtx::from_ctx(ctx));
guard.insert(key, m.clone());
m
}
};
let mut y_hat = mont.to_mont_u512(&y0);
let m_hat = mont.m_hat;
loop {
// Cancellation check (fast and frequent as in cpu-fast)
if cancel.load(AtomicOrdering::Relaxed) {
return EngineStatus::Cancelled { hash_count };
}
// Compute distance from Montgomery accumulator: normalize then hash via pow-core
let y_norm = mont.from_mont_u512(&y_hat);
let distance = pow_core::distance_from_y(ctx, y_norm);
hash_count = hash_count.saturating_add(1);
if is_valid_distance(ctx, distance) {
let work = current.to_big_endian();
return EngineStatus::Found {
candidate: Candidate {
nonce: current,
work,
distance,
},
hash_count,
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

@@ -18,8 +18,7 @@ default = ["baseline", "std"]
# std feature wires through to dependencies that have separate std/no_std builds.
std = [
"primitive-types/std",
"sha2/std",
"sha3/std",
"qp-poseidon-core/std",
]
# Baseline/reference implementation using BigUint-backed modular exponentiation.
@@ -28,8 +27,8 @@ baseline = []
# Enable accelerated modular arithmetic (e.g., Montgomery multiplication for 512-bit modulus).
montgomery = []
# Optionally enable a faster SHA3/Keccak path (hook for SIMD/alt backends).
simd-sha3 = []
# Optionally enable a faster Poseidon2 path (hook for SIMD/alt backends).
simd-poseidon2 = []
# Optional shared types/helpers for GPU engines (no device code here).
gpu-interop = []
@@ -39,8 +38,8 @@ log = { workspace = true }
primitive-types = { workspace = true }
num-bigint = { workspace = true }
num-traits = { workspace = true }
sha2 = { workspace = true }
sha3 = { workspace = true }
qp-poseidon-core = { workspace = true }
qpow-math = { workspace = true }
thiserror = { workspace = true }
anyhow = { workspace = true }

View File

@@ -1,430 +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 SHA3) behind features.
extern crate alloc;
use core::ops::BitXor;
use primitive_types::U512;
#[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 SHA3-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 SHA3-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)
}
/// SHA3-512 over the big-endian bytes of input U512.
pub fn sha3_512(input: U512) -> U512 {
super::sha3_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 SHA3_512(y)
pub fn distance_from_y(ctx: &JobContext, y: U512) -> U512 {
let hashed = sha3_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 SHA2-256(header)
/// - n: 512-bit derived via SHA3-512(header), iteratively rehashed until valid:
/// (odd, composite, coprime with m, and n > m)
fn get_random_rsa_impl(header: &[u8; 32]) -> (U512, U512) {
use sha2::{Digest, Sha256};
use sha3::Sha3_512;
// m from SHA2-256
let mut sha256 = Sha256::new();
sha256.update(header);
let m = U512::from_big_endian(sha256.finalize().as_slice());
// initial n from SHA3-512
let mut sha3 = Sha3_512::new();
sha3.update(header);
let mut n = U512::from_big_endian(sha3.finalize().as_slice());
// Keep hashing until n satisfies constraints
while n % 2u32 == U512::zero() || n <= m || !is_coprime_impl(&m, &n) || is_prime_impl(&n) {
n = sha3_512_impl(n);
}
(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 SHA3-512.
///
/// Note: The reference calls `hash_to_group_bigint` followed by an additional SHA3-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);
sha3_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 SHA3-512 to
/// bound false-positive probability to ~1/2^64 for composites.
fn is_prime_impl(n: &U512) -> bool {
use sha3::{Digest, Sha3_512};
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 SHA3
let mut bases = [U512::zero(); 32];
let mut base_count = 0;
let mut sha3 = Sha3_512::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);
sha3.update(bytes);
// Use the hash to generate a base in [2, n-2]
let hash = U512::from_big_endian(sha3.finalize_reset().as_slice());
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
}
/// SHA3-512 over the big-endian bytes of input `U512`.
fn sha3_512_impl(input: U512) -> U512 {
use sha3::Digest;
use sha3::Sha3_512;
let mut sha3 = Sha3_512::new();
let bytes = input.to_big_endian();
sha3.update(bytes);
U512::from_big_endian(sha3.finalize().as_slice())
}
#[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);
}