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

- Rolling coinbase maturity (1 year). On mainnet, from block 979920, every
generated coin mined at height 973440 or later must be buried 52560 blocks
(365 days of 144 blocks) before it can be spent. Each reward counts from its
own block, so exactly one reward unlocks per block, the first at 1026000.

This replaces the shared release of the existing long coinbase maturity
window: the rewards it holds (973440 to 979919) are no longer all released
together at block 979920. Through block 979919 the rules are unchanged.
Rewards mined before 973440 keep ordinary 100-block maturity.

This rule is separate from RDTS. It is decided by block heights alone and
does not depend on `RdtsExpiryTime` or the BLAKE2b fork, so it stays in force
whether RDTS is extended or lapses.

Coverage ends with issuance: rewards mined at height 6930000 (the first
zero-subsidy height, 33 halvings) or later keep the maturity the other rules
require. The end is set by the height a reward was mined at, not the height
it is spent at, so every covered reward still waits its full 52560 blocks and
nothing is released early. The last covered reward, mined at 6929999,
unlocks at 6982559.

Upgrading nodes check every block from 979920 again using the existing
chainstate revalidation, under the marker `rolling_coinbase_maturity_1y`.
A node that upgrades before block 979920 rewinds nothing. A pruned node that
upgrades later and no longer has those blocks must reindex.

`getdeploymentinfo` reports the rule as `rolling_coinbase_maturity`, and
`getblocktemplate` lists `rolling_coinbase_maturity` in `rules` once it is
active. Wallets and the GUI show the maturity that applies to each reward.
Testnet4, signet and regtest are unchanged; regtest can opt in with
`-testcoinbasematurityrolling=<start_height>:<enforce_height>:<maturity>[:<end_coin_height>]`.
31 changes: 31 additions & 0 deletions src/chainparams.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,37 @@ void ReadRegTestArgs(const ArgsManager& args, CChainParams::RegTestOptions& opti
options.coinbase_maturity_long_release_height = release_height;
}

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

int32_t start_height;
int32_t enforce_height;
int32_t maturity;
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 -testcoinbasematurityrolling=<start_height>:<enforce_height>:<maturity>[:<end_coin_height>].", 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 -testcoinbasematurityrolling=<start_height>:<enforce_height>:<maturity>[:<end_coin_height>].", values[1]));
}
if (!ParseInt32(values[2], &maturity) || maturity <= COINBASE_MATURITY || maturity >= std::numeric_limits<int>::max()) {
throw std::runtime_error(strprintf("Invalid maturity (%s) for -testcoinbasematurityrolling=<start_height>:<enforce_height>:<maturity>[:<end_coin_height>]: must exceed COINBASE_MATURITY.", values[2]));
}
if (values.size() == 4) {
int32_t end_coin_height;
if (!ParseInt32(values[3], &end_coin_height) || end_coin_height <= start_height || end_coin_height >= std::numeric_limits<int>::max()) {
throw std::runtime_error(strprintf("Invalid end coin height (%s) for -testcoinbasematurityrolling=<start_height>:<enforce_height>:<maturity>[:<end_coin_height>]: must exceed start_height.", values[3]));
}
options.coinbase_maturity_rolling_end = end_coin_height;
}

options.coinbase_maturity_rolling_start_height = start_height;
options.coinbase_maturity_rolling_enforce_height = enforce_height;
options.coinbase_maturity_rolling = maturity;
}

for (const std::string& strDeployment : args.GetArgs("-vbparams")) {
std::vector<std::string> vDeploymentParams = SplitString(strDeployment, ':');
if (vDeploymentParams.size() < 3 || 7 < vDeploymentParams.size()) {
Expand Down
1 change: 1 addition & 0 deletions src/chainparamsbase.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +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("-testcoinbasematurityrolling=<start_height>:<enforce_height>:<maturity>[:<end_coin_height>]", "Set rolling coinbase maturity parameters: from <enforce_height> on, generated coins mined at or above <start_height> (and, if given, below <end_coin_height>) need <maturity> blocks. 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>:<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("-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
43 changes: 43 additions & 0 deletions src/consensus/params.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include <consensus/consensus.h>
#include <uint256.h>

#include <algorithm>
#include <chrono>
#include <limits>
#include <map>
Expand Down Expand Up @@ -161,6 +162,48 @@ struct Params {
{
return height >= CoinbaseMaturityLongEnforceHeight && height < CoinbaseMaturityLongReleaseHeight;
}
/**
* Rolling coinbase maturity.
*
* From CoinbaseMaturityRollingEnforceHeight on, every generated coin mined
* at or above CoinbaseMaturityRollingStartHeight must be
* CoinbaseMaturityRolling blocks deep before it can be spent. Each coin is
* measured from its own block, so rewards unlock one per block rather than
* together at a shared release height.
*
* This is independent of RDTS: it is height-based, has no expiry and does
* not depend on RdtsExpiryTime. It only ever adds to the maturity required
* by the rules above, never reduces it. Unscheduled by default.
*
* Coins mined at or above CoinbaseMaturityRollingEndCoinHeight are not
* covered: they keep the maturity required by the rules above. This bounds
* the coverage (e.g. at the first zero-subsidy height, where there is no
* subsidy left to protect) without ever releasing a covered coin early.
*/
int CoinbaseMaturityRolling{COINBASE_MATURITY};
int CoinbaseMaturityRollingStartHeight{std::numeric_limits<int>::max()};
int CoinbaseMaturityRollingEnforceHeight{std::numeric_limits<int>::max()};
int CoinbaseMaturityRollingEndCoinHeight{std::numeric_limits<int>::max()};
bool CoinbaseMaturityRollingActiveAt(int height) const
{
return height >= CoinbaseMaturityRollingEnforceHeight;
}
/**
* Consensus maturity a block at spend_height requires of a generated coin
* mined at coin_height, under all coinbase maturity rules. Validation
* passes the same values to Consensus::CheckTxInputs.
*/
int CoinbaseMaturityRequired(int coin_height, int spend_height) const
{
int maturity{COINBASE_MATURITY};
if (CoinbaseMaturityLongActiveAt(spend_height) && coin_height >= CoinbaseMaturityLongStartHeight) {
maturity = CoinbaseMaturityLong;
}
if (CoinbaseMaturityRollingActiveAt(spend_height) && coin_height >= CoinbaseMaturityRollingStartHeight && coin_height < CoinbaseMaturityRollingEndCoinHeight) {
maturity = std::max(maturity, CoinbaseMaturityRolling);
}
return maturity;
}
std::vector<ChainstateRevalidationDeployment> chainstate_revalidation_deployments;
/** Don't warn about unknown BIP 9 activations below this height.
* This prevents us from warning about the CSV and segwit activations. */
Expand Down
12 changes: 10 additions & 2 deletions src/consensus/tx_verify.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
#include <util/check.h>
#include <util/moneystr.h>

#include <algorithm>

bool IsFinalTx(const CTransaction &tx, int nBlockHeight, int64_t nBlockTime)
{
if (tx.nLockTime == 0)
Expand Down Expand Up @@ -174,7 +176,10 @@ 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)
const int long_maturity_start_height,
const int coinbase_maturity_rolling,
const int rolling_maturity_start_height,
const int rolling_maturity_end_coin_height)
{
// are the actual inputs available?
if (!inputs.HaveInputs(tx)) {
Expand All @@ -196,7 +201,10 @@ bool Consensus::CheckTxInputs(const CTransaction& tx, TxValidationState& state,
// If prev is coinbase, check that it's matured
if (coin.IsCoinBase()) {
const int actual_maturity{nSpendHeight - (int)coin.nHeight};
const int required_maturity{(coin.nHeight >= long_maturity_start_height) ? coinbase_maturity_long : COINBASE_MATURITY};
int required_maturity{(coin.nHeight >= long_maturity_start_height) ? coinbase_maturity_long : COINBASE_MATURITY};
if ((int)coin.nHeight >= rolling_maturity_start_height && (int)coin.nHeight < rolling_maturity_end_coin_height) {
required_maturity = std::max(required_maturity, coinbase_maturity_rolling);
}
if (actual_maturity < required_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
14 changes: 13 additions & 1 deletion src/consensus/tx_verify.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
#define BITCOIN_CONSENSUS_TX_VERIFY_H

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

#include <limits>
#include <stdint.h>
#include <vector>

Expand Down Expand Up @@ -56,11 +58,21 @@ bool CheckOutputSizes(const CTransaction& tx, TxValidationState& state);
* @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] coinbase_maturity_rolling Maturity required by the rolling-maturity rule. It is
* only ever applied as a minimum on top of the rules above.
* @param[in] rolling_maturity_start_height First coinbase height requiring coinbase_maturity_rolling,
* or std::numeric_limits<int>::max() to disable.
* @param[in] rolling_maturity_end_coin_height First coinbase height no longer requiring
* coinbase_maturity_rolling, or
* std::numeric_limits<int>::max() for no end.
* 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);
int long_maturity_start_height,
int coinbase_maturity_rolling = COINBASE_MATURITY,
int rolling_maturity_start_height = std::numeric_limits<int>::max(),
int rolling_maturity_end_coin_height = std::numeric_limits<int>::max());
} // namespace Consensus

/** Auxiliary functions for transaction validation (ideally should not be exposed) */
Expand Down
7 changes: 4 additions & 3 deletions src/interfaces/chain.h
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,10 @@ 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;
//! Number of blocks policy requires a coinbase output mined at
//! coin_height to be buried under before the wallet should treat it as
//! spendable.
virtual int coinbaseMaturity(int coin_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;
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;
//! Get coinbase maturity for a generated coin mined at coin_height.
virtual int coinbaseMaturity(int coin_height) = 0;

//! Get verification progress.
virtual double getVerificationProgress() = 0;
Expand Down
34 changes: 34 additions & 0 deletions src/kernel/chainparams.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,17 @@ static void AddLongCoinbaseMaturityRevalidationDeployment(Consensus::Params& con
});
}

static void AddRollingCoinbaseMaturityRevalidationDeployment(Consensus::Params& consensus)
{
// No stop height: the rule never expires, so every block from the
// enforcement height on is checked again by an upgrading node. Blocks
// before it are unchanged, so a node upgrading earlier rewinds nothing.
consensus.chainstate_revalidation_deployments.push_back({
.name = "rolling_coinbase_maturity_1y",
.start_height = consensus.CoinbaseMaturityRollingEnforceHeight,
});
}

/**
* Main network on which people trade goods and services.
*/
Expand Down Expand Up @@ -146,6 +157,19 @@ class CMainParams : public CChainParams {
consensus.CoinbaseMaturityLong = consensus.CoinbaseMaturityLongReleaseHeight - consensus.CoinbaseMaturityLongStartHeight;
AddLongCoinbaseMaturityRevalidationDeployment(consensus);

// Rolling coinbase maturity: every reward from the start of the long
// maturity window above matures over 52560 blocks (365 days of 144
// blocks) from its own block, with no expiry. It takes over at the
// window's release height, so the rewards the window holds are never
// released together there: one reward unlocks per block, the first at
// 1026000. Through 979919 the rules are unchanged.
consensus.CoinbaseMaturityRollingStartHeight = consensus.CoinbaseMaturityLongStartHeight;
consensus.CoinbaseMaturityRollingEnforceHeight = consensus.CoinbaseMaturityLongReleaseHeight;
consensus.CoinbaseMaturityRolling = 52560;
// first zero-subsidy height (33 halvings), so the rule retires together with issuance
consensus.CoinbaseMaturityRollingEndCoinHeight = 6930000;
AddRollingCoinbaseMaturityRevalidationDeployment(consensus);

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

Expand Down Expand Up @@ -694,6 +718,16 @@ class CRegTestParams : public CChainParams
AddLongCoinbaseMaturityRevalidationDeployment(consensus);
}

if (opts.coinbase_maturity_rolling_start_height) {
consensus.CoinbaseMaturityRollingStartHeight = *opts.coinbase_maturity_rolling_start_height;
consensus.CoinbaseMaturityRollingEnforceHeight = *opts.coinbase_maturity_rolling_enforce_height;
consensus.CoinbaseMaturityRolling = *opts.coinbase_maturity_rolling;
if (opts.coinbase_maturity_rolling_end) {
consensus.CoinbaseMaturityRollingEndCoinHeight = *opts.coinbase_maturity_rolling_end;
}
AddRollingCoinbaseMaturityRevalidationDeployment(consensus);
}

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
4 changes: 4 additions & 0 deletions src/kernel/chainparams.h
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,10 @@ 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_rolling_start_height{};
std::optional<int> coinbase_maturity_rolling_enforce_height{};
std::optional<int> coinbase_maturity_rolling{};
std::optional<int> coinbase_maturity_rolling_end{};
};

static std::unique_ptr<const CChainParams> RegTest(const RegTestOptions& options);
Expand Down
Loading