Skip to content

feat(adaptivefetch): subtreeData fetch gate, armed on FSM RUNNING#745

Merged
freemans13 merged 54 commits into
bsv-blockchain:mainfrom
freemans13:stu/adaptive-subtreedata-fetch
Jun 8, 2026
Merged

feat(adaptivefetch): subtreeData fetch gate, armed on FSM RUNNING#745
freemans13 merged 54 commits into
bsv-blockchain:mainfrom
freemans13:stu/adaptive-subtreedata-fetch

Conversation

@freemans13

@freemans13 freemans13 commented Apr 24, 2026

Copy link
Copy Markdown
Collaborator

Summary

Replaces the always-fetch behaviour for block subtreeData with an adaptive two-mode state machine:

  • Pessimistic (default) — always fetch subtreeData. Safe for every deployment.
  • Optimistic — skip subtreeData; txs are expected to already be in the local UTXO store via propagation. Activated when a rolling window shows > 99% local hit rate.

State transitions are driven entirely by counts observed during normal work — no FSM state, no wall-clock time inside the state machine. Deliberately avoids the class of bug that caused PR #598 to be reverted via #647. A regression test (TestNoWallClockOrFSMDependency) source-scans pkg/adaptivefetch for forbidden imports ("time", blockchain_api, FSMStateType, time.Now, time.Since) and fails the build if anything reintroduces clock / FSM gating.

Replaces:

Cold-start safety: armed on first FSM RUNNING

The gate is pinned pessimistic until the node first reaches FSM RUNNING (fully synced), regardless of the configured bootstrap mode. This is the safety primitive that makes the feature usable: a cold-start IBD, or a restart while well behind the tip, must never skip subtreeData at exactly the moment txs are least likely to be local.

  • pkg/adaptivefetch.State always starts pessimistic. A one-way, idempotent Arm() latch applies the configured bootstrap mode and clears the pre-arm window (so the fake-perfect synthetic samples gathered during IBD can't instantly satisfy the Pess→Opt threshold).
  • Each service trips Arm() the first time the blockchain FSM reaches RUNNING, via WaitForFSMtoTransitionToGivenState. The latch never re-locks — a later catch-up burst (CATCHINGBLOCKS) after the node has been RUNNING may still use optimistic mode, because the node has already proven it is synced.
  • The state machine stays FSM-free. Arm() is a generic "you may now apply bootstrap" trigger; the FSM knowledge lives entirely in the service layer (which already holds the blockchain client). The no-FSM / no-wall-clock invariant — and the regression test enforcing it — therefore still hold.

Architecture

Each of blockvalidation and subtreevalidation constructs its own *adaptivefetch.State at server init and arms it on first RUNNING. Independent per-service instances — no cross-service RPC, no shared state.

  • blockvalidation.blockWorker consults the state before calling fetchSubtreeDataForBlock in the catch-up path (the only place blockvalidation's gate is reachable; live blocks bypass it).
  • subtreevalidation.CheckBlockSubtrees consults the state before probing FileTypeSubtreeData / falling back to HTTP — the gate that governs steady-state / live traffic.

Bootstrap hint: AdaptiveFetch.BootstrapMode setting (pessimistic default; optimistic or auto take effect only once the node has first reached RUNNING).

Intellectual debt acknowledged up-front

The MVP emits synthetic LocalHits=TotalTxs, MissingFetches=0 observations (centralised now in (*State).RecordSyntheticWarmup), so:

  • The Pess→Opt transition is a WindowSize warm-up gate (post-arm), not a real hit-rate measurement.
  • The windowed Opt→Pess auto-recovery can't fire (the single-observation OptToPessMissThreshold trip still can), because MissingFetches is hardcoded 0. A TODO on RecordSyntheticWarmup marks the single place to plumb the real recovered-tx count (len(missingTxHashesCompacted)).

This is safe: a wrong optimistic guess only costs bandwidth — downstream validation still recovers any genuinely-missing txs, so correctness is never at risk. The arm-on-RUNNING latch removes the one dangerous case (going optimistic during cold-start IBD).

Metrics

Two metrics under teranode_adaptive_fetch_* (the hit-rate / missing-fetches series were intentionally not exported while the underlying counts are synthetic — they would publish a permanently-perfect, meaningless series; add them when real counts are plumbed):

  • mode{service} — gauge (0=pess, 1=opt)
  • mode_transitions_total{service,from,to} — counter

Test plan

  • pkg/adaptivefetch — unit tests incl. transitions truth table, bootstrap modes, Arm latch (starts-pessimistic-regardless, defers-until-armed, clears-window-no-instant-flip, idempotent, nil-safe, race-clean), config-validation matrix, ring-buffer wraparound, TestNoWallClockOrFSMDependency guard — 51 tests, -race clean
  • services/subtreevalidation — full package green, incl. new TestCheckBlockSubtrees_Optimistic_SkipsFetchSubtreeData (runs both modes; pessimistic control fetches, optimistic skips)
  • services/blockvalidation — full package green; Test_Start* -race clean (arming goroutine)
  • make lint (affected) — clean · go build ./... — clean
  • Deploy to dev-scale-1/2 with adaptive_fetch_bootstrap_mode=optimistic; verify teranode_adaptive_fetch_mode{service} shows pessimistic during sync and optimistic only after RUNNING, with no transition storms.

🤖 Generated with Claude Code

freemans13 and others added 23 commits April 23, 2026 15:10
Introduces pkg/adaptivefetch with Mode, Config, Observation, State types
and a New constructor. State starts pessimistic, Record appends to a
ring-buffer window but does not yet transition modes. Transitions are
implemented in follow-up commits.

Spec: docs/superpowers/specs/2026-04-23-adaptive-subtreedata-fetch-design.md
Explicit assertions for BootstrapMode=Optimistic and BootstrapMode=Auto
so the resolve-auto-to-pessimistic behaviour is pinned.
Covers every validator branch. If a future edit weakens a check, the
matching sub-test fails with a clear name.
Evaluates the rolling-window average hit rate on each Record and
switches to optimistic when it clears PessToOptHitRateThreshold.
Transition only evaluates once the window is full so early observations
can't flip the mode.
A single observation with MissingFetches > OptToPessMissThreshold flips
the mode back to pessimistic without waiting for the window to fill.
This bounds the recovery cost — the node never attempts thousands of
per-tx fetches before realising it should have downloaded subtreeData.
Covers the case where no single block breaches OptToPessMissThreshold
but the window-wide average of MissingFetches exceeds
OptToPessAvgMissThreshold.
Pins the requirement that State is safe for concurrent use — the
integrations call Record from multiple goroutines per block.
Fails the build if someone re-introduces wall-clock or FSM-state gating
into adaptivefetch. The whole point of this package is to avoid the
class of bug that caused PR bsv-blockchain#598 to be reverted via bsv-blockchain#647.
…nputs

- Pin Mode enum values explicitly (ModePessimistic=0, Optimistic=1, Auto=2).
  Previously iota-based; the metrics gauge treats 0/1 as load-bearing so
  reordering the const block could silently remap persisted values.

- Validate Observation inputs at Record entry. TotalTxs<=0, negative
  LocalHits, LocalHits>TotalTxs, or negative MissingFetches are silently
  dropped. Prevents a corrupted observation from skewing the rolling
  average or triggering spurious transitions.

- Simplify avgHitRateLocked — the TotalTxs==0 guard is now unreachable
  since such observations never enter the window.

Adds TestRecord_IgnoresInvalidObservations and TestRecord_RingBufferWraparound
to pin both behaviours.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Four collectors under teranode_adaptive_fetch_*: mode gauge, hit-rate
histogram, missing-fetches counter, transitions counter. Registered
against a caller-supplied Registerer so tests can isolate.

New's signature gains a prometheus.Registerer parameter. Callers (tests
today; service constructors in follow-up commits) pass
prometheus.DefaultRegisterer in production and prometheus.NewRegistry
in tests.
The initial Observe(0) was to make registry.Gather() return the
histogram before any real observations arrived, but that poisoned
the histogram with a fake 0% hit-rate data point for eternity.
Remove the prefill; keep the gauge Set(0) and counter Add(0) since
those are genuine no-ops on the underlying metric.

Adjust TestMetrics_RegisteredNamesMatchSpec to seed one real
observation before Gather so the test still asserts the histogram
is registered.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Shared struct for the adaptive fetch knobs consumed by both
blockvalidation and subtreevalidation. Defaults match the values
documented in the design spec.
Centralises the string-to-Mode conversion so both service wirings don't
need to duplicate the switch.
Field is unused until the gate is wired into blockWorker in the next
commit. Kept as two separate commits so the wiring change has a
single-purpose diff.

Bootstrap mode parsing and Config validation errors fall back to
hardcoded-defaults rather than failing Server construction —
adaptive-fetch is an optimisation gate, not a correctness gate.
blockWorker now consults Server.adaptiveFetch before calling
fetchSubtreeDataForBlock. In optimistic mode the call is skipped and
contributingPeers is returned as nil; the optimistic-mode miss recovery
happens at the subtree-validation layer (Task 18) which maintains its
own adaptive-fetch state.

Post-work we record a 'perfect' observation so the pessimistic-mode
rolling hit rate can drive Pess→Opt transitions. If the node has no tx
distributor, the first optimistic block will trip Opt→Pess via
subtreevalidation's miss counts.

Introduces Server.fetchSubtreeDataForBlockFn test seam so integration
tests can count fetch calls without a full block validation stack.
Asserts that optimistic mode does NOT invoke fetchSubtreeDataForBlock
while pessimistic mode does. Uses the fetchSubtreeDataForBlockFn seam
introduced in the previous commit.
Field is unused until CheckBlockSubtrees is gated in the next commit.
Errors in config parsing fall back to hardcoded defaults — adaptive-fetch
is an optimisation, not a correctness gate.

Also fixes pkg/adaptivefetch/metrics.go to use registerOrReuse instead
of MustRegister so that calling New() against prometheus.DefaultRegisterer
multiple times (e.g. in subtests that each create a Server via New())
does not panic on duplicate collector registration.

Test helpers in check_block_subtrees_test.go and
check_block_subtrees_large_test.go updated with a pessimistic-mode
adaptiveFetch instance so nil-pointer risks are eliminated.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Per-subtree worker consults Server.adaptiveFetch before the
FileTypeSubtreeData Exists probe (and the HTTP fallback that follows it
when the file is absent). In optimistic mode the probe and fetch are
skipped entirely — the subtree's txs are expected to already be in the
local UTXO store via propagation, and downstream
processTransactionsInLevels naturally handles that by processing 0
transactions from the empty subtreeTxs[subtreeIdx] slice.

Post-work we record an observation so the state machine can transition.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Drives an adaptivefetch.State through pessimistic → optimistic → pessimistic
with synthetic observations, pinning the default config's behaviour at
the layer where blockWorker uses it.
…ckFn in tests

Three defensive fixes found during the Task-20 test sweep:

1. adaptivefetch.State: Mode/ShouldSkipSubtreeData/Record are now nil-safe
   (nil receiver falls back to pessimistic behaviour — always fetch subtreeData).

2. get_blocks.go blockWorker: use fetchSubtreeDataForBlockFn with nil-fallback
   to fetchSubtreeDataForBlock directly, so test Servers constructed without
   the function pointer still work.

3. Server_test.go / catchup_test.go: wire adaptiveFetch + fetchSubtreeDataForBlockFn
   on the two hand-built Server structs in TestServer_catchup and
   setupTestCatchupServer / setupTestCatchupServerWithConfig, providing the
   idiomatic full setup even though the nil-safety above would have covered them.
- Add adaptivefetch.DefaultConfig() so the 6-line config literal lives in
  one place. Use it in both Server fallback branches and in every test
  fixture that wanted the canonical defaults.

- Replace the "perfect-observation" comments in both services with an
  honest explanation: the pessimistic-mode observation is synthetic, the
  Pess→Opt transition is effectively a WindowSize-block warm-up gate,
  and the Opt→Pess safety currently relies on node-restart rather than
  self-correction. Future improvement is to plumb real miss counts
  through the validation path.

Feedback from final code review. Code logic unchanged; defaults and
semantics both preserved.

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

github-actions Bot commented Apr 24, 2026

Copy link
Copy Markdown
Contributor

🤖 Claude Code Review

Status: Complete


Summary

This PR implements an adaptive subtreeData fetching mechanism using a two-mode state machine (pessimistic/optimistic) that learns from transaction locality patterns. The implementation is well-architected with strong safety guarantees and comprehensive test coverage.

Key Strengths

  • Cold-start safety: The arming mechanism ensures nodes stay pessimistic during IBD, only enabling optimistic mode after first reaching RUNNING state
  • Race-condition handling: RecordIfMode pattern correctly handles concurrent workers sampling mode at different times
  • Comprehensive testing: 51 tests in pkg/adaptivefetch including regression guards, race detection, and boundary conditions
  • Clear documentation: Extensive inline comments explain the synthetic observation limitations and future work

Code Quality

The implementation follows project conventions well:

  • Proper error handling and validation
  • Nil-safe receiver methods with fallback behavior
  • Concurrent-safe design with appropriate locking
  • Integration points in blockvalidation and subtreevalidation are clean and well-commented

Areas Noted

Acknowledged limitations (documented in PR description and code):

  • Current observations are synthetic (LocalHits=TotalTxs, MissingFetches=0)
  • Pess→Opt transition is effectively a warm-up timer rather than true hit-rate measurement
  • Automatic Opt→Pess recovery won't fire until real miss counts are plumbed
  • These are intentional MVP trade-offs with clear TODOs and safe fallback behavior

No blocking issues found. The synthetic observations are explicitly acknowledged as intellectual debt with a clear path forward (TODO at state.go:205-209). The safety argument is sound: wrong optimistic guesses only cost bandwidth, not correctness.


History:

  • All inline comments from previous review rounds have been addressed
  • Broken doc references (docs/superpowers/specs/) were fixed in round 3
  • Test error handling strengthened in round 2
  • Threshold boundary conditions corrected in round 2

@github-actions

github-actions Bot commented Apr 24, 2026

Copy link
Copy Markdown
Contributor

Benchmark Comparison Report

Baseline: main (unknown)

Current: PR-745 (bc4823a)

Summary

  • Regressions: 0
  • Improvements: 0
  • Unchanged: 148
  • Significance level: p < 0.05
All benchmark results (sec/op)
Benchmark Baseline Current Change p-value
_NewBlockFromBytes-4 1.783µ 1.935µ ~ 0.100
SplitSyncedParentMap_SetIfNotExists/256_buckets-4 61.78n 61.81n ~ 0.500
SplitSyncedParentMap_SetIfNotExists/16_buckets-4 61.60n 61.68n ~ 0.200
SplitSyncedParentMap_SetIfNotExists/1_bucket-4 61.74n 61.76n ~ 0.700
SplitSyncedParentMap_ConcurrentSetIfNotExists/256_buckets... 31.34n 32.17n ~ 0.400
SplitSyncedParentMap_ConcurrentSetIfNotExists/16_buckets_... 54.47n 53.35n ~ 0.700
SplitSyncedParentMap_ConcurrentSetIfNotExists/1_bucket_pa... 118.6n 115.9n ~ 0.700
MiningCandidate_Stringify_Short-4 261.2n 266.7n ~ 0.100
MiningCandidate_Stringify_Long-4 1.918µ 1.935µ ~ 0.100
MiningSolution_Stringify-4 975.4n 1006.0n ~ 0.100
BlockInfo_MarshalJSON-4 1.838µ 1.822µ ~ 0.100
NewFromBytes-4 128.7n 129.2n ~ 1.000
AddTxBatchColumnar_Validation-4 2.499µ 2.491µ ~ 1.000
OffsetValidationLoop-4 642.0n 635.7n ~ 0.700
Mine_EasyDifficulty-4 60.73µ 60.91µ ~ 0.700
Mine_WithAddress-4 6.905µ 7.523µ ~ 0.700
BlockAssembler_AddTx-4 0.02718n 0.02596n ~ 1.000
AddNode-4 10.99 10.95 ~ 1.000
AddNodeWithMap-4 11.01 11.85 ~ 0.400
DirectSubtreeAdd/4_per_subtree-4 57.07n 55.85n ~ 0.700
DirectSubtreeAdd/64_per_subtree-4 29.13n 28.91n ~ 0.400
DirectSubtreeAdd/256_per_subtree-4 27.86n 27.84n ~ 0.600
DirectSubtreeAdd/1024_per_subtree-4 26.94n 26.54n ~ 0.400
DirectSubtreeAdd/2048_per_subtree-4 26.11n 26.05n ~ 0.400
SubtreeProcessorAdd/4_per_subtree-4 289.3n 292.0n ~ 0.700
SubtreeProcessorAdd/64_per_subtree-4 284.2n 296.5n ~ 0.700
SubtreeProcessorAdd/256_per_subtree-4 287.4n 289.5n ~ 1.000
SubtreeProcessorAdd/1024_per_subtree-4 278.3n 288.5n ~ 0.700
SubtreeProcessorAdd/2048_per_subtree-4 277.8n 294.2n ~ 0.100
SubtreeProcessorRotate/4_per_subtree-4 285.8n 283.9n ~ 0.400
SubtreeProcessorRotate/64_per_subtree-4 285.2n 278.8n ~ 0.400
SubtreeProcessorRotate/256_per_subtree-4 283.1n 288.3n ~ 0.700
SubtreeProcessorRotate/1024_per_subtree-4 284.4n 286.6n ~ 0.700
SubtreeNodeAddOnly/4_per_subtree-4 55.21n 55.00n ~ 0.200
SubtreeNodeAddOnly/64_per_subtree-4 36.02n 36.05n ~ 1.000
SubtreeNodeAddOnly/256_per_subtree-4 35.05n 35.09n ~ 0.700
SubtreeNodeAddOnly/1024_per_subtree-4 34.34n 34.38n ~ 1.000
SubtreeCreationOnly/4_per_subtree-4 110.2n 111.3n ~ 0.700
SubtreeCreationOnly/64_per_subtree-4 352.1n 354.4n ~ 0.400
SubtreeCreationOnly/256_per_subtree-4 1.239µ 1.246µ ~ 0.500
SubtreeCreationOnly/1024_per_subtree-4 3.809µ 3.796µ ~ 0.400
SubtreeCreationOnly/2048_per_subtree-4 6.799µ 6.921µ ~ 0.700
SubtreeProcessorOverheadBreakdown/64_per_subtree-4 285.5n 285.7n ~ 0.700
SubtreeProcessorOverheadBreakdown/1024_per_subtree-4 281.7n 280.9n ~ 1.000
ParallelGetAndSetIfNotExists/1k_nodes-4 2.050m 2.012m ~ 0.100
ParallelGetAndSetIfNotExists/10k_nodes-4 5.254m 5.395m ~ 0.100
ParallelGetAndSetIfNotExists/50k_nodes-4 7.286m 7.491m ~ 0.100
ParallelGetAndSetIfNotExists/100k_nodes-4 9.766m 10.487m ~ 0.100
SequentialGetAndSetIfNotExists/1k_nodes-4 1.819m 1.768m ~ 0.200
SequentialGetAndSetIfNotExists/10k_nodes-4 4.424m 4.500m ~ 0.200
SequentialGetAndSetIfNotExists/50k_nodes-4 13.60m 13.77m ~ 0.700
SequentialGetAndSetIfNotExists/100k_nodes-4 25.39m 26.35m ~ 0.100
ProcessOwnBlockSubtreeNodesParallel/1k_nodes-4 2.104m 2.061m ~ 0.200
ProcessOwnBlockSubtreeNodesParallel/10k_nodes-4 8.635m 8.519m ~ 0.700
ProcessOwnBlockSubtreeNodesParallel/100k_nodes-4 14.22m 13.35m ~ 0.100
ProcessOwnBlockSubtreeNodesSequential/1k_nodes-4 1.854m 1.835m ~ 0.400
ProcessOwnBlockSubtreeNodesSequential/10k_nodes-4 8.220m 8.134m ~ 0.700
ProcessOwnBlockSubtreeNodesSequential/100k_nodes-4 45.19m 45.05m ~ 1.000
DiskTxMap_SetIfNotExists-4 3.569µ 3.628µ ~ 0.400
DiskTxMap_SetIfNotExists_Parallel-4 3.378µ 3.251µ ~ 0.700
DiskTxMap_ExistenceOnly-4 344.8n 329.5n ~ 0.100
Queue-4 149.6n 152.3n ~ 0.100
AtomicPointer-4 2.505n 2.827n ~ 0.100
ReorgOptimizations/DedupFilterPipeline/Old/10K-4 692.8µ 689.9µ ~ 0.700
ReorgOptimizations/DedupFilterPipeline/New/10K-4 617.3µ 637.8µ ~ 0.400
ReorgOptimizations/AllMarkFalse/Old/10K-4 98.43µ 96.74µ ~ 0.700
ReorgOptimizations/AllMarkFalse/New/10K-4 50.11µ 50.44µ ~ 0.700
ReorgOptimizations/HashSlicePool/Old/10K-4 50.95µ 44.38µ ~ 0.700
ReorgOptimizations/HashSlicePool/New/10K-4 8.790µ 8.675µ ~ 0.700
ReorgOptimizations/NodeFlags/Old/10K-4 3.412µ 3.480µ ~ 0.700
ReorgOptimizations/NodeFlags/New/10K-4 1.161µ 1.190µ ~ 0.400
ReorgOptimizations/DedupFilterPipeline/Old/100K-4 8.014m 7.987m ~ 1.000
ReorgOptimizations/DedupFilterPipeline/New/100K-4 8.914m 8.546m ~ 0.100
ReorgOptimizations/AllMarkFalse/Old/100K-4 932.1µ 952.1µ ~ 0.200
ReorgOptimizations/AllMarkFalse/New/100K-4 545.8µ 547.1µ ~ 0.700
ReorgOptimizations/HashSlicePool/Old/100K-4 472.9µ 423.1µ ~ 0.100
ReorgOptimizations/HashSlicePool/New/100K-4 196.0µ 202.1µ ~ 0.400
ReorgOptimizations/NodeFlags/Old/100K-4 38.04µ 35.55µ ~ 0.100
ReorgOptimizations/NodeFlags/New/100K-4 13.25µ 11.74µ ~ 0.100
TxMapSetIfNotExists-4 38.50n 38.69n ~ 1.000
TxMapSetIfNotExistsDuplicate-4 32.15n 32.13n ~ 1.000
ChannelSendReceive-4 461.8n 477.3n ~ 0.100
CalcBlockWork-4 370.1n 372.6n ~ 0.700
CalculateWork-4 500.4n 509.9n ~ 0.700
CheckOldBlockIDs/on-chain-prefetch/1000-4 63.32µ 63.30µ ~ 1.000
CheckOldBlockIDs/off-chain-prefetch/1000-4 54.90µ 57.17µ ~ 1.000
CheckOldBlockIDs/on-chain-prefetch/10000-4 462.3µ 486.3µ ~ 0.100
CheckOldBlockIDs/off-chain-prefetch/10000-4 356.3µ 370.7µ ~ 0.100
BuildBlockLocatorString_Helpers/Size_10-4 1.446µ 1.509µ ~ 0.100
BuildBlockLocatorString_Helpers/Size_100-4 13.89µ 14.57µ ~ 0.100
BuildBlockLocatorString_Helpers/Size_1000-4 137.6µ 137.7µ ~ 1.000
CatchupWithHeaderCache-4 104.7m 104.6m ~ 0.700
_BufferPoolAllocation/16KB-4 3.946µ 4.147µ ~ 0.100
_BufferPoolAllocation/32KB-4 8.978µ 10.798µ ~ 0.200
_BufferPoolAllocation/64KB-4 20.63µ 17.63µ ~ 0.700
_BufferPoolAllocation/128KB-4 33.04µ 36.42µ ~ 0.100
_BufferPoolAllocation/512KB-4 135.1µ 146.1µ ~ 0.100
_BufferPoolConcurrent/32KB-4 19.41µ 23.02µ ~ 0.200
_BufferPoolConcurrent/64KB-4 31.65µ 33.61µ ~ 0.100
_BufferPoolConcurrent/512KB-4 153.8µ 157.0µ ~ 0.400
_SubtreeDeserializationWithBufferSizes/16KB-4 680.1µ 663.4µ ~ 1.000
_SubtreeDeserializationWithBufferSizes/32KB-4 674.8µ 668.9µ ~ 1.000
_SubtreeDeserializationWithBufferSizes/64KB-4 656.0µ 695.8µ ~ 0.200
_SubtreeDeserializationWithBufferSizes/128KB-4 664.4µ 670.8µ ~ 0.700
_SubtreeDeserializationWithBufferSizes/512KB-4 631.8µ 629.5µ ~ 0.700
_SubtreeDataDeserializationWithBufferSizes/16KB-4 37.31m 37.24m ~ 0.400
_SubtreeDataDeserializationWithBufferSizes/32KB-4 37.19m 37.18m ~ 1.000
_SubtreeDataDeserializationWithBufferSizes/64KB-4 37.32m 36.71m ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/128KB-4 37.52m 36.39m ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/512KB-4 36.89m 36.93m ~ 0.700
_PooledVsNonPooled/Pooled-4 739.3n 741.1n ~ 0.700
_PooledVsNonPooled/NonPooled-4 8.094µ 8.857µ ~ 0.100
_MemoryFootprint/Current_512KB_32concurrent-4 6.787µ 6.811µ ~ 1.000
_MemoryFootprint/Proposed_32KB_32concurrent-4 9.567µ 9.992µ ~ 0.100
_MemoryFootprint/Alternative_64KB_32concurrent-4 9.209µ 9.230µ ~ 1.000
_prepareTxsPerLevel-4 423.6m 426.2m ~ 0.700
_prepareTxsPerLevelOrdered-4 4.291m 4.064m ~ 0.700
_prepareTxsPerLevel_Comparison/Original-4 410.4m 428.6m ~ 0.400
_prepareTxsPerLevel_Comparison/Optimized-4 4.929m 3.971m ~ 0.100
SubtreeSizes/10k_tx_4_per_subtree-4 1.289m 1.262m ~ 0.700
SubtreeSizes/10k_tx_16_per_subtree-4 301.1µ 297.7µ ~ 1.000
SubtreeSizes/10k_tx_64_per_subtree-4 71.88µ 71.33µ ~ 1.000
SubtreeSizes/10k_tx_256_per_subtree-4 17.78µ 17.83µ ~ 1.000
SubtreeSizes/10k_tx_512_per_subtree-4 8.815µ 8.833µ ~ 0.700
SubtreeSizes/10k_tx_1024_per_subtree-4 4.452µ 4.392µ ~ 0.100
SubtreeSizes/10k_tx_2k_per_subtree-4 2.221µ 2.219µ ~ 1.000
BlockSizeScaling/10k_tx_64_per_subtree-4 69.70µ 70.65µ ~ 0.100
BlockSizeScaling/10k_tx_256_per_subtree-4 17.38µ 17.56µ ~ 0.100
BlockSizeScaling/10k_tx_1024_per_subtree-4 4.361µ 4.408µ ~ 0.700
BlockSizeScaling/50k_tx_64_per_subtree-4 374.6µ 367.0µ ~ 0.700
BlockSizeScaling/50k_tx_256_per_subtree-4 88.46µ 87.85µ ~ 1.000
BlockSizeScaling/50k_tx_1024_per_subtree-4 21.94µ 21.80µ ~ 0.700
SubtreeAllocations/small_subtrees_exists_check-4 151.8µ 152.5µ ~ 0.400
SubtreeAllocations/small_subtrees_data_fetch-4 162.0µ 159.4µ ~ 0.700
SubtreeAllocations/small_subtrees_full_validation-4 311.1µ 308.6µ ~ 0.400
SubtreeAllocations/medium_subtrees_exists_check-4 9.045µ 8.899µ ~ 0.200
SubtreeAllocations/medium_subtrees_data_fetch-4 9.605µ 9.587µ ~ 1.000
SubtreeAllocations/medium_subtrees_full_validation-4 17.97µ 17.50µ ~ 0.100
SubtreeAllocations/large_subtrees_exists_check-4 2.164µ 2.126µ ~ 0.200
SubtreeAllocations/large_subtrees_data_fetch-4 2.284µ 2.247µ ~ 0.700
SubtreeAllocations/large_subtrees_full_validation-4 4.465µ 4.332µ ~ 0.400
StoreBlock_Sequential/BelowCSVHeight-4 323.7µ 327.1µ ~ 0.200
StoreBlock_Sequential/AboveCSVHeight-4 330.4µ 323.1µ ~ 0.100
GetUtxoHashes-4 267.1n 264.6n ~ 0.400
GetUtxoHashes_ManyOutputs-4 43.15µ 43.26µ ~ 0.700
_NewMetaDataFromBytes-4 227.6n 229.1n ~ 0.700
_Bytes-4 401.4n 394.4n ~ 0.100
_MetaBytes-4 137.9n 137.4n ~ 0.400

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

@freemans13 freemans13 self-assigned this Apr 24, 2026
freemans13 and others added 2 commits April 24, 2026 14:02
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

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 21 out of 21 changed files in this pull request and generated 1 comment.

Comment on lines +164 to +169
if prev != s.mode {
s.metrics.transitions.WithLabelValues(s.serviceName, prev.String(), s.mode.String()).Inc()
s.emitMode()
}
}

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 - real bug. Mode transitions now reset the rolling window (s.window = s.window[:0], s.windowHead = 0) inside recordWithMode, immediately after the transition is detected. Each mode's thresholds are now evaluated only against observations collected while in that mode. Added regression test TestTransition_ClearsWindow_NoImmediateBackflip that demonstrates the bounce-back case (Pess→Opt trip into Opt→Pess single-block trip via MissingFetches=100 with LocalHits=TotalTxs, then a single perfect pessimistic Record which previously would have re-tripped Pess→Opt immediately — now correctly stays pessimistic until the new window fills from scratch). The test fails on the old code and passes on the new.

Copilot flagged that mode transitions did not clear the rolling window, so
observations recorded while in the previous mode polluted the next mode's
threshold evaluation. Concrete failure case: Pess->Opt trip leaves the
window full of hit-rate=1.0 samples; an Opt->Pess single-block trip via
MissingFetches=100 with LocalHits=TotalTxs replaces one slot but the other
slots remain hit-rate=1.0; the very next pessimistic Record satisfies the
Pess->Opt threshold and bounces back to optimistic immediately.

Fix: clear s.window and s.windowHead inside recordWithMode immediately
after a transition is detected. Each mode's thresholds are now evaluated
only against observations collected while in that mode.

Added TestTransition_ClearsWindow_NoImmediateBackflip as a regression
guard.

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

Comment thread services/subtreevalidation/Server.go Outdated
Comment on lines +220 to +221
logger.Errorf("[SubtreeValidation] adaptive_fetch config invalid (%v) — using hardcoded defaults", afErr)
af, _ = adaptivefetch.New(adaptivefetch.DefaultConfig(), "subtreevalidation", prometheus.DefaultRegisterer)

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 both Servers. The invalid-config fallback now starts from DefaultConfig() but overrides BootstrapMode to ModePessimistic before calling New(). A typo in any of the numeric adaptive_fetch_* settings can no longer silently re-enable optimistic skipping via the ModeAuto default. Updated log message and comments to match.

Comment thread services/blockvalidation/Server.go Outdated
Comment on lines +322 to +323
logger.Errorf("[BlockValidation] adaptive_fetch config invalid (%v) — using hardcoded defaults", afErr)
af, _ = adaptivefetch.New(adaptivefetch.DefaultConfig(), "blockvalidation", prometheus.DefaultRegisterer)

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 both Servers. The invalid-config fallback now starts from DefaultConfig() but overrides BootstrapMode to ModePessimistic before calling New(). A typo in any of the numeric adaptive_fetch_* settings can no longer silently re-enable optimistic skipping via the ModeAuto default. Updated log message and comments to match.

Copilot flagged that adaptivefetch.DefaultConfig() ships with BootstrapMode
ModeAuto for direct package callers / tests. The Server.go fallback used on
adaptivefetch.New() validation failure (numeric adaptive_fetch_* typo)
therefore silently re-enabled auto transitions / optimistic skipping
despite the operator-facing default being pessimistic. Override
BootstrapMode to ModePessimistic in both Server fallbacks so invalid
config never results in a less-safe mode.

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

Comment on lines +360 to +362
// The Opt→Pess transition will not fire from observations alone —
// it relies on downstream processTransactionsInLevels surfacing a
// validation error and the block failing. Until real

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.

Reworded — clearer now that adaptivefetch.maybeTransition() only inspects the values passed to Record/RecordIfMode, and downstream processTransactionsInLevels validation failures are not fed back into the state machine. So the Opt→Pess trip cannot fire from validation failures either; it requires the missing-fetches plumbing or an operator-driven config change + restart.

Comment thread pkg/adaptivefetch/metrics.go Outdated
Namespace: "teranode",
Subsystem: "adaptive_fetch",
Name: "missing_fetches_total",
Help: "Running total of transactions recovered via processMissingTransactions, by service.",

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 point — the Help string now reads 'Running total of transactions recovered/fetched individually after an optimistic-mode skip, by service.' (service-agnostic, no more reference to subtreevalidation-specific processMissingTransactions).

… note

Copilot review:
- pkg/adaptivefetch/metrics.go: missing_fetches_total Help no longer
  references subtreevalidation-specific processMissingTransactions, since
  the metric is emitted by multiple services (blockvalidation + subtree-
  validation). Reworded to 'transactions recovered/fetched individually
  after an optimistic-mode skip, by service.'
- services/subtreevalidation/check_block_subtrees.go: clarified that the
  Opt->Pess transition cannot fire from validation failures either;
  adaptivefetch only inspects Record/RecordIfMode arguments.

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

@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
3.8% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

freemans13 and others added 3 commits May 29, 2026 13:19
# Conflicts:
#	services/subtreevalidation/Server.go
#	services/subtreevalidation/check_block_subtrees.go
#	services/subtreevalidation/check_block_subtrees_test.go
Until the validation hot paths plumb real LocalHits/MissingFetches counts,
callers stamp synthetic observations (LocalHits=TotalTxs, MissingFetches=0),
so teranode_adaptive_fetch_hit_rate was always 1.0 and
teranode_adaptive_fetch_missing_fetches_total always 0 — a permanently
perfect, meaningless series that risks false operator confidence.

Remove both collectors and their per-observation emission. Keep the mode
gauge and mode_transitions_total, which carry real signal. The Observation
fields and maybeTransition logic are unchanged — they remain the state
machine's decision inputs and the hook for real counts later.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@freemans13

Copy link
Copy Markdown
Collaborator Author

Review feedback addressed

Constant metrics removed (9517e0e1b)
Dropped teranode_adaptive_fetch_hit_rate and teranode_adaptive_fetch_missing_fetches_total. Because callers currently stamp synthetic observations (LocalHits=TotalTxs, MissingFetches=0), those two series were permanently 1.0 / 0 — a perfect-looking, meaningless signal that risked false operator confidence. Kept mode (gauge) and mode_transitions_total (counter), which carry real signal. The Observation fields and maybeTransition logic are untouched — they remain the state machine's decision inputs and the hook for real counts when the validation hot paths plumb them through. A NotContains test pins the removal.

Default mode — confirmed already correct: adaptive_fetch_bootstrap_mode defaults to pessimistic (pinned, never drifts) and is configurable to optimistic or auto. Both config-error fallbacks pin to pessimistic, so a typo can't silently enable skipping. No change needed.

Out-of-scope subtreeData fetch paths — reviewed (RevalidateBlock, normal single-subtree validation, legacy netsync). None is a hole that reintroduces always-fetch in the targeted scenario; no action needed.

Still open (operational, not code)

  • Optimistic mode has no per-tx local-presence check and no auto Opt→Pess recovery (MissingFetches always 0 today). Safety net is a hard block-validation failure; recovery from a degraded optimistic node needs an operator restart. This is the acknowledged MVP debt — recommend a runbook note until real miss-count plumbing lands.
  • auto is a warm-up gate, not a measurement. With synthetic observations, Pess→Opt fires after WindowSize observations regardless of true locality (and subtreevalidation records one per subtree, so a single large block can flip it). Documented in the setting's longdesc; flagging for a decision on whether to expose auto before real measurement lands.

🤖 Generated with Claude Code

The gate only toggles an eager subtreeData *prewarm*. Full validation runs
in every mode and recovers genuinely-missing txs on demand
(ValidateSubtreeInternal -> getSubtreeMissingTxs -> processMissingTransactions),
so a wrong optimistic guess costs bandwidth at validation time, never
correctness or data integrity.

The previous comments and the bootstrap_mode longdesc implied optimistic mode
was a safety gamble requiring an operator restart to "recover". Reword in plain
English to state accurately:
- optimistic is a bandwidth/performance choice, not a safety switch
- a missing tx is still fetched and validated normally
- the unfired Opt->Pess auto-trip is a metrics-accuracy gap (mode gauge can
  stay stuck on optimistic), not a correctness risk
- setting bootstrap_mode=pessimistic + restart is a tuning choice, not recovery

Comments/docs only — no behaviour change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

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

Re-reviewed against my three earlier asks. The state machine itself is in good shape — both transition directions, inclusive thresholds, the RecordIfMode stale-mode guard, and the no-wall-clock invariant are all tested now, pkg/adaptivefetch is at 94.4%, and you've made the docs honestly describe the one-way limitation. Three things still open:

  • The subtreevalidation optimistic-skip path has zero test coverage (check_block_subtrees.go:348-429). Coverage profiling shows the optimistic branch is never taken — only the pessimistic-fetch body runs. blockvalidation has TestBlockWorker_Optimistic_SkipsFetchSubtreeData; subtreevalidation needs the equivalent (set ModeOptimistic, assert the subtree_data fetch is skipped). This was the primary ask and the skip is the whole point of the feature here.
  • SonarQube duplication is still failing — now 3.8% (was 3.1%, threshold 3%). The two ~34-line observation-recording blocks (get_blocks.go:263-299, check_block_subtrees.go:431-464) are un-extracted; a shared recordAdaptiveFetchObservation helper fixes the gate and centralizes the MissingFetches plumbing.
  • "self-adjusting" still overclaims. MissingFetches=0 is still hardcoded at both call sites, so the Opt→Pess auto-recovery trip can't fire — it's one-way Pess→Opt. The honest prose/longdesc is good, but please either rename to reflect one-way semantics or add a tracked TODO(#issue) at both sites for wiring the real count (len(missingTxHashesCompacted), SubtreeValidation.go:685-693). Also the PR body lists 4 metrics; the code registers 2.

Minor: adaptivefetch.New config-validation failure is swallowed silently in blockvalidation/Server.go:317 (the pessimistic fallback is safe, but log that the setting was ignored). Add the skip test + extract the dup + fix the title/TODO and I'll approve.

…ockchain#745 review

Pin the adaptive-fetch gate pessimistic until the node first reaches FSM
RUNNING, so cold-start IBD / restart-while-behind never skips subtreeData at
the moment txs are least likely to be local. New() now always starts
pessimistic; a one-way, idempotent Arm() latch (tripped by each service on the
first RUNNING) applies the configured bootstrap mode and clears the pre-arm
window so fake-perfect IBD samples can't cause an instant flip. The latch never
re-locks, so a post-RUNNING catch-up burst may still use optimistic mode.

pkg/adaptivefetch stays FSM-free: Arm is a generic trigger and the FSM wait
(WaitForFSMtoTransitionToGivenState RUNNING) lives in the service layer, so
TestNoWallClockOrFSMDependency still holds and the bsv-blockchain#598/bsv-blockchain#647 clock/FSM-gating
class-of-bug is not reintroduced into the algorithm.

Also addresses the review:
- Add TestCheckBlockSubtrees_Optimistic_SkipsFetchSubtreeData covering the
  previously-untested optimistic skip branch in subtreevalidation (runs both
  modes so it cannot pass trivially).
- Extract (*State).RecordSyntheticWarmup, collapsing the two duplicated
  ~34-line observation-recording blocks (SonarQube duplication).
- Log the previously-swallowed fallback adaptivefetch.New error in both
  blockvalidation and subtreevalidation Server constructors.
- Honest docs/TODO for the synthetic MissingFetches=0 one-way limitation;
  fix "four collectors" -> two comment drift; update settings longdesc to
  describe the arm-on-RUNNING behaviour.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@freemans13 freemans13 changed the title feat(adaptivefetch): self-adjusting subtreeData fetch gate feat(adaptivefetch): subtreeData fetch gate, armed on FSM RUNNING Jun 5, 2026
@freemans13

Copy link
Copy Markdown
Collaborator Author

Addressed in 874232d (pushed, fast-forward). Mapping to each point:

1. subtreevalidation optimistic-skip coverage — Added TestCheckBlockSubtrees_Optimistic_SkipsFetchSubtreeData. Subtlety: the /subtree_data endpoint is hit by both the pessimistic prewarm and the downstream on-demand recovery (getSubtreeMissingTxs), so a subtree with missing txs would fetch in both modes and prove nothing. The test therefore uses a coinbase-only subtree (zero recoverable txs → recovery never runs) and exercises both modes against identical input: the pessimistic control fetches /subtree_data, the optimistic subject records zero. It can't pass trivially.

2. SonarQube duplication — Extracted (*adaptivefetch.State).RecordSyntheticWarmup, which carries the rationale comment once and centralises the MissingFetches plumbing. The two ~34-line blocks in get_blocks.go and check_block_subtrees.go are now one-liners.

3. "self-adjusting" overclaim — Reworked rather than just renamed (title is now …armed on FSM RUNNING). Digging into this with @freemans13 surfaced a latent bug behind the overclaim: with BootstrapMode=auto the fake-perfect synthetic observations flip the gate optimistic after ~10 blocks during IBD — the worst possible moment. Fix: the gate is now pinned pessimistic until the node first reaches FSM RUNNING via a one-way Arm() latch in pkg/adaptivefetch, tripped by each service through WaitForFSMtoTransitionToGivenState. Crucially pkg/adaptivefetch stays FSM-free — Arm() is a generic trigger; the FSM knowledge lives in the service layer — so TestNoWallClockOrFSMDependency still passes and the #598/#647 clock/FSM class-of-bug is not reintroduced into the algorithm. The synthetic MissingFetches=0 one-way limitation remains (now a tracked TODO on RecordSyntheticWarmup), and the PR body is honest about it. Metrics corrected 4 → 2: 9517e0e had already dropped the synthetic hit_rate/misses series; the body now matches the code (and the stale "four collectors" comment is fixed).

Minor — Server.go fallback — The primary invalid-config error has actually been logged via Errorf since 5bd167d (predates this review). The residual silent swallow was the fallback construction af, _ = New(safeFallback, …); that now logs too. af stays nil on the (unreachable-in-practice) fallback failure and the gate's nil-receiver methods behave pessimistically — safe degrade.

Verification: pkg/adaptivefetch (51 tests, -race), full services/subtreevalidation, and full services/blockvalidation suites all green; build + vet + lint (affected) clean.

@freemans13 freemans13 requested a review from oskarszoon June 5, 2026 12:18
@sonarqubecloud

sonarqubecloud Bot commented Jun 5, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
3.8% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

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

Re-review @ 874232d — LGTM, approving. The arming rework holds up: consensus-safe (the optimistic skip is prewarm-only; block.Valid()/CheckMerkleRoot and the SubtreeValidation.go:793-795 fence are unconditional, so it can't admit an unvalidated block), race-clean (-race on pkg/adaptivefetch passes), and no data-withholding exploit (recovery cost caps at the pessimistic baseline via the 20% full-fetch knee). All the substantive earlier asks are resolved — the subtreevalidation skip test is non-trivial, the overclaim language is gone, metrics are down to 2, and both the primary and fallback New() failures log.

Two non-blocking follow-ups (not gating this approval):

  • The SonarQube duplication gate is still red (3.8%). The RecordSyntheticWarmup extraction didn't move it — the failing dup is the near-identical adaptivefetch.New + bootstrap-parse + fallback-ctor block in subtreevalidation/Server.go:200-233 and blockvalidation/Server.go:301-337 (deltas = service name + log prefix). A shared adaptivefetch.NewFromSettings(settings, name, reg) would clear it — keep all four per-service log lines. Worth doing whenever, the gate will keep flagging it.
  • pkg/adaptivefetch/state.go:205 is a bare TODO(adaptivefetch): plumb real recovered-tx count. Give it an issue ref (TODO(adaptivefetch, #NNNN):) so the one-way-mode debt stays tracked.

@freemans13 freemans13 requested a review from ordishs June 5, 2026 15:52

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

Approve. Reviewed the full diff plus the surrounding CheckBlockSubtrees flow to verify the load-bearing claim.

Correctness verified

  • missingSubtrees is computed independently of the gate, and validateMissingSubtreesWithOrderedRetry runs ValidateSubtreeInternal for every subtree regardless of mode — the optimistic path only nils out the bulk prewarm. Missing txs are still recovered on demand via getSubtreeMissingTxs → processMissingTransactions. End state is identical; a wrong optimistic guess costs bandwidth, not correctness.
  • FileTypeSubtreeToCheck is stored before the gate, so validation always has the tx list.
  • contributingPeers = nil in optimistic mode only affects peer-reputation crediting — correct, since no peer provided data.
  • Fully nil-safe; the modeAtSample + RecordIfMode snapshot correctly drops cross-mode observations; shared DefaultRegisterer registration handled via registerOrReuse. go vet ./pkg/adaptivefetch/ clean.

The arm-on-RUNNING latch genuinely closes the cold-start hazard that sank #598, and the source-scan guard (TestNoWallClockOrFSMDependency) keeps the package clock/FSM-free. Test coverage is strong — the integration tests run both modes against identical inputs with a positive control, so they can't pass trivially.

Follow-ups (none blocking)

  • Medium / operational: because MissingFetches is hardcoded 0, the windowed Opt→Pess auto-recovery never fires. A node mistakenly run optimistic on a distributor-less deployment will recover txs individually forever — potentially more expensive than the bulk prewarm — with the mode gauge under-reporting the cost. Correctness is never at risk, but please track the RecordSyntheticWarmup TODO (plumb len(missingTxHashesCompacted)) as a real issue before relying on optimistic/auto outside a known-good cluster.
  • Low: no armed{service} metric is exported, so the pinned-pessimistic window is indistinguishable from configured-pessimistic on the mode gauge — an armed gauge would make the deploy-verification step trivial to confirm.
  • Low: arm-goroutine wiring is asymmetric (g.Go in blockvalidation vs bare go func() in subtreevalidation); both correct, worth aligning.

Recommend completing the dev-scale-1/2 deploy verification (the one unchecked box) before enabling optimistic in anger.

@freemans13 freemans13 merged commit 6b9ebd2 into bsv-blockchain:main Jun 8, 2026
34 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