Skip to content

Softfork: 1-year rolling coinbase maturity, no release cliff, independent of RDTS - #476

Open
satoshipuzzles wants to merge 5 commits into
bitcoinknots:29.x-knotsfrom
satoshipuzzles:rolling-coinbase-maturity-1y
Open

satoshipuzzles wants to merge 5 commits into
bitcoinknots:29.x-knotsfrom
satoshipuzzles:rolling-coinbase-maturity-1y

Conversation

@satoshipuzzles

@satoshipuzzles satoshipuzzles commented Oct 22, 2026 •

Copy link
Copy Markdown

What

On mainnet, from block 979920, every reward mined at height 973440 or later must be buried 52,560 blocks (365 days × 144) before it can be spent. Each reward is measured from its own block, so exactly one reward unlocks per block, a year after it was mined. Coverage ends with issuance: rewards mined at 6,930,000 (the first zero-subsidy height, about 2140) or later keep ordinary maturity.

No cliff

#419 holds the rewards mined from 973440 to 979919 and then releases them all together at block 979920, which is about 6,400 rewards in one block. Default relay policy already holds each coinbase to 6,480 confirmations, so in practice that release is only reachable by miners including the spends directly in their own blocks. This PR takes over at that block and makes the per-reward schedule a consensus rule, so that shared release never happens:

Rewards mined #419 alone This PR
before 973440 100 blocks 100 blocks (unchanged)
973440 – 979919 all spendable together at 979920 each at its own height + 52,560
979920 – 6,929,999 100 blocks each at its own height + 52,560
6,930,000 on (no subsidy left) 100 blocks 100 blocks

The first reward unlocks at 1,026,000, and one more unlocks at every block after that. The end is set by the height a reward was mined at, not the height it is spent at, so there is no end-date release either: the last covered reward (mined at 6,929,999) still waits its full year and unlocks on its own at 6,982,559. Proposals tied to RDTS release rewards early as expiry approaches; this one doesn't.

Independent of RDTS

The rule is decided by block heights alone. It does not read RdtsExpiryTime, the BLAKE2b fork height, or any median time past. Whether RDTS is extended, replaced or lapses in September 2027, coinbase maturity stays a year. Any later change to it is its own decision.

How this differs from the open proposals

Depth Rolling Covers #419's window Ends
#419 (merged) 6,480 no, all released at 979920 — 979920
#429 until RDTS expiry (relay policy also holds a year of MTP, no end) no, all released at expiry yes with RDTS
#434 four periods no, four release dates yes with RDTS
#467 6,480 yes yes with RDTS
#468 25,920 yes no, those still release at 979920 with RDTS (rewards mined before expiry keep it)
#468 + #477 25,920; window keeps 6,480 yes yes, at 6,480 with RDTS
#468 + #478 25,920 yes yes, at 25,920 with RDTS
#468 + #479 25,920; 100 for 979920–986399 yes yes, at 25,920 with RDTS
#469 40,320 yes yes never (makes RDTS permanent too)
this PR 52,560 yes yes with issuance (rewards mined from 6,930,000 on), no cliff; RDTS untouched

As of October 23. #477, #478 and #479 are add-ons to #468 and need it merged first.

Consensus rule

  • For a block at spend_height >= 979920, a coinbase output with 973440 <= coin.nHeight < 6930000 needs spend_height - coin.nHeight >= 52560.
  • It applies only on top of the existing rules (std::max), so it never shortens a maturity another rule requires.
  • Through 979919 the valid-block set is identical to 29.4.2. From 979920 it is a strict subset. That makes this a soft fork relative to T.Softfork: Long coinbase maturity (part 1 of 3) #419.
  • The rule is height-based, so it is the same on every branch and can be checked from the coins view alone. There is no UTXO database change.

Relay, wallet, GUI, RPC

  • Mempool acceptance checks the next block's rules.
  • Reorg cleanup evicts a mempool spend that becomes premature again.
  • Wallet and GUI: interfaces::Chain::coinbaseMaturity() and interfaces::Node::coinbaseMaturity() now take the coin's height. Covered rewards show 52,560, and older rewards keep their current maturity.
  • getdeploymentinfo has a new rolling_coinbase_maturity entry of type flagday. Its height is 979920, its coinbase_start_height is 973440 and its coinbase_end_height (last covered reward) is 6929999, along with maturity and active. It has no height_end or expiry_time.
  • getblocktemplate lists rolling_coinbase_maturity in rules once the rule is active.

Upgrade

A new chainstate revalidation deployment, rolling_coinbase_maturity_1y, runs from 979920 with no stop height. A node that upgrades before 979920 rewinds nothing. One that upgrades later checks every block from 979920 again, and a pruned node missing that history must reindex.

Other networks

Testnet4, signet and regtest are unchanged. Regtest can opt in with -testcoinbasematurityrolling=<start_height>:<enforce_height>:<maturity>[:<end_coin_height>].

Tests

  • Unit tests (coinbase_maturity_tests):

    • the mainnet schedule and the revalidation marker
    • no cliff: without this rule, more than 6,000 rewards release at 979920; with it, every reward from 973440 to 1,200,000 unlocks at exactly its own height + 52,560
    • the rules through 979919 are unchanged
    • RDTS independence: the required maturity is identical for every RDTS expiry (none, past, 2027, never) and with the BLAKE2b fork height moved
    • CheckTxInputs boundaries (one block short fails, exact depth passes)
    • other networks untouched, and the regtest option
    • end bound: 6,929,999 is covered and 6,930,000 is not, in both the maturity rule and CheckTxInputs
    • clean activation: an enforce height no later than the start height plus the earlier rule's depth never re-locks a reward that was already spendable
  • Functional test (mempool_rolling_coinbase_maturity.py): uses a scaled long window and rolling rule. It runs on three nodes: without RDTS, with RDTS active, and with RDTS already expired. On each node it checks that:

    • malformed options are rejected
    • rewards from before the window keep 100-block maturity
    • nothing is released at the window's release height
    • each block releases exactly one reward while the next one is still refused, by both policy and consensus
    • a reorg evicts a spend that has become premature again
    • deployment and template reporting are correct, and the rule survives a restart

    With the rolling rule removed, this test fails at the cliff check.

  • Functional test (feature_rolling_maturity_end_bound.py): rewards before enforcement spend at 100, covered rewards are refused from enforcement until exactly their full depth (one block short fails, by block submission), rewards past the end height and before the start height keep 100, the rule survives a restart, and getdeploymentinfo reports the end. With the end bound removed, this test fails.

The end bound, clean-activation test and end-bound functional test are by @MarcanoFilms.

Local results on macOS (Release, no GUI): the coinbase_maturity_tests, txvalidation_tests, mempool_tests, wallet_tests and validation_chainstatemanager_tests unit suites pass. These functional tests also pass: mempool_rolling_coinbase_maturity.py, feature_rolling_maturity_end_bound.py, mempool_long_coinbase_maturity.py, mempool_spend_coinbase.py, wallet_coinbase_category.py --descriptors, feature_chainstate_revalidation.py, feature_rdts.py, feature_rdts_migration.py, rpc_blockchain.py (v1 and v2 transport) and mining_basic.py. The GUI change, one line in transactiondesc.cpp, was not compiled locally.

Notes for reviewers

What happens to the rewards in #419's 45-day window (mined at 973440–979919)

That is 6,480 rewards, about 20,000 XBT in subsidy plus fees.

Under #419 today:

  • Each of these rewards needs 6,480 confirmations, but only until block 979920.
  • The first one, mined at 973440, only reaches 6,480 confirmations at 979920 itself, so none of them unlock earlier.
  • At block 979920 the rule switches off. Every reward mined from 973440 to 979820 (about 6,380 rewards) becomes valid to spend in that single block under consensus. Default relay policy still requires 6,480 confirmations per reward, so only a miner including the spend directly in a block can use this.
  • The last ~100 rewards (979821–979919) follow at their own height + 100.
  • At the recent pace of about 9.3 minutes per block, 979920 is roughly the end of October 2026.

Under this PR:

  • Up to 979919 nothing changes; these rewards are locked exactly as they are now.
  • At 979920 nothing is released. The rolling rule takes over in that same block.
  • Each reward then unlocks 52,560 blocks after its own block:
Mined at Unlocks at Approx. (9.3 min/block)
973440 (first in window) 1026000 late August 2027
976000 1028560 early September 2027
979919 (last in window) 1032479 mid-September 2027
979920 and later own height + 52,560 one per block after that

From 1026000 on, one reward (about 3.125 XBT plus fees) unlocks per block, with no gap between the window's rewards and later ones.

Trade-off: Rewards in the window were mined while a 45-day hold was in force, and this PR extends them to a year. The earliest wait a year instead of 45 days; the last wait a year instead of about 100 blocks. #467 and #469 also extend these same rewards. The alternative is to cover only rewards mined from 979920 on. That leaves these rewards at #419's terms, but keeps the release of about 6,380 rewards in block 979920, which is the cliff this PR exists to remove.

🤖 Generated with Claude Code

…dent of RDTS

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

This takes over from the long coinbase maturity window (bitcoinknots#419) at its release
height, so the rewards it holds are not all released together at 979920.
Through 979919 the rules are unchanged; rewards mined before 973440 keep
ordinary maturity. The rule is decided by heights alone, has no expiry and
does not depend on RdtsExpiryTime or the BLAKE2b fork.

Mempool acceptance and reorg cleanup enforce it for the next block. The
wallet and GUI ask for the maturity at each coin's height.
getdeploymentinfo reports rolling_coinbase_maturity and getblocktemplate
lists it in rules once active. Upgrading nodes check every block from
979920 again under the rolling_coinbase_maturity_1y revalidation marker.
Regtest can opt in with
-testcoinbasematurityrolling=<start>:<enforce>:<maturity>.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@satoshipuzzles
satoshipuzzles force-pushed the rolling-coinbase-maturity-1y branch from 6434ff1 to 808a1c3 Compare October 22, 2026 16:24
@satoshipuzzles satoshipuzzles changed the title T.Softfork: 1-year rolling coinbase maturity, no release cliff, independent of RDTS Softfork: 1-year rolling coinbase maturity, no release cliff, independent of RDTS Oct 22, 2026
@SeventyEi8ht

Copy link
Copy Markdown

Concept NACK.

This PR applies a new 52,560-block maturity to rewards that have already been mined under the existing 6,480-block rule (heights 973440–979918). Those coinbases were accepted under one set of consensus rules. Changing the number of blocks that must pass before they may be spent is a retroactive alteration of the terms that applied when the blocks were produced.

A forward-looking maturity extension that only affects rewards mined after a defined activation height is a checkable consensus rule. Rewriting the maturity of already-accepted outputs is not. Nodes cannot derive the new longer lock from the blocks alone; they must apply a rule that did not exist when those blocks were mined. That reintroduces a trusted third party to determine the spendability of outputs the network has already accepted, which is the mechanism the protocol was designed to remove.

I support temporary, height-based maturity extensions that apply only to future rewards. I do not support re-locking coinbases that have already been mined.

@Daamir-Al-Adel

Copy link
Copy Markdown

Concept ACK.

This seems good to me. Many plebs would like this, based on polling data.
Rolling 1 year CBM with coverage on #419

@mynpub

mynpub commented Oct 23, 2026

Copy link
Copy Markdown

nack

@Kilombino

Copy link
Copy Markdown

Tested ACK 808a1c3

  • Built from source; relevant functional tests pass (mempool_rolling_coinbase_maturity, mempool_long_coinbase_maturity, feature_chainstate_revalidation, feature_assumeutxo, feature_reindex, mempool_reorg, mempool_accept, rpc_blockchain, mining_basic, wallet_balance, feature_rdts).
  • Regtest with -testcoinbasematurityrolling=150:300:200: before enforcement a covered coinbase spends at depth 106 (mempool and block); after enforcement a pre-start coinbase spends normally, a covered one is accepted at depth 200 and rejected at 199 and at 112, and a block including such a spend fails with bad-txns-premature-spend-of-coinbase. getdeploymentinfo reports the rolling_coinbase_maturity flag day correctly.
  • Mainnet: started on an already-synced pruned datadir (tip below 979920): revalidation took about a second with no rewind or reindex, it followed the tip on the same chain as my production node, and getdeploymentinfo shows rolling_coinbase_maturity at 979920 with maturity 52560.

I prefer a one-year rolling maturity with no release cliff and no dependence on RDTS.

@MarcanoFilms

Copy link
Copy Markdown

Concept ACK on the depth, the permanence, and the RDTS independence — this is the design I announced I was preparing in my #468 comment, and I'm glad to see it on the table. I have a working branch implementing it (consensus + policy + tests green on top of rc2), plus an on-chain evidence package measured on my unpruned node, and I'd offer two amendments that directly address the objections already raised in this thread:

1. Start coverage at 979919, not 973440. @SeventyEi8ht's NACK is about re-encumbering rewards mined under shipped rules, and it will keep costing this proposal ACKs. Dropping retroactivity loses less than it seems: relay policy already holds the 973440–979918 window to 6,480 depth regardless, so the "6,400 rewards in one block" release at 979920 is blunted in practice — only direct miner inclusion bypasses it.

2. Don't race the 979920 flag day. rc2 (#468) is already deployed on live nodes. Activating instead at any height below 1,005,839 (= 979919 + 25920) is provably clean: no covered reward will ever have been spendable under the 180-day rule, so nothing ever flips from spendable back to locked — the objection that sank re-locking proposals before simply cannot arise. That removes the 8-day review crunch entirely; my branch carries a unit test asserting this property.

Evidence (full package with figures and reproducible scripts available): measured on this chain, 34.3% of pre-CBM coinbase value was spent within 110 blocks of mining — within 10 blocks of the earliest legal moment. That fast-flip is what any expiring deterrent reverts to. And the covered-era freeze is fully binding: 15,452 XBT across 421,674 outputs, zero value moved.

Happy to share the branch and coordinate rather than open a competing PR — the outcome matters more than the authorship.

@FlyTheElephant1

FlyTheElephant1 commented Oct 23, 2026 •

Copy link
Copy Markdown

I like the idea, I would prefer a shorter permanent though.

I do think that we need a permanent baseline maturation length, I think a permanent should be on the low side though and a temporary should be higher than permanent.

@Kilombino

Copy link
Copy Markdown

@FlyTheElephant1

I think a permanent should be on the low side though and a temporary should be higher than permanent.

The cliff is why I prefer a single rolling period: when a temporary higher maturity ends, everything it held back becomes spendable at once. A permanent rolling period releases coins one block at a time. If the permanent length is reduced later, it can be done the same way, as a rolling change with no cliff.

@satoshipuzzles

Copy link
Copy Markdown
Author

Thanks for the review, and @Kilombino for the tested ACK.

@SeventyEi8ht: the window rewards were not mined under a promise of 45 days. The 29.4.2 release notes said maturity would be extended "up to 350 days" and put miners "on notice" that they "may need to wait much longer". The 45 days was the first step, announced as such. This PR treats every reward from 973440 on the same way, one year from its own block, instead of giving the earliest rewards a shorter lock than everyone after them.

@MarcanoFilms: thanks, and I'd welcome working together. On amendment 1, starting at 979919 would give rewards mined in the window a 45-day lock and everyone after them a year, so I'd rather keep one rule for all of them. You're right that default relay policy already holds each reward to 6,480 confirmations, so the 979920 release is only reachable through direct miner inclusion. I'll correct the description. That still leaves the miners who produced those rewards able to spend them in their own blocks, which this closes. On amendment 2, because this covers the window it has to ship before 979920. If it misses that, I'll move the start height forward rather than re-lock anything that has already become spendable.

@FlyTheElephant1: as @Kilombino said, a later reduction can also be done on a rolling basis, without a cliff.

@papier-krieger

Copy link
Copy Markdown

Concept ACK.

This is by far the most consequential PR on the table because it stays true to the path previously agreed upon by the community. While the transition from 45 days to 1 year in #419 was technically fixed, the vast majority of plebs undoubtedly understood it as a rolling mechanism.

PR #476 finally aligns the project’s direction with the spirit of that original community plan. It sends the correct signal, honors our most devoted plebs, and purges the influence of adversaries trying to weaken the baseline.

@FlyTheElephant1

Copy link
Copy Markdown

If the permanent needs to be reduced its a hardfork @satoshipuzzles a later reduction cannot be done on a rolling basis if the rule is permanent without doing a hardfork, which is why i think we need a low baseline permanent, I'd go slightly lower than the lowest number that the average person can rationalize for the permanent. maybe 45 or 60 days.

I am in support of a higher maturity length temporary, but I think even if we have one of those we need a lower permanent one as well.

@MarcanoFilms

Copy link
Copy Markdown

Thanks — glad to work together, and the description correction sounds right.

On uniformity: I agree one rule for every reward from 973440 is the cleanest form, and your reading of the 29.4.2 notice is fair. My concern is practical: rc3 just replaced #468 with #429 in the release spec, so the 979920 slot is spoken for and the pre-flag-day path for this PR is effectively closed. That makes the fallback you described — moving the start height forward rather than re-locking anything — the version that will actually ship. I'd suggest we prepare it now rather than the day after the flag day.

Concretely, I have a working branch (two commits on top of rc2) that implements exactly that fallback shape, and I'd be glad to hand it over or send patches so it lands under this PR:

  • Coverage from 979919, depth 52,560, rolling, permanent, no RDTS reads.
  • Activation at a parameterized flag-day height with a provable clean-activation property: for any activation below 1,005,839 (= 979919 + 25920), no covered reward can ever have been spendable under a shallower rule, so nothing flips from spendable back to locked — there's a unit test asserting the bound.
  • One refinement to consider: end coverage at the first zero-subsidy height (6,930,000), so the rule retires together with issuance. It answers "when does this end?" with "when the subsidy does" — permanent in every practical sense, and nothing about it prevents future consensus from revisiting sooner.
  • Regtest support (-testcoinbasematuritysubsidyera=<activation>:<start>:<end>:<depth>), getdeploymentinfo reporting, and the mempool-accept policy pass — the functional test caught that policy was missing from the accept path, the same policy-vs-consensus divergence class that review found by hand in T.Softfork: Long coinbase maturity (part 1 of 3) #419 and T.Softfork: Long coinbase maturity (part 2 of 3): extend from 45 days to 1 year #429, so it's covered by an automated test here. Being height-pure, the functional test needs no mocktime or MTP handling at all.

One property worth noting for whichever rule wins at 979920: this fallback composes cleanly on top of either. Stacked over #429's freeze it even absorbs the expiry cliff for covered rewards — every reward mined from activation onward reaches its 52,560th block after the RDTS expiry, so release stays one-reward-per-block instead of everything at once. The on-chain evidence package (measured spend-depth distribution, the 973440+ freeze verification, release-profile comparisons) is ready for the PR body as well.

If you're open to it, tell me where to send the branch — or I can open it against your fork so the commits land under this PR.

@satoshipuzzles

Copy link
Copy Markdown
Author

@FlyTheElephant1 good catch, and you're right. I put it badly. Lowering a permanent maturity later would be a hard fork, rolling or not: nodes still enforcing 52,560 would reject blocks that spend a reward sooner. Phasing a reduction in avoids the release cliff, not the fork. @Kilombino's point about the cliff stands; the fork point is yours.

For anyone skimming: this PR itself is a soft fork. It only adds a restriction, so non-upgraded nodes accept every block upgraded nodes produce. The hard-fork question only comes up if someone later wants to loosen it.

So the real choice is which direction should be easy:

  • Permanent year (this PR): lengthening is a soft fork; shortening needs a hard fork.
  • Low permanent baseline plus a temporary higher lock: shortening happens automatically at the end date; keeping the year needs a soft fork to extend it.

I chose the first on purpose. The lock exists to make attacks on the chain's incentives expensive, and a deterrent with an end date is one an attacker can simply wait out. Every expiry also becomes another fight over extending it. A baseline that protects the chain should be hard to weaken, and lowering it should take the broad agreement a hard fork requires.

If reviewers prefer your shape, it can be done without bringing the cliff back: end the temporary layer by the height a reward was mined at, not the height it's spent at. Rewards mined before the end height keep their full year from their own block; later rewards get the baseline. Nothing already locked is released early. I'd consider that as an amendment if there's support for it, but I'd rather not change the design days before 979920 without it.

@satoshipuzzles

Copy link
Copy Markdown
Author

Thanks, and yes, let's work together. Small correction on timing: rc4 (16:55 UTC today) went back to #468, so the 979920 slot is still in flux. Your bigger point stands, though. Neither candidate carries this PR, so the fallback is the version most likely to ship, and it makes sense to have it ready now.

Please open your branch as a PR against satoshipuzzles/bitcoin (base rolling-coinbase-maturity-1y) and I'll review and test it there before anything changes here. I'd keep the current 973440 coverage until the release contents are settled, then switch to the fallback if this misses 979920. Your clean-activation bound (below 1,005,839) assumes #468 ships; if #429 ships instead, the bound is RDTS expiry, so it's worth a test for each.

Ending coverage at the first zero-subsidy height (6,930,000) is fine with me: it's effectively permanent and ends by mined height, so there's no cliff. And yes, please put the evidence package in; measured data will help reviewers more than anything I can argue.

MarcanoFilms and others added 4 commits October 23, 2026 13:41
…ight

The rolling rule exists to protect subsidy-bearing rewards, so there is
nothing left for it to cover once issuance ends. Bound its coverage by a
new CoinbaseMaturityRollingEndCoinHeight: generated coins mined at or
above it keep the maturity the earlier rules require, while every coin
below it stays covered for its full rolling depth, so no covered reward
is ever released early. On mainnet the bound is 6930000, the first
zero-subsidy height (33 halvings), so the rule retires together with
issuance instead of regulating fee-only coinbases forever.

-testcoinbasematurityrolling gains an optional fourth field to schedule
the bound on regtest, and getdeploymentinfo reports the last covered
coin height as coinbase_end_height when the coverage ends.
Pin down the property that makes a later rolling enforce height safe to
ship: as long as the enforce height does not exceed the covered start
height plus the maturity the earlier rules already require, no covered
reward was ever spendable before enforcement, so the rule upgrade only
extends pending maturities and cannot re-lock a coin a node might have
seen spent. The synthetic schedule mirrors the mainnet situation of an
interim rule (a bitcoinknots#419-style long window here) holding every covered
reward past the enforce height, and the test comment maps the bound to
the candidate interim rules for mainnet.
Exercise the end coin height and the enforce height end to end on
regtest with the four-field -testcoinbasematurityrolling schedule:
before enforcement a covered reward spends at the historical 100 in the
mempool and in a block (policy follows consensus, there is no early
hold); from enforcement a covered reward is rejected by both below the
full depth, with the exact boundary checked via directly submitted
blocks (depth-1 fails, depth passes); rewards mined before the start
height or at the end coin height keep the historical 100 even while the
rule is enforced; the schedule and its reported coinbase_end_height
survive a restart.
The wallet and GUI maturity helper still covered every reward from the
start height on, so rewards mined at or after the end coin height would
show (and be held at) 52560 blocks while consensus and relay accept them
at the earlier rules. Also describe the end bound in the release note.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Kilombino

Copy link
Copy Markdown

tested ACK 83a1f68

Re-tested after the end-bound commits (5f5affa..83a1f68):

  • Built without GUI. coinbase_maturity_tests and the full test_bitcoin suite pass.
  • feature_rolling_maturity_end_bound.py, mempool_rolling_coinbase_maturity.py and mempool_long_coinbase_maturity.py pass.
  • Manual regtest with -testcoinbasematurityrolling=150:300:200:260, checked through testmempoolaccept and generateblock:
    • once enforced, covered coins (150–259) are rejected below depth 200, both in the mempool and in a mined block, and accepted at exactly 200;
    • the last covered coin (259) is still held, while the first coin past the end (260) and later ones fall back to the 100-block rule;
    • coins below the start height are unaffected;
    • getdeploymentinfo reports coinbase_end_height: 259.
  • Pruned mainnet node synced to the tip (978678) on this commit, with the same block hash as my production node. It reports coinbase_start_height: 973440, coinbase_end_height: 6929999, maturity: 52560, height: 979920.

Ending coverage by the height a reward was mined at keeps the property I cared about: no covered reward is released early, and there is still no cliff.

@MarcanoFilms

Copy link
Copy Markdown

Mutation testing of the stacked maturity rule

Ran a jasonsopko-style pass over the two sites that enforce this rule —
Consensus::CheckTxInputs (block validation) and Params::CoinbaseMaturityRequired
(mempool / wallet / GUI / getdeploymentinfo) — 9 single-line mutants on the start
boundary, the end-coin-height boundary, the enforce boundary, and the std::max.

7 killed immediately. The 2 survivors were the same defect in both sites:
replacing max(required, rolling) with = rolling, i.e. letting the rolling rule
reduce a maturity the long rule already requires. They survived because every
existing test exercises the overlap with the rolling depth dominating
(rolling > long), so max(long, rolling) == rolling and dropping the max is
invisible.

On the mainnet schedule the long window releases exactly where rolling begins
(RollingEnforceHeight == LongReleaseHeight), so the windows never overlap and the
rolling depth (52560) dominates anyway — there the survivors are equivalent mutants.
But "only ever adds to the maturity required by the rules above, never reduces it" is
the invariant stated for the rule, and the regtest flags can construct overlapping
windows with long > rolling, so it's worth pinning directly.

A test that locks it at both sites (long=400 > rolling=200, the stricter depth must
win) kills both survivors → 9/9. Opened as satoshipuzzles#2 against the
PR branch (test only, no consensus change). Full matrix and the survivor analysis are
in the evidence writeup I can attach to the PR body.

boundary / operator result
end-coin-height < → <=, removed killed (unit + feature_rolling_maturity_end_bound)
start >= → > killed (unit + mempool_rolling_coinbase_maturity)
enforce >= → > killed (unit + mempool_rolling_coinbase_maturity)
max(req, rolling) → rolling (both sites) survived → now killed by the #2 test

@Antonbtc2020

Copy link
Copy Markdown

Concept ACK.
In order to not repeat the author's points, I see PR#476 main contribution in correcting Bitcoin mining incentives.

@keyandchain21

Copy link
Copy Markdown

Concept ACK. I believe this permanently establishes proper miner incentives while addressing the "cliff" concerns.

@MarcanoFilms

Copy link
Copy Markdown

Following up on the collaboration here with a technical case for this design over the other rolling candidates, grounded in the diff and measured on my own unpruned node. For context on where I'm coming from: I mine this chain independently — my own Knots node and DATUM gateway, no pool — so whatever depth ships sits on my own paycheck.

1. This is the only rolling proposal that also removes the 979920 release

The whole reason the thread converged on rolling was to avoid releasing a year of rewards in a single block. #468 does that going forward, but it leaves #419's window (heights 973440–979919, 6,480 rewards = exactly 20,250 XBT in subsidy, plus fees) releasing at 979920 under consensus. Measured on my own node at the current tip (978824), the 5,385 of those blocks already mined carry 16,828.125 XBT subsidy plus 12.80 XBT in fees — fees run about 0.0024 XBT/block here, so the whole window lands near 20,265 XBT. Of those, the rewards at 100+ confirmations when #419's rule lapses — heights 973440–979820, 6,381 rewards = 19,940.625 XBT in subsidy plus fees — become spendable in block 979920 itself under consensus; the last 99 follow at their own height + 100. Default relay policy still holds each to 6,480 confirmations, so in practice only a miner including the spends directly in its block can take that release — but it is valid under consensus, and that is exactly the "all at once" event the rolling shape exists to prevent, sitting at the front door.

This PR takes over at 979920 and puts those same rewards on the per-reward schedule, so nothing is released there. #468 can only match that with an add-on (#477/#478/#479) that has to merge on top of it; here it's the base behavior, covered by the clean-activation test that proves an enforce height no later than start + the earlier rule's depth never re-locks a reward that was already spendable.

2. Height-only, so there is no second discontinuity and no coupling to RDTS

#468 binds maturity to RdtsExpiryTime (1819756800): a reward whose mining-block parent MTP is below expiry needs 25,920, and one at or above expiry drops back to 100 (CheckTxInputs returns COINBASE_MATURITY). That is a ~180-day step in the required hold across a single block around 1 Sep 2027, and it ties the lifetime of the maturity rule to an unrelated sunset. stackitdeep raised this on #468 directly ("I don't think this needs to be tied to the RDTS expiration").

This rule reads block height alone — not RdtsExpiryTime, not the BLAKE2b fork height, not any MTP. Coverage is monotone from 979920 until issuance ends at 6,930,000, where returning to 100 is natural because there's no subsidy left to encumber. Whether RDTS is extended, replaced or lapses, maturity stays put, and any future change to it is decided on its own merits rather than inherited from RDTS's calendar.

3. The "rolling encumbers more coins/day" argument points at the longer window

The strongest case made for 180 days was that a rolling window encumbers more coins per day than an expiring one-year rule that releases everything at once — true, but that comparison is against #429, not against another rolling window. At steady state a rolling rule holds exactly its depth in flight (one unlock per block), so this PR keeps 52,560 coinbases encumbered at any instant versus 25,920 — twice the standing deterrent — while still being rolling and still having no cliff. On the metric used to argue for rolling, the longer window wins.

4. On the retroactivity objection (@SeventyEi8ht)

I think this is the one concern that deserves a concrete answer rather than a rebuttal. Two points:

  • The 29.4.2 notes extended maturity "up to 350 days" and put miners on notice they "may need to wait much longer"; the 45 days was announced as the first step, so the window rewards were not mined under a settled 45-day promise.
  • But if that reading stays contested, it doesn't have to block the rest of the design. Moving the start height to 979920 (covering only rewards mined from the flag day on) leaves the window at T.Softfork: Long coinbase maturity (part 1 of 3) #419's terms and drops the retroactivity question entirely, while keeping the depth, the permanence, and the RDTS-independence intact. The only thing it gives back is cliff removal for those 6,480 rewards. I'd rather keep the no-cliff version, but I'd like that fallback on the table as a committed option so the objection can't be used to sink the whole approach.

Test status

Built green on top of rc2 (consensus + policy + tests). I contributed the end-bound coverage, the clean-activation test, and feature_rolling_maturity_end_bound.py, and ran a mutation pass over both enforcement sites (CheckTxInputs and CoinbaseMaturityRequired): 7 of 9 single-line mutants killed immediately, and the 2 survivors — replacing max(required, rolling) with a bare assignment — are now pinned by the max-invariant test, so a mutant that lets this rule shorten a stricter existing maturity fails. The 979920-release figures above are measured on that unpruned node (tip 978824); happy to share the per-height breakdown if it's useful.

@Kilombino

Kilombino commented Oct 24, 2026 •

Copy link
Copy Markdown

Withdrawn: I had put together an rc5 test build with this PR instead of #468, but we are going with the 180-day rule from #468 that is already in the rc, so I took the build down.

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.