Mutant created against commit b821585.
+1 -1 · cpp
This description is AI generated, mistakes can be made.
Intent: Break the BIP 9 rule that the signalling tally covers the whole retarget period: the oldest block of the window (the first block of the period) is never examined, so a deployment needs threshold signals out of period - 1 blocks. The case for calling this critical rather than high is that it is a pure consensus divergence with no attacker needed; the case for a lower label is that it only bites when the period is within one block of the threshold.
Behaviour change: Original tallies blocks [first..last] of the period (nPeriod blocks). Mutant tallies [first+1..last] (nPeriod-1 blocks). Whenever the first block of the period signals and the remaining blocks give exactly threshold-1 ... threshold signals, the two disagree on whether the period reaches threshold, i.e. on LOCKED_IN vs STARTED/FAILED.
Trigger: A mainnet deployment period in which exactly threshold (1916) of the 2016 blocks signal and one of the signalling blocks is the first block of the period: unpatched nodes go LOCKED_IN and, one period later, ACTIVE and start enforcing the new rules; a mutated node stays STARTED. Reproducible in regtest with -vbparams testdummy:: and a 144-block period containing exactly 108 signals including block 0 of the period.
Spec violation: BIP 9: 'we tally the bits set ... int count = 0; walk = block; for (i = 0; i < 2016; i++) { walk = walk.parent; if (...) count++; } if (count >= threshold) return LOCKED_IN;' - the tally is over the full 2016-block retarget period.
Severity if undetected: critical
Why tests miss it: walked all six chains of versionbits_tests.cpp:versionbits_test by hand (the marginal period at Mine(3000,...) has a non-signalling block 2000 in both the FAILED and the exact-900 LOCKED_IN scenario, so the count is unchanged); both chains of check_computeblockversion (firstChain never signals; secondChain signals every block, 143 >= 108); rpc_blockchain.py's 144-block all-signalling period at height 287 (143 >= 108); grepped src/test and test/functional for other callers of GetStateFor
Plausibility: Off-by-one in a hand-written walk-back loop is the single most common mistake in this kind of code, and i = 1 reads like a deliberate 'the first step is the block itself' adjustment - especially since BIP 9's own pseudocode advances walk = walk.parent before testing, which invites exactly this confusion.
Equivalence risk: It is a no-op for any period where the first block's signal is not decisive - which is almost every period in practice, including every period in the existing tests. It changes an observable state only when the tally sits exactly on the threshold boundary.
Commands and environment reported by the submitter
cmake -B build -DBUILD_GUI=OFF -DWITH_ZMQ=OFF -DENABLE_IPC=OFF && cmake --build build -j$(nproc)ctest --test-dir build -j$(nproc) && build/test/functional/test_runner.py -j$(nproc)mutant-harness operator: boundary; location: AbstractThresholdConditionChecker::GetStateFor (STARTED case) lines 87-92
Imported from mutant-harness 6cd939b.
Commands are shown as text and never executed by MutantHub.
0 comments
No comments yet
Discuss equivalence, reproduction difficulties, or a test that kills this mutant.