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