Repository: taikoxyz/taiko-mono
Pull request #19113: feat(protocol): multiple improvements for PreconfWhitelist2 
Author: login: dantaik
Labels: name: area.protocol
Created: 2025-03-18T02:30:56Z | Merged: 2025-03-19T06:38:02Z

BEGIN_COMMIT_OVERRIDE
feat(protocol): allow owner to change "operator change delay" (even to zero)
feat(protocol): allow operators to remove themselves from the whitelist
refactor(protocol): optimize getOperatorForCurrentEpoch gas cost
END_COMMIT_OVERRIDE


Diff:
diff --git a/.github/workflows/repo--merge-gatekeeper.yml b/.github/workflows/repo--merge-gatekeeper.yml
index 70171f00d0e..78f230b7227 100644
--- a/.github/workflows/repo--merge-gatekeeper.yml
+++ b/.github/workflows/repo--merge-gatekeeper.yml
@@ -21,6 +21,8 @@ jobs:
       - name: Run Merge Gatekeeper
         uses: upsidr/merge-gatekeeper@v1
         with:
-          timeout: 5400
-          interval: 30
+          timeout: 1800 
+          interval: 60
+          debug: true
+          strict: true
           token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/packages/protocol/.gas-snapshot b/packages/protocol/.gas-snapshot
index df1937cb759..57efaaa60ab 100644
--- a/packages/protocol/.gas-snapshot
+++ b/packages/protocol/.gas-snapshot
@@ -70,9 +70,9 @@ InboxTest_ProposeAndProve:test_inbox_reprove_the_same_batch_with_same_transition
 InboxTest_ProposeAndProve:test_inbox_ring_buffer_will_be_reused() (gas: 14706617)
 InboxTest_ProposeAndProve:test_proposeBatch_reverts_for_invalid_proposer_and_operator() (gas: 165337)
 InboxTest_StopBatch:test_inbox_num_batches_verified() (gas: 8163061)
-PreconfRouterTest:test_preconfRouter_proposeBatch() (gas: 381935)
-PreconfRouterTest:test_preconfRouter_proposeBatch_notOperator() (gas: 333101)
-PreconfRouterTest:test_preconfRouter_proposeBatch_proposerNotSender() (gas: 380666)
+PreconfRouterTest:test_preconfRouter_proposeBatch() (gas: 382790)
+PreconfRouterTest:test_preconfRouter_proposeBatch_notOperator() (gas: 333956)
+PreconfRouterTest:test_preconfRouter_proposeBatch_proposerNotSender() (gas: 381521)
 SendMessageToDelegateOwner:testAddress1() (gas: 2391)
 TestAutomataDcapV3Attestation:testAttestation() (gas: 4848498)
 TestAutomataDcapV3Attestation:testParsedCustomQuoteBinAttestation() (gas: 4553350)
@@ -97,21 +97,26 @@ TestMerkleClaimable:test_verifyClaim_twice_while_its_ongoing() (gas: 66484)
 TestMerkleClaimable:test_verifyClaim_when_it_ends() (gas: 61402)
 TestMerkleClaimable:test_verifyClaim_when_it_starts() (gas: 61389)
 TestMerkleClaimable:test_verifyClaim_with_invalid_proofs_while_its_ongoing() (gas: 48963)
-TestPreconfWhitelist2:test_whitelist2_addOrRemoveTheSameOperatorTwiceWillRevert() (gas: 98789)
-TestPreconfWhitelist2:test_whitelist2_addThenRemoveOneOperator() (gas: 235682)
-TestPreconfWhitelist2:test_whitelist2_addThenRemoveTwoOperators() (gas: 302991)
-TestPreconfWhitelist2:test_whitelist2_consolidate_whenEmpty_not_revert() (gas: 14441)
-TestPreconfWhitelist2:test_whitelist2_removeNonExistingOperatorWillRevert() (gas: 22924)
-TestPreconfWhitelist:test_addOperator() (gas: 93468)
-TestPreconfWhitelist:test_addOperator_alreadyAdded() (gas: 96116)
+TestPreconfWhitelist2:test_whitelist2_addBackRemovedOperator() (gas: 106253)
+TestPreconfWhitelist2:test_whitelist2_addOrRemoveTheSameOperatorTwiceWillRevert() (gas: 83349)
+TestPreconfWhitelist2:test_whitelist2_consolidate_whenEmpty_not_revert() (gas: 19691)
+TestPreconfWhitelist2:test_whitelist2_delay2epoch_addThenRemoveOneOperator() (gas: 257137)
+TestPreconfWhitelist2:test_whitelist2_delay2epoch_addThenRemoveTwoOperators() (gas: 305697)
+TestPreconfWhitelist2:test_whitelist2_noDelay_addThenRemoveOneOperator() (gas: 154492)
+TestPreconfWhitelist2:test_whitelist2_removeNonExistingOperatorWillRevert() (gas: 25132)
+TestPreconfWhitelist2:test_whitelist2_selfRemoval() (gas: 174648)
+TestPreconfWhitelist2:test_whitelistNoDelay_consolidationPreservesOrder() (gas: 252778)
+TestPreconfWhitelist2:test_whitelistNoDelay_consolidationWillNotChangeCurrentEpochOperator() (gas: 299367)
+TestPreconfWhitelist:test_addOperator() (gas: 93753)
+TestPreconfWhitelist:test_addOperator_alreadyAdded() (gas: 96401)
 TestPreconfWhitelist:test_addOperator_invalidAddress() (gas: 18126)
 TestPreconfWhitelist:test_addOperator_onlyOwner() (gas: 28873)
 TestPreconfWhitelist:test_getOperatorForCurrentEpoch_emptyList() (gas: 15559)
-TestPreconfWhitelist:test_getOperatorForCurrentEpoch_immediateRoot() (gas: 316879)
-TestPreconfWhitelist:test_getOperatorForCurrentEpoch_iteratedRoot() (gas: 323323)
-TestPreconfWhitelist:test_removeOperator() (gas: 181946)
-TestPreconfWhitelist:test_removeOperator_invalidIndex() (gas: 100765)
-TestPreconfWhitelist:test_removeOperator_onlyOwner() (gas: 101094)
+TestPreconfWhitelist:test_getOperatorForCurrentEpoch_immediateRoot() (gas: 317734)
+TestPreconfWhitelist:test_getOperatorForCurrentEpoch_iteratedRoot() (gas: 324178)
+TestPreconfWhitelist:test_removeOperator() (gas: 183083)
+TestPreconfWhitelist:test_removeOperator_invalidIndex() (gas: 101050)
+TestPreconfWhitelist:test_removeOperator_onlyOwner() (gas: 101379)
 TestTokenUnlock:test_tokenunlock_delegate() (gas: 192919)
 TestTokenUnlock:test_tokenunlock_multiple_vest_withdrawal() (gas: 200996)
 TestTokenUnlock:test_tokenunlock_multiple_vest_withdrawing() (gas: 444309)
diff --git a/packages/protocol/contract_layout_layer1.md b/packages/protocol/contract_layout_layer1.md
index 235c4c8c1ad..b39acd3418a 100644
--- a/packages/protocol/contract_layout_layer1.md
+++ b/packages/protocol/contract_layout_layer1.md
@@ -1383,6 +1383,10 @@
 |
 | operatorCount               | uint8                                                     | 253  | 0      | 1     |
 |
+| operatorChangeDelay         | uint8                                                     | 253  | 1      | 1     |
+|
+| havingPerfectOperators      | bool                                                      | 253  | 2      | 1     |
+|
 | __gap                       | uint256[47]                                               | 254  | 0      | 1504  |
 ╰-----------------------------+-----------------------------------------------------------+------+--------+-------+-----------------------------------------------------------------------╯
 
diff --git a/packages/protocol/contracts/layer1/preconf/iface/IPreconfWhitelist.sol b/packages/protocol/contracts/layer1/preconf/iface/IPreconfWhitelist.sol
index a2e185707ca..d96dd460566 100644
--- a/packages/protocol/contracts/layer1/preconf/iface/IPreconfWhitelist.sol
+++ b/packages/protocol/contracts/layer1/preconf/iface/IPreconfWhitelist.sol
@@ -6,11 +6,13 @@ pragma solidity ^0.8.24;
 interface IPreconfWhitelist {
     /// @notice Emitted when a new operator is added to the whitelist.
     /// @param operator The address of the operator that was added.
-    event OperatorAdded(address indexed operator);
+    /// @param activeSince The timestamp when the operator became active.
+    event OperatorAdded(address indexed operator, uint256 activeSince);
 
     /// @notice Emitted when an operator is removed from the whitelist.
     /// @param operator The address of the operator that was removed.
-    event OperatorRemoved(address indexed operator);
+    /// @param inactiveSince The timestamp when the operator became inactive.
+    event OperatorRemoved(address indexed operator, uint256 inactiveSince);
 
     error InvalidOperatorIndex();
     error InvalidOperatorCount();
diff --git a/packages/protocol/contracts/layer1/preconf/impl/PreconfWhitelist.sol b/packages/protocol/contracts/layer1/preconf/impl/PreconfWhitelist.sol
index ddb3e25e254..bfb9abb497b 100644
--- a/packages/protocol/contracts/layer1/preconf/impl/PreconfWhitelist.sol
+++ b/packages/protocol/contracts/layer1/preconf/impl/PreconfWhitelist.sol
@@ -39,7 +39,7 @@ contract PreconfWhitelist is EssentialContract, IPreconfWhitelist {
 
         isOperator[_operatorAddress] = true;
 
-        emit OperatorAdded(_operatorAddress);
+        emit OperatorAdded(_operatorAddress, block.timestamp);
     }
 
     /// @inheritdoc IPreconfWhitelist
@@ -62,7 +62,7 @@ contract PreconfWhitelist is EssentialContract, IPreconfWhitelist {
 
         isOperator[removedOperator] = false;
 
-        emit OperatorRemoved(removedOperator);
+        emit OperatorRemoved(removedOperator, block.timestamp);
     }
 
     /// @inheritdoc IPreconfWhitelist
diff --git a/packages/protocol/contracts/layer1/preconf/impl/PreconfWhitelist2.sol b/packages/protocol/contracts/layer1/preconf/impl/PreconfWhitelist2.sol
index e50499ef2c7..09d9189dfb5 100644
--- a/packages/protocol/contracts/layer1/preconf/impl/PreconfWhitelist2.sol
+++ b/packages/protocol/contracts/layer1/preconf/impl/PreconfWhitelist2.sol
@@ -16,82 +16,101 @@ contract PreconfWhitelist2 is EssentialContract, IPreconfWhitelist {
         uint8 index; // Index in operatorMapping.
     }
 
-    event Consolidated();
+    event Consolidated(uint8 previousCount, uint8 newCount, bool havingPerfectOperators);
+    event OperatorChangeDelaySet(uint8 delay);
 
     mapping(address operator => OperatorInfo info) public operators;
     mapping(uint256 index => address operator) public operatorMapping;
     uint8 public operatorCount;
+    uint8 public operatorChangeDelay; // in epochs
+
+    // all operators in operatorMapping are active and none of them is to be deactivated.
+    bool public havingPerfectOperators;
 
     uint256[47] private __gap;
 
-    function init(address _owner) external initializer {
+    constructor(address _resolver) EssentialContract(_resolver) { }
+
+    function init(address _owner, uint8 _operatorChangeDelay) external initializer {
         __Essential_init(_owner);
+        operatorChangeDelay = _operatorChangeDelay;
+        havingPerfectOperators = true;
     }
 
-    constructor(address _resolver) EssentialContract(_resolver) { }
+    function setOperatorChangeDelay(uint8 _operatorChangeDelay) external onlyOwner {
+        operatorChangeDelay = _operatorChangeDelay;
+        emit OperatorChangeDelaySet(_operatorChangeDelay);
+    }
 
     /// @inheritdoc IPreconfWhitelist
     function addOperator(address _operator) external onlyOwner {
-        require(_operator != address(0), InvalidOperatorAddress());
-        require(operators[_operator].activeSince == 0, OperatorAlreadyExists());
-
-        uint8 _operatorCount = operatorCount;
-        operators[_operator] = OperatorInfo({
-            activeSince: epochStartTimestamp(2),
-            inactiveSince: 0, // no removal scheduled.
-            index: _operatorCount
-        });
-        operatorMapping[_operatorCount] = _operator;
-        unchecked {
-            operatorCount = _operatorCount + 1;
-        }
-
-        emit OperatorAdded(_operator);
+        _addOperator(_operator, operatorChangeDelay);
     }
 
     /// @inheritdoc IPreconfWhitelist
     function removeOperator(uint256 _operatorIndex) external onlyOwner {
         require(_operatorIndex < operatorCount, InvalidOperatorIndex());
-        _removeOperator(operatorMapping[_operatorIndex]);
+        _removeOperator(operatorMapping[_operatorIndex], operatorChangeDelay);
+    }
+
+    /// @notice Removes an operator by address who will become inactive in two epochs.
+    /// @param _operator The address of the operator to remove.
+    /// @param _effectiveImmediately True if the removal should be effective immediately.
+    function removeOperator(address _operator, bool _effectiveImmediately) external onlyOwner {
+        _removeOperator(_operator, _effectiveImmediately ? 0 : operatorChangeDelay);
     }
 
     /// @notice Removes an operator by address who will become inactive in two epochs.
     /// @param _operator The address of the operator to remove.
     function removeOperator(address _operator) external onlyOwner {
-        _removeOperator(_operator);
+        _removeOperator(_operator, operatorChangeDelay);
+    }
+
+    /// @notice Allows the caller to remove themselves as an operator immediately.
+    function removeSelf() external {
+        _removeOperator(msg.sender, 0);
     }
 
     /// @notice Consolidates the operator mapping by removing operators whose removal epoch has
-    /// passed, swapping removed operators with the last entry, and decrementing the operatorCount.
+    /// passed, maintaining the order of active operators, and decrementing the operatorCount.
     function consolidate() external {
         uint64 currentEpoch = epochStartTimestamp(0);
         uint8 i;
-        uint8 _operatorCount = operatorCount;
+        uint8 _previousCount = operatorCount;
+        uint8 _operatorCount = _previousCount;
+
+        bool _havingPerfectOperators = true;
 
         while (i < _operatorCount) {
             address operator = operatorMapping[i];
             OperatorInfo memory info = operators[operator];
+
             // Check if the operator is scheduled for removal and the removal epoch has passed
             if (info.inactiveSince != 0 && info.inactiveSince <= currentEpoch) {
-                uint8 lastIndex = _operatorCount - 1;
-                // Only perform swap if not the last element
-                if (i != lastIndex) {
-                    address lastOperator = operatorMapping[lastIndex];
-                    operators[lastOperator].index = i;
-                    operatorMapping[i] = lastOperator;
+                // Shift all subsequent operators one position to the left
+                for (uint8 j = i; j < _operatorCount - 1; j++) {
+                    address nextOperator = operatorMapping[j + 1];
+                    operators[nextOperator].index = j;
+                    operatorMapping[j] = nextOperator;
                 }
-                // Remove the operator from the mapping
+                // Remove the last operator as it has been shifted
                 delete operators[operator];
-                delete operatorMapping[lastIndex];
-                _operatorCount--;
-                // Do not increment i to check the swapped entry
+                delete operatorMapping[--_operatorCount];
+                // Do not increment i to check the new entry at position i
             } else {
+                if (_havingPerfectOperators) {
+                    if (info.activeSince == 0 || info.activeSince > currentEpoch) {
+                        _havingPerfectOperators = false;
+                    }
+                }
+
                 ++i;
             }
         }
 
         operatorCount = _operatorCount;
-        emit Consolidated();
+        havingPerfectOperators = _havingPerfectOperators;
+        emit Consolidated(_previousCount, _operatorCount, _havingPerfectOperators);
     }
 
     /// @inheritdoc IPreconfWhitelist
@@ -104,6 +123,18 @@ contract PreconfWhitelist2 is EssentialContract, IPreconfWhitelist {
         return _getOperatorForEpoch(epochStartTimestamp(1));
     }
 
+    /// @notice Returns the operator candidates for the current epoch.
+    /// @return An array of addresses representing the operator candidates.
+    function getOperatorCandidatesForCurrentEpoch() external view returns (address[] memory) {
+        return _getOperatorCandidatesForEpoch(epochStartTimestamp(0));
+    }
+
+    /// @notice Returns the operator candidates for the next epoch.
+    /// @return An array of addresses representing the operator candidates.
+    function getOperatorCandidatesForNextEpoch() external view returns (address[] memory) {
+        return _getOperatorCandidatesForEpoch(epochStartTimestamp(1));
+    }
+
     // Returns true if the operator is active in the given epoch.
     function isOperatorActive(
         address _operator,
@@ -124,12 +155,59 @@ contract PreconfWhitelist2 is EssentialContract, IPreconfWhitelist {
         }
     }
 
-    function epochStartTimestamp(uint256 offset) public view returns (uint64) {
+    function epochStartTimestamp(uint256 _offset) public view returns (uint64) {
         return uint64(
-            LibPreconfUtils.getEpochTimestamp() + offset * LibPreconfConstants.SECONDS_IN_EPOCH
+            LibPreconfUtils.getEpochTimestamp() + _offset * LibPreconfConstants.SECONDS_IN_EPOCH
         );
     }
 
+    function _addOperator(address _operator, uint8 _operatorChangeDelay) internal {
+        require(_operator != address(0), InvalidOperatorAddress());
+        require(operators[_operator].activeSince == 0, OperatorAlreadyExists());
+
+        uint8 _operatorCount = operatorCount;
+        uint64 activeSince = epochStartTimestamp(_operatorChangeDelay);
+        operators[_operator] = OperatorInfo({
+            activeSince: activeSince,
+            inactiveSince: 0, // no removal scheduled.
+            index: _operatorCount
+        });
+        operatorMapping[_operatorCount] = _operator;
+        unchecked {
+            operatorCount = _operatorCount + 1;
+        }
+
+        if (_operatorChangeDelay != 0) {
+            havingPerfectOperators = false;
+        }
+
+        emit OperatorAdded(_operator, activeSince);
+    }
+
+    function _removeOperator(address _operator, uint8 _operatorChangeDelay) internal {
+        require(_operator != address(0), InvalidOperatorAddress());
+        OperatorInfo memory info = operators[_operator];
+        require(info.inactiveSince == 0, OperatorAlreadyRemoved());
+        require(info.activeSince != 0, InvalidOperatorAddress());
+
+        uint8 _lastOperatorIndex = operatorCount - 1;
+        if (_operatorChangeDelay == 0 && operators[_operator].index == _lastOperatorIndex) {
+            // If delay is 0 and operator is the last one, remove directly
+            delete operators[_operator];
+            delete operatorMapping[_lastOperatorIndex];
+            operatorCount = _lastOperatorIndex;
+            emit OperatorRemoved(_operator, block.timestamp);
+        } else {
+            uint64 inactiveSince = epochStartTimestamp(_operatorChangeDelay);
+            operators[_operator].inactiveSince = inactiveSince;
+            operators[_operator].activeSince = 0;
+
+            havingPerfectOperators = false;
+            emit OperatorRemoved(_operator, inactiveSince);
+        }
+    }
+
+    /// @dev The cost of this function is primarily linear with respect to operatorCount.
     function _getOperatorForEpoch(uint64 _epochTimestamp) internal view returns (address) {
         if (_epochTimestamp < LibPreconfConstants.SECONDS_IN_EPOCH) {
             return address(0);
@@ -137,40 +215,48 @@ contract PreconfWhitelist2 is EssentialContract, IPreconfWhitelist {
 
         // Use the previous epoch's start timestamp as the random number, if it is not available
         // (zero), return address(0) directly.
-        bytes32 root = LibPreconfUtils.getBeaconBlockRoot(
-            _epochTimestamp - LibPreconfConstants.SECONDS_IN_EPOCH
+        uint256 rand = uint256(
+            LibPreconfUtils.getBeaconBlockRoot(
+                _epochTimestamp - LibPreconfConstants.SECONDS_IN_EPOCH
+            )
         );
 
-        if (root == 0 || operatorCount == 0) return address(0);
+        if (rand == 0) return address(0);
+
+        uint256 _operatorCount = operatorCount;
+        if (_operatorCount == 0) return address(0);
 
-        uint8 candidateindex = uint8(uint256(root) % operatorCount);
-        for (uint8 i; i < operatorCount; ++i) {
-            address candidate = operatorMapping[candidateindex];
-            if (isOperatorActive(candidate, _epochTimestamp)) {
-                return candidate;
+        if (havingPerfectOperators) {
+            return operatorMapping[rand % _operatorCount];
+        } else {
+            address[] memory candidates = new address[](_operatorCount);
+            uint256 count;
+            for (uint256 i; i < _operatorCount; ++i) {
+                address operator = operatorMapping[i];
+                if (isOperatorActive(operator, _epochTimestamp)) {
+                    candidates[count++] = operator;
+                }
             }
-            candidateindex = (candidateindex + 1) % operatorCount;
+            if (count == 0) return address(0);
+            return candidates[rand % count];
         }
-
-        return address(0);
     }
 
-    /// @notice Remove an operator.
-    /// @dev Normally, removal is scheduled for current + 2 epochs.
-    /// If the operator has not yet become active, it will be active for one epoch and removed in
-    /// activeSince + 1 epoch.
-    function _removeOperator(address operator) internal {
-        require(operator != address(0), InvalidOperatorAddress());
-        OperatorInfo memory info = operators[operator];
-        require(info.activeSince != 0, InvalidOperatorAddress());
-        require(info.inactiveSince == 0, OperatorAlreadyRemoved());
-
-        uint64 inactiveSince = epochStartTimestamp(2);
-        if (inactiveSince <= info.activeSince) {
-            inactiveSince = info.activeSince + uint64(LibPreconfConstants.SECONDS_IN_EPOCH);
+    function _getOperatorCandidatesForEpoch(uint64 _epochTimestamp)
+        internal
+        view
+        returns (address[] memory operators_)
+    {
+        operators_ = new address[](operatorCount);
+        uint256 count;
+        for (uint256 i; i < operatorCount; ++i) {
+            if (isOperatorActive(operatorMapping[i], _epochTimestamp)) {
+                operators_[count++] = operatorMapping[i];
+            }
         }
-        operators[operator].inactiveSince = inactiveSince;
 
-        emit OperatorRemoved(operator);
+        assembly {
+            mstore(operators_, count)
+        }
     }
 }
diff --git a/packages/protocol/test/layer1/preconf/whitelist/PeconfWhitelist2.t.sol b/packages/protocol/test/layer1/preconf/whitelist/PeconfWhitelist2.t.sol
index 82f3d790d2f..84fad0e51a6 100644
--- a/packages/protocol/test/layer1/preconf/whitelist/PeconfWhitelist2.t.sol
+++ b/packages/protocol/test/layer1/preconf/whitelist/PeconfWhitelist2.t.sol
@@ -7,6 +7,7 @@ import "../mocks/MockBeaconBlockRoot.sol";
 
 contract TestPreconfWhitelist2 is Layer1Test {
     PreconfWhitelist2 internal whitelist;
+    PreconfWhitelist2 internal whitelistNoDelay;
     address internal whitelistOwner;
     BeaconBlockRootImpl internal beaconBlockRootImpl;
 
@@ -16,14 +17,22 @@ contract TestPreconfWhitelist2 is Layer1Test {
             deploy({
                 name: "preconf_whitelist2",
                 impl: address(new PreconfWhitelist2(address(resolver))),
-                data: abi.encodeCall(PreconfWhitelist2.init, (whitelistOwner))
+                data: abi.encodeCall(PreconfWhitelist2.init, (whitelistOwner, 2))
+            })
+        );
+
+        whitelistNoDelay = PreconfWhitelist2(
+            deploy({
+                name: "preconf_whitelist2",
+                impl: address(new PreconfWhitelist2(address(resolver))),
+                data: abi.encodeCall(PreconfWhitelist2.init, (whitelistOwner, 0))
             })
         );
 
         vm.warp(LibPreconfConstants.SECONDS_IN_SLOT + LibPreconfConstants.SECONDS_IN_EPOCH);
     }
 
-    function test_whitelist2_addThenRemoveOneOperator() external {
+    function test_whitelist2_delay2epoch_addThenRemoveOneOperator() external {
         _setBeaconBlockRoot(bytes32(uint256(7)));
 
         assertEq(whitelist.getOperatorForCurrentEpoch(), address(0));
@@ -34,10 +43,12 @@ contract TestPreconfWhitelist2 is Layer1Test {
 
         assertEq(whitelist.operatorCount(), 1);
         assertEq(whitelist.operatorMapping(0), Bob);
+        assertEq(whitelist.havingPerfectOperators(), false);
 
         whitelist.consolidate();
         assertEq(whitelist.operatorCount(), 1);
         assertEq(whitelist.operatorMapping(0), Bob);
+        assertEq(whitelist.havingPerfectOperators(), false);
 
         (uint64 activeSince, uint64 inactiveSince, uint8 index) = whitelist.operators(Bob);
         assertEq(activeSince, whitelist.epochStartTimestamp(2));
@@ -56,19 +67,23 @@ contract TestPreconfWhitelist2 is Layer1Test {
         assertEq(whitelist.getOperatorForCurrentEpoch(), Bob);
         assertEq(whitelist.getOperatorForNextEpoch(), Bob);
 
+        whitelist.consolidate();
+        assertEq(whitelist.havingPerfectOperators(), true);
+
         vm.prank(whitelistOwner);
         whitelist.removeOperator(Bob);
 
         assertEq(whitelist.operatorCount(), 1);
         assertEq(whitelist.operatorMapping(0), Bob);
+        assertEq(whitelist.havingPerfectOperators(), false);
 
         whitelist.consolidate();
         assertEq(whitelist.operatorCount(), 1);
         assertEq(whitelist.operatorMapping(0), Bob);
+        assertEq(whitelist.havingPerfectOperators(), false);
 
-        uint256 oldActiveSince = activeSince;
         (activeSince, inactiveSince, index) = whitelist.operators(Bob);
-        assertEq(activeSince, oldActiveSince);
+        assertEq(activeSince, 0);
         assertEq(inactiveSince, whitelist.epochStartTimestamp(2));
         assertEq(index, 0);
 
@@ -86,9 +101,10 @@ contract TestPreconfWhitelist2 is Layer1Test {
         whitelist.consolidate();
         assertEq(whitelist.operatorCount(), 0);
         assertEq(whitelist.operatorMapping(0), address(0));
+        assertEq(whitelist.havingPerfectOperators(), true);
     }
 
-    function test_whitelist2_addThenRemoveTwoOperators() external {
+    function test_whitelist2_delay2epoch_addThenRemoveTwoOperators() external {
         _setBeaconBlockRoot(bytes32(uint256(7)));
 
         assertEq(whitelist.getOperatorForCurrentEpoch(), address(0));
@@ -102,11 +118,13 @@ contract TestPreconfWhitelist2 is Layer1Test {
         assertEq(whitelist.operatorCount(), 2);
         assertEq(whitelist.operatorMapping(0), Alice);
         assertEq(whitelist.operatorMapping(1), Bob);
+        assertEq(whitelist.havingPerfectOperators(), false);
 
         whitelist.consolidate();
         assertEq(whitelist.operatorCount(), 2);
         assertEq(whitelist.operatorMapping(0), Alice);
         assertEq(whitelist.operatorMapping(1), Bob);
+        assertEq(whitelist.havingPerfectOperators(), false);
 
         (uint64 activeSince, uint64 inactiveSince, uint8 index) = whitelist.operators(Alice);
         assertEq(activeSince, whitelist.epochStartTimestamp(2));
@@ -129,6 +147,7 @@ contract TestPreconfWhitelist2 is Layer1Test {
         _advanceOneEpoch();
         assertEq(whitelist.getOperatorForCurrentEpoch(), Bob);
         assertEq(whitelist.getOperatorForNextEpoch(), Bob);
+        assertEq(whitelist.havingPerfectOperators(), false);
 
         vm.prank(whitelistOwner);
         whitelist.removeOperator(Alice);
@@ -143,14 +162,15 @@ contract TestPreconfWhitelist2 is Layer1Test {
         assertEq(whitelist.operatorCount(), 2);
         assertEq(whitelist.operatorMapping(0), Alice);
         assertEq(whitelist.operatorMapping(1), Bob);
+        assertEq(whitelist.havingPerfectOperators(), false);
 
         (activeSince, inactiveSince, index) = whitelist.operators(Alice);
-        assertTrue(activeSince != 0);
+        assertEq(activeSince, 0);
         assertEq(inactiveSince, whitelist.epochStartTimestamp(2));
         assertEq(index, 0);
 
         (activeSince, inactiveSince, index) = whitelist.operators(Bob);
-        assertTrue(activeSince != 0);
+        assertEq(activeSince, 0);
         assertEq(inactiveSince, whitelist.epochStartTimestamp(2));
         assertEq(index, 1);
 
@@ -169,6 +189,7 @@ contract TestPreconfWhitelist2 is Layer1Test {
         assertEq(whitelist.operatorCount(), 0);
         assertEq(whitelist.operatorMapping(0), address(0));
         assertEq(whitelist.operatorMapping(1), address(0));
+        assertEq(whitelist.havingPerfectOperators(), true);
     }
 
     function test_whitelist2_addOrRemoveTheSameOperatorTwiceWillRevert() external {
@@ -183,6 +204,51 @@ contract TestPreconfWhitelist2 is Layer1Test {
         vm.stopPrank();
     }
 
+    function test_whitelist2_addBackRemovedOperator() external {
+        vm.startPrank(whitelistOwner);
+        whitelist.addOperator(Alice);
+
+        whitelist.removeOperator(Alice);
+
+        whitelist.addOperator(Alice);
+        vm.stopPrank();
+    }
+
+    function test_whitelist2_selfRemoval() external {
+        vm.startPrank(whitelistOwner);
+        whitelist.addOperator(Alice);
+        whitelist.addOperator(Bob);
+        vm.stopPrank();
+
+        vm.prank(Alice);
+        whitelist.removeSelf();
+        assertEq(whitelist.operatorCount(), 2);
+        assertEq(whitelist.operatorMapping(0), Alice);
+        assertEq(whitelist.operatorMapping(1), Bob);
+
+        assertEq(whitelist.getOperatorForCurrentEpoch(), address(0));
+        assertEq(whitelist.getOperatorForNextEpoch(), address(0));
+        assertEq(whitelist.havingPerfectOperators(), false);
+
+        whitelist.consolidate();
+        assertEq(whitelist.operatorCount(), 1);
+        assertEq(whitelist.operatorMapping(0), Bob);
+        assertEq(whitelist.havingPerfectOperators(), false);
+
+        vm.prank(Bob);
+        whitelist.removeSelf();
+
+        assertEq(whitelist.operatorCount(), 0);
+
+        assertEq(whitelist.getOperatorForCurrentEpoch(), address(0));
+        assertEq(whitelist.getOperatorForNextEpoch(), address(0));
+        assertEq(whitelist.havingPerfectOperators(), false);
+
+        whitelist.consolidate();
+        assertEq(whitelist.operatorCount(), 0);
+        assertEq(whitelist.havingPerfectOperators(), true);
+    }
+
     function test_whitelist2_removeNonExistingOperatorWillRevert() external {
         vm.startPrank(whitelistOwner);
         vm.expectRevert(IPreconfWhitelist.InvalidOperatorAddress.selector);
@@ -192,6 +258,89 @@ contract TestPreconfWhitelist2 is Layer1Test {
 
     function test_whitelist2_consolidate_whenEmpty_not_revert() external {
         whitelist.consolidate();
+        assertEq(whitelist.havingPerfectOperators(), true);
+    }
+
+    function test_whitelist2_noDelay_addThenRemoveOneOperator() external {
+        _setBeaconBlockRoot(bytes32(uint256(7)));
+
+        vm.prank(whitelistOwner);
+        whitelistNoDelay.addOperator(Bob);
+
+        assertEq(whitelistNoDelay.operatorCount(), 1);
+        assertEq(whitelistNoDelay.operatorMapping(0), Bob);
+        assertEq(whitelistNoDelay.havingPerfectOperators(), true);
+
+        (uint64 activeSince, uint64 inactiveSince, uint8 index) = whitelistNoDelay.operators(Bob);
+        assertEq(activeSince, whitelistNoDelay.epochStartTimestamp(0));
+        assertEq(inactiveSince, 0);
+        assertEq(index, 0);
+
+        assertEq(whitelistNoDelay.getOperatorForCurrentEpoch(), Bob);
+        assertEq(whitelistNoDelay.getOperatorForNextEpoch(), Bob);
+
+        vm.prank(whitelistOwner);
+        whitelistNoDelay.removeOperator(Bob);
+
+        assertEq(whitelistNoDelay.operatorCount(), 0);
+        assertEq(whitelistNoDelay.havingPerfectOperators(), true);
+
+        whitelistNoDelay.consolidate();
+        assertEq(whitelistNoDelay.operatorCount(), 0);
+        assertEq(whitelistNoDelay.havingPerfectOperators(), true);
+
+        assertEq(whitelistNoDelay.getOperatorForCurrentEpoch(), address(0));
+        assertEq(whitelistNoDelay.getOperatorForNextEpoch(), address(0));
+    }
+
+    function test_whitelistNoDelay_consolidationPreservesOrder() external {
+        _setBeaconBlockRoot(bytes32(uint256(7)));
+
+        vm.startPrank(whitelistOwner);
+        whitelistNoDelay.addOperator(Alice);
+        whitelistNoDelay.addOperator(Bob);
+        whitelistNoDelay.addOperator(Carol);
+
+        address[] memory candidates = whitelistNoDelay.getOperatorCandidatesForCurrentEpoch();
+        assertEq(candidates.length, 3);
+        assertEq(candidates[0], Alice);
+        assertEq(candidates[1], Bob);
+        assertEq(candidates[2], Carol);
+        assertEq(whitelistNoDelay.havingPerfectOperators(), true);
+
+        whitelistNoDelay.removeOperator(Alice);
+        assertEq(whitelistNoDelay.havingPerfectOperators(), false);
+
+        whitelistNoDelay.consolidate();
+        candidates = whitelistNoDelay.getOperatorCandidatesForCurrentEpoch();
+        assertEq(candidates.length, 2);
+        assertEq(candidates[0], Bob);
+        assertEq(candidates[1], Carol);
+        assertEq(whitelistNoDelay.havingPerfectOperators(), true);
+
+        vm.stopPrank();
+    }
+
+    function test_whitelistNoDelay_consolidationWillNotChangeCurrentEpochOperator() external {
+        _setBeaconBlockRoot(bytes32(uint256(5)));
+
+        vm.startPrank(whitelistOwner);
+        whitelistNoDelay.addOperator(Alice);
+        whitelistNoDelay.addOperator(Bob);
+        whitelistNoDelay.addOperator(Carol);
+        whitelistNoDelay.addOperator(David);
+        whitelistNoDelay.removeOperator(Alice);
+        assertEq(whitelistNoDelay.havingPerfectOperators(), false);
+
+        address operatorBeforeConsolidate = whitelistNoDelay.getOperatorForCurrentEpoch();
+
+        whitelistNoDelay.consolidate();
+
+        address operatorAfterConsolidate = whitelistNoDelay.getOperatorForCurrentEpoch();
+        assertEq(operatorBeforeConsolidate, operatorAfterConsolidate);
+        assertEq(whitelistNoDelay.havingPerfectOperators(), true);
+
+        vm.stopPrank();
     }
 
     function _setBeaconBlockRoot(bytes32 _root) internal {


Reviews:
user: login: smartprogrammer93
state: DISMISSED
author_association: COLLABORATOR
submitted_at: 2025-03-18T02:46:10Z
user: login: YoGhurt111
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-18T04:26:57Z
user: login: smtmfft
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T04:34:18Z
user: login: YoGhurt111
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-18T04:41:17Z
user: login: linoscope
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-18T05:15:21Z
user: login: linoscope
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-18T05:16:13Z
user: login: AnshuJalan
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-18T05:52:14Z
user: login: AnshuJalan
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-18T05:53:30Z
user: login: dantaik
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T07:44:35Z
user: login: dantaik
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T07:45:24Z
user: login: smartprogrammer93
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-18T07:49:01Z
user: login: dantaik
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T08:34:10Z
user: login: dantaik
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T08:34:38Z
user: login: AnshuJalan
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-18T08:57:12Z
user: login: linoscope
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-18T09:52:14Z
user: login: linoscope
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-18T09:57:47Z
user: login: dantaik
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T09:59:22Z
user: login: dantaik
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T11:23:06Z
user: login: dantaik
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T11:26:07Z
user: login: linoscope
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-18T12:22:59Z
user: login: linoscope
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-18T12:32:50Z
user: login: linoscope
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-18T12:33:37Z
user: login: linoscope
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-18T12:34:38Z
user: login: smartprogrammer93
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-18T22:45:19Z
user: login: dantaik
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T02:19:53Z
user: login: YoGhurt111
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-19T06:08:34Z
user: login: mask-pp
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T06:08:51Z
user: login: smtmfft
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T06:09:06Z
user: login: davidtaikocha
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-19T06:12:26Z
user: login: mask-pp
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T06:37:10Z

Review comments:
diff_hunk: @@ -16,61 +16,62 @@ contract PreconfWhitelist2 is EssentialContract, IPreconfWhitelist {
         uint8 index; // Index in operatorMapping.
     }
 
-    event Consolidated();
+    event Consolidated(uint8 previousCount, uint8 newCount);
+    event OperatorChangeDelaySet(uint8 delay);
 
     mapping(address operator => OperatorInfo info) public operators;
     mapping(uint256 index => address operator) public operatorMapping;
     uint8 public operatorCount;
+    uint8 public operatorChangeDelay; // in epochs
 
     uint256[47] private __gap;
 
-    function init(address _owner) external initializer {
+    constructor(address _resolver) EssentialContract(_resolver) { }
+
+    function init(address _owner, uint8 _operatorChangeDelay) external initializer {
         __Essential_init(_owner);
+        operatorChangeDelay = _operatorChangeDelay;
     }
 
-    constructor(address _resolver) EssentialContract(_resolver) { }
+    function setOperatorChangeDelay(uint8 _operatorChangeDelay) external onlyOwner {
+        operatorChangeDelay = _operatorChangeDelay;
+        emit OperatorChangeDelaySet(_operatorChangeDelay);
+    }
 
     /// @inheritdoc IPreconfWhitelist
     function addOperator(address _operator) external onlyOwner {
-        require(_operator != address(0), InvalidOperatorAddress());
-        require(operators[_operator].activeSince == 0, OperatorAlreadyExists());
-
-        uint8 _operatorCount = operatorCount;
-        operators[_operator] = OperatorInfo({
-            activeSince: epochStartTimestamp(2),
-            inactiveSince: 0, // no removal scheduled.
-            index: _operatorCount
-        });
-        operatorMapping[_operatorCount] = _operator;
-        unchecked {
-            operatorCount = _operatorCount + 1;
-        }
-
-        emit OperatorAdded(_operator);
+        _addOperator(_operator, operatorChangeDelay);
     }
 
     /// @inheritdoc IPreconfWhitelist
     function removeOperator(uint256 _operatorIndex) external onlyOwner {
         require(_operatorIndex < operatorCount, InvalidOperatorIndex());
-        _removeOperator(operatorMapping[_operatorIndex]);
+        _removeOperator(operatorMapping[_operatorIndex], operatorChangeDelay);
     }
 
     /// @notice Removes an operator by address who will become inactive in two epochs.
     /// @param _operator The address of the operator to remove.
     function removeOperator(address _operator) external onlyOwner {
-        _removeOperator(_operator);
+        _removeOperator(_operator, operatorChangeDelay);
+    }
+
+    //// @notice Allows the caller to remove themselves as an operator immediately.
path: packages/protocol/contracts/layer1/preconf/impl/PreconfWhitelist2.sol
user: login: linoscope
body: Nit: Think the additional `/` is unintentional
```suggestion
    /// @notice Allows the caller to remove themselves as an operator immediately.
```
created_at: 2025-03-18T04:48:11Z
author_association: COLLABORATOR
diff_hunk: @@ -16,61 +16,62 @@ contract PreconfWhitelist2 is EssentialContract, IPreconfWhitelist {
         uint8 index; // Index in operatorMapping.
     }
 
-    event Consolidated();
+    event Consolidated(uint8 previousCount, uint8 newCount);
+    event OperatorChangeDelaySet(uint8 delay);
 
     mapping(address operator => OperatorInfo info) public operators;
     mapping(uint256 index => address operator) public operatorMapping;
     uint8 public operatorCount;
+    uint8 public operatorChangeDelay; // in epochs
 
     uint256[47] private __gap;
 
-    function init(address _owner) external initializer {
+    constructor(address _resolver) EssentialContract(_resolver) { }
+
+    function init(address _owner, uint8 _operatorChangeDelay) external initializer {
         __Essential_init(_owner);
+        operatorChangeDelay = _operatorChangeDelay;
     }
 
-    constructor(address _resolver) EssentialContract(_resolver) { }
+    function setOperatorChangeDelay(uint8 _operatorChangeDelay) external onlyOwner {
+        operatorChangeDelay = _operatorChangeDelay;
+        emit OperatorChangeDelaySet(_operatorChangeDelay);
+    }
 
     /// @inheritdoc IPreconfWhitelist
     function addOperator(address _operator) external onlyOwner {
-        require(_operator != address(0), InvalidOperatorAddress());
-        require(operators[_operator].activeSince == 0, OperatorAlreadyExists());
-
-        uint8 _operatorCount = operatorCount;
-        operators[_operator] = OperatorInfo({
-            activeSince: epochStartTimestamp(2),
-            inactiveSince: 0, // no removal scheduled.
-            index: _operatorCount
-        });
-        operatorMapping[_operatorCount] = _operator;
-        unchecked {
-            operatorCount = _operatorCount + 1;
-        }
-
-        emit OperatorAdded(_operator);
+        _addOperator(_operator, operatorChangeDelay);
     }
 
     /// @inheritdoc IPreconfWhitelist
     function removeOperator(uint256 _operatorIndex) external onlyOwner {
         require(_operatorIndex < operatorCount, InvalidOperatorIndex());
-        _removeOperator(operatorMapping[_operatorIndex]);
+        _removeOperator(operatorMapping[_operatorIndex], operatorChangeDelay);
     }
 
     /// @notice Removes an operator by address who will become inactive in two epochs.
     /// @param _operator The address of the operator to remove.
     function removeOperator(address _operator) external onlyOwner {
-        _removeOperator(_operator);
+        _removeOperator(_operator, operatorChangeDelay);
+    }
+
+    //// @notice Allows the caller to remove themselves as an operator immediately.
+    function removeSelf() external {
+        _removeOperator(msg.sender, 0);
     }
 
     /// @notice Consolidates the operator mapping by removing operators whose removal epoch has
     /// passed, swapping removed operators with the last entry, and decrementing the operatorCount.
     function consolidate() external {
path: packages/protocol/contracts/layer1/preconf/impl/PreconfWhitelist2.sol
user: login: linoscope
body: Can't this function be called by anyone, and as a result, potentially change the return value of `getOperatorForCurrentEpoch()` by changing the ordering of the `operators`? 
created_at: 2025-03-18T05:14:55Z
author_association: COLLABORATOR
diff_hunk: @@ -16,61 +16,62 @@ contract PreconfWhitelist2 is EssentialContract, IPreconfWhitelist {
         uint8 index; // Index in operatorMapping.
     }
 
-    event Consolidated();
+    event Consolidated(uint8 previousCount, uint8 newCount);
+    event OperatorChangeDelaySet(uint8 delay);
 
     mapping(address operator => OperatorInfo info) public operators;
     mapping(uint256 index => address operator) public operatorMapping;
     uint8 public operatorCount;
+    uint8 public operatorChangeDelay; // in epochs
 
     uint256[47] private __gap;
 
-    function init(address _owner) external initializer {
+    constructor(address _resolver) EssentialContract(_resolver) { }
+
+    function init(address _owner, uint8 _operatorChangeDelay) external initializer {
         __Essential_init(_owner);
+        operatorChangeDelay = _operatorChangeDelay;
     }
 
-    constructor(address _resolver) EssentialContract(_resolver) { }
+    function setOperatorChangeDelay(uint8 _operatorChangeDelay) external onlyOwner {
+        operatorChangeDelay = _operatorChangeDelay;
+        emit OperatorChangeDelaySet(_operatorChangeDelay);
+    }
 
     /// @inheritdoc IPreconfWhitelist
     function addOperator(address _operator) external onlyOwner {
-        require(_operator != address(0), InvalidOperatorAddress());
-        require(operators[_operator].activeSince == 0, OperatorAlreadyExists());
-
-        uint8 _operatorCount = operatorCount;
-        operators[_operator] = OperatorInfo({
-            activeSince: epochStartTimestamp(2),
-            inactiveSince: 0, // no removal scheduled.
-            index: _operatorCount
-        });
-        operatorMapping[_operatorCount] = _operator;
-        unchecked {
-            operatorCount = _operatorCount + 1;
-        }
-
-        emit OperatorAdded(_operator);
+        _addOperator(_operator, operatorChangeDelay);
     }
 
     /// @inheritdoc IPreconfWhitelist
     function removeOperator(uint256 _operatorIndex) external onlyOwner {
         require(_operatorIndex < operatorCount, InvalidOperatorIndex());
-        _removeOperator(operatorMapping[_operatorIndex]);
+        _removeOperator(operatorMapping[_operatorIndex], operatorChangeDelay);
     }
 
     /// @notice Removes an operator by address who will become inactive in two epochs.
     /// @param _operator The address of the operator to remove.
     function removeOperator(address _operator) external onlyOwner {
-        _removeOperator(_operator);
+        _removeOperator(_operator, operatorChangeDelay);
+    }
+
+    //// @notice Allows the caller to remove themselves as an operator immediately.
+    function removeSelf() external {
+        _removeOperator(msg.sender, 0);
     }
 
     /// @notice Consolidates the operator mapping by removing operators whose removal epoch has
     /// passed, swapping removed operators with the last entry, and decrementing the operatorCount.
     function consolidate() external {
path: packages/protocol/contracts/layer1/preconf/impl/PreconfWhitelist2.sol
user: login: linoscope
body: ~I think we want to swap the removed operator with the next operator in the `operators` array (not the last), but that requires shifting a lot of indexes..~ Edit: Well, even then the shifted indexes can change the result of `getOperatorForCurrentEpoch`    h
created_at: 2025-03-18T05:16:13Z
author_association: COLLABORATOR
diff_hunk: @@ -124,12 +125,43 @@ contract PreconfWhitelist2 is EssentialContract, IPreconfWhitelist {
         }
     }
 
-    function epochStartTimestamp(uint256 offset) public view returns (uint64) {
+    function epochStartTimestamp(uint256 _offset) public view returns (uint64) {
         return uint64(
-            LibPreconfUtils.getEpochTimestamp() + offset * LibPreconfConstants.SECONDS_IN_EPOCH
+            LibPreconfUtils.getEpochTimestamp() + _offset * LibPreconfConstants.SECONDS_IN_EPOCH
         );
     }
 
+    function _addOperator(address _operator, uint8 _operatorChangeDelay) internal {
+        require(_operator != address(0), InvalidOperatorAddress());
+        require(operators[_operator].activeSince == 0, OperatorAlreadyExists());
path: packages/protocol/contracts/layer1/preconf/impl/PreconfWhitelist2.sol
user: login: AnshuJalan
body: A removed operator cannot be re-added since `activeSince` for a removed operator is not reset. Is this intended? 
created_at: 2025-03-18T05:33:17Z
author_association: COLLABORATOR
diff_hunk: @@ -16,61 +16,62 @@ contract PreconfWhitelist2 is EssentialContract, IPreconfWhitelist {
         uint8 index; // Index in operatorMapping.
     }
 
-    event Consolidated();
+    event Consolidated(uint8 previousCount, uint8 newCount);
+    event OperatorChangeDelaySet(uint8 delay);
 
     mapping(address operator => OperatorInfo info) public operators;
     mapping(uint256 index => address operator) public operatorMapping;
     uint8 public operatorCount;
+    uint8 public operatorChangeDelay; // in epochs
 
     uint256[47] private __gap;
 
-    function init(address _owner) external initializer {
+    constructor(address _resolver) EssentialContract(_resolver) { }
+
+    function init(address _owner, uint8 _operatorChangeDelay) external initializer {
         __Essential_init(_owner);
+        operatorChangeDelay = _operatorChangeDelay;
     }
 
-    constructor(address _resolver) EssentialContract(_resolver) { }
+    function setOperatorChangeDelay(uint8 _operatorChangeDelay) external onlyOwner {
+        operatorChangeDelay = _operatorChangeDelay;
+        emit OperatorChangeDelaySet(_operatorChangeDelay);
+    }
 
     /// @inheritdoc IPreconfWhitelist
     function addOperator(address _operator) external onlyOwner {
-        require(_operator != address(0), InvalidOperatorAddress());
-        require(operators[_operator].activeSince == 0, OperatorAlreadyExists());
-
-        uint8 _operatorCount = operatorCount;
-        operators[_operator] = OperatorInfo({
-            activeSince: epochStartTimestamp(2),
-            inactiveSince: 0, // no removal scheduled.
-            index: _operatorCount
-        });
-        operatorMapping[_operatorCount] = _operator;
-        unchecked {
-            operatorCount = _operatorCount + 1;
-        }
-
-        emit OperatorAdded(_operator);
+        _addOperator(_operator, operatorChangeDelay);
     }
 
     /// @inheritdoc IPreconfWhitelist
     function removeOperator(uint256 _operatorIndex) external onlyOwner {
         require(_operatorIndex < operatorCount, InvalidOperatorIndex());
-        _removeOperator(operatorMapping[_operatorIndex]);
+        _removeOperator(operatorMapping[_operatorIndex], operatorChangeDelay);
     }
 
     /// @notice Removes an operator by address who will become inactive in two epochs.
     /// @param _operator The address of the operator to remove.
     function removeOperator(address _operator) external onlyOwner {
-        _removeOperator(_operator);
+        _removeOperator(_operator, operatorChangeDelay);
+    }
+
+    //// @notice Allows the caller to remove themselves as an operator immediately.
+    function removeSelf() external {
+        _removeOperator(msg.sender, 0);
     }
 
     /// @notice Consolidates the operator mapping by removing operators whose removal epoch has
     /// passed, swapping removed operators with the last entry, and decrementing the operatorCount.
     function consolidate() external {
path: packages/protocol/contracts/layer1/preconf/impl/PreconfWhitelist2.sol
user: login: AnshuJalan
body: ^ I have a similar concern
created_at: 2025-03-18T05:53:30Z
author_association: COLLABORATOR
diff_hunk: @@ -16,61 +16,62 @@ contract PreconfWhitelist2 is EssentialContract, IPreconfWhitelist {
         uint8 index; // Index in operatorMapping.
     }
 
-    event Consolidated();
+    event Consolidated(uint8 previousCount, uint8 newCount);
+    event OperatorChangeDelaySet(uint8 delay);
 
     mapping(address operator => OperatorInfo info) public operators;
     mapping(uint256 index => address operator) public operatorMapping;
     uint8 public operatorCount;
+    uint8 public operatorChangeDelay; // in epochs
 
     uint256[47] private __gap;
 
-    function init(address _owner) external initializer {
+    constructor(address _resolver) EssentialContract(_resolver) { }
+
+    function init(address _owner, uint8 _operatorChangeDelay) external initializer {
         __Essential_init(_owner);
+        operatorChangeDelay = _operatorChangeDelay;
     }
 
-    constructor(address _resolver) EssentialContract(_resolver) { }
+    function setOperatorChangeDelay(uint8 _operatorChangeDelay) external onlyOwner {
+        operatorChangeDelay = _operatorChangeDelay;
+        emit OperatorChangeDelaySet(_operatorChangeDelay);
+    }
 
     /// @inheritdoc IPreconfWhitelist
     function addOperator(address _operator) external onlyOwner {
-        require(_operator != address(0), InvalidOperatorAddress());
-        require(operators[_operator].activeSince == 0, OperatorAlreadyExists());
-
-        uint8 _operatorCount = operatorCount;
-        operators[_operator] = OperatorInfo({
-            activeSince: epochStartTimestamp(2),
-            inactiveSince: 0, // no removal scheduled.
-            index: _operatorCount
-        });
-        operatorMapping[_operatorCount] = _operator;
-        unchecked {
-            operatorCount = _operatorCount + 1;
-        }
-
-        emit OperatorAdded(_operator);
+        _addOperator(_operator, operatorChangeDelay);
     }
 
     /// @inheritdoc IPreconfWhitelist
     function removeOperator(uint256 _operatorIndex) external onlyOwner {
         require(_operatorIndex < operatorCount, InvalidOperatorIndex());
-        _removeOperator(operatorMapping[_operatorIndex]);
+        _removeOperator(operatorMapping[_operatorIndex], operatorChangeDelay);
     }
 
     /// @notice Removes an operator by address who will become inactive in two epochs.
     /// @param _operator The address of the operator to remove.
     function removeOperator(address _operator) external onlyOwner {
-        _removeOperator(_operator);
+        _removeOperator(_operator, operatorChangeDelay);
+    }
+
+    //// @notice Allows the caller to remove themselves as an operator immediately.
+    function removeSelf() external {
+        _removeOperator(msg.sender, 0);
     }
 
     /// @notice Consolidates the operator mapping by removing operators whose removal epoch has
     /// passed, swapping removed operators with the last entry, and decrementing the operatorCount.
     function consolidate() external {
path: packages/protocol/contracts/layer1/preconf/impl/PreconfWhitelist2.sol
user: login: dantaik
body: > ~I think we want to swap the removed operator with the next operator in the `operators` array (not the last), but that requires shifting a lot of indexes..~ Edit: Well, even then the shifted indexes can change the result of `getOperatorForCurrentEpoch`

Good catch.
created_at: 2025-03-18T07:44:35Z
author_association: MEMBER
diff_hunk: @@ -124,12 +125,43 @@ contract PreconfWhitelist2 is EssentialContract, IPreconfWhitelist {
         }
     }
 
-    function epochStartTimestamp(uint256 offset) public view returns (uint64) {
+    function epochStartTimestamp(uint256 _offset) public view returns (uint64) {
         return uint64(
-            LibPreconfUtils.getEpochTimestamp() + offset * LibPreconfConstants.SECONDS_IN_EPOCH
+            LibPreconfUtils.getEpochTimestamp() + _offset * LibPreconfConstants.SECONDS_IN_EPOCH
         );
     }
 
+    function _addOperator(address _operator, uint8 _operatorChangeDelay) internal {
+        require(_operator != address(0), InvalidOperatorAddress());
+        require(operators[_operator].activeSince == 0, OperatorAlreadyExists());
path: packages/protocol/contracts/layer1/preconf/impl/PreconfWhitelist2.sol
user: login: dantaik
body: This is another good catch, I'll need to add a test for it. We should allow adding a removed operator back.
created_at: 2025-03-18T07:45:18Z
author_association: MEMBER
diff_hunk: @@ -124,12 +125,43 @@ contract PreconfWhitelist2 is EssentialContract, IPreconfWhitelist {
         }
     }
 
-    function epochStartTimestamp(uint256 offset) public view returns (uint64) {
+    function epochStartTimestamp(uint256 _offset) public view returns (uint64) {
         return uint64(
-            LibPreconfUtils.getEpochTimestamp() + offset * LibPreconfConstants.SECONDS_IN_EPOCH
+            LibPreconfUtils.getEpochTimestamp() + _offset * LibPreconfConstants.SECONDS_IN_EPOCH
         );
     }
 
+    function _addOperator(address _operator, uint8 _operatorChangeDelay) internal {
+        require(_operator != address(0), InvalidOperatorAddress());
+        require(operators[_operator].activeSince == 0, OperatorAlreadyExists());
path: packages/protocol/contracts/layer1/preconf/impl/PreconfWhitelist2.sol
user: login: smartprogrammer93
body: It can be added after calling consolidate 
created_at: 2025-03-18T07:49:01Z
author_association: COLLABORATOR
diff_hunk: @@ -16,61 +16,62 @@ contract PreconfWhitelist2 is EssentialContract, IPreconfWhitelist {
         uint8 index; // Index in operatorMapping.
     }
 
-    event Consolidated();
+    event Consolidated(uint8 previousCount, uint8 newCount);
+    event OperatorChangeDelaySet(uint8 delay);
 
     mapping(address operator => OperatorInfo info) public operators;
     mapping(uint256 index => address operator) public operatorMapping;
     uint8 public operatorCount;
+    uint8 public operatorChangeDelay; // in epochs
 
     uint256[47] private __gap;
 
-    function init(address _owner) external initializer {
+    constructor(address _resolver) EssentialContract(_resolver) { }
+
+    function init(address _owner, uint8 _operatorChangeDelay) external initializer {
         __Essential_init(_owner);
+        operatorChangeDelay = _operatorChangeDelay;
     }
 
-    constructor(address _resolver) EssentialContract(_resolver) { }
+    function setOperatorChangeDelay(uint8 _operatorChangeDelay) external onlyOwner {
+        operatorChangeDelay = _operatorChangeDelay;
+        emit OperatorChangeDelaySet(_operatorChangeDelay);
+    }
 
     /// @inheritdoc IPreconfWhitelist
     function addOperator(address _operator) external onlyOwner {
-        require(_operator != address(0), InvalidOperatorAddress());
-        require(operators[_operator].activeSince == 0, OperatorAlreadyExists());
-
-        uint8 _operatorCount = operatorCount;
-        operators[_operator] = OperatorInfo({
-            activeSince: epochStartTimestamp(2),
-            inactiveSince: 0, // no removal scheduled.
-            index: _operatorCount
-        });
-        operatorMapping[_operatorCount] = _operator;
-        unchecked {
-            operatorCount = _operatorCount + 1;
-        }
-
-        emit OperatorAdded(_operator);
+        _addOperator(_operator, operatorChangeDelay);
     }
 
     /// @inheritdoc IPreconfWhitelist
     function removeOperator(uint256 _operatorIndex) external onlyOwner {
         require(_operatorIndex < operatorCount, InvalidOperatorIndex());
-        _removeOperator(operatorMapping[_operatorIndex]);
+        _removeOperator(operatorMapping[_operatorIndex], operatorChangeDelay);
     }
 
     /// @notice Removes an operator by address who will become inactive in two epochs.
     /// @param _operator The address of the operator to remove.
     function removeOperator(address _operator) external onlyOwner {
-        _removeOperator(_operator);
+        _removeOperator(_operator, operatorChangeDelay);
+    }
+
+    //// @notice Allows the caller to remove themselves as an operator immediately.
+    function removeSelf() external {
+        _removeOperator(msg.sender, 0);
     }
 
     /// @notice Consolidates the operator mapping by removing operators whose removal epoch has
     /// passed, swapping removed operators with the last entry, and decrementing the operatorCount.
     function consolidate() external {
path: packages/protocol/contracts/layer1/preconf/impl/PreconfWhitelist2.sol
user: login: dantaik
body: Fixed by new commits with one more test. Please take another look.
created_at: 2025-03-18T08:34:10Z
author_association: MEMBER
diff_hunk: @@ -124,12 +125,43 @@ contract PreconfWhitelist2 is EssentialContract, IPreconfWhitelist {
         }
     }
 
-    function epochStartTimestamp(uint256 offset) public view returns (uint64) {
+    function epochStartTimestamp(uint256 _offset) public view returns (uint64) {
         return uint64(
-            LibPreconfUtils.getEpochTimestamp() + offset * LibPreconfConstants.SECONDS_IN_EPOCH
+            LibPreconfUtils.getEpochTimestamp() + _offset * LibPreconfConstants.SECONDS_IN_EPOCH
         );
     }
 
+    function _addOperator(address _operator, uint8 _operatorChangeDelay) internal {
+        require(_operator != address(0), InvalidOperatorAddress());
+        require(operators[_operator].activeSince == 0, OperatorAlreadyExists());
path: packages/protocol/contracts/layer1/preconf/impl/PreconfWhitelist2.sol
user: login: dantaik
body: Fixed by new commits with one more test. Please take another look.
created_at: 2025-03-18T08:34:38Z
author_association: MEMBER
diff_hunk: @@ -16,82 +16,82 @@ contract PreconfWhitelist2 is EssentialContract, IPreconfWhitelist {
         uint8 index; // Index in operatorMapping.
     }
 
-    event Consolidated();
+    event Consolidated(uint8 previousCount, uint8 newCount);
+    event OperatorChangeDelaySet(uint8 delay);
 
     mapping(address operator => OperatorInfo info) public operators;
     mapping(uint256 index => address operator) public operatorMapping;
     uint8 public operatorCount;
+    uint8 public operatorChangeDelay; // in epochs
 
     uint256[47] private __gap;
 
-    function init(address _owner) external initializer {
+    constructor(address _resolver) EssentialContract(_resolver) { }
+
+    function init(address _owner, uint8 _operatorChangeDelay) external initializer {
         __Essential_init(_owner);
+        operatorChangeDelay = _operatorChangeDelay;
     }
 
-    constructor(address _resolver) EssentialContract(_resolver) { }
+    function setOperatorChangeDelay(uint8 _operatorChangeDelay) external onlyOwner {
+        operatorChangeDelay = _operatorChangeDelay;
+        emit OperatorChangeDelaySet(_operatorChangeDelay);
+    }
 
     /// @inheritdoc IPreconfWhitelist
     function addOperator(address _operator) external onlyOwner {
-        require(_operator != address(0), InvalidOperatorAddress());
-        require(operators[_operator].activeSince == 0, OperatorAlreadyExists());
-
-        uint8 _operatorCount = operatorCount;
-        operators[_operator] = OperatorInfo({
-            activeSince: epochStartTimestamp(2),
-            inactiveSince: 0, // no removal scheduled.
-            index: _operatorCount
-        });
-        operatorMapping[_operatorCount] = _operator;
-        unchecked {
-            operatorCount = _operatorCount + 1;
-        }
-
-        emit OperatorAdded(_operator);
+        _addOperator(_operator, operatorChangeDelay);
     }
 
     /// @inheritdoc IPreconfWhitelist
     function removeOperator(uint256 _operatorIndex) external onlyOwner {
         require(_operatorIndex < operatorCount, InvalidOperatorIndex());
-        _removeOperator(operatorMapping[_operatorIndex]);
+        _removeOperator(operatorMapping[_operatorIndex], operatorChangeDelay);
     }
 
     /// @notice Removes an operator by address who will become inactive in two epochs.
     /// @param _operator The address of the operator to remove.
     function removeOperator(address _operator) external onlyOwner {
-        _removeOperator(_operator);
+        _removeOperator(_operator, operatorChangeDelay);
+    }
+
+    /// @notice Allows the caller to remove themselves as an operator immediately.
+    function removeSelf() external {
+        _removeOperator(msg.sender, 0);
     }
 
     /// @notice Consolidates the operator mapping by removing operators whose removal epoch has
-    /// passed, swapping removed operators with the last entry, and decrementing the operatorCount.
+    /// passed, maintaining the order of active operators, and decrementing the operatorCount.
     function consolidate() external {
         uint64 currentEpoch = epochStartTimestamp(0);
         uint8 i;
-        uint8 _operatorCount = operatorCount;
+        uint8 _previousCount = operatorCount;
+        uint8 _operatorCount = _previousCount;
 
         while (i < _operatorCount) {
             address operator = operatorMapping[i];
             OperatorInfo memory info = operators[operator];
+
             // Check if the operator is scheduled for removal and the removal epoch has passed
             if (info.inactiveSince != 0 && info.inactiveSince <= currentEpoch) {
-                uint8 lastIndex = _operatorCount - 1;
-                // Only perform swap if not the last element
-                if (i != lastIndex) {
-                    address lastOperator = operatorMapping[lastIndex];
-                    operators[lastOperator].index = i;
-                    operatorMapping[i] = lastOperator;
+                // Shift all subsequent operators one position to the left
+                for (uint8 j = i; j < _operatorCount - 1; j++) {
+                    address nextOperator = operatorMapping[j + 1];
+                    operators[nextOperator].index = j;
+                    operatorMapping[j] = nextOperator;
                 }
-                // Remove the operator from the mapping
+                // Remove the last operator as it has been shifted
                 delete operators[operator];
-                delete operatorMapping[lastIndex];
+                delete operatorMapping[_operatorCount - 1];
                 _operatorCount--;
path: packages/protocol/contracts/layer1/preconf/impl/PreconfWhitelist2.sol
user: login: AnshuJalan
body: Can change this to `--operatorCount`
created_at: 2025-03-18T08:57:04Z
author_association: COLLABORATOR
diff_hunk: @@ -16,61 +16,62 @@ contract PreconfWhitelist2 is EssentialContract, IPreconfWhitelist {
         uint8 index; // Index in operatorMapping.
     }
 
-    event Consolidated();
+    event Consolidated(uint8 previousCount, uint8 newCount);
+    event OperatorChangeDelaySet(uint8 delay);
 
     mapping(address operator => OperatorInfo info) public operators;
     mapping(uint256 index => address operator) public operatorMapping;
     uint8 public operatorCount;
+    uint8 public operatorChangeDelay; // in epochs
 
     uint256[47] private __gap;
 
-    function init(address _owner) external initializer {
+    constructor(address _resolver) EssentialContract(_resolver) { }
+
+    function init(address _owner, uint8 _operatorChangeDelay) external initializer {
         __Essential_init(_owner);
+        operatorChangeDelay = _operatorChangeDelay;
     }
 
-    constructor(address _resolver) EssentialContract(_resolver) { }
+    function setOperatorChangeDelay(uint8 _operatorChangeDelay) external onlyOwner {
+        operatorChangeDelay = _operatorChangeDelay;
+        emit OperatorChangeDelaySet(_operatorChangeDelay);
+    }
 
     /// @inheritdoc IPreconfWhitelist
     function addOperator(address _operator) external onlyOwner {
-        require(_operator != address(0), InvalidOperatorAddress());
-        require(operators[_operator].activeSince == 0, OperatorAlreadyExists());
-
-        uint8 _operatorCount = operatorCount;
-        operators[_operator] = OperatorInfo({
-            activeSince: epochStartTimestamp(2),
-            inactiveSince: 0, // no removal scheduled.
-            index: _operatorCount
-        });
-        operatorMapping[_operatorCount] = _operator;
-        unchecked {
-            operatorCount = _operatorCount + 1;
-        }
-
-        emit OperatorAdded(_operator);
+        _addOperator(_operator, operatorChangeDelay);
     }
 
     /// @inheritdoc IPreconfWhitelist
     function removeOperator(uint256 _operatorIndex) external onlyOwner {
         require(_operatorIndex < operatorCount, InvalidOperatorIndex());
-        _removeOperator(operatorMapping[_operatorIndex]);
+        _removeOperator(operatorMapping[_operatorIndex], operatorChangeDelay);
     }
 
     /// @notice Removes an operator by address who will become inactive in two epochs.
     /// @param _operator The address of the operator to remove.
     function removeOperator(address _operator) external onlyOwner {
-        _removeOperator(_operator);
+        _removeOperator(_operator, operatorChangeDelay);
+    }
+
+    //// @notice Allows the caller to remove themselves as an operator immediately.
+    function removeSelf() external {
+        _removeOperator(msg.sender, 0);
     }
 
     /// @notice Consolidates the operator mapping by removing operators whose removal epoch has
     /// passed, swapping removed operators with the last entry, and decrementing the operatorCount.
     function consolidate() external {
path: packages/protocol/contracts/layer1/preconf/impl/PreconfWhitelist2.sol
user: login: linoscope
body: Maybe I am missing something, but doesn't the issue still exist, even if we preserve the ordering of the operators due to the `uint8(uint256(root) % operatorCount)` changing? For example:

```
root = 5

Before consolidate():
- operatorCount = 4
- operatorMapping = [A, B, C, D]
- root % 4 = 1 (selects B)

After consolidate() removes A:
- operatorCount = 3
- operatorMapping = [B, C, D]
- root % 3 = 2 (selects D)
```
created_at: 2025-03-18T09:52:14Z
author_association: COLLABORATOR
diff_hunk: @@ -16,61 +16,62 @@ contract PreconfWhitelist2 is EssentialContract, IPreconfWhitelist {
         uint8 index; // Index in operatorMapping.
     }
 
-    event Consolidated();
+    event Consolidated(uint8 previousCount, uint8 newCount);
+    event OperatorChangeDelaySet(uint8 delay);
 
     mapping(address operator => OperatorInfo info) public operators;
     mapping(uint256 index => address operator) public operatorMapping;
     uint8 public operatorCount;
+    uint8 public operatorChangeDelay; // in epochs
 
     uint256[47] private __gap;
 
-    function init(address _owner) external initializer {
+    constructor(address _resolver) EssentialContract(_resolver) { }
+
+    function init(address _owner, uint8 _operatorChangeDelay) external initializer {
         __Essential_init(_owner);
+        operatorChangeDelay = _operatorChangeDelay;
     }
 
-    constructor(address _resolver) EssentialContract(_resolver) { }
+    function setOperatorChangeDelay(uint8 _operatorChangeDelay) external onlyOwner {
+        operatorChangeDelay = _operatorChangeDelay;
+        emit OperatorChangeDelaySet(_operatorChangeDelay);
+    }
 
     /// @inheritdoc IPreconfWhitelist
     function addOperator(address _operator) external onlyOwner {
-        require(_operator != address(0), InvalidOperatorAddress());
-        require(operators[_operator].activeSince == 0, OperatorAlreadyExists());
-
-        uint8 _operatorCount = operatorCount;
-        operators[_operator] = OperatorInfo({
-            activeSince: epochStartTimestamp(2),
-            inactiveSince: 0, // no removal scheduled.
-            index: _operatorCount
-        });
-        operatorMapping[_operatorCount] = _operator;
-        unchecked {
-            operatorCount = _operatorCount + 1;
-        }
-
-        emit OperatorAdded(_operator);
+        _addOperator(_operator, operatorChangeDelay);
     }
 
     /// @inheritdoc IPreconfWhitelist
     function removeOperator(uint256 _operatorIndex) external onlyOwner {
         require(_operatorIndex < operatorCount, InvalidOperatorIndex());
-        _removeOperator(operatorMapping[_operatorIndex]);
+        _removeOperator(operatorMapping[_operatorIndex], operatorChangeDelay);
     }
 
     /// @notice Removes an operator by address who will become inactive in two epochs.
     /// @param _operator The address of the operator to remove.
     function removeOperator(address _operator) external onlyOwner {
-        _removeOperator(_operator);
+        _removeOperator(_operator, operatorChangeDelay);
+    }
+
+    //// @notice Allows the caller to remove themselves as an operator immediately.
+    function removeSelf() external {
+        _removeOperator(msg.sender, 0);
     }
 
     /// @notice Consolidates the operator mapping by removing operators whose removal epoch has
     /// passed, swapping removed operators with the last entry, and decrementing the operatorCount.
     function consolidate() external {
path: packages/protocol/contracts/layer1/preconf/impl/PreconfWhitelist2.sol
user: login: linoscope
body: If we find it too complex to fix this, I think it's fine to keep the removed operators in the array; removals should be rare, cost-wise they only add one additional loop+check, and the whitelist is a temporal solution anyway.
created_at: 2025-03-18T09:57:47Z
author_association: COLLABORATOR
diff_hunk: @@ -16,61 +16,62 @@ contract PreconfWhitelist2 is EssentialContract, IPreconfWhitelist {
         uint8 index; // Index in operatorMapping.
     }
 
-    event Consolidated();
+    event Consolidated(uint8 previousCount, uint8 newCount);
+    event OperatorChangeDelaySet(uint8 delay);
 
     mapping(address operator => OperatorInfo info) public operators;
     mapping(uint256 index => address operator) public operatorMapping;
     uint8 public operatorCount;
+    uint8 public operatorChangeDelay; // in epochs
 
     uint256[47] private __gap;
 
-    function init(address _owner) external initializer {
+    constructor(address _resolver) EssentialContract(_resolver) { }
+
+    function init(address _owner, uint8 _operatorChangeDelay) external initializer {
         __Essential_init(_owner);
+        operatorChangeDelay = _operatorChangeDelay;
     }
 
-    constructor(address _resolver) EssentialContract(_resolver) { }
+    function setOperatorChangeDelay(uint8 _operatorChangeDelay) external onlyOwner {
+        operatorChangeDelay = _operatorChangeDelay;
+        emit OperatorChangeDelaySet(_operatorChangeDelay);
+    }
 
     /// @inheritdoc IPreconfWhitelist
     function addOperator(address _operator) external onlyOwner {
-        require(_operator != address(0), InvalidOperatorAddress());
-        require(operators[_operator].activeSince == 0, OperatorAlreadyExists());
-
-        uint8 _operatorCount = operatorCount;
-        operators[_operator] = OperatorInfo({
-            activeSince: epochStartTimestamp(2),
-            inactiveSince: 0, // no removal scheduled.
-            index: _operatorCount
-        });
-        operatorMapping[_operatorCount] = _operator;
-        unchecked {
-            operatorCount = _operatorCount + 1;
-        }
-
-        emit OperatorAdded(_operator);
+        _addOperator(_operator, operatorChangeDelay);
     }
 
     /// @inheritdoc IPreconfWhitelist
     function removeOperator(uint256 _operatorIndex) external onlyOwner {
         require(_operatorIndex < operatorCount, InvalidOperatorIndex());
-        _removeOperator(operatorMapping[_operatorIndex]);
+        _removeOperator(operatorMapping[_operatorIndex], operatorChangeDelay);
     }
 
     /// @notice Removes an operator by address who will become inactive in two epochs.
     /// @param _operator The address of the operator to remove.
     function removeOperator(address _operator) external onlyOwner {
-        _removeOperator(_operator);
+        _removeOperator(_operator, operatorChangeDelay);
+    }
+
+    //// @notice Allows the caller to remove themselves as an operator immediately.
+    function removeSelf() external {
+        _removeOperator(msg.sender, 0);
     }
 
     /// @notice Consolidates the operator mapping by removing operators whose removal epoch has
     /// passed, swapping removed operators with the last entry, and decrementing the operatorCount.
     function consolidate() external {
path: packages/protocol/contracts/layer1/preconf/impl/PreconfWhitelist2.sol
user: login: dantaik
body: Yes, still a bug here...
created_at: 2025-03-18T09:59:21Z
author_association: MEMBER
diff_hunk: @@ -16,61 +16,62 @@ contract PreconfWhitelist2 is EssentialContract, IPreconfWhitelist {
         uint8 index; // Index in operatorMapping.
     }
 
-    event Consolidated();
+    event Consolidated(uint8 previousCount, uint8 newCount);
+    event OperatorChangeDelaySet(uint8 delay);
 
     mapping(address operator => OperatorInfo info) public operators;
     mapping(uint256 index => address operator) public operatorMapping;
     uint8 public operatorCount;
+    uint8 public operatorChangeDelay; // in epochs
 
     uint256[47] private __gap;
 
-    function init(address _owner) external initializer {
+    constructor(address _resolver) EssentialContract(_resolver) { }
+
+    function init(address _owner, uint8 _operatorChangeDelay) external initializer {
         __Essential_init(_owner);
+        operatorChangeDelay = _operatorChangeDelay;
     }
 
-    constructor(address _resolver) EssentialContract(_resolver) { }
+    function setOperatorChangeDelay(uint8 _operatorChangeDelay) external onlyOwner {
+        operatorChangeDelay = _operatorChangeDelay;
+        emit OperatorChangeDelaySet(_operatorChangeDelay);
+    }
 
     /// @inheritdoc IPreconfWhitelist
     function addOperator(address _operator) external onlyOwner {
-        require(_operator != address(0), InvalidOperatorAddress());
-        require(operators[_operator].activeSince == 0, OperatorAlreadyExists());
-
-        uint8 _operatorCount = operatorCount;
-        operators[_operator] = OperatorInfo({
-            activeSince: epochStartTimestamp(2),
-            inactiveSince: 0, // no removal scheduled.
-            index: _operatorCount
-        });
-        operatorMapping[_operatorCount] = _operator;
-        unchecked {
-            operatorCount = _operatorCount + 1;
-        }
-
-        emit OperatorAdded(_operator);
+        _addOperator(_operator, operatorChangeDelay);
     }
 
     /// @inheritdoc IPreconfWhitelist
     function removeOperator(uint256 _operatorIndex) external onlyOwner {
         require(_operatorIndex < operatorCount, InvalidOperatorIndex());
-        _removeOperator(operatorMapping[_operatorIndex]);
+        _removeOperator(operatorMapping[_operatorIndex], operatorChangeDelay);
     }
 
     /// @notice Removes an operator by address who will become inactive in two epochs.
     /// @param _operator The address of the operator to remove.
     function removeOperator(address _operator) external onlyOwner {
-        _removeOperator(_operator);
+        _removeOperator(_operator, operatorChangeDelay);
+    }
+
+    //// @notice Allows the caller to remove themselves as an operator immediately.
+    function removeSelf() external {
+        _removeOperator(msg.sender, 0);
     }
 
     /// @notice Consolidates the operator mapping by removing operators whose removal epoch has
     /// passed, swapping removed operators with the last entry, and decrementing the operatorCount.
     function consolidate() external {
path: packages/protocol/contracts/layer1/preconf/impl/PreconfWhitelist2.sol
user: login: dantaik
body: Fixed, but the gas cost is almost linear to `operatorCount`, haven't yet figured out a way to improve it.
created_at: 2025-03-18T11:23:06Z
author_association: MEMBER
diff_hunk: @@ -124,12 +142,45 @@ contract PreconfWhitelist2 is EssentialContract, IPreconfWhitelist {
         }
     }
 
-    function epochStartTimestamp(uint256 offset) public view returns (uint64) {
+    function epochStartTimestamp(uint256 _offset) public view returns (uint64) {
         return uint64(
-            LibPreconfUtils.getEpochTimestamp() + offset * LibPreconfConstants.SECONDS_IN_EPOCH
+            LibPreconfUtils.getEpochTimestamp() + _offset * LibPreconfConstants.SECONDS_IN_EPOCH
         );
     }
 
+    function _addOperator(address _operator, uint8 _operatorChangeDelay) internal {
+        require(_operator != address(0), InvalidOperatorAddress());
+        require(operators[_operator].activeSince == 0, OperatorAlreadyExists());
+
+        uint8 _operatorCount = operatorCount;
+        uint64 activeSince = epochStartTimestamp(_operatorChangeDelay);
+        operators[_operator] = OperatorInfo({
+            activeSince: activeSince,
+            inactiveSince: 0, // no removal scheduled.
+            index: _operatorCount
+        });
+        operatorMapping[_operatorCount] = _operator;
+        unchecked {
+            operatorCount = _operatorCount + 1;
+        }
+
+        emit OperatorAdded(_operator, activeSince);
+    }
+
+    function _removeOperator(address _operator, uint8 _operatorChangeDelay) internal {
+        require(_operator != address(0), InvalidOperatorAddress());
+        OperatorInfo memory info = operators[_operator];
+        require(info.inactiveSince == 0, OperatorAlreadyRemoved());
+        require(info.activeSince != 0, InvalidOperatorAddress());
+
+        uint64 inactiveSince = epochStartTimestamp(_operatorChangeDelay);
+        operators[_operator].inactiveSince = inactiveSince;
+        operators[_operator].activeSince = 0;
+
+        emit OperatorRemoved(_operator, inactiveSince);
+    }
+
+    /// @dev The cost of this function is primarily linear with respect to operatorCount.
     function _getOperatorForEpoch(uint64 _epochTimestamp) internal view returns (address) {
path: packages/protocol/contracts/layer1/preconf/impl/PreconfWhitelist2.sol
user: login: dantaik
body: open for gas-efficiency improvement ideas.
created_at: 2025-03-18T11:26:07Z
author_association: MEMBER
diff_hunk: @@ -194,6 +229,72 @@ contract TestPreconfWhitelist2 is Layer1Test {
         whitelist.consolidate();
     }
 
+    function test_whitelist2_noDelay_addThenRemoveOneOperator() external {
+        _setBeaconBlockRoot(bytes32(uint256(7)));
+
+        vm.prank(whitelistOwner);
+        whitelistNoDelay.addOperator(Bob);
+
+        assertEq(whitelistNoDelay.operatorCount(), 1);
+        assertEq(whitelistNoDelay.operatorMapping(0), Bob);
+
+        (uint64 activeSince, uint64 inactiveSince, uint8 index) = whitelistNoDelay.operators(Bob);
+        assertEq(activeSince, whitelistNoDelay.epochStartTimestamp(0));
+        assertEq(inactiveSince, 0);
+        assertEq(index, 0);
+
+        assertEq(whitelistNoDelay.getOperatorForCurrentEpoch(), Bob);
+        assertEq(whitelistNoDelay.getOperatorForNextEpoch(), Bob);
+
+        vm.prank(whitelistOwner);
+        whitelistNoDelay.removeOperator(Bob);
+
+        assertEq(whitelistNoDelay.operatorCount(), 1);
+        assertEq(whitelistNoDelay.operatorMapping(0), Bob);
+
+        (activeSince, inactiveSince, index) = whitelistNoDelay.operators(Bob);
+        assertEq(activeSince, 0);
+        assertEq(inactiveSince, whitelistNoDelay.epochStartTimestamp(0));
+        assertEq(index, 0);
+
+        whitelistNoDelay.consolidate();
+        assertEq(whitelistNoDelay.operatorCount(), 0);
+        assertEq(whitelistNoDelay.operatorMapping(0), address(0));
+
+        (activeSince, inactiveSince, index) = whitelistNoDelay.operators(Bob);
+        assertEq(activeSince, 0);
+        assertEq(inactiveSince, 0);
+        assertEq(index, 0);
+
+        assertEq(whitelistNoDelay.getOperatorForCurrentEpoch(), address(0));
+        assertEq(whitelistNoDelay.getOperatorForNextEpoch(), address(0));
+    }
+
+    function test_whitelistNoDelay_consolidationPreservesOrder() external {
+        _setBeaconBlockRoot(bytes32(uint256(7)));
+
+        vm.startPrank(whitelistOwner);
+        whitelistNoDelay.addOperator(Alice);
+        whitelistNoDelay.addOperator(Bob);
+        whitelistNoDelay.addOperator(Carol);
+
+        address[] memory candidates = whitelistNoDelay.getOperatorCandidatesForCurrentEpoch();
+        assertEq(candidates.length, 3);
+        assertEq(candidates[0], Alice);
+        assertEq(candidates[1], Bob);
+        assertEq(candidates[2], Carol);
+
+        whitelistNoDelay.removeOperator(Alice);
+
+        whitelistNoDelay.consolidate();
+        candidates = whitelistNoDelay.getOperatorCandidatesForCurrentEpoch();
+        assertEq(candidates.length, 2);
+        assertEq(candidates[0], Bob);
+        assertEq(candidates[1], Carol);
+
+        vm.stopPrank();
+    }
+
path: packages/protocol/test/layer1/preconf/whitelist/PeconfWhitelist2.t.sol
user: login: linoscope
body: Suggestion of an additional test:

```suggestion

    function test_whitelistNoDelay_consolidationWillNotChangeCurrentEpochOperator() external {
        _setBeaconBlockRoot(bytes32(uint256(5)));

        vm.startPrank(whitelistOwner);
        whitelistNoDelay.addOperator(Alice);
        whitelistNoDelay.addOperator(Bob);
        whitelistNoDelay.addOperator(Carol);
        whitelistNoDelay.addOperator(David);
        whitelistNoDelay.removeOperator(Alice);

        address operatorBeforeConsolidate = whitelistNoDelay.getOperatorForCurrentEpoch();
        whitelistNoDelay.consolidate();
        address operatorAfterConsolidate = whitelistNoDelay.getOperatorForCurrentEpoch();
        assertEq(operatorBeforeConsolidate, operatorAfterConsolidate);

        vm.stopPrank();
    }
```
created_at: 2025-03-18T12:22:59Z
author_association: COLLABORATOR
diff_hunk: @@ -16,61 +16,62 @@ contract PreconfWhitelist2 is EssentialContract, IPreconfWhitelist {
         uint8 index; // Index in operatorMapping.
     }
 
-    event Consolidated();
+    event Consolidated(uint8 previousCount, uint8 newCount);
+    event OperatorChangeDelaySet(uint8 delay);
 
     mapping(address operator => OperatorInfo info) public operators;
     mapping(uint256 index => address operator) public operatorMapping;
     uint8 public operatorCount;
+    uint8 public operatorChangeDelay; // in epochs
 
     uint256[47] private __gap;
 
-    function init(address _owner) external initializer {
+    constructor(address _resolver) EssentialContract(_resolver) { }
+
+    function init(address _owner, uint8 _operatorChangeDelay) external initializer {
         __Essential_init(_owner);
+        operatorChangeDelay = _operatorChangeDelay;
     }
 
-    constructor(address _resolver) EssentialContract(_resolver) { }
+    function setOperatorChangeDelay(uint8 _operatorChangeDelay) external onlyOwner {
+        operatorChangeDelay = _operatorChangeDelay;
+        emit OperatorChangeDelaySet(_operatorChangeDelay);
+    }
 
     /// @inheritdoc IPreconfWhitelist
     function addOperator(address _operator) external onlyOwner {
-        require(_operator != address(0), InvalidOperatorAddress());
-        require(operators[_operator].activeSince == 0, OperatorAlreadyExists());
-
-        uint8 _operatorCount = operatorCount;
-        operators[_operator] = OperatorInfo({
-            activeSince: epochStartTimestamp(2),
-            inactiveSince: 0, // no removal scheduled.
-            index: _operatorCount
-        });
-        operatorMapping[_operatorCount] = _operator;
-        unchecked {
-            operatorCount = _operatorCount + 1;
-        }
-
-        emit OperatorAdded(_operator);
+        _addOperator(_operator, operatorChangeDelay);
     }
 
     /// @inheritdoc IPreconfWhitelist
     function removeOperator(uint256 _operatorIndex) external onlyOwner {
         require(_operatorIndex < operatorCount, InvalidOperatorIndex());
-        _removeOperator(operatorMapping[_operatorIndex]);
+        _removeOperator(operatorMapping[_operatorIndex], operatorChangeDelay);
     }
 
     /// @notice Removes an operator by address who will become inactive in two epochs.
     /// @param _operator The address of the operator to remove.
     function removeOperator(address _operator) external onlyOwner {
-        _removeOperator(_operator);
+        _removeOperator(_operator, operatorChangeDelay);
+    }
+
+    //// @notice Allows the caller to remove themselves as an operator immediately.
+    function removeSelf() external {
+        _removeOperator(msg.sender, 0);
     }
 
     /// @notice Consolidates the operator mapping by removing operators whose removal epoch has
     /// passed, swapping removed operators with the last entry, and decrementing the operatorCount.
     function consolidate() external {
path: packages/protocol/contracts/layer1/preconf/impl/PreconfWhitelist2.sol
user: login: linoscope
body: The added cost in `getOperatorForEpoch` makes me wonder if `consolidate()` is really worth it. Alternative being just keeping the removed in the array, as mentioned above

> I think it's fine to keep the removed operators in the array; removals should be rare, cost-wise they only add one additional loop+check, and the whitelist is a temporal solution anyway.
created_at: 2025-03-18T12:32:50Z
author_association: COLLABORATOR
diff_hunk: @@ -16,61 +16,62 @@ contract PreconfWhitelist2 is EssentialContract, IPreconfWhitelist {
         uint8 index; // Index in operatorMapping.
     }
 
-    event Consolidated();
+    event Consolidated(uint8 previousCount, uint8 newCount);
+    event OperatorChangeDelaySet(uint8 delay);
 
     mapping(address operator => OperatorInfo info) public operators;
     mapping(uint256 index => address operator) public operatorMapping;
     uint8 public operatorCount;
+    uint8 public operatorChangeDelay; // in epochs
 
     uint256[47] private __gap;
 
-    function init(address _owner) external initializer {
+    constructor(address _resolver) EssentialContract(_resolver) { }
+
+    function init(address _owner, uint8 _operatorChangeDelay) external initializer {
         __Essential_init(_owner);
+        operatorChangeDelay = _operatorChangeDelay;
     }
 
-    constructor(address _resolver) EssentialContract(_resolver) { }
+    function setOperatorChangeDelay(uint8 _operatorChangeDelay) external onlyOwner {
+        operatorChangeDelay = _operatorChangeDelay;
+        emit OperatorChangeDelaySet(_operatorChangeDelay);
+    }
 
     /// @inheritdoc IPreconfWhitelist
     function addOperator(address _operator) external onlyOwner {
-        require(_operator != address(0), InvalidOperatorAddress());
-        require(operators[_operator].activeSince == 0, OperatorAlreadyExists());
-
-        uint8 _operatorCount = operatorCount;
-        operators[_operator] = OperatorInfo({
-            activeSince: epochStartTimestamp(2),
-            inactiveSince: 0, // no removal scheduled.
-            index: _operatorCount
-        });
-        operatorMapping[_operatorCount] = _operator;
-        unchecked {
-            operatorCount = _operatorCount + 1;
-        }
-
-        emit OperatorAdded(_operator);
+        _addOperator(_operator, operatorChangeDelay);
     }
 
     /// @inheritdoc IPreconfWhitelist
     function removeOperator(uint256 _operatorIndex) external onlyOwner {
         require(_operatorIndex < operatorCount, InvalidOperatorIndex());
-        _removeOperator(operatorMapping[_operatorIndex]);
+        _removeOperator(operatorMapping[_operatorIndex], operatorChangeDelay);
     }
 
     /// @notice Removes an operator by address who will become inactive in two epochs.
     /// @param _operator The address of the operator to remove.
     function removeOperator(address _operator) external onlyOwner {
-        _removeOperator(_operator);
+        _removeOperator(_operator, operatorChangeDelay);
+    }
+
+    //// @notice Allows the caller to remove themselves as an operator immediately.
+    function removeSelf() external {
+        _removeOperator(msg.sender, 0);
     }
 
     /// @notice Consolidates the operator mapping by removing operators whose removal epoch has
     /// passed, swapping removed operators with the last entry, and decrementing the operatorCount.
     function consolidate() external {
path: packages/protocol/contracts/layer1/preconf/impl/PreconfWhitelist2.sol
user: login: linoscope
body: Keeping `consolidate()` means we get `O(operatorCount)` in `getOperatorForEpoch` while removing `consolidate()` and keeping out-dated operators in array means `O(removedOperatorCount)` in worst case. So if we have 3 removed operators it becomes worth it in terms of cost complexity.
created_at: 2025-03-18T12:33:37Z
author_association: COLLABORATOR
diff_hunk: @@ -16,61 +16,62 @@ contract PreconfWhitelist2 is EssentialContract, IPreconfWhitelist {
         uint8 index; // Index in operatorMapping.
     }
 
-    event Consolidated();
+    event Consolidated(uint8 previousCount, uint8 newCount);
+    event OperatorChangeDelaySet(uint8 delay);
 
     mapping(address operator => OperatorInfo info) public operators;
     mapping(uint256 index => address operator) public operatorMapping;
     uint8 public operatorCount;
+    uint8 public operatorChangeDelay; // in epochs
 
     uint256[47] private __gap;
 
-    function init(address _owner) external initializer {
+    constructor(address _resolver) EssentialContract(_resolver) { }
+
+    function init(address _owner, uint8 _operatorChangeDelay) external initializer {
         __Essential_init(_owner);
+        operatorChangeDelay = _operatorChangeDelay;
     }
 
-    constructor(address _resolver) EssentialContract(_resolver) { }
+    function setOperatorChangeDelay(uint8 _operatorChangeDelay) external onlyOwner {
+        operatorChangeDelay = _operatorChangeDelay;
+        emit OperatorChangeDelaySet(_operatorChangeDelay);
+    }
 
     /// @inheritdoc IPreconfWhitelist
     function addOperator(address _operator) external onlyOwner {
-        require(_operator != address(0), InvalidOperatorAddress());
-        require(operators[_operator].activeSince == 0, OperatorAlreadyExists());
-
-        uint8 _operatorCount = operatorCount;
-        operators[_operator] = OperatorInfo({
-            activeSince: epochStartTimestamp(2),
-            inactiveSince: 0, // no removal scheduled.
-            index: _operatorCount
-        });
-        operatorMapping[_operatorCount] = _operator;
-        unchecked {
-            operatorCount = _operatorCount + 1;
-        }
-
-        emit OperatorAdded(_operator);
+        _addOperator(_operator, operatorChangeDelay);
     }
 
     /// @inheritdoc IPreconfWhitelist
     function removeOperator(uint256 _operatorIndex) external onlyOwner {
         require(_operatorIndex < operatorCount, InvalidOperatorIndex());
-        _removeOperator(operatorMapping[_operatorIndex]);
+        _removeOperator(operatorMapping[_operatorIndex], operatorChangeDelay);
     }
 
     /// @notice Removes an operator by address who will become inactive in two epochs.
     /// @param _operator The address of the operator to remove.
     function removeOperator(address _operator) external onlyOwner {
-        _removeOperator(_operator);
+        _removeOperator(_operator, operatorChangeDelay);
+    }
+
+    //// @notice Allows the caller to remove themselves as an operator immediately.
+    function removeSelf() external {
+        _removeOperator(msg.sender, 0);
     }
 
     /// @notice Consolidates the operator mapping by removing operators whose removal epoch has
     /// passed, swapping removed operators with the last entry, and decrementing the operatorCount.
     function consolidate() external {
path: packages/protocol/contracts/layer1/preconf/impl/PreconfWhitelist2.sol
user: login: linoscope
body: Anyway, think we can revisit if cost becomes an issue and do not need to be a blocker right now. Other than this non-blocking point, LGTM.
created_at: 2025-03-18T12:34:38Z
author_association: COLLABORATOR
diff_hunk: @@ -16,61 +16,62 @@ contract PreconfWhitelist2 is EssentialContract, IPreconfWhitelist {
         uint8 index; // Index in operatorMapping.
     }
 
-    event Consolidated();
+    event Consolidated(uint8 previousCount, uint8 newCount);
+    event OperatorChangeDelaySet(uint8 delay);
 
     mapping(address operator => OperatorInfo info) public operators;
     mapping(uint256 index => address operator) public operatorMapping;
     uint8 public operatorCount;
+    uint8 public operatorChangeDelay; // in epochs
 
     uint256[47] private __gap;
 
-    function init(address _owner) external initializer {
+    constructor(address _resolver) EssentialContract(_resolver) { }
+
+    function init(address _owner, uint8 _operatorChangeDelay) external initializer {
         __Essential_init(_owner);
+        operatorChangeDelay = _operatorChangeDelay;
     }
 
-    constructor(address _resolver) EssentialContract(_resolver) { }
+    function setOperatorChangeDelay(uint8 _operatorChangeDelay) external onlyOwner {
+        operatorChangeDelay = _operatorChangeDelay;
+        emit OperatorChangeDelaySet(_operatorChangeDelay);
+    }
 
     /// @inheritdoc IPreconfWhitelist
     function addOperator(address _operator) external onlyOwner {
-        require(_operator != address(0), InvalidOperatorAddress());
-        require(operators[_operator].activeSince == 0, OperatorAlreadyExists());
-
-        uint8 _operatorCount = operatorCount;
-        operators[_operator] = OperatorInfo({
-            activeSince: epochStartTimestamp(2),
-            inactiveSince: 0, // no removal scheduled.
-            index: _operatorCount
-        });
-        operatorMapping[_operatorCount] = _operator;
-        unchecked {
-            operatorCount = _operatorCount + 1;
-        }
-
-        emit OperatorAdded(_operator);
+        _addOperator(_operator, operatorChangeDelay);
     }
 
     /// @inheritdoc IPreconfWhitelist
     function removeOperator(uint256 _operatorIndex) external onlyOwner {
         require(_operatorIndex < operatorCount, InvalidOperatorIndex());
-        _removeOperator(operatorMapping[_operatorIndex]);
+        _removeOperator(operatorMapping[_operatorIndex], operatorChangeDelay);
     }
 
     /// @notice Removes an operator by address who will become inactive in two epochs.
     /// @param _operator The address of the operator to remove.
     function removeOperator(address _operator) external onlyOwner {
-        _removeOperator(_operator);
+        _removeOperator(_operator, operatorChangeDelay);
+    }
+
+    //// @notice Allows the caller to remove themselves as an operator immediately.
+    function removeSelf() external {
+        _removeOperator(msg.sender, 0);
     }
 
     /// @notice Consolidates the operator mapping by removing operators whose removal epoch has
     /// passed, swapping removed operators with the last entry, and decrementing the operatorCount.
     function consolidate() external {
path: packages/protocol/contracts/layer1/preconf/impl/PreconfWhitelist2.sol
user: login: dantaik
body: Optimized the gas cost of `getOperatorForEpoch` to O(1) when all operators in operatorMapping are active and none of them is to be deactivated. See https://github.com/taikoxyz/taiko-mono/pull/19113/commits/a3049810219851b3e559f499f030095c2a8c831a
created_at: 2025-03-19T02:19:53Z
author_association: MEMBER

PR comments:
user: login: linoscope
created_at: 2025-03-18T04:56:56Z
body: Right now, only the operator themselves can remove themself from the lookahead without delay easily. For the whitelist owner to emergency-exit the operator, it requires 3 txs:
- Set the delay to 0.
- Remove the operator.
- Re-set the delay to 2.

What do you think of adding an `emergencyExitOperator` function that the whitelist owner can call so the whitelist owner can also exit preconf operators without delay with a single tx?

The use case I have in mind is that the whitelist owner monitors the preconfs from the outside and can force-exit preconfers who are not preconfing for some period of time.
author_association: COLLABORATOR
user: login: dantaik
created_at: 2025-03-18T09:57:02Z
body: > Right now, only the operator themselves can remove themself from the lookahead without delay easily. For the whitelist owner to emergency-exit the operator, it requires 3 txs:
> 
> * Set the delay to 0.
> * Remove the operator.
> * Re-set the delay to 2.
> 
> What do you think of adding an `emergencyExitOperator` function that the whitelist owner can call so the whitelist owner can also exit preconf operators without delay with a single tx?
> 
> The use case I have in mind is that the whitelist owner monitors the preconfs from the outside and can force-exit preconfers who are not preconfing for some period of time.

Added `function removeOperator(address _operator, bool _effectiveImmediately) external onlyOwner `
author_association: MEMBER
user: login: linoscope
created_at: 2025-03-19T05:35:36Z
body: LGTM
author_association: COLLABORATOR