@aztec/epoch-cache 0.0.1-commit.b655e406 → 0.0.1-commit.b6e433891
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/dest/config.d.ts +5 -3
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +5 -2
- package/dest/epoch_cache.d.ts +86 -43
- package/dest/epoch_cache.d.ts.map +1 -1
- package/dest/epoch_cache.js +127 -46
- package/dest/index.d.ts +1 -1
- package/dest/test/index.d.ts +2 -0
- package/dest/test/index.d.ts.map +1 -0
- package/dest/test/index.js +1 -0
- package/dest/test/test_epoch_cache.d.ts +91 -0
- package/dest/test/test_epoch_cache.d.ts.map +1 -0
- package/dest/test/test_epoch_cache.js +195 -0
- package/package.json +10 -10
- package/src/config.ts +11 -9
- package/src/epoch_cache.ts +189 -67
- package/src/test/index.ts +1 -0
- package/src/test/test_epoch_cache.ts +238 -0
package/src/epoch_cache.ts
CHANGED
|
@@ -1,51 +1,65 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createEthereumChain } from '@aztec/ethereum/chain';
|
|
2
|
+
import { makeL1HttpTransport } from '@aztec/ethereum/client';
|
|
3
|
+
import { NoCommitteeError, RollupContract } from '@aztec/ethereum/contracts';
|
|
4
|
+
import { EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
2
5
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
3
6
|
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
4
7
|
import { DateProvider } from '@aztec/foundation/timer';
|
|
5
8
|
import {
|
|
6
|
-
EmptyL1RollupConstants,
|
|
7
9
|
type L1RollupConstants,
|
|
8
10
|
getEpochAtSlot,
|
|
9
11
|
getEpochNumberAtTimestamp,
|
|
10
12
|
getSlotAtTimestamp,
|
|
11
13
|
getSlotRangeForEpoch,
|
|
12
14
|
getTimestampForSlot,
|
|
13
|
-
getTimestampRangeForEpoch,
|
|
14
15
|
} from '@aztec/stdlib/epoch-helpers';
|
|
15
16
|
|
|
16
|
-
import { createPublicClient, encodeAbiParameters,
|
|
17
|
+
import { createPublicClient, encodeAbiParameters, keccak256 } from 'viem';
|
|
17
18
|
|
|
18
19
|
import { type EpochCacheConfig, getEpochCacheConfigEnvVars } from './config.js';
|
|
19
20
|
|
|
21
|
+
/** When proposer pipelining is enabled, the proposer builds one slot ahead. */
|
|
22
|
+
export const PROPOSER_PIPELINING_SLOT_OFFSET = 1;
|
|
23
|
+
|
|
24
|
+
/** Flat return type for compound epoch/slot getters. */
|
|
20
25
|
export type EpochAndSlot = {
|
|
21
|
-
|
|
22
|
-
|
|
26
|
+
slot: SlotNumber;
|
|
27
|
+
epoch: EpochNumber;
|
|
23
28
|
ts: bigint;
|
|
24
29
|
};
|
|
25
30
|
|
|
26
31
|
export type EpochCommitteeInfo = {
|
|
27
32
|
committee: EthAddress[] | undefined;
|
|
28
33
|
seed: bigint;
|
|
29
|
-
epoch:
|
|
34
|
+
epoch: EpochNumber;
|
|
35
|
+
/** True if the epoch is within an open escape hatch window. */
|
|
36
|
+
isEscapeHatchOpen: boolean;
|
|
30
37
|
};
|
|
31
38
|
|
|
32
|
-
export type SlotTag = 'now' | 'next' |
|
|
39
|
+
export type SlotTag = 'now' | 'next' | SlotNumber;
|
|
33
40
|
|
|
34
41
|
export interface EpochCacheInterface {
|
|
35
42
|
getCommittee(slot: SlotTag | undefined): Promise<EpochCommitteeInfo>;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
getSlotNow(): SlotNumber;
|
|
44
|
+
getTargetSlot(): SlotNumber;
|
|
45
|
+
getEpochNow(): EpochNumber;
|
|
46
|
+
getTargetEpoch(): EpochNumber;
|
|
47
|
+
getEpochAndSlotNow(): EpochAndSlot & { nowMs: bigint };
|
|
48
|
+
getEpochAndSlotInNextL1Slot(): EpochAndSlot & { nowSeconds: bigint };
|
|
49
|
+
/** Returns epoch/slot info for the next L1 slot with pipeline offset applied. */
|
|
50
|
+
getTargetEpochAndSlotInNextL1Slot(): EpochAndSlot & { nowSeconds: bigint };
|
|
51
|
+
isProposerPipeliningEnabled(): boolean;
|
|
52
|
+
isEscapeHatchOpen(epoch: EpochNumber): Promise<boolean>;
|
|
53
|
+
isEscapeHatchOpenAtSlot(slot: SlotTag): Promise<boolean>;
|
|
54
|
+
getProposerIndexEncoding(epoch: EpochNumber, slot: SlotNumber, seed: bigint): `0x${string}`;
|
|
55
|
+
computeProposerIndex(slot: SlotNumber, epoch: EpochNumber, seed: bigint, size: bigint): bigint;
|
|
56
|
+
getCurrentAndNextSlot(): { currentSlot: SlotNumber; nextSlot: SlotNumber };
|
|
57
|
+
getTargetAndNextSlot(): { targetSlot: SlotNumber; nextSlot: SlotNumber };
|
|
58
|
+
getProposerAttesterAddressInSlot(slot: SlotNumber): Promise<EthAddress | undefined>;
|
|
46
59
|
getRegisteredValidators(): Promise<EthAddress[]>;
|
|
47
60
|
isInCommittee(slot: SlotTag, validator: EthAddress): Promise<boolean>;
|
|
48
61
|
filterInCommittee(slot: SlotTag, validators: EthAddress[]): Promise<EthAddress[]>;
|
|
62
|
+
getL1Constants(): L1RollupConstants;
|
|
49
63
|
}
|
|
50
64
|
|
|
51
65
|
/**
|
|
@@ -58,19 +72,27 @@ export interface EpochCacheInterface {
|
|
|
58
72
|
* Note: This class is very dependent on the system clock being in sync.
|
|
59
73
|
*/
|
|
60
74
|
export class EpochCache implements EpochCacheInterface {
|
|
61
|
-
|
|
75
|
+
// eslint-disable-next-line aztec-custom/no-non-primitive-in-collections
|
|
76
|
+
protected cache: Map<EpochNumber, EpochCommitteeInfo> = new Map();
|
|
62
77
|
private allValidators: Set<string> = new Set();
|
|
63
78
|
private lastValidatorRefresh = 0;
|
|
64
79
|
private readonly log: Logger = createLogger('epoch-cache');
|
|
65
80
|
|
|
81
|
+
protected enableProposerPipelining: boolean;
|
|
82
|
+
|
|
66
83
|
constructor(
|
|
67
84
|
private rollup: RollupContract,
|
|
68
|
-
private readonly l1constants: L1RollupConstants
|
|
85
|
+
private readonly l1constants: L1RollupConstants & {
|
|
86
|
+
lagInEpochsForValidatorSet: number;
|
|
87
|
+
lagInEpochsForRandao: number;
|
|
88
|
+
},
|
|
69
89
|
private readonly dateProvider: DateProvider = new DateProvider(),
|
|
70
|
-
protected readonly config = { cacheSize: 12, validatorRefreshIntervalSeconds: 60 },
|
|
90
|
+
protected readonly config = { cacheSize: 12, validatorRefreshIntervalSeconds: 60, enableProposerPipelining: false },
|
|
71
91
|
) {
|
|
92
|
+
this.enableProposerPipelining = this.config.enableProposerPipelining;
|
|
72
93
|
this.log.debug(`Initialized EpochCache`, {
|
|
73
94
|
l1constants,
|
|
95
|
+
enableProposerPipelining: this.enableProposerPipelining,
|
|
74
96
|
});
|
|
75
97
|
}
|
|
76
98
|
|
|
@@ -89,71 +111,158 @@ export class EpochCache implements EpochCacheInterface {
|
|
|
89
111
|
const chain = createEthereumChain(config.l1RpcUrls, config.l1ChainId);
|
|
90
112
|
const publicClient = createPublicClient({
|
|
91
113
|
chain: chain.chainInfo,
|
|
92
|
-
transport:
|
|
114
|
+
transport: makeL1HttpTransport(config.l1RpcUrls, { timeout: config.l1HttpTimeoutMS }),
|
|
93
115
|
pollingInterval: config.viemPollingIntervalMS,
|
|
94
116
|
});
|
|
95
117
|
rollup = new RollupContract(publicClient, rollupOrAddress.toString());
|
|
96
118
|
}
|
|
97
119
|
|
|
98
|
-
const [
|
|
120
|
+
const [
|
|
121
|
+
l1StartBlock,
|
|
122
|
+
l1GenesisTime,
|
|
123
|
+
proofSubmissionEpochs,
|
|
124
|
+
slotDuration,
|
|
125
|
+
epochDuration,
|
|
126
|
+
lagInEpochsForValidatorSet,
|
|
127
|
+
lagInEpochsForRandao,
|
|
128
|
+
targetCommitteeSize,
|
|
129
|
+
rollupManaLimit,
|
|
130
|
+
] = await Promise.all([
|
|
99
131
|
rollup.getL1StartBlock(),
|
|
100
132
|
rollup.getL1GenesisTime(),
|
|
101
133
|
rollup.getProofSubmissionEpochs(),
|
|
102
134
|
rollup.getSlotDuration(),
|
|
103
135
|
rollup.getEpochDuration(),
|
|
136
|
+
rollup.getLagInEpochsForValidatorSet(),
|
|
137
|
+
rollup.getLagInEpochsForRandao(),
|
|
138
|
+
rollup.getTargetCommitteeSize(),
|
|
139
|
+
rollup.getManaLimit(),
|
|
104
140
|
] as const);
|
|
105
141
|
|
|
106
|
-
const l1RollupConstants
|
|
142
|
+
const l1RollupConstants = {
|
|
107
143
|
l1StartBlock,
|
|
108
144
|
l1GenesisTime,
|
|
109
145
|
proofSubmissionEpochs: Number(proofSubmissionEpochs),
|
|
110
146
|
slotDuration: Number(slotDuration),
|
|
111
147
|
epochDuration: Number(epochDuration),
|
|
112
148
|
ethereumSlotDuration: config.ethereumSlotDuration,
|
|
149
|
+
lagInEpochsForValidatorSet: Number(lagInEpochsForValidatorSet),
|
|
150
|
+
lagInEpochsForRandao: Number(lagInEpochsForRandao),
|
|
151
|
+
targetCommitteeSize: Number(targetCommitteeSize),
|
|
152
|
+
rollupManaLimit: Number(rollupManaLimit),
|
|
113
153
|
};
|
|
114
154
|
|
|
115
|
-
return new EpochCache(rollup, l1RollupConstants, deps.dateProvider
|
|
155
|
+
return new EpochCache(rollup, l1RollupConstants, deps.dateProvider, {
|
|
156
|
+
cacheSize: 12,
|
|
157
|
+
validatorRefreshIntervalSeconds: 60,
|
|
158
|
+
enableProposerPipelining: config.enableProposerPipelining,
|
|
159
|
+
});
|
|
116
160
|
}
|
|
117
161
|
|
|
118
162
|
public getL1Constants(): L1RollupConstants {
|
|
119
163
|
return this.l1constants;
|
|
120
164
|
}
|
|
121
165
|
|
|
122
|
-
public
|
|
123
|
-
|
|
124
|
-
|
|
166
|
+
public isProposerPipeliningEnabled(): boolean {
|
|
167
|
+
return this.enableProposerPipelining;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
public getSlotNow(): SlotNumber {
|
|
171
|
+
return this.getEpochAndSlotNow().slot;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
public getTargetSlot(): SlotNumber {
|
|
175
|
+
const slotNow = this.getSlotNow();
|
|
176
|
+
const offset = this.isProposerPipeliningEnabled() ? PROPOSER_PIPELINING_SLOT_OFFSET : 0;
|
|
177
|
+
return SlotNumber(slotNow + offset);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
public getEpochNow(): EpochNumber {
|
|
181
|
+
return this.getEpochAndSlotNow().epoch;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
public getTargetEpoch(): EpochNumber {
|
|
185
|
+
return getEpochAtSlot(this.getTargetSlot(), this.l1constants);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
public getEpochAndSlotNow(): EpochAndSlot & { nowMs: bigint } {
|
|
189
|
+
const nowMs = BigInt(this.dateProvider.now());
|
|
190
|
+
const nowSeconds = nowMs / 1000n;
|
|
191
|
+
return { ...this.getEpochAndSlotAtTimestamp(nowSeconds), nowMs };
|
|
125
192
|
}
|
|
126
193
|
|
|
127
194
|
public nowInSeconds(): bigint {
|
|
128
195
|
return BigInt(Math.floor(this.dateProvider.now() / 1000));
|
|
129
196
|
}
|
|
130
197
|
|
|
131
|
-
private getEpochAndSlotAtSlot(slot:
|
|
132
|
-
|
|
133
|
-
const ts = getTimestampRangeForEpoch(epoch, this.l1constants)[0];
|
|
134
|
-
return { epoch, ts, slot };
|
|
198
|
+
private getEpochAndSlotAtSlot(slot: SlotNumber): EpochAndSlot {
|
|
199
|
+
return this.getEpochAndSlotAtTimestamp(getTimestampForSlot(slot, this.l1constants));
|
|
135
200
|
}
|
|
136
201
|
|
|
137
|
-
public getEpochAndSlotInNextL1Slot(): EpochAndSlot & {
|
|
138
|
-
const
|
|
139
|
-
const nextSlotTs =
|
|
140
|
-
return { ...this.getEpochAndSlotAtTimestamp(nextSlotTs),
|
|
202
|
+
public getEpochAndSlotInNextL1Slot(): EpochAndSlot & { nowSeconds: bigint } {
|
|
203
|
+
const nowSeconds = this.nowInSeconds();
|
|
204
|
+
const nextSlotTs = nowSeconds + BigInt(this.l1constants.ethereumSlotDuration);
|
|
205
|
+
return { ...this.getEpochAndSlotAtTimestamp(nextSlotTs), nowSeconds };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
public getTargetEpochAndSlotInNextL1Slot(): EpochAndSlot & { nowSeconds: bigint } {
|
|
209
|
+
if (!this.isProposerPipeliningEnabled()) {
|
|
210
|
+
return this.getEpochAndSlotInNextL1Slot();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const result = this.getEpochAndSlotInNextL1Slot();
|
|
214
|
+
const offset = PROPOSER_PIPELINING_SLOT_OFFSET;
|
|
215
|
+
const targetSlot = SlotNumber(result.slot + offset);
|
|
216
|
+
return { ...result, slot: targetSlot, epoch: getEpochAtSlot(targetSlot, this.l1constants) };
|
|
141
217
|
}
|
|
142
218
|
|
|
143
219
|
private getEpochAndSlotAtTimestamp(ts: bigint): EpochAndSlot {
|
|
144
220
|
const slot = getSlotAtTimestamp(ts, this.l1constants);
|
|
221
|
+
const epoch = getEpochNumberAtTimestamp(ts, this.l1constants);
|
|
145
222
|
return {
|
|
146
|
-
epoch: getEpochNumberAtTimestamp(ts, this.l1constants),
|
|
147
|
-
ts: getTimestampForSlot(slot, this.l1constants),
|
|
148
223
|
slot,
|
|
224
|
+
epoch,
|
|
225
|
+
ts: getTimestampForSlot(slot, this.l1constants),
|
|
149
226
|
};
|
|
150
227
|
}
|
|
151
228
|
|
|
152
|
-
public getCommitteeForEpoch(epoch:
|
|
229
|
+
public getCommitteeForEpoch(epoch: EpochNumber): Promise<EpochCommitteeInfo> {
|
|
153
230
|
const [startSlot] = getSlotRangeForEpoch(epoch, this.l1constants);
|
|
154
231
|
return this.getCommittee(startSlot);
|
|
155
232
|
}
|
|
156
233
|
|
|
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
|
+
*/
|
|
240
|
+
public async isEscapeHatchOpen(epoch: EpochNumber): Promise<boolean> {
|
|
241
|
+
const cached = this.cache.get(epoch);
|
|
242
|
+
if (cached) {
|
|
243
|
+
return cached.isEscapeHatchOpen;
|
|
244
|
+
}
|
|
245
|
+
const info = await this.getCommitteeForEpoch(epoch);
|
|
246
|
+
return info.isEscapeHatchOpen;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Returns whether the escape hatch is open for the epoch containing the given slot.
|
|
251
|
+
*
|
|
252
|
+
* This is a lightweight helper intended for callers that already have a slot number and only
|
|
253
|
+
* need the escape hatch flag (without pulling full committee info).
|
|
254
|
+
*/
|
|
255
|
+
public async isEscapeHatchOpenAtSlot(slot: SlotTag = 'now'): Promise<boolean> {
|
|
256
|
+
const epoch =
|
|
257
|
+
slot === 'now'
|
|
258
|
+
? this.getEpochNow()
|
|
259
|
+
: slot === 'next'
|
|
260
|
+
? this.getEpochAndSlotInNextL1Slot().epoch
|
|
261
|
+
: getEpochAtSlot(slot, this.l1constants);
|
|
262
|
+
|
|
263
|
+
return await this.isEscapeHatchOpen(epoch);
|
|
264
|
+
}
|
|
265
|
+
|
|
157
266
|
/**
|
|
158
267
|
* Get the current validator set
|
|
159
268
|
* @param nextSlot - If true, get the validator set for the next slot.
|
|
@@ -181,7 +290,7 @@ export class EpochCache implements EpochCacheInterface {
|
|
|
181
290
|
return epochData;
|
|
182
291
|
}
|
|
183
292
|
|
|
184
|
-
private getEpochAndTimestamp(slot: SlotTag = 'now') {
|
|
293
|
+
private getEpochAndTimestamp(slot: SlotTag = 'now'): { epoch: EpochNumber; ts: bigint } {
|
|
185
294
|
if (slot === 'now') {
|
|
186
295
|
return this.getEpochAndSlotNow();
|
|
187
296
|
} else if (slot === 'next') {
|
|
@@ -191,28 +300,39 @@ export class EpochCache implements EpochCacheInterface {
|
|
|
191
300
|
}
|
|
192
301
|
}
|
|
193
302
|
|
|
194
|
-
private async computeCommittee(when: { epoch:
|
|
303
|
+
private async computeCommittee(when: { epoch: EpochNumber; ts: bigint }): Promise<EpochCommitteeInfo> {
|
|
195
304
|
const { ts, epoch } = when;
|
|
196
|
-
const [
|
|
197
|
-
|
|
198
|
-
|
|
305
|
+
const [committee, seedBuffer, l1Timestamp, isEscapeHatchOpen] = await Promise.all([
|
|
306
|
+
this.rollup.getCommitteeAt(ts),
|
|
307
|
+
this.rollup.getSampleSeedAt(ts),
|
|
308
|
+
this.rollup.client.getBlock({ includeTransactions: false }).then(b => b.timestamp),
|
|
309
|
+
this.rollup.isEscapeHatchOpen(epoch),
|
|
310
|
+
]);
|
|
311
|
+
const { lagInEpochsForValidatorSet, epochDuration, slotDuration } = this.l1constants;
|
|
312
|
+
const sub = BigInt(lagInEpochsForValidatorSet) * BigInt(epochDuration) * BigInt(slotDuration);
|
|
313
|
+
if (ts - sub > l1Timestamp) {
|
|
314
|
+
throw new Error(
|
|
315
|
+
`Cannot query committee for future epoch ${epoch} with timestamp ${ts} (current L1 time is ${l1Timestamp}). Check your Ethereum node is synced.`,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
return { committee, seed: seedBuffer.toBigInt(), epoch, isEscapeHatchOpen };
|
|
199
319
|
}
|
|
200
320
|
|
|
201
321
|
/**
|
|
202
322
|
* Get the ABI encoding of the proposer index - see ValidatorSelectionLib.sol computeProposerIndex
|
|
203
323
|
*/
|
|
204
|
-
getProposerIndexEncoding(epoch:
|
|
324
|
+
getProposerIndexEncoding(epoch: EpochNumber, slot: SlotNumber, seed: bigint): `0x${string}` {
|
|
205
325
|
return encodeAbiParameters(
|
|
206
326
|
[
|
|
207
327
|
{ type: 'uint256', name: 'epoch' },
|
|
208
328
|
{ type: 'uint256', name: 'slot' },
|
|
209
329
|
{ type: 'uint256', name: 'seed' },
|
|
210
330
|
],
|
|
211
|
-
[epoch, slot, seed],
|
|
331
|
+
[BigInt(epoch), BigInt(slot), seed],
|
|
212
332
|
);
|
|
213
333
|
}
|
|
214
334
|
|
|
215
|
-
public computeProposerIndex(slot:
|
|
335
|
+
public computeProposerIndex(slot: SlotNumber, epoch: EpochNumber, seed: bigint, size: bigint): bigint {
|
|
216
336
|
// if committe size is 0, then mod 1 is 0
|
|
217
337
|
if (size === 0n) {
|
|
218
338
|
return 0n;
|
|
@@ -220,25 +340,24 @@ export class EpochCache implements EpochCacheInterface {
|
|
|
220
340
|
return BigInt(keccak256(this.getProposerIndexEncoding(epoch, slot, seed))) % size;
|
|
221
341
|
}
|
|
222
342
|
|
|
223
|
-
/**
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
* We return the next proposer's attester address as the node will check if it is the proposer at the next ethereum block,
|
|
227
|
-
* which can be the next slot. If this is the case, then it will send proposals early.
|
|
228
|
-
*/
|
|
229
|
-
public async getProposerAttesterAddressInCurrentOrNextSlot(): Promise<{
|
|
230
|
-
currentSlot: bigint;
|
|
231
|
-
nextSlot: bigint;
|
|
232
|
-
currentProposer: EthAddress | undefined;
|
|
233
|
-
nextProposer: EthAddress | undefined;
|
|
234
|
-
}> {
|
|
235
|
-
const current = this.getEpochAndSlotNow();
|
|
343
|
+
/** Returns the current and next L2 slot in next eth L1 Slot. */
|
|
344
|
+
public getCurrentAndNextSlot(): { currentSlot: SlotNumber; nextSlot: SlotNumber } {
|
|
345
|
+
const currentSlot = this.getSlotNow();
|
|
236
346
|
const next = this.getEpochAndSlotInNextL1Slot();
|
|
237
347
|
|
|
238
348
|
return {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
349
|
+
currentSlot,
|
|
350
|
+
nextSlot: next.slot,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Returns the taget and next L2 slot in the next L1 slot */
|
|
355
|
+
public getTargetAndNextSlot(): { targetSlot: SlotNumber; nextSlot: SlotNumber } {
|
|
356
|
+
const targetSlot = this.getTargetSlot();
|
|
357
|
+
const next = this.getTargetEpochAndSlotInNextL1Slot();
|
|
358
|
+
|
|
359
|
+
return {
|
|
360
|
+
targetSlot,
|
|
242
361
|
nextSlot: next.slot,
|
|
243
362
|
};
|
|
244
363
|
}
|
|
@@ -248,7 +367,7 @@ export class EpochCache implements EpochCacheInterface {
|
|
|
248
367
|
* @returns The proposer attester address. If the committee does not exist, we throw a NoCommitteeError.
|
|
249
368
|
* If the committee is empty (i.e. target committee size is 0, and anyone can propose), we return undefined.
|
|
250
369
|
*/
|
|
251
|
-
public getProposerAttesterAddressInSlot(slot:
|
|
370
|
+
public getProposerAttesterAddressInSlot(slot: SlotNumber): Promise<EthAddress | undefined> {
|
|
252
371
|
const epochAndSlot = this.getEpochAndSlotAtSlot(slot);
|
|
253
372
|
return this.getProposerAttesterAddressAt(epochAndSlot);
|
|
254
373
|
}
|
|
@@ -282,7 +401,10 @@ export class EpochCache implements EpochCacheInterface {
|
|
|
282
401
|
return committee[Number(proposerIndex)];
|
|
283
402
|
}
|
|
284
403
|
|
|
285
|
-
public getProposerFromEpochCommittee(
|
|
404
|
+
public getProposerFromEpochCommittee(
|
|
405
|
+
epochCommitteeInfo: EpochCommitteeInfo,
|
|
406
|
+
slot: SlotNumber,
|
|
407
|
+
): EthAddress | undefined {
|
|
286
408
|
if (!epochCommitteeInfo.committee || epochCommitteeInfo.committee.length === 0) {
|
|
287
409
|
return undefined;
|
|
288
410
|
}
|
|
@@ -320,9 +442,9 @@ export class EpochCache implements EpochCacheInterface {
|
|
|
320
442
|
const validatorRefreshTime = this.lastValidatorRefresh + validatorRefreshIntervalMs;
|
|
321
443
|
if (validatorRefreshTime < this.dateProvider.now()) {
|
|
322
444
|
const currentSet = await this.rollup.getAttesters();
|
|
323
|
-
this.allValidators = new Set(currentSet);
|
|
445
|
+
this.allValidators = new Set(currentSet.map(v => v.toString()));
|
|
324
446
|
this.lastValidatorRefresh = this.dateProvider.now();
|
|
325
447
|
}
|
|
326
|
-
return Array.from(this.allValidators.keys().map(v => EthAddress.fromString(v))
|
|
448
|
+
return Array.from(this.allValidators.keys()).map(v => EthAddress.fromString(v));
|
|
327
449
|
}
|
|
328
450
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './test_epoch_cache.js';
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
2
|
+
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
3
|
+
import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers';
|
|
4
|
+
import { getEpochAtSlot, getSlotAtTimestamp, getTimestampRangeForEpoch } from '@aztec/stdlib/epoch-helpers';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
type EpochAndSlot,
|
|
8
|
+
type EpochCacheInterface,
|
|
9
|
+
type EpochCommitteeInfo,
|
|
10
|
+
PROPOSER_PIPELINING_SLOT_OFFSET,
|
|
11
|
+
type SlotTag,
|
|
12
|
+
} from '../epoch_cache.js';
|
|
13
|
+
|
|
14
|
+
/** Default L1 constants for testing. */
|
|
15
|
+
const DEFAULT_L1_CONSTANTS: L1RollupConstants = {
|
|
16
|
+
l1StartBlock: 0n,
|
|
17
|
+
l1GenesisTime: 0n,
|
|
18
|
+
slotDuration: 24,
|
|
19
|
+
epochDuration: 16,
|
|
20
|
+
ethereumSlotDuration: 12,
|
|
21
|
+
proofSubmissionEpochs: 2,
|
|
22
|
+
targetCommitteeSize: 48,
|
|
23
|
+
rollupManaLimit: Number.MAX_SAFE_INTEGER,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A test implementation of EpochCacheInterface that allows manual configuration
|
|
28
|
+
* of committee, proposer, slot, and escape hatch state for use in tests.
|
|
29
|
+
*
|
|
30
|
+
* Unlike the real EpochCache, this class doesn't require any RPC connections
|
|
31
|
+
* or mock setup. Simply use the setter methods to configure the test state.
|
|
32
|
+
*/
|
|
33
|
+
export class TestEpochCache implements EpochCacheInterface {
|
|
34
|
+
private committee: EthAddress[] = [];
|
|
35
|
+
private proposerAddress: EthAddress | undefined;
|
|
36
|
+
private currentSlot: SlotNumber = SlotNumber(0);
|
|
37
|
+
private escapeHatchOpen: boolean = false;
|
|
38
|
+
private seed: bigint = 0n;
|
|
39
|
+
private registeredValidators: EthAddress[] = [];
|
|
40
|
+
private l1Constants: L1RollupConstants;
|
|
41
|
+
private proposerPipeliningEnabled = false;
|
|
42
|
+
|
|
43
|
+
constructor(l1Constants: Partial<L1RollupConstants> = {}) {
|
|
44
|
+
this.l1Constants = { ...DEFAULT_L1_CONSTANTS, ...l1Constants };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Sets the committee members. Used in validation and attestation flows.
|
|
49
|
+
* @param committee - Array of committee member addresses.
|
|
50
|
+
*/
|
|
51
|
+
setCommittee(committee: EthAddress[]): this {
|
|
52
|
+
this.committee = committee;
|
|
53
|
+
return this;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Sets the proposer address returned by getProposerAttesterAddressInSlot.
|
|
58
|
+
* @param proposer - The address of the current proposer.
|
|
59
|
+
*/
|
|
60
|
+
setProposer(proposer: EthAddress | undefined): this {
|
|
61
|
+
this.proposerAddress = proposer;
|
|
62
|
+
return this;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Sets the current slot number.
|
|
67
|
+
* @param slot - The slot number to set.
|
|
68
|
+
*/
|
|
69
|
+
setCurrentSlot(slot: SlotNumber): this {
|
|
70
|
+
this.currentSlot = slot;
|
|
71
|
+
return this;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Sets whether the escape hatch is open.
|
|
76
|
+
* @param open - True if escape hatch should be open.
|
|
77
|
+
*/
|
|
78
|
+
setEscapeHatchOpen(open: boolean): this {
|
|
79
|
+
this.escapeHatchOpen = open;
|
|
80
|
+
return this;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Sets the randomness seed used for proposer selection.
|
|
85
|
+
* @param seed - The seed value.
|
|
86
|
+
*/
|
|
87
|
+
setSeed(seed: bigint): this {
|
|
88
|
+
this.seed = seed;
|
|
89
|
+
return this;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Sets the list of registered validators (all validators, not just committee).
|
|
94
|
+
* @param validators - Array of validator addresses.
|
|
95
|
+
*/
|
|
96
|
+
setRegisteredValidators(validators: EthAddress[]): this {
|
|
97
|
+
this.registeredValidators = validators;
|
|
98
|
+
return this;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Sets the L1 constants used for epoch/slot calculations.
|
|
103
|
+
* @param constants - Partial constants to override defaults.
|
|
104
|
+
*/
|
|
105
|
+
setL1Constants(constants: Partial<L1RollupConstants>): this {
|
|
106
|
+
this.l1Constants = { ...this.l1Constants, ...constants };
|
|
107
|
+
return this;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
getL1Constants(): L1RollupConstants {
|
|
111
|
+
return this.l1Constants;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
setProposerPipeliningEnabled(enabled: boolean): void {
|
|
115
|
+
this.proposerPipeliningEnabled = enabled;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
getCommittee(_slot?: SlotTag): Promise<EpochCommitteeInfo> {
|
|
119
|
+
const epoch = getEpochAtSlot(this.currentSlot, this.l1Constants);
|
|
120
|
+
return Promise.resolve({
|
|
121
|
+
committee: this.committee,
|
|
122
|
+
epoch,
|
|
123
|
+
seed: this.seed,
|
|
124
|
+
isEscapeHatchOpen: this.escapeHatchOpen,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
getSlotNow(): SlotNumber {
|
|
129
|
+
return this.currentSlot;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
getTargetSlot(): SlotNumber {
|
|
133
|
+
return this.proposerPipeliningEnabled
|
|
134
|
+
? SlotNumber(this.currentSlot + PROPOSER_PIPELINING_SLOT_OFFSET)
|
|
135
|
+
: this.currentSlot;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
getEpochNow(): EpochNumber {
|
|
139
|
+
return getEpochAtSlot(this.currentSlot, this.l1Constants);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
getTargetEpoch(): EpochNumber {
|
|
143
|
+
return getEpochAtSlot(this.getTargetSlot(), this.l1Constants);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
isProposerPipeliningEnabled(): boolean {
|
|
147
|
+
return this.proposerPipeliningEnabled;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
getEpochAndSlotNow(): EpochAndSlot & { nowMs: bigint } {
|
|
151
|
+
const epochNow = getEpochAtSlot(this.currentSlot, this.l1Constants);
|
|
152
|
+
const ts = getTimestampRangeForEpoch(epochNow, this.l1Constants)[0];
|
|
153
|
+
return {
|
|
154
|
+
epoch: epochNow,
|
|
155
|
+
slot: this.currentSlot,
|
|
156
|
+
ts,
|
|
157
|
+
nowMs: ts * 1000n,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
getEpochAndSlotInNextL1Slot(): EpochAndSlot & { nowSeconds: bigint } {
|
|
162
|
+
const nowTs = getTimestampRangeForEpoch(getEpochAtSlot(this.currentSlot, this.l1Constants), this.l1Constants)[0];
|
|
163
|
+
const nextSlotTs = nowTs + BigInt(this.l1Constants.ethereumSlotDuration);
|
|
164
|
+
const nextSlot = getSlotAtTimestamp(nextSlotTs, this.l1Constants);
|
|
165
|
+
const epochNow = getEpochAtSlot(nextSlot, this.l1Constants);
|
|
166
|
+
const ts = getTimestampRangeForEpoch(epochNow, this.l1Constants)[0];
|
|
167
|
+
return {
|
|
168
|
+
epoch: epochNow,
|
|
169
|
+
slot: nextSlot,
|
|
170
|
+
ts,
|
|
171
|
+
nowSeconds: nowTs,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
getTargetEpochAndSlotInNextL1Slot(): EpochAndSlot & { nowSeconds: bigint } {
|
|
176
|
+
const result = this.getEpochAndSlotInNextL1Slot();
|
|
177
|
+
const offset = this.isProposerPipeliningEnabled() ? PROPOSER_PIPELINING_SLOT_OFFSET : 0;
|
|
178
|
+
const targetSlot = SlotNumber(result.slot + offset);
|
|
179
|
+
return { ...result, slot: targetSlot, epoch: getEpochAtSlot(targetSlot, this.l1Constants) };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
getProposerIndexEncoding(epoch: EpochNumber, slot: SlotNumber, seed: bigint): `0x${string}` {
|
|
183
|
+
// Simple encoding for testing purposes
|
|
184
|
+
return `0x${epoch.toString(16).padStart(64, '0')}${slot.toString(16).padStart(64, '0')}${seed.toString(16).padStart(64, '0')}`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
computeProposerIndex(slot: SlotNumber, _epoch: EpochNumber, _seed: bigint, size: bigint): bigint {
|
|
188
|
+
if (size === 0n) {
|
|
189
|
+
return 0n;
|
|
190
|
+
}
|
|
191
|
+
return BigInt(slot) % size;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
getCurrentAndNextSlot(): { currentSlot: SlotNumber; nextSlot: SlotNumber } {
|
|
195
|
+
const currentSlot = this.getSlotNow();
|
|
196
|
+
const next = this.getEpochAndSlotInNextL1Slot();
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
currentSlot,
|
|
200
|
+
nextSlot: next.slot,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
getTargetAndNextSlot(): { targetSlot: SlotNumber; nextSlot: SlotNumber } {
|
|
205
|
+
const targetSlot = this.getTargetSlot();
|
|
206
|
+
const next = this.getTargetEpochAndSlotInNextL1Slot();
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
targetSlot,
|
|
210
|
+
nextSlot: next.slot,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
getProposerAttesterAddressInSlot(_slot: SlotNumber): Promise<EthAddress | undefined> {
|
|
215
|
+
return Promise.resolve(this.proposerAddress);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
getRegisteredValidators(): Promise<EthAddress[]> {
|
|
219
|
+
return Promise.resolve(this.registeredValidators);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
isInCommittee(_slot: SlotTag, validator: EthAddress): Promise<boolean> {
|
|
223
|
+
return Promise.resolve(this.committee.some(v => v.equals(validator)));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
filterInCommittee(_slot: SlotTag, validators: EthAddress[]): Promise<EthAddress[]> {
|
|
227
|
+
const committeeSet = new Set(this.committee.map(v => v.toString()));
|
|
228
|
+
return Promise.resolve(validators.filter(v => committeeSet.has(v.toString())));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
isEscapeHatchOpen(_epoch: EpochNumber): Promise<boolean> {
|
|
232
|
+
return Promise.resolve(this.escapeHatchOpen);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
isEscapeHatchOpenAtSlot(_slot?: SlotTag): Promise<boolean> {
|
|
236
|
+
return Promise.resolve(this.escapeHatchOpen);
|
|
237
|
+
}
|
|
238
|
+
}
|