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
55 changes: 55 additions & 0 deletions doc/release-notes-batched-coinbase-maturity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
### Batched extended coinbase maturity (temporary softfork)

Coinbase outputs created while this deployment is active use a longer
maturity that is attached to the creating block, not to the spend:

- ~1/6 of window blocks (`creating_block_hash % 6 == 0`): 2016 confirmations
- ~2/6 of window blocks (`% 6` in {1,2}): 4032 confirmations
- ~1/2 of window blocks (`% 6` in {3,4,5}): 8064 confirmations

The assignment is `GetUint64(0) % 6` of the creating block's hash, so
the tranche is unknown until proof-of-work is found (miners cannot pick
a short height in advance). Coinbase transaction size does not change.
There is no extra output and no DATUM/script change.

The window is the same shape as RDTS: first block whose parent MTP
reaches the start time, through the last block whose parent MTP is still
below the RDTS expiry. After expiry, **new** coinbases go back to the
ordinary 100-block maturity. Coinbases created *inside* the window keep
their 2016/4032/8064 lock until they actually mature — expiry does not
unlock them early.

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 as a floor.

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 until
their assigned tranche matures (or, for coins created after expiry, 100
blocks). 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, if that reward was created in the window.

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;
after this, *new* coinbases use 100), `active` (for the next block's
*creation* rule) 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
new coinbases are still being created under the rule; no client-side
support is required.

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

if (const auto arg{args.GetArg("-extendedcoinbasematurity", "")}; !arg.empty()) {
if (!options.rdts_expiry_time) {
throw std::runtime_error("-extendedcoinbasematurity requires -rdtsexpiry=<time> (window ends at RDTS expiry).");
}
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
1 change: 1 addition & 0 deletions src/chainparamsbase.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,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>", "Schedule the batched extended coinbase-maturity softfork: coinbases created from the first block whose parent MTP reaches <time> use the 2016/4032/8064 schedule from creating-block hash % 6 (~1/6, ~2/6, ~1/2 of blocks). The lock sticks to those coins after RDTS expiry. Requires -rdtsexpiry, which <time> must precede. (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("-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
7 changes: 7 additions & 0 deletions src/consensus/consensus.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ 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;
/** Batched extended coinbase maturity (temporary softfork). Assigned by
* creating-block hash GetUint64(0) % 6 (low 64 bits / displayed-hash tail)
* so ~1/6 of window coinbases mature at 2016, ~2/6 at 4032, and ~1/2 at
* 8064. The high word is unusable: PoW zeros it. No extra coinbase outputs. */
static const int EXTENDED_COINBASE_MATURITY_SHORT = 2016;
static const int EXTENDED_COINBASE_MATURITY_MID = 4032;
static const int EXTENDED_COINBASE_MATURITY_LONG = 8064;

static const int WITNESS_SCALE_FACTOR = 4;

Expand Down
17 changes: 17 additions & 0 deletions src/consensus/params.h
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,15 @@ struct Params {
* behaviour is unchanged on chains that do not set it.
*/
int64_t RdtsExpiryTime{std::numeric_limits<int64_t>::min()};
/**
* Batched extended coinbase maturity. Coinbase outputs created in a
* block whose parent's median-time-past has reached this start time
* and has not yet reached RdtsExpiryTime keep a longer maturity
* (see RequiredCoinbaseMaturity) even after RDTS expires. Coinbases
* created after expiry use COINBASE_MATURITY again.
* Default leaves the deployment unscheduled.
*/
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 +204,14 @@ struct Params {
{
return IsBlake2bHeight(height) && mtp_prev < RdtsExpiryTime;
}

/** Whether a coinbase created in a block with this parent MTP is
* subject to the batched extended-maturity schedule. The schedule
* sticks to those coins after expiry. */
bool ExtendedCoinbaseMaturityCreatedAt(int64_t mtp_prev) const
{
return mtp_prev >= ExtendedCoinbaseMaturityStartTime && mtp_prev < RdtsExpiryTime;
}
};

} // namespace Consensus
Expand Down
43 changes: 39 additions & 4 deletions src/consensus/tx_verify.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include <primitives/transaction.h>
#include <script/interpreter.h>
#include <util/check.h>
#include <uint256.h>
#include <util/moneystr.h>

bool IsFinalTx(const CTransaction &tx, int nBlockHeight, int64_t nBlockTime)
Expand Down Expand Up @@ -172,7 +173,31 @@ 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::CoinbaseHashMod6(const uint256& creating_block_hash)
{
// Low 64 bits of the uint256 (tail of the displayed hash), not the
// PoW-zeroed high word. GetUint64(3) cannot produce residues 4 or 5
// at current mainnet difficulty; GetUint64(0) stays uniform.
return static_cast<int>(creating_block_hash.GetUint64(0) % 6);
}

int Consensus::RequiredCoinbaseMaturity(int coinbase_height, int ext_start_height, int ext_expiry_height, int hash_mod6)
{
if (coinbase_height < ext_start_height || coinbase_height >= ext_expiry_height) {
return COINBASE_MATURITY;
}
// ~1/6 of window blocks: 2016; ~2/6: 4032; ~1/2: 8064. Residue is
// the creating block's hash (GetUint64(0) % 6), unknown until PoW.
const int batch = hash_mod6 % 6;
if (batch < 0) {
return EXTENDED_COINBASE_MATURITY_LONG;
}
if (batch == 0) return EXTENDED_COINBASE_MATURITY_SHORT;
if (batch == 1 || batch == 2) return EXTENDED_COINBASE_MATURITY_MID;
return EXTENDED_COINBASE_MATURITY_LONG;
}

bool Consensus::CheckTxInputs(const CTransaction& tx, TxValidationState& state, const CCoinsViewCache& inputs, int nSpendHeight, CAmount& txfee, const CheckTxInputsRules rules, int ext_start_height, int ext_expiry_height, const CBlockIndex* hash_tip)
{
// are the actual inputs available?
if (!inputs.HaveInputs(tx)) {
Expand All @@ -192,9 +217,19 @@ 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()) {
int hash_mod6 = 5;
if (coin.nHeight >= ext_start_height && coin.nHeight < ext_expiry_height) {
// No silent "long tranche" fallback: a missing tip would
// reject spends that the rest of the network accepts.
const CBlockIndex* created{Assert(hash_tip)->GetAncestor(coin.nHeight)};
hash_mod6 = CoinbaseHashMod6(Assert(created)->GetBlockHash());
}
const int maturity = RequiredCoinbaseMaturity(coin.nHeight, ext_start_height, ext_expiry_height, hash_mod6);
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
21 changes: 20 additions & 1 deletion src/consensus/tx_verify.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@

#include <consensus/amount.h>

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

class CBlockIndex;
class CCoinsViewCache;
class CTransaction;
class TxValidationState;
class uint256;

/** Transaction validation functions */

Expand Down Expand Up @@ -49,13 +51,30 @@ namespace Consensus {
*/
bool CheckOutputSizes(const CTransaction& tx, TxValidationState& state);

int CoinbaseHashMod6(const uint256& creating_block_hash);

/**
* Confirmations required before a coinbase created at coinbase_height may
* be spent. Coins created in [ext_start_height, ext_expiry_height) keep
* the batched extended schedule after RDTS expiry. hash_mod6 is
* CoinbaseHashMod6 of the creating block (0..5). Pass INT_MAX for both
* bounds when the deployment is unscheduled. hash_mod6 is ignored outside
* the window. Default 5 is only for unit tests that pass the residue
* explicitly.
*/
int RequiredCoinbaseMaturity(int coinbase_height, int ext_start_height, int ext_expiry_height, int hash_mod6 = 5);

/**
* 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[in] hash_tip Must be passed explicitly. Required (non-null) when a
* coinbase input was created inside the extended-maturity window.
* Creating-block hash is hash_tip->GetAncestor(coin.nHeight)->GetBlockHash().
* Callers that pass disabled (INT_MAX) bounds may pass nullptr.
* @param[out] txfee Set to the transaction fee if successful.
* 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 ext_start_height, int ext_expiry_height, const CBlockIndex* hash_tip);
} // namespace Consensus

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

//! Confirmations a coinbase created at coinbase_height still needs, plus
//! the usual +1 wallet convention applied by the caller if desired.
//! Returns the consensus maturity (100, 2016, 8064, or 26280).
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
6 changes: 6 additions & 0 deletions src/kernel/chainparams.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ class CMainParams : public CChainParams {
// via versionbits; the stall at 961633 prevented it from ever
// reaching ACTIVE, and that deployment has been removed.)
consensus.RdtsExpiryTime = 1819756800; // September 1st, 2027 00:00 UTC
// Batched extended coinbase maturity: start unset until flag day.
// consensus.ExtendedCoinbaseMaturityStartTime = ...;

consensus.nMinimumChainWork = uint256{"00000000000000000000000000000000000000013e00277374c9f9eeadc70200"};
consensus.defaultAssumeValid = uint256{"0000000000000078ed1e20cac1acf78df6d1060c78059fb6331e17141c881fc8"}; // 964264
Expand Down Expand Up @@ -665,6 +667,10 @@ class CRegTestParams : public CChainParams
consensus.RdtsExpiryTime = *opts.rdts_expiry_time;
}

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
4 changes: 4 additions & 0 deletions src/kernel/chainparams.h
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,10 @@ 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{};
//! Start MTP for the batched extended coinbase-maturity rule
//! (see -extendedcoinbasematurity). Requires rdts_expiry_time and
//! must precede it. Unscheduled when unset.
std::optional<int64_t> extended_coinbase_maturity_start_time{};
std::optional<std::vector<unsigned char>> blake2b_headline{};
};

Expand Down
20 changes: 20 additions & 0 deletions src/node/interfaces.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
#include <chainparamsbase.h>
#include <common/args.h>
#include <common/pcp.h>
#include <consensus/consensus.h>
#include <consensus/tx_verify.h>
#include <consensus/merkle.h>
#include <consensus/validation.h>
#include <deploymentstatus.h>
Expand Down Expand Up @@ -581,6 +583,24 @@ 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;
int ext_start, ext_expiry;
ExtendedCoinbaseMaturityBounds(chainman().GetConsensus(), *tip, ext_start, ext_expiry);
int hash_mod6 = 5;
if (coinbase_height >= ext_start && coinbase_height < ext_expiry) {
// Height comes from a wallet TxStateConfirmed. ActiveChain()[h]
// is the creating block while that coin still exists. A miss is a
// disconnect race (entry about to be dropped); do not crash RPC.
const CBlockIndex* created{chainman().ActiveChain()[coinbase_height]};
if (!created) return COINBASE_MATURITY;
hash_mod6 = Consensus::CoinbaseHashMod6(created->GetBlockHash());
}
return Consensus::RequiredCoinbaseMaturity(coinbase_height, ext_start, ext_expiry, hash_mod6);
}
bool pruneLockExists(const std::string& name) const override
{
LOCK(cs_main);
Expand Down
4 changes: 3 additions & 1 deletion src/qt/transactiondesc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,9 @@ QString TransactionDesc::toHTML(interfaces::Node& node, interfaces::Wallet& wall

if (wtx.is_coinbase)
{
quint32 numBlocksToMaturity = COINBASE_MATURITY + 1;
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