Repository navigation
fix(blockvalidation): reject blocks whose coinbase is not final, and stop block assembly mining them #2017
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
freemans13
merged 14 commits into
bsv-blockchain:main
from
freemans13:stu/coinbase-finality-check
Oct 28, 2026
Merged
fix(blockvalidation): reject blocks whose coinbase is not final, and stop block assembly mining them #2017
Changes from 5 commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
e76c136
fix(blockvalidation): reject blocks whose coinbase is not final
freemans13 b8867d8
Merge remote-tracking branch 'upstream/main' into stu/coinbase-finali…
freemans13 cc43626
fix(blockassembly): never mine a block whose coinbase is not final
freemans13 db54c95
test(blockassembly): read the coinbase finality median time past from…
freemans13 1acd9f9
test(blockvalidation): run the quick-validate coinbase finality test …
freemans13 1ebc56d
fix(blockassembly): keep the submit path's median-time-past lookup ou…
freemans13 fa510dd
docs(changelog): say that a coinbase-less mining solution is still un…
freemans13 20825dd
Merge remote-tracking branch 'origin/main' into stu/coinbase-finality…
icellan 8164da4
fix(blockassembly): check proof of work before the coinbase finality …
freemans13 c48c952
Merge remote-tracking branch 'upstream/main' into pr2017-loop
freemans13 7d59091
refactor(blockassembly): move the coinbase time-lock check into its o…
freemans13 70ea819
Merge upstream/main into stu/coinbase-finality-check
freemans13 1999dc2
fix(blockassembly): log a coinbase finality refusal after the proof-o…
freemans13 2f1bf5d
fix(blockassembly): name the block hash in the coinbase finality refusal
freemans13 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| package model | ||
|
|
||
| import ( | ||
| "github.com/bsv-blockchain/go-bt/v2" | ||
| "github.com/bsv-blockchain/go-chaincfg" | ||
| "github.com/bsv-blockchain/teranode/util" | ||
| ) | ||
|
|
||
| // lockTimeThreshold is bitcoin-sv's LOCKTIME_THRESHOLD: an nLockTime below it is a block height, | ||
| // at or above it a unix timestamp. | ||
| const lockTimeThreshold = 500_000_000 | ||
|
|
||
| // CoinbaseFinalityViolation applies bitcoin-sv's IsFinalTx to a block's coinbase and returns | ||
| // bitcoin-sv's reject reason, bad-txns-nonfinal, when the coinbase is not final at height in a block | ||
| // stamped blockTime, or "" when it is. bitcoin-sv's ContextualCheckBlock applies IsFinalTx to every | ||
| // transaction in the block; regular transactions get it from the validator, but the coinbase never | ||
| // goes through the validator, so a block with a non-final coinbase was accepted | ||
| // (bitcoin-sv/teranode#4981). | ||
| // | ||
| // A time-based nLockTime is compared as ContextualCheckBlock does: against blockTime before CSV | ||
| // activates, against the parent's median time past (BIP113) from CSV onwards. parentMedianTimePast | ||
| // is the median time past of the 11 blocks ending at the block's parent, or 0 when the caller has no | ||
| // ancestor headers. In that case, when the verdict depends on it (CoinbaseFinalityNeedsMedianTimePast), | ||
| // the coinbase is passed, the same way Valid skips the median-time-past header rule without ancestor | ||
| // headers; the sequence-number short-circuit and a height-based nLockTime are still enforced. | ||
| // | ||
| // Deliberately a reason rather than an error, like CoinbaseCommonRuleViolation: the verdict depends | ||
| // on whether the body is bound to its header, which only the caller knows. The caller must guarantee | ||
| // a non-nil coinbaseTx with at least one input. | ||
| func CoinbaseFinalityViolation(coinbaseTx *bt.Tx, blockTime uint32, height uint32, params *chaincfg.Params, parentMedianTimePast uint32) string { | ||
| lockTimeCutoff := blockTime | ||
| if height >= params.CSVHeight { | ||
| if parentMedianTimePast == 0 && CoinbaseFinalityNeedsMedianTimePast(coinbaseTx, height, params) { | ||
| return "" | ||
| } | ||
|
|
||
| lockTimeCutoff = parentMedianTimePast | ||
| } | ||
|
|
||
| if util.IsTransactionFinal(coinbaseTx, height, lockTimeCutoff) != nil { | ||
| return "bad-txns-nonfinal" | ||
| } | ||
|
|
||
| return "" | ||
| } | ||
|
|
||
| // CoinbaseFinalityNeedsMedianTimePast reports whether CoinbaseFinalityViolation's verdict on | ||
| // coinbaseTx at height depends on the parent's median time past. That is only so from CSV onwards, | ||
| // for a time-based nLockTime, and when some input's sequence number is not final; in every other | ||
| // case the verdict is the same whatever parentMedianTimePast is passed, including 0. A caller that | ||
| // has no ancestor headers to hand uses this to fetch the median time past only when it matters. | ||
| func CoinbaseFinalityNeedsMedianTimePast(coinbaseTx *bt.Tx, height uint32, params *chaincfg.Params) bool { | ||
| if height < params.CSVHeight || coinbaseTx.LockTime < lockTimeThreshold { | ||
| return false | ||
| } | ||
|
|
||
| for _, input := range coinbaseTx.Inputs { | ||
| if input.SequenceNumber != bt.DefaultSequenceNumber { | ||
| return true | ||
| } | ||
| } | ||
|
|
||
| return false | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Coinbase finality only needs the coinbase, height, header time and parent MTP. No subtrees. Yet it runs in ValidWithBinding, not beside CheckHeaderContextual. With optimistic mining on, a block whose coinbase has a non-final sequence and nLockTime >= height passes CheckHeaderContextual, gets added as the tip and is built on, and only the background Valid then invalidates it, which forces a reorg. bitcoin-sv rejects it in ContextualCheckBlock before accepting it. Move the check into the synchronous contextual step.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One thing to keep in mind if this moves: at the optimistic pre-add point (BlockValidation.go, just before
AddBlock), the coinbase is only bound to the header for a subtree-less body (CheckCoinbaseOnlyBodyBound). For a body with subtrees, a finality failure there is a verdict on an unbound coinbase. It should not mark the block invalid or penalize the peer as a header verdict would. The safe shape is probably: on a violation, skip the optimistic add and fall through to the fullValidpath, which classifies it withbindErr. The existing coinbase rules (BIP34 height, bad-cb-length) have the same exposure under optimistic mining today, so whatever is chosen could cover them too.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have left the check where it is in this PR and flagged the placement to Stu as a design decision. Here is why.
The exposure needs an opt-in that is off by default. The pre-
Validadd only happens when bothblockvalidation_optimistic_mining(default true) andblockvalidation_optimistic_mining_peer_blocks(default false,settings/settings.go:416) are set. Catch-up and the legacy route are never optimistic. On a default node every peer block runs the fullValid, finality included, beforeAddBlock.Under the opt-in, finality has the same exposure as the coinbase rules next to it:
CoinbaseCommonRuleViolation, bad-cb-length and the BIP34 height check. They all sit after the merkle binding for the reason github-actions gave. For a body with subtrees, the coinbase is not bound to the header untilCheckMerkleRootruns insideValid. A finality verdict at the optimistic pre-add point (right afterCheckHeaderContextualinBlockValidation.go) would be a verdict on a coinbase the header may not commit to. Marking the block invalid or charging the peer there would let anyone who swaps the coinbase poison a good header. bitcoin-sv can reject inContextualCheckBlockbecause itsCheckBlockhas already checked the merkle root by then.The safe shape would be: at the pre-add point, run the coinbase rules with the median time past
checkHeaderContextualalready computes. On a violation, skip the optimistic add and fall through to the full path, which classifies the failure withbindErr. No verdict and no penalty at the pre-add point. That should cover all four coinbase rules, not finality alone. The opt-in's own setting docs say to keep it off until theblock.Validintegrity-floor split lands, and that split is where the coinbase rules would naturally move together with the binding. Doing it for one rule here would leave the other three exposed in the same way.Stu, this one is yours: fall-through for all coinbase rules in a follow-up, or wait for the
block.Validsplit.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Deferring this to #2077, which tracks the
block.Validsplit that moves the integrity floor ahead of the optimistic add. Moving finality alone would not close the gap. For a body with subtrees the coinbase is not yet bound to the header at that point, and the BIP34 height,bad-cb-lengthand shared coinbase rules have the same exposure. The flag stays off by default, so a default node fully validates, finality included, before a block joins the chain.