test(blockassembly/subtreeprocessor): add clock seam for deterministic tests#841
Conversation
Adds an unexported queueClock seam on LockFreeQueue so tests can stamp
batches with a controlled time. Default is realQueueClock{} which calls
time.Now(); production behaviour is unchanged.
Verified zero-cost on the per-batch ingestion hot path (queue.go) via
BenchmarkQueue with -benchtime=10s -count=30: +0.5 ns/op, p=0.625 -
indistinguishable from noise.
…cessor Promotes the clock interface to a package-level type (clock / realClock, replacing queue-local queueClock / realQueueClock) and threads it through SubtreeProcessor so the two validFromMillis calculations - in the Start dequeue loop and in dequeueDuringBlockMovement - read through stp.clock. Tests can now install one fake clock and have both batch timestamps (LockFreeQueue) and DoubleSpendWindow boundary calculations honour it. No behaviour change in production. Verified zero-cost on BenchmarkQueue same-day same-machine: 170.4n -> 170.3n, p=0.566 (n=10 x 10s).
Use the new clock seam to characterize the queue's validFromMillis filter
deterministically, without time.Sleep. Five tests / nine subtests pin:
* Start-loop vs dequeueDuringBlockMovement asymmetry at
DoubleSpendWindow=0 (default). The Start loop's zero-guard
short-circuits the filter; the drain formula does not, so
same-millisecond batches get held back during block-movement drains.
Pinned at the formula level (queue_test.go) and at the real call
site (conflicting_queue_race_test.go) with a +1ms control case
that mirrors the existing time.Sleep(5ms) workaround at
conflicting_queue_race_test.go:75 deterministically.
* Inclusive-reject semantics at batch.time == validFromMillis, and
admission one millisecond below the boundary.
* Negative validFromMillis short-circuits the > 0 guard and disables
filtering. Dormant in production (Now().UnixMilli() is in the
trillions) but worth pinning so a future caller or test built on
time.Time{} does not silently lose double-spend protection.
* Clock-backward jump under NTP correction holds batches until the
drain clock recovers past batch.time + window.
|
🤖 Claude Code Review Status: Complete Current Review: Summary: Strengths:
|
Benchmark Comparison ReportBaseline: Current: Summary
All benchmark results (sec/op)
Threshold: >10% with p < 0.05 | Generated: 2026-05-11 11:51 UTC |
|



Summary
Introduces an unexported
clockinterface inservices/blockassembly/subtreeprocessor, injected intoLockFreeQueueandSubtreeProcessor. The production default isrealClock{}(callstime.Now()); tests install a fake to drive deterministic timestamps withouttime.Sleep.Three
time.Now()call sites are now mediated by the seam:queue.go:63-batch.timeon enqueueSubtreeProcessor.go:812-validFromMillisin the Start-loop dequeueSubtreeProcessor.go:3789-validFromMillisindequeueDuringBlockMovementFive new tests / nine subtests in
queue_test.goandconflicting_queue_race_test.gouse the seam to characterize the queue'svalidFromMillisfilter at queue.go:96 (boundary semantics, negative-cutoff bypass, clock-backstep behaviour, and the asymmetry between the twovalidFromMillisformulas atDoubleSpendWindow=0).Motivation
blockassemblyhas time-dependent correctness logic gated byDoubleSpendWindow. The existing test workaround for this istime.Sleep(conflicting_queue_race_test.go:75sleeps 5ms before callingdequeueDuringBlockMovement). A clock seam replaces wall-time waits with deterministic time, which is the prerequisite for any property/invariant testing of reorgs and drain ordering.Perf gate
The hot path is
LockFreeQueue.enqueueBatch(one timestamp per batch, called from every producer). Benchmarked on this machine with-count=30 -benchtime=10sbefore merging, and again same-day same-machine after the SubtreeProcessor seam was added:main)No measurable regression. Interface dispatch at the timestamp call is free here.
Latent finding (not addressed in this PR)
While writing the characterization tests, the two
validFromMillisformulas diverge atDoubleSpendWindow == 0(the documented default)::807-813) - zero-guarded:validFromMillis = 0→ queue filter off.dequeueDuringBlockMovement(:3789) - unconditional:validFromMillis = Now().UnixMilli()→ filter active, rejects same-ms batches.conflicting_queue_race_test.go:71-75papers over this with a 5ms sleep. The tests in this PR pin the current (asymmetric) behaviour. I'll open a follow-up to align the drain formula with the Start-loop zero-guard once this seam lands.Test plan
go vet ./services/blockassembly/subtreeprocessor/- cleango test -race -count=1 ./services/blockassembly/subtreeprocessor/- pass (153s)go test -race -count=1 ./services/blockassembly/- pass (102s)staticcheckandgolangci-lintare pinned to Go 1.25.5 locally vs the repo's Go 1.26.0 toolchain - pre-existing tooling mismatch, expecting CI to run clean.