diff --git a/src/kernel.cpp b/src/kernel.cpp index 75fa2fd..22df60e 100644 --- a/src/kernel.cpp +++ b/src/kernel.cpp @@ -31,24 +31,27 @@ int64_t GetWeight(int64_t nIntervalBeginning, int64_t nIntervalEnd) if (nAge < 0) return 0; - // After v5 fork: use soft cap of 7 days instead of hard nStakeMaxAge. - // This prevents "stake surprise" where a whale who was offline for weeks - // comes back with massively amplified staking power and dominates blocks. - // The 7-day cap still allows generous accumulation while limiting abuse. - static const int64_t STAKE_AGE_SOFT_CAP = 7 * 24 * 60 * 60; // 7 days - // Activation gate: the soft cap shipped 2026-04-20 without a height/time - // gate, retroactively invalidating earlier blocks staked with long-aged - // coins (e.g. coins idle through the 2022-2026 freeze). Apply the cap - // only to stakes after the activation timestamp; historical stakes - // validate under the rules they were created with (uncapped age). - static const int64_t STAKE_AGE_SOFT_CAP_ACTIVATION = 1776000000; // 2026-04-12 ~13:20 UTC - if (pindexBest && pindexBest->nHeight >= FORK_HEIGHT_V5) - { - if (nIntervalEnd >= STAKE_AGE_SOFT_CAP_ACTIVATION) - return min(nAge, STAKE_AGE_SOFT_CAP); - return nAge; - } - + // Original Peercoin/PPCoin behavior: hard cap at nStakeMaxAge. + // + // Historical context: an earlier V5-fork variant of this function + // replaced the cap with a 7-day SOFT cap (STAKE_AGE_SOFT_CAP), with an + // activation gate of 2026-04-12. The intent was to limit "stake + // surprise" from whales returning after long offline periods. The + // side effect was to cap long-dormant coins at the same weight as + // freshly-staked coins, eliminating the diamond-hands incentive that + // makes PoS economically meaningful for long-term holders. + // + // The chain froze at block 2,224,763 on 2026-07-18 — over 14 days + // later — with no blocks produced during the entire soft-cap window. + // Reverting to the original uncapped cap restores the original + // Peercoin staking economics for future blocks. + // + // Validation safety: the soft-cap branch was gated to require + // nIntervalEnd >= 1776000000 (2026-04-12), AND pindexBest->nHeight + // >= FORK_HEIGHT_V5. The chain never advanced past block 2,224,763 + // during the soft-cap window, so no historical block was ever + // validated under the soft cap. Therefore reverting this branch + // changes zero historical block validation results. return min(nAge, (int64_t)nStakeMaxAge); } diff --git a/src/test/consensus_safety_tests.cpp b/src/test/consensus_safety_tests.cpp index 06497ea..ee99784 100644 --- a/src/test/consensus_safety_tests.cpp +++ b/src/test/consensus_safety_tests.cpp @@ -310,31 +310,40 @@ BOOST_AUTO_TEST_CASE(coin_age_weight_monotonic) } } -// ─── Stake age soft cap (P1 — V5 fork economic rule) ────────────────────── -// The V5 fork (FORK_HEIGHT_V5) replaced the hard nStakeMaxAge cap with a -// 7-day soft cap. The cap only applies to stakes AFTER the activation -// timestamp (1776000000 = 2026-04-12 13:20 UTC). This is a soft fork -// rule — historical blocks staked before activation are unaffected. +// ─── Stake age cap (reverted to original Peercoin behavior) ──────────────── +// As of the post-July-18-2026 chain freeze fix, GetWeight uses the +// original `min(nAge, nStakeMaxAge)` formula with no soft cap. This test +// verifies that: +// (1) nStakeMaxAge (12h default) caps weight for any age beyond it. +// (2) A coin aged exactly at nStakeMaxAge returns weight == nStakeMaxAge. +// (3) The function returns 0 for coins below nStakeMinAge. // -// We test it in a way that does NOT depend on pindexBest (which is a -// global state) by using a fixed "now" that's well past activation and -// a height that's pre-V5. Pre-V5 path is in src/kernel.cpp:25-53. -BOOST_AUTO_TEST_CASE(stake_age_soft_cap_does_not_apply_pre_v5) +// Validation safety: no historical block (≤ 2,224,763) was ever minted +// under the previous soft-cap rule, because the chain froze before any +// post-2026-04-12 block was produced. Reverting GetWeight therefore +// changes zero historical block validation results. +BOOST_AUTO_TEST_CASE(stake_age_cap_uses_nStakeMaxAge_only) { - int64_t now = 1777000000; // well past 1776000000 activation - // With pindexBest == nullptr, the pre-V5 path runs (line 52 in - // kernel.cpp): min(nAge, nStakeMaxAge). nStakeMaxAge is 12 hours. - int64_t veryOld = now - nStakeMinAge - (10 * 24 * 60 * 60); // 10 days old - int64_t weight = GetWeight(veryOld, now); - // Pre-V5 cap is nStakeMaxAge = 43200 (12 hours). - BOOST_CHECK_EQUAL(weight, (int64_t)nStakeMaxAge); + int64_t now = 1777000000; + // 10-day-old coin should be capped at nStakeMaxAge (12h) + int64_t veryOld = now - nStakeMinAge - (10 * 24 * 60 * 60); + BOOST_CHECK_EQUAL(GetWeight(veryOld, now), (int64_t)nStakeMaxAge); - // Right at the cap boundary: + // Exactly at the cap boundary int64_t atMaxAge = now - nStakeMinAge - nStakeMaxAge; BOOST_CHECK_EQUAL(GetWeight(atMaxAge, now), (int64_t)nStakeMaxAge); - // One second past: also capped. + + // One second past the cap: also capped int64_t justPastMax = now - nStakeMinAge - nStakeMaxAge - 1; BOOST_CHECK_EQUAL(GetWeight(justPastMax, now), (int64_t)nStakeMaxAge); + + // Below nStakeMinAge: weight is 0 + int64_t tooYoung = now - nStakeMinAge + 60; + BOOST_CHECK_EQUAL(GetWeight(tooYoung, now), (int64_t)0); + + // Coin aged between min and max: weight = age exactly (no clamp applied) + int64_t midAge = now - nStakeMinAge - (60 * 60); // 1 hour + BOOST_CHECK_EQUAL(GetWeight(midAge, now), (int64_t)(60 * 60)); } // ─── PoS validation fast path must be height-based (P0) ───────────────────