Skip to content

Properly construct coinbase BUMP for legacy blocks with 2+ subtrees#952

Merged
galt-tr merged 1 commit into
bsv-blockchain:mainfrom
galt-tr:cbBumpFix
May 27, 2026
Merged

Properly construct coinbase BUMP for legacy blocks with 2+ subtrees#952
galt-tr merged 1 commit into
bsv-blockchain:mainfrom
galt-tr:cbBumpFix

Conversation

@galt-tr

@galt-tr galt-tr commented May 27, 2026

Copy link
Copy Markdown
Contributor

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

  • In BlockValidation.go, updated computeAndSetCoinbaseBUMP to 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 method liftFinalSubtreeRootForBUMP to perform this logic. [1] [2]

Testing: Unit and production regression tests

  • Added coinbase_bump_multisubtree_test.go with 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.
  • Added production_repro_test.go with 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.

@github-actions

github-actions Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor

🤖 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:

  • Correctly mirrors the lifting logic from model.Block.CheckMerkleRoot (lines 1445-1451) and blockassembly.createMerkleTreeFromSubtrees (lines 1541-1552)
  • The fix is minimal and surgical, adding exactly 11 lines to perform the lift when len(block.Subtrees) > 1
  • Properly loads the final subtree, checks if it's smaller than the first, and lifts its root to match the first subtree's height
  • Error handling is consistent with surrounding code patterns

Test Coverage:

  • coinbase_bump_multisubtree_test.go: Comprehensive unit tests covering the problematic case (multi-subtree with smaller final), plus regression guards for single-subtree and equal-size cases
  • production_repro_test.go: Uses literal mainnet block data (blocks 950675 and 950739) to reproduce the production defect and prove the fix works
  • Tests validate the invariant: computed BUMP must reconcile to block-header merkle root regardless of subtree decomposition

Documentation:

  • Inline comments clearly explain the rationale, referencing the production RCAs and connecting to existing code paths
  • Test file headers provide excellent context about the production issue being fixed

This is a well-engineered fix that follows the project's verification-first approach with strong test evidence.

@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown
Contributor

Benchmark Comparison Report

Baseline: main (unknown)

Current: PR-952 (cb66d8b)

Summary

  • Regressions: 0
  • Improvements: 0
  • Unchanged: 144
  • Significance level: p < 0.05
All benchmark results (sec/op)
Benchmark Baseline Current Change p-value
_NewBlockFromBytes-4 1.755µ 1.748µ ~ 0.700
SplitSyncedParentMap_SetIfNotExists/256_buckets-4 61.96n 61.75n ~ 1.000
SplitSyncedParentMap_SetIfNotExists/16_buckets-4 61.65n 61.50n ~ 0.100
SplitSyncedParentMap_SetIfNotExists/1_bucket-4 61.73n 61.68n ~ 1.000
SplitSyncedParentMap_ConcurrentSetIfNotExists/256_buckets... 35.05n 31.87n ~ 0.100
SplitSyncedParentMap_ConcurrentSetIfNotExists/16_buckets_... 53.84n 52.66n ~ 0.200
SplitSyncedParentMap_ConcurrentSetIfNotExists/1_bucket_pa... 162.2n 125.6n ~ 0.100
MiningCandidate_Stringify_Short-4 272.8n 271.8n ~ 0.100
MiningCandidate_Stringify_Long-4 1.961µ 1.911µ ~ 0.100
MiningSolution_Stringify-4 997.7n 973.1n ~ 0.100
BlockInfo_MarshalJSON-4 1.855µ 1.817µ ~ 0.100
NewFromBytes-4 122.2n 129.7n ~ 0.100
AddTxBatchColumnar_Validation-4 2.382µ 2.436µ ~ 0.400
OffsetValidationLoop-4 720.0n 720.2n ~ 1.000
Mine_EasyDifficulty-4 60.82µ 61.79µ ~ 0.100
Mine_WithAddress-4 7.689µ 7.029µ ~ 0.200
DirectSubtreeAdd/4_per_subtree-4 58.21n 58.33n ~ 1.000
DirectSubtreeAdd/64_per_subtree-4 29.54n 29.94n ~ 0.700
DirectSubtreeAdd/256_per_subtree-4 28.33n 29.00n ~ 0.100
DirectSubtreeAdd/1024_per_subtree-4 27.25n 27.86n ~ 0.100
DirectSubtreeAdd/2048_per_subtree-4 26.90n 27.49n ~ 0.100
SubtreeProcessorAdd/4_per_subtree-4 286.7n 289.8n ~ 0.200
SubtreeProcessorAdd/64_per_subtree-4 285.7n 285.6n ~ 1.000
SubtreeProcessorAdd/256_per_subtree-4 280.7n 285.5n ~ 0.400
SubtreeProcessorAdd/1024_per_subtree-4 278.1n 277.7n ~ 0.500
SubtreeProcessorAdd/2048_per_subtree-4 276.4n 278.2n ~ 0.700
SubtreeProcessorRotate/4_per_subtree-4 282.1n 279.6n ~ 0.400
SubtreeProcessorRotate/64_per_subtree-4 277.8n 279.0n ~ 0.700
SubtreeProcessorRotate/256_per_subtree-4 278.7n 281.3n ~ 0.400
SubtreeProcessorRotate/1024_per_subtree-4 281.4n 278.8n ~ 0.100
SubtreeNodeAddOnly/4_per_subtree-4 54.60n 54.57n ~ 1.000
SubtreeNodeAddOnly/64_per_subtree-4 34.40n 34.41n ~ 1.000
SubtreeNodeAddOnly/256_per_subtree-4 33.43n 33.23n ~ 0.100
SubtreeNodeAddOnly/1024_per_subtree-4 32.69n 32.60n ~ 0.100
SubtreeCreationOnly/4_per_subtree-4 114.0n 114.7n ~ 1.000
SubtreeCreationOnly/64_per_subtree-4 397.5n 397.2n ~ 0.700
SubtreeCreationOnly/256_per_subtree-4 1.341µ 1.353µ ~ 0.100
SubtreeCreationOnly/1024_per_subtree-4 4.392µ 4.393µ ~ 0.700
SubtreeCreationOnly/2048_per_subtree-4 8.083µ 8.036µ ~ 1.000
SubtreeProcessorOverheadBreakdown/64_per_subtree-4 275.6n 276.2n ~ 0.700
SubtreeProcessorOverheadBreakdown/1024_per_subtree-4 277.8n 276.2n ~ 0.200
ParallelGetAndSetIfNotExists/1k_nodes-4 2.160m 2.172m ~ 1.000
ParallelGetAndSetIfNotExists/10k_nodes-4 5.208m 5.197m ~ 1.000
ParallelGetAndSetIfNotExists/50k_nodes-4 6.977m 7.026m ~ 0.700
ParallelGetAndSetIfNotExists/100k_nodes-4 9.450m 9.594m ~ 0.100
SequentialGetAndSetIfNotExists/1k_nodes-4 1.948m 1.946m ~ 0.700
SequentialGetAndSetIfNotExists/10k_nodes-4 4.311m 4.339m ~ 0.100
SequentialGetAndSetIfNotExists/50k_nodes-4 11.99m 12.17m ~ 0.200
SequentialGetAndSetIfNotExists/100k_nodes-4 21.90m 22.05m ~ 0.100
ProcessOwnBlockSubtreeNodesParallel/1k_nodes-4 2.233m 2.228m ~ 0.700
ProcessOwnBlockSubtreeNodesParallel/10k_nodes-4 8.149m 8.117m ~ 0.700
ProcessOwnBlockSubtreeNodesParallel/100k_nodes-4 12.78m 12.70m ~ 0.700
ProcessOwnBlockSubtreeNodesSequential/1k_nodes-4 1.991m 1.971m ~ 0.400
ProcessOwnBlockSubtreeNodesSequential/10k_nodes-4 7.400m 7.414m ~ 0.400
ProcessOwnBlockSubtreeNodesSequential/100k_nodes-4 39.59m 39.45m ~ 0.200
DiskTxMap_SetIfNotExists-4 3.772µ 4.053µ ~ 0.400
DiskTxMap_SetIfNotExists_Parallel-4 3.383µ 3.522µ ~ 0.100
DiskTxMap_ExistenceOnly-4 402.2n 330.7n ~ 0.700
Queue-4 190.6n 188.1n ~ 0.100
AtomicPointer-4 4.571n 4.819n ~ 0.400
ReorgOptimizations/DedupFilterPipeline/Old/10K-4 927.7µ 948.2µ ~ 0.400
ReorgOptimizations/DedupFilterPipeline/New/10K-4 809.5µ 811.0µ ~ 0.400
ReorgOptimizations/AllMarkFalse/Old/10K-4 109.0µ 110.9µ ~ 0.100
ReorgOptimizations/AllMarkFalse/New/10K-4 62.96µ 62.18µ ~ 0.100
ReorgOptimizations/HashSlicePool/Old/10K-4 57.39µ 59.14µ ~ 0.100
ReorgOptimizations/HashSlicePool/New/10K-4 11.78µ 11.59µ ~ 0.700
ReorgOptimizations/NodeFlags/Old/10K-4 4.845µ 4.979µ ~ 0.100
ReorgOptimizations/NodeFlags/New/10K-4 1.676µ 1.643µ ~ 0.100
ReorgOptimizations/DedupFilterPipeline/Old/100K-4 10.01m 10.34m ~ 0.100
ReorgOptimizations/DedupFilterPipeline/New/100K-4 10.71m 11.05m ~ 0.100
ReorgOptimizations/AllMarkFalse/Old/100K-4 1.114m 1.133m ~ 0.100
ReorgOptimizations/AllMarkFalse/New/100K-4 686.8µ 685.3µ ~ 0.700
ReorgOptimizations/HashSlicePool/Old/100K-4 608.4µ 680.4µ ~ 0.100
ReorgOptimizations/HashSlicePool/New/100K-4 311.6µ 328.5µ ~ 1.000
ReorgOptimizations/NodeFlags/Old/100K-4 46.65µ 46.67µ ~ 1.000
ReorgOptimizations/NodeFlags/New/100K-4 16.39µ 15.87µ ~ 0.100
TxMapSetIfNotExists-4 52.58n 52.99n ~ 0.400
TxMapSetIfNotExistsDuplicate-4 40.19n 40.44n ~ 0.400
ChannelSendReceive-4 639.6n 591.7n ~ 0.100
BlockAssembler_AddTx-4 0.02837n 0.02775n ~ 1.000
AddNode-4 11.97 11.47 ~ 0.700
AddNodeWithMap-4 13.06 11.85 ~ 0.100
CalcBlockWork-4 530.6n 511.3n ~ 1.000
CalculateWork-4 697.8n 698.1n ~ 1.000
BuildBlockLocatorString_Helpers/Size_10-4 1.221µ 1.224µ ~ 0.500
BuildBlockLocatorString_Helpers/Size_100-4 11.54µ 11.65µ ~ 0.400
BuildBlockLocatorString_Helpers/Size_1000-4 128.3µ 115.3µ ~ 1.000
CatchupWithHeaderCache-4 104.3m 104.4m ~ 0.200
_BufferPoolAllocation/16KB-4 4.051µ 4.008µ ~ 0.700
_BufferPoolAllocation/32KB-4 11.12µ 10.11µ ~ 0.700
_BufferPoolAllocation/64KB-4 19.99µ 19.55µ ~ 0.100
_BufferPoolAllocation/128KB-4 39.28µ 31.28µ ~ 0.200
_BufferPoolAllocation/512KB-4 116.4µ 123.1µ ~ 0.400
_BufferPoolConcurrent/32KB-4 19.40µ 19.67µ ~ 0.200
_BufferPoolConcurrent/64KB-4 31.04µ 32.26µ ~ 0.100
_BufferPoolConcurrent/512KB-4 151.0µ 156.9µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/16KB-4 648.3µ 697.9µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/32KB-4 644.1µ 693.0µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/64KB-4 645.3µ 691.9µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/128KB-4 652.0µ 689.7µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/512KB-4 659.2µ 723.1µ ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/16KB-4 36.85m 36.47m ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/32KB-4 36.52m 36.80m ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/64KB-4 36.69m 37.14m ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/128KB-4 36.84m 36.65m ~ 0.700
_SubtreeDataDeserializationWithBufferSizes/512KB-4 36.82m 36.41m ~ 0.700
_PooledVsNonPooled/Pooled-4 741.6n 743.8n ~ 0.200
_PooledVsNonPooled/NonPooled-4 9.400µ 9.442µ ~ 0.700
_MemoryFootprint/Current_512KB_32concurrent-4 7.039µ 7.095µ ~ 0.400
_MemoryFootprint/Proposed_32KB_32concurrent-4 9.678µ 11.030µ ~ 0.100
_MemoryFootprint/Alternative_64KB_32concurrent-4 9.384µ 11.422µ ~ 0.100
SubtreeSizes/10k_tx_4_per_subtree-4 1.346m 1.344m ~ 1.000
SubtreeSizes/10k_tx_16_per_subtree-4 322.1µ 328.5µ ~ 0.100
SubtreeSizes/10k_tx_64_per_subtree-4 75.28µ 78.02µ ~ 0.100
SubtreeSizes/10k_tx_256_per_subtree-4 19.02µ 19.01µ ~ 1.000
SubtreeSizes/10k_tx_512_per_subtree-4 9.557µ 9.286µ ~ 0.100
SubtreeSizes/10k_tx_1024_per_subtree-4 4.732µ 4.638µ ~ 0.400
SubtreeSizes/10k_tx_2k_per_subtree-4 2.311µ 2.390µ ~ 0.100
BlockSizeScaling/10k_tx_64_per_subtree-4 74.24µ 75.26µ ~ 0.100
BlockSizeScaling/10k_tx_256_per_subtree-4 18.63µ 19.25µ ~ 0.100
BlockSizeScaling/10k_tx_1024_per_subtree-4 4.648µ 4.707µ ~ 0.200
BlockSizeScaling/50k_tx_64_per_subtree-4 389.4µ 396.3µ ~ 1.000
BlockSizeScaling/50k_tx_256_per_subtree-4 92.48µ 94.90µ ~ 0.100
BlockSizeScaling/50k_tx_1024_per_subtree-4 23.01µ 23.64µ ~ 0.100
SubtreeAllocations/small_subtrees_exists_check-4 158.0µ 159.1µ ~ 0.400
SubtreeAllocations/small_subtrees_data_fetch-4 164.2µ 163.8µ ~ 0.700
SubtreeAllocations/small_subtrees_full_validation-4 324.1µ 321.5µ ~ 0.400
SubtreeAllocations/medium_subtrees_exists_check-4 9.309µ 9.165µ ~ 0.400
SubtreeAllocations/medium_subtrees_data_fetch-4 9.472µ 9.528µ ~ 1.000
SubtreeAllocations/medium_subtrees_full_validation-4 18.97µ 19.20µ ~ 0.100
SubtreeAllocations/large_subtrees_exists_check-4 2.229µ 2.247µ ~ 0.400
SubtreeAllocations/large_subtrees_data_fetch-4 2.329µ 2.301µ ~ 0.700
SubtreeAllocations/large_subtrees_full_validation-4 4.747µ 4.749µ ~ 1.000
_prepareTxsPerLevel-4 313.5m 310.5m ~ 0.200
_prepareTxsPerLevelOrdered-4 2.647m 2.772m ~ 0.100
_prepareTxsPerLevel_Comparison/Original-4 307.7m 305.8m ~ 0.200
_prepareTxsPerLevel_Comparison/Optimized-4 2.837m 2.740m ~ 0.100
StoreBlock_Sequential/BelowCSVHeight-4 334.5µ 336.3µ ~ 0.100
StoreBlock_Sequential/AboveCSVHeight-4 338.2µ 340.6µ ~ 0.700
GetUtxoHashes-4 274.2n 274.1n ~ 1.000
GetUtxoHashes_ManyOutputs-4 45.55µ 47.77µ ~ 0.100
_NewMetaDataFromBytes-4 215.2n 213.6n ~ 1.000
_Bytes-4 395.7n 400.5n ~ 0.400
_MetaBytes-4 139.1n 140.1n ~ 0.700

Threshold: >10% with p < 0.05 | Generated: 2026-05-27 04:39 UTC

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 computeAndSetCoinbaseBUMP to 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.

Comment on lines +78 to +91
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 oskarszoon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@galt-tr galt-tr merged commit 8ad683a into bsv-blockchain:main May 27, 2026
25 checks passed
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.

4 participants