Skip to content

Commit 6feb805

Browse files
spalladinoclaude
andcommitted
fix(sequencer-client): account for next L1 block slot in fee prediction window
Addresses PR #22116 review comment 8: if the next L1 block lands in the next L2 slot, the prediction window should start from that slot. Also moves publicClient to FeePredictor constructor instead of passing it on every call. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 5b222d5 commit 6feb805

3 files changed

Lines changed: 93 additions & 26 deletions

File tree

yarn-project/sequencer-client/src/global_variable_builder/fee_predictor.test.ts

Lines changed: 67 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ describe('FeePredictor', () => {
2525
let rollup: RollupContract;
2626

2727
let slotDuration: number;
28+
let ethereumSlotDuration: number;
2829
let l1GenesisTime: bigint;
30+
let dateProvider: DateProvider;
2931

3032
beforeAll(async () => {
3133
const privateKeyRaw = '0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba';
@@ -47,7 +49,9 @@ describe('FeePredictor', () => {
4749
rollupCheatCodes = RollupCheatCodes.create([rpcUrl], deployed.l1ContractAddresses, new DateProvider());
4850

4951
slotDuration = await rollup.getSlotDuration();
52+
ethereumSlotDuration = DefaultL1ContractsConfig.ethereumSlotDuration;
5053
l1GenesisTime = await rollup.getL1GenesisTime();
54+
dateProvider = new DateProvider();
5155
}, 60_000);
5256

5357
afterAll(async () => {
@@ -71,15 +75,29 @@ describe('FeePredictor', () => {
7175
const l1Fee = await rollup.getManaMinFeeAt(getTimestamp(startSlot), true);
7276

7377
for (const manaUsage of Object.values(ManaUsageEstimate)) {
74-
const predictor = new FeePredictor(rollup, slotDuration, l1GenesisTime);
75-
const predicted = await predictor.getPredictedMinFees(publicClient, manaUsage);
78+
const predictor = new FeePredictor(
79+
rollup,
80+
publicClient,
81+
dateProvider,
82+
slotDuration,
83+
l1GenesisTime,
84+
ethereumSlotDuration,
85+
);
86+
const predicted = await predictor.getPredictedMinFees(manaUsage);
7687
expect(predicted[0].feePerL2Gas).toBe(l1Fee);
7788
}
7889
});
7990

8091
it('all slots match L1 with ManaUsageEstimate.None and zero congestion', async () => {
81-
const predictor = new FeePredictor(rollup, slotDuration, l1GenesisTime);
82-
const predicted = await predictor.getPredictedMinFees(publicClient, ManaUsageEstimate.None);
92+
const predictor = new FeePredictor(
93+
rollup,
94+
publicClient,
95+
dateProvider,
96+
slotDuration,
97+
l1GenesisTime,
98+
ethereumSlotDuration,
99+
);
100+
const predicted = await predictor.getPredictedMinFees(ManaUsageEstimate.None);
83101

84102
const startSlot = await getPredictionStartSlot();
85103
for (let i = 0; i < predicted.length; i++) {
@@ -99,8 +117,15 @@ describe('FeePredictor', () => {
99117
await cheatCodes.mine();
100118
await rollupCheatCodes.updateL1GasFeeOracle();
101119

102-
const predictor = new FeePredictor(rollup, slotDuration, l1GenesisTime);
103-
const predicted = await predictor.getPredictedMinFees(publicClient, ManaUsageEstimate.None);
120+
const predictor = new FeePredictor(
121+
rollup,
122+
publicClient,
123+
dateProvider,
124+
slotDuration,
125+
l1GenesisTime,
126+
ethereumSlotDuration,
127+
);
128+
const predicted = await predictor.getPredictedMinFees(ManaUsageEstimate.None);
104129

105130
const startSlot = await getPredictionStartSlot();
106131
for (let i = 0; i < predicted.length; i++) {
@@ -117,17 +142,31 @@ describe('FeePredictor', () => {
117142
await cheatCodes.mine();
118143
await rollupCheatCodes.advanceSlots(3);
119144

120-
const predictor = new FeePredictor(rollup, slotDuration, l1GenesisTime);
121-
const predicted = await predictor.getPredictedMinFees(publicClient, ManaUsageEstimate.None);
145+
const predictor = new FeePredictor(
146+
rollup,
147+
publicClient,
148+
dateProvider,
149+
slotDuration,
150+
l1GenesisTime,
151+
ethereumSlotDuration,
152+
);
153+
const predicted = await predictor.getPredictedMinFees(ManaUsageEstimate.None);
122154

123155
const startSlot = await getPredictionStartSlot();
124156
const l1Fee = await rollup.getManaMinFeeAt(getTimestamp(startSlot), true);
125157
expect(predicted[0].feePerL2Gas).toBe(l1Fee);
126158
});
127159

128160
it('returns exactly FEE_ORACLE_LAG entries', async () => {
129-
const predictor = new FeePredictor(rollup, slotDuration, l1GenesisTime);
130-
const predicted = await predictor.getPredictedMinFees(publicClient, ManaUsageEstimate.Target);
161+
const predictor = new FeePredictor(
162+
rollup,
163+
publicClient,
164+
dateProvider,
165+
slotDuration,
166+
l1GenesisTime,
167+
ethereumSlotDuration,
168+
);
169+
const predicted = await predictor.getPredictedMinFees(ManaUsageEstimate.Target);
131170
expect(predicted.length).toBe(FEE_ORACLE_LAG);
132171
});
133172

@@ -155,8 +194,15 @@ describe('FeePredictor', () => {
155194
const assumedManaUsed =
156195
estimate === ManaUsageEstimate.None ? 0n : estimate === ManaUsageEstimate.Target ? manaTarget : manaLimit;
157196

158-
const predictor = new FeePredictor(rollup, slotDuration, l1GenesisTime);
159-
const predicted = await predictor.getPredictedMinFees(publicClient, estimate);
197+
const predictor = new FeePredictor(
198+
rollup,
199+
publicClient,
200+
dateProvider,
201+
slotDuration,
202+
l1GenesisTime,
203+
ethereumSlotDuration,
204+
);
205+
const predicted = await predictor.getPredictedMinFees(estimate);
160206

161207
const rollupAddress = EthAddress.fromString(rollup.address);
162208

@@ -272,8 +318,15 @@ describe('FeePredictor', () => {
272318

273319
// Step through 6 successive slots, creating a fresh predictor each time.
274320
for (let step = 0; step < 6; step++) {
275-
const predictor = new FeePredictor(rollup, slotDuration, l1GenesisTime);
276-
const predicted = await predictor.getPredictedMinFees(publicClient, ManaUsageEstimate.None);
321+
const predictor = new FeePredictor(
322+
rollup,
323+
publicClient,
324+
dateProvider,
325+
slotDuration,
326+
l1GenesisTime,
327+
ethereumSlotDuration,
328+
);
329+
const predicted = await predictor.getPredictedMinFees(ManaUsageEstimate.None);
277330

278331
expect(predicted.length).toBe(FEE_ORACLE_LAG);
279332

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

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { type L1FeeData, MAX_FEE_ASSET_PRICE_MODIFIER_BPS, type RollupContract } from '@aztec/ethereum/contracts';
22
import { SlotNumber } from '@aztec/foundation/branded-types';
33
import { times } from '@aztec/foundation/collection';
4-
import { getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
4+
import type { DateProvider } from '@aztec/foundation/timer';
5+
import { getSlotAtNextL1Block, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
56
import { FEE_ORACLE_LAG, GasFees, ManaUsageEstimate, computeExcessMana, computeManaMinFee } from '@aztec/stdlib/gas';
67

78
/** Cached rollup state for fee prediction. Refreshed once per L1 block. */
@@ -29,22 +30,22 @@ export class FeePredictor {
2930

3031
constructor(
3132
private readonly rollupContract: RollupContract,
33+
private readonly publicClient: { getBlockNumber: () => Promise<bigint> },
34+
private readonly dateProvider: DateProvider,
3235
private readonly slotDuration: number,
3336
private readonly l1GenesisTime: bigint,
37+
private readonly ethereumSlotDuration: number,
3438
) {}
3539

3640
/** Returns predicted min fees for each slot in the prediction window. */
37-
async getPredictedMinFees(
38-
publicClient: { getBlockNumber: () => Promise<bigint> },
39-
manaUsage: ManaUsageEstimate,
40-
): Promise<GasFees[]> {
41-
const state = await this.getState(publicClient);
41+
async getPredictedMinFees(manaUsage: ManaUsageEstimate): Promise<GasFees[]> {
42+
const state = await this.getState();
4243
return this.computePredictions(state, manaUsage);
4344
}
4445

4546
/** Fetches and caches rollup state. Refreshes when L1 block number advances. */
46-
private async getState(publicClient: { getBlockNumber: () => Promise<bigint> }): Promise<FeeOracleState> {
47-
const blockNumber = await publicClient.getBlockNumber();
47+
private async getState(): Promise<FeeOracleState> {
48+
const blockNumber = await this.publicClient.getBlockNumber();
4849
if (this.cachedL1BlockNumber === undefined || blockNumber > this.cachedL1BlockNumber) {
4950
this.cachedL1BlockNumber = blockNumber;
5051
this.cachedState = this.fetchState();
@@ -65,8 +66,14 @@ export class FeePredictor {
6566
]);
6667

6768
const lastSlot = lastCheckpoint.slotNumber;
68-
// Start from the later of: the slot after the last checkpoint, or the current slot.
69-
const nextSlot = SlotNumber.add(lastSlot, 1) > currentSlot ? SlotNumber.add(lastSlot, 1) : currentSlot;
69+
// The next L1 block may land in the next L2 slot, so we need to account for that
70+
const slotAtNextL1Block = getSlotAtNextL1Block(BigInt(this.dateProvider.nowInSeconds()), {
71+
l1GenesisTime: this.l1GenesisTime,
72+
slotDuration: this.slotDuration,
73+
ethereumSlotDuration: this.ethereumSlotDuration,
74+
});
75+
// Start from the latest of: slot after last checkpoint, current slot, or slot at next L1 block.
76+
const nextSlot = SlotNumber(Math.max(SlotNumber.add(lastSlot, 1), currentSlot, slotAtNextL1Block));
7077
const feeHeader = lastCheckpoint.feeHeader;
7178

7279
const slotConfig = { slotDuration: this.slotDuration, l1GenesisTime: this.l1GenesisTime };

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,14 @@ export class FeeProviderImpl implements FeeProvider {
4545
this.l1GenesisTime = config.l1GenesisTime;
4646

4747
this.rollupContract = new RollupContract(this.publicClient, config.l1Contracts.rollupAddress);
48-
this.feePredictor = new FeePredictor(this.rollupContract, config.slotDuration, config.l1GenesisTime);
48+
this.feePredictor = new FeePredictor(
49+
this.rollupContract,
50+
this.publicClient,
51+
this.dateProvider,
52+
config.slotDuration,
53+
config.l1GenesisTime,
54+
config.ethereumSlotDuration,
55+
);
4956
}
5057

5158
/**
@@ -83,7 +90,7 @@ export class FeeProviderImpl implements FeeProvider {
8390
}
8491

8592
public getPredictedMinFees(manaUsage?: ManaUsageEstimate): Promise<GasFees[]> {
86-
return this.feePredictor.getPredictedMinFees(this.publicClient, manaUsage ?? ManaUsageEstimate.Target);
93+
return this.feePredictor.getPredictedMinFees(manaUsage ?? ManaUsageEstimate.Target);
8794
}
8895
}
8996

0 commit comments

Comments
 (0)