@aztec/epoch-cache 0.0.1-commit.5de5ca79e → 0.0.1-commit.6201a7b05
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 +40 -14
- package/dest/epoch_cache.d.ts.map +1 -1
- package/dest/epoch_cache.js +162 -51
- 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 +209 -52
- 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;
|
|
@@ -98,7 +116,6 @@ export declare class EpochCache implements EpochCacheInterface {
|
|
|
98
116
|
getEpochAndSlotNow(): EpochAndSlot & {
|
|
99
117
|
nowMs: bigint;
|
|
100
118
|
};
|
|
101
|
-
nowInSeconds(): bigint;
|
|
102
119
|
private getEpochAndSlotAtSlot;
|
|
103
120
|
getEpochAndSlotInNextL1Slot(): EpochAndSlot & {
|
|
104
121
|
nowSeconds: bigint;
|
|
@@ -108,12 +125,7 @@ export declare class EpochCache implements EpochCacheInterface {
|
|
|
108
125
|
};
|
|
109
126
|
private getEpochAndSlotAtTimestamp;
|
|
110
127
|
getCommitteeForEpoch(epoch: EpochNumber): Promise<EpochCommitteeInfo>;
|
|
111
|
-
/**
|
|
112
|
-
* Returns whether the escape hatch is open for the given epoch.
|
|
113
|
-
*
|
|
114
|
-
* Uses the already-cached EpochCommitteeInfo when available. If not cached, it will fetch
|
|
115
|
-
* the epoch committee info (which includes the escape hatch flag) and return it.
|
|
116
|
-
*/
|
|
128
|
+
/** Returns whether the escape hatch is open for the given epoch. */
|
|
117
129
|
isEscapeHatchOpen(epoch: EpochNumber): Promise<boolean>;
|
|
118
130
|
/**
|
|
119
131
|
* Returns whether the escape hatch is open for the epoch containing the given slot.
|
|
@@ -123,13 +135,26 @@ export declare class EpochCache implements EpochCacheInterface {
|
|
|
123
135
|
*/
|
|
124
136
|
isEscapeHatchOpenAtSlot(slot?: SlotTag): Promise<boolean>;
|
|
125
137
|
/**
|
|
126
|
-
* Get the current validator set
|
|
127
|
-
*
|
|
128
|
-
*
|
|
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.
|
|
129
143
|
*/
|
|
130
144
|
getCommittee(slot?: SlotTag): Promise<EpochCommitteeInfo>;
|
|
131
145
|
private getEpochAndTimestamp;
|
|
132
|
-
|
|
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;
|
|
133
158
|
/**
|
|
134
159
|
* Get the ABI encoding of the proposer index - see ValidatorSelectionLib.sol computeProposerIndex
|
|
135
160
|
*/
|
|
@@ -140,7 +165,7 @@ export declare class EpochCache implements EpochCacheInterface {
|
|
|
140
165
|
currentSlot: SlotNumber;
|
|
141
166
|
nextSlot: SlotNumber;
|
|
142
167
|
};
|
|
143
|
-
/** Returns the
|
|
168
|
+
/** Returns the target and next L2 slot in the next L1 slot. */
|
|
144
169
|
getTargetAndNextSlot(): {
|
|
145
170
|
targetSlot: SlotNumber;
|
|
146
171
|
nextSlot: SlotNumber;
|
|
@@ -165,4 +190,5 @@ export declare class EpochCache implements EpochCacheInterface {
|
|
|
165
190
|
filterInCommittee(slot: SlotTag, validators: EthAddress[]): Promise<EthAddress[]>;
|
|
166
191
|
getRegisteredValidators(): Promise<EthAddress[]>;
|
|
167
192
|
}
|
|
168
|
-
|
|
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":"AAEA,OAAO,EAAoB,cAAc,EAAE,MAAM,2BAA2B,CAAC;
|
|
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
|
@@ -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, 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
|
-
|
|
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
|
}
|
|
@@ -121,18 +128,15 @@ import { getEpochCacheConfigEnvVars } from './config.js';
|
|
|
121
128
|
nowMs
|
|
122
129
|
};
|
|
123
130
|
}
|
|
124
|
-
nowInSeconds() {
|
|
125
|
-
return BigInt(Math.floor(this.dateProvider.now() / 1000));
|
|
126
|
-
}
|
|
127
131
|
getEpochAndSlotAtSlot(slot) {
|
|
128
132
|
return this.getEpochAndSlotAtTimestamp(getTimestampForSlot(slot, this.l1constants));
|
|
129
133
|
}
|
|
130
134
|
getEpochAndSlotInNextL1Slot() {
|
|
131
|
-
const nowSeconds = this.nowInSeconds();
|
|
132
|
-
const nextSlotTs = nowSeconds
|
|
135
|
+
const nowSeconds = this.dateProvider.nowInSeconds();
|
|
136
|
+
const nextSlotTs = getNextL1SlotTimestamp(nowSeconds, this.l1constants);
|
|
133
137
|
return {
|
|
134
138
|
...this.getEpochAndSlotAtTimestamp(nextSlotTs),
|
|
135
|
-
nowSeconds
|
|
139
|
+
nowSeconds: BigInt(nowSeconds)
|
|
136
140
|
};
|
|
137
141
|
}
|
|
138
142
|
getTargetEpochAndSlotInNextL1Slot() {
|
|
@@ -161,16 +165,7 @@ import { getEpochCacheConfigEnvVars } from './config.js';
|
|
|
161
165
|
const [startSlot] = getSlotRangeForEpoch(epoch, this.l1constants);
|
|
162
166
|
return this.getCommittee(startSlot);
|
|
163
167
|
}
|
|
164
|
-
/**
|
|
165
|
-
* Returns whether the escape hatch is open for the given epoch.
|
|
166
|
-
*
|
|
167
|
-
* Uses the already-cached EpochCommitteeInfo when available. If not cached, it will fetch
|
|
168
|
-
* the epoch committee info (which includes the escape hatch flag) and return it.
|
|
169
|
-
*/ async isEscapeHatchOpen(epoch) {
|
|
170
|
-
const cached = this.cache.get(epoch);
|
|
171
|
-
if (cached) {
|
|
172
|
-
return cached.isEscapeHatchOpen;
|
|
173
|
-
}
|
|
168
|
+
/** Returns whether the escape hatch is open for the given epoch. */ async isEscapeHatchOpen(epoch) {
|
|
174
169
|
const info = await this.getCommitteeForEpoch(epoch);
|
|
175
170
|
return info.isEscapeHatchOpen;
|
|
176
171
|
}
|
|
@@ -184,26 +179,43 @@ import { getEpochCacheConfigEnvVars } from './config.js';
|
|
|
184
179
|
return await this.isEscapeHatchOpen(epoch);
|
|
185
180
|
}
|
|
186
181
|
/**
|
|
187
|
-
* Get the current validator set
|
|
188
|
-
*
|
|
189
|
-
*
|
|
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.
|
|
190
187
|
*/ async getCommittee(slot = 'now') {
|
|
191
188
|
const { epoch, ts } = this.getEpochAndTimestamp(slot);
|
|
192
|
-
|
|
193
|
-
|
|
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;
|
|
194
193
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
//
|
|
200
|
-
if
|
|
201
|
-
|
|
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;
|
|
202
218
|
}
|
|
203
|
-
this.cache.set(epoch, epochData);
|
|
204
|
-
const toPurge = Array.from(this.cache.keys()).sort((a, b)=>Number(b - a)).slice(this.config.cacheSize);
|
|
205
|
-
toPurge.forEach((key)=>this.cache.delete(key));
|
|
206
|
-
return epochData;
|
|
207
219
|
}
|
|
208
220
|
getEpochAndTimestamp(slot = 'now') {
|
|
209
221
|
if (slot === 'now') {
|
|
@@ -214,27 +226,121 @@ import { getEpochCacheConfigEnvVars } from './config.js';
|
|
|
214
226
|
return this.getEpochAndSlotAtSlot(slot);
|
|
215
227
|
}
|
|
216
228
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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([
|
|
220
312
|
this.rollup.getCommitteeAt(ts),
|
|
221
313
|
this.rollup.getSampleSeedAt(ts),
|
|
222
|
-
this.rollup.client.getBlock({
|
|
314
|
+
prefetched?.latestBlock ?? this.rollup.client.getBlock({
|
|
223
315
|
includeTransactions: false
|
|
224
|
-
})
|
|
316
|
+
}),
|
|
317
|
+
prefetched !== undefined ? prefetched.finalizedBlock : getFinalizedL1Block(this.rollup.client),
|
|
225
318
|
this.rollup.isEscapeHatchOpen(epoch)
|
|
226
319
|
]);
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
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.`);
|
|
231
323
|
}
|
|
232
|
-
|
|
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 = {
|
|
233
329
|
committee,
|
|
234
330
|
seed: seedBuffer.toBigInt(),
|
|
235
331
|
epoch,
|
|
236
332
|
isEscapeHatchOpen
|
|
237
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;
|
|
238
344
|
}
|
|
239
345
|
/**
|
|
240
346
|
* Get the ABI encoding of the proposer index - see ValidatorSelectionLib.sol computeProposerIndex
|
|
@@ -273,12 +379,16 @@ import { getEpochCacheConfigEnvVars } from './config.js';
|
|
|
273
379
|
nextSlot: next.slot
|
|
274
380
|
};
|
|
275
381
|
}
|
|
276
|
-
/** Returns the
|
|
277
|
-
const
|
|
278
|
-
const
|
|
382
|
+
/** Returns the target and next L2 slot in the next L1 slot. */ getTargetAndNextSlot() {
|
|
383
|
+
const nowSeconds = BigInt(this.dateProvider.nowInSeconds());
|
|
384
|
+
const offset = this.isProposerPipeliningEnabled() ? PROPOSER_PIPELINING_SLOT_OFFSET : 0;
|
|
385
|
+
const currentSlot = getSlotAtTimestamp(nowSeconds, this.l1constants);
|
|
386
|
+
const targetSlot = SlotNumber(currentSlot + offset);
|
|
387
|
+
const nextL2SlotOnL1 = getSlotAtNextL1Block(nowSeconds, this.l1constants);
|
|
388
|
+
const nextSlot = SlotNumber(nextL2SlotOnL1 + offset);
|
|
279
389
|
return {
|
|
280
390
|
targetSlot,
|
|
281
|
-
nextSlot
|
|
391
|
+
nextSlot
|
|
282
392
|
};
|
|
283
393
|
}
|
|
284
394
|
/**
|
|
@@ -338,10 +448,11 @@ import { getEpochCacheConfigEnvVars } from './config.js';
|
|
|
338
448
|
async getRegisteredValidators() {
|
|
339
449
|
const validatorRefreshIntervalMs = this.config.validatorRefreshIntervalSeconds * 1000;
|
|
340
450
|
const validatorRefreshTime = this.lastValidatorRefresh + validatorRefreshIntervalMs;
|
|
341
|
-
|
|
342
|
-
|
|
451
|
+
const now = this.dateProvider.now();
|
|
452
|
+
if (validatorRefreshTime < now) {
|
|
453
|
+
const currentSet = await this.rollup.getAttesters(BigInt(Math.floor(now / 1000)));
|
|
343
454
|
this.allValidators = new Set(currentSet.map((v)=>v.toString()));
|
|
344
|
-
this.lastValidatorRefresh =
|
|
455
|
+
this.lastValidatorRefresh = now;
|
|
345
456
|
}
|
|
346
457
|
return Array.from(this.allValidators.keys()).map((v)=>EthAddress.fromString(v));
|
|
347
458
|
}
|
|
@@ -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,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidGVzdF9lcG9jaF9jYWNoZS5kLnRzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL3Rlc3QvdGVzdF9lcG9jaF9jYWNoZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsV0FBVyxFQUFFLFVBQVUsRUFBRSxNQUFNLGlDQUFpQyxDQUFDO0FBQzFFLE9BQU8sRUFBRSxVQUFVLEVBQUUsTUFBTSwrQkFBK0IsQ0FBQztBQUMzRCxPQUFPLEtBQUssRUFBRSxpQkFBaUIsRUFBRSxNQUFNLDZCQUE2QixDQUFDO0FBR3JFLE9BQU8sRUFDTCxLQUFLLFlBQVksRUFDakIsS0FBSyxtQkFBbUIsRUFDeEIsS0FBSyxrQkFBa0IsRUFFdkIsS0FBSyxPQUFPLEVBQ2IsTUFBTSxtQkFBbUIsQ0FBQztBQWMzQjs7Ozs7O0dBTUc7QUFDSCxxQkFBYSxjQUFlLFlBQVcsbUJBQW1CO0lBQ3hELE9BQU8sQ0FBQyxTQUFTLENBQW9CO0lBQ3JDLE9BQU8sQ0FBQyxlQUFlLENBQXlCO0lBQ2hELE9BQU8sQ0FBQyxXQUFXLENBQTZCO0lBQ2hELE9BQU8sQ0FBQyxlQUFlLENBQWtCO0lBQ3pDLE9BQU8sQ0FBQyxJQUFJLENBQWM7SUFDMUIsT0FBTyxDQUFDLG9CQUFvQixDQUFvQjtJQUNoRCxPQUFPLENBQUMsV0FBVyxDQUFvQjtJQUN2QyxPQUFPLENBQUMseUJBQXlCLENBQVM7SUFFMUMsWUFBWSxXQUFXLEdBQUUsT0FBTyxDQUFDLGlCQUFpQixDQUFNLEVBRXZEO0lBRUQ7OztPQUdHO0lBQ0gsWUFBWSxDQUFDLFNBQVMsRUFBRSxVQUFVLEVBQUUsR0FBRyxJQUFJLENBRzFDO0lBRUQ7OztPQUdHO0lBQ0gsV0FBVyxDQUFDLFFBQVEsRUFBRSxVQUFVLEdBQUcsU0FBUyxHQUFHLElBQUksQ0FHbEQ7SUFFRDs7O09BR0c7SUFDSCxjQUFjLENBQUMsSUFBSSxFQUFFLFVBQVUsR0FBRyxJQUFJLENBR3JDO0lBRUQ7OztPQUdHO0lBQ0gsa0JBQWtCLENBQUMsSUFBSSxFQUFFLE9BQU8sR0FBRyxJQUFJLENBR3RDO0lBRUQ7OztPQUdHO0lBQ0gsT0FBTyxDQUFDLElBQUksRUFBRSxNQUFNLEdBQUcsSUFBSSxDQUcxQjtJQUVEOzs7T0FHRztJQUNILHVCQUF1QixDQUFDLFVBQVUsRUFBRSxVQUFVLEVBQUUsR0FBRyxJQUFJLENBR3REO0lBRUQ7OztPQUdHO0lBQ0gsY0FBYyxDQUFDLFNBQVMsRUFBRSxPQUFPLENBQUMsaUJBQWlCLENBQUMsR0FBRyxJQUFJLENBRzFEO0lBRUQsY0FBYyxJQUFJLGlCQUFpQixDQUVsQztJQUVELDRCQUE0QixDQUFDLE9BQU8sRUFBRSxPQUFPLEdBQUcsSUFBSSxDQUVuRDtJQUVELFlBQVksQ0FBQyxLQUFLLENBQUMsRUFBRSxPQUFPLEdBQUcsT0FBTyxDQUFDLGtCQUFrQixDQUFDLENBUXpEO0lBRUQsVUFBVSxJQUFJLFVBQVUsQ0FFdkI7SUFFRCxhQUFhLElBQUksVUFBVSxDQUkxQjtJQUVELFdBQVcsSUFBSSxXQUFXLENBRXpCO0lBRUQsY0FBYyxJQUFJLFdBQVcsQ0FFNUI7SUFFRCwyQkFBMkIsSUFBSSxPQUFPLENBRXJDO0lBRUQsZ0JBQWdCLElBQUksTUFBTSxDQUV6QjtJQUVELGtCQUFrQixJQUFJLFlBQVksR0FBRztRQUFFLEtBQUssRUFBRSxNQUFNLENBQUE7S0FBRSxDQVNyRDtJQUVELDJCQUEyQixJQUFJLFlBQVksR0FBRztRQUFFLFVBQVUsRUFBRSxNQUFNLENBQUE7S0FBRSxDQVluRTtJQUVELGlDQUFpQyxJQUFJLFlBQVksR0FBRztRQUFFLFVBQVUsRUFBRSxNQUFNLENBQUE7S0FBRSxDQUt6RTtJQUVELHdCQUF3QixDQUFDLEtBQUssRUFBRSxXQUFXLEVBQUUsSUFBSSxFQUFFLFVBQVUsRUFBRSxJQUFJLEVBQUUsTUFBTSxHQUFHLEtBQUssTUFBTSxFQUFFLENBRzFGO0lBRUQsb0JBQW9CLENBQUMsSUFBSSxFQUFFLFVBQVUsRUFBRSxNQUFNLEVBQUUsV0FBVyxFQUFFLEtBQUssRUFBRSxNQUFNLEVBQUUsSUFBSSxFQUFFLE1BQU0sR0FBRyxNQUFNLENBSy9GO0lBRUQscUJBQXFCLElBQUk7UUFBRSxXQUFXLEVBQUUsVUFBVSxDQUFDO1FBQUMsUUFBUSxFQUFFLFVBQVUsQ0FBQTtLQUFFLENBUXpFO0lBRUQsb0JBQW9CLElBQUk7UUFBRSxVQUFVLEVBQUUsVUFBVSxDQUFDO1FBQUMsUUFBUSxFQUFFLFVBQVUsQ0FBQTtLQUFFLENBUXZFO0lBRUQsZ0NBQWdDLENBQUMsS0FBSyxFQUFFLFVBQVUsR0FBRyxPQUFPLENBQUMsVUFBVSxHQUFHLFNBQVMsQ0FBQyxDQUVuRjtJQUVELHVCQUF1QixJQUFJLE9BQU8sQ0FBQyxVQUFVLEVBQUUsQ0FBQyxDQUUvQztJQUVELGFBQWEsQ0FBQyxLQUFLLEVBQUUsT0FBTyxFQUFFLFNBQVMsRUFBRSxVQUFVLEdBQUcsT0FBTyxDQUFDLE9BQU8sQ0FBQyxDQUVyRTtJQUVELGlCQUFpQixDQUFDLEtBQUssRUFBRSxPQUFPLEVBQUUsVUFBVSxFQUFFLFVBQVUsRUFBRSxHQUFHLE9BQU8sQ0FBQyxVQUFVLEVBQUUsQ0FBQyxDQUdqRjtJQUVELGlCQUFpQixDQUFDLE1BQU0sRUFBRSxXQUFXLEdBQUcsT0FBTyxDQUFDLE9BQU8sQ0FBQyxDQUV2RDtJQUVELHVCQUF1QixDQUFDLEtBQUssQ0FBQyxFQUFFLE9BQU8sR0FBRyxPQUFPLENBQUMsT0FBTyxDQUFDLENBRXpEO0NBQ0YifQ==
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test_epoch_cache.d.ts","sourceRoot":"","sources":["../../src/test/test_epoch_cache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAC1E,OAAO,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAC3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAGrE,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EAEvB,KAAK,OAAO,EACb,MAAM,mBAAmB,CAAC;AAc3B;;;;;;GAMG;AACH,qBAAa,cAAe,YAAW,mBAAmB;IACxD,OAAO,CAAC,SAAS,CAAoB;IACrC,OAAO,CAAC,eAAe,CAAyB;IAChD,OAAO,CAAC,WAAW,CAA6B;IAChD,OAAO,CAAC,eAAe,CAAkB;IACzC,OAAO,CAAC,IAAI,CAAc;IAC1B,OAAO,CAAC,oBAAoB,CAAoB;IAChD,OAAO,CAAC,WAAW,CAAoB;IACvC,OAAO,CAAC,yBAAyB,CAAS;IAE1C,YAAY,WAAW,GAAE,OAAO,CAAC,iBAAiB,CAAM,EAEvD;IAED;;;OAGG;IACH,YAAY,CAAC,SAAS,EAAE,UAAU,EAAE,GAAG,IAAI,CAG1C;IAED;;;OAGG;IACH,WAAW,CAAC,QAAQ,EAAE,UAAU,GAAG,SAAS,GAAG,IAAI,CAGlD;IAED;;;OAGG;IACH,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,CAGrC;IAED;;;OAGG;IACH,kBAAkB,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAGtC;IAED;;;OAGG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAG1B;IAED;;;OAGG;IACH,uBAAuB,CAAC,UAAU,EAAE,UAAU,EAAE,GAAG,IAAI,CAGtD;IAED;;;OAGG;IACH,cAAc,CAAC,SAAS,EAAE,OAAO,CAAC,iBAAiB,CAAC,GAAG,IAAI,CAG1D;IAED,cAAc,IAAI,iBAAiB,CAElC;IAED,4BAA4B,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAEnD;IAED,YAAY,CAAC,KAAK,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAQzD;IAED,UAAU,IAAI,UAAU,CAEvB;IAED,aAAa,IAAI,UAAU,CAI1B;IAED,WAAW,IAAI,WAAW,CAEzB;IAED,cAAc,IAAI,WAAW,CAE5B;IAED,2BAA2B,IAAI,OAAO,CAErC;IAED,kBAAkB,IAAI,YAAY,GAAG;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CASrD;IAED,2BAA2B,IAAI,YAAY,GAAG;QAAE,UAAU,EAAE,MAAM,CAAA;KAAE,CAYnE;IAED,iCAAiC,IAAI,YAAY,GAAG;QAAE,UAAU,EAAE,MAAM,CAAA;KAAE,CAKzE;IAED,wBAAwB,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,KAAK,MAAM,EAAE,CAG1F;IAED,oBAAoB,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAK/F;IAED,qBAAqB,IAAI;QAAE,WAAW,EAAE,UAAU,CAAC;QAAC,QAAQ,EAAE,UAAU,CAAA;KAAE,CAQzE;IAED,oBAAoB,IAAI;QAAE,UAAU,EAAE,UAAU,CAAC;QAAC,QAAQ,EAAE,UAAU,CAAA;KAAE,CAQvE;IAED,gCAAgC,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,CAEnF;IAED,uBAAuB,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC,CAE/C;IAED,aAAa,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,CAErE;IAED,iBAAiB,CAAC,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAGjF;IAED,iBAAiB,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,CAEvD;IAED,uBAAuB,CAAC,KAAK,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAEzD;CACF"}
|
|
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.6201a7b05",
|
|
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.6201a7b05",
|
|
30
|
+
"@aztec/foundation": "0.0.1-commit.6201a7b05",
|
|
31
|
+
"@aztec/l1-artifacts": "0.0.1-commit.6201a7b05",
|
|
32
|
+
"@aztec/stdlib": "0.0.1-commit.6201a7b05",
|
|
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
|
@@ -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';
|
|
@@ -9,8 +10,11 @@ import {
|
|
|
9
10
|
type L1RollupConstants,
|
|
10
11
|
getEpochAtSlot,
|
|
11
12
|
getEpochNumberAtTimestamp,
|
|
13
|
+
getNextL1SlotTimestamp,
|
|
14
|
+
getSlotAtNextL1Block,
|
|
12
15
|
getSlotAtTimestamp,
|
|
13
16
|
getSlotRangeForEpoch,
|
|
17
|
+
getStartTimestampForEpoch,
|
|
14
18
|
getTimestampForSlot,
|
|
15
19
|
} from '@aztec/stdlib/epoch-helpers';
|
|
16
20
|
|
|
@@ -38,6 +42,22 @@ export type EpochCommitteeInfo = {
|
|
|
38
42
|
|
|
39
43
|
export type SlotTag = 'now' | 'next' | SlotNumber;
|
|
40
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
|
+
|
|
41
61
|
export interface EpochCacheInterface {
|
|
42
62
|
getCommittee(slot: SlotTag | undefined): Promise<EpochCommitteeInfo>;
|
|
43
63
|
getSlotNow(): SlotNumber;
|
|
@@ -49,6 +69,7 @@ export interface EpochCacheInterface {
|
|
|
49
69
|
/** Returns epoch/slot info for the next L1 slot with pipeline offset applied. */
|
|
50
70
|
getTargetEpochAndSlotInNextL1Slot(): EpochAndSlot & { nowSeconds: bigint };
|
|
51
71
|
isProposerPipeliningEnabled(): boolean;
|
|
72
|
+
pipeliningOffset(): number;
|
|
52
73
|
isEscapeHatchOpen(epoch: EpochNumber): Promise<boolean>;
|
|
53
74
|
isEscapeHatchOpenAtSlot(slot: SlotTag): Promise<boolean>;
|
|
54
75
|
getProposerIndexEncoding(epoch: EpochNumber, slot: SlotNumber, seed: bigint): `0x${string}`;
|
|
@@ -72,8 +93,12 @@ export interface EpochCacheInterface {
|
|
|
72
93
|
* Note: This class is very dependent on the system clock being in sync.
|
|
73
94
|
*/
|
|
74
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
|
+
*/
|
|
75
100
|
// eslint-disable-next-line aztec-custom/no-non-primitive-in-collections
|
|
76
|
-
protected cache: Map<EpochNumber,
|
|
101
|
+
protected cache: Map<EpochNumber, CachedEpochEntry | Promise<CachedEpochEntry>> = new Map();
|
|
77
102
|
private allValidators: Set<string> = new Set();
|
|
78
103
|
private lastValidatorRefresh = 0;
|
|
79
104
|
private readonly log: Logger = createLogger('epoch-cache');
|
|
@@ -167,6 +192,10 @@ export class EpochCache implements EpochCacheInterface {
|
|
|
167
192
|
return this.enableProposerPipelining;
|
|
168
193
|
}
|
|
169
194
|
|
|
195
|
+
public pipeliningOffset(): number {
|
|
196
|
+
return this.enableProposerPipelining ? PROPOSER_PIPELINING_SLOT_OFFSET : 0;
|
|
197
|
+
}
|
|
198
|
+
|
|
170
199
|
public getSlotNow(): SlotNumber {
|
|
171
200
|
return this.getEpochAndSlotNow().slot;
|
|
172
201
|
}
|
|
@@ -191,18 +220,14 @@ export class EpochCache implements EpochCacheInterface {
|
|
|
191
220
|
return { ...this.getEpochAndSlotAtTimestamp(nowSeconds), nowMs };
|
|
192
221
|
}
|
|
193
222
|
|
|
194
|
-
public nowInSeconds(): bigint {
|
|
195
|
-
return BigInt(Math.floor(this.dateProvider.now() / 1000));
|
|
196
|
-
}
|
|
197
|
-
|
|
198
223
|
private getEpochAndSlotAtSlot(slot: SlotNumber): EpochAndSlot {
|
|
199
224
|
return this.getEpochAndSlotAtTimestamp(getTimestampForSlot(slot, this.l1constants));
|
|
200
225
|
}
|
|
201
226
|
|
|
202
227
|
public getEpochAndSlotInNextL1Slot(): EpochAndSlot & { nowSeconds: bigint } {
|
|
203
|
-
const nowSeconds = this.nowInSeconds();
|
|
204
|
-
const nextSlotTs = nowSeconds
|
|
205
|
-
return { ...this.getEpochAndSlotAtTimestamp(nextSlotTs), nowSeconds };
|
|
228
|
+
const nowSeconds = this.dateProvider.nowInSeconds();
|
|
229
|
+
const nextSlotTs = getNextL1SlotTimestamp(nowSeconds, this.l1constants);
|
|
230
|
+
return { ...this.getEpochAndSlotAtTimestamp(nextSlotTs), nowSeconds: BigInt(nowSeconds) };
|
|
206
231
|
}
|
|
207
232
|
|
|
208
233
|
public getTargetEpochAndSlotInNextL1Slot(): EpochAndSlot & { nowSeconds: bigint } {
|
|
@@ -231,17 +256,8 @@ export class EpochCache implements EpochCacheInterface {
|
|
|
231
256
|
return this.getCommittee(startSlot);
|
|
232
257
|
}
|
|
233
258
|
|
|
234
|
-
/**
|
|
235
|
-
* Returns whether the escape hatch is open for the given epoch.
|
|
236
|
-
*
|
|
237
|
-
* Uses the already-cached EpochCommitteeInfo when available. If not cached, it will fetch
|
|
238
|
-
* the epoch committee info (which includes the escape hatch flag) and return it.
|
|
239
|
-
*/
|
|
259
|
+
/** Returns whether the escape hatch is open for the given epoch. */
|
|
240
260
|
public async isEscapeHatchOpen(epoch: EpochNumber): Promise<boolean> {
|
|
241
|
-
const cached = this.cache.get(epoch);
|
|
242
|
-
if (cached) {
|
|
243
|
-
return cached.isEscapeHatchOpen;
|
|
244
|
-
}
|
|
245
261
|
const info = await this.getCommitteeForEpoch(epoch);
|
|
246
262
|
return info.isEscapeHatchOpen;
|
|
247
263
|
}
|
|
@@ -264,30 +280,49 @@ export class EpochCache implements EpochCacheInterface {
|
|
|
264
280
|
}
|
|
265
281
|
|
|
266
282
|
/**
|
|
267
|
-
* Get the current validator set
|
|
268
|
-
*
|
|
269
|
-
*
|
|
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.
|
|
270
288
|
*/
|
|
271
289
|
public async getCommittee(slot: SlotTag = 'now'): Promise<EpochCommitteeInfo> {
|
|
272
290
|
const { epoch, ts } = this.getEpochAndTimestamp(slot);
|
|
273
291
|
|
|
274
|
-
|
|
275
|
-
|
|
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;
|
|
276
297
|
}
|
|
277
298
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
return epochData;
|
|
299
|
+
// Resolved entry: return it if finalized or still fresh.
|
|
300
|
+
if (cached && (cached.finalized || !this.isStale(cached))) {
|
|
301
|
+
return cached.data;
|
|
282
302
|
}
|
|
283
|
-
this.cache.set(epoch, epochData);
|
|
284
303
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
+
}
|
|
289
316
|
|
|
290
|
-
|
|
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
|
+
}
|
|
291
326
|
}
|
|
292
327
|
|
|
293
328
|
private getEpochAndTimestamp(slot: SlotTag = 'now'): { epoch: EpochNumber; ts: bigint } {
|
|
@@ -300,22 +335,140 @@ export class EpochCache implements EpochCacheInterface {
|
|
|
300
335
|
}
|
|
301
336
|
}
|
|
302
337
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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([
|
|
306
438
|
this.rollup.getCommitteeAt(ts),
|
|
307
439
|
this.rollup.getSampleSeedAt(ts),
|
|
308
|
-
this.rollup.client.getBlock({ includeTransactions: false })
|
|
440
|
+
prefetched?.latestBlock ?? this.rollup.client.getBlock({ includeTransactions: false }),
|
|
441
|
+
prefetched !== undefined ? prefetched.finalizedBlock : getFinalizedL1Block(this.rollup.client),
|
|
309
442
|
this.rollup.isEscapeHatchOpen(epoch),
|
|
310
443
|
]);
|
|
311
|
-
|
|
312
|
-
const
|
|
313
|
-
|
|
444
|
+
|
|
445
|
+
const samplingTs = this.getSamplingTimestamp(epoch);
|
|
446
|
+
|
|
447
|
+
if (samplingTs > latestBlock.timestamp) {
|
|
314
448
|
throw new Error(
|
|
315
|
-
`Cannot query committee for future epoch ${epoch}
|
|
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.`,
|
|
316
452
|
);
|
|
317
453
|
}
|
|
318
|
-
|
|
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;
|
|
319
472
|
}
|
|
320
473
|
|
|
321
474
|
/**
|
|
@@ -351,15 +504,18 @@ export class EpochCache implements EpochCacheInterface {
|
|
|
351
504
|
};
|
|
352
505
|
}
|
|
353
506
|
|
|
354
|
-
/** Returns the
|
|
507
|
+
/** Returns the target and next L2 slot in the next L1 slot. */
|
|
355
508
|
public getTargetAndNextSlot(): { targetSlot: SlotNumber; nextSlot: SlotNumber } {
|
|
356
|
-
const
|
|
357
|
-
const
|
|
509
|
+
const nowSeconds = BigInt(this.dateProvider.nowInSeconds());
|
|
510
|
+
const offset = this.isProposerPipeliningEnabled() ? PROPOSER_PIPELINING_SLOT_OFFSET : 0;
|
|
358
511
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
512
|
+
const currentSlot = getSlotAtTimestamp(nowSeconds, this.l1constants);
|
|
513
|
+
const targetSlot = SlotNumber(currentSlot + offset);
|
|
514
|
+
|
|
515
|
+
const nextL2SlotOnL1 = getSlotAtNextL1Block(nowSeconds, this.l1constants);
|
|
516
|
+
const nextSlot = SlotNumber(nextL2SlotOnL1 + offset);
|
|
517
|
+
|
|
518
|
+
return { targetSlot, nextSlot };
|
|
363
519
|
}
|
|
364
520
|
|
|
365
521
|
/**
|
|
@@ -440,10 +596,11 @@ export class EpochCache implements EpochCacheInterface {
|
|
|
440
596
|
async getRegisteredValidators(): Promise<EthAddress[]> {
|
|
441
597
|
const validatorRefreshIntervalMs = this.config.validatorRefreshIntervalSeconds * 1000;
|
|
442
598
|
const validatorRefreshTime = this.lastValidatorRefresh + validatorRefreshIntervalMs;
|
|
443
|
-
|
|
444
|
-
|
|
599
|
+
const now = this.dateProvider.now();
|
|
600
|
+
if (validatorRefreshTime < now) {
|
|
601
|
+
const currentSet = await this.rollup.getAttesters(BigInt(Math.floor(now / 1000)));
|
|
445
602
|
this.allValidators = new Set(currentSet.map(v => v.toString()));
|
|
446
|
-
this.lastValidatorRefresh =
|
|
603
|
+
this.lastValidatorRefresh = now;
|
|
447
604
|
}
|
|
448
605
|
return Array.from(this.allValidators.keys()).map(v => EthAddress.fromString(v));
|
|
449
606
|
}
|
|
@@ -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];
|