diff --git a/.github/workflows/devnet-deploys.yml b/.github/workflows/devnet-deploys.yml index 1561702cb01..c2b385f971d 100644 --- a/.github/workflows/devnet-deploys.yml +++ b/.github/workflows/devnet-deploys.yml @@ -547,6 +547,7 @@ jobs: echo "TF_VAR_INBOX_CONTRACT_ADDRESS=$(extract inboxAddress)" >>$GITHUB_ENV echo "TF_VAR_OUTBOX_CONTRACT_ADDRESS=$(extract outboxAddress)" >>$GITHUB_ENV echo "TF_VAR_FEE_JUICE_CONTRACT_ADDRESS=$(extract feeJuiceAddress)" >>$GITHUB_ENV + echo "TF_VAR_STAKING_ASSET_CONTRACT_ADDRESS=$(extract stakingAssetAddress)" >>$GITHUB_ENV echo "TF_VAR_FEE_JUICE_PORTAL_CONTRACT_ADDRESS=$(extract feeJuicePortalAddress)" >>$GITHUB_ENV - name: Apply l1-contracts Terraform @@ -678,6 +679,7 @@ jobs: aws s3 cp ${{ env.CONTRACT_S3_BUCKET }}/${{ env.DEPLOY_TAG }}/basic_contracts.json ./basic_contracts.json echo "TF_VAR_FEE_JUICE_CONTRACT_ADDRESS=$(jq -r '.feeJuiceAddress' ./l1_contracts.json)" >>$GITHUB_ENV + echo "TF_VAR_STAKING_ASSET_CONTRACT_ADDRESS=$(jq -r '.stakingAssetAddress' ./l1_contracts.json)" >>$GITHUB_ENV echo "TF_VAR_DEV_COIN_CONTRACT_ADDRESS=$(jq -r '.devCoinL1' ./basic_contracts.json)" >>$GITHUB_ENV - name: Deploy Faucet diff --git a/.github/workflows/sepolia-deploy.yml b/.github/workflows/sepolia-deploy.yml index d908f89827d..4f736c56dd8 100644 --- a/.github/workflows/sepolia-deploy.yml +++ b/.github/workflows/sepolia-deploy.yml @@ -85,6 +85,7 @@ jobs: echo "TF_VAR_OUTBOX_CONTRACT_ADDRESS=$(extract outboxAddress)" >>$GITHUB_ENV echo "TF_VAR_AVAILABILITY_ORACLE_CONTRACT_ADDRESS=$(extract availabilityOracleAddress)" >>$GITHUB_ENV echo "TF_VAR_FEE_JUICE_CONTRACT_ADDRESS=$(extract feeJuiceAddress)" >>$GITHUB_ENV + echo "TF_VAR_STAKING_ASSET_CONTRACT_ADDRESS=$(extract stakingAssetAddress)" >>$GITHUB_ENV echo "TF_VAR_FEE_JUICE_PORTAL_CONTRACT_ADDRESS=$(extract feeJuicePortalAddress)" >>$GITHUB_ENV - name: Apply l1-contracts Terraform diff --git a/l1-contracts/src/core/Leonidas.sol b/l1-contracts/src/core/Leonidas.sol index 01899cd3003..28113a64fc9 100644 --- a/l1-contracts/src/core/Leonidas.sol +++ b/l1-contracts/src/core/Leonidas.sol @@ -5,11 +5,13 @@ pragma solidity >=0.8.27; import {ILeonidas, EpochData, LeonidasStorage} from "@aztec/core/interfaces/ILeonidas.sol"; import {Signature} from "@aztec/core/libraries/crypto/SignatureLib.sol"; import {DataStructures} from "@aztec/core/libraries/DataStructures.sol"; +import {Errors} from "@aztec/core/libraries/Errors.sol"; import {LeonidasLib} from "@aztec/core/libraries/LeonidasLib/LeonidasLib.sol"; import { Timestamp, Slot, Epoch, SlotLib, EpochLib, TimeFns } from "@aztec/core/libraries/TimeMath.sol"; -import {Ownable} from "@oz/access/Ownable.sol"; +import {Staking} from "@aztec/core/staking/Staking.sol"; +import {IERC20} from "@oz/token/ERC20/IERC20.sol"; import {EnumerableSet} from "@oz/utils/structs/EnumerableSet.sol"; /** @@ -19,16 +21,13 @@ import {EnumerableSet} from "@oz/utils/structs/EnumerableSet.sol"; * He define the structure needed for committee and leader selection and provides logic for validating that * the block and its "evidence" follows his rules. * - * @dev Leonidas is depending on Ares to add/remove warriors to/from his army competently. - * * @dev Leonidas have one thing in mind, he provide a reference of the LOGIC going on for the spartan selection. * He is not concerned about gas costs, he is a king, he just throw gas in the air like no-one cares. * It will be the duty of his successor (Pleistarchus) to optimize the costs with same functionality. * */ -contract Leonidas is Ownable, TimeFns, ILeonidas { +contract Leonidas is Staking, TimeFns, ILeonidas { using EnumerableSet for EnumerableSet.AddressSet; - using LeonidasLib for LeonidasStorage; using SlotLib for Slot; using EpochLib for Epoch; @@ -40,50 +39,22 @@ contract Leonidas is Ownable, TimeFns, ILeonidas { // The time that the contract was deployed Timestamp public immutable GENESIS_TIME; - LeonidasStorage private store; + LeonidasStorage private leonidasStore; constructor( address _ares, + IERC20 _stakingAsset, + uint256 _minimumStake, uint256 _slotDuration, uint256 _epochDuration, uint256 _targetCommitteeSize - ) Ownable(_ares) TimeFns(_slotDuration, _epochDuration) { + ) Staking(_ares, _stakingAsset, _minimumStake) TimeFns(_slotDuration, _epochDuration) { GENESIS_TIME = Timestamp.wrap(block.timestamp); SLOT_DURATION = _slotDuration; EPOCH_DURATION = _epochDuration; TARGET_COMMITTEE_SIZE = _targetCommitteeSize; } - /** - * @notice Adds a validator to the validator set - * - * @dev Only ARES can add validators - * - * @dev Will setup the epoch if needed BEFORE adding the validator. - * This means that the validator will effectively be added to the NEXT epoch. - * - * @param _validator - The validator to add - */ - function addValidator(address _validator) external override(ILeonidas) onlyOwner { - setupEpoch(); - _addValidator(_validator); - } - - /** - * @notice Removes a validator from the validator set - * - * @dev Only ARES can add validators - * - * @dev Will setup the epoch if needed BEFORE removing the validator. - * This means that the validator will effectively be removed from the NEXT epoch. - * - * @param _validator - The validator to remove - */ - function removeValidator(address _validator) external override(ILeonidas) onlyOwner { - setupEpoch(); - store.validatorSet.remove(_validator); - } - /** * @notice Get the validator set for a given epoch * @@ -99,7 +70,7 @@ contract Leonidas is Ownable, TimeFns, ILeonidas { override(ILeonidas) returns (address[] memory) { - return store.epochs[_epoch].committee; + return leonidasStore.epochs[_epoch].committee; } /** @@ -107,18 +78,32 @@ contract Leonidas is Ownable, TimeFns, ILeonidas { * @return The validator set for the current epoch */ function getCurrentEpochCommittee() external view override(ILeonidas) returns (address[] memory) { - return store.getCommitteeAt(getCurrentEpoch(), TARGET_COMMITTEE_SIZE); + return LeonidasLib.getCommitteeAt( + leonidasStore, stakingStore, getCurrentEpoch(), TARGET_COMMITTEE_SIZE + ); } - /** - * @notice Get the validator set - * - * @dev Consider removing this to replace with a `size` and individual getter. - * - * @return The validator set - */ - function getValidators() external view override(ILeonidas) returns (address[] memory) { - return store.validatorSet.values(); + function deposit(address _attester, address _proposer, address _withdrawer, uint256 _amount) + public + override(Staking) + { + setupEpoch(); + require( + _attester != address(0) && _proposer != address(0), + Errors.Leonidas__InvalidDeposit(_attester, _proposer) + ); + super.deposit(_attester, _proposer, _withdrawer, _amount); + } + + function initiateWithdraw(address _attester, address _recipient) + public + override(Staking) + returns (bool) + { + // @note The attester might be chosen for the epoch, so the delay must be long enough + // to allow for that. + setupEpoch(); + return super.initiateWithdraw(_attester, _recipient); } /** @@ -127,49 +112,31 @@ contract Leonidas is Ownable, TimeFns, ILeonidas { * - Set the seed for the epoch * - Update the last seed * - * @dev Since this is a reference optimising for simplicity, we store the actual validator set in the epoch structure. + * @dev Since this is a reference optimising for simplicity, we leonidasStore the actual validator set in the epoch structure. * This is very heavy on gas, so start crying because the gas here will melt the poles * https://i.giphy.com/U1aN4HTfJ2SmgB2BBK.webp */ function setupEpoch() public override(ILeonidas) { Epoch epochNumber = getCurrentEpoch(); - EpochData storage epoch = store.epochs[epochNumber]; + EpochData storage epoch = leonidasStore.epochs[epochNumber]; if (epoch.sampleSeed == 0) { - epoch.sampleSeed = store.getSampleSeed(epochNumber); - epoch.nextSeed = store.lastSeed = _computeNextSeed(epochNumber); - - epoch.committee = store.sampleValidators(epoch.sampleSeed, TARGET_COMMITTEE_SIZE); + epoch.sampleSeed = LeonidasLib.getSampleSeed(leonidasStore, epochNumber); + epoch.nextSeed = leonidasStore.lastSeed = _computeNextSeed(epochNumber); + epoch.committee = + LeonidasLib.sampleValidators(stakingStore, epoch.sampleSeed, TARGET_COMMITTEE_SIZE); } } /** - * @notice Get the number of validators in the validator set + * @notice Get the attester set * - * @return The number of validators in the validator set - */ - function getValidatorCount() public view override(ILeonidas) returns (uint256) { - return store.validatorSet.length(); - } - - /** - * @notice Get the number of validators in the validator set - * - * @return The number of validators in the validator set - */ - function getValidatorAt(uint256 _index) public view override(ILeonidas) returns (address) { - return store.validatorSet.at(_index); - } - - /** - * @notice Checks if an address is in the validator set - * - * @param _validator - The address to check + * @dev Consider removing this to replace with a `size` and individual getter. * - * @return True if the address is in the validator set, false otherwise + * @return The validator set */ - function isValidator(address _validator) public view override(ILeonidas) returns (bool) { - return store.validatorSet.contains(_validator); + function getAttesters() public view override(ILeonidas) returns (address[] memory) { + return stakingStore.attesters.values(); } /** @@ -241,7 +208,9 @@ contract Leonidas is Ownable, TimeFns, ILeonidas { function getProposerAt(Timestamp _ts) public view override(ILeonidas) returns (address) { Slot slot = getSlotAt(_ts); Epoch epochNumber = getEpochAtSlot(slot); - return store.getProposerAt(slot, epochNumber, TARGET_COMMITTEE_SIZE); + return LeonidasLib.getProposerAt( + leonidasStore, stakingStore, slot, epochNumber, TARGET_COMMITTEE_SIZE + ); } /** @@ -277,12 +246,19 @@ contract Leonidas is Ownable, TimeFns, ILeonidas { return Epoch.wrap(_slotNumber.unwrap() / EPOCH_DURATION); } - /** - * @notice Adds a validator to the set WITHOUT setting up the epoch - * @param _validator - The validator to add - */ - function _addValidator(address _validator) internal { - store.validatorSet.add(_validator); + // Can be used to add validators without setting up the epoch, useful for the initial set. + function _cheat__Deposit( + address _attester, + address _proposer, + address _withdrawer, + uint256 _amount + ) internal { + require( + _attester != address(0) && _proposer != address(0), + Errors.Leonidas__InvalidDeposit(_attester, _proposer) + ); + + super.deposit(_attester, _proposer, _withdrawer, _amount); } /** @@ -308,7 +284,16 @@ contract Leonidas is Ownable, TimeFns, ILeonidas { DataStructures.ExecutionFlags memory _flags ) internal view { Epoch epochNumber = getEpochAtSlot(_slot); - store.validateLeonidas(_slot, epochNumber, _signatures, _digest, _flags, TARGET_COMMITTEE_SIZE); + LeonidasLib.validateLeonidas( + leonidasStore, + stakingStore, + _slot, + epochNumber, + _signatures, + _digest, + _flags, + TARGET_COMMITTEE_SIZE + ); } /** diff --git a/l1-contracts/src/core/Rollup.sol b/l1-contracts/src/core/Rollup.sol index f706f40d523..11ee424ba4d 100644 --- a/l1-contracts/src/core/Rollup.sol +++ b/l1-contracts/src/core/Rollup.sol @@ -7,6 +7,7 @@ import {IProofCommitmentEscrow} from "@aztec/core/interfaces/IProofCommitmentEsc import { IRollup, ITestRollup, + CheatDepositArgs, FeeHeader, ManaBaseFeeComponents, BlockLog, @@ -40,6 +41,7 @@ import {Outbox} from "@aztec/core/messagebridge/Outbox.sol"; import {ProofCommitmentEscrow} from "@aztec/core/ProofCommitmentEscrow.sol"; import {IRewardDistributor} from "@aztec/governance/interfaces/IRewardDistributor.sol"; import {MockVerifier} from "@aztec/mock/MockVerifier.sol"; +import {Ownable} from "@oz/access/Ownable.sol"; import {IERC20} from "@oz/token/ERC20/IERC20.sol"; import {EIP712} from "@oz/utils/cryptography/EIP712.sol"; import {Vm} from "forge-std/Vm.sol"; @@ -49,6 +51,7 @@ struct Config { uint256 aztecEpochDuration; uint256 targetCommitteeSize; uint256 aztecEpochProofClaimWindowInL2Slots; + uint256 minimumStake; } /** @@ -57,7 +60,7 @@ struct Config { * @notice Rollup contract that is concerned about readability and velocity of development * not giving a damn about gas costs. */ -contract Rollup is EIP712("Aztec Rollup", "1"), Leonidas, IRollup, ITestRollup { +contract Rollup is EIP712("Aztec Rollup", "1"), Ownable, Leonidas, IRollup, ITestRollup { using SlotLib for Slot; using EpochLib for Epoch; using ProposeLib for ProposeArgs; @@ -97,14 +100,17 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Leonidas, IRollup, ITestRollup { constructor( IFeeJuicePortal _fpcJuicePortal, IRewardDistributor _rewardDistributor, + IERC20 _stakingAsset, bytes32 _vkTreeRoot, bytes32 _protocolContractTreeRoot, address _ares, - address[] memory _validators, Config memory _config ) + Ownable(_ares) Leonidas( _ares, + _stakingAsset, + _config.minimumStake, _config.aztecSlotDuration, _config.aztecEpochDuration, _config.targetCommitteeSize @@ -145,8 +151,15 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Leonidas, IRollup, ITestRollup { post: L1FeeData({baseFee: block.basefee, blobFee: _getBlobBaseFee()}), slotOfChange: LIFETIME }); - for (uint256 i = 0; i < _validators.length; i++) { - _addValidator(_validators[i]); + } + + function cheat__InitialiseValidatorSet(CheatDepositArgs[] memory _args) + external + override(ITestRollup) + onlyOwner + { + for (uint256 i = 0; i < _args.length; i++) { + _cheat__Deposit(_args[i].attester, _args[i].proposer, _args[i].withdrawer, _args[i].amount); } setupEpoch(); } diff --git a/l1-contracts/src/core/interfaces/ILeonidas.sol b/l1-contracts/src/core/interfaces/ILeonidas.sol index 256abed990e..9624ead5043 100644 --- a/l1-contracts/src/core/interfaces/ILeonidas.sol +++ b/l1-contracts/src/core/interfaces/ILeonidas.sol @@ -3,12 +3,11 @@ pragma solidity >=0.8.27; import {Timestamp, Slot, Epoch} from "@aztec/core/libraries/TimeMath.sol"; -import {EnumerableSet} from "@oz/utils/structs/EnumerableSet.sol"; /** * @notice The data structure for an epoch - * @param committee - The validator set for the epoch - * @param sampleSeed - The seed used to sample the validator set of the epoch + * @param committee - The attesters for the epoch + * @param sampleSeed - The seed used to sample the attesters of the epoch * @param nextSeed - The seed used to influence the NEXT epoch */ struct EpochData { @@ -18,7 +17,6 @@ struct EpochData { } struct LeonidasStorage { - EnumerableSet.AddressSet validatorSet; // A mapping to snapshots of the validator set mapping(Epoch => EpochData) epochs; // The last stored randao value, same value as `seed` in the last inserted epoch @@ -26,10 +24,6 @@ struct LeonidasStorage { } interface ILeonidas { - // Changing depending on sybil mechanism and slashing enforcement - function addValidator(address _validator) external; - function removeValidator(address _validator) external; - // Likely changing to optimize in Pleistarchus function setupEpoch() external; function getCurrentProposer() external view returns (address); @@ -38,9 +32,6 @@ interface ILeonidas { // Stable function getCurrentEpoch() external view returns (Epoch); function getCurrentSlot() external view returns (Slot); - function isValidator(address _validator) external view returns (bool); - function getValidatorCount() external view returns (uint256); - function getValidatorAt(uint256 _index) external view returns (address); // Consider removing below this point function getTimestampForSlot(Slot _slotNumber) external view returns (Timestamp); @@ -49,7 +40,7 @@ interface ILeonidas { // Get the current epoch committee function getCurrentEpochCommittee() external view returns (address[] memory); function getEpochCommittee(Epoch _epoch) external view returns (address[] memory); - function getValidators() external view returns (address[] memory); + function getAttesters() external view returns (address[] memory); function getEpochAt(Timestamp _ts) external view returns (Epoch); function getSlotAt(Timestamp _ts) external view returns (Slot); diff --git a/l1-contracts/src/core/interfaces/IRollup.sol b/l1-contracts/src/core/interfaces/IRollup.sol index 387fe706175..fb22590932e 100644 --- a/l1-contracts/src/core/interfaces/IRollup.sol +++ b/l1-contracts/src/core/interfaces/IRollup.sol @@ -53,11 +53,19 @@ struct RollupStore { IVerifier epochProofVerifier; } +struct CheatDepositArgs { + address attester; + address proposer; + address withdrawer; + uint256 amount; +} + interface ITestRollup { function setEpochVerifier(address _verifier) external; function setVkTreeRoot(bytes32 _vkTreeRoot) external; function setProtocolContractTreeRoot(bytes32 _protocolContractTreeRoot) external; function setAssumeProvenThroughBlockNumber(uint256 _blockNumber) external; + function cheat__InitialiseValidatorSet(CheatDepositArgs[] memory _args) external; function getManaBaseFeeComponentsAt(Timestamp _timestamp, bool _inFeeAsset) external view diff --git a/l1-contracts/src/core/interfaces/IStaking.sol b/l1-contracts/src/core/interfaces/IStaking.sol index 12d1cce4ab9..e2d469dd8a3 100644 --- a/l1-contracts/src/core/interfaces/IStaking.sol +++ b/l1-contracts/src/core/interfaces/IStaking.sol @@ -3,6 +3,7 @@ pragma solidity >=0.8.27; import {Timestamp} from "@aztec/core/libraries/TimeMath.sol"; +import {EnumerableSet} from "@oz/utils/structs/EnumerableSet.sol"; // None -> Does not exist in our setup // Validating -> Participating as validator @@ -33,6 +34,12 @@ struct Exit { address recipient; } +struct StakingStorage { + EnumerableSet.AddressSet attesters; + mapping(address attester => ValidatorInfo) info; + mapping(address attester => Exit) exits; +} + interface IStaking { event Deposit( address indexed attester, address indexed proposer, address indexed withdrawer, uint256 amount diff --git a/l1-contracts/src/core/libraries/Errors.sol b/l1-contracts/src/core/libraries/Errors.sol index 32d6e3a65ba..3b97bb534b5 100644 --- a/l1-contracts/src/core/libraries/Errors.sol +++ b/l1-contracts/src/core/libraries/Errors.sol @@ -98,6 +98,7 @@ library Errors { // Sequencer Selection (Leonidas) error Leonidas__EpochNotSetup(); // 0xcf4e597e error Leonidas__InvalidProposer(address expected, address actual); // 0xd02d278e + error Leonidas__InvalidDeposit(address attester, address proposer); // 0x1ef9a54b error Leonidas__InsufficientAttestations(uint256 minimumNeeded, uint256 provided); // 0xbf1ca4cb error Leonidas__InsufficientAttestationsProvided(uint256 minimumNeeded, uint256 provided); // 0xb3a697c2 diff --git a/l1-contracts/src/core/libraries/LeonidasLib/LeonidasLib.sol b/l1-contracts/src/core/libraries/LeonidasLib/LeonidasLib.sol index 28bc684fd84..56b465bbbc6 100644 --- a/l1-contracts/src/core/libraries/LeonidasLib/LeonidasLib.sol +++ b/l1-contracts/src/core/libraries/LeonidasLib/LeonidasLib.sol @@ -3,6 +3,7 @@ pragma solidity >=0.8.27; import {EpochData, LeonidasStorage} from "@aztec/core/interfaces/ILeonidas.sol"; +import {StakingStorage} from "@aztec/core/interfaces/IStaking.sol"; import {SampleLib} from "@aztec/core/libraries/crypto/SampleLib.sol"; import {SignatureLib, Signature} from "@aztec/core/libraries/crypto/SignatureLib.sol"; import {DataStructures} from "@aztec/core/libraries/DataStructures.sol"; @@ -25,28 +26,30 @@ library LeonidasLib { * @return The validators for the given epoch */ function sampleValidators( - LeonidasStorage storage _store, + StakingStorage storage _stakingStore, uint256 _seed, uint256 _targetCommitteeSize ) external view returns (address[] memory) { - return _sampleValidators(_store, _seed, _targetCommitteeSize); + return _sampleValidators(_stakingStore, _seed, _targetCommitteeSize); } function getProposerAt( - LeonidasStorage storage _store, + LeonidasStorage storage _leonidasStore, + StakingStorage storage _stakingStore, Slot _slot, Epoch _epochNumber, uint256 _targetCommitteeSize ) external view returns (address) { - return _getProposerAt(_store, _slot, _epochNumber, _targetCommitteeSize); + return _getProposerAt(_leonidasStore, _stakingStore, _slot, _epochNumber, _targetCommitteeSize); } function getCommitteeAt( - LeonidasStorage storage _store, + LeonidasStorage storage _leonidasStore, + StakingStorage storage _stakingStore, Epoch _epochNumber, uint256 _targetCommitteeSize ) external view returns (address[] memory) { - return _getCommitteeAt(_store, _epochNumber, _targetCommitteeSize); + return _getCommitteeAt(_leonidasStore, _stakingStore, _epochNumber, _targetCommitteeSize); } /** @@ -66,7 +69,8 @@ library LeonidasLib { * @param _digest - The digest of the block */ function validateLeonidas( - LeonidasStorage storage _store, + LeonidasStorage storage _leonidasStore, + StakingStorage storage _stakingStore, Slot _slot, Epoch _epochNumber, Signature[] memory _signatures, @@ -74,7 +78,16 @@ library LeonidasLib { DataStructures.ExecutionFlags memory _flags, uint256 _targetCommitteeSize ) external view { - address proposer = _getProposerAt(_store, _slot, _epochNumber, _targetCommitteeSize); + // Same logic as we got in getProposerAt + // Done do avoid duplicate computing the committee + address[] memory committee = + _getCommitteeAt(_leonidasStore, _stakingStore, _epochNumber, _targetCommitteeSize); + address attester = committee.length == 0 + ? address(0) + : committee[computeProposerIndex( + _epochNumber, _slot, getSampleSeed(_leonidasStore, _epochNumber), committee.length + )]; + address proposer = _stakingStore.info[attester].proposer; // @todo Consider getting rid of this option. // If the proposer is open, we allow anyone to propose without needing any signatures @@ -85,16 +98,10 @@ library LeonidasLib { // @todo We should allow to provide a signature instead of needing the proposer to broadcast. require(proposer == msg.sender, Errors.Leonidas__InvalidProposer(proposer, msg.sender)); - // @note This is NOT the efficient way to do it, but it is a very convenient way for us to do it - // that allows us to reduce the number of code paths. Also when changed with optimistic for - // pleistarchus, this will be changed, so we can live with it. - if (_flags.ignoreSignatures) { return; } - address[] memory committee = _getCommitteeAt(_store, _epochNumber, _targetCommitteeSize); - uint256 needed = committee.length * 2 / 3 + 1; require( _signatures.length >= needed, @@ -137,7 +144,7 @@ library LeonidasLib { * * @return The sample seed for the epoch */ - function getSampleSeed(LeonidasStorage storage _store, Epoch _epoch) + function getSampleSeed(LeonidasStorage storage _leonidasStore, Epoch _epoch) internal view returns (uint256) @@ -145,17 +152,17 @@ library LeonidasLib { if (Epoch.unwrap(_epoch) == 0) { return type(uint256).max; } - uint256 sampleSeed = _store.epochs[_epoch].sampleSeed; + uint256 sampleSeed = _leonidasStore.epochs[_epoch].sampleSeed; if (sampleSeed != 0) { return sampleSeed; } - sampleSeed = _store.epochs[_epoch - Epoch.wrap(1)].nextSeed; + sampleSeed = _leonidasStore.epochs[_epoch - Epoch.wrap(1)].nextSeed; if (sampleSeed != 0) { return sampleSeed; } - return _store.lastSeed; + return _leonidasStore.lastSeed; } /** @@ -167,32 +174,33 @@ library LeonidasLib { * @return The validators for the given epoch */ function _sampleValidators( - LeonidasStorage storage _store, + StakingStorage storage _stakingStore, uint256 _seed, uint256 _targetCommitteeSize ) private view returns (address[] memory) { - uint256 validatorSetSize = _store.validatorSet.length(); + uint256 validatorSetSize = _stakingStore.attesters.length(); if (validatorSetSize == 0) { return new address[](0); } // If we have less validators than the target committee size, we just return the full set if (validatorSetSize <= _targetCommitteeSize) { - return _store.validatorSet.values(); + return _stakingStore.attesters.values(); } - uint256[] memory indicies = + uint256[] memory indices = SampleLib.computeCommitteeClever(_targetCommitteeSize, validatorSetSize, _seed); address[] memory committee = new address[](_targetCommitteeSize); for (uint256 i = 0; i < _targetCommitteeSize; i++) { - committee[i] = _store.validatorSet.at(indicies[i]); + committee[i] = _stakingStore.attesters.at(indices[i]); } return committee; } function _getProposerAt( - LeonidasStorage storage _store, + LeonidasStorage storage _leonidasStore, + StakingStorage storage _stakingStore, Slot _slot, Epoch _epochNumber, uint256 _targetCommitteeSize @@ -201,21 +209,26 @@ library LeonidasLib { // it does not need to actually return the full committee and then draw from it // it can just return the proposer directly, but then we duplicate the code // which we just don't have room for right now... - address[] memory committee = _getCommitteeAt(_store, _epochNumber, _targetCommitteeSize); + address[] memory committee = + _getCommitteeAt(_leonidasStore, _stakingStore, _epochNumber, _targetCommitteeSize); if (committee.length == 0) { return address(0); } - return committee[computeProposerIndex( - _epochNumber, _slot, getSampleSeed(_store, _epochNumber), committee.length + + address attester = committee[computeProposerIndex( + _epochNumber, _slot, getSampleSeed(_leonidasStore, _epochNumber), committee.length )]; + + return _stakingStore.info[attester].proposer; } function _getCommitteeAt( - LeonidasStorage storage _store, + LeonidasStorage storage _leonidasStore, + StakingStorage storage _stakingStore, Epoch _epochNumber, uint256 _targetCommitteeSize ) private view returns (address[] memory) { - EpochData storage epoch = _store.epochs[_epochNumber]; + EpochData storage epoch = _leonidasStore.epochs[_epochNumber]; if (epoch.sampleSeed != 0) { uint256 committeeSize = epoch.committee.length; @@ -226,13 +239,13 @@ library LeonidasLib { } // Allow anyone if there is no validator set - if (_store.validatorSet.length() == 0) { + if (_stakingStore.attesters.length() == 0) { return new address[](0); } // Emulate a sampling of the validators - uint256 sampleSeed = getSampleSeed(_store, _epochNumber); - return _sampleValidators(_store, sampleSeed, _targetCommitteeSize); + uint256 sampleSeed = getSampleSeed(_leonidasStore, _epochNumber); + return _sampleValidators(_stakingStore, sampleSeed, _targetCommitteeSize); } /** diff --git a/l1-contracts/src/core/staking/Staking.sol b/l1-contracts/src/core/staking/Staking.sol index 7f0a0c3b446..0d75e74e1c1 100644 --- a/l1-contracts/src/core/staking/Staking.sol +++ b/l1-contracts/src/core/staking/Staking.sol @@ -3,7 +3,12 @@ pragma solidity >=0.8.27; import { - IStaking, ValidatorInfo, Exit, Status, OperatorInfo + IStaking, + ValidatorInfo, + Exit, + Status, + OperatorInfo, + StakingStorage } from "@aztec/core/interfaces/IStaking.sol"; import {Errors} from "@aztec/core/libraries/Errors.sol"; import {Timestamp} from "@aztec/core/libraries/TimeMath.sol"; @@ -22,11 +27,7 @@ contract Staking is IStaking { IERC20 public immutable STAKING_ASSET; uint256 public immutable MINIMUM_STAKE; - // address <=> index - EnumerableSet.AddressSet internal attesters; - - mapping(address attester => ValidatorInfo) internal info; - mapping(address attester => Exit) internal exits; + StakingStorage internal stakingStore; constructor(address _slasher, IERC20 _stakingAsset, uint256 _minimumStake) { SLASHER = _slasher; @@ -35,10 +36,10 @@ contract Staking is IStaking { } function finaliseWithdraw(address _attester) external override(IStaking) { - ValidatorInfo storage validator = info[_attester]; + ValidatorInfo storage validator = stakingStore.info[_attester]; require(validator.status == Status.EXITING, Errors.Staking__NotExiting(_attester)); - Exit storage exit = exits[_attester]; + Exit storage exit = stakingStore.exits[_attester]; require( exit.exitableAt <= Timestamp.wrap(block.timestamp), Errors.Staking__WithdrawalNotUnlockedYet(Timestamp.wrap(block.timestamp), exit.exitableAt) @@ -47,8 +48,8 @@ contract Staking is IStaking { uint256 amount = validator.stake; address recipient = exit.recipient; - delete exits[_attester]; - delete info[_attester]; + delete stakingStore.exits[_attester]; + delete stakingStore.info[_attester]; STAKING_ASSET.transfer(recipient, amount); @@ -58,14 +59,14 @@ contract Staking is IStaking { function slash(address _attester, uint256 _amount) external override(IStaking) { require(msg.sender == SLASHER, Errors.Staking__NotSlasher(SLASHER, msg.sender)); - ValidatorInfo storage validator = info[_attester]; + ValidatorInfo storage validator = stakingStore.info[_attester]; require(validator.status != Status.NONE, Errors.Staking__NoOneToSlash(_attester)); // There is a special, case, if exiting and past the limit, it is untouchable! require( !( validator.status == Status.EXITING - && exits[_attester].exitableAt <= Timestamp.wrap(block.timestamp) + && stakingStore.exits[_attester].exitableAt <= Timestamp.wrap(block.timestamp) ), Errors.Staking__CannotSlashExitedStake(_attester) ); @@ -75,7 +76,7 @@ contract Staking is IStaking { // When LIVING, he can only start exiting, we don't "really" exit him, because that cost // gas and cost edge cases around recipient, so lets just avoid that. if (validator.status == Status.VALIDATING && validator.stake < MINIMUM_STAKE) { - require(attesters.remove(_attester), Errors.Staking__FailedToRemove(_attester)); + require(stakingStore.attesters.remove(_attester), Errors.Staking__FailedToRemove(_attester)); validator.status = Status.LIVING; } @@ -88,28 +89,11 @@ contract Staking is IStaking { override(IStaking) returns (ValidatorInfo memory) { - return info[_attester]; - } - - function getProposerForAttester(address _attester) - external - view - override(IStaking) - returns (address) - { - return info[_attester].proposer; + return stakingStore.info[_attester]; } function getExit(address _attester) external view override(IStaking) returns (Exit memory) { - return exits[_attester]; - } - - function getAttesterAtIndex(uint256 _index) external view override(IStaking) returns (address) { - return attesters.at(_index); - } - - function getProposerAtIndex(uint256 _index) external view override(IStaking) returns (address) { - return info[attesters.at(_index)].proposer; + return stakingStore.exits[_attester]; } function getOperatorAtIndex(uint256 _index) @@ -118,8 +102,8 @@ contract Staking is IStaking { override(IStaking) returns (OperatorInfo memory) { - address attester = attesters.at(_index); - return OperatorInfo({proposer: info[attester].proposer, attester: attester}); + address attester = stakingStore.attesters.at(_index); + return OperatorInfo({proposer: stakingStore.info[attester].proposer, attester: attester}); } function deposit(address _attester, address _proposer, address _withdrawer, uint256 _amount) @@ -129,12 +113,15 @@ contract Staking is IStaking { { require(_amount >= MINIMUM_STAKE, Errors.Staking__InsufficientStake(_amount, MINIMUM_STAKE)); STAKING_ASSET.transferFrom(msg.sender, address(this), _amount); - require(info[_attester].status == Status.NONE, Errors.Staking__AlreadyRegistered(_attester)); - require(attesters.add(_attester), Errors.Staking__AlreadyActive(_attester)); + require( + stakingStore.info[_attester].status == Status.NONE, + Errors.Staking__AlreadyRegistered(_attester) + ); + require(stakingStore.attesters.add(_attester), Errors.Staking__AlreadyActive(_attester)); // If BLS, need to check possession of private key to avoid attacks. - info[_attester] = ValidatorInfo({ + stakingStore.info[_attester] = ValidatorInfo({ stake: _amount, withdrawer: _withdrawer, proposer: _proposer, @@ -150,7 +137,7 @@ contract Staking is IStaking { override(IStaking) returns (bool) { - ValidatorInfo storage validator = info[_attester]; + ValidatorInfo storage validator = stakingStore.info[_attester]; require( msg.sender == validator.withdrawer, @@ -161,12 +148,12 @@ contract Staking is IStaking { Errors.Staking__NothingToExit(_attester) ); if (validator.status == Status.VALIDATING) { - require(attesters.remove(_attester), Errors.Staking__FailedToRemove(_attester)); + require(stakingStore.attesters.remove(_attester), Errors.Staking__FailedToRemove(_attester)); } // Note that the "amount" is not stored here, but reusing the `validators` // We always exit fully. - exits[_attester] = + stakingStore.exits[_attester] = Exit({exitableAt: Timestamp.wrap(block.timestamp) + EXIT_DELAY, recipient: _recipient}); validator.status = Status.EXITING; @@ -176,6 +163,23 @@ contract Staking is IStaking { } function getActiveAttesterCount() public view override(IStaking) returns (uint256) { - return attesters.length(); + return stakingStore.attesters.length(); + } + + function getProposerForAttester(address _attester) + public + view + override(IStaking) + returns (address) + { + return stakingStore.info[_attester].proposer; + } + + function getAttesterAtIndex(uint256 _index) public view override(IStaking) returns (address) { + return stakingStore.attesters.at(_index); + } + + function getProposerAtIndex(uint256 _index) public view override(IStaking) returns (address) { + return stakingStore.info[stakingStore.attesters.at(_index)].proposer; } } diff --git a/l1-contracts/src/mock/MockFeeJuicePortal.sol b/l1-contracts/src/mock/MockFeeJuicePortal.sol index a7d56cae0e8..0d180556754 100644 --- a/l1-contracts/src/mock/MockFeeJuicePortal.sol +++ b/l1-contracts/src/mock/MockFeeJuicePortal.sol @@ -13,7 +13,7 @@ contract MockFeeJuicePortal is IFeeJuicePortal { IRegistry public constant REGISTRY = IRegistry(address(0)); constructor() { - UNDERLYING = new TestERC20(); + UNDERLYING = new TestERC20("test", "TEST", msg.sender); } function initialize() external override(IFeeJuicePortal) {} diff --git a/l1-contracts/src/mock/TestERC20.sol b/l1-contracts/src/mock/TestERC20.sol index 6236f94d758..3883b407347 100644 --- a/l1-contracts/src/mock/TestERC20.sol +++ b/l1-contracts/src/mock/TestERC20.sol @@ -2,13 +2,31 @@ // docs:start:contract pragma solidity >=0.8.27; +import {Ownable} from "@oz/access/Ownable.sol"; import {ERC20} from "@oz/token/ERC20/ERC20.sol"; -import {IMintableERC20} from "../governance/interfaces/IMintableERC20.sol"; +import {IMintableERC20} from "./../governance/interfaces/IMintableERC20.sol"; -contract TestERC20 is ERC20, IMintableERC20 { - constructor() ERC20("Portal", "PORTAL") {} +contract TestERC20 is ERC20, IMintableERC20, Ownable { + bool public freeForAll = false; - function mint(address _to, uint256 _amount) external override(IMintableERC20) { + modifier ownerOrFreeForAll() { + if (msg.sender != owner() && !freeForAll) { + revert("Not owner or free for all"); + } + _; + } + + constructor(string memory _name, string memory _symbol, address _owner) + ERC20(_name, _symbol) + Ownable(_owner) + {} + + // solhint-disable-next-line comprehensive-interface + function setFreeForAll(bool _freeForAll) external onlyOwner { + freeForAll = _freeForAll; + } + + function mint(address _to, uint256 _amount) external override(IMintableERC20) ownerOrFreeForAll { _mint(_to, _amount); } } diff --git a/l1-contracts/terraform/main.tf b/l1-contracts/terraform/main.tf index 5a720d5c204..d619a827877 100644 --- a/l1-contracts/terraform/main.tf +++ b/l1-contracts/terraform/main.tf @@ -57,6 +57,15 @@ output "fee_juice_contract_address" { value = var.FEE_JUICE_CONTRACT_ADDRESS } +variable "STAKING_ASSET_CONTRACT_ADDRESS" { + type = string + default = "" +} + +output "staking_asset_contract_address" { + value = var.STAKING_ASSET_CONTRACT_ADDRESS +} + variable "FEE_JUICE_PORTAL_CONTRACT_ADDRESS" { type = string default = "" diff --git a/l1-contracts/test/Rollup.t.sol b/l1-contracts/test/Rollup.t.sol index 80d85eb3115..504db52ae57 100644 --- a/l1-contracts/test/Rollup.t.sol +++ b/l1-contracts/test/Rollup.t.sol @@ -74,8 +74,12 @@ contract RollupTest is DecoderBase, TimeFns { */ modifier setUpFor(string memory _name) { { + testERC20 = new TestERC20("test", "TEST", address(this)); + leo = new Leonidas( address(1), + testERC20, + TestConstants.AZTEC_MINIMUM_STAKE, TestConstants.AZTEC_SLOT_DURATION, TestConstants.AZTEC_EPOCH_DURATION, TestConstants.AZTEC_TARGET_COMMITTEE_SIZE @@ -88,7 +92,6 @@ contract RollupTest is DecoderBase, TimeFns { } registry = new Registry(address(this)); - testERC20 = new TestERC20(); feeJuicePortal = new FeeJuicePortal( address(registry), address(testERC20), bytes32(Constants.FEE_JUICE_ADDRESS) ); @@ -98,7 +101,7 @@ contract RollupTest is DecoderBase, TimeFns { testERC20.mint(address(rewardDistributor), 1e6 ether); rollup = new Rollup( - feeJuicePortal, rewardDistributor, bytes32(0), bytes32(0), address(this), new address[](0) + feeJuicePortal, rewardDistributor, testERC20, bytes32(0), bytes32(0), address(this) ); inbox = Inbox(address(rollup.INBOX())); outbox = Outbox(address(rollup.OUTBOX())); diff --git a/l1-contracts/test/TestERC20.t.sol b/l1-contracts/test/TestERC20.t.sol index 3b7abc4cfa7..b95dcd9d49d 100644 --- a/l1-contracts/test/TestERC20.t.sol +++ b/l1-contracts/test/TestERC20.t.sol @@ -7,11 +7,18 @@ contract TestERC20Test is Test { TestERC20 testERC20; function setUp() public { - testERC20 = new TestERC20(); + testERC20 = new TestERC20("test", "TEST", address(this)); } function test_mint() public { testERC20.mint(address(this), 100); assertEq(testERC20.balanceOf(address(this)), 100); } + + function test_mint_only_owner(address _caller) public { + vm.assume(_caller != address(this)); + vm.expectRevert(); + vm.prank(_caller); + testERC20.mint(address(this), 100); + } } diff --git a/l1-contracts/test/fee_portal/depositToAztecPublic.t.sol b/l1-contracts/test/fee_portal/depositToAztecPublic.t.sol index fc68df1a444..8eb7bcd301c 100644 --- a/l1-contracts/test/fee_portal/depositToAztecPublic.t.sol +++ b/l1-contracts/test/fee_portal/depositToAztecPublic.t.sol @@ -27,16 +27,15 @@ contract DepositToAztecPublic is Test { function setUp() public { registry = new Registry(OWNER); - token = new TestERC20(); + token = new TestERC20("test", "TEST", address(this)); feeJuicePortal = new FeeJuicePortal(address(registry), address(token), bytes32(Constants.FEE_JUICE_ADDRESS)); token.mint(address(feeJuicePortal), Constants.FEE_JUICE_INITIAL_MINT); feeJuicePortal.initialize(); rewardDistributor = new RewardDistributor(token, registry, address(this)); - rollup = new Rollup( - feeJuicePortal, rewardDistributor, bytes32(0), bytes32(0), address(this), new address[](0) - ); + rollup = + new Rollup(feeJuicePortal, rewardDistributor, token, bytes32(0), bytes32(0), address(this)); vm.prank(OWNER); registry.upgrade(address(rollup)); @@ -67,9 +66,8 @@ contract DepositToAztecPublic is Test { uint256 numberOfRollups = bound(_numberOfRollups, 1, 5); for (uint256 i = 0; i < numberOfRollups; i++) { - Rollup freshRollup = new Rollup( - feeJuicePortal, rewardDistributor, bytes32(0), bytes32(0), address(this), new address[](0) - ); + Rollup freshRollup = + new Rollup(feeJuicePortal, rewardDistributor, token, bytes32(0), bytes32(0), address(this)); vm.prank(OWNER); registry.upgrade(address(freshRollup)); } diff --git a/l1-contracts/test/fee_portal/distributeFees.t.sol b/l1-contracts/test/fee_portal/distributeFees.t.sol index bfb366e21c7..0308b2d9433 100644 --- a/l1-contracts/test/fee_portal/distributeFees.t.sol +++ b/l1-contracts/test/fee_portal/distributeFees.t.sol @@ -26,16 +26,15 @@ contract DistributeFees is Test { function setUp() public { registry = new Registry(OWNER); - token = new TestERC20(); + token = new TestERC20("test", "TEST", address(this)); feeJuicePortal = new FeeJuicePortal(address(registry), address(token), bytes32(Constants.FEE_JUICE_ADDRESS)); token.mint(address(feeJuicePortal), Constants.FEE_JUICE_INITIAL_MINT); feeJuicePortal.initialize(); rewardDistributor = new RewardDistributor(token, registry, address(this)); - rollup = new Rollup( - feeJuicePortal, rewardDistributor, bytes32(0), bytes32(0), address(this), new address[](0) - ); + rollup = + new Rollup(feeJuicePortal, rewardDistributor, token, bytes32(0), bytes32(0), address(this)); vm.prank(OWNER); registry.upgrade(address(rollup)); @@ -74,9 +73,8 @@ contract DistributeFees is Test { uint256 numberOfRollups = bound(_numberOfRollups, 1, 5); for (uint256 i = 0; i < numberOfRollups; i++) { - Rollup freshRollup = new Rollup( - feeJuicePortal, rewardDistributor, bytes32(0), bytes32(0), address(this), new address[](0) - ); + Rollup freshRollup = + new Rollup(feeJuicePortal, rewardDistributor, token, bytes32(0), bytes32(0), address(this)); vm.prank(OWNER); registry.upgrade(address(freshRollup)); } diff --git a/l1-contracts/test/fees/FeeRollup.t.sol b/l1-contracts/test/fees/FeeRollup.t.sol index ba655ed2e74..8331d66d7ee 100644 --- a/l1-contracts/test/fees/FeeRollup.t.sol +++ b/l1-contracts/test/fees/FeeRollup.t.sol @@ -114,21 +114,24 @@ contract FeeRollupTest is FeeModelTestPoints, DecoderBase { vm.fee(l1Metadata[0].base_fee); vm.blobBaseFee(l1Metadata[0].blob_fee); - asset = new TestERC20(); + asset = new TestERC20("test", "TEST", address(this)); fakeCanonical = new FakeCanonical(IERC20(address(asset))); + asset.transferOwnership(address(fakeCanonical)); + rollup = new Rollup( IFeeJuicePortal(address(fakeCanonical)), IRewardDistributor(address(fakeCanonical)), + asset, bytes32(0), bytes32(0), address(this), - new address[](0), Config({ aztecSlotDuration: SLOT_DURATION, aztecEpochDuration: EPOCH_DURATION, targetCommitteeSize: 48, - aztecEpochProofClaimWindowInL2Slots: 16 + aztecEpochProofClaimWindowInL2Slots: 16, + minimumStake: 100 ether }) ); fakeCanonical.setCanonicalRollup(address(rollup)); diff --git a/l1-contracts/test/governance/coin-issuer/Base.t.sol b/l1-contracts/test/governance/coin-issuer/Base.t.sol index b2812e4c7e9..ada73d4d4ac 100644 --- a/l1-contracts/test/governance/coin-issuer/Base.t.sol +++ b/l1-contracts/test/governance/coin-issuer/Base.t.sol @@ -14,7 +14,9 @@ contract CoinIssuerBase is Test { CoinIssuer internal nom; function _deploy(uint256 _rate) internal { - token = IMintableERC20(address(new TestERC20())); + TestERC20 testERC20 = new TestERC20("test", "TEST", address(this)); + token = IMintableERC20(address(testERC20)); nom = new CoinIssuer(token, _rate, address(this)); + testERC20.transferOwnership(address(nom)); } } diff --git a/l1-contracts/test/governance/governance/base.t.sol b/l1-contracts/test/governance/governance/base.t.sol index 28e51b0e934..cc5a9878a06 100644 --- a/l1-contracts/test/governance/governance/base.t.sol +++ b/l1-contracts/test/governance/governance/base.t.sol @@ -35,7 +35,7 @@ contract GovernanceBase is TestBase { uint256 proposalId; function setUp() public virtual { - token = IMintableERC20(address(new TestERC20())); + token = IMintableERC20(address(new TestERC20("test", "TEST", address(this)))); registry = new Registry(address(this)); governanceProposer = new GovernanceProposer(registry, 677, 1000); diff --git a/l1-contracts/test/governance/reward-distributor/Base.t.sol b/l1-contracts/test/governance/reward-distributor/Base.t.sol index 8b3c6c511b1..4c6014d5a2c 100644 --- a/l1-contracts/test/governance/reward-distributor/Base.t.sol +++ b/l1-contracts/test/governance/reward-distributor/Base.t.sol @@ -16,7 +16,7 @@ contract RewardDistributorBase is Test { RewardDistributor internal rewardDistributor; function setUp() public { - token = IMintableERC20(address(new TestERC20())); + token = IMintableERC20(address(new TestERC20("test", "TEST", address(this)))); registry = new Registry(address(this)); rewardDistributor = new RewardDistributor(token, registry, address(this)); } diff --git a/l1-contracts/test/governance/scenario/UpgradeGovernanceProposerTest.t.sol b/l1-contracts/test/governance/scenario/UpgradeGovernanceProposerTest.t.sol index aea558c9f56..8504653da17 100644 --- a/l1-contracts/test/governance/scenario/UpgradeGovernanceProposerTest.t.sol +++ b/l1-contracts/test/governance/scenario/UpgradeGovernanceProposerTest.t.sol @@ -18,6 +18,8 @@ import {ProposalLib} from "@aztec/governance/libraries/ProposalLib.sol"; import {Errors} from "@aztec/governance/libraries/Errors.sol"; import {NewGovernanceProposerPayload} from "./NewGovernanceProposerPayload.sol"; import {RewardDistributor} from "@aztec/governance/RewardDistributor.sol"; +import {CheatDepositArgs} from "@aztec/core/interfaces/IRollup.sol"; +import {TestConstants} from "../../harnesses/TestConstants.sol"; /** * @title UpgradeGovernanceProposerTest @@ -44,32 +46,36 @@ contract UpgradeGovernanceProposerTest is TestBase { address internal constant EMPEROR = address(uint160(bytes20("EMPEROR"))); function setUp() external { - token = IMintableERC20(address(new TestERC20())); + token = IMintableERC20(address(new TestERC20("test", "TEST", address(this)))); registry = new Registry(address(this)); governanceProposer = new GovernanceProposer(registry, 7, 10); governance = new Governance(token, address(governanceProposer)); - address[] memory initialValidators = new address[](VALIDATOR_COUNT); + CheatDepositArgs[] memory initialValidators = new CheatDepositArgs[](VALIDATOR_COUNT); for (uint256 i = 1; i <= VALIDATOR_COUNT; i++) { uint256 privateKey = uint256(keccak256(abi.encode("validator", i))); address validator = vm.addr(privateKey); privateKeys[validator] = privateKey; validators[i - 1] = validator; - initialValidators[i - 1] = validator; + initialValidators[i - 1] = CheatDepositArgs({ + attester: validator, + proposer: validator, + withdrawer: validator, + amount: TestConstants.AZTEC_MINIMUM_STAKE + }); } RewardDistributor rewardDistributor = new RewardDistributor(token, registry, address(this)); rollup = new Rollup( - new MockFeeJuicePortal(), - rewardDistributor, - bytes32(0), - bytes32(0), - address(this), - initialValidators + new MockFeeJuicePortal(), rewardDistributor, token, bytes32(0), bytes32(0), address(this) ); + token.mint(address(this), TestConstants.AZTEC_MINIMUM_STAKE * VALIDATOR_COUNT); + token.approve(address(rollup), TestConstants.AZTEC_MINIMUM_STAKE * VALIDATOR_COUNT); + rollup.cheat__InitialiseValidatorSet(initialValidators); + registry.upgrade(address(rollup)); registry.transferOwnership(address(governance)); diff --git a/l1-contracts/test/harnesses/Leonidas.sol b/l1-contracts/test/harnesses/Leonidas.sol index f19deae2550..a7c78f304b1 100644 --- a/l1-contracts/test/harnesses/Leonidas.sol +++ b/l1-contracts/test/harnesses/Leonidas.sol @@ -4,11 +4,14 @@ pragma solidity >=0.8.27; import {Leonidas as RealLeonidas} from "@aztec/core/Leonidas.sol"; import {TestConstants} from "./TestConstants.sol"; +import {TestERC20} from "@aztec/mock/TestERC20.sol"; contract Leonidas is RealLeonidas { constructor(address _ares) RealLeonidas( _ares, + new TestERC20("test", "TEST", address(this)), + 100e18, TestConstants.AZTEC_SLOT_DURATION, TestConstants.AZTEC_EPOCH_DURATION, TestConstants.AZTEC_TARGET_COMMITTEE_SIZE diff --git a/l1-contracts/test/harnesses/Rollup.sol b/l1-contracts/test/harnesses/Rollup.sol index 27f78d3864d..41d72b20de9 100644 --- a/l1-contracts/test/harnesses/Rollup.sol +++ b/l1-contracts/test/harnesses/Rollup.sol @@ -6,28 +6,30 @@ import {IFeeJuicePortal} from "@aztec/core/interfaces/IFeeJuicePortal.sol"; import {IRewardDistributor} from "@aztec/governance/interfaces/IRewardDistributor.sol"; import {Rollup as RealRollup, Config} from "@aztec/core/Rollup.sol"; import {TestConstants} from "./TestConstants.sol"; +import {IERC20} from "@oz/token/ERC20/IERC20.sol"; contract Rollup is RealRollup { constructor( IFeeJuicePortal _fpcJuicePortal, IRewardDistributor _rewardDistributor, + IERC20 _stakingAsset, bytes32 _vkTreeRoot, bytes32 _protocolContractTreeRoot, - address _ares, - address[] memory _validators + address _ares ) RealRollup( _fpcJuicePortal, _rewardDistributor, + _stakingAsset, _vkTreeRoot, _protocolContractTreeRoot, _ares, - _validators, Config({ aztecSlotDuration: TestConstants.AZTEC_SLOT_DURATION, aztecEpochDuration: TestConstants.AZTEC_EPOCH_DURATION, targetCommitteeSize: TestConstants.AZTEC_TARGET_COMMITTEE_SIZE, - aztecEpochProofClaimWindowInL2Slots: TestConstants.AZTEC_EPOCH_PROOF_CLAIM_WINDOW_IN_L2_SLOTS + aztecEpochProofClaimWindowInL2Slots: TestConstants.AZTEC_EPOCH_PROOF_CLAIM_WINDOW_IN_L2_SLOTS, + minimumStake: TestConstants.AZTEC_MINIMUM_STAKE }) ) {} diff --git a/l1-contracts/test/harnesses/TestConstants.sol b/l1-contracts/test/harnesses/TestConstants.sol index 4a79b3c97e7..371a2d8f594 100644 --- a/l1-contracts/test/harnesses/TestConstants.sol +++ b/l1-contracts/test/harnesses/TestConstants.sol @@ -9,4 +9,5 @@ library TestConstants { uint256 internal constant AZTEC_EPOCH_DURATION = 16; uint256 internal constant AZTEC_TARGET_COMMITTEE_SIZE = 48; uint256 internal constant AZTEC_EPOCH_PROOF_CLAIM_WINDOW_IN_L2_SLOTS = 13; + uint256 internal constant AZTEC_MINIMUM_STAKE = 100e18; } diff --git a/l1-contracts/test/portals/TokenPortal.t.sol b/l1-contracts/test/portals/TokenPortal.t.sol index 990f3ec6512..c043d69d0cd 100644 --- a/l1-contracts/test/portals/TokenPortal.t.sol +++ b/l1-contracts/test/portals/TokenPortal.t.sol @@ -60,15 +60,10 @@ contract TokenPortalTest is Test { function setUp() public { registry = new Registry(address(this)); - testERC20 = new TestERC20(); + testERC20 = new TestERC20("test", "TEST", address(this)); rewardDistributor = new RewardDistributor(testERC20, registry, address(this)); rollup = new Rollup( - new MockFeeJuicePortal(), - rewardDistributor, - bytes32(0), - bytes32(0), - address(this), - new address[](0) + new MockFeeJuicePortal(), rewardDistributor, testERC20, bytes32(0), bytes32(0), address(this) ); inbox = rollup.INBOX(); outbox = rollup.OUTBOX(); diff --git a/l1-contracts/test/portals/UniswapPortal.t.sol b/l1-contracts/test/portals/UniswapPortal.t.sol index ac646e17bac..fc91ef5d158 100644 --- a/l1-contracts/test/portals/UniswapPortal.t.sol +++ b/l1-contracts/test/portals/UniswapPortal.t.sol @@ -55,12 +55,7 @@ contract UniswapPortalTest is Test { registry = new Registry(address(this)); RewardDistributor rewardDistributor = new RewardDistributor(DAI, registry, address(this)); rollup = new Rollup( - new MockFeeJuicePortal(), - rewardDistributor, - bytes32(0), - bytes32(0), - address(this), - new address[](0) + new MockFeeJuicePortal(), rewardDistributor, DAI, bytes32(0), bytes32(0), address(this) ); registry.upgrade(address(rollup)); diff --git a/l1-contracts/test/prover-coordination/ProofCommitmentEscrow.t.sol b/l1-contracts/test/prover-coordination/ProofCommitmentEscrow.t.sol index 45178dc9b9e..18c07487dfe 100644 --- a/l1-contracts/test/prover-coordination/ProofCommitmentEscrow.t.sol +++ b/l1-contracts/test/prover-coordination/ProofCommitmentEscrow.t.sol @@ -31,7 +31,7 @@ contract TestProofCommitmentEscrow is Test { } function setUp() public { - TOKEN = new TestERC20(); + TOKEN = new TestERC20("test", "TEST", address(this)); ESCROW = new ProofCommitmentEscrow( TOKEN, address(this), TestConstants.AZTEC_SLOT_DURATION, TestConstants.AZTEC_EPOCH_DURATION ); diff --git a/l1-contracts/test/sparta/Sparta.t.sol b/l1-contracts/test/sparta/Sparta.t.sol index ea0c94f7d49..dc5340a39ae 100644 --- a/l1-contracts/test/sparta/Sparta.t.sol +++ b/l1-contracts/test/sparta/Sparta.t.sol @@ -13,7 +13,7 @@ import {Outbox} from "@aztec/core/messagebridge/Outbox.sol"; import {Errors} from "@aztec/core/libraries/Errors.sol"; import {Registry} from "@aztec/governance/Registry.sol"; import {Rollup} from "../harnesses/Rollup.sol"; -import {Leonidas} from "../harnesses/Leonidas.sol"; +import {Leonidas} from "@aztec/core/Leonidas.sol"; import {NaiveMerkle} from "../merkle/Naive.sol"; import {MerkleTestUtil} from "../merkle/TestUtil.sol"; import {TestERC20} from "@aztec/mock/TestERC20.sol"; @@ -23,6 +23,8 @@ import {MockFeeJuicePortal} from "@aztec/mock/MockFeeJuicePortal.sol"; import { ProposeArgs, OracleInput, ProposeLib } from "@aztec/core/libraries/RollupLibs/ProposeLib.sol"; +import {TestConstants} from "../harnesses/TestConstants.sol"; +import {CheatDepositArgs} from "@aztec/core/interfaces/IRollup.sol"; import {Slot, Epoch, SlotLib, EpochLib} from "@aztec/core/libraries/TimeMath.sol"; import {RewardDistributor} from "@aztec/governance/RewardDistributor.sol"; @@ -51,7 +53,9 @@ contract SpartaTest is DecoderBase { TestERC20 internal testERC20; RewardDistributor internal rewardDistributor; Signature internal emptySignature; - mapping(address validator => uint256 privateKey) internal privateKeys; + mapping(address attester => uint256 privateKey) internal attesterPrivateKeys; + mapping(address proposer => uint256 privateKey) internal proposerPrivateKeys; + mapping(address proposer => address attester) internal proposerToAttester; mapping(address => bool) internal _seenValidators; mapping(address => bool) internal _seenCommittee; @@ -61,7 +65,15 @@ contract SpartaTest is DecoderBase { modifier setup(uint256 _validatorCount) { string memory _name = "mixed_block_1"; { - Leonidas leonidas = new Leonidas(address(1)); + Leonidas leonidas = new Leonidas( + address(1), + testERC20, + TestConstants.AZTEC_MINIMUM_STAKE, + TestConstants.AZTEC_SLOT_DURATION, + TestConstants.AZTEC_EPOCH_DURATION, + TestConstants.AZTEC_TARGET_COMMITTEE_SIZE + ); + DecoderBase.Full memory full = load(_name); uint256 slotNumber = full.block.decodedHeader.globalVariables.slotNumber; uint256 initialTime = @@ -69,25 +81,37 @@ contract SpartaTest is DecoderBase { vm.warp(initialTime); } - address[] memory initialValidators = new address[](_validatorCount); + CheatDepositArgs[] memory initialValidators = new CheatDepositArgs[](_validatorCount); + for (uint256 i = 1; i < _validatorCount + 1; i++) { - uint256 privateKey = uint256(keccak256(abi.encode("validator", i))); - address validator = vm.addr(privateKey); - privateKeys[validator] = privateKey; - initialValidators[i - 1] = validator; + uint256 attesterPrivateKey = uint256(keccak256(abi.encode("attester", i))); + address attester = vm.addr(attesterPrivateKey); + attesterPrivateKeys[attester] = attesterPrivateKey; + uint256 proposerPrivateKey = uint256(keccak256(abi.encode("proposer", i))); + address proposer = vm.addr(proposerPrivateKey); + proposerPrivateKeys[proposer] = proposerPrivateKey; + + proposerToAttester[proposer] = attester; + + initialValidators[i - 1] = CheatDepositArgs({ + attester: attester, + proposer: proposer, + withdrawer: address(this), + amount: TestConstants.AZTEC_MINIMUM_STAKE + }); } - testERC20 = new TestERC20(); + testERC20 = new TestERC20("test", "TEST", address(this)); Registry registry = new Registry(address(this)); rewardDistributor = new RewardDistributor(testERC20, registry, address(this)); rollup = new Rollup( - new MockFeeJuicePortal(), - rewardDistributor, - bytes32(0), - bytes32(0), - address(this), - initialValidators + new MockFeeJuicePortal(), rewardDistributor, testERC20, bytes32(0), bytes32(0), address(this) ); + + testERC20.mint(address(this), TestConstants.AZTEC_MINIMUM_STAKE * _validatorCount); + testERC20.approve(address(rollup), TestConstants.AZTEC_MINIMUM_STAKE * _validatorCount); + rollup.cheat__InitialiseValidatorSet(initialValidators); + inbox = Inbox(address(rollup.INBOX())); outbox = Outbox(address(rollup.OUTBOX())); @@ -97,15 +121,15 @@ contract SpartaTest is DecoderBase { _; } - function testInitialCommitteMatch() public setup(4) { - address[] memory validators = rollup.getValidators(); + function testInitialCommitteeMatch() public setup(4) { + address[] memory attesters = rollup.getAttesters(); address[] memory committee = rollup.getCurrentEpochCommittee(); assertEq(rollup.getCurrentEpoch(), 0); - assertEq(validators.length, 4, "Invalid validator set size"); + assertEq(attesters.length, 4, "Invalid validator set size"); assertEq(committee.length, 4, "invalid committee set size"); - for (uint256 i = 0; i < validators.length; i++) { - _seenValidators[validators[i]] = true; + for (uint256 i = 0; i < attesters.length; i++) { + _seenValidators[attesters[i]] = true; } for (uint256 i = 0; i < committee.length; i++) { @@ -114,8 +138,10 @@ contract SpartaTest is DecoderBase { _seenCommittee[committee[i]] = true; } + // The proposer is not necessarily an attester, we have to map it back. We can do this here + // because we created a 1:1 link. In practice, there could be multiple attesters for the same proposer address proposer = rollup.getCurrentProposer(); - assertTrue(_seenCommittee[proposer]); + assertTrue(_seenCommittee[proposerToAttester[proposer]]); } function testProposerForNonSetupEpoch(uint8 _epochsToJump) public setup(4) { @@ -129,14 +155,18 @@ contract SpartaTest is DecoderBase { address expectedProposer = rollup.getCurrentProposer(); // Add a validator which will also setup the epoch - rollup.addValidator(address(0xdead)); + testERC20.mint(address(this), TestConstants.AZTEC_MINIMUM_STAKE); + testERC20.approve(address(rollup), TestConstants.AZTEC_MINIMUM_STAKE); + rollup.deposit( + address(0xdead), address(0xdead), address(0xdead), TestConstants.AZTEC_MINIMUM_STAKE + ); address actualProposer = rollup.getCurrentProposer(); assertEq(expectedProposer, actualProposer, "Invalid proposer"); } function testValidatorSetLargerThanCommittee(bool _insufficientSigs) public setup(100) { - assertGt(rollup.getValidators().length, rollup.TARGET_COMMITTEE_SIZE(), "Not enough validators"); + assertGt(rollup.getAttesters().length, rollup.TARGET_COMMITTEE_SIZE(), "Not enough validators"); uint256 committeeSize = rollup.TARGET_COMMITTEE_SIZE() * 2 / 3 + (_insufficientSigs ? 0 : 1); _testBlock("mixed_block_1", _insufficientSigs, committeeSize, false); @@ -302,7 +332,7 @@ contract SpartaTest is DecoderBase { view returns (Signature memory) { - uint256 privateKey = privateKeys[_signer]; + uint256 privateKey = attesterPrivateKeys[_signer]; bytes32 digest = _digest.toEthSignedMessageHash(); (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest); diff --git a/l1-contracts/test/staking/StakingCheater.sol b/l1-contracts/test/staking/StakingCheater.sol index 224c732c6c9..ba89e1e07ab 100644 --- a/l1-contracts/test/staking/StakingCheater.sol +++ b/l1-contracts/test/staking/StakingCheater.sol @@ -14,14 +14,14 @@ contract StakingCheater is Staking { {} function cheat__SetStatus(address _attester, Status _status) external { - info[_attester].status = _status; + stakingStore.info[_attester].status = _status; } function cheat__AddAttester(address _attester) external { - attesters.add(_attester); + stakingStore.attesters.add(_attester); } function cheat__RemoveAttester(address _attester) external { - attesters.remove(_attester); + stakingStore.attesters.remove(_attester); } } diff --git a/l1-contracts/test/staking/base.t.sol b/l1-contracts/test/staking/base.t.sol index e47b6e8d24a..6aa8eaa8ca4 100644 --- a/l1-contracts/test/staking/base.t.sol +++ b/l1-contracts/test/staking/base.t.sol @@ -19,7 +19,7 @@ contract StakingBase is TestBase { address internal constant SLASHER = address(bytes20("SLASHER")); function setUp() public virtual { - stakingAsset = new TestERC20(); + stakingAsset = new TestERC20("test", "TEST", address(this)); staking = new StakingCheater(SLASHER, stakingAsset, MINIMUM_STAKE); } } diff --git a/spartan/aztec-network/files/config/config-prover-env.sh b/spartan/aztec-network/files/config/config-prover-env.sh index a3eccd01c1b..073547821d4 100644 --- a/spartan/aztec-network/files/config/config-prover-env.sh +++ b/spartan/aztec-network/files/config/config-prover-env.sh @@ -13,6 +13,7 @@ registry_address=$(echo "$output" | grep -oP 'Registry Address: \K0x[a-fA-F0-9]{ inbox_address=$(echo "$output" | grep -oP 'L1 -> L2 Inbox Address: \K0x[a-fA-F0-9]{40}') outbox_address=$(echo "$output" | grep -oP 'L2 -> L1 Outbox Address: \K0x[a-fA-F0-9]{40}') fee_juice_address=$(echo "$output" | grep -oP 'Fee Juice Address: \K0x[a-fA-F0-9]{40}') +staking_asset_address=$(echo "$output" | grep -oP 'Staking Asset Address: \K0x[a-fA-F0-9]{40}') fee_juice_portal_address=$(echo "$output" | grep -oP 'Fee Juice Portal Address: \K0x[a-fA-F0-9]{40}') coin_issuer_address=$(echo "$output" | grep -oP 'CoinIssuer Address: \K0x[a-fA-F0-9]{40}') reward_distributor_address=$(echo "$output" | grep -oP 'RewardDistributor Address: \K0x[a-fA-F0-9]{40}') @@ -27,6 +28,7 @@ export REGISTRY_CONTRACT_ADDRESS=$registry_address export INBOX_CONTRACT_ADDRESS=$inbox_address export OUTBOX_CONTRACT_ADDRESS=$outbox_address export FEE_JUICE_CONTRACT_ADDRESS=$fee_juice_address +export STAKING_ASSET_CONTRACT_ADDRESS=$staking_asset_address export FEE_JUICE_PORTAL_CONTRACT_ADDRESS=$fee_juice_portal_address export COIN_ISSUER_CONTRACT_ADDRESS=$coin_issuer_address export REWARD_DISTRIBUTOR_CONTRACT_ADDRESS=$reward_distributor_address diff --git a/spartan/aztec-network/files/config/config-validator-env.sh b/spartan/aztec-network/files/config/config-validator-env.sh index 03718cfc36d..b2848f8e069 100644 --- a/spartan/aztec-network/files/config/config-validator-env.sh +++ b/spartan/aztec-network/files/config/config-validator-env.sh @@ -13,6 +13,7 @@ registry_address=$(echo "$output" | grep -oP 'Registry Address: \K0x[a-fA-F0-9]{ inbox_address=$(echo "$output" | grep -oP 'L1 -> L2 Inbox Address: \K0x[a-fA-F0-9]{40}') outbox_address=$(echo "$output" | grep -oP 'L2 -> L1 Outbox Address: \K0x[a-fA-F0-9]{40}') fee_juice_address=$(echo "$output" | grep -oP 'Fee Juice Address: \K0x[a-fA-F0-9]{40}') +staking_asset_address=$(echo "$output" | grep -oP 'Staking Asset Address: \K0x[a-fA-F0-9]{40}') fee_juice_portal_address=$(echo "$output" | grep -oP 'Fee Juice Portal Address: \K0x[a-fA-F0-9]{40}') coin_issuer_address=$(echo "$output" | grep -oP 'CoinIssuer Address: \K0x[a-fA-F0-9]{40}') reward_distributor_address=$(echo "$output" | grep -oP 'RewardDistributor Address: \K0x[a-fA-F0-9]{40}') @@ -32,6 +33,7 @@ export REGISTRY_CONTRACT_ADDRESS=$registry_address export INBOX_CONTRACT_ADDRESS=$inbox_address export OUTBOX_CONTRACT_ADDRESS=$outbox_address export FEE_JUICE_CONTRACT_ADDRESS=$fee_juice_address +export STAKING_ASSET_CONTRACT_ADDRESS=$staking_asset_address export FEE_JUICE_PORTAL_CONTRACT_ADDRESS=$fee_juice_portal_address export COIN_ISSUER_CONTRACT_ADDRESS=$coin_issuer_address export REWARD_DISTRIBUTOR_CONTRACT_ADDRESS=$reward_distributor_address diff --git a/spartan/aztec-network/files/config/deploy-l1-contracts.sh b/spartan/aztec-network/files/config/deploy-l1-contracts.sh index f84cc6bcb51..529bb412e62 100644 --- a/spartan/aztec-network/files/config/deploy-l1-contracts.sh +++ b/spartan/aztec-network/files/config/deploy-l1-contracts.sh @@ -21,6 +21,7 @@ registry_address=$(echo "$output" | grep -oP 'Registry Address: \K0x[a-fA-F0-9]{ inbox_address=$(echo "$output" | grep -oP 'L1 -> L2 Inbox Address: \K0x[a-fA-F0-9]{40}') outbox_address=$(echo "$output" | grep -oP 'L2 -> L1 Outbox Address: \K0x[a-fA-F0-9]{40}') fee_juice_address=$(echo "$output" | grep -oP 'Fee Juice Address: \K0x[a-fA-F0-9]{40}') +staking_asset_address=$(echo "$output" | grep -oP 'Staking Asset Address: \K0x[a-fA-F0-9]{40}') fee_juice_portal_address=$(echo "$output" | grep -oP 'Fee Juice Portal Address: \K0x[a-fA-F0-9]{40}') coin_issuer_address=$(echo "$output" | grep -oP 'CoinIssuer Address: \K0x[a-fA-F0-9]{40}') reward_distributor_address=$(echo "$output" | grep -oP 'RewardDistributor Address: \K0x[a-fA-F0-9]{40}') @@ -34,6 +35,7 @@ export REGISTRY_CONTRACT_ADDRESS=$registry_address export INBOX_CONTRACT_ADDRESS=$inbox_address export OUTBOX_CONTRACT_ADDRESS=$outbox_address export FEE_JUICE_CONTRACT_ADDRESS=$fee_juice_address +export STAKING_ASSET_CONTRACT_ADDRESS=$staking_asset_address export FEE_JUICE_PORTAL_CONTRACT_ADDRESS=$fee_juice_portal_address export COIN_ISSUER_CONTRACT_ADDRESS=$coin_issuer_address export REWARD_DISTRIBUTOR_CONTRACT_ADDRESS=$reward_distributor_address diff --git a/spartan/aztec-network/templates/boot-node.yaml b/spartan/aztec-network/templates/boot-node.yaml index cd8fe41aa68..f638d329e64 100644 --- a/spartan/aztec-network/templates/boot-node.yaml +++ b/spartan/aztec-network/templates/boot-node.yaml @@ -210,6 +210,7 @@ data: export INBOX_CONTRACT_ADDRESS={{ .Values.bootNode.contracts.inboxAddress }} export OUTBOX_CONTRACT_ADDRESS={{ .Values.bootNode.contracts.outboxAddress }} export FEE_JUICE_CONTRACT_ADDRESS={{ .Values.bootNode.contracts.feeJuiceAddress }} + export STAKING_ASSET_CONTRACT_ADDRESS={{ .Values.bootNode.contracts.stakingAssetAddress }} export FEE_JUICE_PORTAL_CONTRACT_ADDRESS={{ .Values.bootNode.contracts.feeJuicePortalAddress }} {{- end }} {{if not .Values.network.public }} diff --git a/yarn-project/aztec-faucet/terraform/variables.tf b/yarn-project/aztec-faucet/terraform/variables.tf index f1d2fbf5c86..992719e667d 100644 --- a/yarn-project/aztec-faucet/terraform/variables.tf +++ b/yarn-project/aztec-faucet/terraform/variables.tf @@ -35,6 +35,10 @@ variable "FEE_JUICE_CONTRACT_ADDRESS" { type = string } +variable "STAKING_ASSET_CONTRACT_ADDRESS" { + type = string +} + variable "DEV_COIN_CONTRACT_ADDRESS" { type = string } diff --git a/yarn-project/aztec.js/src/contract/contract.test.ts b/yarn-project/aztec.js/src/contract/contract.test.ts index 4e856779ad4..66a54e8cfb5 100644 --- a/yarn-project/aztec.js/src/contract/contract.test.ts +++ b/yarn-project/aztec.js/src/contract/contract.test.ts @@ -41,6 +41,7 @@ describe('Contract Class', () => { inboxAddress: EthAddress.random(), outboxAddress: EthAddress.random(), feeJuiceAddress: EthAddress.random(), + stakingAssetAddress: EthAddress.random(), feeJuicePortalAddress: EthAddress.random(), governanceAddress: EthAddress.random(), coinIssuerAddress: EthAddress.random(), diff --git a/yarn-project/aztec/src/cli/aztec_start_options.ts b/yarn-project/aztec/src/cli/aztec_start_options.ts index 90b0a970092..6e3b05512bc 100644 --- a/yarn-project/aztec/src/cli/aztec_start_options.ts +++ b/yarn-project/aztec/src/cli/aztec_start_options.ts @@ -143,6 +143,12 @@ export const aztecStartOptions: { [key: string]: AztecStartOption[] } = { defaultValue: undefined, envVar: 'FEE_JUICE_CONTRACT_ADDRESS', }, + { + flag: '--staking-asset-address ', + description: 'The deployed L1 Staking Asset contract address', + defaultValue: undefined, + envVar: 'STAKING_ASSET_CONTRACT_ADDRESS', + }, { flag: '--fee-juice-portal-address ', description: 'The deployed L1 Fee Juice portal contract address', diff --git a/yarn-project/aztec/terraform/node/main.tf b/yarn-project/aztec/terraform/node/main.tf index 4dbef1867cc..ebb85c4461d 100644 --- a/yarn-project/aztec/terraform/node/main.tf +++ b/yarn-project/aztec/terraform/node/main.tf @@ -320,6 +320,10 @@ resource "aws_ecs_task_definition" "aztec-node" { name = "FEE_JUICE_CONTRACT_ADDRESS" value = data.terraform_remote_state.l1_contracts.outputs.fee_juice_contract_address }, + { + name = "STAKING_ASSET_CONTRACT_ADDRESS" + value = data.terraform_remote_state.l1_contracts.outputs.staking_asset_contract_address + }, { name = "FEE_JUICE_PORTAL_CONTRACT_ADDRESS" value = data.terraform_remote_state.l1_contracts.outputs.FEE_JUICE_PORTAL_CONTRACT_ADDRESS diff --git a/yarn-project/aztec/terraform/prover-node/main.tf b/yarn-project/aztec/terraform/prover-node/main.tf index ef30e5fd7e2..0577e0fa77f 100644 --- a/yarn-project/aztec/terraform/prover-node/main.tf +++ b/yarn-project/aztec/terraform/prover-node/main.tf @@ -274,6 +274,7 @@ resource "aws_ecs_task_definition" "aztec-prover-node" { { name = "OUTBOX_CONTRACT_ADDRESS", value = data.terraform_remote_state.l1_contracts.outputs.outbox_contract_address }, { name = "REGISTRY_CONTRACT_ADDRESS", value = data.terraform_remote_state.l1_contracts.outputs.registry_contract_address }, { name = "FEE_JUICE_CONTRACT_ADDRESS", value = data.terraform_remote_state.l1_contracts.outputs.fee_juice_contract_address }, + { name = "STAKING_ASSET_CONTRACT_ADDRESS", value = data.terraform_remote_state.l1_contracts.outputs.staking_asset_contract_address }, { name = "FEE_JUICE_PORTAL_CONTRACT_ADDRESS", value = data.terraform_remote_state.l1_contracts.outputs.FEE_JUICE_PORTAL_CONTRACT_ADDRESS }, // P2P (disabled) diff --git a/yarn-project/cli/src/cmds/infrastructure/sequencers.ts b/yarn-project/cli/src/cmds/infrastructure/sequencers.ts index 341744f0a6c..3e82bcaacc2 100644 --- a/yarn-project/cli/src/cmds/infrastructure/sequencers.ts +++ b/yarn-project/cli/src/cmds/infrastructure/sequencers.ts @@ -1,7 +1,7 @@ import { createCompatibleClient } from '@aztec/aztec.js'; -import { createEthereumChain } from '@aztec/ethereum'; +import { MINIMUM_STAKE, createEthereumChain } from '@aztec/ethereum'; import { type DebugLogger, type LogFn } from '@aztec/foundation/log'; -import { RollupAbi } from '@aztec/l1-artifacts'; +import { RollupAbi, TestERC20Abi } from '@aztec/l1-artifacts'; import { createPublicClient, createWalletClient, getContract, http } from 'viem'; import { mnemonicToAccount } from 'viem/accounts'; @@ -49,7 +49,7 @@ export async function sequencers(opts: { const who = (maybeWho as `0x{string}`) ?? walletClient?.account.address.toString(); if (command === 'list') { - const sequencers = await rollup.read.getValidators(); + const sequencers = await rollup.read.getAttesters(); if (sequencers.length === 0) { log(`No sequencers registered on rollup`); } else { @@ -59,11 +59,26 @@ export async function sequencers(opts: { } } } else if (command === 'add') { - if (!who || !writeableRollup) { + if (!who || !writeableRollup || !walletClient) { throw new Error(`Missing sequencer address`); } + log(`Adding ${who} as sequencer`); - const hash = await writeableRollup.write.addValidator([who]); + + const stakingAsset = getContract({ + address: await rollup.read.STAKING_ASSET(), + abi: TestERC20Abi, + client: walletClient, + }); + + await Promise.all( + [ + await stakingAsset.write.mint([walletClient.account.address, MINIMUM_STAKE], {} as any), + await stakingAsset.write.approve([rollup.address, MINIMUM_STAKE], {} as any), + ].map(txHash => publicClient.waitForTransactionReceipt({ hash: txHash })), + ); + + const hash = await writeableRollup.write.deposit([who, who, who, MINIMUM_STAKE]); await publicClient.waitForTransactionReceipt({ hash }); log(`Added in tx ${hash}`); } else if (command === 'remove') { @@ -71,7 +86,7 @@ export async function sequencers(opts: { throw new Error(`Missing sequencer address`); } log(`Removing ${who} as sequencer`); - const hash = await writeableRollup.write.removeValidator([who]); + const hash = await writeableRollup.write.initiateWithdraw([who, who]); await publicClient.waitForTransactionReceipt({ hash }); log(`Removed in tx ${hash}`); } else if (command === 'who-next') { diff --git a/yarn-project/cli/src/cmds/l1/deploy_l1_contracts.ts b/yarn-project/cli/src/cmds/l1/deploy_l1_contracts.ts index 2d7165ff442..4052658acc9 100644 --- a/yarn-project/cli/src/cmds/l1/deploy_l1_contracts.ts +++ b/yarn-project/cli/src/cmds/l1/deploy_l1_contracts.ts @@ -42,6 +42,7 @@ export async function deployL1Contracts( log(`L1 -> L2 Inbox Address: ${l1ContractAddresses.inboxAddress.toString()}`); log(`L2 -> L1 Outbox Address: ${l1ContractAddresses.outboxAddress.toString()}`); log(`Fee Juice Address: ${l1ContractAddresses.feeJuiceAddress.toString()}`); + log(`Staking Asset Address: ${l1ContractAddresses.stakingAssetAddress.toString()}`); log(`Fee Juice Portal Address: ${l1ContractAddresses.feeJuicePortalAddress.toString()}`); log(`CoinIssuer Address: ${l1ContractAddresses.coinIssuerAddress.toString()}`); log(`RewardDistributor Address: ${l1ContractAddresses.rewardDistributorAddress.toString()}`); diff --git a/yarn-project/cli/src/cmds/l1/update_l1_validators.ts b/yarn-project/cli/src/cmds/l1/update_l1_validators.ts index 4f3d33eb574..916d0c351fe 100644 --- a/yarn-project/cli/src/cmds/l1/update_l1_validators.ts +++ b/yarn-project/cli/src/cmds/l1/update_l1_validators.ts @@ -1,8 +1,8 @@ import { EthCheatCodes } from '@aztec/aztec.js'; import { type EthAddress } from '@aztec/circuits.js'; -import { createEthereumChain, getL1ContractsConfigEnvVars, isAnvilTestChain } from '@aztec/ethereum'; +import { MINIMUM_STAKE, createEthereumChain, getL1ContractsConfigEnvVars, isAnvilTestChain } from '@aztec/ethereum'; import { type DebugLogger, type LogFn } from '@aztec/foundation/log'; -import { RollupAbi } from '@aztec/l1-artifacts'; +import { RollupAbi, TestERC20Abi } from '@aztec/l1-artifacts'; import { createPublicClient, createWalletClient, getContract, http } from 'viem'; import { generatePrivateKey, mnemonicToAccount, privateKeyToAccount } from 'viem/accounts'; @@ -49,8 +49,26 @@ export async function addL1Validator({ client: walletClient, }); + const stakingAsset = getContract({ + address: await rollup.read.STAKING_ASSET(), + abi: TestERC20Abi, + client: walletClient, + }); + + await Promise.all( + [ + await stakingAsset.write.mint([walletClient.account.address, MINIMUM_STAKE], {} as any), + await stakingAsset.write.approve([rollupAddress.toString(), MINIMUM_STAKE], {} as any), + ].map(txHash => publicClient.waitForTransactionReceipt({ hash: txHash })), + ); + dualLog(`Adding validator ${validatorAddress.toString()} to rollup ${rollupAddress.toString()}`); - const txHash = await rollup.write.addValidator([validatorAddress.toString()]); + const txHash = await rollup.write.deposit([ + validatorAddress.toString(), + validatorAddress.toString(), + validatorAddress.toString(), + MINIMUM_STAKE, + ]); dualLog(`Transaction hash: ${txHash}`); await publicClient.waitForTransactionReceipt({ hash: txHash }); if (isAnvilTestChain(chainId)) { @@ -87,7 +105,7 @@ export async function removeL1Validator({ }); dualLog(`Removing validator ${validatorAddress.toString()} from rollup ${rollupAddress.toString()}`); - const txHash = await rollup.write.removeValidator([validatorAddress.toString()]); + const txHash = await rollup.write.initiateWithdraw([validatorAddress.toString(), validatorAddress.toString()]); dualLog(`Transaction hash: ${txHash}`); await publicClient.waitForTransactionReceipt({ hash: txHash }); } @@ -163,7 +181,7 @@ export async function debugRollup({ rpcUrl, chainId, rollupAddress, log }: Rollu log(`Pending block num: ${pendingNum}`); const provenNum = await rollup.read.getProvenBlockNumber(); log(`Proven block num: ${provenNum}`); - const validators = await rollup.read.getValidators(); + const validators = await rollup.read.getAttesters(); log(`Validators: ${validators.map(v => v.toString()).join(', ')}`); const committee = await rollup.read.getCurrentEpochCommittee(); log(`Committee: ${committee.map(v => v.toString()).join(', ')}`); diff --git a/yarn-project/cli/src/cmds/pxe/get_node_info.ts b/yarn-project/cli/src/cmds/pxe/get_node_info.ts index bbef7fde3e8..ea13af3a6dd 100644 --- a/yarn-project/cli/src/cmds/pxe/get_node_info.ts +++ b/yarn-project/cli/src/cmds/pxe/get_node_info.ts @@ -19,6 +19,7 @@ export async function getNodeInfo(rpcUrl: string, pxeRequest: boolean, debugLogg log(` L1 -> L2 Inbox Address: ${info.l1ContractAddresses.inboxAddress.toString()}`); log(` L2 -> L1 Outbox Address: ${info.l1ContractAddresses.outboxAddress.toString()}`); log(` Fee Juice Address: ${info.l1ContractAddresses.feeJuiceAddress.toString()}`); + log(` Staking Asset Address: ${info.l1ContractAddresses.stakingAssetAddress.toString()}`); log(` Fee Juice Portal Address: ${info.l1ContractAddresses.feeJuicePortalAddress.toString()}`); log(` CoinIssuer Address: ${info.l1ContractAddresses.coinIssuerAddress.toString()}`); log(` RewardDistributor Address: ${info.l1ContractAddresses.rewardDistributorAddress.toString()}`); diff --git a/yarn-project/end-to-end/scripts/native-network/deploy-l1-contracts.sh b/yarn-project/end-to-end/scripts/native-network/deploy-l1-contracts.sh index 2d4677b1660..014a1b2b07d 100755 --- a/yarn-project/end-to-end/scripts/native-network/deploy-l1-contracts.sh +++ b/yarn-project/end-to-end/scripts/native-network/deploy-l1-contracts.sh @@ -54,6 +54,7 @@ REGISTRY_CONTRACT_ADDRESS=$(echo "$output" | grep -oP 'Registry Address: \K0x[a- INBOX_CONTRACT_ADDRESS=$(echo "$output" | grep -oP 'L1 -> L2 Inbox Address: \K0x[a-fA-F0-9]{40}') OUTBOX_CONTRACT_ADDRESS=$(echo "$output" | grep -oP 'L2 -> L1 Outbox Address: \K0x[a-fA-F0-9]{40}') FEE_JUICE_CONTRACT_ADDRESS=$(echo "$output" | grep -oP 'Fee Juice Address: \K0x[a-fA-F0-9]{40}') +STAKING_ASSET_CONTRACT_ADDRESS=$(echo "$output" | grep -oP 'Staking Asset Address: \K0x[a-fA-F0-9]{40}') FEE_JUICE_PORTAL_CONTRACT_ADDRESS=$(echo "$output" | grep -oP 'Fee Juice Portal Address: \K0x[a-fA-F0-9]{40}') COIN_ISSUER_CONTRACT_ADDRESS=$(echo "$output" | grep -oP 'CoinIssuer Address: \K0x[a-fA-F0-9]{40}') REWARD_DISTRIBUTOR_CONTRACT_ADDRESS=$(echo "$output" | grep -oP 'RewardDistributor Address: \K0x[a-fA-F0-9]{40}') @@ -67,6 +68,7 @@ export REGISTRY_CONTRACT_ADDRESS=$REGISTRY_CONTRACT_ADDRESS export INBOX_CONTRACT_ADDRESS=$INBOX_CONTRACT_ADDRESS export OUTBOX_CONTRACT_ADDRESS=$OUTBOX_CONTRACT_ADDRESS export FEE_JUICE_CONTRACT_ADDRESS=$FEE_JUICE_CONTRACT_ADDRESS +export STAKING_ASSET_CONTRACT_ADDRESS=$STAKING_ASSET_CONTRACT_ADDRESS export FEE_JUICE_PORTAL_CONTRACT_ADDRESS=$FEE_JUICE_PORTAL_CONTRACT_ADDRESS export COIN_ISSUER_CONTRACT_ADDRESS=$COIN_ISSUER_CONTRACT_ADDRESS export REWARD_DISTRIBUTOR_CONTRACT_ADDRESS=$REWARD_DISTRIBUTOR_CONTRACT_ADDRESS diff --git a/yarn-project/end-to-end/src/e2e_p2p/gossip_network.test.ts b/yarn-project/end-to-end/src/e2e_p2p/gossip_network.test.ts index 6b8401823a4..597a76e0ed7 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/gossip_network.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/gossip_network.test.ts @@ -42,6 +42,7 @@ describe('e2e_p2p_network', () => { }); await t.applyBaseSnapshots(); await t.setup(); + await t.removeInitialNode(); }); afterEach(async () => { 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 3289add932a..0fb208a4932 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 @@ -1,10 +1,9 @@ import { getSchnorrAccount } from '@aztec/accounts/schnorr'; import { type AztecNodeConfig, type AztecNodeService } from '@aztec/aztec-node'; import { type AccountWalletWithSecretKey, EthCheatCodes } from '@aztec/aztec.js'; -import { EthAddress } from '@aztec/circuits.js'; -import { getL1ContractsConfigEnvVars } from '@aztec/ethereum'; +import { MINIMUM_STAKE, getL1ContractsConfigEnvVars } from '@aztec/ethereum'; import { type DebugLogger, createDebugLogger } from '@aztec/foundation/log'; -import { RollupAbi } from '@aztec/l1-artifacts'; +import { RollupAbi, TestERC20Abi } from '@aztec/l1-artifacts'; import { SpamContract } from '@aztec/noir-contracts.js'; import { type BootstrapNode } from '@aztec/p2p'; import { createBootstrapNodeFromPrivateKey } from '@aztec/p2p/mocks'; @@ -14,9 +13,10 @@ import { getContract } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; import { - PRIVATE_KEYS_START_INDEX, + ATTESTER_PRIVATE_KEYS_START_INDEX, + PROPOSER_PRIVATE_KEYS_START_INDEX, createValidatorConfig, - generateNodePrivateKeys, + generatePrivateKeys, } from '../fixtures/setup_p2p_test.js'; import { type ISnapshotManager, @@ -39,8 +39,9 @@ export class P2PNetworkTest { public logger: DebugLogger; public ctx!: SubsystemsContext; - public nodePrivateKeys: `0x${string}`[] = []; - public nodePublicKeys: string[] = []; + public attesterPrivateKeys: `0x${string}`[] = []; + public attesterPublicKeys: string[] = []; + public proposerPrivateKeys: `0x${string}`[] = []; public peerIdPrivateKeys: string[] = []; public bootstrapNodeEnr: string = ''; @@ -54,7 +55,6 @@ export class P2PNetworkTest { public bootstrapNode: BootstrapNode, public bootNodePort: number, private numberOfNodes: number, - initialValidatorAddress: string, initialValidatorConfig: AztecNodeConfig, // If set enable metrics collection metricsPort?: number, @@ -63,18 +63,16 @@ export class P2PNetworkTest { // Set up the base account and node private keys for the initial network deployment this.baseAccount = privateKeyToAccount(`0x${getPrivateKeyFromIndex(0)!.toString('hex')}`); - this.nodePrivateKeys = generateNodePrivateKeys(PRIVATE_KEYS_START_INDEX, numberOfNodes); - this.nodePublicKeys = this.nodePrivateKeys.map(privateKey => privateKeyToAccount(privateKey).address); + this.proposerPrivateKeys = generatePrivateKeys(PROPOSER_PRIVATE_KEYS_START_INDEX, numberOfNodes); + this.attesterPrivateKeys = generatePrivateKeys(ATTESTER_PRIVATE_KEYS_START_INDEX, numberOfNodes); + this.attesterPublicKeys = this.attesterPrivateKeys.map(privateKey => privateKeyToAccount(privateKey).address); this.bootstrapNodeEnr = bootstrapNode.getENR().encodeTxt(); - const initialValidators = [EthAddress.fromString(initialValidatorAddress)]; - this.snapshotManager = createSnapshotManager(`e2e_p2p_network/${testName}`, process.env.E2E_DATA_PATH, { ...initialValidatorConfig, ethereumSlotDuration: l1ContractsConfig.ethereumSlotDuration, salt: 420, - initialValidators, metricsPort: metricsPort, }); } @@ -97,16 +95,8 @@ export class P2PNetworkTest { const bootstrapNodeEnr = bootstrapNode.getENR().encodeTxt(); const initialValidatorConfig = await createValidatorConfig({} as AztecNodeConfig, bootstrapNodeEnr); - const intiailValidatorAddress = privateKeyToAccount(initialValidatorConfig.publisherPrivateKey).address; - - return new P2PNetworkTest( - testName, - bootstrapNode, - port, - numberOfNodes, - intiailValidatorAddress, - initialValidatorConfig, - ); + + return new P2PNetworkTest(testName, bootstrapNode, port, numberOfNodes, initialValidatorConfig); } async applyBaseSnapshots() { @@ -119,25 +109,44 @@ export class P2PNetworkTest { this.logger.verbose(`Adding ${this.numberOfNodes} validators`); - const txHashes: `0x${string}`[] = []; - for (let i = 0; i < this.numberOfNodes; i++) { - const account = privateKeyToAccount(this.nodePrivateKeys[i]!); - this.logger.debug(`Adding ${account.address} as validator`); - const txHash = await rollup.write.addValidator([account.address]); - txHashes.push(txHash); - - this.logger.debug(`Adding ${account.address} as validator`); - } + const stakingAsset = getContract({ + address: deployL1ContractsValues.l1ContractAddresses.stakingAssetAddress.toString(), + abi: TestERC20Abi, + client: deployL1ContractsValues.walletClient, + }); - // Wait for all the transactions adding validators to be mined + const stakeNeeded = MINIMUM_STAKE * BigInt(this.numberOfNodes); await Promise.all( - txHashes.map(txHash => - deployL1ContractsValues.publicClient.waitForTransactionReceipt({ - hash: txHash, - }), - ), + [ + await stakingAsset.write.mint([deployL1ContractsValues.walletClient.account.address, stakeNeeded], {} as any), + await stakingAsset.write.approve( + [deployL1ContractsValues.l1ContractAddresses.rollupAddress.toString(), stakeNeeded], + {} as any, + ), + ].map(txHash => deployL1ContractsValues.publicClient.waitForTransactionReceipt({ hash: txHash })), ); + const validators = []; + + for (let i = 0; i < this.numberOfNodes; i++) { + const attester = privateKeyToAccount(this.attesterPrivateKeys[i]!); + const proposer = privateKeyToAccount(this.proposerPrivateKeys[i]!); + validators.push({ + attester: attester.address, + proposer: proposer.address, + withdrawer: attester.address, + amount: MINIMUM_STAKE, + } as const); + + this.logger.verbose( + `Adding (attester, proposer) pair: (${attester.address}, ${proposer.address}) as validator`, + ); + } + + await deployL1ContractsValues.publicClient.waitForTransactionReceipt({ + hash: await rollup.write.cheat__InitialiseValidatorSet([validators]), + }); + //@note Now we jump ahead to the next epoch such that the validator committee is picked // INTERVAL MINING: If we are using anvil interval mining this will NOT progress the time! // Which means that the validator set will still be empty! So anyone can propose. @@ -195,47 +204,9 @@ export class P2PNetworkTest { } async removeInitialNode() { - await this.snapshotManager.snapshot( - 'remove-inital-validator', - async ({ deployL1ContractsValues, aztecNodeConfig }) => { - const rollup = getContract({ - address: deployL1ContractsValues.l1ContractAddresses.rollupAddress.toString(), - abi: RollupAbi, - client: deployL1ContractsValues.walletClient, - }); - - // Remove the setup validator - const initialValidatorAddress = privateKeyToAccount(`0x${getPrivateKeyFromIndex(0)!.toString('hex')}`).address; - const txHash = await rollup.write.removeValidator([initialValidatorAddress]); - - await deployL1ContractsValues.publicClient.waitForTransactionReceipt({ - hash: txHash, - }); - - //@note Now we jump ahead to the next epoch such that the validator committee is picked - // INTERVAL MINING: If we are using anvil interval mining this will NOT progress the time! - // Which means that the validator set will still be empty! So anyone can propose. - const slotsInEpoch = await rollup.read.EPOCH_DURATION(); - const timestamp = await rollup.read.getTimestampForSlot([slotsInEpoch]); - const cheatCodes = new EthCheatCodes(aztecNodeConfig.l1RpcUrl); - try { - await cheatCodes.warp(Number(timestamp)); - } catch (err) { - this.logger.debug('Warp failed, time already satisfied'); - } - - // 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.ctx.aztecNode.stop(); - }, - ); + await this.snapshotManager.snapshot('remove-inital-validator', async () => { + await this.ctx.aztecNode.stop(); + }); } async setup() { diff --git a/yarn-project/end-to-end/src/e2e_p2p/reqresp.test.ts b/yarn-project/end-to-end/src/e2e_p2p/reqresp.test.ts index c7644b77f3d..d9be627e861 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/reqresp.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/reqresp.test.ts @@ -123,6 +123,11 @@ describe('e2e_p2p_reqresp_tx', () => { client: t.ctx.deployL1ContractsValues.publicClient, }); + const attesters = await rollupContract.read.getAttesters(); + const mappedProposers = await Promise.all( + attesters.map(async attester => await rollupContract.read.getProposerForAttester([attester])), + ); + const currentTime = await t.ctx.cheatCodes.eth.timestamp(); const slotDuration = await rollupContract.read.SLOT_DURATION(); @@ -133,9 +138,11 @@ describe('e2e_p2p_reqresp_tx', () => { const proposer = await rollupContract.read.getProposerAt([nextSlot]); proposers.push(proposer); } - // Get the indexes of the nodes that are responsible for the next two slots - const proposerIndexes = proposers.map(proposer => t.nodePublicKeys.indexOf(proposer)); + const proposerIndexes = proposers.map(proposer => mappedProposers.indexOf(proposer as `0x${string}`)); + + t.logger.info('proposerIndexes: ' + proposerIndexes.join(', ')); + const nodesToTurnOffTxGossip = Array.from({ length: NUM_NODES }, (_, i) => i).filter( i => !proposerIndexes.includes(i), ); diff --git a/yarn-project/end-to-end/src/fixtures/setup_p2p_test.ts b/yarn-project/end-to-end/src/fixtures/setup_p2p_test.ts index f8d4fdacaaa..53c7a3dde62 100644 --- a/yarn-project/end-to-end/src/fixtures/setup_p2p_test.ts +++ b/yarn-project/end-to-end/src/fixtures/setup_p2p_test.ts @@ -13,7 +13,8 @@ import { getEndToEndTestTelemetryClient } from './with_telemetry_utils.js'; // Setup snapshots will create a node with index 0, so all of our loops here // need to start from 1 to avoid running validators with the same key -export const PRIVATE_KEYS_START_INDEX = 1; +export const PROPOSER_PRIVATE_KEYS_START_INDEX = 1; +export const ATTESTER_PRIVATE_KEYS_START_INDEX = 1001; export interface NodeContext { node: AztecNodeService; @@ -22,13 +23,13 @@ export interface NodeContext { account: AztecAddress; } -export function generateNodePrivateKeys(startIndex: number, numberOfNodes: number): `0x${string}`[] { - const nodePrivateKeys: `0x${string}`[] = []; +export function generatePrivateKeys(startIndex: number, numberOfKeys: number): `0x${string}`[] { + const privateKeys: `0x${string}`[] = []; // Do not start from 0 as it is used during setup - for (let i = startIndex; i < startIndex + numberOfNodes; i++) { - nodePrivateKeys.push(`0x${getPrivateKeyFromIndex(i)!.toString('hex')}`); + for (let i = startIndex; i < startIndex + numberOfKeys; i++) { + privateKeys.push(`0x${getPrivateKeyFromIndex(i)!.toString('hex')}`); } - return nodePrivateKeys; + return privateKeys; } export function createNodes( @@ -45,7 +46,7 @@ export function createNodes( const port = bootNodePort + i + 1; const dataDir = dataDirectory ? `${dataDirectory}-${i}` : undefined; - const nodePromise = createNode(config, port, bootstrapNodeEnr, i + PRIVATE_KEYS_START_INDEX, dataDir, metricsPort); + const nodePromise = createNode(config, port, bootstrapNodeEnr, i, dataDir, metricsPort); nodePromises.push(nodePromise); } return Promise.all(nodePromises); @@ -56,17 +57,11 @@ export async function createNode( config: AztecNodeConfig, tcpPort: number, bootstrapNode: string | undefined, - publisherAddressIndex: number, + accountIndex: number, dataDirectory?: string, metricsPort?: number, ) { - const validatorConfig = await createValidatorConfig( - config, - bootstrapNode, - tcpPort, - publisherAddressIndex, - dataDirectory, - ); + const validatorConfig = await createValidatorConfig(config, bootstrapNode, tcpPort, accountIndex, dataDirectory); const telemetryClient = await getEndToEndTestTelemetryClient(metricsPort, /*serviceName*/ `node:${tcpPort}`); @@ -85,11 +80,15 @@ export async function createValidatorConfig( ) { port = port ?? (await getPort()); - const privateKey = getPrivateKeyFromIndex(accountIndex); - const privateKeyHex: `0x${string}` = `0x${privateKey!.toString('hex')}`; + const attesterPrivateKey: `0x${string}` = `0x${getPrivateKeyFromIndex( + ATTESTER_PRIVATE_KEYS_START_INDEX + accountIndex, + )!.toString('hex')}`; + const proposerPrivateKey: `0x${string}` = `0x${getPrivateKeyFromIndex( + PROPOSER_PRIVATE_KEYS_START_INDEX + accountIndex, + )!.toString('hex')}`; - config.publisherPrivateKey = privateKeyHex; - config.validatorPrivateKey = privateKeyHex; + config.validatorPrivateKey = attesterPrivateKey; + config.publisherPrivateKey = proposerPrivateKey; const nodeConfig: AztecNodeConfig = { ...config, diff --git a/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts b/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts index 488e7291bda..8aa2a934b33 100644 --- a/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts +++ b/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts @@ -296,9 +296,9 @@ async function setupFromFresh( const deployL1ContractsValues = await setupL1Contracts(aztecNodeConfig.l1RpcUrl, hdAccount, logger, { salt: opts.salt, - initialValidators: opts.initialValidators, ...deployL1ContractsArgs, ...getL1ContractsConfigEnvVars(), + initialValidators: opts.initialValidators, }); aztecNodeConfig.l1Contracts = deployL1ContractsValues.l1ContractAddresses; aztecNodeConfig.l1PublishRetryIntervalMS = 100; @@ -317,7 +317,7 @@ async function setupFromFresh( const feeJuice = getContract({ address: deployL1ContractsValues.l1ContractAddresses.feeJuiceAddress.toString(), - abi: l1Artifacts.feeJuice.contractAbi, + abi: l1Artifacts.feeAsset.contractAbi, client: deployL1ContractsValues.walletClient, }); diff --git a/yarn-project/end-to-end/src/fixtures/utils.ts b/yarn-project/end-to-end/src/fixtures/utils.ts index db1825bb89c..482dfc15775 100644 --- a/yarn-project/end-to-end/src/fixtures/utils.ts +++ b/yarn-project/end-to-end/src/fixtures/utils.ts @@ -377,7 +377,7 @@ export async function setup( const feeJuice = getContract({ address: deployL1ContractsValues.l1ContractAddresses.feeJuiceAddress.toString(), - abi: l1Artifacts.feeJuice.contractAbi, + abi: l1Artifacts.feeAsset.contractAbi, client: deployL1ContractsValues.walletClient, }); diff --git a/yarn-project/end-to-end/src/shared/cross_chain_test_harness.ts b/yarn-project/end-to-end/src/shared/cross_chain_test_harness.ts index d68750db230..a72729961bf 100644 --- a/yarn-project/end-to-end/src/shared/cross_chain_test_harness.ts +++ b/yarn-project/end-to-end/src/shared/cross_chain_test_harness.ts @@ -76,9 +76,11 @@ export async function deployAndInitializeTokenAndBridgeContracts( underlyingERC20: any; }> { if (!underlyingERC20Address) { - underlyingERC20Address = await deployL1Contract(walletClient, publicClient, TestERC20Abi, TestERC20Bytecode).then( - ({ address }) => address, - ); + underlyingERC20Address = await deployL1Contract(walletClient, publicClient, TestERC20Abi, TestERC20Bytecode, [ + 'Underlying', + 'UND', + walletClient.account.address, + ]).then(({ address }) => address); } const underlyingERC20 = getContract({ address: underlyingERC20Address!.toString(), @@ -86,6 +88,9 @@ export async function deployAndInitializeTokenAndBridgeContracts( client: walletClient, }); + // allow anyone to mint + await underlyingERC20.write.setFreeForAll([true], {} as any); + // deploy the token portal const { address: tokenPortalAddress } = await deployL1Contract( walletClient, diff --git a/yarn-project/end-to-end/src/spartan/gating-passive.test.ts b/yarn-project/end-to-end/src/spartan/gating-passive.test.ts index 6369f912a7a..2285dac373d 100644 --- a/yarn-project/end-to-end/src/spartan/gating-passive.test.ts +++ b/yarn-project/end-to-end/src/spartan/gating-passive.test.ts @@ -41,7 +41,7 @@ const { SPARTAN_DIR, INSTANCE_NAME, } = config; -const debugLogger = createDebugLogger('aztec:spartan-test:reorg'); +const debugLogger = createDebugLogger('aztec:spartan-test:gating-passive'); describe('a test that passively observes the network in the presence of network chaos', () => { jest.setTimeout(60 * 60 * 1000); // 60 minutes @@ -126,14 +126,16 @@ describe('a test that passively observes the network in the presence of network await sleep(Number(epochDuration * slotDuration) * 1000); const newTips = await rollupCheatCodes.getTips(); - const expectedPending = - controlTips.pending + BigInt(Math.floor((1 - MAX_MISSED_SLOT_PERCENT) * Number(epochDuration))); - expect(newTips.pending).toBeGreaterThan(expectedPending); // calculate the percentage of slots missed const perfectPending = controlTips.pending + BigInt(Math.floor(Number(epochDuration))); const missedSlots = Number(perfectPending) - Number(newTips.pending); const missedSlotsPercentage = (missedSlots / Number(epochDuration)) * 100; debugLogger.info(`Missed ${missedSlots} slots, ${missedSlotsPercentage.toFixed(2)}%`); + + // Ensure we missed at most the max allowed slots + // This is in place to ensure that we don't have a bad regression in the network + const maxMissedSlots = Math.floor(Number(epochDuration) * MAX_MISSED_SLOT_PERCENT); + expect(missedSlots).toBeLessThanOrEqual(maxMissedSlots); } }); }); diff --git a/yarn-project/ethereum/src/constants.ts b/yarn-project/ethereum/src/constants.ts index c1f4b34d732..2fea0175aca 100644 --- a/yarn-project/ethereum/src/constants.ts +++ b/yarn-project/ethereum/src/constants.ts @@ -2,3 +2,4 @@ import { type Hex } from 'viem'; export const NULL_KEY: Hex = `0x${'0000000000000000000000000000000000000000000000000000000000000000'}`; export const AZTEC_TEST_CHAIN_ID = 677692; +export const MINIMUM_STAKE = BigInt(100e18); diff --git a/yarn-project/ethereum/src/deploy_l1_contracts.ts b/yarn-project/ethereum/src/deploy_l1_contracts.ts index 32832708c2a..52a7aed1907 100644 --- a/yarn-project/ethereum/src/deploy_l1_contracts.ts +++ b/yarn-project/ethereum/src/deploy_l1_contracts.ts @@ -53,6 +53,7 @@ import { type HDAccount, type PrivateKeyAccount, mnemonicToAccount, privateKeyTo import { foundry } from 'viem/chains'; import { type L1ContractsConfig } from './config.js'; +import { MINIMUM_STAKE } from './constants.js'; import { isAnvilTestChain } from './ethereum_chain.js'; import { type L1ContractAddresses } from './l1_contract_addresses.js'; import { L1TxUtils } from './l1_tx_utils.js'; @@ -127,10 +128,14 @@ export interface L1ContractArtifactsForDeployment { * Rollup contract artifacts */ rollup: ContractArtifacts; + /** + * The token to stake. + */ + stakingAsset: ContractArtifacts; /** * The token to pay for gas. This will be bridged to L2 via the feeJuicePortal below */ - feeJuice: ContractArtifacts; + feeAsset: ContractArtifacts; /** * Fee juice portal contract artifacts. Optional for now as gas is not strictly enforced */ @@ -183,7 +188,11 @@ export const l1Artifacts: L1ContractArtifactsForDeployment = { }, }, }, - feeJuice: { + stakingAsset: { + contractAbi: TestERC20Abi, + contractBytecode: TestERC20Bytecode, + }, + feeAsset: { contractAbi: TestERC20Abi, contractBytecode: TestERC20Bytecode, }, @@ -307,8 +316,19 @@ export const deployL1Contracts = async ( const registryAddress = await govDeployer.deploy(l1Artifacts.registry, [account.address.toString()]); logger.info(`Deployed Registry at ${registryAddress}`); - const feeJuiceAddress = await govDeployer.deploy(l1Artifacts.feeJuice); - logger.info(`Deployed Fee Juice at ${feeJuiceAddress}`); + const feeAssetAddress = await govDeployer.deploy(l1Artifacts.feeAsset, [ + 'FeeJuice', + 'FEE', + account.address.toString(), + ]); + logger.info(`Deployed Fee Juice at ${feeAssetAddress}`); + + const stakingAssetAddress = await govDeployer.deploy(l1Artifacts.stakingAsset, [ + 'Staking', + 'STK', + account.address.toString(), + ]); + logger.info(`Deployed Staking Asset at ${stakingAssetAddress}`); // @todo #8084 // @note These numbers are just chosen to make testing simple. @@ -321,21 +341,23 @@ export const deployL1Contracts = async ( ]); logger.info(`Deployed GovernanceProposer at ${governanceProposerAddress}`); + // @note @LHerskind the assets are expected to be the same at some point, but for better + // configurability they are different for now. const governanceAddress = await govDeployer.deploy(l1Artifacts.governance, [ - feeJuiceAddress.toString(), + feeAssetAddress.toString(), governanceProposerAddress.toString(), ]); logger.info(`Deployed Governance at ${governanceAddress}`); const coinIssuerAddress = await govDeployer.deploy(l1Artifacts.coinIssuer, [ - feeJuiceAddress.toString(), + feeAssetAddress.toString(), 1n * 10n ** 18n, // @todo #8084 governanceAddress.toString(), ]); logger.info(`Deployed CoinIssuer at ${coinIssuerAddress}`); const rewardDistributorAddress = await govDeployer.deploy(l1Artifacts.rewardDistributor, [ - feeJuiceAddress.toString(), + feeAssetAddress.toString(), registryAddress.toString(), governanceAddress.toString(), ]); @@ -348,27 +370,29 @@ export const deployL1Contracts = async ( const feeJuicePortalAddress = await deployer.deploy(l1Artifacts.feeJuicePortal, [ registryAddress.toString(), - feeJuiceAddress.toString(), + feeAssetAddress.toString(), args.l2FeeJuiceAddress.toString(), ]); logger.info(`Deployed Fee Juice Portal at ${feeJuicePortalAddress}`); - const rollupArgs = { + const rollupConfigArgs = { aztecSlotDuration: args.aztecSlotDuration, aztecEpochDuration: args.aztecEpochDuration, targetCommitteeSize: args.aztecTargetCommitteeSize, aztecEpochProofClaimWindowInL2Slots: args.aztecEpochProofClaimWindowInL2Slots, + minimumStake: MINIMUM_STAKE, }; - const rollupAddress = await deployer.deploy(l1Artifacts.rollup, [ + const rollupArgs = [ feeJuicePortalAddress.toString(), rewardDistributorAddress.toString(), + stakingAssetAddress.toString(), args.vkTreeRoot.toString(), args.protocolContractTreeRoot.toString(), account.address.toString(), - args.initialValidators?.map(v => v.toString()) ?? [], - rollupArgs, - ]); - logger.info(`Deployed Rollup at ${rollupAddress}`, rollupArgs); + rollupConfigArgs, + ]; + const rollupAddress = await deployer.deploy(l1Artifacts.rollup, rollupArgs); + logger.info(`Deployed Rollup at ${rollupAddress}`, rollupConfigArgs); await deployer.waitForDeployments(); logger.info(`All core contracts deployed`); @@ -379,9 +403,15 @@ export const deployL1Contracts = async ( client: walletClient, }); - const feeJuice = getContract({ - address: feeJuiceAddress.toString(), - abi: l1Artifacts.feeJuice.contractAbi, + const feeAsset = getContract({ + address: feeAssetAddress.toString(), + abi: l1Artifacts.feeAsset.contractAbi, + client: walletClient, + }); + + const stakingAsset = getContract({ + address: stakingAssetAddress.toString(), + abi: l1Artifacts.stakingAsset.contractAbi, client: walletClient, }); @@ -394,12 +424,40 @@ export const deployL1Contracts = async ( // Transaction hashes to await const txHashes: Hex[] = []; + { + const txHash = await feeAsset.write.setFreeForAll([true], {} as any); + logger.info(`Fee asset set to free for all in ${txHash}`); + txHashes.push(txHash); + } + + if (args.initialValidators && args.initialValidators.length > 0) { + // Mint tokens, approve them, use cheat code to initialise validator set without setting up the epoch. + const stakeNeeded = MINIMUM_STAKE * BigInt(args.initialValidators.length); + await Promise.all( + [ + await stakingAsset.write.mint([walletClient.account.address, stakeNeeded], {} as any), + await stakingAsset.write.approve([rollupAddress.toString(), stakeNeeded], {} as any), + ].map(txHash => publicClient.waitForTransactionReceipt({ hash: txHash })), + ); + + const initiateValidatorSetTxHash = await rollup.write.cheat__InitialiseValidatorSet([ + args.initialValidators.map(v => ({ + attester: v.toString(), + proposer: v.toString(), + withdrawer: v.toString(), + amount: MINIMUM_STAKE, + })), + ]); + txHashes.push(initiateValidatorSetTxHash); + logger.info(`Initialized validator set (${args.initialValidators.join(', ')}) in tx ${initiateValidatorSetTxHash}`); + } + // @note This value MUST match what is in `constants.nr`. It is currently specified here instead of just importing // because there is circular dependency hell. This is a temporary solution. #3342 // @todo #8084 // fund the portal contract with Fee Juice - const FEE_JUICE_INITIAL_MINT = 200000000000000000000; - const mintTxHash = await feeJuice.write.mint([feeJuicePortalAddress.toString(), FEE_JUICE_INITIAL_MINT], {} as any); + const FEE_JUICE_INITIAL_MINT = 200000000000000000000n; + const mintTxHash = await feeAsset.write.mint([feeJuicePortalAddress.toString(), FEE_JUICE_INITIAL_MINT], {} as any); // @note This is used to ensure we fully wait for the transaction when running against a real chain // otherwise we execute subsequent transactions too soon @@ -415,7 +473,7 @@ export const deployL1Contracts = async ( } logger.info( - `Initialized Fee Juice Portal at ${feeJuicePortalAddress} to bridge between L1 ${feeJuiceAddress} to L2 ${args.l2FeeJuiceAddress}`, + `Initialized Fee Juice Portal at ${feeJuicePortalAddress} to bridge between L1 ${feeAssetAddress} to L2 ${args.l2FeeJuiceAddress}`, ); if (isAnvilTestChain(chain.id)) { @@ -493,7 +551,8 @@ export const deployL1Contracts = async ( registryAddress, inboxAddress, outboxAddress, - feeJuiceAddress, + feeJuiceAddress: feeAssetAddress, + stakingAssetAddress, feeJuicePortalAddress, coinIssuerAddress, rewardDistributorAddress, diff --git a/yarn-project/ethereum/src/l1_contract_addresses.ts b/yarn-project/ethereum/src/l1_contract_addresses.ts index 1733e15fc06..eca35f4edea 100644 --- a/yarn-project/ethereum/src/l1_contract_addresses.ts +++ b/yarn-project/ethereum/src/l1_contract_addresses.ts @@ -20,6 +20,7 @@ export const L1ContractsNames = [ 'rewardDistributorAddress', 'governanceProposerAddress', 'governanceAddress', + 'stakingAssetAddress', ] as const; /** Provides the directory of current L1 contract addresses */ @@ -33,6 +34,7 @@ export const L1ContractAddressesSchema = z.object({ inboxAddress: schemas.EthAddress, outboxAddress: schemas.EthAddress, feeJuiceAddress: schemas.EthAddress, + stakingAssetAddress: schemas.EthAddress, feeJuicePortalAddress: schemas.EthAddress, coinIssuerAddress: schemas.EthAddress, rewardDistributorAddress: schemas.EthAddress, @@ -68,6 +70,11 @@ export const l1ContractAddressesMapping: ConfigMappingsType description: 'The deployed L1 Fee Juice contract address.', parseEnv, }, + stakingAssetAddress: { + env: 'STAKING_ASSET_CONTRACT_ADDRESS', + description: 'The deployed L1 staking asset contract address.', + parseEnv, + }, feeJuicePortalAddress: { env: 'FEE_JUICE_PORTAL_CONTRACT_ADDRESS', description: 'The deployed L1 Fee Juice portal contract address.', diff --git a/yarn-project/ethereum/src/test/tx_delayer.test.ts b/yarn-project/ethereum/src/test/tx_delayer.test.ts index f85bcd453cf..1fc1435e80c 100644 --- a/yarn-project/ethereum/src/test/tx_delayer.test.ts +++ b/yarn-project/ethereum/src/test/tx_delayer.test.ts @@ -72,7 +72,11 @@ describe('tx_delayer', () => { }, 20000); it('delays a tx sent through a contract', async () => { - const deployTxHash = await client.deployContract({ abi: TestERC20Abi, bytecode: TestERC20Bytecode, args: [] }); + const deployTxHash = await client.deployContract({ + abi: TestERC20Abi, + bytecode: TestERC20Bytecode, + args: ['test', 'TST', account.address], + }); const { contractAddress, blockNumber } = await client.waitForTransactionReceipt({ hash: deployTxHash, pollingInterval: 100, diff --git a/yarn-project/foundation/src/config/env_var.ts b/yarn-project/foundation/src/config/env_var.ts index 41a41143c91..b12e7c3ea78 100644 --- a/yarn-project/foundation/src/config/env_var.ts +++ b/yarn-project/foundation/src/config/env_var.ts @@ -145,6 +145,7 @@ export type EnvVar = | 'SEQ_REQUIRED_CONFIRMATIONS' | 'SEQ_TX_POLLING_INTERVAL_MS' | 'SEQ_ENFORCE_TIME_TABLE' + | 'STAKING_ASSET_CONTRACT_ADDRESS' | 'REWARD_DISTRIBUTOR_CONTRACT_ADDRESS' | 'TELEMETRY' | 'TEST_ACCOUNTS' diff --git a/yarn-project/pxe/src/pxe_service/test/pxe_service.test.ts b/yarn-project/pxe/src/pxe_service/test/pxe_service.test.ts index 4acbc87e2a4..5899e8af003 100644 --- a/yarn-project/pxe/src/pxe_service/test/pxe_service.test.ts +++ b/yarn-project/pxe/src/pxe_service/test/pxe_service.test.ts @@ -39,6 +39,7 @@ function createPXEService(): Promise { inboxAddress: EthAddress.random(), outboxAddress: EthAddress.random(), feeJuiceAddress: EthAddress.random(), + stakingAssetAddress: EthAddress.random(), feeJuicePortalAddress: EthAddress.random(), governanceAddress: EthAddress.random(), coinIssuerAddress: EthAddress.random(),