Repository navigation
T.Softfork: Long coinbase maturity (part 2 of 3): extend from 45 days to 1 year - #429
chrisguida wants to merge 6 commits into
Conversation
| .name = "long_coinbase_maturity", | ||
| .start_height = consensus.CoinbaseMaturityLongEnforceHeight, | ||
| .stop_height = consensus.CoinbaseMaturityLongReleaseHeight - 1, | ||
| .stop_height = std::numeric_limits<int>::max(), |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| 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}; |
There was a problem hiding this comment.
I prefer to use std::chrono::years{1} or at least rename to COINBASE_MATURITY_POLICY_TIME_SECONDS
There was a problem hiding this comment.
std:chrono::days(365) is an exact match for this code. years(1) is not the same.
There was a problem hiding this comment.
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.
|
|
||
| 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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
f58ee55 to
a049372
Compare
|
The What I ran: the Fix: delete the The |
|
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 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 On a build without the GUI, unit tests pass, and the functional suite passes apart from the |
| 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()) { |
There was a problem hiding this comment.
scriptpubkeyman fuzz hits use-after-free here: initialize_spkm's SelectParams(ChainType::MAIN) frees params chainman still uses. Drop that line.
There was a problem hiding this comment.
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.
| @@ -0,0 +1,12 @@ | |||
| Long coinbase maturity: one year, released with RDTS | |||
There was a problem hiding this comment.
"release with" is confusing terminology here
There was a problem hiding this comment.
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.
| 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. |
|
|
||
| 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>`. |
There was a problem hiding this comment.
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.
| // 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", |
There was a problem hiding this comment.
Suggest changing the reject reason
There was a problem hiding this comment.
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(), |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| 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."); |
There was a problem hiding this comment.
"On this network" seems unnecessary.
| 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)); |
There was a problem hiding this comment.
Feel like this should get rewritten entirely at this point. It might be good to say how many days left...
There was a problem hiding this comment.
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.
a049372 to
bb9d7de
Compare
|
@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. |
|
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.
|
Rebased the change from my comment above onto bb9d7de: jasonsopko@6d1b070 It gates |
jasonsopko
left a comment
There was a problem hiding this comment.
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
|
@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. |
|
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:
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:
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. |
|
@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. |
|
Tested on the live chain; review on #459: #459 (review) |
|
@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). |
|
29.4.2 explicitly stated the rewards are expected to be locked a year. Trying to claim only 45 days is gaslighting. |
PR #429 — The impact of extended coinbase maturity on independent miners and decentralisationI 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. |
|
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. |
|
tested ACK 6d1b070 Built 6d1b070 on Ubuntu 24.04 (x86_64), wallet enabled.
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. |
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.
getdeploymentinforeportsexpiry_timeforlong_coinbase_maturityand no longerheight_endor maturity.