# 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.