Skip to content
48 changes: 48 additions & 0 deletions doc/release-notes-extended-coinbase-maturity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
### Extended coinbase maturity (temporary softfork)

Coinbase outputs created while this deployment is active can only be spent
once the spending block's parent has a median-time-past at least 45 days
(3,888,000 seconds) past the coinbase block's median-time-past,
on top of the usual 100 confirmations. Measuring the delay in median-time-past
rather than blocks keeps it 45 days of real time however fast or slow
blocks come, and a miner cannot shorten it: block times cannot run more than
about two hours ahead of real time, so neither can the median.

The rule applies to every block from the first one whose parent's
median-time-past reaches the deployment's start time, and expires together
with RDTS: from the first block whose parent's median-time-past reaches the
RDTS expiry (2027-09-01 00:00 UTC on mainnet) every coinbase output is again
spendable after 100 confirmations, including those created during the window
that had not yet served their 45 days.

Only outputs created inside the window are affected. A coinbase output created
before activation keeps the 100-block rule throughout, so activating the
deployment cannot lock an output that was already spendable. The 100-block
rule continues to apply to every coinbase output at all times.

The start time is not yet scheduled on mainnet or testnet4 in this release
(see `src/kernel/chainparams.cpp`); the deployment does nothing until it is.

Miners: block rewards mined inside the window cannot be spent for 45 days
(or until the RDTS expiry, whichever comes first). Pools that pay their miners
directly from coinbase outputs pass this delay through to those miners. A
non-upgraded miner that spends its own reward at 100 confirmations produces a
block that upgraded nodes reject.

Wallet: rewards subject to the rule are reported as immature (in
`getbalances` `immature`, `listtransactions` category `immature` and the
GUI) until they can be spent, and are not selected for spending before then,
so the wallet never creates a transaction the network would reject. The
"blocks to maturity" the wallet reports for such a reward is an estimate,
the remaining time at the target block spacing.

`getdeploymentinfo` reports the deployment as `extended_coinbase_maturity`, a
`flagday` entry with `start_time`, `expiry_time` (the RDTS expiry), `active`
(for the next block) and, once the queried chain's median-time-past has reached
the start time, `height`, the activation height on that chain. It is omitted
on chains where the deployment is not scheduled. `getblocktemplate` lists
`extended_coinbase_maturity` in `rules` while the deployment is active; no
client-side support is required.

On regtest the deployment is scheduled with `-extendedcoinbasematurity=<time>`,
which requires `-rdtsexpiry` and must precede it.
14 changes: 14 additions & 0 deletions src/chainparams.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,20 @@ void ReadRegTestArgs(const ArgsManager& args, CChainParams::RegTestOptions& opti
options.rdts_expiry_time = expiry;
}

if (const auto arg{args.GetArg("-extendedcoinbasematurity", "")}; !arg.empty()) {
// The rule expires with RDTS: without an RDTS expiry, or with a start
// at or after it, it could never apply, which is indistinguishable
// from not scheduling it at all.
if (!options.rdts_expiry_time) {
throw std::runtime_error("-extendedcoinbasematurity requires -rdtsexpiry=<time> (the rule expires with RDTS).");
}
int64_t start;
if (!ParseInt64(arg, &start) || start >= *options.rdts_expiry_time) {
throw std::runtime_error(strprintf("Invalid start (%s) for -extendedcoinbasematurity=<time>: must precede the RDTS expiry (%d).", arg, *options.rdts_expiry_time));
}
options.extended_coinbase_maturity_start_time = start;
}

if (const auto arg{args.GetArg("-blake2b_headline")}; arg) {
if (!options.activation_heights.contains(Consensus::BuriedDeployment::DEPLOYMENT_BLAKE2B)) {
throw std::runtime_error("-blake2b_headline requires -testactivationheight=blake2b@<height>");
Expand Down
2 changes: 2 additions & 0 deletions src/chainparamsbase.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <chainparamsbase.h>

#include <common/args.h>
#include <consensus/consensus.h>
#include <tinyformat.h>
#include <util/chaintype.h>

Expand All @@ -18,6 +19,7 @@ void SetupChainParamsBaseOptions(ArgsManager& argsman)
"This is intended for regression testing tools and app development. Equivalent to -chain=regtest.", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS);
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("-extendedcoinbasematurity=<time>", strprintf("Schedule the extended coinbase maturity soft fork: coinbase outputs created from the first block whose parent's median-time-past reaches <time> cannot be spent until the median-time-past has advanced %d seconds past their block's, until the RDTS expiry. Requires -rdtsexpiry, which <time> must precede. (regtest-only)", EXTENDED_COINBASE_MATURITY_TIME), 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("-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);
Expand Down
5 changes: 5 additions & 0 deletions src/consensus/consensus.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ static const unsigned int REDUCED_DATA_MAX_BLOCK_WEIGHT = 800000;
static const int64_t MAX_BLOCK_SIGOPS_COST = 80000;
/** Coinbase transaction outputs can only be spent after this number of new blocks (network rule) */
static const int COINBASE_MATURITY = 100;
/** Extended coinbase maturity, applied by the temporary soft fork to coinbase outputs created while it is
* active: the spending block's parent must have a median-time-past at least this many seconds past the
* coinbase block's median-time-past (45 days, the time 6480 ten-minute blocks would take;
* see Consensus::Params::ExtendedCoinbaseMaturityActiveAt) */
static const int64_t EXTENDED_COINBASE_MATURITY_TIME = 3888000;

static const int WITNESS_SCALE_FACTOR = 4;

Expand Down
25 changes: 25 additions & 0 deletions src/consensus/params.h
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,23 @@ struct Params {
* behaviour is unchanged on chains that do not set it.
*/
int64_t RdtsExpiryTime{std::numeric_limits<int64_t>::min()};
/**
* Extended coinbase maturity, a temporary soft fork: while it is active,
* coinbase outputs created since it activated can only be spent once the
* spending block's parent has a median-time-past at least
* EXTENDED_COINBASE_MATURITY_TIME past the coinbase block's, on top of
* the usual COINBASE_MATURITY blocks. It is active for a block when the
* parent's median-time-past has reached this start time and not yet the
* RDTS expiry (see ExtendedCoinbaseMaturityActiveAt): it expires with
* RDTS, after which every coinbase output is again spendable at
* COINBASE_MATURITY. Outputs created before activation are never
* affected, so activating cannot lock an output that was already
* spendable.
*
* The default leaves the deployment unscheduled (the start follows every
* median-time-past).
*/
int64_t ExtendedCoinbaseMaturityStartTime{std::numeric_limits<int64_t>::max()};
/** Don't warn about unknown BIP 9 activations below this height.
* This prevents us from warning about the CSV and segwit activations. */
int MinBIP9WarningHeight;
Expand Down Expand Up @@ -195,6 +212,14 @@ struct Params {
{
return IsBlake2bHeight(height) && mtp_prev < RdtsExpiryTime;
}

/** Whether the extended coinbase maturity rule applies to a block whose
* parent has the given median-time-past. It expires with RDTS, so it is
* never active on a chain with no RDTS expiry. */
bool ExtendedCoinbaseMaturityActiveAt(int64_t mtp_prev) const
{
return mtp_prev >= ExtendedCoinbaseMaturityStartTime && mtp_prev < RdtsExpiryTime;
}
};

} // namespace Consensus
Expand Down
8 changes: 8 additions & 0 deletions src/interfaces/chain.h
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,14 @@ class Chain
//! pruned), and contains transactions.
virtual bool haveBlockOnDisk(int height) = 0;

//! Whether the temporary extended coinbase maturity soft fork still
//! holds back a coinbase output created at the given height of the
//! active chain from being spent in the block after the current tip,
//! and if so an estimate of the blocks until it no longer does (at
//! least 1). 0 when it does not (the COINBASE_MATURITY depth rule is
//! the caller's to apply).
virtual int extendedCoinbaseBlocksToMaturity(int coinbase_height) = 0;

virtual bool pruneLockExists(const std::string& name) const = 0;
virtual bool updatePruneLock(const std::string& name, const node::PruneLockInfo& lock_info, bool sync=false) = 0;
virtual bool deletePruneLock(const std::string& name) = 0;
Expand Down
12 changes: 12 additions & 0 deletions src/kernel/chainparams.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,10 @@ class CMainParams : public CChainParams {
// reaching ACTIVE, and that deployment has been removed.)
consensus.RdtsExpiryTime = 1819756800; // September 1st, 2027 00:00 UTC

// Extended coinbase maturity (temporary soft fork, expires with RDTS
// above): the start time is set at release cut. Unscheduled until then.
// consensus.ExtendedCoinbaseMaturityStartTime = ...;

consensus.nMinimumChainWork = uint256{"00000000000000000000000000000000000000013e00277374c9f9eeadc70200"};
consensus.defaultAssumeValid = uint256{"0000000000000078ed1e20cac1acf78df6d1060c78059fb6331e17141c881fc8"}; // 964264

Expand Down Expand Up @@ -403,6 +407,7 @@ class CTestNet4Params : public CChainParams {

consensus.Blake2bHeight = 150308;
consensus.RdtsExpiryTime = 1791903600; // October 13th, 2026 15:00:00 UTC
// Extended coinbase maturity: unscheduled until release cut (see mainnet).

consensus.nMinimumChainWork = uint256{"0000000000000000000000000000000000000000000001d6dce8651b6094e4c1"};
consensus.defaultAssumeValid = uint256{"0000000000003ed4f08dbdf6f7d6b271a6bcffce25675cb40aa9fa43179a89f3"}; // 72600
Expand Down Expand Up @@ -665,6 +670,13 @@ class CRegTestParams : public CChainParams
consensus.RdtsExpiryTime = *opts.rdts_expiry_time;
}

// Optionally schedule the extended coinbase maturity soft fork (see
// -extendedcoinbasematurity). It expires with RDTS, so it requires the
// RDTS expiry scheduled above.
if (opts.extended_coinbase_maturity_start_time) {
consensus.ExtendedCoinbaseMaturityStartTime = *opts.extended_coinbase_maturity_start_time;
}

for (const auto& [deployment_pos, version_bits_params] : opts.version_bits_parameters) {
consensus.vDeployments[deployment_pos].nStartTime = version_bits_params.start_time;
consensus.vDeployments[deployment_pos].nTimeout = version_bits_params.timeout;
Expand Down
5 changes: 5 additions & 0 deletions src/kernel/chainparams.h
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,11 @@ class CChainParams
//! height until the parent block's median-time-past reaches this
//! value (see -rdtsexpiry). Requires a blake2b activation height.
std::optional<int64_t> rdts_expiry_time{};
//! If set, the extended coinbase maturity rule applies to blocks
//! whose parent's median-time-past has reached this value, until the
//! RDTS expiry (see -extendedcoinbasematurity). Requires
//! rdts_expiry_time, which it must precede.
std::optional<int64_t> extended_coinbase_maturity_start_time{};
std::optional<std::vector<unsigned char>> blake2b_headline{};
};

Expand Down
15 changes: 15 additions & 0 deletions src/node/interfaces.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <chainparamsbase.h>
#include <common/args.h>
#include <common/pcp.h>
#include <consensus/consensus.h>
#include <consensus/merkle.h>
#include <consensus/validation.h>
#include <deploymentstatus.h>
Expand Down Expand Up @@ -66,6 +67,7 @@

#include <bitcoin-build-config.h> // IWYU pragma: keep

#include <algorithm>
#include <any>
#include <memory>
#include <optional>
Expand Down Expand Up @@ -581,6 +583,19 @@ class ChainImpl : public Chain
const CBlockIndex* block{chainman().ActiveChain()[height]};
return block && ((block->nStatus & BLOCK_HAVE_DATA) != 0) && block->nTx > 0;
}
int extendedCoinbaseBlocksToMaturity(int coinbase_height) override
{
LOCK(::cs_main);
const Consensus::Params& params{chainman().GetConsensus()};
const CBlockIndex* tip{chainman().ActiveChain().Tip()};
if (!tip || !params.ExtendedCoinbaseMaturityActiveAt(tip->GetMedianTimePast())) return 0;
const CBlockIndex* coinbase_block{chainman().ActiveChain()[coinbase_height]};
if (!coinbase_block || !coinbase_block->pprev ||
!params.ExtendedCoinbaseMaturityActiveAt(coinbase_block->pprev->GetMedianTimePast())) return 0;
const int64_t remaining{coinbase_block->GetMedianTimePast() + EXTENDED_COINBASE_MATURITY_TIME - tip->GetMedianTimePast()};
if (remaining <= 0) return 0;
return std::max<int64_t>(1, (remaining + params.nPowTargetSpacing - 1) / params.nPowTargetSpacing);
}
bool pruneLockExists(const std::string& name) const override
{
LOCK(cs_main);
Expand Down
8 changes: 7 additions & 1 deletion src/qt/transactiondesc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,13 @@ QString TransactionDesc::toHTML(interfaces::Node& node, interfaces::Wallet& wall

if (wtx.is_coinbase)
{
quint32 numBlocksToMaturity = COINBASE_MATURITY + 1;
// While still maturing, the wallet's count reflects the maturity that
// actually applies (the temporary extended maturity soft fork may
// require more than COINBASE_MATURITY, as an estimate); once mature,
// show the default.
quint32 numBlocksToMaturity = status.blocks_to_maturity > 0 && status.is_in_main_chain
? status.blocks_to_maturity + status.depth_in_main_chain
: COINBASE_MATURITY + 1;
strHTML += "<br>" + tr("Generated coins must mature %1 blocks before they can be spent. When you generated this block, it was broadcast to the network to be added to the block chain. If it fails to get into the chain, its state will change to \"not accepted\" and it won't be spendable. This may occasionally happen if another node generates a block within a few seconds of yours.").arg(QString::number(numBlocksToMaturity)) + "<br>";
}

Expand Down
Loading
Loading