[WIP] Systematic testing: Test the full KV + History + Consensus stack, with deterministic and randomly-explored interleavings - #8238
Draft
Eddy Ashton (eddyashton) wants to merge 12 commits into
Conversation
This was referenced Aug 31, 2026
Adds a new test suite (real_stack_concurrency_test) that drives a real ccf::kv::Store, a real aft::Aft<LedgerStubProxy>, and a real ccf::MerkleTxHistory together, under genuine OS-thread concurrency and genuine raft view changes - something no existing suite does (kv_test, raft_test, and history_test each stub out at least one of these three). - src/kv/test/interleaving.h: a reusable Checkpoint pause/release primitive and a random_delay helper, for pinning or fuzzing thread interleavings without bespoke per-test machinery. - src/consensus/aft/test/real_stack/fixture.h: RealStackFixture, a harness wiring up the real Store + Aft + MerkleTxHistory, with helpers to drive genuine leadership changes and signature commits. - smoke.cpp: non-concurrent sanity checks for the harness itself. - deterministic.cpp: pinned scenarios covering leadership loss/regain around an in-flight commit, including NOTE_REJECTED_COMMIT_STALL, which documents a real bug where a rejected commit can permanently stall replication until a further election. - fuzzer.cpp: a randomised multi-actor fuzzer (writers, election churn, and a continuous reader) checking the same invariants continuously, plus a slower soak variant using real crypto. Several tests exercise NOTE_IS_PRIMARY_RACE, a pre-existing data race in aft::Aft::is_primary(), and are expected to fail occasionally (or abort the process under ThreadSanitizer) until that is fixed. Registered via add_unit_test with DETECT_DEADLOCKS, under a new "concurrency" CTest label.
…ize into src/commit_concurrency - Add a deterministic, cooperative scheduler (commit_concurrency_model_test) that exhaustively (or randomly, for larger scenarios) explores thread interleavings of the real Store + Aft + MerkleTxHistory stack, by wholesale-swapping ccf::pal::Mutex for a scheduler-aware type across the whole test binary via -include, rather than opting individual call sites in. - Add ccf::pal::unique_lock<LockType>, a labeled drop-in replacement for std::unique_lock/std::lock_guard against ccf::pal::Mutex, so a failing schedule's trace can show real, semantic reasons a lock was held/released, not just an opaque mutex handoff. Apply it at the real call sites in store.h, raft.h, and history.h, with a handful of explicit labels at high-value points (Store::commit()/rollback(), force_become_primary()). - Fix a real bug in the scheduler harness found while wiring this up: the driver thread's reserved actor id could write one entry past the per-actor action-tracking vector once real locks started reporting labels unconditionally - fixed by reserving that slot. - Move all commit-concurrency test suite files (previously split across src/kv/test and src/consensus/aft/test) into a single new top-level directory, src/commit_concurrency/. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… ones - DeterministicScheduler::before_lock()/after_unlock() now call choose_next() unconditionally, so the scheduler considers every ready actor at every real lock acquisition and release, not only when a lock is actually contended - closing the gap where two critical sections separated by an uncontended lock boundary (rather than a hand-placed yield_point()) were never explored interleaved. - The reserved driver "actor" (DriverRegistration) is a deliberate exception: it never contends with a real actor for any lock, so its own incidental lock use only updates ownership bookkeeping, never branches. This fixes a real, deterministic hang the above change first exposed: without this, the driver's own lock use while running real application code (e.g. fixture construction) could get "scheduled away" in favour of a real actor thread that had not started yet, with nothing left to ever hand control back. - Converted the single-writer scenario in rejected_commit_stall.cpp from exhaustive search to random sampling, matching the two-writer scenario: estimate_schedule_count() now reports this scenario's interleaving space at roughly 16.7 million to 4.4 trillion schedules (up from an exact 3 when only contended locks branched), making exhaustive search infeasible for any real-stack scenario. - Updated deterministic_scheduler_test.cpp's toy expectations for the resulting increase in schedule count (6 -> 736) now that every lock/ unlock branches. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CCF_GUARDED_BY/CCF_REQUIRES are in real use elsewhere in this codebase (ccf::tasks, src/ds/, etc.), so unconditionally disabling thread-safety analysis for every ccf::pal::unique_lock user was a real regression, not a no-op - it happened to not bite yet only because none of store.h/ raft.h/impl/state.h/history.h currently use CCF_GUARDED_BY. ccf::pal::unique_lock now carries its own CCF_SCOPED_CAPABILITY/ CCF_ACQUIRE/CCF_RELEASE/CCF_TRY_ACQUIRE annotations, mirroring MutexGuard, giving real static verification for the ordinary case. Clang's built-in std::unique_lock support additionally understands a conditionally-taken lock (construct with std::defer_lock, only sometimes .lock()/.try_lock() depending on runtime state) well enough to verify it; that specific pattern isn't supported for a user-annotated type, and needs an explicit opt-out. Rather than disabling analysis project-wide, CCF_NO_THREAD_SAFETY_ANALYSIS is applied narrowly to just the 3 real call sites that use this pattern (Store::commit_deserialised(), and the two signature-emission paths in history.h), each with a comment explaining why. Also folds in the previous, already-staged Waypoint -> YieldPoint rename and removal of the dead, uncalled set_action(). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…date per-actor state - Add ActorEventKind::Requested, reported by before_lock() as its own decision point before checking whether the lock is even free. Without this, whichever actor happened to be running when it reached an uncontended lock always won it unconditionally - no other actor ever got a chance to reach for the same lock first, since only one actor's code runs at a time. This was a real gap in interleaving coverage, not just a display gap. - Rewrite describe() as a genuine one-event-per-line stream instead of listing every ready-but-not-chosen actor at each decision. Since only one actor's code ever runs at a time, the actor that triggers decision i is always exactly whoever was chosen at decision i-1 - so each line can name that actor and its event directly (e.g. "writer 0 acquires version_lock, elector resumes"), with "resumes" only shown when a different actor is chosen next. decision_path() keeps the full ready/chosen_index data other callers (e.g. backtracking) still need; only the human-facing rendering changed. - Consolidate the three same-sized, separately-indexed std::vectors (finished, blocked_on_lock, current_event) into one std::vector<ActorState>, centralising the +1-for-the-driver sizing reasoning in one place instead of three. - Update deterministic_scheduler_test.cpp's stale schedule-count comment and bound (736 -> 10968), now that Requested adds a third decision point per lock life-cycle. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…pstream #8242 ("Reject stale-view writes before local commit") landed on main during this session's rebase, independently fixing the exact bug the NOTE_REJECTED_COMMIT_STALL-tagged tests here were built to catch - all of them now pass reliably (deterministic and 500-sample model-checked runs alike), so the old "expected to fail until fixed" framing and tag were stale. - Removed the NOTE_REJECTED_COMMIT_STALL tag and its defining comment; the DOCTEST_CHECKs it marked are ordinary passing assertions now. - Reworded the two affected test cases' comments to note they are regression tests for #8242, and to explain what they add beyond kv_test.cpp's own direct, single-threaded test of the same rejection (driving it through a real election instead, and cross-checking TxHistory and raft's own replication index). - Removed the now-defunct middle nullptr (version_resolver) argument from one CommittableTx::commit() call, matching #8242's own signature change and migration note. Comments deliberately avoid narrating what #8242 changed or how - that's what git history is for, and it rots fast; only "this is a regression test for #8242" is kept, since the tests would otherwise look like ordinary, low-value assertions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
"Model checking" implies formal/exhaustive verification, which is misleading here: this codebase already has a genuinely exhaustive, TLC-based model-checking concept (.github/workflows/ci-verification.yml), and this suite's own real-stack scenarios only use explore_random_interleavings() (sampling), not exhaustive search. - src/commit_concurrency/model_checked/ -> scheduled/, alongside deterministic_scheduler.h, deterministic_scheduler_test.cpp, and interleaving_lock_override.h, which now live there too (nothing outside this directory uses any of them). This leaves src/commit_concurrency/ with exactly two peer subdirectories - threaded/ (real OS threads, seeded but not exactly replayable) and scheduled/ (single-process, cooperative, byte-for-byte replayable) - and no loose top-level files. - commit_concurrency_model_test -> commit_concurrency_scheduled_test (CMake target/binary), commit_concurrency_model -> commit_concurrency_scheduled (doctest suite tag). - rejected_commit_stall.cpp -> rejected_commit.cpp: the old name was overly specific to one particular way a rejected commit could go wrong. - src/commit_concurrency/interleaving.h -> threaded/checkpoint.h (+ its test file -> threaded/checkpoint_test.cpp, suite tag "interleaving" -> "checkpoint"): this is a distinct, complementary primitive - a manual pause/release rendezvous for real OS threads - not part of the scheduled/ stack, so "interleavings" was freed up for that instead. - Comments reworded throughout to describe "systematically exploring the interleaving space" (exhaustively where the space is small enough, randomly sampling otherwise), rather than "model checking". Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…irectly The #error-guarded macro required 4 general production headers (store.h, raft.h, impl/state.h, history.h) to know about one specific test target's build assumptions, to protect against a scenario that was never actually happening (none of ccf_kv's/ccf_tasks' own sources include any of those headers) - and provided no more protection than this replacement against a genuinely new future library nobody thought to guard, since it only ever covered libraries that explicitly opted in to the macro. - Removed the #error block from all 4 headers entirely. - ccf_kv's and ccf_tasks' source lists are now shared CMake variables (CCF_KV_SOURCES/CCF_TASKS_SOURCES), used by both their real library targets and commit_concurrency_scheduled_test, which recompiles them directly (as it already did for ccf_tasks) instead of linking the prebuilt library - avoiding an ODR violation by construction, not by assertion. - Removed CCF_STATIC_LIBRARY_BUILD from ccf_kv, ccf_tasks, and ccfcrypto (in cmake/crypto.cmake) entirely. - Added ccf_forbid_layout_sensitive_libraries(), defined directly next to its one call site rather than in cmake/common.cmake, as a safety net: fails CMake configure if ccf_kv/ccf_tasks are ever linked into this target directly instead of recompiled - verified this actually fires for both libraries, then reverted the deliberate breakage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… interception The previous design recompiled ccf_kv's/ccf_tasks' own sources directly into commit_concurrency_scheduled_test (rather than linking the normal, shared libraries) purely so that ccf::pal::Mutex could be swapped, at compile time, for a distinct SchedulerMutex type. This worked, but meant every real lock call site anywhere in the guarded headers had to be recompiled under a different type just to be observed - more moving parts than the goal (deterministically scheduling real lock/unlock calls) actually requires. - ccf::pal::Mutex is now a single, permanent type everywhere, always. Its lock()/try_lock()/unlock() stash their (optional) label in a thread-local hint immediately before making their real call - a small, generically useful piece of always-on introspection state, not itself aware of any test or scheduler. - src/commit_concurrency/scheduled/pthread_mutex_wrap.cpp intercepts the real pthread_mutex_lock/unlock/trylock calls at link time (via -Wl,--wrap=..., only for commit_concurrency_scheduled_test), reads that hint to identify a genuine ccf::pal::Mutex call, and diverts it to DeterministicScheduler's before_lock()/after_unlock() instead of ever reaching the real mutex - with no need to track any mutex's address, and no risk of misattributing an unrelated lock (allocator, iostream, the scheduler's own bookkeeping mutex, etc.), verified empirically before relying on it. - Removed entirely: SchedulerMutex, interleaving_lock_override.h, the -include compile flag, and the CCF_KV_SOURCES/CCF_TASKS_SOURCES recompilation machinery - commit_concurrency_scheduled_test now links ccf_kv/ccf_tasks completely normally, exactly like commit_concurrency_ test does. - Added a smoke test proving interception genuinely engages (checked it actually fails if the -Wl,--wrap=... flags are ever dropped - which separately also fails to link at all in that case). - try_lock() under an active scheduler now aborts with a clear message rather than throwing: std::mutex::try_lock() is noexcept, so an exception escaping it would call std::terminate() anyway, with less control over the diagnostic than doing so explicitly. Validated: exact exhaustive schedule count for the toy scenario in deterministic_scheduler_test.cpp is unchanged (10968); full project build succeeds; all test suites pass in both the normal and TSAN configurations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Eddy Ashton (eddyashton)
force-pushed
the
agents/real-stack-concurrency-fuzz-testing-main
branch
from
September 4, 2026 08:32
f01170a to
d305358
Compare
…currency-fuzz-testing-main Conflicts in include/ccf/pal/locking.h, src/kv/store.h, src/consensus/aft/raft.h, and src/node/history.h, from upstream's "Break the crypto, ds, and pal dependency cycle" (#8265): it moved Mutex/MutexGuard/ConditionVariable from ccf::pal to ccf::ds (leaving ccf::pal as a deprecated compatibility alias), and mechanically reverted store.h/raft.h/history.h's few dozen lock call sites from our own label-carrying ccf::pal::unique_lock back to plain std::lock_guard/ std::unique_lock (since our custom wrapper never existed upstream). Resolved by taking upstream's version of all 4 files wholesale (their functional changes preserved verbatim, including a genuine, unrelated fix to Store::rollback()'s chunker/snapshot ordering), then moving our own label-and-hook mechanism to its new home: ccf::ds::Mutex's own lock()/try_lock()/unlock() now take an optional label (as ccf::pal::Mutex used to), and ccf::ds::unique_lock<LockType> (as ccf::pal::unique_lock used to) is the labeled replacement for std::unique_lock, both now living in include/ccf/ds/locking.h. Only the 8 call sites that carried a genuinely chosen label (4 in store.h, 2 in raft.h, 2 in history.h) were converted back to ccf::ds::unique_lock; every other call site keeps upstream's plain std::lock_guard/std::unique_lock exactly as-is, since a label was never functionally required for the pthread_mutex_wrap.cpp interception to work - only for scheduler describe() output to be more readable at the handful of sites where that mattered enough to name. Updated src/commit_concurrency/scheduled/ to reference ccf::ds instead of ccf::pal throughout for the same reason. Validated: full project build succeeds; all 5 concurrency-adjacent suites (kv_test, raft_test, history_test, commit_concurrency_test, commit_concurrency_scheduled_test) pass in both the normal and TSAN configurations.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This is extremely sketchy, just opening it here for early discussion.
Robot brief explanation
src/commit_concurrency/: a new test suite for KV/Raft/History concurrencyProblem: no existing suite drives a real
ccf::kv::Store, a realaft::Aft, and a realMerkleTxHistorytogether under concurrency. Every existing suite stubs at least one of the three, so bugs in how they interact under real locking/threading go unfound.What's added, two complementary test binaries:
commit_concurrency_test— real OS threads, real timing. ACheckpointprimitive (interleaving.h) lets scenarios pin specific rendezvous points; a randomized fuzzer runs writers, elections, and readers concurrently and checks cross-component invariants (history vs. store vs. raft all agreeing on committed state).commit_concurrency_model_test— a deterministic scheduler that wholesale-swaps everyccf::pal::Mutexin the binary for a scheduler-aware type (via-include, so no call site opts in individually), and treats every lock acquisition/release (not just contended ones) as a branch point in the search. It can exhaust a scenario's interleavings or sample them randomly with a reproducible seed, and every failing schedule prints a full, human-readable decision trace showing real, semantic lock labels from production code itself (e.g.writer (serialise concurrent Store::commit() calls),elector (roll version and history back to tx_id)), not just an opaque mutex handoff.Status: two known-bad scenarios are currently red on purpose (tagged
NOTE_REJECTED_COMMIT_STALLat the specific broken assertions, with one canonical explanation of the underlying bug), reproducing a real gap in how a commit in flight interacts with a concurrent election: a transaction rejected for a stale view can leave a local write applied to the Store that never reaches consensus, and subsequent ordinary commits can keep failing to replicate until a further election restores agreement. Random sampling shows this is reachable via a wide swath of the interleaving space, not one freak timing (though note: uniform sampling over the abstract schedule space is not representative of real-world timing likelihood — it says something about the shape of the bug, not its production frequency). Both binaries build clean under TSAN and pass the existingkv_test/raft_test/history_testsuites unchanged.Known limitation flagged in-repo (
TODOindeterministic_scheduler.h): branching on every single lock/unlock makes the search space for any real-stack scenario enormous (millions to 10^26+ schedules), forcing reliance on random sampling rather than exhaustive search. The next planned step is a per-scenario filter — using the labels already threaded through — to let a test choose a small, targeted set of "interesting" decision points (e.g. specifically the unlock ofversion_lockinStore::commit()) while everything else fast-passes through unbranched. Intended workflow: random search over the full space to find violations, promote each violation to a pinned deterministic regression test, then fuzz narrowly around those known points for cheap ongoing coverage.Note for reviewers: this PR is for discussion/sharing, not merge-ready —
scripts/ci-checks.shwill fail on the intentionalTODO:comment (repo convention normally bansTODO/FIXMEoutright) and on the two deliberately-red test scenarios described above.Robot verbose explanation
Motivation
We currently have no test suite that drives a real
ccf::kv::Store, a realaft::Aft, and a realccf::MerkleTxHistorytogether under concurrency. Every existing suite stubs at least one of the three:kv_test— realStore, but consensus is always a stub (StubConsensus/PrimaryStubConsensus/etc.), andTxHistoryis absent or hand-rolled.raft_test— realAft, but the store isLoggingStubStore, with no real KV semantics or conflict detection.history_test— realStoreandMerkleTxHistory, but consensus is a hand-rolled, single-threaded fake.This gap matters: a prior investigation this session found and fixed a real bug in
Store::commit()'s interaction withStore::rollback()under a concurrent view change, and confirming it required building bespoke pause/rendezvous machinery by hand rather than reusing anything. This PR is the generalized version of that machinery, built as its own suite rather than living insidekv_test.This is additive only. No existing stub classes or test files are touched, consolidated, or removed — they remain legitimate, narrowly-scoped unit tests for their own layers.
What's in
src/commit_concurrency/Two complementary test binaries, both wired into
CMakeLists.txtviaadd_unit_test(...)withDETECT_DEADLOCKS, both under a newconcurrencyCTest label:commit_concurrency_test— real OS threads, real timing.interleaving.hprovides aCheckpointprimitive: any thread can pause at a named point and a controller thread decides when to release it, generalizing the ad hoc pattern from the original bug investigation into one reusable, named primitive.threaded/deterministic.cppports the two hand-pinned scenarios from that investigation across as a harness sanity check, plus additional scenarios covering election-churn variations your review specifically asked about (stepping down to backup vs. reaching candidate again mid-commit).threaded/fuzzer.cpplayers a randomized multi-actor fuzzer on top: N writer threads, an election-churn actor, and a reader thread continuously pollingstore->current_txid()againsthistory's reported state — mirroring the actual production access pattern infrontend.hthat made the original bug externally observable. Seeded (std::mt19937), logged on failure for reproducibility.commit_concurrency_model_test— a deterministic, single-process scheduler for exhaustive/random exploration of interleavings, rather than relying on real OS thread timing to hit a specific ordering.ccf::pal::Mutexitself is wholesale-swapped for a scheduler-aware type across the whole binary via-include(interleaving_lock_override.h), so every real lock instore.h/raft.h/history.hparticipates without any call site opting in.ccf::tasks' own sources are recompiled into this target (not linked from the precompiledccf_tasks.a), because it keeps a process-wide singleton job board that needs the same scheduler-aware locking to stay consistent across explored schedules.deterministic_scheduler.htreats every lock acquisition and release as a decision point (not just contended ones — see "Known limitations" below), plus explicityield_point()s for branching at points with no lock at all.ccf::pal::unique_lock<LockType>(new, ininclude/ccf/pal/locking.h) is a labeled drop-in replacement forstd::unique_lock/std::lock_guard, applied at all 62 real lock call sites across the three production headers. A failing schedule's trace shows real, human-readable reasons a lock was held (e.g.writer (serialise concurrent Store::commit() calls),elector (roll version and history back to tx_id)), not just an opaque sequence of mutex handoffs — this was deliberately built to make failures diagnosable by a human, not just a pass/fail signal.explore_all_interleavings()exhausts a scenario via depth-first search with replay;explore_random_interleavings()samples a fixed, seeded number of schedules when the space is too large;estimate_schedule_count()gives a cheap random-walk estimate of a scenario's true size before committing to either.model_checked/rejected_commit_stall.cppreproduces the same bug as the threaded suite, but via this exhaustive/random mechanism instead of hand-pinned timing.Four production headers (
kv/store.h,consensus/aft/raft.h,consensus/aft/impl/state.h,node/history.h) gained aCCF_STATIC_LIBRARY_BUILD-guarded#error, enforcing they can never be compiled intoccf_kv/ccf_tasks/ccfcrypto— this is what makes it safe forcommit_concurrency_model_testto recompile them under a different lock type without an ODR violation, and it's enforced at compile time, not just by convention.Current status: two scenarios are deliberately red
Per explicit direction this session: tests should assert the correct expected behavior and fail honestly when that behavior isn't yet implemented, rather than being skipped or asserting the current (buggy) behavior. Both
deterministic.cppandrejected_commit_stall.cpphave aNOTE_REJECTED_COMMIT_STALL-tagged test/assertions documenting a real gap: a transaction rejected byStore::commit()for a stale view can leave a local write applied to the Store with no corresponding entry ever reaching consensus — and, worse, every ordinary transaction committed afterwards can keep succeeding locally without reaching consensus either, until a further election restores agreement.Randomly sampling the model-checked version of this scenario (500 samples each, two variants) shows 733 of 1000 sampled schedules that reach the risky state go on to violate the invariant. That's evidence the bug is reachable through a broad part of the interleaving space, not one narrow, contrived ordering — though I want to flag explicitly that this is not a claim about real-world frequency: the scheduler samples uniformly over abstract decision points, which has no relationship to real timing (a lock held for nanoseconds vs. an election taking milliseconds of network round trips). Treat it as evidence about the shape of the bug, not its production likelihood.
Validation
kv_test,raft_test,history_testall pass unchanged (28k+/1M+/46 assertions respectively).commit_concurrency_testandcommit_concurrency_model_testboth build and run cleanly, producing the two known-bad results deterministically across repeated runs.-DTSAN=ONwith zero data race warnings — meaningful given the whole point of this suite is real concurrent access to shared production state.scripts/cpp-format-checks.sh,copyright-checks.sh,includes-checks.sh, andascii-checks.shall pass.What's deliberately not merge-ready, and why this is a draft
scripts/todo-checks.shwill fail. There's an intentionalTODO:comment indeterministic_scheduler.h(repo convention otherwise bansTODO/FIXMEoutright) describing the single most valuable next step, left in deliberately for this PR's discussion rather than filed elsewhere and forgotten.ctestwill report failures for this suite until the underlyingstore.hbug is fixed.Known limitation and the planned next step
Branching on every lock/unlock (rather than only contended ones) makes the search space for any real-stack scenario enormous —
estimate_schedule_count()reports roughly 16.7 million to 4.4 trillion schedules for the single-writer scenario alone, and up to ~10²⁶ for the two-writer one. That forces reliance on random sampling rather than exhaustive search for anything beyond toy scenarios, which is a real loss of rigor compared to what exhaustive search would give.The planned fix (see the
TODOindeterministic_scheduler.h): treat every lock/unlock/yield_point()as only a candidate decision point, and let a per-scenario predicate — matched against the semantic labels already being reported, so no further production code changes are needed — decide which of them actually branch the search versus fast-passing through unchanged. This lets a test dial the search space down to exactly the handful of points it cares about (e.g. specifically the unlock ofversion_lockinStore::commit()), rather than an all-or-nothing choice between "every lock branches" (intractable) and "only explicityield_point()s branch" (may silently miss semantic-lock-ordering bugs). The intended workflow once this exists: random search over the full, unfiltered space to find violations, promote each violation into a deterministic regression test pinned to its exact decision sequence, then fuzz narrowly around those known points for cheap, ongoing, targeted coverage.Explicitly out of scope for this PR
Consolidating or replacing the existing stub zoo (
StubConsensusfamily,LoggingStubStore,DummyConsensus/CompactingConsensus/RollbackConsensus) is a separate, higher-risk decision, not bundled here.