@@ -17,7 +17,9 @@ import {Proposal} from "@aztec/governance/interfaces/IGovernance.sol";
1717import {ProposalLib} from "@aztec/governance/libraries/ProposalLib.sol " ;
1818import {GovernanceProposer} from "@aztec/governance/proposer/GovernanceProposer.sol " ;
1919import {G1Point, G2Point} from "@aztec/shared/libraries/BN254Lib.sol " ;
20- import {CompressedTimeMath, CompressedTimestamp} from "@aztec/shared/libraries/CompressedTimeMath.sol " ;
20+ import {
21+ CompressedTimeMath, CompressedTimestamp, CompressedEpoch
22+ } from "@aztec/shared/libraries/CompressedTimeMath.sol " ;
2123import {IERC20 } from "@oz/token/ERC20/IERC20.sol " ;
2224import {SafeERC20} from "@oz/token/ERC20/utils/SafeERC20.sol " ;
2325import {Math} from "@oz/utils/math/Math.sol " ;
@@ -78,13 +80,15 @@ struct AttesterView {
7880struct StakingStorage {
7981 IERC20 stakingAsset;
8082 address slasher;
83+ uint96 localEjectionThreshold;
8184 GSE gse;
8285 CompressedTimestamp exitDelay;
8386 mapping (address attester = > Exit) exits;
8487 CompressedStakingQueueConfig queueConfig;
8588 StakingQueue entryQueue;
86- Epoch nextFlushableEpoch;
87- uint256 localEjectionThreshold;
89+ CompressedEpoch nextFlushableEpoch;
90+ uint32 availableValidatorFlushes;
91+ bool isBootstrapped;
8892}
8993
9094library StakingLib {
@@ -96,6 +100,8 @@ library StakingLib {
96100 using StakingQueueConfigLib for StakingQueueConfig;
97101 using CompressedTimeMath for CompressedTimestamp;
98102 using CompressedTimeMath for Timestamp;
103+ using CompressedTimeMath for CompressedEpoch;
104+ using CompressedTimeMath for Epoch;
99105
100106 bytes32 private constant STAKING_SLOT = keccak256 ("aztec.core.staking.storage " );
101107
@@ -114,7 +120,7 @@ library StakingLib {
114120 store.slasher = _slasher;
115121 store.queueConfig = _config.compress ();
116122 store.entryQueue.init ();
117- store.localEjectionThreshold = _localEjectionThreshold;
123+ store.localEjectionThreshold = _localEjectionThreshold. toUint96 () ;
118124 }
119125
120126 function setSlasher (address _slasher ) internal {
@@ -130,7 +136,7 @@ library StakingLib {
130136 StakingStorage storage store = getStorage ();
131137
132138 uint256 oldLocalEjectionThreshold = store.localEjectionThreshold;
133- store.localEjectionThreshold = _localEjectionThreshold;
139+ store.localEjectionThreshold = _localEjectionThreshold. toUint96 () ;
134140
135141 emit IStakingCore.LocalEjectionThresholdUpdated (oldLocalEjectionThreshold, _localEjectionThreshold);
136142 }
@@ -304,10 +310,22 @@ library StakingLib {
304310 emit IStakingCore.ValidatorQueued (_attester, _withdrawer);
305311 }
306312
313+ function updateAndGetAvailableFlushes () internal returns (uint256 ) {
314+ (uint256 flushes , Epoch currentEpoch , bool shouldUpdateState ) = _calculateAvailableFlushes ();
315+
316+ if (shouldUpdateState) {
317+ StakingStorage storage store = getStorage ();
318+ store.nextFlushableEpoch = (currentEpoch + Epoch.wrap (1 )).compress ();
319+ store.availableValidatorFlushes = flushes.toUint32 ();
320+ }
321+
322+ return flushes;
323+ }
324+
307325 /**
308326 * @notice Processes the validator entry queue to add new validators to the active set
309- * @dev Processes up to _maxAddableValidators entries from the queue, attempting to deposit each validator into
310- * the Governance Staking Escrow (GSE).
327+ * @dev Processes up to min(maxAddableValidators, _toAdd) entries from the queue,
328+ * attempting to deposit each validator into the Governance Staking Escrow (GSE).
311329 *
312330 * For each validator:
313331 * - Dequeues their entry from the queue
@@ -316,34 +334,34 @@ library StakingLib {
316334 * - On failure: refunds their stake and emits FailedDeposit event
317335 *
318336 * The function will revert if:
319- * - The current epoch is before nextFlushableEpoch (max 1 flush per epoch). This limit:
320- * 1. Controls validator set growth by only allowing a fixed number of additions per epoch (we can flush at
321- * most maxQueueFlushSize validators at once - hence the limit)
322- * 2. Aligns with committee selection which happens once per epoch anyway
323- * 3. Groups validator additions into epoch-sized chunks for efficient binary searches (fewer snapshots in
324- * GSE)
325337 * - A deposit fails due to out of gas (to prevent queue draining attacks)
326338 *
327339 * The function approves the GSE contract to spend the total stake amount needed for all deposits,
328340 * then revokes the approval after processing is complete.
341+ * It also updates the available validator flushes
329342 *
343+ * @param _toAdd - The max number the caller will try to add
330344 */
331- function flushEntryQueue () internal {
332- uint256 maxAddableValidators = getEntryQueueFlushSize ();
345+ function flushEntryQueue (uint256 _toAdd ) internal {
346+ uint256 maxAddableValidators = updateAndGetAvailableFlushes ();
347+
333348 if (maxAddableValidators == 0 ) {
334349 return ;
335350 }
336351
337- Epoch currentEpoch = TimeLib.epochFromTimestamp (Timestamp.wrap (block .timestamp ));
338352 StakingStorage storage store = getStorage ();
339- require (store.nextFlushableEpoch <= currentEpoch, Errors.Staking__QueueAlreadyFlushed (currentEpoch));
340- store.nextFlushableEpoch = currentEpoch + Epoch.wrap (1 );
341- uint256 amount = store.gse.ACTIVATION_THRESHOLD ();
342353
343354 uint256 queueLength = store.entryQueue.length ();
344- uint256 numToDequeue = Math.min (maxAddableValidators, queueLength);
355+ uint256 numToDequeue = Math.min (Math.min (maxAddableValidators, queueLength), _toAdd);
356+
357+ if (numToDequeue == 0 ) {
358+ return ;
359+ }
360+
345361 // Approve the GSE to spend the total stake amount needed for all deposits.
362+ uint256 amount = store.gse.ACTIVATION_THRESHOLD ();
346363 store.stakingAsset.approve (address (store.gse), amount * numToDequeue);
364+ uint256 depositCount = 0 ;
347365 for (uint256 i = 0 ; i < numToDequeue; i++ ) {
348366 DepositArgs memory args = store.entryQueue.dequeue ();
349367 (bool success , bytes memory data ) = address (store.gse).call (
@@ -358,6 +376,7 @@ library StakingLib {
358376 )
359377 );
360378 if (success) {
379+ depositCount++ ;
361380 emit IStakingCore.Deposit (
362381 args.attester, args.withdrawer, args.publicKeyInG1, args.publicKeyInG2, args.proofOfPossession, amount
363382 );
@@ -376,6 +395,17 @@ library StakingLib {
376395 }
377396 }
378397 store.stakingAsset.approve (address (store.gse), 0 );
398+
399+ store.availableValidatorFlushes -= depositCount.toUint32 ();
400+
401+ // If we have reached the bootstrap size, mark it as bootstrapped such that we don't re-enter it.
402+ if (
403+ ! store.isBootstrapped
404+ && getAttesterCountAtTime (Timestamp.wrap (block .timestamp ))
405+ >= store.queueConfig.decompress ().bootstrapValidatorSetSize
406+ ) {
407+ store.isBootstrapped = true ;
408+ }
379409 }
380410
381411 /**
@@ -442,7 +472,7 @@ library StakingLib {
442472 }
443473
444474 function getNextFlushableEpoch () internal view returns (Epoch) {
445- return getStorage ().nextFlushableEpoch;
475+ return getStorage ().nextFlushableEpoch. decompress () ;
446476 }
447477
448478 function getEntryQueueLength () internal view returns (uint256 ) {
@@ -533,39 +563,66 @@ library StakingLib {
533563 * `normalFlushSizeMin` to zero and `normalFlushSizeQuotient` to a very high value. If this is done, this
534564 * function will always return zero and no new validator can enter.
535565 *
566+ * @param _activeAttesterCount - The number of active attesters
536567 * @return - The maximum number of validators that could be flushed from the entry queue.
537568 */
538- function getEntryQueueFlushSize () internal view returns (uint256 ) {
569+ function getEntryQueueFlushSize (uint256 _activeAttesterCount ) internal view returns (uint256 ) {
539570 StakingStorage storage store = getStorage ();
540571 StakingQueueConfig memory config = store.queueConfig.decompress ();
541572
542- uint256 activeAttesterCount = getAttesterCountAtTime (Timestamp.wrap (block .timestamp ));
543573 uint256 queueSize = store.entryQueue.length ();
544574
545575 // Only if there is bootstrap values configured will we look into bootstrap or growth phases.
546- if (config.bootstrapValidatorSetSize > 0 ) {
576+ if (config.bootstrapValidatorSetSize > 0 && ! store.isBootstrapped ) {
547577 // If bootstrap:
548- if (activeAttesterCount == 0 && queueSize < config.bootstrapValidatorSetSize) {
578+ if (_activeAttesterCount == 0 && queueSize < config.bootstrapValidatorSetSize) {
549579 return 0 ;
550580 }
551581
552582 // If growth:
553- if (activeAttesterCount < config.bootstrapValidatorSetSize) {
554- return Math. min ( config.bootstrapFlushSize, config.maxQueueFlushSize) ;
583+ if (_activeAttesterCount < config.bootstrapValidatorSetSize) {
584+ return config.bootstrapFlushSize;
555585 }
556586 }
557587
558588 // If normal:
559589 return Math.min (
560- Math.max (activeAttesterCount / config.normalFlushSizeQuotient, config.normalFlushSizeMin),
590+ Math.max (_activeAttesterCount / config.normalFlushSizeQuotient, config.normalFlushSizeMin),
561591 config.maxQueueFlushSize
562592 );
563593 }
564594
595+ function getAvailableValidatorFlushes () internal view returns (uint256 ) {
596+ (uint256 flushes ,,) = _calculateAvailableFlushes ();
597+ return flushes;
598+ }
599+
600+ function getCachedAvailableValidatorFlushes () internal view returns (uint256 ) {
601+ return getStorage ().availableValidatorFlushes;
602+ }
603+
565604 function getStorage () internal pure returns (StakingStorage storage storageStruct ) {
566605 bytes32 position = STAKING_SLOT;
567606 assembly {
568607 storageStruct.slot := position
569608 }
570609 }
610+
611+ function _calculateAvailableFlushes ()
612+ private
613+ view
614+ returns (uint256 flushes , Epoch currentEpoch , bool shouldUpdateState )
615+ {
616+ StakingStorage storage store = getStorage ();
617+ currentEpoch = TimeLib.epochFromTimestamp (Timestamp.wrap (block .timestamp ));
618+
619+ if (store.nextFlushableEpoch.decompress () > currentEpoch) {
620+ return (store.availableValidatorFlushes, currentEpoch, false );
621+ }
622+
623+ uint256 activeAttesterCount = getAttesterCountAtTime (Timestamp.wrap (block .timestamp ));
624+ uint256 newFlushes = getEntryQueueFlushSize (activeAttesterCount);
625+
626+ return (newFlushes, currentEpoch, true );
627+ }
571628}
0 commit comments