Remove Floating Point from Runtime
- Idea
- Draft
- Review
- Accept
- Implement
- Active
Decision brief
Why this proposal matters
Replace floating-point arithmetic in consensus-critical inflation and rent calculations with deterministic integer arithmetic.
Proposal at a glance
What changes
- Scaled rate: A fractional rate represented as an integer with the fixed denominator RATE_SCALE.
- Decay-per-slot: An integer factor with denominator DECAY_SCALE = 2^64, used to apply one slot of inflation decay.
- Anchor rate: The inflation rate from which subsequent decay is calculated.
Stakeholder map
Who is affected
Builders & client teams Medium impact
No transaction or program interfaces are changed.
Action requirement unknownValidators & operators Medium impact
Validator implementations must use the specified arithmetic and rounding rules after feature activation. Differences from the previous floating-point calculation may occur at rounding boundaries.
Action may be requiredUsers & stakers Medium impact
No transaction or program interfaces are changed.
Action requirement unknownGovernance & ecosystem Impact unknown
No proposal-specific evidence was found for this group.
Action requirement unknownExact source revision
Full proposal document
2be4cb0c0fd1Summary
Replace floating-point arithmetic in consensus-critical inflation and rent calculations with deterministic integer arithmetic.
Motivation
Inflation rewards and rent parameters affect capitalization, account state, and
bank hashes. These values must be computed identically by all validator clients,
without relying on IEEE-754 behavior or platform math-library functions such as
powf.
Alternatives Considered
General-purpose rational arithmetic
Use a general-purpose rational type for all fractional calculations. This provides exact arithmetic but adds unnecessary complexity and overhead compared with the bounded integer representation proposed here.
New Terminology
No new terminologies, to clarify few terms used here:
- Scaled rate: A fractional rate represented as an integer with the fixed denominator RATE_SCALE.
- Decay-per-slot: An integer factor with denominator
DECAY_SCALE = 2^64, used to apply one slot of inflation decay. - Anchor rate: The inflation rate from which subsequent decay is calculated.
Detailed Design
Protocol percentages and ratios are represented as:
Fraction { numerator: u64, denominator: nonzero u64 }
Multiplication by a fraction is defined as:
mul_fraction_floor(value, fraction) = floor(value * fraction.numerator / fraction.denominator)
The multiplication MUST use a widened intermediate at least 128 bits wide, or an exactly equivalent implementation. Rounding MUST be toward zero.
Rust implementation:
struct Fraction {
numerator: u64,
denominator: NonZeroU64,
}
fn mul_fraction_floor(value: u64, fraction: Fraction) -> u64 {
let product: u128 =
u128::from(value) * u128::from(fraction.numerator);
let quotient: u128 =
product / u128::from(fraction.denominator.get());
// Integer division discards any remainder; no round-to-nearest is applied.
u64::try_from(quotient).expect("protocol constants keep the result in u64")
}
Inflation Rewards
The scaled constants below are the integer protocol representation of the existing inflation schedule. Current validator implementations may store these values as floating point, but after activation clients MUST use the integer representation for consensus calculations.
RATE_SCALE = 1_000_000_000_000_000 YEAR_NS = 31_556_925_993_600_000
INITIAL_RATE = 80_000_000_000_000 // existing 0.08 initial inflation TERMINAL_RATE = 15_000_000_000_000 // existing 0.015 terminal inflation PICO_RATE = 100_000_000_000 // existing 0.0001 pico inflation
TAPER_15 = Fraction { numerator: 15, denominator: 100 } TAPER_30 = Fraction { numerator: 30, denominator: 100 }
Taper numerators and denominators MUST fit in u32. Clients MUST validate this
before converting wider stored values.
Each slot-time regime supplies its integer ns_per_slot.
Epoch inflation rewards are computed as:
annual_reward = floor(capitalization * validator_rate / RATE_SCALE)
epoch_reward = floor(annual_reward * slots_in_epoch * ns_per_slot / YEAR_NS)
Clients MUST perform both flooring operations in the order shown.
Inflation calculations MUST return an error on arithmetic overflow, including
overflow of intermediate products, conversion of rewards to u64 lamports, or
addition of rewards to capitalization. A validator MUST panic if an inflation
calculation returns an error. Clients MUST NOT wrap, saturate, or substitute a
decay factor or reward amount on error.
Feature Activation and Reward Boundary
If the feature activates on epoch E -> E+1, clients MUST use integer
calculations starting with rewards earned in epoch E+1, paid during epoch E+2.
Rewards earned in epoch E, paid during epoch E+1, MUST use the legacy
calculation. Under Alpenglow, these rewards have already accrued during epoch E
and MUST NOT be recalculated or converted at activation. At activation, clients
MUST set the stored Inflation.initial to 0.08 and use INITIAL_RATE for
subsequent inflation calculations on every cluster, including testnet.
Inflation Decay
Floating-point exponentiation is replaced with deterministic integer decay.
Clients MUST derive the per-slot factor from the configured ns_per_slot and
taper fraction using the algorithm below. No per-duration decay table is
required.
This also accommodates short slot durations used by tests and local validators
without requiring additional decay constants.
The calculation uses two scales:
RATE_SCALE = 10^15: Initial, terminal, anchor, and calculated validator rates.DECAY_SCALE = 2^64: Per-slot and accumulated decay factors, and intermediate logarithm and exponential approximations.
Each integer represents its value multiplied by the corresponding scale;
dividing by that scale recovers the represented value. For example, an annual
rate of 8% is stored as 80_000_000_000_000 at RATE_SCALE.
Annual rates retain their existing decimal representation at RATE_SCALE.
Decay factors remain in [0, DECAY_SCALE], where DECAY_SCALE represents 1.0.
The finer DECAY_SCALE limits the error amplified by exponentiating a factor
close to 1 over many slots. Factor derivation uses the same scale, so no scale
conversion is needed before accumulating decay.
Derivation of the per-slot factor
The mathematical quantity approximated is:
DECAY_SCALE * (1 - taper)^(ns_per_slot / YEAR_NS)
= DECAY_SCALE * exp(-x)
x = -ln(1 - taper) * ns_per_slot / YEAR_NS
YEAR_NS = 31_556_925_993_600_000
With u32 logarithm inputs widened to u128, the logarithm arithmetic cannot
overflow at DECAY_SCALE. For the 15% and 30% tapers, the truncation error of the
32-term logarithm series is smaller than one scale unit. The exponential
uses a cubic approximation because the per-slot exponent is small; the quadratic
and cubic terms account for the leading corrections to linear decay.
Clients MUST reproduce the results of the following integer algorithm, including its term counts and division positions. Clients MAY use an equivalent implementation if it produces the same result or failure for every input. The mathematical formula above describes the approximation target; it does not require a correctly rounded transcendental result for every possible input.
All values in the Rust implementation are unsigned. Division discards the remainder. Logarithm inputs MUST be widened before arithmetic. Clients MUST NOT use floating-point operations to derive the factor, including as a fallback.
const DECAY_SCALE: u128 = 1 << 64;
const YEAR_NS: u128 = 31_556_925_993_600_000;
// Approximates DECAY_SCALE * ln(numerator / denominator).
fn approximate_logarithm(numerator: u32, denominator: u32) -> Option<u128> {
if denominator == 0 || numerator < denominator {
return None;
}
let numerator = u128::from(numerator);
let denominator = u128::from(denominator);
// Represent z = (numerator - denominator) / (numerator + denominator)
// as a fraction. Then (1 + z) / (1 - z) = numerator / denominator,
// so ln(numerator / denominator) = 2 * (z + z^3/3 + z^5/5 + ...).
let z_num = numerator.checked_sub(denominator)?;
let z_den = numerator.checked_add(denominator)?;
let z_num_squared = z_num.checked_mul(z_num)?;
let z_den_squared = z_den.checked_mul(z_den)?;
// Initially, term is z scaled by DECAY_SCALE.
let mut term = DECAY_SCALE.checked_mul(z_num)? / z_den;
let mut sum = 0u128;
for i in 0..32 {
sum = sum.checked_add(term / (2 * i + 1))?;
if i < 31 {
// Multiply by z^2 to advance to the next scaled odd power.
term = term.checked_mul(z_num_squared)? / z_den_squared;
}
}
sum.checked_mul(2)
}
// Approximates DECAY_SCALE * exp(-x / DECAY_SCALE), including the leading 1.
fn approximate_exponential(x: u128) -> Option<u128> {
let second = x.checked_mul(x)? / (2 * DECAY_SCALE);
let third = second.checked_mul(x)? / (3 * DECAY_SCALE);
let decrement = x.checked_sub(second)?.checked_add(third)?;
DECAY_SCALE.checked_sub(decrement)
}
fn decay_per_slot(ns: u128, taper_num: u32, taper_den: u32) -> Option<u128> {
if ns == 0 || taper_den == 0 || taper_num >= taper_den {
return None;
}
// -ln(1 - taper) = ln(taper_den / (taper_den - taper_num)).
let annual_exponent =
approximate_logarithm(taper_den, taper_den.checked_sub(taper_num)?)?;
let slot_exponent = annual_exponent.checked_mul(ns)? / YEAR_NS;
approximate_exponential(slot_exponent)
}
None denotes a calculation error due to invalid inputs or failure of checked
arithmetic. Clients MUST propagate this error to the validator, which MUST panic
as specified above. Derivation is expected to succeed for the protocol's chosen
slot durations and tapers.
Accumulating decay
For factors in [0, DECAY_SCALE], scaled multiplication is
floor(a * b / DECAY_SCALE). Multiplication by the identity MUST be handled
without evaluating DECAY_SCALE * DECAY_SCALE, which exceeds u128.
Products of two factors strictly below DECAY_SCALE fit in u128.
Decay over n slots MUST use the following right-to-left binary exponentiation.
Clients MUST preserve this association order and floor after each
multiplication.
fn mul_decay_floor(a: u128, b: u128) -> u128 {
if a == DECAY_SCALE {
return b;
}
if b == DECAY_SCALE {
return a;
}
(a * b) >> 64
}
fn pow_decay_floor(mut base: u128, mut n: u64) -> u128 {
let mut result = DECAY_SCALE;
while n > 0 {
if n & 1 != 0 {
result = mul_decay_floor(result, base);
}
n >>= 1;
if n > 0 {
base = mul_decay_floor(base, base);
}
}
result
}
For a constant slot duration, the annual validator rate, expressed in
RATE_SCALE units, is:
factor = decay_per_slot(ns_per_slot, taper.numerator, taper.denominator)
decay_since_anchor = pow_decay_floor(factor, slots_since_anchor)
rate = max(TERMINAL_RATE,
floor(anchor_rate * decay_since_anchor / DECAY_SCALE))
The rate calculation MUST use a u128 intermediate or an exactly equivalent
implementation. anchor_rate is in [0, RATE_SCALE], so this product fits in
u128; division by DECAY_SCALE is equivalent to shifting right by 64 bits.
Clients MUST NOT round the per-slot factor or accumulated decay to RATE_SCALE
before applying it to the anchor rate.
Before SIMD-0550 activation, anchor_rate = INITIAL_RATE and the taper is 15%.
At SIMD-0550 activation, clients MUST calculate the activation anchor rate using
the preceding integer schedule, including its terminal clamp. This anchor
remains in RATE_SCALE units. Clients then apply the decay factor derived for
the 30% taper from the activation slot forward. At zero elapsed slots, decay is
exactly DECAY_SCALE, preserving the integer anchor rate without a
discontinuity.
Rent
SIMD-0194 has already deprecated the rent exemption threshold on mainnet-beta since epoch 943.
Backwards Compatibility
This change is consensus-affecting and requires feature activation at an epoch boundary.
Impact
No transaction or program interfaces are changed.
Validator implementations must use the specified arithmetic and rounding rules after feature activation. Differences from the previous floating-point calculation may occur at rounding boundaries.
Expected epoch reward drift
The following comparison evaluates each integer schedule against the current floating-point calculation. Floating point is a comparison baseline, not an exact mathematical reference.
year = epoch_start_slot / slots_per_year_f64
legacy_rate = max(0.015, 0.08 * (1 - taper)^year)
legacy_reward = trunc(legacy_rate * capitalization * slots_in_epoch
/ slots_per_year_f64)
decay = pow_decay_floor(decay_per_slot, slots_since_anchor)
validator_rate = max(TERMINAL_RATE,
floor(anchor_rate * decay / DECAY_SCALE))
annual_reward = floor(capitalization * validator_rate / RATE_SCALE)
epoch_reward = floor(annual_reward * slots_in_epoch * ns_per_slot / YEAR_NS)
For a normalized capitalization of 1,000,000,000,000,000,000 lamports,
8% initial inflation,
432,000-slot epochs, and the 400ms, 350ms, 300ms, 250ms, and 200ms slot-time
regimes, the largest observed absolute epoch reward differences over 6000 epochs
(epoch indices 0 through 5999) are shown below. Capitalization, taper, and slot
duration are held constant within each schedule. Both integer approaches retain
RATE_SCALE = 10^15 for annual rates and use the same two reward floors.
Differences are in lamports:
| Taper | Original 10^15 decay | Proposed 2^64 decay |
|---|---|---|
| 15% | 31,831,625 | 5,923 |
| 30% | 14,181,312 | 1,061 |
The largest observed difference in total epoch rewards from the floating-point
calculation was 5,923 lamports. The observed relative difference,
abs(integer_epoch_reward - legacy_reward) / legacy_reward, is below
7.03e-11 for both tapers. These are measured results for the comparison above,
not universal error bounds.
Using DECAY_SCALE = 2^64 reduces rounding error in the per-slot factor and
accumulated decay while retaining the existing annual-rate representation.
Deriving the factor from
ns_per_slot also permits other slot durations without adding decay-table
entries.
Security Considerations
These calculations affect consensus-critical state. Implementations must use the specified arithmetic widths, constants, and rounding rules to avoid cross-client divergence.
Intermediate arithmetic must not overflow, and protocol constants must not be recomputed using floating-point arithmetic at runtime.
Sequencing with SIMD-0550
This feature must be activated before or at the same epoch boundary as SIMD-0550. When SIMD-0550 activates, clients compute the SIMD-0550 activation anchor rate using pre-SIMD-0550 integer 15% taper schedule, then apply the integer 30% taper from the SIMD-0550 activation slot forward.
Conformance
Conformance tests MUST include:
- epoch reward rounding vectors
- terminal-rate clamping
- decay test cases with exact expected results for every protocol slot-time regime and taper
- SIMD-0550 activation anchoring
- activation at E -> E+1 with legacy rewards for E paid during E+1 and integer rewards for E+1 paid during E+2
- replay across activation with matching bank hashes
Evidence graph
Related proposals and rollout
One or more sources are unavailable.
Upstream review record
Upstream discussion & review
One or more sources are unavailable.
Provenance
Evidence & technical details
Rollout or chain data, source revisions, freshness and integrity. 3
Provenance
Evidence & technical details
Rollout or chain data, source revisions, freshness and integrity.SIMD
Deployment Status
Feature Gate not yet created
Exact source revision
Sources & integrity
- Proposal document pinned_commit_blob
2be4cb0c0fd1025df8c037b19372f7346119b231 - simd-document document · stale · Oct 29, 2026
2be4cb0c0fd1025df8c037b19372f7346119b231
One or more sources are unavailable.
One or more sources are unavailable.
simd.watch community discussion · SIMD-0607
Powered by Giscus · Sign in with GitHub to comment
Community comments load when this section approaches the viewport.