@aztec/epoch-cache 0.0.1-commit.f5d02921e → 0.0.1-commit.f7ea82942

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.
package/README.md ADDED
@@ -0,0 +1,211 @@
1
+ # Epoch Cache
2
+
3
+ Caches validator committee information per epoch to reduce L1 RPC traffic. Provides
4
+ the current committee, proposer selection, and escape hatch status for any given slot
5
+ or epoch, used by the sequencer, validator client, and other node components that need
6
+ to know who can propose or attest in a given slot.
7
+
8
+ ## Committee Computation
9
+
10
+ Each epoch has a **committee**: a subset of all registered validators selected to
11
+ participate in consensus for that epoch. The committee is determined by three inputs:
12
+
13
+ 1. **The validator set** -- the full list of registered attesters, snapshotted at a
14
+ past point in time.
15
+ 2. **A RANDAO seed** -- pseudo-random value derived from Ethereum's `block.prevrandao`,
16
+ also sampled from the past.
17
+ 3. **A sampling algorithm** -- a Fisher-Yates-style shuffle that picks
18
+ `targetCommitteeSize` indices from the validator set without replacement.
19
+
20
+ Once computed, the committee is fixed for the entire epoch. The L1 rollup contract
21
+ stores a keccak256 commitment of the committee addresses to prevent substitution.
22
+
23
+ ### Sampling Algorithm
24
+
25
+ The sampling draws `targetCommitteeSize` indices from a pool of `validatorSetSize`
26
+ using a sample-without-replacement approach:
27
+
28
+ ```
29
+ for i in 0..committeeSize:
30
+ sampledIndex = keccak256(seed, i) % (poolSize - i)
31
+ committee[i] = pool[sampledIndex]
32
+ swap pool[sampledIndex] with pool[last]
33
+ shrink pool by 1
34
+ ```
35
+
36
+ Each iteration hashes the seed with the iteration index to pick a random position,
37
+ then swaps the picked element to the end and shrinks the pool, ensuring no validator
38
+ is selected twice.
39
+
40
+ ## LAG Values
41
+
42
+ Two lag parameters prevent manipulation of committee composition:
43
+
44
+ ### `lagInEpochsForValidatorSet`
45
+
46
+ When computing the committee for epoch N, the validator set is read from a snapshot
47
+ taken `lagInEpochsForValidatorSet` epochs in the past. The sampling timestamp is:
48
+
49
+ ```
50
+ validatorSetTimestamp = epochStart(N) - (lagInEpochsForValidatorSet * epochDuration * slotDuration)
51
+ ```
52
+
53
+ This prevents an attacker from registering new validators just before an epoch to
54
+ influence who gets selected. The validator set is locked well in advance.
55
+
56
+ ### `lagInEpochsForRandao`
57
+
58
+ The RANDAO seed used for committee selection is sampled from
59
+ `lagInEpochsForRandao` epochs in the past:
60
+
61
+ ```
62
+ randaoTimestamp = epochStart(N) - (lagInEpochsForRandao * epochDuration * slotDuration)
63
+ ```
64
+
65
+ This prevents L1 validators from previewing the randomness and coordinating to
66
+ become proposers.
67
+
68
+ ### Why Two Separate Lags?
69
+
70
+ The constraint `lagInEpochsForValidatorSet >= lagInEpochsForRandao` is enforced.
71
+ If both lags were equal, an attacker who learns the RANDAO seed could still
72
+ register validators in time to be included in the sampled set. By freezing the
73
+ validator set further back than the RANDAO seed, the attacker knows the randomness
74
+ but can no longer change the input population it selects from.
75
+
76
+ ## RANDAO Seed
77
+
78
+ The RANDAO seed provides per-epoch randomness for committee selection and proposer
79
+ assignment. It works as follows:
80
+
81
+ 1. **Checkpointing**: Each epoch, `block.prevrandao` (Ethereum's beacon chain
82
+ randomness) is stored in a checkpointed mapping keyed by epoch timestamp.
83
+ Multiple calls in the same epoch are idempotent.
84
+
85
+ 2. **Seed derivation**: The actual seed used for sampling is:
86
+ ```
87
+ seed = keccak256(abi.encode(epochNumber, storedRandao))
88
+ ```
89
+ where `storedRandao` is the value checkpointed at or before the RANDAO sampling
90
+ timestamp (determined by `lagInEpochsForRandao`). Mixing in the epoch number
91
+ ensures distinct seeds even if the same RANDAO value is reused.
92
+
93
+ 3. **Bootstrap**: The first two epochs use a bootstrapped RANDAO value stored at
94
+ initialization, since there is no prior history to sample from.
95
+
96
+ ## Proposer Selection
97
+
98
+ Within a committee, a single **proposer** is designated for each slot. The proposer
99
+ is responsible for assembling transactions into a block and publishing it.
100
+
101
+ The proposer index within the committee is:
102
+
103
+ ```
104
+ proposerIndex = keccak256(abi.encode(epoch, slot, seed)) % committeeSize
105
+ ```
106
+
107
+ This is deterministic: anyone with the epoch number, slot number, and seed can
108
+ independently compute who the proposer is. Each slot gets a different proposer
109
+ because the slot number is mixed into the hash.
110
+
111
+ ### Proposer Pipelining
112
+
113
+ When proposer pipelining is enabled, the proposer builds for the *next* slot
114
+ rather than the current one (`PROPOSER_PIPELINING_SLOT_OFFSET = 1`). This gives
115
+ the proposer a full slot of lead time to assemble and propagate the block. The
116
+ "target slot" methods on the epoch cache apply this offset automatically.
117
+
118
+ ### Empty Committees
119
+
120
+ If the committee is empty (i.e., `targetCommitteeSize` is 0), anyone can propose.
121
+ The proposer methods return `undefined` in this case rather than throwing. If the
122
+ committee should exist but doesn't (insufficient validators registered), a
123
+ `NoCommitteeError` is thrown.
124
+
125
+ ## Escape Hatch
126
+
127
+ The escape hatch is a censorship-resistance mechanism. It opens periodically
128
+ (every `FREQUENCY` epochs, for `ACTIVE_DURATION` epochs) and allows a single
129
+ designated proposer to submit blocks without committee attestations.
130
+
131
+ ### Candidate System
132
+
133
+ The escape hatch has its own candidate pool, separate from the main validator set:
134
+
135
+ - Candidates join by posting a bond (`BOND_SIZE`).
136
+ - A designated proposer is selected per hatch window using a similar
137
+ RANDAO-based random selection from the candidate set.
138
+ - If the designated proposer fails to propose and prove during their window,
139
+ they are penalized (`FAILED_HATCH_PUNISHMENT`).
140
+ - Candidates exit through a two-step process (`initiateExit` then
141
+ `leaveCandidateSet`) with a withdrawal tax.
142
+
143
+ ### Integration with Epoch Cache
144
+
145
+ The epoch cache queries `isHatchOpen(epoch)` on the escape hatch contract and
146
+ caches the result alongside the committee info for each epoch. This flag is
147
+ exposed via `isEscapeHatchOpen(epoch)` and `isEscapeHatchOpenAtSlot(slot)`,
148
+ used by the sequencer to decide whether to require committee attestations.
149
+
150
+ ## TTL-based Caching with Finalization Tracking
151
+
152
+ Each cache entry stores L1 provenance metadata alongside the committee data:
153
+ the L1 block number, hash, and timestamp at query time, plus a **finalized** flag.
154
+
155
+ ### Finalization Check
156
+
157
+ When fetching committee data, the epoch cache queries both the latest and finalized
158
+ L1 blocks in parallel. It computes the **sampling timestamp** for the epoch:
159
+
160
+ ```
161
+ samplingTs = epochStart(N) - lagInEpochsForRandao * epochDuration * slotDuration
162
+ ```
163
+
164
+ Using `lagInEpochsForRandao` as the binding constraint (it is always
165
+ <= `lagInEpochsForValidatorSet`). If `samplingTs <= l1FinalizedTimestamp`, the
166
+ entry is marked as **finalized**.
167
+
168
+ ### Cache Behaviour
169
+
170
+ - **Finalized entries** are cached permanently (within LRU limits). An L1 reorg
171
+ cannot change data that has been finalized, so there is no risk of stale data.
172
+ - **Non-finalized entries** are cached with a TTL of one Ethereum slot duration
173
+ (typically 12 seconds). After this TTL, the next request triggers a re-fetch
174
+ from L1. On re-fetch, if the data is now finalized, the entry gets promoted
175
+ to permanent.
176
+ - **Unstable epochs** (sampling timestamp beyond the latest L1 block) cause an
177
+ error, since the L1 contract itself would revert.
178
+
179
+ This approach preserves **safety** (stale data from L1 reorgs gets refreshed
180
+ within one Ethereum slot) while maintaining **liveness** (the cache never refuses
181
+ to serve data that L1 accepts, even if L1 finalization stalls).
182
+
183
+ ### Concurrency
184
+
185
+ The cache map stores both resolved entries and in-flight promises. When a fetch
186
+ starts, the promise is placed directly in the cache. Concurrent callers for the
187
+ same epoch detect the promise and await it, ensuring only one L1 query per epoch
188
+ at a time. On failure, the promise is replaced with the previous stale entry (if
189
+ any) so the next caller retries cleanly.
190
+
191
+ ## Caching Strategy
192
+
193
+ The epoch cache stores committee info (committee members, seed, escape hatch
194
+ status) per epoch in an LRU-style map with a configurable size (default 12
195
+ epochs). Cache entries are only created for epochs with non-empty committees;
196
+ empty results are not cached to allow retries.
197
+
198
+ The cache also maintains a separate set of all registered validators, refreshed
199
+ on a configurable interval (`validatorRefreshIntervalSeconds`, default 60s),
200
+ used to check validator registration status independently of committee membership.
201
+
202
+ ## Configuration
203
+
204
+ | Parameter | Default | Purpose |
205
+ |-----------|---------|---------|
206
+ | `cacheSize` | 12 | Max number of epoch committee entries to keep |
207
+ | `validatorRefreshIntervalSeconds` | 60 | How often to refresh the full validator list |
208
+ | `enableProposerPipelining` | false | Build for next slot instead of current |
209
+ | `lagInEpochsForValidatorSet` | (from L1) | How far back to snapshot the validator set |
210
+ | `lagInEpochsForRandao` | (from L1) | How far back to sample the RANDAO seed |
211
+ | `targetCommitteeSize` | (from L1) | Number of validators to select per epoch |
@@ -20,6 +20,18 @@ export type EpochCommitteeInfo = {
20
20
  isEscapeHatchOpen: boolean;
21
21
  };
22
22
  export type SlotTag = 'now' | 'next' | SlotNumber;
23
+ /** Resolved cache entry with L1 provenance metadata. */
24
+ type CachedEpochEntry = {
25
+ data: EpochCommitteeInfo;
26
+ /** L1 block number at which the committee data was originally queried. */
27
+ lastQueryL1BlockNumber: bigint;
28
+ /** L1 block hash at which the committee data was originally queried. Used to detect reorgs. */
29
+ lastQueryL1BlockHash: `0x${string}`;
30
+ /** Latest L1 block timestamp at the time of the most recent refresh (full fetch or lightweight check). */
31
+ lastRefreshL1Timestamp: bigint;
32
+ /** Whether the epoch's sampling data falls within finalized L1 history. */
33
+ finalized: boolean;
34
+ };
23
35
  export interface EpochCacheInterface {
24
36
  getCommittee(slot: SlotTag | undefined): Promise<EpochCommitteeInfo>;
25
37
  getSlotNow(): SlotNumber;
@@ -37,6 +49,7 @@ export interface EpochCacheInterface {
37
49
  nowSeconds: bigint;
38
50
  };
39
51
  isProposerPipeliningEnabled(): boolean;
52
+ pipeliningOffset(): number;
40
53
  isEscapeHatchOpen(epoch: EpochNumber): Promise<boolean>;
41
54
  isEscapeHatchOpenAtSlot(slot: SlotTag): Promise<boolean>;
42
55
  getProposerIndexEncoding(epoch: EpochNumber, slot: SlotNumber, seed: bigint): `0x${string}`;
@@ -73,7 +86,11 @@ export declare class EpochCache implements EpochCacheInterface {
73
86
  validatorRefreshIntervalSeconds: number;
74
87
  enableProposerPipelining: boolean;
75
88
  };
76
- protected cache: Map<EpochNumber, EpochCommitteeInfo>;
89
+ /**
90
+ * Single map holding both resolved entries and in-flight promises.
91
+ * A `Promise` value means a fetch is in progress; concurrent callers await it.
92
+ */
93
+ protected cache: Map<EpochNumber, CachedEpochEntry | Promise<CachedEpochEntry>>;
77
94
  private allValidators;
78
95
  private lastValidatorRefresh;
79
96
  private readonly log;
@@ -91,6 +108,7 @@ export declare class EpochCache implements EpochCacheInterface {
91
108
  }): Promise<EpochCache>;
92
109
  getL1Constants(): L1RollupConstants;
93
110
  isProposerPipeliningEnabled(): boolean;
111
+ pipeliningOffset(): number;
94
112
  getSlotNow(): SlotNumber;
95
113
  getTargetSlot(): SlotNumber;
96
114
  getEpochNow(): EpochNumber;
@@ -107,12 +125,7 @@ export declare class EpochCache implements EpochCacheInterface {
107
125
  };
108
126
  private getEpochAndSlotAtTimestamp;
109
127
  getCommitteeForEpoch(epoch: EpochNumber): Promise<EpochCommitteeInfo>;
110
- /**
111
- * Returns whether the escape hatch is open for the given epoch.
112
- *
113
- * Uses the already-cached EpochCommitteeInfo when available. If not cached, it will fetch
114
- * the epoch committee info (which includes the escape hatch flag) and return it.
115
- */
128
+ /** Returns whether the escape hatch is open for the given epoch. */
116
129
  isEscapeHatchOpen(epoch: EpochNumber): Promise<boolean>;
117
130
  /**
118
131
  * Returns whether the escape hatch is open for the epoch containing the given slot.
@@ -122,13 +135,26 @@ export declare class EpochCache implements EpochCacheInterface {
122
135
  */
123
136
  isEscapeHatchOpenAtSlot(slot?: SlotTag): Promise<boolean>;
124
137
  /**
125
- * Get the current validator set
126
- * @param nextSlot - If true, get the validator set for the next slot.
127
- * @returns The current validator set.
138
+ * Get the current validator set.
139
+ *
140
+ * Returns cached data if the entry is finalized or still fresh (queried less than one
141
+ * Ethereum slot ago). Stale non-finalized entries are re-queried, and concurrent callers
142
+ * coalesce on the same in-flight promise so the L1 query happens only once.
128
143
  */
129
144
  getCommittee(slot?: SlotTag): Promise<EpochCommitteeInfo>;
130
145
  private getEpochAndTimestamp;
131
- private computeCommittee;
146
+ /** Evicts oldest cache entries (resolved or in-flight) beyond cacheSize. */
147
+ private purgeCache;
148
+ /** Returns true if a non-finalized cache entry is older than one Ethereum slot. */
149
+ private isStale;
150
+ /** Whether a cached epoch entry has been marked as finalized. Returns undefined if not cached or still in-flight. */
151
+ isFinalized(epoch: EpochNumber): boolean | undefined;
152
+ /** Returns the latest L1 timestamp stored in the cached entry. Undefined if not cached or in-flight. */
153
+ getCachedLastRefreshL1Timestamp(epoch: EpochNumber): bigint | undefined;
154
+ /** Computes the sampling timestamp for an epoch's committee data. */
155
+ private getSamplingTimestamp;
156
+ private refreshStaleEntry;
157
+ private fetchAndCache;
132
158
  /**
133
159
  * Get the ABI encoding of the proposer index - see ValidatorSelectionLib.sol computeProposerIndex
134
160
  */
@@ -164,4 +190,5 @@ export declare class EpochCache implements EpochCacheInterface {
164
190
  filterInCommittee(slot: SlotTag, validators: EthAddress[]): Promise<EthAddress[]>;
165
191
  getRegisteredValidators(): Promise<EthAddress[]>;
166
192
  }
167
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZXBvY2hfY2FjaGUuZC50cyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy9lcG9jaF9jYWNoZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFFQSxPQUFPLEVBQW9CLGNBQWMsRUFBRSxNQUFNLDJCQUEyQixDQUFDO0FBQzdFLE9BQU8sRUFBRSxXQUFXLEVBQUUsVUFBVSxFQUFFLE1BQU0saUNBQWlDLENBQUM7QUFDMUUsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLCtCQUErQixDQUFDO0FBRTNELE9BQU8sRUFBRSxZQUFZLEVBQUUsTUFBTSx5QkFBeUIsQ0FBQztBQUN2RCxPQUFPLEVBQ0wsS0FBSyxpQkFBaUIsRUFRdkIsTUFBTSw2QkFBNkIsQ0FBQztBQUlyQyxPQUFPLEVBQUUsS0FBSyxnQkFBZ0IsRUFBOEIsTUFBTSxhQUFhLENBQUM7QUFFaEYsK0VBQStFO0FBQy9FLGVBQU8sTUFBTSwrQkFBK0IsSUFBSSxDQUFDO0FBRWpELHdEQUF3RDtBQUN4RCxNQUFNLE1BQU0sWUFBWSxHQUFHO0lBQ3pCLElBQUksRUFBRSxVQUFVLENBQUM7SUFDakIsS0FBSyxFQUFFLFdBQVcsQ0FBQztJQUNuQixFQUFFLEVBQUUsTUFBTSxDQUFDO0NBQ1osQ0FBQztBQUVGLE1BQU0sTUFBTSxrQkFBa0IsR0FBRztJQUMvQixTQUFTLEVBQUUsVUFBVSxFQUFFLEdBQUcsU0FBUyxDQUFDO0lBQ3BDLElBQUksRUFBRSxNQUFNLENBQUM7SUFDYixLQUFLLEVBQUUsV0FBVyxDQUFDO0lBQ25CLCtEQUErRDtJQUMvRCxpQkFBaUIsRUFBRSxPQUFPLENBQUM7Q0FDNUIsQ0FBQztBQUVGLE1BQU0sTUFBTSxPQUFPLEdBQUcsS0FBSyxHQUFHLE1BQU0sR0FBRyxVQUFVLENBQUM7QUFFbEQsTUFBTSxXQUFXLG1CQUFtQjtJQUNsQyxZQUFZLENBQUMsSUFBSSxFQUFFLE9BQU8sR0FBRyxTQUFTLEdBQUcsT0FBTyxDQUFDLGtCQUFrQixDQUFDLENBQUM7SUFDckUsVUFB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
193
+ export {};
194
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZXBvY2hfY2FjaGUuZC50cyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy9lcG9jaF9jYWNoZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFFQSxPQUFPLEVBQW9CLGNBQWMsRUFBRSxNQUFNLDJCQUEyQixDQUFDO0FBRTdFLE9BQU8sRUFBRSxXQUFXLEVBQUUsVUFBVSxFQUFFLE1BQU0saUNBQWlDLENBQUM7QUFDMUUsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLCtCQUErQixDQUFDO0FBRTNELE9BQU8sRUFBRSxZQUFZLEVBQUUsTUFBTSx5QkFBeUIsQ0FBQztBQUN2RCxPQUFPLEVBQ0wsS0FBSyxpQkFBaUIsRUFTdkIsTUFBTSw2QkFBNkIsQ0FBQztBQUlyQyxPQUFPLEVBQUUsS0FBSyxnQkFBZ0IsRUFBOEIsTUFBTSxhQUFhLENBQUM7QUFFaEYsK0VBQStFO0FBQy9FLGVBQU8sTUFBTSwrQkFBK0IsSUFBSSxDQUFDO0FBRWpELHdEQUF3RDtBQUN4RCxNQUFNLE1BQU0sWUFBWSxHQUFHO0lBQ3pCLElBQUksRUFBRSxVQUFVLENBQUM7SUFDakIsS0FBSyxFQUFFLFdBQVcsQ0FBQztJQUNuQixFQUFFLEVBQUUsTUFBTSxDQUFDO0NBQ1osQ0FBQztBQUVGLE1BQU0sTUFBTSxrQkFBa0IsR0FBRztJQUMvQixTQUFTLEVBQUUsVUFBVSxFQUFFLEdBQUcsU0FBUyxDQUFDO0lBQ3BDLElBQUksRUFBRSxNQUFNLENBQUM7SUFDYixLQUFLLEVBQUUsV0FBVyxDQUFDO0lBQ25CLCtEQUErRDtJQUMvRCxpQkFBaUIsRUFBRSxPQUFPLENBQUM7Q0FDNUIsQ0FBQztBQUVGLE1BQU0sTUFBTSxPQUFPLEdBQUcsS0FBSyxHQUFHLE1BQU0sR0FBRyxVQUFVLENBQUM7QUFLbEQsd0RBQXdEO0FBQ3hELEtBQUssZ0JBQWdCLEdBQUc7SUFDdEIsSUFBSSxFQUFFLGtCQUFrQixDQUFDO0lBQ3pCLDBFQUEwRTtJQUMxRSxzQkFBc0IsRUFBRSxNQUFNLENBQUM7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
@@ -1 +1 @@
1
- {"version":3,"file":"epoch_cache.d.ts","sourceRoot":"","sources":["../src/epoch_cache.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"epoch_cache.d.ts","sourceRoot":"","sources":["../src/epoch_cache.ts"],"names":[],"mappings":"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"}
@@ -1,11 +1,12 @@
1
1
  import { createEthereumChain } from '@aztec/ethereum/chain';
2
2
  import { makeL1HttpTransport } from '@aztec/ethereum/client';
3
3
  import { NoCommitteeError, RollupContract } from '@aztec/ethereum/contracts';
4
+ import { getFinalizedL1Block } from '@aztec/ethereum/queries';
4
5
  import { SlotNumber } from '@aztec/foundation/branded-types';
5
6
  import { EthAddress } from '@aztec/foundation/eth-address';
6
7
  import { createLogger } from '@aztec/foundation/log';
7
8
  import { DateProvider } from '@aztec/foundation/timer';
8
- import { getEpochAtSlot, getEpochNumberAtTimestamp, getNextL1SlotTimestamp, getSlotAtNextL1Block, getSlotAtTimestamp, getSlotRangeForEpoch, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
9
+ import { getEpochAtSlot, getEpochNumberAtTimestamp, getNextL1SlotTimestamp, getSlotAtNextL1Block, getSlotAtTimestamp, getSlotRangeForEpoch, getStartTimestampForEpoch, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
9
10
  import { createPublicClient, encodeAbiParameters, keccak256 } from 'viem';
10
11
  import { getEpochCacheConfigEnvVars } from './config.js';
11
12
  /** When proposer pipelining is enabled, the proposer builds one slot ahead. */ export const PROPOSER_PIPELINING_SLOT_OFFSET = 1;
@@ -22,7 +23,10 @@ import { getEpochCacheConfigEnvVars } from './config.js';
22
23
  l1constants;
23
24
  dateProvider;
24
25
  config;
25
- // eslint-disable-next-line aztec-custom/no-non-primitive-in-collections
26
+ /**
27
+ * Single map holding both resolved entries and in-flight promises.
28
+ * A `Promise` value means a fetch is in progress; concurrent callers await it.
29
+ */ // eslint-disable-next-line aztec-custom/no-non-primitive-in-collections
26
30
  cache;
27
31
  allValidators;
28
32
  lastValidatorRefresh;
@@ -99,6 +103,9 @@ import { getEpochCacheConfigEnvVars } from './config.js';
99
103
  isProposerPipeliningEnabled() {
100
104
  return this.enableProposerPipelining;
101
105
  }
106
+ pipeliningOffset() {
107
+ return this.enableProposerPipelining ? PROPOSER_PIPELINING_SLOT_OFFSET : 0;
108
+ }
102
109
  getSlotNow() {
103
110
  return this.getEpochAndSlotNow().slot;
104
111
  }
@@ -158,16 +165,7 @@ import { getEpochCacheConfigEnvVars } from './config.js';
158
165
  const [startSlot] = getSlotRangeForEpoch(epoch, this.l1constants);
159
166
  return this.getCommittee(startSlot);
160
167
  }
161
- /**
162
- * Returns whether the escape hatch is open for the given epoch.
163
- *
164
- * Uses the already-cached EpochCommitteeInfo when available. If not cached, it will fetch
165
- * the epoch committee info (which includes the escape hatch flag) and return it.
166
- */ async isEscapeHatchOpen(epoch) {
167
- const cached = this.cache.get(epoch);
168
- if (cached) {
169
- return cached.isEscapeHatchOpen;
170
- }
168
+ /** Returns whether the escape hatch is open for the given epoch. */ async isEscapeHatchOpen(epoch) {
171
169
  const info = await this.getCommitteeForEpoch(epoch);
172
170
  return info.isEscapeHatchOpen;
173
171
  }
@@ -181,26 +179,43 @@ import { getEpochCacheConfigEnvVars } from './config.js';
181
179
  return await this.isEscapeHatchOpen(epoch);
182
180
  }
183
181
  /**
184
- * Get the current validator set
185
- * @param nextSlot - If true, get the validator set for the next slot.
186
- * @returns The current validator set.
182
+ * Get the current validator set.
183
+ *
184
+ * Returns cached data if the entry is finalized or still fresh (queried less than one
185
+ * Ethereum slot ago). Stale non-finalized entries are re-queried, and concurrent callers
186
+ * coalesce on the same in-flight promise so the L1 query happens only once.
187
187
  */ async getCommittee(slot = 'now') {
188
188
  const { epoch, ts } = this.getEpochAndTimestamp(slot);
189
- if (this.cache.has(epoch)) {
190
- return this.cache.get(epoch);
189
+ const cached = this.cache.get(epoch);
190
+ // In-flight promise: another caller is already fetching this epoch — just await it.
191
+ if (cached instanceof Promise) {
192
+ return (await cached).data;
191
193
  }
192
- const epochData = await this.computeCommittee({
193
- epoch,
194
- ts
195
- });
196
- // If the committee size is 0 or undefined, then do not cache
197
- if (!epochData.committee || epochData.committee.length === 0) {
198
- return epochData;
194
+ // Resolved entry: return it if finalized or still fresh.
195
+ if (cached && (cached.finalized || !this.isStale(cached))) {
196
+ return cached.data;
197
+ }
198
+ // Stale non-finalized entry: do a lightweight refresh first (check block hash + finalized ts).
199
+ // Only fall back to a full re-fetch if the L1 block was reorged.
200
+ if (cached) {
201
+ const promise = this.refreshStaleEntry(cached, epoch, ts);
202
+ this.cache.set(epoch, promise);
203
+ try {
204
+ return (await promise).data;
205
+ } catch (err) {
206
+ this.cache.set(epoch, cached);
207
+ throw err;
208
+ }
209
+ }
210
+ // No entry at all: full fetch.
211
+ const promise = this.fetchAndCache(epoch, ts);
212
+ this.cache.set(epoch, promise);
213
+ try {
214
+ return (await promise).data;
215
+ } catch (err) {
216
+ this.cache.delete(epoch);
217
+ throw err;
199
218
  }
200
- this.cache.set(epoch, epochData);
201
- const toPurge = Array.from(this.cache.keys()).sort((a, b)=>Number(b - a)).slice(this.config.cacheSize);
202
- toPurge.forEach((key)=>this.cache.delete(key));
203
- return epochData;
204
219
  }
205
220
  getEpochAndTimestamp(slot = 'now') {
206
221
  if (slot === 'now') {
@@ -211,27 +226,121 @@ import { getEpochCacheConfigEnvVars } from './config.js';
211
226
  return this.getEpochAndSlotAtSlot(slot);
212
227
  }
213
228
  }
214
- async computeCommittee(when) {
215
- const { ts, epoch } = when;
216
- const [committee, seedBuffer, l1Timestamp, isEscapeHatchOpen] = await Promise.all([
229
+ /** Evicts oldest cache entries (resolved or in-flight) beyond cacheSize. */ purgeCache() {
230
+ if (this.cache.size <= this.config.cacheSize) {
231
+ return;
232
+ }
233
+ const toPurge = Array.from(this.cache.keys()).sort((a, b)=>Number(b - a)).slice(this.config.cacheSize);
234
+ toPurge.forEach((key)=>this.cache.delete(key));
235
+ }
236
+ /** Returns true if a non-finalized cache entry is older than one Ethereum slot. */ isStale(entry) {
237
+ const nowSeconds = BigInt(this.dateProvider.nowInSeconds());
238
+ return nowSeconds - entry.lastRefreshL1Timestamp >= BigInt(this.l1constants.ethereumSlotDuration);
239
+ }
240
+ /** Whether a cached epoch entry has been marked as finalized. Returns undefined if not cached or still in-flight. */ isFinalized(epoch) {
241
+ const entry = this.cache.get(epoch);
242
+ if (!entry || entry instanceof Promise) {
243
+ return undefined;
244
+ }
245
+ return entry.finalized;
246
+ }
247
+ /** Returns the latest L1 timestamp stored in the cached entry. Undefined if not cached or in-flight. */ getCachedLastRefreshL1Timestamp(epoch) {
248
+ const entry = this.cache.get(epoch);
249
+ if (!entry || entry instanceof Promise) {
250
+ return undefined;
251
+ }
252
+ return entry.lastRefreshL1Timestamp;
253
+ }
254
+ /** Computes the sampling timestamp for an epoch's committee data. */ getSamplingTimestamp(epoch) {
255
+ const { lagInEpochsForRandao, epochDuration, slotDuration } = this.l1constants;
256
+ const epochStartTs = getStartTimestampForEpoch(epoch, this.l1constants);
257
+ return epochStartTs - BigInt(lagInEpochsForRandao) * BigInt(epochDuration) * BigInt(slotDuration);
258
+ }
259
+ /**
260
+ * Lightweight refresh for a stale non-finalized entry. Queries only the block hash at
261
+ * the original block number and the finalized block timestamp — avoids the expensive
262
+ * getCommitteeAt and getSampleSeedAt calls on the rollup contract.
263
+ *
264
+ * If the block hash still matches (no L1 reorg), we keep the existing data and just
265
+ * update the provenance timestamp. If the finalized block has caught up, we promote the
266
+ * entry to finalized. If there was a reorg (hash mismatch), we fall back to a full fetch.
267
+ */ async refreshStaleEntry(stale, epoch, ts) {
268
+ const [blockAtOriginal, l1FinalizedBlock, latestBlock] = await Promise.all([
269
+ this.rollup.client.getBlock({
270
+ blockNumber: stale.lastQueryL1BlockNumber,
271
+ includeTransactions: false
272
+ }),
273
+ getFinalizedL1Block(this.rollup.client),
274
+ this.rollup.client.getBlock({
275
+ includeTransactions: false
276
+ })
277
+ ]);
278
+ if (blockAtOriginal.hash === stale.lastQueryL1BlockHash) {
279
+ // No reorg: the data is still valid. Check if we can now mark it as finalized.
280
+ const samplingTs = this.getSamplingTimestamp(epoch);
281
+ const finalized = !!(stale.data.committee && stale.data.committee.length > 0) && l1FinalizedBlock !== undefined && samplingTs <= l1FinalizedBlock.timestamp;
282
+ const refreshed = {
283
+ ...stale,
284
+ lastRefreshL1Timestamp: latestBlock.timestamp,
285
+ finalized
286
+ };
287
+ this.cache.set(epoch, refreshed);
288
+ return refreshed;
289
+ }
290
+ // Reorg detected: block hash mismatch. Do a full re-fetch.
291
+ // Pass the already-fetched block timestamps to avoid redundant queries.
292
+ this.log.warn(`L1 reorg detected for epoch ${epoch}: block ${stale.lastQueryL1BlockNumber} hash changed`, {
293
+ epoch,
294
+ expectedHash: stale.lastQueryL1BlockHash,
295
+ actualHash: blockAtOriginal.hash
296
+ });
297
+ return this.fetchAndCache(epoch, ts, {
298
+ latestBlock,
299
+ finalizedBlock: l1FinalizedBlock
300
+ });
301
+ }
302
+ /**
303
+ * Fetches committee data from L1, determines finalization status, and stores in the cache.
304
+ *
305
+ * Uses `lagInEpochsForRandao` (the binding constraint, always <= lagInEpochsForValidatorSet)
306
+ * and computes the sampling timestamp from the epoch start to match the L1 contract's logic.
307
+ *
308
+ * When called from refreshStaleEntry after a reorg, the latest and finalized blocks are
309
+ * passed in to avoid redundant L1 queries.
310
+ */ async fetchAndCache(epoch, ts, prefetched) {
311
+ const [committee, seedBuffer, latestBlock, finalizedBlock, isEscapeHatchOpen] = await Promise.all([
217
312
  this.rollup.getCommitteeAt(ts),
218
313
  this.rollup.getSampleSeedAt(ts),
219
- this.rollup.client.getBlock({
314
+ prefetched?.latestBlock ?? this.rollup.client.getBlock({
220
315
  includeTransactions: false
221
- }).then((b)=>b.timestamp),
316
+ }),
317
+ prefetched !== undefined ? prefetched.finalizedBlock : getFinalizedL1Block(this.rollup.client),
222
318
  this.rollup.isEscapeHatchOpen(epoch)
223
319
  ]);
224
- const { lagInEpochsForValidatorSet, epochDuration, slotDuration } = this.l1constants;
225
- const sub = BigInt(lagInEpochsForValidatorSet) * BigInt(epochDuration) * BigInt(slotDuration);
226
- if (ts - sub > l1Timestamp) {
227
- throw new Error(`Cannot query committee for future epoch ${epoch} with timestamp ${ts} (current L1 time is ${l1Timestamp}). Check your Ethereum node is synced.`);
320
+ const samplingTs = this.getSamplingTimestamp(epoch);
321
+ if (samplingTs > latestBlock.timestamp) {
322
+ throw new Error(`Cannot query committee for future epoch ${epoch}: ` + `sampling timestamp ${samplingTs} is beyond latest L1 block at ${latestBlock.timestamp}. ` + `Check your Ethereum node is synced.`);
228
323
  }
229
- return {
324
+ // Empty committees are never marked finalized so they always get re-queried after TTL.
325
+ // If L1 has no finalized block yet (devnet startup), entries stay unfinalized.
326
+ const hasCommittee = !!(committee && committee.length > 0);
327
+ const finalized = hasCommittee && finalizedBlock !== undefined && samplingTs <= finalizedBlock.timestamp;
328
+ const data = {
230
329
  committee,
231
330
  seed: seedBuffer.toBigInt(),
232
331
  epoch,
233
332
  isEscapeHatchOpen
234
333
  };
334
+ const entry = {
335
+ data,
336
+ lastQueryL1BlockNumber: latestBlock.number,
337
+ lastQueryL1BlockHash: latestBlock.hash,
338
+ lastRefreshL1Timestamp: latestBlock.timestamp,
339
+ finalized
340
+ };
341
+ this.cache.set(epoch, entry);
342
+ this.purgeCache();
343
+ return entry;
235
344
  }
236
345
  /**
237
346
  * Get the ABI encoding of the proposer index - see ValidatorSelectionLib.sol computeProposerIndex
@@ -62,6 +62,7 @@ export declare class TestEpochCache implements EpochCacheInterface {
62
62
  getEpochNow(): EpochNumber;
63
63
  getTargetEpoch(): EpochNumber;
64
64
  isProposerPipeliningEnabled(): boolean;
65
+ pipeliningOffset(): number;
65
66
  getEpochAndSlotNow(): EpochAndSlot & {
66
67
  nowMs: bigint;
67
68
  };
@@ -88,4 +89,4 @@ export declare class TestEpochCache implements EpochCacheInterface {
88
89
  isEscapeHatchOpen(_epoch: EpochNumber): Promise<boolean>;
89
90
  isEscapeHatchOpenAtSlot(_slot?: SlotTag): Promise<boolean>;
90
91
  }
91
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidGVzdF9lcG9jaF9jYWNoZS5kLnRzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL3Rlc3QvdGVzdF9lcG9jaF9jYWNoZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsV0FBVyxFQUFFLFVBQVUsRUFBRSxNQUFNLGlDQUFpQyxDQUFDO0FBQzFFLE9BQU8sRUFBRSxVQUFVLEVBQUUsTUFBTSwrQkFBK0IsQ0FBQztBQUMzRCxPQUFPLEtBQUssRUFBRSxpQkFBaUIsRUFBRSxNQUFNLDZCQUE2QixDQUFDO0FBR3JFLE9BQU8sRUFDTCxLQUFLLFlBQVksRUFDakIsS0FBSyxtQkFBbUIsRUFDeEIsS0FBSyxrQkFBa0IsRUFFdkIsS0FBSyxPQUFPLEVBQ2IsTUFBTSxtQkFBbUIsQ0FBQztBQWMzQjs7Ozs7O0dBTUc7QUFDSCxxQkFBYSxjQUFlLFlBQVcsbUJBQW1CO0lBQ3hELE9BQU8sQ0FBQyxTQUFTLENBQW9CO0lBQ3JDLE9BQU8sQ0FBQyxlQUFlLENBQXlCO0lBQ2hELE9BQU8sQ0FBQyxXQUFXLENBQTZCO0lBQ2hELE9BQU8sQ0FBQyxlQUFlLENBQWtCO0lBQ3pDLE9BQU8sQ0FBQyxJQUFJLENBQWM7SUFDMUIsT0FBTyxDQUFDLG9CQUFvQixDQUFvQjtJQUNoRCxPQUFPLENBQUMsV0FBVyxDQUFvQjtJQUN2QyxPQUFPLENBQUMseUJBQXlCLENBQVM7SUFFMUMsWUFBWSxXQUFXLEdBQUUsT0FBTyxDQUFDLGlCQUFpQixDQUFNLEVBRXZEO0lBRUQ7OztPQUdHO0lBQ0gsWUFBWSxDQUFDLFNBQVMsRUFBRSxVQUFVLEVBQUUsR0FBRyxJQUFJLENBRzFDO0lBRUQ7OztPQUdHO0lBQ0gsV0FBVyxDQUFDLFFBQVEsRUFBRSxVQUFVLEdBQUcsU0FBUyxHQUFHLElBQUksQ0FHbEQ7SUFFRDs7O09BR0c7SUFDSCxjQUFjLENBQUMsSUFBSSxFQUFFLFVBQVUsR0FBRyxJQUFJLENBR3JDO0lBRUQ7OztPQUdHO0lBQ0gsa0JBQWtCLENBQUMsSUFBSSxFQUFFLE9BQU8sR0FBRyxJQUFJLENBR3RDO0lBRUQ7OztPQUdHO0lBQ0gsT0FBTyxDQUFDLElBQUksRUFBRSxNQUFNLEdBQUcsSUFBSSxDQUcxQjtJQUVEOzs7T0FHRztJQUNILHVCQUF1QixDQUFDLFVBQVUsRUFBRSxVQUFVLEVBQUUsR0FBRyxJQUFJLENBR3REO0lBRUQ7OztPQUdHO0lBQ0gsY0FBYyxDQUFDLFNBQVMsRUFBRSxPQUFPLENBQUMsaUJBQWlCLENBQUMsR0FBRyxJQUFJLENBRzFEO0lBRUQsY0FBYyxJQUFJLGlCQUFpQixDQUVsQztJQUVELDRCQUE0QixDQUFDLE9BQU8sRUFBRSxPQUFPLEdBQUcsSUFBSSxDQUVuRDtJQUVELFlBQVksQ0FBQyxLQUFLLENBQUMsRUFBRSxPQUFPLEdBQUcsT0FBTyxDQUFDLGtCQUFrQixDQUFDLENBUXpEO0lBRUQsVUFBVSxJQUFJLFVBQVUsQ0FFdkI7SUFFRCxhQUFhLElBQUksVUFBVSxDQUkxQjtJQUVELFdBQVcsSUFBSSxXQUFXLENBRXpCO0lBRUQsY0FBYyxJQUFJLFdBQVcsQ0FFNUI7SUFFRCwyQkFBMkIsSUFBSSxPQUFPLENBRXJDO0lBRUQsa0JBQWtCLElBQUksWUFBWSxHQUFHO1FBQUUsS0FBSyxFQUFFLE1BQU0sQ0FBQTtLQUFFLENBU3JEO0lBRUQsMkJBQTJCLElBQUksWUFBWSxHQUFHO1FBQUUsVUFBVSxFQUFFLE1BQU0sQ0FBQTtLQUFFLENBWW5FO0lBRUQsaUNBQWlDLElBQUksWUFBWSxHQUFHO1FBQUUsVUFBVSxFQUFFLE1BQU0sQ0FBQTtLQUFFLENBS3pFO0lBRUQsd0JBQXdCLENBQUMsS0FBSyxFQUFFLFdBQVcsRUFBRSxJQUFJLEVBQUUsVUFBVSxFQUFFLElBQUksRUFBRSxNQUFNLEdBQUcsS0FBSyxNQUFNLEVBQUUsQ0FHMUY7SUFFRCxvQkFBb0IsQ0FBQyxJQUFJLEVBQUUsVUFBVSxFQUFFLE1BQU0sRUFBRSxXQUFXLEVBQUUsS0FBSyxFQUFFLE1BQU0sRUFBRSxJQUFJLEVBQUUsTUFBTSxHQUFHLE1BQU0sQ0FLL0Y7SUFFRCxxQkFBcUIsSUFBSTtRQUFFLFdBQVcsRUFBRSxVQUFVLENBQUM7UUFBQyxRQUFRLEVBQUUsVUFBVSxDQUFBO0tBQUUsQ0FRekU7SUFFRCxvQkFBb0IsSUFBSTtRQUFFLFVBQVUsRUFBRSxVQUFVLENBQUM7UUFBQyxRQUFRLEVBQUUsVUFBVSxDQUFBO0tBQUUsQ0FRdkU7SUFFRCxnQ0FBZ0MsQ0FBQyxLQUFLLEVBQUUsVUFBVSxHQUFHLE9BQU8sQ0FBQyxVQUFVLEdBQUcsU0FBUyxDQUFDLENBRW5GO0lBRUQsdUJBQXVCLElBQUksT0FBTyxDQUFDLFVBQVUsRUFBRSxDQUFDLENBRS9DO0lBRUQsYUFBYSxDQUFDLEtBQUssRUFBRSxPQUFPLEVBQUUsU0FBUyxFQUFFLFVBQVUsR0FBRyxPQUFPLENBQUMsT0FBTyxDQUFDLENBRXJFO0lBRUQsaUJBQWlCLENBQUMsS0FBSyxFQUFFLE9BQU8sRUFBRSxVQUFVLEVBQUUsVUFBVSxFQUFFLEdBQUcsT0FBTyxDQUFDLFVBQVUsRUFBRSxDQUFDLENBR2pGO0lBRUQsaUJBQWlCLENBQUMsTUFBTSxFQUFFLFdBQVcsR0FBRyxPQUFPLENBQUMsT0FBTyxDQUFDLENBRXZEO0lBRUQsdUJBQXVCLENBQUMsS0FBSyxDQUFDLEVBQUUsT0FBTyxHQUFHLE9BQU8sQ0FBQyxPQUFPLENBQUMsQ0FFekQ7Q0FDRiJ9
92
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidGVzdF9lcG9jaF9jYWNoZS5kLnRzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL3Rlc3QvdGVzdF9lcG9jaF9jYWNoZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsV0FBVyxFQUFFLFVBQVUsRUFBRSxNQUFNLGlDQUFpQyxDQUFDO0FBQzFFLE9BQU8sRUFBRSxVQUFVLEVBQUUsTUFBTSwrQkFBK0IsQ0FBQztBQUMzRCxPQUFPLEtBQUssRUFBRSxpQkFBaUIsRUFBRSxNQUFNLDZCQUE2QixDQUFDO0FBR3JFLE9BQU8sRUFDTCxLQUFLLFlBQVksRUFDakIsS0FBSyxtQkFBbUIsRUFDeEIsS0FBSyxrQkFBa0IsRUFFdkIsS0FBSyxPQUFPLEVBQ2IsTUFBTSxtQkFBbUIsQ0FBQztBQWMzQjs7Ozs7O0dBTUc7QUFDSCxxQkFBYSxjQUFlLFlBQVcsbUJBQW1CO0lBQ3hELE9BQU8sQ0FBQyxTQUFTLENBQW9CO0lBQ3JDLE9BQU8sQ0FBQyxlQUFlLENBQXlCO0lBQ2hELE9BQU8sQ0FBQyxXQUFXLENBQTZCO0lBQ2hELE9BQU8sQ0FBQyxlQUFlLENBQWtCO0lBQ3pDLE9BQU8sQ0FBQyxJQUFJLENBQWM7SUFDMUIsT0FBTyxDQUFDLG9CQUFvQixDQUFvQjtJQUNoRCxPQUFPLENBQUMsV0FBVyxDQUFvQjtJQUN2QyxPQUFPLENBQUMseUJBQXlCLENBQVM7SUFFMUMsWUFBWSxXQUFXLEdBQUUsT0FBTyxDQUFDLGlCQUFpQixDQUFNLEVBRXZEO0lBRUQ7OztPQUdHO0lBQ0gsWUFBWSxDQUFDLFNBQVMsRUFBRSxVQUFVLEVBQUUsR0FBRyxJQUFJLENBRzFDO0lBRUQ7OztPQUdHO0lBQ0gsV0FBVyxDQUFDLFFBQVEsRUFBRSxVQUFVLEdBQUcsU0FBUyxHQUFHLElBQUksQ0FHbEQ7SUFFRDs7O09BR0c7SUFDSCxjQUFjLENBQUMsSUFBSSxFQUFFLFVBQVUsR0FBRyxJQUFJLENBR3JDO0lBRUQ7OztPQUdHO0lBQ0gsa0JBQWtC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
@@ -1 +1 @@
1
- {"version":3,"file":"test_epoch_cache.d.ts","sourceRoot":"","sources":["../../src/test/test_epoch_cache.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"test_epoch_cache.d.ts","sourceRoot":"","sources":["../../src/test/test_epoch_cache.ts"],"names":[],"mappings":"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"}
@@ -114,6 +114,9 @@ import { PROPOSER_PIPELINING_SLOT_OFFSET } from '../epoch_cache.js';
114
114
  isProposerPipeliningEnabled() {
115
115
  return this.proposerPipeliningEnabled;
116
116
  }
117
+ pipeliningOffset() {
118
+ return this.proposerPipeliningEnabled ? PROPOSER_PIPELINING_SLOT_OFFSET : 0;
119
+ }
117
120
  getEpochAndSlotNow() {
118
121
  const epochNow = getEpochAtSlot(this.currentSlot, this.l1Constants);
119
122
  const ts = getTimestampRangeForEpoch(epochNow, this.l1Constants)[0];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aztec/epoch-cache",
3
- "version": "0.0.1-commit.f5d02921e",
3
+ "version": "0.0.1-commit.f7ea82942",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dest/index.js",
@@ -26,10 +26,10 @@
26
26
  "../package.common.json"
27
27
  ],
28
28
  "dependencies": {
29
- "@aztec/ethereum": "0.0.1-commit.f5d02921e",
30
- "@aztec/foundation": "0.0.1-commit.f5d02921e",
31
- "@aztec/l1-artifacts": "0.0.1-commit.f5d02921e",
32
- "@aztec/stdlib": "0.0.1-commit.f5d02921e",
29
+ "@aztec/ethereum": "0.0.1-commit.f7ea82942",
30
+ "@aztec/foundation": "0.0.1-commit.f7ea82942",
31
+ "@aztec/l1-artifacts": "0.0.1-commit.f7ea82942",
32
+ "@aztec/stdlib": "0.0.1-commit.f7ea82942",
33
33
  "dotenv": "^16.0.3",
34
34
  "get-port": "^7.1.0",
35
35
  "jest-mock-extended": "^4.0.0",
@@ -1,6 +1,7 @@
1
1
  import { createEthereumChain } from '@aztec/ethereum/chain';
2
2
  import { makeL1HttpTransport } from '@aztec/ethereum/client';
3
3
  import { NoCommitteeError, RollupContract } from '@aztec/ethereum/contracts';
4
+ import { getFinalizedL1Block } from '@aztec/ethereum/queries';
4
5
  import { EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
5
6
  import { EthAddress } from '@aztec/foundation/eth-address';
6
7
  import { type Logger, createLogger } from '@aztec/foundation/log';
@@ -13,6 +14,7 @@ import {
13
14
  getSlotAtNextL1Block,
14
15
  getSlotAtTimestamp,
15
16
  getSlotRangeForEpoch,
17
+ getStartTimestampForEpoch,
16
18
  getTimestampForSlot,
17
19
  } from '@aztec/stdlib/epoch-helpers';
18
20
 
@@ -40,6 +42,22 @@ export type EpochCommitteeInfo = {
40
42
 
41
43
  export type SlotTag = 'now' | 'next' | SlotNumber;
42
44
 
45
+ /** Minimal L1 block info used for cache provenance. */
46
+ type L1BlockInfo = { number: bigint; hash: `0x${string}`; timestamp: bigint };
47
+
48
+ /** Resolved cache entry with L1 provenance metadata. */
49
+ type CachedEpochEntry = {
50
+ data: EpochCommitteeInfo;
51
+ /** L1 block number at which the committee data was originally queried. */
52
+ lastQueryL1BlockNumber: bigint;
53
+ /** L1 block hash at which the committee data was originally queried. Used to detect reorgs. */
54
+ lastQueryL1BlockHash: `0x${string}`;
55
+ /** Latest L1 block timestamp at the time of the most recent refresh (full fetch or lightweight check). */
56
+ lastRefreshL1Timestamp: bigint;
57
+ /** Whether the epoch's sampling data falls within finalized L1 history. */
58
+ finalized: boolean;
59
+ };
60
+
43
61
  export interface EpochCacheInterface {
44
62
  getCommittee(slot: SlotTag | undefined): Promise<EpochCommitteeInfo>;
45
63
  getSlotNow(): SlotNumber;
@@ -51,6 +69,7 @@ export interface EpochCacheInterface {
51
69
  /** Returns epoch/slot info for the next L1 slot with pipeline offset applied. */
52
70
  getTargetEpochAndSlotInNextL1Slot(): EpochAndSlot & { nowSeconds: bigint };
53
71
  isProposerPipeliningEnabled(): boolean;
72
+ pipeliningOffset(): number;
54
73
  isEscapeHatchOpen(epoch: EpochNumber): Promise<boolean>;
55
74
  isEscapeHatchOpenAtSlot(slot: SlotTag): Promise<boolean>;
56
75
  getProposerIndexEncoding(epoch: EpochNumber, slot: SlotNumber, seed: bigint): `0x${string}`;
@@ -74,8 +93,12 @@ export interface EpochCacheInterface {
74
93
  * Note: This class is very dependent on the system clock being in sync.
75
94
  */
76
95
  export class EpochCache implements EpochCacheInterface {
96
+ /**
97
+ * Single map holding both resolved entries and in-flight promises.
98
+ * A `Promise` value means a fetch is in progress; concurrent callers await it.
99
+ */
77
100
  // eslint-disable-next-line aztec-custom/no-non-primitive-in-collections
78
- protected cache: Map<EpochNumber, EpochCommitteeInfo> = new Map();
101
+ protected cache: Map<EpochNumber, CachedEpochEntry | Promise<CachedEpochEntry>> = new Map();
79
102
  private allValidators: Set<string> = new Set();
80
103
  private lastValidatorRefresh = 0;
81
104
  private readonly log: Logger = createLogger('epoch-cache');
@@ -169,6 +192,10 @@ export class EpochCache implements EpochCacheInterface {
169
192
  return this.enableProposerPipelining;
170
193
  }
171
194
 
195
+ public pipeliningOffset(): number {
196
+ return this.enableProposerPipelining ? PROPOSER_PIPELINING_SLOT_OFFSET : 0;
197
+ }
198
+
172
199
  public getSlotNow(): SlotNumber {
173
200
  return this.getEpochAndSlotNow().slot;
174
201
  }
@@ -229,17 +256,8 @@ export class EpochCache implements EpochCacheInterface {
229
256
  return this.getCommittee(startSlot);
230
257
  }
231
258
 
232
- /**
233
- * Returns whether the escape hatch is open for the given epoch.
234
- *
235
- * Uses the already-cached EpochCommitteeInfo when available. If not cached, it will fetch
236
- * the epoch committee info (which includes the escape hatch flag) and return it.
237
- */
259
+ /** Returns whether the escape hatch is open for the given epoch. */
238
260
  public async isEscapeHatchOpen(epoch: EpochNumber): Promise<boolean> {
239
- const cached = this.cache.get(epoch);
240
- if (cached) {
241
- return cached.isEscapeHatchOpen;
242
- }
243
261
  const info = await this.getCommitteeForEpoch(epoch);
244
262
  return info.isEscapeHatchOpen;
245
263
  }
@@ -262,30 +280,49 @@ export class EpochCache implements EpochCacheInterface {
262
280
  }
263
281
 
264
282
  /**
265
- * Get the current validator set
266
- * @param nextSlot - If true, get the validator set for the next slot.
267
- * @returns The current validator set.
283
+ * Get the current validator set.
284
+ *
285
+ * Returns cached data if the entry is finalized or still fresh (queried less than one
286
+ * Ethereum slot ago). Stale non-finalized entries are re-queried, and concurrent callers
287
+ * coalesce on the same in-flight promise so the L1 query happens only once.
268
288
  */
269
289
  public async getCommittee(slot: SlotTag = 'now'): Promise<EpochCommitteeInfo> {
270
290
  const { epoch, ts } = this.getEpochAndTimestamp(slot);
271
291
 
272
- if (this.cache.has(epoch)) {
273
- return this.cache.get(epoch)!;
292
+ const cached = this.cache.get(epoch);
293
+
294
+ // In-flight promise: another caller is already fetching this epoch — just await it.
295
+ if (cached instanceof Promise) {
296
+ return (await cached).data;
274
297
  }
275
298
 
276
- const epochData = await this.computeCommittee({ epoch, ts });
277
- // If the committee size is 0 or undefined, then do not cache
278
- if (!epochData.committee || epochData.committee.length === 0) {
279
- return epochData;
299
+ // Resolved entry: return it if finalized or still fresh.
300
+ if (cached && (cached.finalized || !this.isStale(cached))) {
301
+ return cached.data;
280
302
  }
281
- this.cache.set(epoch, epochData);
282
303
 
283
- const toPurge = Array.from(this.cache.keys())
284
- .sort((a, b) => Number(b - a))
285
- .slice(this.config.cacheSize);
286
- toPurge.forEach(key => this.cache.delete(key));
304
+ // Stale non-finalized entry: do a lightweight refresh first (check block hash + finalized ts).
305
+ // Only fall back to a full re-fetch if the L1 block was reorged.
306
+ if (cached) {
307
+ const promise = this.refreshStaleEntry(cached, epoch, ts);
308
+ this.cache.set(epoch, promise);
309
+ try {
310
+ return (await promise).data;
311
+ } catch (err) {
312
+ this.cache.set(epoch, cached);
313
+ throw err;
314
+ }
315
+ }
287
316
 
288
- return epochData;
317
+ // No entry at all: full fetch.
318
+ const promise = this.fetchAndCache(epoch, ts);
319
+ this.cache.set(epoch, promise);
320
+ try {
321
+ return (await promise).data;
322
+ } catch (err) {
323
+ this.cache.delete(epoch);
324
+ throw err;
325
+ }
289
326
  }
290
327
 
291
328
  private getEpochAndTimestamp(slot: SlotTag = 'now'): { epoch: EpochNumber; ts: bigint } {
@@ -298,22 +335,140 @@ export class EpochCache implements EpochCacheInterface {
298
335
  }
299
336
  }
300
337
 
301
- private async computeCommittee(when: { epoch: EpochNumber; ts: bigint }): Promise<EpochCommitteeInfo> {
302
- const { ts, epoch } = when;
303
- const [committee, seedBuffer, l1Timestamp, isEscapeHatchOpen] = await Promise.all([
338
+ /** Evicts oldest cache entries (resolved or in-flight) beyond cacheSize. */
339
+ private purgeCache(): void {
340
+ if (this.cache.size <= this.config.cacheSize) {
341
+ return;
342
+ }
343
+ const toPurge = Array.from(this.cache.keys())
344
+ .sort((a, b) => Number(b - a))
345
+ .slice(this.config.cacheSize);
346
+ toPurge.forEach(key => this.cache.delete(key));
347
+ }
348
+
349
+ /** Returns true if a non-finalized cache entry is older than one Ethereum slot. */
350
+ private isStale(entry: CachedEpochEntry): boolean {
351
+ const nowSeconds = BigInt(this.dateProvider.nowInSeconds());
352
+ return nowSeconds - entry.lastRefreshL1Timestamp >= BigInt(this.l1constants.ethereumSlotDuration);
353
+ }
354
+
355
+ /** Whether a cached epoch entry has been marked as finalized. Returns undefined if not cached or still in-flight. */
356
+ public isFinalized(epoch: EpochNumber): boolean | undefined {
357
+ const entry = this.cache.get(epoch);
358
+ if (!entry || entry instanceof Promise) {
359
+ return undefined;
360
+ }
361
+ return entry.finalized;
362
+ }
363
+
364
+ /** Returns the latest L1 timestamp stored in the cached entry. Undefined if not cached or in-flight. */
365
+ public getCachedLastRefreshL1Timestamp(epoch: EpochNumber): bigint | undefined {
366
+ const entry = this.cache.get(epoch);
367
+ if (!entry || entry instanceof Promise) {
368
+ return undefined;
369
+ }
370
+ return entry.lastRefreshL1Timestamp;
371
+ }
372
+
373
+ /** Computes the sampling timestamp for an epoch's committee data. */
374
+ private getSamplingTimestamp(epoch: EpochNumber): bigint {
375
+ const { lagInEpochsForRandao, epochDuration, slotDuration } = this.l1constants;
376
+ const epochStartTs = getStartTimestampForEpoch(epoch, this.l1constants);
377
+ return epochStartTs - BigInt(lagInEpochsForRandao) * BigInt(epochDuration) * BigInt(slotDuration);
378
+ }
379
+
380
+ /**
381
+ * Lightweight refresh for a stale non-finalized entry. Queries only the block hash at
382
+ * the original block number and the finalized block timestamp — avoids the expensive
383
+ * getCommitteeAt and getSampleSeedAt calls on the rollup contract.
384
+ *
385
+ * If the block hash still matches (no L1 reorg), we keep the existing data and just
386
+ * update the provenance timestamp. If the finalized block has caught up, we promote the
387
+ * entry to finalized. If there was a reorg (hash mismatch), we fall back to a full fetch.
388
+ */
389
+ private async refreshStaleEntry(stale: CachedEpochEntry, epoch: EpochNumber, ts: bigint): Promise<CachedEpochEntry> {
390
+ const [blockAtOriginal, l1FinalizedBlock, latestBlock] = await Promise.all([
391
+ this.rollup.client.getBlock({ blockNumber: stale.lastQueryL1BlockNumber, includeTransactions: false }),
392
+ getFinalizedL1Block(this.rollup.client),
393
+ this.rollup.client.getBlock({ includeTransactions: false }),
394
+ ]);
395
+
396
+ if (blockAtOriginal.hash === stale.lastQueryL1BlockHash) {
397
+ // No reorg: the data is still valid. Check if we can now mark it as finalized.
398
+ const samplingTs = this.getSamplingTimestamp(epoch);
399
+ const finalized =
400
+ !!(stale.data.committee && stale.data.committee.length > 0) &&
401
+ l1FinalizedBlock !== undefined &&
402
+ samplingTs <= l1FinalizedBlock.timestamp;
403
+
404
+ const refreshed: CachedEpochEntry = {
405
+ ...stale,
406
+ lastRefreshL1Timestamp: latestBlock.timestamp,
407
+ finalized,
408
+ };
409
+ this.cache.set(epoch, refreshed);
410
+ return refreshed;
411
+ }
412
+
413
+ // Reorg detected: block hash mismatch. Do a full re-fetch.
414
+ // Pass the already-fetched block timestamps to avoid redundant queries.
415
+ this.log.warn(`L1 reorg detected for epoch ${epoch}: block ${stale.lastQueryL1BlockNumber} hash changed`, {
416
+ epoch,
417
+ expectedHash: stale.lastQueryL1BlockHash,
418
+ actualHash: blockAtOriginal.hash,
419
+ });
420
+ return this.fetchAndCache(epoch, ts, { latestBlock, finalizedBlock: l1FinalizedBlock });
421
+ }
422
+
423
+ /**
424
+ * Fetches committee data from L1, determines finalization status, and stores in the cache.
425
+ *
426
+ * Uses `lagInEpochsForRandao` (the binding constraint, always <= lagInEpochsForValidatorSet)
427
+ * and computes the sampling timestamp from the epoch start to match the L1 contract's logic.
428
+ *
429
+ * When called from refreshStaleEntry after a reorg, the latest and finalized blocks are
430
+ * passed in to avoid redundant L1 queries.
431
+ */
432
+ private async fetchAndCache(
433
+ epoch: EpochNumber,
434
+ ts: bigint,
435
+ prefetched?: { latestBlock: L1BlockInfo; finalizedBlock: { timestamp: bigint } | undefined },
436
+ ): Promise<CachedEpochEntry> {
437
+ const [committee, seedBuffer, latestBlock, finalizedBlock, isEscapeHatchOpen] = await Promise.all([
304
438
  this.rollup.getCommitteeAt(ts),
305
439
  this.rollup.getSampleSeedAt(ts),
306
- this.rollup.client.getBlock({ includeTransactions: false }).then(b => b.timestamp),
440
+ prefetched?.latestBlock ?? this.rollup.client.getBlock({ includeTransactions: false }),
441
+ prefetched !== undefined ? prefetched.finalizedBlock : getFinalizedL1Block(this.rollup.client),
307
442
  this.rollup.isEscapeHatchOpen(epoch),
308
443
  ]);
309
- const { lagInEpochsForValidatorSet, epochDuration, slotDuration } = this.l1constants;
310
- const sub = BigInt(lagInEpochsForValidatorSet) * BigInt(epochDuration) * BigInt(slotDuration);
311
- if (ts - sub > l1Timestamp) {
444
+
445
+ const samplingTs = this.getSamplingTimestamp(epoch);
446
+
447
+ if (samplingTs > latestBlock.timestamp) {
312
448
  throw new Error(
313
- `Cannot query committee for future epoch ${epoch} with timestamp ${ts} (current L1 time is ${l1Timestamp}). Check your Ethereum node is synced.`,
449
+ `Cannot query committee for future epoch ${epoch}: ` +
450
+ `sampling timestamp ${samplingTs} is beyond latest L1 block at ${latestBlock.timestamp}. ` +
451
+ `Check your Ethereum node is synced.`,
314
452
  );
315
453
  }
316
- return { committee, seed: seedBuffer.toBigInt(), epoch, isEscapeHatchOpen };
454
+
455
+ // Empty committees are never marked finalized so they always get re-queried after TTL.
456
+ // If L1 has no finalized block yet (devnet startup), entries stay unfinalized.
457
+ const hasCommittee = !!(committee && committee.length > 0);
458
+ const finalized = hasCommittee && finalizedBlock !== undefined && samplingTs <= finalizedBlock.timestamp;
459
+ const data: EpochCommitteeInfo = { committee, seed: seedBuffer.toBigInt(), epoch, isEscapeHatchOpen };
460
+ const entry: CachedEpochEntry = {
461
+ data,
462
+ lastQueryL1BlockNumber: latestBlock.number!,
463
+ lastQueryL1BlockHash: latestBlock.hash!,
464
+ lastRefreshL1Timestamp: latestBlock.timestamp,
465
+ finalized,
466
+ };
467
+
468
+ this.cache.set(epoch, entry);
469
+ this.purgeCache();
470
+
471
+ return entry;
317
472
  }
318
473
 
319
474
  /**
@@ -147,6 +147,10 @@ export class TestEpochCache implements EpochCacheInterface {
147
147
  return this.proposerPipeliningEnabled;
148
148
  }
149
149
 
150
+ pipeliningOffset(): number {
151
+ return this.proposerPipeliningEnabled ? PROPOSER_PIPELINING_SLOT_OFFSET : 0;
152
+ }
153
+
150
154
  getEpochAndSlotNow(): EpochAndSlot & { nowMs: bigint } {
151
155
  const epochNow = getEpochAtSlot(this.currentSlot, this.l1Constants);
152
156
  const ts = getTimestampRangeForEpoch(epochNow, this.l1Constants)[0];