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
14 changes: 14 additions & 0 deletions doc/release-notes-longmaturity-rdts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
Long coinbase maturity: one year, expiring with RDTS
----------------------------------------------------

The temporary softfork that Bitcoin Knots 29.4.2 deployed to hold generated bitcoins for longer is extended: instead of ending at block 979920, it expires together with RDTS. Bitcoins generated by block 973440 or any later block cannot be spent in any block whose parent has a median time past earlier than 2027 September 1st 00:00 UTC. From the first block past that, they are spendable at the original maturity of 100 blocks.

This is a softfork on top of 29.4.2. Up to block 979919 the rules are the same, so nothing changes until then. Block 979920 is the first that 29.4.2 could accept and this version reject (one spending bitcoins generated since block 973440), so miners and nodes need to upgrade before it, expected around the end of October at the current block rate (the date moves with the hashrate).

A 29.4.2 node that upgrades only after block 979920 rechecks the blocks it connected from that height on at its first start: it rewinds to block 979919 and reconnects everything above it. A node coming from 29.4.1 or from another implementation has no record of having checked the long maturity rule at all, so it rewinds to block 973439 instead, as it would when upgrading to 29.4.2. Either way the rewind needs the block and undo data of those blocks, so a pruned node that upgrades late may have to reindex. Upgrading before block 979920 avoids all of this.

Policy and the wallet now apply the same delay to every coin generated by block 973440 or later, measured in time rather than in blocks: the mempool does not relay a spend of such a coin until the median time past of the chain tip is at least one year (365 days) past that of the block that generated it, and the wallet counts the coin as immature until then. Coins generated before block 973440 need only the original maturity of 100 blocks; 29.4.2 policy held them for 6480 blocks, which they all reach by block 979919. For the covered coins this is never sooner than the consensus expiry, which the mempool also enforces directly, so a wallet holding them can no longer select one that consensus would still reject. The GUI shows how many days such a coin still has to wait; the number of blocks to maturity shown for it elsewhere is an estimate, updated with every block.

A spend held by the consensus rule is rejected as `bad-txns-premature-spend-of-coinbase-long` and one held by the policy delay as `premature-spend-of-coinbase-long`; the original 100-block rule keeps `bad-txns-premature-spend-of-coinbase`.

`getdeploymentinfo` reports the expiry as `expiry_time` for `long_coinbase_maturity`, the same field RDTS uses, and no longer reports `height_end` or `maturity` for it. The regtest option `-testcoinbasematuritylong` now takes `<start_height>:<enforce_height>:<expiry_time>`.
18 changes: 7 additions & 11 deletions src/chainparams.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

#include <chainparamsbase.h>
#include <common/args.h>
#include <consensus/consensus.h>
#include <consensus/params.h>
#include <deploymentinfo.h>
#include <logging.h>
Expand Down Expand Up @@ -104,28 +103,25 @@ 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));
throw std::runtime_error(strprintf("Invalid format (%s) for -testcoinbasematuritylong=<start_height>:<enforce_height>:<expiry_time>.", *arg));
}

int32_t start_height;
int32_t enforce_height;
int32_t release_height;
int64_t expiry_time;
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>:<expiry_time>.", 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>:<expiry_time>.", 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]));
}
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 (!ParseInt64(values[2], &expiry_time) || expiry_time <= 0) {
throw std::runtime_error(strprintf("Invalid expiry time (%s) for -testcoinbasematuritylong=<start_height>:<enforce_height>:<expiry_time>.", values[2]));
}

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

for (const std::string& strDeployment : args.GetArgs("-vbparams")) {
Expand Down
2 changes: 1 addition & 1 deletion src/chainparamsbase.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +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=<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("-testcoinbasematuritylong=<start_height>:<enforce_height>:<expiry_time>", "Set long coinbase maturity parameters: from block <enforce_height>, generated coins of block <start_height> or later cannot be spent until the parent block's median-time-past reaches <expiry_time>. 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
35 changes: 30 additions & 5 deletions src/consensus/params.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
#ifndef BITCOIN_CONSENSUS_PARAMS_H
#define BITCOIN_CONSENSUS_PARAMS_H

#include <consensus/consensus.h>
#include <uint256.h>

#include <chrono>
Expand Down Expand Up @@ -153,13 +152,39 @@ struct Params {
* behaviour is unchanged on chains that do not set it.
*/
int64_t RdtsExpiryTime{std::numeric_limits<int64_t>::min()};
int CoinbaseMaturityLong{COINBASE_MATURITY};
/**
* Long coinbase maturity temporary softfork.
*
* From CoinbaseMaturityLongEnforceHeight, coinbases of height
* CoinbaseMaturityLongStartHeight or later cannot be spent at any depth
* until the rule expires. A block is past expiry when its parent's
* median-time-past has reached CoinbaseMaturityLongReleaseTime (the same
* boundary as RdtsExpiryTime), so the boundary is monotone along any
* chain and the rules for the next block are knowable in advance. Every
* other coinbase needs COINBASE_MATURITY as always.
*
* The defaults leave the rule unscheduled.
*/
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
int64_t CoinbaseMaturityLongReleaseTime{std::numeric_limits<int64_t>::min()};
bool CoinbaseMaturityLongScheduled() const
{
return CoinbaseMaturityLongStartHeight != std::numeric_limits<int>::max();
}
/** Whether the rule applies to a block at the given height whose parent has the given median-time-past. */
bool CoinbaseMaturityLongActiveAt(int height, int64_t mtp_prev) const
{
return height >= CoinbaseMaturityLongEnforceHeight && mtp_prev < CoinbaseMaturityLongReleaseTime;
}
/**
* First coinbase height that cannot be spent in a block at the given
* height whose parent has the given median-time-past, or
* std::numeric_limits<int>::max() when the rule does not apply to it.
*/
int CoinbaseMaturityLongHeldFrom(int height, int64_t mtp_prev) const
{
return height >= CoinbaseMaturityLongEnforceHeight && height < CoinbaseMaturityLongReleaseHeight;
return CoinbaseMaturityLongActiveAt(height, mtp_prev) ? CoinbaseMaturityLongStartHeight : std::numeric_limits<int>::max();
}
std::vector<ChainstateRevalidationDeployment> chainstate_revalidation_deployments;
/** Don't warn about unknown BIP 9 activations below this height.
Expand Down
8 changes: 5 additions & 3 deletions src/consensus/tx_verify.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,6 @@ bool Consensus::CheckOutputSizes(const CTransaction& tx, TxValidationState& stat
}

bool Consensus::CheckTxInputs(const CTransaction& tx, TxValidationState& state, const CCoinsViewCache& inputs, int nSpendHeight, CAmount& txfee, const CheckTxInputsRules rules,
const int coinbase_maturity_long,
const int long_maturity_start_height)
{
// are the actual inputs available?
Expand All @@ -195,9 +194,12 @@ bool Consensus::CheckTxInputs(const CTransaction& tx, TxValidationState& state,

// If prev is coinbase, check that it's matured
if (coin.IsCoinBase()) {
if (coin.nHeight >= static_cast<uint32_t>(long_maturity_start_height)) {
return state.Invalid(TxValidationResult::TX_PREMATURE_SPEND, "bad-txns-premature-spend-of-coinbase-long",
strprintf("tried to spend coinbase of block %d, held by the long coinbase maturity rule", coin.nHeight));
}
const int actual_maturity{nSpendHeight - (int)coin.nHeight};
const int required_maturity{(coin.nHeight >= long_maturity_start_height) ? coinbase_maturity_long : COINBASE_MATURITY};
if (actual_maturity < required_maturity) {
if (actual_maturity < COINBASE_MATURITY) {
return state.Invalid(TxValidationResult::TX_PREMATURE_SPEND, "bad-txns-premature-spend-of-coinbase",
strprintf("tried to spend coinbase at depth %d", actual_maturity));
}
Expand Down
6 changes: 2 additions & 4 deletions src/consensus/tx_verify.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,11 @@ bool CheckOutputSizes(const CTransaction& tx, TxValidationState& state);
* 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] coinbase_maturity_long Maturity required by the long-maturity rule.
* @param[in] long_maturity_start_height First coinbase height requiring coinbase_maturity_long,
* or std::numeric_limits<int>::max() to disable.
* @param[in] long_maturity_start_height Coinbases of this height or later cannot be spent at any depth
* (long coinbase maturity rule), or std::numeric_limits<int>::max() to disable.
* Preconditions: tx.IsCoinBase() is false.
*/
[[nodiscard]] bool CheckTxInputs(const CTransaction& tx, TxValidationState& state, const CCoinsViewCache& inputs, int nSpendHeight, CAmount& txfee, CheckTxInputsRules rules,
int coinbase_maturity_long,
int long_maturity_start_height);
} // namespace Consensus

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

//! Number of blocks policy requires a coinbase output to be buried under
//! before the wallet should treat it as spendable.
virtual int coinbaseMaturity() = 0;
//! Whether the long coinbase maturity rule is scheduled on this chain.
virtual bool coinbaseMaturityLongScheduled() = 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;
Expand Down
4 changes: 2 additions & 2 deletions src/interfaces/node.h
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,8 @@ class Node
//! Get last block time.
virtual int64_t getLastBlockTime() = 0;

//! Get coinbase maturity.
virtual int coinbaseMaturity() = 0;
//! Whether the long coinbase maturity rule is scheduled on this chain.
virtual bool coinbaseMaturityLongScheduled() = 0;

//! Get verification progress.
virtual double getVerificationProgress() = 0;
Expand Down
2 changes: 2 additions & 0 deletions src/interfaces/wallet.h
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,8 @@ struct WalletTxStatus
{
int block_height;
int blocks_to_maturity;
//! Seconds of median time past the long coinbase maturity policy hold still has to run, 0 if none
int64_t maturity_time_left;
int depth_in_main_chain;
unsigned int time_received;
uint32_t lock_time;
Expand Down
17 changes: 10 additions & 7 deletions src/kernel/chainparams.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,14 @@ static CBlock CreateGenesisBlock(uint32_t nTime, uint32_t nNonce, uint32_t nBits

static void AddLongCoinbaseMaturityRevalidationDeployment(Consensus::Params& consensus)
{
// The rule expires by time, so the last height it applies to depends on
// the chain: every block from the enforce height on may need checking.
// Blocks keep being marked as checked under this name after the expiry,
// so an extension of the rule needs a new deployment name.
consensus.chainstate_revalidation_deployments.push_back({
.name = "long_coinbase_maturity",
.start_height = consensus.CoinbaseMaturityLongEnforceHeight,
.stop_height = consensus.CoinbaseMaturityLongReleaseHeight - 1,
.stop_height = std::numeric_limits<int>::max(),

@kwsantiago kwsantiago Sep 23, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

With no stop height, a node with no marker takes the enforce height as its rewind target, so a late upgrade rewinds from wherever its tip is and that cost grows without bound.

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.

The stop height doesn't set the rewind. It always runs from the tip down to the lowest block the node's marker doesn't cover, so a node with no marker rewinds to 973439 with or without a stop height, exactly as it would upgrading to 29.4.2, and a 29.4.2 node rewinds to 979919 either way. What bounds the cost is upgrading before 979920, where nothing needs rechecking.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This will keep flagging blocks as checked long after they actually stop getting checked... I suppose it's acceptable if there's no better way, but it means any extension would need a new name.

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.

Agreed, and the comment now says so: blocks keep being marked as checked under this name after the expiry, so an extension of the rule needs a new deployment name. A stop time next to the stop height in the revalidation framework would be the cleaner way if we ever need it; I did not want to touch the framework in this PR.

});
}

Expand Down Expand Up @@ -140,10 +144,10 @@ class CMainParams : public CChainParams {
// reaching ACTIVE, and that deployment has been removed.)
consensus.RdtsExpiryTime = 1819756800; // September 1st, 2027 00:00 UTC

// Long coinbase maturity: held until RDTS expires
consensus.CoinbaseMaturityLongStartHeight = 973440;
consensus.CoinbaseMaturityLongEnforceHeight = 973440;
consensus.CoinbaseMaturityLongReleaseHeight = 979920;
consensus.CoinbaseMaturityLong = consensus.CoinbaseMaturityLongReleaseHeight - consensus.CoinbaseMaturityLongStartHeight;
consensus.CoinbaseMaturityLongReleaseTime = consensus.RdtsExpiryTime;
AddLongCoinbaseMaturityRevalidationDeployment(consensus);

consensus.nMinimumChainWork = uint256{"00000000000000000000000000000000000000013e00277374c9f9eeadc70200"};
Expand Down Expand Up @@ -419,10 +423,10 @@ class CTestNet4Params : public CChainParams {
consensus.Blake2bHeight = 150308;
consensus.RdtsExpiryTime = 1791903600; // October 13th, 2026 15:00:00 UTC

// Long coinbase maturity: held until RDTS expires
consensus.CoinbaseMaturityLongStartHeight = 151406;
consensus.CoinbaseMaturityLongEnforceHeight = 151550;
consensus.CoinbaseMaturityLongReleaseHeight = 158111;
consensus.CoinbaseMaturityLong = consensus.CoinbaseMaturityLongReleaseHeight - consensus.CoinbaseMaturityLongStartHeight;
consensus.CoinbaseMaturityLongReleaseTime = consensus.RdtsExpiryTime;
AddLongCoinbaseMaturityRevalidationDeployment(consensus);

consensus.nMinimumChainWork = uint256{"0000000000000000000000000000000000000000000001d6dce8651b6094e4c1"};
Expand Down Expand Up @@ -689,8 +693,7 @@ class CRegTestParams : public CChainParams
if (opts.coinbase_maturity_long_start_height) {
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.CoinbaseMaturityLongReleaseTime = *opts.coinbase_maturity_long_release_time;
AddLongCoinbaseMaturityRevalidationDeployment(consensus);
}

Expand Down
4 changes: 3 additions & 1 deletion src/kernel/chainparams.h
Original file line number Diff line number Diff line change
Expand Up @@ -171,9 +171,11 @@ class CChainParams
//! value (see -rdtsexpiry). Requires a blake2b activation height.
std::optional<int64_t> rdts_expiry_time{};
std::optional<std::vector<unsigned char>> blake2b_headline{};
//! Long coinbase maturity schedule (see -testcoinbasematuritylong):
//! all three are set together.
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<int64_t> coinbase_maturity_long_release_time{};
};

static std::unique_ptr<const CChainParams> RegTest(const RegTestOptions& options);
Expand Down
6 changes: 3 additions & 3 deletions src/node/interfaces.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ class NodeImpl : public Node
}
return chainman().GetParams().GenesisBlock().GetBlockTime(); // Genesis block's time of current network
}
int coinbaseMaturity() override { return chainman().GetConsensus().CoinbaseMaturityLong; }
bool coinbaseMaturityLongScheduled() override { return chainman().GetConsensus().CoinbaseMaturityLongScheduled(); }
double getVerificationProgress() override
{
return chainman().GuessVerificationProgress(WITH_LOCK(chainman().GetMutex(), return chainman().ActiveChain().Tip()));
Expand Down Expand Up @@ -582,9 +582,9 @@ class ChainImpl : public Chain
const CBlockIndex* block{chainman().ActiveChain()[height]};
return block && ((block->nStatus & BLOCK_HAVE_DATA) != 0) && block->nTx > 0;
}
int coinbaseMaturity() override
bool coinbaseMaturityLongScheduled() override
{
return chainman().GetConsensus().CoinbaseMaturityLong;
return chainman().GetConsensus().CoinbaseMaturityLongScheduled();
}
bool pruneLockExists(const std::string& name) const override
{
Expand Down
Loading