Skip to content

fix(blockvalidation): an in-flight tagging run no longer overwrites an invalidation's mined_set reset - #2018

Open
freemans13 wants to merge 9 commits into
bsv-blockchain:mainfrom
freemans13:stu/bv-mined-set-cas
Open

freemans13 wants to merge 9 commits into
bsv-blockchain:mainfrom
freemans13:stu/bv-mined-set-cas

Conversation

@freemans13

@freemans13 freemans13 commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

What happened

Block validation tags a block's transactions as mined in the UTXO store, then sets the block's mined_set flag to say the tagging is done. The function that does this, setTxMinedStatus, decides at the start whether to tag or untag and whether the block is on the main chain. When it finishes, it set mined_set = true with an unconditional UPDATE.

Three writers clear mined_set so the block is processed again under its new state: InvalidateBlock, RevalidateBlock, and ClearBlockMinedSet, which catchup calls on old-chain blocks when it finds a fork. If one of them ran while a tagging run was in flight, the run's final write overwrote the clear. After that, two things skipped the block for good. One is the setMined worker's guard that ignores blocks already marked mined. The other is the periodic sweep, which only lists blocks with mined_set = false.

The result was an invalid block with its transactions still tagged, or a revalidated block with no tags, both carrying mined_set = true. The spec's proof test reproduces this on main (GAP-BV-058, issue bitcoin-sv/teranode#4989).

Changes

  • Each block gets a mined_set generation. The blockchain store adds a mined_set_gen column to blocks, created on startup with default 0 on Postgres and SQLite. ClearBlockMinedSet, InvalidateBlock and RevalidateBlock each add one to it when they clear mined_set. A counter is used rather than the invalid flag because ClearBlockMinedSet does not touch invalid, and an invalidate followed by a revalidate inside one run would put invalid back where it started.
  • SetBlockMinedSet writes only while the generation is unchanged. A tagging run reads the generation from GetBlockHeader before any work and passes it back. The update adds AND mined_set_gen = $2. If no row matches, the store reads the block again: a missing block is a storage error, a moved generation is ErrStateError, and an unchanged generation is also a storage error, so a fault that is not the guard cannot loop forever as a free re-queue.
  • A refused write re-queues the block after a backoff, and leaves its retry counter alone. setTxMinedStatus returns a dedicated re-queue error. The setMined worker neither charges nor resets the block's retry counter on it, and re-queues through the same backoff path a failed run uses (1 s at least). Charging would push a busy block toward the "manual intervention required" drop. Resetting would let one refusal wipe out the attempts a block with real storage errors has used. The backoff stops a generation that keeps moving from driving back-to-back full re-tags of a large block.
  • A run that starts on a stale invalid flag also re-queues. The worker reads invalid before the run reads the generation. An invalidation that lands between those two reads is already counted in the generation, so the guard cannot catch it. The run compares the flag it was given with the one it read and re-queues without tagging if they differ.
  • The generation travels on gRPC as an optional field. SetBlockMinedSetRequest gains optional uint64 mined_set_generation = 2, and GetBlockHeaderResponse gains mined_set_generation = 16. optional lets the server tell "not sent" from 0. An absent field means an older caller, and gets the old unconditional write.
  • CHANGELOG, proto docs, the blockchain reference and the set-tx-mined sequence diagram describe the conditional write.

Testing

  • The three TestGapBV058_* tests in services/blockvalidation/gap_bv058_test.go park a real tagging run inside SetMinedMulti on the sqlitememory store, run the interfering write, then release it. They cover InvalidateBlock, ClearBlockMinedSet, and the stale-flag window. Each asserts that mined_set stays false, that no tags are left on an invalid block, and that the run returns the re-queue error rather than nil or a plain failure.
  • TestSetMinedChanWorker_RequeueKeepsRetryCounterAndBacksOff drives the real worker. It seeds a retry counter of 1, forces a re-queue, and checks that the block waits out a backoff, that the counter stays at 1, and that the block is processed again afterwards. Restoring the immediate re-queue that cleared the counter makes it fail.
  • TestSQL_SetBlockMinedSet covers the normal write, that each of the three clearing writers bumps the generation, refusal after each of them, invalidate-then-revalidate inside one run, an untagging run on an invalid block, the unconditional write, and an unknown block, on sqlite and on a postgres testcontainer. TestSQL_SetBlockMinedSet_UnchangedGenerationIsStorageError covers the unchanged-generation fault. TestBlockchainSchema_AddsMinedSetGenToExistingBlocks checks the column is added to an existing postgres database.
  • TestClientSetBlockMinedSet and TestSetBlockMinedSet check that ErrStateError survives the gRPC wrap and unwrap, that a generation of 0 is still sent as present, and that a present generation reaches the store.
  • go test -race -tags testtxmetacache passes on the blockvalidation, blockchain store, blockchain service, blockpersister and legacy netsync packages. golangci-lint is clean on the touched packages.

Limits and trade-offs

  • The catchup fork case is not closed by this change. Catchup clears mined_set on old-chain blocks before the reorg happens (catchup.go, step 5.5). The re-queued run therefore still finds the block on the main chain, tags it with onLongestChain = true, and sets mined_set = true, and nothing clears it again after the real reorg. That ordering predates this PR. Closing it needs a clear after the reorg, which is a separate change.
  • Mixed versions keep working, but the race stays open until both services run the new version. An older block validation service sends no generation and gets the unconditional write. A new block validation service talking to an older blockchain service has its field ignored.
  • The refusal reuses ErrStateError, the code the FSM (the node's run-state machine) also uses. Nothing else on the SetBlockMinedSet path returns it. A dedicated code would mean editing errors/error.proto.
  • A re-queued block now waits at least 1 s before it is processed again, where the earlier version of this PR re-queued at once.

Refs bitcoin-sv/teranode#4989

🤖 Generated with Claude Code

https://claude.ai/code/session_016HzSYZUnpxNSz4kXw4T5yR

…n invalidation's mined_set reset

setTxMinedStatus reads the block's invalid flag once, tags or untags the
block's transactions, then sets mined_set = true. That write was an
unconditional UPDATE. If InvalidateBlock or RevalidateBlock ran in
between, they had cleared mined_set so the block would be processed
again; the overwrite made the setMined worker's MinedSet guard and the
mined-not-set sweep both skip it for good. The result was an invalid
block with its transactions still tagged, or a revalidated block with
none.

SetBlockMinedSet now takes the invalid flag the run started from and
only writes while the row still has it. A mismatch returns
ErrStateError, and setTxMinedStatus re-queues the block and returns nil,
so the refusal does not charge the retry counter. The flag travels in a
new expectInvalid field on SetBlockMinedSetRequest.

The GAP-BV-058 proof test is now an untagged regression test that also
asserts the re-queue and the nil return. New store tests cover both
directions of the race on sqlite and on postgres, and a client test
checks ErrStateError survives the gRPC round trip.

Refs bitcoin-sv/teranode#4989

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013BLSCaXeSCCbSqjX4s5KBe
@freemans13 freemans13 self-assigned this Oct 21, 2026
…ions in TestClientSetBlockMinedSet

The repo's test convention is testify require, not assert; the two
assertions added for the refused-write case now stop the subtest on failure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UJ2B4FENo6G5s2TDXXvdKs
@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Benchmark Comparison Report

Baseline: main (unknown)

Current: PR-2018 (8c21c3e)

Summary

  • Regressions: 0
  • Improvements: 0
  • Unchanged: 169
  • Significance level: p < 0.05
All benchmark results (sec/op)
Benchmark Baseline Current Change p-value
_NewBlockFromBytes-4 1.716µ 1.710µ ~ 0.400
Block_ValidOrderAndBlessed_DiskVsMemory/leaves=1024/memory-4 11.90m 11.94m ~ 1.000
Block_ValidOrderAndBlessed_DiskVsMemory/leaves=1024/disk_1-4 13.01m 12.89m ~ 0.100
Block_ValidOrderAndBlessed_DiskVsMemory/leaves=1024/disk_2-4 13.94m 14.04m ~ 0.200
Block_ValidOrderAndBlessed_DiskVsMemory/leaves=16384/memo... 27.70m 27.54m ~ 1.000
Block_ValidOrderAndBlessed_DiskVsMemory/leaves=16384/disk... 37.52m 37.57m ~ 0.700
Block_ValidOrderAndBlessed_DiskVsMemory/leaves=16384/disk... 37.64m 37.29m ~ 0.100
Block_ValidOrderAndBlessed_DiskVsMemory/leaves=131072/mem... 118.1m 118.2m ~ 0.700
Block_ValidOrderAndBlessed_DiskVsMemory/leaves=131072/dis... 252.6m 254.0m ~ 0.400
Block_ValidOrderAndBlessed_DiskVsMemory/leaves=131072/dis... 253.7m 254.0m ~ 1.000
BlockTxMap/txs=16384/nodeindex-4 2.219m 2.174m ~ 0.100
BlockTxMap/txs=16384/swiss-4 16.27m 15.91m ~ 0.100
BlockTxMap/txs=262144/nodeindex-4 22.71m 22.48m ~ 0.100
BlockTxMap/txs=262144/swiss-4 56.62m 57.50m ~ 0.100
BlockTxMap/txs=4194304/nodeindex-4 484.8m 484.8m ~ 0.700
BlockTxMap/txs=4194304/swiss-4 1.009 1.026 ~ 0.400
TxMapPut_MemoryVsDisk/txs=100000/memory-4 10.48m 10.59m ~ 0.100
TxMapPut_MemoryVsDisk/txs=100000/disk_1-4 12.19m 12.73m ~ 0.100
TxMapPut_MemoryVsDisk/txs=100000/disk_2-4 11.35m 11.60m ~ 0.100
TxMapGetFrozen_MemoryVsDisk/txs=100000/memory/hit-4 5.321m 5.721m ~ 0.700
TxMapGetFrozen_MemoryVsDisk/txs=100000/memory/miss-4 3.451m 3.712m ~ 0.200
TxMapGetFrozen_MemoryVsDisk/txs=100000/disk_1/hit-4 2.489m 2.645m ~ 0.100
TxMapGetFrozen_MemoryVsDisk/txs=100000/disk_1/miss-4 2.437m 2.586m ~ 0.100
TxMapGetFrozen_MemoryVsDisk/txs=100000/disk_2/hit-4 2.515m 2.586m ~ 0.100
TxMapGetFrozen_MemoryVsDisk/txs=100000/disk_2/miss-4 2.449m 2.648m ~ 0.100
ParentSpendsSetIfNotExists_MemoryVsDisk/inputs=200000/dis... 27.14m 26.99m ~ 1.000
ParentSpendsSetIfNotExists_MemoryVsDisk/inputs=200000/dis... 24.78m 25.22m ~ 0.200
InpointDedup/txs=16K-4 5.427m 5.413m ~ 1.000
InpointDedup/txs=128K-4 21.49m 21.72m ~ 0.700
InpointDedup/txs=1M-4 149.9m 151.4m ~ 0.200
InpointDedup_TinyRuns-4 56.44m 56.96m ~ 0.400
MiningCandidate_Stringify_Short-4 232.1n 232.0n ~ 0.700
MiningCandidate_Stringify_Long-4 1.610µ 1.635µ ~ 0.500
MiningSolution_Stringify-4 804.3n 805.9n ~ 1.000
BlockInfo_MarshalJSON-4 1.974µ 1.965µ ~ 1.000
DecodeSubtreeMeta/go-subtree-4 225.5m 226.2m ~ 0.700
DecodeSubtreeMeta/packed-4 69.44m 71.06m ~ 0.700
DecodeSubtreeMeta/go-subtree/parallel-4 118.5m 117.6m ~ 1.000
DecodeSubtreeMeta/packed/parallel-4 39.11m 44.70m ~ 1.000
ValidateSubtreeNodesMatchKey/leaves=1024-4 254.1µ 253.9µ ~ 0.700
ValidateSubtreeNodesMatchKey/leaves=1048576-4 109.4m 109.5m ~ 1.000
NewFromBytes-4 125.0n 127.3n ~ 0.700
AddTxBatchColumnar_Validation-4 2.151µ 2.159µ ~ 1.000
OffsetValidationLoop-4 640.7n 642.2n ~ 0.200
BuildSubtreeMeta/serialized-4 13.40m 13.28m ~ 1.000
BuildSubtreeMeta/decoded-4 24.61m 26.68m ~ 0.700
Mine_EasyDifficulty-4 37.62µ 37.88µ ~ 0.700
Mine_WithAddress-4 4.213µ 4.373µ ~ 0.200
DiskTxMap_SetIfNotExists-4 522.8n 528.1n ~ 0.400
DiskTxMap_SetIfNotExists_Parallel-4 225.0n 184.8n ~ 0.200
DiskTxMap_ExistenceOnly-4 285.9n 284.9n ~ 1.000
Queue-4 168.8n 168.6n ~ 0.500
AtomicPointer-4 4.096n 4.016n ~ 1.000
TxMapSetIfNotExists-4 62.92n 63.08n ~ 1.000
TxMapSetIfNotExistsDuplicate-4 47.42n 47.22n ~ 0.700
ChannelSendReceive-4 589.1n 593.0n ~ 0.100
DirectSubtreeAdd/4_per_subtree-4 60.65n 60.53n ~ 1.000
DirectSubtreeAdd/64_per_subtree-4 27.40n 27.56n ~ 0.400
DirectSubtreeAdd/256_per_subtree-4 25.95n 26.40n ~ 1.000
DirectSubtreeAdd/1024_per_subtree-4 23.75n 24.53n ~ 0.700
DirectSubtreeAdd/2048_per_subtree-4 24.28n 24.15n ~ 0.400
SubtreeProcessorAdd/4_per_subtree-4 146.5n 153.7n ~ 0.200
SubtreeProcessorAdd/64_per_subtree-4 163.0n 163.6n ~ 1.000
SubtreeProcessorAdd/256_per_subtree-4 166.3n 167.9n ~ 1.000
SubtreeProcessorAdd/1024_per_subtree-4 172.3n 175.7n ~ 1.000
SubtreeProcessorAdd/2048_per_subtree-4 178.7n 176.2n ~ 1.000
SubtreeProcessorRotate/4_per_subtree-4 183.5n 179.0n ~ 0.400
SubtreeProcessorRotate/64_per_subtree-4 181.0n 186.8n ~ 0.100
SubtreeProcessorRotate/256_per_subtree-4 179.2n 184.1n ~ 0.700
SubtreeProcessorRotate/1024_per_subtree-4 181.3n 181.4n ~ 1.000
SubtreeNodeAddOnly/4_per_subtree-4 65.87n 64.40n ~ 0.100
SubtreeNodeAddOnly/64_per_subtree-4 49.82n 52.27n ~ 0.400
SubtreeNodeAddOnly/256_per_subtree-4 48.64n 48.85n ~ 1.000
SubtreeNodeAddOnly/1024_per_subtree-4 47.96n 47.88n ~ 1.000
SubtreeCreationOnly/4_per_subtree-4 108.9n 107.8n ~ 1.000
SubtreeCreationOnly/64_per_subtree-4 303.6n 296.0n ~ 1.000
SubtreeCreationOnly/256_per_subtree-4 936.8n 964.4n ~ 1.000
SubtreeCreationOnly/1024_per_subtree-4 2.673µ 2.653µ ~ 0.100
SubtreeCreationOnly/2048_per_subtree-4 4.788µ 4.848µ ~ 1.000
SubtreeProcessorOverheadBreakdown/64_per_subtree-4 140.3n 138.5n ~ 0.700
SubtreeProcessorOverheadBreakdown/1024_per_subtree-4 174.0n 170.0n ~ 0.700
ParallelGetAndSetIfNotExists/1k_nodes-4 608.5µ 522.8µ ~ 0.700
ParallelGetAndSetIfNotExists/10k_nodes-4 4.053m 3.886m ~ 0.700
ParallelGetAndSetIfNotExists/50k_nodes-4 5.842m 5.233m ~ 0.100
ParallelGetAndSetIfNotExists/100k_nodes-4 7.603m 7.027m ~ 0.100
SequentialGetAndSetIfNotExists/1k_nodes-4 510.0µ 463.6µ ~ 0.400
SequentialGetAndSetIfNotExists/10k_nodes-4 4.271m 3.742m ~ 0.100
SequentialGetAndSetIfNotExists/50k_nodes-4 17.04m 15.19m ~ 0.100
SequentialGetAndSetIfNotExists/100k_nodes-4 29.07m 25.28m ~ 0.100
ProcessOwnBlockSubtreeNodesParallel/1k_nodes-4 707.5µ 627.5µ ~ 1.000
ProcessOwnBlockSubtreeNodesParallel/10k_nodes-4 5.991m 5.918m ~ 0.700
ProcessOwnBlockSubtreeNodesParallel/100k_nodes-4 9.224m 8.647m ~ 0.100
ProcessOwnBlockSubtreeNodesSequential/1k_nodes-4 455.9µ 725.9µ ~ 0.400
ProcessOwnBlockSubtreeNodesSequential/10k_nodes-4 8.035m 6.181m ~ 0.700
ProcessOwnBlockSubtreeNodesSequential/100k_nodes-4 46.81m 36.99m ~ 0.100
BlockAssembler_AddTx-4 0.01855n 0.01663n ~ 1.000
AddNode-4 11.35 11.42 ~ 0.400
AddNodeWithMap-4 11.68 11.75 ~ 0.400
CalcBlockWork-4 514.4n 516.2n ~ 0.700
CalculateWork-4 695.9n 700.9n ~ 0.700
CheckOldBlockIDs/on-chain-prefetch/1000-4 31.99µ 32.98µ ~ 0.700
CheckOldBlockIDs/in-memory-chain-check/1000-4 849.9µ 877.5µ ~ 0.200
CheckOldBlockIDs/on-chain-prefetch/10000-4 232.6µ 236.6µ ~ 1.000
CheckOldBlockIDs/in-memory-chain-check/10000-4 1.240m 1.231m ~ 0.700
BuildBlockLocatorString_Helpers/Size_10-4 824.1n 755.5n ~ 0.100
BuildBlockLocatorString_Helpers/Size_100-4 7.626µ 7.045µ ~ 0.100
BuildBlockLocatorString_Helpers/Size_1000-4 76.16µ 70.47µ ~ 0.100
CatchupWithHeaderCache-4 104.8m 104.8m ~ 0.700
_prepareTxsPerLevel-4 453.6m 447.8m ~ 0.400
_prepareTxsPerLevelOrdered-4 4.550m 4.736m ~ 0.700
_prepareTxsPerLevel_Comparison/Original-4 456.7m 449.0m ~ 0.700
_prepareTxsPerLevel_Comparison/Optimized-4 4.469m 4.318m ~ 0.100
_BufferPoolAllocation/16KB-4 4.702µ 5.108µ ~ 0.700
_BufferPoolAllocation/32KB-4 10.58µ 12.13µ ~ 0.400
_BufferPoolAllocation/64KB-4 18.94µ 18.62µ ~ 1.000
_BufferPoolAllocation/128KB-4 34.46µ 34.70µ ~ 1.000
_BufferPoolAllocation/512KB-4 143.2µ 136.8µ ~ 0.100
_BufferPoolConcurrent/32KB-4 21.27µ 21.09µ ~ 1.000
_BufferPoolConcurrent/64KB-4 33.38µ 33.72µ ~ 0.400
_BufferPoolConcurrent/512KB-4 170.3µ 178.0µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/16KB-4 645.5µ 674.2µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/32KB-4 636.2µ 662.7µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/64KB-4 630.0µ 664.3µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/128KB-4 624.9µ 661.3µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/512KB-4 634.5µ 667.4µ ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/16KB-4 34.76m 35.90m ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/32KB-4 34.90m 36.17m ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/64KB-4 34.89m 35.69m ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/128KB-4 35.18m 35.96m ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/512KB-4 34.62m 35.84m ~ 0.100
_PooledVsNonPooled/Pooled-4 822.9n 828.6n ~ 0.100
_PooledVsNonPooled/NonPooled-4 8.325µ 8.711µ ~ 0.400
_MemoryFootprint/Current_512KB_32concurrent-4 7.383µ 8.834µ ~ 0.100
_MemoryFootprint/Proposed_32KB_32concurrent-4 9.720µ 12.194µ ~ 0.100
_MemoryFootprint/Alternative_64KB_32concurrent-4 8.965µ 11.005µ ~ 0.100
SubtreeSizes/10k_tx_4_per_subtree-4 1.047m 1.128m ~ 0.100
SubtreeSizes/10k_tx_16_per_subtree-4 246.7µ 255.1µ ~ 0.700
SubtreeSizes/10k_tx_64_per_subtree-4 58.35µ 60.49µ ~ 0.100
SubtreeSizes/10k_tx_256_per_subtree-4 14.87µ 15.46µ ~ 0.200
SubtreeSizes/10k_tx_512_per_subtree-4 7.345µ 7.728µ ~ 0.100
SubtreeSizes/10k_tx_1024_per_subtree-4 3.669µ 3.854µ ~ 0.400
SubtreeSizes/10k_tx_2k_per_subtree-4 1.847µ 1.886µ ~ 0.700
BlockSizeScaling/10k_tx_64_per_subtree-4 61.57µ 62.78µ ~ 0.400
BlockSizeScaling/10k_tx_256_per_subtree-4 15.14µ 15.70µ ~ 1.000
BlockSizeScaling/10k_tx_1024_per_subtree-4 3.640µ 4.013µ ~ 0.100
BlockSizeScaling/50k_tx_64_per_subtree-4 302.8µ 313.4µ ~ 0.100
BlockSizeScaling/50k_tx_256_per_subtree-4 75.58µ 78.23µ ~ 0.100
BlockSizeScaling/50k_tx_1024_per_subtree-4 21.01µ 20.74µ ~ 0.700
SubtreeAllocations/small_subtrees_exists_check-4 140.8µ 140.7µ ~ 1.000
SubtreeAllocations/small_subtrees_data_fetch-4 150.5µ 149.5µ ~ 0.700
SubtreeAllocations/small_subtrees_full_validation-4 296.4µ 297.4µ ~ 0.700
SubtreeAllocations/medium_subtrees_exists_check-4 8.926µ 8.885µ ~ 0.700
SubtreeAllocations/medium_subtrees_data_fetch-4 9.602µ 9.597µ ~ 0.700
SubtreeAllocations/medium_subtrees_full_validation-4 18.60µ 18.88µ ~ 1.000
SubtreeAllocations/large_subtrees_exists_check-4 2.246µ 2.226µ ~ 0.300
SubtreeAllocations/large_subtrees_data_fetch-4 2.393µ 2.413µ ~ 0.400
SubtreeAllocations/large_subtrees_full_validation-4 4.730µ 4.719µ ~ 0.700
StoreBlockForkPath/inMemoryChainCheck=false-4 619.1µ 633.8µ ~ 0.700
StoreBlockForkPath/inMemoryChainCheck=true-4 1.292m 1.285m ~ 0.400
StoreBlock_Sequential/BelowCSVHeight-4 195.7µ 193.9µ ~ 0.700
StoreBlock_Sequential/AboveCSVHeight-4 194.7µ 197.6µ ~ 0.400
CheckSpendAndCreateMultiList_FanIn/parents=80000-4 52.18m 50.77m ~ 0.400
CheckSpendAndCreateMultiList_FanIn/parents=160000-4 119.5m 127.6m ~ 0.100
GetUtxoHashes-4 297.6n 296.2n ~ 0.700
GetUtxoHashes_ManyOutputs-4 42.08µ 42.17µ ~ 0.700
MetaBytes-4 68.34n 68.52n ~ 0.400
_NewMetaDataFromBytes-4 224.3n 225.9n ~ 1.000
_Bytes-4 341.8n 337.9n ~ 0.100
_MetaBytes-4 106.7n 107.9n ~ 0.100

Threshold: >10% with p < 0.05 | Generated: 2026-10-29 12:32 UTC

@freemans13
freemans13 requested review from icellan and ordishs and removed request for icellan October 21, 2026 11:47
@freemans13
freemans13 marked this pull request as ready for review October 21, 2026 11:47

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

The fix is correct for the stated scope. Making the mined_set write conditional on invalid, plus the re-queue, closes GAP-BV-058. The tests derive from the requirement, and they cover sqlite and postgres.

Two points to address before merge:

  1. Upgrade order. A pre-upgrade client sends expectInvalid = false. If it tags an invalid block against a new server, the server refuses with ErrStateError. The old client has no re-queue. It counts a retry, and after setMinedMaxRetries it drops the block. Document the upgrade order, or make the field optional and skip the condition when unset.
  2. CHANGELOG. AGENTS.md requires an ## [Unreleased] entry. This PR adds a proto field and a new refusal on SetBlockMinedSet.

Minor: blockchain_reference.md, blockchainProto.md and the block_validation_set_tx_mined.puml diagram still describe an unconditional write.

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

The compare-and-set closes the invalidate race but guards the wrong field; details inline.

Also missing: a CHANGELOG.md entry under ## [Unreleased] for the SetBlockMinedSetRequest.expectInvalid proto change and the new ERR_STATE_ERROR response from SetBlockMinedSet, including the mixed-version upgrade behaviour.

Comment thread stores/blockchain/sql/SetBlockMinedSet.go Outdated
Comment thread stores/blockchain/sql/SetBlockMinedSet.go Outdated
Comment thread services/blockchain/blockchain_api/blockchain_api.proto Outdated
freemans13 and others added 2 commits October 27, 2026 11:15
…the invalid flag

The guard on invalid missed two cases. ClearBlockMinedSet resets mined_set
without touching invalid, so a run that started before a catchup reset could
still write mined_set=true over it. An invalidate then revalidate inside one
run also put invalid back to its starting value, so the stale result landed.

blocks gains mined_set_gen (default 0, added on startup). ClearBlockMinedSet,
InvalidateBlock and RevalidateBlock bump it. GetBlockHeader returns it, the
tagging run captures it before any work, and SetBlockMinedSet only writes while
it is unchanged. A moved generation is still ERR_STATE_ERROR, so the run
re-queues without charging a retry.

The run also re-queues when the invalid flag the worker passed in differs
from the one it reads itself, since the generation is captured after that gap.

On the zero-rows path the RowsAffected error is now checked, and an unchanged
generation returns a storage error, so a real storage fault is charged
retries and cannot loop for free.

The request field is now optional uint64 mined_set_generation. A request
without it, from an older blockvalidation, gets the old unconditional write,
so mixed versions keep working. CHANGELOG entry, proto and reference docs and
the set-tx-mined diagram are updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016HzSYZhvqpYZPHF3YjMa3F
CHANGELOG.md conflicted only on adjacent entries; both are kept. The new
set_tx_mined_revert_test call to SetBlockMinedSet gains the generation argument.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016HzSYZhvqpYZPHF3YjMa3F
@github-actions

github-actions Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

🤖 Claude Code Review

Status: Complete

Current Review:
No new issues found in cbfd4d5 or 7d79be1.

Still open from earlier passes, with no change requested: re-queues never count against the retry limit. A block whose generation keeps moving is re-tagged once per backoff interval with no upper bound. Only explicit clear, invalidate and revalidate operations move the generation, so this should be bounded in practice.

History:

  • ✅ Fixed (da5cf29): a refused write reset the retry counter and re-queued with no backoff (@icellan's thread)
  • ✅ Fixed (da5cf29): the CHANGELOG claimed the catchup fork case was closed when it isn't (@icellan's thread)
  • ✅ Fixed (e56bd60): generation guard replaces the invalid flag, RowsAffected handling, optional proto field. I've left all threads for the reviewer to resolve.

Worth running locally (not run here): go test -race ./stores/blockchain/sql/ -run 'SetBlockMinedSet|SchemaCurrent' (the Postgres upgrade test needs TestContainers).

@freemans13

Copy link
Copy Markdown
Collaborator Author

@ordishs, all three points from your review are addressed in e56bd60 (merged with upstream/main as b5eafe7).

  1. Upgrade order. The request field is now optional uint64 mined_set_generation. When it is absent, as from a pre-upgrade blockvalidation, the server does the old unconditional write. An old client is never refused and never burns its retries. A new client against an old server has the field ignored and also gets the unconditional write. No upgrade order is required. The race stays open until both services are upgraded. Note that the guard is no longer on invalid: @icellan showed that ClearBlockMinedSet and an invalidate-then-revalidate both slip past it. It is now a per-block mined_set_gen counter that every mined_set clear bumps. Details are in the inline threads.
  2. CHANGELOG. Added under ## [Unreleased] → "Breaking — behaviour changes on upgrade". It covers the new column, both proto fields, the new ERR_STATE_ERROR refusal, and the mixed-version behaviour.
  3. Docs. blockchain_reference.md (SetBlockMinedSet), blockchainProto.md (both request and response fields) and block_validation_set_tx_mined.puml now describe the conditional write. The SVG was regenerated from the .puml.

Your review approved d37b022. The design changed since then, so a second look would be worth it.

@freemans13

Copy link
Copy Markdown
Collaborator Author

@icellan, both points from your review are addressed in e56bd60 (merged with upstream/main as b5eafe7).

  • Wrong field. The guard is now a per-block mined_set_gen generation. ClearBlockMinedSet, InvalidateBlock and RevalidateBlock bump it, and the run compares against the value it captured at start. The catchup clear case and the ABA case each have a test that fails without the fix. Details are in the inline reply.
  • CHANGELOG. Added under ## [Unreleased] → "Breaking — behaviour changes on upgrade". It covers the new optional SetBlockMinedSetRequest.mined_set_generation, GetBlockHeaderResponse.mined_set_generation, the ERR_STATE_ERROR refusal, and the mixed-version behaviour: an absent field gets the old unconditional write, so no upgrade order is required.

I left the three inline threads open for you to resolve.

@freemans13
freemans13 requested a review from icellan October 27, 2026 11:25

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

The generation counter fixes both lost-update paths, and the optional field keeps mixed-version deploys working. The earlier comments are addressed. Two non-blocking points inline.

The PR description is stale. It still describes the invalid = $2 / bool expectInvalid design and lists the generation counter as not done, so update it before the squash-merge.

Comment thread services/blockvalidation/BlockValidation.go Outdated
Comment thread services/blockvalidation/gap_bv058_test.go
freemans13 and others added 2 commits October 28, 2026 12:23
CHANGELOG.md conflicted because this branch and upstream each added an entry at the
top of the breaking-changes list. Both entries are kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016HzSYZUnpxNSz4kXw4T5yR
…koff and keeps the retry counter

When the mined_set generation moved under a tagging run, setTxMinedStatus put the
block straight back on setMinedChan and returned nil. The worker then took nil as
success and deleted the block's retry counter, so a block that had used 9 of its 10
attempts on real storage errors started again from 0 after one refusal. The re-queue
also had no wait, so a generation that kept moving drove back-to-back full re-tags.

setTxMinedStatus now returns a dedicated re-queue error for both re-queue cases (the
refused write and the stale invalid flag). The worker leaves the retry counter as it
is, neither charged nor reset, and re-queues through the existing backoff path, at
least 1 s.

The CHANGELOG entry and the ClearBlockMinedSet test comment no longer imply the
catchup fork case is closed. Catchup clears mined_set before the reorg, so the
re-queued run still tags the block as on the main chain. That ordering predates
this branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016HzSYZUnpxNSz4kXw4T5yR
@freemans13

Copy link
Copy Markdown
Collaborator Author

@icellan, the PR description is rewritten for the generation design as of da5cf29. It now describes the mined_set_gen column, the optional mined_set_generation field, the re-queue with backoff that leaves the retry counter alone, and lists the catchup fork ordering as an open limit. Your two inline points are answered on their threads.

CHANGELOG.md conflicted because this branch and upstream (the mining candidate
coinbase change) each added an entry at the top of the breaking-changes list. Both
entries are kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016HzSYZUnpxNSz4kXw4T5yR
@freemans13

Copy link
Copy Markdown
Collaborator Author

Claude Code Review note on fd94b38: re-queues are not charged against the retry limit. That is deliberate, and it is what @icellan asked for on the retry-counter thread. A re-queue means the block state moved, which is not a failure, so charging it could drop a block as "manual intervention required" for no fault of its own. Only ClearBlockMinedSet, InvalidateBlock and RevalidateBlock move the generation, and each re-queue waits at least 1 s, so the repeat rate is bounded by how often an operator or catchup resets the block. No change.

freemans13 and others added 2 commits October 29, 2026 12:10
Resolves the CHANGELOG.md conflict by keeping both Unreleased entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016HzSYWhzVWctuAFYQTLyXn
…n engines

The postgres and sqlite schema builders each carried the same 14-line
probe-then-ALTER block for mined_set_gen. SonarQube counted it as 34 of the
37 new lines in sql.go and failed the PR on duplication (3.3% against a 3%
limit). Both now call addMinedSetGenColumn with their engine's probe query.
Behaviour is unchanged: the postgres upgrade test still drops the column,
reopens the store and sees it added back with existing rows at 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016HzSYWhzVWctuAFYQTLyXn
@sonarqubecloud

Copy link
Copy Markdown

@freemans13

Copy link
Copy Markdown
Collaborator Author

Claude Code Review on cbfd4d5 found no new issues. The one carried-over note, that re-queues do not count against the retry limit, is unchanged and was answered in the earlier reply on fd94b38. cbfd4d5 moves the duplicated mined_set_gen migration into one helper, which clears the SonarQube duplication gate. No behaviour change.

This branch has not been deployed

No deployments
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.

3 participants