From 808a1c3e3041c02bc4fe0bbd9cb6034b5772ff8e Mon Sep 17 00:00:00 2001 From: Satoshi_Puzzles <127057721+satoshipuzzles@users.noreply.github.com> Date: Thu, 22 Oct 2026 08:51:06 -0700 Subject: [PATCH 1/5] Softfork: 1-year rolling coinbase maturity, no release cliff, independent of RDTS On mainnet, from block 979920, every generated coin mined at height 973440 or later must be buried 52560 blocks (365 days of 144 blocks) before it can be spent. Each reward counts from its own block, so exactly one reward unlocks per block, the first at 1026000. This takes over from the long coinbase maturity window (#419) at its release height, so the rewards it holds are not all released together at 979920. Through 979919 the rules are unchanged; rewards mined before 973440 keep ordinary maturity. The rule is decided by heights alone, has no expiry and does not depend on RdtsExpiryTime or the BLAKE2b fork. Mempool acceptance and reorg cleanup enforce it for the next block. The wallet and GUI ask for the maturity at each coin's height. getdeploymentinfo reports rolling_coinbase_maturity and getblocktemplate lists it in rules once active. Upgrading nodes check every block from 979920 again under the rolling_coinbase_maturity_1y revalidation marker. Regtest can opt in with -testcoinbasematurityrolling=::. Co-Authored-By: Claude Opus 5.5 --- ...release-notes-rolling-coinbase-maturity.md | 27 +++ src/chainparams.cpp | 24 +++ src/chainparamsbase.cpp | 1 + src/consensus/params.h | 37 ++++ src/consensus/tx_verify.cpp | 11 +- src/consensus/tx_verify.h | 10 +- src/interfaces/chain.h | 7 +- src/interfaces/node.h | 4 +- src/kernel/chainparams.cpp | 29 +++ src/kernel/chainparams.h | 3 + src/node/interfaces.cpp | 19 +- src/qt/transactiondesc.cpp | 2 +- src/rpc/blockchain.cpp | 19 +- src/rpc/mining.cpp | 3 + src/test/coinbase_maturity_tests.cpp | 175 +++++++++++++++- src/validation.cpp | 14 +- src/wallet/wallet.cpp | 4 +- .../mempool_rolling_coinbase_maturity.py | 188 ++++++++++++++++++ test/functional/test_runner.py | 1 + 19 files changed, 560 insertions(+), 18 deletions(-) create mode 100644 doc/release-notes-rolling-coinbase-maturity.md create mode 100755 test/functional/mempool_rolling_coinbase_maturity.py diff --git a/doc/release-notes-rolling-coinbase-maturity.md b/doc/release-notes-rolling-coinbase-maturity.md new file mode 100644 index 000000000000..cb7b0acbcb98 --- /dev/null +++ b/doc/release-notes-rolling-coinbase-maturity.md @@ -0,0 +1,27 @@ +Consensus +--------- + +- Rolling coinbase maturity (1 year). On mainnet, from block 979920, every + generated coin mined at height 973440 or later must be buried 52560 blocks + (365 days of 144 blocks) before it can be spent. Each reward counts from its + own block, so exactly one reward unlocks per block, the first at 1026000. + + This replaces the shared release of the existing long coinbase maturity + window: the rewards it holds (973440 to 979919) are no longer all released + together at block 979920. Through block 979919 the rules are unchanged. + Rewards mined before 973440 keep ordinary 100-block maturity. + + This rule is separate from RDTS. It is decided by block heights alone, has + no expiry and does not depend on `RdtsExpiryTime` or the BLAKE2b fork, so it + stays in force whether RDTS is extended or lapses. + + Upgrading nodes check every block from 979920 again using the existing + chainstate revalidation, under the marker `rolling_coinbase_maturity_1y`. + A node that upgrades before block 979920 rewinds nothing. A pruned node that + upgrades later and no longer has those blocks must reindex. + + `getdeploymentinfo` reports the rule as `rolling_coinbase_maturity`, and + `getblocktemplate` lists `rolling_coinbase_maturity` in `rules` once it is + active. Wallets and the GUI show the maturity that applies to each reward. + Testnet4, signet and regtest are unchanged; regtest can opt in with + `-testcoinbasematurityrolling=::`. diff --git a/src/chainparams.cpp b/src/chainparams.cpp index e2851d88c986..f03ddcfc5d63 100644 --- a/src/chainparams.cpp +++ b/src/chainparams.cpp @@ -128,6 +128,30 @@ void ReadRegTestArgs(const ArgsManager& args, CChainParams::RegTestOptions& opti options.coinbase_maturity_long_release_height = release_height; } + if (const auto arg{args.GetArg("-testcoinbasematurityrolling")}; arg) { + const auto values{SplitString(*arg, ':')}; + if (values.size() != 3) { + throw std::runtime_error(strprintf("Invalid format (%s) for -testcoinbasematurityrolling=::.", *arg)); + } + + int32_t start_height; + int32_t enforce_height; + int32_t maturity; + if (!ParseInt32(values[0], &start_height) || start_height < 0 || start_height >= std::numeric_limits::max()) { + throw std::runtime_error(strprintf("Invalid start height (%s) for -testcoinbasematurityrolling=::.", values[0])); + } + if (!ParseInt32(values[1], &enforce_height) || enforce_height < 2 || enforce_height >= std::numeric_limits::max()) { + throw std::runtime_error(strprintf("Invalid enforce height (%s) for -testcoinbasematurityrolling=::.", values[1])); + } + if (!ParseInt32(values[2], &maturity) || maturity <= COINBASE_MATURITY || maturity >= std::numeric_limits::max()) { + throw std::runtime_error(strprintf("Invalid maturity (%s) for -testcoinbasematurityrolling=::: must exceed COINBASE_MATURITY.", values[2])); + } + + options.coinbase_maturity_rolling_start_height = start_height; + options.coinbase_maturity_rolling_enforce_height = enforce_height; + options.coinbase_maturity_rolling = maturity; + } + for (const std::string& strDeployment : args.GetArgs("-vbparams")) { std::vector vDeploymentParams = SplitString(strDeployment, ':'); if (vDeploymentParams.size() < 3 || 7 < vDeploymentParams.size()) { diff --git a/src/chainparamsbase.cpp b/src/chainparamsbase.cpp index de5a866c7c4f..c3fcd8f156f8 100644 --- a/src/chainparamsbase.cpp +++ b/src/chainparamsbase.cpp @@ -19,6 +19,7 @@ void SetupChainParamsBaseOptions(ArgsManager& argsman) argsman.AddArg("-testactivationheight=name@height.", "Set the activation height of 'name' (segwit, bip34, dersig, cltv, csv, blake2b). (regtest-only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST); argsman.AddArg("-rdtsexpiry=