From 284c8f8e4504ff8e8d633dc291c20111a0406273 Mon Sep 17 00:00:00 2001 From: just-mitch <68168980+just-mitch@users.noreply.github.com> Date: Thu, 31 Oct 2024 21:54:03 -0400 Subject: [PATCH] feat: simulate validateEpochProofQuoteHeader in the future (#9641) This is important because by default the simulation uses the time of the previous block. So without this the proposer in slot `n` will simulate but be told they cannot claim because they were not the proposer in slot `n-1`. --- l1-contracts/src/core/Rollup.sol | 96 +++++++++---------- l1-contracts/src/core/interfaces/IRollup.sol | 10 +- l1-contracts/test/Rollup.t.sol | 48 +++++++--- .../src/publisher/l1-publisher.ts | 12 ++- 4 files changed, 95 insertions(+), 71 deletions(-) diff --git a/l1-contracts/src/core/Rollup.sol b/l1-contracts/src/core/Rollup.sol index 2be3634d580..9a627d5d690 100644 --- a/l1-contracts/src/core/Rollup.sol +++ b/l1-contracts/src/core/Rollup.sol @@ -121,15 +121,6 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Leonidas, IRollup, ITestRollup { setupEpoch(); } - function quoteToDigest(EpochProofQuoteLib.EpochProofQuote memory quote) - public - view - override(IRollup) - returns (bytes32) - { - return _hashTypedDataV4(EpochProofQuoteLib.hash(quote)); - } - /** * @notice Prune the pending chain up to the last proven block * @@ -153,25 +144,6 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Leonidas, IRollup, ITestRollup { assumeProvenThroughBlockNumber = blockNumber; } - function fakeBlockNumberAsProven(uint256 blockNumber) private { - if (blockNumber > tips.provenBlockNumber && blockNumber <= tips.pendingBlockNumber) { - tips.provenBlockNumber = blockNumber; - - // If this results on a new epoch, create a fake claim for it - // Otherwise nextEpochToProve will report an old epoch - Epoch epoch = getEpochForBlock(blockNumber); - if (Epoch.unwrap(epoch) == 0 || Epoch.unwrap(epoch) > Epoch.unwrap(proofClaim.epochToProve)) { - proofClaim = DataStructures.EpochProofClaim({ - epochToProve: epoch, - basisPointFee: 0, - bondAmount: 0, - bondProvider: address(0), - proposerClaimant: msg.sender - }); - } - } - } - /** * @notice Set the verifier contract * @@ -367,7 +339,8 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Leonidas, IRollup, ITestRollup { Slot slot = getSlotAt(_ts); // Consider if a prune will hit in this slot - uint256 pendingBlockNumber = _canPruneAt(_ts) ? tips.provenBlockNumber : tips.pendingBlockNumber; + uint256 pendingBlockNumber = + _canPruneAtTime(_ts) ? tips.provenBlockNumber : tips.pendingBlockNumber; Slot lastSlot = blocks[pendingBlockNumber].slotNumber; @@ -441,7 +414,7 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Leonidas, IRollup, ITestRollup { public override(IRollup) { - validateEpochProofRightClaim(_quote); + validateEpochProofRightClaimAtTime(Timestamp.wrap(block.timestamp), _quote); Slot currentSlot = getCurrentSlot(); Epoch epochToProve = getEpochToProve(); @@ -545,6 +518,15 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Leonidas, IRollup, ITestRollup { } } + function quoteToDigest(EpochProofQuoteLib.EpochProofQuote memory quote) + public + view + override(IRollup) + returns (bytes32) + { + return _hashTypedDataV4(EpochProofQuoteLib.hash(quote)); + } + /** * @notice Returns the computed public inputs for the given epoch proof. * @@ -684,17 +666,21 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Leonidas, IRollup, ITestRollup { return publicInputs; } - function validateEpochProofRightClaim(EpochProofQuoteLib.SignedEpochProofQuote calldata _quote) - public - view - override(IRollup) - { + function validateEpochProofRightClaimAtTime( + Timestamp _ts, + EpochProofQuoteLib.SignedEpochProofQuote calldata _quote + ) public view override(IRollup) { SignatureLib.verify(_quote.signature, _quote.quote.prover, quoteToDigest(_quote.quote)); - Slot currentSlot = getCurrentSlot(); - address currentProposer = getCurrentProposer(); + Slot currentSlot = getSlotAt(_ts); + address currentProposer = getProposerAt(_ts); Epoch epochToProve = getEpochToProve(); + require( + _quote.quote.validUntilSlot >= currentSlot, + Errors.Rollup__QuoteExpired(currentSlot, _quote.quote.validUntilSlot) + ); + require( _quote.quote.basisPointFee <= 10_000, Errors.Rollup__InvalidBasisPointFee(_quote.quote.basisPointFee) @@ -734,11 +720,6 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Leonidas, IRollup, ITestRollup { _quote.quote.bondAmount <= availableFundsInEscrow, Errors.Rollup__InsufficientFundsInEscrow(_quote.quote.bondAmount, availableFundsInEscrow) ); - - require( - _quote.quote.validUntilSlot >= currentSlot, - Errors.Rollup__QuoteExpired(currentSlot, _quote.quote.validUntilSlot) - ); } /** @@ -794,6 +775,10 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Leonidas, IRollup, ITestRollup { return bytes32(0); } + function canPrune() public view override(IRollup) returns (bool) { + return _canPruneAtTime(Timestamp.wrap(block.timestamp)); + } + function _prune() internal { // TODO #8656 delete proofClaim; @@ -809,11 +794,7 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Leonidas, IRollup, ITestRollup { emit PrunedPending(tips.provenBlockNumber, pending); } - function canPrune() public view returns (bool) { - return _canPruneAt(Timestamp.wrap(block.timestamp)); - } - - function _canPruneAt(Timestamp _ts) internal view returns (bool) { + function _canPruneAtTime(Timestamp _ts) internal view returns (bool) { if ( tips.pendingBlockNumber == tips.provenBlockNumber || tips.pendingBlockNumber <= assumeProvenThroughBlockNumber @@ -861,7 +842,7 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Leonidas, IRollup, ITestRollup { DataStructures.ExecutionFlags memory _flags ) internal view { uint256 pendingBlockNumber = - _canPruneAt(_currentTime) ? tips.provenBlockNumber : tips.pendingBlockNumber; + _canPruneAtTime(_currentTime) ? tips.provenBlockNumber : tips.pendingBlockNumber; _validateHeaderForSubmissionBase( _header, _currentTime, _txEffectsHash, pendingBlockNumber, _flags ); @@ -986,4 +967,23 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Leonidas, IRollup, ITestRollup { require(_header.globalVariables.gasFees.feePerL2Gas == 0, Errors.Rollup__NonZeroL2Fee()); } } + + function fakeBlockNumberAsProven(uint256 blockNumber) private { + if (blockNumber > tips.provenBlockNumber && blockNumber <= tips.pendingBlockNumber) { + tips.provenBlockNumber = blockNumber; + + // If this results on a new epoch, create a fake claim for it + // Otherwise nextEpochToProve will report an old epoch + Epoch epoch = getEpochForBlock(blockNumber); + if (Epoch.unwrap(epoch) == 0 || Epoch.unwrap(epoch) > Epoch.unwrap(proofClaim.epochToProve)) { + proofClaim = DataStructures.EpochProofClaim({ + epochToProve: epoch, + basisPointFee: 0, + bondAmount: 0, + bondProvider: address(0), + proposerClaimant: msg.sender + }); + } + } + } } diff --git a/l1-contracts/src/core/interfaces/IRollup.sol b/l1-contracts/src/core/interfaces/IRollup.sol index 24dca3a436f..c3141e0fc40 100644 --- a/l1-contracts/src/core/interfaces/IRollup.sol +++ b/l1-contracts/src/core/interfaces/IRollup.sol @@ -32,8 +32,6 @@ interface IRollup { function prune() external; - function canPrune() external view returns (bool); - function claimEpochProofRight(EpochProofQuoteLib.SignedEpochProofQuote calldata _quote) external; function propose( @@ -102,14 +100,16 @@ interface IRollup { function archive() external view returns (bytes32); function archiveAt(uint256 _blockNumber) external view returns (bytes32); + function canPrune() external view returns (bool); function getProvenBlockNumber() external view returns (uint256); function getPendingBlockNumber() external view returns (uint256); function getEpochToProve() external view returns (Epoch); function getClaimableEpoch() external view returns (Epoch); function getEpochForBlock(uint256 blockNumber) external view returns (Epoch); - function validateEpochProofRightClaim(EpochProofQuoteLib.SignedEpochProofQuote calldata _quote) - external - view; + function validateEpochProofRightClaimAtTime( + Timestamp _ts, + EpochProofQuoteLib.SignedEpochProofQuote calldata _quote + ) external view; function getEpochProofPublicInputs( uint256 _epochSize, bytes32[7] calldata _args, diff --git a/l1-contracts/test/Rollup.t.sol b/l1-contracts/test/Rollup.t.sol index 6677ac68f06..457e3507845 100644 --- a/l1-contracts/test/Rollup.t.sol +++ b/l1-contracts/test/Rollup.t.sol @@ -115,6 +115,23 @@ contract RollupTest is DecoderBase { vm.warp(Timestamp.unwrap(rollup.getTimestampForSlot(Slot.wrap(_slot)))); } + function testClaimInTheFuture(uint256 _futureSlot) public setUpFor("mixed_block_1") { + uint256 futureSlot = bound(_futureSlot, 1, 1e20); + _testBlock("mixed_block_1", false, 1); + + rollup.validateEpochProofRightClaimAtTime(Timestamp.wrap(block.timestamp), signedQuote); + + Timestamp t = rollup.getTimestampForSlot(quote.validUntilSlot + Slot.wrap(futureSlot)); + vm.expectRevert( + abi.encodeWithSelector( + Errors.Rollup__QuoteExpired.selector, + Slot.wrap(futureSlot) + quote.validUntilSlot, + signedQuote.quote.validUntilSlot + ) + ); + rollup.validateEpochProofRightClaimAtTime(t, signedQuote); + } + function testClaimableEpoch(uint256 epochForMixedBlock) public setUpFor("mixed_block_1") { epochForMixedBlock = bound(epochForMixedBlock, 1, 10); vm.expectRevert(abi.encodeWithSelector(Errors.Rollup__NoEpochToProve.selector)); @@ -266,6 +283,8 @@ contract RollupTest is DecoderBase { function testClaimTwice() public setUpFor("mixed_block_1") { _testBlock("mixed_block_1", false, 1); + quote.validUntilSlot = Epoch.wrap(1e9).toSlots(); + signedQuote = _quoteToSignedQuote(quote); rollup.claimEpochProofRight(signedQuote); @@ -291,7 +310,8 @@ contract RollupTest is DecoderBase { function testClaimOutsideClaimPhase() public setUpFor("mixed_block_1") { _testBlock("mixed_block_1", false, 1); - + quote.validUntilSlot = Epoch.wrap(1e9).toSlots(); + signedQuote = _quoteToSignedQuote(quote); warpToL2Slot(Constants.AZTEC_EPOCH_DURATION + rollup.CLAIM_DURATION_IN_L2_SLOTS()); vm.expectRevert( @@ -840,19 +860,6 @@ contract RollupTest is DecoderBase { _submitEpochProof(rollup, 1, preArchive, data.archive, preBlockHash, wrongBlockHash, bytes32(0)); } - function _quoteToSignedQuote(EpochProofQuoteLib.EpochProofQuote memory _quote) - internal - view - returns (EpochProofQuoteLib.SignedEpochProofQuote memory) - { - bytes32 digest = rollup.quoteToDigest(_quote); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest); - return EpochProofQuoteLib.SignedEpochProofQuote({ - quote: _quote, - signature: SignatureLib.Signature({isEmpty: false, v: v, r: r, s: s}) - }); - } - function _testBlock(string memory name, bool _submitProof) public { _testBlock(name, _submitProof, 0); } @@ -998,4 +1005,17 @@ contract RollupTest is DecoderBase { _rollup.submitEpochRootProof(_epochSize, args, fees, aggregationObject, proof); } + + function _quoteToSignedQuote(EpochProofQuoteLib.EpochProofQuote memory _quote) + internal + view + returns (EpochProofQuoteLib.SignedEpochProofQuote memory) + { + bytes32 digest = rollup.quoteToDigest(_quote); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest); + return EpochProofQuoteLib.SignedEpochProofQuote({ + quote: _quote, + signature: SignatureLib.Signature({isEmpty: false, v: v, r: r, s: s}) + }); + } } diff --git a/yarn-project/sequencer-client/src/publisher/l1-publisher.ts b/yarn-project/sequencer-client/src/publisher/l1-publisher.ts index 1b8fea3571f..4486cd6c859 100644 --- a/yarn-project/sequencer-client/src/publisher/l1-publisher.ts +++ b/yarn-project/sequencer-client/src/publisher/l1-publisher.ts @@ -239,8 +239,11 @@ export class L1Publisher { // FIXME: This should not throw if unable to propose but return a falsey value, so // we can differentiate between errors when hitting the L1 rollup contract (eg RPC error) // which may require a retry, vs actually not being the turn for proposing. - const ts = BigInt((await this.publicClient.getBlock()).timestamp + BigInt(ETHEREUM_SLOT_DURATION)); - const [slot, blockNumber] = await this.rollupContract.read.canProposeAtTime([ts, `0x${archive.toString('hex')}`]); + const timeOfNextL1Slot = BigInt((await this.publicClient.getBlock()).timestamp + BigInt(ETHEREUM_SLOT_DURATION)); + const [slot, blockNumber] = await this.rollupContract.read.canProposeAtTime([ + timeOfNextL1Slot, + `0x${archive.toString('hex')}`, + ]); return [slot, blockNumber]; } @@ -302,9 +305,10 @@ export class L1Publisher { } public async validateProofQuote(quote: EpochProofQuote): Promise { - const args = [quote.toViemArgs()] as const; + const timeOfNextL1Slot = BigInt((await this.publicClient.getBlock()).timestamp + BigInt(ETHEREUM_SLOT_DURATION)); + const args = [timeOfNextL1Slot, quote.toViemArgs()] as const; try { - await this.rollupContract.read.validateEpochProofRightClaim(args, { account: this.account }); + await this.rollupContract.read.validateEpochProofRightClaimAtTime(args, { account: this.account }); } catch (err) { const errorName = tryGetCustomErrorName(err); this.log.warn(`Proof quote validation failed: ${errorName}`);