@@ -35,26 +35,70 @@ revocable).
3535
3636### Key invariants
3737
38- - ` 1 <= quorum < validators.length <= MAX_VALIDATORS ` (256). The strict
39- inequality between ` quorum ` and ` validators.length ` guarantees that no
40- quorum-sized coalition can brick or fully capture the registry by
41- shrinking the set down to its own membership.
42- - Each validator signature carries a signer-chosen nonce. The nonce is
43- recorded in a per-validator bitmap on successful verify and permanently
44- single-use; sigs at any unused nonce above the validator's floor can be
38+ Every property below holds across every reachable state transition. The
39+ A/B/C/D/E grouping is reused in the invariant test harness so a failing
40+ run points straight at the clause that broke.
41+
42+ ** A. Validator set & quorum structure**
43+
44+ - ** A1.** ` 1 <= quorum < validators.length <= MAX_VALIDATORS ` (256). The
45+ strict inequality between ` quorum ` and ` validators.length ` guarantees
46+ that no quorum-sized coalition can brick or fully capture the registry
47+ by shrinking the set down to its own membership.
48+ - ** A2.** The validator set never contains duplicates and never contains
49+ ` address(0) ` . ` _addValidator ` rejects both; the set itself is a
50+ solady ` EnumerableSetLib.AddressSet ` so duplicates are structurally
51+ impossible.
52+ - ** A3.** The three public views over the validator set
53+ (` validators() ` , ` validatorAt(i) ` , ` isValidator(a) ` ) agree on every
54+ element. ` validatorsLength() ` equals ` validators().length ` .
55+
56+ ** B. Nonce discipline**
57+
58+ - ** B1.** ` validatorNonceFloor[v] ` is monotonically non-decreasing.
59+ ` setValidatorNonce ` is strict-increase; ` _removeValidator ` and
60+ ` revokeAllRoles ` saturate to ` type(uint256).max ` ; no other writer
61+ exists.
62+ - ** B2.** Once a validator has ever been removed, their nonce floor is
63+ ` type(uint256).max ` forever — even if the same address is later
64+ re-added. Combined with the bitmap's permanent-set property this
65+ closes the re-add replay window: no signature the retired address
66+ ever produced can pass ` NonceBelowFloor ` again.
67+ - ** B3.** Each validator signature carries a signer-chosen nonce. The
68+ nonce is recorded in a per-validator bitmap on successful verify and
69+ permanently single-use — a bit set to ` 1 ` never returns to ` 0 ` . Sigs
70+ at any unused nonce at or above the validator's current floor can be
4571 submitted ** in any order** .
46- - A per-validator ` nonceFloor ` is the mass-invalidate lever. Sigs at a
47- nonce strictly below the floor are rejected ` NonceBelowFloor ` . The
48- validator themselves raises the floor via ` setValidatorNonce ` ; removal
49- saturates it to ` type(uint256).max ` so a removed validator's address is
50- permanently retired.
51- - ` isWhitelisted(a) ` never returns ` WhitelistStatus.Whitelisted ` while
52- ` block.timestamp < pausedUntil ` , regardless of underlying storage.
53- - While paused, ` whitelist ` and ` unwhitelist ` revert with ` ContractPaused ` .
54- Compliance ` pauseFor ` / ` unpause ` and every rotation path
55- (` setValidatorNonce ` , all ` *BySig ` admin functions) remain reachable — the
56- circuit breaker exists to buy the quorum time to rotate, not to freeze
57- rotation itself.
72+
73+ ** C. Pause state machine**
74+
75+ - ** C1.** ` pausedUntil ` moves forward via ` pauseFor ` (extend-only,
76+ reverts ` PauseNotExtended ` on any call that wouldn't strictly extend
77+ the current window) and drops to ` 0 ` via ` unpause ` . It never shrinks
78+ to a non-zero value.
79+ - ** C2.** ` isPaused() ` is a pure function of ` pausedUntil ` and
80+ ` block.timestamp ` — no cached pause state anywhere.
81+ - ** C3.** While paused, ` whitelist ` and ` unwhitelist ` revert with
82+ ` ContractPaused ` . Compliance ` pauseFor ` / ` unpause ` and every
83+ rotation path (` setValidatorNonce ` , all ` *BySig ` admin functions)
84+ remain reachable — the circuit breaker exists to buy the quorum time
85+ to rotate, not to freeze rotation itself.
86+
87+ ** D. ` isWhitelisted ` semantics**
88+
89+ - ** D1.** While paused, ` isWhitelisted(a) ` never returns
90+ ` Whitelisted ` or ` NotWhitelisted ` — the return is always one of the
91+ ` Paused* ` variants, regardless of the underlying attestation bit.
92+ - ** D2.** While live (not paused), ` isWhitelisted(a) ` returns exactly
93+ one of ` Whitelisted ` / ` NotWhitelisted ` , matching the ` _isWhitelisted `
94+ bit.
95+
96+ ** E. Ownership irreversibility**
97+
98+ - ** E1.** Once ` owner() ` becomes ` address(0) ` , it stays there.
99+ ` renounceOwnership() ` is the only writer to zero and has no inverse;
100+ the Phase-1 admin surface cannot be re-opened after handover to the
101+ validator quorum.
58102
59103### Return-value enum
60104
0 commit comments