# QPoW Mining: Workflow and Algorithms
This document explains the Quantum Proof of Work (QPoW) design used by the Quantus Network, focusing on the end-to-end mining workflow and the algorithms that define validity, difficulty adjustment, and chain selection.
Key components (by crate/directory):
- Runtime pallet: pallets/qpow
- Math/verification: qpow-math
- Client (mining worker, block import, chain selection): client/consensus/qpow
- Node service and miner orchestration: node
- Mining rewards: pallets/mining-rewards
- External miner protocol: EXTERNAL_MINER_PROTOCOL.md and miner-api
Terms used throughout:
- mining hash: the block header hash being mined, i.e., the header pre-hash (no seal)
- seal: the 64-byte nonce placed in the header’s digest as the PoW seal
- distance threshold (aka threshold): U512 target bound for acceptance
- difficulty: derived inversely from threshold
- work: cumulative chain work used in fork choice
## 1) High-level mining flow
1. Proposing a block:
- The node builds a block proposal from the best chain.
- A pre-runtime digest is optionally inserted that encodes the miner rewards account (for rewards attribution).
- The proposal header hash (pre-hash) becomes the mining hash.
2. Mining metadata and loop:
- The mining worker produces metadata: { best_hash, pre_hash, difficulty } and exposes it to a miner loop (local or external).
- The miner searches for a 64-byte nonce such that the QPoW validity condition holds under the current threshold (from runtime).
3. Finding a solution:
- For each nonce candidate, compute the distance with respect to the mining hash (see Section 2) and check if distance <= threshold.
- If a solution is found, submit the 64-byte nonce as a seal.
4. Import and verification:
- On submission, the node imports the block, extracting the seal from the header digest and passing it to the runtime to verify via the QPoW runtime API.
- The runtime recomputes the target/distance and accepts or rejects the block accordingly.
5. Difficulty and rewards:
- OnFinalize, the runtime updates difficulty (via threshold adjustment) using observed block time EMA.
- The mining-rewards pallet extracts the miner from the pre-runtime digest and mints rewards and fees accordingly.
- Chain finalization proceeds at a bounded reorg depth.
## 2) QPoW proof definition
QPoW defines validity based on a deterministic RSA-like group construction derived from the mining hash and a Poseidon2 permutation to map group elements to a 512-bit field. The proof search is a hunt for a 64-byte nonce that minimizes the XOR distance between a fixed target element and the nonce-dependent element.
Given:
- h = mining hash (pre-hash) as U512 derived from [u8; 32]
- s = nonce as U512 derived from [u8; 64]
- (m, n) = group parameters derived deterministically from h (see 2.1)
- Poseidon2-512: a cryptographic permutation producing 512-bit outputs
Definitions:
- Base exponentiation (no hash): g(h, s) = m^(h + s) mod n
- Poseidon mapping: G(h, s) = Poseidon2_512(g(h, s))
- Target element: T = G(h, 0)
- Nonce element: X(s) = G(h, s)
- Distance: D(s) = T XOR X(s) as U512
Validity condition:
- A nonce s is valid if D(s) <= threshold, where threshold is dynamic and maintained by the runtime pallet.
Properties:
- D(s) is symmetric XOR distance; lower is better.
- threshold is a U512 bound; smaller threshold implies higher difficulty.
### 2.1) Deriving RSA-like parameters (m, n)
To define the group element computation, QPoW derives a pair (m, n) from the mining hash using Poseidon2:
- m is a 256-bit integer: m = U512(Poseidon2_256(h_bytes))
- n is a 512-bit integer initialized as n0 = U512(Poseidon2_512(m_bytes)) and then rerolled via Poseidon if constraints fail.
Constraints for n:
- n must be odd,
- n > m,
- gcd(m, n) = 1 (coprime),
- n must be composite (not prime).
Reroll step:
- While any constraint fails, set n := U512(Poseidon2_512(n_bytes)) and re-check.
Notes:
- is_coprime uses Euclidean GCD.
- is_prime uses a Miller–Rabin test with 32 deterministic bases derived from Poseidon2, giving a negligible false positive rate (~2^-64).
### 2.2) Mapping to group and distance
- Compute g(h, s) = m^(h + s) mod n.
- Map via Poseidon2-512: G(h, s) = Poseidon2_512(to_bytes(g(h, s))).
- Define T = G(h, 0) (fixed per block) and X(s) = G(h, s).
- The proof’s score is the XOR distance D(s) = T XOR X(s).
- Accept if D(s) <= threshold.
## 3) Mining loop and optimizations
Brute-force mining iterates over nonces s and checks D(s) <= threshold. QPoW includes an optimization for scanning contiguous nonce ranges efficiently:
Incremental exponentiation:
- Let v0 = m^(h + s0) mod n for a starting nonce s0.
- For each next nonce s = s_prev + 1, we can update:
v_next = (v_prev * m) mod n
- This avoids recomputing the full exponentiation for each s.
- At each step, compute X(s) = Poseidon2_512(v) and check distance against the threshold.
Practical loop:
- The node mines nonces in small batches (e.g., 300) using this incremental method.
- If no solution is found, it advances the starting nonce and repeats.
## 4) Difficulty and threshold adjustment
The runtime maintains a dynamic distance threshold and derives difficulty from it.
Key storage variables:
- CurrentDistanceThreshold: U512
- TotalWork: U512 (cumulative)
- BlockTimeEma: u64 (ms)
- LastBlockTime, LastBlockDuration: u64 (ms)
Parameters (runtime constants):
- InitialDistanceThresholdExponent: u32
- DifficultyAdjustPercentClamp: u8 (percent)
- TargetBlockTime: u64 (ms)
- EmaAlpha: u32 (0..1000; 1000 => 1.0)
- FixedU128Scale: u128 (default 10^18)
- MaxReorgDepth: u32
- MaxDistanceMultiplier: u32 (multiplier for max distance)
Initialization:
- initial_threshold = 1 << InitialDistanceThresholdExponent
- max_distance = initial_threshold << MaxDistanceMultiplier
- min_distance = 1
Difficulty:
- difficulty = max_distance / CurrentDistanceThreshold
- This grows when threshold shrinks, and vice versa.
Threshold adjustment (per block, on_finalize):
1) Measure block_time = now - LastBlockTime (ms) and update BlockTimeEma using EmaAlpha (scaled by 1000).
2) Compute ratio = clamp(observed/target, 1 ± clamp%), where:
- observed = BlockTimeEma
- target = TargetBlockTime
- clamp% = DifficultyAdjustPercentClamp
3) Adjust threshold:
- Represent ratio as FixedU128; then:
new_threshold ≈ (CurrentDistanceThreshold / FixedU128Scale) * ratio_fixed
- Clamp into [min_distance, max_distance]
4) Update CurrentDistanceThreshold = new_threshold
5) Update TotalWork += difficulty
6) Reset counters for next iteration; record timestamps
Notes:
- The division by FixedU128Scale first avoids overflow before scaling by ratio.
- Threshold is never allowed to drop below 1 or exceed max_distance.
## 5) Verification and seals
Seal structure:
- A seal is exactly 64 bytes (U512) representing the nonce.
Import path:
- The node extracts the seal from the header digest (it must carry the correct consensus engine ID).
- The node passes (parent_hash, pre_hash, nonce) to the runtime QPoW API.
- The runtime recomputes (m, n), T, X(s), distance D(s), and validates D(s) <= threshold (fetched from runtime state).
- If validation fails or the seal length is not 64 bytes, the block is rejected.
Important: The PoW engine ID in header digests uses sp_consensus_pow::POW_ENGINE_ID (b"pow_") for both the pre-runtime digest and the seal item. The QPoW runtime API is exposed via sp_consensus_qpow::QPoWApi, but the digest engine ID used in headers remains the POW_ENGINE_ID constant.
## 6) Fork-choice (HeaviestChain) and finalization
HeaviestChain:
- The client selects the best chain based on total cumulative work reported by the runtime (QPoW: TotalWork).
- Among leaves:
- Choose the chain with the highest total work.
- If equal work, tie-break by greater height.
- Enforce a maximum reorg depth: if switching to a higher-work chain exceeds MaxReorgDepth, that chain is marked “ignored”.
- Ignored chains:
- The client persists a small flag for chain heads that exceed reorg limits to prevent oscillation.
Finalization:
- Periodically finalize blocks older than MaxReorgDepth - 1 behind the current best.
- This provides stable finality under bounded reorg assumptions.
## 7) Mining rewards
The mining-rewards pallet:
- OnFinalize, extracts the miner account from the pre-runtime digest (under POW_ENGINE_ID). The digest payload is the SCALE-encoded AccountId.
- Mints:
- Miner block reward (configurable constant)
- Treasury block reward (configurable constant)
- Transaction fees collected during the block (credited to the miner)
- Stores and emits transfer proofs (qp_wormhole::TransferProofs) as needed by the chain’s token model.
If no miner pre-runtime digest is present or decode fails:
- Rewards are minted to the Treasury account.
## 8) External miner support
The node can offload mining to an external service over HTTP.
Protocol (see EXTERNAL_MINER_PROTOCOL.md, miner-api):
- POST /mine
- Provides: job_id, mining_hash (pre-hash), distance_threshold, nonce_start, nonce_end
- Accepted => miner starts scanning the nonce range
- GET /result/{job_id}
- Returns status; on completion contains winning work (64-byte nonce hex)
- POST /cancel/{job_id}
- Cancels a job (used when mining metadata changes or node shuts down)
Node behavior:
- When mining metadata changes (e.g., new best block), the node cancels any stale job and submits a fresh job to the external miner.
- On solution, the node re-checks that the mining metadata version is still current and submits the seal.
## 9) Edge cases, guards, and safety notes
- Zero nonce guard: nonce == 0 is disallowed during validation and considered invalid (treated as a programming error during verify paths).
- Seal length: Exactly 64 bytes. Any other length results in rejection.
- Determinism: (m, n) generation is fully deterministic from the mining hash; miners and validators derive identical parameters.
- Modular arithmetic:
- Modular exponentiation uses big-integer arithmetic; the incremental update path ensures correctness via tested equivalence against full mod_pow for s, s+1, ...
- Primality checking:
- Miller–Rabin with deterministic Poseidon-derived bases increases robustness of composite n selection.
- Bounded difficulty changes:
- The clamp (±DifficultyAdjustPercentClamp%) limits threshold adjustments per block.
- Bounded distance threshold:
- Threshold is clamped to [1, max_distance] to prevent degenerate states.
- Reorg safety:
- MaxReorgDepth bounds reorgs; chains that would require deeper reorgs are ignored.
- Rewards attribution:
- Rewards rely on the miner’s AccountId embedded into a pre-runtime header digest (POW_ENGINE_ID). If missing/invalid, rewards go to treasury.
## 10) Putting it together: miner pseudocode
This is a straightforward, local miner loop using the incremental exponentiation optimization:
1) Get mining metadata from the node:
- h = pre_hash (32 bytes)
- threshold = runtime.get_distance_threshold()
- best_hash = chain tip (for staleness checks)
2) Derive group parameters:
- (m, n) = get_random_rsa(h)
3) Precompute:
- T = Poseidon2_512( m^(h + 0) mod n )
4) Initialize a range [s, s + k):
- v = m^(h + s) mod n
5) For i in 0..k:
- nonce = s + i
- X = Poseidon2_512(v)
- D = T XOR X
- If D <= threshold => return nonce
- v = (v * m) mod n
6) If not found:
- s += k and repeat (or request a new job if metadata changed)
7) On success:
- Submit the 64-byte big-endian encoding of nonce as the seal.
## 11) Runtime API surface (consumer view)
The node/client interacts with the runtime via sp_consensus_qpow::QPoWApi to:
- verify_nonce_on_import_block(pre_hash, nonce)
- verify_nonce_local_mining(pre_hash, nonce) // local check variant
- get_distance_threshold()
- get_difficulty()
- get_max_distance()
- get_total_work()
- get_block_time_ema()
- get_last_block_time()
- get_last_block_duration()
- get_max_reorg_depth()
- get_chain_height()
- get_random_rsa(h)
- hash_to_group_bigint(h, m, n, s)
Note: Names reflect the runtime implementation; precise signatures include concrete types (e.g., [u8; 32]/[u8; 64]/U512).
## 12) Configuration checklist
- TargetBlockTime (ms): desired average block time.
- EmaAlpha (0..1000): smoothing factor for block time EMA.
- DifficultyAdjustPercentClamp (%): maximum per-block threshold adjustment.
- InitialDistanceThresholdExponent: sets the initial threshold magnitude.
- MaxDistanceMultiplier: caps threshold to protect from unbounded growth.
- MaxReorgDepth: bounds fork switch depth.
- Miner and Treasury rewards per block (mining-rewards pallet).
- Rewards address: ensure a valid AccountId is embedded via pre-runtime digest when authoring.
## 13) Practical notes
- Local mining batch size:
- The default local miner scans small batches (e.g., 300 nonces) to remain responsive to new metadata and to cooperate with other node tasks.
- Logging and observability:
- Enable `sc_consensus_pow=trace`, `qpow=debug`, `math=debug` to inspect mining math, threshold adjustments, and verification.
- Deterministic end-to-end:
- Miners, validators, and full nodes perform identical math given (h, s). Disagreements imply malformed seals or non-canonical/malformed headers.
## 14) Glossary
- Mining hash / pre-hash: The block header hash without the PoW seal.
- Seal: The 64-byte nonce placed in the header digest under the PoW engine ID.
- Threshold: The U512 bound that the XOR distance must not exceed for a valid solution.
- Difficulty: Inverse of threshold relative to a fixed max_distance, difficulty = max_distance / threshold.
- Total work: Cumulative sum of per-block difficulty, used by fork-choice.
- Reorg depth: Number of blocks that would be replaced if switching to another chain head.
- Poseidon2: The permutation used to map group elements to 512-bit values for distance calculation.
---
By following this specification, a miner (local or external) can interoperate with the Quantus Network’s QPoW consensus, produce valid seals, and submit mined blocks that the runtime will accept and reward.