Skip to content

Fix merkle proof calculation with proper coinbase replacement#993

Merged
galt-tr merged 1 commit into
bsv-blockchain:mainfrom
galt-tr:fix992
Jun 2, 2026
Merged

Fix merkle proof calculation with proper coinbase replacement#993
galt-tr merged 1 commit into
bsv-blockchain:mainfrom
galt-tr:fix992

Conversation

@galt-tr

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

Copy link
Copy Markdown
Contributor

Fixes #992

This pull request significantly improves the correctness and robustness of Merkle proof generation and BUMP encoding, especially around coinbase transaction handling and subtree padding. It ensures that proofs are compatible with the go-bc reference implementation and conform to BRC-74 by treating the block's Merkle tree as a single flat structure, correctly handling global offsets, and mirroring all block header calculations (including coinbase placeholder replacement and subtree padding). The changes also introduce comprehensive end-to-end regression tests to prevent future regressions.

Key changes include:

Correctness of Merkle Proof Construction and BUMP Encoding

  • Refactored ConvertToBUMP in format.go to compute and use global transaction offsets (not subtree-local), ensuring BUMP proofs are valid for blocks with multiple subtrees and compatible with go-bc. Sibling offsets at each level are now calculated using the transaction's global leaf offset.
  • Updated test expectations in format_test.go to match the new global offset logic, ensuring tests verify the correct BUMP structure and offsets.

Coinbase Placeholder and Subtree Padding Handling

  • In merkle_proof.go, added logic to detect and replace the coinbase placeholder in the first subtree with the actual coinbase txid, mirroring block header calculation. Also, implemented logic to pad the final subtree if incomplete, ensuring the Merkle root matches the block header. [1] [2]
  • Adjusted subtree proof generation to use a coinbase-replaced clone of the subtree when necessary, and to append "lift" levels for incomplete final subtrees so the proof reaches the correct lifted root. [1] [2]

Proof Flags and Offset Calculation

  • Fixed calculation of left/right sibling flags for subtree-level proofs, ensuring correct sibling ordering for all transaction indices. Combined subtree and block-level flags into a single sequence for verification.

Testing and Regression Coverage

  • Added a comprehensive end-to-end regression test in coinbase_placeholder_crosscheck_test.go that covers coinbase placeholder replacement, subtree padding, and BUMP verification with go-bc, preventing regressions for these critical cases.

Minor Improvements

  • Imported math/bits in merkle_proof.go for efficient bit operations when calculating subtree heights and offsets.

@galt-tr galt-tr changed the title Fix merkle proof calculation for txs in subtree 0 Fix merkle proof calculation with proper coinbase replacement May 30, 2026
@github-actions

github-actions Bot commented May 30, 2026

Copy link
Copy Markdown
Contributor

🤖 Claude Code Review

Status: Complete


Summary

This PR correctly fixes merkle proof generation to mirror model.Block.CheckMerkleRoot by:

  • Replacing coinbase placeholder with actual coinbase txid in first subtree
  • Padding incomplete final subtrees to match first subtree height
  • Using global transaction offsets in BUMP encoding (fixing multi-subtree blocks)
  • Adding comprehensive regression tests with go-bc cross-validation

The implementation aligns with BRC-74 and ensures proofs are verifiable by reference implementations.

Issues Found

[Minor] ConstructSubtreeMerkleProof doesn't apply the same coinbase/padding transformations as ConstructMerkleProof — see comment below for details.


Overall this is a solid fix with excellent test coverage. The one minor issue doesn't affect the main use case (transaction proofs) that this PR addresses.

@github-actions

Copy link
Copy Markdown
Contributor

[Minor] Missing coinbase/padding transformations in subtree-level proofs

ConstructSubtreeMerkleProof (merkle_proof.go:388) uses raw block.Subtrees hashes without the coinbase replacement and final-subtree padding that ConstructMerkleProof applies (lines 225-241).

This inconsistency means subtree-level proofs won't verify against the block header merkle root for blocks with coinbase placeholders or incomplete final subtrees.

Recommendation: Apply the same firstRoot/lastRoot logic, or document why subtree-level proofs are exempt.

@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown
Contributor

Benchmark Comparison Report

Baseline: main (unknown)

Current: PR-993 (49d7dee)

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.286µ 1.247µ ~ 0.100
SplitSyncedParentMap_SetIfNotExists/256_buckets-4 54.96n 55.30n ~ 0.200
SplitSyncedParentMap_SetIfNotExists/16_buckets-4 54.96n 55.11n ~ 0.100
SplitSyncedParentMap_SetIfNotExists/1_bucket-4 55.04n 55.18n ~ 0.100
SplitSyncedParentMap_ConcurrentSetIfNotExists/256_buckets... 25.34n 25.27n ~ 1.000
SplitSyncedParentMap_ConcurrentSetIfNotExists/16_buckets_... 43.94n 43.43n ~ 0.200
SplitSyncedParentMap_ConcurrentSetIfNotExists/1_bucket_pa... 104.80n 97.20n ~ 0.100
MiningCandidate_Stringify_Short-4 168.1n 169.6n ~ 0.700
MiningCandidate_Stringify_Long-4 1.252µ 1.246µ ~ 0.400
MiningSolution_Stringify-4 647.8n 644.6n ~ 0.700
BlockInfo_MarshalJSON-4 1.332µ 1.335µ ~ 0.400
NewFromBytes-4 131.1n 130.5n ~ 0.400
AddTxBatchColumnar_Validation-4 2.574µ 2.620µ ~ 1.000
OffsetValidationLoop-4 636.2n 639.2n ~ 0.700
Mine_EasyDifficulty-4 60.35µ 60.56µ ~ 1.000
Mine_WithAddress-4 6.795µ 6.865µ ~ 0.100
BlockAssembler_AddTx-4 0.02698n 0.02634n ~ 1.000
AddNode-4 11.46 10.76 ~ 0.200
AddNodeWithMap-4 11.81 11.69 ~ 1.000
DirectSubtreeAdd/4_per_subtree-4 56.44n 55.90n ~ 1.000
DirectSubtreeAdd/64_per_subtree-4 28.89n 29.10n ~ 0.700
DirectSubtreeAdd/256_per_subtree-4 27.74n 27.96n ~ 1.000
DirectSubtreeAdd/1024_per_subtree-4 26.40n 26.48n ~ 0.200
DirectSubtreeAdd/2048_per_subtree-4 26.06n 26.04n ~ 0.300
SubtreeProcessorAdd/4_per_subtree-4 296.0n 301.7n ~ 1.000
SubtreeProcessorAdd/64_per_subtree-4 288.8n 295.5n ~ 0.100
SubtreeProcessorAdd/256_per_subtree-4 285.3n 289.1n ~ 1.000
SubtreeProcessorAdd/1024_per_subtree-4 276.5n 278.5n ~ 0.700
SubtreeProcessorAdd/2048_per_subtree-4 277.7n 281.2n ~ 0.200
SubtreeProcessorRotate/4_per_subtree-4 279.5n 284.0n ~ 0.400
SubtreeProcessorRotate/64_per_subtree-4 279.1n 286.9n ~ 0.100
SubtreeProcessorRotate/256_per_subtree-4 280.3n 289.7n ~ 0.100
SubtreeProcessorRotate/1024_per_subtree-4 281.7n 283.5n ~ 0.200
SubtreeNodeAddOnly/4_per_subtree-4 55.47n 55.21n ~ 0.200
SubtreeNodeAddOnly/64_per_subtree-4 36.21n 36.13n ~ 0.700
SubtreeNodeAddOnly/256_per_subtree-4 35.11n 35.14n ~ 0.700
SubtreeNodeAddOnly/1024_per_subtree-4 34.55n 34.61n ~ 1.000
SubtreeCreationOnly/4_per_subtree-4 111.4n 110.3n ~ 0.100
SubtreeCreationOnly/64_per_subtree-4 349.6n 348.5n ~ 0.700
SubtreeCreationOnly/256_per_subtree-4 1.284µ 1.235µ ~ 0.700
SubtreeCreationOnly/1024_per_subtree-4 3.772µ 3.797µ ~ 0.700
SubtreeCreationOnly/2048_per_subtree-4 6.757µ 6.868µ ~ 0.200
SubtreeProcessorOverheadBreakdown/64_per_subtree-4 283.4n 288.7n ~ 0.700
SubtreeProcessorOverheadBreakdown/1024_per_subtree-4 284.4n 288.0n ~ 0.200
ParallelGetAndSetIfNotExists/1k_nodes-4 2.013m 2.014m ~ 1.000
ParallelGetAndSetIfNotExists/10k_nodes-4 5.223m 5.253m ~ 0.100
ParallelGetAndSetIfNotExists/50k_nodes-4 7.353m 7.532m ~ 0.700
ParallelGetAndSetIfNotExists/100k_nodes-4 10.65m 10.19m ~ 0.200
SequentialGetAndSetIfNotExists/1k_nodes-4 1.810m 1.804m ~ 0.400
SequentialGetAndSetIfNotExists/10k_nodes-4 4.466m 4.900m ~ 0.100
SequentialGetAndSetIfNotExists/50k_nodes-4 13.62m 13.62m ~ 1.000
SequentialGetAndSetIfNotExists/100k_nodes-4 25.24m 26.11m ~ 0.200
ProcessOwnBlockSubtreeNodesParallel/1k_nodes-4 2.053m 2.068m ~ 0.400
ProcessOwnBlockSubtreeNodesParallel/10k_nodes-4 8.565m 8.505m ~ 0.200
ProcessOwnBlockSubtreeNodesParallel/100k_nodes-4 13.52m 13.60m ~ 0.100
ProcessOwnBlockSubtreeNodesSequential/1k_nodes-4 1.832m 1.824m ~ 1.000
ProcessOwnBlockSubtreeNodesSequential/10k_nodes-4 8.368m 8.398m ~ 1.000
ProcessOwnBlockSubtreeNodesSequential/100k_nodes-4 45.00m 44.99m ~ 1.000
DiskTxMap_SetIfNotExists-4 3.682µ 4.027µ ~ 0.100
DiskTxMap_SetIfNotExists_Parallel-4 3.391µ 3.761µ ~ 0.100
DiskTxMap_ExistenceOnly-4 319.2n 394.4n ~ 0.100
Queue-4 186.8n 193.7n ~ 0.100
AtomicPointer-4 3.629n 3.621n ~ 1.000
ReorgOptimizations/DedupFilterPipeline/Old/10K-4 830.8µ 882.0µ ~ 0.100
ReorgOptimizations/DedupFilterPipeline/New/10K-4 755.1µ 842.3µ ~ 0.100
ReorgOptimizations/AllMarkFalse/Old/10K-4 104.7µ 119.2µ ~ 0.200
ReorgOptimizations/AllMarkFalse/New/10K-4 64.11µ 64.62µ ~ 0.200
ReorgOptimizations/HashSlicePool/Old/10K-4 58.57µ 53.19µ ~ 0.100
ReorgOptimizations/HashSlicePool/New/10K-4 10.94µ 10.94µ ~ 1.000
ReorgOptimizations/NodeFlags/Old/10K-4 4.695µ 4.635µ ~ 1.000
ReorgOptimizations/NodeFlags/New/10K-4 1.519µ 1.522µ ~ 0.800
ReorgOptimizations/DedupFilterPipeline/Old/100K-4 9.227m 9.506m ~ 0.400
ReorgOptimizations/DedupFilterPipeline/New/100K-4 9.225m 11.419m ~ 0.100
ReorgOptimizations/AllMarkFalse/Old/100K-4 1.087m 1.160m ~ 0.100
ReorgOptimizations/AllMarkFalse/New/100K-4 705.5µ 709.3µ ~ 0.700
ReorgOptimizations/HashSlicePool/Old/100K-4 534.1µ 556.2µ ~ 0.100
ReorgOptimizations/HashSlicePool/New/100K-4 205.9µ 209.7µ ~ 0.400
ReorgOptimizations/NodeFlags/Old/100K-4 48.81µ 50.09µ ~ 0.100
ReorgOptimizations/NodeFlags/New/100K-4 17.09µ 17.36µ ~ 0.700
TxMapSetIfNotExists-4 49.46n 50.12n ~ 0.100
TxMapSetIfNotExistsDuplicate-4 41.29n 41.48n ~ 0.100
ChannelSendReceive-4 602.0n 630.7n ~ 0.100
CalcBlockWork-4 359.8n 361.6n ~ 0.700
CalculateWork-4 492.9n 499.8n ~ 0.400
BuildBlockLocatorString_Helpers/Size_10-4 1.363µ 1.374µ ~ 0.400
BuildBlockLocatorString_Helpers/Size_100-4 13.10µ 13.24µ ~ 1.000
BuildBlockLocatorString_Helpers/Size_1000-4 156.7µ 160.1µ ~ 1.000
CatchupWithHeaderCache-4 104.6m 104.4m ~ 0.700
_BufferPoolAllocation/16KB-4 4.008µ 4.053µ ~ 0.400
_BufferPoolAllocation/32KB-4 10.223µ 8.913µ ~ 0.700
_BufferPoolAllocation/64KB-4 16.99µ 22.10µ ~ 0.100
_BufferPoolAllocation/128KB-4 34.86µ 32.55µ ~ 0.100
_BufferPoolAllocation/512KB-4 135.8µ 115.8µ ~ 0.200
_BufferPoolConcurrent/32KB-4 19.60µ 19.72µ ~ 0.700
_BufferPoolConcurrent/64KB-4 32.08µ 32.06µ ~ 1.000
_BufferPoolConcurrent/512KB-4 151.8µ 150.4µ ~ 0.400
_SubtreeDeserializationWithBufferSizes/16KB-4 667.5µ 637.5µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/32KB-4 657.4µ 651.2µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/64KB-4 655.6µ 634.1µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/128KB-4 644.6µ 628.5µ ~ 0.400
_SubtreeDeserializationWithBufferSizes/512KB-4 635.0µ 622.3µ ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/16KB-4 36.82m 37.68m ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/32KB-4 37.66m 36.81m ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/64KB-4 37.34m 37.55m ~ 1.000
_SubtreeDataDeserializationWithBufferSizes/128KB-4 37.01m 36.94m ~ 0.400
_SubtreeDataDeserializationWithBufferSizes/512KB-4 37.23m 37.02m ~ 0.400
_PooledVsNonPooled/Pooled-4 744.7n 746.5n ~ 0.700
_PooledVsNonPooled/NonPooled-4 8.269µ 7.959µ ~ 0.400
_MemoryFootprint/Current_512KB_32concurrent-4 6.696µ 6.642µ ~ 0.700
_MemoryFootprint/Proposed_32KB_32concurrent-4 9.668µ 9.932µ ~ 0.100
_MemoryFootprint/Alternative_64KB_32concurrent-4 9.295µ 9.598µ ~ 0.100
SubtreeSizes/10k_tx_4_per_subtree-4 1.370m 1.383m ~ 0.700
SubtreeSizes/10k_tx_16_per_subtree-4 321.9µ 324.2µ ~ 0.400
SubtreeSizes/10k_tx_64_per_subtree-4 77.21µ 78.47µ ~ 0.100
SubtreeSizes/10k_tx_256_per_subtree-4 19.35µ 19.55µ ~ 0.400
SubtreeSizes/10k_tx_512_per_subtree-4 9.576µ 9.754µ ~ 0.200
SubtreeSizes/10k_tx_1024_per_subtree-4 4.812µ 4.802µ ~ 1.000
SubtreeSizes/10k_tx_2k_per_subtree-4 2.374µ 2.397µ ~ 0.700
BlockSizeScaling/10k_tx_64_per_subtree-4 76.30µ 76.55µ ~ 0.700
BlockSizeScaling/10k_tx_256_per_subtree-4 19.26µ 19.47µ ~ 0.700
BlockSizeScaling/10k_tx_1024_per_subtree-4 4.753µ 4.834µ ~ 0.200
BlockSizeScaling/50k_tx_64_per_subtree-4 402.0µ 396.1µ ~ 0.700
BlockSizeScaling/50k_tx_256_per_subtree-4 95.33µ 96.79µ ~ 0.700
BlockSizeScaling/50k_tx_1024_per_subtree-4 23.67µ 23.93µ ~ 0.200
SubtreeAllocations/small_subtrees_exists_check-4 160.9µ 161.7µ ~ 0.400
SubtreeAllocations/small_subtrees_data_fetch-4 166.7µ 168.7µ ~ 0.100
SubtreeAllocations/small_subtrees_full_validation-4 326.5µ 330.9µ ~ 0.400
SubtreeAllocations/medium_subtrees_exists_check-4 9.473µ 9.609µ ~ 0.400
SubtreeAllocations/medium_subtrees_data_fetch-4 9.917µ 10.071µ ~ 0.100
SubtreeAllocations/medium_subtrees_full_validation-4 19.38µ 19.62µ ~ 0.100
SubtreeAllocations/large_subtrees_exists_check-4 2.262µ 2.297µ ~ 0.200
SubtreeAllocations/large_subtrees_data_fetch-4 2.398µ 2.445µ ~ 0.100
SubtreeAllocations/large_subtrees_full_validation-4 4.792µ 4.890µ ~ 0.200
_prepareTxsPerLevel-4 400.5m 397.8m ~ 0.700
_prepareTxsPerLevelOrdered-4 3.503m 3.748m ~ 0.400
_prepareTxsPerLevel_Comparison/Original-4 400.1m 398.7m ~ 1.000
_prepareTxsPerLevel_Comparison/Optimized-4 3.719m 3.467m ~ 0.200
StoreBlock_Sequential/BelowCSVHeight-4 338.4µ 338.0µ ~ 1.000
StoreBlock_Sequential/AboveCSVHeight-4 341.1µ 335.4µ ~ 0.100
GetUtxoHashes-4 269.2n 271.4n ~ 1.000
GetUtxoHashes_ManyOutputs-4 49.19µ 45.56µ ~ 0.100
_NewMetaDataFromBytes-4 214.4n 215.2n ~ 0.600
_Bytes-4 396.1n 396.9n ~ 0.700
_MetaBytes-4 137.9n 140.2n ~ 0.200

Threshold: >10% with p < 0.05 | Generated: 2026-05-30 02:49 UTC

@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. All three defects from #992 plus the incomplete-final-subtree lift are fixed, and the proof path now mirrors CheckMerkleRoot — verified via go-bc CalculateRootGivenTxid across coinbase, first-subtree (idx 1 & 2), multi-subtree, and incomplete-final cases (incl. a brute-force sweep of all positions in a 4-subtree block; all reconstruct the header root, no placeholder leaks). The coinbase-replaced subtree clone is a genuine deep copy, so the cached subtree isn't mutated; offset/bit math checks out; build + tests green. Good call building the test subtrees with the real CoinbasePlaceholderHashValue — that's the exact gap the old tests had.

One fast-follow, not blocking this PR: ConstructSubtreeMerkleProof (merkle_proof.go:388) still builds from raw block.Subtrees without the coinbase-replace/lift, so subtree-inclusion proofs — the GetMerkleProof fallback when the queried hash is a subtree root rather than a txid — stay invalid for first-subtree / incomplete-final blocks. Same bug class, different entry point, currently untested. Worth the same fix + a go-bc crosscheck before GET /api/v1/merkle_proof/{subtreeHash} is relied on.

@oskarszoon oskarszoon requested a review from ordishs June 2, 2026 12:52

@ordishs ordishs left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approve — careful, well-reasoned correctness fix backed by go-bc reference cross-checks.

Verified locally:

  • go test ./util/bump/... ./util/merkleproof/... passes; go vet clean.
  • Proof construction faithfully mirrors model.Block.CheckMerkleRoot (coinbase replacement for subtree 0, lift only the incomplete final subtree, top tree over effective roots).
  • Traced the global-offset model through a non-power-of-2 subtree count with an incomplete final subtree — offsets, top-level odd-duplication, and lift levels all reconstruct the same root, confirmed end-to-end via the go-bc parser in the new cross-check test.
  • Duplicate() deep-copies Nodes, so the clone-and-replace path does not mutate the cached subtree.

The bugs fixed are real: multi-subtree BUMP proofs were previously unverifiable. Test coverage is strong (odd-index/flag bug, coinbase tx, multi-subtree, incomplete final subtree, placeholder-exclusion assertion).

Non-blocking follow-ups:

  • BUMP emits self-duplicate/lift siblings as FlagData with explicit hash rather than FlagDuplicate (0x01). Reconstructs correctly in go-bc but is non-canonical vs BRC-74 and larger; a brief comment documenting the deliberate go-bc-compatibility tradeoff would help future readers.
  • ConstructMerkleProof assumes (rather than re-checks) the power-of-two/single-incomplete-final invariants that CheckMerkleRoot enforces; a defense-in-depth guard would be cheap insurance.
  • Minor extra I/O per proof (first/last subtree fetches) — acceptable.

@galt-tr galt-tr merged commit b7ca48f into bsv-blockchain:main Jun 2, 2026
27 checks passed
freemans13 added a commit to freemans13/teranode that referenced this pull request Jun 2, 2026
…tegration

Merge stu/blockvalidation-inmemory-checkold (ca7ef31) — off-chain prefetch
for checkOldBlockIDs plus the upstream/main it carried (bsv-blockchain#1005, bsv-blockchain#993, bsv-blockchain#1022).

Conflict resolutions:
- services/legacy/netsync/manager.go: kept integration's gap-watchdog
  self-heal (requestBlocksFromTip / maybeResyncOnMissingParent /
  missingParentResyncInterval); the incoming side had no content there.
- stores/utxo/aerospike/batch_create.go: pass nil for the new GetBinsToStore
  arena param (main added *bt.Arena; integration-only BatchCreate caller
  predates it). nil = non-arena route, output-equivalent per create_arena_test.
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.

[BUG] Assets API merkle proofs invalid: coinbase placeholder not replaced + wrong subtree flags + wrong multi-subtree BUMP offsets

3 participants