Summary
BlockAssembler.setBestBlockHeader treats any tip-jump of ≥ 2 blocks as a reorg, runs through handleReorg / subtreeProcessor.Reset(moveBack, moveForward, ...), and logs "best block header is not the same as the previous best block header, reorging". In practice the vast majority of these "reorgs" are not chain reorgs at all — they are forward-only catch-ups (moveBack = 0) where BA simply fell behind the tip while processing a heavy block.
Code
services/blockassembly/BlockAssembler.go:705-727:
switch {
case bestBlockchainBlockHeader.Hash().IsEqual(bestBlockAccordingToBlockAssembly.Hash()):
// no-op (gap 0)
return
case !bestBlockchainBlockHeader.HashPrevBlock.IsEqual(bestBlockAccordingToBlockAssembly.Hash()):
// logs "reorging", calls handleReorg(...)
ctxLogger.Infof("[BlockAssembler][%s] best block header is not the same as the previous best block header, reorging: %s", ...)
b.setCurrentRunningState(StateReorging)
err = b.handleReorg(ctx, bestBlockchainBlockHeader, bestBlockchainBlockHeaderMeta.Height)
...
default:
// "moving up" — single-block forward (gap 1)
}
The middle branch fires whenever the new tip's HashPrevBlock != BA.head, which is anything from gap of 2 to gap of (CoinbaseMaturity - 1) blocks. At gap ≥ CoinbaseMaturity (100) BA does a full hard reset; below that it Resets via subtreeProcessor.Reset(moveBack=0, moveForward=N, ...) which loads N forward blocks' tx maps simultaneously.
Reproduction (production, today)
bsva-ovh-teranode-ttn-eu-4, mainnet legacy sync. Container CPU-capped to 1 core (separate quickstart issue) made it worse, but the path itself is independent.
Timeline:
12:38:10 BA: moveForwardBlock height 1605444 (txCount=192951, size=37.6 MB)
12:45:01 BA: DONE in 6m51s (single block, processing subtrees into transaction map)
12:45:01 BA: tip now 1605463 — 19 blocks ahead. Logs "reorging".
12:45:15 BA: handleReorg moveBackBlocks=0, moveForwardBlocks=19 — DONE in 388 ms
Go memstats during the heavy block: HeapSys ≈ 10.8 GB, HeapReleased ≈ 10.2 GB. The Reset path loads all forward block tx maps at once.
Pattern: a single big block stalls BA → legacy keeps ingesting → tip drifts → BA logs "reorging" with moveBack=0.
Why this matters
- Misleading log line. Operators reading "reorging" assume a chain split. The actual condition is "BA is behind the tip by ≥ 2." A grep for "reorg" in logs no longer answers "did we reorg?".
- Wrong memory shape. The Reset path loads N tx maps simultaneously to support a moveBack rollback. For
moveBack=0 there is nothing to roll back — iterating moveForwardBlock per block would have the same effect with bounded memory.
- Common case during sync. With 192k-tx blocks taking minutes to process, gap-of-2 is routine during legacy sync, not exceptional.
History
Not a regression — current behaviour was introduced in 91dec7f80 (2023-08-01). Before that, gap-of-2 just logged an error and continue'd (also wrong, just differently). The new handleReorg call replaced the error log; the assumption that "any gap = reorg" survived.
Proposed change
Split the middle branch by moveBack count, not by gap size:
case !bestBlockchainBlockHeader.HashPrevBlock.IsEqual(BA.head):
moveBack, moveForward := computeMoves(...)
if len(moveBack) == 0 {
// pure catch-up — iterate forward, no Reset
for _, blk := range moveForward {
stp.moveForwardBlock(...)
}
} else {
// real reorg with rollback — existing Reset path
b.handleReorg(ctx, ...)
}
Benefits:
- Log says "catching up" vs "reorging" honestly.
- No N-block-wide tx-map allocation when moveBack=0.
- Real reorgs still take the existing Reset path.
Related
Summary
BlockAssembler.setBestBlockHeadertreats any tip-jump of ≥ 2 blocks as a reorg, runs throughhandleReorg/subtreeProcessor.Reset(moveBack, moveForward, ...), and logs"best block header is not the same as the previous best block header, reorging". In practice the vast majority of these "reorgs" are not chain reorgs at all — they are forward-only catch-ups (moveBack = 0) where BA simply fell behind the tip while processing a heavy block.Code
services/blockassembly/BlockAssembler.go:705-727:The middle branch fires whenever the new tip's
HashPrevBlock != BA.head, which is anything from gap of 2 to gap of (CoinbaseMaturity - 1) blocks. At gap ≥ CoinbaseMaturity (100) BA does a full hard reset; below that it Resets viasubtreeProcessor.Reset(moveBack=0, moveForward=N, ...)which loads N forward blocks' tx maps simultaneously.Reproduction (production, today)
bsva-ovh-teranode-ttn-eu-4, mainnet legacy sync. Container CPU-capped to 1 core (separate quickstart issue) made it worse, but the path itself is independent.Timeline:
Go memstats during the heavy block:
HeapSys ≈ 10.8 GB,HeapReleased ≈ 10.2 GB. The Reset path loads all forward block tx maps at once.Pattern: a single big block stalls BA → legacy keeps ingesting → tip drifts → BA logs "reorging" with
moveBack=0.Why this matters
moveBack=0there is nothing to roll back — iteratingmoveForwardBlockper block would have the same effect with bounded memory.History
Not a regression — current behaviour was introduced in
91dec7f80(2023-08-01). Before that, gap-of-2 just logged an error andcontinue'd (also wrong, just differently). The newhandleReorgcall replaced the error log; the assumption that "any gap = reorg" survived.Proposed change
Split the middle branch by
moveBackcount, not by gap size:Benefits:
Related
bsv-blockchain/teranode-quickstart#3).BatchPreviousOutputsDecoratefan-out fix (perf(aerospike): fan out BatchPreviousOutputsDecorate per-tx #893) that addressed the upstream of this — legacy was previously a much smaller part of the latency budget.