Skip to content

Commit

Permalink
feat: fee foresight support (#10262)
Browse files Browse the repository at this point in the history
  • Loading branch information
LHerskind authored Nov 28, 2024
1 parent d260eaa commit 9e19244
Show file tree
Hide file tree
Showing 7 changed files with 151 additions and 32 deletions.
39 changes: 24 additions & 15 deletions l1-contracts/src/core/Rollup.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
}
Expand All @@ -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();
}

/**
Expand All @@ -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
Expand Down
6 changes: 3 additions & 3 deletions l1-contracts/src/core/interfaces/IRollup.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion l1-contracts/test/Rollup.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
101 changes: 97 additions & 4 deletions l1-contracts/test/fees/FeeRollup.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.

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

Expand Down Expand Up @@ -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
Expand All @@ -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();
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<GasFees> {
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])));
}

/**
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit 9e19244

Please sign in to comment.