diff --git a/l1-contracts/src/core/Leonidas.sol b/l1-contracts/src/core/Leonidas.sol index 77244bec628..213b6da3925 100644 --- a/l1-contracts/src/core/Leonidas.sol +++ b/l1-contracts/src/core/Leonidas.sol @@ -42,13 +42,17 @@ contract Leonidas is Staking, TimeFns, ILeonidas { LeonidasStorage private leonidasStore; constructor( - address _ares, IERC20 _stakingAsset, uint256 _minimumStake, + uint256 _slashingQuorum, + uint256 _roundSize, uint256 _slotDuration, uint256 _epochDuration, uint256 _targetCommitteeSize - ) Staking(_ares, _stakingAsset, _minimumStake) TimeFns(_slotDuration, _epochDuration) { + ) + Staking(_stakingAsset, _minimumStake, _slashingQuorum, _roundSize) + TimeFns(_slotDuration, _epochDuration) + { GENESIS_TIME = Timestamp.wrap(block.timestamp); SLOT_DURATION = _slotDuration; EPOCH_DURATION = _epochDuration; diff --git a/l1-contracts/src/core/Rollup.sol b/l1-contracts/src/core/Rollup.sol index 2e6dedc15b2..531f58186eb 100644 --- a/l1-contracts/src/core/Rollup.sol +++ b/l1-contracts/src/core/Rollup.sol @@ -51,6 +51,8 @@ struct Config { uint256 targetCommitteeSize; uint256 aztecEpochProofClaimWindowInL2Slots; uint256 minimumStake; + uint256 slashingQuorum; + uint256 slashingRoundSize; } /** @@ -110,15 +112,15 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Ownable, Leonidas, IRollup, ITes ) Ownable(_ares) Leonidas( - _ares, _stakingAsset, _config.minimumStake, + _config.slashingQuorum, + _config.slashingRoundSize, _config.aztecSlotDuration, _config.aztecEpochDuration, _config.targetCommitteeSize ) { - rollupStore.epochProofVerifier = new MockVerifier(); FEE_JUICE_PORTAL = _fpcJuicePortal; REWARD_DISTRIBUTOR = _rewardDistributor; ASSET = _fpcJuicePortal.UNDERLYING(); @@ -127,14 +129,16 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Ownable, Leonidas, IRollup, ITes ); INBOX = IInbox(address(new Inbox(address(this), Constants.L1_TO_L2_MSG_SUBTREE_HEIGHT))); OUTBOX = IOutbox(address(new Outbox(address(this)))); - rollupStore.vkTreeRoot = _vkTreeRoot; - rollupStore.protocolContractTreeRoot = _protocolContractTreeRoot; VERSION = 1; L1_BLOCK_AT_GENESIS = block.number; CLAIM_DURATION_IN_L2_SLOTS = _config.aztecEpochProofClaimWindowInL2Slots; IS_FOUNDRY_TEST = VM_ADDRESS.code.length > 0; + rollupStore.epochProofVerifier = new MockVerifier(); + rollupStore.vkTreeRoot = _vkTreeRoot; + rollupStore.protocolContractTreeRoot = _protocolContractTreeRoot; + // Genesis block rollupStore.blocks[0] = BlockLog({ feeHeader: FeeHeader({ diff --git a/l1-contracts/src/core/interfaces/ISlasher.sol b/l1-contracts/src/core/interfaces/ISlasher.sol new file mode 100644 index 00000000000..6ad8c695719 --- /dev/null +++ b/l1-contracts/src/core/interfaces/ISlasher.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Aztec Labs. +pragma solidity >=0.8.27; + +import {IPayload} from "@aztec/governance/interfaces/IPayload.sol"; + +interface ISlasher { + function slash(IPayload _payload) external returns (bool); +} diff --git a/l1-contracts/src/core/staking/Slasher.sol b/l1-contracts/src/core/staking/Slasher.sol new file mode 100644 index 00000000000..39e44791ff2 --- /dev/null +++ b/l1-contracts/src/core/staking/Slasher.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Aztec Labs. +pragma solidity >=0.8.27; + +import {ISlasher} from "@aztec/core/interfaces/ISlasher.sol"; +import {SlashingProposer} from "@aztec/core/staking/SlashingProposer.sol"; +import {IPayload} from "@aztec/governance/interfaces/IPayload.sol"; + +contract Slasher is ISlasher { + SlashingProposer public immutable PROPOSER; + + event SlashFailed(address target, bytes data, bytes returnData); + + error Slasher__CallerNotProposer(address caller, address proposer); // 0x44c1f74f + + constructor(uint256 _n, uint256 _m) { + PROPOSER = new SlashingProposer(msg.sender, this, _n, _m); + } + + function slash(IPayload _payload) external override(ISlasher) returns (bool) { + require( + msg.sender == address(PROPOSER), Slasher__CallerNotProposer(msg.sender, address(PROPOSER)) + ); + + IPayload.Action[] memory actions = _payload.getActions(); + + for (uint256 i = 0; i < actions.length; i++) { + // Allow failure of individual calls but emit the failure! + (bool success, bytes memory returnData) = actions[i].target.call(actions[i].data); + if (!success) { + emit SlashFailed(actions[i].target, actions[i].data, returnData); + } + } + + return true; + } +} diff --git a/l1-contracts/src/core/staking/SlashingProposer.sol b/l1-contracts/src/core/staking/SlashingProposer.sol new file mode 100644 index 00000000000..dfd445af937 --- /dev/null +++ b/l1-contracts/src/core/staking/SlashingProposer.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Aztec Labs. +pragma solidity >=0.8.27; + +import {ISlasher} from "@aztec/core/interfaces/ISlasher.sol"; +import {IGovernanceProposer} from "@aztec/governance/interfaces/IGovernanceProposer.sol"; +import {IPayload} from "@aztec/governance/interfaces/IPayload.sol"; +import {EmpireBase} from "@aztec/governance/proposer/EmpireBase.sol"; + +/** + * @notice A SlashingProposer implementation following the empire model + */ +contract SlashingProposer is IGovernanceProposer, EmpireBase { + address public immutable INSTANCE; + ISlasher public immutable SLASHER; + + constructor(address _instance, ISlasher _slasher, uint256 _slashingQuorum, uint256 _roundSize) + EmpireBase(_slashingQuorum, _roundSize) + { + INSTANCE = _instance; + SLASHER = _slasher; + } + + function getExecutor() public view override(EmpireBase, IGovernanceProposer) returns (address) { + return address(SLASHER); + } + + function getInstance() public view override(EmpireBase, IGovernanceProposer) returns (address) { + return INSTANCE; + } + + function _execute(IPayload _proposal) internal override(EmpireBase) returns (bool) { + return SLASHER.slash(_proposal); + } +} diff --git a/l1-contracts/src/core/staking/Staking.sol b/l1-contracts/src/core/staking/Staking.sol index 0d75e74e1c1..5e928f64c56 100644 --- a/l1-contracts/src/core/staking/Staking.sol +++ b/l1-contracts/src/core/staking/Staking.sol @@ -12,6 +12,7 @@ import { } from "@aztec/core/interfaces/IStaking.sol"; import {Errors} from "@aztec/core/libraries/Errors.sol"; import {Timestamp} from "@aztec/core/libraries/TimeMath.sol"; +import {Slasher} from "@aztec/core/staking/Slasher.sol"; import {IERC20} from "@oz/token/ERC20/IERC20.sol"; import {SafeERC20} from "@oz/token/ERC20/utils/SafeERC20.sol"; import {EnumerableSet} from "@oz/utils/structs/EnumerableSet.sol"; @@ -23,14 +24,19 @@ contract Staking is IStaking { // Constant pulled out of the ass Timestamp public constant EXIT_DELAY = Timestamp.wrap(60 * 60 * 24); - address public immutable SLASHER; + Slasher public immutable SLASHER; IERC20 public immutable STAKING_ASSET; uint256 public immutable MINIMUM_STAKE; StakingStorage internal stakingStore; - constructor(address _slasher, IERC20 _stakingAsset, uint256 _minimumStake) { - SLASHER = _slasher; + constructor( + IERC20 _stakingAsset, + uint256 _minimumStake, + uint256 _slashingQuorum, + uint256 _roundSize + ) { + SLASHER = new Slasher(_slashingQuorum, _roundSize); STAKING_ASSET = _stakingAsset; MINIMUM_STAKE = _minimumStake; } @@ -57,7 +63,9 @@ contract Staking is IStaking { } function slash(address _attester, uint256 _amount) external override(IStaking) { - require(msg.sender == SLASHER, Errors.Staking__NotSlasher(SLASHER, msg.sender)); + require( + msg.sender == address(SLASHER), Errors.Staking__NotSlasher(address(SLASHER), msg.sender) + ); ValidatorInfo storage validator = stakingStore.info[_attester]; require(validator.status != Status.NONE, Errors.Staking__NoOneToSlash(_attester)); diff --git a/l1-contracts/src/governance/CoinIssuer.sol b/l1-contracts/src/governance/CoinIssuer.sol index 37ac8f18b4d..33a0c06df0e 100644 --- a/l1-contracts/src/governance/CoinIssuer.sol +++ b/l1-contracts/src/governance/CoinIssuer.sol @@ -33,7 +33,7 @@ contract CoinIssuer is ICoinIssuer, Ownable { */ function mint(address _to, uint256 _amount) external override(ICoinIssuer) onlyOwner { uint256 maxMint = mintAvailable(); - require(_amount <= maxMint, Errors.CoinIssuer__InssuficientMintAvailable(maxMint, _amount)); + require(_amount <= maxMint, Errors.CoinIssuer__InsufficientMintAvailable(maxMint, _amount)); timeOfLastMint = block.timestamp; ASSET.mint(_to, _amount); } diff --git a/l1-contracts/src/governance/interfaces/IGovernanceProposer.sol b/l1-contracts/src/governance/interfaces/IGovernanceProposer.sol index 7539446a1de..52ac72e8d6f 100644 --- a/l1-contracts/src/governance/interfaces/IGovernanceProposer.sol +++ b/l1-contracts/src/governance/interfaces/IGovernanceProposer.sol @@ -3,19 +3,19 @@ pragma solidity >=0.8.27; import {Slot} from "@aztec/core/libraries/TimeMath.sol"; -import {IGovernance} from "@aztec/governance/interfaces/IGovernance.sol"; import {IPayload} from "@aztec/governance/interfaces/IPayload.sol"; interface IGovernanceProposer { event VoteCast(IPayload indexed proposal, uint256 indexed round, address indexed voter); - event ProposalPushed(IPayload indexed proposal, uint256 indexed round); + event ProposalExecuted(IPayload indexed proposal, uint256 indexed round); - function vote(IPayload _proposa) external returns (bool); - function pushProposal(uint256 _roundNumber) external returns (bool); + function vote(IPayload _proposal) external returns (bool); + function executeProposal(uint256 _roundNumber) external returns (bool); function yeaCount(address _instance, uint256 _round, IPayload _proposal) external view returns (uint256); function computeRound(Slot _slot) external view returns (uint256); - function getGovernance() external view returns (IGovernance); + function getInstance() external view returns (address); + function getExecutor() external view returns (address); } diff --git a/l1-contracts/src/governance/libraries/Errors.sol b/l1-contracts/src/governance/libraries/Errors.sol index fb835660287..b6654440e55 100644 --- a/l1-contracts/src/governance/libraries/Errors.sol +++ b/l1-contracts/src/governance/libraries/Errors.sol @@ -45,20 +45,20 @@ library Errors { error Governance__ProposalLib__ZeroYeaVotesNeeded(); error Governance__ProposalLib__MoreYeaVoteThanExistNeeded(); - error GovernanceProposer__CanOnlyPushProposalInPast(); // 0x49fdf611" - error GovernanceProposer__FailedToPropose(IPayload proposal); // 0x6ca2a2ed - error GovernanceProposer__InstanceHaveNoCode(address instance); // 0x20a3b441 - error GovernanceProposer__InsufficientVotes(); // 0xba1e05ef + error GovernanceProposer__CanOnlyExecuteProposalInPast(); // 0x8bf1d3b8 + error GovernanceProposer__FailedToPropose(IPayload proposal); // 0x8d94fbfc + error GovernanceProposer__InstanceHaveNoCode(address instance); // 0x5fa92625 + error GovernanceProposer__InsufficientVotes(uint256 votesCast, uint256 votesNeeded); // 0xd4ad89c2 error GovernanceProposer__InvalidNAndMValues(uint256 n, uint256 m); // 0x520d9704 error GovernanceProposer__NCannotBeLargerTHanM(uint256 n, uint256 m); // 0x2fdfc063 error GovernanceProposer__OnlyProposerCanVote(address caller, address proposer); // 0xba27df38 error GovernanceProposer__ProposalAlreadyExecuted(uint256 roundNumber); // 0x7aeacb17 - error GovernanceProposer__ProposalCannotBeAddressZero(); // 0xdb3e4b6e - error GovernanceProposer__ProposalHaveNoCode(IPayload proposal); // 0xdce0615b - error GovernanceProposer__ProposalTooOld(uint256 roundNumber, uint256 currentRoundNumber); //0x02283b1a - error GovernanceProposer__VoteAlreadyCastForSlot(Slot slot); //0xc2201452 + error GovernanceProposer__ProposalCannotBeAddressZero(); // 0x16ac1942 + error GovernanceProposer__ProposalHaveNoCode(IPayload proposal); // 0xb69440a1 + error GovernanceProposer__ProposalTooOld(uint256 roundNumber, uint256 currentRoundNumber); // 0xc3d7aa4f + error GovernanceProposer__VoteAlreadyCastForSlot(Slot slot); // 0x3a6150ca - error CoinIssuer__InssuficientMintAvailable(uint256 available, uint256 needed); // 0xf268b931 + error CoinIssuer__InsufficientMintAvailable(uint256 available, uint256 needed); // 0xa1cc8799 error Registry__RollupAlreadyRegistered(address rollup); // 0x3c34eabf error Registry__RollupNotRegistered(address rollup); // 0xa1fee4cf diff --git a/l1-contracts/src/governance/GovernanceProposer.sol b/l1-contracts/src/governance/proposer/EmpireBase.sol similarity index 81% rename from l1-contracts/src/governance/GovernanceProposer.sol rename to l1-contracts/src/governance/proposer/EmpireBase.sol index 7e665ee3aa8..349fa7b880c 100644 --- a/l1-contracts/src/governance/GovernanceProposer.sol +++ b/l1-contracts/src/governance/proposer/EmpireBase.sol @@ -4,10 +4,8 @@ pragma solidity >=0.8.27; import {ILeonidas} from "@aztec/core/interfaces/ILeonidas.sol"; import {Slot, SlotLib} from "@aztec/core/libraries/TimeMath.sol"; -import {IGovernance} from "@aztec/governance/interfaces/IGovernance.sol"; import {IGovernanceProposer} from "@aztec/governance/interfaces/IGovernanceProposer.sol"; import {IPayload} from "@aztec/governance/interfaces/IPayload.sol"; -import {IRegistry} from "@aztec/governance/interfaces/IRegistry.sol"; import {Errors} from "@aztec/governance/libraries/Errors.sol"; /** @@ -17,7 +15,7 @@ import {Errors} from "@aztec/governance/libraries/Errors.sol"; * This also means that the implementation here will need to be "updated" if * the interfaces of the sequencer selection changes, for example going optimistic. */ -contract GovernanceProposer is IGovernanceProposer { +abstract contract EmpireBase is IGovernanceProposer { using SlotLib for Slot; struct RoundAccounting { @@ -29,14 +27,12 @@ contract GovernanceProposer is IGovernanceProposer { uint256 public constant LIFETIME_IN_ROUNDS = 5; - IRegistry public immutable REGISTRY; uint256 public immutable N; uint256 public immutable M; mapping(address instance => mapping(uint256 roundNumber => RoundAccounting)) public rounds; - constructor(IRegistry _registry, uint256 _n, uint256 _m) { - REGISTRY = _registry; + constructor(uint256 _n, uint256 _m) { N = _n; M = _m; @@ -57,11 +53,12 @@ contract GovernanceProposer is IGovernanceProposer { * @return True if executed successfully, false otherwise */ function vote(IPayload _proposal) external override(IGovernanceProposer) returns (bool) { - require( + // For now, skipping this as the check is not really needed but there were not full agreement + /*require( address(_proposal).code.length > 0, Errors.GovernanceProposer__ProposalHaveNoCode(_proposal) - ); + );*/ - address instance = REGISTRY.getRollup(); + address instance = getInstance(); require(instance.code.length > 0, Errors.GovernanceProposer__InstanceHaveNoCode(instance)); ILeonidas selection = ILeonidas(instance); @@ -94,22 +91,26 @@ contract GovernanceProposer is IGovernanceProposer { } /** - * @notice Push the proposal to the appela + * @notice Executes the proposal using the `_execute` function * * @param _roundNumber - The round number to execute * * @return True if executed successfully, false otherwise */ - function pushProposal(uint256 _roundNumber) external override(IGovernanceProposer) returns (bool) { + function executeProposal(uint256 _roundNumber) + external + override(IGovernanceProposer) + returns (bool) + { // Need to ensure that the round is not active. - address instance = REGISTRY.getRollup(); + address instance = getInstance(); require(instance.code.length > 0, Errors.GovernanceProposer__InstanceHaveNoCode(instance)); ILeonidas selection = ILeonidas(instance); Slot currentSlot = selection.getCurrentSlot(); uint256 currentRound = computeRound(currentSlot); - require(_roundNumber < currentRound, Errors.GovernanceProposer__CanOnlyPushProposalInPast()); + require(_roundNumber < currentRound, Errors.GovernanceProposer__CanOnlyExecuteProposalInPast()); require( _roundNumber + LIFETIME_IN_ROUNDS >= currentRound, Errors.GovernanceProposer__ProposalTooOld(_roundNumber, currentRound) @@ -120,16 +121,14 @@ contract GovernanceProposer is IGovernanceProposer { require( round.leader != IPayload(address(0)), Errors.GovernanceProposer__ProposalCannotBeAddressZero() ); - require(round.yeaCount[round.leader] >= N, Errors.GovernanceProposer__InsufficientVotes()); + uint256 votesCast = round.yeaCount[round.leader]; + require(votesCast >= N, Errors.GovernanceProposer__InsufficientVotes(votesCast, N)); round.executed = true; - emit ProposalPushed(round.leader, _roundNumber); + emit ProposalExecuted(round.leader, _roundNumber); - require( - getGovernance().propose(round.leader), - Errors.GovernanceProposer__FailedToPropose(round.leader) - ); + require(_execute(round.leader), Errors.GovernanceProposer__FailedToPropose(round.leader)); return true; } @@ -162,7 +161,8 @@ contract GovernanceProposer is IGovernanceProposer { return _slot.unwrap() / M; } - function getGovernance() public view override(IGovernanceProposer) returns (IGovernance) { - return IGovernance(REGISTRY.getGovernance()); - } + // Virtual functions + function getInstance() public view virtual override(IGovernanceProposer) returns (address); + function getExecutor() public view virtual override(IGovernanceProposer) returns (address); + function _execute(IPayload _proposal) internal virtual returns (bool); } diff --git a/l1-contracts/src/governance/proposer/GovernanceProposer.sol b/l1-contracts/src/governance/proposer/GovernanceProposer.sol new file mode 100644 index 00000000000..734a42172e5 --- /dev/null +++ b/l1-contracts/src/governance/proposer/GovernanceProposer.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Aztec Labs. +pragma solidity >=0.8.27; + +import {IGovernance} from "@aztec/governance/interfaces/IGovernance.sol"; +import {IGovernanceProposer} from "@aztec/governance/interfaces/IGovernanceProposer.sol"; +import {IPayload} from "@aztec/governance/interfaces/IPayload.sol"; +import {IRegistry} from "@aztec/governance/interfaces/IRegistry.sol"; +import {EmpireBase} from "./EmpireBase.sol"; + +/** + * @notice A GovernanceProposer implementation following the empire model + * Beware that while governance generally do not care about the implementation + * this implementation will since it is dependent on the sequencer selection. + * This also means that the implementation here will need to be "updated" if + * the interfaces of the sequencer selection changes, for example going optimistic. + */ +contract GovernanceProposer is IGovernanceProposer, EmpireBase { + IRegistry public immutable REGISTRY; + + constructor(IRegistry _registry, uint256 _n, uint256 _m) EmpireBase(_n, _m) { + REGISTRY = _registry; + } + + function getExecutor() public view override(EmpireBase, IGovernanceProposer) returns (address) { + return REGISTRY.getGovernance(); + } + + function getInstance() public view override(EmpireBase, IGovernanceProposer) returns (address) { + return REGISTRY.getRollup(); + } + + function _execute(IPayload _proposal) internal override(EmpireBase) returns (bool) { + return IGovernance(getExecutor()).propose(_proposal); + } +} diff --git a/l1-contracts/src/periphery/SlashFactory.sol b/l1-contracts/src/periphery/SlashFactory.sol new file mode 100644 index 00000000000..14904c1f62e --- /dev/null +++ b/l1-contracts/src/periphery/SlashFactory.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Aztec Labs. +pragma solidity >=0.8.27; + +import {ILeonidas} from "@aztec/core/interfaces/ILeonidas.sol"; +import {Epoch} from "@aztec/core/libraries/TimeMath.sol"; +import {IPayload} from "@aztec/governance/interfaces/IPayload.sol"; +import {ISlashFactory} from "./interfaces/ISlashFactory.sol"; +import {SlashPayload} from "./SlashPayload.sol"; + +contract SlashFactory is ISlashFactory { + ILeonidas public immutable LEONIDAS; + + constructor(ILeonidas _leonidas) { + LEONIDAS = _leonidas; + } + + function createSlashPayload(Epoch _epoch, uint256 _amount) + external + override(ISlashFactory) + returns (IPayload) + { + (address predictedAddress, bool isDeployed) = getAddressAndIsDeployed(_epoch, _amount); + + if (isDeployed) { + return IPayload(predictedAddress); + } + + SlashPayload payload = + new SlashPayload{salt: bytes32(Epoch.unwrap(_epoch))}(_epoch, LEONIDAS, _amount); + + emit SlashPayloadCreated(address(payload), _epoch, _amount); + return IPayload(address(payload)); + } + + function getAddressAndIsDeployed(Epoch _epoch, uint256 _amount) + public + view + override(ISlashFactory) + returns (address, bool) + { + address predictedAddress = _computeSlashPayloadAddress(_epoch, _amount); + bool isDeployed = predictedAddress.code.length > 0; + return (predictedAddress, isDeployed); + } + + function _computeSlashPayloadAddress(Epoch _epoch, uint256 _amount) + internal + view + returns (address) + { + bytes32 salt = bytes32(Epoch.unwrap(_epoch)); + return address( + uint160( + uint256( + keccak256( + abi.encodePacked( + bytes1(0xff), + address(this), + salt, + keccak256( + abi.encodePacked( + type(SlashPayload).creationCode, abi.encode(_epoch, LEONIDAS, _amount) + ) + ) + ) + ) + ) + ) + ); + } +} diff --git a/l1-contracts/src/periphery/SlashPayload.sol b/l1-contracts/src/periphery/SlashPayload.sol new file mode 100644 index 00000000000..4410cfe0ae9 --- /dev/null +++ b/l1-contracts/src/periphery/SlashPayload.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Aztec Labs. +pragma solidity >=0.8.27; + +import {ILeonidas} from "@aztec/core/interfaces/ILeonidas.sol"; +import {IStaking} from "@aztec/core/interfaces/IStaking.sol"; +import {Epoch} from "@aztec/core/libraries/TimeMath.sol"; +import {IPayload} from "@aztec/governance/interfaces/IPayload.sol"; + +/** + * @notice The simplest payload that you can find, slash all attesters for an epoch. + */ +contract SlashPayload is IPayload { + Epoch public immutable EPOCH; + ILeonidas public immutable LEONIDAS; + uint256 public immutable AMOUNT; + + constructor(Epoch _epoch, ILeonidas _leonidas, uint256 _amount) { + EPOCH = _epoch; + LEONIDAS = _leonidas; + AMOUNT = _amount; + } + + function getActions() external view override(IPayload) returns (IPayload.Action[] memory) { + address[] memory attesters = ILeonidas(LEONIDAS).getEpochCommittee(EPOCH); + IPayload.Action[] memory actions = new IPayload.Action[](attesters.length); + + for (uint256 i = 0; i < attesters.length; i++) { + actions[i] = IPayload.Action({ + target: address(LEONIDAS), + data: abi.encodeWithSelector(IStaking.slash.selector, attesters[i], AMOUNT) + }); + } + + return actions; + } +} diff --git a/l1-contracts/src/periphery/interfaces/ISlashFactory.sol b/l1-contracts/src/periphery/interfaces/ISlashFactory.sol new file mode 100644 index 00000000000..7300cfbba84 --- /dev/null +++ b/l1-contracts/src/periphery/interfaces/ISlashFactory.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Aztec Labs. +pragma solidity >=0.8.27; + +import {Epoch} from "@aztec/core/libraries/TimeMath.sol"; +import {IPayload} from "@aztec/governance/interfaces/IPayload.sol"; + +interface ISlashFactory { + event SlashPayloadCreated( + address indexed payloadAddress, Epoch indexed epoch, uint256 indexed amount + ); + + function createSlashPayload(Epoch _epoch, uint256 _amount) external returns (IPayload); + function getAddressAndIsDeployed(Epoch _epoch, uint256 _amount) + external + view + returns (address, bool); +} diff --git a/l1-contracts/terraform/main.tf b/l1-contracts/terraform/main.tf index d619a827877..a9b9b4a3faa 100644 --- a/l1-contracts/terraform/main.tf +++ b/l1-contracts/terraform/main.tf @@ -109,4 +109,13 @@ variable "GOVERNANCE_CONTRACT_ADDRESS" { output "GOVERNANCE_CONTRACT_ADDRESS" { value = var.GOVERNANCE_CONTRACT_ADDRESS -} \ No newline at end of file +} + +variable "SLASH_FACTORY_CONTRACT_ADDRESS" { + type = string + default = "" +} + +output "SLASH_FACTORY_CONTRACT_ADDRESS" { + value = var.SLASH_FACTORY_CONTRACT_ADDRESS +} diff --git a/l1-contracts/test/Rollup.t.sol b/l1-contracts/test/Rollup.t.sol index 0164b72a770..f060e39bb5f 100644 --- a/l1-contracts/test/Rollup.t.sol +++ b/l1-contracts/test/Rollup.t.sol @@ -75,9 +75,10 @@ contract RollupTest is DecoderBase, TimeFns { testERC20 = new TestERC20("test", "TEST", address(this)); leo = new Leonidas( - address(1), testERC20, TestConstants.AZTEC_MINIMUM_STAKE, + TestConstants.AZTEC_SLASHING_QUORUM, + TestConstants.AZTEC_SLASHING_ROUND_SIZE, TestConstants.AZTEC_SLOT_DURATION, TestConstants.AZTEC_EPOCH_DURATION, TestConstants.AZTEC_TARGET_COMMITTEE_SIZE diff --git a/l1-contracts/test/fees/FeeRollup.t.sol b/l1-contracts/test/fees/FeeRollup.t.sol index 4463ea51cfb..b01f373bd9a 100644 --- a/l1-contracts/test/fees/FeeRollup.t.sol +++ b/l1-contracts/test/fees/FeeRollup.t.sol @@ -131,7 +131,9 @@ contract FeeRollupTest is FeeModelTestPoints, DecoderBase { aztecEpochDuration: EPOCH_DURATION, targetCommitteeSize: 48, aztecEpochProofClaimWindowInL2Slots: 16, - minimumStake: 100 ether + minimumStake: TestConstants.AZTEC_MINIMUM_STAKE, + slashingQuorum: TestConstants.AZTEC_SLASHING_QUORUM, + slashingRoundSize: TestConstants.AZTEC_SLASHING_ROUND_SIZE }) ); fakeCanonical.setCanonicalRollup(address(rollup)); diff --git a/l1-contracts/test/governance/coin-issuer/mint.t.sol b/l1-contracts/test/governance/coin-issuer/mint.t.sol index 29304fbb589..229d2f52d91 100644 --- a/l1-contracts/test/governance/coin-issuer/mint.t.sol +++ b/l1-contracts/test/governance/coin-issuer/mint.t.sol @@ -35,7 +35,7 @@ contract MintTest is CoinIssuerBase { // it reverts uint256 amount = bound(_amount, maxMint + 1, type(uint256).max); vm.expectRevert( - abi.encodeWithSelector(Errors.CoinIssuer__InssuficientMintAvailable.selector, maxMint, amount) + abi.encodeWithSelector(Errors.CoinIssuer__InsufficientMintAvailable.selector, maxMint, amount) ); nom.mint(address(0xdead), amount); } diff --git a/l1-contracts/test/governance/governance-proposer/Base.t.sol b/l1-contracts/test/governance/governance-proposer/Base.t.sol index 1f720911438..bbfd7b54828 100644 --- a/l1-contracts/test/governance/governance-proposer/Base.t.sol +++ b/l1-contracts/test/governance/governance-proposer/Base.t.sol @@ -4,7 +4,7 @@ pragma solidity >=0.8.27; import {Test} from "forge-std/Test.sol"; import {Registry} from "@aztec/governance/Registry.sol"; -import {GovernanceProposer} from "@aztec/governance/GovernanceProposer.sol"; +import {GovernanceProposer} from "@aztec/governance/proposer/GovernanceProposer.sol"; import {IPayload} from "@aztec/governance/interfaces/IPayload.sol"; diff --git a/l1-contracts/test/governance/governance-proposer/constructor.t.sol b/l1-contracts/test/governance/governance-proposer/constructor.t.sol index f32b8aefa59..327ef727701 100644 --- a/l1-contracts/test/governance/governance-proposer/constructor.t.sol +++ b/l1-contracts/test/governance/governance-proposer/constructor.t.sol @@ -2,12 +2,22 @@ pragma solidity >=0.8.27; import {Test} from "forge-std/Test.sol"; -import {GovernanceProposer} from "@aztec/governance/GovernanceProposer.sol"; +import {GovernanceProposer} from "@aztec/governance/proposer/GovernanceProposer.sol"; import {Errors} from "@aztec/governance/libraries/Errors.sol"; import {IRegistry} from "@aztec/governance/interfaces/IRegistry.sol"; +contract FakeRegistry { + function getGovernance() external pure returns (address) { + return address(0x01); + } + + function getRollup() external pure returns (address) { + return address(0x02); + } +} + contract ConstructorTest is Test { - IRegistry internal constant REGISTRY = IRegistry(address(0x02)); + IRegistry internal REGISTRY = IRegistry(address(new FakeRegistry())); function test_WhenNIsLessThanOrEqualHalfOfM(uint256 _n, uint256 _m) external { // it revert @@ -42,5 +52,7 @@ contract ConstructorTest is Test { assertEq(address(g.REGISTRY()), address(REGISTRY)); assertEq(g.N(), n); assertEq(g.M(), m); + assertEq(g.getExecutor(), address(REGISTRY.getGovernance()), "executor"); + assertEq(g.getInstance(), address(REGISTRY.getRollup()), "instance"); } } diff --git a/l1-contracts/test/governance/governance-proposer/pushProposal.t.sol b/l1-contracts/test/governance/governance-proposer/executeProposal.t.sol similarity index 87% rename from l1-contracts/test/governance/governance-proposer/pushProposal.t.sol rename to l1-contracts/test/governance/governance-proposer/executeProposal.t.sol index 26de9a2f343..db7bd38c352 100644 --- a/l1-contracts/test/governance/governance-proposer/pushProposal.t.sol +++ b/l1-contracts/test/governance/governance-proposer/executeProposal.t.sol @@ -11,7 +11,7 @@ import {Slot, SlotLib, Timestamp} from "@aztec/core/libraries/TimeMath.sol"; import {FaultyGovernance} from "./mocks/FaultyGovernance.sol"; import {FalsyGovernance} from "./mocks/FalsyGovernance.sol"; -contract PushProposalTest is GovernanceProposerBase { +contract ExecuteProposalTest is GovernanceProposerBase { using SlotLib for Slot; Leonidas internal leonidas; @@ -26,11 +26,11 @@ contract PushProposalTest is GovernanceProposerBase { Errors.GovernanceProposer__InstanceHaveNoCode.selector, address(0xdead) ) ); - governanceProposer.pushProposal(_roundNumber); + governanceProposer.executeProposal(_roundNumber); } modifier givenCanonicalInstanceHoldCode() { - leonidas = new Leonidas(address(this)); + leonidas = new Leonidas(); vm.prank(registry.getGovernance()); registry.upgrade(address(leonidas)); @@ -42,9 +42,9 @@ contract PushProposalTest is GovernanceProposerBase { function test_WhenRoundNotInPast() external givenCanonicalInstanceHoldCode { // it revert vm.expectRevert( - abi.encodeWithSelector(Errors.GovernanceProposer__CanOnlyPushProposalInPast.selector) + abi.encodeWithSelector(Errors.GovernanceProposer__CanOnlyExecuteProposalInPast.selector) ); - governanceProposer.pushProposal(0); + governanceProposer.executeProposal(0); } modifier whenRoundInPast() { @@ -74,7 +74,7 @@ contract PushProposalTest is GovernanceProposerBase { governanceProposer.computeRound(leonidas.getCurrentSlot()) ) ); - governanceProposer.pushProposal(0); + governanceProposer.executeProposal(0); } modifier whenRoundInRecentPast() { @@ -105,13 +105,13 @@ contract PushProposalTest is GovernanceProposerBase { ) ) ); - governanceProposer.pushProposal(1); + governanceProposer.executeProposal(1); } vm.expectRevert( abi.encodeWithSelector(Errors.GovernanceProposer__ProposalAlreadyExecuted.selector, 1) ); - governanceProposer.pushProposal(1); + governanceProposer.executeProposal(1); } modifier givenRoundNotExecutedBefore() { @@ -144,7 +144,7 @@ contract PushProposalTest is GovernanceProposerBase { vm.expectRevert( abi.encodeWithSelector(Errors.GovernanceProposer__ProposalCannotBeAddressZero.selector) ); - governanceProposer.pushProposal(0); + governanceProposer.executeProposal(0); } modifier givenLeaderIsNotAddress0() { @@ -164,13 +164,17 @@ contract PushProposalTest is GovernanceProposerBase { vm.prank(proposer); governanceProposer.vote(proposal); + uint256 votesNeeded = governanceProposer.N(); + vm.warp( Timestamp.unwrap( leonidas.getTimestampForSlot(leonidas.getCurrentSlot() + Slot.wrap(governanceProposer.M())) ) ); - vm.expectRevert(abi.encodeWithSelector(Errors.GovernanceProposer__InsufficientVotes.selector)); - governanceProposer.pushProposal(1); + vm.expectRevert( + abi.encodeWithSelector(Errors.GovernanceProposer__InsufficientVotes.selector, 1, votesNeeded) + ); + governanceProposer.executeProposal(1); } modifier givenSufficientYea(uint256 _yeas) { @@ -204,7 +208,7 @@ contract PushProposalTest is GovernanceProposerBase { // it revert // When using a new registry we change the governanceProposer's interpetation of time :O - Leonidas freshInstance = new Leonidas(address(this)); + Leonidas freshInstance = new Leonidas(); vm.prank(registry.getGovernance()); registry.upgrade(address(freshInstance)); @@ -215,9 +219,9 @@ contract PushProposalTest is GovernanceProposerBase { // As time is perceived differently, round 1 is currently in the future vm.expectRevert( - abi.encodeWithSelector(Errors.GovernanceProposer__CanOnlyPushProposalInPast.selector) + abi.encodeWithSelector(Errors.GovernanceProposer__CanOnlyExecuteProposalInPast.selector) ); - governanceProposer.pushProposal(1); + governanceProposer.executeProposal(1); // Jump 2 rounds, since we are currently in round 0 vm.warp( @@ -230,7 +234,7 @@ contract PushProposalTest is GovernanceProposerBase { vm.expectRevert( abi.encodeWithSelector(Errors.GovernanceProposer__ProposalCannotBeAddressZero.selector) ); - governanceProposer.pushProposal(1); + governanceProposer.executeProposal(1); } function test_GivenGovernanceCallReturnFalse(uint256 _yeas) @@ -249,7 +253,7 @@ contract PushProposalTest is GovernanceProposerBase { vm.expectRevert( abi.encodeWithSelector(Errors.GovernanceProposer__FailedToPropose.selector, proposal) ); - governanceProposer.pushProposal(1); + governanceProposer.executeProposal(1); } function test_GivenGovernanceCallFails(uint256 _yeas) @@ -266,7 +270,7 @@ contract PushProposalTest is GovernanceProposerBase { vm.etch(address(governance), address(faulty).code); vm.expectRevert(abi.encodeWithSelector(FaultyGovernance.Faulty.selector)); - governanceProposer.pushProposal(1); + governanceProposer.executeProposal(1); } function test_GivenGovernanceCallSucceeds(uint256 _yeas) @@ -279,11 +283,11 @@ contract PushProposalTest is GovernanceProposerBase { givenSufficientYea(_yeas) { // it update executed to true - // it emits {ProposalPushed} event + // it emits {ProposalExecuted} event // it return true vm.expectEmit(true, true, true, true, address(governanceProposer)); - emit IGovernanceProposer.ProposalPushed(proposal, 1); - assertTrue(governanceProposer.pushProposal(1)); + emit IGovernanceProposer.ProposalExecuted(proposal, 1); + assertTrue(governanceProposer.executeProposal(1)); (, IPayload leader, bool executed) = governanceProposer.rounds(address(leonidas), 1); assertTrue(executed); assertEq(address(leader), address(proposal)); diff --git a/l1-contracts/test/governance/governance-proposer/pushProposal.tree b/l1-contracts/test/governance/governance-proposer/executeProposal.tree similarity index 93% rename from l1-contracts/test/governance/governance-proposer/pushProposal.tree rename to l1-contracts/test/governance/governance-proposer/executeProposal.tree index 2188b588fde..9b2acb0f3fd 100644 --- a/l1-contracts/test/governance/governance-proposer/pushProposal.tree +++ b/l1-contracts/test/governance/governance-proposer/executeProposal.tree @@ -1,4 +1,4 @@ -PushProposalTest +ExecuteProposalTest ├── given canonical instance hold no code │ └── it revert └── given canonical instance hold code @@ -25,5 +25,5 @@ PushProposalTest │ └── it revert └── given governance call succeeds ├── it update executed to true - ├── it emits {ProposalPushed} event + ├── it emits {ProposalExecuted} event └── it return true \ No newline at end of file diff --git a/l1-contracts/test/governance/governance-proposer/vote.t.sol b/l1-contracts/test/governance/governance-proposer/vote.t.sol index f78f9f009e0..91c28363912 100644 --- a/l1-contracts/test/governance/governance-proposer/vote.t.sol +++ b/l1-contracts/test/governance/governance-proposer/vote.t.sol @@ -15,7 +15,8 @@ contract VoteTest is GovernanceProposerBase { address internal proposer = address(0); Leonidas internal leonidas; - function test_WhenProposalHoldNoCode() external { + // Skipping this test since the it matches the for now skipped check in `EmpireBase::vote` + function skip__test_WhenProposalHoldNoCode() external { // it revert vm.expectRevert( abi.encodeWithSelector(Errors.GovernanceProposer__ProposalHaveNoCode.selector, proposal) @@ -39,7 +40,7 @@ contract VoteTest is GovernanceProposerBase { } modifier givenCanonicalRollupHoldCode() { - leonidas = new Leonidas(address(this)); + leonidas = new Leonidas(); vm.prank(registry.getGovernance()); registry.upgrade(address(leonidas)); @@ -138,7 +139,7 @@ contract VoteTest is GovernanceProposerBase { uint256 leonidasRound = governanceProposer.computeRound(leonidasSlot); uint256 yeaBefore = governanceProposer.yeaCount(address(leonidas), leonidasRound, proposal); - Leonidas freshInstance = new Leonidas(address(this)); + Leonidas freshInstance = new Leonidas(); vm.prank(registry.getGovernance()); registry.upgrade(address(freshInstance)); diff --git a/l1-contracts/test/governance/governance/base.t.sol b/l1-contracts/test/governance/governance/base.t.sol index cc5a9878a06..05a125f10ff 100644 --- a/l1-contracts/test/governance/governance/base.t.sol +++ b/l1-contracts/test/governance/governance/base.t.sol @@ -3,7 +3,7 @@ pragma solidity >=0.8.27; import {TestBase} from "@test/base/Base.sol"; import {Governance} from "@aztec/governance/Governance.sol"; -import {GovernanceProposer} from "@aztec/governance/GovernanceProposer.sol"; +import {GovernanceProposer} from "@aztec/governance/proposer/GovernanceProposer.sol"; import {Registry} from "@aztec/governance/Registry.sol"; import {DataStructures} from "@aztec/governance/libraries/DataStructures.sol"; import {IMintableERC20} from "@aztec/governance/interfaces/IMintableERC20.sol"; diff --git a/l1-contracts/test/governance/scenario/NewGovernanceProposerPayload.sol b/l1-contracts/test/governance/scenario/NewGovernanceProposerPayload.sol index 613dc7006b4..a4cb726dc2e 100644 --- a/l1-contracts/test/governance/scenario/NewGovernanceProposerPayload.sol +++ b/l1-contracts/test/governance/scenario/NewGovernanceProposerPayload.sol @@ -4,7 +4,7 @@ pragma solidity >=0.8.27; import {IPayload} from "@aztec/governance/interfaces/IPayload.sol"; import {IRegistry} from "@aztec/governance/interfaces/IRegistry.sol"; import {Governance} from "@aztec/governance/Governance.sol"; -import {GovernanceProposer} from "@aztec/governance/GovernanceProposer.sol"; +import {GovernanceProposer} from "@aztec/governance/proposer/GovernanceProposer.sol"; /** * @title NewGovernanceProposerPayload diff --git a/l1-contracts/test/governance/scenario/UpgradeGovernanceProposerTest.t.sol b/l1-contracts/test/governance/scenario/UpgradeGovernanceProposerTest.t.sol index 8504653da17..f5fd35a9a34 100644 --- a/l1-contracts/test/governance/scenario/UpgradeGovernanceProposerTest.t.sol +++ b/l1-contracts/test/governance/scenario/UpgradeGovernanceProposerTest.t.sol @@ -6,7 +6,7 @@ import {TestBase} from "@test/base/Base.sol"; import {IMintableERC20} from "@aztec/governance/interfaces/IMintableERC20.sol"; import {Rollup} from "../../harnesses/Rollup.sol"; import {Governance} from "@aztec/governance/Governance.sol"; -import {GovernanceProposer} from "@aztec/governance/GovernanceProposer.sol"; +import {GovernanceProposer} from "@aztec/governance/proposer/GovernanceProposer.sol"; import {Registry} from "@aztec/governance/Registry.sol"; import {DataStructures} from "@aztec/governance/libraries/DataStructures.sol"; import {IMintableERC20} from "@aztec/governance/interfaces/IMintableERC20.sol"; @@ -92,7 +92,7 @@ contract UpgradeGovernanceProposerTest is TestBase { vm.warp(Timestamp.unwrap(rollup.getTimestampForSlot(rollup.getCurrentSlot() + Slot.wrap(1)))); } - governanceProposer.pushProposal(0); + governanceProposer.executeProposal(0); proposal = governance.getProposal(0); assertEq(address(proposal.payload), address(payload)); diff --git a/l1-contracts/test/governance/scenario/slashing/Slashing.t.sol b/l1-contracts/test/governance/scenario/slashing/Slashing.t.sol new file mode 100644 index 00000000000..e2a40cec054 --- /dev/null +++ b/l1-contracts/test/governance/scenario/slashing/Slashing.t.sol @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.27; + +import {TestBase} from "@test/base/Base.sol"; + +import {Errors} from "@aztec/core/libraries/Errors.sol"; +import {Registry} from "@aztec/governance/Registry.sol"; +import {Rollup, Config} from "@aztec/core/Rollup.sol"; +import {TestERC20} from "@aztec/mock/TestERC20.sol"; +import {MockFeeJuicePortal} from "@aztec/mock/MockFeeJuicePortal.sol"; +import {TestConstants} from "../../../harnesses/TestConstants.sol"; +import {CheatDepositArgs} from "@aztec/core/interfaces/IRollup.sol"; + +import {RewardDistributor} from "@aztec/governance/RewardDistributor.sol"; + +import {SlashFactory} from "@aztec/periphery/SlashFactory.sol"; +import {Slasher, IPayload} from "@aztec/core/staking/Slasher.sol"; +import {ILeonidas} from "@aztec/core/interfaces/ILeonidas.sol"; +import {Status, ValidatorInfo} from "@aztec/core/interfaces/IStaking.sol"; + +import {Errors} from "@aztec/core/libraries/Errors.sol"; +import {Timestamp} from "@aztec/core/libraries/TimeMath.sol"; + +import {CheatDepositArgs} from "@aztec/core/interfaces/IRollup.sol"; +import {SlashingProposer} from "@aztec/core/staking/SlashingProposer.sol"; + +import {Slot, SlotLib, Epoch} from "@aztec/core/libraries/TimeMath.sol"; + +contract SlashingScenario is TestBase { + using SlotLib for Slot; + + TestERC20 internal testERC20; + RewardDistributor internal rewardDistributor; + Rollup internal rollup; + Slasher internal slasher; + SlashFactory internal slashFactory; + SlashingProposer internal slashingProposer; + + function test_Slashing() public { + uint256 validatorCount = 4; + + CheatDepositArgs[] memory initialValidators = new CheatDepositArgs[](validatorCount); + + for (uint256 i = 1; i < validatorCount + 1; i++) { + uint256 attesterPrivateKey = uint256(keccak256(abi.encode("attester", i))); + address attester = vm.addr(attesterPrivateKey); + uint256 proposerPrivateKey = uint256(keccak256(abi.encode("proposer", i))); + address proposer = vm.addr(proposerPrivateKey); + + initialValidators[i - 1] = CheatDepositArgs({ + attester: attester, + proposer: proposer, + withdrawer: address(this), + amount: TestConstants.AZTEC_MINIMUM_STAKE + }); + } + + testERC20 = new TestERC20("test", "TEST", address(this)); + Registry registry = new Registry(address(this)); + rewardDistributor = new RewardDistributor(testERC20, registry, address(this)); + rollup = new Rollup({ + _fpcJuicePortal: new MockFeeJuicePortal(), + _rewardDistributor: rewardDistributor, + _stakingAsset: testERC20, + _vkTreeRoot: bytes32(0), + _protocolContractTreeRoot: bytes32(0), + _ares: address(this), + _config: 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, + minimumStake: TestConstants.AZTEC_MINIMUM_STAKE, + slashingQuorum: TestConstants.AZTEC_SLASHING_QUORUM, + slashingRoundSize: TestConstants.AZTEC_SLASHING_ROUND_SIZE + }) + }); + slasher = rollup.SLASHER(); + slashingProposer = slasher.PROPOSER(); + slashFactory = new SlashFactory(ILeonidas(address(rollup))); + + testERC20.mint(address(this), TestConstants.AZTEC_MINIMUM_STAKE * validatorCount); + testERC20.approve(address(rollup), TestConstants.AZTEC_MINIMUM_STAKE * validatorCount); + rollup.cheat__InitialiseValidatorSet(initialValidators); + + // Lets make a proposal to slash! + + uint256 slashAmount = 10e18; + IPayload payload = slashFactory.createSlashPayload(Epoch.wrap(0), slashAmount); + + // Cast a bunch of votes + vm.warp(Timestamp.unwrap(rollup.getTimestampForSlot(Slot.wrap(1)))); + + for (uint256 i = 0; i < 10; i++) { + address proposer = rollup.getCurrentProposer(); + vm.prank(proposer); + slashingProposer.vote(payload); + vm.warp(Timestamp.unwrap(rollup.getTimestampForSlot(rollup.getCurrentSlot() + Slot.wrap(1)))); + } + + address[] memory attesters = rollup.getAttesters(); + uint256[] memory stakes = new uint256[](attesters.length); + + for (uint256 i = 0; i < attesters.length; i++) { + ValidatorInfo memory info = rollup.getInfo(attesters[i]); + stakes[i] = info.stake; + assertTrue(info.status == Status.VALIDATING, "Invalid status"); + } + + slashingProposer.executeProposal(0); + + // Make sure that the slash was successful, + // Meaning that validators are now LIVING and have lost the slash amount + for (uint256 i = 0; i < attesters.length; i++) { + ValidatorInfo memory info = rollup.getInfo(attesters[i]); + uint256 stake = info.stake; + assertEq(stake, stakes[i] - slashAmount, "Invalid stake"); + assertTrue(info.status == Status.LIVING, "Invalid status"); + } + } +} diff --git a/l1-contracts/test/harnesses/Leonidas.sol b/l1-contracts/test/harnesses/Leonidas.sol index a7c78f304b1..c52eb301589 100644 --- a/l1-contracts/test/harnesses/Leonidas.sol +++ b/l1-contracts/test/harnesses/Leonidas.sol @@ -7,11 +7,12 @@ import {TestConstants} from "./TestConstants.sol"; import {TestERC20} from "@aztec/mock/TestERC20.sol"; contract Leonidas is RealLeonidas { - constructor(address _ares) + constructor() RealLeonidas( - _ares, new TestERC20("test", "TEST", address(this)), 100e18, + TestConstants.AZTEC_SLASHING_QUORUM, + TestConstants.AZTEC_SLASHING_ROUND_SIZE, 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 41d72b20de9..27d55a9913e 100644 --- a/l1-contracts/test/harnesses/Rollup.sol +++ b/l1-contracts/test/harnesses/Rollup.sol @@ -29,7 +29,9 @@ contract Rollup is RealRollup { aztecEpochDuration: TestConstants.AZTEC_EPOCH_DURATION, targetCommitteeSize: TestConstants.AZTEC_TARGET_COMMITTEE_SIZE, aztecEpochProofClaimWindowInL2Slots: TestConstants.AZTEC_EPOCH_PROOF_CLAIM_WINDOW_IN_L2_SLOTS, - minimumStake: TestConstants.AZTEC_MINIMUM_STAKE + minimumStake: TestConstants.AZTEC_MINIMUM_STAKE, + slashingQuorum: TestConstants.AZTEC_SLASHING_QUORUM, + slashingRoundSize: TestConstants.AZTEC_SLASHING_ROUND_SIZE }) ) {} diff --git a/l1-contracts/test/harnesses/TestConstants.sol b/l1-contracts/test/harnesses/TestConstants.sol index 371a2d8f594..aad8edd6db0 100644 --- a/l1-contracts/test/harnesses/TestConstants.sol +++ b/l1-contracts/test/harnesses/TestConstants.sol @@ -10,4 +10,6 @@ library TestConstants { 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; + uint256 internal constant AZTEC_SLASHING_QUORUM = 6; + uint256 internal constant AZTEC_SLASHING_ROUND_SIZE = 10; } diff --git a/l1-contracts/test/sparta/Sparta.t.sol b/l1-contracts/test/sparta/Sparta.t.sol index 165dd9f7b4f..eccf797ee16 100644 --- a/l1-contracts/test/sparta/Sparta.t.sol +++ b/l1-contracts/test/sparta/Sparta.t.sol @@ -12,7 +12,7 @@ import {Inbox} from "@aztec/core/messagebridge/Inbox.sol"; 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 {Rollup, Config} from "@aztec/core/Rollup.sol"; import {Leonidas} from "@aztec/core/Leonidas.sol"; import {NaiveMerkle} from "../merkle/Naive.sol"; import {MerkleTestUtil} from "../merkle/TestUtil.sol"; @@ -27,6 +27,11 @@ 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"; + +import {SlashFactory} from "@aztec/periphery/SlashFactory.sol"; +import {Slasher, IPayload} from "@aztec/core/staking/Slasher.sol"; +import {ILeonidas} from "@aztec/core/interfaces/ILeonidas.sol"; +import {Status, ValidatorInfo} from "@aztec/core/interfaces/IStaking.sol"; // solhint-disable comprehensive-interface /** @@ -44,6 +49,8 @@ contract SpartaTest is DecoderBase { bool shouldRevert; } + SlashFactory internal slashFactory; + Slasher internal slasher; Inbox internal inbox; Outbox internal outbox; Rollup internal rollup; @@ -64,9 +71,10 @@ contract SpartaTest is DecoderBase { string memory _name = "mixed_block_1"; { Leonidas leonidas = new Leonidas( - address(1), testERC20, TestConstants.AZTEC_MINIMUM_STAKE, + TestConstants.AZTEC_SLASHING_QUORUM, + TestConstants.AZTEC_SLASHING_ROUND_SIZE, TestConstants.AZTEC_SLOT_DURATION, TestConstants.AZTEC_EPOCH_DURATION, TestConstants.AZTEC_TARGET_COMMITTEE_SIZE @@ -102,9 +110,25 @@ contract SpartaTest is DecoderBase { 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, testERC20, bytes32(0), bytes32(0), address(this) - ); + rollup = new Rollup({ + _fpcJuicePortal: new MockFeeJuicePortal(), + _rewardDistributor: rewardDistributor, + _stakingAsset: testERC20, + _vkTreeRoot: bytes32(0), + _protocolContractTreeRoot: bytes32(0), + _ares: address(this), + _config: 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, + minimumStake: TestConstants.AZTEC_MINIMUM_STAKE, + slashingQuorum: TestConstants.AZTEC_SLASHING_QUORUM, + slashingRoundSize: TestConstants.AZTEC_SLASHING_ROUND_SIZE + }) + }); + slasher = rollup.SLASHER(); + slashFactory = new SlashFactory(ILeonidas(address(rollup))); testERC20.mint(address(this), TestConstants.AZTEC_MINIMUM_STAKE * _validatorCount); testERC20.approve(address(rollup), TestConstants.AZTEC_MINIMUM_STAKE * _validatorCount); @@ -180,6 +204,40 @@ contract SpartaTest is DecoderBase { _testBlock("mixed_block_2", false, 3, false); } + function testNukeFromOrbit() public setup(4) { + // We propose some blocks, and have a bunch of validators attest to them. + // Then we slash EVERYONE that was in the committees because the epoch never + // got finalised. + // This is LIKELY, not the action you really want to take, you want to slash + // the people actually attesting, etc, but for simplicity we can do this as showcase. + _testBlock("mixed_block_1", false, 3, false); + _testBlock("mixed_block_2", false, 3, false); + + address[] memory attesters = rollup.getAttesters(); + uint256[] memory stakes = new uint256[](attesters.length); + + for (uint256 i = 0; i < attesters.length; i++) { + ValidatorInfo memory info = rollup.getInfo(attesters[i]); + stakes[i] = info.stake; + assertTrue(info.status == Status.VALIDATING, "Invalid status"); + } + + // We say, these things are bad, call the baba yaga to take care of them! + uint256 slashAmount = 10e18; + IPayload slashPayload = slashFactory.createSlashPayload(rollup.getCurrentEpoch(), slashAmount); + vm.prank(address(slasher.PROPOSER())); + slasher.slash(slashPayload); + + // Make sure that the slash was successful, + // Meaning that validators are now LIVING and have lost the slash amount + for (uint256 i = 0; i < attesters.length; i++) { + ValidatorInfo memory info = rollup.getInfo(attesters[i]); + uint256 stake = info.stake; + assertEq(stake, stakes[i] - slashAmount, "Invalid stake"); + assertTrue(info.status == Status.LIVING, "Invalid status"); + } + } + function testInvalidProposer() public setup(4) { _testBlock("mixed_block_1", true, 3, true); } diff --git a/l1-contracts/test/staking/StakingCheater.sol b/l1-contracts/test/staking/StakingCheater.sol index ba89e1e07ab..a886a3d2f72 100644 --- a/l1-contracts/test/staking/StakingCheater.sol +++ b/l1-contracts/test/staking/StakingCheater.sol @@ -9,9 +9,12 @@ import {EnumerableSet} from "@oz/utils/structs/EnumerableSet.sol"; contract StakingCheater is Staking { using EnumerableSet for EnumerableSet.AddressSet; - constructor(address _slasher, IERC20 _stakingAsset, uint256 _minimumStake) - Staking(_slasher, _stakingAsset, _minimumStake) - {} + constructor( + IERC20 _stakingAsset, + uint256 _minimumStake, + uint256 _slashingQuorum, + uint256 _roundSize + ) Staking(_stakingAsset, _minimumStake, _slashingQuorum, _roundSize) {} function cheat__SetStatus(address _attester, Status _status) external { stakingStore.info[_attester].status = _status; diff --git a/l1-contracts/test/staking/base.t.sol b/l1-contracts/test/staking/base.t.sol index 6aa8eaa8ca4..441d418d244 100644 --- a/l1-contracts/test/staking/base.t.sol +++ b/l1-contracts/test/staking/base.t.sol @@ -16,10 +16,13 @@ contract StakingBase is TestBase { address internal constant ATTESTER = address(bytes20("ATTESTER")); address internal constant WITHDRAWER = address(bytes20("WITHDRAWER")); address internal constant RECIPIENT = address(bytes20("RECIPIENT")); - address internal constant SLASHER = address(bytes20("SLASHER")); + + address internal SLASHER; function setUp() public virtual { stakingAsset = new TestERC20("test", "TEST", address(this)); - staking = new StakingCheater(SLASHER, stakingAsset, MINIMUM_STAKE); + staking = new StakingCheater(stakingAsset, MINIMUM_STAKE, 1, 1); + + SLASHER = address(staking.SLASHER()); } } diff --git a/spartan/aztec-network/files/config/config-prover-env.sh b/spartan/aztec-network/files/config/config-prover-env.sh index 073547821d4..2d56ed1c897 100644 --- a/spartan/aztec-network/files/config/config-prover-env.sh +++ b/spartan/aztec-network/files/config/config-prover-env.sh @@ -19,6 +19,7 @@ coin_issuer_address=$(echo "$output" | grep -oP 'CoinIssuer Address: \K0x[a-fA-F reward_distributor_address=$(echo "$output" | grep -oP 'RewardDistributor Address: \K0x[a-fA-F0-9]{40}') governance_proposer_address=$(echo "$output" | grep -oP 'GovernanceProposer Address: \K0x[a-fA-F0-9]{40}') governance_address=$(echo "$output" | grep -oP 'Governance Address: \K0x[a-fA-F0-9]{40}') +slash_factory_address=$(echo "$output" | grep -oP 'SlashFactory Address: \K0x[a-fA-F0-9]{40}') # Write the addresses to a file in the shared volume cat </shared/contracts/contracts.env @@ -34,6 +35,7 @@ export COIN_ISSUER_CONTRACT_ADDRESS=$coin_issuer_address export REWARD_DISTRIBUTOR_CONTRACT_ADDRESS=$reward_distributor_address export GOVERNANCE_PROPOSER_CONTRACT_ADDRESS=$governance_proposer_address export GOVERNANCE_CONTRACT_ADDRESS=$governance_address +export SLASH_FACTORY_CONTRACT_ADDRESS=$slash_factory_address EOF cat /shared/contracts/contracts.env diff --git a/spartan/aztec-network/files/config/config-validator-env.sh b/spartan/aztec-network/files/config/config-validator-env.sh index b2848f8e069..05d55e437f3 100644 --- a/spartan/aztec-network/files/config/config-validator-env.sh +++ b/spartan/aztec-network/files/config/config-validator-env.sh @@ -19,6 +19,7 @@ coin_issuer_address=$(echo "$output" | grep -oP 'CoinIssuer Address: \K0x[a-fA-F reward_distributor_address=$(echo "$output" | grep -oP 'RewardDistributor Address: \K0x[a-fA-F0-9]{40}') governance_proposer_address=$(echo "$output" | grep -oP 'GovernanceProposer Address: \K0x[a-fA-F0-9]{40}') governance_address=$(echo "$output" | grep -oP 'Governance Address: \K0x[a-fA-F0-9]{40}') +slash_factory_address=$(echo "$output" | grep -oP 'SlashFactory Address: \K0x[a-fA-F0-9]{40}') # We assume that there is an env var set for validator keys from the config map # We get the index in the config map from the pod name, which will have the validator index within it @@ -39,6 +40,7 @@ export COIN_ISSUER_CONTRACT_ADDRESS=$coin_issuer_address export REWARD_DISTRIBUTOR_CONTRACT_ADDRESS=$reward_distributor_address export GOVERNANCE_PROPOSER_CONTRACT_ADDRESS=$governance_proposer_address export GOVERNANCE_CONTRACT_ADDRESS=$governance_address +export SLASH_FACTORY_CONTRACT_ADDRESS=$slash_factory_address export VALIDATOR_PRIVATE_KEY=$private_key export L1_PRIVATE_KEY=$private_key export SEQ_PUBLISHER_PRIVATE_KEY=$private_key diff --git a/spartan/aztec-network/files/config/deploy-l1-contracts.sh b/spartan/aztec-network/files/config/deploy-l1-contracts.sh index 1f4c56599f7..366a00bd41f 100644 --- a/spartan/aztec-network/files/config/deploy-l1-contracts.sh +++ b/spartan/aztec-network/files/config/deploy-l1-contracts.sh @@ -36,6 +36,7 @@ coin_issuer_address=$(echo "$output" | grep -oP 'CoinIssuer Address: \K0x[a-fA-F reward_distributor_address=$(echo "$output" | grep -oP 'RewardDistributor Address: \K0x[a-fA-F0-9]{40}') governance_proposer_address=$(echo "$output" | grep -oP 'GovernanceProposer Address: \K0x[a-fA-F0-9]{40}') governance_address=$(echo "$output" | grep -oP 'Governance Address: \K0x[a-fA-F0-9]{40}') +slash_factory_address=$(echo "$output" | grep -oP 'SlashFactory Address: \K0x[a-fA-F0-9]{40}') # Write the addresses to a file in the shared volume cat < /shared/contracts/contracts.env @@ -50,6 +51,7 @@ export COIN_ISSUER_CONTRACT_ADDRESS=$coin_issuer_address export REWARD_DISTRIBUTOR_CONTRACT_ADDRESS=$reward_distributor_address export GOVERNANCE_PROPOSER_CONTRACT_ADDRESS=$governance_proposer_address export GOVERNANCE_CONTRACT_ADDRESS=$governance_address +export SLASH_FACTORY_CONTRACT_ADDRESS=$slash_factory_address EOF cat /shared/contracts/contracts.env diff --git a/yarn-project/archiver/package.json b/yarn-project/archiver/package.json index c802f7aaf33..c773e74ca99 100644 --- a/yarn-project/archiver/package.json +++ b/yarn-project/archiver/package.json @@ -110,4 +110,4 @@ "engines": { "node": ">=18" } -} \ No newline at end of file +} diff --git a/yarn-project/aztec-faucet/package.json b/yarn-project/aztec-faucet/package.json index 64290005c93..5c0c9a01b91 100644 --- a/yarn-project/aztec-faucet/package.json +++ b/yarn-project/aztec-faucet/package.json @@ -94,4 +94,4 @@ "engines": { "node": ">=18" } -} \ No newline at end of file +} diff --git a/yarn-project/aztec-node/package.json b/yarn-project/aztec-node/package.json index ad55b09fe21..6ace5e54c53 100644 --- a/yarn-project/aztec-node/package.json +++ b/yarn-project/aztec-node/package.json @@ -106,4 +106,4 @@ "engines": { "node": ">=18" } -} \ No newline at end of file +} diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index 60e2ce1ca22..fa94d3dfe73 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -73,7 +73,7 @@ import { createP2PClient, } from '@aztec/p2p'; import { ProtocolContractAddress } from '@aztec/protocol-contracts'; -import { GlobalVariableBuilder, type L1Publisher, SequencerClient } from '@aztec/sequencer-client'; +import { GlobalVariableBuilder, type L1Publisher, SequencerClient, createSlasherClient } from '@aztec/sequencer-client'; import { PublicProcessorFactory } from '@aztec/simulator'; import { Attributes, type TelemetryClient, type Traceable, type Tracer, trackSpan } from '@aztec/telemetry-client'; import { NoopTelemetryClient } from '@aztec/telemetry-client/noop'; @@ -177,8 +177,10 @@ export class AztecNodeService implements AztecNode, Traceable { telemetry, ); + const slasherClient = await createSlasherClient(config, archiver, telemetry); + // start both and wait for them to sync from the block source - await Promise.all([p2pClient.start(), worldStateSynchronizer.start()]); + await Promise.all([p2pClient.start(), worldStateSynchronizer.start(), slasherClient.start()]); const validatorClient = await createValidatorClient(config, rollupAddress, { p2pClient, telemetry, dateProvider }); @@ -189,6 +191,7 @@ export class AztecNodeService implements AztecNode, Traceable { validatorClient, p2pClient, worldStateSynchronizer, + slasherClient, contractDataSource: archiver, l2BlockSource: archiver, l1ToL2MessageSource: archiver, diff --git a/yarn-project/aztec.js/package.json b/yarn-project/aztec.js/package.json index 830f8f697d5..b38d7e93dca 100644 --- a/yarn-project/aztec.js/package.json +++ b/yarn-project/aztec.js/package.json @@ -119,4 +119,4 @@ "engines": { "node": ">=18" } -} \ No newline at end of file +} diff --git a/yarn-project/aztec.js/src/contract/contract.test.ts b/yarn-project/aztec.js/src/contract/contract.test.ts index 66a54e8cfb5..f45eb0203d1 100644 --- a/yarn-project/aztec.js/src/contract/contract.test.ts +++ b/yarn-project/aztec.js/src/contract/contract.test.ts @@ -47,6 +47,7 @@ describe('Contract Class', () => { coinIssuerAddress: EthAddress.random(), rewardDistributorAddress: EthAddress.random(), governanceProposerAddress: EthAddress.random(), + slashFactoryAddress: EthAddress.random(), }; const mockNodeInfo: NodeInfo = { nodeVersion: 'vx.x.x', diff --git a/yarn-project/bb-prover/package.json b/yarn-project/bb-prover/package.json index d32a9343fb1..1c109743f2c 100644 --- a/yarn-project/bb-prover/package.json +++ b/yarn-project/bb-prover/package.json @@ -111,4 +111,4 @@ "engines": { "node": ">=18" } -} \ No newline at end of file +} diff --git a/yarn-project/bot/package.json b/yarn-project/bot/package.json index fcd38c0be00..e44cad7a4a2 100644 --- a/yarn-project/bot/package.json +++ b/yarn-project/bot/package.json @@ -91,4 +91,4 @@ "engines": { "node": ">=18" } -} \ No newline at end of file +} diff --git a/yarn-project/circuit-types/package.json b/yarn-project/circuit-types/package.json index 94e6136a449..ea574a81948 100644 --- a/yarn-project/circuit-types/package.json +++ b/yarn-project/circuit-types/package.json @@ -106,4 +106,4 @@ "engines": { "node": ">=18" } -} \ No newline at end of file +} diff --git a/yarn-project/cli/package.json b/yarn-project/cli/package.json index 31dcf1da552..7a9d39d31f8 100644 --- a/yarn-project/cli/package.json +++ b/yarn-project/cli/package.json @@ -127,4 +127,4 @@ "engines": { "node": ">=18" } -} \ No newline at end of file +} diff --git a/yarn-project/cli/src/cmds/infrastructure/sequencers.ts b/yarn-project/cli/src/cmds/infrastructure/sequencers.ts index a3e6c77d39d..cf5dbe7bdc1 100644 --- a/yarn-project/cli/src/cmds/infrastructure/sequencers.ts +++ b/yarn-project/cli/src/cmds/infrastructure/sequencers.ts @@ -1,5 +1,5 @@ import { createCompatibleClient } from '@aztec/aztec.js'; -import { MINIMUM_STAKE, createEthereumChain } from '@aztec/ethereum'; +import { createEthereumChain, getL1ContractsConfigEnvVars } from '@aztec/ethereum'; import { type LogFn, type Logger } from '@aztec/foundation/log'; import { RollupAbi, TestERC20Abi } from '@aztec/l1-artifacts'; @@ -71,14 +71,16 @@ export async function sequencers(opts: { client: walletClient, }); + const config = getL1ContractsConfigEnvVars(); + await Promise.all( [ - await stakingAsset.write.mint([walletClient.account.address, MINIMUM_STAKE], {} as any), - await stakingAsset.write.approve([rollup.address, MINIMUM_STAKE], {} as any), + await stakingAsset.write.mint([walletClient.account.address, config.minimumStake], {} as any), + await stakingAsset.write.approve([rollup.address, config.minimumStake], {} as any), ].map(txHash => publicClient.waitForTransactionReceipt({ hash: txHash })), ); - const hash = await writeableRollup.write.deposit([who, who, who, MINIMUM_STAKE]); + const hash = await writeableRollup.write.deposit([who, who, who, config.minimumStake]); await publicClient.waitForTransactionReceipt({ hash }); log(`Added in tx ${hash}`); } else if (command === 'remove') { 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 21ac9d71ec6..39b4bfd4635 100644 --- a/yarn-project/cli/src/cmds/l1/deploy_l1_contracts.ts +++ b/yarn-project/cli/src/cmds/l1/deploy_l1_contracts.ts @@ -48,5 +48,6 @@ export async function deployL1Contracts( log(`RewardDistributor Address: ${l1ContractAddresses.rewardDistributorAddress.toString()}`); log(`GovernanceProposer Address: ${l1ContractAddresses.governanceProposerAddress.toString()}`); log(`Governance Address: ${l1ContractAddresses.governanceAddress.toString()}`); + log(`SlashFactory Address: ${l1ContractAddresses.slashFactoryAddress.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 7d5edca07ba..40d06e2fd6d 100644 --- a/yarn-project/cli/src/cmds/l1/update_l1_validators.ts +++ b/yarn-project/cli/src/cmds/l1/update_l1_validators.ts @@ -1,6 +1,6 @@ import { EthCheatCodes } from '@aztec/aztec.js'; import { type EthAddress } from '@aztec/circuits.js'; -import { MINIMUM_STAKE, createEthereumChain, getL1ContractsConfigEnvVars, isAnvilTestChain } from '@aztec/ethereum'; +import { createEthereumChain, getL1ContractsConfigEnvVars, isAnvilTestChain } from '@aztec/ethereum'; import { type LogFn, type Logger } from '@aztec/foundation/log'; import { RollupAbi, TestERC20Abi } from '@aztec/l1-artifacts'; @@ -40,6 +40,7 @@ export async function addL1Validator({ log, debugLogger, }: RollupCommandArgs & LoggerArgs & { validatorAddress: EthAddress }) { + const config = getL1ContractsConfigEnvVars(); const dualLog = makeDualLog(log, debugLogger); const publicClient = getPublicClient(rpcUrl, chainId); const walletClient = getWalletClient(rpcUrl, chainId, privateKey, mnemonic); @@ -57,8 +58,8 @@ export async function addL1Validator({ await Promise.all( [ - await stakingAsset.write.mint([walletClient.account.address, MINIMUM_STAKE], {} as any), - await stakingAsset.write.approve([rollupAddress.toString(), MINIMUM_STAKE], {} as any), + await stakingAsset.write.mint([walletClient.account.address, config.minimumStake], {} as any), + await stakingAsset.write.approve([rollupAddress.toString(), config.minimumStake], {} as any), ].map(txHash => publicClient.waitForTransactionReceipt({ hash: txHash })), ); @@ -67,7 +68,7 @@ export async function addL1Validator({ validatorAddress.toString(), validatorAddress.toString(), validatorAddress.toString(), - MINIMUM_STAKE, + config.minimumStake, ]); dualLog(`Transaction hash: ${txHash}`); await publicClient.waitForTransactionReceipt({ hash: txHash }); 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 329b2428297..fdd44f8d1db 100644 --- a/yarn-project/cli/src/cmds/pxe/get_node_info.ts +++ b/yarn-project/cli/src/cmds/pxe/get_node_info.ts @@ -34,6 +34,7 @@ export async function getNodeInfo( rewardDistributor: info.l1ContractAddresses.rewardDistributorAddress.toString(), governanceProposer: info.l1ContractAddresses.governanceProposerAddress.toString(), governance: info.l1ContractAddresses.governanceAddress.toString(), + slashFactory: info.l1ContractAddresses.slashFactoryAddress.toString(), }, protocolContractAddresses: { classRegisterer: info.protocolContractAddresses.classRegisterer.toString(), @@ -59,6 +60,7 @@ export async function getNodeInfo( log(` RewardDistributor Address: ${info.l1ContractAddresses.rewardDistributorAddress.toString()}`); log(` GovernanceProposer Address: ${info.l1ContractAddresses.governanceProposerAddress.toString()}`); log(` Governance Address: ${info.l1ContractAddresses.governanceAddress.toString()}`); + log(` SlashFactory Address: ${info.l1ContractAddresses.slashFactoryAddress.toString()}`); log(`L2 Contract Addresses:`); log(` Class Registerer: ${info.protocolContractAddresses.classRegisterer.toString()}`); diff --git a/yarn-project/end-to-end/scripts/e2e_test_config.yml b/yarn-project/end-to-end/scripts/e2e_test_config.yml index 2fb7902c93f..2ea76eb0e91 100644 --- a/yarn-project/end-to-end/scripts/e2e_test_config.yml +++ b/yarn-project/end-to-end/scripts/e2e_test_config.yml @@ -90,6 +90,8 @@ tests: e2e_p2p_gossip: test_path: 'e2e_p2p/gossip_network.test.ts' with_alerts: true + e2e_p2p_slashing: + test_path: 'e2e_p2p/slashing.test.ts' e2e_p2p_upgrade_governance_proposer: test_path: 'e2e_p2p/upgrade_governance_proposer.test.ts' e2e_p2p_rediscovery: 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 2f1d670620c..9c87ef3332c 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 @@ -63,6 +63,7 @@ COIN_ISSUER_CONTRACT_ADDRESS=$(echo "$output" | grep -oP 'CoinIssuer Address: \K REWARD_DISTRIBUTOR_CONTRACT_ADDRESS=$(echo "$output" | grep -oP 'RewardDistributor Address: \K0x[a-fA-F0-9]{40}') GOVERNANCE_PROPOSER_CONTRACT_ADDRESS=$(echo "$output" | grep -oP 'GovernanceProposer Address: \K0x[a-fA-F0-9]{40}') GOVERNANCE_CONTRACT_ADDRESS=$(echo "$output" | grep -oP 'Governance Address: \K0x[a-fA-F0-9]{40}') +SLASH_FACTORY_CONTRACT_ADDRESS=$(echo "$output" | grep -oP 'SlashFactory Address: \K0x[a-fA-F0-9]{40}') # Save contract addresses to state/l1-contracts.env cat <$(git rev-parse --show-toplevel)/yarn-project/end-to-end/scripts/native-network/state/l1-contracts.env @@ -77,6 +78,7 @@ export COIN_ISSUER_CONTRACT_ADDRESS=$COIN_ISSUER_CONTRACT_ADDRESS export REWARD_DISTRIBUTOR_CONTRACT_ADDRESS=$REWARD_DISTRIBUTOR_CONTRACT_ADDRESS export GOVERNANCE_PROPOSER_CONTRACT_ADDRESS=$GOVERNANCE_PROPOSER_CONTRACT_ADDRESS export GOVERNANCE_CONTRACT_ADDRESS=$GOVERNANCE_CONTRACT_ADDRESS +export SLASH_FACTORY_CONTRACT_ADDRESS=$SLASH_FACTORY_CONTRACT_ADDRESS EOCONFIG echo "Contract addresses saved to state/l1-contracts.env" 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 2d55474e7c8..a3971c7cac9 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,7 +1,7 @@ import { getSchnorrAccount } from '@aztec/accounts/schnorr'; import { type AztecNodeConfig, type AztecNodeService } from '@aztec/aztec-node'; import { type AccountWalletWithSecretKey } from '@aztec/aztec.js'; -import { MINIMUM_STAKE, getL1ContractsConfigEnvVars } from '@aztec/ethereum'; +import { L1TxUtils, getL1ContractsConfigEnvVars } from '@aztec/ethereum'; import { EthCheatCodesWithState } from '@aztec/ethereum/test'; import { type Logger, createLogger } from '@aztec/foundation/log'; import { RollupAbi, TestERC20Abi } from '@aztec/l1-artifacts'; @@ -53,6 +53,8 @@ export class P2PNetworkTest { private cleanupInterval: NodeJS.Timeout | undefined = undefined; + private gasUtils: L1TxUtils | undefined = undefined; + constructor( testName: string, public bootstrapNode: BootstrapNode, @@ -61,6 +63,7 @@ export class P2PNetworkTest { initialValidatorConfig: AztecNodeConfig, // If set enable metrics collection metricsPort?: number, + assumeProvenThrough?: number, ) { this.logger = createLogger(`e2e:e2e_p2p:${testName}`); @@ -72,12 +75,24 @@ export class P2PNetworkTest { this.bootstrapNodeEnr = bootstrapNode.getENR().encodeTxt(); - this.snapshotManager = createSnapshotManager(`e2e_p2p_network/${testName}`, process.env.E2E_DATA_PATH, { - ...initialValidatorConfig, - ethereumSlotDuration: l1ContractsConfig.ethereumSlotDuration, - salt: 420, - metricsPort: metricsPort, - }); + this.snapshotManager = createSnapshotManager( + `e2e_p2p_network/${testName}`, + process.env.E2E_DATA_PATH, + { + ...initialValidatorConfig, + ethereumSlotDuration: l1ContractsConfig.ethereumSlotDuration, + salt: 420, + metricsPort: metricsPort, + }, + { + aztecEpochDuration: initialValidatorConfig.aztecEpochDuration ?? l1ContractsConfig.aztecEpochDuration, + aztecEpochProofClaimWindowInL2Slots: + initialValidatorConfig.aztecEpochProofClaimWindowInL2Slots ?? + l1ContractsConfig.aztecEpochProofClaimWindowInL2Slots, + assumeProvenThrough: assumeProvenThrough ?? Number.MAX_SAFE_INTEGER, + initialValidators: [], + }, + ); } /** @@ -96,15 +111,13 @@ export class P2PNetworkTest { this.logger.info('Syncing mock system time'); const { dateProvider, deployL1ContractsValues } = this.ctx!; // Send a tx and only update the time after the tx is mined, as eth time is not continuous - const tx = await deployL1ContractsValues.walletClient.sendTransaction({ + const receipt = await this.gasUtils!.sendAndMonitorTransaction({ to: this.baseAccount.address, + data: '0x', value: 1n, - account: this.baseAccount, - }); - const receipt = await deployL1ContractsValues.publicClient.waitForTransactionReceipt({ - hash: tx, }); const timestamp = await deployL1ContractsValues.publicClient.getBlock({ blockNumber: receipt.blockNumber }); + this.logger.info(`Timestamp: ${timestamp.timestamp}`); dateProvider.setTime(Number(timestamp.timestamp) * 1000); } @@ -113,11 +126,15 @@ export class P2PNetworkTest { numberOfNodes, basePort, metricsPort, + initialConfig, + assumeProvenThrough, }: { testName: string; numberOfNodes: number; basePort?: number; metricsPort?: number; + initialConfig?: Partial; + assumeProvenThrough?: number; }) { const port = basePort || (await getPort()); @@ -125,9 +142,20 @@ export class P2PNetworkTest { const bootstrapNode = await createBootstrapNodeFromPrivateKey(BOOTSTRAP_NODE_PRIVATE_KEY, port, telemetry); const bootstrapNodeEnr = bootstrapNode.getENR().encodeTxt(); - const initialValidatorConfig = await createValidatorConfig({} as AztecNodeConfig, bootstrapNodeEnr); + const initialValidatorConfig = await createValidatorConfig( + (initialConfig ?? {}) as AztecNodeConfig, + bootstrapNodeEnr, + ); - return new P2PNetworkTest(testName, bootstrapNode, port, numberOfNodes, initialValidatorConfig); + return new P2PNetworkTest( + testName, + bootstrapNode, + port, + numberOfNodes, + initialValidatorConfig, + metricsPort, + assumeProvenThrough, + ); } async applyBaseSnapshots() { @@ -148,7 +176,7 @@ export class P2PNetworkTest { client: deployL1ContractsValues.walletClient, }); - const stakeNeeded = MINIMUM_STAKE * BigInt(this.numberOfNodes); + const stakeNeeded = l1ContractsConfig.minimumStake * BigInt(this.numberOfNodes); await Promise.all( [ await stakingAsset.write.mint( @@ -171,7 +199,7 @@ export class P2PNetworkTest { attester: attester.address, proposer: proposer.address, withdrawer: attester.address, - amount: MINIMUM_STAKE, + amount: l1ContractsConfig.minimumStake, } as const); this.logger.verbose( @@ -266,6 +294,20 @@ export class P2PNetworkTest { async setup() { this.ctx = await this.snapshotManager.setup(); this.startSyncMockSystemTimeInterval(); + + this.gasUtils = new L1TxUtils( + this.ctx.deployL1ContractsValues.publicClient, + this.ctx.deployL1ContractsValues.walletClient, + this.logger, + { + gasLimitBufferPercentage: 20n, + maxGwei: 500n, + minGwei: 1n, + maxAttempts: 3, + checkIntervalMs: 100, + stallTimeMs: 1000, + }, + ); } async stopNodes(nodes: AztecNodeService[]) { diff --git a/yarn-project/end-to-end/src/e2e_p2p/slashing.test.ts b/yarn-project/end-to-end/src/e2e_p2p/slashing.test.ts new file mode 100644 index 00000000000..fcb6cca9c3f --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_p2p/slashing.test.ts @@ -0,0 +1,264 @@ +import { type AztecNodeService } from '@aztec/aztec-node'; +import { sleep } from '@aztec/aztec.js'; +import { RollupAbi, SlashFactoryAbi, SlasherAbi, SlashingProposerAbi } from '@aztec/l1-artifacts'; + +import fs from 'fs'; +import { getAddress, getContract, parseEventLogs } from 'viem'; + +import { shouldCollectMetrics } from '../fixtures/fixtures.js'; +import { createNodes } from '../fixtures/setup_p2p_test.js'; +import { P2PNetworkTest } from './p2p_network.js'; +import { createPXEServiceAndSubmitTransactions } from './shared.js'; + +// Don't set this to a higher value than 9 because each node will use a different L1 publisher account and anvil seeds +const NUM_NODES = 4; +const BOOT_NODE_UDP_PORT = 40600; + +const DATA_DIR = './data/slashing'; + +// This test is showcasing that slashing can happen, abusing that our nodes are honest but stupid +// making them slash themselves. +describe('e2e_p2p_slashing', () => { + let t: P2PNetworkTest; + let nodes: AztecNodeService[]; + + const slashingQuorum = 6; + const slashingRoundSize = 10; + + beforeEach(async () => { + t = await P2PNetworkTest.create({ + testName: 'e2e_p2p_slashing', + numberOfNodes: NUM_NODES, + basePort: BOOT_NODE_UDP_PORT, + metricsPort: shouldCollectMetrics(), + initialConfig: { + aztecEpochDuration: 1, + aztecEpochProofClaimWindowInL2Slots: 1, + slashingQuorum, + slashingRoundSize, + }, + assumeProvenThrough: 1, + }); + + await t.applyBaseSnapshots(); + await t.setup(); + await t.removeInitialNode(); + }); + + afterEach(async () => { + await t.stopNodes(nodes); + await t.teardown(); + for (let i = 0; i < NUM_NODES; i++) { + fs.rmSync(`${DATA_DIR}-${i}`, { recursive: true, force: true }); + } + }); + + it('should slash the attesters', async () => { + // create the bootstrap node for the network + if (!t.bootstrapNodeEnr) { + throw new Error('Bootstrap node ENR is not available'); + } + + const rollup = getContract({ + address: t.ctx.deployL1ContractsValues!.l1ContractAddresses.rollupAddress.toString(), + abi: RollupAbi, + client: t.ctx.deployL1ContractsValues!.walletClient, + }); + + const slasherContract = getContract({ + address: getAddress(await rollup.read.SLASHER()), + abi: SlasherAbi, + client: t.ctx.deployL1ContractsValues.publicClient, + }); + + const slashingProposer = getContract({ + address: getAddress(await slasherContract.read.PROPOSER()), + abi: SlashingProposerAbi, + client: t.ctx.deployL1ContractsValues.publicClient, + }); + + const slashFactory = getContract({ + address: getAddress(t.ctx.deployL1ContractsValues.l1ContractAddresses.slashFactoryAddress.toString()), + abi: SlashFactoryAbi, + client: t.ctx.deployL1ContractsValues.publicClient, + }); + + const slashingInfo = async () => { + const bn = await t.ctx.cheatCodes.eth.blockNumber(); + const slotNumber = await rollup.read.getCurrentSlot(); + const roundNumber = await slashingProposer.read.computeRound([slotNumber]); + const instanceAddress = t.ctx.deployL1ContractsValues.l1ContractAddresses.rollupAddress.toString(); + const info = await slashingProposer.read.rounds([instanceAddress, roundNumber]); + const leaderVotes = await slashingProposer.read.yeaCount([instanceAddress, roundNumber, info[1]]); + return { bn, slotNumber, roundNumber, info, leaderVotes }; + }; + + const jumpToNextRound = async () => { + t.logger.info(`Jumping to next round`); + const roundSize = await slashingProposer.read.M(); + const nextRoundTimestamp = await rollup.read.getTimestampForSlot([ + ((await rollup.read.getCurrentSlot()) / roundSize) * roundSize + roundSize, + ]); + await t.ctx.cheatCodes.eth.warp(Number(nextRoundTimestamp)); + + await t.syncMockSystemTime(); + }; + + t.ctx.aztecNodeConfig.validatorReexecute = false; + + // create our network of nodes and submit txs into each of them + // the number of txs per node and the number of txs per rollup + // should be set so that the only way for rollups to be built + // is if the txs are successfully gossiped around the nodes. + t.logger.info('Creating nodes'); + nodes = await createNodes( + t.ctx.aztecNodeConfig, + t.ctx.dateProvider, + t.bootstrapNodeEnr, + NUM_NODES, + BOOT_NODE_UDP_PORT, + DATA_DIR, + // To collect metrics - run in aztec-packages `docker compose --profile metrics up` and set COLLECT_METRICS=true + shouldCollectMetrics(), + ); + + // We are overriding the slashing amount to 1, such that the slashing will "really" happen. + for (const node of nodes) { + const seqClient = node.getSequencer(); + if (!seqClient) { + throw new Error('Sequencer not found'); + } + const sequencer = (seqClient as any).sequencer; + const slasher = (sequencer as any).slasherClient; + slasher.slashingAmount = 1n; + } + + // wait a bit for peers to discover each other + await sleep(4000); + + let sInfo = await slashingInfo(); + + const votesNeeded = await slashingProposer.read.N(); + + // We should push us to land exactly at the next round + await jumpToNextRound(); + + // Produce blocks until we hit an issue with pruning. + // Then we should jump in time to the next round so we are sure that we have the votes + // Then we just sit on our hands and wait. + + const seqClient = nodes[0].getSequencer(); + if (!seqClient) { + throw new Error('Sequencer not found'); + } + const sequencer = (seqClient as any).sequencer; + const slasher = (sequencer as any).slasherClient; + + t.logger.info(`Producing blocks until we hit a pruning event`); + + // Run for up to the slashing round size, or as long as needed to get a slash event + // Variable because sometimes hit race-condition issues with attestations. + for (let i = 0; i < slashingRoundSize; i++) { + t.logger.info('Submitting transactions'); + const bn = await nodes[0].getBlockNumber(); + await createPXEServiceAndSubmitTransactions(t.logger, nodes[0], 1); + + t.logger.info(`Waiting for block number to change`); + while (bn === (await nodes[0].getBlockNumber())) { + await sleep(1000); + } + + if (slasher.slashEvents.length > 0) { + t.logger.info(`We have a slash event ${i}`); + break; + } + } + + expect(slasher.slashEvents.length).toBeGreaterThan(0); + + // We should push us to land exactly at the next round + await jumpToNextRound(); + + // For the next round we will try to cast votes. + // Stop early if we have enough votes. + t.logger.info(`Waiting for votes to be cast`); + for (let i = 0; i < slashingRoundSize; i++) { + t.logger.info('Waiting for slot number to change and votes to be cast'); + const slotNumber = await rollup.read.getCurrentSlot(); + t.logger.info(`Waiting for block number to change`); + while (slotNumber === (await rollup.read.getCurrentSlot())) { + await sleep(1000); + } + sInfo = await slashingInfo(); + t.logger.info(`We have ${sInfo.leaderVotes} votes in round ${sInfo.roundNumber} on ${sInfo.info[1]}`); + if (sInfo.leaderVotes > votesNeeded) { + t.logger.info(`We have sufficient votes`); + break; + } + } + + t.logger.info('Deploy the actual payload for slashing!'); + const slashEvent = slasher.slashEvents[0]; + await t.ctx.deployL1ContractsValues.publicClient.waitForTransactionReceipt({ + hash: await slashFactory.write.createSlashPayload([slashEvent.epoch, slashEvent.amount], { + account: t.ctx.deployL1ContractsValues.walletClient.account, + }), + }); + + t.logger.info(`We jump in time to the next round to execute`); + await jumpToNextRound(); + const attestersPre = await rollup.read.getAttesters(); + + for (const attester of attestersPre) { + const attesterInfo = await rollup.read.getInfo([attester]); + // Check that status isValidating + expect(attesterInfo.status).toEqual(1); + } + + t.logger.info(`Push the proposal, SLASHING!`); + const tx = await slashingProposer.write.executeProposal([sInfo.roundNumber], { + account: t.ctx.deployL1ContractsValues.walletClient.account, + }); + await t.ctx.deployL1ContractsValues.publicClient.waitForTransactionReceipt({ + hash: tx, + }); + + const receipt = await t.ctx.deployL1ContractsValues.publicClient.getTransactionReceipt({ + hash: tx, + }); + + const slashingEvents = parseEventLogs({ + abi: RollupAbi, + logs: receipt.logs, + }).filter(log => log.eventName === 'Slashed'); + + const attestersSlashed = slashingEvents.map(event => { + // Because TS is a little nagging bitch + return (event.args as any).attester; + }); + + // Convert attestersPre elements to lowercase for consistent comparison + const normalizedAttestersPre = attestersPre.map(addr => addr.toLowerCase()); + const normalizedAttestersSlashed = attestersSlashed.map(addr => addr.toLowerCase()); + expect(new Set(normalizedAttestersPre)).toEqual(new Set(normalizedAttestersSlashed)); + + const instanceAddress = t.ctx.deployL1ContractsValues.l1ContractAddresses.rollupAddress.toString(); + const infoPost = await slashingProposer.read.rounds([instanceAddress, sInfo.roundNumber]); + + expect(sInfo.info[0]).toEqual(infoPost[0]); + expect(sInfo.info[1]).toEqual(infoPost[1]); + expect(sInfo.info[2]).toEqual(false); + expect(infoPost[2]).toEqual(true); + + const attestersPost = await rollup.read.getAttesters(); + + for (const attester of attestersPre) { + const attesterInfo = await rollup.read.getInfo([attester]); + // Check that status is Living + expect(attesterInfo.status).toEqual(2); + } + const committee = await rollup.read.getEpochCommittee([slashEvent.epoch]); + expect(attestersPre.length).toBe(committee.length); + expect(attestersPost.length).toBe(0); + }, 1_000_000); +}); diff --git a/yarn-project/end-to-end/src/e2e_p2p/upgrade_governance_proposer.test.ts b/yarn-project/end-to-end/src/e2e_p2p/upgrade_governance_proposer.test.ts index 9499bce2eef..bad8f5a7c7b 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/upgrade_governance_proposer.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/upgrade_governance_proposer.test.ts @@ -162,7 +162,10 @@ describe('e2e_p2p_governance_proposer', () => { await waitL1Block(); - const txHash = await governanceProposer.write.pushProposal([govData.round], { account: emperor, gas: 1_000_000n }); + const txHash = await governanceProposer.write.executeProposal([govData.round], { + account: emperor, + gas: 1_000_000n, + }); await t.ctx.deployL1ContractsValues.publicClient.waitForTransactionReceipt({ hash: txHash }); const token = getContract({ 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 2acb78f7d47..a4cbd1afa3a 100644 --- a/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts +++ b/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts @@ -316,9 +316,9 @@ async function setupFromFresh( } const deployL1ContractsValues = await setupL1Contracts(aztecNodeConfig.l1RpcUrl, hdAccount, logger, { + ...getL1ContractsConfigEnvVars(), salt: opts.salt, ...deployL1ContractsArgs, - ...getL1ContractsConfigEnvVars(), initialValidators: opts.initialValidators, }); aztecNodeConfig.l1Contracts = deployL1ContractsValues.l1ContractAddresses; diff --git a/yarn-project/ethereum/src/config.ts b/yarn-project/ethereum/src/config.ts index eda0024870b..4bb362d886e 100644 --- a/yarn-project/ethereum/src/config.ts +++ b/yarn-project/ethereum/src/config.ts @@ -1,4 +1,9 @@ -import { type ConfigMappingsType, getConfigFromMappings, numberConfigHelper } from '@aztec/foundation/config'; +import { + type ConfigMappingsType, + bigintConfigHelper, + getConfigFromMappings, + numberConfigHelper, +} from '@aztec/foundation/config'; export type L1ContractsConfig = { /** How many seconds an L1 slot lasts. */ @@ -11,6 +16,16 @@ export type L1ContractsConfig = { aztecTargetCommitteeSize: number; /** The number of L2 slots that we can wait for a proof of an epoch to be produced. */ aztecEpochProofClaimWindowInL2Slots: number; + /** The minimum stake for a validator. */ + minimumStake: bigint; + /** The slashing quorum */ + slashingQuorum: number; + /** The slashing round size */ + slashingRoundSize: number; + /** Governance proposing quorum */ + governanceProposerQuorum: number; + /** Governance proposing round size */ + governanceProposerRoundSize: number; }; export const DefaultL1ContractsConfig: L1ContractsConfig = { @@ -19,6 +34,11 @@ export const DefaultL1ContractsConfig: L1ContractsConfig = { aztecEpochDuration: 16, aztecTargetCommitteeSize: 48, aztecEpochProofClaimWindowInL2Slots: 13, + minimumStake: BigInt(100e18), + slashingQuorum: 6, + slashingRoundSize: 10, + governanceProposerQuorum: 6, + governanceProposerRoundSize: 10, }; export const l1ContractsConfigMappings: ConfigMappingsType = { @@ -47,6 +67,31 @@ export const l1ContractsConfigMappings: ConfigMappingsType = description: 'The number of L2 slots that we can wait for a proof of an epoch to be produced.', ...numberConfigHelper(DefaultL1ContractsConfig.aztecEpochProofClaimWindowInL2Slots), }, + minimumStake: { + env: 'AZTEC_MINIMUM_STAKE', + description: 'The minimum stake for a validator.', + ...bigintConfigHelper(DefaultL1ContractsConfig.minimumStake), + }, + slashingQuorum: { + env: 'AZTEC_SLASHING_QUORUM', + description: 'The slashing quorum', + ...numberConfigHelper(DefaultL1ContractsConfig.slashingQuorum), + }, + slashingRoundSize: { + env: 'AZTEC_SLASHING_ROUND_SIZE', + description: 'The slashing round size', + ...numberConfigHelper(DefaultL1ContractsConfig.slashingRoundSize), + }, + governanceProposerQuorum: { + env: 'AZTEC_GOVERNANCE_PROPOSER_QUORUM', + description: 'The governance proposing quorum', + ...numberConfigHelper(DefaultL1ContractsConfig.governanceProposerQuorum), + }, + governanceProposerRoundSize: { + env: 'AZTEC_GOVERNANCE_PROPOSER_ROUND_SIZE', + description: 'The governance proposing round size', + ...numberConfigHelper(DefaultL1ContractsConfig.governanceProposerRoundSize), + }, }; export function getL1ContractsConfigEnvVars(): L1ContractsConfig { diff --git a/yarn-project/ethereum/src/constants.ts b/yarn-project/ethereum/src/constants.ts index 2fea0175aca..c1f4b34d732 100644 --- a/yarn-project/ethereum/src/constants.ts +++ b/yarn-project/ethereum/src/constants.ts @@ -2,4 +2,3 @@ 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 31b2c1eeb50..657c9ff7145 100644 --- a/yarn-project/ethereum/src/deploy_l1_contracts.ts +++ b/yarn-project/ethereum/src/deploy_l1_contracts.ts @@ -26,6 +26,8 @@ import { RollupAbi, RollupBytecode, RollupLinkReferences, + SlashFactoryAbi, + SlashFactoryBytecode, TestERC20Abi, TestERC20Bytecode, } from '@aztec/l1-artifacts'; @@ -53,7 +55,6 @@ 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'; @@ -156,6 +157,10 @@ export interface L1ContractArtifactsForDeployment { * Governance contract artifacts. */ governance: ContractArtifacts; + /** + * SlashFactory contract artifacts. + */ + slashFactory: ContractArtifacts; } export const l1Artifacts: L1ContractArtifactsForDeployment = { @@ -216,6 +221,10 @@ export const l1Artifacts: L1ContractArtifactsForDeployment = { contractAbi: GovernanceAbi, contractBytecode: GovernanceBytecode, }, + slashFactory: { + contractAbi: SlashFactoryAbi, + contractBytecode: SlashFactoryBytecode, + }, }; export interface DeployL1ContractsArgs extends L1ContractsConfig { @@ -331,14 +340,10 @@ export const deployL1Contracts = async ( ]); logger.verbose(`Deployed Staking Asset at ${stakingAssetAddress}`); - // @todo #8084 - // @note These numbers are just chosen to make testing simple. - const quorumSize = 6n; - const roundSize = 10n; const governanceProposerAddress = await govDeployer.deploy(l1Artifacts.governanceProposer, [ registryAddress.toString(), - quorumSize, - roundSize, + args.governanceProposerQuorum, + args.governanceProposerRoundSize, ]); logger.verbose(`Deployed GovernanceProposer at ${governanceProposerAddress}`); @@ -382,7 +387,9 @@ export const deployL1Contracts = async ( aztecEpochDuration: args.aztecEpochDuration, targetCommitteeSize: args.aztecTargetCommitteeSize, aztecEpochProofClaimWindowInL2Slots: args.aztecEpochProofClaimWindowInL2Slots, - minimumStake: MINIMUM_STAKE, + minimumStake: args.minimumStake, + slashingQuorum: args.slashingQuorum, + slashingRoundSize: args.slashingRoundSize, }; const rollupArgs = [ feeJuicePortalAddress.toString(), @@ -396,6 +403,9 @@ export const deployL1Contracts = async ( const rollupAddress = await deployer.deploy(l1Artifacts.rollup, rollupArgs); logger.verbose(`Deployed Rollup at ${rollupAddress}`, rollupConfigArgs); + const slashFactoryAddress = await deployer.deploy(l1Artifacts.slashFactory, [rollupAddress.toString()]); + logger.info(`Deployed SlashFactory at ${slashFactoryAddress}`); + await deployer.waitForDeployments(); logger.verbose(`All core contracts have been deployed`); @@ -434,7 +444,7 @@ export const deployL1Contracts = async ( 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); + const stakeNeeded = args.minimumStake * BigInt(args.initialValidators.length); await Promise.all( [ await stakingAsset.write.mint([walletClient.account.address, stakeNeeded], {} as any), @@ -447,7 +457,7 @@ export const deployL1Contracts = async ( attester: v.toString(), proposer: v.toString(), withdrawer: v.toString(), - amount: MINIMUM_STAKE, + amount: args.minimumStake, })), ]); txHashes.push(initiateValidatorSetTxHash); @@ -560,6 +570,7 @@ export const deployL1Contracts = async ( rewardDistributorAddress, governanceProposerAddress, governanceAddress, + slashFactoryAddress, }; logger.info(`Aztec L1 contracts initialized`, l1Contracts); diff --git a/yarn-project/ethereum/src/l1_contract_addresses.ts b/yarn-project/ethereum/src/l1_contract_addresses.ts index eca35f4edea..aca32ba2dd2 100644 --- a/yarn-project/ethereum/src/l1_contract_addresses.ts +++ b/yarn-project/ethereum/src/l1_contract_addresses.ts @@ -21,6 +21,7 @@ export const L1ContractsNames = [ 'governanceProposerAddress', 'governanceAddress', 'stakingAssetAddress', + 'slashFactoryAddress', ] as const; /** Provides the directory of current L1 contract addresses */ @@ -40,6 +41,7 @@ export const L1ContractAddressesSchema = z.object({ rewardDistributorAddress: schemas.EthAddress, governanceProposerAddress: schemas.EthAddress, governanceAddress: schemas.EthAddress, + slashFactoryAddress: schemas.EthAddress, }) satisfies ZodFor; const parseEnv = (val: string) => EthAddress.fromString(val); @@ -100,4 +102,9 @@ export const l1ContractAddressesMapping: ConfigMappingsType description: 'The deployed L1 governance contract address', parseEnv, }, + slashFactoryAddress: { + env: 'SLASH_FACTORY_CONTRACT_ADDRESS', + description: 'The deployed L1 slashFactory contract address', + parseEnv, + }, }; diff --git a/yarn-project/foundation/src/config/env_var.ts b/yarn-project/foundation/src/config/env_var.ts index 76c58270b1f..2b10202bfd4 100644 --- a/yarn-project/foundation/src/config/env_var.ts +++ b/yarn-project/foundation/src/config/env_var.ts @@ -146,6 +146,7 @@ export type EnvVar = | 'SEQ_REQUIRED_CONFIRMATIONS' | 'SEQ_TX_POLLING_INTERVAL_MS' | 'SEQ_ENFORCE_TIME_TABLE' + | 'SLASH_FACTORY_CONTRACT_ADDRESS' | 'STAKING_ASSET_CONTRACT_ADDRESS' | 'REWARD_DISTRIBUTOR_CONTRACT_ADDRESS' | 'TELEMETRY' @@ -173,6 +174,11 @@ export type EnvVar = | 'AZTEC_EPOCH_DURATION' | 'AZTEC_TARGET_COMMITTEE_SIZE' | 'AZTEC_EPOCH_PROOF_CLAIM_WINDOW_IN_L2_SLOTS' + | 'AZTEC_MINIMUM_STAKE' + | 'AZTEC_SLASHING_QUORUM' + | 'AZTEC_SLASHING_ROUND_SIZE' + | 'AZTEC_GOVERNANCE_PROPOSER_QUORUM' + | 'AZTEC_GOVERNANCE_PROPOSER_ROUND_SIZE' | 'L1_GAS_LIMIT_BUFFER_PERCENTAGE' | 'L1_GAS_LIMIT_BUFFER_FIXED' | 'L1_GAS_PRICE_MIN' diff --git a/yarn-project/l1-artifacts/package.json b/yarn-project/l1-artifacts/package.json index f5469172c8e..6963e95d3cd 100644 --- a/yarn-project/l1-artifacts/package.json +++ b/yarn-project/l1-artifacts/package.json @@ -32,4 +32,4 @@ "generated" ], "types": "./dest/index.d.ts" -} \ No newline at end of file +} diff --git a/yarn-project/l1-artifacts/scripts/generate-artifacts.sh b/yarn-project/l1-artifacts/scripts/generate-artifacts.sh index 896467dfd05..7bf69aafef3 100755 --- a/yarn-project/l1-artifacts/scripts/generate-artifacts.sh +++ b/yarn-project/l1-artifacts/scripts/generate-artifacts.sh @@ -30,6 +30,10 @@ CONTRACTS=( "l1-contracts:NewGovernanceProposerPayload" "l1-contracts:LeonidasLib" "l1-contracts:ExtRollupLib" + "l1-contracts:SlashingProposer" + "l1-contracts:Slasher" + "l1-contracts:EmpireBase" + "l1-contracts:SlashFactory" ) # Read the error ABI's once and store it in COMBINED_ERRORS variable diff --git a/yarn-project/p2p/package.json b/yarn-project/p2p/package.json index a24e71fbc38..5be467e88f8 100644 --- a/yarn-project/p2p/package.json +++ b/yarn-project/p2p/package.json @@ -123,4 +123,4 @@ "engines": { "node": ">=18" } -} \ No newline at end of file +} diff --git a/yarn-project/proof-verifier/package.json b/yarn-project/proof-verifier/package.json index 1af22444af1..9a1fed2dba8 100644 --- a/yarn-project/proof-verifier/package.json +++ b/yarn-project/proof-verifier/package.json @@ -81,4 +81,4 @@ "../../foundation/src/jest/setup.mjs" ] } -} \ No newline at end of file +} diff --git a/yarn-project/protocol-contracts/package.json b/yarn-project/protocol-contracts/package.json index aab9d4b37e3..b63606a7f63 100644 --- a/yarn-project/protocol-contracts/package.json +++ b/yarn-project/protocol-contracts/package.json @@ -101,4 +101,4 @@ "engines": { "node": ">=18" } -} \ No newline at end of file +} diff --git a/yarn-project/prover-client/package.json b/yarn-project/prover-client/package.json index c395650e41e..9278090708f 100644 --- a/yarn-project/prover-client/package.json +++ b/yarn-project/prover-client/package.json @@ -109,4 +109,4 @@ "engines": { "node": ">=18" } -} \ No newline at end of file +} diff --git a/yarn-project/prover-node/package.json b/yarn-project/prover-node/package.json index 53fa2943cc4..4d0fc44fd24 100644 --- a/yarn-project/prover-node/package.json +++ b/yarn-project/prover-node/package.json @@ -96,4 +96,4 @@ "engines": { "node": ">=18" } -} \ No newline at end of file +} 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 d0dc0103bbf..51bd6ce16cc 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 @@ -44,6 +44,7 @@ async function createPXEService(): Promise { coinIssuerAddress: EthAddress.random(), rewardDistributorAddress: EthAddress.random(), governanceProposerAddress: EthAddress.random(), + slashFactoryAddress: EthAddress.random(), }; node.getL1ContractAddresses.mockResolvedValue(mockedContracts); diff --git a/yarn-project/sequencer-client/package.json b/yarn-project/sequencer-client/package.json index 1fcf01f6a85..458e96fe47c 100644 --- a/yarn-project/sequencer-client/package.json +++ b/yarn-project/sequencer-client/package.json @@ -53,6 +53,7 @@ "viem": "^2.7.15" }, "devDependencies": { + "@aztec/archiver": "workspace:^", "@aztec/kv-store": "workspace:^", "@jest/globals": "^29.5.0", "@types/jest": "^29.5.0", @@ -118,4 +119,4 @@ "../../foundation/src/jest/setup.mjs" ] } -} \ No newline at end of file +} diff --git a/yarn-project/sequencer-client/src/client/sequencer-client.ts b/yarn-project/sequencer-client/src/client/sequencer-client.ts index ba9987262c2..533e6068f8d 100644 --- a/yarn-project/sequencer-client/src/client/sequencer-client.ts +++ b/yarn-project/sequencer-client/src/client/sequencer-client.ts @@ -11,6 +11,7 @@ import { type SequencerClientConfig } from '../config.js'; import { GlobalVariableBuilder } from '../global_variable_builder/index.js'; import { L1Publisher } from '../publisher/index.js'; import { Sequencer, type SequencerConfig } from '../sequencer/index.js'; +import { type SlasherClient } from '../slasher/index.js'; import { TxValidatorFactory } from '../tx_validator/tx_validator_factory.js'; /** @@ -38,6 +39,7 @@ export class SequencerClient { validatorClient: ValidatorClient | undefined; // allowed to be undefined while we migrate p2pClient: P2P; worldStateSynchronizer: WorldStateSynchronizer; + slasherClient: SlasherClient; contractDataSource: ContractDataSource; l2BlockSource: L2BlockSource; l1ToL2MessageSource: L1ToL2MessageSource; @@ -49,6 +51,7 @@ export class SequencerClient { validatorClient, p2pClient, worldStateSynchronizer, + slasherClient, contractDataSource, l2BlockSource, l1ToL2MessageSource, @@ -71,6 +74,7 @@ export class SequencerClient { globalsBuilder, p2pClient, worldStateSynchronizer, + slasherClient, new LightweightBlockBuilderFactory(telemetryClient), l2BlockSource, l1ToL2MessageSource, diff --git a/yarn-project/sequencer-client/src/index.ts b/yarn-project/sequencer-client/src/index.ts index cb826f2c545..e352f458c1b 100644 --- a/yarn-project/sequencer-client/src/index.ts +++ b/yarn-project/sequencer-client/src/index.ts @@ -1,6 +1,7 @@ export * from './client/index.js'; export * from './config.js'; export * from './publisher/index.js'; +export * from './slasher/index.js'; // Used by the node to simulate public parts of transactions. Should these be moved to a shared library? // ISSUE(#9832) diff --git a/yarn-project/sequencer-client/src/publisher/l1-publisher.ts b/yarn-project/sequencer-client/src/publisher/l1-publisher.ts index f2580de6a6f..3708057d49b 100644 --- a/yarn-project/sequencer-client/src/publisher/l1-publisher.ts +++ b/yarn-project/sequencer-client/src/publisher/l1-publisher.ts @@ -17,24 +17,18 @@ import { type Proof, type RootRollupPublicInputs, } from '@aztec/circuits.js'; -import { - type EthereumChain, - type L1ContractsConfig, - L1TxUtils, - type L1TxUtilsConfig, - createEthereumChain, -} from '@aztec/ethereum'; +import { type EthereumChain, type L1ContractsConfig, L1TxUtils, createEthereumChain } from '@aztec/ethereum'; import { makeTuple } from '@aztec/foundation/array'; import { toHex } from '@aztec/foundation/bigint-buffer'; import { Blob } from '@aztec/foundation/blob'; import { areArraysEqual, compactArray, times } from '@aztec/foundation/collection'; import { type Signature } from '@aztec/foundation/eth-signature'; import { Fr } from '@aztec/foundation/fields'; -import { createLogger } from '@aztec/foundation/log'; +import { type Logger, createLogger } from '@aztec/foundation/log'; import { type Tuple, serializeToBuffer } from '@aztec/foundation/serialize'; import { InterruptibleSleep } from '@aztec/foundation/sleep'; import { Timer } from '@aztec/foundation/timer'; -import { ExtRollupLibAbi, GovernanceProposerAbi, LeonidasLibAbi, RollupAbi } from '@aztec/l1-artifacts'; +import { EmpireBaseAbi, ExtRollupLibAbi, LeonidasLibAbi, RollupAbi, SlasherAbi } from '@aztec/l1-artifacts'; import { type TelemetryClient } from '@aztec/telemetry-client'; import pick from 'lodash.pick'; @@ -137,6 +131,13 @@ export type L1SubmitEpochProofArgs = { proof: Proof; }; +export enum VoteType { + GOVERNANCE, + SLASHING, +} + +type GetSlashPayloadCallBack = (slotNumber: bigint) => Promise; + /** * Publishes L2 blocks to L1. This implementation does *not* retry a transaction in * the event of network congestion, but should work for local development. @@ -151,20 +152,25 @@ export class L1Publisher { private interrupted = false; private metrics: L1PublisherMetrics; - private payload: EthAddress = EthAddress.ZERO; - private myLastVote: bigint = 0n; + protected governanceLog = createLogger('sequencer:publisher:governance'); + protected governanceProposerAddress?: EthAddress; + private governancePayload: EthAddress = EthAddress.ZERO; + + protected slashingLog = createLogger('sequencer:publisher:slashing'); + protected slashingProposerAddress?: EthAddress; + private getSlashPayload?: GetSlashPayloadCallBack = undefined; + + private myLastVotes: Record = { + [VoteType.GOVERNANCE]: 0n, + [VoteType.SLASHING]: 0n, + }; protected log = createLogger('sequencer:publisher'); - protected governanceLog = createLogger('sequencer:publisher:governance'); protected rollupContract: GetContractReturnType< typeof RollupAbi, WalletClient >; - protected governanceProposerContract?: GetContractReturnType< - typeof GovernanceProposerAbi, - WalletClient - > = undefined; protected publicClient: PublicClient; protected walletClient: WalletClient; @@ -180,7 +186,7 @@ export class L1Publisher { private readonly l1TxUtils: L1TxUtils; constructor( - config: TxSenderConfig & PublisherConfig & Pick & L1TxUtilsConfig, + config: TxSenderConfig & PublisherConfig & Pick, client: TelemetryClient, ) { this.sleepTimeMs = config?.l1PublishRetryIntervalMS ?? 60_000; @@ -207,16 +213,31 @@ export class L1Publisher { }); if (l1Contracts.governanceProposerAddress) { - this.governanceProposerContract = getContract({ - address: getAddress(l1Contracts.governanceProposerAddress.toString()), - abi: GovernanceProposerAbi, - client: this.walletClient, - }); + this.governanceProposerAddress = EthAddress.fromString(l1Contracts.governanceProposerAddress.toString()); } this.l1TxUtils = new L1TxUtils(this.publicClient, this.walletClient, this.log, config); } + public registerSlashPayloadGetter(callback: GetSlashPayloadCallBack) { + this.getSlashPayload = callback; + } + + private async getSlashingProposerAddress() { + if (this.slashingProposerAddress) { + return this.slashingProposerAddress; + } + + const slasherAddress = await this.rollupContract.read.SLASHER(); + const slasher = getContract({ + address: getAddress(slasherAddress.toString()), + abi: SlasherAbi, + client: this.walletClient, + }); + this.slashingProposerAddress = EthAddress.fromString(await slasher.read.PROPOSER()); + return this.slashingProposerAddress; + } + get publisherAddress() { return this.account.address; } @@ -232,12 +253,12 @@ export class L1Publisher { }); } - public getPayLoad() { - return this.payload; + public getGovernancePayload() { + return this.governancePayload; } - public setPayload(payload: EthAddress) { - this.payload = payload; + public setGovernancePayload(payload: EthAddress) { + this.governancePayload = payload; } public getSenderAddress(): EthAddress { @@ -452,68 +473,106 @@ export class L1Publisher { calldataGas: getCalldataGasUsage(calldata), }; } - - public async castVote(slotNumber: bigint, timestamp: bigint): Promise { - if (this.payload.equals(EthAddress.ZERO)) { + public async castVote(slotNumber: bigint, timestamp: bigint, voteType: VoteType) { + // @todo This function can be optimized by doing some of the computations locally instead of calling the L1 contracts + if (this.myLastVotes[voteType] >= slotNumber) { return false; } - if (!this.governanceProposerContract) { - return false; - } + const voteConfig = async (): Promise< + { payload: EthAddress; voteContractAddress: EthAddress; logger: Logger } | undefined + > => { + if (voteType === VoteType.GOVERNANCE) { + if (this.governancePayload.equals(EthAddress.ZERO)) { + return undefined; + } + if (!this.governanceProposerAddress) { + return undefined; + } + return { + payload: this.governancePayload, + voteContractAddress: this.governanceProposerAddress, + logger: this.governanceLog, + }; + } else if (voteType === VoteType.SLASHING) { + if (!this.getSlashPayload) { + return undefined; + } + const slashingProposerAddress = await this.getSlashingProposerAddress(); + if (!slashingProposerAddress) { + return undefined; + } + + const slashPayload = await this.getSlashPayload(slotNumber); + + if (!slashPayload) { + return undefined; + } + + return { + payload: slashPayload, + voteContractAddress: slashingProposerAddress, + logger: this.slashingLog, + }; + } else { + throw new Error('Invalid vote type'); + } + }; - if (this.myLastVote >= slotNumber) { + const vConfig = await voteConfig(); + + if (!vConfig) { return false; } - // @todo This can be optimized A LOT by doing the computation instead of making calls to L1, but it is very convenient - // for when we keep changing the values and don't want to have multiple versions of the same logic implemented. + const { payload, voteContractAddress, logger } = vConfig; + + const voteContract = getContract({ + address: getAddress(voteContractAddress.toString()), + abi: EmpireBaseAbi, + client: this.walletClient, + }); const [proposer, roundNumber] = await Promise.all([ this.rollupContract.read.getProposerAt([timestamp]), - this.governanceProposerContract.read.computeRound([slotNumber]), + voteContract.read.computeRound([slotNumber]), ]); if (proposer.toLowerCase() !== this.account.address.toLowerCase()) { return false; } - const [slotForLastVote] = await this.governanceProposerContract.read.rounds([ - this.rollupContract.address, - roundNumber, - ]); + const [slotForLastVote] = await voteContract.read.rounds([this.rollupContract.address, roundNumber]); if (slotForLastVote >= slotNumber) { return false; } - // Storing these early such that a quick entry again would not send another tx, - // revert the state if there is a failure. - const cachedMyLastVote = this.myLastVote; - this.myLastVote = slotNumber; - - this.governanceLog.verbose(`Casting vote for ${this.payload}`); + const cachedMyLastVote = this.myLastVotes[voteType]; + this.myLastVotes[voteType] = slotNumber; let txHash; try { - txHash = await this.governanceProposerContract.write.vote([this.payload.toString()], { account: this.account }); + txHash = await voteContract.write.vote([payload.toString()], { + account: this.account, + }); } catch (err) { const msg = prettyLogViemErrorMsg(err); - this.governanceLog.error(`Failed to vote`, msg); - this.myLastVote = cachedMyLastVote; + logger.error(`Failed to vote`, msg); + this.myLastVotes[voteType] = cachedMyLastVote; return false; } if (txHash) { const receipt = await this.getTransactionReceipt(txHash); if (!receipt) { - this.governanceLog.warn(`Failed to get receipt for tx ${txHash}`); - this.myLastVote = cachedMyLastVote; + logger.warn(`Failed to get receipt for tx ${txHash}`); + this.myLastVotes[voteType] = cachedMyLastVote; return false; } } - this.governanceLog.info(`Cast vote for ${this.payload}`); + logger.info(`Cast vote for ${payload}`); return true; } diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts index 72498b7e86c..bef44746427 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts @@ -47,6 +47,7 @@ import { type MockProxy, mock, mockFn } from 'jest-mock-extended'; import { type GlobalVariableBuilder } from '../global_variable_builder/global_builder.js'; import { type L1Publisher } from '../publisher/l1-publisher.js'; +import { type SlasherClient } from '../slasher/index.js'; import { TxValidatorFactory } from '../tx_validator/tx_validator_factory.js'; import { Sequencer } from './sequencer.js'; import { SequencerState } from './utils.js'; @@ -188,6 +189,8 @@ describe('sequencer', () => { createBlockProposal: mockFn().mockResolvedValue(createBlockProposal()), }); + const slasherClient = mock(); + const l1GenesisTime = Math.floor(Date.now() / 1000); sequencer = new TestSubject( publisher, @@ -196,6 +199,7 @@ describe('sequencer', () => { globalVariableBuilder, p2p, worldState, + slasherClient, blockBuilderFactory, l2BlockSource, l1ToL2MessageSource, diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.ts index adc3bda49e2..eb202d865b1 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.ts @@ -35,8 +35,9 @@ import { Attributes, type TelemetryClient, type Tracer, trackSpan } from '@aztec import { type ValidatorClient } from '@aztec/validator-client'; import { type GlobalVariableBuilder } from '../global_variable_builder/global_builder.js'; -import { type L1Publisher } from '../publisher/l1-publisher.js'; +import { type L1Publisher, VoteType } from '../publisher/l1-publisher.js'; import { prettyLogViemErrorMsg } from '../publisher/utils.js'; +import { type SlasherClient } from '../slasher/slasher_client.js'; import { type TxValidatorFactory } from '../tx_validator/tx_validator_factory.js'; import { getDefaultAllowedSetupFunctions } from './allowed.js'; import { type SequencerConfig } from './config.js'; @@ -101,6 +102,7 @@ export class Sequencer { private globalsBuilder: GlobalVariableBuilder, private p2pClient: P2P, private worldState: WorldStateSynchronizer, + private slasherClient: SlasherClient, private blockBuilderFactory: BlockBuilderFactory, private l2BlockSource: L2BlockSource, private l1ToL2MessageSource: L1ToL2MessageSource, @@ -117,6 +119,9 @@ export class Sequencer { // Register the block builder with the validator client for re-execution this.validatorClient?.registerBlockBuilder(this.buildBlock.bind(this)); + + // Register the slasher on the publisher to fetch slashing payloads + this.publisher.registerSlashPayloadGetter(this.slasherClient.getSlashPayload.bind(this.slasherClient)); } get tracer(): Tracer { @@ -158,7 +163,7 @@ export class Sequencer { this.maxBlockSizeInBytes = config.maxBlockSizeInBytes; } if (config.governanceProposerPayload) { - this.publisher.setPayload(config.governanceProposerPayload); + this.publisher.setGovernancePayload(config.governanceProposerPayload); } this.enforceTimeTable = config.enforceTimeTable === true; @@ -204,6 +209,7 @@ export class Sequencer { this.log.debug(`Stopping sequencer`); await this.validatorClient?.stop(); await this.runningPromise?.stop(); + await this.slasherClient?.stop(); this.publisher.interrupt(); this.setState(SequencerState.STOPPED, 0n, true /** force */); this.log.info('Stopped sequencer'); @@ -273,7 +279,8 @@ export class Sequencer { slot, ); - void this.publisher.castVote(slot, newGlobalVariables.timestamp.toBigInt()); + void this.publisher.castVote(slot, newGlobalVariables.timestamp.toBigInt(), VoteType.GOVERNANCE); + void this.publisher.castVote(slot, newGlobalVariables.timestamp.toBigInt(), VoteType.SLASHING); if (!this.shouldProposeBlock(historicalHeader, {})) { return; diff --git a/yarn-project/sequencer-client/src/slasher/factory.ts b/yarn-project/sequencer-client/src/slasher/factory.ts new file mode 100644 index 00000000000..85decb074ea --- /dev/null +++ b/yarn-project/sequencer-client/src/slasher/factory.ts @@ -0,0 +1,22 @@ +import type { L2BlockSource } from '@aztec/circuit-types'; +import { type L1ContractsConfig, type L1ReaderConfig } from '@aztec/ethereum'; +import { createLogger } from '@aztec/foundation/log'; +import { type AztecKVStore } from '@aztec/kv-store'; +import { type DataStoreConfig } from '@aztec/kv-store/config'; +import { createStore } from '@aztec/kv-store/lmdb'; +import { type TelemetryClient } from '@aztec/telemetry-client'; +import { NoopTelemetryClient } from '@aztec/telemetry-client/noop'; + +import { SlasherClient } from './slasher_client.js'; +import { type SlasherConfig } from './slasher_client.js'; + +export const createSlasherClient = async ( + _config: SlasherConfig & DataStoreConfig & L1ContractsConfig & L1ReaderConfig, + l2BlockSource: L2BlockSource, + telemetry: TelemetryClient = new NoopTelemetryClient(), + deps: { store?: AztecKVStore } = {}, +) => { + const config = { ..._config }; + const store = deps.store ?? (await createStore('slasher', config, createLogger('slasher:lmdb'))); + return new SlasherClient(config, store, l2BlockSource, telemetry); +}; diff --git a/yarn-project/sequencer-client/src/slasher/index.ts b/yarn-project/sequencer-client/src/slasher/index.ts new file mode 100644 index 00000000000..e33e274a76b --- /dev/null +++ b/yarn-project/sequencer-client/src/slasher/index.ts @@ -0,0 +1,2 @@ +export * from './slasher_client.js'; +export { createSlasherClient } from './factory.js'; diff --git a/yarn-project/sequencer-client/src/slasher/slasher_client.test.ts b/yarn-project/sequencer-client/src/slasher/slasher_client.test.ts new file mode 100644 index 00000000000..1e070060a63 --- /dev/null +++ b/yarn-project/sequencer-client/src/slasher/slasher_client.test.ts @@ -0,0 +1,133 @@ +import { MockL2BlockSource } from '@aztec/archiver/test'; +import { L2Block } from '@aztec/circuit-types'; +import { + type L1ContractAddresses, + type L1ContractsConfig, + type L1ReaderConfig, + getL1ContractsConfigEnvVars, +} from '@aztec/ethereum'; +import { EthAddress } from '@aztec/foundation/eth-address'; +import { retryUntil } from '@aztec/foundation/retry'; +import { sleep } from '@aztec/foundation/sleep'; +import { type AztecKVStore } from '@aztec/kv-store'; +import { openTmpStore } from '@aztec/kv-store/lmdb'; + +import { expect } from '@jest/globals'; + +import { SlasherClient, type SlasherConfig } from './slasher_client.js'; + +// Most of this test are directly copied from the P2P client test. +describe('In-Memory Slasher Client', () => { + let blockSource: MockL2BlockSource; + let kvStore: AztecKVStore; + let client: SlasherClient; + let config: SlasherConfig & L1ContractsConfig & L1ReaderConfig; + + beforeEach(() => { + blockSource = new MockL2BlockSource(); + blockSource.createBlocks(100); + + const l1Config = getL1ContractsConfigEnvVars(); + + // Need some configuration here. Can be a basic bitch config really. + config = { + ...l1Config, + blockCheckIntervalMS: 100, + blockRequestBatchSize: 20, + l1Contracts: { + slashFactoryAddress: EthAddress.ZERO, + } as unknown as L1ContractAddresses, + l1RpcUrl: 'http://127.0.0.1:8545', + l1ChainId: 1, + viemPollingIntervalMS: 1000, + }; + + kvStore = openTmpStore(); + client = new SlasherClient(config, kvStore, blockSource); + }); + + const advanceToProvenBlock = async (getProvenBlockNumber: number, provenEpochNumber = getProvenBlockNumber) => { + blockSource.setProvenBlockNumber(getProvenBlockNumber); + blockSource.setProvenEpochNumber(provenEpochNumber); + await retryUntil( + () => Promise.resolve(client.getSyncedProvenBlockNum() >= getProvenBlockNumber), + 'synced', + 10, + 0.1, + ); + }; + + afterEach(async () => { + if (client.isReady()) { + await client.stop(); + } + }); + + it('can start & stop', async () => { + expect(client.isReady()).toEqual(false); + + await client.start(); + expect(client.isReady()).toEqual(true); + + await client.stop(); + expect(client.isReady()).toEqual(false); + }); + + it('restores the previous block number it was at', async () => { + await client.start(); + await client.stop(); + + const client2 = new SlasherClient(config, kvStore, blockSource); + expect(client2.getSyncedLatestBlockNum()).toEqual(client.getSyncedLatestBlockNum()); + }); + + describe('Chain prunes', () => { + it('moves the tips on a chain reorg', async () => { + blockSource.setProvenBlockNumber(0); + await client.start(); + + await advanceToProvenBlock(90); + + await expect(client.getL2Tips()).resolves.toEqual({ + latest: { number: 100, hash: expect.any(String) }, + proven: { number: 90, hash: expect.any(String) }, + finalized: { number: 90, hash: expect.any(String) }, + }); + + blockSource.removeBlocks(10); + + // give the client a chance to react to the reorg + await sleep(100); + + await expect(client.getL2Tips()).resolves.toEqual({ + latest: { number: 90, hash: expect.any(String) }, + proven: { number: 90, hash: expect.any(String) }, + finalized: { number: 90, hash: expect.any(String) }, + }); + + blockSource.addBlocks([L2Block.random(91), L2Block.random(92)]); + + // give the client a chance to react to the new blocks + await sleep(100); + + await expect(client.getL2Tips()).resolves.toEqual({ + latest: { number: 92, hash: expect.any(String) }, + proven: { number: 90, hash: expect.any(String) }, + finalized: { number: 90, hash: expect.any(String) }, + }); + + const slashEvents = (client as any).slashEvents; + + const slotsIntoRound = BigInt(90) % BigInt(config.slashingRoundSize); + const toNext = slotsIntoRound == 0n ? 0n : BigInt(config.slashingRoundSize) - slotsIntoRound; + const expectedLifetime = BigInt(90) + toNext + BigInt(config.slashingRoundSize); + + expect(slashEvents).toHaveLength(1); + expect(slashEvents[0]).toEqual({ + epoch: BigInt(Math.floor(90 / config.aztecEpochDuration)), + amount: 0n, + lifetime: expectedLifetime, + }); + }); + }); +}); diff --git a/yarn-project/sequencer-client/src/slasher/slasher_client.ts b/yarn-project/sequencer-client/src/slasher/slasher_client.ts new file mode 100644 index 00000000000..cc5e60e25ea --- /dev/null +++ b/yarn-project/sequencer-client/src/slasher/slasher_client.ts @@ -0,0 +1,402 @@ +import { + type L2Block, + type L2BlockId, + type L2BlockSource, + L2BlockStream, + type L2BlockStreamEvent, + type L2Tips, +} from '@aztec/circuit-types'; +import { INITIAL_L2_BLOCK_NUM } from '@aztec/circuits.js/constants'; +import { type L1ContractsConfig, type L1ReaderConfig, createEthereumChain } from '@aztec/ethereum'; +import { EthAddress } from '@aztec/foundation/eth-address'; +import { createLogger } from '@aztec/foundation/log'; +import { type AztecKVStore, type AztecMap, type AztecSingleton } from '@aztec/kv-store'; +import { SlashFactoryAbi } from '@aztec/l1-artifacts'; +import { type TelemetryClient, WithTracer } from '@aztec/telemetry-client'; +import { NoopTelemetryClient } from '@aztec/telemetry-client/noop'; + +import { + type Chain, + type GetContractReturnType, + type HttpTransport, + type PublicClient, + createPublicClient, + getAddress, + getContract, + http, +} from 'viem'; + +/** + * Enum defining the possible states of the Slasher client. + */ +export enum SlasherClientState { + IDLE, + SYNCHING, + RUNNING, + STOPPED, +} + +/** + * The synchronization status of the Slasher client. + */ +export interface SlasherSyncState { + /** + * The current state of the slasher client. + */ + state: SlasherClientState; + /** + * The block number that the slasher client is synced to. + */ + syncedToL2Block: L2BlockId; +} + +export interface SlasherConfig { + blockCheckIntervalMS: number; + blockRequestBatchSize: number; +} + +type SlashEvent = { + epoch: bigint; + amount: bigint; + lifetime: bigint; +}; + +/** + * @notice A Hypomeiones slasher client implementation + * + * Hypomeiones: a class of individuals in ancient Sparta who were considered inferior or lesser citizens compared + * to the full Spartan citizens. + * + * The implementation here is less than ideal. It exists, not to be the end all be all, but to show that + * slashing can be done with this mechanism. + * + * The implementation is VERY brute in the sense that it only looks for pruned blocks and then tries to slash + * the full committee of that. + * If it sees a prune, it will mark the full epoch as "to be slashed". + * + * Also, it is not particularly smart around what it should if there were to be multiple slashing events. + * + * A few improvements: + * - Only vote on the proposal if it is possible to reach, e.g., if 6 votes are needed and only 4 slots are left don't vote. + * - Stop voting on a payload once it is processed. + * - Only vote on the proposal if it have not already been executed + * - Caveat, we need to fully decide if it is acceptable to have the same payload address multiple times. In the current + * slash factory that could mean slashing the same committee for the same error multiple times. + * - Decide how to deal with multiple slashing events in the same round. + * - This could be that multiple epochs are pruned in the same round, but with the current naive implementation we could end up + * slashing only the first, because the "lifetime" of the second would have passed after that vote + */ +export class SlasherClient extends WithTracer { + private currentState = SlasherClientState.IDLE; + private syncPromise = Promise.resolve(); + private syncResolve?: () => void = undefined; + private latestBlockNumberAtStart = -1; + private provenBlockNumberAtStart = -1; + + private synchedBlockHashes: AztecMap; + private synchedLatestBlockNumber: AztecSingleton; + private synchedProvenBlockNumber: AztecSingleton; + + private blockStream; + + private slashEvents: SlashEvent[] = []; + + protected slashFactoryContract?: GetContractReturnType> = + undefined; + + // The amount to slash for a prune. + // Note that we set it to 0, such that no actual slashing will happen, but the event will be fired, + // showing that the slashing mechanism is working. + private slashingAmount: bigint = 0n; + + constructor( + private config: SlasherConfig & L1ContractsConfig & L1ReaderConfig, + private store: AztecKVStore, + private l2BlockSource: L2BlockSource, + telemetry: TelemetryClient = new NoopTelemetryClient(), + private log = createLogger('slasher'), + ) { + super(telemetry, 'slasher'); + + this.blockStream = new L2BlockStream(l2BlockSource, this, this, createLogger('slasher:block_stream'), { + batchSize: config.blockRequestBatchSize, + pollIntervalMS: config.blockCheckIntervalMS, + }); + + this.synchedBlockHashes = store.openMap('slasher_block_hashes'); + this.synchedLatestBlockNumber = store.openSingleton('slasher_last_l2_block'); + this.synchedProvenBlockNumber = store.openSingleton('slasher_last_proven_l2_block'); + + if (config.l1Contracts.slashFactoryAddress && config.l1Contracts.slashFactoryAddress !== EthAddress.ZERO) { + const chain = createEthereumChain(config.l1RpcUrl, config.l1ChainId); + const publicClient = createPublicClient({ + chain: chain.chainInfo, + transport: http(chain.rpcUrl), + pollingInterval: config.viemPollingIntervalMS, + }); + + this.slashFactoryContract = getContract({ + address: getAddress(config.l1Contracts.slashFactoryAddress.toString()), + abi: SlashFactoryAbi, + client: publicClient, + }); + } else { + this.log.warn('No slash factory address found, slashing will not be enabled'); + } + + this.log.info(`Slasher client initialized`); + } + + // This is where we should put a bunch of the improvements mentioned earlier. + public async getSlashPayload(slotNumber: bigint): Promise { + if (!this.slashFactoryContract) { + return undefined; + } + + // As long as the slot is greater than the lifetime, we want to keep deleting the first element + // since it will not make sense to include anymore. + while (this.slashEvents.length > 0 && this.slashEvents[0].lifetime < slotNumber) { + this.slashEvents.shift(); + } + + if (this.slashEvents.length == 0) { + return undefined; + } + + const slashEvent = this.slashEvents[0]; + + const [payloadAddress, isDeployed] = await this.slashFactoryContract.read.getAddressAndIsDeployed([ + slashEvent.epoch, + slashEvent.amount, + ]); + + if (!isDeployed) { + // The proposal cannot be executed until it is deployed + this.log.verbose(`Voting on not yet deployed payload: ${payloadAddress}`); + } + + return EthAddress.fromString(payloadAddress); + } + + public getL2BlockHash(number: number): Promise { + return Promise.resolve(this.synchedBlockHashes.get(number)); + } + + public getL2Tips(): Promise { + const latestBlockNumber = this.getSyncedLatestBlockNum(); + let latestBlockHash: string | undefined; + const provenBlockNumber = this.getSyncedProvenBlockNum(); + let provenBlockHash: string | undefined; + + if (latestBlockNumber > 0) { + latestBlockHash = this.synchedBlockHashes.get(latestBlockNumber); + if (typeof latestBlockHash === 'undefined') { + this.log.warn(`Block hash for latest block ${latestBlockNumber} not found`); + throw new Error(); + } + } + + if (provenBlockNumber > 0) { + provenBlockHash = this.synchedBlockHashes.get(provenBlockNumber); + if (typeof provenBlockHash === 'undefined') { + this.log.warn(`Block hash for proven block ${provenBlockNumber} not found`); + throw new Error(); + } + } + + return Promise.resolve({ + latest: { hash: latestBlockHash!, number: latestBlockNumber }, + proven: { hash: provenBlockHash!, number: provenBlockNumber }, + finalized: { hash: provenBlockHash!, number: provenBlockNumber }, + }); + } + + public async handleBlockStreamEvent(event: L2BlockStreamEvent): Promise { + this.log.debug(`Handling block stream event ${event.type}`); + switch (event.type) { + case 'blocks-added': + await this.handleLatestL2Blocks(event.blocks); + break; + case 'chain-finalized': + // TODO (alexg): I think we can prune the block hashes map here + break; + case 'chain-proven': { + const from = this.getSyncedProvenBlockNum() + 1; + const limit = event.blockNumber - from + 1; + await this.handleProvenL2Blocks(await this.l2BlockSource.getBlocks(from, limit)); + break; + } + case 'chain-pruned': + await this.handlePruneL2Blocks(event.blockNumber); + break; + default: { + const _: never = event; + break; + } + } + } + + public async start() { + if (this.currentState === SlasherClientState.STOPPED) { + throw new Error('Slasher already stopped'); + } + if (this.currentState !== SlasherClientState.IDLE) { + return this.syncPromise; + } + + // get the current latest block numbers + this.latestBlockNumberAtStart = await this.l2BlockSource.getBlockNumber(); + this.provenBlockNumberAtStart = await this.l2BlockSource.getProvenBlockNumber(); + + const syncedLatestBlock = this.getSyncedLatestBlockNum() + 1; + const syncedProvenBlock = this.getSyncedProvenBlockNum() + 1; + + // if there are blocks to be retrieved, go to a synching state + if (syncedLatestBlock <= this.latestBlockNumberAtStart || syncedProvenBlock <= this.provenBlockNumberAtStart) { + this.setCurrentState(SlasherClientState.SYNCHING); + this.syncPromise = new Promise(resolve => { + this.syncResolve = resolve; + }); + this.log.verbose(`Starting sync from ${syncedLatestBlock} (last proven ${syncedProvenBlock})`); + } else { + // if no blocks to be retrieved, go straight to running + this.setCurrentState(SlasherClientState.RUNNING); + this.syncPromise = Promise.resolve(); + this.log.verbose(`Block ${syncedLatestBlock} (proven ${syncedProvenBlock}) already beyond current block`); + } + + this.blockStream.start(); + this.log.verbose(`Started block downloader from block ${syncedLatestBlock}`); + + return this.syncPromise; + } + + /** + * Allows consumers to stop the instance of the slasher client. + * 'ready' will now return 'false' and the running promise that keeps the client synced is interrupted. + */ + public async stop() { + this.log.debug('Stopping Slasher client...'); + await this.blockStream.stop(); + this.log.debug('Stopped block downloader'); + this.setCurrentState(SlasherClientState.STOPPED); + this.log.info('Slasher client stopped.'); + } + + /** + * Public function to check if the slasher client is fully synced and ready to receive txs. + * @returns True if the slasher client is ready to receive txs. + */ + public isReady() { + return this.currentState === SlasherClientState.RUNNING; + } + + /** + * Public function to check the latest block number that the slasher client is synced to. + * @returns Block number of latest L2 Block we've synced with. + */ + public getSyncedLatestBlockNum() { + return this.synchedLatestBlockNumber.get() ?? INITIAL_L2_BLOCK_NUM - 1; + } + + /** + * Public function to check the latest proven block number that the slasher client is synced to. + * @returns Block number of latest proven L2 Block we've synced with. + */ + public getSyncedProvenBlockNum() { + return this.synchedProvenBlockNumber.get() ?? INITIAL_L2_BLOCK_NUM - 1; + } + + /** + * Method to check the status of the slasher client. + * @returns Information about slasher client status: state & syncedToBlockNum. + */ + public async getStatus(): Promise { + const blockNumber = this.getSyncedLatestBlockNum(); + const blockHash = + blockNumber == 0 + ? '' + : await this.l2BlockSource.getBlockHeader(blockNumber).then(header => header?.hash().toString()); + return Promise.resolve({ + state: this.currentState, + syncedToL2Block: { number: blockNumber, hash: blockHash }, + } as SlasherSyncState); + } + + /** + * Handles new blocks + * @param blocks - A list of blocks that the slasher client needs to store block hashes for + * @returns Empty promise. + */ + private async handleLatestL2Blocks(blocks: L2Block[]): Promise { + if (!blocks.length) { + return Promise.resolve(); + } + + const lastBlockNum = blocks[blocks.length - 1].number; + await Promise.all(blocks.map(block => this.synchedBlockHashes.set(block.number, block.hash().toString()))); + await this.synchedLatestBlockNumber.set(lastBlockNum); + this.log.debug(`Synched to latest block ${lastBlockNum}`); + this.startServiceIfSynched(); + } + + /** + * Handles new proven blocks by updating the proven block number + * @param blocks - A list of proven L2 blocks. + * @returns Empty promise. + */ + private async handleProvenL2Blocks(blocks: L2Block[]): Promise { + if (!blocks.length) { + return Promise.resolve(); + } + const lastBlockNum = blocks[blocks.length - 1].number; + await this.synchedProvenBlockNumber.set(lastBlockNum); + this.log.debug(`Synched to proven block ${lastBlockNum}`); + + this.startServiceIfSynched(); + } + + private async handlePruneL2Blocks(latestBlock: number): Promise { + const blockHeader = await this.l2BlockSource.getBlockHeader(latestBlock); + const slotNumber = blockHeader ? blockHeader.globalVariables.slotNumber.toBigInt() : BigInt(0); + const epochNumber = slotNumber / BigInt(this.config.aztecEpochDuration); + this.log.info(`Detected chain prune. Punishing the validators at epoch ${epochNumber}`); + + // Set the lifetime such that we have a full round that we could vote throughout. + const slotsIntoRound = slotNumber % BigInt(this.config.slashingRoundSize); + const toNext = slotsIntoRound == 0n ? 0n : BigInt(this.config.slashingRoundSize) - slotsIntoRound; + + const lifetime = slotNumber + toNext + BigInt(this.config.slashingRoundSize); + + this.slashEvents.push({ + epoch: epochNumber, + amount: this.slashingAmount, + lifetime, + }); + + await this.synchedLatestBlockNumber.set(latestBlock); + } + + private startServiceIfSynched() { + if ( + this.currentState === SlasherClientState.SYNCHING && + this.getSyncedLatestBlockNum() >= this.latestBlockNumberAtStart && + this.getSyncedProvenBlockNum() >= this.provenBlockNumberAtStart + ) { + this.log.debug(`Synched to blocks at start`); + this.setCurrentState(SlasherClientState.RUNNING); + if (this.syncResolve !== undefined) { + this.syncResolve(); + } + } + } + + /** + * Method to set the value of the current state. + * @param newState - New state value. + */ + private setCurrentState(newState: SlasherClientState) { + this.currentState = newState; + this.log.debug(`Moved to state ${SlasherClientState[this.currentState]}`); + } +} diff --git a/yarn-project/sequencer-client/tsconfig.json b/yarn-project/sequencer-client/tsconfig.json index 9a8615c0299..4161156625e 100644 --- a/yarn-project/sequencer-client/tsconfig.json +++ b/yarn-project/sequencer-client/tsconfig.json @@ -60,6 +60,9 @@ { "path": "../world-state" }, + { + "path": "../archiver" + }, { "path": "../kv-store" } diff --git a/yarn-project/validator-client/package.json b/yarn-project/validator-client/package.json index e14fc7e52c3..b760d9aedb6 100644 --- a/yarn-project/validator-client/package.json +++ b/yarn-project/validator-client/package.json @@ -97,4 +97,4 @@ "engines": { "node": ">=18" } -} \ No newline at end of file +} diff --git a/yarn-project/world-state/package.json b/yarn-project/world-state/package.json index b3cc7dadddd..ec920a94cfc 100644 --- a/yarn-project/world-state/package.json +++ b/yarn-project/world-state/package.json @@ -102,4 +102,4 @@ "engines": { "node": ">=18" } -} \ No newline at end of file +} diff --git a/yarn-project/yarn.lock b/yarn-project/yarn.lock index 3bc9084e506..ab634c1dc24 100644 --- a/yarn-project/yarn.lock +++ b/yarn-project/yarn.lock @@ -1154,6 +1154,7 @@ __metadata: version: 0.0.0-use.local resolution: "@aztec/sequencer-client@workspace:sequencer-client" dependencies: + "@aztec/archiver": "workspace:^" "@aztec/aztec.js": "workspace:^" "@aztec/bb-prover": "workspace:^" "@aztec/circuit-types": "workspace:^"