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 .github/workflows/run-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: Trigger Inherited CI

on:
push:
branches: [ main ]
workflow_dispatch: # Allows you to trigger this manually from the Actions UI

jobs:
run-bitcoin-ci:
# Syntax: OWNER/REPOSITORY/PATH@BRANCH
uses: FlyTheElephant1/bitcoin/.github/workflows/ci.yml@29.x-knots

# Optional: Automatically passes secrets if the inherited workflow requires any
secrets: inherit
53 changes: 53 additions & 0 deletions doc/release-notes-batched-coinbase-maturity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
### 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 (`(height - start) % 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 by block height, so 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/8064/26280 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 (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
6 changes: 6 additions & 0 deletions src/consensus/consensus.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ 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
* (coinbase_height - window_start) % 6 so 1/6 of window coinbases mature

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Is batching needed here? Attaching maturity to the creating height already staggers unlocks, one coinbase per block. The single-date cliff only exists when everything unlocks at the expiry.

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.

Well if there is a very long time where no coins reach maturity, then they all of a sudden start reaching maturity, it causes a large supply shock. The purpose of bringing some small amount forward of the mean and the bulk of it after the mean is to achieve the desired result without as high of a risk of causing a stall.

Prevents single calendar events whereby people will attempt to try and gamify the market dynamics by creating 3 different calendar events whereby some amount begin maturation. Also forces miners to begin thinking about things in multiple different future timeframes instead of one timeframe.

Single event date causes instability, and could lead to a stall (the stall condition is mostly solved in #403 solution in a different way than here)

@FlyTheElephant1 FlyTheElephant1 Sep 11, 2026 •

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.

This is an image of the per block maturation cycle. from activation to end of rdts, and from end of rtds until it is back to baseline.

(should be noted that the chart is slightly wrong here, there would be a 100 block period where the maturation rate would be 3.125 baseline after rdts ends before the first post rdts 100 block maturation begins happening simultaneous to the backlog)

maturation_schedule_symmetric

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 also fixes a maturation cliff present in #402 and #403 whereby 6480 blocks all mature in a single block at rdts expiry by extending the rule for grandfathered blocks only. to produce the above chart. (assuming all of the code works as intended.)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I could be missing something here, but along the same lines as kwsantiago's thoughts, wouldn't the 2016 tranche be gameable? If I had huge hashrate (10-15 ph) in a stratum pool I could mine every 6th block as hard as possible and mine a lot of them. Drop off in the other tranches and the DA could go down because minimal hashrate is mining in those tranches. This hurts those not gaming the system (the ones that the system is trying to protect) because they would find mainly those blocks in the long tranches. Worse, if the attacker wanted to do this for a few epochs and then just kill hashrate all together.

One way to possibly fix the "I know what tranche this block is in" is to use something miners know only after working on a block - hash % 6. hash % 6 would be statistically distributed, not as uniform as height based.

And back to kwsantiago's point, are tranches necessary?

@FlyTheElephant1 FlyTheElephant1 Sep 11, 2026 •

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.

No I don't think so. This doesn't change the reward of any particular block, the divisor by 6 creates a vesting schedule. whereby 1/6th matures at 2016 blocks, 2/6ths mature at 4032 blocks, and the remaining 3/6ths matures at 8064 blocks after being mined.

The purpose of it is to smooth the network maturation rate and prevent events where the maturation in total spikes (relative to 402 and 403). 402 and 403 matures after a set number of blocks or time and then matures all at once when rdts expires. 404 matures gradually in phases over 3 set numbers of blocks and any block that was subject to the rule is always subject to the rule so there arent a huge number of blocks maturing on the block where rdts expires. This proposal is intended to reduce the number of ways for people to game things.

The tranches are neccessary to remove single events that speculators can try to pump and dump the market, and to smooth out the supply dynamic effects. (again, does not alter the reward quantity for any block)

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.

I guess one other answer to another part of this question on why batch is this: each individual block could have three maturations, but this change from my understanding would require tripling the size of the coinbase transaction for each block. Batching by modulo6blockheight keeps data allocation low while providing an almost same effect to everyone except for true solo miners.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

"No, I don't think so." Not sure what you mean by that. I wasn't saying that the reward is changed by your approach. I get the tranches/schedule. I'm saying as a miner I know EXACTLY what tranche the block I'm about to work on goes in BEFORE I work on it. If I know this, I think I can game it. The only way to prevent that is if the tranche is unknowable before I mine the block, which is what using hash modulo 6 accomplishes.

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 only way you could game it is by shutting off your hashrate and getting less blocks. You can game it if you want less blocks and equipment sitting dormant.

Hash modulo could work to fix that and if people want this proposal then it could be added. I wanted to try and keep it as simple as possible so that the largest number of people could understand what it does while they figure out what they want to do.

Good idea.

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.

pull 409 resolves AcesHigh70 concern. Thank you aces.
#409

* at 2016, 2/6 at 4032, and 1/2 at 8064. 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;
Comment thread
FlyTheElephant1 marked this conversation as resolved.
Resolved

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
24 changes: 20 additions & 4 deletions src/consensus/tx_verify.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,20 @@ 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 ext_start_height, int ext_expiry_height)
{
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. Relative to
// the first window height so the cadence does not depend on abs height.
const int batch = (coinbase_height - ext_start_height) % 6;
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)
{
// are the actual inputs available?
if (!inputs.HaveInputs(tx)) {
Expand All @@ -192,9 +205,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, ext_start_height, ext_expiry_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
11 changes: 10 additions & 1 deletion src/consensus/tx_verify.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

#include <consensus/amount.h>

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

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

/**
* 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. Pass INT_MAX for both
* bounds when the deployment is unscheduled on this chain.
*/
int RequiredCoinbaseMaturity(int coinbase_height, int ext_start_height, int ext_expiry_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.
* 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 = (std::numeric_limits<int>::max)(), int ext_expiry_height = (std::numeric_limits<int>::max)());
} // 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
11 changes: 11 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,15 @@ 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);
return Consensus::RequiredCoinbaseMaturity(coinbase_height, ext_start, ext_expiry);
}
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