Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions doc/release-notes-coinbase-rolling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
RDTS continues; rolling 40320-block coinbase maturity
----------------------------------------------------

On mainnet, RDTS (BIP110) no longer expires: `RdtsExpiryTime` was
1819756800 (2027-09-01 00:00 UTC) and is now `INT64_MAX`. Its rules (the
800000-weight block limit, 34-byte output scripts, the OP_RETURN data limit
and the input rules it gates) stay in force at every height from the BLAKE2b
fork. Keeping rules in force is strictly more restrictive than letting them
lapse, so this is a soft fork.

The rolling coinbase maturity continues with RDTS, as #467 made it, at 40320
blocks (~280 days, about the length of a human pregnancy) instead of 6480
(~45 days). Rewards mined from height 973440 each mature 40320 blocks after
the block that mined them; there is no hold until a date and no release. The
rule matches #419's valid-block set before height 979920 (no covered reward
can reach 6480 confirmations sooner) and only restricts it from 979920
onward, where #419 returns covered rewards to 100 and #467 keeps 6480 until
2027-09-01. Testnet4 keeps its original height-based window; other public
networks and default regtest are unchanged.

The existing revalidation marker extends beyond #419's height window, bounded
by parent MTP. A #419 node upgrading after 979920 rewinds to 979919, even
when its tip is already past expiry, to recheck pre-expiry history and cached
descendants. Without a marker, revalidation begins at enforcement minus one.
Pruned nodes missing required history refuse startup before disconnecting
blocks and must reindex. Nodes that ran different experimental rules sharing
the marker must also reindex. Assumeutxo background validation must complete
pre-snapshot coverage.

Mempool admission, reorg filtering, wallet credit, coin selection and GUI
maturity retain #419 behavior: every coinbase needs the configured long
depth for policy and wallet availability, including after consensus expiry.
Policy therefore remains stricter than consensus after expiry. Wallet credit
retains its one-confirmation margin. RPC category help describes applicable
maturity; deployment reporting shows the configured long depth, parent-MTP
expiry and next-block active state. GBT stops advertising the rule at expiry.

Configured regtest accepts `-testcoinbasematuritylong=start:enforce:release[:maturity]`
and `-testcoinbasematuritylongexpiry=time`. The expiry defaults to the configured
RDTS expiry, or INT64_MAX if none, and must match `-rdtsexpiry` when both are set.
29 changes: 24 additions & 5 deletions src/chainparams.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -103,31 +103,50 @@ void ReadRegTestArgs(const ArgsManager& args, CChainParams::RegTestOptions& opti

if (const auto arg{args.GetArg("-testcoinbasematuritylong")}; arg) {
const auto values{SplitString(*arg, ':')};
if (values.size() != 3) {
throw std::runtime_error(strprintf("Invalid format (%s) for -testcoinbasematuritylong=<start_height>:<enforce_height>:<release_height>.", *arg));
if (values.size() < 3 || values.size() > 4) {
throw std::runtime_error(strprintf("Invalid format (%s) for -testcoinbasematuritylong=<start_height>:<enforce_height>:<release_height>[:<maturity>].", *arg));
}

int32_t start_height;
int32_t enforce_height;
int32_t release_height;
if (!ParseInt32(values[0], &start_height) || start_height < 0 || start_height >= std::numeric_limits<int>::max()) {
throw std::runtime_error(strprintf("Invalid start height (%s) for -testcoinbasematuritylong=<start_height>:<enforce_height>:<release_height>.", values[0]));
throw std::runtime_error(strprintf("Invalid start height (%s) for -testcoinbasematuritylong=<start_height>:<enforce_height>:<release_height>[:<maturity>].", values[0]));
}
if (!ParseInt32(values[1], &enforce_height) || enforce_height < 2 || enforce_height >= std::numeric_limits<int>::max()) {
throw std::runtime_error(strprintf("Invalid enforce height (%s) for -testcoinbasematuritylong=<start_height>:<enforce_height>:<release_height>.", values[1]));
throw std::runtime_error(strprintf("Invalid enforce height (%s) for -testcoinbasematuritylong=<start_height>:<enforce_height>:<release_height>[:<maturity>].", values[1]));
}
if (!ParseInt32(values[2], &release_height) || release_height <= start_height || release_height <= enforce_height || release_height >= std::numeric_limits<int>::max()) {
throw std::runtime_error(strprintf("Invalid release height (%s) for -testcoinbasematuritylong=<start_height>:<enforce_height>:<release_height>.", values[2]));
throw std::runtime_error(strprintf("Invalid release height (%s) for -testcoinbasematuritylong=<start_height>:<enforce_height>:<release_height>[:<maturity>].", values[2]));
}
if (release_height - start_height <= COINBASE_MATURITY) {
throw std::runtime_error(strprintf("Invalid height range (%s) for -testcoinbasematuritylong=<start_height>:<enforce_height>:<release_height>: release_height - start_height must exceed COINBASE_MATURITY.", *arg));
}

if (values.size() == 4) {
int32_t maturity;
if (!ParseInt32(values[3], &maturity) || maturity < COINBASE_MATURITY || maturity >= std::numeric_limits<int>::max()) {
throw std::runtime_error(strprintf("Invalid maturity (%s) for -testcoinbasematuritylong.", values[3]));
}
options.coinbase_maturity_long_maturity = maturity;
}

options.coinbase_maturity_long_start_height = start_height;
options.coinbase_maturity_long_enforce_height = enforce_height;
options.coinbase_maturity_long_release_height = release_height;
}

if (const auto arg{args.GetArg("-testcoinbasematuritylongexpiry")}; arg) {
int64_t expiry;
if (!options.coinbase_maturity_long_start_height || !ParseInt64(*arg, &expiry) || expiry <= 1296688602) {
throw std::runtime_error("Invalid expiry for -testcoinbasematuritylongexpiry: requires -testcoinbasematuritylong and a time after regtest genesis (1296688602).");
}
if (options.rdts_expiry_time && *options.rdts_expiry_time != expiry) {
throw std::runtime_error("-testcoinbasematuritylongexpiry must match -rdtsexpiry when both are specified.");
}
options.coinbase_maturity_long_expiry_time = expiry;
}

for (const std::string& strDeployment : args.GetArgs("-vbparams")) {
std::vector<std::string> vDeploymentParams = SplitString(strDeployment, ':');
if (vDeploymentParams.size() < 3 || 7 < vDeploymentParams.size()) {
Expand Down
3 changes: 2 additions & 1 deletion src/chainparamsbase.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ 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=<time>", "Schedule the RDTS expiry: RDTS rules apply to blocks from the blake2b activation height until the parent block's median-time-past reaches <time>. Requires -testactivationheight=blake2b@<height>. (regtest-only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST);
argsman.AddArg("-blake2b_headline=<headline>", "Specify consensus-critical proof-of-time news headline. Requires -testactivationheight=blake2b@<height>. (regtest-only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST);
argsman.AddArg("-testcoinbasematuritylong=<start_height>:<enforce_height>:<release_height>", "Set long coinbase maturity parameters. Note that <start_height> must not be changed once used with a chainstate. (regtest-only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST);
argsman.AddArg("-testcoinbasematuritylongexpiry=<time>", "Set the parent median time past releasing long coinbase consensus maturity to 100; requires -testcoinbasematuritylong. Policy and wallet retain the long depth. Must match -rdtsexpiry if set. (regtest-only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST);
argsman.AddArg("-testcoinbasematuritylong=<start_height>:<enforce_height>:<release_height>[:<maturity>]", "Set the original long coinbase maturity schedule, continuing the same rolling depth until RDTS expiry. Optional <maturity> overrides the original depth for synthetic boundary tests. Use -testcoinbasematuritylongexpiry to set the expiry (otherwise -rdtsexpiry, or no expiry if neither is set). Note that <start_height> must not be changed once used with a chainstate. (regtest-only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST);
argsman.AddArg("-testnet", "Use the testnet3 chain. Equivalent to -chain=test. Support for testnet3 is deprecated and will be removed in an upcoming release. Consider moving to testnet4 now by using -testnet4.", ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS);
argsman.AddArg("-testnet4", "Use the testnet4 chain. Equivalent to -chain=testnet4.", ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS);
argsman.AddArg("-vbparams=deployment:start:end[:min_activation_height]", "Use given start/end times and min_activation_height for specified version bits deployment (regtest-only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS);
Expand Down
18 changes: 16 additions & 2 deletions src/consensus/params.h
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ struct ChainstateRevalidationDeployment {
int start_height{std::numeric_limits<int>::max()};
/** Last block height whose cached validation may be invalidated. */
int stop_height{std::numeric_limits<int>::max()};
/** Exclude blocks whose parent median time past has reached this time. */
int64_t stop_time{std::numeric_limits<int64_t>::max()};
};

/**
Expand Down Expand Up @@ -153,13 +155,25 @@ struct Params {
* behaviour is unchanged on chains that do not set it.
*/
int64_t RdtsExpiryTime{std::numeric_limits<int64_t>::min()};
// Only opted-in chains continue rolling maturity until RDTS expires.
int CoinbaseMaturityLong{COINBASE_MATURITY};
bool CoinbaseMaturityLongUntilRdts{false};
int CoinbaseMaturityLongStartHeight{std::numeric_limits<int>::max()};
int CoinbaseMaturityLongEnforceHeight{std::numeric_limits<int>::max()};
int CoinbaseMaturityLongReleaseHeight{std::numeric_limits<int>::max()};
bool CoinbaseMaturityLongActiveAt(int height) const
int CoinbaseMaturityLongAt(int height, int64_t mtp_prev) const
{
return height >= CoinbaseMaturityLongEnforceHeight && height < CoinbaseMaturityLongReleaseHeight;
if (height < CoinbaseMaturityLongEnforceHeight) return COINBASE_MATURITY;
if (CoinbaseMaturityLongUntilRdts) return mtp_prev < RdtsExpiryTime ? CoinbaseMaturityLong : COINBASE_MATURITY;
return height < CoinbaseMaturityLongReleaseHeight ? CoinbaseMaturityLong : COINBASE_MATURITY;
}
bool CoinbaseMaturityLongActiveAt(int height, int64_t mtp_prev) const
{
return CoinbaseMaturityLongAt(height, mtp_prev) > COINBASE_MATURITY;
}
int CoinbaseMaturityAt(int coin_height, int spend_height, int64_t mtp_prev) const
{
return coin_height >= CoinbaseMaturityLongStartHeight ? CoinbaseMaturityLongAt(spend_height, mtp_prev) : COINBASE_MATURITY;
}
std::vector<ChainstateRevalidationDeployment> chainstate_revalidation_deployments;
/** Don't warn about unknown BIP 9 activations below this height.
Expand Down
28 changes: 21 additions & 7 deletions src/kernel/chainparams.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,11 @@ static void AddLongCoinbaseMaturityRevalidationDeployment(Consensus::Params& con
consensus.chainstate_revalidation_deployments.push_back({
.name = "long_coinbase_maturity",
.start_height = consensus.CoinbaseMaturityLongEnforceHeight,
.stop_height = consensus.CoinbaseMaturityLongReleaseHeight - 1,
// Extending the existing marker makes #419 nodes revalidate from
// the old release height, even on upgrade after expiry. With no marker,
// start at enforcement. Expired blocks still inherit demoted ancestors.
.stop_height = consensus.CoinbaseMaturityLongUntilRdts ? std::numeric_limits<int>::max() : consensus.CoinbaseMaturityLongReleaseHeight - 1,
.stop_time = consensus.CoinbaseMaturityLongUntilRdts ? consensus.RdtsExpiryTime : std::numeric_limits<int64_t>::max(),
});
}

Expand Down Expand Up @@ -134,16 +138,24 @@ class CMainParams : public CChainParams {
consensus.Blake2bTargetShift = 22;

// RDTS: its rules apply to every block from Blake2bHeight (the
// flag-day activation, set at release cut) until the parent block's
// median-time-past reaches RdtsExpiryTime. (RDTS previously activated
// flag-day activation, set at release cut). (RDTS previously activated
// via versionbits; the stall at 961633 prevented it from ever
// reaching ACTIVE, and that deployment has been removed.)
consensus.RdtsExpiryTime = 1819756800; // September 1st, 2027 00:00 UTC

//
// RDTS continues: no expiry. It was scheduled to end when the parent
// block's median-time-past reached 1819756800 (September 1st, 2027
// 00:00 UTC); keeping its rules in force is strictly more restrictive
// than letting them lapse, so this remains a soft fork.
consensus.RdtsExpiryTime = std::numeric_limits<int64_t>::max();

// The rolling coinbase maturity continues with RDTS (as in #467), at
// 40320 blocks (~280 days) instead of 6480 (~45 days). With RDTS
// continuing, the rolling rule does not lapse either.
consensus.CoinbaseMaturityLongStartHeight = 973440;
consensus.CoinbaseMaturityLongEnforceHeight = 973440;
consensus.CoinbaseMaturityLongReleaseHeight = 979920;
consensus.CoinbaseMaturityLong = consensus.CoinbaseMaturityLongReleaseHeight - consensus.CoinbaseMaturityLongStartHeight;
consensus.CoinbaseMaturityLongUntilRdts = true;
consensus.CoinbaseMaturityLong = 40320;
AddLongCoinbaseMaturityRevalidationDeployment(consensus);

consensus.nMinimumChainWork = uint256{"00000000000000000000000000000000000000013e00277374c9f9eeadc70200"};
Expand Down Expand Up @@ -690,7 +702,9 @@ class CRegTestParams : public CChainParams
consensus.CoinbaseMaturityLongStartHeight = *opts.coinbase_maturity_long_start_height;
consensus.CoinbaseMaturityLongEnforceHeight = *opts.coinbase_maturity_long_enforce_height;
consensus.CoinbaseMaturityLongReleaseHeight = *opts.coinbase_maturity_long_release_height;
consensus.CoinbaseMaturityLong = consensus.CoinbaseMaturityLongReleaseHeight - consensus.CoinbaseMaturityLongStartHeight;
consensus.CoinbaseMaturityLong = opts.coinbase_maturity_long_maturity.value_or(consensus.CoinbaseMaturityLongReleaseHeight - consensus.CoinbaseMaturityLongStartHeight);
consensus.CoinbaseMaturityLongUntilRdts = true;
consensus.RdtsExpiryTime = opts.coinbase_maturity_long_expiry_time.value_or(opts.rdts_expiry_time.value_or(std::numeric_limits<int64_t>::max()));
AddLongCoinbaseMaturityRevalidationDeployment(consensus);
}

Expand Down
2 changes: 2 additions & 0 deletions src/kernel/chainparams.h
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ class CChainParams
std::optional<int> coinbase_maturity_long_start_height{};
std::optional<int> coinbase_maturity_long_enforce_height{};
std::optional<int> coinbase_maturity_long_release_height{};
std::optional<int> coinbase_maturity_long_maturity{};
std::optional<int64_t> coinbase_maturity_long_expiry_time{};
};

static std::unique_ptr<const CChainParams> RegTest(const RegTestOptions& options);
Expand Down
14 changes: 9 additions & 5 deletions src/rpc/blockchain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1911,9 +1911,9 @@ namespace {
const std::vector<RPCResult> RPCHelpForDeployment{
{RPCResult::Type::STR, "type", "one of \"buried\", \"bip9\", \"flagday\""},
{RPCResult::Type::NUM, "height", /*optional=*/true, "height of the first block which enforces the rules (only for \"buried\" and \"flagday\" types, or \"bip9\" type with \"active\" status)"},
{RPCResult::Type::NUM, "height_end", /*optional=*/true, "height of the last block which enforces the rules (only for temporary deployments)"},
{RPCResult::Type::NUM, "height_end", /*optional=*/true, "height of the last block which enforces the rules (only for temporary deployments with a height-based release)"},
{RPCResult::Type::NUM, "coinbase_start_height", /*optional=*/true, "height of the first generated coin covered by the long coinbase maturity rule (only for \"long_coinbase_maturity\")"},
{RPCResult::Type::NUM, "maturity", /*optional=*/true, "required coinbase maturity depth for covered generated coins (only for \"long_coinbase_maturity\")"},
{RPCResult::Type::NUM, "maturity", /*optional=*/true, "configured long coinbase maturity depth for covered generated coins (policy and wallet may retain this depth after consensus expiry) (only for \"long_coinbase_maturity\")"},
{RPCResult::Type::BOOL, "active", "true if the consensus rules are enforced for the next block (policy may enforce related rules independently of this flag)"},
{RPCResult::Type::NUM_TIME, "expiry_time", /*optional=*/true, "median time past at and after which the rules are no longer enforced (only for \"flagday\" type; a block is past expiry when its parent's median time past has reached this value)"},
{RPCResult::Type::OBJ, "bip9", /*optional=*/true, "status of bip9 softforks (only for \"bip9\" type)",
Expand Down Expand Up @@ -1964,15 +1964,19 @@ void RdtsFlagDayDescPushBack(const CBlockIndex* blockindex, UniValue& softforks,
void LongCoinbaseMaturityDescPushBack(const CBlockIndex* blockindex, UniValue& softforks, const ChainstateManager& chainman)
{
const Consensus::Params& params{chainman.GetConsensus()};
if (params.CoinbaseMaturityLongReleaseHeight == std::numeric_limits<int>::max()) return;
if (params.CoinbaseMaturityLongStartHeight == std::numeric_limits<int>::max()) return;

UniValue rv(UniValue::VOBJ);
rv.pushKV("type", "flagday");
rv.pushKV("height", params.CoinbaseMaturityLongEnforceHeight);
rv.pushKV("height_end", params.CoinbaseMaturityLongReleaseHeight - 1);
if (params.CoinbaseMaturityLongUntilRdts) {
rv.pushKV("expiry_time", params.RdtsExpiryTime);
} else {
rv.pushKV("height_end", params.CoinbaseMaturityLongReleaseHeight - 1);
}
rv.pushKV("coinbase_start_height", params.CoinbaseMaturityLongStartHeight);
rv.pushKV("maturity", params.CoinbaseMaturityLong);
rv.pushKV("active", params.CoinbaseMaturityLongActiveAt(blockindex->nHeight + 1));
rv.pushKV("active", params.CoinbaseMaturityLongActiveAt(blockindex->nHeight + 1, blockindex->GetMedianTimePast()));
softforks.pushKV("long_coinbase_maturity", std::move(rv));
}

Expand Down
2 changes: 1 addition & 1 deletion src/rpc/mining.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1084,7 +1084,7 @@ static UniValue TemplateToJSON(const Consensus::Params& consensusParams, const C
if (rdts_active) {
aRules.push_back("reduced_data");
}
if (pindexPrev != nullptr && consensusParams.CoinbaseMaturityLongActiveAt(pindexPrev->nHeight + 1)) {
if (pindexPrev != nullptr && consensusParams.CoinbaseMaturityLongActiveAt(pindexPrev->nHeight + 1, pindexPrev->GetMedianTimePast())) {
aRules.push_back("long_coinbase_maturity");
}

Expand Down
Loading