Skip to content

Commit 9e19244

Browse files
authored
feat: fee foresight support (#10262)
1 parent d260eaa commit 9e19244

File tree

7 files changed

+151
-32
lines changed

7 files changed

+151
-32
lines changed

l1-contracts/src/core/Rollup.sol

+24-15
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,7 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Leonidas, IRollup, ITestRollup {
463463
bytes32 _txsEffectsHash,
464464
DataStructures.ExecutionFlags memory _flags
465465
) external view override(IRollup) {
466-
uint256 manaBaseFee = getManaBaseFee(true);
466+
uint256 manaBaseFee = getManaBaseFeeAt(_currentTime, true);
467467
HeaderLib.Header memory header = HeaderLib.decode(_header);
468468
_validateHeader(
469469
header, _signatures, _digest, _currentTime, manaBaseFee, _txsEffectsHash, _flags
@@ -552,7 +552,8 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Leonidas, IRollup, ITestRollup {
552552
HeaderLib.Header memory header = HeaderLib.decode(_args.header);
553553

554554
setupEpoch();
555-
ManaBaseFeeComponents memory components = getManaBaseFeeComponents(true);
555+
ManaBaseFeeComponents memory components =
556+
getManaBaseFeeComponentsAt(Timestamp.wrap(block.timestamp), true);
556557
uint256 manaBaseFee = FeeMath.summedBaseFee(components);
557558
_validateHeader({
558559
_header: header,
@@ -657,13 +658,13 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Leonidas, IRollup, ITestRollup {
657658
);
658659
}
659660

660-
/**
661-
* @notice Gets the current l1 fees
662-
*
663-
* @return The current l1 fees
664-
*/
665-
function getCurrentL1Fees() public view override(IRollup) returns (L1FeeData memory) {
666-
Slot slot = getCurrentSlot();
661+
function getL1FeesAt(Timestamp _timestamp)
662+
public
663+
view
664+
override(IRollup)
665+
returns (L1FeeData memory)
666+
{
667+
Slot slot = getSlotAt(_timestamp);
667668
if (slot < l1GasOracleValues.slotOfChange) {
668669
return l1GasOracleValues.pre;
669670
}
@@ -677,9 +678,13 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Leonidas, IRollup, ITestRollup {
677678
*
678679
* @return The mana base fee
679680
*/
680-
function getManaBaseFee(bool _inFeeAsset) public view override(IRollup) returns (uint256) {
681-
ManaBaseFeeComponents memory components = getManaBaseFeeComponents(_inFeeAsset);
682-
return components.summedBaseFee();
681+
function getManaBaseFeeAt(Timestamp _timestamp, bool _inFeeAsset)
682+
public
683+
view
684+
override(IRollup)
685+
returns (uint256)
686+
{
687+
return getManaBaseFeeComponentsAt(_timestamp, _inFeeAsset).summedBaseFee();
683688
}
684689

685690
/**
@@ -694,18 +699,22 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Leonidas, IRollup, ITestRollup {
694699
*
695700
* @return The mana base fee components
696701
*/
697-
function getManaBaseFeeComponents(bool _inFeeAsset)
702+
function getManaBaseFeeComponentsAt(Timestamp _timestamp, bool _inFeeAsset)
698703
public
699704
view
700705
override(ITestRollup)
701706
returns (ManaBaseFeeComponents memory)
702707
{
703-
FeeHeader storage parentFeeHeader = blocks[tips.pendingBlockNumber].feeHeader;
708+
// If we can prune, we use the proven block, otherwise the pending block
709+
uint256 blockOfInterest =
710+
canPruneAtTime(_timestamp) ? tips.provenBlockNumber : tips.pendingBlockNumber;
711+
712+
FeeHeader storage parentFeeHeader = blocks[blockOfInterest].feeHeader;
704713
uint256 excessMana = (parentFeeHeader.excessMana + parentFeeHeader.manaUsed).clampedAdd(
705714
-int256(FeeMath.MANA_TARGET)
706715
);
707716

708-
L1FeeData memory fees = getCurrentL1Fees();
717+
L1FeeData memory fees = getL1FeesAt(_timestamp);
709718
uint256 dataCost =
710719
Math.mulDiv(3 * BLOB_GAS_PER_BLOB, fees.blobFee, FeeMath.MANA_TARGET, Math.Rounding.Ceil);
711720
uint256 gasUsed = FeeMath.L1_GAS_PER_BLOCK_PROPOSED + 3 * GAS_PER_BLOB_POINT_EVALUATION

l1-contracts/src/core/interfaces/IRollup.sol

+3-3
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ interface ITestRollup {
4444
function setVkTreeRoot(bytes32 _vkTreeRoot) external;
4545
function setProtocolContractTreeRoot(bytes32 _protocolContractTreeRoot) external;
4646
function setAssumeProvenThroughBlockNumber(uint256 _blockNumber) external;
47-
function getManaBaseFeeComponents(bool _inFeeAsset)
47+
function getManaBaseFeeComponentsAt(Timestamp _timestamp, bool _inFeeAsset)
4848
external
4949
view
5050
returns (ManaBaseFeeComponents memory);
@@ -120,8 +120,8 @@ interface IRollup {
120120
returns (bytes32);
121121
function getBlock(uint256 _blockNumber) external view returns (BlockLog memory);
122122
function getFeeAssetPrice() external view returns (uint256);
123-
function getManaBaseFee(bool _inFeeAsset) external view returns (uint256);
124-
function getCurrentL1Fees() external view returns (L1FeeData memory);
123+
function getManaBaseFeeAt(Timestamp _timestamp, bool _inFeeAsset) external view returns (uint256);
124+
function getL1FeesAt(Timestamp _timestamp) external view returns (L1FeeData memory);
125125

126126
function archive() external view returns (bytes32);
127127
function archiveAt(uint256 _blockNumber) external view returns (bytes32);

l1-contracts/test/Rollup.t.sol

+1-1
Original file line numberDiff line numberDiff line change
@@ -1011,7 +1011,7 @@ contract RollupTest is DecoderBase, TimeFns {
10111011
}
10121012

10131013
function _updateHeaderBaseFee(bytes memory _header) internal view returns (bytes memory) {
1014-
uint256 baseFee = rollup.getManaBaseFee(true);
1014+
uint256 baseFee = rollup.getManaBaseFeeAt(Timestamp.wrap(block.timestamp), true);
10151015
assembly {
10161016
mstore(add(_header, add(0x20, 0x0228)), baseFee)
10171017
}

l1-contracts/test/fees/FeeRollup.t.sol

+97-4
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {IRewardDistributor} from "@aztec/governance/interfaces/IRewardDistributo
3939
import {OracleInput} from "@aztec/core/libraries/FeeMath.sol";
4040
import {ProposeArgs, OracleInput, ProposeLib} from "@aztec/core/libraries/ProposeLib.sol";
4141
import {IERC20} from "@oz/token/ERC20/IERC20.sol";
42+
import {FeeMath} from "@aztec/core/libraries/FeeMath.sol";
4243

4344
import {
4445
FeeHeader as FeeHeaderModel,
@@ -80,6 +81,8 @@ contract FakeCanonical {
8081
contract FeeRollupTest is FeeModelTestPoints, DecoderBase {
8182
using SlotLib for Slot;
8283
using EpochLib for Epoch;
84+
using FeeMath for uint256;
85+
using FeeMath for ManaBaseFeeComponents;
8386
// We need to build a block that we can submit. We will be using some values from
8487
// the empty blocks, but otherwise populate using the fee model test points.
8588

@@ -171,7 +174,11 @@ contract FeeRollupTest is FeeModelTestPoints, DecoderBase {
171174
+ point.outputs.mana_base_fee_components_in_fee_asset.congestion_cost
172175
);
173176

174-
assertEq(manaBaseFee, rollup.getManaBaseFee(true), "mana base fee mismatch");
177+
assertEq(
178+
manaBaseFee,
179+
rollup.getManaBaseFeeAt(Timestamp.wrap(block.timestamp), true),
180+
"mana base fee mismatch"
181+
);
175182

176183
uint256 manaSpent = point.block_header.mana_spent;
177184

@@ -212,12 +219,92 @@ contract FeeRollupTest is FeeModelTestPoints, DecoderBase {
212219
});
213220
}
214221

222+
function test__FeeModelPrune() public {
223+
// Submit a few blocks, then compute what the fees would be with/without a potential prune
224+
// and ensure that they match what happens.
225+
Slot nextSlot = Slot.wrap(1);
226+
for (uint256 i = 0; i < SLOT_DURATION / 12 * 5; i++) {
227+
_loadL1Metadata(i);
228+
229+
if (rollup.getCurrentSlot() == nextSlot) {
230+
TestPoint memory point = points[nextSlot.unwrap() - 1];
231+
Block memory b = getBlock();
232+
233+
rollup.propose(
234+
ProposeArgs({
235+
header: b.header,
236+
archive: b.archive,
237+
blockHash: b.blockHash,
238+
oracleInput: OracleInput({
239+
provingCostModifier: point.oracle_input.proving_cost_modifier,
240+
feeAssetPriceModifier: point.oracle_input.fee_asset_price_modifier
241+
}),
242+
txHashes: b.txHashes
243+
}),
244+
b.signatures,
245+
b.body
246+
);
247+
nextSlot = nextSlot + Slot.wrap(1);
248+
}
249+
}
250+
251+
FeeHeader memory parentFeeHeaderNoPrune =
252+
rollup.getBlock(rollup.getPendingBlockNumber()).feeHeader;
253+
uint256 excessManaNoPrune = (
254+
parentFeeHeaderNoPrune.excessMana + parentFeeHeaderNoPrune.manaUsed
255+
).clampedAdd(-int256(FeeMath.MANA_TARGET));
256+
257+
FeeHeader memory parentFeeHeaderPrune = rollup.getBlock(rollup.getProvenBlockNumber()).feeHeader;
258+
uint256 excessManaPrune = (parentFeeHeaderPrune.excessMana + parentFeeHeaderPrune.manaUsed)
259+
.clampedAdd(-int256(FeeMath.MANA_TARGET));
260+
261+
assertGt(excessManaNoPrune, excessManaPrune, "excess mana should be lower if we prune");
262+
263+
// Find the point in time where we can prune. We can be smarter, but I'm not trying to be smart here
264+
// trying to be foolproof, for I am a fool.
265+
uint256 timeOfPrune = block.timestamp;
266+
while (!rollup.canPruneAtTime(Timestamp.wrap(timeOfPrune))) {
267+
timeOfPrune += SLOT_DURATION;
268+
}
269+
270+
ManaBaseFeeComponents memory componentsPrune =
271+
rollup.getManaBaseFeeComponentsAt(Timestamp.wrap(timeOfPrune), true);
272+
273+
// If we assume that everything is proven, we will see what the fee would be if we did not prune.
274+
rollup.setAssumeProvenThroughBlockNumber(10000);
275+
ManaBaseFeeComponents memory componentsNoPrune =
276+
rollup.getManaBaseFeeComponentsAt(Timestamp.wrap(timeOfPrune), true);
277+
278+
// The congestion multipliers should be different, with the no-prune being higher
279+
// as it is based on the accumulated excess mana.
280+
assertGt(
281+
componentsNoPrune.congestionMultiplier,
282+
componentsPrune.congestionMultiplier,
283+
"congestion multiplier should be higher if we do not prune"
284+
);
285+
286+
assertEq(
287+
componentsPrune.congestionMultiplier,
288+
FeeMath.congestionMultiplier(excessManaPrune),
289+
"congestion multiplier mismatch for prune"
290+
);
291+
assertEq(
292+
componentsNoPrune.congestionMultiplier,
293+
FeeMath.congestionMultiplier(excessManaNoPrune),
294+
"congestion multiplier mismatch for no-prune"
295+
);
296+
}
297+
215298
function test_FeeModelEquivalence() public {
216299
Slot nextSlot = Slot.wrap(1);
217300
Epoch nextEpoch = Epoch.wrap(1);
218301

219302
// Loop through all of the L1 metadata
220303
for (uint256 i = 0; i < l1Metadata.length; i++) {
304+
// Predict what the fee will be before we jump in time!
305+
uint256 baseFeePrediction =
306+
rollup.getManaBaseFeeAt(Timestamp.wrap(l1Metadata[i].timestamp), true);
307+
221308
_loadL1Metadata(i);
222309

223310
// For every "new" slot we encounter, we construct a block using current L1 Data
@@ -226,11 +313,13 @@ contract FeeRollupTest is FeeModelTestPoints, DecoderBase {
226313
if (rollup.getCurrentSlot() == nextSlot) {
227314
TestPoint memory point = points[nextSlot.unwrap() - 1];
228315

229-
L1FeeData memory fees = rollup.getCurrentL1Fees();
316+
L1FeeData memory fees = rollup.getL1FeesAt(Timestamp.wrap(block.timestamp));
230317
uint256 feeAssetPrice = rollup.getFeeAssetPrice();
231318

232-
ManaBaseFeeComponents memory components = rollup.getManaBaseFeeComponents(false);
233-
ManaBaseFeeComponents memory componentsFeeAsset = rollup.getManaBaseFeeComponents(true);
319+
ManaBaseFeeComponents memory components =
320+
rollup.getManaBaseFeeComponentsAt(Timestamp.wrap(block.timestamp), false);
321+
ManaBaseFeeComponents memory componentsFeeAsset =
322+
rollup.getManaBaseFeeComponentsAt(Timestamp.wrap(block.timestamp), true);
234323
BlockLog memory parentBlockLog = rollup.getBlock(nextSlot.unwrap() - 1);
235324

236325
Block memory b = getBlock();
@@ -252,6 +341,10 @@ contract FeeRollupTest is FeeModelTestPoints, DecoderBase {
252341

253342
BlockLog memory blockLog = rollup.getBlock(nextSlot.unwrap());
254343

344+
assertEq(
345+
baseFeePrediction, componentsFeeAsset.summedBaseFee(), "base fee prediction mismatch"
346+
);
347+
255348
assertEq(
256349
componentsFeeAsset.congestionCost,
257350
blockLog.feeHeader.congestionCost,

yarn-project/circuits.js/src/contract/artifact_hash.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { readFileSync } from 'fs';
77
import { getPathToFixture, getTestContractArtifact } from '../tests/fixtures.js';
88
import { computeArtifactHash } from './artifact_hash.js';
99

10-
const TEST_CONTRACT_ARTIFACT_HASH = `"0x1d429080e986cf55e59203b4229063bf9b4d875e832fe56c5257303075110190"`;
10+
const TEST_CONTRACT_ARTIFACT_HASH = `"0x19142676527045a118066698e292cc35db16ab4d7bd16610d35d2e1c607eb8b2"`;
1111

1212
describe('ArtifactHash', () => {
1313
it('calculates the artifact hash', () => {

yarn-project/end-to-end/src/composed/integration_l1_publisher.test.ts

+9-6
Original file line numberDiff line numberDiff line change
@@ -367,16 +367,17 @@ describe('L1Publisher integration', () => {
367367

368368
const ts = (await publicClient.getBlock()).timestamp;
369369
const slot = await rollup.read.getSlotAt([ts + BigInt(config.ethereumSlotDuration)]);
370+
const timestamp = await rollup.read.getTimestampForSlot([slot]);
370371

371372
const globalVariables = new GlobalVariables(
372373
new Fr(chainId),
373374
new Fr(config.version),
374375
new Fr(1 + i),
375376
new Fr(slot),
376-
new Fr(await rollup.read.getTimestampForSlot([slot])),
377+
new Fr(timestamp),
377378
coinbase,
378379
feeRecipient,
379-
new GasFees(Fr.ZERO, new Fr(await rollup.read.getManaBaseFee([true]))),
380+
new GasFees(Fr.ZERO, new Fr(await rollup.read.getManaBaseFeeAt([timestamp, true]))),
380381
);
381382

382383
const block = await buildBlock(globalVariables, txs, currentL1ToL2Messages);
@@ -479,15 +480,16 @@ describe('L1Publisher integration', () => {
479480

480481
const ts = (await publicClient.getBlock()).timestamp;
481482
const slot = await rollup.read.getSlotAt([ts + BigInt(config.ethereumSlotDuration)]);
483+
const timestamp = await rollup.read.getTimestampForSlot([slot]);
482484
const globalVariables = new GlobalVariables(
483485
new Fr(chainId),
484486
new Fr(config.version),
485487
new Fr(1 + i),
486488
new Fr(slot),
487-
new Fr(await rollup.read.getTimestampForSlot([slot])),
489+
new Fr(timestamp),
488490
coinbase,
489491
feeRecipient,
490-
new GasFees(Fr.ZERO, new Fr(await rollup.read.getManaBaseFee([true]))),
492+
new GasFees(Fr.ZERO, new Fr(await rollup.read.getManaBaseFeeAt([timestamp, true]))),
491493
);
492494
const block = await buildBlock(globalVariables, txs, l1ToL2Messages);
493495
prevHeader = block.header;
@@ -554,15 +556,16 @@ describe('L1Publisher integration', () => {
554556
const txs = [makeEmptyProcessedTx(), makeEmptyProcessedTx()];
555557
const ts = (await publicClient.getBlock()).timestamp;
556558
const slot = await rollup.read.getSlotAt([ts + BigInt(config.ethereumSlotDuration)]);
559+
const timestamp = await rollup.read.getTimestampForSlot([slot]);
557560
const globalVariables = new GlobalVariables(
558561
new Fr(chainId),
559562
new Fr(config.version),
560563
new Fr(1),
561564
new Fr(slot),
562-
new Fr(await rollup.read.getTimestampForSlot([slot])),
565+
new Fr(timestamp),
563566
coinbase,
564567
feeRecipient,
565-
new GasFees(Fr.ZERO, new Fr(await rollup.read.getManaBaseFee([true]))),
568+
new GasFees(Fr.ZERO, new Fr(await rollup.read.getManaBaseFeeAt([timestamp, true]))),
566569
);
567570
const block = await buildBlock(globalVariables, txs, l1ToL2Messages);
568571
prevHeader = block.header;

yarn-project/sequencer-client/src/global_variable_builder/global_builder.ts

+16-2
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,21 @@ export class GlobalVariableBuilder implements GlobalVariableBuilderInterface {
4646
});
4747
}
4848

49+
/**
50+
* Computes the "current" base fees, e.g., the price that you currently should pay to get include in the next block
51+
* @returns Base fees for the expected next block
52+
*/
4953
public async getCurrentBaseFees(): Promise<GasFees> {
50-
return new GasFees(Fr.ZERO, new Fr(await this.rollupContract.read.getManaBaseFee([true])));
54+
// Since this might be called in the middle of a slot where a block might have been published,
55+
// we need to fetch the last block written, and estimate the earliest timestamp for the next block.
56+
// The timestamp of that last block will act as a lower bound for the next block.
57+
58+
const lastBlock = await this.rollupContract.read.getBlock([await this.rollupContract.read.getPendingBlockNumber()]);
59+
const earliestTimestamp = await this.rollupContract.read.getTimestampForSlot([lastBlock.slotNumber + 1n]);
60+
const nextEthTimestamp = BigInt((await this.publicClient.getBlock()).timestamp + BigInt(this.ethereumSlotDuration));
61+
const timestamp = earliestTimestamp > nextEthTimestamp ? earliestTimestamp : nextEthTimestamp;
62+
63+
return new GasFees(Fr.ZERO, new Fr(await this.rollupContract.read.getManaBaseFeeAt([timestamp, true])));
5164
}
5265

5366
/**
@@ -77,7 +90,8 @@ export class GlobalVariableBuilder implements GlobalVariableBuilderInterface {
7790
const slotFr = new Fr(slotNumber);
7891
const timestampFr = new Fr(timestamp);
7992

80-
const gasFees = await this.getCurrentBaseFees();
93+
// We can skip much of the logic in getCurrentBaseFees since it we already check that we are not within a slot elsewhere.
94+
const gasFees = new GasFees(Fr.ZERO, new Fr(await this.rollupContract.read.getManaBaseFeeAt([timestamp, true])));
8195

8296
const globalVariables = new GlobalVariables(
8397
chainId,

0 commit comments

Comments
 (0)