Skip to content

fix(blockchain): do not cache a negative GetBlockIsMined answer - #1676

Open
ordishs wants to merge 1 commit into
bsv-blockchain:mainfrom
ordishs:fix/getblockismined-negative-cache
Open

fix(blockchain): do not cache a negative GetBlockIsMined answer#1676
ordishs wants to merge 1 commit into
bsv-blockchain:mainfrom
ordishs:fix/getblockismined-negative-cache

Conversation

@ordishs

@ordishs ordishs commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Summary

stores/blockchain/sql caches GetBlockIsMined results in the response cache for the 2-minute TTL — including false. A poll that reads mined_set just before the update lands caches the negative, and every caller then sees a stale false until the TTL expires or another write resets the cache.

Observed live on a mainnet IBD: the svp2p bridge waits ~80 s for the parent's mined_set before failing the child block; a cached negative outlives that budget, so block 56 failed repeatedly and sync stalled for 4 hours. services/svp2p/bridge/handle_block.go polls GetBlockIsMined in exactly this shape, and the same wait exists for any caller that polls.

Change

Cache only a true answer. A positive is permanent; a negative is transient by nature and re-reading it is one indexed row lookup.

Test

TestGetBlockIsMined_NegativeAnswerIsNotCached reproduces the race shape (read false → update mined_set directly → read again) and fails on the old code with the stale cached false.

go test ./stores/blockchain/sql/ — ok. golangci-lint — 0 issues.

A poll that reads mined_set before the update lands cached false for the
response-cache TTL (2 min). The svp2p bridge waits at most ~80 s for the
parent's mined_set, so a stale negative stranded the tip+1 block on a live
mainnet sync. Only a positive answer is stable; cache that alone.

(cherry picked from commit ff21e7d)
// Cache the mining status result
cacheOp.Set(isMined, s.cacheTTL)
if isMined {
cacheOp.Set(isMined, s.cacheTTL)

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.

[Minor] The fix is correct and eliminates the observed stale-negative race. One factual nuance: the PR description says "A positive is permanent," but mined_set can transition truefalse via ClearBlockMinedSet, InvalidateBlock, and RevalidateBlock (fork/reorg handling). Caching true here is still safe because all three of those paths call ResetResponseCache() — worth relying on that fact rather than permanence.

Note the symmetric read-then-cache TOCTOU race still exists for the true case (a concurrent reorg reset landing between the DB read on line 75 and this Set, leaving a stale true for the TTL). That is far rarer and, for the svp2p IBD caller motivating this fix, a stale true is the benign direction — so this is not blocking, just noting the residual.

@github-actions

Copy link
Copy Markdown
Contributor

🤖 Claude Code Review

Status: Complete

Current Review:
The fix is correct and well-targeted. Caching only a true result eliminates the stale-negative race described in the PR — a GetBlockIsMined read that observes false and then loses the read-then-cache race against a concurrent SetBlockMinedSet no longer poisons the response cache for the full TTL. I verified that every path which transitions mined_set back to false (ClearBlockMinedSet, InvalidateBlock, RevalidateBlock) and the true-setting path (SetBlockMinedSet) all call ResetResponseCache(), so caching true remains safe across reorg/invalidate handling.

The added test reproduces the exact race shape (read false → mutate mined_set directly in the DB → re-read) and would fail against the pre-change code. Imports are already present; no compile concerns.

One [Minor] factual note left inline: the PR description’s "a positive is permanent" is not strictly true (positives can be reverted on reorg), and a symmetric — but far rarer and, for the motivating IBD caller, benign — stale-true TOCTOU race still exists. Not blocking.

No blocking issues found.

@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown
Contributor

Benchmark Comparison Report

Baseline: main (unknown)

Current: PR-1676 (58aa81f)

Summary

  • Regressions: 0
  • Improvements: 0
  • Unchanged: 139
  • Significance level: p < 0.05
All benchmark results (sec/op)
Benchmark Baseline Current Change p-value
_NewBlockFromBytes-4 1.615µ 1.784µ ~ 0.700
Block_ValidOrderAndBlessed_DiskVsMemory/leaves=1024/memory-4 12.38m 12.56m ~ 0.700
Block_ValidOrderAndBlessed_DiskVsMemory/leaves=1024/disk_1-4 12.84m 12.86m ~ 0.700
Block_ValidOrderAndBlessed_DiskVsMemory/leaves=1024/disk_2-4 12.83m 12.88m ~ 0.100
Block_ValidOrderAndBlessed_DiskVsMemory/leaves=16384/memo... 31.08m 34.80m ~ 0.100
Block_ValidOrderAndBlessed_DiskVsMemory/leaves=16384/disk... 40.15m 45.26m ~ 0.100
Block_ValidOrderAndBlessed_DiskVsMemory/leaves=16384/disk... 40.04m 42.89m ~ 0.100
SplitSyncedParentMap_SetIfNotExists/256_buckets-4 70.80n 70.85n ~ 0.400
SplitSyncedParentMap_SetIfNotExists/16_buckets-4 70.73n 70.99n ~ 0.100
SplitSyncedParentMap_SetIfNotExists/1_bucket-4 70.86n 70.91n ~ 0.700
SplitSyncedParentMap_ConcurrentSetIfNotExists/256_buckets... 32.06n 34.92n ~ 0.100
SplitSyncedParentMap_ConcurrentSetIfNotExists/16_buckets_... 53.62n 60.00n ~ 0.100
SplitSyncedParentMap_ConcurrentSetIfNotExists/1_bucket_pa... 126.3n 141.3n ~ 0.100
MiningCandidate_Stringify_Short-4 189.9n 191.7n ~ 0.300
MiningCandidate_Stringify_Long-4 1.315µ 1.307µ ~ 0.500
MiningSolution_Stringify-4 688.8n 674.7n ~ 0.100
BlockInfo_MarshalJSON-4 1.510µ 1.504µ ~ 0.700
NewFromBytes-4 153.1n 139.7n ~ 1.000
AddTxBatchColumnar_Validation-4 1.896µ 1.917µ ~ 0.700
OffsetValidationLoop-4 719.2n 719.0n ~ 1.000
Mine_EasyDifficulty-4 60.49µ 61.33µ ~ 1.000
Mine_WithAddress-4 7.045µ 6.929µ ~ 0.100
DirectSubtreeAdd/4_per_subtree-4 58.17n 64.11n ~ 0.100
DirectSubtreeAdd/64_per_subtree-4 29.89n 30.07n ~ 0.700
DirectSubtreeAdd/256_per_subtree-4 29.08n 29.00n ~ 0.700
DirectSubtreeAdd/1024_per_subtree-4 27.85n 27.89n ~ 0.300
DirectSubtreeAdd/2048_per_subtree-4 27.52n 27.54n ~ 1.000
SubtreeProcessorAdd/4_per_subtree-4 240.7n 235.5n ~ 0.200
SubtreeProcessorAdd/64_per_subtree-4 234.8n 233.5n ~ 0.200
SubtreeProcessorAdd/256_per_subtree-4 236.6n 234.4n ~ 0.500
SubtreeProcessorAdd/1024_per_subtree-4 229.6n 228.0n ~ 0.400
SubtreeProcessorAdd/2048_per_subtree-4 228.9n 227.3n ~ 0.100
SubtreeProcessorRotate/4_per_subtree-4 232.1n 232.2n ~ 1.000
SubtreeProcessorRotate/64_per_subtree-4 231.2n 229.3n ~ 0.200
SubtreeProcessorRotate/256_per_subtree-4 231.5n 228.8n ~ 0.100
SubtreeProcessorRotate/1024_per_subtree-4 229.1n 232.8n ~ 0.200
SubtreeNodeAddOnly/4_per_subtree-4 54.57n 56.12n ~ 0.100
SubtreeNodeAddOnly/64_per_subtree-4 33.99n 34.54n ~ 0.100
SubtreeNodeAddOnly/256_per_subtree-4 33.23n 33.84n ~ 0.100
SubtreeNodeAddOnly/1024_per_subtree-4 32.48n 32.62n ~ 0.100
SubtreeCreationOnly/4_per_subtree-4 114.3n 136.2n ~ 0.100
SubtreeCreationOnly/64_per_subtree-4 396.8n 388.5n ~ 0.700
SubtreeCreationOnly/256_per_subtree-4 1.267µ 1.288µ ~ 0.100
SubtreeCreationOnly/1024_per_subtree-4 4.248µ 4.999µ ~ 0.100
SubtreeCreationOnly/2048_per_subtree-4 7.420µ 8.552µ ~ 0.100
SubtreeProcessorOverheadBreakdown/64_per_subtree-4 229.0n 231.1n ~ 0.100
SubtreeProcessorOverheadBreakdown/1024_per_subtree-4 229.0n 232.5n ~ 0.100
ParallelGetAndSetIfNotExists/1k_nodes-4 10.02m 12.30m ~ 0.100
ParallelGetAndSetIfNotExists/10k_nodes-4 13.07m 16.26m ~ 0.100
ParallelGetAndSetIfNotExists/50k_nodes-4 15.58m 19.82m ~ 0.100
ParallelGetAndSetIfNotExists/100k_nodes-4 18.86m 17.85m ~ 0.400
SequentialGetAndSetIfNotExists/1k_nodes-4 9.103m 13.625m ~ 0.100
SequentialGetAndSetIfNotExists/10k_nodes-4 13.54m 17.26m ~ 0.100
SequentialGetAndSetIfNotExists/50k_nodes-4 19.78m 23.30m ~ 0.100
SequentialGetAndSetIfNotExists/100k_nodes-4 25.75m 35.57m ~ 0.100
ProcessOwnBlockSubtreeNodesParallel/1k_nodes-4 12.10m 12.40m ~ 0.700
ProcessOwnBlockSubtreeNodesParallel/10k_nodes-4 13.88m 14.40m ~ 0.700
ProcessOwnBlockSubtreeNodesParallel/100k_nodes-4 18.41m 19.92m ~ 0.100
ProcessOwnBlockSubtreeNodesSequential/1k_nodes-4 13.38m 15.03m ~ 1.000
ProcessOwnBlockSubtreeNodesSequential/10k_nodes-4 14.62m 15.68m ~ 0.200
ProcessOwnBlockSubtreeNodesSequential/100k_nodes-4 49.46m 53.58m ~ 0.100
DiskTxMap_SetIfNotExists-4 3.658µ 3.727µ ~ 1.000
DiskTxMap_SetIfNotExists_Parallel-4 3.436µ 3.454µ ~ 1.000
DiskTxMap_ExistenceOnly-4 390.5n 326.9n ~ 0.100
Queue-4 192.8n 190.2n ~ 0.100
AtomicPointer-4 4.147n 4.186n ~ 1.000
TxMapSetIfNotExists-4 62.83n 63.24n ~ 0.400
TxMapSetIfNotExistsDuplicate-4 46.97n 47.38n ~ 0.400
ChannelSendReceive-4 588.4n 583.8n ~ 1.000
BlockAssembler_AddTx-4 0.02663n 0.02835n ~ 0.400
AddNode-4 10.74 10.96 ~ 0.200
AddNodeWithMap-4 11.19 10.96 ~ 1.000
CalcBlockWork-4 524.6n 528.7n ~ 0.400
CalculateWork-4 711.0n 715.6n ~ 0.700
CheckOldBlockIDs/on-chain-prefetch/1000-4 62.82µ 62.40µ ~ 1.000
CheckOldBlockIDs/in-memory-chain-check/1000-4 1.503m 1.519m ~ 0.700
CheckOldBlockIDs/on-chain-prefetch/10000-4 469.9µ 461.8µ ~ 0.100
CheckOldBlockIDs/in-memory-chain-check/10000-4 2.117m 2.136m ~ 0.700
BuildBlockLocatorString_Helpers/Size_10-4 1.361µ 1.351µ ~ 0.100
BuildBlockLocatorString_Helpers/Size_100-4 12.97µ 13.03µ ~ 1.000
BuildBlockLocatorString_Helpers/Size_1000-4 128.7µ 128.8µ ~ 0.500
CatchupWithHeaderCache-4 106.8m 106.6m ~ 0.200
_BufferPoolAllocation/16KB-4 4.311µ 3.822µ ~ 1.000
_BufferPoolAllocation/32KB-4 9.019µ 7.669µ ~ 0.700
_BufferPoolAllocation/64KB-4 18.52µ 15.82µ ~ 0.100
_BufferPoolAllocation/128KB-4 37.17µ 27.60µ ~ 0.100
_BufferPoolAllocation/512KB-4 118.3µ 118.3µ ~ 1.000
_BufferPoolConcurrent/32KB-4 22.55µ 18.84µ ~ 0.100
_BufferPoolConcurrent/64KB-4 31.62µ 31.17µ ~ 1.000
_BufferPoolConcurrent/512KB-4 152.8µ 159.4µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/16KB-4 742.3µ 675.2µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/32KB-4 728.1µ 597.9µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/64KB-4 612.9µ 598.3µ ~ 0.200
_SubtreeDeserializationWithBufferSizes/128KB-4 609.7µ 595.0µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/512KB-4 646.7µ 602.7µ ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/16KB-4 37.13m 36.37m ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/32KB-4 37.09m 36.55m ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/64KB-4 36.86m 36.44m ~ 0.700
_SubtreeDataDeserializationWithBufferSizes/128KB-4 37.05m 37.10m ~ 1.000
_SubtreeDataDeserializationWithBufferSizes/512KB-4 36.57m 36.49m ~ 0.700
_PooledVsNonPooled/Pooled-4 831.7n 842.2n ~ 0.100
_PooledVsNonPooled/NonPooled-4 8.007µ 8.177µ ~ 0.700
_MemoryFootprint/Current_512KB_32concurrent-4 7.683µ 8.058µ ~ 0.200
_MemoryFootprint/Proposed_32KB_32concurrent-4 9.338µ 11.975µ ~ 0.100
_MemoryFootprint/Alternative_64KB_32concurrent-4 9.357µ 10.399µ ~ 0.100
_prepareTxsPerLevel-4 542.1m 406.1m ~ 0.100
_prepareTxsPerLevelOrdered-4 6.047m 3.845m ~ 0.100
_prepareTxsPerLevel_Comparison/Original-4 521.2m 404.4m ~ 0.100
_prepareTxsPerLevel_Comparison/Optimized-4 6.708m 3.866m ~ 0.100
SubtreeSizes/10k_tx_4_per_subtree-4 1.033m 1.021m ~ 0.700
SubtreeSizes/10k_tx_16_per_subtree-4 232.3µ 230.1µ ~ 0.700
SubtreeSizes/10k_tx_64_per_subtree-4 57.50µ 57.03µ ~ 0.400
SubtreeSizes/10k_tx_256_per_subtree-4 14.97µ 14.97µ ~ 1.000
SubtreeSizes/10k_tx_512_per_subtree-4 7.407µ 7.491µ ~ 0.700
SubtreeSizes/10k_tx_1024_per_subtree-4 3.715µ 3.818µ ~ 0.100
SubtreeSizes/10k_tx_2k_per_subtree-4 1.859µ 1.910µ ~ 0.100
BlockSizeScaling/10k_tx_64_per_subtree-4 59.99µ 62.98µ ~ 0.200
BlockSizeScaling/10k_tx_256_per_subtree-4 15.29µ 15.91µ ~ 0.700
BlockSizeScaling/10k_tx_1024_per_subtree-4 3.780µ 3.861µ ~ 0.700
BlockSizeScaling/50k_tx_64_per_subtree-4 307.8µ 307.4µ ~ 1.000
BlockSizeScaling/50k_tx_256_per_subtree-4 77.62µ 77.14µ ~ 1.000
BlockSizeScaling/50k_tx_1024_per_subtree-4 19.25µ 19.50µ ~ 0.700
SubtreeAllocations/small_subtrees_exists_check-4 128.2µ 126.7µ ~ 1.000
SubtreeAllocations/small_subtrees_data_fetch-4 134.5µ 133.9µ ~ 1.000
SubtreeAllocations/small_subtrees_full_validation-4 262.3µ 257.6µ ~ 0.400
SubtreeAllocations/medium_subtrees_exists_check-4 8.013µ 8.013µ ~ 1.000
SubtreeAllocations/medium_subtrees_data_fetch-4 8.302µ 8.393µ ~ 0.200
SubtreeAllocations/medium_subtrees_full_validation-4 15.75µ 15.80µ ~ 1.000
SubtreeAllocations/large_subtrees_exists_check-4 1.982µ 1.955µ ~ 0.200
SubtreeAllocations/large_subtrees_data_fetch-4 2.059µ 2.050µ ~ 1.000
SubtreeAllocations/large_subtrees_full_validation-4 3.998µ 3.959µ ~ 0.400
StoreBlock_Sequential/BelowCSVHeight-4 361.5µ 353.4µ ~ 0.100
StoreBlock_Sequential/AboveCSVHeight-4 365.0µ 354.7µ ~ 1.000
GetUtxoHashes-4 229.4n 219.4n ~ 0.100
GetUtxoHashes_ManyOutputs-4 39.31µ 39.36µ ~ 0.700
MetaBytes-4 71.02n 72.91n ~ 0.100
_NewMetaDataFromBytes-4 235.6n 220.9n ~ 0.100
_Bytes-4 372.7n 348.0n ~ 0.100
_MetaBytes-4 121.2n 112.2n ~ 0.100

Threshold: >10% with p < 0.05 | Generated: 2026-08-31 17:15 UTC

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.

1 participant