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):
-
Within tolerance band – If
solsfalls betweentarget * 9/10andtarget * 11/10, difficulty remains unchanged. -
Zero sols – When
sols == 0, difficulty drops aggressively but is capped byMAX_STEP_DOWN(3 bits). -
Over‑target – For
sols > target * 1.1, difficulty increases proportionally tolog₂(sols/target), divided byUP_SLOWDOWN(2), and clamped toMAX_STEP_UP(2 bits). -
Under‑target – For
sols < target * 0.9, difficulty decreases by a similar logarithmic step, limited toMAX_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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →