Skip to content

T.Softfork: Long coinbase maturity (part 2 of 3): extend from 45 days to 1 year - #429

Open
chrisguida wants to merge 6 commits into
bitcoinknots:29.x-knotsfrom
chrisguida:long-coinbase-maturity-rdts-expiry-29+knots
Open

chrisguida wants to merge 6 commits into
bitcoinknots:29.x-knotsfrom
chrisguida:long-coinbase-maturity-rdts-expiry-29+knots

Conversation

@chrisguida

@chrisguida chrisguida commented Sep 23, 2026 •

Copy link
Copy Markdown

Follow-up to #419. Consensus: coins generated since block 973440 stay unspendable until RDTS expires (parent MTP >= 2027-09-01 00:00 UTC) instead of until block 979920. Policy and wallet: every generated coin is held for one year of MTP instead of 6480 blocks (this part does not expire).

Up to block 979919 the rules are identical to 29.4.2; from 979920 they diverge, so that is the effective flag day: it must be deployed before then (currently expected late Oct or early Nov). A 29.4.2 node upgrading later rewinds to 979919 and revalidates at first start (one coming from 29.4.1 or Core has no revalidation marker and rewinds to 973439, as it would upgrading to 29.4.2); a pruned one may need a reindex.

getdeploymentinfo reports expiry_time for long_coinbase_maturity and no longer height_end or maturity.

.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.

Comment thread src/policy/policy.h Outdated
static constexpr int32_t MAX_STANDARD_TX_WEIGHT{400000};
/** Median time past a coinbase must have aged for before a spend of it is relayed/mined, while
* the long coinbase maturity rule is scheduled (Consensus::Params::CoinbaseMaturityLongScheduled) */
static constexpr int64_t COINBASE_MATURITY_POLICY_TIME{365 * 24 * 60 * 60};

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 prefer to use std::chrono::years{1} or at least rename to COINBASE_MATURITY_POLICY_TIME_SECONDS

@stackitdeep stackitdeep Sep 25, 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.

std:chrono::days(365) is an exact match for this code. years(1) is not the same.

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.

Changed to std::chrono::days{365} (a std::chrono::year is 365.2425 days, so years{1} would not be the value the tests and the notes describe); the three use sites convert with TicksSeconds() from util/time.h.

Comment thread doc/release-notes-longmaturity-rdts.md Outdated

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 between the 17th and the 24th of October.

A node that upgrades only after block 979920 rechecks the blocks it connected from that height on at its first start. This needs their block and undo data: a pruned node that upgrades late may have to reindex, and any node that upgrades late rewinds and reconnects everything since block 979919. Upgrading before block 979920 avoids both.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Holds only from 29.4.2, whose marker stops at 979919. From 29.4.1 or Core there's no marker and the rewind target is 973439.

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.

Right, that sentence was only true for a 29.4.2 node. Reworded: a 29.4.2 node rewinds to 979919; a node coming from 29.4.1 or another implementation has no marker and rewinds to 973439, as it would upgrading to 29.4.2; either way pruned nodes may need a reindex.

@jasonsopko

Copy link
Copy Markdown

The fuzzer,address,undefined,integer failure reproduces on a049372 and is the fuzz harness, not the consensus change.

What I ran: the libfuzzer preset (clang 18, undefined,address,fuzzer), then FUZZ=scriptpubkeyman fuzz -runs=1 on an empty corpus, which executes the empty input CI wrote as its crash unit. ASan reports a heap-use-after-free in Consensus::Params::CoinbaseMaturityLongScheduled(), reached from CWallet::SetLastBlockProcessedInMem through ChainImpl::coinbaseMaturityLongScheduled and chainman().GetConsensus(). The freed object is the CChainParams that TestingSetup created: initialize_spkm() builds the setup, whose chainman keeps a const CChainParams& into globalChainParams, and then calls SelectParams(ChainType::MAIN) a second time, which replaces the unique_ptr and frees the object the chainman still points at. The dangling reference predates this PR: the merge-base 4663b7b runs clean on the same input with the same harness line in place. This PR adds the first call on that path that dereferences it.

Fix: delete the SelectParams(ChainType::MAIN); line in src/wallet/test/fuzz/scriptpubkeyman.cpp, which is what Core did in bitcoin#32624 (779e782). With that one deletion on top of a049372 the target runs clean on the same input.

The test each commit failure is tool_cli_completion.py and is not this PR either. The branch is based on 4663b7b, where contrib/completions/bash/bitcoin-cli.bash is still the hand-written file; 29.x-knots switched to the generated one in 271fd20 and eeb9cc1, and the test compares the file in the tree with what it generates, so every commit of this branch fails it. The PR touches no completion file. A rebase onto the current tip should clear it.

@jasonsopko

Copy link
Copy Markdown

As the PR body says, policy and the wallet now hold every generated coin for a year of MTP (9425ce5, 6517e88), not only those from CoinbaseMaturityLongStartHeight on. Consensus holds only coinbases from 973440 on. 29.4.2 policy holds every coinbase too, but for 6480 blocks, which everything below 973440 reaches by block 979919. At tip 975842, blocks 961640 to 973439 have 46,571 unspent coinbase outputs worth 11,599.6 BTC, of which 9,084.0 BTC already relays on 29.4.2. With this PR a node refuses to relay or mine a spend of any of them until a year of MTP after its block, 2027-08-12 to 2027-09-21, and its wallet counts them as immature. The same goes for pre-fork coinbases from the last year.

The largest pool in that range (2,237 blocks) has spent all but 118.6 of its 6,993 BTC, so 99% of the 11,599.6 BTC is in blocks other miners found.

Could the policy hold start at CoinbaseMaturityLongStartHeight, the same line the consensus rule uses? Every coinbase from there on keeps its year, and older ones need the ordinary 100 blocks. Until block 979919 that makes a node with this change relay, and its wallet spend, the newest of them before 29.4.2 nodes accept them: at tip 975842, blocks 969364 to 973439, 2,515.6 BTC. This commit does that for the mempool, the wallet and the Qt text, with the release notes and the two long-maturity functional tests updated:

jasonsopko@6390618

On a build without the GUI, unit tests pass, and the functional suite passes apart from the tool_cli_completion.py base failure from my earlier comment; the Qt line is compile-checked only. Both changed tests fail with the code change reverted.

Comment thread src/wallet/wallet.cpp
m_last_block_processed = block_hash;
m_last_block_processed_height = block_height;
m_last_block_processed_mtp = 0;
if (block_height >= 0 && HaveChain() && chain().coinbaseMaturityLongScheduled()) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

scriptpubkeyman fuzz hits use-after-free here: initialize_spkm's SelectParams(ChainType::MAIN) frees params chainman still uses. Drop that line.

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.

Confirmed, same stack here under ASan. Luke asked for the upstream fix rather than a one-off deletion, so the drop comes in as a backport of bitcoin#32624 in #445 (one extra upstream commit from bitcoin#32977 is needed for the new spkm_migration target to pass its corpus on 29.x; details there). This PR stays as is; its fuzz job builds the merge with 29.x-knots, so it goes green once that lands.

Comment thread doc/release-notes-longmaturity-rdts.md Outdated
@@ -0,0 +1,12 @@
Long coinbase maturity: one year, released with RDTS

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.

"release with" is confusing terminology here

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.

Changed to "expiring with RDTS", and the notes now say expiry/expires throughout instead of release; the regtest option's third field and its error message say expiry too. Internal identifiers (CoinbaseMaturityLongReleaseTime) and the test file name are unchanged; I can rename those as well if you want the code to match the words.

Comment thread doc/release-notes-longmaturity-rdts.md Outdated
Long coinbase maturity: one year, released 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 ends when RDTS expires. 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 ordinary maturity of 100 blocks.

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.

s/ordinary/original/

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.

Done.

Comment thread doc/release-notes-longmaturity-rdts.md Outdated

Policy and the wallet now apply the same delay to every generated coin, measured in time rather than in blocks: the mempool does not relay a spend of a generated 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. For bitcoins generated since block 973440 this is never sooner than the consensus release, which the mempool also enforces directly, so a wallet holding generated bitcoins can no longer select coins that the network would not accept. The number of blocks to maturity shown for such coins is an estimate, updated with every block.

`getdeploymentinfo` reports the release time as `expiry_time` for `long_coinbase_maturity`, and no longer reports `height_end` or `maturity` for it. The regtest option `-testcoinbasematuritylong` now takes `<start_height>:<enforce_height>:<release_time>`.

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.

expiry_time?

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 is the field RDTS already reports, and the deployment is reported as type flagday like RDTS, so the end of the rule shows up the same way for both. The sentence now says expiry rather than release.

Comment thread src/consensus/tx_verify.cpp Outdated
// 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",

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.

Suggest changing the reject reason

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.

Changed: a spend held by the long rule is rejected as bad-txns-premature-spend-of-coinbase-long, and the mempool's one-year policy hold as premature-spend-of-coinbase-long; the 100-block rule keeps bad-txns-premature-spend-of-coinbase. Tests and notes updated. If you prefer other names, say which.

.name = "long_coinbase_maturity",
.start_height = consensus.CoinbaseMaturityLongEnforceHeight,
.stop_height = consensus.CoinbaseMaturityLongReleaseHeight - 1,
.stop_height = std::numeric_limits<int>::max(),

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.

Comment thread src/qt/transactiondesc.cpp Outdated
const int numBlocksToMaturity{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));
if (node.coinbaseMaturityLongScheduled()) {
strHTML += " " + tr("On this network, generated coins must also be at least one year old before they can be spent.");

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.

"On this network" seems unnecessary.

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.

Dropped.

Comment thread src/qt/transactiondesc.cpp Outdated
const int numBlocksToMaturity{node.coinbaseMaturity() + 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>";
const int numBlocksToMaturity{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));

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.

Feel like this should get rewritten entirely at this point. It might be good to say how many days left...

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.

Rewritten. The wallet now exposes how long the one-year hold still has to run (in median time past, a new maturity_time_left in WalletTxStatus), and the description says "Generated coins must mature 101 blocks before they can be spent. They must also be at least one year old, so these can be spent in about N days." followed by the existing broadcast/not-accepted explanation. While the time hold is what applies, the Credit line says "matures in about N days" instead of an estimated block count. Compile-checked with Qt 5 here.

@chrisguida
chrisguida force-pushed the long-coinbase-maturity-rdts-expiry-29+knots branch from a049372 to bb9d7de Compare October 7, 2026 22:25
@chrisguida

Copy link
Copy Markdown
Author

@jasonsopko Thanks, same analysis here. Fixed as a backport of bitcoin#32624 in #445 (see the thread on wallet.cpp above), and the "test each commit" red is indeed the base's completion file.

@mynpub

mynpub commented Oct 10, 2026

Copy link
Copy Markdown

concept nack

The one-year policy and wallet hold applied to every coinbase, so it
also held coinbases from below CoinbaseMaturityLongStartHeight that
consensus holds only for the original 100 blocks and that 29.4.2 policy
relays by block 979919. On mainnet that is 46,123 unspent outputs worth
11,074.2 BTC from blocks 961640 to 973439 (tip 976563), plus pre-fork
coinbases from the last year. Apply the hold from the start height on in
PreChecks, the reorg filter and the wallet, which the GUI's day count
reads. Older coinbases need only the original maturity of 100 blocks.
29.4.2 policy holds them for 6480 blocks, which they all reach by block
979919, so until then a node with this change relays, and its wallet
spends, the newest of them before 29.4.2 nodes accept them.

mempool_long_coinbase_maturity.py and
feature_long_coinbase_maturity_release_time.py now cover a pre-start
coinbase spend that the mempool accepts and keeps when the tip is
invalidated, and a pre-start coinbase that counts as trusted in the
wallet. Both fail with the code change reverted. On a build without the
GUI, unit tests pass and the functional suite passes apart from
tool_cli_completion.py, which compares against the hand-written
completion file still on its base; this commit touches neither that file
nor any RPC help.
@jasonsopko

Copy link
Copy Markdown

Rebased the change from my comment above onto bb9d7de: jasonsopko@6d1b070

It gates PreChecks and the reorg filter, and the wallet through your new GetTxLongMaturityTime, which the GUI's day count reads, so the Qt hunk from the first version is gone. At tip 976563 there are still 46,123 unspent coinbase outputs from blocks 961640 to 973439, worth 11,074.2 BTC, and 29.4.2 already relays spends of 9,099.9 BTC of them; with this PR as it stands, a node holds all of them for a year of MTP after their block. Both changed tests fail with the code change reverted; on a build without the GUI, unit tests pass and the functional suite passes apart from the same tool_cli_completion.py base failure.

@jasonsopko jasonsopko left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Tested bb9d7de on regtest against a v29.4.2.knots20260508 build, both binaries side by side, plus this branch's own tests.

In mainnet order it is a softfork. Below 29.4.2's release height both reject a block spending a covered coinbase (bad-txns-premature-spend-of-coinbase from 29.4.2, bad-txns-premature-spend-of-coinbase-long from this branch); at the release height 29.4.2 accepts it and this branch rejects it. A 29.4.2 datadir that has already accepted that block, restarted on this branch, rewinds only to the release height minus one, where its 29.4.2 marker stops, marks the block invalid, and does not rewind again on the next restart. The expiry is exact: with the parent's MTP at T-1 the rule holds in block validation, the mempool, the template rules and getdeploymentinfo; at T it releases; a reorg back to T-1 evicts the released spend.

Is testnet4 meant to end the hold early? There 29.4.2 holds coinbases from 151406 until block 158111, and this branch releases them when the parent's MTP reaches 2026-10-13 15:00 UTC. At the last 1,000 blocks' average of 20.6 minutes, that time comes around height 152,980, while block 158111 is 5,327 blocks past tip 152784, about 76 days at that rate. For that stretch a node with this branch accepts blocks that 29.4.2 testnet4 nodes reject. In that order on regtest, this branch accepts a block spending a covered coinbase, and 29.4.2 rejects it and stays on its own tip while the other node builds on it. The one-year policy hold keeps a node with this branch from relaying such a spend or putting it in a block template, but 29.4.1 follows the same chain with no hold and mines one once the coinbase is 100 blocks deep. If it mines one after the expiry, nodes with this branch would follow that block and 29.4.2 nodes would not. If the early end is intended, the release notes could say testnet4 is not a softfork on 29.4.2. If not, holding until both the old release height and the expiry time would keep it one; on mainnet that changes nothing.

ctest passes (146/146), and the functional suite passes apart from tool_cli_completion.py (base). Test script: https://gist.github.com/jasonsopko/b2ef9bf122290d67d794551f059f92a7

@chrisguida

Copy link
Copy Markdown
Author

@jasonsopko Taken as is: 6d1b070 is now the last commit of the branch, author and message unchanged. Re-checked here on top of bb9d7de: build without warnings, the unit tests, and the two changed functional tests plus the mempool and wallet ones around them. The two new test cases catch the old behaviour: they fail when the code change is reverted and pass with it. Policy and the wallet now start the one-year hold at 973440 and leave older coinbases at the original maturity; consensus is untouched, so the relay gap you describe for the newest pre-start coinbases lasts until block 979919 and nothing else changes.

@dren3451

Copy link
Copy Markdown

Question about PR #429 and #434: What is the economic justification for locking newly mined coins for up to one year?

I’d like to better understand the reasoning behind extending coinbase maturity, especially when comparing newly mined coins with the existing supply inherited from Bitcoin.

Let’s look at the numbers:

  • At the BLAKE2b fork (block 961,640), the chain inherited approximately 20.07 million BTC worth of previously issued supply.
  • By block 976,710, approximately 47,097 new coins had been generated through block subsidies since the BLAKE2b fork.
  • That’s only about 0.235% of the pre-existing supply.
  • In other words, the inherited supply is approximately 425 times larger than the newly mined amount.

These are approximate issuance figures, not estimates of the actively circulating supply. Nevertheless, the difference is enormous.

My questions to the developers and community are:

  1. What specific economic or security problem does a one-year coinbase lock solve? If newly mined coins represent such a tiny percentage of the total supply, how significant can their selling pressure actually be?
  2. Why are newly mined coins subject to such strict restrictions while the much larger inherited supply remains transferable? Existing BTC holders can potentially move substantial quantities of XBT, while miners securing the new chain must wait months to access their rewards.
  3. Could this policy unintentionally favor large, well-funded mining operations? Small independent miners need to cover electricity, hosting, hardware, and maintenance costs. A one-year lock creates a significant financial burden for them, potentially forcing smaller participants out of the network.
  4. Could reducing miners’ liquidity also reduce network security? If the restrictions make mining economically unattractive, some miners may leave, reducing total hashrate and potentially increasing concentration among the remaining operators.
  5. Is there quantitative evidence that newly mined coins are causing a meaningful economic problem? Has anyone compared actual coinbase spending activity, liquidity, and selling pressure against the much larger inherited supply?
  6. If the main objective is to encourage independent mining and discourage centralized pool dependence, why use a restriction that also affects honest SOLO miners running their own Knots nodes? Wouldn’t more targeted incentives be more effective?
  7. Why not consider the staged approach proposed in PR T.Softfork: Release long coinbase maturity in four periods #434? Gradual unlocking could provide a compromise between the developers’ objectives and miners’ need for predictable access to their rewards. However, would the proposed one-year wallet and mempool policy undermine that benefit?

I understand that protecting the network and encouraging genuine decentralization are important goals. I’m not arguing that every mining reward must be immediately spendable.

But given that approximately 20 million coins already existed before the fork, while miners have generated only tens of thousands of additional coins, I believe such a substantial restriction deserves a clear, evidence-based economic justification.

How does locking this comparatively tiny amount of newly mined supply improve decentralization or network security enough to outweigh the costs imposed on miners?

I would genuinely appreciate hearing the developers’ reasoning and whether less restrictive alternatives have been considered.

PR #429: #429

PR #434: #434

@chrisguida

chrisguida commented Oct 14, 2026 •

Copy link
Copy Markdown
Author

@jasonsopko, on your testnet4 finding (review #429 (review)): Thanks, your reading is right: as pushed, the testnet4 hold ends with the RDTS expiry (2026-10-13 15:00 UTC) while 29.4.2 holds until block 158111. Rather than special-casing it here, #459 extends the testnet4 RDTS expiry to 2027-04-01 00:00 UTC, which is past block 158111 at any plausible block rate; the hold here follows the expiry, so testnet4 stays a softfork on 29.4.2 and no code in this PR changes. The extension applies retroactively (the deployment had already expired when it was scheduled), with a chainstate revalidation entry so nodes that connected blocks 152963+ without the rules check them again. Mainnet is unaffected: 979920 comes long before 2027-09-01.

@jasonsopko

Copy link
Copy Markdown

Tested on the live chain; review on #459: #459 (review)

@dren3451

dren3451 commented Oct 17, 2026 •

Copy link
Copy Markdown

@luke-jr I'd push back on "defeats the point" a bit, at least for the first period.

Coinbases from 973440 to 979919 were mined while 29.4.2 was the rule, and 29.4.2 releases all of them at 979920. That's exactly what period 1 here keeps. Re-locking them doesn't deter anything, the hashing already happened. It only changes the deal for people who mined under the rule you shipped. Deterrence works going forward, from whenever the new rule is known.

I went through the code at 6bfad7a: period 1 matches 29.4.2 consensus exactly, and the policy/wallet hold follows the schedule instead of a flat year, so the wallet never shows coins as immature that consensus already released.

On the "real miner" question behind step 3: I've been reading the DATUM gateway source while trying to prove my own setup is independent. In pooled mode the pool sets the coinbase payees and holds the XOR key, so only the pool can even tell a share is a block. From the block alone that looks the same as solo, and a pool can pay a single address. So whatever step 3 ends up being, it'll be hard to enforce by consensus without hitting solo miners too. Holding everyone's coinbase for a year as a bridge to that seems like a lot.

If the full schedule is too much, please at least fold period 1 into #429: release what was mined before 979920 at 979920, like 29.4.2 already does. And +1 to jasonsopko's point in #459 that the hold should have its own end date instead of following RDTS (that applies to the last period here too).

@luke-jr

luke-jr commented Oct 17, 2026

Copy link
Copy Markdown
Collaborator

29.4.2 explicitly stated the rewards are expected to be locked a year. Trying to claim only 45 days is gaslighting.

@independentb2miner

Copy link
Copy Markdown

PR #429 — The impact of extended coinbase maturity on independent miners and decentralisation

I would like to offer the perspective of an individual, independent BTCB2 miner on PR #429 and the proposed extension of coinbase maturity to September 2027.

I invested in a Goldshell SC5 Pro and run my own BLAKE2b Knots node and DATUM gateway, with substantial ongoing electricity costs. I passionately believed in this hard fork and did exactly what we were encouraged to do: mine independently, validate our own blocks and support decentralisation.

My objective has always been to accumulate BTCB2 over the long term, not to extract short-term mining profits. I intend to retain as much as practicable of my rewards, selling only enough to cover electricity costs and sustain my participation. I am a committed, independent, self-validating miner — not a mercenary hashrate operator chasing whichever chain offers the highest immediate return.

As I understood it, the original 45-day maturity restriction was intended to discourage mercenary hashrate and short-term mining behaviour that could undermine the security and decentralisation of this young chain. I supported that objective and accepted the initial delay as a reasonable commitment. My objection is not to the principle of a maturity restriction, but to extending it to a full year, including rewards already earned, with potentially devastating consequences for genuine, self-validating miners.

If the proposed one-year extension is adopted, I will almost certainly have to stop mining. I simply cannot finance nearly another year of electricity without access to at least part of my mining rewards. Surely many other small miners face the same predicament?

If independent miners like us switch off, hashrate diversity could decline, leaving the network more concentrated and potentially more vulnerable. Isn't this precisely the opposite of what BTCB2 was intended to achieve?

My greatest concern is not simply the financial loss, but the destruction of trust. Miners have committed real capital and ongoing expenditure based on the existing consensus rules, only to face the prospect of those rules changing fundamentally within weeks.

A policy that effectively favours well-capitalised miners over independent plebs seems disturbingly reminiscent of the very problems this hard fork was intended to address.

An alternative approach, such as the rolling 6,480-block (approximately 45-day) maturity proposed in PR #467, deserves consideration on its technical and economic merits. Such an arrangement could preserve a meaningful deterrent against short-term opportunistic mining while allowing committed independent miners to cover their operating costs.

It would also give the network time to evaluate the consequences of the current restrictions before committing to a substantially longer or permanent maturity policy.

I recognise that there are important technical and consensus considerations, but I sincerely hope the maintainers will evaluate this and other proportionate alternatives before adopting a one-year lock.

Surely we should be encouraging the next generation of independent, self-validating miners, not making their participation economically impossible?

I suggest that the long-term credibility of BTCB2 depends not only on its consensus rules, but on the trust and continued participation of those who believed in this hard fork enough to invest their time and treasure in securing and decentralising its network.

@kwsantiago

Copy link
Copy Markdown

Two measured properties of the flag-day shape. Every covered reward releases in one block: ~157,000 BTC at the 10-minute target, ~175,000 at the current 532 s/block. And the hold decays with mining time, 315 days for a reward mined now and near zero just before expiry, averaging 157 days, so a flat 180-day rolling depth averages more hold than this does while concentrating nothing.

@Kilombino

Copy link
Copy Markdown

tested ACK 6d1b070

Built 6d1b070 on Ubuntu 24.04 (x86_64), wallet enabled.

  • Unit tests: all 763 test cases pass, including coinbase_maturity_tests.
  • Functional tests: the full suite passes, including feature_long_coinbase_maturity_release_time.py, mempool_long_coinbase_maturity.py, feature_chainstate_revalidation.py and wallet_balance.py. The one exception is tool_cli_completion.py, which fails the same way on the base commit 4663b7b, so it is not caused by this PR.
  • By hand on regtest: the PR's test option -testcoinbasematuritylong lets you pick the release time (on mainnet it is fixed at 2027-09-01, which a test cannot wait for). I set it 30 days ahead and moved the node's clock forward. I mined 260 blocks and tried to spend the coinbase of block 150 (110 confirmations):
    • before the release time, sendrawtransaction rejects it with bad-txns-premature-spend-of-coinbase-long, and generateblock with that spend fails TestBlockValidity with the same reason;
    • once the median time past is past the release time, a block containing the spend is accepted, while the mempool still refuses it (premature-spend-of-coinbase-long, less than 365 days of median time past);
    • the wallet counts the covered coinbases as immature and the ones from before the start height as spendable.

Why I support one year: miners who run more machines than they can pay the electricity for without selling their rewards right away are the ones who should sell those machines, and this rule will push them to. That is a good outcome. It is a chance to make mining responsible and open to everyone, not only to whoever can dump coins the day after mining them.

I am not speaking from the sidelines. I mine BLAKE2b myself with a Goldshell SC5 Pro II, 24/7, and on top of that I rent hashrate every day. My live hashrate is public on my website, https://kilombino.com, under "MY BLAKE2B HASHRATE". My own rewards from block 973440 on are held by this rule, and I am fine with that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.