@aztec/epoch-cache 5.0.0-private.20260319 → 6.0.0-nightly.20260602
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 -52
- 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 +6 -6
- package/src/epoch_cache.ts +209 -53
- 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,8 +23,10 @@ import { getEpochCacheConfigEnvVars } from './config.js';
|
|
|
22
23
|
l1constants;
|
|
23
24
|
dateProvider;
|
|
24
25
|
config;
|
|
25
|
-
|
|
26
|
-
|
|
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
|
+
*/ cache;
|
|
27
30
|
allValidators;
|
|
28
31
|
lastValidatorRefresh;
|
|
29
32
|
log;
|
|
@@ -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
|
}
|
|
@@ -121,18 +127,15 @@ import { getEpochCacheConfigEnvVars } from './config.js';
|
|
|
121
127
|
nowMs
|
|
122
128
|
};
|
|
123
129
|
}
|
|
124
|
-
nowInSeconds() {
|
|
125
|
-
return BigInt(Math.floor(this.dateProvider.now() / 1000));
|
|
126
|
-
}
|
|
127
130
|
getEpochAndSlotAtSlot(slot) {
|
|
128
131
|
return this.getEpochAndSlotAtTimestamp(getTimestampForSlot(slot, this.l1constants));
|
|
129
132
|
}
|
|
130
133
|
getEpochAndSlotInNextL1Slot() {
|
|
131
|
-
const nowSeconds = this.nowInSeconds();
|
|
132
|
-
const nextSlotTs = nowSeconds
|
|
134
|
+
const nowSeconds = this.dateProvider.nowInSeconds();
|
|
135
|
+
const nextSlotTs = getNextL1SlotTimestamp(nowSeconds, this.l1constants);
|
|
133
136
|
return {
|
|
134
137
|
...this.getEpochAndSlotAtTimestamp(nextSlotTs),
|
|
135
|
-
nowSeconds
|
|
138
|
+
nowSeconds: BigInt(nowSeconds)
|
|
136
139
|
};
|
|
137
140
|
}
|
|
138
141
|
getTargetEpochAndSlotInNextL1Slot() {
|
|
@@ -161,16 +164,7 @@ import { getEpochCacheConfigEnvVars } from './config.js';
|
|
|
161
164
|
const [startSlot] = getSlotRangeForEpoch(epoch, this.l1constants);
|
|
162
165
|
return this.getCommittee(startSlot);
|
|
163
166
|
}
|
|
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
|
-
}
|
|
167
|
+
/** Returns whether the escape hatch is open for the given epoch. */ async isEscapeHatchOpen(epoch) {
|
|
174
168
|
const info = await this.getCommitteeForEpoch(epoch);
|
|
175
169
|
return info.isEscapeHatchOpen;
|
|
176
170
|
}
|
|
@@ -184,26 +178,43 @@ import { getEpochCacheConfigEnvVars } from './config.js';
|
|
|
184
178
|
return await this.isEscapeHatchOpen(epoch);
|
|
185
179
|
}
|
|
186
180
|
/**
|
|
187
|
-
* Get the current validator set
|
|
188
|
-
*
|
|
189
|
-
*
|
|
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.
|
|
190
186
|
*/ async getCommittee(slot = 'now') {
|
|
191
187
|
const { epoch, ts } = this.getEpochAndTimestamp(slot);
|
|
192
|
-
|
|
193
|
-
|
|
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;
|
|
194
192
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
//
|
|
200
|
-
if
|
|
201
|
-
|
|
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;
|
|
202
217
|
}
|
|
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
218
|
}
|
|
208
219
|
getEpochAndTimestamp(slot = 'now') {
|
|
209
220
|
if (slot === 'now') {
|
|
@@ -214,27 +225,121 @@ import { getEpochCacheConfigEnvVars } from './config.js';
|
|
|
214
225
|
return this.getEpochAndSlotAtSlot(slot);
|
|
215
226
|
}
|
|
216
227
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
+
getFinalizedL1Block(this.rollup.client),
|
|
273
|
+
this.rollup.client.getBlock({
|
|
274
|
+
includeTransactions: false
|
|
275
|
+
})
|
|
276
|
+
]);
|
|
277
|
+
if (blockAtOriginal.hash === stale.lastQueryL1BlockHash) {
|
|
278
|
+
// No reorg: the data is still valid. Check if we can now mark it as finalized.
|
|
279
|
+
const samplingTs = this.getSamplingTimestamp(epoch);
|
|
280
|
+
const finalized = !!(stale.data.committee && stale.data.committee.length > 0) && l1FinalizedBlock !== undefined && samplingTs <= l1FinalizedBlock.timestamp;
|
|
281
|
+
const refreshed = {
|
|
282
|
+
...stale,
|
|
283
|
+
lastRefreshL1Timestamp: latestBlock.timestamp,
|
|
284
|
+
finalized
|
|
285
|
+
};
|
|
286
|
+
this.cache.set(epoch, refreshed);
|
|
287
|
+
return refreshed;
|
|
288
|
+
}
|
|
289
|
+
// Reorg detected: block hash mismatch. Do a full re-fetch.
|
|
290
|
+
// Pass the already-fetched block timestamps to avoid redundant queries.
|
|
291
|
+
this.log.warn(`L1 reorg detected for epoch ${epoch}: block ${stale.lastQueryL1BlockNumber} hash changed`, {
|
|
292
|
+
epoch,
|
|
293
|
+
expectedHash: stale.lastQueryL1BlockHash,
|
|
294
|
+
actualHash: blockAtOriginal.hash
|
|
295
|
+
});
|
|
296
|
+
return this.fetchAndCache(epoch, ts, {
|
|
297
|
+
latestBlock,
|
|
298
|
+
finalizedBlock: l1FinalizedBlock
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Fetches committee data from L1, determines finalization status, and stores in the cache.
|
|
303
|
+
*
|
|
304
|
+
* Uses `lagInEpochsForRandao` (the binding constraint, always <= lagInEpochsForValidatorSet)
|
|
305
|
+
* and computes the sampling timestamp from the epoch start to match the L1 contract's logic.
|
|
306
|
+
*
|
|
307
|
+
* When called from refreshStaleEntry after a reorg, the latest and finalized blocks are
|
|
308
|
+
* passed in to avoid redundant L1 queries.
|
|
309
|
+
*/ async fetchAndCache(epoch, ts, prefetched) {
|
|
310
|
+
const [committee, seedBuffer, latestBlock, finalizedBlock, isEscapeHatchOpen] = await Promise.all([
|
|
220
311
|
this.rollup.getCommitteeAt(ts),
|
|
221
312
|
this.rollup.getSampleSeedAt(ts),
|
|
222
|
-
this.rollup.client.getBlock({
|
|
313
|
+
prefetched?.latestBlock ?? this.rollup.client.getBlock({
|
|
223
314
|
includeTransactions: false
|
|
224
|
-
})
|
|
315
|
+
}),
|
|
316
|
+
prefetched !== undefined ? prefetched.finalizedBlock : getFinalizedL1Block(this.rollup.client),
|
|
225
317
|
this.rollup.isEscapeHatchOpen(epoch)
|
|
226
318
|
]);
|
|
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.`);
|
|
319
|
+
const samplingTs = this.getSamplingTimestamp(epoch);
|
|
320
|
+
if (samplingTs > latestBlock.timestamp) {
|
|
321
|
+
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
322
|
}
|
|
232
|
-
|
|
323
|
+
// Empty committees are never marked finalized so they always get re-queried after TTL.
|
|
324
|
+
// If L1 has no finalized block yet (devnet startup), entries stay unfinalized.
|
|
325
|
+
const hasCommittee = !!(committee && committee.length > 0);
|
|
326
|
+
const finalized = hasCommittee && finalizedBlock !== undefined && samplingTs <= finalizedBlock.timestamp;
|
|
327
|
+
const data = {
|
|
233
328
|
committee,
|
|
234
329
|
seed: seedBuffer.toBigInt(),
|
|
235
330
|
epoch,
|
|
236
331
|
isEscapeHatchOpen
|
|
237
332
|
};
|
|
333
|
+
const entry = {
|
|
334
|
+
data,
|
|
335
|
+
lastQueryL1BlockNumber: latestBlock.number,
|
|
336
|
+
lastQueryL1BlockHash: latestBlock.hash,
|
|
337
|
+
lastRefreshL1Timestamp: latestBlock.timestamp,
|
|
338
|
+
finalized
|
|
339
|
+
};
|
|
340
|
+
this.cache.set(epoch, entry);
|
|
341
|
+
this.purgeCache();
|
|
342
|
+
return entry;
|
|
238
343
|
}
|
|
239
344
|
/**
|
|
240
345
|
* Get the ABI encoding of the proposer index - see ValidatorSelectionLib.sol computeProposerIndex
|
|
@@ -273,12 +378,16 @@ import { getEpochCacheConfigEnvVars } from './config.js';
|
|
|
273
378
|
nextSlot: next.slot
|
|
274
379
|
};
|
|
275
380
|
}
|
|
276
|
-
/** Returns the
|
|
277
|
-
const
|
|
278
|
-
const
|
|
381
|
+
/** Returns the target and next L2 slot in the next L1 slot. */ getTargetAndNextSlot() {
|
|
382
|
+
const nowSeconds = BigInt(this.dateProvider.nowInSeconds());
|
|
383
|
+
const offset = this.isProposerPipeliningEnabled() ? PROPOSER_PIPELINING_SLOT_OFFSET : 0;
|
|
384
|
+
const currentSlot = getSlotAtTimestamp(nowSeconds, this.l1constants);
|
|
385
|
+
const targetSlot = SlotNumber(currentSlot + offset);
|
|
386
|
+
const nextL2SlotOnL1 = getSlotAtNextL1Block(nowSeconds, this.l1constants);
|
|
387
|
+
const nextSlot = SlotNumber(nextL2SlotOnL1 + offset);
|
|
279
388
|
return {
|
|
280
389
|
targetSlot,
|
|
281
|
-
nextSlot
|
|
390
|
+
nextSlot
|
|
282
391
|
};
|
|
283
392
|
}
|
|
284
393
|
/**
|
|
@@ -338,10 +447,11 @@ import { getEpochCacheConfigEnvVars } from './config.js';
|
|
|
338
447
|
async getRegisteredValidators() {
|
|
339
448
|
const validatorRefreshIntervalMs = this.config.validatorRefreshIntervalSeconds * 1000;
|
|
340
449
|
const validatorRefreshTime = this.lastValidatorRefresh + validatorRefreshIntervalMs;
|
|
341
|
-
|
|
342
|
-
|
|
450
|
+
const now = this.dateProvider.now();
|
|
451
|
+
if (validatorRefreshTime < now) {
|
|
452
|
+
const currentSet = await this.rollup.getAttesters(BigInt(Math.floor(now / 1000)));
|
|
343
453
|
this.allValidators = new Set(currentSet.map((v)=>v.toString()));
|
|
344
|
-
this.lastValidatorRefresh =
|
|
454
|
+
this.lastValidatorRefresh = now;
|
|
345
455
|
}
|
|
346
456
|
return Array.from(this.allValidators.keys()).map((v)=>EthAddress.fromString(v));
|
|
347
457
|
}
|
|
@@ -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": "
|
|
3
|
+
"version": "6.0.0-nightly.20260602",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./dest/index.js",
|
|
@@ -26,16 +26,16 @@
|
|
|
26
26
|
"../package.common.json"
|
|
27
27
|
],
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@aztec/ethereum": "
|
|
30
|
-
"@aztec/foundation": "
|
|
31
|
-
"@aztec/l1-artifacts": "
|
|
32
|
-
"@aztec/stdlib": "
|
|
29
|
+
"@aztec/ethereum": "6.0.0-nightly.20260602",
|
|
30
|
+
"@aztec/foundation": "6.0.0-nightly.20260602",
|
|
31
|
+
"@aztec/l1-artifacts": "6.0.0-nightly.20260602",
|
|
32
|
+
"@aztec/stdlib": "6.0.0-nightly.20260602",
|
|
33
33
|
"dotenv": "^16.0.3",
|
|
34
34
|
"get-port": "^7.1.0",
|
|
35
35
|
"jest-mock-extended": "^4.0.0",
|
|
36
36
|
"tslib": "^2.4.0",
|
|
37
37
|
"viem": "npm:@aztec/viem@2.38.2",
|
|
38
|
-
"zod": "^
|
|
38
|
+
"zod": "^4"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@jest/globals": "^30.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,11 @@ 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 {
|
|
75
|
-
|
|
76
|
-
|
|
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
|
+
*/
|
|
100
|
+
protected cache: Map<EpochNumber, CachedEpochEntry | Promise<CachedEpochEntry>> = new Map();
|
|
77
101
|
private allValidators: Set<string> = new Set();
|
|
78
102
|
private lastValidatorRefresh = 0;
|
|
79
103
|
private readonly log: Logger = createLogger('epoch-cache');
|
|
@@ -167,6 +191,10 @@ export class EpochCache implements EpochCacheInterface {
|
|
|
167
191
|
return this.enableProposerPipelining;
|
|
168
192
|
}
|
|
169
193
|
|
|
194
|
+
public pipeliningOffset(): number {
|
|
195
|
+
return this.enableProposerPipelining ? PROPOSER_PIPELINING_SLOT_OFFSET : 0;
|
|
196
|
+
}
|
|
197
|
+
|
|
170
198
|
public getSlotNow(): SlotNumber {
|
|
171
199
|
return this.getEpochAndSlotNow().slot;
|
|
172
200
|
}
|
|
@@ -191,18 +219,14 @@ export class EpochCache implements EpochCacheInterface {
|
|
|
191
219
|
return { ...this.getEpochAndSlotAtTimestamp(nowSeconds), nowMs };
|
|
192
220
|
}
|
|
193
221
|
|
|
194
|
-
public nowInSeconds(): bigint {
|
|
195
|
-
return BigInt(Math.floor(this.dateProvider.now() / 1000));
|
|
196
|
-
}
|
|
197
|
-
|
|
198
222
|
private getEpochAndSlotAtSlot(slot: SlotNumber): EpochAndSlot {
|
|
199
223
|
return this.getEpochAndSlotAtTimestamp(getTimestampForSlot(slot, this.l1constants));
|
|
200
224
|
}
|
|
201
225
|
|
|
202
226
|
public getEpochAndSlotInNextL1Slot(): EpochAndSlot & { nowSeconds: bigint } {
|
|
203
|
-
const nowSeconds = this.nowInSeconds();
|
|
204
|
-
const nextSlotTs = nowSeconds
|
|
205
|
-
return { ...this.getEpochAndSlotAtTimestamp(nextSlotTs), nowSeconds };
|
|
227
|
+
const nowSeconds = this.dateProvider.nowInSeconds();
|
|
228
|
+
const nextSlotTs = getNextL1SlotTimestamp(nowSeconds, this.l1constants);
|
|
229
|
+
return { ...this.getEpochAndSlotAtTimestamp(nextSlotTs), nowSeconds: BigInt(nowSeconds) };
|
|
206
230
|
}
|
|
207
231
|
|
|
208
232
|
public getTargetEpochAndSlotInNextL1Slot(): EpochAndSlot & { nowSeconds: bigint } {
|
|
@@ -231,17 +255,8 @@ export class EpochCache implements EpochCacheInterface {
|
|
|
231
255
|
return this.getCommittee(startSlot);
|
|
232
256
|
}
|
|
233
257
|
|
|
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
|
-
*/
|
|
258
|
+
/** Returns whether the escape hatch is open for the given epoch. */
|
|
240
259
|
public async isEscapeHatchOpen(epoch: EpochNumber): Promise<boolean> {
|
|
241
|
-
const cached = this.cache.get(epoch);
|
|
242
|
-
if (cached) {
|
|
243
|
-
return cached.isEscapeHatchOpen;
|
|
244
|
-
}
|
|
245
260
|
const info = await this.getCommitteeForEpoch(epoch);
|
|
246
261
|
return info.isEscapeHatchOpen;
|
|
247
262
|
}
|
|
@@ -264,30 +279,49 @@ export class EpochCache implements EpochCacheInterface {
|
|
|
264
279
|
}
|
|
265
280
|
|
|
266
281
|
/**
|
|
267
|
-
* Get the current validator set
|
|
268
|
-
*
|
|
269
|
-
*
|
|
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.
|
|
270
287
|
*/
|
|
271
288
|
public async getCommittee(slot: SlotTag = 'now'): Promise<EpochCommitteeInfo> {
|
|
272
289
|
const { epoch, ts } = this.getEpochAndTimestamp(slot);
|
|
273
290
|
|
|
274
|
-
|
|
275
|
-
|
|
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;
|
|
276
296
|
}
|
|
277
297
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
return epochData;
|
|
298
|
+
// Resolved entry: return it if finalized or still fresh.
|
|
299
|
+
if (cached && (cached.finalized || !this.isStale(cached))) {
|
|
300
|
+
return cached.data;
|
|
282
301
|
}
|
|
283
|
-
this.cache.set(epoch, epochData);
|
|
284
302
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
+
}
|
|
289
315
|
|
|
290
|
-
|
|
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
|
+
}
|
|
291
325
|
}
|
|
292
326
|
|
|
293
327
|
private getEpochAndTimestamp(slot: SlotTag = 'now'): { epoch: EpochNumber; ts: bigint } {
|
|
@@ -300,22 +334,140 @@ export class EpochCache implements EpochCacheInterface {
|
|
|
300
334
|
}
|
|
301
335
|
}
|
|
302
336
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
+
getFinalizedL1Block(this.rollup.client),
|
|
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) &&
|
|
400
|
+
l1FinalizedBlock !== undefined &&
|
|
401
|
+
samplingTs <= l1FinalizedBlock.timestamp;
|
|
402
|
+
|
|
403
|
+
const refreshed: CachedEpochEntry = {
|
|
404
|
+
...stale,
|
|
405
|
+
lastRefreshL1Timestamp: latestBlock.timestamp,
|
|
406
|
+
finalized,
|
|
407
|
+
};
|
|
408
|
+
this.cache.set(epoch, refreshed);
|
|
409
|
+
return refreshed;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Reorg detected: block hash mismatch. Do a full re-fetch.
|
|
413
|
+
// Pass the already-fetched block timestamps to avoid redundant queries.
|
|
414
|
+
this.log.warn(`L1 reorg detected for epoch ${epoch}: block ${stale.lastQueryL1BlockNumber} hash changed`, {
|
|
415
|
+
epoch,
|
|
416
|
+
expectedHash: stale.lastQueryL1BlockHash,
|
|
417
|
+
actualHash: blockAtOriginal.hash,
|
|
418
|
+
});
|
|
419
|
+
return this.fetchAndCache(epoch, ts, { latestBlock, finalizedBlock: l1FinalizedBlock });
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Fetches committee data from L1, determines finalization status, and stores in the cache.
|
|
424
|
+
*
|
|
425
|
+
* Uses `lagInEpochsForRandao` (the binding constraint, always <= lagInEpochsForValidatorSet)
|
|
426
|
+
* and computes the sampling timestamp from the epoch start to match the L1 contract's logic.
|
|
427
|
+
*
|
|
428
|
+
* When called from refreshStaleEntry after a reorg, the latest and finalized blocks are
|
|
429
|
+
* passed in to avoid redundant L1 queries.
|
|
430
|
+
*/
|
|
431
|
+
private async fetchAndCache(
|
|
432
|
+
epoch: EpochNumber,
|
|
433
|
+
ts: bigint,
|
|
434
|
+
prefetched?: { latestBlock: L1BlockInfo; finalizedBlock: { timestamp: bigint } | undefined },
|
|
435
|
+
): Promise<CachedEpochEntry> {
|
|
436
|
+
const [committee, seedBuffer, latestBlock, finalizedBlock, isEscapeHatchOpen] = await Promise.all([
|
|
306
437
|
this.rollup.getCommitteeAt(ts),
|
|
307
438
|
this.rollup.getSampleSeedAt(ts),
|
|
308
|
-
this.rollup.client.getBlock({ includeTransactions: false })
|
|
439
|
+
prefetched?.latestBlock ?? this.rollup.client.getBlock({ includeTransactions: false }),
|
|
440
|
+
prefetched !== undefined ? prefetched.finalizedBlock : getFinalizedL1Block(this.rollup.client),
|
|
309
441
|
this.rollup.isEscapeHatchOpen(epoch),
|
|
310
442
|
]);
|
|
311
|
-
|
|
312
|
-
const
|
|
313
|
-
|
|
443
|
+
|
|
444
|
+
const samplingTs = this.getSamplingTimestamp(epoch);
|
|
445
|
+
|
|
446
|
+
if (samplingTs > latestBlock.timestamp) {
|
|
314
447
|
throw new Error(
|
|
315
|
-
`Cannot query committee for future epoch ${epoch}
|
|
448
|
+
`Cannot query committee for future epoch ${epoch}: ` +
|
|
449
|
+
`sampling timestamp ${samplingTs} is beyond latest L1 block at ${latestBlock.timestamp}. ` +
|
|
450
|
+
`Check your Ethereum node is synced.`,
|
|
316
451
|
);
|
|
317
452
|
}
|
|
318
|
-
|
|
453
|
+
|
|
454
|
+
// Empty committees are never marked finalized so they always get re-queried after TTL.
|
|
455
|
+
// If L1 has no finalized block yet (devnet startup), entries stay unfinalized.
|
|
456
|
+
const hasCommittee = !!(committee && committee.length > 0);
|
|
457
|
+
const finalized = hasCommittee && finalizedBlock !== undefined && samplingTs <= finalizedBlock.timestamp;
|
|
458
|
+
const data: EpochCommitteeInfo = { committee, seed: seedBuffer.toBigInt(), epoch, isEscapeHatchOpen };
|
|
459
|
+
const entry: CachedEpochEntry = {
|
|
460
|
+
data,
|
|
461
|
+
lastQueryL1BlockNumber: latestBlock.number!,
|
|
462
|
+
lastQueryL1BlockHash: latestBlock.hash!,
|
|
463
|
+
lastRefreshL1Timestamp: latestBlock.timestamp,
|
|
464
|
+
finalized,
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
this.cache.set(epoch, entry);
|
|
468
|
+
this.purgeCache();
|
|
469
|
+
|
|
470
|
+
return entry;
|
|
319
471
|
}
|
|
320
472
|
|
|
321
473
|
/**
|
|
@@ -351,15 +503,18 @@ export class EpochCache implements EpochCacheInterface {
|
|
|
351
503
|
};
|
|
352
504
|
}
|
|
353
505
|
|
|
354
|
-
/** Returns the
|
|
506
|
+
/** Returns the target and next L2 slot in the next L1 slot. */
|
|
355
507
|
public getTargetAndNextSlot(): { targetSlot: SlotNumber; nextSlot: SlotNumber } {
|
|
356
|
-
const
|
|
357
|
-
const
|
|
508
|
+
const nowSeconds = BigInt(this.dateProvider.nowInSeconds());
|
|
509
|
+
const offset = this.isProposerPipeliningEnabled() ? PROPOSER_PIPELINING_SLOT_OFFSET : 0;
|
|
358
510
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
511
|
+
const currentSlot = getSlotAtTimestamp(nowSeconds, this.l1constants);
|
|
512
|
+
const targetSlot = SlotNumber(currentSlot + offset);
|
|
513
|
+
|
|
514
|
+
const nextL2SlotOnL1 = getSlotAtNextL1Block(nowSeconds, this.l1constants);
|
|
515
|
+
const nextSlot = SlotNumber(nextL2SlotOnL1 + offset);
|
|
516
|
+
|
|
517
|
+
return { targetSlot, nextSlot };
|
|
363
518
|
}
|
|
364
519
|
|
|
365
520
|
/**
|
|
@@ -440,10 +595,11 @@ export class EpochCache implements EpochCacheInterface {
|
|
|
440
595
|
async getRegisteredValidators(): Promise<EthAddress[]> {
|
|
441
596
|
const validatorRefreshIntervalMs = this.config.validatorRefreshIntervalSeconds * 1000;
|
|
442
597
|
const validatorRefreshTime = this.lastValidatorRefresh + validatorRefreshIntervalMs;
|
|
443
|
-
|
|
444
|
-
|
|
598
|
+
const now = this.dateProvider.now();
|
|
599
|
+
if (validatorRefreshTime < now) {
|
|
600
|
+
const currentSet = await this.rollup.getAttesters(BigInt(Math.floor(now / 1000)));
|
|
445
601
|
this.allValidators = new Set(currentSet.map(v => v.toString()));
|
|
446
|
-
this.lastValidatorRefresh =
|
|
602
|
+
this.lastValidatorRefresh = now;
|
|
447
603
|
}
|
|
448
604
|
return Array.from(this.allValidators.keys()).map(v => EthAddress.fromString(v));
|
|
449
605
|
}
|
|
@@ -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];
|