site/lib/emission.mjs is the emission rule as igneum.rs computes it (block_subsidy, launch_ramp, an exact floor-sum for minted-so-far); its tests reproduce the node's own test values and a devnet coinbase (block 2622db76: payload 454,486,399 at DAA 125,064, outputs 454,485,299 = E(125,063), what the merged parent declared). Every live_blocks row gains tx_count, evm_miner (the IGNA tag, else the vote key's low 20 bytes), proof_records (IGNP section), subsidy_sompi, paid_sompi, selected_parent, number, detail. No extra RPC per block: measured 282 against 283 wRPC and 785 against 776 EVM calls per minute before and after. live_state.rpc_load and live_state.supply_check are new. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
122 lines
5.7 KiB
JavaScript
122 lines
5.7 KiB
JavaScript
// Igneum emission, spec 2.5 (docs/spec/02-consensus.md) as the node computes it
|
|
// (vendor/igneum-node/consensus/core/src/igneum.rs: block_subsidy, launch_ramp). Shared by the observer's coinbase
|
|
// check (tools/observer/observer.mjs) and the public /api/supply. BigInt throughout; sompi is the 8-decimal unit the
|
|
// devnet pays in (open item O-2.6 keeps Kaspa's SOMPI_PER_KASPA), the EVM side shows the same amount at 18 decimals.
|
|
|
|
export const SOMPI_PER_IGN = 100_000_000n;
|
|
export const SECONDS_PER_YEAR = 31_557_600n; // 365.25 days
|
|
export const HALVING_INTERVAL_SECONDS = 2n * SECONDS_PER_YEAR; // 63,115,200
|
|
export const YEAR_ONE_EMISSION_SOMPI = 1_000_000_000n * SOMPI_PER_IGN;
|
|
export const BASE_SUBSIDY_PER_SECOND_SOMPI = YEAR_ONE_EMISSION_SOMPI / SECONDS_PER_YEAR; // 3,168,808,781 (floor)
|
|
export const SUPPLY_CAP_SOMPI = 4_000_000_000n * SOMPI_PER_IGN;
|
|
export const LAUNCH_RAMP_SECONDS = 2_592_000n; // 30 days
|
|
export const LAUNCH_RAMP_START_PERCENT = 10n;
|
|
export const PROVING_POOL_SHARE_PERCENT = 20n;
|
|
export const SUBSIDY_PERIODS = 33; // period 32 pays 0
|
|
|
|
/** Per-second subsidy of two-year period i: BASE >> i (igneum.rs SUBSIDY_PER_SECOND_BY_PERIOD). */
|
|
export function subsidyPerSecond(period) {
|
|
const i = Math.min(Math.max(0, period), SUBSIDY_PERIODS - 1);
|
|
return BASE_SUBSIDY_PER_SECOND_SOMPI >> BigInt(i);
|
|
}
|
|
|
|
/** igneum.rs launch_ramp: linear from 10% at second 0 to 100% at LAUNCH_RAMP_SECONDS, integer floor. */
|
|
export function launchRamp(fullSubsidy, secondsSinceGenesis) {
|
|
const s = BigInt(secondsSinceGenesis);
|
|
if (s >= LAUNCH_RAMP_SECONDS) return fullSubsidy;
|
|
const numerator = LAUNCH_RAMP_START_PERCENT * LAUNCH_RAMP_SECONDS + (100n - LAUNCH_RAMP_START_PERCENT) * s;
|
|
const denominator = 100n * LAUNCH_RAMP_SECONDS;
|
|
return fullSubsidy * numerator / denominator;
|
|
}
|
|
|
|
/** igneum.rs block_subsidy(daa_score, bps): per-block subsidy in sompi paid for each blue block merged at this DAA score. */
|
|
export function blockSubsidy(daaScore, bps = 1) {
|
|
const b = BigInt(bps);
|
|
const seconds = BigInt(daaScore) / b;
|
|
const period = Number(seconds / HALVING_INTERVAL_SECONDS);
|
|
const perBlock = subsidyPerSecond(period) / b;
|
|
return launchRamp(perBlock, seconds);
|
|
}
|
|
|
|
/** The ramp factor at a DAA score as a decimal in [0.1, 1], for display. */
|
|
export function rampFactor(daaScore) {
|
|
const s = BigInt(daaScore);
|
|
if (s >= LAUNCH_RAMP_SECONDS) return 1;
|
|
return Number(LAUNCH_RAMP_START_PERCENT * LAUNCH_RAMP_SECONDS + (100n - LAUNCH_RAMP_START_PERCENT) * s) / Number(100n * LAUNCH_RAMP_SECONDS);
|
|
}
|
|
|
|
// sum_{i=0}^{n-1} floor((a*i + b) / m), exact in O(log) (the classic floor-sum; a, b >= 0, m > 0, BigInt).
|
|
export function floorSum(n, m, a, b) {
|
|
let ans = 0n;
|
|
for (;;) {
|
|
if (a >= m) { ans += (n - 1n) * n / 2n * (a / m); a %= m; }
|
|
if (b >= m) { ans += n * (b / m); b %= m; }
|
|
const yMax = a * n + b;
|
|
if (yMax < m) return ans;
|
|
const n2 = yMax / m, b2 = yMax % m;
|
|
[n, m, a, b] = [n2, a, m, b2];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Coins minted by the rule from DAA 0 up to (not including) `daaScore`, in sompi, at `bps` = 1: the sum of
|
|
* blockSubsidy(t) over every DAA second t. Exact: the ramp part is a floor-sum, the rest is per-period arithmetic.
|
|
* This is the schedule; the chain pays blockSubsidy per blue block it merges (and per red inside the window), so
|
|
* the paid total tracks it one block per DAA step. The observer's coinbase check (live_state.supply_check) reports
|
|
* the difference it sees.
|
|
*/
|
|
export function mintedByRule(daaScore) {
|
|
let t = BigInt(daaScore);
|
|
if (t <= 0n) return 0n;
|
|
let total = 0n;
|
|
// ramp: per-second = floor(B * (10R + 90 s) / (100 R)) = floor((90B s + 10RB) / (100R))
|
|
const B = BASE_SUBSIDY_PER_SECOND_SOMPI, R = LAUNCH_RAMP_SECONDS;
|
|
const rampSeconds = t < R ? t : R;
|
|
total += floorSum(rampSeconds, 100n * R, 90n * B, 10n * R * B);
|
|
if (t <= R) return total;
|
|
// after the ramp, period by period
|
|
let from = R;
|
|
while (from < t) {
|
|
const period = Number(from / HALVING_INTERVAL_SECONDS);
|
|
const periodEnd = BigInt(period + 1) * HALVING_INTERVAL_SECONDS;
|
|
const to = t < periodEnd ? t : periodEnd;
|
|
total += (to - from) * subsidyPerSecond(period);
|
|
from = to;
|
|
}
|
|
return total;
|
|
}
|
|
|
|
/** Minted by the rule when every period has paid out: the cap minus the ramp's withheld part and the floors. */
|
|
export function mintedAtEnd() {
|
|
return mintedByRule(BigInt(SUBSIDY_PERIODS) * HALVING_INTERVAL_SECONDS);
|
|
}
|
|
|
|
/** The halving table: one row per two-year period until the per-second subsidy is 0. */
|
|
export function halvingTable(maxRows = SUBSIDY_PERIODS) {
|
|
const rows = [];
|
|
for (let i = 0; i < Math.min(maxRows, SUBSIDY_PERIODS); i++) {
|
|
const start = BigInt(i) * HALVING_INTERVAL_SECONDS;
|
|
rows.push({
|
|
period: i,
|
|
start_daa: start,
|
|
end_daa: start + HALVING_INTERVAL_SECONDS,
|
|
per_second_sompi: subsidyPerSecond(i),
|
|
minted_by_end_sompi: mintedByRule(start + HALVING_INTERVAL_SECONDS),
|
|
});
|
|
}
|
|
return rows;
|
|
}
|
|
|
|
/** sompi -> decimal IGN string with up to 8 places, no exponent, no rounding. */
|
|
export function sompiToIgn(sompi, places = 8) {
|
|
const v = BigInt(sompi); const neg = v < 0n; const a = neg ? -v : v;
|
|
const whole = a / SOMPI_PER_IGN, frac = (a % SOMPI_PER_IGN).toString().padStart(8, '0').slice(0, places).replace(/0+$/, '');
|
|
return (neg ? '-' : '') + whole.toString() + (frac ? '.' + frac : '');
|
|
}
|
|
|
|
/** wei (18 decimals) -> decimal IGN string, up to `places` places. */
|
|
export function weiToIgn(wei, places = 6) {
|
|
const v = BigInt(wei); const neg = v < 0n; const a = neg ? -v : v; const U = 10n ** 18n;
|
|
const whole = a / U, frac = (a % U).toString().padStart(18, '0').slice(0, places).replace(/0+$/, '');
|
|
return (neg ? '-' : '') + whole.toString() + (frac ? '.' + frac : '');
|
|
}
|