Skip to content

fix(blockchain): don't cache a not-found GetBlockExists result - #2054

Merged
icellan merged 1 commit into
bsv-blockchain:mainfrom
icellan:fix/blockchain-getblockexists-no-negative-cache
Oct 27, 2026
Merged

icellan merged 1 commit into
bsv-blockchain:mainfrom
icellan:fix/blockchain-getblockexists-no-negative-cache

Conversation

@icellan

@icellan icellan commented Oct 26, 2026

Copy link
Copy Markdown
Contributor

Problem

SQL.GetBlockExists cached a not-found result in the response cache. StoreBlock resets that cache only after its commit, so a "not found" cached before the commit can still be served, in the window between commit and reset, for a block that is already stored. The generation counter stops a Set(false) that races the reset, but not a read of a false cached earlier.

Block validation decides from GetBlockExists(parent) whether a child goes to catchup (queueFetchedBlock), and a needless catchup is expensive: on the scaling env on 2026-10-26 one meant moving to CATCHINGBLOCKS and re-downloading a 274 GB block. That incident was a different race (#1997 covers it, #2053 limits the damage). This closes the store-side window for the same decision.

Change

Only a found result is cached. A not-found lookup is one indexed query on blocks.hash.

BlockValidation.blockExistsCache already caches only positives, so both layers now agree.

Cost

Every caller looks up a block per block or per announcement (block validation parent checks, catchup entry checks, legacy netsync). The asset handler only reaches it from the admin invalidate/revalidate routes. Lookups of distinct unknown hashes already missed the cache. Only repeated lookups of the same absent hash now go to the database.

Tests

  • TestSQLGetBlockExists_NeverCachesNotFound: a not-found result leaves no cache entry, and a found result is still cached. It fails with the old Set(false).
  • go test -race ./stores/blockchain/... passes, and make lint reports 0 issues.

Follow-ups (not in this PR)

  • GetBlockHeaders caches an empty result for an unknown start hash, in chainWalkCache (10 min TTL), which a plain extend does not clear. GetBlockHeaderIDs already guards against this.
  • LocateBlockHeaders discards the GetBlockExists bool and always takes locator[0].

StoreBlock clears the SQL response cache only after its commit, so a
"not found" cached before the commit can be served, for the moment between
commit and reset, for a block that is already stored. Block validation sends
a child whose parent reads as missing to catchup, so even that short window
is not worth the cache. Only a found result is cached now; a not-found lookup
is one indexed query.
@github-actions

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

🤖 Claude Code Review

Status: Complete

Current Review:
No issues found.

The change is small and correct. StoreBlock calls ResetResponseCache() only after the transaction commits, so a false cached earlier could be served between the commit and the reset. Not caching the not-found result closes that window, and it also stops a deployment with more than one store instance (whose response caches are not invalidated by each other) from serving a stale negative for up to the TTL. The true path is unchanged. Neither Client.GetBlockExists nor LocalClient adds its own cache, so this fix covers every caller. The updated godoc and inline comments match the implementation.

Non-blocking tradeoff: every lookup of an unknown hash (e.g. peer announcements of new blocks) now queries the DB instead of hitting the cache. The query is a single indexed lookup on blocks.hash, so the cost should be small. The new test exercises both the not-cached-false and the cached-true paths, uses require and the sqlitememory store, and follows project conventions.

@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown
Contributor

Benchmark Comparison Report

Baseline: main (unknown)

Current: PR-2054 (21d8ce4)

Summary

  • Regressions: 0
  • Improvements: 0
  • Unchanged: 185
  • Significance level: p < 0.05
All benchmark results (sec/op)
Benchmark Baseline Current Change p-value
_NewBlockFromBytes-4 946.9n 964.9n ~ 0.700
Block_ValidOrderAndBlessed_DiskVsMemory/leaves=1024/memory-4 11.14m 11.09m ~ 0.700
Block_ValidOrderAndBlessed_DiskVsMemory/leaves=1024/disk_1-4 12.29m 11.77m ~ 0.200
Block_ValidOrderAndBlessed_DiskVsMemory/leaves=1024/disk_2-4 13.25m 12.81m ~ 0.100
Block_ValidOrderAndBlessed_DiskVsMemory/leaves=16384/memo... 20.47m 20.61m ~ 1.000
Block_ValidOrderAndBlessed_DiskVsMemory/leaves=16384/disk... 26.45m 26.37m ~ 0.700
Block_ValidOrderAndBlessed_DiskVsMemory/leaves=16384/disk... 26.92m 28.85m ~ 0.100
Block_ValidOrderAndBlessed_DiskVsMemory/leaves=131072/mem... 76.92m 77.89m ~ 0.700
Block_ValidOrderAndBlessed_DiskVsMemory/leaves=131072/dis... 188.9m 191.5m ~ 0.700
Block_ValidOrderAndBlessed_DiskVsMemory/leaves=131072/dis... 190.5m 190.4m ~ 0.700
BlockTxMap/txs=16384/nodeindex-4 1.553m 1.551m ~ 1.000
BlockTxMap/txs=16384/swiss-4 6.991m 6.824m ~ 0.200
BlockTxMap/txs=262144/nodeindex-4 11.87m 11.88m ~ 0.700
BlockTxMap/txs=262144/swiss-4 26.98m 26.40m ~ 0.700
BlockTxMap/txs=4194304/nodeindex-4 288.1m 297.3m ~ 0.200
BlockTxMap/txs=4194304/swiss-4 643.5m 667.1m ~ 0.100
TxMapPut_MemoryVsDisk/txs=100000/memory-4 4.366m 4.380m ~ 0.700
TxMapPut_MemoryVsDisk/txs=100000/disk_1-4 7.942m 7.882m ~ 0.700
TxMapPut_MemoryVsDisk/txs=100000/disk_2-4 7.103m 7.208m ~ 0.200
TxMapGetFrozen_MemoryVsDisk/txs=100000/memory/hit-4 2.658m 2.806m ~ 0.100
TxMapGetFrozen_MemoryVsDisk/txs=100000/memory/miss-4 2.092m 2.275m ~ 0.100
TxMapGetFrozen_MemoryVsDisk/txs=100000/disk_1/hit-4 1.167m 1.218m ~ 0.100
TxMapGetFrozen_MemoryVsDisk/txs=100000/disk_1/miss-4 1.272m 1.307m ~ 0.100
TxMapGetFrozen_MemoryVsDisk/txs=100000/disk_2/hit-4 1.165m 1.209m ~ 0.100
TxMapGetFrozen_MemoryVsDisk/txs=100000/disk_2/miss-4 1.283m 1.342m ~ 0.100
ParentSpendsSetIfNotExists_MemoryVsDisk/inputs=200000/dis... 18.03m 17.90m ~ 0.400
ParentSpendsSetIfNotExists_MemoryVsDisk/inputs=200000/dis... 16.34m 16.38m ~ 1.000
InpointDedup/txs=16K-4 3.455m 3.450m ~ 1.000
InpointDedup/txs=128K-4 12.80m 13.01m ~ 0.200
InpointDedup/txs=1M-4 96.26m 97.17m ~ 0.700
InpointDedup_TinyRuns-4 33.31m 33.69m ~ 0.200
MiningCandidate_Stringify_Short-4 110.1n 107.7n ~ 0.700
MiningCandidate_Stringify_Long-4 821.7n 830.2n ~ 0.100
MiningSolution_Stringify-4 436.3n 427.2n ~ 1.000
BlockInfo_MarshalJSON-4 1.155µ 1.156µ ~ 1.000
DecodeSubtreeMeta/go-subtree-4 108.5m 108.6m ~ 1.000
DecodeSubtreeMeta/packed-4 30.41m 31.01m ~ 0.100
DecodeSubtreeMeta/go-subtree/parallel-4 58.38m 58.28m ~ 1.000
DecodeSubtreeMeta/packed/parallel-4 16.95m 18.06m ~ 1.000
ValidateSubtreeNodesMatchKey/leaves=1024-4 188.4µ 187.6µ ~ 1.000
ValidateSubtreeNodesMatchKey/leaves=1048576-4 57.21m 56.70m ~ 0.700
NewFromBytes-4 126.6n 151.9n ~ 0.100
AddTxBatchColumnar_Validation-4 1.998µ 2.011µ ~ 0.700
OffsetValidationLoop-4 636.5n 637.9n ~ 0.600
BuildSubtreeMeta/serialized-4 8.843m 9.711m ~ 0.100
BuildSubtreeMeta/decoded-4 18.81m 21.40m ~ 0.100
Mine_EasyDifficulty-4 65.65µ 65.41µ ~ 1.000
Mine_WithAddress-4 7.257µ 7.889µ ~ 0.200
BlockAssembler_AddTx-4 0.02050n 0.02085n ~ 1.000
AddNode-4 10.14 10.68 ~ 0.100
AddNodeWithMap-4 10.50 11.01 ~ 0.100
DirectSubtreeAdd/4_per_subtree-4 58.86n 60.38n ~ 0.700
DirectSubtreeAdd/64_per_subtree-4 30.99n 31.31n ~ 0.700
DirectSubtreeAdd/256_per_subtree-4 29.55n 29.56n ~ 0.800
DirectSubtreeAdd/1024_per_subtree-4 28.43n 28.50n ~ 0.400
DirectSubtreeAdd/2048_per_subtree-4 28.02n 28.01n ~ 1.000
SubtreeProcessorAdd/4_per_subtree-4 243.5n 244.9n ~ 0.700
SubtreeProcessorAdd/64_per_subtree-4 243.1n 243.2n ~ 1.000
SubtreeProcessorAdd/256_per_subtree-4 245.8n 241.3n ~ 1.000
SubtreeProcessorAdd/1024_per_subtree-4 238.2n 235.3n ~ 1.000
SubtreeProcessorAdd/2048_per_subtree-4 237.1n 233.6n ~ 0.100
SubtreeProcessorRotate/4_per_subtree-4 237.1n 235.9n ~ 0.600
SubtreeProcessorRotate/64_per_subtree-4 235.9n 238.2n ~ 0.200
SubtreeProcessorRotate/256_per_subtree-4 236.2n 263.2n ~ 0.100
SubtreeProcessorRotate/1024_per_subtree-4 235.3n 271.4n ~ 0.100
SubtreeNodeAddOnly/4_per_subtree-4 54.26n 59.85n ~ 0.100
SubtreeNodeAddOnly/64_per_subtree-4 33.97n 33.89n ~ 0.600
SubtreeNodeAddOnly/256_per_subtree-4 32.87n 33.07n ~ 0.100
SubtreeNodeAddOnly/1024_per_subtree-4 32.39n 32.59n ~ 0.200
SubtreeCreationOnly/4_per_subtree-4 120.3n 147.1n ~ 0.100
SubtreeCreationOnly/64_per_subtree-4 403.0n 397.9n ~ 1.000
SubtreeCreationOnly/256_per_subtree-4 1.286µ 1.285µ ~ 1.000
SubtreeCreationOnly/1024_per_subtree-4 4.281µ 4.939µ ~ 0.100
SubtreeCreationOnly/2048_per_subtree-4 7.531µ 9.037µ ~ 0.100
SubtreeProcessorOverheadBreakdown/64_per_subtree-4 237.8n 231.7n ~ 0.700
SubtreeProcessorOverheadBreakdown/1024_per_subtree-4 235.7n 238.4n ~ 0.100
ParallelGetAndSetIfNotExists/1k_nodes-4 668.0µ 747.1µ ~ 0.400
ParallelGetAndSetIfNotExists/10k_nodes-4 4.360m 4.490m ~ 0.100
ParallelGetAndSetIfNotExists/50k_nodes-4 6.457m 6.889m ~ 0.200
ParallelGetAndSetIfNotExists/100k_nodes-4 8.742m 9.083m ~ 0.700
SequentialGetAndSetIfNotExists/1k_nodes-4 571.7µ 637.9µ ~ 0.100
SequentialGetAndSetIfNotExists/10k_nodes-4 4.704m 5.585m ~ 0.100
SequentialGetAndSetIfNotExists/50k_nodes-4 17.85m 18.08m ~ 0.400
SequentialGetAndSetIfNotExists/100k_nodes-4 30.55m 30.76m ~ 0.400
ProcessOwnBlockSubtreeNodesParallel/1k_nodes-4 746.1µ 816.6µ ~ 0.400
ProcessOwnBlockSubtreeNodesParallel/10k_nodes-4 7.069m 7.234m ~ 0.400
ProcessOwnBlockSubtreeNodesParallel/100k_nodes-4 12.14m 11.89m ~ 0.400
ProcessOwnBlockSubtreeNodesSequential/1k_nodes-4 825.3µ 679.9µ ~ 1.000
ProcessOwnBlockSubtreeNodesSequential/10k_nodes-4 8.211m 8.314m ~ 0.400
ProcessOwnBlockSubtreeNodesSequential/100k_nodes-4 48.44m 49.70m ~ 0.200
DiskTxMap_SetIfNotExists-4 598.6n 611.3n ~ 0.700
DiskTxMap_SetIfNotExists_Parallel-4 243.1n 290.7n ~ 0.400
DiskTxMap_ExistenceOnly-4 270.2n 274.7n ~ 0.100
Queue-4 205.0n 207.1n ~ 0.400
AtomicPointer-4 8.165n 8.241n ~ 0.200
TxMapSetIfNotExists-4 55.03n 55.82n ~ 1.000
TxMapSetIfNotExistsDuplicate-4 49.49n 49.91n ~ 0.700
ChannelSendReceive-4 680.2n 696.8n ~ 0.100
CalcBlockWork-4 523.3n 521.0n ~ 1.000
CalculateWork-4 686.6n 699.1n ~ 0.200
CheckOldBlockIDs/on-chain-prefetch/1000-4 61.46µ 59.72µ ~ 0.400
CheckOldBlockIDs/in-memory-chain-check/1000-4 1.555m 1.488m ~ 1.000
CheckOldBlockIDs/on-chain-prefetch/10000-4 464.4µ 518.1µ ~ 0.700
CheckOldBlockIDs/in-memory-chain-check/10000-4 2.193m 2.150m ~ 0.100
BuildBlockLocatorString_Helpers/Size_10-4 1.360µ 1.319µ ~ 0.100
BuildBlockLocatorString_Helpers/Size_100-4 12.91µ 12.51µ ~ 0.100
BuildBlockLocatorString_Helpers/Size_1000-4 127.6µ 124.2µ ~ 0.100
CatchupWithHeaderCache-4 107.4m 107.3m ~ 0.100
CatchupSubtreeDataPrefetch/even/budget=off-4 502.7m 491.6m ~ 0.100
CatchupSubtreeDataPrefetch/even/budget=256Mi-4 503.3m 501.9m ~ 0.100
CatchupSubtreeDataPrefetch/even/budget=128Mi-4 491.4m 492.3m ~ 0.400
CatchupSubtreeDataPrefetch/skewed/budget=off-4 505.6m 507.3m ~ 0.400
CatchupSubtreeDataPrefetch/skewed/budget=256Mi-4 506.5m 507.7m ~ 1.000
CatchupSubtreeDataPrefetch/skewed/budget=128Mi-4 528.2m 519.4m ~ 0.100
CatchupSubtreeDataPrefetch/oversized/budget=off-4 1.322 1.302 ~ 1.000
CatchupSubtreeDataPrefetch/oversized/budget=256Mi-4 1.483 1.465 ~ 0.400
CatchupSubtreeDataPrefetch/oversized/budget=128Mi-4 1.482 1.476 ~ 1.000
CatchupSubtreeDataPrefetchLatency/latency=0s-4 225.8m 227.8m ~ 1.000
CatchupSubtreeDataPrefetchLatency/latency=20ms-4 2.913 2.922 ~ 0.100
CatchupSubtreeDataPrefetchLatency/latency=200ms-4 26.72 26.73 ~ 1.000
TxIDTwiceUncached/400B_small-4 2.004µ 2.096µ ~ 0.100
TxIDTwiceUncached/34KB_mainnet_shape-4 66.85µ 69.57µ ~ 0.100
TxIDOnceCached/400B_small-4 1.365µ 1.428µ ~ 0.100
TxIDOnceCached/34KB_mainnet_shape-4 39.88µ 41.22µ ~ 0.200
SubtreeSizes/10k_tx_4_per_subtree-4 1.451m 1.381m ~ 0.200
SubtreeSizes/10k_tx_16_per_subtree-4 338.5µ 321.7µ ~ 0.100
SubtreeSizes/10k_tx_64_per_subtree-4 80.49µ 76.89µ ~ 0.100
SubtreeSizes/10k_tx_256_per_subtree-4 19.94µ 18.97µ ~ 0.100
SubtreeSizes/10k_tx_512_per_subtree-4 9.894µ 9.399µ ~ 0.100
SubtreeSizes/10k_tx_1024_per_subtree-4 4.857µ 4.613µ ~ 0.100
SubtreeSizes/10k_tx_2k_per_subtree-4 2.411µ 2.296µ ~ 0.100
BlockSizeScaling/10k_tx_64_per_subtree-4 77.39µ 73.44µ ~ 0.100
BlockSizeScaling/10k_tx_256_per_subtree-4 19.25µ 18.44µ ~ 0.100
BlockSizeScaling/10k_tx_1024_per_subtree-4 4.818µ 4.623µ ~ 0.100
BlockSizeScaling/50k_tx_64_per_subtree-4 403.9µ 391.1µ ~ 0.100
BlockSizeScaling/50k_tx_256_per_subtree-4 96.30µ 92.43µ ~ 0.100
BlockSizeScaling/50k_tx_1024_per_subtree-4 24.32µ 22.74µ ~ 0.100
SubtreeAllocations/small_subtrees_exists_check-4 163.0µ 162.6µ ~ 0.700
SubtreeAllocations/small_subtrees_data_fetch-4 162.9µ 163.1µ ~ 1.000
SubtreeAllocations/small_subtrees_full_validation-4 333.4µ 325.3µ ~ 0.100
SubtreeAllocations/medium_subtrees_exists_check-4 9.870µ 9.319µ ~ 0.100
SubtreeAllocations/medium_subtrees_data_fetch-4 9.606µ 9.530µ ~ 0.400
SubtreeAllocations/medium_subtrees_full_validation-4 19.52µ 18.56µ ~ 0.100
SubtreeAllocations/large_subtrees_exists_check-4 2.399µ 2.208µ ~ 0.100
SubtreeAllocations/large_subtrees_data_fetch-4 2.316µ 2.332µ ~ 0.700
SubtreeAllocations/large_subtrees_full_validation-4 4.867µ 4.663µ ~ 0.100
_prepareTxsPerLevel-4 250.2m 262.0m ~ 0.400
_prepareTxsPerLevelOrdered-4 2.318m 2.262m ~ 1.000
_prepareTxsPerLevel_Comparison/Original-4 265.4m 256.0m ~ 0.400
_prepareTxsPerLevel_Comparison/Optimized-4 2.295m 2.079m ~ 0.100
_BufferPoolAllocation/16KB-4 3.076µ 3.259µ ~ 0.700
_BufferPoolAllocation/32KB-4 8.234µ 7.051µ ~ 0.100
_BufferPoolAllocation/64KB-4 15.03µ 15.17µ ~ 1.000
_BufferPoolAllocation/128KB-4 16.78µ 15.66µ ~ 0.700
_BufferPoolAllocation/512KB-4 59.05µ 61.82µ ~ 0.400
_BufferPoolConcurrent/32KB-4 15.11µ 15.20µ ~ 1.000
_BufferPoolConcurrent/64KB-4 25.20µ 24.53µ ~ 0.400
_BufferPoolConcurrent/512KB-4 135.6µ 134.1µ ~ 1.000
_SubtreeDeserializationWithBufferSizes/16KB-4 496.4µ 496.3µ ~ 1.000
_SubtreeDeserializationWithBufferSizes/32KB-4 491.6µ 500.2µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/64KB-4 490.7µ 498.0µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/128KB-4 491.3µ 497.9µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/512KB-4 495.1µ 494.2µ ~ 0.700
_SubtreeDataDeserializationWithBufferSizes/16KB-4 26.09m 26.46m ~ 0.700
_SubtreeDataDeserializationWithBufferSizes/32KB-4 26.37m 26.32m ~ 1.000
_SubtreeDataDeserializationWithBufferSizes/64KB-4 26.18m 26.18m ~ 1.000
_SubtreeDataDeserializationWithBufferSizes/128KB-4 26.26m 26.24m ~ 1.000
_SubtreeDataDeserializationWithBufferSizes/512KB-4 26.08m 26.20m ~ 0.100
_PooledVsNonPooled/Pooled-4 636.4n 640.4n ~ 0.100
_PooledVsNonPooled/NonPooled-4 6.320µ 6.701µ ~ 0.100
_MemoryFootprint/Current_512KB_32concurrent-4 5.673µ 5.973µ ~ 0.100
_MemoryFootprint/Proposed_32KB_32concurrent-4 8.539µ 9.000µ ~ 0.100
_MemoryFootprint/Alternative_64KB_32concurrent-4 8.568µ 7.829µ ~ 0.100
StoreBlockForkPath/inMemoryChainCheck=false-4 644.8µ 642.9µ ~ 0.700
StoreBlockForkPath/inMemoryChainCheck=true-4 1.294m 1.275m ~ 0.400
StoreBlock_Sequential/BelowCSVHeight-4 199.6µ 195.9µ ~ 0.100
StoreBlock_Sequential/AboveCSVHeight-4 197.0µ 195.7µ ~ 0.400
CheckSpendAndCreateMultiList_FanIn/parents=80000-4 48.31m 48.32m ~ 1.000
CheckSpendAndCreateMultiList_FanIn/parents=160000-4 126.2m 125.1m ~ 1.000
GetUtxoHashes-4 273.6n 275.4n ~ 0.100
GetUtxoHashes_ManyOutputs-4 43.86µ 43.03µ ~ 0.200
MetaBytes-4 70.46n 69.91n ~ 1.000
_NewMetaDataFromBytes-4 230.9n 230.8n ~ 1.000
_Bytes-4 353.2n 354.7n ~ 0.400
_MetaBytes-4 110.9n 110.4n ~ 0.800

Threshold: >10% with p < 0.05 | Generated: 2026-10-26 14:45 UTC

@freemans13 freemans13 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.

Approved. No actionable findings at 118e316.

Removing negative caching closes the window between a block's commit and the response-cache reset. Positive caching retains its generation checks, and the blockchain clients and block-validation callers preserve the same boolean/error contract.

Validation:

  • go test -race -tags testtxmetacache -count=2 ./stores/blockchain/... passed.
  • Ran TestSQLGetBlockExists_NeverCachesNotFound against the pre-fix implementation through a Go overlay. It failed on the cached-false assertion in both repetitions, confirming that the test detects the removed behavior.

Repeated lookups of the same absent hash now perform an indexed database query. This is the intended tradeoff; I did not benchmark its cost.

@icellan
icellan merged commit d7b2606 into bsv-blockchain:main Oct 27, 2026
50 of 51 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