How rewards work

Every block reward is split fairly by accepted work.

Every reward allocation is funded by a real block — and weighted by what your shares were actually worth. No reserves. No round cliffs. No hopping advantage.

The model, in one minute.

icemining uses Kernel-PPLNS — a no-reserve revenue-share model built on Rosenfeld's unit-based framework. Three properties matter most.

Block-funded

Every reward comes from a confirmed block.

There is no reserve, no PPS smoothing, no IOU queue. When a block matures, that block's reward funds the allocation for the work that contributed to it. Conservation is enforced exactly, per block.

Work-measured window

The window is expected blocks of work — not a count of shares.

Your share takes up window space proportional to your assigned difficulty divided by the live network difficulty. A difficulty-1000 share carries 1000× the weight of a difficulty-1 share. Vardiff doesn't distort what you earn.

Smooth ramp & smooth tail

Newer work counts more, with no hard cliff.

Shares are weighted with a smooth, normalized truncated-exponential kernel — the work closest to the block earns the most, older work fades gracefully. No abrupt drop when you join, no abrupt drop when you leave.

Show the formulas

1. Share work units. Each accepted share contributes work units based on the difficulty the pool assigned to it and the live network difficulty at the moment of acceptance.

ui=qisiDi

where s_i is the assigned share difficulty, D_i is the network difficulty at acceptance, and q_i is an optional reward-eligibility factor.

2. Window sizing. The window covers the last L expected blocks of work, chosen so that the probability of seeing at least one block in the window meets a target coverage:

Pr[no block in N expected blocks]=eNNcov=ln(1target_coverage)

With target_coverage = 0.999, this gives N_cov ≈ 6.908 expected blocks of window width.

3. Truncated-exponential kernel. The production kernel — newer work counts more, with a smooth tail that fades to zero at L:

kτ,L(x)={ex/ττ(1eL/τ)if 0xL0otherwise

The denominator 1 − e^(−L/τ) is the normalization constant that keeps the kernel a proper probability density. Production ratio: L = 4τ.

4. Per-block conservation. Every block's reward allocations come from that block's confirmed reward, with conservation enforced exactly:

ibmineri+ibfeei+dustb=Rbmatured

The right-hand side is the actual confirmed block reward for block b. The left-hand side sums all per-share miner allocations miner_i, the per-share pool fee fee_i, and any remaining dust. Normal non-empty allocations redistribute remainder atoms before this check, so dust is zero and the two sides are equal — bit-exact.

How common pool reward models compare.

Common PPLNS variants often become unfair when block rewards, network difficulty, pool hashrate, or vardiff change unless they account for share difficulty and define the window in work units. Kernel-PPLNS makes that unit explicit: expected blocks of accepted work.

Upstream NOMP built-in PROP

Round-based proportional reward processor.

  • Not PPLNS: the denominator is the current block round, then resets after a found block.
  • Round luck changes value timing: short rounds concentrate reward; long rounds dilute every round share.
  • Raw-share PROP has the same vardiff hazard unless share difficulty is explicitly weighted.
  • Simple and fast, but round boundaries expose ordinary proportional pool-hopping behavior.

Fixed-count PPLNS-N

Last N accepted shares are eligible.

  • If N counts raw shares, diff-1 and diff-1000 shares consume the same slot.
  • N shares is not a stable amount of expected-block work when vardiff or network difficulty changes.
  • Low assigned difficulty can occupy more slots without proportional work unless shares are diff-weighted.
  • Hard cutoff: shares older than the Nth most recent contribute zero.

Fixed-time PPLNS-T

Last T seconds or hours are eligible.

  • Wall-clock time is not work; expected-block coverage drifts with pool hashrate and network difficulty.
  • Block luck changes how often rewards mature, not how much expected work T seconds contains.
  • Raw-count implementations still need difficulty weighting, or vardiff over-weights lower-diff workers.
  • Hard cutoff at the time boundary creates a join/leave step.

Kernel-PPLNS

Window measured in expected blocks of work.

  • Share width equals assigned difficulty divided by network difficulty.
  • Vardiff becomes more, smaller intervals or fewer, larger intervals: same proportional work.
  • Window length is expected-block work, sized by public coverage and ramp policy.
  • Truncated-exponential weighting fades smoothly instead of dropping at a boundary.
  • No reserve. Every credited coin comes from a confirmed, matured block.

Ramp-up and ramp-down — no cliffs.

When a miner joins, their share of the next block grows smoothly as their work fills the kernel window. When they leave, their share fades the same way. Fixed-count PPLNS-N steps in a single jolt; Kernel-PPLNS curves.

Chart 01 · Ramp behavior

Your share of a found block, over time in the pool.

x: time since join (expected blocks of work) · y: share of next block
Kernel-PPLNS smooth curve vs PPLNS-N trapezoid ramp.

The cyan curve is Kernel-PPLNS. The dashed amber line is fixed-count PPLNS-N. Same hashrate, same time in pool — but the smooth kernel spreads the transition over the window length L instead of snapping. No cliff at join, no cliff at leave.

The kernel decides how recent work is weighted.

Each share inside the window earns weight from a smooth, normalized truncated-exponential kernel. Work close to the block earns the most; older work fades gracefully toward the tail at L.

Chart 02 · Kernel shape

Truncated-exponential — newer work earns more.

x: share age (work units back from block) · y: kernel weight k(x)
Truncated exponential kernel shape.

The cyan curve is the production kernel kτ,L(x). Roughly 63% of the kernel mass sits inside the first τ of work — meaning the most recent block-time of work earns the bulk of the reward. At L = 4τ the remaining weight is only about 1.8% of the peak, then the bounded-support kernel cuts to zero.

Low block rates use a wall-clock ramp cap.

The target coverage window is measured in expected blocks of work, but slow reward scopes should not force week-long ramps. A published wall-clock cap reduces L when cadence is low, trading some coverage for faster, transparent convergence.

Chart 03 · Ramp time vs pool block rate

Same policy, slow scopes clamp the window.

x: wall-clock days · y: miner's share of next block ÷ fair share
Three lines on a single wall-clock axis with a 3-day ramp cap. A 1-block/day pool is capped to a 3-day ramp with lower effective coverage; 10-block/day and 100-block/day pools keep the full 99.9% coverage target and ramp much faster.

A single miner joins at day 0 and leaves at day 10. The target is 99.9% coverage (N ≈ 6.9 expected blocks), but the published max ramp is 3 days. A 1-block/day scope is therefore capped at L = 3.0, giving about 95% effective coverage and a 3-day full-tail ramp. Faster scopes keep the full target: 10 blocks/day ramps in 16.6 hours, and 100 blocks/day in 1.7 hours. This is the fair tradeoff: shorter ramp, higher variance, disclosed up front. The cap is a fixed public policy, not a round-timing signal.

Same expected reward at every scale and every difficulty.

Two miners with very different scales — a single small rig and a whale running many — and very different assigned share difficulties. Simulated head-to-head, both end up at the same fair reward per unit of work. The whale's curve is nearly flat from day one; the small miner's wobbles, then converges.

Chart 04 · Fairness

Tiny miner vs whale — 30 days, ~28,800 blocks.

x: days of mining · y: realized reward ÷ fair reward
batch 1 / 8 · seed 7
Realized reward / fair reward over 30 simulated days. Whale converges to 1.00 within hours; tiny miner wobbles between 0.7 and 1.3 in the first week, settling to 1.00 by day 30.

Both lines converge to 1.00exactly the fair reward. Each seed batch is one independent 30-day Monte Carlo run. The whale stays essentially flat from day one; the tiny miner may finish a little above or below fair, which is normal variance over 28,800 pool blocks.

The tiny miner wobbles more during week one because they have only a handful of shares inside the kernel window at any moment, so each block's small reward allocation is bumpier. The whale has thousands of shares in-window so each block's reward allocation is essentially deterministic. Both effects average out over thousands of blocks — and crucially, both miners converge to the same expected reward per unit of work, regardless of scale or share difficulty.

What that looks like for an actual miner.

Same three pool block rates, same ramp cap, same kernel policy — now with a real miner running through a Monte Carlo simulation. The expected reward is identical at every rate; what changes is how long it takes the realized line to look like the expectation.

Chart 05 · Realized rewards vs pool block rate

5% miner, 30 days, three pool rates.

x: wall-clock days · y: realized cumulative reward ÷ fair reward
batch 1 / 8 · seed 11
Three Monte Carlo trajectories on a single 30-day wall-clock axis. The 1-block/day pool's line still moves visibly because only about 30 pool blocks land in the window. The 10-block/day line settles near fair over the month. The 100-block/day line is essentially flat at 1.00× from hour two.

All three lines target 1.00× fair in expectation — Kernel-PPLNS credits the same expected rate regardless of pool cadence. The miner earns a smooth fraction of every block via the kernel; what changes between pools is how many blocks the pool finds during the window. Over 30 days that's ~30 pool blocks on a 1-block/day chain (few events to average over — the line still moves visibly), ~300 on a 10-block/day chain (settles near fair over the month), and ~3,000 on a 100-block/day chain (effectively flat from hour two). The same wobble applies whether you're 1% or 25% of the pool — bumpiness is set by event count, not your hashrate. Slow chains mean fewer pool blocks per month; fast chains mean smoother realized rewards. Plan for the cadence you're mining.

How smooth your monthly rewards look, by pool cadence.

Variance is what you actually feel — "is this month within 5% of fair, or 50%?" In a kernel-PPLNS pool, variance comes from the random count of pool blocks during your observation window. Faster pool = more blocks = smoother rewards. The same for any miner size, since you receive a smooth fraction of every block instead of all-or-nothing.

Chart 06 · Variance vs pool block rate

How bumpy are your rewards, by pool cadence.

x: pool block rate (log) · y: typical reward swing, ± % from fair
Four downward-sloping curves on a log-scale x-axis showing typical reward swing for different observation windows. Faster pools and longer windows both shrink the swing. At a 100-block/day pool, a typical month is within ±2% of fair; at 1 block/day, ±18%.
Where these curves come from (math)

"Typical swing" plotted on the chart is one standard deviation of cumulative reward expressed as a percent of fair. About two out of three observation windows land inside ± that swing.

Let r be the pool block rate (blocks per day), T the observation window (days), h the miner's hashrate fraction of the pool, and R the block reward. The number of pool blocks N in the window is Poisson-distributed:

NPoisson(Tr)

In Kernel-PPLNS, each block credits the miner a smooth fraction h of the reward — the kernel averages over many in-window shares, so each block's allocation is close to deterministic. Cumulative reward over N blocks is therefore:

PhRN

All randomness in P comes from the random count N. For a Poisson, mean and variance both equal T·r, so:

Var(P)=(hR)2Var(N)=(hR)2Tr

The chart's curves are the coefficient of variation — standard deviation divided by mean, then expressed as a percent:

CV=Var(P)E[P]=1Tr

Notice h cancels out: the swing is the same for every miner, regardless of hashrate fraction. That's because Kernel-PPLNS credits a smooth fraction of every block — there's no all-or-nothing block luck per miner, only across the pool as a whole.

Verified empirically by Monte Carlo (60 trials per row, with explicit Poisson block-count sampling, 5%-of-pool miner). Measured swing matches the analytical formula within ~20% — Kernel-PPLNS is slightly smoother than the pure-Poisson approximation because per-block share-count averaging reduces variance further:

pool ratewindowanalyticmeasured
1 / day30 days18.3%16.1%
10 / day30 days5.8%4.9%
100 / day30 days1.8%1.9%
10 / day7 days12.0%9.7%

Each curve shows the typical swing in your reward vs fair — about two out of three months will land inside that ± band, half over and half under. On a 100-block/day pool, a typical month is within ±2% of fair — reward flow is rock steady. On a 1-block/day chain it's ±18%, so some months you'll come in well over fair and some well under (it averages out, but month-to-month is bumpy).

Two practical takeaways: (1) the swing is the same whether you're 1% or 25% of the pool — every miner gets a smooth fraction of every block, so size doesn't change your variance; and (2) mining longer always smooths things out — three months on a 10-block/day pool feels as steady as one month on a 100-block/day pool.

Your work earns — coming or going.

Comparing a hopper to a full-time miner is unfair: the hopper mined less, so of course they earned less. The honest test is each miner's realized reward divided by what they should earn given the hours they actually mined. In Kernel-PPLNS that ratio should land near 1.00 for fixed schedules — no visible systematic edge for ordinary on/off hopping.

Chart 07 · Hopping resistance

Realized reward ÷ fair-for-time-mined.

x: wall-clock days · y: each miner's realized reward ÷ their fair reward
batch 1 / 8 · seed 11
Four lines on a 28-day wall-clock axis: a steady miner, an early-shift hopper (mornings only), a late-shift hopper (evenings only), and a weekend-only hopper. All four lines hover near 1.00× — every miner earned within normal noise of their fair share for the time they actually mined.

Four miners with the same 5%-of-pool hashrate when present, but very different schedules: a steady miner (28 days), early-shift hopper (mornings only, 14 days), late-shift hopper (evenings only, 14 days), and a weekend-only hopper (8 days). Each line is the average of 40 independent simulations — a single seed shows ±10% noise from block luck (see Chart 06), so we average across many to isolate the hopping effect from variance.

Across 8 independent pre-rendered seed batches (try the Next seed batch button above), every schedule's mean lands within about 1% of fair — no systematic edge for any hopping pattern. The simulation runs from steady-state prehistory and backfills the aggregate background when a hopper is off, so the pool's expected hashrate stays constant. Fair is computed as the kernel-weighted expected payout given each miner's actual on/off schedule, so ramp-tail rewards are attributed to the right shift.

Per-trial spread differs by schedule — the weekend hopper shows ±4% per single batch vs ±2% for steady — exactly as Chart 06's variance formula predicts: fewer mining days equals fewer pool blocks to average over. That's not a hopping penalty; it's the same luck-variance every miner faces. Fixed schedules do not show a systematic payout edge here.

Hopping fairness check (math and audit)

Let a(t) be a fixed on/off schedule: 1 while the miner is submitting shares, 0 while away. For a block at wall time t, the kernel-weighted active fraction is:

A(t)=0Lk(u)a(tu/r)du

Here r is the pool block rate in blocks per day, u is age in expected-block work units, and k(u) is the normalized payout kernel.

In the continuous-hashrate limit, a miner with on-time hashrate fraction h contributes expected kernel mass hA(t). If the background fills the missing hashrate when the hopper is off, total expected kernel mass remains 1, so the fair per-block share is:

fair(t)=RhA(t)hA(t)+1hA(t)=RhA(t)

The subtle part is that the simulator uses finite Poisson shares, and actual payout uses a ratio. In general, E[g/G] is not exactly E[g]/E[G]. The audit mode estimates the nonlinear ratio directly:

ρ=bE[gbgb+Gbother]bhA(tb)
scheduleaudit ratiokernel activeinterpretation
steady0.9995060.999997no premium
early0.9995600.500307no premium
late0.9995060.499690no premium
weekend0.9994270.283923no premium

Reproduce with uv run scripts/reward_graphs/hopping.py --audit --audit-samples 100000 --audit-bins 512. Ratios are slightly below 1 because finite share granularity leaves a small denominator effect; the important result is that early shift is not above late shift in the direct E[g/G] audit.

The shaded zone on the chart is for the plotted mean of 40 simulations, not for one simulation. For schedule j in a seed batch:

SEMj=sj40,band1σ=maxjSEMj

The 2σ band is twice that value. This is why the shaded zone is much narrower than the legend's ±SD number: the legend shows one-trial spread, while the plotted line averages 40 independent trials.

Every block's reward is allocated from that block alone.

Per-block conservation is enforced exactly: miner reward allocations plus the pool fee plus any remaining dust equals the actual matured block reward. Remainder atoms are assigned before closure, so normal allocation dust is zero.

Chart 08 · Block-funded conservation

Per-block accounting closes — to the atom.

block reward: 1,000,000,000 atoms · pool fee: 1.00% · largest-remainder atom redistribution
A block-conservative XELIS reward flow for block 1234. The block pays 1,000,000,000 atoms, payout keys are floored, deterministic largest-remainder atoms are redistributed, and normal allocation dust is zero.
Show related code

The allocator computes each payout key's floored gross and fee amounts plus their exact integer remainders. It then pays the missing whole atoms by deterministic largest-remainder order before the final conservation check. A successful non-empty allocation distributes the full daemon-credited reward, so normal dust is zero.

/// Allocation result for one block. Conservation MUST hold:
/// `Σ miner_atomic + Σ fee_atomic + dust_atomic == actual_reward_atomic`.
pub struct BlockAllocation {
    pub dust_atomic: u128,
    pub total_miner_atomic: u128,
    pub total_fee_atomic: u128,
}

// For every payout key K, before integer-atom flooring:
//
//   exact_gross_K = actual_reward_atomic * G_K / G_b
//   exact_fee_K   = actual_reward_atomic * F_K / (G_b * 10_000)
//
// The helper returns floor(exact) and the exact U256 remainder in one
// stack-only U512/U256 division. The remainder is the payout ranking key.
//
for (key, agg) in per_key {
    let (gross_atomic_u256, gross_remainder) =
        u256_mul_div_floor_rem(&reward_atomic_u256, &agg.g_k_q128, &g_b)?;
    let gross_atomic = u256_to_u128_saturating(&gross_atomic_u256);

    let (fee_atomic_u256, fee_remainder) =
        u256_mul_div_floor_rem(&reward_atomic_u256, &agg.f_k_q128, &denom_g_b_times_10000)?;
    let fee_atomic = u256_to_u128_saturating(&fee_atomic_u256);

    payouts.push(SettlementScratch {
        key,
        gross_atomic,
        fee_atomic,
        gross_remainder,
        fee_remainder,
        gross_incremented: false,
    });
}

// Floor residuals are whole atoms still owed by this block:
//
//   actual_reward_atomic == Σ floor(exact_gross_K) + gross_residual
//   0 <= gross_residual < payout_key_count
//
// With 10,000 payout keys there can be up to 9,999 residual atoms, but
// they are paid out here, not left as block dust.
let fee_residual = fee_target - total_fee_floor;
distribute_fee_remainder(&mut payouts, fee_residual);

let gross_residual = actual_reward_atomic - total_gross_floor;
distribute_gross_remainder(&mut payouts, gross_residual);

// After redistribution, each key's miner+fee equals its final gross.
for p in payouts {
    if p.fee_atomic > p.gross_atomic {
        return Err(AllocatorError::ConservationViolated {
            got: p.fee_atomic,
            expected: p.gross_atomic,
        });
    }
    let miner_atomic = p.gross_atomic - p.fee_atomic;
    total_miner = total_miner.saturating_add(miner_atomic);
    total_fee = total_fee.saturating_add(p.fee_atomic);
}

let distributed = total_miner.saturating_add(total_fee);
if distributed != actual_reward_atomic {
    return Err(AllocatorError::ConservationViolated {
        got: distributed,
        expected: actual_reward_atomic,
    });
}

// What this guarantees:
//
//   Σ miner_atomic + Σ fee_atomic == actual_reward_atomic
//   dust_atomic == 0
//
// The residual bound still scales with payout-key count, but residual
// atoms are deterministically assigned instead of being swept as dust.
let dust_atomic = 0;

Source: crates/pplns-engine/src/allocator.rs

One confirmed block, expanded as an atom-level ledger. The matured block reward is exactly 1,000,000,000 atoms: miner reward allocations (cyan, 990,000,000 atoms) plus the 1% pool fee (green, 10,000,000 atoms). Deterministic largest-remainder rounding pays the whole residual atoms to payout keys, so normal allocation dust is 0 atoms. The closure error is 0 atoms, bit-exact.

The important distinction is residual versus dust. With 10,000 distinct payout keys, the initial gross floors can leave as many as 9,999 whole atoms unresolved. This allocator does not leave those atoms in dust: it ranks the exact remainders and assigns each residual atom once, then requires the final distributed total to equal the daemon-credited reward exactly.

What the reward formula closes by construction.

The most common reward-accounting attacks against fixed-window PPLNS variants are eliminated by Kernel-PPLNS by design — not by policy or monitoring, but by the math.

Pool hopping

No round to hop.

The window is measured in work units, not rounds, and never resets. Coming and going just means you stop earning new weight while you're away — no penalty, no advantage.

Vardiff manipulation

Assigned, not achieved.

Reward accounting uses the share difficulty the pool assigned before work was issued — not the difficulty actually achieved. Posterior tweaks earn nothing.

Delayed submission

Cutoff at job issuance.

The reward cutoff is frozen at the moment the block-finding job was issued — not when it was received. Padding with late shares is impossible.

Posterior difficulty

Frozen at acceptance.

Each share carries its work units from the moment of acceptance. After-the-fact difficulty inflation is rejected outright.

Mine on a pool that rewards what you actually contributed.

Block-funded rewards. A work-measured window. A smooth kernel that credits the work you do — no cliff when you arrive, no penalty when you leave.

Get started