@aztec-labs/aztec-node 6.1.0-nightly.20261102 → 6.1.0-nightly.20261104

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,13 @@
1
1
  import type { EpochCacheInterface } from '@aztec-labs/epoch-cache';
2
2
  import { type RollupContract } from '@aztec-labs/ethereum/contracts';
3
+ import { type SlotNumber } from '@aztec-labs/foundation/branded-types';
3
4
  import { type Logger } from '@aztec-labs/foundation/log';
4
5
  import { type DateProvider } from '@aztec-labs/foundation/timer';
5
- import type { L2BlockSource, L2Frontier } from '@aztec-labs/stdlib/block';
6
+ import type { L2BlockId, L2BlockSource, L2Frontier } from '@aztec-labs/stdlib/block';
7
+ import type { GasFees } from '@aztec-labs/stdlib/gas';
6
8
  import type { CoordinationSignatureContext } from '@aztec-labs/stdlib/p2p';
7
9
  import type { CheckpointGlobalVariables, GlobalVariableBuilder } from '@aztec-labs/stdlib/tx';
10
+ import type { CheckpointOpeningFeeSource } from '@aztec-labs/validator-client';
8
11
  import { type BoundaryFeeKey } from './next_block_planner.js';
9
12
  /** Default interval for the background refresh, well below L1's block time. */
10
13
  export declare const DEFAULT_REFRESH_INTERVAL_MS = 1000;
@@ -51,7 +54,7 @@ export interface NextBlockFeeCacheDeps {
51
54
  * Before {@link start} (or after {@link stop}) requests still price inline through the same refresh path, which
52
55
  * is what tests and TXE-like environments rely on.
53
56
  */
54
- export declare class NextBlockFeeCache {
57
+ export declare class NextBlockFeeCache implements CheckpointOpeningFeeSource {
55
58
  private readonly blockSource;
56
59
  private readonly globalVariableBuilder;
57
60
  private readonly rollupContract;
@@ -83,17 +86,47 @@ export declare class NextBlockFeeCache {
83
86
  getBoundaryGlobals(key: BoundaryFeeKey, frontier: L2Frontier, opts?: {
84
87
  maxWaitMs?: number;
85
88
  }): Promise<CheckpointGlobalVariables | undefined>;
89
+ /**
90
+ * The gas fees the first block of a checkpoint at `slot` built on `parent` must carry, for checking a block proposal
91
+ * against. Keyed on the proposal's slot and parent rather than on the slot this node's clock would plan: the frontier
92
+ * must end at `parent`, and the boundary is planned for `slot` from it. A matching record is served from memory; on
93
+ * a miss the request joins the refresh in flight and then runs a pass planned on `slot`, so it gets the exact price
94
+ * and not a fallback.
95
+ *
96
+ * Undefined when this node's view does not hold the data to produce the price the proposer computes yet: the
97
+ * frontier does not end at `parent` (the parent's checkpoint proposal has not been processed, or the chain moved
98
+ * on), or the checkpointed tip has not reached the parent's parent, whose fee state the pipelined plan reads from
99
+ * L1 (see {@link pricesAsProposer}). The caller waits for a later archiver sync rather than accepting the pinned-tips
100
+ * approximation. Throws when the L1 read fails.
101
+ *
102
+ * Only a record priced at the frontier's L1 block, or at a later one, is served. The L1 fee oracle can change without
103
+ * moving the frontier (anyone may call its update), and the background loop only catches up within a refresh
104
+ * interval, so an older record could carry a price the proposer has already moved past.
105
+ */
106
+ getCheckpointOpeningGasFees(slot: SlotNumber, parent: L2BlockId): Promise<GasFees | undefined>;
86
107
  /**
87
108
  * Runs one refresh pass, or joins the one already running. Single-flight is what keeps a burst of requests
88
109
  * during a transition down to a single L1 round trip, shared with the background loop.
89
110
  * @param frontier - Snapshot to plan from; read fresh from the archiver when omitted, as the loop does.
111
+ * @param slot - Slot to plan the boundary for in place of this node's clock slot.
90
112
  */
91
- refresh(frontier?: L2Frontier): Promise<void>;
113
+ refresh(frontier?: L2Frontier, slot?: SlotNumber): Promise<void>;
114
+ private findOrPrice;
92
115
  private runRefresh;
93
- /** The freshest record matching `key`, or undefined when none is recent enough to trust. */
116
+ /**
117
+ * The freshest record matching `key`, or undefined when none is recent enough to trust. With `minL1SyncPoint`, a
118
+ * record also has to be priced at that L1 block or a later one.
119
+ */
94
120
  private findRecord;
95
121
  /** Awaits `promise` for at most `maxWaitMs`, swallowing both a timeout and the promise's own failure. */
96
122
  private waitFor;
123
+ /**
124
+ * Whether {@link buildOverridesPlan} prices `newCheckpoint` with the plan its proposer uses, rather than with the
125
+ * pinned-tips fallback. The sequencer never builds on a proposed parent more than one checkpoint ahead of its
126
+ * checkpointed tip, so a frontier in that state trails L1: the parent's parent, whose fee header the pipelined plan
127
+ * reads, is not checkpointed in it yet, and the fallback price is not one a proposer would have used.
128
+ */
129
+ private pricesAsProposer;
97
130
  /**
98
131
  * Builds the chain-state overrides plan passed to `buildCheckpointGlobalVariables`, mirroring the sequencer
99
132
  * (which always pins tips to neutralize prunes). When pipelining, the plan carries the proposed parent's
@@ -107,4 +140,4 @@ export declare class NextBlockFeeCache {
107
140
  */
108
141
  private buildOverridesPlan;
109
142
  }
110
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibmV4dF9ibG9ja19mZWVfY2FjaGUuZC50cyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy9henRlYy1ub2RlL25leHRfYmxvY2svbmV4dF9ibG9ja19mZWVfY2FjaGUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxLQUFLLEVBQUUsbUJBQW1CLEVBQUUsTUFBTSx5QkFBeUIsQ0FBQztBQUNuRSxPQUFPLEVBQ0wsS0FBSyxjQUFjLEVBR3BCLE1BQU0sZ0NBQWdDLENBQUM7QUFHeEMsT0FBTyxFQUFFLEtBQUssTUFBTSxFQUFnQixNQUFNLDRCQUE0QixDQUFDO0FBRXZFLE9BQU8sRUFBRSxLQUFLLFlBQVksRUFBa0IsTUFBTSw4QkFBOEIsQ0FBQztBQUVqRixPQUFPLEtBQUssRUFBZSxhQUFhLEVBQUUsVUFBVSxFQUFFLE1BQU0sMEJBQTBCLENBQUM7QUFFdkYsT0FBTyxLQUFLLEVBQUUsNEJBQTRCLEVBQUUsTUFBTSx3QkFBd0IsQ0FBQztBQUMzRSxPQUFPLEtBQUssRUFBRSx5QkFBeUIsRUFBRSxxQkFBcUIsRUFBRSxNQUFNLHVCQUF1QixDQUFDO0FBRTlGLE9BQU8sRUFDTCxLQUFLLGNBQWMsRUFNcEIsTUFBTSx5QkFBeUIsQ0FBQztBQUVqQywrRUFBK0U7QUFDL0UsZUFBTyxNQUFNLDJCQUEyQixPQUFPLENBQUM7QUFFaEQ7Ozs7R0FJRztBQUNILGVBQU8sTUFBTSxpQkFBaUIsS0FBSyxDQUFDO0FBYXBDLGtFQUFrRTtBQUNsRSxNQUFNLFdBQVcscUJBQXFCO0lBQ3BDLFdBQVcsRUFBRSxhQUFhLENBQUM7SUFDM0IscUJBQXFCLEVBQUUscUJBQXFCLENBQUM7SUFDN0M7Ozs7O09BS0c7SUFDSCxjQUFjLENBQUMsRUFBRSxjQUFjLENBQUM7SUFDaEMsVUFBVSxFQUFFLG1CQUFtQixDQUFDO0lBQ2hDLGdCQUFnQixFQUFFLDRCQUE0QixDQUFDO0lBQy9DLFlBQVksRUFBRSxZQUFZLENBQUM7SUFDM0IsR0FBRyxDQUFDLEVBQUUsTUFBTSxDQUFDO0NBQ2Q7QUFFRDs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7R0FvQkc7QUFDSCxxQkFBYSxpQkFBaUI7SUFDNUIsT0FBTyxDQUFDLFFBQVEsQ0FBQyxXQUFXLENBQWdCO0lBQzVDLE9BQU8sQ0FBQyxRQUFRLENBQUMscUJBQXFCLENBQXdCO0lBQzlELE9BQU8sQ0FBQyxRQUFRLENBQUMsY0FBYyxDQUE2QjtJQUM1RCxPQUFPLENBQUMsUUFBUSxDQUFDLFVBQVUsQ0FBc0I7SUFDakQsT0FBTyxDQUFDLFFBQVEsQ0FBQyxnQkFBZ0IsQ0FBK0I7SUFDaEUsT0FBTyxDQUFDLFFBQVEsQ0FBQyxZQUFZLENBQWU7SUFDNUMsT0FBTyxDQUFDLFFBQVEsQ0FBQyxHQUFHLENBQVM7SUFFN0IsT0FBTyxDQUFDLE9BQU8sQ0FBZ0M7SUFDL0MsT0FBTyxDQUFDLFFBQVEsQ0FBZ0M7SUFDaEQsT0FBTyxDQUFDLGlCQUFpQixDQUErQjtJQUN4RCxPQUFPLENBQUMsV0FBVyxDQUE2QjtJQUNoRCxPQUFPLENBQUMsZUFBZSxDQUE0QjtJQUVuRCxZQUFZLElBQUksRUFBRSxxQkFBcUIsRUFRdEM7SUFFRDs7Ozs7T0FLRztJQUNJLEtBQUssQ0FBQyxpQkFBaUIsU0FBOEIsR0FBRyxJQUFJLENBTWxFO0lBRVksSUFBSSxJQUFJLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0FNakM7SUFFRDs7Ozs7O09BTUc7SUFDVSxrQkFBa0IsQ0FDN0IsR0FBRyxFQUFFLGNBQWMsRUFDbkIsUUFBUSxFQUFFLFVBQVUsRUFDcEIsSUFBSSxDQUFDLEVBQUU7UUFBRSxTQUFTLENBQUMsRUFBRSxNQUFNLENBQUE7S0FBRSxHQUM1QixPQUFPLENBQUMseUJBQXlCLEdBQUcsU0FBUyxDQUFDLENBY2hEO0lBRUQ7Ozs7T0FJRztJQUNJLE9BQU8sQ0FBQyxRQUFRLENBQUMsRUFBRSxVQUFVLEdBQUcsT0FBTyxDQUFDLElBQUksQ0FBQyxDQVluRDtZQUVhLFVBQVU7SUF1Q3hCLDRGQUE0RjtJQUM1RixPQUFPLENBQUMsVUFBVTtJQVFsQix5R0FBeUc7SUFDekcsT0FBTyxDQUFDLE9BQU87SUFNZjs7Ozs7Ozs7OztPQVVHO0lBQ0gsT0FBTyxDQUFDLGtCQUFrQjtDQThEM0IifQ==
143
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibmV4dF9ibG9ja19mZWVfY2FjaGUuZC50cyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy9henRlYy1ub2RlL25leHRfYmxvY2svbmV4dF9ibG9ja19mZWVfY2FjaGUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxLQUFLLEVBQUUsbUJBQW1CLEVBQUUsTUFBTSx5QkFBeUIsQ0FBQztBQUNuRSxPQUFPLEVBQ0wsS0FBSyxjQUFjLEVBR3BCLE1BQU0sZ0NBQWdDLENBQUM7QUFDeEMsT0FBTyxFQUFvQixLQUFLLFVBQVUsRUFBRSxNQUFNLHNDQUFzQyxDQUFDO0FBRXpGLE9BQU8sRUFBRSxLQUFLLE1BQU0sRUFBZ0IsTUFBTSw0QkFBNEIsQ0FBQztBQUV2RSxPQUFPLEVBQUUsS0FBSyxZQUFZLEVBQWtCLE1BQU0sOEJBQThCLENBQUM7QUFFakYsT0FBTyxLQUFLLEVBQWUsU0FBUyxFQUFFLGFBQWEsRUFBRSxVQUFVLEVBQUUsTUFBTSwwQkFBMEIsQ0FBQztBQUVsRyxPQUFPLEtBQUssRUFBRSxPQUFPLEVBQUUsTUFBTSx3QkFBd0IsQ0FBQztBQUN0RCxPQUFPLEtBQUssRUFBRSw0QkFBNEIsRUFBRSxNQUFNLHdCQUF3QixDQUFDO0FBQzNFLE9BQU8sS0FBSyxFQUFFLHlCQUF5QixFQUFFLHFCQUFxQixFQUFFLE1BQU0sdUJBQXVCLENBQUM7QUFDOUYsT0FBTyxLQUFLLEVBQUUsMEJBQTBCLEVBQUUsTUFBTSw4QkFBOEIsQ0FBQztBQUUvRSxPQUFPLEVBQ0wsS0FBSyxjQUFjLEVBTXBCLE1BQU0seUJBQXlCLENBQUM7QUFFakMsK0VBQStFO0FBQy9FLGVBQU8sTUFBTSwyQkFBMkIsT0FBTyxDQUFDO0FBRWhEOzs7O0dBSUc7QUFDSCxlQUFPLE1BQU0saUJBQWlCLEtBQUssQ0FBQztBQWFwQyxrRUFBa0U7QUFDbEUsTUFBTSxXQUFXLHFCQUFxQjtJQUNwQyxXQUFXLEVBQUUsYUFBYSxDQUFDO0lBQzNCLHFCQUFxQixFQUFFLHFCQUFxQixDQUFDO0lBQzdDOzs7OztPQUtHO0lBQ0gsY0FBYyxDQUFDLEVBQUUsY0FBYyxDQUFDO0lBQ2hDLFVBQVUsRUFBRSxtQkFBbUIsQ0FBQztJQUNoQyxnQkFBZ0IsRUFBRSw0QkFBNEIsQ0FBQztJQUMvQyxZQUFZLEVBQUUsWUFBWSxDQUFDO0lBQzNCLEdBQUcsQ0FBQyxFQUFFLE1BQU0sQ0FBQztDQUNkO0FBRUQ7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0dBb0JHO0FBQ0gscUJBQWEsaUJBQWtCLFlBQVcsMEJBQTBCO0lBQ2xFLE9BQU8sQ0FBQyxRQUFRLENBQUMsV0FBVyxDQUFnQjtJQUM1QyxPQUFPLENBQUMsUUFBUSxDQUFDLHFCQUFxQixDQUF3QjtJQUM5RCxPQUFPLENBQUMsUUFBUSxDQUFDLGNBQWMsQ0FBNkI7SUFDNUQsT0FBTyxDQUFDLFFBQVEsQ0FBQyxVQUFVLENBQXNCO0lBQ2pELE9BQU8sQ0FBQyxRQUFRLENBQUMsZ0JBQWdCLENBQStCO0lBQ2hFLE9BQU8sQ0FBQyxRQUFRLENBQUMsWUFBWSxDQUFlO0lBQzVDLE9BQU8sQ0FBQyxRQUFRLENBQUMsR0FBRyxDQUFTO0lBRTdCLE9BQU8sQ0FBQyxPQUFPLENBQWdDO0lBQy9DLE9BQU8sQ0FBQyxRQUFRLENBQWdDO0lBQ2hELE9BQU8sQ0FBQyxpQkFBaUIsQ0FBK0I7SUFDeEQsT0FBTyxDQUFDLFdBQVcsQ0FBNkI7SUFDaEQsT0FBTyxDQUFDLGVBQWUsQ0FBNEI7SUFFbkQsWUFBWSxJQUFJLEVBQUUscUJBQXFCLEVBUXRDO0lBRUQ7Ozs7O09BS0c7SUFDSSxLQUFLLENBQUMsaUJBQWlCLFNBQThCLEdBQUcsSUFBSSxDQU1sRTtJQUVZLElBQUksSUFBSSxPQUFPLENBQUMsSUFBSSxDQUFDLENBTWpDO0lBRUQ7Ozs7OztPQU1HO0lBQ0ksa0JBQWtCLENBQ3ZCLEdBQUcsRUFBRSxjQUFjLEVBQ25CLFFBQVEsRUFBRSxVQUFVLEVBQ3BCLElBQUksQ0FBQyxFQUFFO1FBQUUsU0FBUyxDQUFDLEVBQUUsTUFBTSxDQUFBO0tBQUUsR0FDNUIsT0FBTyxDQUFDLHlCQUF5QixHQUFHLFNBQVMsQ0FBQyxDQUVoRDtJQUVEOzs7Ozs7Ozs7Ozs7Ozs7O09BZ0JHO0lBQ1UsMkJBQTJCLENBQUMsSUFBSSxFQUFFLFVBQVUsRUFBRSxNQUFNLEVBQUUsU0FBUyxHQUFHLE9BQU8sQ0FBQyxPQUFPLEdBQUcsU0FBUyxDQUFDLENBeUIxRztJQUVEOzs7OztPQUtHO0lBQ0ksT0FBTyxDQUFDLFFBQVEsQ0FBQyxFQUFFLFVBQVUsRUFBRSxJQUFJLENBQUMsRUFBRSxVQUFVLEdBQUcsT0FBTyxDQUFDLElBQUksQ0FBQyxDQVl0RTtZQU9hLFdBQVc7WUFxQlgsVUFBVTtJQXVDeEI7OztPQUdHO0lBQ0gsT0FBTyxDQUFDLFVBQVU7SUFZbEIseUdBQXlHO0lBQ3pHLE9BQU8sQ0FBQyxPQUFPO0lBTWY7Ozs7O09BS0c7SUFDSCxPQUFPLENBQUMsZ0JBQWdCO0lBWXhCOzs7Ozs7Ozs7O09BVUc7SUFDSCxPQUFPLENBQUMsa0JBQWtCO0NBOEQzQiJ9
@@ -1 +1 @@
1
- {"version":3,"file":"next_block_fee_cache.d.ts","sourceRoot":"","sources":["../../../src/aztec-node/next_block/next_block_fee_cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AACnE,OAAO,EACL,KAAK,cAAc,EAGpB,MAAM,gCAAgC,CAAC;AAGxC,OAAO,EAAE,KAAK,MAAM,EAAgB,MAAM,4BAA4B,CAAC;AAEvE,OAAO,EAAE,KAAK,YAAY,EAAkB,MAAM,8BAA8B,CAAC;AAEjF,OAAO,KAAK,EAAe,aAAa,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAEvF,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,wBAAwB,CAAC;AAC3E,OAAO,KAAK,EAAE,yBAAyB,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAE9F,OAAO,EACL,KAAK,cAAc,EAMpB,MAAM,yBAAyB,CAAC;AAEjC,+EAA+E;AAC/E,eAAO,MAAM,2BAA2B,OAAO,CAAC;AAEhD;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,KAAK,CAAC;AAapC,kEAAkE;AAClE,MAAM,WAAW,qBAAqB;IACpC,WAAW,EAAE,aAAa,CAAC;IAC3B,qBAAqB,EAAE,qBAAqB,CAAC;IAC7C;;;;;OAKG;IACH,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,UAAU,EAAE,mBAAmB,CAAC;IAChC,gBAAgB,EAAE,4BAA4B,CAAC;IAC/C,YAAY,EAAE,YAAY,CAAC;IAC3B,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAgB;IAC5C,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAwB;IAC9D,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA6B;IAC5D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAsB;IACjD,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAA+B;IAChE,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAe;IAC5C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAE7B,OAAO,CAAC,OAAO,CAAgC;IAC/C,OAAO,CAAC,QAAQ,CAAgC;IAChD,OAAO,CAAC,iBAAiB,CAA+B;IACxD,OAAO,CAAC,WAAW,CAA6B;IAChD,OAAO,CAAC,eAAe,CAA4B;IAEnD,YAAY,IAAI,EAAE,qBAAqB,EAQtC;IAED;;;;;OAKG;IACI,KAAK,CAAC,iBAAiB,SAA8B,GAAG,IAAI,CAMlE;IAEY,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAMjC;IAED;;;;;;OAMG;IACU,kBAAkB,CAC7B,GAAG,EAAE,cAAc,EACnB,QAAQ,EAAE,UAAU,EACpB,IAAI,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,GAC5B,OAAO,CAAC,yBAAyB,GAAG,SAAS,CAAC,CAchD;IAED;;;;OAIG;IACI,OAAO,CAAC,QAAQ,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAYnD;YAEa,UAAU;IAuCxB,4FAA4F;IAC5F,OAAO,CAAC,UAAU;IAQlB,yGAAyG;IACzG,OAAO,CAAC,OAAO;IAMf;;;;;;;;;;OAUG;IACH,OAAO,CAAC,kBAAkB;CA8D3B"}
1
+ {"version":3,"file":"next_block_fee_cache.d.ts","sourceRoot":"","sources":["../../../src/aztec-node/next_block/next_block_fee_cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AACnE,OAAO,EACL,KAAK,cAAc,EAGpB,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAoB,KAAK,UAAU,EAAE,MAAM,sCAAsC,CAAC;AAEzF,OAAO,EAAE,KAAK,MAAM,EAAgB,MAAM,4BAA4B,CAAC;AAEvE,OAAO,EAAE,KAAK,YAAY,EAAkB,MAAM,8BAA8B,CAAC;AAEjF,OAAO,KAAK,EAAe,SAAS,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAElG,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AACtD,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,wBAAwB,CAAC;AAC3E,OAAO,KAAK,EAAE,yBAAyB,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC9F,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,8BAA8B,CAAC;AAE/E,OAAO,EACL,KAAK,cAAc,EAMpB,MAAM,yBAAyB,CAAC;AAEjC,+EAA+E;AAC/E,eAAO,MAAM,2BAA2B,OAAO,CAAC;AAEhD;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,KAAK,CAAC;AAapC,kEAAkE;AAClE,MAAM,WAAW,qBAAqB;IACpC,WAAW,EAAE,aAAa,CAAC;IAC3B,qBAAqB,EAAE,qBAAqB,CAAC;IAC7C;;;;;OAKG;IACH,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,UAAU,EAAE,mBAAmB,CAAC;IAChC,gBAAgB,EAAE,4BAA4B,CAAC;IAC/C,YAAY,EAAE,YAAY,CAAC;IAC3B,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,iBAAkB,YAAW,0BAA0B;IAClE,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAgB;IAC5C,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAwB;IAC9D,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA6B;IAC5D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAsB;IACjD,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAA+B;IAChE,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAe;IAC5C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAE7B,OAAO,CAAC,OAAO,CAAgC;IAC/C,OAAO,CAAC,QAAQ,CAAgC;IAChD,OAAO,CAAC,iBAAiB,CAA+B;IACxD,OAAO,CAAC,WAAW,CAA6B;IAChD,OAAO,CAAC,eAAe,CAA4B;IAEnD,YAAY,IAAI,EAAE,qBAAqB,EAQtC;IAED;;;;;OAKG;IACI,KAAK,CAAC,iBAAiB,SAA8B,GAAG,IAAI,CAMlE;IAEY,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAMjC;IAED;;;;;;OAMG;IACI,kBAAkB,CACvB,GAAG,EAAE,cAAc,EACnB,QAAQ,EAAE,UAAU,EACpB,IAAI,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,GAC5B,OAAO,CAAC,yBAAyB,GAAG,SAAS,CAAC,CAEhD;IAED;;;;;;;;;;;;;;;;OAgBG;IACU,2BAA2B,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAyB1G;IAED;;;;;OAKG;IACI,OAAO,CAAC,QAAQ,CAAC,EAAE,UAAU,EAAE,IAAI,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAYtE;YAOa,WAAW;YAqBX,UAAU;IAuCxB;;;OAGG;IACH,OAAO,CAAC,UAAU;IAYlB,yGAAyG;IACzG,OAAO,CAAC,OAAO;IAMf;;;;;OAKG;IACH,OAAO,CAAC,gBAAgB;IAYxB;;;;;;;;;;OAUG;IACH,OAAO,CAAC,kBAAkB;CA8D3B"}
@@ -81,30 +81,61 @@ import { boundaryFeeKeyEquals, computeBoundaryFeeKey, getClockSlot, planNextBloc
81
81
  * A matching record under the staleness cutoff is served straight away. Otherwise the caller joins the single
82
82
  * shared refresh: a simulation waits for it and surfaces its failure, while a quote passes `maxWaitMs` and
83
83
  * falls back to whatever the cache already holds rather than turning an L1 outage into a multi-second RPC.
84
- */ async getBoundaryGlobals(key, frontier, opts) {
85
- const maxWaitMs = opts?.maxWaitMs;
86
- // An uncapped reader gets more than one attempt because the refresh it joins may be one that started from an
87
- // older frontier and therefore priced a different boundary; the next pass is its own. Still never concurrent.
88
- const attempts = maxWaitMs === undefined ? MAX_REFRESH_ATTEMPTS : 1;
89
- for(let attempt = 0; attempt < attempts; attempt++){
90
- const hit = this.findRecord(key);
91
- if (hit) {
92
- return hit.globals;
93
- }
94
- const refresh = this.refresh(frontier);
95
- await (maxWaitMs === undefined ? refresh : this.waitFor(refresh, maxWaitMs));
84
+ */ getBoundaryGlobals(key, frontier, opts) {
85
+ return this.findOrPrice(key, frontier, opts?.maxWaitMs);
86
+ }
87
+ /**
88
+ * The gas fees the first block of a checkpoint at `slot` built on `parent` must carry, for checking a block proposal
89
+ * against. Keyed on the proposal's slot and parent rather than on the slot this node's clock would plan: the frontier
90
+ * must end at `parent`, and the boundary is planned for `slot` from it. A matching record is served from memory; on
91
+ * a miss the request joins the refresh in flight and then runs a pass planned on `slot`, so it gets the exact price
92
+ * and not a fallback.
93
+ *
94
+ * Undefined when this node's view does not hold the data to produce the price the proposer computes yet: the
95
+ * frontier does not end at `parent` (the parent's checkpoint proposal has not been processed, or the chain moved
96
+ * on), or the checkpointed tip has not reached the parent's parent, whose fee state the pipelined plan reads from
97
+ * L1 (see {@link pricesAsProposer}). The caller waits for a later archiver sync rather than accepting the pinned-tips
98
+ * approximation. Throws when the L1 read fails.
99
+ *
100
+ * Only a record priced at the frontier's L1 block, or at a later one, is served. The L1 fee oracle can change without
101
+ * moving the frontier (anyone may call its update), and the background loop only catches up within a refresh
102
+ * interval, so an older record could carry a price the proposer has already moved past.
103
+ */ async getCheckpointOpeningGasFees(slot, parent) {
104
+ const frontier = await this.blockSource.getL2Frontier();
105
+ const tip = frontier.tips.proposed;
106
+ if (tip.number !== parent.number || tip.hash !== parent.hash) {
107
+ this.log.debug(`Cannot price the checkpoint opened at slot ${slot}: the proposed tip is not its parent`, {
108
+ slot,
109
+ parent,
110
+ tip
111
+ });
112
+ return undefined;
96
113
  }
97
- return this.findRecord(key)?.globals;
114
+ const plan = planNextBlock(frontier, slot);
115
+ const newCheckpoint = plan.newCheckpoint;
116
+ if (!newCheckpoint || newCheckpoint.targetSlot !== slot || !this.pricesAsProposer(frontier, newCheckpoint)) {
117
+ this.log.debug(`Cannot price the checkpoint opened at slot ${slot} as its proposer does from this frontier`, {
118
+ slot,
119
+ parent,
120
+ targetSlot: newCheckpoint?.targetSlot,
121
+ proposedCheckpointNumber: newCheckpoint?.proposedCheckpointData?.checkpointNumber,
122
+ checkpointedCheckpointNumber: newCheckpoint?.checkpointedCheckpointNumber
123
+ });
124
+ return undefined;
125
+ }
126
+ const key = computeBoundaryFeeKey(plan, frontier.pendingChainValidationStatus);
127
+ return key && (await this.findOrPrice(key, frontier, undefined, slot, frontier.l1SyncPoint))?.gasFees;
98
128
  }
99
129
  /**
100
130
  * Runs one refresh pass, or joins the one already running. Single-flight is what keeps a burst of requests
101
131
  * during a transition down to a single L1 round trip, shared with the background loop.
102
132
  * @param frontier - Snapshot to plan from; read fresh from the archiver when omitted, as the loop does.
103
- */ refresh(frontier) {
133
+ * @param slot - Slot to plan the boundary for in place of this node's clock slot.
134
+ */ refresh(frontier, slot) {
104
135
  if (this.inFlightRefresh) {
105
136
  return this.inFlightRefresh;
106
137
  }
107
- const refresh = this.runRefresh(frontier).finally(()=>{
138
+ const refresh = this.runRefresh(frontier, slot).finally(()=>{
108
139
  this.inFlightRefresh = undefined;
109
140
  });
110
141
  // Every caller awaits the returned promise and reports its failure, but the stored copy may only be awaited by
@@ -113,9 +144,27 @@ import { boundaryFeeKeyEquals, computeBoundaryFeeKey, getClockSlot, planNextBloc
113
144
  this.inFlightRefresh = refresh;
114
145
  return refresh;
115
146
  }
116
- async runRefresh(frontier) {
147
+ /**
148
+ * Serves the record matching `key`, or joins the shared refresh to produce it, as {@link getBoundaryGlobals}
149
+ * describes. `slot` plans a refresh this reader starts for that slot rather than for the clock slot.
150
+ * `minL1SyncPoint`, when set, skips records priced before that L1 block (see {@link pricedAtOrAfter}).
151
+ */ async findOrPrice(key, frontier, maxWaitMs, slot, minL1SyncPoint) {
152
+ // An uncapped reader gets more than one attempt because the refresh it joins may be one that started from an
153
+ // older frontier and therefore priced a different boundary; the next pass is its own. Still never concurrent.
154
+ const attempts = maxWaitMs === undefined ? MAX_REFRESH_ATTEMPTS : 1;
155
+ for(let attempt = 0; attempt < attempts; attempt++){
156
+ const hit = this.findRecord(key, minL1SyncPoint);
157
+ if (hit) {
158
+ return hit.globals;
159
+ }
160
+ const refresh = this.refresh(frontier, slot);
161
+ await (maxWaitMs === undefined ? refresh : this.waitFor(refresh, maxWaitMs));
162
+ }
163
+ return this.findRecord(key, minL1SyncPoint)?.globals;
164
+ }
165
+ async runRefresh(frontier, slot) {
117
166
  const snapshot = frontier ?? await this.blockSource.getL2Frontier();
118
- const plan = planNextBlock(snapshot, getClockSlot(this.epochCache));
167
+ const plan = planNextBlock(snapshot, slot ?? getClockSlot(this.epochCache));
119
168
  const key = computeBoundaryFeeKey(plan, snapshot.pendingChainValidationStatus);
120
169
  if (!key || !plan.newCheckpoint) {
121
170
  // Mid-checkpoint: the fee is frozen in the in-progress checkpoint's header, so there is nothing to price.
@@ -152,18 +201,34 @@ import { boundaryFeeKeyEquals, computeBoundaryFeeKey, getClockSlot, planNextBloc
152
201
  }
153
202
  this.current = record;
154
203
  }
155
- /** The freshest record matching `key`, or undefined when none is recent enough to trust. */ findRecord(key) {
204
+ /**
205
+ * The freshest record matching `key`, or undefined when none is recent enough to trust. With `minL1SyncPoint`, a
206
+ * record also has to be priced at that L1 block or a later one.
207
+ */ findRecord(key, minL1SyncPoint) {
156
208
  const maxAgeMs = MAX_AGE_INTERVALS * this.refreshIntervalMs;
157
209
  const now = this.dateProvider.now();
158
210
  return [
159
211
  this.current,
160
212
  this.previous
161
- ].find((record)=>record !== undefined && boundaryFeeKeyEquals(record.key, key) && now - record.refreshedAtMs < maxAgeMs);
213
+ ].find((record)=>record !== undefined && boundaryFeeKeyEquals(record.key, key) && now - record.refreshedAtMs < maxAgeMs && (minL1SyncPoint === undefined || pricedAtOrAfter(record.l1SyncPoint, minL1SyncPoint)));
162
214
  }
163
215
  /** Awaits `promise` for at most `maxWaitMs`, swallowing both a timeout and the promise's own failure. */ waitFor(promise, maxWaitMs) {
164
216
  return executeTimeout(()=>promise, maxWaitMs).catch((err)=>this.log.debug(`Refreshing the next-block boundary fee failed or timed out`, err));
165
217
  }
166
218
  /**
219
+ * Whether {@link buildOverridesPlan} prices `newCheckpoint` with the plan its proposer uses, rather than with the
220
+ * pinned-tips fallback. The sequencer never builds on a proposed parent more than one checkpoint ahead of its
221
+ * checkpointed tip, so a frontier in that state trails L1: the parent's parent, whose fee header the pipelined plan
222
+ * reads, is not checkpointed in it yet, and the fallback price is not one a proposer would have used.
223
+ */ pricesAsProposer(frontier, newCheckpoint) {
224
+ const { proposedCheckpointData, checkpointedCheckpointNumber } = newCheckpoint;
225
+ if (proposedCheckpointData) {
226
+ return this.rollupContract !== undefined && proposedCheckpointData.checkpointNumber === checkpointedCheckpointNumber + 1;
227
+ }
228
+ // On a valid chain with no proposed parent the pinned-tips plan is the one the proposer builds.
229
+ return this.rollupContract !== undefined || frontier.pendingChainValidationStatus.valid;
230
+ }
231
+ /**
167
232
  * Builds the chain-state overrides plan passed to `buildCheckpointGlobalVariables`, mirroring the sequencer
168
233
  * (which always pins tips to neutralize prunes). When pipelining, the plan carries the proposed parent's
169
234
  * archive, temp-checkpoint-log cell, and locally-derived fee header; when the pending chain is invalid, it
@@ -223,3 +288,12 @@ import { boundaryFeeKeyEquals, computeBoundaryFeeKey, getClockSlot, planNextBloc
223
288
  function l1SyncPointEquals(a, b) {
224
289
  return a === undefined || b === undefined ? a === b : a.blockHash.equals(b.blockHash);
225
290
  }
291
+ /**
292
+ * Whether a record priced at `pricedAt` reflects L1 at least as recent as `min`. At the same height the hashes must
293
+ * match, since a reorg that replaces that block can carry different fee state.
294
+ */ function pricedAtOrAfter(pricedAt, min) {
295
+ if (pricedAt === undefined || pricedAt.blockNumber < min.blockNumber) {
296
+ return false;
297
+ }
298
+ return pricedAt.blockNumber > min.blockNumber || pricedAt.blockHash.equals(min.blockHash);
299
+ }
package/dest/factory.d.ts CHANGED
@@ -42,4 +42,4 @@ export interface CreateAztecNodeOptions {
42
42
  * @returns A fully synced Aztec Node for use in development/testing.
43
43
  */
44
44
  export declare function createAztecNodeService(inputConfig: AztecNodeConfig, deps?: CreateAztecNodeDeps, options?: CreateAztecNodeOptions): Promise<AztecNodeService>;
45
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZmFjdG9yeS5kLnRzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc3JjL2ZhY3RvcnkudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBYUEsT0FBTyxFQUFFLEtBQUssTUFBTSxFQUFnQixNQUFNLDRCQUE0QixDQUFDO0FBQ3ZFLE9BQU8sRUFBRSxZQUFZLEVBQUUsTUFBTSw4QkFBOEIsQ0FBQztBQUk1RCxPQUFPLEVBQUUsS0FBSyxhQUFhLEVBQW1CLE1BQU0saUJBQWlCLENBQUM7QUFDdEUsT0FBTyxFQUFtQixLQUFLLGNBQWMsRUFBb0IsTUFBTSx5QkFBeUIsQ0FBQztBQUVqRyxPQUFPLEVBQ0wsS0FBSyw4QkFBOEIsRUFJbkMsS0FBSyxrQkFBa0IsRUFDeEIsTUFBTSw4QkFBOEIsQ0FBQztBQWtCdEMsT0FBTyxLQUFLLEVBQUUsV0FBVyxFQUFFLE1BQU0sZ0NBQWdDLENBQUM7QUFDbEUsT0FBTyxFQUFFLEtBQUssZUFBZSxFQUFzQixNQUFNLDhCQUE4QixDQUFDO0FBQ3hGLE9BQU8sRUFDTCxLQUFLLHNCQUFzQixFQVE1QixNQUFNLDhCQUE4QixDQUFDO0FBQ3RDLE9BQU8sS0FBSyxFQUFFLDBCQUEwQixFQUFFLE1BQU0sdUNBQXVDLENBQUM7QUFJeEYsT0FBTyxFQUFFLEtBQUssZUFBZSxFQUE4QixNQUFNLHdCQUF3QixDQUFDO0FBRTFGLE9BQU8sRUFBRSxnQkFBZ0IsRUFBRSxNQUFNLHdCQUF3QixDQUFDO0FBRzFELG9HQUFvRztBQUNwRyxNQUFNLFdBQVcsbUJBQW1CO0lBQ2xDLFNBQVMsQ0FBQyxFQUFFLGVBQWUsQ0FBQztJQUM1QixNQUFNLENBQUMsRUFBRSxNQUFNLENBQUM7SUFDaEIsU0FBUyxDQUFDLEVBQUUsa0JBQWtCLENBQUM7SUFDL0IsWUFBWSxDQUFDLEVBQUUsWUFBWSxDQUFDO0lBQzVCLGFBQWEsQ0FBQyxFQUFFLGFBQWEsQ0FBQztJQUM5QixjQUFjLENBQUMsRUFBRSxPQUFPLENBQUMsY0FBYyxDQUFDLENBQUM7SUFDekMsb0JBQW9CLENBQUMsRUFBRSwwQkFBMEIsQ0FBQztJQUNsRDs7O09BR0c7SUFDSCw4QkFBOEIsQ0FBQyxFQUFFLDhCQUE4QixDQUFDO0lBQ2hFOzs7O09BSUc7SUFDSCxzQkFBc0IsQ0FBQyxFQUFFLHNCQUFzQixDQUFDO0NBQ2pEO0FBRUQsNkVBQTZFO0FBQzdFLE1BQU0sV0FBVyxzQkFBc0I7SUFDckMsT0FBTyxDQUFDLEVBQUUsV0FBVyxDQUFDO0lBQ3RCLGtCQUFrQixDQUFDLEVBQUUsT0FBTyxDQUFDO0lBQzdCLG1CQUFtQixDQUFDLEVBQUUsT0FBTyxDQUFDO0NBQy9CO0FBRUQ7Ozs7R0FJRztBQUNILHdCQUFzQixzQkFBc0IsQ0FDMUMsV0FBVyxFQUFFLGVBQWUsRUFDNUIsSUFBSSxHQUFFLG1CQUF3QixFQUM5QixPQUFPLEdBQUUsc0JBQTJCLEdBQ25DLE9BQU8sQ0FBQyxnQkFBZ0IsQ0FBQyxDQXNtQjNCIn0=
45
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZmFjdG9yeS5kLnRzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc3JjL2ZhY3RvcnkudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBYUEsT0FBTyxFQUFFLEtBQUssTUFBTSxFQUFnQixNQUFNLDRCQUE0QixDQUFDO0FBQ3ZFLE9BQU8sRUFBRSxZQUFZLEVBQUUsTUFBTSw4QkFBOEIsQ0FBQztBQUk1RCxPQUFPLEVBQUUsS0FBSyxhQUFhLEVBQW1CLE1BQU0saUJBQWlCLENBQUM7QUFDdEUsT0FBTyxFQUFtQixLQUFLLGNBQWMsRUFBb0IsTUFBTSx5QkFBeUIsQ0FBQztBQUVqRyxPQUFPLEVBQ0wsS0FBSyw4QkFBOEIsRUFJbkMsS0FBSyxrQkFBa0IsRUFDeEIsTUFBTSw4QkFBOEIsQ0FBQztBQWtCdEMsT0FBTyxLQUFLLEVBQUUsV0FBVyxFQUFFLE1BQU0sZ0NBQWdDLENBQUM7QUFDbEUsT0FBTyxFQUFFLEtBQUssZUFBZSxFQUFzQixNQUFNLDhCQUE4QixDQUFDO0FBQ3hGLE9BQU8sRUFDTCxLQUFLLHNCQUFzQixFQVE1QixNQUFNLDhCQUE4QixDQUFDO0FBQ3RDLE9BQU8sS0FBSyxFQUFFLDBCQUEwQixFQUFFLE1BQU0sdUNBQXVDLENBQUM7QUFJeEYsT0FBTyxFQUFFLEtBQUssZUFBZSxFQUE4QixNQUFNLHdCQUF3QixDQUFDO0FBRTFGLE9BQU8sRUFBRSxnQkFBZ0IsRUFBRSxNQUFNLHdCQUF3QixDQUFDO0FBRzFELG9HQUFvRztBQUNwRyxNQUFNLFdBQVcsbUJBQW1CO0lBQ2xDLFNBQVMsQ0FBQyxFQUFFLGVBQWUsQ0FBQztJQUM1QixNQUFNLENBQUMsRUFBRSxNQUFNLENBQUM7SUFDaEIsU0FBUyxDQUFDLEVBQUUsa0JBQWtCLENBQUM7SUFDL0IsWUFBWSxDQUFDLEVBQUUsWUFBWSxDQUFDO0lBQzVCLGFBQWEsQ0FBQyxFQUFFLGFBQWEsQ0FBQztJQUM5QixjQUFjLENBQUMsRUFBRSxPQUFPLENBQUMsY0FBYyxDQUFDLENBQUM7SUFDekMsb0JBQW9CLENBQUMsRUFBRSwwQkFBMEIsQ0FBQztJQUNsRDs7O09BR0c7SUFDSCw4QkFBOEIsQ0FBQyxFQUFFLDhCQUE4QixDQUFDO0lBQ2hFOzs7O09BSUc7SUFDSCxzQkFBc0IsQ0FBQyxFQUFFLHNCQUFzQixDQUFDO0NBQ2pEO0FBRUQsNkVBQTZFO0FBQzdFLE1BQU0sV0FBVyxzQkFBc0I7SUFDckMsT0FBTyxDQUFDLEVBQUUsV0FBVyxDQUFDO0lBQ3RCLGtCQUFrQixDQUFDLEVBQUUsT0FBTyxDQUFDO0lBQzdCLG1CQUFtQixDQUFDLEVBQUUsT0FBTyxDQUFDO0NBQy9CO0FBRUQ7Ozs7R0FJRztBQUNILHdCQUFzQixzQkFBc0IsQ0FDMUMsV0FBVyxFQUFFLGVBQWUsRUFDNUIsSUFBSSxHQUFFLG1CQUF3QixFQUM5QixPQUFPLEdBQUUsc0JBQTJCLEdBQ25DLE9BQU8sQ0FBQyxnQkFBZ0IsQ0FBQyxDQWluQjNCIn0=
@@ -1 +1 @@
1
- {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../src/factory.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,KAAK,MAAM,EAAgB,MAAM,4BAA4B,CAAC;AACvE,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAI5D,OAAO,EAAE,KAAK,aAAa,EAAmB,MAAM,iBAAiB,CAAC;AACtE,OAAO,EAAmB,KAAK,cAAc,EAAoB,MAAM,yBAAyB,CAAC;AAEjG,OAAO,EACL,KAAK,8BAA8B,EAInC,KAAK,kBAAkB,EACxB,MAAM,8BAA8B,CAAC;AAkBtC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAClE,OAAO,EAAE,KAAK,eAAe,EAAsB,MAAM,8BAA8B,CAAC;AACxF,OAAO,EACL,KAAK,sBAAsB,EAQ5B,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,uCAAuC,CAAC;AAIxF,OAAO,EAAE,KAAK,eAAe,EAA8B,MAAM,wBAAwB,CAAC;AAE1F,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAG1D,oGAAoG;AACpG,MAAM,WAAW,mBAAmB;IAClC,SAAS,CAAC,EAAE,eAAe,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,kBAAkB,CAAC;IAC/B,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,cAAc,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,CAAC;IACzC,oBAAoB,CAAC,EAAE,0BAA0B,CAAC;IAClD;;;OAGG;IACH,8BAA8B,CAAC,EAAE,8BAA8B,CAAC;IAChE;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,sBAAsB,CAAC;CACjD;AAED,6EAA6E;AAC7E,MAAM,WAAW,sBAAsB;IACrC,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED;;;;GAIG;AACH,wBAAsB,sBAAsB,CAC1C,WAAW,EAAE,eAAe,EAC5B,IAAI,GAAE,mBAAwB,EAC9B,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,gBAAgB,CAAC,CAsmB3B"}
1
+ {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../src/factory.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,KAAK,MAAM,EAAgB,MAAM,4BAA4B,CAAC;AACvE,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAI5D,OAAO,EAAE,KAAK,aAAa,EAAmB,MAAM,iBAAiB,CAAC;AACtE,OAAO,EAAmB,KAAK,cAAc,EAAoB,MAAM,yBAAyB,CAAC;AAEjG,OAAO,EACL,KAAK,8BAA8B,EAInC,KAAK,kBAAkB,EACxB,MAAM,8BAA8B,CAAC;AAkBtC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAClE,OAAO,EAAE,KAAK,eAAe,EAAsB,MAAM,8BAA8B,CAAC;AACxF,OAAO,EACL,KAAK,sBAAsB,EAQ5B,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,uCAAuC,CAAC;AAIxF,OAAO,EAAE,KAAK,eAAe,EAA8B,MAAM,wBAAwB,CAAC;AAE1F,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAG1D,oGAAoG;AACpG,MAAM,WAAW,mBAAmB;IAClC,SAAS,CAAC,EAAE,eAAe,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,kBAAkB,CAAC;IAC/B,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,cAAc,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,CAAC;IACzC,oBAAoB,CAAC,EAAE,0BAA0B,CAAC;IAClD;;;OAGG;IACH,8BAA8B,CAAC,EAAE,8BAA8B,CAAC;IAChE;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,sBAAsB,CAAC;CACjD;AAED,6EAA6E;AAC7E,MAAM,WAAW,sBAAsB;IACrC,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED;;;;GAIG;AACH,wBAAsB,sBAAsB,CAC1C,WAAW,EAAE,eAAe,EAC5B,IAAI,GAAE,mBAAwB,EAC9B,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,gBAAgB,CAAC,CAinB3B"}
package/dest/factory.js CHANGED
@@ -32,7 +32,7 @@ import { FullNodeCheckpointsBuilder as CheckpointsBuilder, FullNodeCheckpointsBu
32
32
  import { createWorldState, createWorldStateSynchronizer } from '@aztec-labs/world-state';
33
33
  import { createPublicClient } from 'viem';
34
34
  import { createKeyStoreForValidator } from './aztec-node/config.js';
35
- import { AdmissionMinFeesProvider, NextBlockPredictor } from './aztec-node/next_block/index.js';
35
+ import { AdmissionMinFeesProvider, NextBlockFeeCache, NextBlockPredictor } from './aztec-node/next_block/index.js';
36
36
  import { AztecNodeService } from './aztec-node/server.js';
37
37
  import { createSentinel } from './sentinel/factory.js';
38
38
  /**
@@ -200,7 +200,10 @@ import { createSentinel } from './sentinel/factory.js';
200
200
  const feeProvider = new FeeProviderImpl(dateProvider, publicClient, globalVariableBuilderConfig, archiver);
201
201
  await feeProvider.start();
202
202
  started.push(feeProvider);
203
- const nextBlockPredictor = NextBlockPredictor.create({
203
+ // One fee cache serves both the RPC-side predictor and the proposal handler, which checks a checkpoint's first
204
+ // block against the same L1 price.
205
+ const nextBlockLog = log.createChild('next-block-predictor');
206
+ const nextBlockFeeCache = new NextBlockFeeCache({
204
207
  blockSource: archiver,
205
208
  globalVariableBuilder,
206
209
  rollupContract,
@@ -210,7 +213,13 @@ import { createSentinel } from './sentinel/factory.js';
210
213
  rollupAddress: config.rollupAddress
211
214
  },
212
215
  dateProvider,
213
- log: log.createChild('next-block-predictor')
216
+ log: nextBlockLog.createChild('fee-cache')
217
+ });
218
+ const nextBlockPredictor = new NextBlockPredictor({
219
+ blockSource: archiver,
220
+ feeCache: nextBlockFeeCache,
221
+ epochCache,
222
+ log: nextBlockLog
214
223
  });
215
224
  nextBlockPredictor.start();
216
225
  started.push(nextBlockPredictor);
@@ -263,6 +272,7 @@ import { createSentinel } from './sentinel/factory.js';
263
272
  keyStoreManager,
264
273
  blobClient,
265
274
  reexecutionTracker,
275
+ checkpointFeeSource: nextBlockFeeCache,
266
276
  slashingProtectionDb: deps.slashingProtectionDb,
267
277
  blockProposalObservers: deps.blockProposalObservers
268
278
  });
@@ -299,6 +309,7 @@ import { createSentinel } from './sentinel/factory.js';
299
309
  dateProvider,
300
310
  telemetry,
301
311
  reexecutionTracker,
312
+ checkpointFeeSource: nextBlockFeeCache,
302
313
  blockProposalObservers: deps.blockProposalObservers
303
314
  });
304
315
  proposalHandler.register(p2pClient, reexecute, archiver);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aztec-labs/aztec-node",
3
- "version": "6.1.0-nightly.20261102",
3
+ "version": "6.1.0-nightly.20261104",
4
4
  "main": "dest/index.js",
5
5
  "type": "module",
6
6
  "exports": {
@@ -66,37 +66,37 @@
66
66
  },
67
67
  "dependencies": {
68
68
  "@aztec-foundation/bb.js": "6.0.0-rc.7",
69
- "@aztec-labs/archiver": "6.1.0-nightly.20261102",
70
- "@aztec-labs/bb-prover": "6.1.0-nightly.20261102",
71
- "@aztec-labs/blob-client": "6.1.0-nightly.20261102",
72
- "@aztec-labs/blob-lib": "6.1.0-nightly.20261102",
73
- "@aztec-labs/constants": "6.1.0-nightly.20261102",
74
- "@aztec-labs/epoch-cache": "6.1.0-nightly.20261102",
75
- "@aztec-labs/ethereum": "6.1.0-nightly.20261102",
76
- "@aztec-labs/foundation": "6.1.0-nightly.20261102",
77
- "@aztec-labs/kv-store": "6.1.0-nightly.20261102",
78
- "@aztec-labs/node-keystore": "6.1.0-nightly.20261102",
79
- "@aztec-labs/node-lib": "6.1.0-nightly.20261102",
80
- "@aztec-labs/p2p": "6.1.0-nightly.20261102",
81
- "@aztec-labs/protocol-contracts": "6.1.0-nightly.20261102",
82
- "@aztec-labs/prover-client": "6.1.0-nightly.20261102",
83
- "@aztec-labs/prover-node": "6.1.0-nightly.20261102",
84
- "@aztec-labs/sequencer-client": "6.1.0-nightly.20261102",
85
- "@aztec-labs/simulator": "6.1.0-nightly.20261102",
86
- "@aztec-labs/slasher": "6.1.0-nightly.20261102",
87
- "@aztec-labs/standard-contracts": "6.1.0-nightly.20261102",
88
- "@aztec-labs/stdlib": "6.1.0-nightly.20261102",
89
- "@aztec-labs/telemetry-client": "6.1.0-nightly.20261102",
90
- "@aztec-labs/validator-client": "6.1.0-nightly.20261102",
91
- "@aztec-labs/validator-ha-signer": "6.1.0-nightly.20261102",
92
- "@aztec-labs/world-state": "6.1.0-nightly.20261102",
69
+ "@aztec-labs/archiver": "6.1.0-nightly.20261104",
70
+ "@aztec-labs/bb-prover": "6.1.0-nightly.20261104",
71
+ "@aztec-labs/blob-client": "6.1.0-nightly.20261104",
72
+ "@aztec-labs/blob-lib": "6.1.0-nightly.20261104",
73
+ "@aztec-labs/constants": "6.1.0-nightly.20261104",
74
+ "@aztec-labs/epoch-cache": "6.1.0-nightly.20261104",
75
+ "@aztec-labs/ethereum": "6.1.0-nightly.20261104",
76
+ "@aztec-labs/foundation": "6.1.0-nightly.20261104",
77
+ "@aztec-labs/kv-store": "6.1.0-nightly.20261104",
78
+ "@aztec-labs/node-keystore": "6.1.0-nightly.20261104",
79
+ "@aztec-labs/node-lib": "6.1.0-nightly.20261104",
80
+ "@aztec-labs/p2p": "6.1.0-nightly.20261104",
81
+ "@aztec-labs/protocol-contracts": "6.1.0-nightly.20261104",
82
+ "@aztec-labs/prover-client": "6.1.0-nightly.20261104",
83
+ "@aztec-labs/prover-node": "6.1.0-nightly.20261104",
84
+ "@aztec-labs/sequencer-client": "6.1.0-nightly.20261104",
85
+ "@aztec-labs/simulator": "6.1.0-nightly.20261104",
86
+ "@aztec-labs/slasher": "6.1.0-nightly.20261104",
87
+ "@aztec-labs/standard-contracts": "6.1.0-nightly.20261104",
88
+ "@aztec-labs/stdlib": "6.1.0-nightly.20261104",
89
+ "@aztec-labs/telemetry-client": "6.1.0-nightly.20261104",
90
+ "@aztec-labs/validator-client": "6.1.0-nightly.20261104",
91
+ "@aztec-labs/validator-ha-signer": "6.1.0-nightly.20261104",
92
+ "@aztec-labs/world-state": "6.1.0-nightly.20261104",
93
93
  "koa": "^2.16.1",
94
94
  "koa-router": "^13.1.1",
95
95
  "tslib": "^2.4.0",
96
96
  "viem": "2.57.1"
97
97
  },
98
98
  "devDependencies": {
99
- "@aztec-labs/noir-protocol-circuits-types": "6.1.0-nightly.20261102",
99
+ "@aztec-labs/noir-protocol-circuits-types": "6.1.0-nightly.20261104",
100
100
  "@jest/globals": "^30.0.0",
101
101
  "@types/jest": "^30.0.0",
102
102
  "@types/node": "^22.15.17",
@@ -4,16 +4,18 @@ import {
4
4
  SimulationOverridesBuilder,
5
5
  type SimulationOverridesPlan,
6
6
  } from '@aztec-labs/ethereum/contracts';
7
- import { CheckpointNumber } from '@aztec-labs/foundation/branded-types';
7
+ import { CheckpointNumber, type SlotNumber } from '@aztec-labs/foundation/branded-types';
8
8
  import { EthAddress } from '@aztec-labs/foundation/eth-address';
9
9
  import { type Logger, createLogger } from '@aztec-labs/foundation/log';
10
10
  import { RunningPromise } from '@aztec-labs/foundation/promise';
11
11
  import { type DateProvider, executeTimeout } from '@aztec-labs/foundation/timer';
12
12
  import { AztecAddress } from '@aztec-labs/stdlib/aztec-address';
13
- import type { L1SyncPoint, L2BlockSource, L2Frontier } from '@aztec-labs/stdlib/block';
13
+ import type { L1SyncPoint, L2BlockId, L2BlockSource, L2Frontier } from '@aztec-labs/stdlib/block';
14
14
  import { buildCheckpointSimulationOverridesPlan } from '@aztec-labs/stdlib/checkpoint';
15
+ import type { GasFees } from '@aztec-labs/stdlib/gas';
15
16
  import type { CoordinationSignatureContext } from '@aztec-labs/stdlib/p2p';
16
17
  import type { CheckpointGlobalVariables, GlobalVariableBuilder } from '@aztec-labs/stdlib/tx';
18
+ import type { CheckpointOpeningFeeSource } from '@aztec-labs/validator-client';
17
19
 
18
20
  import {
19
21
  type BoundaryFeeKey,
@@ -83,7 +85,7 @@ export interface NextBlockFeeCacheDeps {
83
85
  * Before {@link start} (or after {@link stop}) requests still price inline through the same refresh path, which
84
86
  * is what tests and TXE-like environments rely on.
85
87
  */
86
- export class NextBlockFeeCache {
88
+ export class NextBlockFeeCache implements CheckpointOpeningFeeSource {
87
89
  private readonly blockSource: L2BlockSource;
88
90
  private readonly globalVariableBuilder: GlobalVariableBuilder;
89
91
  private readonly rollupContract: RollupContract | undefined;
@@ -137,36 +139,69 @@ export class NextBlockFeeCache {
137
139
  * shared refresh: a simulation waits for it and surfaces its failure, while a quote passes `maxWaitMs` and
138
140
  * falls back to whatever the cache already holds rather than turning an L1 outage into a multi-second RPC.
139
141
  */
140
- public async getBoundaryGlobals(
142
+ public getBoundaryGlobals(
141
143
  key: BoundaryFeeKey,
142
144
  frontier: L2Frontier,
143
145
  opts?: { maxWaitMs?: number },
144
146
  ): Promise<CheckpointGlobalVariables | undefined> {
145
- const maxWaitMs = opts?.maxWaitMs;
146
- // An uncapped reader gets more than one attempt because the refresh it joins may be one that started from an
147
- // older frontier and therefore priced a different boundary; the next pass is its own. Still never concurrent.
148
- const attempts = maxWaitMs === undefined ? MAX_REFRESH_ATTEMPTS : 1;
149
- for (let attempt = 0; attempt < attempts; attempt++) {
150
- const hit = this.findRecord(key);
151
- if (hit) {
152
- return hit.globals;
153
- }
154
- const refresh = this.refresh(frontier);
155
- await (maxWaitMs === undefined ? refresh : this.waitFor(refresh, maxWaitMs));
147
+ return this.findOrPrice(key, frontier, opts?.maxWaitMs);
148
+ }
149
+
150
+ /**
151
+ * The gas fees the first block of a checkpoint at `slot` built on `parent` must carry, for checking a block proposal
152
+ * against. Keyed on the proposal's slot and parent rather than on the slot this node's clock would plan: the frontier
153
+ * must end at `parent`, and the boundary is planned for `slot` from it. A matching record is served from memory; on
154
+ * a miss the request joins the refresh in flight and then runs a pass planned on `slot`, so it gets the exact price
155
+ * and not a fallback.
156
+ *
157
+ * Undefined when this node's view does not hold the data to produce the price the proposer computes yet: the
158
+ * frontier does not end at `parent` (the parent's checkpoint proposal has not been processed, or the chain moved
159
+ * on), or the checkpointed tip has not reached the parent's parent, whose fee state the pipelined plan reads from
160
+ * L1 (see {@link pricesAsProposer}). The caller waits for a later archiver sync rather than accepting the pinned-tips
161
+ * approximation. Throws when the L1 read fails.
162
+ *
163
+ * Only a record priced at the frontier's L1 block, or at a later one, is served. The L1 fee oracle can change without
164
+ * moving the frontier (anyone may call its update), and the background loop only catches up within a refresh
165
+ * interval, so an older record could carry a price the proposer has already moved past.
166
+ */
167
+ public async getCheckpointOpeningGasFees(slot: SlotNumber, parent: L2BlockId): Promise<GasFees | undefined> {
168
+ const frontier = await this.blockSource.getL2Frontier();
169
+ const tip = frontier.tips.proposed;
170
+ if (tip.number !== parent.number || tip.hash !== parent.hash) {
171
+ this.log.debug(`Cannot price the checkpoint opened at slot ${slot}: the proposed tip is not its parent`, {
172
+ slot,
173
+ parent,
174
+ tip,
175
+ });
176
+ return undefined;
156
177
  }
157
- return this.findRecord(key)?.globals;
178
+ const plan = planNextBlock(frontier, slot);
179
+ const newCheckpoint = plan.newCheckpoint;
180
+ if (!newCheckpoint || newCheckpoint.targetSlot !== slot || !this.pricesAsProposer(frontier, newCheckpoint)) {
181
+ this.log.debug(`Cannot price the checkpoint opened at slot ${slot} as its proposer does from this frontier`, {
182
+ slot,
183
+ parent,
184
+ targetSlot: newCheckpoint?.targetSlot,
185
+ proposedCheckpointNumber: newCheckpoint?.proposedCheckpointData?.checkpointNumber,
186
+ checkpointedCheckpointNumber: newCheckpoint?.checkpointedCheckpointNumber,
187
+ });
188
+ return undefined;
189
+ }
190
+ const key = computeBoundaryFeeKey(plan, frontier.pendingChainValidationStatus);
191
+ return key && (await this.findOrPrice(key, frontier, undefined, slot, frontier.l1SyncPoint))?.gasFees;
158
192
  }
159
193
 
160
194
  /**
161
195
  * Runs one refresh pass, or joins the one already running. Single-flight is what keeps a burst of requests
162
196
  * during a transition down to a single L1 round trip, shared with the background loop.
163
197
  * @param frontier - Snapshot to plan from; read fresh from the archiver when omitted, as the loop does.
198
+ * @param slot - Slot to plan the boundary for in place of this node's clock slot.
164
199
  */
165
- public refresh(frontier?: L2Frontier): Promise<void> {
200
+ public refresh(frontier?: L2Frontier, slot?: SlotNumber): Promise<void> {
166
201
  if (this.inFlightRefresh) {
167
202
  return this.inFlightRefresh;
168
203
  }
169
- const refresh = this.runRefresh(frontier).finally(() => {
204
+ const refresh = this.runRefresh(frontier, slot).finally(() => {
170
205
  this.inFlightRefresh = undefined;
171
206
  });
172
207
  // Every caller awaits the returned promise and reports its failure, but the stored copy may only be awaited by
@@ -176,9 +211,35 @@ export class NextBlockFeeCache {
176
211
  return refresh;
177
212
  }
178
213
 
179
- private async runRefresh(frontier?: L2Frontier): Promise<void> {
214
+ /**
215
+ * Serves the record matching `key`, or joins the shared refresh to produce it, as {@link getBoundaryGlobals}
216
+ * describes. `slot` plans a refresh this reader starts for that slot rather than for the clock slot.
217
+ * `minL1SyncPoint`, when set, skips records priced before that L1 block (see {@link pricedAtOrAfter}).
218
+ */
219
+ private async findOrPrice(
220
+ key: BoundaryFeeKey,
221
+ frontier: L2Frontier,
222
+ maxWaitMs: number | undefined,
223
+ slot?: SlotNumber,
224
+ minL1SyncPoint?: L1SyncPoint,
225
+ ): Promise<CheckpointGlobalVariables | undefined> {
226
+ // An uncapped reader gets more than one attempt because the refresh it joins may be one that started from an
227
+ // older frontier and therefore priced a different boundary; the next pass is its own. Still never concurrent.
228
+ const attempts = maxWaitMs === undefined ? MAX_REFRESH_ATTEMPTS : 1;
229
+ for (let attempt = 0; attempt < attempts; attempt++) {
230
+ const hit = this.findRecord(key, minL1SyncPoint);
231
+ if (hit) {
232
+ return hit.globals;
233
+ }
234
+ const refresh = this.refresh(frontier, slot);
235
+ await (maxWaitMs === undefined ? refresh : this.waitFor(refresh, maxWaitMs));
236
+ }
237
+ return this.findRecord(key, minL1SyncPoint)?.globals;
238
+ }
239
+
240
+ private async runRefresh(frontier?: L2Frontier, slot?: SlotNumber): Promise<void> {
180
241
  const snapshot = frontier ?? (await this.blockSource.getL2Frontier());
181
- const plan = planNextBlock(snapshot, getClockSlot(this.epochCache));
242
+ const plan = planNextBlock(snapshot, slot ?? getClockSlot(this.epochCache));
182
243
  const key = computeBoundaryFeeKey(plan, snapshot.pendingChainValidationStatus);
183
244
  if (!key || !plan.newCheckpoint) {
184
245
  // Mid-checkpoint: the fee is frozen in the in-progress checkpoint's header, so there is nothing to price.
@@ -215,12 +276,19 @@ export class NextBlockFeeCache {
215
276
  this.current = record;
216
277
  }
217
278
 
218
- /** The freshest record matching `key`, or undefined when none is recent enough to trust. */
219
- private findRecord(key: BoundaryFeeKey): BoundaryFeeRecord | undefined {
279
+ /**
280
+ * The freshest record matching `key`, or undefined when none is recent enough to trust. With `minL1SyncPoint`, a
281
+ * record also has to be priced at that L1 block or a later one.
282
+ */
283
+ private findRecord(key: BoundaryFeeKey, minL1SyncPoint?: L1SyncPoint): BoundaryFeeRecord | undefined {
220
284
  const maxAgeMs = MAX_AGE_INTERVALS * this.refreshIntervalMs;
221
285
  const now = this.dateProvider.now();
222
286
  return [this.current, this.previous].find(
223
- record => record !== undefined && boundaryFeeKeyEquals(record.key, key) && now - record.refreshedAtMs < maxAgeMs,
287
+ record =>
288
+ record !== undefined &&
289
+ boundaryFeeKeyEquals(record.key, key) &&
290
+ now - record.refreshedAtMs < maxAgeMs &&
291
+ (minL1SyncPoint === undefined || pricedAtOrAfter(record.l1SyncPoint, minL1SyncPoint)),
224
292
  );
225
293
  }
226
294
 
@@ -231,6 +299,24 @@ export class NextBlockFeeCache {
231
299
  );
232
300
  }
233
301
 
302
+ /**
303
+ * Whether {@link buildOverridesPlan} prices `newCheckpoint` with the plan its proposer uses, rather than with the
304
+ * pinned-tips fallback. The sequencer never builds on a proposed parent more than one checkpoint ahead of its
305
+ * checkpointed tip, so a frontier in that state trails L1: the parent's parent, whose fee header the pipelined plan
306
+ * reads, is not checkpointed in it yet, and the fallback price is not one a proposer would have used.
307
+ */
308
+ private pricesAsProposer(frontier: L2Frontier, newCheckpoint: NewCheckpointPlan): boolean {
309
+ const { proposedCheckpointData, checkpointedCheckpointNumber } = newCheckpoint;
310
+ if (proposedCheckpointData) {
311
+ return (
312
+ this.rollupContract !== undefined &&
313
+ proposedCheckpointData.checkpointNumber === checkpointedCheckpointNumber + 1
314
+ );
315
+ }
316
+ // On a valid chain with no proposed parent the pinned-tips plan is the one the proposer builds.
317
+ return this.rollupContract !== undefined || frontier.pendingChainValidationStatus.valid;
318
+ }
319
+
234
320
  /**
235
321
  * Builds the chain-state overrides plan passed to `buildCheckpointGlobalVariables`, mirroring the sequencer
236
322
  * (which always pins tips to neutralize prunes). When pipelining, the plan carries the proposed parent's
@@ -309,3 +395,14 @@ export class NextBlockFeeCache {
309
395
  function l1SyncPointEquals(a: L1SyncPoint | undefined, b: L1SyncPoint | undefined): boolean {
310
396
  return a === undefined || b === undefined ? a === b : a.blockHash.equals(b.blockHash);
311
397
  }
398
+
399
+ /**
400
+ * Whether a record priced at `pricedAt` reflects L1 at least as recent as `min`. At the same height the hashes must
401
+ * match, since a reorg that replaces that block can carry different fee state.
402
+ */
403
+ function pricedAtOrAfter(pricedAt: L1SyncPoint | undefined, min: L1SyncPoint): boolean {
404
+ if (pricedAt === undefined || pricedAt.blockNumber < min.blockNumber) {
405
+ return false;
406
+ }
407
+ return pricedAt.blockNumber > min.blockNumber || pricedAt.blockHash.equals(min.blockHash);
408
+ }
package/src/factory.ts CHANGED
@@ -60,7 +60,7 @@ import { createWorldState, createWorldStateSynchronizer } from '@aztec-labs/worl
60
60
  import { createPublicClient } from 'viem';
61
61
 
62
62
  import { type AztecNodeConfig, createKeyStoreForValidator } from './aztec-node/config.js';
63
- import { AdmissionMinFeesProvider, NextBlockPredictor } from './aztec-node/next_block/index.js';
63
+ import { AdmissionMinFeesProvider, NextBlockFeeCache, NextBlockPredictor } from './aztec-node/next_block/index.js';
64
64
  import { AztecNodeService } from './aztec-node/server.js';
65
65
  import { createSentinel } from './sentinel/factory.js';
66
66
 
@@ -290,14 +290,23 @@ export async function createAztecNodeService(
290
290
  await feeProvider.start();
291
291
  started.push(feeProvider);
292
292
 
293
- const nextBlockPredictor = NextBlockPredictor.create({
293
+ // One fee cache serves both the RPC-side predictor and the proposal handler, which checks a checkpoint's first
294
+ // block against the same L1 price.
295
+ const nextBlockLog = log.createChild('next-block-predictor');
296
+ const nextBlockFeeCache = new NextBlockFeeCache({
294
297
  blockSource: archiver,
295
298
  globalVariableBuilder,
296
299
  rollupContract,
297
300
  epochCache,
298
301
  signatureContext: { chainId: ethereumChain.chainInfo.id, rollupAddress: config.rollupAddress },
299
302
  dateProvider,
300
- log: log.createChild('next-block-predictor'),
303
+ log: nextBlockLog.createChild('fee-cache'),
304
+ });
305
+ const nextBlockPredictor = new NextBlockPredictor({
306
+ blockSource: archiver,
307
+ feeCache: nextBlockFeeCache,
308
+ epochCache,
309
+ log: nextBlockLog,
301
310
  });
302
311
  nextBlockPredictor.start();
303
312
  started.push(nextBlockPredictor);
@@ -383,6 +392,7 @@ export async function createAztecNodeService(
383
392
  keyStoreManager,
384
393
  blobClient,
385
394
  reexecutionTracker,
395
+ checkpointFeeSource: nextBlockFeeCache,
386
396
  slashingProtectionDb: deps.slashingProtectionDb,
387
397
  blockProposalObservers: deps.blockProposalObservers,
388
398
  });
@@ -423,6 +433,7 @@ export async function createAztecNodeService(
423
433
  dateProvider,
424
434
  telemetry,
425
435
  reexecutionTracker,
436
+ checkpointFeeSource: nextBlockFeeCache,
426
437
  blockProposalObservers: deps.blockProposalObservers,
427
438
  });
428
439
  proposalHandler.register(p2pClient, reexecute, archiver);