Skip to content

Consensus: Extended coinbase maturity temporary softfork (expires with RDTS) (MTP variant) - #403

Closed
chrisguida wants to merge 7 commits into
bitcoinknots:29.x-knotsfrom
chrisguida:extended-coinbase-maturity-mtp-29+knots
Closed

chrisguida wants to merge 7 commits into
bitcoinknots:29.x-knotsfrom
chrisguida:extended-coinbase-maturity-mtp-29+knots

Conversation

@chrisguida

Copy link
Copy Markdown

MTP variant of #402

@4Ethos

4Ethos commented Sep 11, 2026

Copy link
Copy Markdown

Concept ACK

@18650Engineer

18650Engineer commented Sep 11, 2026 •

Copy link
Copy Markdown

Can we add in the code an automatic extension of original amount of days if the threat still exists?
Concept ACK

@jasonsopko

Copy link
Copy Markdown

Tested 9cdd536, same build and the same set of tests as #402, all pass, including the non-enforcing-peer case I described there. Worth saying that this branch's own test runs in 2 seconds against 166 for #402, because it does not have to mine 26280 blocks to reach the boundary, and it covers two things #402's cannot reach cheaply: a reorg that drops the median back below the maturity time, and the wallet going from immature to trusted.

Median-time-past answers the units point, and the spacing I measured on #402 says that was the substantive one on this chain. But it moves the other finding rather than leaving it where it was. The lock is still the smaller of the rule and the time left before the RDTS expiry, so an output created a day before expiry still serves a day. Raising the nominal term from what 26280 blocks currently buys, about 51 days, to a real 182.5 days raises the longest lock without moving the shortest. Every individual lock is at least as long as it would be under #402, so this is not a regression, but the gap between a miner present at activation and one starting at the tail of the window goes from roughly 51 days to roughly 182.5. That gap is the thing Kyle was pointing at, and this branch widens it by three and a half times.

It also puts a date on the flag day. RDTS expires 2027-09-01, so activation has to fall before 2027-03-02 for any output at all to serve a full six months, which is 172 days from today. After that the expiry, not this rule, sets every lock.

Kyle's suggestion on #402, fixing the requirement when the output is created and letting it outlive RDTS, is the shape that closes this, and neither branch does it. If the truncation stays deliberate, I would put it in the release note in those words: outputs created late in the window are locked until the expiry and no longer. That is the sentence a miner deciding when to start will look for, and right now it is only derivable from the "including those created during the window that had not yet served their six months" clause.

Three small things:

  • PreChecks walks every input and calls AccessCoin again before testing whether the rule is active at all. ConnectBlock hoists that flag into extended_coinbase_maturity_active and PreChecks can do the same.
  • The wallet asks extendedCoinbaseBlocksToMaturity, which takes cs_main, for every confirmed coinbase whatever its depth. Consensus: Extended coinbase maturity temporary softfork (expires with RDTS) #402 guards it behind a depth test first. On a mining wallet with a lot of rewards that is a cs_main acquisition per coinbase on every coin selection.
  • Both release notes end with "requires -rdtsexpiry and must precede it", which reads as an ordering requirement on the command line. The code means the start time has to be less than the expiry time.

@iohzrd

iohzrd commented Sep 11, 2026

Copy link
Copy Markdown

Concept ACK

2 similar comments
@sturms

sturms commented Sep 11, 2026

Copy link
Copy Markdown

Concept ACK

@SpectrGen1

Copy link
Copy Markdown

Concept ACK

@kwsantiago

Copy link
Copy Markdown

Built this branch and ran it on regtest. Three things, one with a patch.

Everything covered unlocks in one block at expiry (src/validation.cpp:299)

The release notes call this out, but the size of it is worth seeing. Three covered coinbases, expiry scheduled 10 days after the start, all well past 100 confirmations:

before expiry   height 133: age 599916s of 3888000s -> allowed=False
                height 134: age 599916s of 3888000s -> allowed=False
                height 135: age 599916s of 3888000s -> allowed=False
after expiry    height 133: age 864000s of 3888000s -> allowed=True
                height 134: age 864000s of 3888000s -> allowed=True
                height 135: age 864000s of 3888000s -> allowed=True
all three mined in one block

They unlock at 22% of the required maturity, together, so whatever the tail of the window was buying is handed back in a single block. Dropping the early return at 299 leaves the creating-block test at 303 to establish coverage on its own: each coin then serves its own 45 days from its own block, and the release is spread over 45 days instead of landing at once. #404 and #409 both make the schedule stick to the coin for this reason.

The window cannot end before the RDTS expiry (src/consensus/params.h:221)

mtp_prev < RdtsExpiryTime makes running all the way to the expiry the only expressible deployment, so shipping a short window and extending it once there has been wider input is not available. That ordering is the safe one: extending a window later only rejects more blocks, so it stays a soft fork, while shortening one is a hard fork, since a coin created inside the old window needs 45 days to old nodes and nothing to new ones. Start long and you cannot walk it back.

Patch on top of your head commit, if it is useful: https://github.com/privkeyio/bitcoin/tree/extended-coinbase-maturity-end-time

It adds ExtendedCoinbaseMaturityEndTime and makes the effective end std::min(end, RdtsExpiryTime). The default is INT64_MAX, so the minimum is the RDTS expiry and current behavior is unchanged, and the deployment still can never outlast RDTS, it can only close earlier. The regtest option becomes -extendedcoinbasematurity=<start>[:<end>] and getdeploymentinfo reports the effective expiry. Your functional test passes; it needed one line changed because the error message text moved, and I added rejection cases for the new field. A window closing on its own schedule with RDTS still live:

active at mtp 1296788703: expiry_time 1297652602, active True
coin mined inside the window, depth 111: allowed=False
mtp 1297652703, rdts expiry 1336788602, active=False
coin mined after the end, depth 100: allowed=True

This only moves when the window ends, so it is orthogonal to the first point: with a 30-day window the batch unlock lands 30 days in rather than a year in.

Minor (src/validation.cpp:311)

Coverage anchors on coinbase_block->pprev at 305, but the deadline anchors on the creating block itself, whose median-time-past its own miner can pull down by roughly a block interval. Worth minutes rather than days, but BIP 68 uses the h-1 anchor for both and 305 already does.

@chrisguida

Copy link
Copy Markdown
Author

Closing: superseded by #419 (shipped in 29.4.2) and its follow-up #429.

@chrisguida chrisguida closed this Sep 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants