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

Coinbase outputs created while this deployment is active can only be spent
once buried under 6480 blocks (45 days of ten-minute blocks),
instead of the usual 100. 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 reached 6480.

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 6480 blocks
(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.

`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> need %d confirmations to be spent, until the RDTS expiry. Requires -rdtsexpiry, which <time> must precede. (regtest-only)", EXTENDED_COINBASE_MATURITY), 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
3 changes: 3 additions & 0 deletions src/consensus/consensus.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ 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;
/** Coinbase maturity applied by the temporary extended-maturity soft fork to coinbase outputs created
* while it is active: 45 days of ten-minute blocks (see Consensus::Params::ExtendedCoinbaseMaturityActiveAt) */
static const int EXTENDED_COINBASE_MATURITY = 6480;

static const int WITNESS_SCALE_FACTOR = 4;

Expand Down
24 changes: 24 additions & 0 deletions src/consensus/params.h
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,22 @@ 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
* buried under EXTENDED_COINBASE_MATURITY blocks rather than
* COINBASE_MATURITY. 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 +211,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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rule expires with RDTS and outputs revert to COINBASE_MATURITY at that point, so the effective lock is min(EXTENDED_COINBASE_MATURITY, blocks until expiry). The expiry is public in chainparams, so an opportunistic miner can mine the tail of the window and pay almost nothing, while miners present at activation absorb the full six months.

Is the truncation deliberate? Attaching the maturity to when the output was created rather than when it is spent would make the cost uniform, at the price of outliving RDTS.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was intentional, but it's certainly still open for discussion

* 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
16 changes: 12 additions & 4 deletions src/consensus/tx_verify.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,12 @@ bool Consensus::CheckOutputSizes(const CTransaction& tx, TxValidationState& stat
return true;
}

bool Consensus::CheckTxInputs(const CTransaction& tx, TxValidationState& state, const CCoinsViewCache& inputs, int nSpendHeight, CAmount& txfee, const CheckTxInputsRules rules)
int Consensus::RequiredCoinbaseMaturity(int coinbase_height, int extended_maturity_start_height)
{
return coinbase_height >= extended_maturity_start_height ? EXTENDED_COINBASE_MATURITY : COINBASE_MATURITY;
}

bool Consensus::CheckTxInputs(const CTransaction& tx, TxValidationState& state, const CCoinsViewCache& inputs, int nSpendHeight, CAmount& txfee, const CheckTxInputsRules rules, const int extended_maturity_start_height)
{
// are the actual inputs available?
if (!inputs.HaveInputs(tx)) {
Expand All @@ -192,9 +197,12 @@ bool Consensus::CheckTxInputs(const CTransaction& tx, TxValidationState& state,
assert(!coin.IsSpent());

// If prev is coinbase, check that it's matured
if (coin.IsCoinBase() && nSpendHeight - coin.nHeight < COINBASE_MATURITY) {
return state.Invalid(TxValidationResult::TX_PREMATURE_SPEND, "bad-txns-premature-spend-of-coinbase",
strprintf("tried to spend coinbase at depth %d", nSpendHeight - coin.nHeight));
if (coin.IsCoinBase()) {
const int maturity{RequiredCoinbaseMaturity(coin.nHeight, extended_maturity_start_height)};
if (nSpendHeight - coin.nHeight < maturity) {
return state.Invalid(TxValidationResult::TX_PREMATURE_SPEND, "bad-txns-premature-spend-of-coinbase",
strprintf("tried to spend coinbase at depth %d (maturity %d)", nSpendHeight - coin.nHeight, maturity));
}
}

// Check for negative or overflow input values
Expand Down
13 changes: 12 additions & 1 deletion src/consensus/tx_verify.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,24 @@ namespace Consensus {
*/
bool CheckOutputSizes(const CTransaction& tx, TxValidationState& state);

/**
* Number of blocks a coinbase output created at coinbase_height must be buried
* under before it may be spent: EXTENDED_COINBASE_MATURITY for outputs created
* at or after extended_maturity_start_height, COINBASE_MATURITY otherwise.
* Callers pass ExtendedCoinbaseMaturityStartHeight() (validation.h) for the
* spending block, which is std::numeric_limits<int>::max() (no output
* qualifies) whenever the extended-maturity soft fork is not active for it.
*/
int RequiredCoinbaseMaturity(int coinbase_height, int extended_maturity_start_height);

/**
* Check whether all inputs of this transaction are valid (no double spends and amounts)
* This does not modify the UTXO set. This does not check scripts and sigs.
* @param[out] txfee Set to the transaction fee if successful.
* @param[in] extended_maturity_start_height See RequiredCoinbaseMaturity.
* Preconditions: tx.IsCoinBase() is false.
*/
[[nodiscard]] bool CheckTxInputs(const CTransaction& tx, TxValidationState& state, const CCoinsViewCache& inputs, int nSpendHeight, CAmount& txfee, CheckTxInputsRules rules);
[[nodiscard]] bool CheckTxInputs(const CTransaction& tx, TxValidationState& state, const CCoinsViewCache& inputs, int nSpendHeight, CAmount& txfee, CheckTxInputsRules rules, int extended_maturity_start_height);
} // namespace Consensus

/** Auxiliary functions for transaction validation (ideally should not be exposed) */
Expand Down
6 changes: 6 additions & 0 deletions src/interfaces/chain.h
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,12 @@ class Chain
//! pruned), and contains transactions.
virtual bool haveBlockOnDisk(int height) = 0;

//! Number of blocks a coinbase output created at the given height must
//! be buried under before it can be spent in the block following the
//! current chain tip: COINBASE_MATURITY, or EXTENDED_COINBASE_MATURITY
//! while the temporary extended-maturity soft fork applies to it.
virtual int coinbaseMaturity(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
9 changes: 9 additions & 0 deletions src/node/interfaces.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
#include <chainparamsbase.h>
#include <common/args.h>
#include <common/pcp.h>
#include <consensus/consensus.h>
#include <consensus/merkle.h>
#include <consensus/tx_verify.h>
#include <consensus/validation.h>
#include <deploymentstatus.h>
#include <external_signer.h>
Expand Down Expand Up @@ -581,6 +583,13 @@ class ChainImpl : public Chain
const CBlockIndex* block{chainman().ActiveChain()[height]};
return block && ((block->nStatus & BLOCK_HAVE_DATA) != 0) && block->nTx > 0;
}
int coinbaseMaturity(int coinbase_height) override
{
LOCK(::cs_main);
const CBlockIndex* tip{chainman().ActiveChain().Tip()};
if (!tip) return COINBASE_MATURITY;
return Consensus::RequiredCoinbaseMaturity(coinbase_height, ExtendedCoinbaseMaturityStartHeight(chainman().GetConsensus(), *tip));
}
bool pruneLockExists(const std::string& name) const override
{
LOCK(cs_main);
Expand Down
7 changes: 6 additions & 1 deletion src/qt/transactiondesc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,12 @@ 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); 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