@aztec/epoch-cache 0.82.2-alpha-testnet.4 → 0.82.3

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.
@@ -1,17 +1,20 @@
1
- /// <reference types="node" resolution-mode="require"/>
2
1
  import { RollupContract } from '@aztec/ethereum';
3
2
  import { EthAddress } from '@aztec/foundation/eth-address';
4
3
  import { DateProvider } from '@aztec/foundation/timer';
5
4
  import { type L1RollupConstants } from '@aztec/stdlib/epoch-helpers';
6
- import { EventEmitter } from 'node:events';
7
5
  import { type EpochCacheConfig } from './config.js';
8
6
  type EpochAndSlot = {
9
7
  epoch: bigint;
10
8
  slot: bigint;
11
9
  ts: bigint;
12
10
  };
11
+ export type EpochCommitteeInfo = {
12
+ committee: EthAddress[];
13
+ seed: bigint;
14
+ epoch: bigint;
15
+ };
13
16
  export interface EpochCacheInterface {
14
- getCommittee(nextSlot: boolean): Promise<EthAddress[]>;
17
+ getCommittee(slot: 'now' | 'next' | bigint | undefined): Promise<EpochCommitteeInfo>;
15
18
  getEpochAndSlotNow(): EpochAndSlot;
16
19
  getProposerIndexEncoding(epoch: bigint, slot: bigint, seed: bigint): `0x${string}`;
17
20
  computeProposerIndex(slot: bigint, epoch: bigint, seed: bigint, size: bigint): bigint;
@@ -27,37 +30,38 @@ export interface EpochCacheInterface {
27
30
  * Epoch cache
28
31
  *
29
32
  * This class is responsible for managing traffic to the l1 node, by caching the validator set.
33
+ * Keeps the last N epochs in cache.
30
34
  * It also provides a method to get the current or next proposer, and to check who is in the current slot.
31
35
  *
32
- * If the epoch changes, then we update the stored validator set.
33
- *
34
36
  * Note: This class is very dependent on the system clock being in sync.
35
37
  */
36
- export declare class EpochCache extends EventEmitter<{
37
- committeeChanged: [EthAddress[], bigint];
38
- }> implements EpochCacheInterface {
38
+ export declare class EpochCache implements EpochCacheInterface {
39
39
  private rollup;
40
40
  private readonly l1constants;
41
41
  private readonly dateProvider;
42
- private committee;
43
- private cachedEpoch;
44
- private cachedSampleSeed;
42
+ private readonly config;
43
+ private cache;
45
44
  private readonly log;
46
- constructor(rollup: RollupContract, initialValidators?: EthAddress[], initialSampleSeed?: bigint, l1constants?: L1RollupConstants, dateProvider?: DateProvider);
45
+ constructor(rollup: RollupContract, initialEpoch?: bigint, initialValidators?: EthAddress[], initialSampleSeed?: bigint, l1constants?: L1RollupConstants, dateProvider?: DateProvider, config?: {
46
+ cacheSize: number;
47
+ });
47
48
  static create(rollupAddress: EthAddress, config?: EpochCacheConfig, deps?: {
48
49
  dateProvider?: DateProvider;
49
50
  }): Promise<EpochCache>;
50
- private nowInSeconds;
51
+ getL1Constants(): L1RollupConstants;
51
52
  getEpochAndSlotNow(): EpochAndSlot;
53
+ private nowInSeconds;
54
+ private getEpochAndSlotAtSlot;
52
55
  private getEpochAndSlotInNextSlot;
53
56
  private getEpochAndSlotAtTimestamp;
54
57
  /**
55
58
  * Get the current validator set
56
- *
57
59
  * @param nextSlot - If true, get the validator set for the next slot.
58
60
  * @returns The current validator set.
59
61
  */
60
- getCommittee(nextSlot?: boolean): Promise<EthAddress[]>;
62
+ getCommittee(slot?: 'now' | 'next' | bigint): Promise<EpochCommitteeInfo>;
63
+ private getEpochAndTimestamp;
64
+ private computeCommittee;
61
65
  /**
62
66
  * Get the ABI encoding of the proposer index - see ValidatorSelectionLib.sol computeProposerIndex
63
67
  */
@@ -68,9 +72,6 @@ export declare class EpochCache extends EventEmitter<{
68
72
  *
69
73
  * We return the next proposer as the node will check if it is the proposer at the next ethereum block, which
70
74
  * can be the next slot. If this is the case, then it will send proposals early.
71
- *
72
- * If we are at an epoch boundary, then we can update the cache for the next epoch, this is the last check
73
- * we do in the validator client, so we can update the cache here.
74
75
  */
75
76
  getProposerInCurrentOrNextSlot(): Promise<{
76
77
  currentProposer: EthAddress;
@@ -78,6 +79,7 @@ export declare class EpochCache extends EventEmitter<{
78
79
  currentSlot: bigint;
79
80
  nextSlot: bigint;
80
81
  }>;
82
+ private getProposerAt;
81
83
  /**
82
84
  * Check if a validator is in the current epoch's committee
83
85
  */
@@ -1 +1 @@
1
- {"version":3,"file":"epoch_cache.d.ts","sourceRoot":"","sources":["../src/epoch_cache.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,cAAc,EAAuB,MAAM,iBAAiB,CAAC;AACtE,OAAO,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAE3D,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACvD,OAAO,EAEL,KAAK,iBAAiB,EAGvB,MAAM,6BAA6B,CAAC;AAErC,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAG3C,OAAO,EAAE,KAAK,gBAAgB,EAA8B,MAAM,aAAa,CAAC;AAEhF,KAAK,YAAY,GAAG;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;CACZ,CAAC;AAEF,MAAM,WAAW,mBAAmB;IAClC,YAAY,CAAC,QAAQ,EAAE,OAAO,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC;IACvD,kBAAkB,IAAI,YAAY,CAAC;IACnC,wBAAwB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,KAAK,MAAM,EAAE,CAAC;IACnF,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IACtF,8BAA8B,IAAI,OAAO,CAAC;QACxC,eAAe,EAAE,UAAU,CAAC;QAC5B,YAAY,EAAE,UAAU,CAAC;QACzB,WAAW,EAAE,MAAM,CAAC;QACpB,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC,CAAC;IACH,aAAa,CAAC,SAAS,EAAE,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACxD;AAED;;;;;;;;;GASG;AACH,qBAAa,UACX,SAAQ,YAAY,CAAC;IAAE,gBAAgB,EAAE,CAAC,UAAU,EAAE,EAAE,MAAM,CAAC,CAAA;CAAE,CACjE,YAAW,mBAAmB;IAQ5B,OAAO,CAAC,MAAM;IAGd,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,OAAO,CAAC,QAAQ,CAAC,YAAY;IAV/B,OAAO,CAAC,SAAS,CAAe;IAChC,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,gBAAgB,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAuC;gBAGjD,MAAM,EAAE,cAAc,EAC9B,iBAAiB,GAAE,UAAU,EAAO,EACpC,iBAAiB,GAAE,MAAW,EACb,WAAW,GAAE,iBAA0C,EACvD,YAAY,GAAE,YAAiC;WAWrD,MAAM,CACjB,aAAa,EAAE,UAAU,EACzB,MAAM,CAAC,EAAE,gBAAgB,EACzB,IAAI,GAAE;QAAE,YAAY,CAAC,EAAE,YAAY,CAAA;KAAO;IAoC5C,OAAO,CAAC,YAAY;IAIpB,kBAAkB,IAAI,YAAY;IAIlC,OAAO,CAAC,yBAAyB;IAKjC,OAAO,CAAC,0BAA0B;IAQlC;;;;;OAKG;IACG,YAAY,CAAC,QAAQ,GAAE,OAAe,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IAuBpE;;OAEG;IACH,wBAAwB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,KAAK,MAAM,EAAE;IAWlF,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM;IAIrF;;;;;;;;OAQG;IACG,8BAA8B,IAAI,OAAO,CAAC;QAC9C,eAAe,EAAE,UAAU,CAAC;QAC5B,YAAY,EAAE,UAAU,CAAC;QACzB,WAAW,EAAE,MAAM,CAAC;QACpB,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC;IA+BF;;OAEG;IACG,aAAa,CAAC,SAAS,EAAE,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC;CAI7D"}
1
+ {"version":3,"file":"epoch_cache.d.ts","sourceRoot":"","sources":["../src/epoch_cache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAuB,MAAM,iBAAiB,CAAC;AACtE,OAAO,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAE3D,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACvD,OAAO,EAEL,KAAK,iBAAiB,EAKvB,MAAM,6BAA6B,CAAC;AAIrC,OAAO,EAAE,KAAK,gBAAgB,EAA8B,MAAM,aAAa,CAAC;AAEhF,KAAK,YAAY,GAAG;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;CACZ,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,SAAS,EAAE,UAAU,EAAE,CAAC;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,WAAW,mBAAmB;IAClC,YAAY,CAAC,IAAI,EAAE,KAAK,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACrF,kBAAkB,IAAI,YAAY,CAAC;IACnC,wBAAwB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,KAAK,MAAM,EAAE,CAAC;IACnF,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IACtF,8BAA8B,IAAI,OAAO,CAAC;QACxC,eAAe,EAAE,UAAU,CAAC;QAC5B,YAAY,EAAE,UAAU,CAAC;QACzB,WAAW,EAAE,MAAM,CAAC;QACpB,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC,CAAC;IACH,aAAa,CAAC,SAAS,EAAE,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACxD;AAED;;;;;;;;GAQG;AACH,qBAAa,UAAW,YAAW,mBAAmB;IAKlD,OAAO,CAAC,MAAM;IAId,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,OAAO,CAAC,QAAQ,CAAC,YAAY;IAC7B,OAAO,CAAC,QAAQ,CAAC,MAAM;IAVzB,OAAO,CAAC,KAAK,CAA8C;IAC3D,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAuC;gBAGjD,MAAM,EAAE,cAAc,EAC9B,YAAY,GAAE,MAAW,EACzB,iBAAiB,GAAE,UAAU,EAAO,EACpC,iBAAiB,GAAE,MAAW,EACb,WAAW,GAAE,iBAA0C,EACvD,YAAY,GAAE,YAAiC,EAC/C,MAAM;;KAAoB;WAWhC,MAAM,CACjB,aAAa,EAAE,UAAU,EACzB,MAAM,CAAC,EAAE,gBAAgB,EACzB,IAAI,GAAE;QAAE,YAAY,CAAC,EAAE,YAAY,CAAA;KAAO;IAsCrC,cAAc,IAAI,iBAAiB;IAInC,kBAAkB,IAAI,YAAY;IAIzC,OAAO,CAAC,YAAY;IAIpB,OAAO,CAAC,qBAAqB;IAM7B,OAAO,CAAC,yBAAyB;IAKjC,OAAO,CAAC,0BAA0B;IAQlC;;;;OAIG;IACU,YAAY,CAAC,IAAI,GAAE,KAAK,GAAG,MAAM,GAAG,MAAc,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAkB7F,OAAO,CAAC,oBAAoB;YAUd,gBAAgB;IAO9B;;OAEG;IACH,wBAAwB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,KAAK,MAAM,EAAE;IAWlF,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM;IAIrF;;;;;OAKG;IACG,8BAA8B,IAAI,OAAO,CAAC;QAC9C,eAAe,EAAE,UAAU,CAAC;QAC5B,YAAY,EAAE,UAAU,CAAC;QACzB,WAAW,EAAE,MAAM,CAAC;QACpB,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC;YAYY,aAAa;IAO3B;;OAEG;IACG,aAAa,CAAC,SAAS,EAAE,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC;CAI7D"}
@@ -2,36 +2,44 @@ import { RollupContract, createEthereumChain } from '@aztec/ethereum';
2
2
  import { EthAddress } from '@aztec/foundation/eth-address';
3
3
  import { createLogger } from '@aztec/foundation/log';
4
4
  import { DateProvider } from '@aztec/foundation/timer';
5
- import { EmptyL1RollupConstants, getEpochNumberAtTimestamp, getSlotAtTimestamp } from '@aztec/stdlib/epoch-helpers';
6
- import { EventEmitter } from 'node:events';
5
+ import { EmptyL1RollupConstants, getEpochAtSlot, getEpochNumberAtTimestamp, getSlotAtTimestamp, getTimestampRangeForEpoch } from '@aztec/stdlib/epoch-helpers';
7
6
  import { createPublicClient, encodeAbiParameters, fallback, http, keccak256 } from 'viem';
8
7
  import { getEpochCacheConfigEnvVars } from './config.js';
9
8
  /**
10
9
  * Epoch cache
11
10
  *
12
11
  * This class is responsible for managing traffic to the l1 node, by caching the validator set.
12
+ * Keeps the last N epochs in cache.
13
13
  * It also provides a method to get the current or next proposer, and to check who is in the current slot.
14
14
  *
15
- * If the epoch changes, then we update the stored validator set.
16
- *
17
15
  * Note: This class is very dependent on the system clock being in sync.
18
- */ export class EpochCache extends EventEmitter {
16
+ */ export class EpochCache {
19
17
  rollup;
20
18
  l1constants;
21
19
  dateProvider;
22
- committee;
23
- cachedEpoch;
24
- cachedSampleSeed;
20
+ config;
21
+ cache;
25
22
  log;
26
- constructor(rollup, initialValidators = [], initialSampleSeed = 0n, l1constants = EmptyL1RollupConstants, dateProvider = new DateProvider()){
27
- super(), this.rollup = rollup, this.l1constants = l1constants, this.dateProvider = dateProvider, this.log = createLogger('epoch-cache');
28
- this.committee = initialValidators;
29
- this.cachedSampleSeed = initialSampleSeed;
30
- this.log.debug(`Initialized EpochCache with constants and validators`, {
23
+ constructor(rollup, initialEpoch = 0n, initialValidators = [], initialSampleSeed = 0n, l1constants = EmptyL1RollupConstants, dateProvider = new DateProvider(), config = {
24
+ cacheSize: 12
25
+ }){
26
+ this.rollup = rollup;
27
+ this.l1constants = l1constants;
28
+ this.dateProvider = dateProvider;
29
+ this.config = config;
30
+ this.cache = new Map();
31
+ this.log = createLogger('epoch-cache');
32
+ this.cache.set(initialEpoch, {
33
+ epoch: initialEpoch,
34
+ committee: initialValidators,
35
+ seed: initialSampleSeed
36
+ });
37
+ this.log.debug(`Initialized EpochCache with ${initialValidators.length} validators`, {
31
38
  l1constants,
32
- initialValidators
39
+ initialValidators,
40
+ initialSampleSeed,
41
+ initialEpoch
33
42
  });
34
- this.cachedEpoch = getEpochNumberAtTimestamp(this.nowInSeconds(), this.l1constants);
35
43
  }
36
44
  static async create(rollupAddress, config, deps = {}) {
37
45
  config = config ?? getEpochCacheConfigEnvVars();
@@ -42,11 +50,12 @@ import { getEpochCacheConfigEnvVars } from './config.js';
42
50
  pollingInterval: config.viemPollingIntervalMS
43
51
  });
44
52
  const rollup = new RollupContract(publicClient, rollupAddress.toString());
45
- const [l1StartBlock, l1GenesisTime, initialValidators, sampleSeed] = await Promise.all([
53
+ const [l1StartBlock, l1GenesisTime, initialValidators, sampleSeed, epochNumber] = await Promise.all([
46
54
  rollup.getL1StartBlock(),
47
55
  rollup.getL1GenesisTime(),
48
56
  rollup.getCurrentEpochCommittee(),
49
- rollup.getCurrentSampleSeed()
57
+ rollup.getCurrentSampleSeed(),
58
+ rollup.getEpochNumber()
50
59
  ]);
51
60
  const l1RollupConstants = {
52
61
  l1StartBlock,
@@ -55,14 +64,26 @@ import { getEpochCacheConfigEnvVars } from './config.js';
55
64
  epochDuration: config.aztecEpochDuration,
56
65
  ethereumSlotDuration: config.ethereumSlotDuration
57
66
  };
58
- return new EpochCache(rollup, initialValidators.map((v)=>EthAddress.fromString(v)), sampleSeed, l1RollupConstants, deps.dateProvider);
67
+ return new EpochCache(rollup, epochNumber, initialValidators.map((v)=>EthAddress.fromString(v)), sampleSeed, l1RollupConstants, deps.dateProvider);
59
68
  }
60
- nowInSeconds() {
61
- return BigInt(Math.floor(this.dateProvider.now() / 1000));
69
+ getL1Constants() {
70
+ return this.l1constants;
62
71
  }
63
72
  getEpochAndSlotNow() {
64
73
  return this.getEpochAndSlotAtTimestamp(this.nowInSeconds());
65
74
  }
75
+ nowInSeconds() {
76
+ return BigInt(Math.floor(this.dateProvider.now() / 1000));
77
+ }
78
+ getEpochAndSlotAtSlot(slot) {
79
+ const epoch = getEpochAtSlot(slot, this.l1constants);
80
+ const ts = getTimestampRangeForEpoch(slot, this.l1constants)[0];
81
+ return {
82
+ epoch,
83
+ ts,
84
+ slot
85
+ };
86
+ }
66
87
  getEpochAndSlotInNextSlot() {
67
88
  const nextSlotTs = this.nowInSeconds() + BigInt(this.l1constants.slotDuration);
68
89
  return this.getEpochAndSlotAtTimestamp(nextSlotTs);
@@ -76,30 +97,43 @@ import { getEpochCacheConfigEnvVars } from './config.js';
76
97
  }
77
98
  /**
78
99
  * Get the current validator set
79
- *
80
100
  * @param nextSlot - If true, get the validator set for the next slot.
81
101
  * @returns The current validator set.
82
- */ async getCommittee(nextSlot = false) {
83
- // If the current epoch has changed, then we need to make a request to update the validator set
84
- const { epoch: calculatedEpoch, ts } = nextSlot ? this.getEpochAndSlotInNextSlot() : this.getEpochAndSlotNow();
85
- if (calculatedEpoch !== this.cachedEpoch) {
86
- this.log.debug(`Updating validator set for new epoch ${calculatedEpoch}`, {
87
- epoch: calculatedEpoch,
88
- previousEpoch: this.cachedEpoch
89
- });
90
- const [committeeAtTs, sampleSeedAtTs] = await Promise.all([
91
- this.rollup.getCommitteeAt(ts),
92
- this.rollup.getSampleSeedAt(ts)
93
- ]);
94
- this.committee = committeeAtTs.map((v)=>EthAddress.fromString(v));
95
- this.cachedEpoch = calculatedEpoch;
96
- this.cachedSampleSeed = sampleSeedAtTs;
97
- this.log.debug(`Updated validator set for epoch ${calculatedEpoch}`, {
98
- commitee: this.committee
99
- });
100
- this.emit('committeeChanged', this.committee, calculatedEpoch);
102
+ */ async getCommittee(slot = 'now') {
103
+ const { epoch, ts } = this.getEpochAndTimestamp(slot);
104
+ if (this.cache.has(epoch)) {
105
+ return this.cache.get(epoch);
101
106
  }
102
- return this.committee;
107
+ const epochData = await this.computeCommittee({
108
+ epoch,
109
+ ts
110
+ });
111
+ this.cache.set(epoch, epochData);
112
+ const toPurge = Array.from(this.cache.keys()).sort((a, b)=>Number(b - a)).slice(this.config.cacheSize);
113
+ toPurge.forEach((key)=>this.cache.delete(key));
114
+ return epochData;
115
+ }
116
+ getEpochAndTimestamp(slot = 'now') {
117
+ if (slot === 'now') {
118
+ return this.getEpochAndSlotNow();
119
+ } else if (slot === 'next') {
120
+ return this.getEpochAndSlotInNextSlot();
121
+ } else {
122
+ return this.getEpochAndSlotAtSlot(slot);
123
+ }
124
+ }
125
+ async computeCommittee(when) {
126
+ const { ts, epoch } = when;
127
+ const [committeeHex, seed] = await Promise.all([
128
+ this.rollup.getCommitteeAt(ts),
129
+ this.rollup.getSampleSeedAt(ts)
130
+ ]);
131
+ const committee = committeeHex.map((v)=>EthAddress.fromString(v));
132
+ return {
133
+ committee,
134
+ seed,
135
+ epoch
136
+ };
103
137
  }
104
138
  /**
105
139
  * Get the ABI encoding of the proposer index - see ValidatorSelectionLib.sol computeProposerIndex
@@ -131,34 +165,26 @@ import { getEpochCacheConfigEnvVars } from './config.js';
131
165
  *
132
166
  * We return the next proposer as the node will check if it is the proposer at the next ethereum block, which
133
167
  * can be the next slot. If this is the case, then it will send proposals early.
134
- *
135
- * If we are at an epoch boundary, then we can update the cache for the next epoch, this is the last check
136
- * we do in the validator client, so we can update the cache here.
137
168
  */ async getProposerInCurrentOrNextSlot() {
138
- // Validators are sorted by their index in the committee, and getValidatorSet will cache
139
- const committee = await this.getCommittee();
140
- const { slot: currentSlot, epoch: currentEpoch } = this.getEpochAndSlotNow();
141
- const { slot: nextSlot, epoch: nextEpoch } = this.getEpochAndSlotInNextSlot();
142
- // Compute the proposer in this and the next slot
143
- const proposerIndex = this.computeProposerIndex(currentSlot, this.cachedEpoch, this.cachedSampleSeed, BigInt(committee.length));
144
- // Check if the next proposer is in the next epoch
145
- if (nextEpoch !== currentEpoch) {
146
- await this.getCommittee(/*next slot*/ true);
147
- }
148
- const nextProposerIndex = this.computeProposerIndex(nextSlot, this.cachedEpoch, this.cachedSampleSeed, BigInt(committee.length));
149
- const currentProposer = committee[Number(proposerIndex)];
150
- const nextProposer = committee[Number(nextProposerIndex)];
169
+ const current = this.getEpochAndSlotNow();
170
+ const next = this.getEpochAndSlotInNextSlot();
151
171
  return {
152
- currentProposer,
153
- nextProposer,
154
- currentSlot,
155
- nextSlot
172
+ currentProposer: await this.getProposerAt(current),
173
+ nextProposer: await this.getProposerAt(next),
174
+ currentSlot: current.slot,
175
+ nextSlot: next.slot
156
176
  };
157
177
  }
178
+ async getProposerAt(when) {
179
+ const { epoch, slot } = when;
180
+ const { seed, committee } = await this.getCommittee(slot);
181
+ const proposerIndex = this.computeProposerIndex(slot, epoch, seed, BigInt(committee.length));
182
+ return committee[Number(proposerIndex)];
183
+ }
158
184
  /**
159
185
  * Check if a validator is in the current epoch's committee
160
186
  */ async isInCommittee(validator) {
161
- const committee = await this.getCommittee();
187
+ const { committee } = await this.getCommittee();
162
188
  return committee.some((v)=>v.equals(validator));
163
189
  }
164
190
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aztec/epoch-cache",
3
- "version": "0.82.2-alpha-testnet.4",
3
+ "version": "0.82.3",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dest/index.js",
@@ -28,10 +28,10 @@
28
28
  "../package.common.json"
29
29
  ],
30
30
  "dependencies": {
31
- "@aztec/ethereum": "0.82.2-alpha-testnet.4",
32
- "@aztec/foundation": "0.82.2-alpha-testnet.4",
33
- "@aztec/l1-artifacts": "0.82.2-alpha-testnet.4",
34
- "@aztec/stdlib": "0.82.2-alpha-testnet.4",
31
+ "@aztec/ethereum": "0.82.3",
32
+ "@aztec/foundation": "0.82.3",
33
+ "@aztec/l1-artifacts": "0.82.3",
34
+ "@aztec/stdlib": "0.82.3",
35
35
  "@viem/anvil": "^0.0.10",
36
36
  "dotenv": "^16.0.3",
37
37
  "get-port": "^7.1.0",
@@ -5,11 +5,12 @@ import { DateProvider } from '@aztec/foundation/timer';
5
5
  import {
6
6
  EmptyL1RollupConstants,
7
7
  type L1RollupConstants,
8
+ getEpochAtSlot,
8
9
  getEpochNumberAtTimestamp,
9
10
  getSlotAtTimestamp,
11
+ getTimestampRangeForEpoch,
10
12
  } from '@aztec/stdlib/epoch-helpers';
11
13
 
12
- import { EventEmitter } from 'node:events';
13
14
  import { createPublicClient, encodeAbiParameters, fallback, http, keccak256 } from 'viem';
14
15
 
15
16
  import { type EpochCacheConfig, getEpochCacheConfigEnvVars } from './config.js';
@@ -20,8 +21,14 @@ type EpochAndSlot = {
20
21
  ts: bigint;
21
22
  };
22
23
 
24
+ export type EpochCommitteeInfo = {
25
+ committee: EthAddress[];
26
+ seed: bigint;
27
+ epoch: bigint;
28
+ };
29
+
23
30
  export interface EpochCacheInterface {
24
- getCommittee(nextSlot: boolean): Promise<EthAddress[]>;
31
+ getCommittee(slot: 'now' | 'next' | bigint | undefined): Promise<EpochCommitteeInfo>;
25
32
  getEpochAndSlotNow(): EpochAndSlot;
26
33
  getProposerIndexEncoding(epoch: bigint, slot: bigint, seed: bigint): `0x${string}`;
27
34
  computeProposerIndex(slot: bigint, epoch: bigint, seed: bigint, size: bigint): bigint;
@@ -38,35 +45,31 @@ export interface EpochCacheInterface {
38
45
  * Epoch cache
39
46
  *
40
47
  * This class is responsible for managing traffic to the l1 node, by caching the validator set.
48
+ * Keeps the last N epochs in cache.
41
49
  * It also provides a method to get the current or next proposer, and to check who is in the current slot.
42
50
  *
43
- * If the epoch changes, then we update the stored validator set.
44
- *
45
51
  * Note: This class is very dependent on the system clock being in sync.
46
52
  */
47
- export class EpochCache
48
- extends EventEmitter<{ committeeChanged: [EthAddress[], bigint] }>
49
- implements EpochCacheInterface
50
- {
51
- private committee: EthAddress[];
52
- private cachedEpoch: bigint;
53
- private cachedSampleSeed: bigint;
53
+ export class EpochCache implements EpochCacheInterface {
54
+ private cache: Map<bigint, EpochCommitteeInfo> = new Map();
54
55
  private readonly log: Logger = createLogger('epoch-cache');
55
56
 
56
57
  constructor(
57
58
  private rollup: RollupContract,
59
+ initialEpoch: bigint = 0n,
58
60
  initialValidators: EthAddress[] = [],
59
61
  initialSampleSeed: bigint = 0n,
60
62
  private readonly l1constants: L1RollupConstants = EmptyL1RollupConstants,
61
63
  private readonly dateProvider: DateProvider = new DateProvider(),
64
+ private readonly config = { cacheSize: 12 },
62
65
  ) {
63
- super();
64
- this.committee = initialValidators;
65
- this.cachedSampleSeed = initialSampleSeed;
66
-
67
- this.log.debug(`Initialized EpochCache with constants and validators`, { l1constants, initialValidators });
68
-
69
- this.cachedEpoch = getEpochNumberAtTimestamp(this.nowInSeconds(), this.l1constants);
66
+ this.cache.set(initialEpoch, { epoch: initialEpoch, committee: initialValidators, seed: initialSampleSeed });
67
+ this.log.debug(`Initialized EpochCache with ${initialValidators.length} validators`, {
68
+ l1constants,
69
+ initialValidators,
70
+ initialSampleSeed,
71
+ initialEpoch,
72
+ });
70
73
  }
71
74
 
72
75
  static async create(
@@ -84,11 +87,12 @@ export class EpochCache
84
87
  });
85
88
 
86
89
  const rollup = new RollupContract(publicClient, rollupAddress.toString());
87
- const [l1StartBlock, l1GenesisTime, initialValidators, sampleSeed] = await Promise.all([
90
+ const [l1StartBlock, l1GenesisTime, initialValidators, sampleSeed, epochNumber] = await Promise.all([
88
91
  rollup.getL1StartBlock(),
89
92
  rollup.getL1GenesisTime(),
90
93
  rollup.getCurrentEpochCommittee(),
91
94
  rollup.getCurrentSampleSeed(),
95
+ rollup.getEpochNumber(),
92
96
  ] as const);
93
97
 
94
98
  const l1RollupConstants: L1RollupConstants = {
@@ -101,6 +105,7 @@ export class EpochCache
101
105
 
102
106
  return new EpochCache(
103
107
  rollup,
108
+ epochNumber,
104
109
  initialValidators.map(v => EthAddress.fromString(v)),
105
110
  sampleSeed,
106
111
  l1RollupConstants,
@@ -108,12 +113,22 @@ export class EpochCache
108
113
  );
109
114
  }
110
115
 
116
+ public getL1Constants(): L1RollupConstants {
117
+ return this.l1constants;
118
+ }
119
+
120
+ public getEpochAndSlotNow(): EpochAndSlot {
121
+ return this.getEpochAndSlotAtTimestamp(this.nowInSeconds());
122
+ }
123
+
111
124
  private nowInSeconds(): bigint {
112
125
  return BigInt(Math.floor(this.dateProvider.now() / 1000));
113
126
  }
114
127
 
115
- getEpochAndSlotNow(): EpochAndSlot {
116
- return this.getEpochAndSlotAtTimestamp(this.nowInSeconds());
128
+ private getEpochAndSlotAtSlot(slot: bigint): EpochAndSlot {
129
+ const epoch = getEpochAtSlot(slot, this.l1constants);
130
+ const ts = getTimestampRangeForEpoch(slot, this.l1constants)[0];
131
+ return { epoch, ts, slot };
117
132
  }
118
133
 
119
134
  private getEpochAndSlotInNextSlot(): EpochAndSlot {
@@ -131,31 +146,42 @@ export class EpochCache
131
146
 
132
147
  /**
133
148
  * Get the current validator set
134
- *
135
149
  * @param nextSlot - If true, get the validator set for the next slot.
136
150
  * @returns The current validator set.
137
151
  */
138
- async getCommittee(nextSlot: boolean = false): Promise<EthAddress[]> {
139
- // If the current epoch has changed, then we need to make a request to update the validator set
140
- const { epoch: calculatedEpoch, ts } = nextSlot ? this.getEpochAndSlotInNextSlot() : this.getEpochAndSlotNow();
141
-
142
- if (calculatedEpoch !== this.cachedEpoch) {
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);
152
+ public async getCommittee(slot: 'now' | 'next' | bigint = 'now'): Promise<EpochCommitteeInfo> {
153
+ const { epoch, ts } = this.getEpochAndTimestamp(slot);
154
+
155
+ if (this.cache.has(epoch)) {
156
+ return this.cache.get(epoch)!;
156
157
  }
157
158
 
158
- return this.committee;
159
+ const epochData = await this.computeCommittee({ epoch, ts });
160
+ this.cache.set(epoch, epochData);
161
+
162
+ const toPurge = Array.from(this.cache.keys())
163
+ .sort((a, b) => Number(b - a))
164
+ .slice(this.config.cacheSize);
165
+ toPurge.forEach(key => this.cache.delete(key));
166
+
167
+ return epochData;
168
+ }
169
+
170
+ private getEpochAndTimestamp(slot: 'now' | 'next' | bigint = 'now') {
171
+ if (slot === 'now') {
172
+ return this.getEpochAndSlotNow();
173
+ } else if (slot === 'next') {
174
+ return this.getEpochAndSlotInNextSlot();
175
+ } else {
176
+ return this.getEpochAndSlotAtSlot(slot);
177
+ }
178
+ }
179
+
180
+ private async computeCommittee(when: { epoch: bigint; ts: bigint }): Promise<EpochCommitteeInfo> {
181
+ const { ts, epoch } = when;
182
+ const [committeeHex, seed] = await Promise.all([this.rollup.getCommitteeAt(ts), this.rollup.getSampleSeedAt(ts)]);
183
+ const committee = committeeHex.map((v: `0x${string}`) => EthAddress.fromString(v));
184
+ return { committee, seed, epoch };
159
185
  }
160
186
 
161
187
  /**
@@ -181,9 +207,6 @@ export class EpochCache
181
207
  *
182
208
  * We return the next proposer as the node will check if it is the proposer at the next ethereum block, which
183
209
  * 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.
187
210
  */
188
211
  async getProposerInCurrentOrNextSlot(): Promise<{
189
212
  currentProposer: EthAddress;
@@ -191,41 +214,29 @@ export class EpochCache
191
214
  currentSlot: bigint;
192
215
  nextSlot: bigint;
193
216
  }> {
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
- );
206
-
207
- // Check if the next proposer is in the next epoch
208
- if (nextEpoch !== currentEpoch) {
209
- await this.getCommittee(/*next slot*/ true);
210
- }
211
- const nextProposerIndex = this.computeProposerIndex(
212
- nextSlot,
213
- this.cachedEpoch,
214
- this.cachedSampleSeed,
215
- BigInt(committee.length),
216
- );
217
+ const current = this.getEpochAndSlotNow();
218
+ const next = this.getEpochAndSlotInNextSlot();
217
219
 
218
- const currentProposer = committee[Number(proposerIndex)];
219
- const nextProposer = committee[Number(nextProposerIndex)];
220
+ return {
221
+ currentProposer: await this.getProposerAt(current),
222
+ nextProposer: await this.getProposerAt(next),
223
+ currentSlot: current.slot,
224
+ nextSlot: next.slot,
225
+ };
226
+ }
220
227
 
221
- return { currentProposer, nextProposer, currentSlot, nextSlot };
228
+ private async getProposerAt(when: EpochAndSlot) {
229
+ const { epoch, slot } = when;
230
+ const { seed, committee } = await this.getCommittee(slot);
231
+ const proposerIndex = this.computeProposerIndex(slot, epoch, seed, BigInt(committee.length));
232
+ return committee[Number(proposerIndex)];
222
233
  }
223
234
 
224
235
  /**
225
236
  * Check if a validator is in the current epoch's committee
226
237
  */
227
238
  async isInCommittee(validator: EthAddress): Promise<boolean> {
228
- const committee = await this.getCommittee();
239
+ const { committee } = await this.getCommittee();
229
240
  return committee.some(v => v.equals(validator));
230
241
  }
231
242
  }
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=timestamp_provider.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"timestamp_provider.d.ts","sourceRoot":"","sources":["../src/timestamp_provider.ts"],"names":[],"mappings":""}
File without changes
File without changes