@aztec/epoch-cache 0.0.1-commit.f5d02921e → 0.0.1-commit.f650c0a5c
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 +211 -0
- package/dest/epoch_cache.d.ts +39 -12
- package/dest/epoch_cache.d.ts.map +1 -1
- package/dest/epoch_cache.js +151 -38
- package/dest/test/test_epoch_cache.d.ts +2 -1
- package/dest/test/test_epoch_cache.d.ts.map +1 -1
- package/dest/test/test_epoch_cache.js +3 -0
- package/package.json +5 -5
- package/src/epoch_cache.ts +186 -35
- package/src/test/test_epoch_cache.ts +4 -0
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 |
|
package/dest/epoch_cache.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
*
|
|
127
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
193
|
+
export {};
|
|
194
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZXBvY2hfY2FjaGUuZC50cyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy9lcG9jaF9jYWNoZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFFQSxPQUFPLEVBQW9CLGNBQWMsRUFBRSxNQUFNLDJCQUEyQixDQUFDO0FBQzdFLE9BQU8sRUFBRSxXQUFXLEVBQUUsVUFBVSxFQUFFLE1BQU0saUNBQWlDLENBQUM7QUFDMUUsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLCtCQUErQixDQUFDO0FBRTNELE9BQU8sRUFBRSxZQUFZLEVBQUUsTUFBTSx5QkFBeUIsQ0FBQztBQUN2RCxPQUFPLEVBQ0wsS0FBSyxpQkFBaUIsRUFTdkIsTUFBTSw2QkFBNkIsQ0FBQztBQUlyQyxPQUFPLEVBQUUsS0FBSyxnQkFBZ0IsRUFBOEIsTUFBTSxhQUFhLENBQUM7QUFFaEYsK0VBQStFO0FBQy9FLGVBQU8sTUFBTSwrQkFBK0IsSUFBSSxDQUFDO0FBRWpELHdEQUF3RDtBQUN4RCxNQUFNLE1BQU0sWUFBWSxHQUFHO0lBQ3pCLElBQUksRUFBRSxVQUFVLENBQUM7SUFDakIsS0FBSyxFQUFFLFdBQVcsQ0FBQztJQUNuQixFQUFFLEVBQUUsTUFBTSxDQUFDO0NBQ1osQ0FBQztBQUVGLE1BQU0sTUFBTSxrQkFBa0IsR0FBRztJQUMvQixTQUFTLEVBQUUsVUFBVSxFQUFFLEdBQUcsU0FBUyxDQUFDO0lBQ3BDLElBQUksRUFBRSxNQUFNLENBQUM7SUFDYixLQUFLLEVBQUUsV0FBVyxDQUFDO0lBQ25CLCtEQUErRDtJQUMvRCxpQkFBaUIsRUFBRSxPQUFPLENBQUM7Q0FDNUIsQ0FBQztBQUVGLE1BQU0sTUFBTSxPQUFPLEdBQUcsS0FBSyxHQUFHLE1BQU0sR0FBRyxVQUFVLENBQUM7QUFLbEQsd0RBQXdEO0FBQ3hELEtBQUssZ0JBQWdCLEdBQUc7SUFDdEIsSUFBSSxFQUFFLGtCQUFrQixDQUFDO0lBQ3pCLDBFQUEwRTtJQUMxRSxzQkFBc0IsRUFBRSxNQUFNLENBQUM7SUFDL0IsK0ZBQStGO0lBQy9GLG9CQUFvQixFQUFFLEtBQUssTUFBTSxFQUFFLENBQUM7SUFDcEMsMEdBQTBHO0lBQzFHLHNCQUFzQixFQUFFLE1BQU0sQ0FBQztJQUMvQiwyRUFBMkU7SUFDM0UsU0FBUyxFQUFFLE9BQU8sQ0FBQztDQUNwQixDQUFDO0FBRUYsTUFBTSxXQUFXLG1CQUFtQjtJQUNsQyxZQUFZLENBQUMsSUFBSSxFQUFFLE9BQU8sR0FBRyxTQUFTLEdBQUcsT0FBTyxDQUFDLGtCQUFrQixDQUFDLENBQUM7SUFDckUsVUFBVSxJQUFJLFVBQVUsQ0FBQztJQUN6QixhQUFhLElBQUksVUFBVSxDQUFDO0lBQzVCLFdBQVcsSUFBSSxXQUFXLENBQUM7SUFDM0IsY0FBYyxJQUFJLFdBQVcsQ0FBQztJQUM5QixrQkFBa0IsSUFBSSxZQUFZLEdBQUc7UUFBRSxLQUFLLEVBQUUsTUFBTSxDQUFBO0tBQUUsQ0FBQztJQUN2RCwyQkFBMkIsSUFBSSxZQUFZLEdBQUc7UUFBRSxVQUFVLEVBQUUsTUFBTSxDQUFBO0tBQUUsQ0FBQztJQUNyRSxpRkFBaUY7SUFDakYsaUNBQWlDLElBQUksWUFBWSxHQUFHO1FBQUUsVUFBVSxFQUFFLE1BQU0sQ0FBQTtLQUFFLENBQUM7SUFDM0UsMkJBQTJCLElBQUksT0FBTyxDQUFDO0lBQ3ZDLGdCQUFnQixJQUFJLE1BQU0sQ0FBQztJQUMzQixpQkFBaUIsQ0FBQyxLQUFLLEVBQUUsV0FBVyxHQUFHLE9BQU8sQ0FBQyxPQUFPLENBQUMsQ0FBQztJQUN4RCx1QkFBdUIsQ0FBQyxJQUFJLEVBQUUsT0FBTyxHQUFHLE9BQU8sQ0FBQyxPQUFPLENBQUMsQ0FBQztJQUN6RCx3QkFBd0IsQ0FBQyxLQUFLLEVBQUUsV0FBVyxFQUFFLElBQUksRUFBRSxVQUFVLEVBQUUsSUFBSSxFQUFFLE1BQU0sR0FBRyxLQUFLLE1BQU0sRUFBRSxDQUFDO0lBQzVGLG9CQUFvQixDQUFDLElBQUksRUFBRSxVQUFVLEVBQUUsS0FBSyxFQUFFLFdBQVcsRUFBRSxJQUFJLEVBQUUsTUFBTSxFQUFFLElBQUksRUFBRSxNQUFNLEdBQUcsTUFBTSxDQUFDO0lBQy9GLHFCQUFxQixJQUFJO1FBQUUsV0FBVyxFQUFFLFVBQVUsQ0FBQztRQUFDLFFBQVEsRUFBRSxVQUFVLENBQUE7S0FBRSxDQUFDO0lBQzNFLG9CQUFvQixJQUFJO1FBQUUsVUFBVSxFQUFFLFVBQVUsQ0FBQztRQUFDLFFBQVEsRUFBRSxVQUFVLENBQUE7S0FBRSxDQUFDO0lBQ3pFLGdDQUFnQyxDQUFDLElBQUksRUFBRSxVQUFVLEdBQUcsT0FBTyxDQUFDLFVBQVUsR0FBRyxTQUFTLENBQUMsQ0FBQztJQUNwRix1QkFBdUIsSUFBSSxPQUFPLENBQUMsVUFBVSxFQUFFLENBQUMsQ0FBQztJQUNqRCxhQUFhLENBQUMsSUFBSSxFQUFFLE9BQU8sRUFBRSxTQUFTLEVBQUUsVUFBVSxHQUFHLE9BQU8sQ0FBQyxPQUFPLENBQUMsQ0FBQztJQUN0RSxpQkFBaUIsQ0FBQyxJQUFJLEVBQUUsT0FBTyxFQUFFLFVBQVUsRUFBRSxVQUFVLEVBQUUsR0FBRyxPQUFPLENBQUMsVUFBVSxFQUFFLENBQUMsQ0FBQztJQUNsRixjQUFjLElBQUksaUJBQWlCLENBQUM7Q0FDckM7QUFFRDs7Ozs7Ozs7R0FRRztBQUNILHFCQUFhLFVBQVcsWUFBVyxtQkFBbUI7SUFjbEQsT0FBTyxDQUFDLE1BQU07SUFDZCxPQUFPLENBQUMsUUFBUSxDQUFDLFdBQVc7SUFJNUIsT0FBTyxDQUFDLFFBQVEsQ0FBQyxZQUFZO0lBQzdCLFNBQVMsQ0FBQyxRQUFRLENBQUMsTUFBTTs7Ozs7SUFuQjNCOzs7T0FHRztJQUVILFNBQVMsQ0FBQyxLQUFLLEVBQUUsR0FBRyxDQUFDLFdBQVcsRUFBRSxnQkFBZ0IsR0FBRyxPQUFPLENBQUMsZ0JBQWdCLENBQUMsQ0FBQyxDQUFhO0lBQzVGLE9BQU8sQ0FBQyxhQUFhLENBQTBCO0lBQy9DLE9BQU8sQ0FBQyxvQkFBb0IsQ0FBSztJQUNqQyxPQUFPLENBQUMsUUFBUSxDQUFDLEdBQUcsQ0FBdUM7SUFFM0QsU0FBUyxDQUFDLHdCQUF3QixFQUFFLE9BQU8sQ0FBQztJQUU1QyxZQUNVLE1BQU0sRUFBRSxjQUFjLEVBQ2IsV0FBVyxFQUFFLGlCQUFpQixHQUFHO1FBQ2hELDBCQUEwQixFQUFFLE1BQU0sQ0FBQztRQUNuQyxvQkFBb0IsRUFBRSxNQUFNLENBQUM7S0FDOUIsRUFDZ0IsWUFBWSxHQUFFLFlBQWlDLEVBQzdDLE1BQU07Ozs7S0FBMEYsRUFPcEg7SUFFRCxPQUFhLE1BQU0sQ0FDakIsZUFBZSxFQUFFLFVBQVUsR0FBRyxjQUFjLEVBQzVDLE1BQU0sQ0FBQyxFQUFFLGdCQUFnQixFQUN6QixJQUFJLEdBQUU7UUFBRSxZQUFZLENBQUMsRUFBRSxZQUFZLENBQUE7S0FBTyx1QkEwRDNDO0lBRU0sY0FBYyxJQUFJLGlCQUFpQixDQUV6QztJQUVNLDJCQUEyQixJQUFJLE9BQU8sQ0FFNUM7SUFFTSxnQkFBZ0IsSUFBSSxNQUFNLENBRWhDO0lBRU0sVUFBVSxJQUFJLFVBQVUsQ0FFOUI7SUFFTSxhQUFhLElBQUksVUFBVSxDQUlqQztJQUVNLFdBQVcsSUFBSSxXQUFXLENBRWhDO0lBRU0sY0FBYyxJQUFJLFdBQVcsQ0FFbkM7SUFFTSxrQkFBa0IsSUFBSSxZQUFZLEdBQUc7UUFBRSxLQUFLLEVBQUUsTUFBTSxDQUFBO0tBQUUsQ0FJNUQ7SUFFRCxPQUFPLENBQUMscUJBQXFCO0lBSXRCLDJCQUEyQixJQUFJLFlBQVksR0FBRztRQUFFLFVBQVUsRUFBRSxNQUFNLENBQUE7S0FBRSxDQUkxRTtJQUVNLGlDQUFpQyxJQUFJLFlBQVksR0FBRztRQUFFLFVBQVUsRUFBRSxNQUFNLENBQUE7S0FBRSxDQVNoRjtJQUVELE9BQU8sQ0FBQywwQkFBMEI7SUFVM0Isb0JBQW9CLENBQUMsS0FBSyxFQUFFLFdBQVcsR0FBRyxPQUFPLENBQUMsa0JBQWtCLENBQUMsQ0FHM0U7SUFFRCxvRUFBb0U7SUFDdkQsaUJBQWlCLENBQUMsS0FBSyxFQUFFLFdBQVcsR0FBRyxPQUFPLENBQUMsT0FBTyxDQUFDLENBR25FO0lBRUQ7Ozs7O09BS0c7SUFDVSx1QkFBdUIsQ0FBQyxJQUFJLEdBQUUsT0FBZSxHQUFHLE9BQU8sQ0FBQyxPQUFPLENBQUMsQ0FTNUU7SUFFRDs7Ozs7O09BTUc7SUFDVSxZQUFZLENBQUMsSUFBSSxHQUFFLE9BQWUsR0FBRyxPQUFPLENBQUMsa0JBQWtCLENBQUMsQ0FxQzVFO0lBRUQsT0FBTyxDQUFDLG9CQUFvQjtJQVU1Qiw0RUFBNEU7SUFDNUUsT0FBTyxDQUFDLFVBQVU7SUFVbEIsbUZBQW1GO0lBQ25GLE9BQU8sQ0FBQyxPQUFPO0lBS2YscUhBQXFIO0lBQzlHLFdBQVcsQ0FBQyxLQUFLLEVBQUUsV0FBVyxHQUFHLE9BQU8sR0FBRyxTQUFTLENBTTFEO0lBRUQsd0dBQXdHO0lBQ2pHLCtCQUErQixDQUFDLEtBQUssRUFBRSxXQUFXLEdBQUcsTUFBTSxHQUFHLFNBQVMsQ0FNN0U7SUFFRCxxRUFBcUU7SUFDckUsT0FBTyxDQUFDLG9CQUFvQjtZQWVkLGlCQUFpQjtZQXlDakIsYUFBYTtJQXlDM0I7O09BRUc7SUFDSCx3QkFBd0IsQ0FBQyxLQUFLLEVBQUUsV0FBVyxFQUFFLElBQUksRUFBRSxVQUFVLEVBQUUsSUFBSSxFQUFFLE1BQU0sR0FBRyxLQUFLLE1BQU0sRUFBRSxDQVMxRjtJQUVNLG9CQUFvQixDQUFDLElBQUksRUFBRSxVQUFVLEVBQUUsS0FBSyxFQUFFLFdBQVcsRUFBRSxJQUFJLEVBQUUsTUFBTSxFQUFFLElBQUksRUFBRSxNQUFNLEdBQUcsTUFBTSxDQU1wRztJQUVELGdFQUFnRTtJQUN6RCxxQkFBcUIsSUFBSTtRQUFFLFdBQVcsRUFBRSxVQUFVLENBQUM7UUFBQyxRQUFRLEVBQUUsVUFBVSxDQUFBO0tBQUUsQ0FRaEY7SUFFRCwrREFBK0Q7SUFDeEQsb0JBQW9CLElBQUk7UUFBRSxVQUFVLEVBQUUsVUFBVSxDQUFDO1FBQUMsUUFBUSxFQUFFLFVBQVUsQ0FBQTtLQUFFLENBVzlFO0lBRUQ7Ozs7T0FJRztJQUNJLGdDQUFnQyxDQUFDLElBQUksRUFBRSxVQUFVLEdBQUcsT0FBTyxDQUFDLFVBQVUsR0FBRyxTQUFTLENBQUMsQ0FHekY7SUFFRDs7OztPQUlHO0lBQ0ksb0NBQW9DLElBQUksT0FBTyxDQUFDLFVBQVUsR0FBRyxTQUFTLENBQUMsQ0FHN0U7WUFRYSw0QkFBNEI7SUFhbkMsNkJBQTZCLENBQ2xDLGtCQUFrQixFQUFFLGtCQUFrQixFQUN0QyxJQUFJLEVBQUUsVUFBVSxHQUNmLFVBQVUsR0FBRyxTQUFTLENBWXhCO0lBRUQsNERBQTREO0lBQ3RELGFBQWEsQ0FBQyxJQUFJLEVBQUUsT0FBTyxFQUFFLFNBQVMsRUFBRSxVQUFVLEdBQUcsT0FBTyxDQUFDLE9BQU8sQ0FBQyxDQU0xRTtJQUVELCtGQUErRjtJQUN6RixpQkFBaUIsQ0FBQyxJQUFJLEVBQUUsT0FBTyxFQUFFLFVBQVUsRUFBRSxVQUFVLEVBQUUsR0FBRyxPQUFPLENBQUMsVUFBVSxFQUFFLENBQUMsQ0FPdEY7SUFFSyx1QkFBdUIsSUFBSSxPQUFPLENBQUMsVUFBVSxFQUFFLENBQUMsQ0FVckQ7Q0FDRiJ9
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"epoch_cache.d.ts","sourceRoot":"","sources":["../src/epoch_cache.ts"],"names":[],"mappings":"AAEA,OAAO,EAAoB,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAC7E,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAC1E,OAAO,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAE3D,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACvD,OAAO,EACL,KAAK,iBAAiB,
|
|
1
|
+
{"version":3,"file":"epoch_cache.d.ts","sourceRoot":"","sources":["../src/epoch_cache.ts"],"names":[],"mappings":"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"}
|
package/dest/epoch_cache.js
CHANGED
|
@@ -5,7 +5,7 @@ import { SlotNumber } from '@aztec/foundation/branded-types';
|
|
|
5
5
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
6
6
|
import { createLogger } from '@aztec/foundation/log';
|
|
7
7
|
import { DateProvider } from '@aztec/foundation/timer';
|
|
8
|
-
import { getEpochAtSlot, getEpochNumberAtTimestamp, getNextL1SlotTimestamp, getSlotAtNextL1Block, getSlotAtTimestamp, getSlotRangeForEpoch, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
8
|
+
import { getEpochAtSlot, getEpochNumberAtTimestamp, getNextL1SlotTimestamp, getSlotAtNextL1Block, getSlotAtTimestamp, getSlotRangeForEpoch, getStartTimestampForEpoch, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
9
9
|
import { createPublicClient, encodeAbiParameters, keccak256 } from 'viem';
|
|
10
10
|
import { getEpochCacheConfigEnvVars } from './config.js';
|
|
11
11
|
/** When proposer pipelining is enabled, the proposer builds one slot ahead. */ export const PROPOSER_PIPELINING_SLOT_OFFSET = 1;
|
|
@@ -22,7 +22,10 @@ import { getEpochCacheConfigEnvVars } from './config.js';
|
|
|
22
22
|
l1constants;
|
|
23
23
|
dateProvider;
|
|
24
24
|
config;
|
|
25
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Single map holding both resolved entries and in-flight promises.
|
|
27
|
+
* A `Promise` value means a fetch is in progress; concurrent callers await it.
|
|
28
|
+
*/ // eslint-disable-next-line aztec-custom/no-non-primitive-in-collections
|
|
26
29
|
cache;
|
|
27
30
|
allValidators;
|
|
28
31
|
lastValidatorRefresh;
|
|
@@ -99,6 +102,9 @@ import { getEpochCacheConfigEnvVars } from './config.js';
|
|
|
99
102
|
isProposerPipeliningEnabled() {
|
|
100
103
|
return this.enableProposerPipelining;
|
|
101
104
|
}
|
|
105
|
+
pipeliningOffset() {
|
|
106
|
+
return this.enableProposerPipelining ? PROPOSER_PIPELINING_SLOT_OFFSET : 0;
|
|
107
|
+
}
|
|
102
108
|
getSlotNow() {
|
|
103
109
|
return this.getEpochAndSlotNow().slot;
|
|
104
110
|
}
|
|
@@ -158,16 +164,7 @@ import { getEpochCacheConfigEnvVars } from './config.js';
|
|
|
158
164
|
const [startSlot] = getSlotRangeForEpoch(epoch, this.l1constants);
|
|
159
165
|
return this.getCommittee(startSlot);
|
|
160
166
|
}
|
|
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
|
-
}
|
|
167
|
+
/** Returns whether the escape hatch is open for the given epoch. */ async isEscapeHatchOpen(epoch) {
|
|
171
168
|
const info = await this.getCommitteeForEpoch(epoch);
|
|
172
169
|
return info.isEscapeHatchOpen;
|
|
173
170
|
}
|
|
@@ -181,26 +178,43 @@ import { getEpochCacheConfigEnvVars } from './config.js';
|
|
|
181
178
|
return await this.isEscapeHatchOpen(epoch);
|
|
182
179
|
}
|
|
183
180
|
/**
|
|
184
|
-
* Get the current validator set
|
|
185
|
-
*
|
|
186
|
-
*
|
|
181
|
+
* Get the current validator set.
|
|
182
|
+
*
|
|
183
|
+
* Returns cached data if the entry is finalized or still fresh (queried less than one
|
|
184
|
+
* Ethereum slot ago). Stale non-finalized entries are re-queried, and concurrent callers
|
|
185
|
+
* coalesce on the same in-flight promise so the L1 query happens only once.
|
|
187
186
|
*/ async getCommittee(slot = 'now') {
|
|
188
187
|
const { epoch, ts } = this.getEpochAndTimestamp(slot);
|
|
189
|
-
|
|
190
|
-
|
|
188
|
+
const cached = this.cache.get(epoch);
|
|
189
|
+
// In-flight promise: another caller is already fetching this epoch — just await it.
|
|
190
|
+
if (cached instanceof Promise) {
|
|
191
|
+
return (await cached).data;
|
|
191
192
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
}
|
|
196
|
-
//
|
|
197
|
-
if
|
|
198
|
-
|
|
193
|
+
// Resolved entry: return it if finalized or still fresh.
|
|
194
|
+
if (cached && (cached.finalized || !this.isStale(cached))) {
|
|
195
|
+
return cached.data;
|
|
196
|
+
}
|
|
197
|
+
// Stale non-finalized entry: do a lightweight refresh first (check block hash + finalized ts).
|
|
198
|
+
// Only fall back to a full re-fetch if the L1 block was reorged.
|
|
199
|
+
if (cached) {
|
|
200
|
+
const promise = this.refreshStaleEntry(cached, epoch, ts);
|
|
201
|
+
this.cache.set(epoch, promise);
|
|
202
|
+
try {
|
|
203
|
+
return (await promise).data;
|
|
204
|
+
} catch (err) {
|
|
205
|
+
this.cache.set(epoch, cached);
|
|
206
|
+
throw err;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// No entry at all: full fetch.
|
|
210
|
+
const promise = this.fetchAndCache(epoch, ts);
|
|
211
|
+
this.cache.set(epoch, promise);
|
|
212
|
+
try {
|
|
213
|
+
return (await promise).data;
|
|
214
|
+
} catch (err) {
|
|
215
|
+
this.cache.delete(epoch);
|
|
216
|
+
throw err;
|
|
199
217
|
}
|
|
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
218
|
}
|
|
205
219
|
getEpochAndTimestamp(slot = 'now') {
|
|
206
220
|
if (slot === 'now') {
|
|
@@ -211,27 +225,126 @@ import { getEpochCacheConfigEnvVars } from './config.js';
|
|
|
211
225
|
return this.getEpochAndSlotAtSlot(slot);
|
|
212
226
|
}
|
|
213
227
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
228
|
+
/** Evicts oldest cache entries (resolved or in-flight) beyond cacheSize. */ purgeCache() {
|
|
229
|
+
if (this.cache.size <= this.config.cacheSize) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const toPurge = Array.from(this.cache.keys()).sort((a, b)=>Number(b - a)).slice(this.config.cacheSize);
|
|
233
|
+
toPurge.forEach((key)=>this.cache.delete(key));
|
|
234
|
+
}
|
|
235
|
+
/** Returns true if a non-finalized cache entry is older than one Ethereum slot. */ isStale(entry) {
|
|
236
|
+
const nowSeconds = BigInt(this.dateProvider.nowInSeconds());
|
|
237
|
+
return nowSeconds - entry.lastRefreshL1Timestamp >= BigInt(this.l1constants.ethereumSlotDuration);
|
|
238
|
+
}
|
|
239
|
+
/** Whether a cached epoch entry has been marked as finalized. Returns undefined if not cached or still in-flight. */ isFinalized(epoch) {
|
|
240
|
+
const entry = this.cache.get(epoch);
|
|
241
|
+
if (!entry || entry instanceof Promise) {
|
|
242
|
+
return undefined;
|
|
243
|
+
}
|
|
244
|
+
return entry.finalized;
|
|
245
|
+
}
|
|
246
|
+
/** Returns the latest L1 timestamp stored in the cached entry. Undefined if not cached or in-flight. */ getCachedLastRefreshL1Timestamp(epoch) {
|
|
247
|
+
const entry = this.cache.get(epoch);
|
|
248
|
+
if (!entry || entry instanceof Promise) {
|
|
249
|
+
return undefined;
|
|
250
|
+
}
|
|
251
|
+
return entry.lastRefreshL1Timestamp;
|
|
252
|
+
}
|
|
253
|
+
/** Computes the sampling timestamp for an epoch's committee data. */ getSamplingTimestamp(epoch) {
|
|
254
|
+
const { lagInEpochsForRandao, epochDuration, slotDuration } = this.l1constants;
|
|
255
|
+
const epochStartTs = getStartTimestampForEpoch(epoch, this.l1constants);
|
|
256
|
+
return epochStartTs - BigInt(lagInEpochsForRandao) * BigInt(epochDuration) * BigInt(slotDuration);
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Lightweight refresh for a stale non-finalized entry. Queries only the block hash at
|
|
260
|
+
* the original block number and the finalized block timestamp — avoids the expensive
|
|
261
|
+
* getCommitteeAt and getSampleSeedAt calls on the rollup contract.
|
|
262
|
+
*
|
|
263
|
+
* If the block hash still matches (no L1 reorg), we keep the existing data and just
|
|
264
|
+
* update the provenance timestamp. If the finalized block has caught up, we promote the
|
|
265
|
+
* entry to finalized. If there was a reorg (hash mismatch), we fall back to a full fetch.
|
|
266
|
+
*/ async refreshStaleEntry(stale, epoch, ts) {
|
|
267
|
+
const [blockAtOriginal, l1FinalizedBlock, latestBlock] = await Promise.all([
|
|
268
|
+
this.rollup.client.getBlock({
|
|
269
|
+
blockNumber: stale.lastQueryL1BlockNumber,
|
|
270
|
+
includeTransactions: false
|
|
271
|
+
}),
|
|
272
|
+
this.rollup.client.getBlock({
|
|
273
|
+
blockTag: 'finalized',
|
|
274
|
+
includeTransactions: false
|
|
275
|
+
}),
|
|
276
|
+
this.rollup.client.getBlock({
|
|
277
|
+
includeTransactions: false
|
|
278
|
+
})
|
|
279
|
+
]);
|
|
280
|
+
if (blockAtOriginal.hash === stale.lastQueryL1BlockHash) {
|
|
281
|
+
// No reorg: the data is still valid. Check if we can now mark it as finalized.
|
|
282
|
+
const samplingTs = this.getSamplingTimestamp(epoch);
|
|
283
|
+
const finalized = !!(stale.data.committee && stale.data.committee.length > 0) && samplingTs <= l1FinalizedBlock.timestamp;
|
|
284
|
+
const refreshed = {
|
|
285
|
+
...stale,
|
|
286
|
+
lastRefreshL1Timestamp: latestBlock.timestamp,
|
|
287
|
+
finalized
|
|
288
|
+
};
|
|
289
|
+
this.cache.set(epoch, refreshed);
|
|
290
|
+
return refreshed;
|
|
291
|
+
}
|
|
292
|
+
// Reorg detected: block hash mismatch. Do a full re-fetch.
|
|
293
|
+
// Pass the already-fetched block timestamps to avoid redundant queries.
|
|
294
|
+
this.log.warn(`L1 reorg detected for epoch ${epoch}: block ${stale.lastQueryL1BlockNumber} hash changed`, {
|
|
295
|
+
epoch,
|
|
296
|
+
expectedHash: stale.lastQueryL1BlockHash,
|
|
297
|
+
actualHash: blockAtOriginal.hash
|
|
298
|
+
});
|
|
299
|
+
return this.fetchAndCache(epoch, ts, {
|
|
300
|
+
latestBlock,
|
|
301
|
+
finalizedBlock: l1FinalizedBlock
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Fetches committee data from L1, determines finalization status, and stores in the cache.
|
|
306
|
+
*
|
|
307
|
+
* Uses `lagInEpochsForRandao` (the binding constraint, always <= lagInEpochsForValidatorSet)
|
|
308
|
+
* and computes the sampling timestamp from the epoch start to match the L1 contract's logic.
|
|
309
|
+
*
|
|
310
|
+
* When called from refreshStaleEntry after a reorg, the latest and finalized blocks are
|
|
311
|
+
* passed in to avoid redundant L1 queries.
|
|
312
|
+
*/ async fetchAndCache(epoch, ts, prefetched) {
|
|
313
|
+
const [committee, seedBuffer, latestBlock, finalizedBlock, isEscapeHatchOpen] = await Promise.all([
|
|
217
314
|
this.rollup.getCommitteeAt(ts),
|
|
218
315
|
this.rollup.getSampleSeedAt(ts),
|
|
219
|
-
this.rollup.client.getBlock({
|
|
316
|
+
prefetched?.latestBlock ?? this.rollup.client.getBlock({
|
|
220
317
|
includeTransactions: false
|
|
221
|
-
})
|
|
318
|
+
}),
|
|
319
|
+
prefetched?.finalizedBlock ?? this.rollup.client.getBlock({
|
|
320
|
+
blockTag: 'finalized',
|
|
321
|
+
includeTransactions: false
|
|
322
|
+
}),
|
|
222
323
|
this.rollup.isEscapeHatchOpen(epoch)
|
|
223
324
|
]);
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
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.`);
|
|
325
|
+
const samplingTs = this.getSamplingTimestamp(epoch);
|
|
326
|
+
if (samplingTs > latestBlock.timestamp) {
|
|
327
|
+
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
328
|
}
|
|
229
|
-
|
|
329
|
+
// Empty committees are never marked finalized so they always get re-queried after TTL.
|
|
330
|
+
const hasCommittee = !!(committee && committee.length > 0);
|
|
331
|
+
const finalized = hasCommittee && samplingTs <= finalizedBlock.timestamp;
|
|
332
|
+
const data = {
|
|
230
333
|
committee,
|
|
231
334
|
seed: seedBuffer.toBigInt(),
|
|
232
335
|
epoch,
|
|
233
336
|
isEscapeHatchOpen
|
|
234
337
|
};
|
|
338
|
+
const entry = {
|
|
339
|
+
data,
|
|
340
|
+
lastQueryL1BlockNumber: latestBlock.number,
|
|
341
|
+
lastQueryL1BlockHash: latestBlock.hash,
|
|
342
|
+
lastRefreshL1Timestamp: latestBlock.timestamp,
|
|
343
|
+
finalized
|
|
344
|
+
};
|
|
345
|
+
this.cache.set(epoch, entry);
|
|
346
|
+
this.purgeCache();
|
|
347
|
+
return entry;
|
|
235
348
|
}
|
|
236
349
|
/**
|
|
237
350
|
* 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,
|
|
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.
|
|
3
|
+
"version": "0.0.1-commit.f650c0a5c",
|
|
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.
|
|
30
|
-
"@aztec/foundation": "0.0.1-commit.
|
|
31
|
-
"@aztec/l1-artifacts": "0.0.1-commit.
|
|
32
|
-
"@aztec/stdlib": "0.0.1-commit.
|
|
29
|
+
"@aztec/ethereum": "0.0.1-commit.f650c0a5c",
|
|
30
|
+
"@aztec/foundation": "0.0.1-commit.f650c0a5c",
|
|
31
|
+
"@aztec/l1-artifacts": "0.0.1-commit.f650c0a5c",
|
|
32
|
+
"@aztec/stdlib": "0.0.1-commit.f650c0a5c",
|
|
33
33
|
"dotenv": "^16.0.3",
|
|
34
34
|
"get-port": "^7.1.0",
|
|
35
35
|
"jest-mock-extended": "^4.0.0",
|
package/src/epoch_cache.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
getSlotAtNextL1Block,
|
|
14
14
|
getSlotAtTimestamp,
|
|
15
15
|
getSlotRangeForEpoch,
|
|
16
|
+
getStartTimestampForEpoch,
|
|
16
17
|
getTimestampForSlot,
|
|
17
18
|
} from '@aztec/stdlib/epoch-helpers';
|
|
18
19
|
|
|
@@ -40,6 +41,22 @@ export type EpochCommitteeInfo = {
|
|
|
40
41
|
|
|
41
42
|
export type SlotTag = 'now' | 'next' | SlotNumber;
|
|
42
43
|
|
|
44
|
+
/** Minimal L1 block info used for cache provenance. */
|
|
45
|
+
type L1BlockInfo = { number: bigint; hash: `0x${string}`; timestamp: bigint };
|
|
46
|
+
|
|
47
|
+
/** Resolved cache entry with L1 provenance metadata. */
|
|
48
|
+
type CachedEpochEntry = {
|
|
49
|
+
data: EpochCommitteeInfo;
|
|
50
|
+
/** L1 block number at which the committee data was originally queried. */
|
|
51
|
+
lastQueryL1BlockNumber: bigint;
|
|
52
|
+
/** L1 block hash at which the committee data was originally queried. Used to detect reorgs. */
|
|
53
|
+
lastQueryL1BlockHash: `0x${string}`;
|
|
54
|
+
/** Latest L1 block timestamp at the time of the most recent refresh (full fetch or lightweight check). */
|
|
55
|
+
lastRefreshL1Timestamp: bigint;
|
|
56
|
+
/** Whether the epoch's sampling data falls within finalized L1 history. */
|
|
57
|
+
finalized: boolean;
|
|
58
|
+
};
|
|
59
|
+
|
|
43
60
|
export interface EpochCacheInterface {
|
|
44
61
|
getCommittee(slot: SlotTag | undefined): Promise<EpochCommitteeInfo>;
|
|
45
62
|
getSlotNow(): SlotNumber;
|
|
@@ -51,6 +68,7 @@ export interface EpochCacheInterface {
|
|
|
51
68
|
/** Returns epoch/slot info for the next L1 slot with pipeline offset applied. */
|
|
52
69
|
getTargetEpochAndSlotInNextL1Slot(): EpochAndSlot & { nowSeconds: bigint };
|
|
53
70
|
isProposerPipeliningEnabled(): boolean;
|
|
71
|
+
pipeliningOffset(): number;
|
|
54
72
|
isEscapeHatchOpen(epoch: EpochNumber): Promise<boolean>;
|
|
55
73
|
isEscapeHatchOpenAtSlot(slot: SlotTag): Promise<boolean>;
|
|
56
74
|
getProposerIndexEncoding(epoch: EpochNumber, slot: SlotNumber, seed: bigint): `0x${string}`;
|
|
@@ -74,8 +92,12 @@ export interface EpochCacheInterface {
|
|
|
74
92
|
* Note: This class is very dependent on the system clock being in sync.
|
|
75
93
|
*/
|
|
76
94
|
export class EpochCache implements EpochCacheInterface {
|
|
95
|
+
/**
|
|
96
|
+
* Single map holding both resolved entries and in-flight promises.
|
|
97
|
+
* A `Promise` value means a fetch is in progress; concurrent callers await it.
|
|
98
|
+
*/
|
|
77
99
|
// eslint-disable-next-line aztec-custom/no-non-primitive-in-collections
|
|
78
|
-
protected cache: Map<EpochNumber,
|
|
100
|
+
protected cache: Map<EpochNumber, CachedEpochEntry | Promise<CachedEpochEntry>> = new Map();
|
|
79
101
|
private allValidators: Set<string> = new Set();
|
|
80
102
|
private lastValidatorRefresh = 0;
|
|
81
103
|
private readonly log: Logger = createLogger('epoch-cache');
|
|
@@ -169,6 +191,10 @@ export class EpochCache implements EpochCacheInterface {
|
|
|
169
191
|
return this.enableProposerPipelining;
|
|
170
192
|
}
|
|
171
193
|
|
|
194
|
+
public pipeliningOffset(): number {
|
|
195
|
+
return this.enableProposerPipelining ? PROPOSER_PIPELINING_SLOT_OFFSET : 0;
|
|
196
|
+
}
|
|
197
|
+
|
|
172
198
|
public getSlotNow(): SlotNumber {
|
|
173
199
|
return this.getEpochAndSlotNow().slot;
|
|
174
200
|
}
|
|
@@ -229,17 +255,8 @@ export class EpochCache implements EpochCacheInterface {
|
|
|
229
255
|
return this.getCommittee(startSlot);
|
|
230
256
|
}
|
|
231
257
|
|
|
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
|
-
*/
|
|
258
|
+
/** Returns whether the escape hatch is open for the given epoch. */
|
|
238
259
|
public async isEscapeHatchOpen(epoch: EpochNumber): Promise<boolean> {
|
|
239
|
-
const cached = this.cache.get(epoch);
|
|
240
|
-
if (cached) {
|
|
241
|
-
return cached.isEscapeHatchOpen;
|
|
242
|
-
}
|
|
243
260
|
const info = await this.getCommitteeForEpoch(epoch);
|
|
244
261
|
return info.isEscapeHatchOpen;
|
|
245
262
|
}
|
|
@@ -262,30 +279,49 @@ export class EpochCache implements EpochCacheInterface {
|
|
|
262
279
|
}
|
|
263
280
|
|
|
264
281
|
/**
|
|
265
|
-
* Get the current validator set
|
|
266
|
-
*
|
|
267
|
-
*
|
|
282
|
+
* Get the current validator set.
|
|
283
|
+
*
|
|
284
|
+
* Returns cached data if the entry is finalized or still fresh (queried less than one
|
|
285
|
+
* Ethereum slot ago). Stale non-finalized entries are re-queried, and concurrent callers
|
|
286
|
+
* coalesce on the same in-flight promise so the L1 query happens only once.
|
|
268
287
|
*/
|
|
269
288
|
public async getCommittee(slot: SlotTag = 'now'): Promise<EpochCommitteeInfo> {
|
|
270
289
|
const { epoch, ts } = this.getEpochAndTimestamp(slot);
|
|
271
290
|
|
|
272
|
-
|
|
273
|
-
|
|
291
|
+
const cached = this.cache.get(epoch);
|
|
292
|
+
|
|
293
|
+
// In-flight promise: another caller is already fetching this epoch — just await it.
|
|
294
|
+
if (cached instanceof Promise) {
|
|
295
|
+
return (await cached).data;
|
|
274
296
|
}
|
|
275
297
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
return epochData;
|
|
298
|
+
// Resolved entry: return it if finalized or still fresh.
|
|
299
|
+
if (cached && (cached.finalized || !this.isStale(cached))) {
|
|
300
|
+
return cached.data;
|
|
280
301
|
}
|
|
281
|
-
this.cache.set(epoch, epochData);
|
|
282
302
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
303
|
+
// Stale non-finalized entry: do a lightweight refresh first (check block hash + finalized ts).
|
|
304
|
+
// Only fall back to a full re-fetch if the L1 block was reorged.
|
|
305
|
+
if (cached) {
|
|
306
|
+
const promise = this.refreshStaleEntry(cached, epoch, ts);
|
|
307
|
+
this.cache.set(epoch, promise);
|
|
308
|
+
try {
|
|
309
|
+
return (await promise).data;
|
|
310
|
+
} catch (err) {
|
|
311
|
+
this.cache.set(epoch, cached);
|
|
312
|
+
throw err;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
287
315
|
|
|
288
|
-
|
|
316
|
+
// No entry at all: full fetch.
|
|
317
|
+
const promise = this.fetchAndCache(epoch, ts);
|
|
318
|
+
this.cache.set(epoch, promise);
|
|
319
|
+
try {
|
|
320
|
+
return (await promise).data;
|
|
321
|
+
} catch (err) {
|
|
322
|
+
this.cache.delete(epoch);
|
|
323
|
+
throw err;
|
|
324
|
+
}
|
|
289
325
|
}
|
|
290
326
|
|
|
291
327
|
private getEpochAndTimestamp(slot: SlotTag = 'now'): { epoch: EpochNumber; ts: bigint } {
|
|
@@ -298,22 +334,137 @@ export class EpochCache implements EpochCacheInterface {
|
|
|
298
334
|
}
|
|
299
335
|
}
|
|
300
336
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
337
|
+
/** Evicts oldest cache entries (resolved or in-flight) beyond cacheSize. */
|
|
338
|
+
private purgeCache(): void {
|
|
339
|
+
if (this.cache.size <= this.config.cacheSize) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const toPurge = Array.from(this.cache.keys())
|
|
343
|
+
.sort((a, b) => Number(b - a))
|
|
344
|
+
.slice(this.config.cacheSize);
|
|
345
|
+
toPurge.forEach(key => this.cache.delete(key));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** Returns true if a non-finalized cache entry is older than one Ethereum slot. */
|
|
349
|
+
private isStale(entry: CachedEpochEntry): boolean {
|
|
350
|
+
const nowSeconds = BigInt(this.dateProvider.nowInSeconds());
|
|
351
|
+
return nowSeconds - entry.lastRefreshL1Timestamp >= BigInt(this.l1constants.ethereumSlotDuration);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Whether a cached epoch entry has been marked as finalized. Returns undefined if not cached or still in-flight. */
|
|
355
|
+
public isFinalized(epoch: EpochNumber): boolean | undefined {
|
|
356
|
+
const entry = this.cache.get(epoch);
|
|
357
|
+
if (!entry || entry instanceof Promise) {
|
|
358
|
+
return undefined;
|
|
359
|
+
}
|
|
360
|
+
return entry.finalized;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Returns the latest L1 timestamp stored in the cached entry. Undefined if not cached or in-flight. */
|
|
364
|
+
public getCachedLastRefreshL1Timestamp(epoch: EpochNumber): bigint | undefined {
|
|
365
|
+
const entry = this.cache.get(epoch);
|
|
366
|
+
if (!entry || entry instanceof Promise) {
|
|
367
|
+
return undefined;
|
|
368
|
+
}
|
|
369
|
+
return entry.lastRefreshL1Timestamp;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** Computes the sampling timestamp for an epoch's committee data. */
|
|
373
|
+
private getSamplingTimestamp(epoch: EpochNumber): bigint {
|
|
374
|
+
const { lagInEpochsForRandao, epochDuration, slotDuration } = this.l1constants;
|
|
375
|
+
const epochStartTs = getStartTimestampForEpoch(epoch, this.l1constants);
|
|
376
|
+
return epochStartTs - BigInt(lagInEpochsForRandao) * BigInt(epochDuration) * BigInt(slotDuration);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Lightweight refresh for a stale non-finalized entry. Queries only the block hash at
|
|
381
|
+
* the original block number and the finalized block timestamp — avoids the expensive
|
|
382
|
+
* getCommitteeAt and getSampleSeedAt calls on the rollup contract.
|
|
383
|
+
*
|
|
384
|
+
* If the block hash still matches (no L1 reorg), we keep the existing data and just
|
|
385
|
+
* update the provenance timestamp. If the finalized block has caught up, we promote the
|
|
386
|
+
* entry to finalized. If there was a reorg (hash mismatch), we fall back to a full fetch.
|
|
387
|
+
*/
|
|
388
|
+
private async refreshStaleEntry(stale: CachedEpochEntry, epoch: EpochNumber, ts: bigint): Promise<CachedEpochEntry> {
|
|
389
|
+
const [blockAtOriginal, l1FinalizedBlock, latestBlock] = await Promise.all([
|
|
390
|
+
this.rollup.client.getBlock({ blockNumber: stale.lastQueryL1BlockNumber, includeTransactions: false }),
|
|
391
|
+
this.rollup.client.getBlock({ blockTag: 'finalized', includeTransactions: false }),
|
|
392
|
+
this.rollup.client.getBlock({ includeTransactions: false }),
|
|
393
|
+
]);
|
|
394
|
+
|
|
395
|
+
if (blockAtOriginal.hash === stale.lastQueryL1BlockHash) {
|
|
396
|
+
// No reorg: the data is still valid. Check if we can now mark it as finalized.
|
|
397
|
+
const samplingTs = this.getSamplingTimestamp(epoch);
|
|
398
|
+
const finalized =
|
|
399
|
+
!!(stale.data.committee && stale.data.committee.length > 0) && samplingTs <= l1FinalizedBlock.timestamp;
|
|
400
|
+
|
|
401
|
+
const refreshed: CachedEpochEntry = {
|
|
402
|
+
...stale,
|
|
403
|
+
lastRefreshL1Timestamp: latestBlock.timestamp,
|
|
404
|
+
finalized,
|
|
405
|
+
};
|
|
406
|
+
this.cache.set(epoch, refreshed);
|
|
407
|
+
return refreshed;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Reorg detected: block hash mismatch. Do a full re-fetch.
|
|
411
|
+
// Pass the already-fetched block timestamps to avoid redundant queries.
|
|
412
|
+
this.log.warn(`L1 reorg detected for epoch ${epoch}: block ${stale.lastQueryL1BlockNumber} hash changed`, {
|
|
413
|
+
epoch,
|
|
414
|
+
expectedHash: stale.lastQueryL1BlockHash,
|
|
415
|
+
actualHash: blockAtOriginal.hash,
|
|
416
|
+
});
|
|
417
|
+
return this.fetchAndCache(epoch, ts, { latestBlock, finalizedBlock: l1FinalizedBlock });
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Fetches committee data from L1, determines finalization status, and stores in the cache.
|
|
422
|
+
*
|
|
423
|
+
* Uses `lagInEpochsForRandao` (the binding constraint, always <= lagInEpochsForValidatorSet)
|
|
424
|
+
* and computes the sampling timestamp from the epoch start to match the L1 contract's logic.
|
|
425
|
+
*
|
|
426
|
+
* When called from refreshStaleEntry after a reorg, the latest and finalized blocks are
|
|
427
|
+
* passed in to avoid redundant L1 queries.
|
|
428
|
+
*/
|
|
429
|
+
private async fetchAndCache(
|
|
430
|
+
epoch: EpochNumber,
|
|
431
|
+
ts: bigint,
|
|
432
|
+
prefetched?: { latestBlock: L1BlockInfo; finalizedBlock: { timestamp: bigint } },
|
|
433
|
+
): Promise<CachedEpochEntry> {
|
|
434
|
+
const [committee, seedBuffer, latestBlock, finalizedBlock, isEscapeHatchOpen] = await Promise.all([
|
|
304
435
|
this.rollup.getCommitteeAt(ts),
|
|
305
436
|
this.rollup.getSampleSeedAt(ts),
|
|
306
|
-
this.rollup.client.getBlock({ includeTransactions: false })
|
|
437
|
+
prefetched?.latestBlock ?? this.rollup.client.getBlock({ includeTransactions: false }),
|
|
438
|
+
prefetched?.finalizedBlock ?? this.rollup.client.getBlock({ blockTag: 'finalized', includeTransactions: false }),
|
|
307
439
|
this.rollup.isEscapeHatchOpen(epoch),
|
|
308
440
|
]);
|
|
309
|
-
|
|
310
|
-
const
|
|
311
|
-
|
|
441
|
+
|
|
442
|
+
const samplingTs = this.getSamplingTimestamp(epoch);
|
|
443
|
+
|
|
444
|
+
if (samplingTs > latestBlock.timestamp) {
|
|
312
445
|
throw new Error(
|
|
313
|
-
`Cannot query committee for future epoch ${epoch}
|
|
446
|
+
`Cannot query committee for future epoch ${epoch}: ` +
|
|
447
|
+
`sampling timestamp ${samplingTs} is beyond latest L1 block at ${latestBlock.timestamp}. ` +
|
|
448
|
+
`Check your Ethereum node is synced.`,
|
|
314
449
|
);
|
|
315
450
|
}
|
|
316
|
-
|
|
451
|
+
|
|
452
|
+
// Empty committees are never marked finalized so they always get re-queried after TTL.
|
|
453
|
+
const hasCommittee = !!(committee && committee.length > 0);
|
|
454
|
+
const finalized = hasCommittee && samplingTs <= finalizedBlock.timestamp;
|
|
455
|
+
const data: EpochCommitteeInfo = { committee, seed: seedBuffer.toBigInt(), epoch, isEscapeHatchOpen };
|
|
456
|
+
const entry: CachedEpochEntry = {
|
|
457
|
+
data,
|
|
458
|
+
lastQueryL1BlockNumber: latestBlock.number!,
|
|
459
|
+
lastQueryL1BlockHash: latestBlock.hash!,
|
|
460
|
+
lastRefreshL1Timestamp: latestBlock.timestamp,
|
|
461
|
+
finalized,
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
this.cache.set(epoch, entry);
|
|
465
|
+
this.purgeCache();
|
|
466
|
+
|
|
467
|
+
return entry;
|
|
317
468
|
}
|
|
318
469
|
|
|
319
470
|
/**
|
|
@@ -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];
|