Skip to content

fix(subtreevalidation): consult FileTypeSubtree before HTTP fallback#863

Merged
freemans13 merged 8 commits into
bsv-blockchain:mainfrom
freemans13:stu/subtreevalidation-find-local-subtree
May 22, 2026
Merged

fix(subtreevalidation): consult FileTypeSubtree before HTTP fallback#863
freemans13 merged 8 commits into
bsv-blockchain:mainfrom
freemans13:stu/subtreevalidation-find-local-subtree

Conversation

@freemans13

@freemans13 freemans13 commented May 14, 2026

Copy link
Copy Markdown
Collaborator

Summary

CheckBlockSubtrees and three sibling code paths only checked for FileTypeSubtreeToCheck (the "downloaded from peer, pending validation" marker) before deciding the subtree was missing locally and falling back to an HTTP fetch. On the legacy catch-up / quickValidationMode path the subtree has already been validated inline and is written as FileTypeSubtree (the "already validated" marker), not FileTypeSubtreeToCheck.

The HTTP fallback then tries to fetch from a synthetic baseURL="legacy" whose URL legacy/subtree/<hash> has no scheme — the request fails outright, even though the file is right there in the store. Outside the legacy path the same gap caused needless network round-trips (or spurious not-found errors) any time a node already had a validated subtree on disk.

Fix

Add a findLocalSubtreeFile helper that consults both file types and reports which one (if any) holds the subtree. The same logic is added in two places — one per service — kept package-local to avoid a cross-service import:

  • services/subtreevalidation/findLocalSubtreeFile (method on Server)
  • services/blockvalidation/findLocalSubtreeFile (package function, new file find_local_subtree.go)

Callers are switched to use it and to read from whichever file type was found:

File Function Site
services/subtreevalidation/check_block_subtrees.go CheckBlockSubtrees originally reported site
services/subtreevalidation/SubtreeValidation.go getSubtreeTxHashes local-bytes fast path before peer fetch
services/blockvalidation/get_blocks.go fetchAndStoreSubtree catch-up subtree load
services/blockvalidation/quick_validate.go validateSubtrees / readSubtree quick-validate retry path

In quick_validate.go the existing follow-up Exists(FileTypeSubtree) is skipped when the lookup already returned FileTypeSubtree, saving a store round-trip.

Errors from the local-store check are now classified as StorageError rather than ProcessingError, matching the rest of the read paths.

Tests

  • services/subtreevalidation: TestFindLocalSubtreeFile (FileTypeSubtreeToCheck present, FileTypeSubtree only (quickValidationMode/legacy), neither present); plus targeted tests in check_block_subtrees_test.go and invalid_subtree_test.go.
  • services/blockvalidation: TestFindLocalSubtreeFile mirror suite, plus a get_blocks_test.go case covering the catch-up fast path under both file types.

go test -race -short ./services/subtreevalidation/ ./services/blockvalidation/ — green. go vet clean.

Context

Originally bundled in #723; pulled out as a focused PR after #723's core change (broadening quickValidationMode to CATCHINGBLOCKS) was superseded by #847. The scope was widened from "just CheckBlockSubtrees" to all four sites once gh and code-review surfaced the same anti-pattern in getSubtreeTxHashes, fetchAndStoreSubtree, and the quick-validate retry path.

Note on CI

The current red test job is a flake in TestSyncManager_extendFromTxMap_NilParentTx, a test added in #862 that lives in services/legacy/netsync/ — a package this PR does not touch. The fixture builds a txmap.NewSyncedMap with limit=2, inserts 2 entries, then Sets on an existing key; go-tx-map evicts a random item on update once the limit is hit, so ~50% of runs the child is dropped and the SUT can't find it. Tracked separately; not in scope here.

CheckBlockSubtrees only checked for FileTypeSubtreeToCheck (the
"downloaded from peer, pending validation" marker) before deciding the
subtree was missing locally and falling back to an HTTP fetch. On the
legacy catch-up / quickValidationMode path the subtree is already
validated inline and is written as FileTypeSubtree (the "validated"
marker), not FileTypeSubtreeToCheck.

The HTTP fallback then tries to fetch from a synthetic baseURL="legacy"
whose URL "legacy/subtree/<hash>" has no scheme — the request fails
outright, even though the file is right there in the store.

New helper findLocalSubtreeFile consults both file types and reports
which one (if any) holds the subtree. CheckBlockSubtrees uses its
return value to read from the right file type and skip the HTTP
fallback when local data is present.

Tests:
- TestFindLocalSubtreeFile/FileTypeSubtreeToCheck present
- TestFindLocalSubtreeFile/FileTypeSubtree only (quickValidationMode/legacy)
- TestFindLocalSubtreeFile/neither present

Originally bundled in bsv-blockchain#723; pulled out as a focused PR after bsv-blockchain#723's
core change (CATCHINGBLOCKS quick-validation gating) was superseded
by bsv-blockchain#847.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented May 14, 2026

Copy link
Copy Markdown
Contributor

🤖 Claude Code Review

Status: Complete


Current Review:

This PR introduces a well-designed fix for a subtle bug where CheckBlockSubtrees and related functions only looked for FileTypeSubtreeToCheck when checking for local subtrees, missing FileTypeSubtree files written by legacy catch-up paths. This caused unnecessary HTTP fallback attempts that would fail outright for baseURL="legacy".

Code Quality: The implementation is clean, properly tested, and includes appropriate error handling. The author has addressed all feedback from previous review rounds:

  • Error propagation fixed (NewStorageError used consistently)
  • Test coverage includes both file types and edge cases
  • httpmock defensive guards added to prevent test timeouts
  • Error message formatting corrected

No issues found. The PR follows AGENTS.md guidelines with minimal changes, comprehensive tests, and clear documentation. Ready for merge pending human approval.

@github-actions

github-actions Bot commented May 14, 2026

Copy link
Copy Markdown
Contributor

Benchmark Comparison Report

Baseline: main (unknown)

Current: PR-863 (c03c339)

Summary

  • Regressions: 0
  • Improvements: 0
  • Unchanged: 144
  • Significance level: p < 0.05
All benchmark results (sec/op)
Benchmark Baseline Current Change p-value
_NewBlockFromBytes-4 1.682µ 1.691µ ~ 0.400
SplitSyncedParentMap_SetIfNotExists/256_buckets-4 61.50n 61.41n ~ 0.700
SplitSyncedParentMap_SetIfNotExists/16_buckets-4 61.48n 61.70n ~ 0.100
SplitSyncedParentMap_SetIfNotExists/1_bucket-4 61.50n 61.51n ~ 1.000
SplitSyncedParentMap_ConcurrentSetIfNotExists/256_buckets... 29.58n 29.61n ~ 0.700
SplitSyncedParentMap_ConcurrentSetIfNotExists/16_buckets_... 50.37n 50.78n ~ 1.000
SplitSyncedParentMap_ConcurrentSetIfNotExists/1_bucket_pa... 105.5n 105.2n ~ 0.700
MiningCandidate_Stringify_Short-4 260.9n 261.8n ~ 0.400
MiningCandidate_Stringify_Long-4 1.899µ 1.894µ ~ 0.100
MiningSolution_Stringify-4 958.8n 959.3n ~ 1.000
BlockInfo_MarshalJSON-4 1.795µ 1.787µ ~ 0.400
NewFromBytes-4 143.1n 144.5n ~ 1.000
AddTxBatchColumnar_Validation-4 2.384µ 2.369µ ~ 1.000
OffsetValidationLoop-4 718.7n 718.6n ~ 0.800
Mine_EasyDifficulty-4 60.29µ 60.39µ ~ 0.700
Mine_WithAddress-4 6.834µ 6.903µ ~ 0.100
BlockAssembler_AddTx-4 0.02672n 0.02647n ~ 1.000
AddNode-4 10.88 11.13 ~ 0.400
AddNodeWithMap-4 11.90 11.78 ~ 0.400
DirectSubtreeAdd/4_per_subtree-4 59.69n 59.57n ~ 1.000
DirectSubtreeAdd/64_per_subtree-4 28.85n 29.39n ~ 0.400
DirectSubtreeAdd/256_per_subtree-4 27.27n 28.51n ~ 0.100
DirectSubtreeAdd/1024_per_subtree-4 26.22n 26.77n ~ 0.100
DirectSubtreeAdd/2048_per_subtree-4 25.86n 26.45n ~ 0.100
SubtreeProcessorAdd/4_per_subtree-4 285.1n 374.1n ~ 0.100
SubtreeProcessorAdd/64_per_subtree-4 280.9n 422.0n ~ 0.100
SubtreeProcessorAdd/256_per_subtree-4 283.0n 412.4n ~ 0.100
SubtreeProcessorAdd/1024_per_subtree-4 273.9n 391.6n ~ 0.100
SubtreeProcessorAdd/2048_per_subtree-4 276.1n 373.0n ~ 0.100
SubtreeProcessorRotate/4_per_subtree-4 279.0n 361.3n ~ 0.100
SubtreeProcessorRotate/64_per_subtree-4 277.3n 346.4n ~ 0.100
SubtreeProcessorRotate/256_per_subtree-4 278.0n 420.8n ~ 0.100
SubtreeProcessorRotate/1024_per_subtree-4 277.7n 418.5n ~ 0.100
SubtreeNodeAddOnly/4_per_subtree-4 55.48n 56.13n ~ 0.100
SubtreeNodeAddOnly/64_per_subtree-4 34.76n 34.94n ~ 0.200
SubtreeNodeAddOnly/256_per_subtree-4 33.68n 34.00n ~ 0.100
SubtreeNodeAddOnly/1024_per_subtree-4 32.95n 33.04n ~ 0.100
SubtreeCreationOnly/4_per_subtree-4 117.8n 119.4n ~ 0.100
SubtreeCreationOnly/64_per_subtree-4 421.6n 454.7n ~ 0.100
SubtreeCreationOnly/256_per_subtree-4 1.407µ 1.492µ ~ 0.100
SubtreeCreationOnly/1024_per_subtree-4 4.610µ 4.770µ ~ 0.100
SubtreeCreationOnly/2048_per_subtree-4 8.707µ 8.919µ ~ 0.200
SubtreeProcessorOverheadBreakdown/64_per_subtree-4 277.6n 374.8n ~ 0.100
SubtreeProcessorOverheadBreakdown/1024_per_subtree-4 276.1n 396.7n ~ 0.100
ParallelGetAndSetIfNotExists/1k_nodes-4 2.242m 2.319m ~ 0.100
ParallelGetAndSetIfNotExists/10k_nodes-4 5.511m 5.561m ~ 0.100
ParallelGetAndSetIfNotExists/50k_nodes-4 7.973m 8.650m ~ 0.100
ParallelGetAndSetIfNotExists/100k_nodes-4 10.84m 12.10m ~ 0.100
SequentialGetAndSetIfNotExists/1k_nodes-4 1.967m 2.091m ~ 0.100
SequentialGetAndSetIfNotExists/10k_nodes-4 4.564m 5.946m ~ 0.100
SequentialGetAndSetIfNotExists/50k_nodes-4 12.84m 21.44m ~ 0.100
SequentialGetAndSetIfNotExists/100k_nodes-4 23.54m 33.64m ~ 0.100
ProcessOwnBlockSubtreeNodesParallel/1k_nodes-4 2.322m 2.392m ~ 0.100
ProcessOwnBlockSubtreeNodesParallel/10k_nodes-4 8.312m 8.355m ~ 0.200
ProcessOwnBlockSubtreeNodesParallel/100k_nodes-4 14.08m 15.21m ~ 0.100
ProcessOwnBlockSubtreeNodesSequential/1k_nodes-4 1.996m 2.061m ~ 0.200
ProcessOwnBlockSubtreeNodesSequential/10k_nodes-4 9.102m 8.704m ~ 0.100
ProcessOwnBlockSubtreeNodesSequential/100k_nodes-4 44.77m 47.52m ~ 0.700
DiskTxMap_SetIfNotExists-4 4.013µ 4.145µ ~ 0.400
DiskTxMap_SetIfNotExists_Parallel-4 3.624µ 3.656µ ~ 1.000
DiskTxMap_ExistenceOnly-4 317.4n 316.6n ~ 1.000
Queue-4 197.6n 196.0n ~ 0.500
AtomicPointer-4 8.131n 8.130n ~ 0.500
ReorgOptimizations/DedupFilterPipeline/Old/10K-4 752.7µ 751.1µ ~ 0.400
ReorgOptimizations/DedupFilterPipeline/New/10K-4 696.9µ 698.9µ ~ 0.700
ReorgOptimizations/AllMarkFalse/Old/10K-4 106.0µ 105.4µ ~ 0.700
ReorgOptimizations/AllMarkFalse/New/10K-4 58.49µ 58.47µ ~ 1.000
ReorgOptimizations/HashSlicePool/Old/10K-4 55.84µ 55.29µ ~ 0.700
ReorgOptimizations/HashSlicePool/New/10K-4 11.80µ 11.82µ ~ 0.200
ReorgOptimizations/NodeFlags/Old/10K-4 4.812µ 4.873µ ~ 0.400
ReorgOptimizations/NodeFlags/New/10K-4 1.651µ 1.643µ ~ 0.800
ReorgOptimizations/DedupFilterPipeline/Old/100K-4 10.673m 9.678m ~ 0.100
ReorgOptimizations/DedupFilterPipeline/New/100K-4 10.85m 10.67m ~ 0.100
ReorgOptimizations/AllMarkFalse/Old/100K-4 1.135m 1.122m ~ 0.100
ReorgOptimizations/AllMarkFalse/New/100K-4 733.0µ 745.3µ ~ 0.700
ReorgOptimizations/HashSlicePool/Old/100K-4 598.7µ 549.7µ ~ 0.100
ReorgOptimizations/HashSlicePool/New/100K-4 355.7µ 355.3µ ~ 0.400
ReorgOptimizations/NodeFlags/Old/100K-4 47.50µ 50.09µ ~ 0.100
ReorgOptimizations/NodeFlags/New/100K-4 15.83µ 17.49µ ~ 0.100
TxMapSetIfNotExists-4 53.05n 52.63n ~ 1.000
TxMapSetIfNotExistsDuplicate-4 48.13n 48.19n ~ 1.000
ChannelSendReceive-4 658.0n 680.7n ~ 0.100
CalcBlockWork-4 541.9n 543.4n ~ 1.000
CalculateWork-4 696.1n 702.4n ~ 0.700
BuildBlockLocatorString_Helpers/Size_10-4 1.761µ 1.408µ ~ 0.100
BuildBlockLocatorString_Helpers/Size_100-4 13.22µ 13.48µ ~ 0.100
BuildBlockLocatorString_Helpers/Size_1000-4 131.0µ 132.7µ ~ 0.200
CatchupWithHeaderCache-4 104.2m 104.6m ~ 0.100
_BufferPoolAllocation/16KB-4 3.739µ 3.803µ ~ 0.100
_BufferPoolAllocation/32KB-4 9.047µ 7.698µ ~ 0.700
_BufferPoolAllocation/64KB-4 16.96µ 21.38µ ~ 0.700
_BufferPoolAllocation/128KB-4 26.58µ 31.11µ ~ 0.200
_BufferPoolAllocation/512KB-4 102.4µ 103.6µ ~ 0.400
_BufferPoolConcurrent/32KB-4 18.16µ 18.59µ ~ 0.400
_BufferPoolConcurrent/64KB-4 29.41µ 29.69µ ~ 1.000
_BufferPoolConcurrent/512KB-4 144.8µ 151.7µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/16KB-4 612.5µ 626.0µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/32KB-4 605.9µ 625.7µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/64KB-4 601.6µ 616.8µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/128KB-4 588.0µ 601.5µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/512KB-4 585.6µ 592.1µ ~ 0.700
_SubtreeDataDeserializationWithBufferSizes/16KB-4 36.78m 35.86m ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/32KB-4 36.39m 35.73m ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/64KB-4 36.20m 36.09m ~ 1.000
_SubtreeDataDeserializationWithBufferSizes/128KB-4 36.32m 35.69m ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/512KB-4 36.57m 35.79m ~ 0.100
_PooledVsNonPooled/Pooled-4 740.7n 740.3n ~ 1.000
_PooledVsNonPooled/NonPooled-4 7.631µ 8.425µ ~ 0.100
_MemoryFootprint/Current_512KB_32concurrent-4 6.531µ 6.619µ ~ 0.100
_MemoryFootprint/Proposed_32KB_32concurrent-4 9.608µ 10.421µ ~ 0.100
_MemoryFootprint/Alternative_64KB_32concurrent-4 9.063µ 10.645µ ~ 0.100
SubtreeSizes/10k_tx_4_per_subtree-4 1.379m 1.379m ~ 1.000
SubtreeSizes/10k_tx_16_per_subtree-4 321.2µ 323.1µ ~ 0.400
SubtreeSizes/10k_tx_64_per_subtree-4 76.35µ 76.22µ ~ 1.000
SubtreeSizes/10k_tx_256_per_subtree-4 18.87µ 18.90µ ~ 0.700
SubtreeSizes/10k_tx_512_per_subtree-4 9.416µ 9.416µ ~ 0.800
SubtreeSizes/10k_tx_1024_per_subtree-4 4.642µ 4.665µ ~ 0.400
SubtreeSizes/10k_tx_2k_per_subtree-4 2.309µ 2.310µ ~ 0.600
BlockSizeScaling/10k_tx_64_per_subtree-4 73.56µ 76.25µ ~ 0.100
BlockSizeScaling/10k_tx_256_per_subtree-4 18.69µ 18.64µ ~ 1.000
BlockSizeScaling/10k_tx_1024_per_subtree-4 4.602µ 4.661µ ~ 0.100
BlockSizeScaling/50k_tx_64_per_subtree-4 397.0µ 386.2µ ~ 0.700
BlockSizeScaling/50k_tx_256_per_subtree-4 91.48µ 93.26µ ~ 0.200
BlockSizeScaling/50k_tx_1024_per_subtree-4 23.05µ 22.89µ ~ 0.200
SubtreeAllocations/small_subtrees_exists_check-4 159.3µ 157.2µ ~ 0.700
SubtreeAllocations/small_subtrees_data_fetch-4 160.4µ 165.8µ ~ 0.700
SubtreeAllocations/small_subtrees_full_validation-4 319.4µ 321.3µ ~ 1.000
SubtreeAllocations/medium_subtrees_exists_check-4 9.210µ 9.203µ ~ 0.700
SubtreeAllocations/medium_subtrees_data_fetch-4 9.479µ 9.559µ ~ 1.000
SubtreeAllocations/medium_subtrees_full_validation-4 18.64µ 19.00µ ~ 0.100
SubtreeAllocations/large_subtrees_exists_check-4 2.209µ 2.205µ ~ 1.000
SubtreeAllocations/large_subtrees_data_fetch-4 2.309µ 2.325µ ~ 0.100
SubtreeAllocations/large_subtrees_full_validation-4 4.737µ 4.712µ ~ 1.000
_prepareTxsPerLevel-4 327.7m 306.6m ~ 0.100
_prepareTxsPerLevelOrdered-4 2.810m 2.809m ~ 1.000
_prepareTxsPerLevel_Comparison/Original-4 311.3m 315.7m ~ 0.200
_prepareTxsPerLevel_Comparison/Optimized-4 2.871m 3.031m ~ 0.200
StoreBlock_Sequential/BelowCSVHeight-4 329.2µ 332.6µ ~ 0.100
StoreBlock_Sequential/AboveCSVHeight-4 334.1µ 334.3µ ~ 0.700
GetUtxoHashes-4 255.7n 258.5n ~ 0.200
GetUtxoHashes_ManyOutputs-4 42.07µ 42.37µ ~ 0.200
_NewMetaDataFromBytes-4 228.7n 229.8n ~ 0.100
_Bytes-4 401.0n 399.8n ~ 0.700
_MetaBytes-4 139.8n 138.2n ~ 0.400

Threshold: >10% with p < 0.05 | Generated: 2026-05-21 13:16 UTC

@freemans13 freemans13 self-assigned this May 14, 2026
@freemans13 freemans13 requested a review from Copilot May 19, 2026 12:42

Copilot AI 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.

Pull request overview

This PR fixes CheckBlockSubtrees incorrectly falling back to HTTP subtree downloads when the subtree is already present locally under FileTypeSubtree (the “already validated” marker used by legacy catch-up / quick validation), by checking both relevant file types before attempting the HTTP fetch.

Changes:

  • Add findLocalSubtreeFile helper to detect whether a subtree exists locally under FileTypeSubtreeToCheck or FileTypeSubtree.
  • Update CheckBlockSubtrees to read the subtree from whichever local file type exists, avoiding the HTTP fallback when local data is present.
  • Add unit tests covering all findLocalSubtreeFile presence/absence combinations.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
services/subtreevalidation/check_block_subtrees.go Consult both subtree file markers before HTTP fallback; read subtree using the discovered local file type.
services/subtreevalidation/check_block_subtrees_test.go Add targeted unit tests for findLocalSubtreeFile to prevent regressions (including legacy/quick-validation scenario).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

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

Approve. findLocalSubtreeFile cleanly extracts the both-types check before HTTP fallback. Content-addressed store = no stale-data risk. Both-present priority (FileTypeSubtreeToCheck wins) matches normal p2p ordering.

Nits:

  • New tests use assert.Equal — repo convention (testing.md) is require. Lines 2668, 2681, 2691 of check_block_subtrees_test.go.
  • Missing test for both file types simultaneously present — documents the priority.

Same root cause as bsv-blockchain#863: callers that look up a subtree by a single file
type miss the case where a sibling code path has already promoted it to
FileTypeSubtree. They then needlessly re-fetch from the peer (or, on the
legacy catch-up path where baseURL="legacy" has no scheme, fail outright).

Apply the dual-check pattern in three more places:

- get_blocks.go:fetchAndStoreSubtree — catch-up path
- quick_validate.go:readSubtree — retry path that already documents
  "for retry scenarios" in its FileTypeSubtree existence check
- SubtreeValidation.go:getSubtreeTxHashes — uses existing
  findLocalSubtreeFile helper

The blockvalidation package gets a sibling helper (free function over
blob.Store) to avoid a cross-service import. Regression tests cover the
FileTypeSubtree-only path for each call site.
u.logger.Debugf("[getSubtreeTxHashes][%s] found subtreeToCheck file in store, using it instead of network request", subtreeHash.String())
// Use the local subtree file (under either FileTypeSubtreeToCheck or
// FileTypeSubtree) instead of a network request — see findLocalSubtreeFile.
if localFileType, localExists, _ := u.findLocalSubtreeFile(spanCtx, *subtreeHash); localExists {

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] Error silently discarded in inline condition

The inline if statement on line 966 discards the error returned by findLocalSubtreeFile:

if localFileType, localExists, _ := u.findLocalSubtreeFile(spanCtx, *subtreeHash); localExists {

If findLocalSubtreeFile returns an error (storage access failure), the code silently proceeds to attempt an HTTP fetch rather than reporting the storage error to the caller.

Suggested fix:

localFileType, localExists, err := u.findLocalSubtreeFile(spanCtx, *subtreeHash)
if err != nil {
    return nil, errors.NewStorageError("[getSubtreeTxHashes][%s] failed to check local store", subtreeHash.String(), err)
}
if localExists {
    // existing logic...
}

This ensures storage errors are surfaced rather than masked by a fallback fetch.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in the next push. findLocalSubtreeFile errors are now surfaced as a StorageError from getSubtreeTxHashes instead of silently falling through to the HTTP path (which would be especially misleading for baseURL="legacy").

Copilot AI 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.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Comment on lines +966 to +968
if localFileType, localExists, _ := u.findLocalSubtreeFile(spanCtx, *subtreeHash); localExists {
localBytes, getErr := u.subtreeStore.Get(spanCtx, subtreeHash[:], localFileType)
if getErr == nil && localBytes != nil {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — fixed. getSubtreeTxHashes now propagates errors from findLocalSubtreeFile as a StorageError instead of silently falling through to HTTP (which would be especially misleading for baseURL="legacy").

Comment on lines +471 to +474
// No HTTP responder registered; if the implementation falls through
// to the network this call must fail.
stat := gocore.NewStat("test")
hashes, err := server.getSubtreeTxHashes(context.Background(), stat, &subtreeHash, testPeerURL)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good defensive suggestion — applied. The test now activates httpmock against util.HTTPClient() with no responders (so any stray HTTP fallback fails fast instead of waiting on the default 60s timeout) and wraps the call in a 5s context timeout as a second line of defence.

…reeTxHashes

Don't discard the error from findLocalSubtreeFile when the subtree store
returns a real failure — surface it as a StorageError instead of silently
falling through to the HTTP path. The fallback would be especially
misleading for baseURL="legacy" (synthetic scheme-less URL).

Also harden TestGetSubtreeTxHashes_LocalFile: activate httpmock against
util.HTTPClient() with no responders and wrap the call in a 5s context
timeout so any future regression that falls back to HTTP fails fast
rather than hanging on the default 60s client timeout.

Copilot AI 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.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 5 comments.

Comment on lines +970 to +973
if localExists {
localBytes, getErr := u.subtreeStore.Get(spanCtx, subtreeHash[:], localFileType)
if getErr == nil && localBytes != nil {
u.logger.Debugf("[getSubtreeTxHashes][%s] found local subtree file in store (%s), using it instead of network request", subtreeHash.String(), localFileType.String())

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point — same class of bug as the previous round. getErr is now propagated as a StorageError instead of being swallowed, and a nil-byte response despite Exists=true is treated as a storage error rather than silently falling through to HTTP.

// request fails outright.
localFileType, localExists, err := u.findLocalSubtreeFile(gCtx, subtreeHash)
if err != nil {
return errors.NewProcessingError("[CheckBlockSubtrees][%s] failed to check if subtree exists in store", subtreeHash.String(), err)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — switched to NewStorageError to match ValidateSubtreeInternal's convention for the same "failed to check if subtree exists in store" condition.

Comment on lines +964 to +967
// Use the local subtree file (under either FileTypeSubtreeToCheck or
// FileTypeSubtree) instead of a network request — see findLocalSubtreeFile.
localFileType, localExists, err := u.findLocalSubtreeFile(spanCtx, *subtreeHash)
if err != nil {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reasonable observation about the extra round-trip on remote backends, but declining for this PR — refactoring findLocalSubtreeFile to return a reader/bytes (rather than just the located file type) touches four call sites with different consumption patterns (one wants bytes, two want GetIoReader, one wants both file type + later Get). That's beyond the scope of this fix and risks lifecycle/close-handling regressions. Happy to do it in a follow-up PR focused on the helper signature.

Comment on lines 722 to +732
func (u *BlockValidation) readSubtree(ctx context.Context, block *model.Block, subtreeIdx int, subtreeHash *chainhash.Hash) subtreeResult {
// get the subtree from disk, should be in .subtreeToCheck
subtreeReader, err := u.subtreeStore.GetIoReader(ctx, subtreeHash[:], fileformat.FileTypeSubtreeToCheck)
// On retry the subtree may already be promoted to FileTypeSubtree (the
// "already validated" marker) and FileTypeSubtreeToCheck cleaned up, so
// consult both file types — see findLocalSubtreeFile.
localFileType, localExists, err := findLocalSubtreeFile(ctx, u.subtreeStore, *subtreeHash)
if err != nil {
return subtreeResult{err: errors.NewStorageError("[getBlockTransactions][%s] failed to locate subtree %s", block.Hash().String(), subtreeHash.String(), err)}
}
if !localExists {
return subtreeResult{err: errors.NewNotFoundError("[getBlockTransactions][%s] subtree %s not found locally", block.Hash().String(), subtreeHash.String())}
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Declining — the [getBlockTransactions] prefix is the pre-existing convention throughout this whole function (lines 753, 761, 767, 777, 785, 790 all use it). The two lines this PR added were intentionally matching the surrounding style. Renaming all of them is a separate cleanup that belongs in its own PR per the small-diff rule.

Comment thread services/blockvalidation/get_blocks.go Outdated
// See findLocalSubtreeFile for why both must be consulted.
localFileType, localExists, err := findLocalSubtreeFile(ctx, u.subtreeStore, *subtreeHash)
if err != nil {
return nil, errors.NewProcessingError("[catchup:fetchAndStoreSubtree] Error checking subtree existence for %s: %v", subtreeHash.String(), err)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — confirmed by reading errors.New: when the last param is an error it gets popped out as the wrapped error, leaving the format string with one too few args (so %v becomes %!v(MISSING)). Fixed: removed the trailing %v from the format string and changed to NewStorageError (which is the right code for a store Exists failure).

- subtreevalidation/SubtreeValidation.go (getSubtreeTxHashes):
  propagate the subtreeStore.Get error (and treat a nil-bytes result
  despite Exists=true) as StorageError, instead of silently falling
  through to the HTTP path on store failure.

- subtreevalidation/check_block_subtrees.go (CheckBlockSubtrees):
  classify the findLocalSubtreeFile failure as StorageError to match
  ValidateSubtreeInternal's convention for the same condition.

- blockvalidation/get_blocks.go (fetchAndStoreSubtree):
  the previous format string had "%s: %v" with two trailing params,
  but the last (err) is consumed by errors.New as the wrapped error,
  leaving %v unfilled (%!v(MISSING)). Drop the %v and reclassify as
  StorageError.
@sonarqubecloud

Copy link
Copy Markdown

@freemans13 freemans13 merged commit a642dd4 into bsv-blockchain:main May 22, 2026
27 of 28 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