Skip to content

fix(utxo/sql): detect double-spend when competing txs share a batch slot#824

Merged
sugh01 merged 1 commit into
bsv-blockchain:test/adversarial-double-spend-coveragefrom
sugh01:test/adversarial-double-spend-coverage-fix-ds-logic
May 7, 2026
Merged

fix(utxo/sql): detect double-spend when competing txs share a batch slot#824
sugh01 merged 1 commit into
bsv-blockchain:test/adversarial-double-spend-coveragefrom
sugh01:test/adversarial-double-spend-coverage-fix-ds-logic

Conversation

@sugh01

@sugh01 sugh01 commented May 7, 2026

Copy link
Copy Markdown
Collaborator

No description provided.

@github-actions

github-actions Bot commented May 7, 2026

Copy link
Copy Markdown
Contributor

🤖 Claude Code Review

Status: Complete


Summary:
The logic correctly detects double-spends when two competing transactions attempt to spend the same UTXO within a single batch. The implementation properly distinguishes between idempotent re-spends (same spending data) and actual double-spends (different spending data).

Key Changes:

  • Enhanced deduplication logic to check spending data, not just UTXO keys
  • Double-spend attempts are immediately rejected with UtxoSpentError
  • Idempotent duplicates continue to succeed as expected
  • First-arrival-wins policy is correctly enforced

Analysis:
The fix addresses a scenario where the old code would silently allow a double-spend if both competing transactions were processed in the same batch. The new code at sql.go:2041-2050 compares spending data and raises an error for the second transaction when data differs.

Observations:

  1. Logic correctness: Verified through multiple scenarios including edge cases with 3+ competing entries
  2. Error handling: Proper fallback to ProcessingError if spending data parse fails
  3. Success propagation: Line 2295 correctly uses bytes.Equal check to avoid marking double-spends as successful
  4. No tests added: The PR modifies critical double-spend detection logic but does not include new tests

Per AGENTS.md guidelines, tests should accompany changes to core validation logic, especially for security-critical paths like double-spend detection.

@sonarqubecloud

sonarqubecloud Bot commented May 7, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
61.1% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

@github-actions

github-actions Bot commented May 7, 2026

Copy link
Copy Markdown
Contributor

Benchmark Comparison Report

Baseline: main (unknown)

Current: PR-824 (595e0b2)

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.695µ 1.959µ ~ 0.200
SplitSyncedParentMap_SetIfNotExists/256_buckets-4 61.87n 61.67n ~ 0.400
SplitSyncedParentMap_SetIfNotExists/16_buckets-4 61.68n 61.75n ~ 1.000
SplitSyncedParentMap_SetIfNotExists/1_bucket-4 61.73n 61.78n ~ 0.100
SplitSyncedParentMap_ConcurrentSetIfNotExists/256_buckets... 30.78n 30.70n ~ 1.000
SplitSyncedParentMap_ConcurrentSetIfNotExists/16_buckets_... 52.80n 50.82n ~ 0.100
SplitSyncedParentMap_ConcurrentSetIfNotExists/1_bucket_pa... 107.3n 107.1n ~ 0.700
MiningCandidate_Stringify_Short-4 263.6n 264.6n ~ 0.200
MiningCandidate_Stringify_Long-4 1.921µ 1.909µ ~ 0.100
MiningSolution_Stringify-4 981.4n 1003.0n ~ 0.100
BlockInfo_MarshalJSON-4 1.770µ 1.774µ ~ 0.400
NewFromBytes-4 127.7n 127.3n ~ 0.400
Mine_EasyDifficulty-4 61.28µ 61.19µ ~ 0.400
Mine_WithAddress-4 7.152µ 7.118µ ~ 0.700
BlockAssembler_AddTx-4 0.02714n 0.02913n ~ 0.700
AddNode-4 10.81 10.91 ~ 0.700
AddNodeWithMap-4 11.34 11.20 ~ 1.000
DiskTxMap_SetIfNotExists-4 3.410µ 3.432µ ~ 1.000
DiskTxMap_SetIfNotExists_Parallel-4 3.235µ 3.323µ ~ 1.000
DiskTxMap_ExistenceOnly-4 296.2n 284.8n ~ 0.100
Queue-4 192.1n 190.9n ~ 0.100
AtomicPointer-4 4.502n 4.909n ~ 0.700
ReorgOptimizations/DedupFilterPipeline/Old/10K-4 858.6µ 839.2µ ~ 0.100
ReorgOptimizations/DedupFilterPipeline/New/10K-4 845.7µ 799.8µ ~ 0.100
ReorgOptimizations/AllMarkFalse/Old/10K-4 109.0µ 102.9µ ~ 0.100
ReorgOptimizations/AllMarkFalse/New/10K-4 61.64µ 61.73µ ~ 0.700
ReorgOptimizations/HashSlicePool/Old/10K-4 61.20µ 68.43µ ~ 0.200
ReorgOptimizations/HashSlicePool/New/10K-4 11.31µ 11.42µ ~ 0.700
ReorgOptimizations/NodeFlags/Old/10K-4 5.114µ 5.520µ ~ 0.200
ReorgOptimizations/NodeFlags/New/10K-4 1.830µ 1.773µ ~ 1.000
ReorgOptimizations/DedupFilterPipeline/Old/100K-4 9.347m 9.186m ~ 1.000
ReorgOptimizations/DedupFilterPipeline/New/100K-4 9.232m 9.030m ~ 0.100
ReorgOptimizations/AllMarkFalse/Old/100K-4 1.071m 1.137m ~ 0.200
ReorgOptimizations/AllMarkFalse/New/100K-4 673.8µ 675.9µ ~ 0.700
ReorgOptimizations/HashSlicePool/Old/100K-4 596.5µ 624.5µ ~ 0.100
ReorgOptimizations/HashSlicePool/New/100K-4 313.1µ 313.7µ ~ 0.700
ReorgOptimizations/NodeFlags/Old/100K-4 51.40µ 54.49µ ~ 0.200
ReorgOptimizations/NodeFlags/New/100K-4 18.95µ 19.24µ ~ 0.400
TxMapSetIfNotExists-4 50.70n 51.15n ~ 0.200
TxMapSetIfNotExistsDuplicate-4 38.03n 37.94n ~ 0.700
ChannelSendReceive-4 583.3n 615.1n ~ 0.100
DirectSubtreeAdd/4_per_subtree-4 78.50n 78.27n ~ 1.000
DirectSubtreeAdd/64_per_subtree-4 43.18n 42.78n ~ 0.100
DirectSubtreeAdd/256_per_subtree-4 41.65n 41.53n ~ 0.700
DirectSubtreeAdd/1024_per_subtree-4 40.75n 40.02n ~ 0.100
DirectSubtreeAdd/2048_per_subtree-4 39.87n 40.11n ~ 0.400
SubtreeProcessorAdd/4_per_subtree-4 350.2n 351.3n ~ 1.000
SubtreeProcessorAdd/64_per_subtree-4 327.7n 342.5n ~ 0.100
SubtreeProcessorAdd/256_per_subtree-4 328.7n 332.5n ~ 0.700
SubtreeProcessorAdd/1024_per_subtree-4 323.9n 337.5n ~ 0.100
SubtreeProcessorAdd/2048_per_subtree-4 327.5n 332.9n ~ 0.400
SubtreeProcessorRotate/4_per_subtree-4 328.8n 344.0n ~ 0.200
SubtreeProcessorRotate/64_per_subtree-4 326.6n 340.1n ~ 0.100
SubtreeProcessorRotate/256_per_subtree-4 323.9n 338.8n ~ 0.100
SubtreeProcessorRotate/1024_per_subtree-4 329.8n 340.7n ~ 0.100
SubtreeNodeAddOnly/4_per_subtree-4 90.92n 92.63n ~ 0.400
SubtreeNodeAddOnly/64_per_subtree-4 66.56n 67.26n ~ 0.400
SubtreeNodeAddOnly/256_per_subtree-4 65.92n 66.36n ~ 0.100
SubtreeNodeAddOnly/1024_per_subtree-4 65.46n 65.36n ~ 0.500
SubtreeCreationOnly/4_per_subtree-4 174.0n 161.7n ~ 0.100
SubtreeCreationOnly/64_per_subtree-4 790.5n 627.0n ~ 0.100
SubtreeCreationOnly/256_per_subtree-4 2.854µ 2.246µ ~ 0.100
SubtreeCreationOnly/1024_per_subtree-4 8.583µ 6.735µ ~ 0.100
SubtreeCreationOnly/2048_per_subtree-4 14.91µ 12.86µ ~ 0.100
SubtreeProcessorOverheadBreakdown/64_per_subtree-4 348.0n 330.2n ~ 0.400
SubtreeProcessorOverheadBreakdown/1024_per_subtree-4 327.3n 330.5n ~ 1.000
ParallelGetAndSetIfNotExists/1k_nodes-4 701.8µ 685.3µ ~ 0.100
ParallelGetAndSetIfNotExists/10k_nodes-4 1.900m 1.855m ~ 0.700
ParallelGetAndSetIfNotExists/50k_nodes-4 10.003m 9.437m ~ 0.100
ParallelGetAndSetIfNotExists/100k_nodes-4 19.26m 18.79m ~ 0.400
SequentialGetAndSetIfNotExists/1k_nodes-4 725.1µ 722.7µ ~ 1.000
SequentialGetAndSetIfNotExists/10k_nodes-4 3.731m 3.590m ~ 0.400
SequentialGetAndSetIfNotExists/50k_nodes-4 12.81m 13.00m ~ 1.000
SequentialGetAndSetIfNotExists/100k_nodes-4 24.55m 24.90m ~ 0.400
ProcessOwnBlockSubtreeNodesParallel/1k_nodes-4 764.4µ 782.9µ ~ 0.700
ProcessOwnBlockSubtreeNodesParallel/10k_nodes-4 4.842m 5.021m ~ 0.200
ProcessOwnBlockSubtreeNodesParallel/100k_nodes-4 22.18m 22.52m ~ 0.700
ProcessOwnBlockSubtreeNodesSequential/1k_nodes-4 894.7µ 786.5µ ~ 0.100
ProcessOwnBlockSubtreeNodesSequential/10k_nodes-4 6.827m 7.423m ~ 0.100
ProcessOwnBlockSubtreeNodesSequential/100k_nodes-4 48.31m 51.21m ~ 0.200
CalcBlockWork-4 566.2n 526.8n ~ 0.700
CalculateWork-4 702.0n 709.2n ~ 0.700
BuildBlockLocatorString_Helpers/Size_10-4 1.318µ 1.303µ ~ 0.200
BuildBlockLocatorString_Helpers/Size_100-4 12.59µ 12.60µ ~ 0.700
BuildBlockLocatorString_Helpers/Size_1000-4 132.6µ 126.3µ ~ 1.000
CatchupWithHeaderCache-4 104.3m 104.3m ~ 1.000
_prepareTxsPerLevel-4 414.9m 414.4m ~ 1.000
_prepareTxsPerLevelOrdered-4 3.687m 3.806m ~ 0.700
_prepareTxsPerLevel_Comparison/Original-4 418.8m 419.5m ~ 0.400
_prepareTxsPerLevel_Comparison/Optimized-4 3.616m 3.770m ~ 0.100
SubtreeSizes/10k_tx_4_per_subtree-4 1.363m 1.413m ~ 0.200
SubtreeSizes/10k_tx_16_per_subtree-4 326.9µ 330.7µ ~ 1.000
SubtreeSizes/10k_tx_64_per_subtree-4 77.27µ 78.46µ ~ 0.100
SubtreeSizes/10k_tx_256_per_subtree-4 19.33µ 19.65µ ~ 0.100
SubtreeSizes/10k_tx_512_per_subtree-4 9.564µ 9.717µ ~ 0.100
SubtreeSizes/10k_tx_1024_per_subtree-4 4.781µ 4.807µ ~ 0.400
SubtreeSizes/10k_tx_2k_per_subtree-4 2.378µ 2.396µ ~ 0.100
BlockSizeScaling/10k_tx_64_per_subtree-4 76.23µ 76.35µ ~ 0.400
BlockSizeScaling/10k_tx_256_per_subtree-4 19.12µ 19.01µ ~ 0.100
BlockSizeScaling/10k_tx_1024_per_subtree-4 4.723µ 4.765µ ~ 1.000
BlockSizeScaling/50k_tx_64_per_subtree-4 393.6µ 400.1µ ~ 0.700
BlockSizeScaling/50k_tx_256_per_subtree-4 93.99µ 95.11µ ~ 0.200
BlockSizeScaling/50k_tx_1024_per_subtree-4 23.50µ 23.69µ ~ 0.100
SubtreeAllocations/small_subtrees_exists_check-4 167.6µ 160.9µ ~ 0.200
SubtreeAllocations/small_subtrees_data_fetch-4 167.1µ 170.4µ ~ 0.700
SubtreeAllocations/small_subtrees_full_validation-4 333.4µ 329.1µ ~ 0.200
SubtreeAllocations/medium_subtrees_exists_check-4 9.491µ 9.399µ ~ 0.200
SubtreeAllocations/medium_subtrees_data_fetch-4 9.906µ 9.965µ ~ 1.000
SubtreeAllocations/medium_subtrees_full_validation-4 19.17µ 19.46µ ~ 0.100
SubtreeAllocations/large_subtrees_exists_check-4 2.270µ 2.259µ ~ 0.500
SubtreeAllocations/large_subtrees_data_fetch-4 2.399µ 2.400µ ~ 0.700
SubtreeAllocations/large_subtrees_full_validation-4 4.820µ 4.854µ ~ 0.100
_BufferPoolAllocation/16KB-4 2.496µ 2.346µ ~ 0.100
_BufferPoolAllocation/32KB-4 4.956µ 5.905µ ~ 0.100
_BufferPoolAllocation/64KB-4 12.15µ 11.90µ ~ 0.700
_BufferPoolAllocation/128KB-4 21.77µ 23.32µ ~ 0.100
_BufferPoolAllocation/512KB-4 90.26µ 81.59µ ~ 0.100
_BufferPoolConcurrent/32KB-4 13.97µ 12.13µ ~ 0.100
_BufferPoolConcurrent/64KB-4 21.40µ 19.50µ ~ 0.100
_BufferPoolConcurrent/512KB-4 109.6µ 106.4µ ~ 0.200
_SubtreeDeserializationWithBufferSizes/16KB-4 506.4µ 491.6µ ~ 0.700
_SubtreeDeserializationWithBufferSizes/32KB-4 507.1µ 479.4µ ~ 0.200
_SubtreeDeserializationWithBufferSizes/64KB-4 518.5µ 490.0µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/128KB-4 514.1µ 484.3µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/512KB-4 510.0µ 483.6µ ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/16KB-4 28.31m 28.71m ~ 0.700
_SubtreeDataDeserializationWithBufferSizes/32KB-4 28.25m 27.50m ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/64KB-4 27.89m 27.47m ~ 0.700
_SubtreeDataDeserializationWithBufferSizes/128KB-4 28.20m 27.59m ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/512KB-4 28.27m 27.61m ~ 0.700
_PooledVsNonPooled/Pooled-4 645.2n 647.8n ~ 0.600
_PooledVsNonPooled/NonPooled-4 4.707µ 4.900µ ~ 1.000
_MemoryFootprint/Current_512KB_32concurrent-4 5.769µ 5.623µ ~ 0.200
_MemoryFootprint/Proposed_32KB_32concurrent-4 7.809µ 7.219µ ~ 0.200
_MemoryFootprint/Alternative_64KB_32concurrent-4 7.419µ 7.187µ ~ 0.200
StoreBlock_Sequential/BelowCSVHeight-4 333.3µ 337.0µ ~ 0.400
StoreBlock_Sequential/AboveCSVHeight-4 343.0µ 334.4µ ~ 0.700
GetUtxoHashes-4 255.9n 255.4n ~ 0.400
GetUtxoHashes_ManyOutputs-4 43.58µ 44.97µ ~ 0.400
_NewMetaDataFromBytes-4 240.3n 240.2n ~ 0.600
_Bytes-4 619.5n 625.6n ~ 0.200
_MetaBytes-4 561.7n 567.6n ~ 0.700

Threshold: >10% with p < 0.05 | Generated: 2026-05-07 09:11 UTC

@sugh01 sugh01 merged commit a06974a into bsv-blockchain:test/adversarial-double-spend-coverage May 7, 2026
25 checks passed
@sugh01 sugh01 deleted the test/adversarial-double-spend-coverage-fix-ds-logic branch May 7, 2026 11:01
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