How Difficulty Is Adjusted in Amadeus Protocol's UPoW: A Technical Deep Dive

Amadeus Protocol adjusts difficulty in its Uni‑Proof‑of‑Work (UPoW) consensus by retargeting each epoch based on the number of solved proofs‑of‑work (sols) produced, using a logarithmic step algorithm with ±10% tolerance bands and hard caps on per‑epoch changes.

The UPoW difficulty adjustment mechanism ensures stable block production rates despite fluctuating network hash power. This article examines the complete implementation in the amadeusprotocol/node codebase, covering the mathematical retarget algorithm in sol_difficulty.rs and the epoch orchestration logic in epoch.rs.

The Retarget Algorithm in sol_difficulty.rs

The core difficulty calculation lives in ex/native/rdb/src/consensus/bic/sol_difficulty.rs. The next() function implements a bounded, tolerance‑driven retarget scheme that prevents volatile difficulty swings.

Function Signature and Constants

// Lines 6-15 in sol_difficulty.rs
const TOL_NUM: u64 = 1;      // Tolerance numerator
const TOL_DEN: u64 = 10;     // 10% tolerance band
const MAX_STEP_UP: u32 = 2;  // Maximum difficulty increase per epoch
const MAX_STEP_DOWN: u32 = 3; // Maximum difficulty decrease per epoch
const UP_SLOWDOWN: u64 = 2;  // Slowdown factor for upward adjustments
const DIFF_MIN_BITS: u32 = 20;
const DIFF_MAX_BITS: u32 = 64;

The Four Adjustment Cases

The next(prev_bits, sols, target) → u32 function handles four distinct scenarios (lines 43-58):

  1. Within tolerance band – If sols falls between target * 9/10 and target * 11/10, difficulty remains unchanged.

  2. Zero sols – When sols == 0, difficulty drops aggressively but is capped by MAX_STEP_DOWN (3 bits).

  3. Over‑target – For sols > target * 1.1, difficulty increases proportionally to log₂(sols/target), divided by UP_SLOWDOWN (2), and clamped to MAX_STEP_UP (2 bits).

  4. Under‑target – For sols < target * 0.9, difficulty decreases by a similar logarithmic step, limited to MAX_STEP_DOWN (3 bits).

Logarithmic Ratio Helpers

The algorithm uses efficient integer arithmetic for ratio calculations:

// Helper functions in sol_difficulty.rs
fn ceil_div(a: u64, b: u64) -> u64;
fn ilog2_floor(n: u64) -> u32;
fn ceil_log2_ratio(num: u64, den: u64) -> u32;
fn clamp_bits(bits: u32) -> u32;  // Enforces DIFF_MIN_BITS/DIFF_MAX_BITS bounds

Epoch Boundary Invocation in epoch.rs

The retarget algorithm is triggered at each epoch's conclusion via update_difficulty_and_log_sols in ex/native/rdb/src/consensus/bic/epoch.rs.

Step‑by‑Step Execution Flow

Step Action Source Lines
1 Read current difficulty from bic:epoch:diff_bits 43-44
2 Select target based on fork height 48-52
3 Call sol_difficulty::next() with computed parameters 53
4 Persist new difficulty to current and historical keys 54-56

Fork‑Dependent Target Selection

The protocol supports two distinct sol targets depending on epoch height:

// Lines 48-52 in epoch.rs
let target = if epoch_height >= protocol::forkheight2() {
    sol_difficulty::TARGET_SOLS_EPOCH2  // 180_000
} else {
    sol_difficulty::TARGET_SOLS_EPOCH   // 380_000
};
  • Pre‑FORKHEIGHT2 (epoch < 767): Target of 380,000 sols/epoch
  • Post‑FORKHEIGHT2 (epoch ≥ 767): Reduced target of 180,000 sols/epoch

Practical Simulation Example

Below is a runnable Rust example demonstrating the UPoW difficulty adjustment logic:

use crate::consensus::bic::sol_difficulty;

/// Simulate difficulty retarget for any epoch
fn simulate_retgt(prev_bits: u32, sols_this_epoch: u64, epoch_number: u64) {
    // Mirror the fork-aware target selection from epoch.rs
    let target = if epoch_number >= 767 { // FORKHEIGHT2
        sol_difficulty::TARGET_SOLS_EPOCH2  // 180_000
    } else {
        sol_difficulty::TARGET_SOLS_EPOCH   // 380_000
    };

    let next_bits = sol_difficulty::next(prev_bits, sols_this_epoch, target);
    
    println!(
        "Epoch {} – prev: {} bits, sols: {}, target: {}, next: {} bits",
        epoch_number, prev_bits, sols_this_epoch, target, next_bits
    );
}

fn main() {
    // Legacy epoch: under-target by 16% → steps down 1 bit
    simulate_retgt(23, 318_000, 500);  // Output: next = 22
    
    // Post-fork: exactly on target (180k) → no change
    simulate_retgt(23, 180_000, 800);  // Output: next = 23
    
    // Post-fork: 77% over target → steps up 1 bit (capped by MAX_STEP_UP=2)
    simulate_retgt(23, 318_000, 800);  // Output: next = 24
}

Complete Adjustment Flow


┌─────────────────────┐
│  End of Epoch       │
│  (total_sols known) │
└─────────┬───────────┘
          ▼
┌─────────────────────┐
│ update_difficulty_  │
│   and_log_sols()    │
│  • Read old diff    │
│  • Select target    │
│  • Call next()      │
└─────────┬───────────┘
          ▼
┌─────────────────────┐
│ sol_difficulty::next│
│  • Apply tolerance  │
│  • Compute log-step │
│  • Clamp to bounds  │
└─────────┬───────────┘
          ▼
┌─────────────────────┐
│  Store new diff to  │
│  bic:epoch:diff_bits│
│  (current + epoch)  │
└─────────────────────┘

Monitoring and Observability

The current difficulty is exposed for external monitoring via prometheus.ex:


# In ex/lib/http/prometheus.ex

:prometheus_gauge.set(:amadeus_difficulty_bits, diff_bits)

This allows operators to track UPoW difficulty adjustment trends in real time.

Summary

  • Tolerance band: ±10% around target prevents unnecessary oscillations
  • Step limits: Maximum 2 bits up, 3 bits down per epoch
  • Logarithmic scaling: Adjustments proportional to log₂(ratio) not linear ratio
  • Fork-aware targets: 380,000 pre‑fork, 180,000 post‑fork sols/epoch
  • Hard bounds: Difficulty always clamped between 20 and 64 bits
  • Zero‑sol handling: Aggressive but bounded decrease prevents deadlock

Frequently Asked Questions

How does UPoW difficulty differ from Bitcoin's difficulty adjustment?

Bitcoin retargets every 2016 blocks (~2 weeks) using a simple ratio formula with 4× bounds. UPoW adjusts every epoch (duration variable, typically hours not weeks) using logarithmic steps with tighter 10% tolerance bands and asymmetric step limits (2 up, 3 down). The logarithmic approach smooths volatility compared to Bitcoin's direct proportionality.

What happens if no sols are produced in an entire epoch?

The difficulty drops by up to 3 bits (MAX_STEP_DOWN). This is the most aggressive single‑epoch decrease allowed, designed to recover from catastrophic hash‑power loss without allowing instant trivial mining. After multiple zero‑sol epochs, difficulty approaches DIFF_MIN_BITS (20).

Why was the sol target reduced from 380,000 to 180,000 at FORKHEIGHT2?

The 53% target reduction (to TARGET_SOLS_EPOCH2) recalibrated epoch duration expectations as the protocol matured. Lower targets produce more frequent retargeting opportunities, improving responsiveness to hash‑power changes while maintaining the same ±10% tolerance and logarithmic step mechanics.

Can difficulty change by more than 2-3 bits in consecutive epochs?

Yes, but only through sustained divergence from target. The per‑epoch caps (MAX_STEP_UP/MAX_STEP_DOWN) are absolute, so a sustained 50% over‑target condition would require multiple epochs to fully correct. This intentional inertia prevents difficulty overreaction to transient hash‑power spikes.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →