Properly construct coinbase BUMP for legacy blocks with 2+ subtrees#952
Conversation
|
🤖 Claude Code Review Status: Complete Current Review: No issues found. This PR implements a critical fix for coinbase BUMP computation in multi-subtree blocks with excellent technical quality: Implementation Quality:
Test Coverage:
Documentation:
This is a well-engineered fix that follows the project's verification-first approach with strong test evidence. |
|
Benchmark Comparison ReportBaseline: Current: Summary
All benchmark results (sec/op)
Threshold: >10% with p < 0.05 | Generated: 2026-05-27 04:39 UTC |
There was a problem hiding this comment.
Pull request overview
Fixes coinbase BUMP (Block Update Merkle Proof) construction for peer-received legacy blocks with multiple subtrees by ensuring the block-level proof is built from the same “lifted” (padded) final-subtree root used in the header merkle-root computation, preventing reconciliation failures when the final subtree is shorter than the first.
Changes:
- Update
computeAndSetCoinbaseBUMPto lift the final subtree root (when needed) before generating the block-level proof. - Add production-fixture regression tests that reproduce the real-world inconsistency and verify the lift-based reconciliation.
- Add validation-path unit tests ensuring computed coinbase BUMPs reconcile to the header merkle root across key subtree layouts.
Reviewed changes
Copilot reviewed 3 out of 7 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
services/blockvalidation/BlockValidation.go |
Lifts the final subtree root (when shorter) before computing coinbase BUMP, aligning proof construction with model.Block.CheckMerkleRoot / block assembly behavior. |
services/blockvalidation/coinbase_bump_multisubtree_test.go |
Adds unit tests for validation-path coinbase BUMP reconciliation across subtree partitioning scenarios. |
util/bump/production_repro_test.go |
Adds production-data-driven tests reproducing the observed defect and confirming that lifting the final subtree root reconciles to the header merkle root. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| t.Run("two equal-size subtrees (regression guard)", func(t *testing.T) { | ||
| ctx := context.Background() | ||
| store := blobmemory.New() | ||
| cb := newTestCoinbaseTx(t, height) | ||
|
|
||
| subtree0 := newCoinbaseSubtree(t, store, 4, []byte{0x41, 0x42, 0x43}) | ||
| subtree1 := newLeafSubtree(t, store, 4, []byte{0x51, 0x52, 0x53, 0x54}) // same size, no lift needed | ||
|
|
||
| canonical := canonicalRoot(t, cb, subtree0, subtree1) | ||
| block := newMultiSubtreeBlock(t, ctx, cb, height, canonical, subtree0, subtree1) | ||
|
|
||
| assertValidationBUMPReconciles(t, ctx, store, block, cb, canonical) | ||
| }) | ||
| } |
oskarszoon
left a comment
There was a problem hiding this comment.
Approve. Lift is a faithful copy of the identical logic at model.Block.CheckMerkleRoot:1445-1452 and blockassembly.createMerkleTreeFromSubtrees:1541-1553 — block-validation was the missing third site, now symmetric. BUMP format unchanged (single hash-value substitution at one slot, no path-length / flag / offset changes). subtree.RootHashPadded uses canonical BSV duplicate-last-leaf SHA256d-up padding.
Production tests use real mainnet blocks (950675, 950739) served by two different peers — both authoritative, same canonical block. Pre-fix 2-subtree variants didn't reconcile; post-fix TestProductionCoinbaseBUMP_LiftReconcilesFinalSubtree patches the broken BUMP's top sibling to the lifted root and proves reconciliation.
Worth a follow-up ticket (NOT this PR)
The same class of bug exists in the general-tx BUMP path at util/merkleproof/merkle_proof.go:159 (ConstructMerkleProof) and :247 (ConstructSubtreeMerkleProof). Both pass raw block.Subtrees to GenerateBlockMerkleProof without the lift. PR #952 correctly scopes itself to the coinbase case (coinbase always at subtree 0, full-size, so the worst case doesn't apply), but tx-in-non-final-subtree BUMPs from received blocks with a smaller final subtree won't reconcile to the header. Tx-in-final-subtree case needs liftDepth self-hashing rows between the within-subtree and block-level proof segments.



This pull request fixes a critical bug in the computation of the coinbase BUMP (Block Update Merkle Proof) for blocks with multiple subtrees, specifically when the final subtree is smaller than the first. The fix ensures that the block-level proof used for the BUMP is constructed from subtree roots that are "lifted" (padded) to the correct height, matching the logic used in block header merkle root computation. This resolves a production issue where the computed BUMP did not reconcile with the block-header merkle root in such cases. The PR also adds comprehensive unit and production-data-driven tests to prevent regressions and document the fix.
Bug fix: Multi-subtree coinbase BUMP computation
BlockValidation.go, updatedcomputeAndSetCoinbaseBUMPto lift the final subtree's root to the first subtree's height before computing the coinbase BUMP, ensuring the proof matches the block-header merkle root. Added a new helper methodliftFinalSubtreeRootForBUMPto perform this logic. [1] [2]Testing: Unit and production regression tests
coinbase_bump_multisubtree_test.gowith unit tests that verify the coinbase BUMP always reconciles to the block-header merkle root for single-subtree, equal-size multi-subtree, and the problematic "multi-subtree with smaller final" cases.production_repro_test.gowith tests using real mainnet block data to reproduce the production defect and prove the fix works by lifting the final subtree root, ensuring reconciliation in all observed cases.