Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa9e7ca5f2 | ||
|
|
6902c650a9 | ||
|
|
e0f7beef74 | ||
|
|
e92c4f0769 | ||
|
|
06ddee497d | ||
|
|
63d7e114c2 | ||
|
|
57f69b6aae | ||
|
|
f35660c9e9 | ||
|
|
3466eff8b0 | ||
|
|
c0bfca592d | ||
|
|
6912470e84 | ||
|
|
534b3ac2f8 | ||
|
|
ff3e65361a |
146
.github/workflows/cuda-builder.yml
vendored
146
.github/workflows/cuda-builder.yml
vendored
@@ -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
|
||||
|
||||
13
.github/workflows/release-proposal.yml
vendored
13
.github/workflows/release-proposal.yml
vendored
@@ -96,15 +96,9 @@ jobs:
|
||||
|
||||
# Increment based on type
|
||||
if [[ "$VERSION_TYPE" == "major" ]]; then
|
||||
if [[ "$major" == "0" ]]; then # Handle 0.x.y -> 0.(x+1).0
|
||||
major=$major
|
||||
minor=$((minor + 1))
|
||||
patch=0
|
||||
else
|
||||
major=$((major + 1))
|
||||
minor=0
|
||||
patch=0
|
||||
fi
|
||||
major=$((major + 1))
|
||||
minor=0
|
||||
patch=0
|
||||
elif [[ "$VERSION_TYPE" == "minor" ]]; then
|
||||
minor=$((minor + 1))
|
||||
patch=0
|
||||
@@ -166,7 +160,6 @@ jobs:
|
||||
cargo update -p engine-cpu --precise "$new_cargo_version"
|
||||
cargo update -p engine-gpu-cuda --precise "$new_cargo_version"
|
||||
cargo update -p engine-gpu-opencl --precise "$new_cargo_version"
|
||||
cargo update -p engine-montgomery --precise "$new_cargo_version"
|
||||
|
||||
# Verify everything compiles correctly
|
||||
cargo check --workspace
|
||||
|
||||
273
Cargo.lock
generated
273
Cargo.lock
generated
@@ -26,6 +26,12 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anes"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.20"
|
||||
@@ -187,6 +193,12 @@ version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
|
||||
|
||||
[[package]]
|
||||
name = "cast"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.38"
|
||||
@@ -203,6 +215,33 @@ version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
|
||||
|
||||
[[package]]
|
||||
name = "ciborium"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
|
||||
dependencies = [
|
||||
"ciborium-io",
|
||||
"ciborium-ll",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ciborium-io"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
|
||||
|
||||
[[package]]
|
||||
name = "ciborium-ll"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
|
||||
dependencies = [
|
||||
"ciborium-io",
|
||||
"half",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cl-sys"
|
||||
version = "0.4.3"
|
||||
@@ -287,6 +326,42 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "criterion"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f"
|
||||
dependencies = [
|
||||
"anes",
|
||||
"cast",
|
||||
"ciborium",
|
||||
"clap",
|
||||
"criterion-plot",
|
||||
"is-terminal",
|
||||
"itertools 0.10.5",
|
||||
"num-traits 0.2.19",
|
||||
"once_cell",
|
||||
"oorandom",
|
||||
"plotters",
|
||||
"rayon",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"tinytemplate",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "criterion-plot"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
|
||||
dependencies = [
|
||||
"cast",
|
||||
"itertools 0.10.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam"
|
||||
version = "0.8.4"
|
||||
@@ -349,16 +424,6 @@ version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-bigint"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
|
||||
dependencies = [
|
||||
"rand_core 0.6.4",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.6"
|
||||
@@ -477,19 +542,21 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "engine-cpu"
|
||||
version = "0.3.0"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"criterion",
|
||||
"hex",
|
||||
"log",
|
||||
"pow-core",
|
||||
"primitive-types",
|
||||
"rand 0.8.5",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "engine-gpu-cuda"
|
||||
version = "0.3.0"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cust",
|
||||
@@ -508,7 +575,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "engine-gpu-opencl"
|
||||
version = "0.3.0"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"log",
|
||||
@@ -518,19 +585,6 @@ dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "engine-montgomery"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"crypto-bigint",
|
||||
"engine-cpu",
|
||||
"log",
|
||||
"metrics",
|
||||
"num-traits 0.2.19",
|
||||
"pow-core",
|
||||
"primitive-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enum_primitive"
|
||||
version = "0.1.1"
|
||||
@@ -775,6 +829,17 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "half"
|
||||
version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e54c115d4f30f52c67202f079c5f9d8b49db4691f460fdb0b4c2e838261b2ba5"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crunchy",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.16.0"
|
||||
@@ -1063,6 +1128,15 @@ version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.10.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.14.0"
|
||||
@@ -1136,7 +1210,7 @@ checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
|
||||
|
||||
[[package]]
|
||||
name = "metrics"
|
||||
version = "0.3.0"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"log",
|
||||
@@ -1166,7 +1240,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "miner-cli"
|
||||
version = "0.3.0"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"env_logger",
|
||||
@@ -1177,14 +1251,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "miner-service"
|
||||
version = "0.3.0"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"crossbeam-channel",
|
||||
"engine-cpu",
|
||||
"engine-gpu-cuda",
|
||||
"engine-gpu-opencl",
|
||||
"engine-montgomery",
|
||||
"hex",
|
||||
"log",
|
||||
"metrics",
|
||||
@@ -1202,7 +1275,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "miner-telemetry"
|
||||
version = "0.3.0"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures 0.3.31",
|
||||
@@ -1383,13 +1456,19 @@ version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
|
||||
|
||||
[[package]]
|
||||
name = "oorandom"
|
||||
version = "11.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
|
||||
|
||||
[[package]]
|
||||
name = "p3-dft"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3b2764a3982d22d62aa933c8de6f9d71d8a474c9110b69e675dea1887bdeffc"
|
||||
dependencies = [
|
||||
"itertools",
|
||||
"itertools 0.14.0",
|
||||
"p3-field",
|
||||
"p3-matrix",
|
||||
"p3-maybe-rayon",
|
||||
@@ -1403,7 +1482,7 @@ version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc13a73509fe09c67b339951ca8d4cc6e61c9bf08c130dbc90dda52452918cc2"
|
||||
dependencies = [
|
||||
"itertools",
|
||||
"itertools 0.14.0",
|
||||
"num-bigint",
|
||||
"p3-maybe-rayon",
|
||||
"p3-util",
|
||||
@@ -1437,7 +1516,7 @@ version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8e1e9f69c2fe15768b3ceb2915edb88c47398aa22c485d8163deab2a47fe194"
|
||||
dependencies = [
|
||||
"itertools",
|
||||
"itertools 0.14.0",
|
||||
"p3-field",
|
||||
"p3-maybe-rayon",
|
||||
"p3-util",
|
||||
@@ -1485,7 +1564,7 @@ version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72d5db8f05a26d706dfd8aaf7aa4272ca4f3e7a075db897ec7108f24fad78759"
|
||||
dependencies = [
|
||||
"itertools",
|
||||
"itertools 0.14.0",
|
||||
"p3-field",
|
||||
"serde",
|
||||
]
|
||||
@@ -1594,6 +1673,34 @@ version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "plotters"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747"
|
||||
dependencies = [
|
||||
"num-traits 0.2.19",
|
||||
"plotters-backend",
|
||||
"plotters-svg",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "plotters-backend"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a"
|
||||
|
||||
[[package]]
|
||||
name = "plotters-svg"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670"
|
||||
dependencies = [
|
||||
"plotters-backend",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.3"
|
||||
@@ -1605,7 +1712,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pow-core"
|
||||
version = "0.3.0"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"hex",
|
||||
@@ -1614,6 +1721,7 @@ dependencies = [
|
||||
"num-traits 0.2.19",
|
||||
"primitive-types",
|
||||
"qp-poseidon-core",
|
||||
"qpow-math",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
@@ -1670,19 +1778,45 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "qp-poseidon-core"
|
||||
version = "0.9.5"
|
||||
name = "qp-poseidon-constants"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec326fc2631a929de09a38af2613a3db5230882c12a2f68205693ec632751e8b"
|
||||
checksum = "4d56b56652e9f44a43de9593e75d7c3e0c3a352e10675cf3024e5b3175711cd3"
|
||||
dependencies = [
|
||||
"p3-field",
|
||||
"p3-goldilocks",
|
||||
"p3-poseidon2",
|
||||
"rand 0.9.2",
|
||||
"rand_chacha 0.9.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "qp-poseidon-core"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e658a373a7fb22babeda9ffcc8af0a894e6e3c008272ed735509eccb7769ead3"
|
||||
dependencies = [
|
||||
"p3-field",
|
||||
"p3-goldilocks",
|
||||
"p3-poseidon2",
|
||||
"p3-symmetric",
|
||||
"rand 0.9.2",
|
||||
"qp-poseidon-constants",
|
||||
"rand_chacha 0.9.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "qpow-math"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/Quantus-Network/chain.git#a5b9e4cc6900641884171a9972e0276ebb73a3ab"
|
||||
dependencies = [
|
||||
"hex",
|
||||
"log",
|
||||
"num-bigint",
|
||||
"num-traits 0.2.19",
|
||||
"primitive-types",
|
||||
"qp-poseidon-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quantus-miner-api"
|
||||
version = "0.0.3"
|
||||
@@ -1782,6 +1916,26 @@ dependencies = [
|
||||
"getrandom 0.3.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
|
||||
dependencies = [
|
||||
"either",
|
||||
"rayon-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon-core"
|
||||
version = "1.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
|
||||
dependencies = [
|
||||
"crossbeam-deque",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.17"
|
||||
@@ -1929,6 +2083,15 @@ version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scoped-tls"
|
||||
version = "1.0.1"
|
||||
@@ -2174,6 +2337,16 @@ dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinytemplate"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.47.1"
|
||||
@@ -2481,6 +2654,16 @@ version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "want"
|
||||
version = "0.3.1"
|
||||
@@ -2602,6 +2785,16 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.80"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fbe734895e869dc429d78c4b433f8d17d95f8d05317440b4fad5ab2d33e596dc"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.26.11"
|
||||
|
||||
@@ -3,7 +3,6 @@ members = [
|
||||
"crates/engine-cpu",
|
||||
"crates/engine-gpu-cuda",
|
||||
"crates/engine-gpu-opencl",
|
||||
"crates/engine-montgomery",
|
||||
"crates/metrics",
|
||||
"crates/miner-cli",
|
||||
"crates/miner-service",
|
||||
@@ -17,7 +16,7 @@ resolver = "2"
|
||||
edition = "2021"
|
||||
authors = ["Quantus Network"]
|
||||
description = "Quantus External Miner Workspace"
|
||||
version = "0.3.0"
|
||||
version = "1.0.0"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1"
|
||||
@@ -31,7 +30,8 @@ num-bigint = { version = "0.4", features = ["rand"] }
|
||||
num-traits = "0.2"
|
||||
num_cpus = "1.16"
|
||||
primitive-types = { version = "0.13.1", default-features = false }
|
||||
qp-poseidon-core = { version = "0.9.5", default-features = false }
|
||||
qp-poseidon-core = { version = "1.0.1", default-features = false }
|
||||
qpow-math = { git = "https://github.com/Quantus-Network/chain.git", package = "qpow-math", default-features = false }
|
||||
quantus-miner-api = { version = "0.0.3", default-features = false }
|
||||
rand = { version = "0.8.5", default-features = false }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
@@ -39,8 +39,7 @@ serde_json = { version = "1.0.132", default-features = false }
|
||||
thiserror = "1"
|
||||
tokio = { version = "1.36", features = ["full"] }
|
||||
warp = "0.3"
|
||||
# Use local pow-core crate instead of the external qpow-math; keep API compatibility at the engine layer.
|
||||
# qpow-math intentionally omitted here.
|
||||
# pow-core now delegates to qpow-math for core functionality while providing optimized mining scaffolding.
|
||||
|
||||
# GPU backend dependencies are declared in individual engine crates (no optional deps in workspace)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
56
crates/engine-cpu/benches/cpu_engine_bench.rs
Normal file
56
crates/engine-cpu/benches/cpu_engine_bench.rs
Normal 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);
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
use core::cmp::Ordering;
|
||||
|
||||
use pow_core::{distance_for_nonce, is_valid_distance, JobContext};
|
||||
use pow_core::{is_valid_nonce, JobContext};
|
||||
use primitive_types::U512;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering as AtomicOrdering};
|
||||
use std::time::Duration;
|
||||
@@ -31,7 +31,7 @@ pub struct Range {
|
||||
pub struct Candidate {
|
||||
pub nonce: U512,
|
||||
pub work: [u8; 64], // big-endian representation of nonce
|
||||
pub distance: U512, // achieved distance for this nonce
|
||||
pub hash: U512, // output hash for this nonce
|
||||
}
|
||||
|
||||
/// Origin of a found candidate.
|
||||
@@ -78,8 +78,8 @@ pub trait MinerEngine: Send + Sync {
|
||||
/// Human-readable engine name (for logs/metrics).
|
||||
fn name(&self) -> &'static str;
|
||||
|
||||
/// Prepare a precomputed context for a job (header + threshold).
|
||||
fn prepare_context(&self, header_hash: [u8; 32], threshold: U512) -> JobContext;
|
||||
/// Prepare a precomputed context for a job (header + difficulty).
|
||||
fn prepare_context(&self, header_hash: [u8; 32], difficulty: U512) -> JobContext;
|
||||
|
||||
/// Search an inclusive nonce range with cancellation support.
|
||||
///
|
||||
@@ -109,8 +109,8 @@ impl MinerEngine for BaselineCpuEngine {
|
||||
"cpu-baseline"
|
||||
}
|
||||
|
||||
fn prepare_context(&self, header_hash: [u8; 32], threshold: U512) -> JobContext {
|
||||
JobContext::new(header_hash, threshold)
|
||||
fn prepare_context(&self, header_hash: [u8; 32], difficulty: U512) -> JobContext {
|
||||
JobContext::new(header_hash, difficulty)
|
||||
}
|
||||
|
||||
fn search_range(&self, ctx: &JobContext, range: Range, cancel: &AtomicBool) -> EngineStatus {
|
||||
@@ -128,17 +128,18 @@ impl MinerEngine for BaselineCpuEngine {
|
||||
return EngineStatus::Cancelled { hash_count };
|
||||
}
|
||||
|
||||
// Compute distance for this nonce using the context.
|
||||
let distance = distance_for_nonce(ctx, current);
|
||||
// Compute hash for this nonce using Bitcoin-style double Poseidon2
|
||||
let (is_valid, hash) =
|
||||
is_valid_nonce(ctx.header, current.to_big_endian(), ctx.difficulty);
|
||||
hash_count = hash_count.saturating_add(1);
|
||||
|
||||
// Check if it's valid under threshold.
|
||||
if is_valid_distance(ctx, distance) {
|
||||
// Check if it meets difficulty target
|
||||
if is_valid {
|
||||
let work = current.to_big_endian();
|
||||
let candidate = Candidate {
|
||||
nonce: current,
|
||||
work,
|
||||
distance,
|
||||
hash,
|
||||
};
|
||||
return EngineStatus::Found {
|
||||
candidate,
|
||||
@@ -178,12 +179,12 @@ impl MinerEngine for FastCpuEngine {
|
||||
"cpu-fast"
|
||||
}
|
||||
|
||||
fn prepare_context(&self, header_hash: [u8; 32], threshold: U512) -> JobContext {
|
||||
JobContext::new(header_hash, threshold)
|
||||
fn prepare_context(&self, header_hash: [u8; 32], difficulty: U512) -> JobContext {
|
||||
JobContext::new(header_hash, difficulty)
|
||||
}
|
||||
|
||||
fn search_range(&self, ctx: &JobContext, range: Range, cancel: &AtomicBool) -> EngineStatus {
|
||||
use pow_core::{distance_from_y, init_worker_y0, is_valid_distance, step_mul};
|
||||
use pow_core::{hash_from_nonce, is_valid_hash, step_nonce};
|
||||
|
||||
// Ensure start <= end (inclusive range). If not, treat as exhausted.
|
||||
if range.start > range.end {
|
||||
@@ -191,7 +192,6 @@ impl MinerEngine for FastCpuEngine {
|
||||
}
|
||||
|
||||
let mut current = range.start;
|
||||
let mut y = init_worker_y0(ctx, current);
|
||||
let mut hash_count: u64 = 0;
|
||||
|
||||
loop {
|
||||
@@ -200,17 +200,17 @@ impl MinerEngine for FastCpuEngine {
|
||||
return EngineStatus::Cancelled { hash_count };
|
||||
}
|
||||
|
||||
// Compute distance from current accumulator
|
||||
let distance = distance_from_y(ctx, y);
|
||||
// Compute hash using Bitcoin-style double Poseidon2
|
||||
let hash = hash_from_nonce(ctx, current);
|
||||
hash_count = hash_count.saturating_add(1);
|
||||
|
||||
if is_valid_distance(ctx, distance) {
|
||||
if is_valid_hash(ctx, hash) {
|
||||
let work = current.to_big_endian();
|
||||
return EngineStatus::Found {
|
||||
candidate: Candidate {
|
||||
nonce: current,
|
||||
work,
|
||||
distance,
|
||||
hash,
|
||||
},
|
||||
hash_count,
|
||||
origin: FoundOrigin::Cpu,
|
||||
@@ -218,13 +218,14 @@ impl MinerEngine for FastCpuEngine {
|
||||
}
|
||||
|
||||
if current == range.end {
|
||||
break EngineStatus::Exhausted { hash_count };
|
||||
break;
|
||||
}
|
||||
|
||||
// Advance to next nonce: y <- y * m (mod n), current <- current + 1
|
||||
y = step_mul(ctx, y);
|
||||
current = current.saturating_add(U512::one());
|
||||
// Advance to next nonce
|
||||
current = step_nonce(current);
|
||||
}
|
||||
|
||||
EngineStatus::Exhausted { hash_count }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,9 +260,9 @@ impl MinerEngine for ChainManipulatorEngine {
|
||||
"cpu-chain-manipulator"
|
||||
}
|
||||
|
||||
fn prepare_context(&self, header_hash: [u8; 32], threshold: U512) -> JobContext {
|
||||
fn prepare_context(&self, header_hash: [u8; 32], difficulty: U512) -> JobContext {
|
||||
// Per-block throttling: do NOT increment here. We increment on Found (i.e., when a block is solved).
|
||||
let ctx = JobContext::new(header_hash, threshold);
|
||||
let ctx = JobContext::new(header_hash, difficulty);
|
||||
// Debug: log current throttle state at job start
|
||||
log::debug!(
|
||||
target: "miner",
|
||||
@@ -274,16 +275,14 @@ impl MinerEngine for ChainManipulatorEngine {
|
||||
}
|
||||
|
||||
fn search_range(&self, ctx: &JobContext, range: Range, cancel: &AtomicBool) -> EngineStatus {
|
||||
use pow_core::{distance_from_y, init_worker_y0, is_valid_distance, step_mul};
|
||||
use pow_core::{hash_from_nonce, is_valid_hash, step_nonce};
|
||||
|
||||
if range.start > range.end {
|
||||
return EngineStatus::Exhausted { hash_count: 0 };
|
||||
}
|
||||
|
||||
// Fast incremental path (same as cpu-fast) to start with high hashrate:
|
||||
// y0 = m^(h + start_nonce) mod n, then y = y * m (mod n) each step.
|
||||
// Bitcoin-style hashing path
|
||||
let mut current = range.start;
|
||||
let mut y = init_worker_y0(ctx, current);
|
||||
let mut hash_count: u64 = 0;
|
||||
let mut batch_counter: u64 = 0;
|
||||
|
||||
@@ -302,8 +301,8 @@ impl MinerEngine for ChainManipulatorEngine {
|
||||
return EngineStatus::Cancelled { hash_count };
|
||||
}
|
||||
|
||||
// Compute distance from current accumulator (incremental path).
|
||||
let distance = distance_from_y(ctx, y);
|
||||
// Compute hash using Bitcoin-style double Poseidon2
|
||||
let hash = hash_from_nonce(ctx, current);
|
||||
hash_count = hash_count.saturating_add(1);
|
||||
batch_counter = batch_counter.saturating_add(1);
|
||||
|
||||
@@ -311,7 +310,7 @@ impl MinerEngine for ChainManipulatorEngine {
|
||||
#[allow(unused_variables)]
|
||||
let _dbg_batch = batch_counter;
|
||||
|
||||
if is_valid_distance(ctx, distance) {
|
||||
if is_valid_hash(ctx, hash) {
|
||||
let work = current.to_big_endian();
|
||||
// Increment solved-block counter so the NEXT block throttles more.
|
||||
let _new_idx = self.job_index.fetch_add(1, AtomicOrdering::Relaxed) + 1;
|
||||
@@ -333,7 +332,7 @@ impl MinerEngine for ChainManipulatorEngine {
|
||||
candidate: Candidate {
|
||||
nonce: current,
|
||||
work,
|
||||
distance,
|
||||
hash,
|
||||
},
|
||||
hash_count,
|
||||
origin: FoundOrigin::Cpu,
|
||||
@@ -348,9 +347,7 @@ impl MinerEngine for ChainManipulatorEngine {
|
||||
|
||||
// Advance
|
||||
if current < range.end {
|
||||
// y <- y * m (mod n); current <- current + 1
|
||||
y = step_mul(ctx, y);
|
||||
current = current.saturating_add(U512::one());
|
||||
current = step_nonce(current);
|
||||
} else {
|
||||
break EngineStatus::Exhausted { hash_count };
|
||||
}
|
||||
@@ -371,60 +368,16 @@ mod tests {
|
||||
|
||||
fn make_ctx() -> JobContext {
|
||||
let header = [1u8; 32];
|
||||
let threshold = U512::MAX; // permissive threshold for "found" parity test
|
||||
JobContext::new(header, threshold)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn baseline_and_fast_engines_find_same_candidate_on_small_range() {
|
||||
let ctx = make_ctx();
|
||||
|
||||
let range = Range {
|
||||
start: U512::from(0u64),
|
||||
end: U512::from(100u64),
|
||||
};
|
||||
|
||||
let cancel = AtomicBool::new(false);
|
||||
|
||||
let baseline = BaselineCpuEngine::new();
|
||||
let fast = FastCpuEngine::new();
|
||||
|
||||
let b_status = baseline.search_range(&ctx, range.clone(), &cancel);
|
||||
let f_status = fast.search_range(&ctx, range.clone(), &cancel);
|
||||
|
||||
match (b_status, f_status) {
|
||||
(
|
||||
EngineStatus::Found {
|
||||
candidate: b_cand,
|
||||
hash_count: b_hashes,
|
||||
origin: _,
|
||||
},
|
||||
EngineStatus::Found {
|
||||
candidate: f_cand,
|
||||
hash_count: f_hashes,
|
||||
origin: _,
|
||||
},
|
||||
) => {
|
||||
assert_eq!(
|
||||
b_cand.nonce, f_cand.nonce,
|
||||
"engines disagreed on winning nonce"
|
||||
);
|
||||
assert_eq!(
|
||||
b_cand.distance, f_cand.distance,
|
||||
"engines disagreed on distance"
|
||||
);
|
||||
assert_eq!(b_hashes, f_hashes, "engines disagreed on hash_count");
|
||||
}
|
||||
(b, f) => panic!("expected Found/Found, got baseline={b:?}, fast={f:?}"),
|
||||
}
|
||||
let difficulty = U512::from(1u64); // easy difficulty for "found" parity test
|
||||
JobContext::new(header, difficulty)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn engine_returns_exhausted_when_no_solution_in_range() {
|
||||
// Use a very strict threshold to make solutions effectively impossible in a tiny range.
|
||||
// Use a very hard difficulty to make solutions effectively impossible in a tiny range.
|
||||
let header = [2u8; 32];
|
||||
let threshold = U512::zero();
|
||||
let ctx = JobContext::new(header, threshold);
|
||||
let difficulty = U512::MAX;
|
||||
let ctx = JobContext::new(header, difficulty);
|
||||
|
||||
let range = Range {
|
||||
start: U512::from(1u64),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
[package]
|
||||
name = "engine-montgomery"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
description = "Montgomery-optimized CPU mining engine for Quantus External Miner"
|
||||
|
||||
[lib]
|
||||
name = "engine_montgomery"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
metrics = ["dep:metrics"]
|
||||
|
||||
[dependencies]
|
||||
# Reuse the engine trait and shared types exposed by engine-cpu
|
||||
engine-cpu = { path = "../engine-cpu" }
|
||||
# Access job context and core PoW math interfaces
|
||||
pow-core = { path = "../pow-core" }
|
||||
|
||||
# Workspace-aligned deps
|
||||
primitive-types = { workspace = true }
|
||||
log = { workspace = true }
|
||||
|
||||
num-traits = { workspace = true }
|
||||
metrics = { path = "../metrics", optional = true }
|
||||
|
||||
# Fixed-width big-integer backend for 512-bit Montgomery ops
|
||||
crypto-bigint = "0.5"
|
||||
@@ -1,210 +0,0 @@
|
||||
# engine-montgomery
|
||||
|
||||
Montgomery-optimized CPU mining engine for the Quantus External Miner.
|
||||
|
||||
This crate implements the same public `MinerEngine` trait as the baseline and fast CPU engines, while replacing the per-nonce modular multiplication with a fixed-width 8×64-bit Montgomery implementation. It is selectable via the CLI flag:
|
||||
|
||||
- `--engine cpu-montgomery`
|
||||
|
||||
The engine mirrors `cpu-fast` control flow and metrics emission so that apples-to-apples comparisons can be made between engines.
|
||||
|
||||
Highlights:
|
||||
- Fixed-width 512-bit arithmetic using 8×64-bit limbs.
|
||||
- Portable CIOS (Coarsely Integrated Operand Scanning) Montgomery multiply with `u128` intermediates.
|
||||
- Direct Poseidon2 over big-endian limbs to avoid intermediate big-integer conversions.
|
||||
- Per-job precompute cache (Montgomery params and `m_hat`) to reduce setup overhead.
|
||||
- Runtime backend selection for microarchitecture-optimized kernels (x86_64 BMI2-only and BMI2+ADX; aarch64 UMULH).
|
||||
- Metrics label for backend selection to aid dashboards and A/B analysis.
|
||||
|
||||
---
|
||||
|
||||
## Algorithm Overview
|
||||
|
||||
QPoW distance per nonce is computed as:
|
||||
|
||||
1) y update (group accumulation):
|
||||
- y0 = m^(h + start_nonce) mod n (one-time per worker).
|
||||
- y_{k+1} = (y_k * m) mod n (one multiply per nonce).
|
||||
|
||||
2) Distance = target XOR Poseidon2_512(y), where y is encoded as 64 big-endian bytes.
|
||||
|
||||
The engine focuses on optimizing step (1) with Montgomery multiplication and reducing overhead in step (2) by hashing directly over the final big-endian representation of y (no extra big-int intermediates).
|
||||
|
||||
---
|
||||
|
||||
## Montgomery Arithmetic (512-bit, 8×64 limbs)
|
||||
|
||||
We implement a 512-bit Montgomery field backed by 8 little-endian 64-bit limbs. For modulus `n` (odd composite, constrained by pow-core), we precompute:
|
||||
|
||||
- `n0_inv = -n^{-1} mod 2^64` (using Newton–Raphson).
|
||||
- `R = 2^(64*8) mod n` (implicit via representation).
|
||||
- `R^2 mod n` (computed once per job using the existing big-integer reference).
|
||||
- `to_mont(x) = x * R mod n = mont_mul(x, R^2)`.
|
||||
- `from_mont(x̂) = x̂ * 1 mod n = mont_mul(x̂, 1)`.
|
||||
|
||||
The core multiply is a portable CIOS Montgomery reduction:
|
||||
|
||||
```
|
||||
- acc <- 0
|
||||
- For each limb i in a:
|
||||
acc += a[i] * b
|
||||
m = (acc[0] * n0_inv) mod 2^64
|
||||
acc += m * n
|
||||
acc = acc >> 64 (shift down one limb)
|
||||
- If acc >= n: acc -= n
|
||||
- return acc
|
||||
```
|
||||
|
||||
Where `acc` is a 9-limb `u128` accumulator to simplify carries.
|
||||
|
||||
This yields a single multiply+reduce per nonce with a branchless inner loop (except final conditional subtract).
|
||||
|
||||
---
|
||||
|
||||
## Hashing Strategy
|
||||
|
||||
- We keep `y` in Montgomery domain during iteration to minimize transforms.
|
||||
- Before Poseidon2, we convert the residue `ŷ` back to the normal domain via `mont_mul(ŷ, 1)` and serialize as big-endian 64 bytes.
|
||||
- We reuse a single `Poseidon2Core` hasher per search call and `hash_512()` each iteration to reduce construction overhead.
|
||||
|
||||
This preserves consensus behavior while avoiding unnecessary big-int allocations or conversions.
|
||||
|
||||
---
|
||||
|
||||
## Backend Selection and Optimizations
|
||||
|
||||
At runtime, the engine selects a Montgomery multiply backend based on CPU and environment:
|
||||
|
||||
- Portable (default/fallback)
|
||||
- `mont_mul_portable`: pure-Rust, `u128`-based CIOS.
|
||||
- Available everywhere.
|
||||
|
||||
- x86_64 (runtime detected)
|
||||
- BMI2-only (`_mulx_u64`):
|
||||
- `mont_mul_bmi2`: uses BMI2 MULX to get 128-bit products efficiently.
|
||||
- Single carry chain (easier to validate; broadly available on newer CPUs).
|
||||
- BMI2+ADX:
|
||||
- `mont_mul_bmi2_adx`: implemented using MULX + ADCX/ADOX dual carry chains for higher ILP.
|
||||
|
||||
- aarch64
|
||||
- UMULH/ADCS:
|
||||
- `mont_mul_aarch64`: implemented using UMULH for high halves and ADCS-style accumulation via 64-bit ops to reduce dependency on `u128` where beneficial.
|
||||
- Default backend on Apple Silicon/macOS and Linux ARM64.
|
||||
|
||||
You can override backend selection for testing:
|
||||
|
||||
- `MINER_MONT_BACKEND=portable|bmi2|bmi2-adx|umulh`
|
||||
|
||||
The engine logs the selected backend (and exports it via metrics) at job start. Unsupported overrides safely fall back with a clear warning.
|
||||
|
||||
---
|
||||
|
||||
## Metrics
|
||||
|
||||
The engine emits the same per-job and per-thread metrics as other engines, but adds a backend info gauge:
|
||||
|
||||
- `miner_engine_backend{engine="cpu-montgomery", backend="<name>"} = 1`
|
||||
|
||||
This makes it easy to pivot in Grafana by backend.
|
||||
|
||||
All other metrics (hash rates, progress chunking cadence, counters) are identical to `cpu-fast` for apples-to-apples comparisons.
|
||||
|
||||
---
|
||||
|
||||
## Correctness and Tests
|
||||
|
||||
We keep the portable CIOS path as the ground-truth reference for optimized kernels, and we cross-check against pow-core’s BigUint-based implementations.
|
||||
|
||||
Property tests included:
|
||||
|
||||
- Portable Montgomery vs reference incremental multiply:
|
||||
- `from_mont(mul(to_mont(y), to_mont(m))) == step_mul(y)` across multiple steps.
|
||||
|
||||
- BMI2 vs Portable:
|
||||
- For randomized sequences, `bmi2` backend must match `portable` exactly at each step.
|
||||
- On non-x86_64 platforms, the test still runs but both backends fall back to `portable`.
|
||||
|
||||
- aarch64 UMULH vs Portable:
|
||||
- For randomized sequences, `aarch64-umulh` must match `portable` exactly at each step.
|
||||
- On non-aarch64 platforms, both tags fall back to `portable`.
|
||||
|
||||
- End-to-end parity:
|
||||
- `cpu-montgomery` vs `cpu-fast` on a small inclusive range (distance and winner parity; identical hash_count accounting).
|
||||
|
||||
We recommend running the property tests on machines with and without BMI2/ADX and on aarch64 to cover all optimized code paths.
|
||||
|
||||
---
|
||||
|
||||
## Safety
|
||||
|
||||
- The crate uses `#![deny(unsafe_code)]`.
|
||||
- `unsafe` is scoped only to tiny backend functions:
|
||||
- x86_64: `_mulx_u64` and inline asm for ADCX/ADOX dual carry chains.
|
||||
- aarch64: restricted intrinsics (e.g., UMULH) behind a small boundary.
|
||||
- All other code remains safe Rust.
|
||||
- The portable path is always available as a fallback for correctness/regression checks.
|
||||
|
||||
---
|
||||
|
||||
## Performance Notes
|
||||
|
||||
- Relative gains depend on how much time the miner spends in Poseidon2 vs modular multiply.
|
||||
- Direct-hash-from-residue + precompute caching already yields a measurable uplift over `cpu-fast`.
|
||||
- The BMI2-only path should improve throughput on supporting x86_64 hardware.
|
||||
- BMI2+ADX typically produces the highest gains on the multiply itself (often 1.5–2.0×), with end-to-end uplift bounded by Poseidon2 share per nonce.
|
||||
- aarch64 UMULH/ADCS brings similar relative gains on Apple Silicon and other ARM64 platforms.
|
||||
|
||||
To minimize orchestration overhead in the service:
|
||||
- Increase `--progress-chunk-ms` (e.g., 3000–5000) on both engines when comparing, to reduce update traffic and context switching.
|
||||
|
||||
---
|
||||
|
||||
## Runtime and CI Tips
|
||||
|
||||
- Selecting the engine:
|
||||
- `quantus-miner --engine cpu-montgomery`
|
||||
|
||||
- For A/B:
|
||||
- Keep workers and chunking identical across instances.
|
||||
- If testing backends explicitly:
|
||||
- `MINER_MONT_BACKEND=bmi2` or `bmi2-adx` on capable x86_64 hardware
|
||||
- `MINER_MONT_BACKEND=umulh` on aarch64
|
||||
- Check logs and metrics for the selected backend label.
|
||||
|
||||
- Observability:
|
||||
- Ensure metrics exporter is enabled (`--metrics-port ...`) for dashboards.
|
||||
- Filter or group by `engine="cpu-montgomery"` and backend metric to compare microarchitectural paths.
|
||||
|
||||
---
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [x] Portable 8×64 CIOS (u128)
|
||||
- [x] Per-job precompute cache (`n0_inv`, `R^2 mod n`, `m_hat`)
|
||||
- [x] Direct Poseidon2 from normalized big-endian bytes
|
||||
- [x] Backend selection with log + metric
|
||||
- [x] x86_64 BMI2-only MULX kernel
|
||||
- [x] x86_64 BMI2+ADX (MULX + ADCX/ADOX) dual carry chain
|
||||
- [x] aarch64 UMULH/ADCS kernel (macOS/Linux ARM64)
|
||||
- [ ] Optional benchmark micro-harness (ns/op for mont_mul backends)
|
||||
- [ ] Extend tests with more randomized vectors and edge-case sweeps
|
||||
|
||||
---
|
||||
|
||||
## Design Rationale
|
||||
|
||||
- Keep the interface and metrics identical to `cpu-fast` so that any performance deltas reflect algorithmic/microarchitectural improvements rather than service overhead.
|
||||
- Keep a portable, well-reviewed core (CIOS) as a correctness reference.
|
||||
- Add microarchitecture-optimized kernels behind runtime dispatch and an env override for safe, controlled rollouts.
|
||||
- Log and export backend selection so A/B comparisons and regressions are easy to track.
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
- Changes to backends should include:
|
||||
- Property tests vs portable.
|
||||
- End-to-end parity checks vs `cpu-fast`.
|
||||
- A note in this README describing the optimization and any preconditions (e.g., required CPU features).
|
||||
- Keep unsafe code minimal, private, and well-commented.
|
||||
- Prefer small, focused PRs for each backend/optimization to simplify review and bisecting.
|
||||
@@ -1,974 +0,0 @@
|
||||
#![deny(rust_2018_idioms)]
|
||||
#![deny(unsafe_code)]
|
||||
|
||||
//! Montgomery-optimized CPU mining engine (scaffolding).
|
||||
//!
|
||||
//! Goals:
|
||||
//! - Mirror the cpu-fast engine behavior and metrics (hash counts, progress cadence).
|
||||
//! - Provide a drop-in engine selectable via `--engine cpu-montgomery`.
|
||||
//! - Introduce a crypto-bigint based 512-bit fixed-width backend scaffold for future
|
||||
//! Montgomery multiplication/reduction.
|
||||
//!
|
||||
//! Current state:
|
||||
//! - The search loop mirrors `engine-cpu`'s `FastCpuEngine` to ensure apples-for-apples
|
||||
//! metrics and correctness parity.
|
||||
//! - A lightweight Montgomery scaffolding is included (conversions and parameter
|
||||
//! container) using `crypto-bigint`, ready to be integrated into the hot path.
|
||||
//!
|
||||
//! Next steps (planned):
|
||||
//! - Replace the per-step `step_mul` with Montgomery domain multiplication:
|
||||
//! y_hat <- montgomery_mul(y_hat, m_hat, n, n') with y kept in Montgomery domain.
|
||||
//! - Precompute `R`, `R^2`, and `n'` once per job/thread and transform inputs.
|
||||
//! - Convert out of Montgomery before Poseidon2-512 distance computation.
|
||||
//!
|
||||
//! Important: We intentionally keep emissions (hash_count increments, control flow)
|
||||
//! identical to `cpu-fast` so metrics are directly comparable when pitting
|
||||
//! `--engine cpu-fast` vs `--engine cpu-montgomery`.
|
||||
|
||||
use core::cmp::Ordering;
|
||||
|
||||
use engine_cpu::EngineStatus;
|
||||
use engine_cpu::{EngineCandidate as Candidate, EngineRange as Range, MinerEngine};
|
||||
use pow_core::compat;
|
||||
use pow_core::{init_worker_y0, is_valid_distance, JobContext};
|
||||
use primitive_types::U512;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::{AtomicBool, Ordering as AtomicOrdering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
/// Montgomery engine for CPU.
|
||||
pub struct MontgomeryCpuEngine {
|
||||
#[allow(clippy::type_complexity)]
|
||||
cache: Mutex<HashMap<([u8; 64], [u8; 64]), Arc<mont_portable::MontCtx>>>,
|
||||
}
|
||||
|
||||
impl Default for MontgomeryCpuEngine {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
cache: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MontgomeryCpuEngine {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl MinerEngine for MontgomeryCpuEngine {
|
||||
fn name(&self) -> &'static str {
|
||||
"cpu-montgomery"
|
||||
}
|
||||
|
||||
fn prepare_context(&self, header_hash: [u8; 32], threshold: U512) -> JobContext {
|
||||
// Build the standard pow-core context (m, n, target, etc).
|
||||
// Montgomery parameters are computed per search call for now; we can
|
||||
// later thread them through a custom context wrapper if needed.
|
||||
JobContext::new(header_hash, threshold)
|
||||
}
|
||||
|
||||
fn search_range(&self, ctx: &JobContext, range: Range, cancel: &AtomicBool) -> EngineStatus {
|
||||
// NOTE: For initial integration we mirror the cpu-fast engine logic to ensure
|
||||
// metrics parity and correctness, while keeping the Montgomery scaffolding
|
||||
// ready for optimization in follow-ups.
|
||||
|
||||
if range.start > range.end {
|
||||
return EngineStatus::Exhausted { hash_count: 0 };
|
||||
}
|
||||
|
||||
// One-time init per thread: y0 = m^(h + start_nonce) mod n
|
||||
let mut current = range.start;
|
||||
let y0 = init_worker_y0(ctx, current);
|
||||
let mut hash_count: u64 = 0;
|
||||
|
||||
// Initialize or reuse per-job Montgomery params and residues from cache
|
||||
let mont = {
|
||||
let key = (ctx.m.to_big_endian(), ctx.n.to_big_endian());
|
||||
let mut guard = self.cache.lock().unwrap();
|
||||
if let Some(m) = guard.get(&key) {
|
||||
m.clone()
|
||||
} else {
|
||||
let m = Arc::new(mont_portable::MontCtx::from_ctx(ctx));
|
||||
guard.insert(key, m.clone());
|
||||
m
|
||||
}
|
||||
};
|
||||
let mut y_hat = mont.to_mont_u512(&y0);
|
||||
let m_hat = mont.m_hat;
|
||||
|
||||
loop {
|
||||
// Cancellation check (fast and frequent as in cpu-fast)
|
||||
if cancel.load(AtomicOrdering::Relaxed) {
|
||||
return EngineStatus::Cancelled { hash_count };
|
||||
}
|
||||
|
||||
// Compute distance from Montgomery accumulator: normalize then hash via pow-core
|
||||
let y_norm = mont.from_mont_u512(&y_hat);
|
||||
let distance = pow_core::distance_from_y(ctx, y_norm);
|
||||
hash_count = hash_count.saturating_add(1);
|
||||
|
||||
if is_valid_distance(ctx, distance) {
|
||||
let work = current.to_big_endian();
|
||||
return EngineStatus::Found {
|
||||
candidate: Candidate {
|
||||
nonce: current,
|
||||
work,
|
||||
distance,
|
||||
},
|
||||
hash_count,
|
||||
origin: engine_cpu::FoundOrigin::Cpu,
|
||||
};
|
||||
}
|
||||
|
||||
// Advance or finish
|
||||
match current.cmp(&range.end) {
|
||||
Ordering::Less => {
|
||||
// Incremental step in Montgomery domain: y_hat <- y_hat * m_hat (mod n)
|
||||
y_hat = mont.mul(&y_hat, &m_hat);
|
||||
current = current.saturating_add(U512::one());
|
||||
}
|
||||
_ => {
|
||||
break EngineStatus::Exhausted { hash_count };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Montgomery scaffolding with crypto-bigint.
|
||||
/// This module provides conversions and parameter containers that we can use
|
||||
/// to wire up a fixed-width limb backend for 512-bit operations.
|
||||
mod mont_portable {
|
||||
use super::*;
|
||||
|
||||
// Montgomery context with portable CIOS 8x64 implementation (u128 intermediates).
|
||||
// Limbs are stored little-endian (limb 0 is least significant).
|
||||
type MulFn = fn(&[u64; 8], &[u64; 8], &[u64; 8], u64) -> [u64; 8];
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MontCtx {
|
||||
n: [u64; 8],
|
||||
n0_inv: u64, // -n^{-1} mod 2^64
|
||||
r2: [u64; 8], // R^2 mod n
|
||||
pub m_hat: [u64; 8],
|
||||
mul_fn: MulFn,
|
||||
}
|
||||
|
||||
impl MontCtx {
|
||||
pub fn from_ctx(ctx: &JobContext) -> Self {
|
||||
let n = u512_to_le(ctx.n);
|
||||
let n0_inv = mont_n0_inv(n[0]);
|
||||
let r2_u512 = compat::mod_pow(&U512::from(2u32), &U512::from(1024u32), &ctx.n);
|
||||
let r2 = u512_to_le(r2_u512);
|
||||
let m = u512_to_le(ctx.m);
|
||||
let (mul_fn, backend) = select_backend();
|
||||
log::info!(target: "miner", "cpu-montgomery backend selected: {backend}");
|
||||
#[cfg(feature = "metrics")]
|
||||
{
|
||||
metrics::set_engine_backend("cpu-montgomery", backend);
|
||||
}
|
||||
let m_hat = mul_fn(&m, &r2, &n, n0_inv);
|
||||
MontCtx {
|
||||
n,
|
||||
n0_inv,
|
||||
r2,
|
||||
m_hat,
|
||||
mul_fn,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_mont_u512(&self, x: &U512) -> [u64; 8] {
|
||||
let xl = u512_to_le(*x);
|
||||
(self.mul_fn)(&xl, &self.r2, &self.n, self.n0_inv)
|
||||
}
|
||||
|
||||
#[allow(clippy::wrong_self_convention)]
|
||||
pub fn from_mont_u512(&self, x_hat: &[u64; 8]) -> U512 {
|
||||
let one = {
|
||||
let mut o = [0u64; 8];
|
||||
o[0] = 1;
|
||||
o
|
||||
};
|
||||
let norm_le = (self.mul_fn)(x_hat, &one, &self.n, self.n0_inv);
|
||||
let norm_be = le_to_be_bytes(&norm_le);
|
||||
U512::from_big_endian(&norm_be)
|
||||
}
|
||||
|
||||
pub fn mul(&self, a_hat: &[u64; 8], b_hat: &[u64; 8]) -> [u64; 8] {
|
||||
(self.mul_fn)(a_hat, b_hat, &self.n, self.n0_inv)
|
||||
}
|
||||
|
||||
// Test-only helpers to enable forcing a specific backend and to access limb-level conversions.
|
||||
// These are useful for property tests and backend A/B validations.
|
||||
#[cfg(test)]
|
||||
pub fn from_ctx_with_backend_tag(ctx: &JobContext, tag: &str) -> Self {
|
||||
let n = u512_to_le(ctx.n);
|
||||
let n0_inv = mont_n0_inv(n[0]);
|
||||
let r2_u512 = compat::mod_pow(&U512::from(2u32), &U512::from(1024u32), &ctx.n);
|
||||
let r2 = u512_to_le(r2_u512);
|
||||
let m = u512_to_le(ctx.m);
|
||||
|
||||
// Choose mul_fn by tag; fall back to portable when not applicable or CPU features missing.
|
||||
let (mul_fn, _backend): (MulFn, &'static str) = match tag {
|
||||
"x86_64-bmi2-adx" | "bmi2-adx" => {
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
{
|
||||
if std::is_x86_feature_detected!("bmi2")
|
||||
&& std::is_x86_feature_detected!("adx")
|
||||
{
|
||||
(mont_mul_bmi2_adx, "x86_64-bmi2-adx")
|
||||
} else if std::is_x86_feature_detected!("bmi2") {
|
||||
(mont_mul_bmi2, "x86_64-bmi2")
|
||||
} else {
|
||||
(mont_mul_portable, "portable")
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_arch = "x86_64"))]
|
||||
{
|
||||
(mont_mul_portable, "portable")
|
||||
}
|
||||
}
|
||||
"x86_64-bmi2" | "bmi2" => {
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
{
|
||||
if std::is_x86_feature_detected!("bmi2") {
|
||||
(mont_mul_bmi2, "x86_64-bmi2")
|
||||
} else {
|
||||
(mont_mul_portable, "portable")
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_arch = "x86_64"))]
|
||||
{
|
||||
(mont_mul_portable, "portable")
|
||||
}
|
||||
}
|
||||
"aarch64-umulh" | "umulh" => {
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
{
|
||||
(mont_mul_aarch64, "aarch64-umulh")
|
||||
}
|
||||
#[cfg(not(target_arch = "aarch64"))]
|
||||
{
|
||||
(mont_mul_portable, "portable")
|
||||
}
|
||||
}
|
||||
_ => (mont_mul_portable, "portable"),
|
||||
};
|
||||
|
||||
let m_hat = mul_fn(&m, &r2, &n, n0_inv);
|
||||
MontCtx {
|
||||
n,
|
||||
n0_inv,
|
||||
r2,
|
||||
m_hat,
|
||||
mul_fn,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn to_mont_le_limbs(&self, x: &U512) -> [u64; 8] {
|
||||
(self.mul_fn)(&u512_to_le(*x), &self.r2, &self.n, self.n0_inv)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::wrong_self_convention)]
|
||||
pub fn from_mont_le_limbs(&self, x_hat: &[u64; 8]) -> [u64; 8] {
|
||||
let one = {
|
||||
let mut o = [0u64; 8];
|
||||
o[0] = 1;
|
||||
o
|
||||
};
|
||||
(self.mul_fn)(x_hat, &one, &self.n, self.n0_inv)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn u512_to_le(x: U512) -> [u64; 8] {
|
||||
let be = x.to_big_endian();
|
||||
let mut limbs = [0u64; 8];
|
||||
// Split BE into 8 chunks, then reverse to get LE limb order (least-significant first).
|
||||
for i in 0..8 {
|
||||
let mut bytes = [0u8; 8];
|
||||
bytes.copy_from_slice(&be[i * 8..(i + 1) * 8]);
|
||||
limbs[i] = u64::from_be_bytes(bytes);
|
||||
}
|
||||
limbs.reverse();
|
||||
limbs
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn le_to_be_bytes(limbs: &[u64; 8]) -> [u8; 64] {
|
||||
let mut out = [0u8; 64];
|
||||
for i in 0..8 {
|
||||
let chunk = limbs[7 - i].to_be_bytes();
|
||||
out[i * 8..(i + 1) * 8].copy_from_slice(&chunk);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
// Compute n0_inv = -n[0]^{-1} mod 2^64 using Newton–Raphson (n[0] must be odd).
|
||||
#[inline]
|
||||
fn mont_n0_inv(n0: u64) -> u64 {
|
||||
// Compute inverse of n0 modulo 2^64
|
||||
let mut x = 1u64;
|
||||
// 6 iterations suffice for 64-bit modulus
|
||||
for _ in 0..6 {
|
||||
let t = x.wrapping_mul(n0);
|
||||
x = x.wrapping_mul(2u64.wrapping_sub(t));
|
||||
}
|
||||
x.wrapping_neg()
|
||||
}
|
||||
|
||||
// Portable CIOS Montgomery multiplication: returns (a * b * R^{-1}) mod n
|
||||
#[inline]
|
||||
fn mont_mul_portable(a: &[u64; 8], b: &[u64; 8], n: &[u64; 8], n0_inv: u64) -> [u64; 8] {
|
||||
const MASK: u128 = 0xFFFF_FFFF_FFFF_FFFFu128;
|
||||
let mut acc = [0u128; 9];
|
||||
|
||||
for &ai_u64 in a.iter().take(8) {
|
||||
// acc += ai * b
|
||||
let ai = ai_u64 as u128;
|
||||
let mut carry = 0u128;
|
||||
for j in 0..8 {
|
||||
let sum = acc[j] + ai * (b[j] as u128) + carry;
|
||||
acc[j] = sum & MASK;
|
||||
carry = sum >> 64;
|
||||
}
|
||||
acc[8] += carry;
|
||||
|
||||
// m = (acc[0] * n0_inv) mod 2^64
|
||||
let m = ((acc[0] as u64).wrapping_mul(n0_inv)) as u128;
|
||||
|
||||
// acc += m * n
|
||||
let mut carry2 = 0u128;
|
||||
for j in 0..8 {
|
||||
let sum = acc[j] + m * (n[j] as u128) + carry2;
|
||||
acc[j] = sum & MASK;
|
||||
carry2 = sum >> 64;
|
||||
}
|
||||
acc[8] += carry2;
|
||||
|
||||
// shift acc right by one limb
|
||||
for j in 0..8 {
|
||||
acc[j] = acc[j + 1];
|
||||
}
|
||||
acc[8] = 0;
|
||||
}
|
||||
|
||||
// Convert acc (little-endian limbs) to u64 array
|
||||
let mut res = [0u64; 8];
|
||||
for j in 0..8 {
|
||||
res[j] = acc[j] as u64;
|
||||
}
|
||||
|
||||
// Conditional subtract modulus if res >= n
|
||||
if ge_le(&res, n) {
|
||||
sub_le_in_place(&mut res, n);
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn ge_le(a: &[u64; 8], b: &[u64; 8]) -> bool {
|
||||
for i in (0..8).rev() {
|
||||
if a[i] != b[i] {
|
||||
return a[i] > b[i];
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn sub_le_in_place(a: &mut [u64; 8], b: &[u64; 8]) {
|
||||
let mut borrow: u128 = 0;
|
||||
for i in 0..8 {
|
||||
let ai = a[i] as u128;
|
||||
let bi = b[i] as u128;
|
||||
let tmp = (1u128 << 64) + ai - bi - borrow;
|
||||
a[i] = (tmp & 0xFFFF_FFFF_FFFF_FFFFu128) as u64;
|
||||
borrow = if tmp >> 64 == 0 { 1 } else { 0 };
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn select_backend() -> (MulFn, &'static str) {
|
||||
// Optional override via env:
|
||||
// MINER_MONT_BACKEND=portable|bmi2|bmi2-adx
|
||||
if let Ok(val) = std::env::var("MINER_MONT_BACKEND") {
|
||||
let forced = val.to_ascii_lowercase();
|
||||
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
{
|
||||
let bmi2 = std::is_x86_feature_detected!("bmi2");
|
||||
let adx = std::is_x86_feature_detected!("adx");
|
||||
match forced.as_str() {
|
||||
"portable" => {
|
||||
log::warn!(target: "miner", "cpu-montgomery backend override: forced portable");
|
||||
return (mont_mul_portable, "forced-portable");
|
||||
}
|
||||
"bmi2-adx" | "adx" => {
|
||||
if bmi2 && adx {
|
||||
log::warn!(target: "miner", "cpu-montgomery backend override: forced x86_64-bmi2-adx");
|
||||
return (mont_mul_bmi2_adx, "forced-x86_64-bmi2-adx");
|
||||
} else if bmi2 {
|
||||
log::warn!(target: "miner", "cpu-montgomery backend override requested bmi2-adx but ADX unavailable; falling back to x86_64-bmi2");
|
||||
return (mont_mul_bmi2, "x86_64-bmi2");
|
||||
} else {
|
||||
log::warn!(target: "miner", "cpu-montgomery backend override requested bmi2-adx but BMI2/ADX unavailable; falling back to x86_64-generic");
|
||||
return (mont_mul_portable, "x86_64-generic");
|
||||
}
|
||||
}
|
||||
"bmi2" => {
|
||||
if bmi2 {
|
||||
log::warn!(target: "miner", "cpu-montgomery backend override: forced x86_64-bmi2");
|
||||
return (mont_mul_bmi2, "forced-x86_64-bmi2");
|
||||
} else {
|
||||
log::warn!(target: "miner", "cpu-montgomery backend override requested bmi2 but BMI2 unavailable; falling back to x86_64-generic");
|
||||
return (mont_mul_portable, "x86_64-generic");
|
||||
}
|
||||
}
|
||||
other => {
|
||||
log::warn!(target: "miner", "cpu-montgomery backend override '{other}' is not recognized on x86_64; using auto-detect");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
{
|
||||
match forced.as_str() {
|
||||
"portable" => {
|
||||
log::warn!(target: "miner", "cpu-montgomery backend override: forced portable");
|
||||
return (mont_mul_portable, "forced-portable");
|
||||
}
|
||||
// x86-only hints on aarch64 -> warn and ignore
|
||||
"bmi2" | "bmi2-adx" | "adx" => {
|
||||
log::warn!(target: "miner", "cpu-montgomery backend override '{}' not supported on aarch64; using auto-detect", forced);
|
||||
}
|
||||
other => {
|
||||
log::warn!(target: "miner", "cpu-montgomery backend override '{}' is not recognized on aarch64; using auto-detect", other);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
|
||||
{
|
||||
match forced.as_str() {
|
||||
"portable" => {
|
||||
log::warn!(target: "miner", "cpu-montgomery backend override: forced portable");
|
||||
return (mont_mul_portable, "forced-portable");
|
||||
}
|
||||
other => {
|
||||
log::warn!(target: "miner", "cpu-montgomery backend override '{}' not supported on this arch; using auto-detect", other);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-detection (default) per architecture
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
{
|
||||
let bmi2 = std::is_x86_feature_detected!("bmi2");
|
||||
|
||||
if bmi2 {
|
||||
(mont_mul_bmi2, "x86_64-bmi2")
|
||||
} else {
|
||||
(mont_mul_portable, "x86_64-generic")
|
||||
}
|
||||
}
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
{
|
||||
(mont_mul_aarch64, "aarch64-umulh")
|
||||
}
|
||||
#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
|
||||
{
|
||||
(mont_mul_portable, "portable")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
#[inline]
|
||||
#[allow(unsafe_code)]
|
||||
fn mont_mul_bmi2(a: &[u64; 8], b: &[u64; 8], n: &[u64; 8], n0_inv: u64) -> [u64; 8] {
|
||||
// BMI2-optimized CIOS using MULX. Single carry chain with u128 accumulation.
|
||||
// Limbs: little-endian (limb 0 = least significant).
|
||||
use core::arch::x86_64::_mulx_u64;
|
||||
const MASK: u128 = 0xFFFF_FFFF_FFFF_FFFFu128;
|
||||
|
||||
// 9-limb accumulator in u128 to simplify carries
|
||||
let mut acc = [0u128; 9];
|
||||
|
||||
for &ai in a.iter().take(8) {
|
||||
// acc += ai * b
|
||||
let mut carry: u128 = 0;
|
||||
for j in 0..8 {
|
||||
let mut hi: u64 = 0;
|
||||
// lo = (ai * b[j])_lo, hi = (ai * b[j])_hi
|
||||
let lo = unsafe { _mulx_u64(ai, b[j], &mut hi) };
|
||||
let sum = acc[j] + (lo as u128) + carry;
|
||||
acc[j] = sum & MASK;
|
||||
carry = (sum >> 64) + (hi as u128);
|
||||
}
|
||||
acc[8] += carry;
|
||||
|
||||
// m = (acc[0] * n0_inv) mod 2^64
|
||||
let m = (acc[0] as u64).wrapping_mul(n0_inv);
|
||||
|
||||
// acc += m * n
|
||||
let mut carry2: u128 = 0;
|
||||
for j in 0..8 {
|
||||
let mut hi2: u64 = 0;
|
||||
let lo2 = unsafe { _mulx_u64(m, n[j], &mut hi2) };
|
||||
let sum2 = acc[j] + (lo2 as u128) + carry2;
|
||||
acc[j] = sum2 & MASK;
|
||||
carry2 = (sum2 >> 64) + (hi2 as u128);
|
||||
}
|
||||
acc[8] += carry2;
|
||||
|
||||
// shift acc right by one limb (drop acc[0])
|
||||
for j in 0..8 {
|
||||
acc[j] = acc[j + 1];
|
||||
}
|
||||
acc[8] = 0;
|
||||
}
|
||||
|
||||
// Convert acc to u64 limbs (little-endian)
|
||||
let mut res = [0u64; 8];
|
||||
for j in 0..8 {
|
||||
res[j] = acc[j] as u64;
|
||||
}
|
||||
|
||||
// Conditional subtraction: if res >= n then res -= n
|
||||
if ge_le(&res, n) {
|
||||
sub_le_in_place(&mut res, n);
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
#[inline]
|
||||
#[allow(unsafe_code)]
|
||||
#[allow(unused_variables, unused_mut)]
|
||||
fn mont_mul_bmi2_adx(a: &[u64; 8], b: &[u64; 8], n: &[u64; 8], n0_inv: u64) -> [u64; 8] {
|
||||
// BMI2+ADX-optimized CIOS using MULX + dual carry chains (ADCX/ADOX).
|
||||
// We retain the same CIOS structure as the portable path:
|
||||
// - acc is a 9-limb accumulator in 64-bit limbs (little-endian)
|
||||
// - For each i:
|
||||
// acc += a[i] * b
|
||||
// m = (acc[0] * n0_inv) mod 2^64
|
||||
// acc += m * n
|
||||
// acc >>= 64 (drop acc[0])
|
||||
//
|
||||
// The inner adds use two independent carry chains:
|
||||
// - ADCX chain accumulates low halves into acc[j] (carry via CF)
|
||||
// - ADOX chain accumulates high halves into a separate running carry (OF)
|
||||
//
|
||||
// Note: We keep the logic in Rust around the asm! blocks for readability; the hot add paths
|
||||
// are emitted via inline assembly for ADX utilization.
|
||||
use core::arch::x86_64::_mulx_u64;
|
||||
use std::arch::asm;
|
||||
|
||||
let mut acc: [u64; 9] = [0; 9];
|
||||
|
||||
// helper: add (lo, hi) into acc[j] with dual carry chains (CF/OF)
|
||||
#[inline(always)]
|
||||
unsafe fn adx_accumulate(acc_j: &mut u64, lo: u64, hi: u64, of_carry: &mut u64) {
|
||||
// Clear CF and OF, then do:
|
||||
// acc_j += lo using ADCX (CF chain)
|
||||
// of_carry += hi using ADOX (OF chain)
|
||||
asm!(
|
||||
// Clear CF and OF carry chains
|
||||
"xor r8d, r8d",
|
||||
"adcx r8, r8",
|
||||
"adox r8, r8",
|
||||
// acc_j = acc_j + lo + CF
|
||||
"adcx {acc_j}, {lo}",
|
||||
// of_carry = of_carry + hi + OF
|
||||
"adox {ofc}, {hi}",
|
||||
acc_j = inout(reg) *acc_j,
|
||||
ofc = inout(reg) *of_carry,
|
||||
lo = in(reg) lo,
|
||||
hi = in(reg) hi,
|
||||
out("r8") _,
|
||||
options(nomem, nostack)
|
||||
);
|
||||
}
|
||||
|
||||
// helper: fold OF carry into next limb (acc[k] += of_carry)
|
||||
#[inline(always)]
|
||||
unsafe fn adx_fold_of(acc_k: &mut u64, of_carry: &mut u64) {
|
||||
// Fold the overflow-chain carry into acc_k using ADCX with cleared CF.
|
||||
asm!(
|
||||
"xor r8d, r8d",
|
||||
"adcx r8, r8",
|
||||
"adcx {acc_k}, {ofc}",
|
||||
acc_k = inout(reg) *acc_k,
|
||||
ofc = inout(reg) *of_carry,
|
||||
out("r8") _,
|
||||
options(nomem, nostack)
|
||||
);
|
||||
}
|
||||
|
||||
for &ai in a.iter().take(8) {
|
||||
// acc += ai * b
|
||||
let mut of_carry: u64 = 0;
|
||||
|
||||
// iterate j=0..7: accumulate ai*b[j] into acc[j].. with dual chains
|
||||
for j in 0..8 {
|
||||
let mut hi: u64 = 0;
|
||||
let lo: u64 = unsafe { _mulx_u64(ai, b[j], &mut hi) };
|
||||
|
||||
// acc[j] += lo (CF chain), of_carry += hi (OF chain)
|
||||
unsafe { adx_accumulate(&mut acc[j], lo, hi, &mut of_carry) };
|
||||
}
|
||||
// Propagate remaining OF carry into acc[8]
|
||||
unsafe { adx_fold_of(&mut acc[8], &mut of_carry) };
|
||||
|
||||
// m = (acc[0] * n0_inv) mod 2^64
|
||||
let m: u64 = (acc[0]).wrapping_mul(n0_inv);
|
||||
|
||||
// acc += m * n
|
||||
of_carry = 0;
|
||||
for j in 0..8 {
|
||||
let mut hi2: u64 = 0;
|
||||
let lo2: u64 = unsafe { _mulx_u64(m, n[j], &mut hi2) };
|
||||
|
||||
// acc[j] += lo2 (CF chain), of_carry += hi2 (OF chain)
|
||||
unsafe { adx_accumulate(&mut acc[j], lo2, hi2, &mut of_carry) };
|
||||
}
|
||||
// Propagate remaining OF carry into acc[8]
|
||||
unsafe { adx_fold_of(&mut acc[8], &mut of_carry) };
|
||||
|
||||
// shift acc right by one limb (drop acc[0])
|
||||
for j in 0..8 {
|
||||
acc[j] = acc[j + 1];
|
||||
}
|
||||
acc[8] = 0;
|
||||
}
|
||||
|
||||
// Conditional subtraction: if acc >= n then acc -= n
|
||||
let mut res = [0u64; 8];
|
||||
res.copy_from_slice(&acc[0..8]);
|
||||
|
||||
if ge_le(&res, n) {
|
||||
sub_le_in_place(&mut res, n);
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
#[inline]
|
||||
#[allow(unsafe_code)]
|
||||
fn mont_mul_aarch64(a: &[u64; 8], b: &[u64; 8], n: &[u64; 8], n0_inv: u64) -> [u64; 8] {
|
||||
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:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -39,6 +39,7 @@ primitive-types = { workspace = true }
|
||||
num-bigint = { workspace = true }
|
||||
num-traits = { workspace = true }
|
||||
qp-poseidon-core = { workspace = true }
|
||||
qpow-math = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
|
||||
|
||||
@@ -1,423 +1,192 @@
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
// Local QPoW math core with a compatibility API mirroring the original `qpow-math` crate,
|
||||
// plus new scaffolding for an optimized path (precompute + incremental evaluation).
|
||||
//
|
||||
// This crate intentionally provides:
|
||||
// - `compat` API: Drop-in functions like `is_valid_nonce` and `get_nonce_distance`
|
||||
// - `JobContext`: Precomputed constants (m, n, target, threshold) for a given header
|
||||
// - Incremental helpers: `init_worker_y0`, `step_mul`, and `distance_from_y`
|
||||
// to enable replacing per-nonce exponentiation with a single modular multiplication.
|
||||
//
|
||||
// Notes:
|
||||
// - Current implementation mirrors the reference algorithm using BigUint-based modular arithmetic.
|
||||
// - Future work will gate accelerated paths (e.g., Montgomery, SIMD Poseidon2) behind features.
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
use core::ops::BitXor;
|
||||
use primitive_types::U512;
|
||||
use qp_poseidon_core::Poseidon2Core;
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
use log::{debug, error};
|
||||
pub use qpow_math::{get_nonce_hash, is_valid_nonce, mine_range};
|
||||
|
||||
pub mod compat {
|
||||
//! Compatibility layer that mirrors the original `qpow-math` crate API.
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Check QPoW validity for a given `header` and `nonce` against `threshold`.
|
||||
///
|
||||
/// Returns a boolean indicating validity. If you also need the computed distance,
|
||||
/// use `is_valid_nonce_with_distance`.
|
||||
pub fn is_valid_nonce(header: [u8; 32], nonce: [u8; 64], threshold: U512) -> bool {
|
||||
let (ok, _) = is_valid_nonce_with_distance(header, nonce, threshold);
|
||||
ok
|
||||
}
|
||||
|
||||
/// Same as `is_valid_nonce`, but also returns the computed distance (U512).
|
||||
pub fn is_valid_nonce_with_distance(
|
||||
header: [u8; 32],
|
||||
nonce: [u8; 64],
|
||||
threshold: U512,
|
||||
) -> (bool, U512) {
|
||||
if nonce == [0u8; 64] {
|
||||
#[cfg(feature = "std")]
|
||||
error!(
|
||||
"is_valid_nonce should not be called with 0 nonce, but was for header: {header:?}"
|
||||
);
|
||||
return (false, U512::zero());
|
||||
}
|
||||
|
||||
let distance_achieved = get_nonce_distance(header, nonce);
|
||||
#[cfg(feature = "std")]
|
||||
debug!(target: "pow-core", "distance = {distance_achieved}..., threshold = {threshold}...");
|
||||
|
||||
(distance_achieved <= threshold, distance_achieved)
|
||||
}
|
||||
|
||||
/// Compute the QPoW distance for (header, nonce).
|
||||
///
|
||||
/// distance = target XOR H(m^(h + nonce) mod n)
|
||||
/// where (m, n) are derived deterministically from the header, and H is Poseidon2-512.
|
||||
pub fn get_nonce_distance(header: [u8; 32], nonce: [u8; 64]) -> U512 {
|
||||
super::get_nonce_distance_impl(header, nonce)
|
||||
}
|
||||
|
||||
/// Generate a pair (m, n) deterministically from the header.
|
||||
pub fn get_random_rsa(header: &[u8; 32]) -> (U512, U512) {
|
||||
super::get_random_rsa_impl(header)
|
||||
}
|
||||
|
||||
/// Check if two numbers are coprime using the Euclidean algorithm.
|
||||
pub fn is_coprime(a: &U512, b: &U512) -> bool {
|
||||
super::is_coprime_impl(a, b)
|
||||
}
|
||||
|
||||
/// Miller–Rabin primality test used by `get_random_rsa`.
|
||||
pub fn is_prime(n: &U512) -> bool {
|
||||
super::is_prime_impl(n)
|
||||
}
|
||||
|
||||
/// Apply the reference "hash-to-group" function then Poseidon2-512.
|
||||
pub fn hash_to_group_bigint_sha(h: &U512, m: &U512, n: &U512, solution: &U512) -> U512 {
|
||||
super::hash_to_group_bigint_sha_impl(h, m, n, solution)
|
||||
}
|
||||
|
||||
/// Reference hash-to-group function: computes m^(h + solution) mod n.
|
||||
pub fn hash_to_group_bigint(h: &U512, m: &U512, n: &U512, solution: &U512) -> U512 {
|
||||
super::hash_to_group_bigint_impl(h, m, n, solution)
|
||||
}
|
||||
|
||||
/// Reference modular exponentiation via BigUint.
|
||||
pub fn mod_pow(base: &U512, exponent: &U512, modulus: &U512) -> U512 {
|
||||
super::mod_pow_impl(base, exponent, modulus)
|
||||
}
|
||||
|
||||
/// Poseidon2-512 over the big-endian bytes of input U512.
|
||||
pub fn poseidon2_512(input: U512) -> U512 {
|
||||
super::poseidon2_512_impl(input)
|
||||
}
|
||||
}
|
||||
|
||||
/// Precomputed context for a single mining job (header + threshold).
|
||||
///
|
||||
/// This enables an optimized path:
|
||||
/// - Precompute (m, n) and target = H(m^(h + 0) mod n) once per job.
|
||||
/// - For each worker, compute `y0 = m^(h + start_nonce) mod n` once.
|
||||
/// - For each subsequent nonce, update y = (y * m) mod n (O(1) per step).
|
||||
/// - Distance at a step is `target XOR H(y)`.
|
||||
#[derive(Clone, Debug)]
|
||||
/// Job context for Bitcoin-style PoW mining with double Poseidon2 hashing
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct JobContext {
|
||||
pub header: [u8; 32],
|
||||
pub header_int: U512,
|
||||
pub threshold: U512,
|
||||
|
||||
pub m: U512,
|
||||
pub n: U512,
|
||||
pub difficulty: U512,
|
||||
pub target: U512,
|
||||
}
|
||||
|
||||
impl JobContext {
|
||||
/// Build a new context by deriving (m, n) and target from the header.
|
||||
pub fn new(header: [u8; 32], threshold: U512) -> Self {
|
||||
let header_int = U512::from_big_endian(&header);
|
||||
let (m, n) = get_random_rsa_impl(&header);
|
||||
let target = hash_to_group_bigint_sha_impl(&header_int, &m, &n, &U512::zero());
|
||||
/// Build a new context from header and difficulty
|
||||
pub fn new(header: [u8; 32], difficulty: U512) -> Self {
|
||||
// In Bitcoin-style PoW, target = max_target / difficulty
|
||||
let max_target = U512::MAX;
|
||||
let target = max_target / difficulty;
|
||||
|
||||
JobContext {
|
||||
header,
|
||||
header_int,
|
||||
threshold,
|
||||
m,
|
||||
n,
|
||||
difficulty,
|
||||
target,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute y0 = m^(h + start_nonce) mod n for a worker's starting nonce.
|
||||
///
|
||||
/// This is the one-time exponentiation cost per worker/thread. Subsequent steps can
|
||||
/// use `step_mul` to advance y with a single modular multiplication.
|
||||
pub fn init_worker_y0(ctx: &JobContext, start_nonce: U512) -> U512 {
|
||||
let sum = ctx.header_int.saturating_add(start_nonce);
|
||||
mod_pow_impl(&ctx.m, &sum, &ctx.n)
|
||||
/// Initialize a worker with starting nonce (no special initialization needed for Bitcoin-style)
|
||||
pub fn init_worker_nonce(start_nonce: U512) -> U512 {
|
||||
start_nonce
|
||||
}
|
||||
|
||||
/// Advance y by one nonce: y <- y * m (mod n).
|
||||
pub fn step_mul(ctx: &JobContext, y: U512) -> U512 {
|
||||
mod_mul_impl(&y, &ctx.m, &ctx.n)
|
||||
/// Advance nonce by one (simple increment for Bitcoin-style)
|
||||
pub fn step_nonce(nonce: U512) -> U512 {
|
||||
nonce.saturating_add(U512::from(1u64))
|
||||
}
|
||||
|
||||
/// Compute distance for the current y:
|
||||
/// distance = target XOR Poseidon2_512(y)
|
||||
pub fn distance_from_y(ctx: &JobContext, y: U512) -> U512 {
|
||||
let hashed = poseidon2_512_impl(y);
|
||||
ctx.target.bitxor(hashed)
|
||||
/// Compute hash for the current nonce using Bitcoin-style double Poseidon2
|
||||
pub fn hash_from_nonce(ctx: &JobContext, nonce: U512) -> U512 {
|
||||
let nonce_bytes = nonce.to_big_endian();
|
||||
qpow_math::get_nonce_hash(ctx.header, nonce_bytes)
|
||||
}
|
||||
|
||||
/// Convenience: compute distance for an arbitrary nonce using the context.
|
||||
pub fn distance_for_nonce(ctx: &JobContext, nonce: U512) -> U512 {
|
||||
let y = mod_pow_impl(&ctx.m, &ctx.header_int.saturating_add(nonce), &ctx.n);
|
||||
distance_from_y(ctx, y)
|
||||
/// Check if hash meets difficulty target
|
||||
pub fn is_valid_hash(ctx: &JobContext, hash: U512) -> bool {
|
||||
hash < ctx.target
|
||||
}
|
||||
|
||||
/// Convenience: check if a distance is valid under the context's threshold.
|
||||
pub fn is_valid_distance(ctx: &JobContext, distance: U512) -> bool {
|
||||
distance <= ctx.threshold
|
||||
}
|
||||
/// Mine a range of nonces starting from start_nonce
|
||||
pub fn mine_nonce_range(ctx: &JobContext, start_nonce: U512, steps: u64) -> Option<(U512, U512)> {
|
||||
let start_nonce_bytes = start_nonce.to_big_endian();
|
||||
|
||||
/// Reference distance computation used by the compat layer.
|
||||
fn get_nonce_distance_impl(header: [u8; 32], nonce: [u8; 64]) -> U512 {
|
||||
if nonce == [0u8; 64] {
|
||||
#[cfg(feature = "std")]
|
||||
debug!(target: "pow-core", "zero nonce");
|
||||
return U512::zero();
|
||||
if let Some((nonce_bytes, hash)) =
|
||||
mine_range(ctx.header, start_nonce_bytes, steps, ctx.difficulty)
|
||||
{
|
||||
let nonce = U512::from_big_endian(&nonce_bytes);
|
||||
Some((nonce, hash))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
||||
let (m, n) = get_random_rsa_impl(&header);
|
||||
let header_int = U512::from_big_endian(&header);
|
||||
let nonce_int = U512::from_big_endian(&nonce);
|
||||
|
||||
let target = hash_to_group_bigint_sha_impl(&header_int, &m, &n, &U512::zero());
|
||||
let nonce_element = hash_to_group_bigint_sha_impl(&header_int, &m, &n, &nonce_int);
|
||||
|
||||
let distance = target.bitxor(nonce_element);
|
||||
#[cfg(feature = "std")]
|
||||
debug!(target: "pow-core", "distance = {distance}");
|
||||
distance
|
||||
}
|
||||
|
||||
/// Generates a pair of RSA-style numbers (m, n) deterministically from input header.
|
||||
///
|
||||
/// - m: 256-bit derived via Poseidon2-256(header)
|
||||
/// - n: 512-bit derived via Poseidon2-512(header), iteratively rehashed until valid:
|
||||
/// (odd, composite, coprime with m, and n > m)
|
||||
fn get_random_rsa_impl(header: &[u8; 32]) -> (U512, U512) {
|
||||
let poseidon = Poseidon2Core::new();
|
||||
|
||||
// m from Poseidon2-256
|
||||
let m_bytes = poseidon.hash_no_pad_bytes(header);
|
||||
let m = U512::from_big_endian(&m_bytes);
|
||||
|
||||
// initial n from Poseidon2-512
|
||||
let mut n_bytes = poseidon.hash_512(&m_bytes);
|
||||
let mut n = U512::from_big_endian(&n_bytes);
|
||||
|
||||
// Keep hashing until n satisfies constraints
|
||||
while n % 2u32 == U512::zero() || n <= m || !is_coprime_impl(&m, &n) || is_prime_impl(&n) {
|
||||
n_bytes = poseidon.hash_512(&n_bytes);
|
||||
n = U512::from_big_endian(&n_bytes);
|
||||
}
|
||||
|
||||
(m, n)
|
||||
}
|
||||
|
||||
/// Check if two numbers are coprime using the Euclidean algorithm.
|
||||
fn is_coprime_impl(a: &U512, b: &U512) -> bool {
|
||||
let mut x = *a;
|
||||
let mut y = *b;
|
||||
|
||||
while y != U512::zero() {
|
||||
let tmp = y;
|
||||
y = x % y;
|
||||
x = tmp;
|
||||
}
|
||||
|
||||
x == U512::one()
|
||||
}
|
||||
|
||||
/// Hash-to-group then Poseidon2-512.
|
||||
///
|
||||
/// Note: The reference calls `hash_to_group_bigint` followed by an additional Poseidon2-512.
|
||||
fn hash_to_group_bigint_sha_impl(h: &U512, m: &U512, n: &U512, solution: &U512) -> U512 {
|
||||
let result = hash_to_group_bigint_impl(h, m, n, solution);
|
||||
poseidon2_512_impl(result)
|
||||
}
|
||||
|
||||
/// Reference hash-to-group big-integer function (no chunk splitting).
|
||||
/// Computes sum = h + solution; then y = m^sum mod n.
|
||||
fn hash_to_group_bigint_impl(h: &U512, m: &U512, n: &U512, solution: &U512) -> U512 {
|
||||
let sum = h.saturating_add(*solution);
|
||||
mod_pow_impl(m, &sum, n)
|
||||
}
|
||||
|
||||
/// Reference modular exponentiation using BigUint square-and-multiply.
|
||||
fn mod_pow_impl(base: &U512, exponent: &U512, modulus: &U512) -> U512 {
|
||||
if *modulus == U512::zero() {
|
||||
panic!("Modulus cannot be zero");
|
||||
}
|
||||
|
||||
use num_bigint::BigUint;
|
||||
use num_traits::{One, Zero};
|
||||
|
||||
// Convert inputs to BigUint
|
||||
let mut base = BigUint::from_bytes_be(&base.to_big_endian());
|
||||
let mut exp = BigUint::from_bytes_be(&exponent.to_big_endian());
|
||||
let modulus = BigUint::from_bytes_be(&modulus.to_big_endian());
|
||||
|
||||
// Initialize result as 1
|
||||
let mut result = BigUint::one();
|
||||
|
||||
// Square-and-multiply algorithm
|
||||
while !exp.is_zero() {
|
||||
if exp.bit(0) {
|
||||
result = (result * &base) % &modulus;
|
||||
}
|
||||
base = (&base * &base) % &modulus;
|
||||
exp >>= 1;
|
||||
}
|
||||
|
||||
U512::from_big_endian(&result.to_bytes_be())
|
||||
}
|
||||
|
||||
/// Reference modular multiplication using BigUint, i.e., (a * b) mod n.
|
||||
fn mod_mul_impl(a: &U512, b: &U512, modulus: &U512) -> U512 {
|
||||
use num_bigint::BigUint;
|
||||
|
||||
if *modulus == U512::zero() {
|
||||
panic!("Modulus cannot be zero");
|
||||
}
|
||||
|
||||
let a_bi = BigUint::from_bytes_be(&a.to_big_endian());
|
||||
let b_bi = BigUint::from_bytes_be(&b.to_big_endian());
|
||||
let n_bi = BigUint::from_bytes_be(&modulus.to_big_endian());
|
||||
|
||||
let prod = (a_bi * b_bi) % n_bi;
|
||||
U512::from_big_endian(&prod.to_bytes_be())
|
||||
}
|
||||
|
||||
/// Miller–Rabin primality test.
|
||||
///
|
||||
/// Deterministically selects k=32 bases hashed from `n` using Poseidon2-512 to
|
||||
/// bound false-positive probability to ~1/2^64 for composites.
|
||||
fn is_prime_impl(n: &U512) -> bool {
|
||||
if *n <= U512::one() {
|
||||
return false;
|
||||
}
|
||||
if *n == U512::from(2u32) || *n == U512::from(3u32) {
|
||||
return true;
|
||||
}
|
||||
if *n % U512::from(2u32) == U512::zero() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// write n-1 as d * 2^r
|
||||
let mut d = *n - U512::one();
|
||||
let mut r = 0u32;
|
||||
while d % U512::from(2u32) == U512::zero() {
|
||||
d /= U512::from(2u32);
|
||||
r += 1;
|
||||
}
|
||||
|
||||
// Generate test bases deterministically from n using Poseidon2
|
||||
let mut bases = [U512::zero(); 32];
|
||||
let mut base_count = 0;
|
||||
let poseidon = Poseidon2Core::new();
|
||||
let mut counter = U512::zero();
|
||||
|
||||
while base_count < 32 {
|
||||
// Hash n concatenated with counter
|
||||
let mut bytes = [0u8; 128];
|
||||
let n_bytes = n.to_big_endian();
|
||||
let counter_bytes = counter.to_big_endian();
|
||||
|
||||
bytes[..64].copy_from_slice(&n_bytes);
|
||||
bytes[64..128].copy_from_slice(&counter_bytes);
|
||||
|
||||
let poseidon_bytes = poseidon.hash_512(&bytes);
|
||||
|
||||
// Use the hash to generate a base in [2, n-2]
|
||||
let hash = U512::from_big_endian(&poseidon_bytes);
|
||||
let base = (hash % (*n - U512::from(4u32))) + U512::from(2u32);
|
||||
bases[base_count] = base;
|
||||
base_count += 1;
|
||||
|
||||
counter += U512::one();
|
||||
}
|
||||
|
||||
'witness: for base in bases {
|
||||
let mut x = mod_pow_impl(&base, &d, n);
|
||||
|
||||
if x == U512::one() || x == *n - U512::one() {
|
||||
continue 'witness;
|
||||
}
|
||||
|
||||
// Square r-1 times
|
||||
for _ in 0..r - 1 {
|
||||
x = mod_pow_impl(&x, &U512::from(2u32), n);
|
||||
if x == *n - U512::one() {
|
||||
continue 'witness;
|
||||
}
|
||||
if x == U512::one() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Poseidon2-512 over the big-endian bytes of input `U512`.
|
||||
fn poseidon2_512_impl(input: U512) -> U512 {
|
||||
let poseidon = Poseidon2Core::new();
|
||||
let bytes = input.to_big_endian();
|
||||
U512::from_big_endian(&poseidon.hash_512(&bytes))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// removed unused helper u512_from_hex
|
||||
|
||||
#[test]
|
||||
fn compat_distance_matches_context_distance() {
|
||||
// Synthetic header and nonce
|
||||
fn test_job_context_creation() {
|
||||
let header = [1u8; 32];
|
||||
let nonce = [2u8; 64];
|
||||
let difficulty = U512::from(1000u64);
|
||||
|
||||
let dist_compat = compat::get_nonce_distance(header, nonce);
|
||||
let ctx = JobContext::new(header, difficulty);
|
||||
|
||||
let ctx = JobContext::new(header, U512::from(123u64));
|
||||
let nonce_int = U512::from_big_endian(&nonce);
|
||||
let dist_ctx = distance_for_nonce(&ctx, nonce_int);
|
||||
|
||||
assert_eq!(dist_compat, dist_ctx);
|
||||
assert_eq!(ctx.header, header);
|
||||
assert_eq!(ctx.difficulty, difficulty);
|
||||
assert_eq!(ctx.target, U512::MAX / difficulty);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn incremental_step_matches_pow_plus_one() {
|
||||
fn test_nonce_stepping() {
|
||||
let start = U512::from(100u64);
|
||||
let next = step_nonce(start);
|
||||
|
||||
assert_eq!(next, U512::from(101u64));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hash_computation() {
|
||||
let header = [1u8; 32];
|
||||
let difficulty = U512::from(1u64);
|
||||
let ctx = JobContext::new(header, difficulty);
|
||||
|
||||
let nonce = U512::from(123u64);
|
||||
let hash1 = hash_from_nonce(&ctx, nonce);
|
||||
let hash2 = hash_from_nonce(&ctx, nonce);
|
||||
|
||||
// Same input should produce same hash
|
||||
assert_eq!(hash1, hash2);
|
||||
|
||||
// Hash should not be zero for non-zero nonce
|
||||
assert_ne!(hash1, U512::zero());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_nonces_different_hashes() {
|
||||
let header = [2u8; 32];
|
||||
let difficulty = U512::from(1u64);
|
||||
let ctx = JobContext::new(header, difficulty);
|
||||
|
||||
let nonce1 = U512::from(100u64);
|
||||
let nonce2 = U512::from(101u64);
|
||||
|
||||
let hash1 = hash_from_nonce(&ctx, nonce1);
|
||||
let hash2 = hash_from_nonce(&ctx, nonce2);
|
||||
|
||||
assert_ne!(hash1, hash2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validity_check() {
|
||||
let header = [3u8; 32];
|
||||
let threshold = U512::from(99999u64);
|
||||
let ctx = JobContext::new(header, threshold);
|
||||
let easy_difficulty = U512::from(1u64);
|
||||
let ctx = JobContext::new(header, easy_difficulty);
|
||||
|
||||
let start = U512::from(1000u64);
|
||||
let y0 = init_worker_y0(&ctx, start);
|
||||
let nonce = U512::from(1u64);
|
||||
let (is_valid, hash) = is_valid_nonce(ctx.header, nonce.to_big_endian(), ctx.difficulty);
|
||||
|
||||
// Distance for start+1 computed two ways should match:
|
||||
// 1) Incremental: step once from y0
|
||||
// 2) Direct exponentiation with nonce = start+1
|
||||
let y1_inc = step_mul(&ctx, y0);
|
||||
let dist_inc = distance_from_y(&ctx, y1_inc);
|
||||
// With very easy difficulty, should be valid
|
||||
assert!(is_valid);
|
||||
assert_ne!(hash, U512::zero());
|
||||
|
||||
let direct = mod_pow_impl(
|
||||
&ctx.m,
|
||||
&ctx.header_int.saturating_add(start + U512::one()),
|
||||
&ctx.n,
|
||||
);
|
||||
let dist_direct = distance_from_y(&ctx, direct);
|
||||
|
||||
assert_eq!(dist_inc, dist_direct);
|
||||
// Verify hash is actually below target
|
||||
assert!(hash < ctx.target);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_nonce_is_zero_distance() {
|
||||
let header = [0xABu8; 32];
|
||||
let nonce = [0u8; 64];
|
||||
let d = compat::get_nonce_distance(header, nonce);
|
||||
assert_eq!(d, U512::zero());
|
||||
fn test_target_calculation() {
|
||||
let header = [4u8; 32];
|
||||
let difficulty = U512::from(256u64);
|
||||
let ctx = JobContext::new(header, difficulty);
|
||||
|
||||
let expected_target = U512::MAX / U512::from(256u64);
|
||||
assert_eq!(ctx.target, expected_target);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hash_matches_qpow_math() {
|
||||
// Test that our JobContext produces the same results as qpow_math directly
|
||||
let header = [1u8; 32];
|
||||
let nonce = U512::from(123u64);
|
||||
let difficulty = U512::from(1000u64);
|
||||
|
||||
let ctx = JobContext::new(header, difficulty);
|
||||
let hash_ctx = hash_from_nonce(&ctx, nonce);
|
||||
|
||||
let nonce_bytes = nonce.to_big_endian();
|
||||
let hash_direct = qpow_math::get_nonce_hash(header, nonce_bytes);
|
||||
|
||||
assert_eq!(hash_ctx, hash_direct);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mine_range_functionality() {
|
||||
let header = [5u8; 32];
|
||||
let difficulty = U512::from(1u64); // Very easy
|
||||
let ctx = JobContext::new(header, difficulty);
|
||||
|
||||
let start_nonce = U512::from(1u64);
|
||||
let result = mine_nonce_range(&ctx, start_nonce, 10);
|
||||
|
||||
// With very easy difficulty, should find a solution quickly
|
||||
if let Some((found_nonce, found_hash)) = result {
|
||||
assert!(found_nonce >= start_nonce);
|
||||
assert!(found_nonce < start_nonce + U512::from(10u64));
|
||||
assert!(found_hash < ctx.target);
|
||||
}
|
||||
// If no solution found, that's also valid behavior
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hard_difficulty_no_solution() {
|
||||
let header = [6u8; 32];
|
||||
let very_hard_difficulty = U512::MAX; // Impossible difficulty
|
||||
let ctx = JobContext::new(header, very_hard_difficulty);
|
||||
|
||||
let start_nonce = U512::from(1u64);
|
||||
let result = mine_nonce_range(&ctx, start_nonce, 5);
|
||||
|
||||
// With impossible difficulty, should not find solution
|
||||
assert!(result.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
48
src/lib.rs
48
src/lib.rs
@@ -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]
|
||||
*/
|
||||
33
src/main.rs
33
src/main.rs
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user