Multi-zkVM, multi-GPU-vendor ZK proof generator for Hemi's ZK proving marketplace (HemiProve).
zkminer watches the marketplace for open proving jobs, estimates whether each one is worth taking, claims the ones that are, generates the proof on a local GPU, and submits it on-chain. Proving runs in separate worker processes — one per GPU — so a wedged or OOM-killed prover can't take the miner down with it.
Backends: RISC Zero (CUDA and ROCm), SP1, OpenVM.
| Rust | 1.93.0 (pinned in rust-toolchain.toml) |
| RISC Zero toolchain | rzup, see below — required to build, and to prove |
| CUDA | toolkit with nvcc (12.8–13.x tested) for NVIDIA workers |
| ROCm | 6.4+ with hipcc for AMD workers |
| Build deps | build-essential cmake pkg-config protobuf-compiler libclang-dev (+ libhipcub-dev librocthrust-dev for ROCm) |
| Foundry | cast — only for the example job submitters under examples/ |
Two rzup components are needed, and they fail at different stages:
curl -sSL https://risczero.com/install | bash
export PATH="$HOME/.risc0/bin:$PATH"
rzup install rust # builds the guest ELFs — without it, cargo build fails
rzup install risc0-groth16 # the Groth16 wrap — without it, every PROOF failsrisc0-groth16 is the nastier of the two: everything compiles and benchmarks fine
without it, and only real proofs fail — reported by the host as the unhelpful
Worker risc0 process died (EOF).
cargo build --release -p zkminer-cli # the CLI; no GPU toolchain neededGPU prover workers are separate binaries, built with the toolchain for their vendor:
# NVIDIA. NVCC/HIPCC must be set explicitly: sppark's build.rs resolves them with
# which::which(), not PATH. Set the other to "off" so it isn't auto-detected.
env NVCC=/usr/local/cuda/bin/nvcc HIPCC=off \
NVCC_APPEND_FLAGS="--expt-relaxed-constexpr -gencode arch=compute_120,code=sm_120" \
cargo build --release -p zkminer-prove-risc0 --features cuda \
--bin zkminer-prove-risc0-cuda --target-dir target/release-cuda
# AMD. Use a SEPARATE --target-dir from the CUDA build to avoid lock contention.
env HIPCC=/opt/rocm/bin/hipcc NVCC=off RISC0_HIP_ARCH="gfx1100" \
cargo build --release -p zkminer-prove-risc0 --features rocm \
--bin zkminer-prove-risc0-rocm --target-dir target/release-rocmNever set RISC0_SKIP_BUILD=1 — it produces empty guest ELF stubs, and every
benchmark then silently reports zero work.
make wraps portable Docker builds: make cli | cuda | rocm | sp1 | openvm | all | test.
The miner looks for worker binaries in ~/.zkminer/provers/ under exact names. The
directory is hand-managed: a missing binary means that backend is silently skipped, with
no error.
~/.zkminer/provers/
├── zkminer-prove-risc0-cuda
├── zkminer-prove-risc0-rocm
├── zkminer-prove-sp1
└── zkminer-prove-openvm
If a worker needs environment the miner's own process won't have (a systemd unit or cron
job does not source ~/.bashrc), install a small wrapper script under the expected
name that sets it up and then execs the real binary — exec preserves the PID and
process group the dispatcher relies on.
Copy or write ~/.zkminer/config.toml:
[chain]
rpc_url = "https://testnet.rpc.hemi.network/rpc"
chain_id = 743111 # Hemi testnet (43111 is mainnet)
[contracts]
hemi_prove = "0x..." # router (diamond proxy)
hemi_prove_staking = "0x..."
hemi_prove_registry = "0x..."
hemi_token = "0x..."
[wallet]
key_source = "env" # reads $ZKMINER_PRIVATE_KEY
key_env_var = "ZKMINER_PRIVATE_KEY"
[prover]
max_concurrent_proofs = 0 # 0 = one proof per detected GPU
skip_benchmark_gate = truezkminer init will scaffold a config, but verify the contract addresses it writes
against the deployment you intend to use before running against a live market — a
wrong address set produces a miner that starts cleanly, sees no jobs, and has no stake.
export ZKMINER_PRIVATE_KEY=0x...
zkminer status # balance, stake, collateral, job stats
zkminer benchmark # measure real per-GPU proving throughput
zkminer benchmark --calibrate # also measure segment size (po2); saves benchmarks.json
zkminer run # interactive TUI
zkminer -v run --headless # headless; -v shows per-job claim/skip reasoningVerbosity is -v/-vv before the subcommand (RUST_LOG is ignored). Logs go to
stderr, so zkminer benchmark --json on stdout stays machine-readable.
Two things worth knowing:
- Don't run
benchmarkwhile the miner is running. Both spawn a worker per GPU and the GPU lock is per-process, so they will double-book VRAM. - If the miner sits idle with jobs available, suspect collateral. Available
collateral is
staked − locked, and a claimed job that passes its deadline locks collateral permanently. This surfaces as a throttled warning at default verbosity.
benchmark --calibrate measures proving time at each segment size and only overrides the
SDK's own choice when one size wins by a decisive margin — differences inside measurement
noise are discarded rather than acted on.
| crate | role |
|---|---|
zkminer-cli |
binary entry point: status, benchmark, run, init |
zkminer-chain |
chain client, auction math, staking, monitor, tx/nonce layer, RPC metering |
zkminer-contracts |
sol!() bindings — the source of truth for the on-chain ABI |
zkminer-prover |
worker dispatcher, ELF registry, benchmarks, guest programs |
zkminer-prover-protocol |
worker↔dispatcher IPC types |
zkminer-prove-risc0 / -sp1 / -openvm |
standalone GPU worker binaries |
zkminer-strategy |
profitability evaluation, cost model, timing prediction |
zkminer-tui |
ratatui interface |
zkminer-config |
TOML config and wallet key loading |
RISC Zero and SP1 are patched to the hemilabs forks (GPU + AMD support) via
[patch.crates-io], pinned by Cargo.lock. Don't run cargo update — those two are
referenced by moving branch heads, so the lockfile is what holds the build on validated
commits. (sppark is pinned by immutable rev and is not exposed to that drift.)
cargo test # unit + integration
ZKVM_ALLOW_SKIP=1 cargo test # skip tests needing GPUs/guest ELFs
# comprehensive zkVM suite — dispatches to the installed GPU workers
cargo test -p zkminer-prover --features risc0,sp1 --test comprehensive_zkvm_test \
-- --test-threads=1 --nocaptureThe zkVM suite is fail-closed: a missing worker or guest ELF fails the test rather
than skipping, so a half-installed box can't look green. Set ZKVM_ALLOW_SKIP=1 to skip
instead. Run it single-threaded to avoid GPU contention, and log to a file rather than
piping to tail, which buffers and can hide a hang.
examples/ holds job submitters for testing against a live market. They read their
signing key from the environment (SUBMITTER_KEY, REQUESTER_KEY) and refuse to run
without it — never commit a funded key. The forge-based submitters additionally need
CONTRACTS_DIR pointing at a HemiProve Foundry checkout; testnet_submitter_cast.sh
needs only cast.
MIT — see LICENSE.