Skip to content

fix: block assembly reset skips intermediate block finalization and handleReorg returns nil after fallback reset#545

Open
freemans13 wants to merge 6 commits intobsv-blockchain:mainfrom
freemans13:fix/block-assembly-reset-bugs
Open

fix: block assembly reset skips intermediate block finalization and handleReorg returns nil after fallback reset#545
freemans13 wants to merge 6 commits intobsv-blockchain:mainfrom
freemans13:fix/block-assembly-reset-bugs

Conversation

@freemans13
Copy link
Copy Markdown
Collaborator

Summary

Fix two bugs in block assembly's reset logic that cause intermediate blocks to be improperly processed during reorgs/resets:

  • Bug 1: SubtreeProcessor.reset() only called finalizeBlockProcessing() (and thus SetBlockProcessedAt) for the last moveForward block. Intermediate blocks never got processed_at set, meaning they were not recognized as fully processed.
  • Bug 2: handleReorg() returned nil after fallback reset (triggered by invalid block or failed Reorg), allowing processNewBlockAnnouncement to overwrite the reset's setBestBlockHeader with a potentially stale value captured before the reset ran.

Changes

Bug 1 Fix (SubtreeProcessor.go)

  • Non-legacy path: Added finalizeBlockProcessing(ctx, block) inside the per-block moveForward loop so each block gets SetBlockProcessedAt called individually.
  • Legacy sync path: Replaced single currentBlockHeader.Store() + updatePrecomputedMiningData() with per-block finalizeBlockProcessing() loop after concurrent coinbase processing.
  • Changed post-loop conditional to only handle the moveBack-only case (no moveForward blocks).

Bug 2 Fix (BlockAssembler.go)

  • After fallback reset() succeeds in handleReorg(), return errors.NewBlockAssemblyResetError(...) instead of nil. This matches the large-reorg path which already correctly returns ErrBlockAssemblyReset, preventing processNewBlockAnnouncement from overwriting the reset's state.

Tests

  • Unit tests (reset_bug_test.go): Two tests that prove both bugs exist and pass after fixes.
  • Integration tests (blockassembly_system_test.go): Two tests using full blockchain daemon (gRPC, real stores) that exercise the reset and reorg-with-invalid-block paths end-to-end.

Testing

All tests pass with no regressions:

ok  github.com/bsv-blockchain/teranode/services/blockassembly         18.460s
ok  github.com/bsv-blockchain/teranode/services/blockassembly/mining   3.961s
ok  github.com/bsv-blockchain/teranode/services/blockassembly/subtreeprocessor  210.212s

Related issue: https://github.com/orgs/bitcoin-sv/projects/5/views/6?pane=issue&itemId=161301562&issue=bitcoin-sv%7Cteranode%7C4507

…andleReorg returns nil after fallback reset

Fix two bugs in block assembly's reset logic:

Bug 1: SubtreeProcessor.reset() only called finalizeBlockProcessing (and thus
SetBlockProcessedAt) for the LAST moveForward block. Intermediate blocks never
got processed_at set, meaning they were not recognized as fully processed.
Fix: call finalizeBlockProcessing for each moveForward block individually in
both the legacy sync and non-legacy paths.

Bug 2: handleReorg() returned nil after fallback reset (triggered by invalid
block or failed Reorg), allowing processNewBlockAnnouncement to overwrite the
reset's setBestBlockHeader with a potentially stale value captured before the
reset ran. Fix: return ErrBlockAssemblyReset after fallback reset, matching
the large-reorg path which already returns this error.

Includes unit tests proving both bugs and integration tests using full
blockchain daemon with gRPC and real stores.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 2, 2026

🤖 Claude Code Review

Status: Complete


Current Review:

No issues found. All previously reported issues from Copilot and icellan have been addressed.

History:

  • ✅ Fixed: Error handling for NewBlockAssembler, NewNBitFromString, and NewTxFromString calls
  • ✅ Fixed: Variable shadowing (utxoStore renamed to utxo)
  • ✅ Fixed: Test comments updated to describe pre-fix behavior
  • ✅ Fixed: No-op reset case now properly refreshes precomputedMiningData
  • ✅ Fixed: Redundant SubtreeProcessor construction removed
  • ✅ Fixed: Intermediate blocks now use lightweight SetBlockProcessedAt instead of full finalizeBlockProcessing
  • ✅ Fixed: processed_at clearing for moveBackBlocks added

Summary:

The PR correctly fixes two related bugs in block assembly reset logic:

  1. Bug 1 - Intermediate moveForward blocks during reset now get processed_at set individually via lightweight SetBlockProcessedAt calls, with only the final block receiving full finalizeBlockProcessing. This avoids redundant work while ensuring all blocks are properly marked.

  2. Bug 2 - handleReorg now returns ErrBlockAssemblyReset after fallback reset (matching the large-reorg path), preventing processNewBlockAnnouncement from overwriting the reset state.

Both bugs have comprehensive test coverage (unit + integration tests). The fix also properly clears processed_at for moveBackBlocks during reset.

@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud bot commented Mar 2, 2026

@freemans13 freemans13 self-assigned this Apr 9, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes two reset/reorg correctness issues in the block assembly pipeline to ensure blocks are consistently finalized and the block assembler state can’t be overwritten with stale best-header data after a fallback reset.

Changes:

  • Finalize every moveForward block during SubtreeProcessor.reset() (legacy and non-legacy paths) so intermediate blocks get processed_at set.
  • Make handleReorg() return ErrBlockAssemblyReset after a fallback reset() so callers don’t overwrite state with stale headers.
  • Add unit + integration tests covering both bugs end-to-end.

Reviewed changes

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

File Description
services/blockassembly/subtreeprocessor/SubtreeProcessor.go Finalize per moveForward block during reset to ensure SetBlockProcessedAt is applied to intermediate blocks.
services/blockassembly/BlockAssembler.go Return ErrBlockAssemblyReset after fallback reset in handleReorg() to prevent stale best-header overwrites.
services/blockassembly/reset_bug_test.go Adds focused unit tests reproducing both reset/reorg bugs.
services/blockassembly/blockassembly_system_test.go Adds integration tests exercising reset-ahead and reorg-with-invalid-block scenarios through the full stack.

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

Comment on lines +59 to +83
ba, _ := NewBlockAssembler(
t.Context(),
ulogger.TestLogger{},
tSettings,
stats,
items.utxoStore,
nil, // blobStore
items.blockchainClient,
items.newSubtreeChan,
)

require.NotNil(t, ba.settings)

// Create SubtreeProcessor WITH utxoStore (unlike setupBlockAssemblyTest which passes nil).
// This is needed because SubtreeProcessor.reset() calls processCoinbaseUtxos()
// which requires utxoStore.Create().
ba.subtreeProcessor, err = subtreeprocessor.NewSubtreeProcessor(
t.Context(),
ulogger.TestLogger{},
ba.settings,
nil, // blobStore
items.blockchainClient,
items.utxoStore, // NOT nil — this is the key difference
items.newSubtreeChan,
)
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

NewBlockAssembler returns (*BlockAssembler, error) but the error is ignored here. If construction fails (e.g., nil ChainCfgParams), the test will continue with a nil/partial BA and fail later in less obvious ways. Please require.NoError on the returned error (and you can likely avoid constructing a second SubtreeProcessor since NewBlockAssembler already creates one with the provided utxoStore).

Copilot uses AI. Check for mistakes.
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.

Fixed: added require.NoError(t, err) immediately after the NewBlockAssembler call. Also removed the redundant second subtreeprocessor.NewSubtreeProcessor construction — NewBlockAssembler already creates one, so the test now uses that directly after asserting no error.

Comment on lines +44 to +46
utxoStore, err := utxostoresql.New(ctx, logger, tSettings, utxoStoreURL)
require.NoError(t, err)
items.utxoStore = utxoStore
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

The local variable name utxoStore shadows the imported utxoStore package, forcing the var _ utxoStore.Store = nil workaround at the bottom. Rename the local variable (e.g., utxo) and drop the unused import + workaround var so the test stays clean and avoids confusing shadowing.

Suggested change
utxoStore, err := utxostoresql.New(ctx, logger, tSettings, utxoStoreURL)
require.NoError(t, err)
items.utxoStore = utxoStore
utxo, err := utxostoresql.New(ctx, logger, tSettings, utxoStoreURL)
require.NoError(t, err)
items.utxoStore = utxo

Copilot uses AI. Check for mistakes.
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.

Fixed: renamed the local variable to utxo and removed the utxoStore package import alias (and the workaround var _ utxoStore.Store = nil at the bottom). No more shadowing.

Comment on lines +116 to +126
// TestResetWithBlockchainAhead_MissesIntermediateBlockProcessing proves that when
// block assembly performs a reset and blockchain is ahead by N blocks, the intermediate
// moveForward blocks are NOT properly processed.
//
// Specifically, SubtreeProcessor.reset() only calls finalizeBlockProcessing (and thus
// SetBlockProcessedAt) for the LAST moveForward block. Intermediate blocks never get
// processed_at set, meaning they are not recognized as fully processed.
//
// This test FAILS with the current code (proving the bug exists).
// After the fix (don't moveForward during reset), all blocks will be processed
// via the normal moveForwardBlock path, which calls finalizeBlockProcessing per block.
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

These test comments still say the test "FAILS with the current code" and describe a different fix approach ("don't moveForward during reset"). Now that the PR changes behavior, please update the comments to say the test would have failed before the fix and align the explanation with the actual implementation (finalize each moveForward block).

Suggested change
// TestResetWithBlockchainAhead_MissesIntermediateBlockProcessing proves that when
// block assembly performs a reset and blockchain is ahead by N blocks, the intermediate
// moveForward blocks are NOT properly processed.
//
// Specifically, SubtreeProcessor.reset() only calls finalizeBlockProcessing (and thus
// SetBlockProcessedAt) for the LAST moveForward block. Intermediate blocks never get
// processed_at set, meaning they are not recognized as fully processed.
//
// This test FAILS with the current code (proving the bug exists).
// After the fix (don't moveForward during reset), all blocks will be processed
// via the normal moveForwardBlock path, which calls finalizeBlockProcessing per block.
// TestResetWithBlockchainAhead_MissesIntermediateBlockProcessing covers a bug where,
// during reset with blockchain ahead by N blocks, intermediate moveForward blocks were
// not properly finalized.
//
// Before the fix, SubtreeProcessor.reset() only called finalizeBlockProcessing (and
// thus SetBlockProcessedAt) for the LAST moveForward block. Intermediate blocks never
// got processed_at set, meaning they were not recognized as fully processed.
//
// This test would have failed before the fix. The fix ensures reset finalizes each
// moveForward block so every intermediate block is marked processed.

Copilot uses AI. Check for mistakes.
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.

Fixed: updated both test function comments to describe the pre-fix behavior (past tense) and clarify that the tests verify the fix works, rather than saying they prove a current bug.

Comment on lines +825 to +827
chainBits, _ := model.NewNBitFromString("207fffff")

coinbaseTx, _ := bt.NewTxFromString("01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff17030200002f6d312d65752f605f77009f74384816a31807ffffffff03ac505763000000001976a914c362d5af234dd4e1f2a1bfbcab90036d38b0aa9f88acaa505763000000001976a9143c22b6d9ba7b50b6d6e615c69d11ecb2ba3db14588acaa505763000000001976a914b7177c7deb43f3869eabc25cfd9f618215f34d5588ac00000000")
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

In the new integration tests, errors from NewNBitFromString / NewTxFromString are ignored. If the parsing ever fails, the test can proceed with nil/zero values and produce misleading failures. Please capture and require.NoError for these errors (consistent with other test assertions in this file).

Suggested change
chainBits, _ := model.NewNBitFromString("207fffff")
coinbaseTx, _ := bt.NewTxFromString("01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff17030200002f6d312d65752f605f77009f74384816a31807ffffffff03ac505763000000001976a914c362d5af234dd4e1f2a1bfbcab90036d38b0aa9f88acaa505763000000001976a9143c22b6d9ba7b50b6d6e615c69d11ecb2ba3db14588acaa505763000000001976a914b7177c7deb43f3869eabc25cfd9f618215f34d5588ac00000000")
chainBits, err := model.NewNBitFromString("207fffff")
require.NoError(t, err)
coinbaseTx, err := bt.NewTxFromString("01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff17030200002f6d312d65752f605f77009f74384816a31807ffffffff03ac505763000000001976a914c362d5af234dd4e1f2a1bfbcab90036d38b0aa9f88acaa505763000000001976a9143c22b6d9ba7b50b6d6e615c69d11ecb2ba3db14588acaa505763000000001976a914b7177c7deb43f3869eabc25cfd9f618215f34d5588ac00000000")
require.NoError(t, err)

Copilot uses AI. Check for mistakes.
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.

Fixed: both NewNBitFromString and NewTxFromString calls in the two new integration tests now capture the error and assert require.NoError before proceeding.

Comment on lines +1161 to 1164
// persist the current state — only needed when there are NO moveForward blocks
// (moveForward blocks are now finalized individually in the loop above)
if len(moveForwardBlocks) == 0 && len(moveBackBlocks) > 0 {
// we only moved back, finalize with the parent of the last block we moved back
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

After reset() clears chainedSubtrees/currentSubtree, there is no finalization path when BOTH moveForwardBlocks and moveBackBlocks are empty (this conditional only handles moveBack-only). In that no-op reset case, precomputedMiningData is not refreshed and can still reference the old subtrees snapshot even though it was just cleared. Consider explicitly updating mining state in the zero-move case (e.g., call updatePrecomputedMiningData and/or finalize based on the current header).

Copilot uses AI. Check for mistakes.
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.

Fixed: added an else if len(moveForwardBlocks) == 0 && len(moveBackBlocks) == 0 branch. In the zero-move case, chainedSubtrees and currentSubtree are cleared by the reset preamble but precomputedMiningData is never refreshed. The fix stores the target blockHeader into currentBlockHeader and calls updatePrecomputedMiningData() so precomputed data refers to the fresh empty subtrees rather than the old (now-closed) ones.

freemans13 and others added 2 commits April 9, 2026 09:35
- Add require.NoError for NewBlockAssembler return value in test setup
- Rename utxoStore local var to utxo to avoid shadowing the imported package; remove workaround import
- Update test comments to describe pre-fix behavior rather than current bugs
- Add require.NoError for NewNBitFromString/NewTxFromString in integration tests
- Handle zero-move reset case: refresh precomputedMiningData after subtrees are cleared

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 9, 2026

Benchmark Comparison Report

Baseline: main (unknown)

Current: PR-545 (938b20e)

Summary

  • Regressions: 0
  • Improvements: 0
  • Unchanged: 153
  • Significance level: p < 0.05
All benchmark results (sec/op)
Benchmark Baseline Current Change p-value
_NewBlockFromBytes-4 1.386µ 1.405µ ~ 0.100
SplitSyncedParentMap_SetIfNotExists/256_buckets-4 61.61n 61.84n ~ 0.100
SplitSyncedParentMap_SetIfNotExists/16_buckets-4 61.75n 64.03n ~ 0.100
SplitSyncedParentMap_SetIfNotExists/1_bucket-4 61.68n 61.81n ~ 0.400
SplitSyncedParentMap_ConcurrentSetIfNotExists/256_buckets... 30.14n 30.28n ~ 0.700
SplitSyncedParentMap_ConcurrentSetIfNotExists/16_buckets_... 52.28n 53.01n ~ 0.700
SplitSyncedParentMap_ConcurrentSetIfNotExists/1_bucket_pa... 105.4n 106.5n ~ 0.600
MiningCandidate_Stringify_Short-4 271.4n 266.6n ~ 0.100
MiningCandidate_Stringify_Long-4 1.874µ 1.878µ ~ 0.400
MiningSolution_Stringify-4 974.3n 978.6n ~ 0.100
BlockInfo_MarshalJSON-4 1.734µ 1.740µ ~ 0.100
NewFromBytes-4 127.9n 127.9n ~ 1.000
Mine_EasyDifficulty-4 58.43µ 58.43µ ~ 1.000
Mine_WithAddress-4 4.688µ 4.801µ ~ 0.700
BlockAssembler_AddTx-4 0.02578n 0.02709n ~ 0.400
AddNode-4 11.02 11.21 ~ 0.200
AddNodeWithMap-4 10.81 10.90 ~ 1.000
DirectSubtreeAdd/4_per_subtree-4 61.62n 59.21n ~ 0.100
DirectSubtreeAdd/64_per_subtree-4 31.42n 29.34n ~ 0.100
DirectSubtreeAdd/256_per_subtree-4 30.31n 27.35n ~ 0.100
DirectSubtreeAdd/1024_per_subtree-4 29.17n 26.26n ~ 0.100
DirectSubtreeAdd/2048_per_subtree-4 28.77n 26.07n ~ 0.100
SubtreeProcessorAdd/4_per_subtree-4 311.6n 311.7n ~ 0.700
SubtreeProcessorAdd/64_per_subtree-4 308.9n 306.7n ~ 0.700
SubtreeProcessorAdd/256_per_subtree-4 307.7n 310.0n ~ 0.400
SubtreeProcessorAdd/1024_per_subtree-4 309.8n 309.7n ~ 1.000
SubtreeProcessorAdd/2048_per_subtree-4 309.9n 304.6n ~ 0.500
SubtreeProcessorRotate/4_per_subtree-4 312.7n 316.8n ~ 0.100
SubtreeProcessorRotate/64_per_subtree-4 314.6n 314.8n ~ 1.000
SubtreeProcessorRotate/256_per_subtree-4 312.7n 316.2n ~ 0.700
SubtreeProcessorRotate/1024_per_subtree-4 301.5n 308.4n ~ 0.100
SubtreeNodeAddOnly/4_per_subtree-4 66.90n 67.02n ~ 0.400
SubtreeNodeAddOnly/64_per_subtree-4 39.09n 39.15n ~ 0.500
SubtreeNodeAddOnly/256_per_subtree-4 38.06n 38.14n ~ 0.700
SubtreeNodeAddOnly/1024_per_subtree-4 37.24n 37.39n ~ 0.400
SubtreeCreationOnly/4_per_subtree-4 165.0n 163.4n ~ 0.100
SubtreeCreationOnly/64_per_subtree-4 658.3n 646.0n ~ 1.000
SubtreeCreationOnly/256_per_subtree-4 2.121µ 2.032µ ~ 0.700
SubtreeCreationOnly/1024_per_subtree-4 5.671µ 5.242µ ~ 0.400
SubtreeCreationOnly/2048_per_subtree-4 7.751µ 9.041µ ~ 1.000
SubtreeProcessorOverheadBreakdown/64_per_subtree-4 306.8n 308.7n ~ 0.700
SubtreeProcessorOverheadBreakdown/1024_per_subtree-4 310.8n 317.7n ~ 0.100
ParallelGetAndSetIfNotExists/1k_nodes-4 968.5µ 1001.3µ ~ 0.100
ParallelGetAndSetIfNotExists/10k_nodes-4 1.932m 2.005m ~ 0.100
ParallelGetAndSetIfNotExists/50k_nodes-4 8.867m 8.673m ~ 0.700
ParallelGetAndSetIfNotExists/100k_nodes-4 17.60m 17.59m ~ 0.700
SequentialGetAndSetIfNotExists/1k_nodes-4 763.1µ 770.7µ ~ 0.700
SequentialGetAndSetIfNotExists/10k_nodes-4 3.031m 3.000m ~ 1.000
SequentialGetAndSetIfNotExists/50k_nodes-4 10.73m 10.81m ~ 0.700
SequentialGetAndSetIfNotExists/100k_nodes-4 20.57m 20.81m ~ 0.100
ProcessOwnBlockSubtreeNodesParallel/1k_nodes-4 1.033m 1.013m ~ 0.100
ProcessOwnBlockSubtreeNodesParallel/10k_nodes-4 4.809m 4.768m ~ 0.700
ProcessOwnBlockSubtreeNodesParallel/100k_nodes-4 19.25m 19.57m ~ 0.400
ProcessOwnBlockSubtreeNodesSequential/1k_nodes-4 854.5µ 850.6µ ~ 1.000
ProcessOwnBlockSubtreeNodesSequential/10k_nodes-4 6.177m 6.023m ~ 0.200
ProcessOwnBlockSubtreeNodesSequential/100k_nodes-4 39.86m 39.79m ~ 1.000
DiskTxMap_SetIfNotExists-4 3.530µ 3.490µ ~ 1.000
DiskTxMap_SetIfNotExists_Parallel-4 3.355µ 3.437µ ~ 0.700
DiskTxMap_ExistenceOnly-4 300.8n 302.9n ~ 0.200
Queue-4 193.4n 197.4n ~ 0.100
AtomicPointer-4 4.895n 4.442n ~ 0.100
ReorgOptimizations/DedupFilterPipeline/Old/10K-4 832.0µ 863.7µ ~ 0.200
ReorgOptimizations/DedupFilterPipeline/New/10K-4 817.8µ 824.8µ ~ 0.200
ReorgOptimizations/AllMarkFalse/Old/10K-4 106.4µ 107.7µ ~ 0.400
ReorgOptimizations/AllMarkFalse/New/10K-4 61.60µ 61.99µ ~ 0.700
ReorgOptimizations/HashSlicePool/Old/10K-4 69.55µ 61.51µ ~ 0.100
ReorgOptimizations/HashSlicePool/New/10K-4 11.55µ 11.39µ ~ 1.000
ReorgOptimizations/NodeFlags/Old/10K-4 5.210µ 6.176µ ~ 0.100
ReorgOptimizations/NodeFlags/New/10K-4 1.775µ 1.802µ ~ 1.000
ReorgOptimizations/DedupFilterPipeline/Old/100K-4 9.600m 9.858m ~ 1.000
ReorgOptimizations/DedupFilterPipeline/New/100K-4 10.26m 10.08m ~ 0.700
ReorgOptimizations/AllMarkFalse/Old/100K-4 1.127m 1.128m ~ 0.400
ReorgOptimizations/AllMarkFalse/New/100K-4 680.4µ 682.8µ ~ 0.400
ReorgOptimizations/HashSlicePool/Old/100K-4 613.3µ 624.0µ ~ 0.100
ReorgOptimizations/HashSlicePool/New/100K-4 302.5µ 304.3µ ~ 0.400
ReorgOptimizations/NodeFlags/Old/100K-4 54.61µ 55.24µ ~ 0.400
ReorgOptimizations/NodeFlags/New/100K-4 19.09µ 19.39µ ~ 0.400
TxMapSetIfNotExists-4 51.75n 51.13n ~ 1.000
TxMapSetIfNotExistsDuplicate-4 38.42n 38.12n ~ 0.100
ChannelSendReceive-4 579.7n 583.6n ~ 0.100
CalcBlockWork-4 502.4n 501.0n ~ 1.000
CalculateWork-4 677.1n 680.3n ~ 0.200
BuildBlockLocatorString_Helpers/Size_10-4 1.302µ 1.306µ ~ 0.700
BuildBlockLocatorString_Helpers/Size_100-4 16.15µ 14.54µ ~ 0.700
BuildBlockLocatorString_Helpers/Size_1000-4 123.6µ 124.7µ ~ 0.700
CatchupWithHeaderCache-4 104.4m 104.3m ~ 0.400
_prepareTxsPerLevel-4 447.9m 492.8m ~ 0.400
_prepareTxsPerLevelOrdered-4 4.509m 4.559m ~ 1.000
_prepareTxsPerLevel_Comparison/Original-4 486.9m 462.8m ~ 0.400
_prepareTxsPerLevel_Comparison/Optimized-4 4.235m 4.661m ~ 0.100
SubtreeProcessor/100_tx_64_per_subtree-4 1.091 1.090 ~ 0.700
SubtreeProcessor/500_tx_64_per_subtree-4 5.483 5.484 ~ 0.700
SubtreeProcessor/500_tx_256_per_subtree-4 5.481 5.478 ~ 0.700
SubtreeProcessor/1k_tx_64_per_subtree-4 10.98 10.97 ~ 0.700
SubtreeProcessor/1k_tx_256_per_subtree-4 10.96 10.98 ~ 0.100
StreamingProcessorPhases/FilterValidated/100_tx-4 513.9µ 521.6µ ~ 0.700
StreamingProcessorPhases/ClassifyProcess/100_tx-4 10.79m 10.79m ~ 1.000
StreamingProcessorPhases/FilterValidated/500_tx-4 4.407m 4.484m ~ 0.400
StreamingProcessorPhases/ClassifyProcess/500_tx-4 11.11m 11.04m ~ 0.400
StreamingProcessorPhases/FilterValidated/1k_tx-4 8.716m 8.819m ~ 0.100
StreamingProcessorPhases/ClassifyProcess/1k_tx-4 11.84m 11.84m ~ 0.700
SubtreeSizes/10k_tx_4_per_subtree-4 1.523m 1.487m ~ 1.000
SubtreeSizes/10k_tx_16_per_subtree-4 362.8µ 367.4µ ~ 0.100
SubtreeSizes/10k_tx_64_per_subtree-4 88.62µ 88.66µ ~ 1.000
SubtreeSizes/10k_tx_256_per_subtree-4 21.57µ 21.57µ ~ 0.700
SubtreeSizes/10k_tx_512_per_subtree-4 10.49µ 10.84µ ~ 0.400
SubtreeSizes/10k_tx_1024_per_subtree-4 5.237µ 5.279µ ~ 0.400
SubtreeSizes/10k_tx_2k_per_subtree-4 2.601µ 2.609µ ~ 0.700
BlockSizeScaling/10k_tx_64_per_subtree-4 82.90µ 82.73µ ~ 1.000
BlockSizeScaling/10k_tx_256_per_subtree-4 20.47µ 20.41µ ~ 0.700
BlockSizeScaling/10k_tx_1024_per_subtree-4 5.041µ 5.069µ ~ 1.000
BlockSizeScaling/50k_tx_64_per_subtree-4 409.4µ 417.0µ ~ 0.200
BlockSizeScaling/50k_tx_256_per_subtree-4 97.50µ 98.47µ ~ 0.400
BlockSizeScaling/50k_tx_1024_per_subtree-4 23.93µ 24.20µ ~ 0.700
SubtreeAllocations/small_subtrees_exists_check-4 160.9µ 162.1µ ~ 0.400
SubtreeAllocations/small_subtrees_data_fetch-4 168.0µ 166.9µ ~ 0.400
SubtreeAllocations/small_subtrees_full_validation-4 327.6µ 327.3µ ~ 0.700
SubtreeAllocations/medium_subtrees_exists_check-4 9.494µ 9.364µ ~ 0.400
SubtreeAllocations/medium_subtrees_data_fetch-4 9.725µ 9.833µ ~ 0.100
SubtreeAllocations/medium_subtrees_full_validation-4 19.07µ 19.46µ ~ 0.700
SubtreeAllocations/large_subtrees_exists_check-4 2.240µ 2.232µ ~ 1.000
SubtreeAllocations/large_subtrees_data_fetch-4 2.337µ 2.340µ ~ 1.000
SubtreeAllocations/large_subtrees_full_validation-4 4.722µ 4.731µ ~ 0.700
_BufferPoolAllocation/16KB-4 3.415µ 3.842µ ~ 0.100
_BufferPoolAllocation/32KB-4 7.186µ 9.076µ ~ 0.700
_BufferPoolAllocation/64KB-4 16.47µ 17.68µ ~ 0.400
_BufferPoolAllocation/128KB-4 27.88µ 31.68µ ~ 0.200
_BufferPoolAllocation/512KB-4 116.4µ 116.1µ ~ 1.000
_BufferPoolConcurrent/32KB-4 18.71µ 18.84µ ~ 0.700
_BufferPoolConcurrent/64KB-4 30.89µ 30.69µ ~ 1.000
_BufferPoolConcurrent/512KB-4 150.3µ 152.3µ ~ 0.700
_SubtreeDeserializationWithBufferSizes/16KB-4 647.2µ 648.7µ ~ 0.700
_SubtreeDeserializationWithBufferSizes/32KB-4 648.2µ 635.9µ ~ 0.400
_SubtreeDeserializationWithBufferSizes/64KB-4 654.8µ 644.3µ ~ 0.700
_SubtreeDeserializationWithBufferSizes/128KB-4 650.5µ 624.1µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/512KB-4 639.1µ 640.9µ ~ 1.000
_SubtreeDataDeserializationWithBufferSizes/16KB-4 36.64m 35.78m ~ 0.200
_SubtreeDataDeserializationWithBufferSizes/32KB-4 36.23m 35.67m ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/64KB-4 36.16m 35.90m ~ 0.200
_SubtreeDataDeserializationWithBufferSizes/128KB-4 36.52m 35.87m ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/512KB-4 35.94m 35.97m ~ 1.000
_PooledVsNonPooled/Pooled-4 833.3n 834.1n ~ 1.000
_PooledVsNonPooled/NonPooled-4 6.809µ 6.871µ ~ 0.400
_MemoryFootprint/Current_512KB_32concurrent-4 7.611µ 7.824µ ~ 0.100
_MemoryFootprint/Proposed_32KB_32concurrent-4 10.84µ 10.81µ ~ 0.700
_MemoryFootprint/Alternative_64KB_32concurrent-4 11.24µ 10.99µ ~ 0.100
StoreBlock_Sequential/BelowCSVHeight-4 305.5µ 315.9µ ~ 0.700
StoreBlock_Sequential/AboveCSVHeight-4 306.4µ 304.7µ ~ 0.400
GetUtxoHashes-4 273.6n 271.6n ~ 0.400
GetUtxoHashes_ManyOutputs-4 46.38µ 50.21µ ~ 0.100
_NewMetaDataFromBytes-4 233.3n 230.4n ~ 0.100
_Bytes-4 618.6n 622.8n ~ 0.700
_MetaBytes-4 569.8n 570.9n ~ 1.000

Threshold: >10% with p < 0.05 | Generated: 2026-04-10 05:58 UTC

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 4 out of 4 changed files in this pull request and generated 3 comments.


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

Comment on lines +70 to +84

// Create SubtreeProcessor WITH utxoStore (unlike setupBlockAssemblyTest which passes nil).
// This is needed because SubtreeProcessor.reset() calls processCoinbaseUtxos()
// which requires utxoStore.Create().
ba.subtreeProcessor, err = subtreeprocessor.NewSubtreeProcessor(
t.Context(),
ulogger.TestLogger{},
ba.settings,
nil, // blobStore
items.blockchainClient,
items.utxoStore, // NOT nil — this is the key difference
items.newSubtreeChan,
)
require.NoError(t, err)

Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

setupBlockAssemblyTestWithUtxoStore() recreates and overwrites ba.subtreeProcessor even though NewBlockAssembler() already constructs a SubtreeProcessor using the provided utxoStore. This makes the test diverge from production wiring and leaves the originally-constructed processor unreferenced (potentially leaking resources/state). Prefer using ba.subtreeProcessor created by NewBlockAssembler (and start/stop that one), removing the second subtreeprocessor.NewSubtreeProcessor() call.

Suggested change
// Create SubtreeProcessor WITH utxoStore (unlike setupBlockAssemblyTest which passes nil).
// This is needed because SubtreeProcessor.reset() calls processCoinbaseUtxos()
// which requires utxoStore.Create().
ba.subtreeProcessor, err = subtreeprocessor.NewSubtreeProcessor(
t.Context(),
ulogger.TestLogger{},
ba.settings,
nil, // blobStore
items.blockchainClient,
items.utxoStore, // NOT nil — this is the key difference
items.newSubtreeChan,
)
require.NoError(t, err)
require.NotNil(t, ba.subtreeProcessor)

Copilot uses AI. Check for mistakes.
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.

Fixed: removed the second subtreeprocessor.NewSubtreeProcessor() call. NewBlockAssembler already passes the utxoStore through to its internal SubtreeProcessor (line 203 of BlockAssembler.go), so the test now uses that directly. The setup now just calls NewBlockAssembler with the utxoStore and asserts the subtreeProcessor is non-nil.

Comment on lines +1154 to +1158
// Finalize each moveForward block sequentially to ensure SetBlockProcessedAt
// is called for all blocks, not just the last one.
for _, block := range moveForwardBlocks {
stp.finalizeBlockProcessing(ctx, block)
}
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

In the legacy-sync reset path, calling finalizeBlockProcessing() for every moveForward block does more than SetBlockProcessedAt (it also adjusts dynamic subtree sizing and snapshots mining data). During large catch-up resets this can add unnecessary work and can mutate sizing state based on stale/irrelevant stats. Consider marking processed_at for intermediate blocks directly (e.g., a lightweight helper) and running full finalizeBlockProcessing only once for the final block (or add a flag to skip sizing/mining-data updates in reset fast-forward loops).

Copilot uses AI. Check for mistakes.
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.

Fixed: in the legacy-sync path, intermediate blocks now use a direct blockchainClient.SetBlockProcessedAt call instead of the full finalizeBlockProcessing. Only the final moveForward block goes through finalizeBlockProcessing, which is the one that should trigger adjustSubtreeSize and updatePrecomputedMiningData with meaningful stats.

Comment on lines 1195 to 1199
// Finalize each moveForward block individually to ensure SetBlockProcessedAt
// is called for all blocks, not just the last one. Without this, intermediate
// moveForward blocks would never get processed_at set.
stp.finalizeBlockProcessing(ctx, block)
}
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

Similarly in the non-legacy reset moveForward loop, finalizeBlockProcessing() is invoked per block, which repeatedly runs adjustSubtreeSize() and updatePrecomputedMiningData(). If the goal is just to ensure processed_at is set for each moveForward block, consider separating that concern from full finalization to avoid repeated sizing/mining-data updates and keep reset behavior closer to normal block advancement semantics.

Copilot uses AI. Check for mistakes.
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.

Fixed: same change applied to the non-legacy path. Intermediate blocks get a lightweight SetBlockProcessedAt call; full finalizeBlockProcessing runs only for the last block in the loop.

- Remove redundant SubtreeProcessor construction in test setup — NewBlockAssembler already passes utxoStore through to its internal SubtreeProcessor
- Use lightweight SetBlockProcessedAt for intermediate reset moveForward blocks instead of full finalizeBlockProcessing, avoiding repeated adjustSubtreeSize and updatePrecomputedMiningData on stale stats; only the final block gets full finalization

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 4 out of 4 changed files in this pull request and generated no new comments.


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

// direct SetBlockProcessedAt call to avoid running adjustSubtreeSize and
// updatePrecomputedMiningData on stale stats repeatedly during fast-forward.
// Only the final block gets full finalizeBlockProcessing.
for i, block := range moveForwardBlocks {
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.

Not missing unsetting processed_at when moveBackBlocks?

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: added a concurrent errgroup after the moveBackBlocks loop that calls SetBlockProcessedAt(ctx, block.Header.Hash(), true) for all moveBack blocks in parallel. This clears their processed_at timestamps since they're being "un-processed" during the reset. Uses the same non-critical error handling pattern as the moveForward path (log warning, don't fail the reset).

// direct SetBlockProcessedAt call to avoid running adjustSubtreeSize and
// updatePrecomputedMiningData on stale stats repeatedly during fast-forward.
// Only the final block gets full finalizeBlockProcessing.
for i, block := range moveForwardBlocks {
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.

Not missing unsetting processed_at when moveBackBlocks?

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.

Same fix covers both paths — the moveBack clearing happens in the shared section before the legacy/non-legacy branch, so it applies to both.

When reset() moves blocks back, their processed_at timestamps must be
cleared — symmetric with the moveForward path that sets them. Uses a
concurrent errgroup for maximum throughput.

Addresses review feedback from @icellan on PR bsv-blockchain#545.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

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

See analysis details on SonarQube Cloud

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