DEV Community

Althaaf Mohamed
Althaaf Mohamed

Posted on

Per-block difficulty adjustment in a post-quantum Proof-of-Work chain (Rust, integer-only) published:

I'm building Quantum-Lattice (QL), a Layer 1 Proof-of-Work chain written in Rust that signs every transaction with ML-DSA-65 (NIST FIPS 204), a post-quantum signature scheme. This post covers one piece of the latest hard fork: moving from a coarse every-10-blocks difficulty retarget to a per-block, fixed-point adjustment.

The old rule

Difficulty was an integer number of leading zero bits, recalculated every 10 blocks and clamped to ±2 bits per step. That is simple, but coarse: it only reacts at window boundaries, it moves in whole bits, and a window can land in a band where the rounded result is zero change.

The new rule

Three ideas:

  1. Fixed-point difficulty. 65,536 units = 1 bit. The target is 2^(256 - d) for fractional d, so difficulty can move in tiny steps.
  2. Integer-only math. Consensus code must give identical results on every machine, so no floats. The fractional power of two uses a cubic polynomial approximation (the same constants as Bitcoin Cash's aserti3 family). Whole-bit values give exactly the old leading-zero-bits result, so history stays valid.
  3. Per-block, relative update. Each block nudges difficulty according to how far its gap was from the 300 s target:
pub fn next_fine(parent_baseline: u32, gap_secs: i64) -> u32 {
    let delta = ((TARGET_BLOCK_TIME_SECS - gap_secs) as i128 * FP_ONE as i128)
        / FORK2_TAU_SECS as i128;           // tau = 3600 s
    let v = parent_baseline as i128 + delta;
    v.clamp(FORK2_MIN_FP as i128, FORK2_MAX_FP as i128) as u32
}
Enter fullscreen mode Exit fullscreen mode

A block that comes after 200 s raises difficulty by about 0.028 bits. One that takes 1,200 s lowers it by about 0.25 bits. Because the updates telescope, the result depends on the end points of a stretch of blocks, not on the order of the gaps in between.

A block is valid if hash < target(difficulty), compared as 256-bit big-endian integers.

Timestamps

Per-block rules lean on timestamps, so those get stricter too: a block's timestamp must be above the median of the previous 11 blocks, and no more than 15 minutes ahead of the node's clock.

Does it behave?

I simulated the controller before shipping it (steady hashrate with exponential block arrivals). With a 3,600 s time constant the steady-state wobble is about 0.25 bits, and it recovers from a 5× hashrate step in either direction within about 60 blocks, and from a 100× exodus in about 40.

On the live chain the switch at block 35,600 went through with no stall and no split. Difficulty now moves every block, and blocks have averaged close to the 5-minute target at roughly 330 MH/s.

What I'd flag

  • This does not stop an attacker with majority hashrate. It makes difficulty harder to disturb and smoother to track, nothing more.
  • A hard fork means every node operator has to run the new build. Nodes on the old rules stop following the chain at the activation height.
  • The hashrate is concentrated among a handful of operators, and the difficulty swings show it. That's a network-growth problem, not a code one.

Code and tests

The consensus code and its tests (known target values, monotonicity, a check that whole-bit values match the old rule exactly, timestamp rules) are in src/consensus.rs:

https://github.com/AlthaafM/Quantum-Lattice

Questions, review and criticism welcome. I'd especially like eyes on the integer approximation and the clamp bounds.

Top comments (0)