Decay Mechanics
Complete technical reference for concentration decay — the sigmoid function, fixed-point arithmetic, cluster tracking, lineage decay, and the decay pool.
Design Principle
Concentration decay is RillCoin's core economic primitive. When any cluster of UTXOs exceeds a threshold concentration relative to the circulating supply, those holdings decay at a rate proportional to their concentration. Decayed tokens flow to the mining reward pool, redistributing wealth to active network participants. All arithmetic is integer-only using checked u64 with no floating point.
Concentration Metric
Concentration is measured in parts-per-billion (PPB). A PPB value of 1,000,000 (one million) represents a concentration of 0.1% of the circulating supply — this is the decay threshold.
// concentration_ppb = cluster_balance × 1_000_000_000 / circulating_supply
//
// cluster_balance: sum of all UTXOs with this cluster_id (in rills)
// circulating_supply: total unspent supply (in rills)
// Result: parts per billion (u64)
pub fn concentration_ppb(cluster_balance: u64, circulating_supply: u64) -> u64 {
cluster_balance
.checked_mul(CONCENTRATION_PRECISION) // × 1_000_000_000
.unwrap_or(u64::MAX)
.checked_div(circulating_supply)
.unwrap_or(0)
}
pub const CONCENTRATION_PRECISION: u64 = 1_000_000_000;
pub const DECAY_C_THRESHOLD_PPB: u64 = 1_000_000; // 0.1% of supplyA cluster must exceed DECAY_C_THRESHOLD_PPB (1,000,000 PPB = 0.1% of supply) before any decay is applied. Below this threshold, the decay rate is exactly zero.
Cluster Index
Every UTXO is tagged with a cluster_id — a 32-byte BLAKE3 hash stored in the TxOutput.cluster_id field. All UTXOs sharing a cluster_id are aggregated into a single cluster balance for decay calculation.
The cluster index is maintained in RocksDB as a mapping:
cluster_id → aggregate_balance (u64 rills)When a UTXO is spent, its value is subtracted from the cluster balance. When a UTXO is created, its value is added. The cluster balance is always the sum of all unspent UTXOs with that cluster_id.
The default cluster_id for wallet-generated outputs is:
default_cluster_id = BLAKE3(ed25519_root_public_key_bytes)Sigmoid Decay Function
The decay rate uses a fixed-point sigmoid lookup table. The sigmoid function maps concentration (normalized by CONCENTRATION_PRECISION) to a rate value. Linear interpolation is used between table entries.
Constants:
SIGMOID_PRECISION = 1,000,000,000— Fixed-point scale for sigmoid outputTABLE_STEP = 500,000,000— Step size between entries (0.5 in float terms)
| x (concentration) | sigmoid(x) | Fixed-point (× 10⁹) |
|---|---|---|
| 0.0 | 0.5000000 | 500,000,000 |
| 0.5 | 0.6224593 | 622,459,300 |
| 1.0 | 0.7310586 | 731,058,600 |
| 1.5 | 0.8175744 | 817,574,400 |
| 2.0 | 0.8807970 | 880,797,000 |
| 2.5 | 0.9241418 | 924,141,800 |
| 3.0 | 0.9525741 | 952,574,100 |
| 3.5 | 0.9706878 | 970,687,800 |
| 4.0 | 0.9820137 | 982,013,700 |
| 4.5 | 0.9890130 | 989,013,000 |
| 5.0 | 0.9933071 | 993,307,100 |
| 5.5 | 0.9959298 | 995,929,800 |
| 6.0 | 0.9975274 | 997,527,400 |
| 6.5 | 0.9984965 | 998,496,500 |
| 7.0 | 0.9990889 | 999,088,900 |
| 7.5 | 0.9994472 | 999,447,200 |
| 8.0 | 0.9996646 | 999,664,600 |
Linear Interpolation
For a concentration value x between table entries, the sigmoid output is linearly interpolated:
pub fn sigmoid_fixed(x: u64) -> u64 {
// x is in fixed-point with CONCENTRATION_PRECISION = 1_000_000_000
// TABLE_STEP = 500_000_000 (represents 0.5 in float)
let idx = (x / TABLE_STEP) as usize;
if idx >= SIGMOID_TABLE.len() - 1 {
return SIGMOID_TABLE[SIGMOID_TABLE.len() - 1];
}
let y0 = SIGMOID_TABLE[idx];
let y1 = SIGMOID_TABLE[idx + 1];
let frac = x % TABLE_STEP; // fractional part within step
// Linear interpolation: y0 + (y1 - y0) * frac / TABLE_STEP
y0 + (y1.saturating_sub(y0))
.checked_mul(frac)
.unwrap_or(0)
/ TABLE_STEP
}Decay Rate Calculation
The effective decay rate in PPB per block is derived from the sigmoid output:
// decay_rate = (sigmoid(concentration_x) - 0.5) × DECAY_R_MAX_PPB × 2
//
// Constants:
// DECAY_R_MAX_PPB = 1_500_000_000 (150% per year at max, in PPB units)
// SIGMOID_HALF = 500_000_000 (0.5 in fixed-point)
pub fn decay_rate_ppb(concentration_ppb: u64) -> u64 {
if concentration_ppb < DECAY_C_THRESHOLD_PPB {
return 0;
}
// Normalize concentration for sigmoid input
let x = concentration_ppb
.checked_mul(CONCENTRATION_PRECISION)
.unwrap_or(u64::MAX)
/ DECAY_C_THRESHOLD_PPB;
let sig = sigmoid_fixed(x);
// Shift by 0.5 and scale to max rate
let shifted = sig.saturating_sub(SIGMOID_HALF);
shifted
.checked_mul(DECAY_R_MAX_PPB)
.unwrap_or(u64::MAX)
.checked_mul(2)
.unwrap_or(u64::MAX)
/ SIGMOID_PRECISION
}
pub const DECAY_R_MAX_PPB: u64 = 1_500_000_000; // 150% per year at max concentration
pub const SIGMOID_HALF: u64 = 500_000_000; // 0.5 in fixed-point
pub const SIGMOID_PRECISION: u64 = 1_000_000_000;Decay Rate Interpretation
| Concentration | % of Supply | Approx. Annual Decay |
|---|---|---|
| Below threshold | < 0.1% | 0% — no decay |
| At threshold (1,000,000 PPB) | 0.1% | ~0% — minimal |
| 1% of supply (10,000,000 PPB) | 1% | ~15% / year |
| 5% of supply (50,000,000 PPB) | 5% | ~120% / year |
| 10%+ of supply | 10%+ | ~150% / year (max) |
Effective Value
The effective value of a UTXO is its nominal value reduced by accrued decay. Decay accrues linearly with the number of blocks the UTXO has been held.
// effective_value = nominal × (1 - decay_rate × blocks_held / DECAY_PRECISION)
//
// DECAY_PRECISION = 10_000_000_000 (10^10)
// All arithmetic uses checked u64 — no floating point.
pub fn effective_value(
nominal: u64,
decay_rate_ppb: u64,
blocks_held: u64,
) -> u64 {
let decay_factor = decay_rate_ppb
.checked_mul(blocks_held)
.unwrap_or(u64::MAX);
let decay_amount = nominal
.checked_mul(decay_factor)
.unwrap_or(u64::MAX)
.checked_div(DECAY_PRECISION)
.unwrap_or(nominal);
nominal.saturating_sub(decay_amount)
}
pub const DECAY_PRECISION: u64 = 10_000_000_000; // 10^10When a UTXO is spent, the difference between nominal and effective value flows to the decay pool. Coin selection in the wallet prefers high-decay UTXOs to minimize ongoing decay losses.
Lineage Decay
In addition to concentration-based decay, RillCoin tracks lineage — how long value has been concentrated in the same cluster lineage. Lineage decay adds a secondary pressure that grows with holding duration, independent of concentration.
| Constant | Value | Description |
|---|---|---|
LINEAGE_HALF_LIFE | 52,596 blocks | ~100 days — lineage weight halves |
LINEAGE_FULL_RESET | 525,960 blocks | ~1 year — lineage resets to zero |
Lineage is tracked per cluster. When a UTXO is spent and a new output is created in the same cluster, the lineage counter increments. Lineage resets to zero after LINEAGE_FULL_RESET blocks without activity, encouraging periodic economic participation.
Decay Pool
All decayed amounts flow into a global decay pool. The decay pool is a persistent balance maintained in RocksDB, credited atomically when UTXOs are spent.
Each block, a fraction of the decay pool is released to the block miner:
decay_pool_release = decay_pool_balance × DECAY_POOL_RELEASE_BPS / 10_000
where DECAY_POOL_RELEASE_BPS = 100 (1% per block)This 1% per block release creates a smooth, continuous flow of redistributed wealth to miners rather than a sudden dump. A pool of 100,000 RILL would release 1,000 RILL to the next block's miner.
The decay pool release is included in the coinbase transaction output value and subject to the same COINBASE_MATURITY = 100 block maturity requirement.
Example: Full Decay Calculation
Suppose a cluster holds 1,000,000 RILL (100,000,000,000,000 rills) and the circulating supply is 10,000,000 RILL (1,000,000,000,000,000 rills). The cluster holds 10% of supply.
1. Concentration PPB:
cluster_balance = 100_000_000_000_000 rills (1,000,000 RILL)
supply = 1_000_000_000_000_000 rills (10,000,000 RILL)
concentration = 100_000_000_000_000 × 1_000_000_000 / 1_000_000_000_000_000
= 100_000_000 PPB (10% of supply)
2. Decay threshold check:
100_000_000 PPB > 1_000_000 PPB (threshold) → proceed
3. Sigmoid lookup:
x = 100_000_000 × 1_000_000_000 / 1_000_000 = 100_000_000_000
(well above table maximum → clamp to SIGMOID_TABLE.last())
sigmoid(x) ≈ 999_664_600 (in fixed-point × 10^9)
4. Decay rate:
shifted = 999_664_600 - 500_000_000 = 499_664_600
rate = 499_664_600 × 1_500_000_000 × 2 / 1_000_000_000
≈ 1_498_993_800 PPB/year
5. Effective value after 1 block (525,960 blocks/year):
decay_rate_per_block = 1_498_993_800 / 525_960 ≈ 2_849 PPB
decay_amount = nominal × 2_849 / 10_000_000_000
For a 1,000,000 RILL UTXO held 1 block:
decay = 100_000_000_000_000 × 2_849 / 10_000_000_000 ≈ 28_490 rills
= 0.00028490 RILL per block