@aztec/epoch-cache 0.0.1-commit.9b94fc1 → 0.0.1-commit.9ee6fcc6

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.
@@ -0,0 +1,195 @@
1
+ import { SlotNumber } from '@aztec/foundation/branded-types';
2
+ import { getEpochAtSlot, getSlotAtTimestamp, getTimestampRangeForEpoch } from '@aztec/stdlib/epoch-helpers';
3
+ import { PROPOSER_PIPELINING_SLOT_OFFSET } from '../epoch_cache.js';
4
+ /** Default L1 constants for testing. */ const DEFAULT_L1_CONSTANTS = {
5
+ l1StartBlock: 0n,
6
+ l1GenesisTime: 0n,
7
+ slotDuration: 24,
8
+ epochDuration: 16,
9
+ ethereumSlotDuration: 12,
10
+ proofSubmissionEpochs: 2,
11
+ targetCommitteeSize: 48,
12
+ rollupManaLimit: Number.MAX_SAFE_INTEGER
13
+ };
14
+ /**
15
+ * A test implementation of EpochCacheInterface that allows manual configuration
16
+ * of committee, proposer, slot, and escape hatch state for use in tests.
17
+ *
18
+ * Unlike the real EpochCache, this class doesn't require any RPC connections
19
+ * or mock setup. Simply use the setter methods to configure the test state.
20
+ */ export class TestEpochCache {
21
+ committee = [];
22
+ proposerAddress;
23
+ currentSlot = SlotNumber(0);
24
+ escapeHatchOpen = false;
25
+ seed = 0n;
26
+ registeredValidators = [];
27
+ l1Constants;
28
+ proposerPipeliningEnabled = false;
29
+ constructor(l1Constants = {}){
30
+ this.l1Constants = {
31
+ ...DEFAULT_L1_CONSTANTS,
32
+ ...l1Constants
33
+ };
34
+ }
35
+ /**
36
+ * Sets the committee members. Used in validation and attestation flows.
37
+ * @param committee - Array of committee member addresses.
38
+ */ setCommittee(committee) {
39
+ this.committee = committee;
40
+ return this;
41
+ }
42
+ /**
43
+ * Sets the proposer address returned by getProposerAttesterAddressInSlot.
44
+ * @param proposer - The address of the current proposer.
45
+ */ setProposer(proposer) {
46
+ this.proposerAddress = proposer;
47
+ return this;
48
+ }
49
+ /**
50
+ * Sets the current slot number.
51
+ * @param slot - The slot number to set.
52
+ */ setCurrentSlot(slot) {
53
+ this.currentSlot = slot;
54
+ return this;
55
+ }
56
+ /**
57
+ * Sets whether the escape hatch is open.
58
+ * @param open - True if escape hatch should be open.
59
+ */ setEscapeHatchOpen(open) {
60
+ this.escapeHatchOpen = open;
61
+ return this;
62
+ }
63
+ /**
64
+ * Sets the randomness seed used for proposer selection.
65
+ * @param seed - The seed value.
66
+ */ setSeed(seed) {
67
+ this.seed = seed;
68
+ return this;
69
+ }
70
+ /**
71
+ * Sets the list of registered validators (all validators, not just committee).
72
+ * @param validators - Array of validator addresses.
73
+ */ setRegisteredValidators(validators) {
74
+ this.registeredValidators = validators;
75
+ return this;
76
+ }
77
+ /**
78
+ * Sets the L1 constants used for epoch/slot calculations.
79
+ * @param constants - Partial constants to override defaults.
80
+ */ setL1Constants(constants) {
81
+ this.l1Constants = {
82
+ ...this.l1Constants,
83
+ ...constants
84
+ };
85
+ return this;
86
+ }
87
+ getL1Constants() {
88
+ return this.l1Constants;
89
+ }
90
+ setProposerPipeliningEnabled(enabled) {
91
+ this.proposerPipeliningEnabled = enabled;
92
+ }
93
+ getCommittee(_slot) {
94
+ const epoch = getEpochAtSlot(this.currentSlot, this.l1Constants);
95
+ return Promise.resolve({
96
+ committee: this.committee,
97
+ epoch,
98
+ seed: this.seed,
99
+ isEscapeHatchOpen: this.escapeHatchOpen
100
+ });
101
+ }
102
+ getSlotNow() {
103
+ return this.currentSlot;
104
+ }
105
+ getTargetSlot() {
106
+ return this.proposerPipeliningEnabled ? SlotNumber(this.currentSlot + PROPOSER_PIPELINING_SLOT_OFFSET) : this.currentSlot;
107
+ }
108
+ getEpochNow() {
109
+ return getEpochAtSlot(this.currentSlot, this.l1Constants);
110
+ }
111
+ getTargetEpoch() {
112
+ return getEpochAtSlot(this.getTargetSlot(), this.l1Constants);
113
+ }
114
+ isProposerPipeliningEnabled() {
115
+ return this.proposerPipeliningEnabled;
116
+ }
117
+ getEpochAndSlotNow() {
118
+ const epochNow = getEpochAtSlot(this.currentSlot, this.l1Constants);
119
+ const ts = getTimestampRangeForEpoch(epochNow, this.l1Constants)[0];
120
+ return {
121
+ epoch: epochNow,
122
+ slot: this.currentSlot,
123
+ ts,
124
+ nowMs: ts * 1000n
125
+ };
126
+ }
127
+ getEpochAndSlotInNextL1Slot() {
128
+ const nowTs = getTimestampRangeForEpoch(getEpochAtSlot(this.currentSlot, this.l1Constants), this.l1Constants)[0];
129
+ const nextSlotTs = nowTs + BigInt(this.l1Constants.ethereumSlotDuration);
130
+ const nextSlot = getSlotAtTimestamp(nextSlotTs, this.l1Constants);
131
+ const epochNow = getEpochAtSlot(nextSlot, this.l1Constants);
132
+ const ts = getTimestampRangeForEpoch(epochNow, this.l1Constants)[0];
133
+ return {
134
+ epoch: epochNow,
135
+ slot: nextSlot,
136
+ ts,
137
+ nowSeconds: nowTs
138
+ };
139
+ }
140
+ getTargetEpochAndSlotInNextL1Slot() {
141
+ const result = this.getEpochAndSlotInNextL1Slot();
142
+ const offset = this.isProposerPipeliningEnabled() ? PROPOSER_PIPELINING_SLOT_OFFSET : 0;
143
+ const targetSlot = SlotNumber(result.slot + offset);
144
+ return {
145
+ ...result,
146
+ slot: targetSlot,
147
+ epoch: getEpochAtSlot(targetSlot, this.l1Constants)
148
+ };
149
+ }
150
+ getProposerIndexEncoding(epoch, slot, seed) {
151
+ // Simple encoding for testing purposes
152
+ return `0x${epoch.toString(16).padStart(64, '0')}${slot.toString(16).padStart(64, '0')}${seed.toString(16).padStart(64, '0')}`;
153
+ }
154
+ computeProposerIndex(slot, _epoch, _seed, size) {
155
+ if (size === 0n) {
156
+ return 0n;
157
+ }
158
+ return BigInt(slot) % size;
159
+ }
160
+ getCurrentAndNextSlot() {
161
+ const currentSlot = this.getSlotNow();
162
+ const next = this.getEpochAndSlotInNextL1Slot();
163
+ return {
164
+ currentSlot,
165
+ nextSlot: next.slot
166
+ };
167
+ }
168
+ getTargetAndNextSlot() {
169
+ const targetSlot = this.getTargetSlot();
170
+ const next = this.getTargetEpochAndSlotInNextL1Slot();
171
+ return {
172
+ targetSlot,
173
+ nextSlot: next.slot
174
+ };
175
+ }
176
+ getProposerAttesterAddressInSlot(_slot) {
177
+ return Promise.resolve(this.proposerAddress);
178
+ }
179
+ getRegisteredValidators() {
180
+ return Promise.resolve(this.registeredValidators);
181
+ }
182
+ isInCommittee(_slot, validator) {
183
+ return Promise.resolve(this.committee.some((v)=>v.equals(validator)));
184
+ }
185
+ filterInCommittee(_slot, validators) {
186
+ const committeeSet = new Set(this.committee.map((v)=>v.toString()));
187
+ return Promise.resolve(validators.filter((v)=>committeeSet.has(v.toString())));
188
+ }
189
+ isEscapeHatchOpen(_epoch) {
190
+ return Promise.resolve(this.escapeHatchOpen);
191
+ }
192
+ isEscapeHatchOpenAtSlot(_slot) {
193
+ return Promise.resolve(this.escapeHatchOpen);
194
+ }
195
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aztec/epoch-cache",
3
- "version": "0.0.1-commit.9b94fc1",
3
+ "version": "0.0.1-commit.9ee6fcc6",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dest/index.js",
@@ -15,10 +15,10 @@
15
15
  "tsconfig": "./tsconfig.json"
16
16
  },
17
17
  "scripts": {
18
- "build": "yarn clean && tsgo -b",
19
- "build:dev": "tsgo -b --watch",
18
+ "build": "yarn clean && ../scripts/tsc.sh",
19
+ "build:dev": "../scripts/tsc.sh --watch",
20
20
  "clean": "rm -rf ./dest .tsbuildinfo",
21
- "start:dev": "concurrently -k \"tsgo -b -w\" \"nodemon --watch dest --exec yarn start\"",
21
+ "start:dev": "concurrently -k \"../scripts/tsc.sh --watch\" \"nodemon --watch dest --exec yarn start\"",
22
22
  "start": "node ./dest/index.js",
23
23
  "test": "NODE_NO_WARNINGS=1 node --experimental-vm-modules ../node_modules/.bin/jest --passWithNoTests --maxWorkers=${JEST_MAX_WORKERS:-8}"
24
24
  },
@@ -26,11 +26,10 @@
26
26
  "../package.common.json"
27
27
  ],
28
28
  "dependencies": {
29
- "@aztec/ethereum": "0.0.1-commit.9b94fc1",
30
- "@aztec/foundation": "0.0.1-commit.9b94fc1",
31
- "@aztec/l1-artifacts": "0.0.1-commit.9b94fc1",
32
- "@aztec/stdlib": "0.0.1-commit.9b94fc1",
33
- "@viem/anvil": "^0.0.10",
29
+ "@aztec/ethereum": "0.0.1-commit.9ee6fcc6",
30
+ "@aztec/foundation": "0.0.1-commit.9ee6fcc6",
31
+ "@aztec/l1-artifacts": "0.0.1-commit.9ee6fcc6",
32
+ "@aztec/stdlib": "0.0.1-commit.9ee6fcc6",
34
33
  "dotenv": "^16.0.3",
35
34
  "get-port": "^7.1.0",
36
35
  "jest-mock-extended": "^4.0.0",
@@ -42,7 +41,7 @@
42
41
  "@jest/globals": "^30.0.0",
43
42
  "@types/jest": "^30.0.0",
44
43
  "@types/node": "^22.15.17",
45
- "@typescript/native-preview": "7.0.0-dev.20251126.1",
44
+ "@typescript/native-preview": "7.0.0-dev.20260113.1",
46
45
  "jest": "^30.0.0",
47
46
  "ts-node": "^10.9.1",
48
47
  "typescript": "^5.3.3"
package/src/config.ts CHANGED
@@ -1,15 +1,17 @@
1
- import {
2
- type L1ContractsConfig,
3
- type L1ReaderConfig,
4
- getL1ContractsConfigEnvVars,
5
- getL1ReaderConfigFromEnv,
6
- } from '@aztec/ethereum';
1
+ import { type L1ContractsConfig, getL1ContractsConfigEnvVars } from '@aztec/ethereum/config';
2
+ import { type L1ReaderConfig, getL1ReaderConfigFromEnv } from '@aztec/ethereum/l1-reader';
3
+ import { type PipelineConfig, getPipelineConfigEnvVars } from '@aztec/stdlib/config';
7
4
 
8
5
  export type EpochCacheConfig = Pick<
9
- L1ReaderConfig & L1ContractsConfig,
10
- 'l1RpcUrls' | 'l1ChainId' | 'viemPollingIntervalMS' | 'ethereumSlotDuration'
6
+ L1ReaderConfig & L1ContractsConfig & PipelineConfig,
7
+ | 'l1RpcUrls'
8
+ | 'l1ChainId'
9
+ | 'viemPollingIntervalMS'
10
+ | 'ethereumSlotDuration'
11
+ | 'l1HttpTimeoutMS'
12
+ | 'enableProposerPipelining'
11
13
  >;
12
14
 
13
15
  export function getEpochCacheConfigEnvVars(): EpochCacheConfig {
14
- return { ...getL1ReaderConfigFromEnv(), ...getL1ContractsConfigEnvVars() };
16
+ return { ...getL1ReaderConfigFromEnv(), ...getL1ContractsConfigEnvVars(), ...getPipelineConfigEnvVars() };
15
17
  }
@@ -1,4 +1,6 @@
1
- import { NoCommitteeError, RollupContract, createEthereumChain } from '@aztec/ethereum';
1
+ import { createEthereumChain } from '@aztec/ethereum/chain';
2
+ import { makeL1HttpTransport } from '@aztec/ethereum/client';
3
+ import { NoCommitteeError, RollupContract } from '@aztec/ethereum/contracts';
2
4
  import { EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
3
5
  import { EthAddress } from '@aztec/foundation/eth-address';
4
6
  import { type Logger, createLogger } from '@aztec/foundation/log';
@@ -7,19 +9,23 @@ import {
7
9
  type L1RollupConstants,
8
10
  getEpochAtSlot,
9
11
  getEpochNumberAtTimestamp,
12
+ getNextL1SlotTimestamp,
10
13
  getSlotAtTimestamp,
11
14
  getSlotRangeForEpoch,
12
15
  getTimestampForSlot,
13
- getTimestampRangeForEpoch,
14
16
  } from '@aztec/stdlib/epoch-helpers';
15
17
 
16
- import { createPublicClient, encodeAbiParameters, fallback, http, keccak256 } from 'viem';
18
+ import { createPublicClient, encodeAbiParameters, keccak256 } from 'viem';
17
19
 
18
20
  import { type EpochCacheConfig, getEpochCacheConfigEnvVars } from './config.js';
19
21
 
22
+ /** When proposer pipelining is enabled, the proposer builds one slot ahead. */
23
+ export const PROPOSER_PIPELINING_SLOT_OFFSET = 1;
24
+
25
+ /** Flat return type for compound epoch/slot getters. */
20
26
  export type EpochAndSlot = {
21
- epoch: EpochNumber;
22
27
  slot: SlotNumber;
28
+ epoch: EpochNumber;
23
29
  ts: bigint;
24
30
  };
25
31
 
@@ -27,25 +33,34 @@ export type EpochCommitteeInfo = {
27
33
  committee: EthAddress[] | undefined;
28
34
  seed: bigint;
29
35
  epoch: EpochNumber;
36
+ /** True if the epoch is within an open escape hatch window. */
37
+ isEscapeHatchOpen: boolean;
30
38
  };
31
39
 
32
40
  export type SlotTag = 'now' | 'next' | SlotNumber;
33
41
 
34
42
  export interface EpochCacheInterface {
35
43
  getCommittee(slot: SlotTag | undefined): Promise<EpochCommitteeInfo>;
36
- getEpochAndSlotNow(): EpochAndSlot;
37
- getEpochAndSlotInNextL1Slot(): EpochAndSlot & { now: bigint };
44
+ getSlotNow(): SlotNumber;
45
+ getTargetSlot(): SlotNumber;
46
+ getEpochNow(): EpochNumber;
47
+ getTargetEpoch(): EpochNumber;
48
+ getEpochAndSlotNow(): EpochAndSlot & { nowMs: bigint };
49
+ getEpochAndSlotInNextL1Slot(): EpochAndSlot & { nowSeconds: bigint };
50
+ /** Returns epoch/slot info for the next L1 slot with pipeline offset applied. */
51
+ getTargetEpochAndSlotInNextL1Slot(): EpochAndSlot & { nowSeconds: bigint };
52
+ isProposerPipeliningEnabled(): boolean;
53
+ isEscapeHatchOpen(epoch: EpochNumber): Promise<boolean>;
54
+ isEscapeHatchOpenAtSlot(slot: SlotTag): Promise<boolean>;
38
55
  getProposerIndexEncoding(epoch: EpochNumber, slot: SlotNumber, seed: bigint): `0x${string}`;
39
56
  computeProposerIndex(slot: SlotNumber, epoch: EpochNumber, seed: bigint, size: bigint): bigint;
40
- getProposerAttesterAddressInCurrentOrNextSlot(): Promise<{
41
- currentProposer: EthAddress | undefined;
42
- nextProposer: EthAddress | undefined;
43
- currentSlot: SlotNumber;
44
- nextSlot: SlotNumber;
45
- }>;
57
+ getCurrentAndNextSlot(): { currentSlot: SlotNumber; nextSlot: SlotNumber };
58
+ getTargetAndNextSlot(): { targetSlot: SlotNumber; nextSlot: SlotNumber };
59
+ getProposerAttesterAddressInSlot(slot: SlotNumber): Promise<EthAddress | undefined>;
46
60
  getRegisteredValidators(): Promise<EthAddress[]>;
47
61
  isInCommittee(slot: SlotTag, validator: EthAddress): Promise<boolean>;
48
62
  filterInCommittee(slot: SlotTag, validators: EthAddress[]): Promise<EthAddress[]>;
63
+ getL1Constants(): L1RollupConstants;
49
64
  }
50
65
 
51
66
  /**
@@ -64,6 +79,8 @@ export class EpochCache implements EpochCacheInterface {
64
79
  private lastValidatorRefresh = 0;
65
80
  private readonly log: Logger = createLogger('epoch-cache');
66
81
 
82
+ protected enableProposerPipelining: boolean;
83
+
67
84
  constructor(
68
85
  private rollup: RollupContract,
69
86
  private readonly l1constants: L1RollupConstants & {
@@ -71,10 +88,12 @@ export class EpochCache implements EpochCacheInterface {
71
88
  lagInEpochsForRandao: number;
72
89
  },
73
90
  private readonly dateProvider: DateProvider = new DateProvider(),
74
- protected readonly config = { cacheSize: 12, validatorRefreshIntervalSeconds: 60 },
91
+ protected readonly config = { cacheSize: 12, validatorRefreshIntervalSeconds: 60, enableProposerPipelining: false },
75
92
  ) {
93
+ this.enableProposerPipelining = this.config.enableProposerPipelining;
76
94
  this.log.debug(`Initialized EpochCache`, {
77
95
  l1constants,
96
+ enableProposerPipelining: this.enableProposerPipelining,
78
97
  });
79
98
  }
80
99
 
@@ -93,7 +112,7 @@ export class EpochCache implements EpochCacheInterface {
93
112
  const chain = createEthereumChain(config.l1RpcUrls, config.l1ChainId);
94
113
  const publicClient = createPublicClient({
95
114
  chain: chain.chainInfo,
96
- transport: fallback(config.l1RpcUrls.map(url => http(url))),
115
+ transport: makeL1HttpTransport(config.l1RpcUrls, { timeout: config.l1HttpTimeoutMS }),
97
116
  pollingInterval: config.viemPollingIntervalMS,
98
117
  });
99
118
  rollup = new RollupContract(publicClient, rollupOrAddress.toString());
@@ -107,6 +126,8 @@ export class EpochCache implements EpochCacheInterface {
107
126
  epochDuration,
108
127
  lagInEpochsForValidatorSet,
109
128
  lagInEpochsForRandao,
129
+ targetCommitteeSize,
130
+ rollupManaLimit,
110
131
  ] = await Promise.all([
111
132
  rollup.getL1StartBlock(),
112
133
  rollup.getL1GenesisTime(),
@@ -115,6 +136,8 @@ export class EpochCache implements EpochCacheInterface {
115
136
  rollup.getEpochDuration(),
116
137
  rollup.getLagInEpochsForValidatorSet(),
117
138
  rollup.getLagInEpochsForRandao(),
139
+ rollup.getTargetCommitteeSize(),
140
+ rollup.getManaLimit(),
118
141
  ] as const);
119
142
 
120
143
  const l1RollupConstants = {
@@ -126,42 +149,77 @@ export class EpochCache implements EpochCacheInterface {
126
149
  ethereumSlotDuration: config.ethereumSlotDuration,
127
150
  lagInEpochsForValidatorSet: Number(lagInEpochsForValidatorSet),
128
151
  lagInEpochsForRandao: Number(lagInEpochsForRandao),
152
+ targetCommitteeSize: Number(targetCommitteeSize),
153
+ rollupManaLimit: Number(rollupManaLimit),
129
154
  };
130
155
 
131
- return new EpochCache(rollup, l1RollupConstants, deps.dateProvider);
156
+ return new EpochCache(rollup, l1RollupConstants, deps.dateProvider, {
157
+ cacheSize: 12,
158
+ validatorRefreshIntervalSeconds: 60,
159
+ enableProposerPipelining: config.enableProposerPipelining,
160
+ });
132
161
  }
133
162
 
134
163
  public getL1Constants(): L1RollupConstants {
135
164
  return this.l1constants;
136
165
  }
137
166
 
138
- public getEpochAndSlotNow(): EpochAndSlot & { now: bigint } {
139
- const now = this.nowInSeconds();
140
- return { ...this.getEpochAndSlotAtTimestamp(now), now };
167
+ public isProposerPipeliningEnabled(): boolean {
168
+ return this.enableProposerPipelining;
169
+ }
170
+
171
+ public getSlotNow(): SlotNumber {
172
+ return this.getEpochAndSlotNow().slot;
173
+ }
174
+
175
+ public getTargetSlot(): SlotNumber {
176
+ const slotNow = this.getSlotNow();
177
+ const offset = this.isProposerPipeliningEnabled() ? PROPOSER_PIPELINING_SLOT_OFFSET : 0;
178
+ return SlotNumber(slotNow + offset);
179
+ }
180
+
181
+ public getEpochNow(): EpochNumber {
182
+ return this.getEpochAndSlotNow().epoch;
183
+ }
184
+
185
+ public getTargetEpoch(): EpochNumber {
186
+ return getEpochAtSlot(this.getTargetSlot(), this.l1constants);
141
187
  }
142
188
 
143
- public nowInSeconds(): bigint {
144
- return BigInt(Math.floor(this.dateProvider.now() / 1000));
189
+ public getEpochAndSlotNow(): EpochAndSlot & { nowMs: bigint } {
190
+ const nowMs = BigInt(this.dateProvider.now());
191
+ const nowSeconds = nowMs / 1000n;
192
+ return { ...this.getEpochAndSlotAtTimestamp(nowSeconds), nowMs };
145
193
  }
146
194
 
147
195
  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 };
196
+ return this.getEpochAndSlotAtTimestamp(getTimestampForSlot(slot, this.l1constants));
197
+ }
198
+
199
+ public getEpochAndSlotInNextL1Slot(): EpochAndSlot & { nowSeconds: bigint } {
200
+ const nowSeconds = this.dateProvider.nowInSeconds();
201
+ const nextSlotTs = getNextL1SlotTimestamp(nowSeconds, this.l1constants);
202
+ return { ...this.getEpochAndSlotAtTimestamp(nextSlotTs), nowSeconds: BigInt(nowSeconds) };
151
203
  }
152
204
 
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 };
205
+ public getTargetEpochAndSlotInNextL1Slot(): EpochAndSlot & { nowSeconds: bigint } {
206
+ if (!this.isProposerPipeliningEnabled()) {
207
+ return this.getEpochAndSlotInNextL1Slot();
208
+ }
209
+
210
+ const result = this.getEpochAndSlotInNextL1Slot();
211
+ const offset = PROPOSER_PIPELINING_SLOT_OFFSET;
212
+ const targetSlot = SlotNumber(result.slot + offset);
213
+ return { ...result, slot: targetSlot, epoch: getEpochAtSlot(targetSlot, this.l1constants) };
157
214
  }
158
215
 
159
216
  private getEpochAndSlotAtTimestamp(ts: bigint): EpochAndSlot {
160
217
  const slot = getSlotAtTimestamp(ts, this.l1constants);
218
+ const epoch = getEpochNumberAtTimestamp(ts, this.l1constants);
161
219
  return {
162
- epoch: getEpochNumberAtTimestamp(ts, this.l1constants),
163
- ts: getTimestampForSlot(slot, this.l1constants),
164
220
  slot,
221
+ epoch,
222
+ ts: getTimestampForSlot(slot, this.l1constants),
165
223
  };
166
224
  }
167
225
 
@@ -170,6 +228,38 @@ export class EpochCache implements EpochCacheInterface {
170
228
  return this.getCommittee(startSlot);
171
229
  }
172
230
 
231
+ /**
232
+ * Returns whether the escape hatch is open for the given epoch.
233
+ *
234
+ * Uses the already-cached EpochCommitteeInfo when available. If not cached, it will fetch
235
+ * the epoch committee info (which includes the escape hatch flag) and return it.
236
+ */
237
+ public async isEscapeHatchOpen(epoch: EpochNumber): Promise<boolean> {
238
+ const cached = this.cache.get(epoch);
239
+ if (cached) {
240
+ return cached.isEscapeHatchOpen;
241
+ }
242
+ const info = await this.getCommitteeForEpoch(epoch);
243
+ return info.isEscapeHatchOpen;
244
+ }
245
+
246
+ /**
247
+ * Returns whether the escape hatch is open for the epoch containing the given slot.
248
+ *
249
+ * This is a lightweight helper intended for callers that already have a slot number and only
250
+ * need the escape hatch flag (without pulling full committee info).
251
+ */
252
+ public async isEscapeHatchOpenAtSlot(slot: SlotTag = 'now'): Promise<boolean> {
253
+ const epoch =
254
+ slot === 'now'
255
+ ? this.getEpochNow()
256
+ : slot === 'next'
257
+ ? this.getEpochAndSlotInNextL1Slot().epoch
258
+ : getEpochAtSlot(slot, this.l1constants);
259
+
260
+ return await this.isEscapeHatchOpen(epoch);
261
+ }
262
+
173
263
  /**
174
264
  * Get the current validator set
175
265
  * @param nextSlot - If true, get the validator set for the next slot.
@@ -197,7 +287,7 @@ export class EpochCache implements EpochCacheInterface {
197
287
  return epochData;
198
288
  }
199
289
 
200
- private getEpochAndTimestamp(slot: SlotTag = 'now') {
290
+ private getEpochAndTimestamp(slot: SlotTag = 'now'): { epoch: EpochNumber; ts: bigint } {
201
291
  if (slot === 'now') {
202
292
  return this.getEpochAndSlotNow();
203
293
  } else if (slot === 'next') {
@@ -209,10 +299,11 @@ export class EpochCache implements EpochCacheInterface {
209
299
 
210
300
  private async computeCommittee(when: { epoch: EpochNumber; ts: bigint }): Promise<EpochCommitteeInfo> {
211
301
  const { ts, epoch } = when;
212
- const [committeeHex, seed, l1Timestamp] = await Promise.all([
302
+ const [committee, seedBuffer, l1Timestamp, isEscapeHatchOpen] = await Promise.all([
213
303
  this.rollup.getCommitteeAt(ts),
214
304
  this.rollup.getSampleSeedAt(ts),
215
305
  this.rollup.client.getBlock({ includeTransactions: false }).then(b => b.timestamp),
306
+ this.rollup.isEscapeHatchOpen(epoch),
216
307
  ]);
217
308
  const { lagInEpochsForValidatorSet, epochDuration, slotDuration } = this.l1constants;
218
309
  const sub = BigInt(lagInEpochsForValidatorSet) * BigInt(epochDuration) * BigInt(slotDuration);
@@ -221,8 +312,7 @@ export class EpochCache implements EpochCacheInterface {
221
312
  `Cannot query committee for future epoch ${epoch} with timestamp ${ts} (current L1 time is ${l1Timestamp}). Check your Ethereum node is synced.`,
222
313
  );
223
314
  }
224
- const committee = committeeHex?.map((v: `0x${string}`) => EthAddress.fromString(v));
225
- return { committee, seed, epoch };
315
+ return { committee, seed: seedBuffer.toBigInt(), epoch, isEscapeHatchOpen };
226
316
  }
227
317
 
228
318
  /**
@@ -247,25 +337,24 @@ export class EpochCache implements EpochCacheInterface {
247
337
  return BigInt(keccak256(this.getProposerIndexEncoding(epoch, slot, seed))) % size;
248
338
  }
249
339
 
250
- /**
251
- * Returns the current and next proposer's attester address
252
- *
253
- * We return the next proposer's attester address as the node will check if it is the proposer at the next ethereum block,
254
- * which can be the next slot. If this is the case, then it will send proposals early.
255
- */
256
- public async getProposerAttesterAddressInCurrentOrNextSlot(): Promise<{
257
- currentSlot: SlotNumber;
258
- nextSlot: SlotNumber;
259
- currentProposer: EthAddress | undefined;
260
- nextProposer: EthAddress | undefined;
261
- }> {
262
- const current = this.getEpochAndSlotNow();
340
+ /** Returns the current and next L2 slot in next eth L1 Slot. */
341
+ public getCurrentAndNextSlot(): { currentSlot: SlotNumber; nextSlot: SlotNumber } {
342
+ const currentSlot = this.getSlotNow();
263
343
  const next = this.getEpochAndSlotInNextL1Slot();
264
344
 
265
345
  return {
266
- currentProposer: await this.getProposerAttesterAddressAt(current),
267
- nextProposer: await this.getProposerAttesterAddressAt(next),
268
- currentSlot: current.slot,
346
+ currentSlot,
347
+ nextSlot: next.slot,
348
+ };
349
+ }
350
+
351
+ /** Returns the taget and next L2 slot in the next L1 slot */
352
+ public getTargetAndNextSlot(): { targetSlot: SlotNumber; nextSlot: SlotNumber } {
353
+ const targetSlot = this.getTargetSlot();
354
+ const next = this.getTargetEpochAndSlotInNextL1Slot();
355
+
356
+ return {
357
+ targetSlot,
269
358
  nextSlot: next.slot,
270
359
  };
271
360
  }
@@ -348,11 +437,12 @@ export class EpochCache implements EpochCacheInterface {
348
437
  async getRegisteredValidators(): Promise<EthAddress[]> {
349
438
  const validatorRefreshIntervalMs = this.config.validatorRefreshIntervalSeconds * 1000;
350
439
  const validatorRefreshTime = this.lastValidatorRefresh + validatorRefreshIntervalMs;
351
- if (validatorRefreshTime < this.dateProvider.now()) {
352
- const currentSet = await this.rollup.getAttesters();
353
- this.allValidators = new Set(currentSet);
354
- this.lastValidatorRefresh = this.dateProvider.now();
440
+ const now = this.dateProvider.now();
441
+ if (validatorRefreshTime < now) {
442
+ const currentSet = await this.rollup.getAttesters(BigInt(Math.floor(now / 1000)));
443
+ this.allValidators = new Set(currentSet.map(v => v.toString()));
444
+ this.lastValidatorRefresh = now;
355
445
  }
356
- return Array.from(this.allValidators.keys().map(v => EthAddress.fromString(v)));
446
+ return Array.from(this.allValidators.keys()).map(v => EthAddress.fromString(v));
357
447
  }
358
448
  }
@@ -0,0 +1 @@
1
+ export * from './test_epoch_cache.js';