diff --git a/l1-contracts/src/core/Rollup.sol b/l1-contracts/src/core/Rollup.sol index 4ab86edf4e05..d1082047a185 100644 --- a/l1-contracts/src/core/Rollup.sol +++ b/l1-contracts/src/core/Rollup.sol @@ -463,7 +463,7 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Leonidas, IRollup, ITestRollup { bytes32 _txsEffectsHash, DataStructures.ExecutionFlags memory _flags ) external view override(IRollup) { - uint256 manaBaseFee = getManaBaseFee(true); + uint256 manaBaseFee = getManaBaseFeeAt(_currentTime, true); HeaderLib.Header memory header = HeaderLib.decode(_header); _validateHeader( header, _signatures, _digest, _currentTime, manaBaseFee, _txsEffectsHash, _flags @@ -552,7 +552,8 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Leonidas, IRollup, ITestRollup { HeaderLib.Header memory header = HeaderLib.decode(_args.header); setupEpoch(); - ManaBaseFeeComponents memory components = getManaBaseFeeComponents(true); + ManaBaseFeeComponents memory components = + getManaBaseFeeComponentsAt(Timestamp.wrap(block.timestamp), true); uint256 manaBaseFee = FeeMath.summedBaseFee(components); _validateHeader({ _header: header, @@ -657,13 +658,13 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Leonidas, IRollup, ITestRollup { ); } - /** - * @notice Gets the current l1 fees - * - * @return The current l1 fees - */ - function getCurrentL1Fees() public view override(IRollup) returns (L1FeeData memory) { - Slot slot = getCurrentSlot(); + function getL1FeesAt(Timestamp _timestamp) + public + view + override(IRollup) + returns (L1FeeData memory) + { + Slot slot = getSlotAt(_timestamp); if (slot < l1GasOracleValues.slotOfChange) { return l1GasOracleValues.pre; } @@ -677,9 +678,13 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Leonidas, IRollup, ITestRollup { * * @return The mana base fee */ - function getManaBaseFee(bool _inFeeAsset) public view override(IRollup) returns (uint256) { - ManaBaseFeeComponents memory components = getManaBaseFeeComponents(_inFeeAsset); - return components.summedBaseFee(); + function getManaBaseFeeAt(Timestamp _timestamp, bool _inFeeAsset) + public + view + override(IRollup) + returns (uint256) + { + return getManaBaseFeeComponentsAt(_timestamp, _inFeeAsset).summedBaseFee(); } /** @@ -694,18 +699,22 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Leonidas, IRollup, ITestRollup { * * @return The mana base fee components */ - function getManaBaseFeeComponents(bool _inFeeAsset) + function getManaBaseFeeComponentsAt(Timestamp _timestamp, bool _inFeeAsset) public view override(ITestRollup) returns (ManaBaseFeeComponents memory) { - FeeHeader storage parentFeeHeader = blocks[tips.pendingBlockNumber].feeHeader; + // If we can prune, we use the proven block, otherwise the pending block + uint256 blockOfInterest = + canPruneAtTime(_timestamp) ? tips.provenBlockNumber : tips.pendingBlockNumber; + + FeeHeader storage parentFeeHeader = blocks[blockOfInterest].feeHeader; uint256 excessMana = (parentFeeHeader.excessMana + parentFeeHeader.manaUsed).clampedAdd( -int256(FeeMath.MANA_TARGET) ); - L1FeeData memory fees = getCurrentL1Fees(); + L1FeeData memory fees = getL1FeesAt(_timestamp); uint256 dataCost = Math.mulDiv(3 * BLOB_GAS_PER_BLOB, fees.blobFee, FeeMath.MANA_TARGET, Math.Rounding.Ceil); uint256 gasUsed = FeeMath.L1_GAS_PER_BLOCK_PROPOSED + 3 * GAS_PER_BLOB_POINT_EVALUATION diff --git a/l1-contracts/src/core/interfaces/IRollup.sol b/l1-contracts/src/core/interfaces/IRollup.sol index f04333c4a22f..f52266dfe8ae 100644 --- a/l1-contracts/src/core/interfaces/IRollup.sol +++ b/l1-contracts/src/core/interfaces/IRollup.sol @@ -44,7 +44,7 @@ interface ITestRollup { function setVkTreeRoot(bytes32 _vkTreeRoot) external; function setProtocolContractTreeRoot(bytes32 _protocolContractTreeRoot) external; function setAssumeProvenThroughBlockNumber(uint256 _blockNumber) external; - function getManaBaseFeeComponents(bool _inFeeAsset) + function getManaBaseFeeComponentsAt(Timestamp _timestamp, bool _inFeeAsset) external view returns (ManaBaseFeeComponents memory); @@ -120,8 +120,8 @@ interface IRollup { returns (bytes32); function getBlock(uint256 _blockNumber) external view returns (BlockLog memory); function getFeeAssetPrice() external view returns (uint256); - function getManaBaseFee(bool _inFeeAsset) external view returns (uint256); - function getCurrentL1Fees() external view returns (L1FeeData memory); + function getManaBaseFeeAt(Timestamp _timestamp, bool _inFeeAsset) external view returns (uint256); + function getL1FeesAt(Timestamp _timestamp) external view returns (L1FeeData memory); function archive() external view returns (bytes32); function archiveAt(uint256 _blockNumber) external view returns (bytes32); diff --git a/l1-contracts/test/Rollup.t.sol b/l1-contracts/test/Rollup.t.sol index 3fc32d369d87..6c5abf747eed 100644 --- a/l1-contracts/test/Rollup.t.sol +++ b/l1-contracts/test/Rollup.t.sol @@ -1011,7 +1011,7 @@ contract RollupTest is DecoderBase, TimeFns { } function _updateHeaderBaseFee(bytes memory _header) internal view returns (bytes memory) { - uint256 baseFee = rollup.getManaBaseFee(true); + uint256 baseFee = rollup.getManaBaseFeeAt(Timestamp.wrap(block.timestamp), true); assembly { mstore(add(_header, add(0x20, 0x0228)), baseFee) } diff --git a/l1-contracts/test/fees/FeeRollup.t.sol b/l1-contracts/test/fees/FeeRollup.t.sol index 512d9fd2f8c6..7f131fb9da58 100644 --- a/l1-contracts/test/fees/FeeRollup.t.sol +++ b/l1-contracts/test/fees/FeeRollup.t.sol @@ -39,6 +39,7 @@ import {IRewardDistributor} from "@aztec/governance/interfaces/IRewardDistributo import {OracleInput} from "@aztec/core/libraries/FeeMath.sol"; import {ProposeArgs, OracleInput, ProposeLib} from "@aztec/core/libraries/ProposeLib.sol"; import {IERC20} from "@oz/token/ERC20/IERC20.sol"; +import {FeeMath} from "@aztec/core/libraries/FeeMath.sol"; import { FeeHeader as FeeHeaderModel, @@ -80,6 +81,8 @@ contract FakeCanonical { contract FeeRollupTest is FeeModelTestPoints, DecoderBase { using SlotLib for Slot; using EpochLib for Epoch; + using FeeMath for uint256; + using FeeMath for ManaBaseFeeComponents; // We need to build a block that we can submit. We will be using some values from // the empty blocks, but otherwise populate using the fee model test points. @@ -171,7 +174,11 @@ contract FeeRollupTest is FeeModelTestPoints, DecoderBase { + point.outputs.mana_base_fee_components_in_fee_asset.congestion_cost ); - assertEq(manaBaseFee, rollup.getManaBaseFee(true), "mana base fee mismatch"); + assertEq( + manaBaseFee, + rollup.getManaBaseFeeAt(Timestamp.wrap(block.timestamp), true), + "mana base fee mismatch" + ); uint256 manaSpent = point.block_header.mana_spent; @@ -212,12 +219,92 @@ contract FeeRollupTest is FeeModelTestPoints, DecoderBase { }); } + function test__FeeModelPrune() public { + // Submit a few blocks, then compute what the fees would be with/without a potential prune + // and ensure that they match what happens. + Slot nextSlot = Slot.wrap(1); + for (uint256 i = 0; i < SLOT_DURATION / 12 * 5; i++) { + _loadL1Metadata(i); + + if (rollup.getCurrentSlot() == nextSlot) { + TestPoint memory point = points[nextSlot.unwrap() - 1]; + Block memory b = getBlock(); + + rollup.propose( + ProposeArgs({ + header: b.header, + archive: b.archive, + blockHash: b.blockHash, + oracleInput: OracleInput({ + provingCostModifier: point.oracle_input.proving_cost_modifier, + feeAssetPriceModifier: point.oracle_input.fee_asset_price_modifier + }), + txHashes: b.txHashes + }), + b.signatures, + b.body + ); + nextSlot = nextSlot + Slot.wrap(1); + } + } + + FeeHeader memory parentFeeHeaderNoPrune = + rollup.getBlock(rollup.getPendingBlockNumber()).feeHeader; + uint256 excessManaNoPrune = ( + parentFeeHeaderNoPrune.excessMana + parentFeeHeaderNoPrune.manaUsed + ).clampedAdd(-int256(FeeMath.MANA_TARGET)); + + FeeHeader memory parentFeeHeaderPrune = rollup.getBlock(rollup.getProvenBlockNumber()).feeHeader; + uint256 excessManaPrune = (parentFeeHeaderPrune.excessMana + parentFeeHeaderPrune.manaUsed) + .clampedAdd(-int256(FeeMath.MANA_TARGET)); + + assertGt(excessManaNoPrune, excessManaPrune, "excess mana should be lower if we prune"); + + // Find the point in time where we can prune. We can be smarter, but I'm not trying to be smart here + // trying to be foolproof, for I am a fool. + uint256 timeOfPrune = block.timestamp; + while (!rollup.canPruneAtTime(Timestamp.wrap(timeOfPrune))) { + timeOfPrune += SLOT_DURATION; + } + + ManaBaseFeeComponents memory componentsPrune = + rollup.getManaBaseFeeComponentsAt(Timestamp.wrap(timeOfPrune), true); + + // If we assume that everything is proven, we will see what the fee would be if we did not prune. + rollup.setAssumeProvenThroughBlockNumber(10000); + ManaBaseFeeComponents memory componentsNoPrune = + rollup.getManaBaseFeeComponentsAt(Timestamp.wrap(timeOfPrune), true); + + // The congestion multipliers should be different, with the no-prune being higher + // as it is based on the accumulated excess mana. + assertGt( + componentsNoPrune.congestionMultiplier, + componentsPrune.congestionMultiplier, + "congestion multiplier should be higher if we do not prune" + ); + + assertEq( + componentsPrune.congestionMultiplier, + FeeMath.congestionMultiplier(excessManaPrune), + "congestion multiplier mismatch for prune" + ); + assertEq( + componentsNoPrune.congestionMultiplier, + FeeMath.congestionMultiplier(excessManaNoPrune), + "congestion multiplier mismatch for no-prune" + ); + } + function test_FeeModelEquivalence() public { Slot nextSlot = Slot.wrap(1); Epoch nextEpoch = Epoch.wrap(1); // Loop through all of the L1 metadata for (uint256 i = 0; i < l1Metadata.length; i++) { + // Predict what the fee will be before we jump in time! + uint256 baseFeePrediction = + rollup.getManaBaseFeeAt(Timestamp.wrap(l1Metadata[i].timestamp), true); + _loadL1Metadata(i); // For every "new" slot we encounter, we construct a block using current L1 Data @@ -226,11 +313,13 @@ contract FeeRollupTest is FeeModelTestPoints, DecoderBase { if (rollup.getCurrentSlot() == nextSlot) { TestPoint memory point = points[nextSlot.unwrap() - 1]; - L1FeeData memory fees = rollup.getCurrentL1Fees(); + L1FeeData memory fees = rollup.getL1FeesAt(Timestamp.wrap(block.timestamp)); uint256 feeAssetPrice = rollup.getFeeAssetPrice(); - ManaBaseFeeComponents memory components = rollup.getManaBaseFeeComponents(false); - ManaBaseFeeComponents memory componentsFeeAsset = rollup.getManaBaseFeeComponents(true); + ManaBaseFeeComponents memory components = + rollup.getManaBaseFeeComponentsAt(Timestamp.wrap(block.timestamp), false); + ManaBaseFeeComponents memory componentsFeeAsset = + rollup.getManaBaseFeeComponentsAt(Timestamp.wrap(block.timestamp), true); BlockLog memory parentBlockLog = rollup.getBlock(nextSlot.unwrap() - 1); Block memory b = getBlock(); @@ -252,6 +341,10 @@ contract FeeRollupTest is FeeModelTestPoints, DecoderBase { BlockLog memory blockLog = rollup.getBlock(nextSlot.unwrap()); + assertEq( + baseFeePrediction, componentsFeeAsset.summedBaseFee(), "base fee prediction mismatch" + ); + assertEq( componentsFeeAsset.congestionCost, blockLog.feeHeader.congestionCost, diff --git a/yarn-project/circuits.js/src/contract/artifact_hash.test.ts b/yarn-project/circuits.js/src/contract/artifact_hash.test.ts index 06f2bb682bfc..af06a189075f 100644 --- a/yarn-project/circuits.js/src/contract/artifact_hash.test.ts +++ b/yarn-project/circuits.js/src/contract/artifact_hash.test.ts @@ -7,7 +7,7 @@ import { readFileSync } from 'fs'; import { getPathToFixture, getTestContractArtifact } from '../tests/fixtures.js'; import { computeArtifactHash } from './artifact_hash.js'; -const TEST_CONTRACT_ARTIFACT_HASH = `"0x1d429080e986cf55e59203b4229063bf9b4d875e832fe56c5257303075110190"`; +const TEST_CONTRACT_ARTIFACT_HASH = `"0x19142676527045a118066698e292cc35db16ab4d7bd16610d35d2e1c607eb8b2"`; describe('ArtifactHash', () => { it('calculates the artifact hash', () => { diff --git a/yarn-project/end-to-end/src/composed/integration_l1_publisher.test.ts b/yarn-project/end-to-end/src/composed/integration_l1_publisher.test.ts index b0bdace0d02a..bf31fcc152a2 100644 --- a/yarn-project/end-to-end/src/composed/integration_l1_publisher.test.ts +++ b/yarn-project/end-to-end/src/composed/integration_l1_publisher.test.ts @@ -367,16 +367,17 @@ describe('L1Publisher integration', () => { const ts = (await publicClient.getBlock()).timestamp; const slot = await rollup.read.getSlotAt([ts + BigInt(config.ethereumSlotDuration)]); + const timestamp = await rollup.read.getTimestampForSlot([slot]); const globalVariables = new GlobalVariables( new Fr(chainId), new Fr(config.version), new Fr(1 + i), new Fr(slot), - new Fr(await rollup.read.getTimestampForSlot([slot])), + new Fr(timestamp), coinbase, feeRecipient, - new GasFees(Fr.ZERO, new Fr(await rollup.read.getManaBaseFee([true]))), + new GasFees(Fr.ZERO, new Fr(await rollup.read.getManaBaseFeeAt([timestamp, true]))), ); const block = await buildBlock(globalVariables, txs, currentL1ToL2Messages); @@ -479,15 +480,16 @@ describe('L1Publisher integration', () => { const ts = (await publicClient.getBlock()).timestamp; const slot = await rollup.read.getSlotAt([ts + BigInt(config.ethereumSlotDuration)]); + const timestamp = await rollup.read.getTimestampForSlot([slot]); const globalVariables = new GlobalVariables( new Fr(chainId), new Fr(config.version), new Fr(1 + i), new Fr(slot), - new Fr(await rollup.read.getTimestampForSlot([slot])), + new Fr(timestamp), coinbase, feeRecipient, - new GasFees(Fr.ZERO, new Fr(await rollup.read.getManaBaseFee([true]))), + new GasFees(Fr.ZERO, new Fr(await rollup.read.getManaBaseFeeAt([timestamp, true]))), ); const block = await buildBlock(globalVariables, txs, l1ToL2Messages); prevHeader = block.header; @@ -554,15 +556,16 @@ describe('L1Publisher integration', () => { const txs = [makeEmptyProcessedTx(), makeEmptyProcessedTx()]; const ts = (await publicClient.getBlock()).timestamp; const slot = await rollup.read.getSlotAt([ts + BigInt(config.ethereumSlotDuration)]); + const timestamp = await rollup.read.getTimestampForSlot([slot]); const globalVariables = new GlobalVariables( new Fr(chainId), new Fr(config.version), new Fr(1), new Fr(slot), - new Fr(await rollup.read.getTimestampForSlot([slot])), + new Fr(timestamp), coinbase, feeRecipient, - new GasFees(Fr.ZERO, new Fr(await rollup.read.getManaBaseFee([true]))), + new GasFees(Fr.ZERO, new Fr(await rollup.read.getManaBaseFeeAt([timestamp, true]))), ); const block = await buildBlock(globalVariables, txs, l1ToL2Messages); prevHeader = block.header; diff --git a/yarn-project/sequencer-client/src/global_variable_builder/global_builder.ts b/yarn-project/sequencer-client/src/global_variable_builder/global_builder.ts index 3ca0a790c77d..ecd911cc97f5 100644 --- a/yarn-project/sequencer-client/src/global_variable_builder/global_builder.ts +++ b/yarn-project/sequencer-client/src/global_variable_builder/global_builder.ts @@ -46,8 +46,21 @@ export class GlobalVariableBuilder implements GlobalVariableBuilderInterface { }); } + /** + * Computes the "current" base fees, e.g., the price that you currently should pay to get include in the next block + * @returns Base fees for the expected next block + */ public async getCurrentBaseFees(): Promise { - return new GasFees(Fr.ZERO, new Fr(await this.rollupContract.read.getManaBaseFee([true]))); + // Since this might be called in the middle of a slot where a block might have been published, + // we need to fetch the last block written, and estimate the earliest timestamp for the next block. + // The timestamp of that last block will act as a lower bound for the next block. + + const lastBlock = await this.rollupContract.read.getBlock([await this.rollupContract.read.getPendingBlockNumber()]); + const earliestTimestamp = await this.rollupContract.read.getTimestampForSlot([lastBlock.slotNumber + 1n]); + const nextEthTimestamp = BigInt((await this.publicClient.getBlock()).timestamp + BigInt(this.ethereumSlotDuration)); + const timestamp = earliestTimestamp > nextEthTimestamp ? earliestTimestamp : nextEthTimestamp; + + return new GasFees(Fr.ZERO, new Fr(await this.rollupContract.read.getManaBaseFeeAt([timestamp, true]))); } /** @@ -77,7 +90,8 @@ export class GlobalVariableBuilder implements GlobalVariableBuilderInterface { const slotFr = new Fr(slotNumber); const timestampFr = new Fr(timestamp); - const gasFees = await this.getCurrentBaseFees(); + // We can skip much of the logic in getCurrentBaseFees since it we already check that we are not within a slot elsewhere. + const gasFees = new GasFees(Fr.ZERO, new Fr(await this.rollupContract.read.getManaBaseFeeAt([timestamp, true]))); const globalVariables = new GlobalVariables( chainId,