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
32 changes: 32 additions & 0 deletions doc/release-notes-coinbase-rolling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
Rolling coinbase maturity until RDTS expiry
------------------------------------------

On mainnet, rewards mined from height 973440 require a rolling 6480-block
maturity while the spending block's parent median time past is below
`RdtsExpiryTime` (2027-09-01 00:00 UTC). Every reward matures individually;
there is no hold until expiry. At expiry, consensus maturity returns to
100 for all rewards, including rewards mined immediately before expiry.
Until expiry, the rule matches #419 before height 979920 and only restricts
#419's valid-block set from 979920 onward. 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
11 changes: 9 additions & 2 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 @@ -143,6 +147,7 @@ class CMainParams : public CChainParams {
consensus.CoinbaseMaturityLongStartHeight = 973440;
consensus.CoinbaseMaturityLongEnforceHeight = 973440;
consensus.CoinbaseMaturityLongReleaseHeight = 979920;
consensus.CoinbaseMaturityLongUntilRdts = true;
consensus.CoinbaseMaturityLong = consensus.CoinbaseMaturityLongReleaseHeight - consensus.CoinbaseMaturityLongStartHeight;
AddLongCoinbaseMaturityRevalidationDeployment(consensus);

Expand Down Expand Up @@ -690,7 +695,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