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