diff --git a/l1-contracts/bootstrap.sh b/l1-contracts/bootstrap.sh index 62f38ac901ea..758fd4a8c0b6 100755 --- a/l1-contracts/bootstrap.sh +++ b/l1-contracts/bootstrap.sh @@ -106,6 +106,7 @@ function inspect { done } + function gas_report { check=${1:-"no"} echo_header "l1-contracts gas report" diff --git a/l1-contracts/gas_benchmark.md b/l1-contracts/gas_benchmark.md index 8a4a7dc15fff..caf86da29bdb 100644 --- a/l1-contracts/gas_benchmark.md +++ b/l1-contracts/gas_benchmark.md @@ -1,62 +1,34 @@ -| src/core/Rollup.sol:Rollup Contract | | | | | | +| src/core/Rollup.sol:Rollup contract | | | | | | +|-------------------------------------|-----------------|----------|----------|----------|---------| | Deployment Cost | Deployment Size | | | | | -|-------------------------------------+-----------------+----------+----------+----------+---------| -| 13939212 | 72687 | | | | | -|-------------------------------------+-----------------+----------+----------+----------+---------| -| | | | | | | -|-------------------------------------+-----------------+----------+----------+----------+---------| -| Function Name | Min | Avg | Median | Max | # Calls | -|-------------------------------------+-----------------+----------+----------+----------+---------| -| cheat__InitialiseValidatorSet | 13825170 | 13825170 | 13825170 | 13825170 | 1 | -|-------------------------------------+-----------------+----------+----------+----------+---------| -| getBlock | 2028 | 2028 | 2028 | 2028 | 12 | -|-------------------------------------+-----------------+----------+----------+----------+---------| -| getBurnAddress | 543 | 543 | 543 | 543 | 1 | -|-------------------------------------+-----------------+----------+----------+----------+---------| -| getCurrentEpoch | 1662 | 1662 | 1662 | 1662 | 397 | -|-------------------------------------+-----------------+----------+----------+----------+---------| -| getCurrentProposer | 142276 | 147339 | 142555 | 304940 | 200 | -|-------------------------------------+-----------------+----------+----------+----------+---------| -| getCurrentSlot | 1308 | 1318 | 1308 | 5308 | 397 | -|-------------------------------------+-----------------+----------+----------+----------+---------| -| getEpochCommittee | 147323 | 152191 | 147335 | 309356 | 100 | -|-------------------------------------+-----------------+----------+----------+----------+---------| -| getEpochForBlock | 1776 | 1776 | 1776 | 1776 | 196 | -|-------------------------------------+-----------------+----------+----------+----------+---------| -| getFeeAssetPortal | 881 | 881 | 881 | 881 | 1 | -|-------------------------------------+-----------------+----------+----------+----------+---------| -| getFeeHeader | 2890 | 2890 | 2890 | 2890 | 95 | -|-------------------------------------+-----------------+----------+----------+----------+---------| -| getManaBaseFeeAt | 33313 | 53419 | 54478 | 55593 | 195 | -|-------------------------------------+-----------------+----------+----------+----------+---------| -| getPendingBlockNumber | 666 | 670 | 666 | 2666 | 401 | -|-------------------------------------+-----------------+----------+----------+----------+---------| -| getProvenBlockNumber | 644 | 644 | 644 | 644 | 3 | -|-------------------------------------+-----------------+----------+----------+----------+---------| -| getTimestampForSlot | 1599 | 1599 | 1599 | 1599 | 195 | -|-------------------------------------+-----------------+----------+----------+----------+---------| -| getVersion | 648 | 668 | 648 | 2648 | 100 | -|-------------------------------------+-----------------+----------+----------+----------+---------| -| setProvingCostPerMana | 26257 | 26284 | 26257 | 29057 | 101 | -|-------------------------------------+-----------------+----------+----------+----------+---------| -| submitEpochRootProof | 1189968 | 1200782 | 1189992 | 1222388 | 3 | -| src/core/messagebridge/Inbox.sol:Inbox Contract | | | | | | +| 8905012 | 44009 | | | | | +| Function Name | min | avg | median | max | # calls | +| cheat__InitialiseValidatorSet | 14773538 | 14773538 | 14773538 | 14773538 | 1 | +| getBlock | 1252 | 1252 | 1252 | 1252 | 12 | +| getBurnAddress | 280 | 280 | 280 | 280 | 1 | +| getCurrentEpoch | 870 | 870 | 870 | 870 | 490 | +| getCurrentProposer | 137000 | 164906 | 137000 | 1532365 | 200 | +| getCurrentSlot | 625 | 629 | 625 | 2625 | 490 | +| getEpochCommittee | 138682 | 138682 | 138682 | 138682 | 100 | +| getEpochForBlock | 1087 | 1087 | 1087 | 1087 | 196 | +| getFeeAssetPortal | 541 | 541 | 541 | 541 | 1 | +| getFeeHeader | 1479 | 1479 | 1479 | 1479 | 95 | +| getManaBaseFeeAt | 15466 | 20025 | 20350 | 24662 | 195 | +| getPendingBlockNumber | 398 | 402 | 398 | 2398 | 494 | +| getProvenBlockNumber | 512 | 512 | 512 | 512 | 3 | +| getTimestampForSlot | 824 | 824 | 824 | 824 | 195 | +| getVersion | 404 | 424 | 404 | 2404 | 100 | +| setProvingCostPerMana | 25937 | 25964 | 25937 | 28737 | 101 | +| submitEpochRootProof | 783299 | 796011 | 783311 | 821423 | 3 | +| src/core/messagebridge/Inbox.sol:Inbox contract | | | | | | +|-------------------------------------------------|-----------------|-----|--------|-----|---------| | Deployment Cost | Deployment Size | | | | | -|-------------------------------------------------+-----------------+-----+--------+-----+---------| -| 0 | 12636 | | | | | -|-------------------------------------------------+-----------------+-----+--------+-----+---------| -| | | | | | | -|-------------------------------------------------+-----------------+-----+--------+-----+---------| -| Function Name | Min | Avg | Median | Max | # Calls | -|-------------------------------------------------+-----------------+-----+--------+-----+---------| -| getFeeAssetPortal | 402 | 402 | 402 | 402 | 1 | -| src/periphery/Forwarder.sol:Forwarder Contract | | | | | | -| Deployment Cost | Deployment Size | | | | | -|------------------------------------------------+-----------------+--------+--------+---------+---------| -| 583246 | 2909 | | | | | -|------------------------------------------------+-----------------+--------+--------+---------+---------| -| | | | | | | -|------------------------------------------------+-----------------+--------+--------+---------+---------| -| Function Name | Min | Avg | Median | Max | # Calls | -|------------------------------------------------+-----------------+--------+--------+---------+---------| -| forward | 758446 | 831897 | 792800 | 2103821 | 100 | +| 0 | 0 | | | | | +| Function Name | min | avg | median | max | # calls | +| getFeeAssetPortal | 234 | 234 | 234 | 234 | 1 | +| src/periphery/Forwarder.sol:Forwarder contract | | | | | | +|------------------------------------------------|-----------------|--------|--------|--------|---------| +| Deployment Cost | Deployment Size | | | | | +| 358690 | 1553 | | | | | +| Function Name | min | avg | median | max | # calls | +| forward | 624506 | 630841 | 629469 | 640840 | 100 | diff --git a/l1-contracts/src/core/Rollup.sol b/l1-contracts/src/core/Rollup.sol index e5cf71bb8164..797c4fd32735 100644 --- a/l1-contracts/src/core/Rollup.sol +++ b/l1-contracts/src/core/Rollup.sol @@ -15,18 +15,17 @@ import { FeeHeader, RollupConfigInput } from "@aztec/core/interfaces/IRollup.sol"; -import { - IStaking, - ValidatorInfo, - Exit, - OperatorInfo, - EnumerableSet -} from "@aztec/core/interfaces/IStaking.sol"; +import {IStaking, ValidatorInfo, Exit, OperatorInfo} from "@aztec/core/interfaces/IStaking.sol"; import {IValidatorSelection} from "@aztec/core/interfaces/IValidatorSelection.sol"; import { FeeLib, FeeHeaderLib, FeeAssetValue, PriceLib } from "@aztec/core/libraries/rollup/FeeLib.sol"; import {HeaderLib} from "@aztec/core/libraries/rollup/HeaderLib.sol"; +import { + AddressSnapshotLib, + SnapshottedAddressSet +} from "@aztec/core/libraries/staking/AddressSnapshotLib.sol"; + import {EpochProofLib} from "./libraries/rollup/EpochProofLib.sol"; import {ProposeLib, ValidateHeaderArgs} from "./libraries/rollup/ProposeLib.sol"; import {ValidatorSelectionLib} from "./libraries/validator-selection/ValidatorSelectionLib.sol"; @@ -59,7 +58,7 @@ import { * about the state of the rollup and test it. */ contract Rollup is IStaking, IValidatorSelection, IRollup, RollupCore { - using EnumerableSet for EnumerableSet.AddressSet; + using AddressSnapshotLib for SnapshottedAddressSet; using TimeLib for Timestamp; using TimeLib for Slot; diff --git a/l1-contracts/src/core/RollupCore.sol b/l1-contracts/src/core/RollupCore.sol index eb443a15929c..20dc0daf00f7 100644 --- a/l1-contracts/src/core/RollupCore.sol +++ b/l1-contracts/src/core/RollupCore.sol @@ -176,7 +176,6 @@ contract RollupCore is external override(IStakingCore) { - setupEpoch(); StakingLib.deposit(_attester, _proposer, _withdrawer, _amount); } @@ -185,7 +184,6 @@ contract RollupCore is override(IStakingCore) returns (bool) { - setupEpoch(); return StakingLib.initiateWithdraw(_attester, _recipient); } diff --git a/l1-contracts/src/core/interfaces/IStaking.sol b/l1-contracts/src/core/interfaces/IStaking.sol index 3460af47db1a..2da8a46ffcab 100644 --- a/l1-contracts/src/core/interfaces/IStaking.sol +++ b/l1-contracts/src/core/interfaces/IStaking.sol @@ -2,9 +2,9 @@ // Copyright 2024 Aztec Labs. pragma solidity >=0.8.27; +import {SnapshottedAddressSet} from "@aztec/core/libraries/staking/AddressSnapshotLib.sol"; import {Timestamp} from "@aztec/core/libraries/TimeMath.sol"; import {IERC20} from "@oz/token/ERC20/IERC20.sol"; -import {EnumerableSet} from "@oz/utils/structs/EnumerableSet.sol"; // None -> Does not exist in our setup // Validating -> Participating as validator @@ -40,7 +40,7 @@ struct StakingStorage { address slasher; uint256 minimumStake; Timestamp exitDelay; - EnumerableSet.AddressSet attesters; + SnapshottedAddressSet attesters; mapping(address attester => ValidatorInfo) info; mapping(address attester => Exit) exits; } diff --git a/l1-contracts/src/core/interfaces/IValidatorSelection.sol b/l1-contracts/src/core/interfaces/IValidatorSelection.sol index 1962dfd899d7..51825e4db508 100644 --- a/l1-contracts/src/core/interfaces/IValidatorSelection.sol +++ b/l1-contracts/src/core/interfaces/IValidatorSelection.sol @@ -11,6 +11,7 @@ import {Timestamp, Slot, Epoch} from "@aztec/core/libraries/TimeLib.sol"; * @param nextSeed - The seed used to influence the NEXT epoch */ struct EpochData { + // TODO: remove in favor of commitment to comittee address[] committee; uint256 sampleSeed; uint256 nextSeed; diff --git a/l1-contracts/src/core/libraries/Errors.sol b/l1-contracts/src/core/libraries/Errors.sol index fe8e44944930..3248621ef91c 100644 --- a/l1-contracts/src/core/libraries/Errors.sol +++ b/l1-contracts/src/core/libraries/Errors.sol @@ -134,4 +134,7 @@ library Errors { // FeeLib error FeeLib__InvalidFeeAssetPriceModifier(); // 0xf2fb32ad + + // AddressSnapshotLib + error AddressSnapshotLib__IndexOutOfBounds(uint256 index, uint256 size); // 0xd789b71a } diff --git a/l1-contracts/src/core/libraries/TimeLib.sol b/l1-contracts/src/core/libraries/TimeLib.sol index ef8edbe7485a..3756e79c5486 100644 --- a/l1-contracts/src/core/libraries/TimeLib.sol +++ b/l1-contracts/src/core/libraries/TimeLib.sol @@ -5,20 +5,24 @@ pragma solidity >=0.8.27; // solhint-disable-next-line no-unused-import import {Timestamp, Slot, Epoch, SlotLib, EpochLib} from "@aztec/core/libraries/TimeMath.sol"; +import {SafeCast} from "@oz/utils/math/SafeCast.sol"; + struct TimeStorage { - uint256 genesisTime; - uint256 slotDuration; // Number of seconds in a slot - uint256 epochDuration; // Number of slots in an epoch + uint128 genesisTime; + uint32 slotDuration; // Number of seconds in a slot + uint32 epochDuration; // Number of slots in an epoch } library TimeLib { + using SafeCast for uint256; + bytes32 private constant TIME_STORAGE_POSITION = keccak256("aztec.time.storage"); function initialize(uint256 _genesisTime, uint256 _slotDuration, uint256 _epochDuration) internal { TimeStorage storage store = getStorage(); - store.genesisTime = _genesisTime; - store.slotDuration = _slotDuration; - store.epochDuration = _epochDuration; + store.genesisTime = _genesisTime.toUint128(); + store.slotDuration = _slotDuration.toUint32(); + store.epochDuration = _epochDuration.toUint32(); } function toTimestamp(Slot _a) internal view returns (Timestamp) { @@ -45,6 +49,7 @@ library TimeLib { function epochFromTimestamp(Timestamp _a) internal view returns (Epoch) { TimeStorage storage store = getStorage(); + return Epoch.wrap( (Timestamp.unwrap(_a) - store.genesisTime) / (store.epochDuration * store.slotDuration) ); diff --git a/l1-contracts/src/core/libraries/staking/AddressSnapshotLib.sol b/l1-contracts/src/core/libraries/staking/AddressSnapshotLib.sol new file mode 100644 index 000000000000..3c63f6463ee2 --- /dev/null +++ b/l1-contracts/src/core/libraries/staking/AddressSnapshotLib.sol @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2025 Aztec Labs. +pragma solidity >=0.8.27; + +import {Errors} from "@aztec/core/libraries/Errors.sol"; +import {Timestamp, Epoch, TimeLib} from "@aztec/core/libraries/TimeLib.sol"; +import {SafeCast} from "@oz/utils/math/SafeCast.sol"; +import {Checkpoints} from "@oz/utils/structs/Checkpoints.sol"; + +/** + * @notice Structure to store a set of addresses with their historical snapshots + * @param size The current number of addresses in the set + * @param checkpoints Mapping of index to array of address snapshots + * @param attestorToIndex Mapping of attestor address to its current index in the set + */ +struct SnapshottedAddressSet { + // This size must also be snapshotted + Checkpoints.Trace224 size; + mapping(uint256 index => Checkpoints.Trace224) checkpoints; + // Store up to date position for each address + mapping(address addr => Index index) addressToIndex; +} + +struct Index { + bool exists; + uint224 index; +} + +/** + * @title AddressSnapshotLib + * @notice A library for managing a set of addresses with historical snapshots + * @dev This library provides functionality similar to EnumerableSet but can track addresses across different epochs + * and allows querying the state of addresses at any point in time + */ +library AddressSnapshotLib { + using SafeCast for *; + using Checkpoints for Checkpoints.Trace224; + + /** + * @notice Adds a validator to the set + * @param _self The storage reference to the set + * @param _address The address to add + * @return bool True if the address was added, false if it was already present + */ + function add(SnapshottedAddressSet storage _self, address _address) internal returns (bool) { + // Prevent against double insertion + if (_self.addressToIndex[_address].exists) { + return false; + } + + // Insert into the end of the array + Epoch _nextEpoch = TimeLib.epochFromTimestamp(Timestamp.wrap(block.timestamp)) + Epoch.wrap(1); + + uint224 index = _self.size.latest(); + + _self.addressToIndex[_address] = Index({exists: true, index: index}); + _self.checkpoints[index].push( + Epoch.unwrap(_nextEpoch).toUint32(), uint160(_address).toUint224() + ); + + _self.size.push(Epoch.unwrap(_nextEpoch).toUint32(), (index + 1).toUint224()); + return true; + } + + /** + * @notice Removes a address from the set by address + * @param _self The storage reference to the set + * @param _address The address of the address to remove + * @return bool True if the address was removed, false if it wasn't found + */ + function remove(SnapshottedAddressSet storage _self, address _address) internal returns (bool) { + Index memory index = _self.addressToIndex[_address]; + if (!index.exists) { + return false; + } + + return _remove(_self, index.index, _address); + } + + /** + * @notice Removes a validator from the set by index + * @param _self The storage reference to the set + * @param _index The index of the validator to remove + * @return bool True if the validator was removed, false otherwise + */ + function remove(SnapshottedAddressSet storage _self, uint224 _index) internal returns (bool) { + address _address = address(_self.checkpoints[_index].latest().toUint160()); + return _remove(_self, _index, _address); + } + + /** + * @notice Removes a validator from the set by index + * @param _self The storage reference to the set + * @param _index The index of the validator to remove + * @return bool True if the validator was removed, false otherwise + */ + function _remove(SnapshottedAddressSet storage _self, uint224 _index, address _address) + internal + returns (bool) + { + uint224 size = _self.size.latest(); + if (_index >= size) { + revert Errors.AddressSnapshotLib__IndexOutOfBounds(_index, size); + } + + // To remove from the list, we push the last item into the index and reduce the size + uint224 lastIndex = size - 1; + uint256 nextEpoch = + Epoch.unwrap(TimeLib.epochFromTimestamp(Timestamp.wrap(block.timestamp)) + Epoch.wrap(1)); + + address lastValidator = address(_self.checkpoints[lastIndex].latest().toUint160()); + + // If we are removing the last item, we cannot swap it with anything + // so we append a new address of zero for this epoch + // And since we are removing it, we set the location to 0 + _self.addressToIndex[_address] = Index({exists: false, index: 0}); + if (lastIndex == _index) { + _self.checkpoints[_index].push(nextEpoch.toUint32(), uint224(0)); + } else { + // Otherwise, we swap the last item with the item we are removing + // and update the location of the last item + _self.addressToIndex[lastValidator] = Index({exists: true, index: _index.toUint224()}); + _self.checkpoints[_index].push(nextEpoch.toUint32(), uint160(lastValidator).toUint224()); + } + + _self.size.push(nextEpoch.toUint32(), (lastIndex).toUint224()); + return true; + } + + /** + * @notice Gets the current address at a specific index at the time right now + * @param _self The storage reference to the set + * @param _index The index to query + * @return address The current address at the given index + */ + function at(SnapshottedAddressSet storage _self, uint256 _index) internal view returns (address) { + Epoch currentEpoch = TimeLib.epochFromTimestamp(Timestamp.wrap(block.timestamp)); + return getAddressFromIndexAtEpoch(_self, _index, currentEpoch); + } + + /** + * @notice Gets the address at a specific index and epoch + * @param _self The storage reference to the set + * @param _index The index to query + * @param _epoch The epoch number to query + * @return address The address at the given index and epoch + */ + function getAddressFromIndexAtEpoch( + SnapshottedAddressSet storage _self, + uint256 _index, + Epoch _epoch + ) internal view returns (address) { + uint256 size = lengthAtEpoch(_self, _epoch); + + if (_index >= size) { + revert Errors.AddressSnapshotLib__IndexOutOfBounds(_index, size); + } + + uint224 addr = _self.checkpoints[_index].upperLookup(Epoch.unwrap(_epoch).toUint32()); + return address(addr.toUint160()); + } + + /** + * @notice Gets the current size of the set + * @param _self The storage reference to the set + * @return uint256 The number of addresses in the set + */ + function length(SnapshottedAddressSet storage _self) internal view returns (uint256) { + Epoch currentEpoch = TimeLib.epochFromTimestamp(Timestamp.wrap(block.timestamp)); + return lengthAtEpoch(_self, currentEpoch); + } + + /** + * @notice Gets the size of the set at a specific epoch + * @param _self The storage reference to the set + * @param _epoch The epoch number to query + * @return uint256 The number of addresses in the set at the given epoch + * + * @dev Note, the values returned from this function are in flux if the epoch is in the future. + */ + function lengthAtEpoch(SnapshottedAddressSet storage _self, Epoch _epoch) + internal + view + returns (uint256) + { + return _self.size.upperLookup(Epoch.unwrap(_epoch).toUint32()); + } + + /** + * @notice Gets all current addresses in the set + * @param _self The storage reference to the set + * @return address[] Array of all current addresses in the set + */ + function values(SnapshottedAddressSet storage _self) internal view returns (address[] memory) { + Epoch currentEpoch = TimeLib.epochFromTimestamp(Timestamp.wrap(block.timestamp)); + return valuesAtEpoch(_self, currentEpoch); + } + + /** + * @notice Gets all addresses in the set at a specific epoch + * @param _self The storage reference to the set + * @param _epoch The epoch number to query + * @return address[] Array of all addresses in the set at the given epoch + * + * @dev Note, the values returned from this function are in flux if the epoch is in the future. + * + */ + function valuesAtEpoch(SnapshottedAddressSet storage _self, Epoch _epoch) + internal + view + returns (address[] memory) + { + uint256 size = lengthAtEpoch(_self, _epoch); + address[] memory vals = new address[](size); + for (uint256 i; i < size;) { + vals[i] = getAddressFromIndexAtEpoch(_self, i, _epoch); + + unchecked { + i++; + } + } + return vals; + } +} diff --git a/l1-contracts/src/core/libraries/staking/StakingLib.sol b/l1-contracts/src/core/libraries/staking/StakingLib.sol index 87ecfda67dfe..efa517fc789f 100644 --- a/l1-contracts/src/core/libraries/staking/StakingLib.sol +++ b/l1-contracts/src/core/libraries/staking/StakingLib.sol @@ -11,13 +11,16 @@ import { IStakingCore } from "@aztec/core/interfaces/IStaking.sol"; import {Errors} from "@aztec/core/libraries/Errors.sol"; +import { + AddressSnapshotLib, + SnapshottedAddressSet +} from "@aztec/core/libraries/staking/AddressSnapshotLib.sol"; import {IERC20} from "@oz/token/ERC20/IERC20.sol"; import {SafeERC20} from "@oz/token/ERC20/utils/SafeERC20.sol"; -import {EnumerableSet} from "@oz/utils/structs/EnumerableSet.sol"; library StakingLib { using SafeERC20 for IERC20; - using EnumerableSet for EnumerableSet.AddressSet; + using AddressSnapshotLib for SnapshottedAddressSet; bytes32 private constant STAKING_SLOT = keccak256("aztec.core.staking.storage"); @@ -78,6 +81,7 @@ library StakingLib { // gas and cost edge cases around recipient, so lets just avoid that. if (validator.status == Status.VALIDATING && validator.stake < store.minimumStake) { require(store.attesters.remove(_attester), Errors.Staking__FailedToRemove(_attester)); + validator.status = Status.LIVING; } diff --git a/l1-contracts/src/core/libraries/validator-selection/ValidatorSelectionLib.sol b/l1-contracts/src/core/libraries/validator-selection/ValidatorSelectionLib.sol index 0d1398e431b3..b58395276d9f 100644 --- a/l1-contracts/src/core/libraries/validator-selection/ValidatorSelectionLib.sol +++ b/l1-contracts/src/core/libraries/validator-selection/ValidatorSelectionLib.sol @@ -10,6 +10,10 @@ import { import {SampleLib} from "@aztec/core/libraries/crypto/SampleLib.sol"; import {SignatureLib, Signature} from "@aztec/core/libraries/crypto/SignatureLib.sol"; import {Errors} from "@aztec/core/libraries/Errors.sol"; +import { + AddressSnapshotLib, + SnapshottedAddressSet +} from "@aztec/core/libraries/staking/AddressSnapshotLib.sol"; import {Timestamp, Slot, Epoch, TimeLib} from "@aztec/core/libraries/TimeLib.sol"; import {MessageHashUtils} from "@oz/utils/cryptography/MessageHashUtils.sol"; import {EnumerableSet} from "@oz/utils/structs/EnumerableSet.sol"; @@ -19,6 +23,7 @@ library ValidatorSelectionLib { using MessageHashUtils for bytes32; using SignatureLib for Signature; using TimeLib for Timestamp; + using AddressSnapshotLib for SnapshottedAddressSet; bytes32 private constant VALIDATOR_SELECTION_STORAGE_POSITION = keccak256("aztec.validator_selection.storage"); @@ -46,7 +51,7 @@ library ValidatorSelectionLib { if (epoch.sampleSeed == 0) { epoch.sampleSeed = getSampleSeed(epochNumber); epoch.nextSeed = store.lastSeed = computeNextSeed(epochNumber); - epoch.committee = sampleValidators(_stakingStore, epoch.sampleSeed); + epoch.committee = sampleValidators(_stakingStore, epochNumber, epoch.sampleSeed); } } @@ -154,21 +159,22 @@ library ValidatorSelectionLib { * * @return The validators for the given epoch */ - function sampleValidators(StakingStorage storage _stakingStore, uint256 _seed) + function sampleValidators(StakingStorage storage _stakingStore, Epoch _epoch, uint256 _seed) internal returns (address[] memory) { - uint256 validatorSetSize = _stakingStore.attesters.length(); + ValidatorSelectionStorage storage store = getStorage(); + uint256 validatorSetSize = _stakingStore.attesters.lengthAtEpoch(_epoch); + if (validatorSetSize == 0) { return new address[](0); } - ValidatorSelectionStorage storage store = getStorage(); uint256 targetCommitteeSize = store.targetCommitteeSize; // If we have less validators than the target committee size, we just return the full set if (validatorSetSize <= targetCommitteeSize) { - return _stakingStore.attesters.values(); + return _stakingStore.attesters.valuesAtEpoch(_epoch); } uint256[] memory indices = @@ -176,7 +182,7 @@ library ValidatorSelectionLib { address[] memory committee = new address[](targetCommitteeSize); for (uint256 i = 0; i < targetCommitteeSize; i++) { - committee[i] = _stakingStore.attesters.at(indices[i]); + committee[i] = _stakingStore.attesters.getAddressFromIndexAtEpoch(indices[i], _epoch); } return committee; } @@ -203,7 +209,8 @@ library ValidatorSelectionLib { // Emulate a sampling of the validators uint256 sampleSeed = getSampleSeed(_epochNumber); - return sampleValidators(_stakingStore, sampleSeed); + + return sampleValidators(_stakingStore, _epochNumber, sampleSeed); } /** diff --git a/l1-contracts/test/benchmark/happy.t.sol b/l1-contracts/test/benchmark/happy.t.sol index 82dbd69d4e6f..4e1c59858849 100644 --- a/l1-contracts/test/benchmark/happy.t.sol +++ b/l1-contracts/test/benchmark/happy.t.sol @@ -290,8 +290,9 @@ contract BenchmarkRollupTest is FeeModelTestPoints, DecoderBase { } function test_Benchmarking() public { - Slot nextSlot = Slot.wrap(1); - Epoch nextEpoch = Epoch.wrap(1); + // Do nothing for the first epoch + Slot nextSlot = Slot.wrap(EPOCH_DURATION + 1); + Epoch nextEpoch = Epoch.wrap(2); rollup.setProvingCostPerMana( EthValue.wrap(points[0].outputs.mana_base_fee_components_in_wei.proving_cost) diff --git a/l1-contracts/test/staking/StakingCheater.sol b/l1-contracts/test/staking/StakingCheater.sol index 3be09350e7d2..450df1a45288 100644 --- a/l1-contracts/test/staking/StakingCheater.sol +++ b/l1-contracts/test/staking/StakingCheater.sol @@ -3,17 +3,25 @@ pragma solidity >=0.8.27; import {IERC20} from "@oz/token/ERC20/IERC20.sol"; -import {EnumerableSet} from "@oz/utils/structs/EnumerableSet.sol"; import { IStaking, ValidatorInfo, Exit, OperatorInfo, Status } from "@aztec/core/interfaces/IStaking.sol"; +import {TimeCheater} from "./TimeCheater.sol"; import {Timestamp} from "@aztec/core/libraries/TimeLib.sol"; import {StakingLib} from "@aztec/core/libraries/staking/StakingLib.sol"; import {Slasher} from "@aztec/core/staking/Slasher.sol"; +import { + AddressSnapshotLib, + SnapshottedAddressSet +} from "@aztec/core/libraries/staking/AddressSnapshotLib.sol"; + +import {TestConstants} from "@test/harnesses/TestConstants.sol"; contract StakingCheater is IStaking { - using EnumerableSet for EnumerableSet.AddressSet; + using AddressSnapshotLib for SnapshottedAddressSet; + + TimeCheater internal timeCheater; constructor( IERC20 _stakingAsset, @@ -21,6 +29,12 @@ contract StakingCheater is IStaking { uint256 _slashingQuorum, uint256 _roundSize ) { + timeCheater = new TimeCheater( + address(this), + block.timestamp, + TestConstants.AZTEC_SLOT_DURATION, + TestConstants.AZTEC_EPOCH_DURATION + ); Timestamp exitDelay = Timestamp.wrap(60 * 60 * 24); Slasher slasher = new Slasher(_slashingQuorum, _roundSize); StakingLib.initialize(_stakingAsset, _minimumStake, exitDelay, address(slasher)); @@ -110,4 +124,8 @@ contract StakingCheater is IStaking { function cheat__RemoveAttester(address _attester) external { StakingLib.getStorage().attesters.remove(_attester); } + + function cheat__progressEpoch() external { + timeCheater.cheat__progressEpoch(); + } } diff --git a/l1-contracts/test/staking/TimeCheater.sol b/l1-contracts/test/staking/TimeCheater.sol new file mode 100644 index 000000000000..9d1394144fd3 --- /dev/null +++ b/l1-contracts/test/staking/TimeCheater.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2025 Aztec Labs. +pragma solidity >=0.8.27; + +import {TimeStorage, TimeLib} from "@aztec/core/libraries/TimeLib.sol"; +import {Vm} from "forge-std/Vm.sol"; + +contract TimeCheater { + Vm public constant vm = Vm(address(bytes20(uint160(uint256(keccak256("hevm cheat code")))))); + bytes32 public constant TIME_STORAGE_POSITION = keccak256("aztec.time.storage"); + + address public immutable target; + uint256 public genesisTime; + uint256 public slotDuration; + uint256 public epochDuration; + + uint256 public currentEpoch; + + constructor( + address _target, + uint256 _genesisTime, + uint256 _slotDuration, + uint256 _epochDuration + ) { + target = _target; + + genesisTime = _genesisTime; + slotDuration = _slotDuration; + epochDuration = _epochDuration; + cheat__setTimeStorage( + TimeStorage({ + genesisTime: uint128(_genesisTime), + slotDuration: uint32(_slotDuration), + epochDuration: uint32(_epochDuration) + }) + ); + } + + function cheat__setTimeStorage(TimeStorage memory _timeStorage) public { + vm.store( + target, + TIME_STORAGE_POSITION, + bytes32( + abi.encodePacked( + // Encoding order is a fun thing. + bytes8(0), + _timeStorage.epochDuration, + _timeStorage.slotDuration, + _timeStorage.genesisTime + ) + ) + ); + } + + function cheat__setEpochNow(uint256 _epoch) public { + vm.warp(genesisTime + 1 + _epoch * slotDuration * epochDuration); + currentEpoch = _epoch; + } + + function cheat__progressEpoch() public { + currentEpoch++; + vm.warp(genesisTime + 1 + currentEpoch * slotDuration * epochDuration); + } + + function cheat_jumpForwardEpochs(uint256 _epochs) public { + currentEpoch += _epochs; + vm.warp(genesisTime + 1 + currentEpoch * slotDuration * epochDuration); + } +} diff --git a/l1-contracts/test/staking/base.t.sol b/l1-contracts/test/staking/base.t.sol index 18fc8cddb105..ebfcdd11198e 100644 --- a/l1-contracts/test/staking/base.t.sol +++ b/l1-contracts/test/staking/base.t.sol @@ -4,6 +4,7 @@ pragma solidity >=0.8.27; import {TestBase} from "@test/base/Base.sol"; import {StakingCheater} from "./StakingCheater.sol"; +import {TestConstants} from "../harnesses/TestConstants.sol"; import {TestERC20} from "@aztec/mock/TestERC20.sol"; contract StakingBase is TestBase { diff --git a/l1-contracts/test/staking/finaliseWithdraw.t.sol b/l1-contracts/test/staking/finaliseWithdraw.t.sol index 4dc11259cae1..b96b7c5a2924 100644 --- a/l1-contracts/test/staking/finaliseWithdraw.t.sol +++ b/l1-contracts/test/staking/finaliseWithdraw.t.sol @@ -32,6 +32,9 @@ contract FinaliseWithdrawTest is StakingBase { _amount: MINIMUM_STAKE }); + // Progress into the next epoch + staking.cheat__progressEpoch(); + vm.prank(WITHDRAWER); staking.initiateWithdraw(ATTESTER, RECIPIENT); diff --git a/l1-contracts/test/staking/getters.t.sol b/l1-contracts/test/staking/getters.t.sol index ceb09a1d4b54..10787e6051c4 100644 --- a/l1-contracts/test/staking/getters.t.sol +++ b/l1-contracts/test/staking/getters.t.sol @@ -16,6 +16,9 @@ contract GettersTest is StakingBase { _withdrawer: WITHDRAWER, _amount: MINIMUM_STAKE }); + + // Progress into the next epoch + staking.cheat__progressEpoch(); } function test_getAttesterAtIndex() external view { diff --git a/l1-contracts/test/staking/initiateWithdraw.t.sol b/l1-contracts/test/staking/initiateWithdraw.t.sol index cb93ad8ed3f1..f08f2c9d1074 100644 --- a/l1-contracts/test/staking/initiateWithdraw.t.sol +++ b/l1-contracts/test/staking/initiateWithdraw.t.sol @@ -27,6 +27,8 @@ contract InitiateWithdrawTest is StakingBase { _amount: MINIMUM_STAKE }); + // Progress into the next epoch + staking.cheat__progressEpoch(); _; } @@ -117,6 +119,12 @@ contract InitiateWithdrawTest is StakingBase { assertEq(exit.recipient, RECIPIENT); info = staking.getInfo(ATTESTER); assertTrue(info.status == Status.EXITING); + + // The active attester count should not change until we reach the next epoch + assertEq(staking.getActiveAttesterCount(), 1); + + // Move to next epoch for changes to take effect + staking.cheat__progressEpoch(); assertEq(staking.getActiveAttesterCount(), 0); } @@ -128,6 +136,12 @@ contract InitiateWithdrawTest is StakingBase { staking.cheat__SetStatus(ATTESTER, Status.LIVING); staking.cheat__RemoveAttester(ATTESTER); + // The active attester count should not change until we reach the next epoch + assertEq(staking.getActiveAttesterCount(), 1); + + // Progress into the next epoch for changes to take effect + staking.cheat__progressEpoch(); + assertEq(stakingAsset.balanceOf(address(staking)), MINIMUM_STAKE); assertEq(stakingAsset.balanceOf(RECIPIENT), 0); Exit memory exit = staking.getExit(ATTESTER); @@ -135,6 +149,7 @@ contract InitiateWithdrawTest is StakingBase { assertEq(exit.recipient, address(0)); ValidatorInfo memory info = staking.getInfo(ATTESTER); assertTrue(info.status == Status.LIVING); + assertEq(staking.getActiveAttesterCount(), 0); vm.expectEmit(true, true, true, true, address(staking)); diff --git a/l1-contracts/test/staking/slash.t.sol b/l1-contracts/test/staking/slash.t.sol index 1d3279ac7a8b..b1edbe1c5eec 100644 --- a/l1-contracts/test/staking/slash.t.sol +++ b/l1-contracts/test/staking/slash.t.sol @@ -41,6 +41,9 @@ contract SlashTest is StakingBase { _withdrawer: WITHDRAWER, _amount: DEPOSIT_AMOUNT }); + + // Progress into the next epoch + staking.cheat__progressEpoch(); _; } @@ -170,6 +173,12 @@ contract SlashTest is StakingBase { info = staking.getInfo(ATTESTER); assertEq(info.stake, balance - slashingAmount); assertTrue(info.status == Status.LIVING); + + // The active attester count should not change until we reach the next epoch + assertEq(staking.getActiveAttesterCount(), activeAttesterCount); + + // Move to next epoch for changes to take effect + staking.cheat__progressEpoch(); assertEq(staking.getActiveAttesterCount(), activeAttesterCount - 1); } } diff --git a/l1-contracts/test/staking_asset_handler/setMintInterval.t.sol b/l1-contracts/test/staking_asset_handler/setMintInterval.t.sol index d07aa8126c8a..37a9409b7cd4 100644 --- a/l1-contracts/test/staking_asset_handler/setMintInterval.t.sol +++ b/l1-contracts/test/staking_asset_handler/setMintInterval.t.sol @@ -51,7 +51,7 @@ contract SetMintIntervalTest is StakingAssetHandlerBase { } function test_WhenOwnerTriesToMintAfterTheNewIntervalHasPassed(uint256 _newMintInterval) external { - _newMintInterval = bound(_newMintInterval, mintInterval + 1, 1e18); + _newMintInterval = bound(_newMintInterval, mintInterval + 1, 1e12); stakingAssetHandler.setMintInterval(_newMintInterval); vm.warp(block.timestamp + _newMintInterval); // it mints diff --git a/l1-contracts/test/validator-selection/ValidatorSelection.t.sol b/l1-contracts/test/validator-selection/ValidatorSelection.t.sol index 4c9fefea708e..53783a1c1011 100644 --- a/l1-contracts/test/validator-selection/ValidatorSelection.t.sol +++ b/l1-contracts/test/validator-selection/ValidatorSelection.t.sol @@ -24,11 +24,12 @@ import {CheatDepositArgs} from "@aztec/core/interfaces/IRollup.sol"; import {Slot, Epoch, EpochLib, Timestamp} from "@aztec/core/libraries/TimeLib.sol"; import {RewardDistributor} from "@aztec/governance/RewardDistributor.sol"; - import {SlashFactory} from "@aztec/periphery/SlashFactory.sol"; import {Slasher, IPayload} from "@aztec/core/staking/Slasher.sol"; import {IValidatorSelection} from "@aztec/core/interfaces/IValidatorSelection.sol"; import {Status, ValidatorInfo} from "@aztec/core/interfaces/IStaking.sol"; + +import {TimeCheater} from "../staking/TimeCheater.sol"; // solhint-disable comprehensive-interface /** @@ -54,6 +55,7 @@ contract ValidatorSelectionTest is DecoderBase { TestERC20 internal testERC20; RewardDistributor internal rewardDistributor; Signature internal emptySignature; + TimeCheater internal timeCheater; mapping(address attester => uint256 privateKey) internal attesterPrivateKeys; mapping(address proposer => uint256 privateKey) internal proposerPrivateKeys; mapping(address proposer => address attester) internal proposerToAttester; @@ -70,6 +72,13 @@ contract ValidatorSelectionTest is DecoderBase { uint256 slotNumber = full.block.decodedHeader.globalVariables.slotNumber; uint256 initialTime = full.block.decodedHeader.globalVariables.timestamp - slotNumber * TestConstants.AZTEC_SLOT_DURATION; + + timeCheater = new TimeCheater( + address(rollup), + initialTime, + TestConstants.AZTEC_SLOT_DURATION, + TestConstants.AZTEC_EPOCH_DURATION + ); vm.warp(initialTime); } @@ -116,13 +125,15 @@ contract ValidatorSelectionTest is DecoderBase { merkleTestUtil = new MerkleTestUtil(); + // Progress into the next epoch for changes to take effect + timeCheater.cheat__progressEpoch(); _; } function testInitialCommitteeMatch() public setup(4) { address[] memory attesters = rollup.getAttesters(); address[] memory committee = rollup.getCurrentEpochCommittee(); - assertEq(rollup.getCurrentEpoch(), 0); + assertEq(rollup.getCurrentEpoch(), 1); assertEq(attesters.length, 4, "Invalid validator set size"); assertEq(committee.length, 4, "invalid committee set size"); diff --git a/l1-contracts/test/validator-selection/address_snapshots/AddressSnapshotsBase.t.sol b/l1-contracts/test/validator-selection/address_snapshots/AddressSnapshotsBase.t.sol new file mode 100644 index 000000000000..7a06576981b8 --- /dev/null +++ b/l1-contracts/test/validator-selection/address_snapshots/AddressSnapshotsBase.t.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2025 Aztec Labs. +pragma solidity >=0.8.27; + +import { + AddressSnapshotLib, + SnapshottedAddressSet +} from "@aztec/core/libraries/staking/AddressSnapshotLib.sol"; + +import {Test} from "forge-std/Test.sol"; +import {TimeLib, TimeStorage, Epoch} from "@aztec/core/libraries/TimeLib.sol"; +import {TimeCheater} from "../../staking/TimeCheater.sol"; +import {TestConstants} from "../../harnesses/TestConstants.sol"; + +contract AddressSetWrapper { + using AddressSnapshotLib for SnapshottedAddressSet; + + SnapshottedAddressSet private validatorSet; + + function add(address _address) public returns (bool) { + return validatorSet.add(_address); + } + + function remove(uint224 _index) public returns (bool) { + return validatorSet.remove(_index); + } + + function remove(address _address) public returns (bool) { + return validatorSet.remove(_address); + } + + function at(uint256 _index) public view returns (address) { + return validatorSet.at(_index); + } + + function getAddressFromIndexAtEpoch(uint256 _index, Epoch _epoch) public view returns (address) { + return validatorSet.getAddressFromIndexAtEpoch(_index, _epoch); + } + + function length() public view returns (uint256) { + return validatorSet.length(); + } + + function lengthAtEpoch(Epoch _epoch) public view returns (uint256) { + return validatorSet.lengthAtEpoch(_epoch); + } + + function values() public view returns (address[] memory) { + return validatorSet.values(); + } + + function valuesAtEpoch(Epoch _epoch) public view returns (address[] memory) { + return validatorSet.valuesAtEpoch(_epoch); + } +} + +contract AddressSnapshotsBase is Test { + using AddressSnapshotLib for SnapshottedAddressSet; + + uint256 private constant SLOT_DURATION = TestConstants.AZTEC_SLOT_DURATION; + uint256 private constant EPOCH_DURATION = TestConstants.AZTEC_EPOCH_DURATION; + uint256 private immutable GENESIS_TIME = block.timestamp; + + AddressSetWrapper internal validatorSet; + TimeCheater internal timeCheater; + + function boundUnique(address[] memory _addrs) internal pure returns (address[] memory) { + // Ensure addresses within _addrSet1 are unique + vm.assume(_addrs.length > 0); + for (uint256 i = 0; i < _addrs.length; i++) { + for (uint256 j = 0; j < i; j++) { + vm.assume(_addrs[i] != _addrs[j]); + } + } + return _addrs; + } + + function setUp() public { + validatorSet = new AddressSetWrapper(); + timeCheater = + new TimeCheater(address(validatorSet), GENESIS_TIME, SLOT_DURATION, EPOCH_DURATION); + } +} diff --git a/l1-contracts/test/validator-selection/address_snapshots/add.t.sol b/l1-contracts/test/validator-selection/address_snapshots/add.t.sol new file mode 100644 index 000000000000..8d231395d615 --- /dev/null +++ b/l1-contracts/test/validator-selection/address_snapshots/add.t.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2025 Aztec Labs. +pragma solidity >=0.8.27; + +import {AddressSnapshotsBase} from "./AddressSnapshotsBase.t.sol"; +import { + AddressSnapshotLib, + SnapshottedAddressSet +} from "@aztec/core/libraries/staking/AddressSnapshotLib.sol"; +import {Epoch} from "@aztec/core/libraries/TimeLib.sol"; +import {Errors} from "@aztec/core/libraries/Errors.sol"; + +contract AddressSnapshotAddTest is AddressSnapshotsBase { + function test_WhenValidatorIsNotInTheSet(address _addr) public { + // It returns true + // It increases the length + // It creates a checkpoint for the next epoch + // It does not change the current epoch + + timeCheater.cheat__setEpochNow(1); + assertTrue(validatorSet.add(_addr)); + assertEq(validatorSet.length(), 0); + + timeCheater.cheat__setEpochNow(2); + assertEq(validatorSet.length(), 1); + + vm.expectRevert( + abi.encodeWithSelector(Errors.AddressSnapshotLib__IndexOutOfBounds.selector, 0, 0) + ); + validatorSet.getAddressFromIndexAtEpoch(0, Epoch.wrap(1)); + + assertEq(validatorSet.getAddressFromIndexAtEpoch(0, Epoch.wrap(2)), _addr); + } + + function test_WhenValidatorIsAlreadyInTheSet(address _addr) public { + // It returns false + + timeCheater.cheat__setEpochNow(1); + validatorSet.add(_addr); + + // Addition should fail, and no writes should be made + vm.record(); + assertFalse(validatorSet.add(_addr)); + + (, bytes32[] memory writes) = vm.accesses(address(validatorSet)); + assertEq(writes.length, 0); + } + + function test_WhenValidatorHasBeenRemovedFromTheSet(address[] memory _addrs) public { + // It can be added again + + _addrs = boundUnique(_addrs); + + timeCheater.cheat__setEpochNow(1); + + // Add all of the addresses + for (uint256 i = 0; i < _addrs.length; i++) { + assertTrue(validatorSet.add(_addrs[i])); + } + + // Addresses should be empty for the current epoch + for (uint256 i = 0; i < _addrs.length; i++) { + vm.expectRevert( + abi.encodeWithSelector(Errors.AddressSnapshotLib__IndexOutOfBounds.selector, i, 0) + ); + validatorSet.getAddressFromIndexAtEpoch(i, Epoch.wrap(1)); + } + + timeCheater.cheat__setEpochNow(2); + // Addresses should now be added + for (uint256 i = 0; i < _addrs.length; i++) { + assertEq(validatorSet.getAddressFromIndexAtEpoch(i, Epoch.wrap(2)), _addrs[i]); + } + + // Remove all of the addresses + for (uint256 i = 0; i < _addrs.length; i++) { + assertTrue(validatorSet.remove(_addrs[i])); + } + + // Addresses should now remain during the current epoch after removal + for (uint256 i = 0; i < _addrs.length; i++) { + assertEq(validatorSet.getAddressFromIndexAtEpoch(i, Epoch.wrap(2)), _addrs[i]); + } + + timeCheater.cheat__setEpochNow(3); + for (uint256 i = 0; i < _addrs.length; i++) { + vm.expectRevert( + abi.encodeWithSelector(Errors.AddressSnapshotLib__IndexOutOfBounds.selector, i, 0) + ); + validatorSet.getAddressFromIndexAtEpoch(i, Epoch.wrap(1)); + } + + // Add all of the addresses again + for (uint256 i = 0; i < _addrs.length; i++) { + assertTrue(validatorSet.add(_addrs[i])); + } + + timeCheater.cheat__setEpochNow(4); + // Assert both sets are in the set + for (uint256 i = 0; i < _addrs.length; i++) { + assertEq(validatorSet.getAddressFromIndexAtEpoch(i, Epoch.wrap(4)), _addrs[i]); + } + } +} diff --git a/l1-contracts/test/validator-selection/address_snapshots/add.tree b/l1-contracts/test/validator-selection/address_snapshots/add.tree new file mode 100644 index 000000000000..25885a04b8ec --- /dev/null +++ b/l1-contracts/test/validator-selection/address_snapshots/add.tree @@ -0,0 +1,11 @@ +AddTest +├── when validator is not in the set +│ ├── it returns true +│ ├── it increases the length +│ ├── it creates a checkpoint for the next epoch +│ └── it does not change the current epoch +├── when validator is already in the set +│ └── it returns false +└── when validator has been removed from the set + └── it can be added again + diff --git a/l1-contracts/test/validator-selection/address_snapshots/at.t.sol b/l1-contracts/test/validator-selection/address_snapshots/at.t.sol new file mode 100644 index 000000000000..bda64d487572 --- /dev/null +++ b/l1-contracts/test/validator-selection/address_snapshots/at.t.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2025 Aztec Labs. +pragma solidity >=0.8.27; + +import { + AddressSnapshotLib, + SnapshottedAddressSet +} from "@aztec/core/libraries/staking/AddressSnapshotLib.sol"; +import {Errors} from "@aztec/core/libraries/Errors.sol"; +import {TimeCheater} from "../../staking/TimeCheater.sol"; +import {AddressSnapshotsBase} from "./AddressSnapshotsBase.t.sol"; + +contract AddressSnapshotAtTest is AddressSnapshotsBase { + function test_WhenNoValidatorsAreRegistered(uint256 _index) public { + // It reverts + vm.expectRevert( + abi.encodeWithSelector(Errors.AddressSnapshotLib__IndexOutOfBounds.selector, _index, 0) + ); + validatorSet.at(_index); + } + + function test_WhenIndexIsOutOfBounds(uint256 _index) public { + validatorSet.add(address(1)); + + vm.expectRevert( + abi.encodeWithSelector(Errors.AddressSnapshotLib__IndexOutOfBounds.selector, _index, 0) + ); + validatorSet.at(_index); + } + + function test_WhenIndexIsValid(address[] memory _addrs) public { + vm.assume(_addrs.length > 2); + _addrs = boundUnique(_addrs); + + // it returns the current validator at that index + timeCheater.cheat__setEpochNow(1); + for (uint256 i = 0; i < _addrs.length; i++) { + validatorSet.add(_addrs[i]); + } + + for (uint256 i = 0; i < _addrs.length; i++) { + vm.expectRevert( + abi.encodeWithSelector(Errors.AddressSnapshotLib__IndexOutOfBounds.selector, i, 0) + ); + validatorSet.at(i); + } + + // it returns the correct validator after reordering + timeCheater.cheat__setEpochNow(2); + for (uint256 i = 0; i < _addrs.length; i++) { + assertEq(validatorSet.at(i), _addrs[i]); + } + + // Remove a random index + // -1 to not remove the last item + uint224 randomIndex = uint224( + uint256(keccak256(abi.encodePacked(block.timestamp, _addrs.length))) % (_addrs.length - 1) + ); + address removedAddr = _addrs[randomIndex]; + validatorSet.remove(randomIndex); + + // All still there + for (uint256 i = 0; i < _addrs.length; i++) { + assertEq(validatorSet.at(i), _addrs[i]); + } + + // Progress in time, the deletion should take place + timeCheater.cheat__setEpochNow(3); + + // The item at the random index should be different, as it has been replaced + assertNotEq(validatorSet.at(randomIndex), removedAddr); + } +} diff --git a/l1-contracts/test/validator-selection/address_snapshots/at.tree b/l1-contracts/test/validator-selection/address_snapshots/at.tree new file mode 100644 index 000000000000..e7542ae3f398 --- /dev/null +++ b/l1-contracts/test/validator-selection/address_snapshots/at.tree @@ -0,0 +1,9 @@ +AtTest +├── when no validators are registered +│ └── it reverts with IndexOutOfBounds +├── when index is out of bounds +│ └── it reverts with IndexOutOfBounds +└── when index is valid + ├── it returns the current validator at that index + └── when validators are removed + └── it returns the correct validator after reordering diff --git a/l1-contracts/test/validator-selection/address_snapshots/getAddressFromIndexAtEpoch.t.sol b/l1-contracts/test/validator-selection/address_snapshots/getAddressFromIndexAtEpoch.t.sol new file mode 100644 index 000000000000..1cad2773e227 --- /dev/null +++ b/l1-contracts/test/validator-selection/address_snapshots/getAddressFromIndexAtEpoch.t.sol @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2025 Aztec Labs. +pragma solidity >=0.8.27; + +import {AddressSnapshotsBase} from "./AddressSnapshotsBase.t.sol"; +import { + AddressSnapshotLib, + SnapshottedAddressSet +} from "@aztec/core/libraries/staking/AddressSnapshotLib.sol"; +import {Epoch} from "@aztec/core/libraries/TimeLib.sol"; +import {Errors} from "@aztec/core/libraries/Errors.sol"; + +contract GetAddressFromIndexAtEpochTest is AddressSnapshotsBase { + using AddressSnapshotLib for SnapshottedAddressSet; + + function test_WhenNoValidatorsAreRegistered() public { + // It throws out of bounds + vm.expectRevert( + abi.encodeWithSelector(Errors.AddressSnapshotLib__IndexOutOfBounds.selector, 0, 0) + ); + validatorSet.getAddressFromIndexAtEpoch(0, Epoch.wrap(0)); + } + + modifier whenValidatorsExist(address[] memory _addrs) { + _addrs = boundUnique(_addrs); + + timeCheater.cheat__setEpochNow(1); + for (uint256 i = 0; i < _addrs.length; i++) { + validatorSet.add(_addrs[i]); + } + timeCheater.cheat__setEpochNow(2); + _; + } + + function test_whenQueryingCurrentEpoch(address[] memory _addrs) + public + whenValidatorsExist(_addrs) + { + _addrs = boundUnique(_addrs); + + // It should return the current validator address + for (uint256 i = 0; i < _addrs.length; i++) { + assertEq(validatorSet.getAddressFromIndexAtEpoch(i, Epoch.wrap(2)), _addrs[i]); + } + } + + function test_WhenValidatorsExist_WhenQueryingFutureEpoch(address[] memory _addrs) + public + whenValidatorsExist(_addrs) + { + _addrs = boundUnique(_addrs); + // It should return the current validator address + for (uint256 i = 0; i < _addrs.length; i++) { + assertEq(validatorSet.getAddressFromIndexAtEpoch(i, Epoch.wrap(3)), _addrs[i]); + } + } + + function test_WhenValidatorsExist_WhenQueryingPastEpoch(address[] memory _addrs, uint224 _index) + public + whenValidatorsExist(_addrs) + { + _addrs = boundUnique(_addrs); + _index = uint224(bound(_index, 0, _addrs.length - 1)); + + // It should return the validator address from the snapshot + assertEq(validatorSet.getAddressFromIndexAtEpoch(_index, Epoch.wrap(2)), _addrs[_index]); + + // In a past epoch, it is out of bounds + vm.expectRevert( + abi.encodeWithSelector(Errors.AddressSnapshotLib__IndexOutOfBounds.selector, _index, 0) + ); + validatorSet.getAddressFromIndexAtEpoch(_index, Epoch.wrap(1)); + } + + function test_WhenValidatorWasRemoved(address[] memory _addrs) public whenValidatorsExist(_addrs) { + _addrs = boundUnique(_addrs); + // It should not remove until the next epoch + + uint224 lastIndex = uint224(_addrs.length - 1); + address lastValidator = _addrs[lastIndex]; + + validatorSet.remove(lastIndex); + assertEq(validatorSet.getAddressFromIndexAtEpoch(lastIndex, Epoch.wrap(2)), lastValidator); + + timeCheater.cheat__setEpochNow(3); + vm.expectRevert( + abi.encodeWithSelector( + Errors.AddressSnapshotLib__IndexOutOfBounds.selector, lastIndex, lastIndex + ) + ); + validatorSet.getAddressFromIndexAtEpoch(lastIndex, Epoch.wrap(3)); + } + + function test_WhenIndexIsOutOfBounds(address[] memory _addrs) public whenValidatorsExist(_addrs) { + // It should throw out of bounds + vm.expectRevert( + abi.encodeWithSelector(Errors.AddressSnapshotLib__IndexOutOfBounds.selector, _addrs.length, 0) + ); + validatorSet.getAddressFromIndexAtEpoch(_addrs.length, Epoch.wrap(1)); + } + + function test_WhenValidatorIsRemovedAndNewOneAddedAtSamePosition(address[] memory _addrs) + public + whenValidatorsExist(_addrs) + { + vm.assume(_addrs.length > 2); + // it maintains both current and historical values correctly + + // Random index to remove + uint224 randomIndex = uint224( + uint256(keccak256(abi.encodePacked(block.timestamp, _addrs.length))) % (_addrs.length - 1) + ); + + // Remove the validator + validatorSet.remove(randomIndex); + + timeCheater.cheat__setEpochNow(3); + + // In epoch 3, there should be a different validator at random index, as it has been replaced with the last validator + // But we should still be able to query the old validator at random index in epoch 2 + + // Check it now contains the last validators + assertEq( + validatorSet.getAddressFromIndexAtEpoch(randomIndex, Epoch.wrap(3)), _addrs[_addrs.length - 1] + ); + + // Check it still contains the old validator at random index in epoch 2 + assertEq( + validatorSet.getAddressFromIndexAtEpoch(randomIndex, Epoch.wrap(2)), _addrs[randomIndex] + ); + } +} diff --git a/l1-contracts/test/validator-selection/address_snapshots/getAddressFromIndexAtEpoch.tree b/l1-contracts/test/validator-selection/address_snapshots/getAddressFromIndexAtEpoch.tree new file mode 100644 index 000000000000..5d9ff425f84d --- /dev/null +++ b/l1-contracts/test/validator-selection/address_snapshots/getAddressFromIndexAtEpoch.tree @@ -0,0 +1,17 @@ +GetAddressFromIndexAtEpochTest +├── when no validators are registered +│ └── it throws out of bounds +├── when validators exist +│ ├── when querying current epoch +│ │ └── it returns the correct validator address +│ ├── when querying future epoch +│ │ └── it returns the correct (current) validator address +│ ├── when querying past epoch +│ │ └── it maintains historical values correctly +│ ├── when validator was removed +│ │ ├── it returns the same address for the current epoch +│ │ └── it returns address(0) for the next epoch +│ └── when validator is removed and new one added at same position +│ └── it maintains both current and historical values correctly +└── when index is out of bounds + └── it throws out of bounds diff --git a/l1-contracts/test/validator-selection/address_snapshots/length.t.sol b/l1-contracts/test/validator-selection/address_snapshots/length.t.sol new file mode 100644 index 000000000000..245f77560fe7 --- /dev/null +++ b/l1-contracts/test/validator-selection/address_snapshots/length.t.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2025 Aztec Labs. +pragma solidity >=0.8.27; + +import { + AddressSnapshotLib, + SnapshottedAddressSet +} from "@aztec/core/libraries/staking/AddressSnapshotLib.sol"; +import {AddressSnapshotsBase} from "./AddressSnapshotsBase.t.sol"; +import {Epoch} from "@aztec/core/libraries/TimeLib.sol"; + +contract AddressSnapshotLengthTest is AddressSnapshotsBase { + using AddressSnapshotLib for SnapshottedAddressSet; + + function test_WhenNoValidatorsAreRegistered() public view { + // It returns 0 + assertEq(validatorSet.length(), 0); + } + + function test_WhenAddingValidators(address[] memory _addrs) public { + _addrs = boundUnique(_addrs); + + // It increases the length + timeCheater.cheat__setEpochNow(1); + // Length starts at zero + assertEq(validatorSet.length(), 0); + + for (uint256 i = 0; i < _addrs.length; i++) { + validatorSet.add(_addrs[i]); + } + // Length remains zero within this epoch + assertEq(validatorSet.length(), 0); + + timeCheater.cheat__setEpochNow(2); + // It increases after the epoch boundary + assertEq(validatorSet.length(), _addrs.length); + + // Length at epoch maintains historical values + assertEq(validatorSet.lengthAtEpoch(Epoch.wrap(1)), 0); + assertEq(validatorSet.lengthAtEpoch(Epoch.wrap(2)), _addrs.length); + } + + // It decrease the length + function test_WhenRemovingValidators(address[] memory _addrs) public { + _addrs = boundUnique(_addrs); + + // It decrease the length + // It maintains historical values correctly + timeCheater.cheat__setEpochNow(1); + for (uint256 i = 0; i < _addrs.length; i++) { + validatorSet.add(_addrs[i]); + } + + timeCheater.cheat__setEpochNow(2); + for (uint256 i = 0; i < _addrs.length; i++) { + validatorSet.remove(_addrs[i]); + } + assertEq(validatorSet.length(), _addrs.length); + + timeCheater.cheat__setEpochNow(3); + assertEq(validatorSet.length(), 0); + + // Length at epoch maintains historical values + assertEq(validatorSet.lengthAtEpoch(Epoch.wrap(1)), 0); + assertEq(validatorSet.lengthAtEpoch(Epoch.wrap(2)), _addrs.length); + assertEq(validatorSet.lengthAtEpoch(Epoch.wrap(3)), 0); + } +} diff --git a/l1-contracts/test/validator-selection/address_snapshots/length.tree b/l1-contracts/test/validator-selection/address_snapshots/length.tree new file mode 100644 index 000000000000..edd05e87888b --- /dev/null +++ b/l1-contracts/test/validator-selection/address_snapshots/length.tree @@ -0,0 +1,8 @@ +LengthTest +├── when no validators are registered +│ └── it returns 0 +├── when adding validators +│ └── it increases the length +└── when removing validators + ├── it decreases the length + └── it maintains historical values correctly diff --git a/l1-contracts/test/validator-selection/address_snapshots/remove.t.sol b/l1-contracts/test/validator-selection/address_snapshots/remove.t.sol new file mode 100644 index 000000000000..5ab9c5bc3f10 --- /dev/null +++ b/l1-contracts/test/validator-selection/address_snapshots/remove.t.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2025 Aztec Labs. +pragma solidity >=0.8.27; + +import { + AddressSnapshotLib, + SnapshottedAddressSet +} from "@aztec/core/libraries/staking/AddressSnapshotLib.sol"; +import {Epoch} from "@aztec/core/libraries/TimeLib.sol"; +import {AddressSnapshotsBase} from "./AddressSnapshotsBase.t.sol"; +import {Errors} from "@aztec/core/libraries/Errors.sol"; + +contract AddressSnapshotRemoveTest is AddressSnapshotsBase { + using AddressSnapshotLib for SnapshottedAddressSet; + + function test_WhenAddressNotInTheSet() public { + // It returns false + assertFalse(validatorSet.remove(address(1))); + } + + function test_WhenValidatorIsInTheSet() public { + // It returns true + // It decreases the length + // It updates the snapshot for that index + // It maintains historical values correctly + + timeCheater.cheat__setEpochNow(1); + validatorSet.add(address(1)); + // Length remains 0 within this epoch + assertEq(validatorSet.length(), 0); + + // Length increases to 1 in the next epoch + timeCheater.cheat__setEpochNow(2); + assertEq(validatorSet.length(), 1); + + assertTrue(validatorSet.remove(address(1))); + // Length remains 1 within this epoch + assertEq(validatorSet.length(), 1); + + timeCheater.cheat__setEpochNow(3); + // Length decreases to 0 in the next epoch + assertEq(validatorSet.length(), 0); + + vm.expectRevert( + abi.encodeWithSelector(Errors.AddressSnapshotLib__IndexOutOfBounds.selector, 0, 0) + ); + validatorSet.getAddressFromIndexAtEpoch(0, Epoch.wrap(3)); + + assertEq(validatorSet.getAddressFromIndexAtEpoch(0, Epoch.wrap(2)), address(1)); + } + + function test_WhenValidatorRemovingAnIndexLargerThanTheCurrentLength() public { + // It reverts + timeCheater.cheat__setEpochNow(1); + vm.expectRevert( + abi.encodeWithSelector(Errors.AddressSnapshotLib__IndexOutOfBounds.selector, 0, 0) + ); + validatorSet.remove(0); + + // Add some validators + validatorSet.add(address(1)); + validatorSet.add(address(2)); + validatorSet.add(address(3)); + + timeCheater.cheat__setEpochNow(2); + vm.expectRevert( + abi.encodeWithSelector(Errors.AddressSnapshotLib__IndexOutOfBounds.selector, 10, 3) + ); + validatorSet.remove(10); + } + + function test_WhenRemovingMultipleValidators() public { + // It maintains correct order of remaining validators + // It updates snapshots correctly for each removal + + timeCheater.cheat__setEpochNow(1); + validatorSet.add(address(1)); + validatorSet.add(address(2)); + validatorSet.add(address(3)); + + timeCheater.cheat__setEpochNow(2); + validatorSet.remove(address(2)); + + timeCheater.cheat__setEpochNow(3); + + address[] memory vals = validatorSet.values(); + assertEq(vals.length, 2); + assertEq(vals[0], address(1)); + assertEq(vals[1], address(3)); + + validatorSet.remove(address(1)); + timeCheater.cheat__setEpochNow(4); + + vals = validatorSet.values(); + assertEq(vals.length, 1); + assertEq(vals[0], address(3)); + + // Verify snapshots + assertEq(validatorSet.getAddressFromIndexAtEpoch(0, Epoch.wrap(2)), address(1)); + assertEq(validatorSet.getAddressFromIndexAtEpoch(1, Epoch.wrap(2)), address(2)); + assertEq(validatorSet.getAddressFromIndexAtEpoch(2, Epoch.wrap(2)), address(3)); + + assertEq(validatorSet.getAddressFromIndexAtEpoch(0, Epoch.wrap(3)), address(1)); + assertEq(validatorSet.getAddressFromIndexAtEpoch(1, Epoch.wrap(3)), address(3)); + } +} diff --git a/l1-contracts/test/validator-selection/address_snapshots/remove.tree b/l1-contracts/test/validator-selection/address_snapshots/remove.tree new file mode 100644 index 000000000000..827f21409654 --- /dev/null +++ b/l1-contracts/test/validator-selection/address_snapshots/remove.tree @@ -0,0 +1,13 @@ +RemoveTest +├── when address not in the set +│ └── it returns false +├── when validator is in the set +│ ├── it returns true +│ ├── it decreases the length +│ ├── it updates the snapshot for that index +│ └── it maintains historical values correctly +├── when validator removing an index larger than the current length +│ └── it reverts +└── when removing multiple validators + ├── it maintains correct order of remaining validators + └── it updates snapshots correctly for each removal diff --git a/l1-contracts/test/validator-selection/address_snapshots/values.t.sol b/l1-contracts/test/validator-selection/address_snapshots/values.t.sol new file mode 100644 index 000000000000..05a30a363395 --- /dev/null +++ b/l1-contracts/test/validator-selection/address_snapshots/values.t.sol @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2025 Aztec Labs. +pragma solidity >=0.8.27; + +import { + AddressSnapshotLib, + SnapshottedAddressSet +} from "@aztec/core/libraries/staking/AddressSnapshotLib.sol"; +import {AddressSnapshotsBase} from "./AddressSnapshotsBase.t.sol"; +import {Epoch} from "@aztec/core/libraries/TimeLib.sol"; +import {Errors} from "@aztec/core/libraries/Errors.sol"; + +contract AddressSnapshotValuesTest is AddressSnapshotsBase { + using AddressSnapshotLib for SnapshottedAddressSet; + + function test_WhenNoValidatorsAreRegistered() public view { + // It returns empty array + address[] memory vals = validatorSet.values(); + assertEq(vals.length, 0); + } + + function test_WhenValidatorsExist(address[] memory _addrs) public { + _addrs = boundUnique(_addrs); + // It returns array with correct length + // It returns array with addresses in order + + timeCheater.cheat__setEpochNow(1); + for (uint256 i = 0; i < _addrs.length; i++) { + validatorSet.add(_addrs[i]); + } + + // Move to next epoch for changes to take effect + timeCheater.cheat__setEpochNow(2); + address[] memory vals = validatorSet.values(); + assertEq(vals.length, _addrs.length); + for (uint256 i = 0; i < _addrs.length; i++) { + assertEq(vals[i], _addrs[i]); + } + } + + function test_WhenValidatorsHaveNotChangedForSomeTime(address[] memory _addrs) public { + _addrs = boundUnique(_addrs); + + // It returns array with correct length + // It returns array with correct addresses in order + + timeCheater.cheat__setEpochNow(1); + for (uint256 i = 0; i < _addrs.length; i++) { + validatorSet.add(_addrs[i]); + } + + timeCheater.cheat__setEpochNow(100); + address[] memory vals = validatorSet.values(); + assertEq(vals.length, _addrs.length); + for (uint256 i = 0; i < _addrs.length; i++) { + assertEq(vals[i], _addrs[i]); + } + + // Values at epoch maintains historical values + address[] memory valsAtEpoch = validatorSet.valuesAtEpoch(Epoch.wrap(1)); + assertEq(valsAtEpoch.length, 0); + } + + function test_WhenValidatorsAreRemoved() public { + // It returns array of remaining validators + timeCheater.cheat__setEpochNow(1); + validatorSet.add(address(1)); + validatorSet.add(address(2)); + validatorSet.add(address(3)); + + timeCheater.cheat__setEpochNow(2); + validatorSet.remove(address(2)); + + timeCheater.cheat__setEpochNow(3); + + address[] memory vals = validatorSet.values(); + assertEq(vals.length, 2); + assertEq(vals[0], address(1)); + assertEq(vals[1], address(3)); + + // Values at epoch maintains historical values + address[] memory valsAtEpoch = validatorSet.valuesAtEpoch(Epoch.wrap(1)); + assertEq(valsAtEpoch.length, 0); + + valsAtEpoch = validatorSet.valuesAtEpoch(Epoch.wrap(2)); + assertEq(valsAtEpoch.length, 3); + assertEq(valsAtEpoch[0], address(1)); + assertEq(valsAtEpoch[1], address(2)); + assertEq(valsAtEpoch[2], address(3)); + + valsAtEpoch = validatorSet.valuesAtEpoch(Epoch.wrap(3)); + assertEq(valsAtEpoch.length, 2); + assertEq(valsAtEpoch[0], address(1)); + assertEq(valsAtEpoch[1], address(3)); + } +} diff --git a/l1-contracts/test/validator-selection/address_snapshots/values.tree b/l1-contracts/test/validator-selection/address_snapshots/values.tree new file mode 100644 index 000000000000..5b4b3fe64751 --- /dev/null +++ b/l1-contracts/test/validator-selection/address_snapshots/values.tree @@ -0,0 +1,11 @@ +ValuesTest +├── when no validators are registered +│ └── it returns empty array +├── when validators exist +│ ├── it returns array with correct length +│ └── it returns array with correct addresses in order +├── when validators have not changed for some time +│ └── it returns array with correct length +│ └── it returns array with correct addresses in order +└── when validators are removed + └── it returns array with remaining validators diff --git a/yarn-project/aztec.js/src/test/rollup_cheat_codes.ts b/yarn-project/aztec.js/src/test/rollup_cheat_codes.ts index efcf163ab9cf..93eb6812869c 100644 --- a/yarn-project/aztec.js/src/test/rollup_cheat_codes.ts +++ b/yarn-project/aztec.js/src/test/rollup_cheat_codes.ts @@ -1,4 +1,4 @@ -import type { ViemPublicClient } from '@aztec/ethereum'; +import { RollupContract, type ViemPublicClient } from '@aztec/ethereum'; import { EthCheatCodes } from '@aztec/ethereum/eth-cheatcodes'; import type { L1ContractAddresses } from '@aztec/ethereum/l1-contract-addresses'; import { EthAddress } from '@aztec/foundation/eth-address'; @@ -54,6 +54,28 @@ export class RollupCheatCodes { }; } + /** + * Logs the current state of the rollup contract. + */ + public async debugRollup() { + const rollup = new RollupContract(this.client, this.rollup.address); + const pendingNum = await rollup.getBlockNumber(); + const provenNum = await rollup.getProvenBlockNumber(); + const validators = await rollup.getAttesters(); + const committee = await rollup.getCurrentEpochCommittee(); + const archive = await rollup.archive(); + const epochNum = await rollup.getEpochNumber(); + const slot = await this.getSlot(); + + this.logger.info(`Pending block num: ${pendingNum}`); + this.logger.info(`Proven block num: ${provenNum}`); + this.logger.info(`Validators: ${validators.map(v => v.toString()).join(', ')}`); + this.logger.info(`Committee: ${committee.map(v => v.toString()).join(', ')}`); + this.logger.info(`Archive: ${archive}`); + this.logger.info(`Epoch num: ${epochNum}`); + this.logger.info(`Slot: ${slot}`); + } + /** Fetches the epoch and slot duration config from the rollup contract */ public async getConfig(): Promise<{ /** Epoch duration */ epochDuration: bigint; diff --git a/yarn-project/end-to-end/src/e2e_p2p/gossip_network_no_cheat.test.ts b/yarn-project/end-to-end/src/e2e_p2p/gossip_network_no_cheat.test.ts index 2eb54fb5b862..853258c1e0a6 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/gossip_network_no_cheat.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/gossip_network_no_cheat.test.ts @@ -121,9 +121,9 @@ describe('e2e_p2p_network', () => { }); } - const attesters = await rollup.read.getAttesters(); - expect(attesters.length).toBe(validators.length); - expect(attesters.length).toBe(NUM_NODES); + // Changes do not take effect until the next epoch + const attestersImmedatelyAfterAdding = await rollup.read.getAttesters(); + expect(attestersImmedatelyAfterAdding.length).toBe(0); // Check that the validators are added correctly const withdrawer = await stakingAssetHandler.read.withdrawer(); @@ -142,6 +142,11 @@ describe('e2e_p2p_network', () => { t.logger.debug('Warp failed, time already satisfied'); } + // Changes have now taken effect + const attesters = await rollup.read.getAttesters(); + expect(attesters.length).toBe(validators.length); + expect(attesters.length).toBe(NUM_NODES); + // Send and await a tx to make sure we mine a block for the warp to correctly progress. await t.ctx.deployL1ContractsValues.publicClient.waitForTransactionReceipt({ hash: await t.ctx.deployL1ContractsValues.walletClient.sendTransaction({ diff --git a/yarn-project/end-to-end/src/e2e_p2p/p2p_network.ts b/yarn-project/end-to-end/src/e2e_p2p/p2p_network.ts index e709bea79933..4bf34bd882f0 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/p2p_network.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/p2p_network.ts @@ -2,7 +2,14 @@ import { getSchnorrWalletWithSecretKey } from '@aztec/accounts/schnorr'; import type { InitialAccountData } from '@aztec/accounts/testing'; import type { AztecNodeConfig, AztecNodeService } from '@aztec/aztec-node'; import type { AccountWalletWithSecretKey } from '@aztec/aztec.js'; -import { RollupContract, getExpectedAddress, getL1ContractsConfigEnvVars } from '@aztec/ethereum'; +import { + L1TxUtils, + RollupContract, + type ViemPublicClient, + type ViemWalletClient, + getExpectedAddress, + getL1ContractsConfigEnvVars, +} from '@aztec/ethereum'; import { ChainMonitor, EthCheatCodesWithState } from '@aztec/ethereum/test'; import { type Logger, createLogger } from '@aztec/foundation/log'; import { ForwarderAbi, ForwarderBytecode, RollupAbi, TestERC20Abi } from '@aztec/l1-artifacts'; @@ -232,13 +239,7 @@ export class P2PNetworkTest { } // Send and await a tx to make sure we mine a block for the warp to correctly progress. - await deployL1ContractsValues.publicClient.waitForTransactionReceipt({ - hash: await deployL1ContractsValues.walletClient.sendTransaction({ - to: this.baseAccount.address, - value: 1n, - account: this.baseAccount, - }), - }); + await this._sendDummyTx(deployL1ContractsValues.publicClient, deployL1ContractsValues.walletClient); // Set the system time in the node, only after we have warped the time and waited for a block // Time is only set in the NEXT block @@ -284,13 +285,10 @@ export class P2PNetworkTest { 'remove-inital-validator', async ({ deployL1ContractsValues, aztecNode, dateProvider }) => { // Send and await a tx to make sure we mine a block for the warp to correctly progress. - const receipt = await deployL1ContractsValues.publicClient.waitForTransactionReceipt({ - hash: await deployL1ContractsValues.walletClient.sendTransaction({ - to: this.baseAccount.address, - value: 1n, - account: this.baseAccount, - }), - }); + const { receipt } = await this._sendDummyTx( + deployL1ContractsValues.publicClient, + deployL1ContractsValues.walletClient, + ); const block = await deployL1ContractsValues.publicClient.getBlock({ blockNumber: receipt.blockNumber, }); @@ -301,6 +299,21 @@ export class P2PNetworkTest { ); } + async sendDummyTx() { + return await this._sendDummyTx( + this.ctx.deployL1ContractsValues.publicClient, + this.ctx.deployL1ContractsValues.walletClient, + ); + } + + private async _sendDummyTx(publicClient: ViemPublicClient, walletClient: ViemWalletClient) { + const l1TxUtils = new L1TxUtils(publicClient, walletClient); + return await l1TxUtils.sendAndMonitorTransaction({ + to: walletClient.account!.address, + value: 1n, + }); + } + async setup() { this.ctx = await this.snapshotManager.setup(); diff --git a/yarn-project/end-to-end/src/e2e_p2p/slashing.test.ts b/yarn-project/end-to-end/src/e2e_p2p/slashing.test.ts index b7a08b22ff4c..7ee0407201b6 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/slashing.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/slashing.test.ts @@ -100,6 +100,10 @@ describe('e2e_p2p_slashing', () => { return { roundNumber: await slashingProposer.read.computeRound([slotNumber]), slotNumber }; }; + const debugRollup = async () => { + await t.ctx.cheatCodes.rollup.debugRollup(); + }; + /** * Get the slashing info for a given round number. * @param roundNumber - The round number to get the slashing info for. @@ -127,6 +131,14 @@ describe('e2e_p2p_slashing', () => { t.ctx.aztecNodeConfig.validatorReexecute = false; t.ctx.aztecNodeConfig.minTxsPerBlock = 0; + // Jump forward to an epoch in the future such that the validator set is not empty + const slotsInEpoch = await rollup.getEpochDuration(); + const epochToJumpInto = 4n; + const timestamp = await rollup.getTimestampForSlot(slotsInEpoch * epochToJumpInto); + await t.ctx.cheatCodes.eth.warp(Number(timestamp)); + // Send tx + await t.sendDummyTx(); + // create our network of nodes and submit txs into each of them // the number of txs per node and the number of txs per rollup // should be set so that the only way for rollups to be built @@ -155,11 +167,15 @@ describe('e2e_p2p_slashing', () => { slasher.slashingAmount = slashingAmount; } + await debugRollup(); + // wait a bit for peers to discover each other await sleep(4000); const votesNeeded = await slashingProposer.read.N(); + await debugRollup(); + // Produce blocks until we hit an issue with pruning. // Then we should jump in time to the next round so we are sure that we have the votes // Then we just sit on our hands and wait. @@ -172,8 +188,12 @@ describe('e2e_p2p_slashing', () => { const slasher = (sequencer as any).slasherClient; let slashEvents: any[] = []; + await debugRollup(); + t.logger.info(`Producing blocks until we hit a pruning event`); + await debugRollup(); + // Run for up to the slashing round size, or as long as needed to get a slash event // Variable because sometimes hit race-condition issues with attestations. for (let i = 0; i < slashingRoundSize; i++) { @@ -195,6 +215,8 @@ describe('e2e_p2p_slashing', () => { } } + await debugRollup(); + expect(slashEvents.length).toBeGreaterThan(0); await waitUntilNextRound(); @@ -202,6 +224,8 @@ describe('e2e_p2p_slashing', () => { const { roundNumber, slotNumber } = await getRoundAndSlotNumber(); let sInfo = await slashingInfo(roundNumber); + await debugRollup(); + // For the next round we will try to cast votes. // Stop early if we have enough votes. t.logger.info(`Waiting for votes to be cast`); @@ -215,8 +239,15 @@ describe('e2e_p2p_slashing', () => { sInfo = await slashingInfo(roundNumber); t.logger.info(`We have ${sInfo.leaderVotes} votes in round ${sInfo.roundNumber} on ${sInfo.info[1]}`); if (sInfo.leaderVotes >= votesNeeded) { - t.logger.info(`We have sufficient votes`); - break; + // We need there to be an actual committee to slash for this round + const epoch = await rollup.getEpochNumberForSlotNumber(sInfo.slotNumber); + const committee = await rollup.getEpochCommittee(epoch); + if (committee.length > 0) { + t.logger.info(`We have sufficient votes, and a committee for epoch ${epoch}`); + break; + } else { + t.logger.info(`No committee found for epoch ${epoch}, waiting for next round`); + } } } @@ -228,6 +259,8 @@ describe('e2e_p2p_slashing', () => { // we don't have that in place. const targetAddress = sInfo.info[1]; + await debugRollup(); + let targetEpoch = 0n; for (let i = 0; i <= slotNumber; i++) { const epoch = await rollup.getEpochNumberForSlotNumber(BigInt(i)); @@ -239,6 +272,8 @@ describe('e2e_p2p_slashing', () => { } } + await debugRollup(); + await l1TxUtils.sendAndMonitorTransaction({ to: slashFactory.address, data: encodeFunctionData({ @@ -248,6 +283,8 @@ describe('e2e_p2p_slashing', () => { }), }); + await debugRollup(); + t.logger.info(`Slash payload for ${targetEpoch}, ${slashingAmount} deployed at ${targetAddress}`); t.logger.info( `Committee for epoch ${targetEpoch}: ${(await rollup.getEpochCommittee(targetEpoch)).map(addr => @@ -255,6 +292,8 @@ describe('e2e_p2p_slashing', () => { )}`, ); + await debugRollup(); + t.logger.info(`We wait until next round to execute the payload`); await waitUntilNextRound(); const attestersPre = await rollup.getAttesters(); @@ -286,6 +325,8 @@ describe('e2e_p2p_slashing', () => { t.logger.info(`Performed slash in ${receipt.transactionHash}`); + await debugRollup(); + const slashingEvents = parseEventLogs({ abi: RollupAbi, logs: receipt.logs, @@ -312,13 +353,27 @@ describe('e2e_p2p_slashing', () => { const attestersPost = await rollup.getAttesters(); + // Attesters next epoch + await t.ctx.cheatCodes.rollup.advanceToNextEpoch(); + // Send tx + await t.sendDummyTx(); + + // Slashed parties should be removed from the validator set in the next epoch + const attestersNextEpoch = await rollup.getAttesters(); + for (const attester of attestersPre) { const attesterInfo = await rollup.getInfo(attester); // Check that status is Living expect(attesterInfo.status).toEqual(2); } + + await debugRollup(); + + // Committee should only update in the next epoch const committee = await rollup.getEpochCommittee(targetEpoch); expect(attestersPre.length).toBe(committee.length); - expect(attestersPost.length).toBe(0); + expect(attestersPost.length).toBe(committee.length); + + expect(attestersNextEpoch.length).toBe(0); }, 1_000_000); });