Skip to content

Commit 3887ea2

Browse files
committed
feat: queue flushing update
1 parent f52afd9 commit 3887ea2

File tree

23 files changed

+409
-216
lines changed

23 files changed

+409
-216
lines changed

l1-contracts/script/StakingAssetHandler.s.sol

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ contract StakingAssetHandlerScript is Test {
5757
domain: DOMAIN,
5858
scope: SCOPE,
5959
skipBindCheck: false, // DO NOT: skip bind check
60-
skipMerkleCheck: true // DO: skip merkle check
60+
skipMerkleCheck: true, // DO: skip merkle check
61+
validatorsToFlush: 48
6162
});
6263

6364
vm.startBroadcast(ME);

l1-contracts/src/core/Rollup.sol

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,14 @@ contract Rollup is IStaking, IValidatorSelection, IRollup, RollupCore {
534534
return RewardLib.getBlockReward();
535535
}
536536

537+
function getAvailableValidatorFlushes() external view override(IStaking) returns (uint256) {
538+
return ValidatorOperationsExtLib.getAvailableValidatorFlushes();
539+
}
540+
541+
function getIsBootstrapped() external view override(IStaking) returns (bool) {
542+
return StakingLib.getStorage().isBootstrapped;
543+
}
544+
537545
function getBurnAddress() external pure override(IRollup) returns (address) {
538546
return RewardLib.BURN_ADDRESS;
539547
}

l1-contracts/src/core/RollupCore.sol

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ contract RollupCore is EIP712("Aztec Rollup", "1"), Ownable, IStakingCore, IVali
228228
// from the onset). It might be updated later to 0 by governance in order to close the validator set for this
229229
// instance. For details see `StakingLib.getEntryQueueFlushSize` function.
230230
require(_config.stakingQueueConfig.normalFlushSizeMin > 0, Errors.Staking__InvalidStakingQueueConfig());
231+
require(_config.stakingQueueConfig.normalFlushSizeQuotient > 0, Errors.Staking__InvalidNormalFlushSizeQuotient());
231232

232233
TimeLib.initialize(
233234
block.timestamp, _config.aztecSlotDuration, _config.aztecEpochDuration, _config.aztecProofSubmissionEpochs
@@ -448,9 +449,14 @@ contract RollupCore is EIP712("Aztec Rollup", "1"), Ownable, IStakingCore, IVali
448449
* @notice Processes the validator entry queue to add new validators to the active set
449450
* @dev Can be called by anyone. The number of validators added is limited by queue configuration.
450451
* This helps maintain a controlled growth rate of the validator set.
452+
* @param _toAdd - The max number the caller will try to add
451453
*/
454+
function flushEntryQueue(uint256 _toAdd) external override(IStakingCore) {
455+
ValidatorOperationsExtLib.flushEntryQueue(_toAdd);
456+
}
457+
452458
function flushEntryQueue() external override(IStakingCore) {
453-
ValidatorOperationsExtLib.flushEntryQueue();
459+
ValidatorOperationsExtLib.flushEntryQueue(type(uint256).max);
454460
}
455461

456462
/**

l1-contracts/src/core/interfaces/IStaking.sol

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ interface IStakingCore {
4646
bool _moveWithLatestRollup
4747
) external;
4848
function flushEntryQueue() external;
49+
function flushEntryQueue(uint256 _toAdd) external;
4950
function initiateWithdraw(address _attester, address _recipient) external returns (bool);
5051
function finalizeWithdraw(address _attester) external;
5152
function slash(address _attester, uint256 _amount) external returns (bool);
@@ -71,4 +72,6 @@ interface IStaking is IStakingCore {
7172
function getStatus(address _attester) external view returns (Status);
7273
function getNextFlushableEpoch() external view returns (Epoch);
7374
function getEntryQueueLength() external view returns (uint256);
75+
function getAvailableValidatorFlushes() external view returns (uint256);
76+
function getIsBootstrapped() external view returns (bool);
7477
}

l1-contracts/src/core/libraries/Errors.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ library Errors {
149149
error Staking__GovernanceAlreadySet();
150150
error Staking__InsufficientBootstrapValidators(uint256 queueSize, uint256 bootstrapFlushSize);
151151
error Staking__InvalidStakingQueueConfig();
152+
error Staking__InvalidNormalFlushSizeQuotient();
152153

153154
// Fee Juice Portal
154155
error FeeJuicePortal__AlreadyInitialized(); // 0xc7a172fe

l1-contracts/src/core/libraries/rollup/StakingLib.sol

Lines changed: 85 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ import {Proposal} from "@aztec/governance/interfaces/IGovernance.sol";
1717
import {ProposalLib} from "@aztec/governance/libraries/ProposalLib.sol";
1818
import {GovernanceProposer} from "@aztec/governance/proposer/GovernanceProposer.sol";
1919
import {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";
2123
import {IERC20} from "@oz/token/ERC20/IERC20.sol";
2224
import {SafeERC20} from "@oz/token/ERC20/utils/SafeERC20.sol";
2325
import {Math} from "@oz/utils/math/Math.sol";
@@ -78,13 +80,15 @@ struct AttesterView {
7880
struct 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

9094
library 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
}

l1-contracts/src/core/libraries/rollup/ValidatorOperationsExtLib.sol

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ library ValidatorOperationsExtLib {
5454
);
5555
}
5656

57-
function flushEntryQueue() external {
58-
StakingLib.flushEntryQueue();
57+
function flushEntryQueue(uint256 _toAdd) external {
58+
StakingLib.flushEntryQueue(_toAdd);
5959
}
6060

6161
function initiateWithdraw(address _attester, address _recipient) external returns (bool) {
@@ -138,6 +138,11 @@ library ValidatorOperationsExtLib {
138138
}
139139

140140
function getEntryQueueFlushSize() external view returns (uint256) {
141-
return StakingLib.getEntryQueueFlushSize();
141+
uint256 activeAttesterCount = StakingLib.getAttesterCountAtTime(Timestamp.wrap(block.timestamp));
142+
return StakingLib.getEntryQueueFlushSize(activeAttesterCount);
143+
}
144+
145+
function getAvailableValidatorFlushes() external view returns (uint256) {
146+
return StakingLib.getAvailableValidatorFlushes();
142147
}
143148
}

l1-contracts/src/mock/MultiAdder.sol

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ struct CheatDepositArgs {
1616

1717
interface IMultiAdder {
1818
function addValidators(CheatDepositArgs[] memory _args) external;
19-
function addValidators(CheatDepositArgs[] memory _args, bool _skipFlush) external;
19+
function addValidators(CheatDepositArgs[] memory _args, uint256 _toFlush) external;
2020
}
2121

2222
contract MultiAdder is IMultiAdder {
@@ -34,10 +34,10 @@ contract MultiAdder is IMultiAdder {
3434
}
3535

3636
function addValidators(CheatDepositArgs[] memory _args) public override(IMultiAdder) {
37-
addValidators(_args, false);
37+
addValidators(_args, type(uint256).max);
3838
}
3939

40-
function addValidators(CheatDepositArgs[] memory _args, bool _skipFlush) public override(IMultiAdder) {
40+
function addValidators(CheatDepositArgs[] memory _args, uint256 _toFlush) public override(IMultiAdder) {
4141
require(msg.sender == OWNER, NotOwner());
4242
for (uint256 i = 0; i < _args.length; i++) {
4343
STAKING.deposit(
@@ -50,8 +50,8 @@ contract MultiAdder is IMultiAdder {
5050
);
5151
}
5252

53-
if (!_skipFlush && STAKING.getCurrentEpoch() >= STAKING.getNextFlushableEpoch()) {
54-
STAKING.flushEntryQueue();
53+
if (_toFlush != 0) {
54+
STAKING.flushEntryQueue(_toFlush);
5555
}
5656
}
5757
}

0 commit comments

Comments
 (0)