Skip to content

Commit

Permalink
feat: make rewards claimable (#11975)
Browse files Browse the repository at this point in the history
Fixes #11949.
  • Loading branch information
LHerskind authored Feb 18, 2025
1 parent 74ede22 commit 8aee21d
Show file tree
Hide file tree
Showing 12 changed files with 141 additions and 41 deletions.
14 changes: 14 additions & 0 deletions l1-contracts/src/core/Rollup.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
63 changes: 45 additions & 18 deletions l1-contracts/src/core/RollupCore.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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));
}
Expand Down
9 changes: 8 additions & 1 deletion l1-contracts/src/core/interfaces/IRollup.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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);
}
Expand Down
3 changes: 3 additions & 0 deletions l1-contracts/src/core/libraries/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
74 changes: 66 additions & 8 deletions l1-contracts/test/MultiProof.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -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") {
Expand All @@ -166,7 +224,7 @@ contract MultiProofTest is RollupBase {
name,
2,
2,
address(bytes20("lasse")),
address(bytes20("alice")),
abi.encodeWithSelector(Errors.Rollup__StartIsNotBuildingOnProven.selector)
);
}
Expand All @@ -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)
);
}
Expand Down
1 change: 0 additions & 1 deletion l1-contracts/test/Rollup.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion l1-contracts/test/fees/FeeRollup.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion l1-contracts/test/harnesses/TestConstants.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion yarn-project/end-to-end/src/e2e_p2p/slashing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ describe('e2e_p2p_slashing', () => {
metricsPort: shouldCollectMetrics(),
initialConfig: {
aztecEpochDuration: 1,
aztecProofSubmissionWindow: 2,
aztecProofSubmissionWindow: 1,
slashingQuorum,
slashingRoundSize,
},
Expand Down
2 changes: 1 addition & 1 deletion yarn-project/end-to-end/src/e2e_simple.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ describe('e2e_simple', () => {
blockCheckIntervalMS: 200,
minTxsPerBlock: 1,
aztecEpochDuration: 8,
aztecProofSubmissionWindow: 16,
aztecProofSubmissionWindow: 15,
aztecSlotDuration: 12,
ethereumSlotDuration: 12,
startProverNode: true,
Expand Down
2 changes: 1 addition & 1 deletion yarn-project/ethereum/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
8 changes: 0 additions & 8 deletions yarn-project/ethereum/src/contracts/rollup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,14 +231,6 @@ export class RollupContract {
return this.rollup.read.getEpochProofPublicInputs(args);
}

public async getEpochToProve(): Promise<bigint | undefined> {
try {
return await this.rollup.read.getEpochToProve();
} catch (err: unknown) {
throw formatViemError(err);
}
}

public async validateHeader(
args: readonly [
`0x${string}`,
Expand Down

0 comments on commit 8aee21d

Please sign in to comment.