diff --git a/l1-contracts/src/core/Rollup.sol b/l1-contracts/src/core/Rollup.sol index 283aafc4828..40dab5df5e9 100644 --- a/l1-contracts/src/core/Rollup.sol +++ b/l1-contracts/src/core/Rollup.sol @@ -4,6 +4,7 @@ pragma solidity >=0.8.18; // Interfaces import {IRollup, ITestRollup} from "./interfaces/IRollup.sol"; +import {IProofCommitmentEscrow} from "./interfaces/IProofCommitmentEscrow.sol"; import {IInbox} from "./interfaces/messagebridge/IInbox.sol"; import {IOutbox} from "./interfaces/messagebridge/IOutbox.sol"; import {IRegistry} from "./interfaces/messagebridge/IRegistry.sol"; @@ -15,13 +16,14 @@ import {HeaderLib} from "./libraries/HeaderLib.sol"; import {Errors} from "./libraries/Errors.sol"; import {Constants} from "./libraries/ConstantsGen.sol"; import {MerkleLib} from "./libraries/MerkleLib.sol"; -import {SignatureLib} from "./sequencer_selection/SignatureLib.sol"; +import {SignatureLib} from "./libraries/SignatureLib.sol"; import {SafeCast} from "@oz/utils/math/SafeCast.sol"; import {DataStructures} from "./libraries/DataStructures.sol"; import {TxsDecoder} from "./libraries/decoders/TxsDecoder.sol"; // Contracts import {MockVerifier} from "../mock/MockVerifier.sol"; +import {MockProofCommitmentEscrow} from "../mock/MockProofCommitmentEscrow.sol"; import {Inbox} from "./messagebridge/Inbox.sol"; import {Outbox} from "./messagebridge/Outbox.sol"; import {Leonidas} from "./sequencer_selection/Leonidas.sol"; @@ -46,20 +48,22 @@ contract Rollup is Leonidas, IRollup, ITestRollup { uint128 slotNumber; } - // @note The number of slots within which a block must be proven - // This number is currently pulled out of thin air and should be replaced when we are not blind - // @todo #8018 - uint256 public constant TIMELINESS_PROVING_IN_SLOTS = 100; + // See https://github.com/AztecProtocol/engineering-designs/blob/main/in-progress/8401-proof-timeliness/proof-timeliness.ipynb + // for justification of CLAIM_DURATION_IN_L2_SLOTS. + uint256 public constant CLAIM_DURATION_IN_L2_SLOTS = 13; + uint256 public constant PROOF_COMMITMENT_MIN_BOND_AMOUNT_IN_TST = 1000; uint256 public immutable L1_BLOCK_AT_GENESIS; IRegistry public immutable REGISTRY; IInbox public immutable INBOX; IOutbox public immutable OUTBOX; + IProofCommitmentEscrow public immutable PROOF_COMMITMENT_ESCROW; uint256 public immutable VERSION; IFeeJuicePortal public immutable FEE_JUICE_PORTAL; IVerifier public verifier; ChainTips public tips; + DataStructures.EpochProofClaim public proofClaim; // @todo Validate assumption: // Currently we assume that the archive root following a block is specific to the block @@ -84,6 +88,7 @@ contract Rollup is Leonidas, IRollup, ITestRollup { verifier = new MockVerifier(); REGISTRY = _registry; FEE_JUICE_PORTAL = _fpcJuicePortal; + PROOF_COMMITMENT_ESCROW = new MockProofCommitmentEscrow(); INBOX = new Inbox(address(this), Constants.L1_TO_L2_MSG_SUBTREE_HEIGHT); OUTBOX = new Outbox(address(this)); vkTreeRoot = _vkTreeRoot; @@ -102,57 +107,17 @@ contract Rollup is Leonidas, IRollup, ITestRollup { setupEpoch(); } - function status(uint256 myHeaderBlockNumber) - external - view - override(IRollup) - returns ( - uint256 provenBlockNumber, - bytes32 provenArchive, - uint256 pendingBlockNumber, - bytes32 pendingArchive, - bytes32 archiveOfMyBlock - ) - { - return ( - tips.provenBlockNumber, - blocks[tips.provenBlockNumber].archive, - tips.pendingBlockNumber, - blocks[tips.pendingBlockNumber].archive, - archiveAt(myHeaderBlockNumber) - ); - } - /** * @notice Prune the pending chain up to the last proven block * * @dev Will revert if there is nothing to prune or if the chain is not ready to be pruned - * - * @dev While in devnet, this will be guarded behind an `onlyOwner` */ - function prune() external override(IRollup) onlyOwner { - if (tips.pendingBlockNumber == tips.provenBlockNumber) { + function prune() external override(IRollup) { + if (!_canPrune()) { revert Errors.Rollup__NothingToPrune(); } - BlockLog storage firstPendingNotInProven = blocks[tips.provenBlockNumber + 1]; - uint256 prunableAtSlot = - uint256(firstPendingNotInProven.slotNumber) + TIMELINESS_PROVING_IN_SLOTS; - uint256 currentSlot = getCurrentSlot(); - - if (currentSlot < prunableAtSlot) { - revert Errors.Rollup__NotReadyToPrune(currentSlot, prunableAtSlot); - } - - uint256 pending = tips.pendingBlockNumber; - - // @note We are not deleting the blocks, but we are "winding back" the pendingTip to the last block that was proven. - // We can do because any new block proposed will overwrite a previous block in the block log, - // so no values should "survive". - // People must therefore read the chain using the pendingTip as a boundary. - tips.pendingBlockNumber = tips.provenBlockNumber; - - emit PrunedPending(tips.provenBlockNumber, pending); + _prune(); } /** @@ -192,6 +157,60 @@ contract Rollup is Leonidas, IRollup, ITestRollup { vkTreeRoot = _vkTreeRoot; } + function claimEpochProofRight(DataStructures.EpochProofQuote calldata _quote) + external + override(IRollup) + { + uint256 currentSlot = getCurrentSlot(); + address currentProposer = getCurrentProposer(); + uint256 epochToProve = getEpochToProve(); + + if (currentProposer != address(0) && currentProposer != msg.sender) { + revert Errors.Leonidas__InvalidProposer(currentProposer, msg.sender); + } + + if (_quote.epochToProve != epochToProve) { + revert Errors.Rollup__NotClaimingCorrectEpoch(epochToProve, _quote.epochToProve); + } + + if (currentSlot % Constants.AZTEC_EPOCH_DURATION >= CLAIM_DURATION_IN_L2_SLOTS) { + revert Errors.Rollup__NotInClaimPhase( + currentSlot % Constants.AZTEC_EPOCH_DURATION, CLAIM_DURATION_IN_L2_SLOTS + ); + } + + // if the epoch to prove is not the one that has been claimed, + // then whatever is in the proofClaim is stale + if (proofClaim.epochToProve == epochToProve && proofClaim.proposerClaimant != address(0)) { + revert Errors.Rollup__ProofRightAlreadyClaimed(); + } + + if (_quote.bondAmount < PROOF_COMMITMENT_MIN_BOND_AMOUNT_IN_TST) { + revert Errors.Rollup__InsufficientBondAmount( + PROOF_COMMITMENT_MIN_BOND_AMOUNT_IN_TST, _quote.bondAmount + ); + } + + if (_quote.validUntilSlot < currentSlot) { + revert Errors.Rollup__QuoteExpired(currentSlot, _quote.validUntilSlot); + } + + // We don't currently unstake, + // but we will as part of https://github.com/AztecProtocol/aztec-packages/issues/8652. + // Blocked on submitting epoch proofs to this contract. + PROOF_COMMITMENT_ESCROW.stakeBond(_quote.bondAmount, _quote.prover); + + proofClaim = DataStructures.EpochProofClaim({ + epochToProve: epochToProve, + basisPointFee: _quote.basisPointFee, + bondAmount: _quote.bondAmount, + bondProvider: _quote.prover, + proposerClaimant: msg.sender + }); + + emit ProofRightClaimed(epochToProve, _quote.prover, msg.sender, _quote.bondAmount, currentSlot); + } + /** * @notice Publishes the body and propose the block * @dev `eth_log_handlers` rely on this function @@ -210,6 +229,9 @@ contract Rollup is Leonidas, IRollup, ITestRollup { SignatureLib.Signature[] memory _signatures, bytes calldata _body ) external override(IRollup) { + if (_canPrune()) { + _prune(); + } bytes32 txsEffectsHash = TxsDecoder.decode(_body); // Decode and validate header @@ -294,6 +316,9 @@ contract Rollup is Leonidas, IRollup, ITestRollup { bytes calldata _aggregationObject, bytes calldata _proof ) external override(IRollup) { + if (_canPrune()) { + _prune(); + } HeaderLib.Header memory header = HeaderLib.decode(_header); if (header.globalVariables.blockNumber > tips.pendingBlockNumber) { @@ -403,6 +428,27 @@ contract Rollup is Leonidas, IRollup, ITestRollup { emit L2ProofVerified(header.globalVariables.blockNumber, _proverId); } + function status(uint256 myHeaderBlockNumber) + external + view + override(IRollup) + returns ( + uint256 provenBlockNumber, + bytes32 provenArchive, + uint256 pendingBlockNumber, + bytes32 pendingArchive, + bytes32 archiveOfMyBlock + ) + { + return ( + tips.provenBlockNumber, + blocks[tips.provenBlockNumber].archive, + tips.pendingBlockNumber, + blocks[tips.pendingBlockNumber].archive, + archiveAt(myHeaderBlockNumber) + ); + } + /** * @notice Check if msg.sender can propose at a given time * @@ -489,6 +535,23 @@ contract Rollup is Leonidas, IRollup, ITestRollup { return tips.pendingBlockNumber; } + /** + * @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(IRollup) returns (uint256) { + if (tips.provenBlockNumber == tips.pendingBlockNumber) { + revert Errors.Rollup__NoEpochToProve(); + } else { + return getEpochAt(blocks[getProvenBlockNumber() + 1].slotNumber); + } + } + /** * @notice Get the archive root of a specific block * @@ -503,6 +566,47 @@ contract Rollup is Leonidas, IRollup, ITestRollup { return bytes32(0); } + function _prune() internal { + // TODO #8656 + delete proofClaim; + + uint256 pending = tips.pendingBlockNumber; + + // @note We are not deleting the blocks, but we are "winding back" the pendingTip to the last block that was proven. + // We can do because any new block proposed will overwrite a previous block in the block log, + // so no values should "survive". + // People must therefore read the chain using the pendingTip as a boundary. + tips.pendingBlockNumber = tips.provenBlockNumber; + + emit PrunedPending(tips.provenBlockNumber, pending); + } + + function _canPrune() internal view returns (bool) { + if (tips.pendingBlockNumber == tips.provenBlockNumber) { + return false; + } + + uint256 currentSlot = getCurrentSlot(); + uint256 oldestPendingEpoch = getEpochAt(blocks[tips.provenBlockNumber + 1].slotNumber); + uint256 startSlotOfPendingEpoch = oldestPendingEpoch * Constants.AZTEC_EPOCH_DURATION; + + // suppose epoch 1 is proven, epoch 2 is pending, epoch 3 is the current epoch. + // we prune the pending chain back to the end of epoch 1 if: + // - the proof claim phase of epoch 3 has ended without a claim to prove epoch 2 (or proof of epoch 2) + // - we reach epoch 4 without a proof of epoch 2 (regardless of whether a proof claim was submitted) + bool inClaimPhase = currentSlot + < startSlotOfPendingEpoch + Constants.AZTEC_EPOCH_DURATION + CLAIM_DURATION_IN_L2_SLOTS; + + bool claimExists = currentSlot < startSlotOfPendingEpoch + 2 * Constants.AZTEC_EPOCH_DURATION + && proofClaim.epochToProve == oldestPendingEpoch && proofClaim.proposerClaimant != address(0); + + if (inClaimPhase || claimExists) { + // If we are in the claim phase, do not prune + return false; + } + return true; + } + /** * @notice Validates the header for submission * diff --git a/l1-contracts/src/core/interfaces/IProofCommitmentEscrow.sol b/l1-contracts/src/core/interfaces/IProofCommitmentEscrow.sol new file mode 100644 index 00000000000..5aa2be81ec8 --- /dev/null +++ b/l1-contracts/src/core/interfaces/IProofCommitmentEscrow.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Aztec Labs. +pragma solidity >=0.8.18; + +import {SignatureLib} from "../libraries/SignatureLib.sol"; + +interface IProofCommitmentEscrow { + function deposit(uint256 _amount) external; + function withdraw(uint256 _amount) external; + function stakeBond(uint256 _bondAmount, address _prover) external; + function unstakeBond(uint256 _bondAmount, address _prover) external; +} diff --git a/l1-contracts/src/core/interfaces/IRollup.sol b/l1-contracts/src/core/interfaces/IRollup.sol index 1f02808bf96..464f4c93a96 100644 --- a/l1-contracts/src/core/interfaces/IRollup.sol +++ b/l1-contracts/src/core/interfaces/IRollup.sol @@ -5,7 +5,7 @@ pragma solidity >=0.8.18; import {IInbox} from "../interfaces/messagebridge/IInbox.sol"; import {IOutbox} from "../interfaces/messagebridge/IOutbox.sol"; -import {SignatureLib} from "../sequencer_selection/SignatureLib.sol"; +import {SignatureLib} from "../libraries/SignatureLib.sol"; import {DataStructures} from "../libraries/DataStructures.sol"; interface ITestRollup { @@ -18,9 +18,18 @@ interface IRollup { event L2BlockProposed(uint256 indexed blockNumber, bytes32 indexed archive); event L2ProofVerified(uint256 indexed blockNumber, bytes32 indexed proverId); event PrunedPending(uint256 provenBlockNumber, uint256 pendingBlockNumber); + event ProofRightClaimed( + uint256 indexed epoch, + address indexed bondProvider, + address indexed proposer, + uint256 bondAmount, + uint256 currentSlot + ); function prune() external; + function claimEpochProofRight(DataStructures.EpochProofQuote calldata _quote) external; + function propose( bytes calldata _header, bytes32 _archive, @@ -48,10 +57,13 @@ interface IRollup { DataStructures.ExecutionFlags memory _flags ) external view; + // solhint-disable-next-line func-name-mixedcase function INBOX() external view returns (IInbox); + // solhint-disable-next-line func-name-mixedcase function OUTBOX() external view returns (IOutbox); + // solhint-disable-next-line func-name-mixedcase function L1_BLOCK_AT_GENESIS() external view returns (uint256); function status(uint256 myHeaderBlockNumber) @@ -81,5 +93,6 @@ interface IRollup { function archiveAt(uint256 _blockNumber) external view returns (bytes32); function getProvenBlockNumber() external view returns (uint256); function getPendingBlockNumber() external view returns (uint256); + function getEpochToProve() external view returns (uint256); function computeTxsEffectsHash(bytes calldata _body) external pure returns (bytes32); } diff --git a/l1-contracts/src/core/libraries/DataStructures.sol b/l1-contracts/src/core/libraries/DataStructures.sol index 5462e09fa1b..8a114ce55c0 100644 --- a/l1-contracts/src/core/libraries/DataStructures.sol +++ b/l1-contracts/src/core/libraries/DataStructures.sol @@ -2,6 +2,8 @@ // Copyright 2023 Aztec Labs. pragma solidity >=0.8.18; +import {SignatureLib} from "./SignatureLib.sol"; + /** * @title Data Structures Library * @author Aztec Labs @@ -79,4 +81,21 @@ library DataStructures { bool ignoreDA; bool ignoreSignatures; } + + struct EpochProofQuote { + SignatureLib.Signature signature; + uint256 epochToProve; + uint256 validUntilSlot; + uint256 bondAmount; + address prover; + uint32 basisPointFee; + } + + struct EpochProofClaim { + uint256 epochToProve; // the epoch that the bond provider is claiming to prove + uint256 basisPointFee; // the fee that the bond provider will receive as a percentage of the block rewards + uint256 bondAmount; // the amount of escrowed funds that the bond provider will stake. Must be at least PROOF_COMMITMENT_BOND_AMOUNT + address bondProvider; // the address that has deposited funds in the escrow contract + address proposerClaimant; // the address of the proposer that submitted the claim + } } diff --git a/l1-contracts/src/core/libraries/Errors.sol b/l1-contracts/src/core/libraries/Errors.sol index db826807b5e..b5086474992 100644 --- a/l1-contracts/src/core/libraries/Errors.sol +++ b/l1-contracts/src/core/libraries/Errors.sol @@ -44,24 +44,29 @@ library Errors { error Outbox__BlockNotProven(uint256 l2BlockNumber); // 0x0e194a6d // Rollup + error Rollup__InsufficientBondAmount(uint256 minimum, uint256 provided); // 0xa165f276 error Rollup__InvalidArchive(bytes32 expected, bytes32 actual); // 0xb682a40e - error Rollup__InvalidProposedArchive(bytes32 expected, bytes32 actual); // 0x32532e73 error Rollup__InvalidBlockNumber(uint256 expected, uint256 actual); // 0xe5edf847 - error Rollup__SlotValueTooLarge(uint256 slot); // 0x7234f4fe - error Rollup__SlotAlreadyInChain(uint256 lastSlot, uint256 proposedSlot); // 0x83510bd0 + error Rollup__InvalidChainId(uint256 expected, uint256 actual); // 0x37b5bc12 error Rollup__InvalidEpoch(uint256 expected, uint256 actual); // 0x3c6d65e6 - error Rollup__TryingToProveNonExistingBlock(); // 0x34ef4954 error Rollup__InvalidInHash(bytes32 expected, bytes32 actual); // 0xcd6f4233 error Rollup__InvalidProof(); // 0xa5b2ba17 - error Rollup__InvalidChainId(uint256 expected, uint256 actual); // 0x37b5bc12 - error Rollup__InvalidVersion(uint256 expected, uint256 actual); // 0x9ef30794 + error Rollup__InvalidProposedArchive(bytes32 expected, bytes32 actual); // 0x32532e73 error Rollup__InvalidTimestamp(uint256 expected, uint256 actual); // 0x3132e895 + error Rollup__InvalidVersion(uint256 expected, uint256 actual); // 0x9ef30794 + error Rollup__NoEpochToProve(); // 0xcbaa3951 + error Rollup__NonSequentialProving(); // 0x1e5be132 + error Rollup__NotClaimingCorrectEpoch(uint256 expected, uint256 actual); // 0xf0e0744d + error Rollup__NothingToPrune(); // 0x850defd3 + error Rollup__NotInClaimPhase(uint256 currentSlotInEpoch, uint256 claimDuration); // 0xe6969f11 + error Rollup__ProofRightAlreadyClaimed(); // 0x2cac5f0a + error Rollup__QuoteExpired(uint256 currentSlot, uint256 quoteSlot); // 0x20a001eb + error Rollup__SlotAlreadyInChain(uint256 lastSlot, uint256 proposedSlot); // 0x83510bd0 + error Rollup__SlotValueTooLarge(uint256 slot); // 0x7234f4fe error Rollup__TimestampInFuture(uint256 max, uint256 actual); // 0x89f30690 error Rollup__TimestampTooOld(); // 0x72ed9c81 + error Rollup__TryingToProveNonExistingBlock(); // 0x34ef4954 error Rollup__UnavailableTxs(bytes32 txsHash); // 0x414906c3 - error Rollup__NothingToPrune(); // 0x850defd3 - error Rollup__NotReadyToPrune(uint256 currentSlot, uint256 prunableAt); // 0x9fdf1614 - error Rollup__NonSequentialProving(); // 0x1e5be132 // Registry error Registry__RollupNotRegistered(address rollup); // 0xa1fee4cf diff --git a/l1-contracts/src/core/sequencer_selection/SignatureLib.sol b/l1-contracts/src/core/libraries/SignatureLib.sol similarity index 88% rename from l1-contracts/src/core/sequencer_selection/SignatureLib.sol rename to l1-contracts/src/core/libraries/SignatureLib.sol index 434e6945c3f..1bb8d6fb050 100644 --- a/l1-contracts/src/core/sequencer_selection/SignatureLib.sol +++ b/l1-contracts/src/core/libraries/SignatureLib.sol @@ -1,6 +1,8 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Aztec Labs. pragma solidity ^0.8.13; -import {Errors} from "../libraries/Errors.sol"; +import {Errors} from "./Errors.sol"; library SignatureLib { struct Signature { diff --git a/l1-contracts/src/core/sequencer_selection/ILeonidas.sol b/l1-contracts/src/core/sequencer_selection/ILeonidas.sol index a9542975205..68a58572236 100644 --- a/l1-contracts/src/core/sequencer_selection/ILeonidas.sol +++ b/l1-contracts/src/core/sequencer_selection/ILeonidas.sol @@ -27,4 +27,7 @@ interface ILeonidas { function getCurrentEpochCommittee() external view returns (address[] memory); function getEpochCommittee(uint256 _epoch) external view returns (address[] memory); function getValidators() external view returns (address[] memory); + + function getEpochAt(uint256 _ts) external view returns (uint256); + function getSlotAt(uint256 _ts) external view returns (uint256); } diff --git a/l1-contracts/src/core/sequencer_selection/Leonidas.sol b/l1-contracts/src/core/sequencer_selection/Leonidas.sol index 48cbaef0579..8a90052500f 100644 --- a/l1-contracts/src/core/sequencer_selection/Leonidas.sol +++ b/l1-contracts/src/core/sequencer_selection/Leonidas.sol @@ -6,8 +6,8 @@ import {DataStructures} from "../libraries/DataStructures.sol"; import {Errors} from "../libraries/Errors.sol"; import {EnumerableSet} from "@oz/utils/structs/EnumerableSet.sol"; import {Ownable} from "@oz/access/Ownable.sol"; -import {SignatureLib} from "./SignatureLib.sol"; import {SampleLib} from "./SampleLib.sol"; +import {SignatureLib} from "../libraries/SignatureLib.sol"; import {Constants} from "../libraries/ConstantsGen.sol"; import {MessageHashUtils} from "@oz/utils/cryptography/MessageHashUtils.sol"; @@ -128,28 +128,6 @@ contract Leonidas is Ownable, ILeonidas { return epochs[_epoch].committee; } - function getCommitteeAt(uint256 _ts) internal view returns (address[] memory) { - uint256 epochNumber = getEpochAt(_ts); - Epoch storage epoch = epochs[epochNumber]; - - if (epoch.sampleSeed != 0) { - uint256 committeeSize = epoch.committee.length; - if (committeeSize == 0) { - return new address[](0); - } - return epoch.committee; - } - - // Allow anyone if there is no validator set - if (validatorSet.length() == 0) { - return new address[](0); - } - - // Emulate a sampling of the validators - uint256 sampleSeed = _getSampleSeed(epochNumber); - return _sampleValidators(sampleSeed); - } - /** * @notice Get the validator set for the current epoch * @return The validator set for the current epoch @@ -169,6 +147,28 @@ contract Leonidas is Ownable, ILeonidas { return validatorSet.values(); } + /** + * @notice Performs a setup of an epoch if needed. The setup will + * - Sample the validator set for the epoch + * - Set the seed for the epoch + * - Update the last seed + * + * @dev Since this is a reference optimising for simplicity, we store the actual validator set in the epoch structure. + * This is very heavy on gas, so start crying because the gas here will melt the poles + * https://i.giphy.com/U1aN4HTfJ2SmgB2BBK.webp + */ + function setupEpoch() public override(ILeonidas) { + uint256 epochNumber = getCurrentEpoch(); + Epoch storage epoch = epochs[epochNumber]; + + if (epoch.sampleSeed == 0) { + epoch.sampleSeed = _getSampleSeed(epochNumber); + epoch.nextSeed = lastSeed = _computeNextSeed(epochNumber); + + epoch.committee = _sampleValidators(epoch.sampleSeed); + } + } + /** * @notice Get the number of validators in the validator set * @@ -198,28 +198,6 @@ contract Leonidas is Ownable, ILeonidas { return validatorSet.contains(_validator); } - /** - * @notice Performs a setup of an epoch if needed. The setup will - * - Sample the validator set for the epoch - * - Set the seed for the epoch - * - Update the last seed - * - * @dev Since this is a reference optimising for simplicity, we store the actual validator set in the epoch structure. - * This is very heavy on gas, so start crying because the gas here will melt the poles - * https://i.giphy.com/U1aN4HTfJ2SmgB2BBK.webp - */ - function setupEpoch() public override(ILeonidas) { - uint256 epochNumber = getCurrentEpoch(); - Epoch storage epoch = epochs[epochNumber]; - - if (epoch.sampleSeed == 0) { - epoch.sampleSeed = _getSampleSeed(epochNumber); - epoch.nextSeed = lastSeed = _computeNextSeed(epochNumber); - - epoch.committee = _sampleValidators(epoch.sampleSeed); - } - } - /** * @notice Get the current epoch number * @@ -314,6 +292,28 @@ contract Leonidas is Ownable, ILeonidas { return committee[_computeProposerIndex(epochNumber, slot, sampleSeed, committee.length)]; } + /** + * @notice Computes the epoch at a specific time + * + * @param _ts - The timestamp to compute the epoch for + * + * @return The computed epoch + */ + function getEpochAt(uint256 _ts) public view override(ILeonidas) returns (uint256) { + return _ts < GENESIS_TIME ? 0 : (_ts - GENESIS_TIME) / (EPOCH_DURATION * SLOT_DURATION); + } + + /** + * @notice Computes the slot at a specific time + * + * @param _ts - The timestamp to compute the slot for + * + * @return The computed slot + */ + function getSlotAt(uint256 _ts) public view override(ILeonidas) returns (uint256) { + return _ts < GENESIS_TIME ? 0 : (_ts - GENESIS_TIME) / SLOT_DURATION; + } + /** * @notice Adds a validator to the set WITHOUT setting up the epoch * @param _validator - The validator to add @@ -322,6 +322,28 @@ contract Leonidas is Ownable, ILeonidas { validatorSet.add(_validator); } + function getCommitteeAt(uint256 _ts) internal view returns (address[] memory) { + uint256 epochNumber = getEpochAt(_ts); + Epoch storage epoch = epochs[epochNumber]; + + if (epoch.sampleSeed != 0) { + uint256 committeeSize = epoch.committee.length; + if (committeeSize == 0) { + return new address[](0); + } + return epoch.committee; + } + + // Allow anyone if there is no validator set + if (validatorSet.length() == 0) { + return new address[](0); + } + + // Emulate a sampling of the validators + uint256 sampleSeed = _getSampleSeed(epochNumber); + return _sampleValidators(sampleSeed); + } + /** * @notice Propose a pending block from the point-of-view of sequencer selection. Will: * - Setup the epoch if needed (if epoch committee is empty skips the rest) @@ -392,6 +414,20 @@ contract Leonidas is Ownable, ILeonidas { } } + /** + * @notice Computes the nextSeed for an epoch + * + * @dev We include the `_epoch` instead of using the randao directly to avoid issues with foundry testing + * where randao == 0. + * + * @param _epoch - The epoch to compute the seed for + * + * @return The computed seed + */ + function _computeNextSeed(uint256 _epoch) private view returns (uint256) { + return uint256(keccak256(abi.encode(_epoch, block.prevrandao))); + } + /** * @notice Samples a validator set for a specific epoch * @@ -452,42 +488,6 @@ contract Leonidas is Ownable, ILeonidas { return lastSeed; } - /** - * @notice Computes the epoch at a specific time - * - * @param _ts - The timestamp to compute the epoch for - * - * @return The computed epoch - */ - function getEpochAt(uint256 _ts) public view returns (uint256) { - return (_ts - GENESIS_TIME) / (EPOCH_DURATION * SLOT_DURATION); - } - - /** - * @notice Computes the slot at a specific time - * - * @param _ts - The timestamp to compute the slot for - * - * @return The computed slot - */ - function getSlotAt(uint256 _ts) public view returns (uint256) { - return (_ts - GENESIS_TIME) / SLOT_DURATION; - } - - /** - * @notice Computes the nextSeed for an epoch - * - * @dev We include the `_epoch` instead of using the randao directly to avoid issues with foundry testing - * where randao == 0. - * - * @param _epoch - The epoch to compute the seed for - * - * @return The computed seed - */ - function _computeNextSeed(uint256 _epoch) private view returns (uint256) { - return uint256(keccak256(abi.encode(_epoch, block.prevrandao))); - } - /** * @notice Computes the index of the committee member that acts as proposer for a given slot * diff --git a/l1-contracts/src/mock/MockProofCommitmentEscrow.sol b/l1-contracts/src/mock/MockProofCommitmentEscrow.sol new file mode 100644 index 00000000000..89e61d3c241 --- /dev/null +++ b/l1-contracts/src/mock/MockProofCommitmentEscrow.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Aztec Labs. +pragma solidity >=0.8.18; + +import {SignatureLib} from "../core/libraries/SignatureLib.sol"; +import {IProofCommitmentEscrow} from "../core/interfaces/IProofCommitmentEscrow.sol"; + +contract MockProofCommitmentEscrow is IProofCommitmentEscrow { + function deposit(uint256 _amount) external override { + // do nothing + } + + function withdraw(uint256 _amount) external override { + // do nothing + } + + function unstakeBond(uint256 _amount, address _prover) external override { + // do nothing + } + + function stakeBond(uint256 _amount, address _prover) external override { + // do nothing + } +} diff --git a/l1-contracts/test/Rollup.t.sol b/l1-contracts/test/Rollup.t.sol index 807d4563b05..27e20595c59 100644 --- a/l1-contracts/test/Rollup.t.sol +++ b/l1-contracts/test/Rollup.t.sol @@ -6,6 +6,7 @@ import {DecoderBase} from "./decoders/Base.sol"; import {DataStructures} from "../src/core/libraries/DataStructures.sol"; import {Constants} from "../src/core/libraries/ConstantsGen.sol"; +import {SignatureLib} from "../src/core/libraries/SignatureLib.sol"; import {Registry} from "../src/core/messagebridge/Registry.sol"; import {Inbox} from "../src/core/messagebridge/Inbox.sol"; @@ -13,9 +14,9 @@ import {Outbox} from "../src/core/messagebridge/Outbox.sol"; import {Errors} from "../src/core/libraries/Errors.sol"; import {Rollup} from "../src/core/Rollup.sol"; import {IFeeJuicePortal} from "../src/core/interfaces/IFeeJuicePortal.sol"; +import {IRollup} from "../src/core/interfaces/IRollup.sol"; import {FeeJuicePortal} from "../src/core/FeeJuicePortal.sol"; import {Leonidas} from "../src/core/sequencer_selection/Leonidas.sol"; -import {SignatureLib} from "../src/core/sequencer_selection/SignatureLib.sol"; import {NaiveMerkle} from "./merkle/Naive.sol"; import {MerkleTestUtil} from "./merkle/TestUtil.sol"; import {PortalERC20} from "./portals/PortalERC20.sol"; @@ -23,6 +24,8 @@ import {PortalERC20} from "./portals/PortalERC20.sol"; import {TxsDecoderHelper} from "./decoders/helpers/TxsDecoderHelper.sol"; import {IERC20Errors} from "@oz/interfaces/draft-IERC6093.sol"; +// solhint-disable comprehensive-interface + /** * Blocks are generated using the `integration_l1_publisher.test.ts` tests. * Main use of these test is shorter cycles when updating the decoder contract. @@ -76,6 +79,284 @@ contract RollupTest is DecoderBase { _; } + function warpToL2Slot(uint256 _slot) public { + vm.warp(rollup.getTimestampForSlot(_slot)); + } + + function testClaimWithNothingToProve() public setUpFor("mixed_block_1") { + assertEq(rollup.getCurrentSlot(), 0, "genesis slot should be zero"); + + DataStructures.EpochProofQuote memory quote = DataStructures.EpochProofQuote({ + signature: SignatureLib.Signature({isEmpty: false, v: 27, r: bytes32(0), s: bytes32(0)}), + epochToProve: 0, + validUntilSlot: 1, + bondAmount: rollup.PROOF_COMMITMENT_MIN_BOND_AMOUNT_IN_TST(), + prover: address(this), + basisPointFee: 0 + }); + + // sanity check that proven/pending tip are at genesis + vm.expectRevert(abi.encodeWithSelector(Errors.Rollup__NoEpochToProve.selector)); + rollup.claimEpochProofRight(quote); + + warpToL2Slot(1); + assertEq(rollup.getCurrentSlot(), 1, "warp to slot 1 failed"); + assertEq(rollup.getCurrentEpoch(), 0, "Invalid current epoch"); + + // empty slots do not move pending chain + vm.expectRevert(abi.encodeWithSelector(Errors.Rollup__NoEpochToProve.selector)); + rollup.claimEpochProofRight(quote); + } + + function testClaimWithWrongEpoch() public setUpFor("mixed_block_1") { + _testBlock("mixed_block_1", false, 0); + + DataStructures.EpochProofQuote memory quote = DataStructures.EpochProofQuote({ + signature: SignatureLib.Signature({isEmpty: false, v: 27, r: bytes32(0), s: bytes32(0)}), + epochToProve: 1, + validUntilSlot: 1, + bondAmount: rollup.PROOF_COMMITMENT_MIN_BOND_AMOUNT_IN_TST(), + prover: address(this), + basisPointFee: 0 + }); + + vm.expectRevert( + abi.encodeWithSelector(Errors.Rollup__NotClaimingCorrectEpoch.selector, 0, quote.epochToProve) + ); + rollup.claimEpochProofRight(quote); + } + + function testClaimWithInsufficientBond() public setUpFor("mixed_block_1") { + _testBlock("mixed_block_1", false, 0); + + DataStructures.EpochProofQuote memory quote = DataStructures.EpochProofQuote({ + signature: SignatureLib.Signature({isEmpty: false, v: 27, r: bytes32(0), s: bytes32(0)}), + epochToProve: 0, + validUntilSlot: 1, + bondAmount: 0, + prover: address(this), + basisPointFee: 0 + }); + + vm.expectRevert( + abi.encodeWithSelector( + Errors.Rollup__InsufficientBondAmount.selector, + rollup.PROOF_COMMITMENT_MIN_BOND_AMOUNT_IN_TST(), + quote.bondAmount + ) + ); + rollup.claimEpochProofRight(quote); + } + + function testClaimPastValidUntil() public setUpFor("mixed_block_1") { + _testBlock("mixed_block_1", false, 0); + + DataStructures.EpochProofQuote memory quote = DataStructures.EpochProofQuote({ + signature: SignatureLib.Signature({isEmpty: false, v: 27, r: bytes32(0), s: bytes32(0)}), + epochToProve: 0, + validUntilSlot: 0, + bondAmount: rollup.PROOF_COMMITMENT_MIN_BOND_AMOUNT_IN_TST(), + prover: address(this), + basisPointFee: 0 + }); + + warpToL2Slot(1); + + vm.expectRevert( + abi.encodeWithSelector(Errors.Rollup__QuoteExpired.selector, 1, quote.validUntilSlot) + ); + rollup.claimEpochProofRight(quote); + } + + function testClaimSimple() public setUpFor("mixed_block_1") { + _testBlock("mixed_block_1", false, 0); + + DataStructures.EpochProofQuote memory quote = DataStructures.EpochProofQuote({ + signature: SignatureLib.Signature({isEmpty: false, v: 27, r: bytes32(0), s: bytes32(0)}), + epochToProve: 0, + validUntilSlot: 1, + bondAmount: rollup.PROOF_COMMITMENT_MIN_BOND_AMOUNT_IN_TST(), + prover: address(0), + basisPointFee: 0 + }); + + warpToL2Slot(1); + + vm.expectEmit(true, true, true, true); + emit IRollup.ProofRightClaimed( + quote.epochToProve, address(0), address(this), quote.bondAmount, 1 + ); + rollup.claimEpochProofRight(quote); + + ( + uint256 epochToProve, + uint256 basisPointFee, + uint256 bondAmount, + address bondProvider, + address proposerClaimant + ) = rollup.proofClaim(); + assertEq(epochToProve, quote.epochToProve, "Invalid epoch to prove"); + assertEq(basisPointFee, quote.basisPointFee, "Invalid basis point fee"); + assertEq(bondAmount, quote.bondAmount, "Invalid bond amount"); + // TODO #8573 + // This will be fixed with proper escrow + assertEq(bondProvider, address(0), "Invalid bond provider"); + assertEq(proposerClaimant, address(this), "Invalid proposer claimant"); + } + + function testClaimTwice() public setUpFor("mixed_block_1") { + _testBlock("mixed_block_1", false, 0); + + DataStructures.EpochProofQuote memory quote = DataStructures.EpochProofQuote({ + signature: SignatureLib.Signature({isEmpty: false, v: 27, r: bytes32(0), s: bytes32(0)}), + epochToProve: 0, + validUntilSlot: 1, + bondAmount: rollup.PROOF_COMMITMENT_MIN_BOND_AMOUNT_IN_TST(), + prover: address(this), + basisPointFee: 0 + }); + + warpToL2Slot(1); + + rollup.claimEpochProofRight(quote); + + vm.expectRevert(abi.encodeWithSelector(Errors.Rollup__ProofRightAlreadyClaimed.selector)); + rollup.claimEpochProofRight(quote); + + warpToL2Slot(2); + vm.expectRevert(abi.encodeWithSelector(Errors.Rollup__ProofRightAlreadyClaimed.selector)); + rollup.claimEpochProofRight(quote); + + // warp to epoch 1 + warpToL2Slot(Constants.AZTEC_EPOCH_DURATION); + assertEq(rollup.getCurrentEpoch(), 1, "Invalid current epoch"); + + // We should still be trying to prove epoch 0 in epoch 1 + vm.expectRevert(abi.encodeWithSelector(Errors.Rollup__ProofRightAlreadyClaimed.selector)); + rollup.claimEpochProofRight(quote); + + // still nothing to prune + vm.expectRevert(abi.encodeWithSelector(Errors.Rollup__NothingToPrune.selector)); + rollup.prune(); + } + + function testClaimOutsideClaimPhase() public setUpFor("mixed_block_1") { + _testBlock("mixed_block_1", false, 0); + + DataStructures.EpochProofQuote memory quote = DataStructures.EpochProofQuote({ + signature: SignatureLib.Signature({isEmpty: false, v: 27, r: bytes32(0), s: bytes32(0)}), + epochToProve: 0, + validUntilSlot: 1, + bondAmount: rollup.PROOF_COMMITMENT_MIN_BOND_AMOUNT_IN_TST(), + prover: address(this), + basisPointFee: 0 + }); + + warpToL2Slot(Constants.AZTEC_EPOCH_DURATION + rollup.CLAIM_DURATION_IN_L2_SLOTS()); + + vm.expectRevert( + abi.encodeWithSelector( + Errors.Rollup__NotInClaimPhase.selector, + rollup.CLAIM_DURATION_IN_L2_SLOTS(), + rollup.CLAIM_DURATION_IN_L2_SLOTS() + ) + ); + rollup.claimEpochProofRight(quote); + } + + function testNoPruneWhenClaimExists() public setUpFor("mixed_block_1") { + _testBlock("mixed_block_1", false, 0); + + DataStructures.EpochProofQuote memory quote = DataStructures.EpochProofQuote({ + signature: SignatureLib.Signature({isEmpty: false, v: 27, r: bytes32(0), s: bytes32(0)}), + epochToProve: 0, + validUntilSlot: 2 * Constants.AZTEC_EPOCH_DURATION, + bondAmount: rollup.PROOF_COMMITMENT_MIN_BOND_AMOUNT_IN_TST(), + prover: address(this), + basisPointFee: 0 + }); + + warpToL2Slot(Constants.AZTEC_EPOCH_DURATION + rollup.CLAIM_DURATION_IN_L2_SLOTS() - 1); + + rollup.claimEpochProofRight(quote); + + warpToL2Slot(Constants.AZTEC_EPOCH_DURATION + rollup.CLAIM_DURATION_IN_L2_SLOTS()); + + vm.expectRevert(abi.encodeWithSelector(Errors.Rollup__NothingToPrune.selector)); + rollup.prune(); + } + + function testPruneWhenClaimExpires() public setUpFor("mixed_block_1") { + _testBlock("mixed_block_1", false, 0); + + DataStructures.EpochProofQuote memory quote = DataStructures.EpochProofQuote({ + signature: SignatureLib.Signature({isEmpty: false, v: 27, r: bytes32(0), s: bytes32(0)}), + epochToProve: 0, + validUntilSlot: 2 * Constants.AZTEC_EPOCH_DURATION, + bondAmount: rollup.PROOF_COMMITMENT_MIN_BOND_AMOUNT_IN_TST(), + prover: address(this), + basisPointFee: 0 + }); + + warpToL2Slot(Constants.AZTEC_EPOCH_DURATION + rollup.CLAIM_DURATION_IN_L2_SLOTS() - 1); + + rollup.claimEpochProofRight(quote); + + warpToL2Slot(Constants.AZTEC_EPOCH_DURATION * 2); + + // We should still be trying to prove epoch 0 in epoch 2 + vm.expectRevert(abi.encodeWithSelector(Errors.Rollup__ProofRightAlreadyClaimed.selector)); + rollup.claimEpochProofRight(quote); + + rollup.prune(); + + vm.expectRevert(abi.encodeWithSelector(Errors.Rollup__NoEpochToProve.selector)); + rollup.claimEpochProofRight(quote); + } + + function testClaimAfterPrune() public setUpFor("mixed_block_1") { + _testBlock("mixed_block_1", false, 0); + + DataStructures.EpochProofQuote memory quote = DataStructures.EpochProofQuote({ + signature: SignatureLib.Signature({isEmpty: false, v: 27, r: bytes32(0), s: bytes32(0)}), + epochToProve: 0, + validUntilSlot: 2 * Constants.AZTEC_EPOCH_DURATION, + bondAmount: rollup.PROOF_COMMITMENT_MIN_BOND_AMOUNT_IN_TST(), + prover: address(this), + basisPointFee: 0 + }); + + warpToL2Slot(Constants.AZTEC_EPOCH_DURATION + rollup.CLAIM_DURATION_IN_L2_SLOTS() - 1); + + rollup.claimEpochProofRight(quote); + + warpToL2Slot(Constants.AZTEC_EPOCH_DURATION * 2); + + rollup.prune(); + + _testBlock("mixed_block_1", false, Constants.AZTEC_EPOCH_DURATION * 2); + + vm.expectEmit(true, true, true, true); + emit IRollup.ProofRightClaimed( + quote.epochToProve, + address(this), + address(this), + quote.bondAmount, + Constants.AZTEC_EPOCH_DURATION * 2 + ); + rollup.claimEpochProofRight(quote); + } + + function testPruneWhenNoProofClaim() public setUpFor("mixed_block_1") { + _testBlock("mixed_block_1", false); + warpToL2Slot(Constants.AZTEC_EPOCH_DURATION + rollup.CLAIM_DURATION_IN_L2_SLOTS() - 1); + vm.expectRevert(abi.encodeWithSelector(Errors.Rollup__NothingToPrune.selector)); + rollup.prune(); + + warpToL2Slot(Constants.AZTEC_EPOCH_DURATION + rollup.CLAIM_DURATION_IN_L2_SLOTS()); + rollup.prune(); + } + function testRevertProveTwice() public setUpFor("mixed_block_1") { DecoderBase.Data memory data = load("mixed_block_1").block; bytes memory header = data.header; @@ -113,13 +394,7 @@ contract RollupTest is DecoderBase { _testBlock("mixed_block_1", false); - uint256 currentSlot = rollup.getCurrentSlot(); - (,, uint128 slot) = rollup.blocks(1); - uint256 prunableAt = uint256(slot) + rollup.TIMELINESS_PROVING_IN_SLOTS(); - - vm.expectRevert( - abi.encodeWithSelector(Errors.Rollup__NotReadyToPrune.selector, currentSlot, prunableAt) - ); + vm.expectRevert(abi.encodeWithSelector(Errors.Rollup__NothingToPrune.selector)); rollup.prune(); } @@ -133,7 +408,7 @@ contract RollupTest is DecoderBase { bytes32 inboxRoot2 = inbox.getRoot(2); (,, uint128 slot) = rollup.blocks(1); - uint256 prunableAt = uint256(slot) + rollup.TIMELINESS_PROVING_IN_SLOTS(); + uint256 prunableAt = uint256(slot) + Constants.AZTEC_EPOCH_DURATION * 2; uint256 timeOfPrune = rollup.getTimestampForSlot(prunableAt); vm.warp(timeOfPrune); @@ -161,7 +436,7 @@ contract RollupTest is DecoderBase { // This means that we keep the `empty_block_1` mostly as is, but replace the slot number // and timestamp as if it was created at a different point in time. This allow us to insert it // as if it was the first block, even after we had originally inserted the mixed block. - // An example where this could happen would be if no-one could proof the mixed block. + // An example where this could happen would be if no-one could prove the mixed block. _testBlock("empty_block_1", false, prunableAt); assertEq(inbox.inProgress(), 3, "Invalid in progress"); @@ -180,6 +455,15 @@ contract RollupTest is DecoderBase { assertNotEq(minHeightEmpty, minHeightMixed, "Invalid min height"); } + function testPruneDuringPropose() public setUpFor("mixed_block_1") { + _testBlock("mixed_block_1", false); + warpToL2Slot(Constants.AZTEC_EPOCH_DURATION * 2); + _testBlock("mixed_block_1", false, Constants.AZTEC_EPOCH_DURATION * 2); + + assertEq(rollup.getPendingBlockNumber(), 1, "Invalid pending block number"); + assertEq(rollup.getProvenBlockNumber(), 0, "Invalid proven block number"); + } + function testBlockFee() public setUpFor("mixed_block_1") { uint256 feeAmount = 2e18; diff --git a/l1-contracts/test/sparta/Sparta.t.sol b/l1-contracts/test/sparta/Sparta.t.sol index 3454245ffbf..44700b1e4f5 100644 --- a/l1-contracts/test/sparta/Sparta.t.sol +++ b/l1-contracts/test/sparta/Sparta.t.sol @@ -6,7 +6,7 @@ import {DecoderBase} from "../decoders/Base.sol"; import {DataStructures} from "../../src/core/libraries/DataStructures.sol"; import {Constants} from "../../src/core/libraries/ConstantsGen.sol"; -import {SignatureLib} from "../../src/core/sequencer_selection/SignatureLib.sol"; +import {SignatureLib} from "../../src/core/libraries/SignatureLib.sol"; import {Registry} from "../../src/core/messagebridge/Registry.sol"; import {Inbox} from "../../src/core/messagebridge/Inbox.sol"; @@ -21,6 +21,8 @@ import {TxsDecoderHelper} from "../decoders/helpers/TxsDecoderHelper.sol"; import {IFeeJuicePortal} from "../../src/core/interfaces/IFeeJuicePortal.sol"; import {MessageHashUtils} from "@oz/utils/cryptography/MessageHashUtils.sol"; +// solhint-disable comprehensive-interface + /** * We are using the same blocks as from Rollup.t.sol. * The tests in this file is testing the sequencer selection @@ -28,6 +30,12 @@ import {MessageHashUtils} from "@oz/utils/cryptography/MessageHashUtils.sol"; contract SpartaTest is DecoderBase { using MessageHashUtils for bytes32; + struct StructToAvoidDeepStacks { + uint256 needed; + address proposer; + bool shouldRevert; + } + Registry internal registry; Inbox internal inbox; Outbox internal outbox; @@ -36,9 +44,10 @@ contract SpartaTest is DecoderBase { TxsDecoderHelper internal txsHelper; PortalERC20 internal portalERC20; - mapping(address validator => uint256 privateKey) internal privateKeys; - SignatureLib.Signature internal emptySignature; + mapping(address validator => uint256 privateKey) internal privateKeys; + mapping(address => bool) internal _seenValidators; + mapping(address => bool) internal _seenCommittee; /** * @notice Set up the contracts needed for the tests with time aligned to the provided block name @@ -78,9 +87,6 @@ contract SpartaTest is DecoderBase { _; } - mapping(address => bool) internal _seenValidators; - mapping(address => bool) internal _seenCommittee; - function testInitialCommitteMatch() public setup(4) { address[] memory validators = rollup.getValidators(); address[] memory committee = rollup.getCurrentEpochCommittee(); @@ -145,12 +151,6 @@ contract SpartaTest is DecoderBase { _testBlock("mixed_block_1", true, 2, false); } - struct StructToAvoidDeepStacks { - uint256 needed; - address proposer; - bool shouldRevert; - } - function _testBlock( string memory _name, bool _expectRevert, diff --git a/yarn-project/end-to-end/src/e2e_synching.test.ts b/yarn-project/end-to-end/src/e2e_synching.test.ts index 2cbb21530fa..a0f980bab34 100644 --- a/yarn-project/end-to-end/src/e2e_synching.test.ts +++ b/yarn-project/end-to-end/src/e2e_synching.test.ts @@ -451,7 +451,7 @@ describe('e2e_synching', () => { const pendingBlockNumber = await rollup.read.getPendingBlockNumber(); await rollup.write.setAssumeProvenThroughBlockNumber([pendingBlockNumber - BigInt(variant.blockCount) / 2n]); - const timeliness = await rollup.read.TIMELINESS_PROVING_IN_SLOTS(); + const timeliness = (await rollup.read.EPOCH_DURATION()) * 2n; const [, , slot] = await rollup.read.blocks([(await rollup.read.getProvenBlockNumber()) + 1n]); const timeJumpTo = await rollup.read.getTimestampForSlot([slot + timeliness]);