Convert repository into Cargo workspace with modular crates - Split code into workspace members: miner-cli, miner-service, pow-core, engine-cpu, engine-gpu-cuda, engine-gpu-opencl, metrics - Moved QPoW math logic into local pow-core crate with compatibility API and incremental helpers - Defined MinerEngine trait and baseline CPU engine in engine-cpu crate - Rewrote service layer in miner-service: job management, HTTP API, engine orchestration - Added CLI binary crate miner-cli with flags: --port, --num-cores, --metrics-port, --engine - Implemented runtime toggle for Prometheus metrics exporter enabled only when --metrics-port provided - Deprecated root src/lib.rs and src/main.rs with notices pointing to new CLI/workspace entrypoints
11 KiB
CPU Mining Algorithm and Job Lifecycle
This document explains how the external miner performs CPU mining for the Resonance (Quantus) network, how jobs are created and tracked, and how results are produced and reported over the HTTP API.
The implementation details referenced below correspond to the crate’s runtime behavior as implemented in:
src/lib.rs(algorithm, job management, HTTP handlers)src/main.rs(CLI/configuration, server bootstrap)api/openapi.yamlandEXTERNAL_MINER_PROTOCOL.md(public API and protocol context)
Overview
At a high level, the miner:
- Receives a mining job from a node via
POST /mine. - Validates the request and initializes a
MiningJob. - Partitions the requested nonce search range across N CPU threads.
- Each thread linearly scans its assigned nonce range, evaluating each candidate via
qpow_math::is_valid_nonce. - The first valid nonce (or best result by distance) triggers an early stop via a shared cancel flag.
- The miner aggregates per-thread progress and reports status and results via
GET /result/{job_id}. - A job can be aborted at any time by
POST /cancel/{job_id}. - Finished or stale jobs are cleaned up after a short retention period.
The implementation uses:
U512for the 512-bit nonce space.crossbeam_channelfor intra-job result/progress reporting.std::threadfor CPU-bound work distribution across cores.tokiofor asynchronous HTTP handling and the mining state loop.qpow_math::{is_valid_nonce, get_nonce_distance}for QPoW validity and scoring.
Core Data Types
-
MiningState- Holds all active jobs in
jobs: Arc<Mutex<HashMap<String, MiningJob>>>. - Tracks
num_coresused for CPU mining. - Periodically polls and advances job state in a background loop (
start_mining_loop).
- Holds all active jobs in
-
MiningJob- Inputs:
header_hash: [u8; 32],distance_threshold: U512,nonce_start: U512,nonce_end: U512. - State:
status,start_time,total_hash_count,best_result,cancel_flag,result_receiver,thread_handles,completed_threads. - Behavior:
start_mining(num_cores): spawns worker threads and sets up a results channel.cancel(): sets cancel flag and joins all worker threads.update_from_results(): consumes worker messages, aggregates progress, picks the best result, and advancesstatus.
- Inputs:
-
ThreadResult- Per-thread message with
hash_countdelta, optionalMiningJobResult, andcompletedflag.
- Per-thread message with
-
MiningJobResult- The best candidate found so far:
nonce: U512,work: [u8; 64],distance: U512. - Note:
workis the 64-byte big-endian representation of the winning nonce.
- The best candidate found so far:
-
QPoWSeal- Encodes the sealing payload returned to the node. In this crate, it includes
nonce: [u8; 64](theworkvalue).
- Encodes the sealing payload returned to the node. In this crate, it includes
Inputs and Validation
The miner accepts jobs via POST /mine with fields defined by resonance_miner_api::MiningRequest. In code, the following fields are validated:
job_id: non-empty string (UUID recommended).mining_hash: 64-char hex string (32 bytes).distance_threshold: decimal string convertible toU512.nonce_start,nonce_end: 128-char hex strings (each 64 bytes), withnonce_start <= nonce_end.
If validation fails, the miner returns a 400 with an error message. If a duplicate job_id is submitted, it returns a 409.
Note: The YAML protocol uses difficulty terminology, while the implementation consumes distance_threshold (decimal string). In QPoW terms here:
distance_thresholdis the acceptance threshold foris_valid_nonce.get_nonce_distancereturns a score (smaller is better) used to select the best candidate when multiple valid nonces are observed near-simultaneously.
Work Distribution Across CPU Threads
A job’s nonce space is partitioned evenly across num_cores worker threads:
- Compute
total_range = (nonce_end - nonce_start) + 1. - Compute
range_per_core = total_range / num_cores. - Compute
remainder = total_range % num_cores. - Assign each thread a contiguous sub-range:
- Thread
igets[nonce_start + i * range_per_core, (start + range_per_core - 1)]. - The final thread receives the
remainderto ensure full coverage. - Ranges are clamped not to exceed
nonce_end.
- Thread
This partitioning ensures:
- Near-equal work per core.
- No overlaps between thread ranges.
- Full coverage of the requested inclusive interval.
The Inner Mining Loop
Each thread runs mine_range(...):
- Iterate
current_noncefromstarttoendinclusive. - For each nonce:
- Convert
current_nonceto a 64-byte big-endian array (U512::to_big_endian()), referred to asnonce_bytes. - Increment a local
hash_count(number of candidates checked). - Call
qpow_math::is_valid_nonce(header_hash, nonce_bytes, distance_threshold).- If valid:
- Compute
distance = qpow_math::get_nonce_distance(header_hash, nonce_bytes). - Send a
ThreadResultcarrying aMiningJobResult { nonce, work: nonce_bytes, distance }through thecrossbeam_channelto the coordinator. - Reset the thread-local
hash_countto 0 (since it was emitted with the result).
- Compute
- If valid:
- Periodically check the shared
cancel_flag. If set, exit early.- The implementation checks the flag each loop iteration, but additionally includes a cutoff check every 4096 iterations to break sooner once cancellation is signaled under high-throughput conditions.
- Convert
At thread termination (either completing its range or due to cancellation), the thread emits a final ThreadResult with completed = true and any remaining hash_count delta.
Coordinating Results and Early Termination
MiningJob::update_from_results() is called by the mining state loop to coordinate progress:
- Drain all available
ThreadResultmessages:- Accumulate
hash_countintototal_hash_count. - When
completed = true, incrementcompleted_threads. - If a
MiningJobResultis present:- Compare against
best_resultbydistance(smaller distance is better). - If it improves the best-so-far:
- Update
best_result. - Set
cancel_flag = trueto stop other threads promptly (first-best-wins policy).
- Update
- Compare against
- Accumulate
- Update the job status:
- If any
best_resultexists:status = Completed. - Else, if all threads are finished and none found a valid nonce:
status = Failed. - Otherwise:
statusremainsRunning.
- If any
This pattern yields:
- Minimal time-to-first-solution via early cancellation.
- A consistent best-result selection when multiple threads race to produce solutions.
Job Lifecycle
-
Submission (node → miner)
POST /mine→handle_mine_requestvalidate_mining_requestensures inputs are structurally valid.MiningState::add_jobinserts aMiningJob, callsstart_mining(num_cores), and acknowledges withaccepted.
-
Execution (miner internal)
start_miningspawns worker threads and attaches a boundedcrossbeam_channelsender/receiver pair.- Threads run
mine_range, pushThreadResultmessages (progress, solutions, completion signals). MiningState::start_mining_loopruns a background task:- Periodically locks the jobs map and calls
update_from_results()on each running job. - Logs job completion and aggregates metrics.
- Retains finished jobs briefly for retrieval; cleans up stale entries after ~5 minutes.
- Periodically locks the jobs map and calls
-
Cancellation (node → miner)
POST /cancel/{job_id}→handle_cancel_requestMiningState::cancel_jobsetscancel_flagand joins all threads.- The job status becomes
Cancelled. The job remains visible for a short retention period.
-
Retrieval (node → miner)
GET /result/{job_id}→handle_result_request- Returns
status ∈ {running, completed, failed, cancelled}plus:nonce(hexU512) andwork([u8; 64]hex) whencompleted.hash_count(total candidates checked) andelapsed_time(seconds) in all cases.
- If
job_iddoes not exist: returnsnot_found.
-
Cleanup (miner internal)
- The mining loop periodically removes jobs that are not
Runningand are older than a short retention window to keep memory usage bounded.
- The mining loop periodically removes jobs that are not
Concurrency and Memory Model
- Threads coordinate via:
Arc<AtomicBool>cancel flag (checked with relaxed ordering).crossbeam_channelfor non-blocking result and progress delivery.
MiningState.jobsisArc<Mutex<HashMap<...>>>(Tokio mutex) to serialize job map updates and queries from async contexts.- Per-thread handles are joined during explicit cancellation or when a job is dropped/removed.
- The result channel is bounded (
num_cores * 2) to avoid unbounded memory usage under bursty result conditions (extremely low thresholds).
Performance Characteristics
- Linear scan with O(1) work per candidate (dominated by
is_valid_nonceandget_nonce_distance). - Load scales with
num_cores(default: all logical CPUs vianum_cpus::get()). - Cost centers:
- Big-endian conversion of
U512to[u8; 64]per candidate. - The
is_valid_nonceand distance computations inqpow_math. - Channel operations and atomic flag checks.
- Big-endian conversion of
- Early-cancel strategy minimizes wasted work after a solution is found.
- Hash rate tracking is coarse-grained across threads (accumulated via periodic messages and final completion reports).
Edge Cases and Guarantees
- Duplicate job IDs are rejected.
- Nonce ranges are inclusive; the miner never searches outside
[nonce_start, nonce_end]. - If no valid nonce is found in the range, the job ends with
Failed. - On
Cancelled, threads stop as soon as they observe the flag; they still flush finalhash_countso the last reported progress is accurate. - The best result is the smallest
distanceobserved; however, the miner typically cancels on the first winning candidate, so ties are rare in practice. - Jobs are retained briefly after completion/cancellation to allow the node to fetch results; they are then cleaned up to bound resource use.
API Mapping
- Submit job:
POST /mine→acceptedorerror. - Poll result:
GET /result/{job_id}→running,completed,failed,cancelled, ornot_found. - Cancel job:
POST /cancel/{job_id}→cancelledornot_found.
Returned fields:
nonce: hex string representation ofU512(no 0x).work: 128-hex-char representation of[u8; 64](no 0x). In this miner, it is the big-endian form of the winningnonce.hash_count: total number of candidates tested for this job.elapsed_time: seconds sincestart_time.
Configuration
--port/MINER_PORT(default 9833)--num-cores/MINER_CORES(default: all available logical CPUs)
Potential Future Improvements
- Adaptive partitioning and dynamic work stealing for uneven ranges or heterogeneous cores.
- SIMD or instruction-level optimizations inside
qpow_mathhot paths. - Batch evaluation of multiple nonces per iteration to amortize overhead of conversions and checks.
- Optional GPU offloading with the same external protocol.
- Rate metrics per thread and moving averages to refine progress estimates.
- Structured telemetry for better observability under load.
In summary, the miner is a CPU-bound, multi-threaded exhaustive search over a 512-bit nonce range, guided by the QPoW validity predicate and a distance-based scoring. It is designed for low-latency solution discovery via early cancellation, with simple and robust job lifecycle management exposed through a small HTTP surface.