diff --git a/l1-contracts/src/core/Rollup.sol b/l1-contracts/src/core/Rollup.sol index 5313180b6fd2..fb1c72b14e48 100644 --- a/l1-contracts/src/core/Rollup.sol +++ b/l1-contracts/src/core/Rollup.sol @@ -461,12 +461,26 @@ contract Rollup is IStaking, IValidatorSelection, IRollup, RollupCore { return rollupStore.epochRewards[_epoch].rewards; } + /** + * @notice Get the rewards for a specific prover for a given epoch + * BEWARE! If the epoch is not past its deadline, this value is the "current" value + * and could change if a provers proves a longer series of blocks. + * + * @param _epoch - The epoch to get the rewards for + * @param _prover - The prover to get the rewards for + * + * @return The rewards for the specific prover for the given epoch + */ function getSpecificProverRewardsForEpoch(Epoch _epoch, address _prover) external view override(IRollup) returns (uint256) { + if (rollupStore.proverClaimed[_prover][_epoch]) { + return 0; + } + EpochRewards storage er = rollupStore.epochRewards[_epoch]; uint256 length = er.longestProvenLength; diff --git a/l1-contracts/src/core/RollupCore.sol b/l1-contracts/src/core/RollupCore.sol index 0e33d1b46bd3..6272fa1010d6 100644 --- a/l1-contracts/src/core/RollupCore.sol +++ b/l1-contracts/src/core/RollupCore.sol @@ -199,6 +199,47 @@ contract RollupCore is rollupStore.provingCostPerMana = _provingCostPerMana; } + function claimSequencerRewards(address _recipient) + external + override(IRollupCore) + returns (uint256) + { + uint256 amount = rollupStore.sequencerRewards[msg.sender]; + rollupStore.sequencerRewards[msg.sender] = 0; + ASSET.transfer(_recipient, amount); + + return amount; + } + + function claimProverRewards(address _recipient, Epoch[] memory _epochs) + external + override(IRollupCore) + returns (uint256) + { + Slot currentSlot = Timestamp.wrap(block.timestamp).slotFromTimestamp(); + uint256 accumulatedRewards = 0; + for (uint256 i = 0; i < _epochs.length; i++) { + Slot deadline = _epochs[i].toSlots() + Slot.wrap(PROOF_SUBMISSION_WINDOW); + require(deadline < currentSlot, Errors.Rollup__NotPastDeadline(deadline, currentSlot)); + + // We can use fancier bitmaps for performance + require( + !rollupStore.proverClaimed[msg.sender][_epochs[i]], + Errors.Rollup__AlreadyClaimed(msg.sender, _epochs[i]) + ); + rollupStore.proverClaimed[msg.sender][_epochs[i]] = true; + + EpochRewards storage e = rollupStore.epochRewards[_epochs[i]]; + if (e.subEpoch[e.longestProvenLength].hasSubmitted[msg.sender]) { + accumulatedRewards += (e.rewards / e.subEpoch[e.longestProvenLength].summedCount); + } + } + + ASSET.transfer(_recipient, accumulatedRewards); + + return accumulatedRewards; + } + function deposit(address _attester, address _proposer, address _withdrawer, uint256 _amount) external override(IStakingCore) @@ -338,7 +379,10 @@ contract RollupCore is interim.deadline = startEpoch.toSlots() + Slot.wrap(PROOF_SUBMISSION_WINDOW); require( - interim.deadline >= Timestamp.wrap(block.timestamp).slotFromTimestamp(), "past deadline" + interim.deadline >= Timestamp.wrap(block.timestamp).slotFromTimestamp(), + Errors.Rollup__PastDeadline( + interim.deadline, Timestamp.wrap(block.timestamp).slotFromTimestamp() + ) ); // By making sure that the previous block is in another epoch, we know that we were @@ -611,23 +655,6 @@ contract RollupCore is return rollupStore.blocks[_blockNumber].slotNumber.epochFromSlot(); } - /** - * @notice Get the epoch that should be proven - * - * @dev This is the epoch that should be proven. It does so by getting the epoch of the block - * following the last proven block. If there is no such block (i.e. the pending chain is - * the same as the proven chain), then revert. - * - * @return uint256 - The epoch to prove - */ - function getEpochToProve() public view override(IRollupCore) returns (Epoch) { - require( - rollupStore.tips.provenBlockNumber != rollupStore.tips.pendingBlockNumber, - Errors.Rollup__NoEpochToProve() - ); - return getEpochForBlock(rollupStore.tips.provenBlockNumber + 1); - } - function canPrune() public view override(IRollupCore) returns (bool) { return canPruneAtTime(Timestamp.wrap(block.timestamp)); } diff --git a/l1-contracts/src/core/interfaces/IRollup.sol b/l1-contracts/src/core/interfaces/IRollup.sol index 16110c624e1e..864f20504d16 100644 --- a/l1-contracts/src/core/interfaces/IRollup.sol +++ b/l1-contracts/src/core/interfaces/IRollup.sol @@ -67,6 +67,9 @@ struct RollupStore { IVerifier epochProofVerifier; mapping(address => uint256) sequencerRewards; mapping(Epoch => EpochRewards) epochRewards; + // @todo Below can be optimised with a bitmap as we can benefit from provers likely proving for epochs close + // to one another. + mapping(address prover => mapping(Epoch epoch => bool claimed)) proverClaimed; EthValue provingCostPerMana; } @@ -96,6 +99,11 @@ interface IRollupCore { event L2ProofVerified(uint256 indexed blockNumber, bytes32 indexed proverId); event PrunedPending(uint256 provenBlockNumber, uint256 pendingBlockNumber); + function claimSequencerRewards(address _recipient) external returns (uint256); + function claimProverRewards(address _recipient, Epoch[] memory _epochs) + external + returns (uint256); + function prune() external; function updateL1GasFeeOracle() external; @@ -124,7 +132,6 @@ interface IRollupCore { function canPrune() external view returns (bool); function canPruneAtTime(Timestamp _ts) external view returns (bool); - function getEpochToProve() external view returns (Epoch); function getEpochForBlock(uint256 _blockNumber) external view returns (Epoch); } diff --git a/l1-contracts/src/core/libraries/Errors.sol b/l1-contracts/src/core/libraries/Errors.sol index cfd9cd9d7502..7cfb6338be0e 100644 --- a/l1-contracts/src/core/libraries/Errors.sol +++ b/l1-contracts/src/core/libraries/Errors.sol @@ -79,6 +79,9 @@ library Errors { error Rollup__StartAndEndNotSameEpoch(Epoch start, Epoch end); error Rollup__StartIsNotFirstBlockOfEpoch(); error Rollup__StartIsNotBuildingOnProven(); + error Rollup__AlreadyClaimed(address prover, Epoch epoch); + error Rollup__NotPastDeadline(Slot deadline, Slot currentSlot); + error Rollup__PastDeadline(Slot deadline, Slot currentSlot); // HeaderLib error HeaderLib__InvalidHeaderSize(uint256 expected, uint256 actual); // 0xf3ccb247 diff --git a/l1-contracts/test/MultiProof.t.sol b/l1-contracts/test/MultiProof.t.sol index a71fdbdd1244..2733dbd6d3a8 100644 --- a/l1-contracts/test/MultiProof.t.sol +++ b/l1-contracts/test/MultiProof.t.sol @@ -45,6 +45,8 @@ contract MultiProofTest is RollupBase { uint256 internal SLOT_DURATION; uint256 internal EPOCH_DURATION; + address internal sequencer = address(bytes20("sequencer")); + constructor() { TimeLib.initialize( block.timestamp, TestConstants.AZTEC_SLOT_DURATION, TestConstants.AZTEC_EPOCH_DURATION @@ -115,8 +117,7 @@ contract MultiProofTest is RollupBase { emit log_named_uint("proven block number", provenBlockNumber); emit log_named_uint("pending block number", pendingBlockNumber); - address[2] memory provers = [address(bytes20("lasse")), address(bytes20("mitch"))]; - address sequencer = address(bytes20("sequencer")); + address[2] memory provers = [address(bytes20("alice")), address(bytes20("bob"))]; emit log_named_decimal_uint("sequencer rewards", rollup.getSequencerRewards(sequencer), 18); emit log_named_decimal_uint( @@ -141,20 +142,77 @@ contract MultiProofTest is RollupBase { } } - function testMultiProof() public setUpFor("mixed_block_1") { + function testMultipleProvers() public setUpFor("mixed_block_1") { + address alice = address(bytes20("alice")); + address bob = address(bytes20("bob")); + _proposeBlock("mixed_block_1", 1, 15e6); _proposeBlock("mixed_block_2", 2, 15e6); assertEq(rollup.getProvenBlockNumber(), 0, "Block already proven"); string memory name = "mixed_block_"; - _proveBlocks(name, 1, 1, address(bytes20("lasse"))); - _proveBlocks(name, 1, 1, address(bytes20("mitch"))); - _proveBlocks(name, 1, 2, address(bytes20("mitch"))); + _proveBlocks(name, 1, 1, alice); + _proveBlocks(name, 1, 1, bob); + _proveBlocks(name, 1, 2, bob); logStatus(); + assertTrue(rollup.getHasSubmitted(Epoch.wrap(0), 1, alice)); + assertFalse(rollup.getHasSubmitted(Epoch.wrap(0), 2, alice)); + assertTrue(rollup.getHasSubmitted(Epoch.wrap(0), 1, bob)); + assertTrue(rollup.getHasSubmitted(Epoch.wrap(0), 2, bob)); + assertEq(rollup.getProvenBlockNumber(), 2, "Block not proven"); + + { + uint256 sequencerRewards = rollup.getSequencerRewards(sequencer); + assertGt(sequencerRewards, 0, "Sequencer rewards is zero"); + vm.prank(sequencer); + uint256 sequencerRewardsClaimed = rollup.claimSequencerRewards(sequencer); + assertEq(sequencerRewardsClaimed, sequencerRewards, "Sequencer rewards not claimed"); + assertEq(rollup.getSequencerRewards(sequencer), 0, "Sequencer rewards not zeroed"); + } + + Epoch[] memory epochs = new Epoch[](1); + epochs[0] = Epoch.wrap(0); + + { + uint256 aliceRewards = rollup.getSpecificProverRewardsForEpoch(Epoch.wrap(0), alice); + assertEq(aliceRewards, 0, "Alice rewards not zero"); + } + + { + uint256 bobRewards = rollup.getSpecificProverRewardsForEpoch(Epoch.wrap(0), bob); + assertGt(bobRewards, 0, "Bob rewards is zero"); + + vm.expectRevert( + abi.encodeWithSelector( + Errors.Rollup__NotPastDeadline.selector, TestConstants.AZTEC_PROOF_SUBMISSION_WINDOW, 2 + ) + ); + vm.prank(bob); + rollup.claimProverRewards(bob, epochs); + + vm.warp( + Timestamp.unwrap( + rollup.getTimestampForSlot(Slot.wrap(TestConstants.AZTEC_PROOF_SUBMISSION_WINDOW + 1)) + ) + ); + vm.prank(bob); + uint256 bobRewardsClaimed = rollup.claimProverRewards(bob, epochs); + + assertEq(bobRewardsClaimed, bobRewards, "Bob rewards not claimed"); + assertEq( + rollup.getSpecificProverRewardsForEpoch(Epoch.wrap(0), bob), 0, "Bob rewards not zeroed" + ); + + vm.expectRevert( + abi.encodeWithSelector(Errors.Rollup__AlreadyClaimed.selector, bob, Epoch.wrap(0)) + ); + vm.prank(bob); + rollup.claimProverRewards(bob, epochs); + } } function testNoHolesInProvenBlocks() public setUpFor("mixed_block_1") { @@ -166,7 +224,7 @@ contract MultiProofTest is RollupBase { name, 2, 2, - address(bytes20("lasse")), + address(bytes20("alice")), abi.encodeWithSelector(Errors.Rollup__StartIsNotBuildingOnProven.selector) ); } @@ -180,7 +238,7 @@ contract MultiProofTest is RollupBase { name, 1, 2, - address(bytes20("lasse")), + address(bytes20("alice")), abi.encodeWithSelector(Errors.Rollup__StartAndEndNotSameEpoch.selector, 0, 1) ); } diff --git a/l1-contracts/test/Rollup.t.sol b/l1-contracts/test/Rollup.t.sol index 2304a4b48616..4901b8e7c247 100644 --- a/l1-contracts/test/Rollup.t.sol +++ b/l1-contracts/test/Rollup.t.sol @@ -281,7 +281,6 @@ contract RollupTest is RollupBase { function testPruneDuringPropose() public setUpFor("mixed_block_1") { _proposeBlock("mixed_block_1", 1); - assertEq(rollup.getEpochToProve(), 0, "Invalid epoch to prove"); // the same block is proposed, with the diff in slot number. _proposeBlock("mixed_block_1", rollup.getProofSubmissionWindow() + 1); diff --git a/l1-contracts/test/fees/FeeRollup.t.sol b/l1-contracts/test/fees/FeeRollup.t.sol index d7c46320c40d..224a9eb5d770 100644 --- a/l1-contracts/test/fees/FeeRollup.t.sol +++ b/l1-contracts/test/fees/FeeRollup.t.sol @@ -141,7 +141,7 @@ contract FeeRollupTest is FeeModelTestPoints, DecoderBase { aztecSlotDuration: SLOT_DURATION, aztecEpochDuration: EPOCH_DURATION, targetCommitteeSize: 48, - aztecProofSubmissionWindow: EPOCH_DURATION * 2, + aztecProofSubmissionWindow: EPOCH_DURATION * 2 - 1, 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 4e7a0ca4f21c..353021ed012e 100644 --- a/l1-contracts/test/harnesses/TestConstants.sol +++ b/l1-contracts/test/harnesses/TestConstants.sol @@ -8,7 +8,7 @@ library TestConstants { uint256 internal constant AZTEC_SLOT_DURATION = 24; uint256 internal constant AZTEC_EPOCH_DURATION = 16; uint256 internal constant AZTEC_TARGET_COMMITTEE_SIZE = 48; - uint256 internal constant AZTEC_PROOF_SUBMISSION_WINDOW = 32; + uint256 internal constant AZTEC_PROOF_SUBMISSION_WINDOW = AZTEC_EPOCH_DURATION * 2 - 1; 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/yarn-project/end-to-end/src/e2e_p2p/slashing.test.ts b/yarn-project/end-to-end/src/e2e_p2p/slashing.test.ts index ff37335e127e..e6fd68c2489f 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/slashing.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/slashing.test.ts @@ -36,7 +36,7 @@ describe('e2e_p2p_slashing', () => { metricsPort: shouldCollectMetrics(), initialConfig: { aztecEpochDuration: 1, - aztecProofSubmissionWindow: 2, + aztecProofSubmissionWindow: 1, slashingQuorum, slashingRoundSize, }, diff --git a/yarn-project/end-to-end/src/e2e_simple.test.ts b/yarn-project/end-to-end/src/e2e_simple.test.ts index f2a1e59b4d7d..84a2f2772dae 100644 --- a/yarn-project/end-to-end/src/e2e_simple.test.ts +++ b/yarn-project/end-to-end/src/e2e_simple.test.ts @@ -36,7 +36,7 @@ describe('e2e_simple', () => { blockCheckIntervalMS: 200, minTxsPerBlock: 1, aztecEpochDuration: 8, - aztecProofSubmissionWindow: 16, + aztecProofSubmissionWindow: 15, aztecSlotDuration: 12, ethereumSlotDuration: 12, startProverNode: true, diff --git a/yarn-project/ethereum/src/config.ts b/yarn-project/ethereum/src/config.ts index 95ea3480b8cd..20b7d43d901b 100644 --- a/yarn-project/ethereum/src/config.ts +++ b/yarn-project/ethereum/src/config.ts @@ -35,7 +35,7 @@ export const DefaultL1ContractsConfig = { aztecSlotDuration: 24, aztecEpochDuration: 16, aztecTargetCommitteeSize: 48, - aztecProofSubmissionWindow: 32, // you have a full epoch to submit a proof after the epoch to prove ends + aztecProofSubmissionWindow: 31, // you have a full epoch to submit a proof after the epoch to prove ends minimumStake: BigInt(100e18), slashingQuorum: 6, slashingRoundSize: 10, diff --git a/yarn-project/ethereum/src/contracts/rollup.ts b/yarn-project/ethereum/src/contracts/rollup.ts index 1def33e1440f..638c2fd77448 100644 --- a/yarn-project/ethereum/src/contracts/rollup.ts +++ b/yarn-project/ethereum/src/contracts/rollup.ts @@ -231,14 +231,6 @@ export class RollupContract { return this.rollup.read.getEpochProofPublicInputs(args); } - public async getEpochToProve(): Promise { - try { - return await this.rollup.read.getEpochToProve(); - } catch (err: unknown) { - throw formatViemError(err); - } - } - public async validateHeader( args: readonly [ `0x${string}`,