Skip to content

feat: adaptive subtreeData skip during catchup when txs exist locally#539

Closed
freemans13 wants to merge 8 commits into
bsv-blockchain:mainfrom
freemans13:feat/adaptive-subtreedata-skip-during-catchup
Closed

feat: adaptive subtreeData skip during catchup when txs exist locally#539
freemans13 wants to merge 8 commits into
bsv-blockchain:mainfrom
freemans13:feat/adaptive-subtreedata-skip-during-catchup

Conversation

@freemans13

@freemans13 freemans13 commented Feb 27, 2026

Copy link
Copy Markdown
Collaborator

Problem

During catchup, subtree validation downloads subtreeData files (full serialized transactions, hundreds of MB each) from peers for every subtree in every block. In environments like dev-scale where a tx-blaster sends each transaction to both nodes, the catching-up node already has all these transactions locally in its UTXO store. Downloading subtreeData in this scenario is pure waste — bandwidth, time, and memory spent re-fetching data we already have.

Solution

For each subtree, stream the tx hashes and batch-check UTXO existence. If all txs exist locally, skip the subtreeData download entirely. If any are missing, abort the check immediately and switch to subtreeData for that subtree and all remaining ones in the block. Subtrees already validated via subtree-only are not re-downloaded.

A shared atomic flag across concurrent goroutines ensures that once a miss is found, every subsequent subtree goes straight to full fetch with zero probing overhead. This is applied across all three catchup code paths: block validation pre-fetch, CheckBlockSubtrees, and the streaming processor pipeline.

Changes

File Change
pkg/utxocheck/existence.go New shared helper CheckAllTxsExistInUTXO() — batch-checks tx hashes via BatchDecorate, skips coinbase, short-circuits on first miss
pkg/utxocheck/existence_test.go 9 unit tests covering all branches
services/blockvalidation/get_blocks.go New fetchBlockSubtreesAdaptive() with per-subtree UTXO probe; wired into blockWorker()
services/blockvalidation/get_blocks_test.go Updated error message assertion for renamed function
services/subtreevalidation/check_block_subtrees.go Added needsSubtreeData atomic flag + UTXO check before subtreeData download
services/subtreevalidation/streaming_processor.go New loadSubtreeOnly() helper + UTXO check in streamAndFilterSubtrees()

Error handling

  • UTXO store errors → log warning, fall back to subtreeData (optimization is advisory)
  • Partial misses → once any subtree has missing txs, all remaining subtrees skip the UTXO check
  • Race/eviction safetyValidateSubtreeInternal has its own fallback via processMissingTransactions() if txs are somehow missing at validation time despite passing the existence check

Test plan

  • go build passes for all modified packages
  • go vet clean
  • make lint — 0 issues
  • pkg/utxocheck — 9/9 unit tests pass
  • services/subtreevalidation — all tests pass
  • services/blockvalidation — all catchup tests pass (TestBlockWorker, TestFetchBlocksConcurrently, TestFetchSubtreeDataForBlock, TestCatchup_*)
  • Manual test on dev-scale: during catchup, logs should show "skipping subtreeData download" for subtrees where txs exist locally

🤖 Generated with Claude Code

During catchup, subtree validation downloads massive subtreeData files from
peers. When both nodes have received the same transactions (e.g. dev-scale
with tx-blaster), the catching-up node already has all txs in its UTXO store.
This optimization checks UTXO existence per-subtree before downloading
subtreeData, skipping the download entirely when all txs exist locally.

Uses a shared atomic flag so that once any subtree has missing txs, all
remaining subtrees skip the probe and go straight to full subtreeData fetch,
minimizing wasted bandwidth.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Feb 27, 2026

Copy link
Copy Markdown
Contributor

🤖 Claude Code Review

Status: Complete

Current Review:
No issues found. The code is well-structured with comprehensive test coverage.

Notable strengths:

  • Excellent error handling with graceful fallback to subtreeData on any UTXO check failures
  • The atomic flag race condition previously identified has been properly addressed with a re-check at line 488
  • Comprehensive unit tests (9 test cases) covering all edge cases in the new utxocheck package
  • Proper handling of coinbase placeholder transactions
  • Good use of batch processing to minimize UTXO store queries

Minor notes:

  • The diagnose package changes correctly suppress prealloc linter warnings for variable-sized result slices
  • The optimization is advisory by design, with all error paths falling back to the existing subtreeData fetch mechanism


g.Go(func() error {
// If flag is already set, go straight to full fetch
if needsSubtreeData.Load() {

@github-actions github-actions Bot Feb 27, 2026

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.

The atomic flag pattern here has a race condition. When multiple goroutines check needsSubtreeData.Load() at line 436 before any have called .Store(true), they all proceed to the UTXO check. If one goroutine finds a missing tx and sets the flag, other goroutines that already passed the check will still download subtreeData even though the flag is now set.

Impact: The optimization won't be as effective as intended - some goroutines will unnecessarily download subtreeData even after the flag is set.

Solution: This is an inherent limitation of the optimization pattern and may be acceptable since it still provides bandwidth savings. However, if you want stricter behavior, consider checking the flag again before calling fetchAndStoreSubtreeData at lines 458 and 464.


Update:Fixed - The code now includes a re-check of the flag at line 449 after fetching the subtree, which closes the race window. This ensures goroutines that are in-flight will check the flag again before proceeding to download subtreeData.

@freemans13 freemans13 self-assigned this Feb 27, 2026
freemans13 and others added 3 commits February 27, 2026 10:23
Add a second flag check after the subtree-hash fetch completes but before
starting the expensive UTXO BatchDecorate call. This closes the window where
a goroutine passes the initial check, another goroutine sets the flag while
the first is fetching hashes, and the first goroutine then does an
unnecessary UTXO existence check.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The UTXO check in check_block_subtrees.go and streaming_processor.go
caused validation to be skipped entirely when all txs existed in the
UTXO store. This broke tests using the SQL backend (which doesn't
store the Creating field) and would also skip block-level validation
in production. The optimization is kept only in fetchBlockSubtreesAdaptive
(pre-fetch phase), which correctly skips subtreeData downloads while
letting the validation pipeline run fully.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@sonarqubecloud

sonarqubecloud Bot commented Mar 2, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
66.3% Coverage on New Code (required ≥ 80%)
8.3% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@github-actions

github-actions Bot commented Apr 9, 2026

Copy link
Copy Markdown
Contributor

Benchmark Comparison Report

Baseline: main (unknown)

Current: PR-539 (8cbf81a)

Summary

  • Regressions: 0
  • Improvements: 0
  • Unchanged: 142
  • Significance level: p < 0.05
All benchmark results (sec/op)
Benchmark Baseline Current Change p-value
_NewBlockFromBytes-4 1.679µ 1.743µ ~ 0.100
SplitSyncedParentMap_SetIfNotExists/256_buckets-4 59.35n 59.36n ~ 0.500
SplitSyncedParentMap_SetIfNotExists/16_buckets-4 59.39n 59.33n ~ 0.400
SplitSyncedParentMap_SetIfNotExists/1_bucket-4 60.01n 62.50n ~ 0.400
SplitSyncedParentMap_ConcurrentSetIfNotExists/256_buckets... 34.33n 33.89n ~ 0.400
SplitSyncedParentMap_ConcurrentSetIfNotExists/16_buckets_... 57.57n 57.89n ~ 0.700
SplitSyncedParentMap_ConcurrentSetIfNotExists/1_bucket_pa... 147.8n 144.8n ~ 0.700
MiningCandidate_Stringify_Short-4 248.5n 251.1n ~ 0.100
MiningCandidate_Stringify_Long-4 1.731µ 1.742µ ~ 0.100
MiningSolution_Stringify-4 886.9n 889.9n ~ 0.400
BlockInfo_MarshalJSON-4 1.716µ 1.713µ ~ 0.400
NewFromBytes-4 132.2n 133.1n ~ 1.000
Mine_EasyDifficulty-4 64.69µ 64.79µ ~ 0.700
Mine_WithAddress-4 5.114µ 4.855µ ~ 0.100
DirectSubtreeAdd/4_per_subtree-4 63.38n 59.71n ~ 0.200
DirectSubtreeAdd/64_per_subtree-4 30.90n 31.87n ~ 0.100
DirectSubtreeAdd/256_per_subtree-4 29.64n 30.24n ~ 0.100
DirectSubtreeAdd/1024_per_subtree-4 28.56n 29.08n ~ 0.100
DirectSubtreeAdd/2048_per_subtree-4 28.11n 28.72n ~ 0.100
SubtreeProcessorAdd/4_per_subtree-4 309.2n 310.1n ~ 1.000
SubtreeProcessorAdd/64_per_subtree-4 307.3n 313.6n ~ 1.000
SubtreeProcessorAdd/256_per_subtree-4 305.9n 313.7n ~ 0.100
SubtreeProcessorAdd/1024_per_subtree-4 312.6n 309.0n ~ 0.500
SubtreeProcessorAdd/2048_per_subtree-4 308.0n 311.4n ~ 0.700
SubtreeProcessorRotate/4_per_subtree-4 317.7n 320.6n ~ 0.700
SubtreeProcessorRotate/64_per_subtree-4 313.4n 313.6n ~ 0.800
SubtreeProcessorRotate/256_per_subtree-4 314.3n 312.0n ~ 0.700
SubtreeProcessorRotate/1024_per_subtree-4 305.0n 301.9n ~ 0.400
SubtreeNodeAddOnly/4_per_subtree-4 67.58n 66.26n ~ 0.100
SubtreeNodeAddOnly/64_per_subtree-4 39.28n 39.56n ~ 1.000
SubtreeNodeAddOnly/256_per_subtree-4 37.92n 37.54n ~ 0.100
SubtreeNodeAddOnly/1024_per_subtree-4 37.51n 36.91n ~ 0.100
SubtreeCreationOnly/4_per_subtree-4 163.2n 164.9n ~ 0.200
SubtreeCreationOnly/64_per_subtree-4 638.9n 671.4n ~ 0.700
SubtreeCreationOnly/256_per_subtree-4 2.030µ 2.046µ ~ 0.700
SubtreeCreationOnly/1024_per_subtree-4 5.611µ 5.589µ ~ 1.000
SubtreeCreationOnly/2048_per_subtree-4 8.186µ 7.506µ ~ 0.700
SubtreeProcessorOverheadBreakdown/64_per_subtree-4 306.4n 305.8n ~ 1.000
SubtreeProcessorOverheadBreakdown/1024_per_subtree-4 309.7n 309.2n ~ 1.000
ParallelGetAndSetIfNotExists/1k_nodes-4 964.5µ 992.5µ ~ 0.100
ParallelGetAndSetIfNotExists/10k_nodes-4 1.930m 1.946m ~ 0.100
ParallelGetAndSetIfNotExists/50k_nodes-4 8.693m 8.737m ~ 1.000
ParallelGetAndSetIfNotExists/100k_nodes-4 17.65m 17.53m ~ 1.000
SequentialGetAndSetIfNotExists/1k_nodes-4 763.9µ 760.9µ ~ 0.200
SequentialGetAndSetIfNotExists/10k_nodes-4 2.974m 2.951m ~ 0.700
SequentialGetAndSetIfNotExists/50k_nodes-4 10.77m 10.64m ~ 0.100
SequentialGetAndSetIfNotExists/100k_nodes-4 20.58m 20.39m ~ 0.400
ProcessOwnBlockSubtreeNodesParallel/1k_nodes-4 1.025m 1.059m ~ 0.400
ProcessOwnBlockSubtreeNodesParallel/10k_nodes-4 4.745m 4.716m ~ 0.400
ProcessOwnBlockSubtreeNodesParallel/100k_nodes-4 19.19m 19.07m ~ 0.100
ProcessOwnBlockSubtreeNodesSequential/1k_nodes-4 836.0µ 831.7µ ~ 0.100
ProcessOwnBlockSubtreeNodesSequential/10k_nodes-4 6.137m 5.999m ~ 0.200
ProcessOwnBlockSubtreeNodesSequential/100k_nodes-4 39.92m 39.26m ~ 0.100
DiskTxMap_SetIfNotExists-4 3.715µ 3.451µ ~ 0.100
DiskTxMap_SetIfNotExists_Parallel-4 3.334µ 3.401µ ~ 0.400
DiskTxMap_ExistenceOnly-4 296.9n 309.6n ~ 0.100
Queue-4 192.7n 193.6n ~ 0.400
AtomicPointer-4 4.668n 5.029n ~ 0.100
ReorgOptimizations/DedupFilterPipeline/Old/10K-4 856.1µ 898.9µ ~ 0.400
ReorgOptimizations/DedupFilterPipeline/New/10K-4 862.7µ 877.9µ ~ 0.200
ReorgOptimizations/AllMarkFalse/Old/10K-4 106.5µ 107.5µ ~ 0.700
ReorgOptimizations/AllMarkFalse/New/10K-4 62.46µ 62.41µ ~ 0.700
ReorgOptimizations/HashSlicePool/Old/10K-4 71.83µ 72.47µ ~ 1.000
ReorgOptimizations/HashSlicePool/New/10K-4 11.94µ 12.19µ ~ 0.100
ReorgOptimizations/NodeFlags/Old/10K-4 5.229µ 5.424µ ~ 0.400
ReorgOptimizations/NodeFlags/New/10K-4 1.796µ 1.995µ ~ 0.200
ReorgOptimizations/DedupFilterPipeline/Old/100K-4 9.511m 9.557m ~ 1.000
ReorgOptimizations/DedupFilterPipeline/New/100K-4 9.464m 9.722m ~ 0.200
ReorgOptimizations/AllMarkFalse/Old/100K-4 1.155m 1.082m ~ 0.100
ReorgOptimizations/AllMarkFalse/New/100K-4 687.9µ 686.2µ ~ 0.400
ReorgOptimizations/HashSlicePool/Old/100K-4 579.9µ 629.1µ ~ 0.100
ReorgOptimizations/HashSlicePool/New/100K-4 315.7µ 292.7µ ~ 1.000
ReorgOptimizations/NodeFlags/Old/100K-4 54.76µ 56.05µ ~ 0.100
ReorgOptimizations/NodeFlags/New/100K-4 17.38µ 19.56µ ~ 0.100
TxMapSetIfNotExists-4 51.40n 51.45n ~ 0.400
TxMapSetIfNotExistsDuplicate-4 37.99n 38.07n ~ 1.000
ChannelSendReceive-4 611.6n 572.6n ~ 0.100
BlockAssembler_AddTx-4 0.02764n 0.02957n ~ 0.700
AddNode-4 12.34 12.24 ~ 0.700
AddNodeWithMap-4 12.01 11.94 ~ 1.000
CalcBlockWork-4 491.9n 544.9n ~ 0.700
CalculateWork-4 677.8n 673.4n ~ 0.100
BuildBlockLocatorString_Helpers/Size_10-4 1.555µ 1.450µ ~ 0.400
BuildBlockLocatorString_Helpers/Size_100-4 12.27µ 12.37µ ~ 0.400
BuildBlockLocatorString_Helpers/Size_1000-4 121.1µ 122.5µ ~ 0.100
CatchupWithHeaderCache-4 104.0m 103.9m ~ 0.200
_BufferPoolAllocation/16KB-4 3.381µ 3.317µ ~ 0.700
_BufferPoolAllocation/32KB-4 7.909µ 7.711µ ~ 0.700
_BufferPoolAllocation/64KB-4 17.72µ 16.00µ ~ 0.100
_BufferPoolAllocation/128KB-4 27.67µ 29.73µ ~ 0.100
_BufferPoolAllocation/512KB-4 113.1µ 100.9µ ~ 0.100
_BufferPoolConcurrent/32KB-4 19.16µ 17.63µ ~ 0.100
_BufferPoolConcurrent/64KB-4 30.94µ 27.59µ ~ 0.100
_BufferPoolConcurrent/512KB-4 139.1µ 140.6µ ~ 0.700
_SubtreeDeserializationWithBufferSizes/16KB-4 621.2µ 616.9µ ~ 0.700
_SubtreeDeserializationWithBufferSizes/32KB-4 620.0µ 611.5µ ~ 0.700
_SubtreeDeserializationWithBufferSizes/64KB-4 628.8µ 611.8µ ~ 1.000
_SubtreeDeserializationWithBufferSizes/128KB-4 633.7µ 604.4µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/512KB-4 640.0µ 625.9µ ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/16KB-4 34.99m 34.96m ~ 0.400
_SubtreeDataDeserializationWithBufferSizes/32KB-4 34.59m 34.48m ~ 1.000
_SubtreeDataDeserializationWithBufferSizes/64KB-4 35.01m 34.54m ~ 0.700
_SubtreeDataDeserializationWithBufferSizes/128KB-4 35.17m 34.50m ~ 0.400
_SubtreeDataDeserializationWithBufferSizes/512KB-4 34.77m 34.38m ~ 0.100
_PooledVsNonPooled/Pooled-4 737.8n 736.0n ~ 0.400
_PooledVsNonPooled/NonPooled-4 6.626µ 6.779µ ~ 0.100
_MemoryFootprint/Current_512KB_32concurrent-4 6.705µ 6.540µ ~ 0.100
_MemoryFootprint/Proposed_32KB_32concurrent-4 9.734µ 9.646µ ~ 0.200
_MemoryFootprint/Alternative_64KB_32concurrent-4 9.148µ 9.494µ ~ 0.400
SubtreeSizes/10k_tx_4_per_subtree-4 1.380m 1.406m ~ 0.400
SubtreeSizes/10k_tx_16_per_subtree-4 329.6µ 334.4µ ~ 0.200
SubtreeSizes/10k_tx_64_per_subtree-4 78.74µ 79.04µ ~ 0.400
SubtreeSizes/10k_tx_256_per_subtree-4 19.59µ 19.67µ ~ 0.700
SubtreeSizes/10k_tx_512_per_subtree-4 9.727µ 9.841µ ~ 0.200
SubtreeSizes/10k_tx_1024_per_subtree-4 4.830µ 4.896µ ~ 0.400
SubtreeSizes/10k_tx_2k_per_subtree-4 2.432µ 2.417µ ~ 0.700
BlockSizeScaling/10k_tx_64_per_subtree-4 76.88µ 77.27µ ~ 0.200
BlockSizeScaling/10k_tx_256_per_subtree-4 19.54µ 19.52µ ~ 0.700
BlockSizeScaling/10k_tx_1024_per_subtree-4 4.868µ 4.910µ ~ 0.400
BlockSizeScaling/50k_tx_64_per_subtree-4 406.5µ 407.1µ ~ 0.700
BlockSizeScaling/50k_tx_256_per_subtree-4 96.85µ 97.59µ ~ 0.700
BlockSizeScaling/50k_tx_1024_per_subtree-4 24.02µ 24.13µ ~ 0.400
SubtreeAllocations/small_subtrees_exists_check-4 159.9µ 163.4µ ~ 0.100
SubtreeAllocations/small_subtrees_data_fetch-4 172.5µ 170.5µ ~ 1.000
SubtreeAllocations/small_subtrees_full_validation-4 338.0µ 341.7µ ~ 1.000
SubtreeAllocations/medium_subtrees_exists_check-4 9.424µ 9.532µ ~ 0.100
SubtreeAllocations/medium_subtrees_data_fetch-4 9.993µ 10.029µ ~ 0.700
SubtreeAllocations/medium_subtrees_full_validation-4 19.81µ 19.67µ ~ 0.400
SubtreeAllocations/large_subtrees_exists_check-4 2.274µ 2.259µ ~ 0.100
SubtreeAllocations/large_subtrees_data_fetch-4 2.428µ 2.446µ ~ 0.400
SubtreeAllocations/large_subtrees_full_validation-4 4.903µ 4.956µ ~ 0.200
_prepareTxsPerLevel-4 384.1m 384.2m ~ 1.000
_prepareTxsPerLevelOrdered-4 4.020m 4.011m ~ 1.000
_prepareTxsPerLevel_Comparison/Original-4 385.3m 394.2m ~ 0.700
_prepareTxsPerLevel_Comparison/Optimized-4 3.931m 4.075m ~ 0.400
StoreBlock_Sequential/BelowCSVHeight-4 352.7µ 355.2µ ~ 0.700
StoreBlock_Sequential/AboveCSVHeight-4 352.8µ 356.9µ ~ 0.400
GetUtxoHashes-4 272.0n 270.5n ~ 1.000
GetUtxoHashes_ManyOutputs-4 45.73µ 47.68µ ~ 0.100
_NewMetaDataFromBytes-4 229.2n 230.1n ~ 0.400
_Bytes-4 601.4n 603.0n ~ 0.100
_MetaBytes-4 548.5n 555.3n ~ 0.200

Threshold: >10% with p < 0.05 | Generated: 2026-04-23 13:37 UTC

@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
67.0% Coverage on New Code (required ≥ 80%)
8.9% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@freemans13

Copy link
Copy Markdown
Collaborator Author

Superseded by #745 — self-discovering adaptive gate replaces the always-on per-subtree UTXO probe. The probe's probing-every-subtree-forever cost is eliminated in steady state, while the recovery path is preserved via subtreevalidation's existing processMissingTransactions fallback.

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.

2 participants