igneum/site/lib/emission.mjs
igneum-labs 8ffcdd2182 Observer: explorer detail per block from the notification, chain block number from the shard plan, RPC load counter, hourly coinbase check against spec 2.5
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>
2026-10-05 19:35:40 +00:00

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 : '');
}