Skip to content

test(blockassembly/subtreeprocessor): rapid property tests for validFromMillis admit logic#848

Merged
liam merged 2 commits into
bsv-blockchain:mainfrom
liam:liam/blockassembly-rapid-properties
May 14, 2026
Merged

test(blockassembly/subtreeprocessor): rapid property tests for validFromMillis admit logic#848
liam merged 2 commits into
bsv-blockchain:mainfrom
liam:liam/blockassembly-rapid-properties

Conversation

@liam

@liam liam commented May 12, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds three property tests that pin the queue filter and dequeueDuringBlockMovement admit behaviour across randomly generated inputs, using pgregory.net/rapid as a new test-only dependency.

New dependency

  • pgregory.net/rapid v1.3.0 - ~3kloc actively maintained property-testing library. Test-only (not imported from production code). Adds one line to go.mod and two lines to go.sum.

Considered the alternatives:

  • testing/quick (stdlib): no shrinking, manual generators per type, awkward for invariants over multiple correlated values.
  • go test -fuzz: requires one FuzzXxx function per property with []byte inputs, verbose for the multi-value tuples here.

Rapid's composable generators (OneOf, Int64Range) and automatic shrinking make these invariants much easier to express and debug if they ever fail.

Properties

Test_propertyDequeueBatchAdmitPredicate - queue-level. Asserts dequeueBatch decisions match the spec at queue.go:96:

admit iff validFromMillis <= 0 OR batch.time < validFromMillis

Generators target three regions: negative cutoff (-1000..0, the bypass case), boundary (batch_time ± 10, the inclusive-reject case), and unrelated cutoffs. 100 cases, sub-millisecond.

Test_propertyDrainAdmitInvariant - integration-level. Drives the real dequeueDuringBlockMovement with random (window, enqueue_clock, drain_clock), including backward clock jumps (NTP-correction territory), same-instant drains, and arbitrary forward advances. Asserts the queue state and subtree contents match the expected admit predicate. 100 cases, ~400ms.

This is the test that would have caught the asymmetry fixed in #846, and now guards against re-introducing it.

Test_propertyAgingAlwaysAdmits - liveness invariant. For any (window, enqueue_time), advancing the drain clock past enqueue_time + window + 1ms must admit the batch. Pins the fully-aged guarantee against future filter changes that might accidentally invert boundaries.

Test plan

  • go vet ./services/blockassembly/... - clean
  • go test -count=1 -run 'Test_property' ./services/blockassembly/subtreeprocessor/ - pass (~1s)
  • go test -race -count=1 -run 'Test_property' ./services/blockassembly/subtreeprocessor/ - pass (~10s)
  • go test -race -count=1 ./services/blockassembly/subtreeprocessor/ - pass (~170s)
  • go mod tidy clean (only rapid added)

Notes

…romMillis admit logic

Adds pgregory.net/rapid (test-only dependency) and three property tests
that pin the queue filter and dequeueDuringBlockMovement admit
behaviour across randomly generated inputs.

Test_propertyDequeueBatchAdmitPredicate: asserts the queue filter at
queue.go:96 produces decisions matching its spec
(validFromMillis <= 0 || batch.time < validFromMillis) for any
(batch_time, validFromMillis) pair, including negative cutoff (bypass
region), boundary region (batch.time-10 to batch.time+10), and
unrelated cutoffs. 100 generated cases.

Test_propertyDrainAdmitInvariant: drives the real dequeueDuringBlockMovement
with random (window, enqueue_clock, drain_clock) - covering backward
clock jumps, same-instant, and arbitrary forward advances - and asserts
the integration outcome matches the expected admit predicate. Catches
regressions in either the validFromMillis formula at
SubtreeProcessor.go:3789-3796 or the queue filter. 100 generated cases.

Test_propertyAgingAlwaysAdmits: liveness invariant. For any
(window, enqueue_time), advancing the drain clock past
(enqueue_time + window + 1ms) must admit the batch. Pins the
fully-aged guarantee against future filter changes that might
accidentally invert boundaries. 100 generated cases.

All properties run sub-millisecond at the queue level. The integration
property runs ~400ms total (100 iterations of newTestProcessorNoStart).
With -race: ~10s for the three properties combined.
@github-actions

github-actions Bot commented May 12, 2026

Copy link
Copy Markdown
Contributor

🤖 Claude Code Review

Status: Complete

Current Review: No issues found


Summary

This PR adds three well-structured property-based tests using pgregory.net/rapid to verify the queue admission logic. The tests correctly validate the boundary conditions and integration behavior of dequeueDuringBlockMovement.

Key Verification Points:

Documentation accuracy - All referenced line numbers and code snippets match the actual implementation:

  • queue.go:96 correctly shows the filter predicate
  • SubtreeProcessor.go:3789-3796 correctly shows the validFromMillis formula
  • Test comments accurately describe the admit predicate

Test logic correctness - The property tests correctly implement the documented predicates:

  • Test_propertyDequeueBatchAdmitPredicate: Direct queue-level validation
  • Test_propertyDrainAdmitInvariant: Integration-level validation with clock manipulation
  • Test_propertyAgingAlwaysAdmits: Liveness guarantee for aged batches

Dependencies - Uses existing test helpers (newTestProcessorNoStart, collectSubtreeHashes, fixedClock) from prior merged PRs

Test hygiene - Properly uses t.Helper() indirection for outerT in rapid callbacks, appropriate cleanup handling

The tests are defensive in nature (regression detection) and align with project verification standards in AGENTS.md.

@github-actions

github-actions Bot commented May 12, 2026

Copy link
Copy Markdown
Contributor

Benchmark Comparison Report

Baseline: main (unknown)

Current: PR-848 (8c6f04e)

Summary

  • Regressions: 0
  • Improvements: 0
  • Unchanged: 142
  • Significance level: p < 0.05
All benchmark results (sec/op)
Benchmark Baseline Current Change p-value
_NewBlockFromBytes-4 1.720µ 1.719µ ~ 1.000
SplitSyncedParentMap_SetIfNotExists/256_buckets-4 62.03n 61.73n ~ 0.400
SplitSyncedParentMap_SetIfNotExists/16_buckets-4 61.67n 61.79n ~ 0.200
SplitSyncedParentMap_SetIfNotExists/1_bucket-4 61.67n 61.67n ~ 0.800
SplitSyncedParentMap_ConcurrentSetIfNotExists/256_buckets... 32.12n 30.43n ~ 0.100
SplitSyncedParentMap_ConcurrentSetIfNotExists/16_buckets_... 57.82n 52.45n ~ 0.100
SplitSyncedParentMap_ConcurrentSetIfNotExists/1_bucket_pa... 136.0n 108.6n ~ 0.100
MiningCandidate_Stringify_Short-4 268.1n 269.5n ~ 0.700
MiningCandidate_Stringify_Long-4 1.991µ 1.920µ ~ 0.100
MiningSolution_Stringify-4 1006.0n 987.6n ~ 0.100
BlockInfo_MarshalJSON-4 1.854µ 1.803µ ~ 0.100
NewFromBytes-4 128.0n 129.6n ~ 0.100
Mine_EasyDifficulty-4 60.91µ 61.37µ ~ 0.700
Mine_WithAddress-4 6.973µ 6.976µ ~ 1.000
BlockAssembler_AddTx-4 0.02591n 0.02959n ~ 0.400
AddNode-4 11.10 10.99 ~ 0.700
AddNodeWithMap-4 10.77 11.02 ~ 1.000
DiskTxMap_SetIfNotExists-4 3.586µ 3.593µ ~ 1.000
DiskTxMap_SetIfNotExists_Parallel-4 3.557µ 3.439µ ~ 1.000
DiskTxMap_ExistenceOnly-4 324.4n 316.0n ~ 0.700
Queue-4 197.3n 189.3n ~ 0.100
AtomicPointer-4 4.620n 4.694n ~ 0.100
ReorgOptimizations/DedupFilterPipeline/Old/10K-4 933.8µ 836.8µ ~ 0.100
ReorgOptimizations/DedupFilterPipeline/New/10K-4 890.5µ 779.4µ ~ 0.100
ReorgOptimizations/AllMarkFalse/Old/10K-4 130.0µ 115.6µ ~ 0.100
ReorgOptimizations/AllMarkFalse/New/10K-4 61.63µ 61.86µ ~ 0.200
ReorgOptimizations/HashSlicePool/Old/10K-4 76.42µ 55.10µ ~ 0.100
ReorgOptimizations/HashSlicePool/New/10K-4 11.75µ 11.54µ ~ 1.000
ReorgOptimizations/NodeFlags/Old/10K-4 6.242µ 4.553µ ~ 0.100
ReorgOptimizations/NodeFlags/New/10K-4 2.476µ 1.558µ ~ 0.100
ReorgOptimizations/DedupFilterPipeline/Old/100K-4 10.13m 10.18m ~ 0.700
ReorgOptimizations/DedupFilterPipeline/New/100K-4 10.31m 10.04m ~ 0.100
ReorgOptimizations/AllMarkFalse/Old/100K-4 1.182m 1.130m ~ 0.100
ReorgOptimizations/AllMarkFalse/New/100K-4 683.7µ 683.2µ ~ 1.000
ReorgOptimizations/HashSlicePool/Old/100K-4 689.2µ 546.1µ ~ 0.100
ReorgOptimizations/HashSlicePool/New/100K-4 292.4µ 315.9µ ~ 0.200
ReorgOptimizations/NodeFlags/Old/100K-4 56.91µ 49.88µ ~ 0.100
ReorgOptimizations/NodeFlags/New/100K-4 19.61µ 17.34µ ~ 0.100
TxMapSetIfNotExists-4 51.50n 51.72n ~ 0.600
TxMapSetIfNotExistsDuplicate-4 38.38n 40.57n ~ 0.400
ChannelSendReceive-4 631.3n 603.8n ~ 0.100
DirectSubtreeAdd/4_per_subtree-4 75.04n 76.00n ~ 0.400
DirectSubtreeAdd/64_per_subtree-4 41.14n 41.69n ~ 0.100
DirectSubtreeAdd/256_per_subtree-4 39.56n 39.84n ~ 0.200
DirectSubtreeAdd/1024_per_subtree-4 38.36n 38.46n ~ 0.500
DirectSubtreeAdd/2048_per_subtree-4 37.99n 37.99n ~ 0.700
SubtreeProcessorAdd/4_per_subtree-4 333.9n 325.2n ~ 0.200
SubtreeProcessorAdd/64_per_subtree-4 318.8n 319.9n ~ 0.400
SubtreeProcessorAdd/256_per_subtree-4 317.7n 318.2n ~ 1.000
SubtreeProcessorAdd/1024_per_subtree-4 305.4n 302.2n ~ 0.400
SubtreeProcessorAdd/2048_per_subtree-4 308.0n 305.6n ~ 0.100
SubtreeProcessorRotate/4_per_subtree-4 309.3n 307.8n ~ 1.000
SubtreeProcessorRotate/64_per_subtree-4 309.7n 303.9n ~ 0.100
SubtreeProcessorRotate/256_per_subtree-4 309.3n 303.2n ~ 0.100
SubtreeProcessorRotate/1024_per_subtree-4 306.3n 309.1n ~ 0.100
SubtreeNodeAddOnly/4_per_subtree-4 87.43n 87.72n ~ 0.700
SubtreeNodeAddOnly/64_per_subtree-4 64.63n 64.77n ~ 1.000
SubtreeNodeAddOnly/256_per_subtree-4 63.68n 63.73n ~ 1.000
SubtreeNodeAddOnly/1024_per_subtree-4 63.28n 63.23n ~ 0.500
SubtreeCreationOnly/4_per_subtree-4 144.7n 145.4n ~ 0.700
SubtreeCreationOnly/64_per_subtree-4 542.9n 537.2n ~ 0.700
SubtreeCreationOnly/256_per_subtree-4 1.881µ 1.852µ ~ 0.200
SubtreeCreationOnly/1024_per_subtree-4 6.104µ 6.117µ ~ 1.000
SubtreeCreationOnly/2048_per_subtree-4 11.14µ 11.13µ ~ 0.400
SubtreeProcessorOverheadBreakdown/64_per_subtree-4 309.8n 308.9n ~ 0.200
SubtreeProcessorOverheadBreakdown/1024_per_subtree-4 310.2n 308.2n ~ 0.100
ParallelGetAndSetIfNotExists/1k_nodes-4 629.5µ 636.2µ ~ 0.200
ParallelGetAndSetIfNotExists/10k_nodes-4 1.687m 1.688m ~ 1.000
ParallelGetAndSetIfNotExists/50k_nodes-4 8.589m 8.554m ~ 1.000
ParallelGetAndSetIfNotExists/100k_nodes-4 17.33m 17.48m ~ 0.100
SequentialGetAndSetIfNotExists/1k_nodes-4 674.6µ 676.7µ ~ 1.000
SequentialGetAndSetIfNotExists/10k_nodes-4 3.221m 3.221m ~ 0.700
SequentialGetAndSetIfNotExists/50k_nodes-4 12.22m 12.16m ~ 0.100
SequentialGetAndSetIfNotExists/100k_nodes-4 23.55m 23.42m ~ 0.100
ProcessOwnBlockSubtreeNodesParallel/1k_nodes-4 694.2µ 703.7µ ~ 0.700
ProcessOwnBlockSubtreeNodesParallel/10k_nodes-4 4.632m 4.615m ~ 0.700
ProcessOwnBlockSubtreeNodesParallel/100k_nodes-4 20.55m 20.90m ~ 0.100
ProcessOwnBlockSubtreeNodesSequential/1k_nodes-4 736.3µ 748.2µ ~ 0.100
ProcessOwnBlockSubtreeNodesSequential/10k_nodes-4 6.466m 6.538m ~ 0.700
ProcessOwnBlockSubtreeNodesSequential/100k_nodes-4 46.27m 46.01m ~ 0.100
CalcBlockWork-4 471.3n 467.3n ~ 0.700
CalculateWork-4 636.5n 626.9n ~ 0.700
BuildBlockLocatorString_Helpers/Size_10-4 1.334µ 1.349µ ~ 0.100
BuildBlockLocatorString_Helpers/Size_100-4 12.83µ 12.79µ ~ 0.200
BuildBlockLocatorString_Helpers/Size_1000-4 134.1µ 127.3µ ~ 0.700
CatchupWithHeaderCache-4 104.5m 104.4m ~ 1.000
_BufferPoolAllocation/16KB-4 3.418µ 3.385µ ~ 0.700
_BufferPoolAllocation/32KB-4 9.145µ 7.478µ ~ 0.400
_BufferPoolAllocation/64KB-4 14.51µ 17.22µ ~ 0.100
_BufferPoolAllocation/128KB-4 28.94µ 29.61µ ~ 0.100
_BufferPoolAllocation/512KB-4 111.7µ 114.7µ ~ 0.200
_BufferPoolConcurrent/32KB-4 17.20µ 18.11µ ~ 0.400
_BufferPoolConcurrent/64KB-4 27.50µ 28.62µ ~ 0.100
_BufferPoolConcurrent/512KB-4 142.5µ 147.7µ ~ 0.200
_SubtreeDeserializationWithBufferSizes/16KB-4 625.5µ 622.6µ ~ 0.400
_SubtreeDeserializationWithBufferSizes/32KB-4 624.4µ 619.9µ ~ 0.400
_SubtreeDeserializationWithBufferSizes/64KB-4 617.1µ 619.2µ ~ 0.400
_SubtreeDeserializationWithBufferSizes/128KB-4 613.5µ 622.4µ ~ 1.000
_SubtreeDeserializationWithBufferSizes/512KB-4 626.8µ 620.5µ ~ 0.200
_SubtreeDataDeserializationWithBufferSizes/16KB-4 35.35m 35.67m ~ 0.700
_SubtreeDataDeserializationWithBufferSizes/32KB-4 35.92m 35.70m ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/64KB-4 35.67m 35.95m ~ 0.400
_SubtreeDataDeserializationWithBufferSizes/128KB-4 36.06m 35.68m ~ 0.700
_SubtreeDataDeserializationWithBufferSizes/512KB-4 35.92m 35.11m ~ 0.100
_PooledVsNonPooled/Pooled-4 738.0n 737.4n ~ 0.800
_PooledVsNonPooled/NonPooled-4 7.052µ 7.301µ ~ 0.700
_MemoryFootprint/Current_512KB_32concurrent-4 6.961µ 6.872µ ~ 0.400
_MemoryFootprint/Proposed_32KB_32concurrent-4 9.114µ 10.128µ ~ 0.100
_MemoryFootprint/Alternative_64KB_32concurrent-4 8.957µ 9.365µ ~ 0.100
_prepareTxsPerLevel-4 409.6m 424.0m ~ 0.400
_prepareTxsPerLevelOrdered-4 3.873m 3.889m ~ 1.000
_prepareTxsPerLevel_Comparison/Original-4 418.4m 421.0m ~ 0.700
_prepareTxsPerLevel_Comparison/Optimized-4 3.696m 3.605m ~ 0.100
SubtreeSizes/10k_tx_4_per_subtree-4 1.459m 1.467m ~ 1.000
SubtreeSizes/10k_tx_16_per_subtree-4 339.8µ 336.8µ ~ 0.400
SubtreeSizes/10k_tx_64_per_subtree-4 84.83µ 82.53µ ~ 0.700
SubtreeSizes/10k_tx_256_per_subtree-4 20.80µ 21.25µ ~ 0.200
SubtreeSizes/10k_tx_512_per_subtree-4 10.26µ 10.34µ ~ 0.100
SubtreeSizes/10k_tx_1024_per_subtree-4 5.224µ 5.104µ ~ 0.100
SubtreeSizes/10k_tx_2k_per_subtree-4 2.584µ 2.560µ ~ 0.700
BlockSizeScaling/10k_tx_64_per_subtree-4 81.40µ 81.88µ ~ 1.000
BlockSizeScaling/10k_tx_256_per_subtree-4 20.55µ 20.66µ ~ 0.400
BlockSizeScaling/10k_tx_1024_per_subtree-4 5.124µ 5.137µ ~ 1.000
BlockSizeScaling/50k_tx_64_per_subtree-4 419.7µ 403.9µ ~ 0.400
BlockSizeScaling/50k_tx_256_per_subtree-4 101.3µ 102.6µ ~ 0.100
BlockSizeScaling/50k_tx_1024_per_subtree-4 25.32µ 25.59µ ~ 1.000
SubtreeAllocations/small_subtrees_exists_check-4 165.6µ 170.4µ ~ 0.100
SubtreeAllocations/small_subtrees_data_fetch-4 174.2µ 176.6µ ~ 0.100
SubtreeAllocations/small_subtrees_full_validation-4 334.1µ 335.1µ ~ 0.400
SubtreeAllocations/medium_subtrees_exists_check-4 10.17µ 10.22µ ~ 0.700
SubtreeAllocations/medium_subtrees_data_fetch-4 11.17µ 11.01µ ~ 0.400
SubtreeAllocations/medium_subtrees_full_validation-4 20.96µ 21.07µ ~ 0.700
SubtreeAllocations/large_subtrees_exists_check-4 2.548µ 2.521µ ~ 0.400
SubtreeAllocations/large_subtrees_data_fetch-4 2.744µ 2.728µ ~ 0.700
SubtreeAllocations/large_subtrees_full_validation-4 5.289µ 5.287µ ~ 0.700
StoreBlock_Sequential/BelowCSVHeight-4 317.7µ 313.3µ ~ 1.000
StoreBlock_Sequential/AboveCSVHeight-4 312.0µ 309.8µ ~ 1.000
GetUtxoHashes-4 259.0n 262.0n ~ 1.000
GetUtxoHashes_ManyOutputs-4 44.86µ 47.69µ ~ 0.200
_NewMetaDataFromBytes-4 239.4n 238.6n ~ 1.000
_Bytes-4 622.5n 627.9n ~ 0.100
_MetaBytes-4 564.8n 568.8n ~ 0.100

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

…id.OneOf draw

gofmt/gci flagged the three rapid.Int64Range arguments in
Test_propertyDequeueBatchAdmitPredicate for inconsistent comment column
alignment. Align all three comments to the same column.
@sonarqubecloud

Copy link
Copy Markdown

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

LGTM. Verified the contract reformulations match source exactly: queue.go:96 and SubtreeProcessor.go:3789-3796 both reproduce verbatim. Three-tier coverage (queue spec → integration → liveness) with deliberate boundary targeting via Int64Range(batchTimeMs-10, batchTimeMs+10) and enqueue + window + 1ms. No mocks, sqlitememory utxo store, deterministic replay via rapid seeds.

Minor nits, none blocking:

  • t.Cleanup accumulates on outer *testing.T across ~100 rapid iterations (acknowledged in PR). If -rapid.checks ever gets raised, the goroutine/channel footprint grows linearly. A short comment at the call site would prevent surprise.
  • The drainTime.Add(-window).UnixMilli() formula is duplicated in tests 2 and 3 — a small helper would keep the expected-value computation in one place.
  • fmt.Sprintf(ctx, ...) in test 2 is computed unconditionally; trivial, but inline %v args in require.Equal would be cheaper.

@liam liam merged commit 4cdd6c4 into bsv-blockchain:main May 14, 2026
31 of 32 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.

3 participants