@aztec/epoch-cache 0.0.0-test.1 → 0.0.1-commit.03f7ef2

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 CHANGED
@@ -1,4 +1,5 @@
1
- import { type L1ContractsConfig, type L1ReaderConfig } from '@aztec/ethereum';
2
- export type EpochCacheConfig = Pick<L1ReaderConfig & L1ContractsConfig, 'l1RpcUrls' | 'l1ChainId' | 'viemPollingIntervalMS' | 'aztecSlotDuration' | 'ethereumSlotDuration' | 'aztecEpochDuration'>;
1
+ import { type L1ContractsConfig } from '@aztec/ethereum/config';
2
+ import { type L1ReaderConfig } from '@aztec/ethereum/l1-reader';
3
+ export type EpochCacheConfig = Pick<L1ReaderConfig & L1ContractsConfig, 'l1RpcUrls' | 'l1ChainId' | 'viemPollingIntervalMS' | 'ethereumSlotDuration'>;
3
4
  export declare function getEpochCacheConfigEnvVars(): EpochCacheConfig;
4
- //# sourceMappingURL=config.d.ts.map
5
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29uZmlnLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvY29uZmlnLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxLQUFLLGlCQUFpQixFQUErQixNQUFNLHdCQUF3QixDQUFDO0FBQzdGLE9BQU8sRUFBRSxLQUFLLGNBQWMsRUFBNEIsTUFBTSwyQkFBMkIsQ0FBQztBQUUxRixNQUFNLE1BQU0sZ0JBQWdCLEdBQUcsSUFBSSxDQUNqQyxjQUFjLEdBQUcsaUJBQWlCLEVBQ2xDLFdBQVcsR0FBRyxXQUFXLEdBQUcsdUJBQXVCLEdBQUcsc0JBQXNCLENBQzdFLENBQUM7QUFFRix3QkFBZ0IsMEJBQTBCLElBQUksZ0JBQWdCLENBRTdEIn0=
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,iBAAiB,EACtB,KAAK,cAAc,EAGpB,MAAM,iBAAiB,CAAC;AAEzB,MAAM,MAAM,gBAAgB,GAAG,IAAI,CACjC,cAAc,GAAG,iBAAiB,EAChC,WAAW,GACX,WAAW,GACX,uBAAuB,GACvB,mBAAmB,GACnB,sBAAsB,GACtB,oBAAoB,CACvB,CAAC;AAEF,wBAAgB,0BAA0B,IAAI,gBAAgB,CAE7D"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,iBAAiB,EAA+B,MAAM,wBAAwB,CAAC;AAC7F,OAAO,EAAE,KAAK,cAAc,EAA4B,MAAM,2BAA2B,CAAC;AAE1F,MAAM,MAAM,gBAAgB,GAAG,IAAI,CACjC,cAAc,GAAG,iBAAiB,EAClC,WAAW,GAAG,WAAW,GAAG,uBAAuB,GAAG,sBAAsB,CAC7E,CAAC;AAEF,wBAAgB,0BAA0B,IAAI,gBAAgB,CAE7D"}
package/dest/config.js CHANGED
@@ -1,4 +1,5 @@
1
- import { getL1ContractsConfigEnvVars, getL1ReaderConfigFromEnv } from '@aztec/ethereum';
1
+ import { getL1ContractsConfigEnvVars } from '@aztec/ethereum/config';
2
+ import { getL1ReaderConfigFromEnv } from '@aztec/ethereum/l1-reader';
2
3
  export function getEpochCacheConfigEnvVars() {
3
4
  return {
4
5
  ...getL1ReaderConfigFromEnv(),
@@ -1,87 +1,123 @@
1
- /// <reference types="node" resolution-mode="require"/>
2
- import { RollupContract } from '@aztec/ethereum';
1
+ import { RollupContract } from '@aztec/ethereum/contracts';
2
+ import { EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
3
3
  import { EthAddress } from '@aztec/foundation/eth-address';
4
4
  import { DateProvider } from '@aztec/foundation/timer';
5
5
  import { type L1RollupConstants } from '@aztec/stdlib/epoch-helpers';
6
- import { EventEmitter } from 'node:events';
7
6
  import { type EpochCacheConfig } from './config.js';
8
- type EpochAndSlot = {
9
- epoch: bigint;
10
- slot: bigint;
7
+ export type EpochAndSlot = {
8
+ epoch: EpochNumber;
9
+ slot: SlotNumber;
11
10
  ts: bigint;
12
11
  };
12
+ export type EpochCommitteeInfo = {
13
+ committee: EthAddress[] | undefined;
14
+ seed: bigint;
15
+ epoch: EpochNumber;
16
+ };
17
+ export type SlotTag = 'now' | 'next' | SlotNumber;
13
18
  export interface EpochCacheInterface {
14
- getCommittee(nextSlot: boolean): Promise<EthAddress[]>;
19
+ getCommittee(slot: SlotTag | undefined): Promise<EpochCommitteeInfo>;
15
20
  getEpochAndSlotNow(): EpochAndSlot;
16
- getProposerIndexEncoding(epoch: bigint, slot: bigint, seed: bigint): `0x${string}`;
17
- computeProposerIndex(slot: bigint, epoch: bigint, seed: bigint, size: bigint): bigint;
18
- getProposerInCurrentOrNextSlot(): Promise<{
19
- currentProposer: EthAddress;
20
- nextProposer: EthAddress;
21
- currentSlot: bigint;
22
- nextSlot: bigint;
21
+ getEpochAndSlotInNextL1Slot(): EpochAndSlot & {
22
+ now: bigint;
23
+ };
24
+ getProposerIndexEncoding(epoch: EpochNumber, slot: SlotNumber, seed: bigint): `0x${string}`;
25
+ computeProposerIndex(slot: SlotNumber, epoch: EpochNumber, seed: bigint, size: bigint): bigint;
26
+ getProposerAttesterAddressInCurrentOrNextSlot(): Promise<{
27
+ currentProposer: EthAddress | undefined;
28
+ nextProposer: EthAddress | undefined;
29
+ currentSlot: SlotNumber;
30
+ nextSlot: SlotNumber;
23
31
  }>;
24
- isInCommittee(validator: EthAddress): Promise<boolean>;
32
+ getRegisteredValidators(): Promise<EthAddress[]>;
33
+ isInCommittee(slot: SlotTag, validator: EthAddress): Promise<boolean>;
34
+ filterInCommittee(slot: SlotTag, validators: EthAddress[]): Promise<EthAddress[]>;
25
35
  }
26
36
  /**
27
37
  * Epoch cache
28
38
  *
29
39
  * This class is responsible for managing traffic to the l1 node, by caching the validator set.
40
+ * Keeps the last N epochs in cache.
30
41
  * It also provides a method to get the current or next proposer, and to check who is in the current slot.
31
42
  *
32
- * If the epoch changes, then we update the stored validator set.
33
- *
34
43
  * Note: This class is very dependent on the system clock being in sync.
35
44
  */
36
- export declare class EpochCache extends EventEmitter<{
37
- committeeChanged: [EthAddress[], bigint];
38
- }> implements EpochCacheInterface {
45
+ export declare class EpochCache implements EpochCacheInterface {
39
46
  private rollup;
40
47
  private readonly l1constants;
41
48
  private readonly dateProvider;
42
- private committee;
43
- private cachedEpoch;
44
- private cachedSampleSeed;
49
+ protected readonly config: {
50
+ cacheSize: number;
51
+ validatorRefreshIntervalSeconds: number;
52
+ };
53
+ protected cache: Map<EpochNumber, EpochCommitteeInfo>;
54
+ private allValidators;
55
+ private lastValidatorRefresh;
45
56
  private readonly log;
46
- constructor(rollup: RollupContract, initialValidators?: EthAddress[], initialSampleSeed?: bigint, l1constants?: L1RollupConstants, dateProvider?: DateProvider);
47
- static create(rollupAddress: EthAddress, config?: EpochCacheConfig, deps?: {
57
+ constructor(rollup: RollupContract, l1constants: L1RollupConstants & {
58
+ lagInEpochsForValidatorSet: number;
59
+ lagInEpochsForRandao: number;
60
+ }, dateProvider?: DateProvider, config?: {
61
+ cacheSize: number;
62
+ validatorRefreshIntervalSeconds: number;
63
+ });
64
+ static create(rollupOrAddress: EthAddress | RollupContract, config?: EpochCacheConfig, deps?: {
48
65
  dateProvider?: DateProvider;
49
66
  }): Promise<EpochCache>;
50
- private nowInSeconds;
51
- getEpochAndSlotNow(): EpochAndSlot;
52
- private getEpochAndSlotInNextSlot;
67
+ getL1Constants(): L1RollupConstants;
68
+ getEpochAndSlotNow(): EpochAndSlot & {
69
+ now: bigint;
70
+ };
71
+ nowInSeconds(): bigint;
72
+ private getEpochAndSlotAtSlot;
73
+ getEpochAndSlotInNextL1Slot(): EpochAndSlot & {
74
+ now: bigint;
75
+ };
53
76
  private getEpochAndSlotAtTimestamp;
77
+ getCommitteeForEpoch(epoch: EpochNumber): Promise<EpochCommitteeInfo>;
54
78
  /**
55
79
  * Get the current validator set
56
- *
57
80
  * @param nextSlot - If true, get the validator set for the next slot.
58
81
  * @returns The current validator set.
59
82
  */
60
- getCommittee(nextSlot?: boolean): Promise<EthAddress[]>;
83
+ getCommittee(slot?: SlotTag): Promise<EpochCommitteeInfo>;
84
+ private getEpochAndTimestamp;
85
+ private computeCommittee;
61
86
  /**
62
87
  * Get the ABI encoding of the proposer index - see ValidatorSelectionLib.sol computeProposerIndex
63
88
  */
64
- getProposerIndexEncoding(epoch: bigint, slot: bigint, seed: bigint): `0x${string}`;
65
- computeProposerIndex(slot: bigint, epoch: bigint, seed: bigint, size: bigint): bigint;
89
+ getProposerIndexEncoding(epoch: EpochNumber, slot: SlotNumber, seed: bigint): `0x${string}`;
90
+ computeProposerIndex(slot: SlotNumber, epoch: EpochNumber, seed: bigint, size: bigint): bigint;
66
91
  /**
67
- * Returns the current and next proposer
92
+ * Returns the current and next proposer's attester address
68
93
  *
69
- * We return the next proposer as the node will check if it is the proposer at the next ethereum block, which
70
- * 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.
94
+ * We return the next proposer's attester address as the node will check if it is the proposer at the next ethereum block,
95
+ * which can be the next slot. If this is the case, then it will send proposals early.
74
96
  */
75
- getProposerInCurrentOrNextSlot(): Promise<{
76
- currentProposer: EthAddress;
77
- nextProposer: EthAddress;
78
- currentSlot: bigint;
79
- nextSlot: bigint;
97
+ getProposerAttesterAddressInCurrentOrNextSlot(): Promise<{
98
+ currentSlot: SlotNumber;
99
+ nextSlot: SlotNumber;
100
+ currentProposer: EthAddress | undefined;
101
+ nextProposer: EthAddress | undefined;
80
102
  }>;
81
103
  /**
82
- * Check if a validator is in the current epoch's committee
104
+ * Get the proposer attester address in the given L2 slot
105
+ * @returns The proposer attester address. If the committee does not exist, we throw a NoCommitteeError.
106
+ * If the committee is empty (i.e. target committee size is 0, and anyone can propose), we return undefined.
107
+ */
108
+ getProposerAttesterAddressInSlot(slot: SlotNumber): Promise<EthAddress | undefined>;
109
+ /**
110
+ * Get the proposer attester address in the next slot
111
+ * @returns The proposer attester address. If the committee does not exist, we throw a NoCommitteeError.
112
+ * If the committee is empty (i.e. target committee size is 0, and anyone can propose), we return undefined.
83
113
  */
84
- isInCommittee(validator: EthAddress): Promise<boolean>;
114
+ getProposerAttesterAddressInNextSlot(): Promise<EthAddress | undefined>;
115
+ private getProposerAttesterAddressAt;
116
+ getProposerFromEpochCommittee(epochCommitteeInfo: EpochCommitteeInfo, slot: SlotNumber): EthAddress | undefined;
117
+ /** Check if a validator is in the given slot's committee */
118
+ isInCommittee(slot: SlotTag, validator: EthAddress): Promise<boolean>;
119
+ /** From the set of given addresses, return all that are on the committee for the given slot */
120
+ filterInCommittee(slot: SlotTag, validators: EthAddress[]): Promise<EthAddress[]>;
121
+ getRegisteredValidators(): Promise<EthAddress[]>;
85
122
  }
86
- export {};
87
- //# sourceMappingURL=epoch_cache.d.ts.map
123
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZXBvY2hfY2FjaGUuZC50cyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy9lcG9jaF9jYWNoZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFDQSxPQUFPLEVBQW9CLGNBQWMsRUFBRSxNQUFNLDJCQUEyQixDQUFDO0FBQzdFLE9BQU8sRUFBRSxXQUFXLEVBQUUsVUFBVSxFQUFFLE1BQU0saUNBQWlDLENBQUM7QUFDMUUsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLCtCQUErQixDQUFDO0FBRTNELE9BQU8sRUFBRSxZQUFZLEVBQUUsTUFBTSx5QkFBeUIsQ0FBQztBQUN2RCxPQUFPLEVBQ0wsS0FBSyxpQkFBaUIsRUFPdkIsTUFBTSw2QkFBNkIsQ0FBQztBQUlyQyxPQUFPLEVBQUUsS0FBSyxnQkFBZ0IsRUFBOEIsTUFBTSxhQUFhLENBQUM7QUFFaEYsTUFBTSxNQUFNLFlBQVksR0FBRztJQUN6QixLQUFLLEVBQUUsV0FBVyxDQUFDO0lBQ25CLElBQUksRUFBRSxVQUFVLENBQUM7SUFDakIsRUFBRSxFQUFFLE1BQU0sQ0FBQztDQUNaLENBQUM7QUFFRixNQUFNLE1BQU0sa0JBQWtCLEdBQUc7SUFDL0IsU0FBUyxFQUFFLFVBQVUsRUFBRSxHQUFHLFNBQVMsQ0FBQztJQUNwQyxJQUFJLEVBQUUsTUFBTSxDQUFDO0lBQ2IsS0FBSyxFQUFFLFdBQVcsQ0FBQztDQUNwQixDQUFDO0FBRUYsTUFBTSxNQUFNLE9BQU8sR0FBRyxLQUFLLEdBQUcsTUFBTSxHQUFHLFVBQVUsQ0FBQztBQUVsRCxNQUFNLFdBQVcsbUJBQW1CO0lBQ2xDLFlBQVksQ0FBQyxJQUFJLEVBQUUsT0FBTyxHQUFHLFNBQVMsR0FBRyxPQUFPLENBQUMsa0JBQWtCLENBQUMsQ0FBQztJQUNyRSxrQkFBa0IsSUFBSSxZQUFZLENBQUM7SUFDbkMsMkJBQTJCLElBQUksWUFBWSxHQUFHO1FBQUUsR0FBRyxFQUFFLE1BQU0sQ0FBQTtLQUFFLENBQUM7SUFDOUQsd0JBQXdCLENBQUMs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
@@ -1 +1 @@
1
- {"version":3,"file":"epoch_cache.d.ts","sourceRoot":"","sources":["../src/epoch_cache.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"epoch_cache.d.ts","sourceRoot":"","sources":["../src/epoch_cache.ts"],"names":[],"mappings":"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"}
@@ -1,105 +1,176 @@
1
- import { RollupContract, createEthereumChain } from '@aztec/ethereum';
1
+ import { createEthereumChain } from '@aztec/ethereum/chain';
2
+ import { NoCommitteeError, RollupContract } from '@aztec/ethereum/contracts';
2
3
  import { EthAddress } from '@aztec/foundation/eth-address';
3
4
  import { createLogger } from '@aztec/foundation/log';
4
5
  import { DateProvider } from '@aztec/foundation/timer';
5
- import { EmptyL1RollupConstants, getEpochNumberAtTimestamp, getSlotAtTimestamp } from '@aztec/stdlib/epoch-helpers';
6
- import { EventEmitter } from 'node:events';
6
+ import { getEpochAtSlot, getEpochNumberAtTimestamp, getSlotAtTimestamp, getSlotRangeForEpoch, getTimestampForSlot, getTimestampRangeForEpoch } from '@aztec/stdlib/epoch-helpers';
7
7
  import { createPublicClient, encodeAbiParameters, fallback, http, keccak256 } from 'viem';
8
8
  import { getEpochCacheConfigEnvVars } from './config.js';
9
9
  /**
10
10
  * Epoch cache
11
11
  *
12
12
  * This class is responsible for managing traffic to the l1 node, by caching the validator set.
13
+ * Keeps the last N epochs in cache.
13
14
  * It also provides a method to get the current or next proposer, and to check who is in the current slot.
14
15
  *
15
- * If the epoch changes, then we update the stored validator set.
16
- *
17
16
  * Note: This class is very dependent on the system clock being in sync.
18
- */ export class EpochCache extends EventEmitter {
17
+ */ export class EpochCache {
19
18
  rollup;
20
19
  l1constants;
21
20
  dateProvider;
22
- committee;
23
- cachedEpoch;
24
- cachedSampleSeed;
21
+ config;
22
+ // eslint-disable-next-line aztec-custom/no-non-primitive-in-collections
23
+ cache;
24
+ allValidators;
25
+ lastValidatorRefresh;
25
26
  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`, {
31
- l1constants,
32
- initialValidators
27
+ constructor(rollup, l1constants, dateProvider = new DateProvider(), config = {
28
+ cacheSize: 12,
29
+ validatorRefreshIntervalSeconds: 60
30
+ }){
31
+ this.rollup = rollup;
32
+ this.l1constants = l1constants;
33
+ this.dateProvider = dateProvider;
34
+ this.config = config;
35
+ this.cache = new Map();
36
+ this.allValidators = new Set();
37
+ this.lastValidatorRefresh = 0;
38
+ this.log = createLogger('epoch-cache');
39
+ this.log.debug(`Initialized EpochCache`, {
40
+ l1constants
33
41
  });
34
- this.cachedEpoch = getEpochNumberAtTimestamp(this.nowInSeconds(), this.l1constants);
35
42
  }
36
- static async create(rollupAddress, config, deps = {}) {
43
+ static async create(rollupOrAddress, config, deps = {}) {
37
44
  config = config ?? getEpochCacheConfigEnvVars();
38
- const chain = createEthereumChain(config.l1RpcUrls, config.l1ChainId);
39
- const publicClient = createPublicClient({
40
- chain: chain.chainInfo,
41
- transport: fallback(config.l1RpcUrls.map((url)=>http(url))),
42
- pollingInterval: config.viemPollingIntervalMS
43
- });
44
- const rollup = new RollupContract(publicClient, rollupAddress.toString());
45
- const [l1StartBlock, l1GenesisTime, initialValidators, sampleSeed] = await Promise.all([
45
+ // Load the rollup contract if we were given an address
46
+ let rollup;
47
+ if ('address' in rollupOrAddress) {
48
+ rollup = rollupOrAddress;
49
+ } else {
50
+ const chain = createEthereumChain(config.l1RpcUrls, config.l1ChainId);
51
+ const publicClient = createPublicClient({
52
+ chain: chain.chainInfo,
53
+ transport: fallback(config.l1RpcUrls.map((url)=>http(url, {
54
+ batch: false
55
+ }))),
56
+ pollingInterval: config.viemPollingIntervalMS
57
+ });
58
+ rollup = new RollupContract(publicClient, rollupOrAddress.toString());
59
+ }
60
+ const [l1StartBlock, l1GenesisTime, proofSubmissionEpochs, slotDuration, epochDuration, lagInEpochsForValidatorSet, lagInEpochsForRandao] = await Promise.all([
46
61
  rollup.getL1StartBlock(),
47
62
  rollup.getL1GenesisTime(),
48
- rollup.getCurrentEpochCommittee(),
49
- rollup.getCurrentSampleSeed()
63
+ rollup.getProofSubmissionEpochs(),
64
+ rollup.getSlotDuration(),
65
+ rollup.getEpochDuration(),
66
+ rollup.getLagInEpochsForValidatorSet(),
67
+ rollup.getLagInEpochsForRandao()
50
68
  ]);
51
69
  const l1RollupConstants = {
52
70
  l1StartBlock,
53
71
  l1GenesisTime,
54
- slotDuration: config.aztecSlotDuration,
55
- epochDuration: config.aztecEpochDuration,
56
- ethereumSlotDuration: config.ethereumSlotDuration
72
+ proofSubmissionEpochs: Number(proofSubmissionEpochs),
73
+ slotDuration: Number(slotDuration),
74
+ epochDuration: Number(epochDuration),
75
+ ethereumSlotDuration: config.ethereumSlotDuration,
76
+ lagInEpochsForValidatorSet: Number(lagInEpochsForValidatorSet),
77
+ lagInEpochsForRandao: Number(lagInEpochsForRandao)
78
+ };
79
+ return new EpochCache(rollup, l1RollupConstants, deps.dateProvider);
80
+ }
81
+ getL1Constants() {
82
+ return this.l1constants;
83
+ }
84
+ getEpochAndSlotNow() {
85
+ const now = this.nowInSeconds();
86
+ return {
87
+ ...this.getEpochAndSlotAtTimestamp(now),
88
+ now
57
89
  };
58
- return new EpochCache(rollup, initialValidators.map((v)=>EthAddress.fromString(v)), sampleSeed, l1RollupConstants, deps.dateProvider);
59
90
  }
60
91
  nowInSeconds() {
61
92
  return BigInt(Math.floor(this.dateProvider.now() / 1000));
62
93
  }
63
- getEpochAndSlotNow() {
64
- return this.getEpochAndSlotAtTimestamp(this.nowInSeconds());
94
+ getEpochAndSlotAtSlot(slot) {
95
+ const epoch = getEpochAtSlot(slot, this.l1constants);
96
+ const ts = getTimestampRangeForEpoch(epoch, this.l1constants)[0];
97
+ return {
98
+ epoch,
99
+ ts,
100
+ slot
101
+ };
65
102
  }
66
- getEpochAndSlotInNextSlot() {
67
- const nextSlotTs = this.nowInSeconds() + BigInt(this.l1constants.slotDuration);
68
- return this.getEpochAndSlotAtTimestamp(nextSlotTs);
103
+ getEpochAndSlotInNextL1Slot() {
104
+ const now = this.nowInSeconds();
105
+ const nextSlotTs = now + BigInt(this.l1constants.ethereumSlotDuration);
106
+ return {
107
+ ...this.getEpochAndSlotAtTimestamp(nextSlotTs),
108
+ now
109
+ };
69
110
  }
70
111
  getEpochAndSlotAtTimestamp(ts) {
112
+ const slot = getSlotAtTimestamp(ts, this.l1constants);
71
113
  return {
72
114
  epoch: getEpochNumberAtTimestamp(ts, this.l1constants),
73
- slot: getSlotAtTimestamp(ts, this.l1constants),
74
- ts
115
+ ts: getTimestampForSlot(slot, this.l1constants),
116
+ slot
75
117
  };
76
118
  }
119
+ getCommitteeForEpoch(epoch) {
120
+ const [startSlot] = getSlotRangeForEpoch(epoch, this.l1constants);
121
+ return this.getCommittee(startSlot);
122
+ }
77
123
  /**
78
124
  * Get the current validator set
79
- *
80
125
  * @param nextSlot - If true, get the validator set for the next slot.
81
126
  * @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);
127
+ */ async getCommittee(slot = 'now') {
128
+ const { epoch, ts } = this.getEpochAndTimestamp(slot);
129
+ if (this.cache.has(epoch)) {
130
+ return this.cache.get(epoch);
131
+ }
132
+ const epochData = await this.computeCommittee({
133
+ epoch,
134
+ ts
135
+ });
136
+ // If the committee size is 0 or undefined, then do not cache
137
+ if (!epochData.committee || epochData.committee.length === 0) {
138
+ return epochData;
101
139
  }
102
- return this.committee;
140
+ this.cache.set(epoch, epochData);
141
+ const toPurge = Array.from(this.cache.keys()).sort((a, b)=>Number(b - a)).slice(this.config.cacheSize);
142
+ toPurge.forEach((key)=>this.cache.delete(key));
143
+ return epochData;
144
+ }
145
+ getEpochAndTimestamp(slot = 'now') {
146
+ if (slot === 'now') {
147
+ return this.getEpochAndSlotNow();
148
+ } else if (slot === 'next') {
149
+ return this.getEpochAndSlotInNextL1Slot();
150
+ } else {
151
+ return this.getEpochAndSlotAtSlot(slot);
152
+ }
153
+ }
154
+ async computeCommittee(when) {
155
+ const { ts, epoch } = when;
156
+ const [committeeHex, seed, l1Timestamp] = await Promise.all([
157
+ this.rollup.getCommitteeAt(ts),
158
+ this.rollup.getSampleSeedAt(ts),
159
+ this.rollup.client.getBlock({
160
+ includeTransactions: false
161
+ }).then((b)=>b.timestamp)
162
+ ]);
163
+ const { lagInEpochsForValidatorSet, epochDuration, slotDuration } = this.l1constants;
164
+ const sub = BigInt(lagInEpochsForValidatorSet) * BigInt(epochDuration) * BigInt(slotDuration);
165
+ if (ts - sub > l1Timestamp) {
166
+ throw new Error(`Cannot query committee for future epoch ${epoch} with timestamp ${ts} (current L1 time is ${l1Timestamp}). Check your Ethereum node is synced.`);
167
+ }
168
+ const committee = committeeHex?.map((v)=>EthAddress.fromString(v));
169
+ return {
170
+ committee,
171
+ seed,
172
+ epoch
173
+ };
103
174
  }
104
175
  /**
105
176
  * Get the ABI encoding of the proposer index - see ValidatorSelectionLib.sol computeProposerIndex
@@ -118,47 +189,95 @@ import { getEpochCacheConfigEnvVars } from './config.js';
118
189
  name: 'seed'
119
190
  }
120
191
  ], [
121
- epoch,
122
- slot,
192
+ BigInt(epoch),
193
+ BigInt(slot),
123
194
  seed
124
195
  ]);
125
196
  }
126
197
  computeProposerIndex(slot, epoch, seed, size) {
198
+ // if committe size is 0, then mod 1 is 0
199
+ if (size === 0n) {
200
+ return 0n;
201
+ }
127
202
  return BigInt(keccak256(this.getProposerIndexEncoding(epoch, slot, seed))) % size;
128
203
  }
129
204
  /**
130
- * Returns the current and next proposer
205
+ * Returns the current and next proposer's attester address
131
206
  *
132
- * We return the next proposer as the node will check if it is the proposer at the next ethereum block, which
133
- * 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
- */ 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)];
207
+ * We return the next proposer's attester address as the node will check if it is the proposer at the next ethereum block,
208
+ * which can be the next slot. If this is the case, then it will send proposals early.
209
+ */ async getProposerAttesterAddressInCurrentOrNextSlot() {
210
+ const current = this.getEpochAndSlotNow();
211
+ const next = this.getEpochAndSlotInNextL1Slot();
151
212
  return {
152
- currentProposer,
153
- nextProposer,
154
- currentSlot,
155
- nextSlot
213
+ currentProposer: await this.getProposerAttesterAddressAt(current),
214
+ nextProposer: await this.getProposerAttesterAddressAt(next),
215
+ currentSlot: current.slot,
216
+ nextSlot: next.slot
156
217
  };
157
218
  }
158
219
  /**
159
- * Check if a validator is in the current epoch's committee
160
- */ async isInCommittee(validator) {
161
- const committee = await this.getCommittee();
220
+ * Get the proposer attester address in the given L2 slot
221
+ * @returns The proposer attester address. If the committee does not exist, we throw a NoCommitteeError.
222
+ * If the committee is empty (i.e. target committee size is 0, and anyone can propose), we return undefined.
223
+ */ getProposerAttesterAddressInSlot(slot) {
224
+ const epochAndSlot = this.getEpochAndSlotAtSlot(slot);
225
+ return this.getProposerAttesterAddressAt(epochAndSlot);
226
+ }
227
+ /**
228
+ * Get the proposer attester address in the next slot
229
+ * @returns The proposer attester address. If the committee does not exist, we throw a NoCommitteeError.
230
+ * If the committee is empty (i.e. target committee size is 0, and anyone can propose), we return undefined.
231
+ */ getProposerAttesterAddressInNextSlot() {
232
+ const epochAndSlot = this.getEpochAndSlotInNextL1Slot();
233
+ return this.getProposerAttesterAddressAt(epochAndSlot);
234
+ }
235
+ /**
236
+ * Get the proposer attester address at a given epoch and slot
237
+ * @param when - The epoch and slot to get the proposer attester address at
238
+ * @returns The proposer attester address. If the committee does not exist, we throw a NoCommitteeError.
239
+ * If the committee is empty (i.e. target committee size is 0, and anyone can propose), we return undefined.
240
+ */ async getProposerAttesterAddressAt(when) {
241
+ const { epoch, slot } = when;
242
+ const { committee, seed } = await this.getCommittee(slot);
243
+ if (!committee) {
244
+ throw new NoCommitteeError();
245
+ } else if (committee.length === 0) {
246
+ return undefined;
247
+ }
248
+ const proposerIndex = this.computeProposerIndex(slot, epoch, seed, BigInt(committee.length));
249
+ return committee[Number(proposerIndex)];
250
+ }
251
+ getProposerFromEpochCommittee(epochCommitteeInfo, slot) {
252
+ if (!epochCommitteeInfo.committee || epochCommitteeInfo.committee.length === 0) {
253
+ return undefined;
254
+ }
255
+ const proposerIndex = this.computeProposerIndex(slot, epochCommitteeInfo.epoch, epochCommitteeInfo.seed, BigInt(epochCommitteeInfo.committee.length));
256
+ return epochCommitteeInfo.committee[Number(proposerIndex)];
257
+ }
258
+ /** Check if a validator is in the given slot's committee */ async isInCommittee(slot, validator) {
259
+ const { committee } = await this.getCommittee(slot);
260
+ if (!committee) {
261
+ return false;
262
+ }
162
263
  return committee.some((v)=>v.equals(validator));
163
264
  }
265
+ /** From the set of given addresses, return all that are on the committee for the given slot */ async filterInCommittee(slot, validators) {
266
+ const { committee } = await this.getCommittee(slot);
267
+ if (!committee) {
268
+ return [];
269
+ }
270
+ const committeeSet = new Set(committee.map((v)=>v.toString()));
271
+ return validators.filter((v)=>committeeSet.has(v.toString()));
272
+ }
273
+ async getRegisteredValidators() {
274
+ const validatorRefreshIntervalMs = this.config.validatorRefreshIntervalSeconds * 1000;
275
+ const validatorRefreshTime = this.lastValidatorRefresh + validatorRefreshIntervalMs;
276
+ if (validatorRefreshTime < this.dateProvider.now()) {
277
+ const currentSet = await this.rollup.getAttesters();
278
+ this.allValidators = new Set(currentSet);
279
+ this.lastValidatorRefresh = this.dateProvider.now();
280
+ }
281
+ return Array.from(this.allValidators.keys().map((v)=>EthAddress.fromString(v)));
282
+ }
164
283
  }
package/dest/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export * from './epoch_cache.js';
2
2
  export * from './config.js';
3
- //# sourceMappingURL=index.d.ts.map
3
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguZC50cyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxjQUFjLGtCQUFrQixDQUFDO0FBQ2pDLGNBQWMsYUFBYSxDQUFDIn0=
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aztec/epoch-cache",
3
- "version": "0.0.0-test.1",
3
+ "version": "0.0.1-commit.03f7ef2",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dest/index.js",
@@ -15,12 +15,10 @@
15
15
  "tsconfig": "./tsconfig.json"
16
16
  },
17
17
  "scripts": {
18
- "build": "yarn clean && tsc -b",
19
- "build:dev": "tsc -b --watch",
18
+ "build": "yarn clean && ../scripts/tsc.sh",
19
+ "build:dev": "../scripts/tsc.sh --watch",
20
20
  "clean": "rm -rf ./dest .tsbuildinfo",
21
- "formatting": "run -T prettier --check ./src && run -T eslint ./src",
22
- "formatting:fix": "run -T eslint --fix ./src && run -T prettier -w ./src",
23
- "start:dev": "tsc-watch -p tsconfig.json --onSuccess 'yarn start'",
21
+ "start:dev": "concurrently -k \"tsgo -b -w\" \"nodemon --watch dest --exec yarn start\"",
24
22
  "start": "node ./dest/index.js",
25
23
  "test": "NODE_NO_WARNINGS=1 node --experimental-vm-modules ../node_modules/.bin/jest --passWithNoTests --maxWorkers=${JEST_MAX_WORKERS:-8}"
26
24
  },
@@ -28,25 +26,26 @@
28
26
  "../package.common.json"
29
27
  ],
30
28
  "dependencies": {
31
- "@aztec/ethereum": "0.0.0-test.1",
32
- "@aztec/foundation": "0.0.0-test.1",
33
- "@aztec/l1-artifacts": "0.0.0-test.1",
34
- "@aztec/stdlib": "0.0.0-test.1",
29
+ "@aztec/ethereum": "0.0.1-commit.03f7ef2",
30
+ "@aztec/foundation": "0.0.1-commit.03f7ef2",
31
+ "@aztec/l1-artifacts": "0.0.1-commit.03f7ef2",
32
+ "@aztec/stdlib": "0.0.1-commit.03f7ef2",
35
33
  "@viem/anvil": "^0.0.10",
36
34
  "dotenv": "^16.0.3",
37
35
  "get-port": "^7.1.0",
38
- "jest-mock-extended": "^3.0.7",
36
+ "jest-mock-extended": "^4.0.0",
39
37
  "tslib": "^2.4.0",
40
- "viem": "2.22.8",
38
+ "viem": "npm:@aztec/viem@2.38.2",
41
39
  "zod": "^3.23.8"
42
40
  },
43
41
  "devDependencies": {
44
- "@jest/globals": "^29.5.0",
45
- "@types/jest": "^29.5.0",
46
- "@types/node": "^18.14.6",
47
- "jest": "^29.5.0",
42
+ "@jest/globals": "^30.0.0",
43
+ "@types/jest": "^30.0.0",
44
+ "@types/node": "^22.15.17",
45
+ "@typescript/native-preview": "7.0.0-dev.20251126.1",
46
+ "jest": "^30.0.0",
48
47
  "ts-node": "^10.9.1",
49
- "typescript": "^5.0.4"
48
+ "typescript": "^5.3.3"
50
49
  },
51
50
  "files": [
52
51
  "dest",
@@ -85,9 +84,13 @@
85
84
  "testTimeout": 120000,
86
85
  "setupFiles": [
87
86
  "../../foundation/src/jest/setup.mjs"
87
+ ],
88
+ "testEnvironment": "../../foundation/src/jest/env.mjs",
89
+ "setupFilesAfterEnv": [
90
+ "../../foundation/src/jest/setupAfterEnv.mjs"
88
91
  ]
89
92
  },
90
93
  "engines": {
91
- "node": ">=18"
94
+ "node": ">=20.10"
92
95
  }
93
96
  }
package/src/config.ts CHANGED
@@ -1,18 +1,9 @@
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';
7
3
 
8
4
  export type EpochCacheConfig = Pick<
9
5
  L1ReaderConfig & L1ContractsConfig,
10
- | 'l1RpcUrls'
11
- | 'l1ChainId'
12
- | 'viemPollingIntervalMS'
13
- | 'aztecSlotDuration'
14
- | 'ethereumSlotDuration'
15
- | 'aztecEpochDuration'
6
+ 'l1RpcUrls' | 'l1ChainId' | 'viemPollingIntervalMS' | 'ethereumSlotDuration'
16
7
  >;
17
8
 
18
9
  export function getEpochCacheConfigEnvVars(): EpochCacheConfig {
@@ -1,231 +1,359 @@
1
- import { RollupContract, createEthereumChain } from '@aztec/ethereum';
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: bigint;
19
- slot: bigint;
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
+ };
32
+
33
+ export type SlotTag = 'now' | 'next' | SlotNumber;
34
+
23
35
  export interface EpochCacheInterface {
24
- getCommittee(nextSlot: boolean): Promise<EthAddress[]>;
36
+ getCommittee(slot: SlotTag | undefined): Promise<EpochCommitteeInfo>;
25
37
  getEpochAndSlotNow(): EpochAndSlot;
26
- getProposerIndexEncoding(epoch: bigint, slot: bigint, seed: bigint): `0x${string}`;
27
- computeProposerIndex(slot: bigint, epoch: bigint, seed: bigint, size: bigint): bigint;
28
- getProposerInCurrentOrNextSlot(): Promise<{
29
- currentProposer: EthAddress;
30
- nextProposer: EthAddress;
31
- currentSlot: bigint;
32
- nextSlot: bigint;
38
+ getEpochAndSlotInNextL1Slot(): EpochAndSlot & { now: bigint };
39
+ getProposerIndexEncoding(epoch: EpochNumber, slot: SlotNumber, seed: bigint): `0x${string}`;
40
+ computeProposerIndex(slot: SlotNumber, epoch: EpochNumber, seed: bigint, size: bigint): bigint;
41
+ getProposerAttesterAddressInCurrentOrNextSlot(): Promise<{
42
+ currentProposer: EthAddress | undefined;
43
+ nextProposer: EthAddress | undefined;
44
+ currentSlot: SlotNumber;
45
+ nextSlot: SlotNumber;
33
46
  }>;
34
- isInCommittee(validator: EthAddress): Promise<boolean>;
47
+ getRegisteredValidators(): Promise<EthAddress[]>;
48
+ isInCommittee(slot: SlotTag, validator: EthAddress): Promise<boolean>;
49
+ filterInCommittee(slot: SlotTag, validators: EthAddress[]): Promise<EthAddress[]>;
35
50
  }
36
51
 
37
52
  /**
38
53
  * Epoch cache
39
54
  *
40
55
  * This class is responsible for managing traffic to the l1 node, by caching the validator set.
56
+ * Keeps the last N epochs in cache.
41
57
  * It also provides a method to get the current or next proposer, and to check who is in the current slot.
42
58
  *
43
- * If the epoch changes, then we update the stored validator set.
44
- *
45
59
  * Note: This class is very dependent on the system clock being in sync.
46
60
  */
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;
61
+ export class EpochCache implements EpochCacheInterface {
62
+ // eslint-disable-next-line aztec-custom/no-non-primitive-in-collections
63
+ protected cache: Map<EpochNumber, EpochCommitteeInfo> = new Map();
64
+ private allValidators: Set<string> = new Set();
65
+ private lastValidatorRefresh = 0;
54
66
  private readonly log: Logger = createLogger('epoch-cache');
55
67
 
56
68
  constructor(
57
69
  private rollup: RollupContract,
58
- initialValidators: EthAddress[] = [],
59
- initialSampleSeed: bigint = 0n,
60
- private readonly l1constants: L1RollupConstants = EmptyL1RollupConstants,
70
+ private readonly l1constants: L1RollupConstants & {
71
+ lagInEpochsForValidatorSet: number;
72
+ lagInEpochsForRandao: number;
73
+ },
61
74
  private readonly dateProvider: DateProvider = new DateProvider(),
75
+ protected readonly config = { cacheSize: 12, validatorRefreshIntervalSeconds: 60 },
62
76
  ) {
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);
77
+ this.log.debug(`Initialized EpochCache`, {
78
+ l1constants,
79
+ });
70
80
  }
71
81
 
72
82
  static async create(
73
- rollupAddress: EthAddress,
83
+ rollupOrAddress: EthAddress | RollupContract,
74
84
  config?: EpochCacheConfig,
75
85
  deps: { dateProvider?: DateProvider } = {},
76
86
  ) {
77
87
  config = config ?? getEpochCacheConfigEnvVars();
78
88
 
79
- const chain = createEthereumChain(config.l1RpcUrls, config.l1ChainId);
80
- const publicClient = createPublicClient({
81
- chain: chain.chainInfo,
82
- transport: fallback(config.l1RpcUrls.map(url => http(url))),
83
- pollingInterval: config.viemPollingIntervalMS,
84
- });
89
+ // Load the rollup contract if we were given an address
90
+ let rollup: RollupContract;
91
+ if ('address' in rollupOrAddress) {
92
+ rollup = rollupOrAddress;
93
+ } else {
94
+ const chain = createEthereumChain(config.l1RpcUrls, config.l1ChainId);
95
+ const publicClient = createPublicClient({
96
+ chain: chain.chainInfo,
97
+ transport: fallback(config.l1RpcUrls.map(url => http(url, { batch: false }))),
98
+ pollingInterval: config.viemPollingIntervalMS,
99
+ });
100
+ rollup = new RollupContract(publicClient, rollupOrAddress.toString());
101
+ }
85
102
 
86
- const rollup = new RollupContract(publicClient, rollupAddress.toString());
87
- const [l1StartBlock, l1GenesisTime, initialValidators, sampleSeed] = await Promise.all([
103
+ const [
104
+ l1StartBlock,
105
+ l1GenesisTime,
106
+ proofSubmissionEpochs,
107
+ slotDuration,
108
+ epochDuration,
109
+ lagInEpochsForValidatorSet,
110
+ lagInEpochsForRandao,
111
+ ] = await Promise.all([
88
112
  rollup.getL1StartBlock(),
89
113
  rollup.getL1GenesisTime(),
90
- rollup.getCurrentEpochCommittee(),
91
- rollup.getCurrentSampleSeed(),
114
+ rollup.getProofSubmissionEpochs(),
115
+ rollup.getSlotDuration(),
116
+ rollup.getEpochDuration(),
117
+ rollup.getLagInEpochsForValidatorSet(),
118
+ rollup.getLagInEpochsForRandao(),
92
119
  ] as const);
93
120
 
94
- const l1RollupConstants: L1RollupConstants = {
121
+ const l1RollupConstants = {
95
122
  l1StartBlock,
96
123
  l1GenesisTime,
97
- slotDuration: config.aztecSlotDuration,
98
- epochDuration: config.aztecEpochDuration,
124
+ proofSubmissionEpochs: Number(proofSubmissionEpochs),
125
+ slotDuration: Number(slotDuration),
126
+ epochDuration: Number(epochDuration),
99
127
  ethereumSlotDuration: config.ethereumSlotDuration,
128
+ lagInEpochsForValidatorSet: Number(lagInEpochsForValidatorSet),
129
+ lagInEpochsForRandao: Number(lagInEpochsForRandao),
100
130
  };
101
131
 
102
- return new EpochCache(
103
- rollup,
104
- initialValidators.map(v => EthAddress.fromString(v)),
105
- sampleSeed,
106
- l1RollupConstants,
107
- deps.dateProvider,
108
- );
132
+ return new EpochCache(rollup, l1RollupConstants, deps.dateProvider);
133
+ }
134
+
135
+ public getL1Constants(): L1RollupConstants {
136
+ return this.l1constants;
137
+ }
138
+
139
+ public getEpochAndSlotNow(): EpochAndSlot & { now: bigint } {
140
+ const now = this.nowInSeconds();
141
+ return { ...this.getEpochAndSlotAtTimestamp(now), now };
109
142
  }
110
143
 
111
- private nowInSeconds(): bigint {
144
+ public nowInSeconds(): bigint {
112
145
  return BigInt(Math.floor(this.dateProvider.now() / 1000));
113
146
  }
114
147
 
115
- getEpochAndSlotNow(): EpochAndSlot {
116
- return this.getEpochAndSlotAtTimestamp(this.nowInSeconds());
148
+ private getEpochAndSlotAtSlot(slot: SlotNumber): EpochAndSlot {
149
+ const epoch = getEpochAtSlot(slot, this.l1constants);
150
+ const ts = getTimestampRangeForEpoch(epoch, this.l1constants)[0];
151
+ return { epoch, ts, slot };
117
152
  }
118
153
 
119
- private getEpochAndSlotInNextSlot(): EpochAndSlot {
120
- const nextSlotTs = this.nowInSeconds() + BigInt(this.l1constants.slotDuration);
121
- return this.getEpochAndSlotAtTimestamp(nextSlotTs);
154
+ public getEpochAndSlotInNextL1Slot(): EpochAndSlot & { now: bigint } {
155
+ const now = this.nowInSeconds();
156
+ const nextSlotTs = now + BigInt(this.l1constants.ethereumSlotDuration);
157
+ return { ...this.getEpochAndSlotAtTimestamp(nextSlotTs), now };
122
158
  }
123
159
 
124
160
  private getEpochAndSlotAtTimestamp(ts: bigint): EpochAndSlot {
161
+ const slot = getSlotAtTimestamp(ts, this.l1constants);
125
162
  return {
126
163
  epoch: getEpochNumberAtTimestamp(ts, this.l1constants),
127
- slot: getSlotAtTimestamp(ts, this.l1constants),
128
- ts,
164
+ ts: getTimestampForSlot(slot, this.l1constants),
165
+ slot,
129
166
  };
130
167
  }
131
168
 
169
+ public getCommitteeForEpoch(epoch: EpochNumber): Promise<EpochCommitteeInfo> {
170
+ const [startSlot] = getSlotRangeForEpoch(epoch, this.l1constants);
171
+ return this.getCommittee(startSlot);
172
+ }
173
+
132
174
  /**
133
175
  * Get the current validator set
134
- *
135
176
  * @param nextSlot - If true, get the validator set for the next slot.
136
177
  * @returns The current validator set.
137
178
  */
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);
179
+ public async getCommittee(slot: SlotTag = 'now'): Promise<EpochCommitteeInfo> {
180
+ const { epoch, ts } = this.getEpochAndTimestamp(slot);
181
+
182
+ if (this.cache.has(epoch)) {
183
+ return this.cache.get(epoch)!;
184
+ }
185
+
186
+ const epochData = await this.computeCommittee({ epoch, ts });
187
+ // If the committee size is 0 or undefined, then do not cache
188
+ if (!epochData.committee || epochData.committee.length === 0) {
189
+ return epochData;
156
190
  }
191
+ this.cache.set(epoch, epochData);
157
192
 
158
- return this.committee;
193
+ const toPurge = Array.from(this.cache.keys())
194
+ .sort((a, b) => Number(b - a))
195
+ .slice(this.config.cacheSize);
196
+ toPurge.forEach(key => this.cache.delete(key));
197
+
198
+ return epochData;
199
+ }
200
+
201
+ private getEpochAndTimestamp(slot: SlotTag = 'now') {
202
+ if (slot === 'now') {
203
+ return this.getEpochAndSlotNow();
204
+ } else if (slot === 'next') {
205
+ return this.getEpochAndSlotInNextL1Slot();
206
+ } else {
207
+ return this.getEpochAndSlotAtSlot(slot);
208
+ }
209
+ }
210
+
211
+ private async computeCommittee(when: { epoch: EpochNumber; ts: bigint }): Promise<EpochCommitteeInfo> {
212
+ const { ts, epoch } = when;
213
+ const [committeeHex, seed, l1Timestamp] = await Promise.all([
214
+ this.rollup.getCommitteeAt(ts),
215
+ this.rollup.getSampleSeedAt(ts),
216
+ this.rollup.client.getBlock({ includeTransactions: false }).then(b => b.timestamp),
217
+ ]);
218
+ const { lagInEpochsForValidatorSet, epochDuration, slotDuration } = this.l1constants;
219
+ const sub = BigInt(lagInEpochsForValidatorSet) * BigInt(epochDuration) * BigInt(slotDuration);
220
+ if (ts - sub > l1Timestamp) {
221
+ throw new Error(
222
+ `Cannot query committee for future epoch ${epoch} with timestamp ${ts} (current L1 time is ${l1Timestamp}). Check your Ethereum node is synced.`,
223
+ );
224
+ }
225
+ const committee = committeeHex?.map((v: `0x${string}`) => EthAddress.fromString(v));
226
+ return { committee, seed, epoch };
159
227
  }
160
228
 
161
229
  /**
162
230
  * Get the ABI encoding of the proposer index - see ValidatorSelectionLib.sol computeProposerIndex
163
231
  */
164
- getProposerIndexEncoding(epoch: bigint, slot: bigint, seed: bigint): `0x${string}` {
232
+ getProposerIndexEncoding(epoch: EpochNumber, slot: SlotNumber, seed: bigint): `0x${string}` {
165
233
  return encodeAbiParameters(
166
234
  [
167
235
  { type: 'uint256', name: 'epoch' },
168
236
  { type: 'uint256', name: 'slot' },
169
237
  { type: 'uint256', name: 'seed' },
170
238
  ],
171
- [epoch, slot, seed],
239
+ [BigInt(epoch), BigInt(slot), seed],
172
240
  );
173
241
  }
174
242
 
175
- computeProposerIndex(slot: bigint, epoch: bigint, seed: bigint, size: bigint): bigint {
243
+ public computeProposerIndex(slot: SlotNumber, epoch: EpochNumber, seed: bigint, size: bigint): bigint {
244
+ // if committe size is 0, then mod 1 is 0
245
+ if (size === 0n) {
246
+ return 0n;
247
+ }
176
248
  return BigInt(keccak256(this.getProposerIndexEncoding(epoch, slot, seed))) % size;
177
249
  }
178
250
 
179
251
  /**
180
- * Returns the current and next proposer
252
+ * Returns the current and next proposer's attester address
181
253
  *
182
- * We return the next proposer as the node will check if it is the proposer at the next ethereum block, which
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.
254
+ * We return the next proposer's attester address as the node will check if it is the proposer at the next ethereum block,
255
+ * which can be the next slot. If this is the case, then it will send proposals early.
187
256
  */
188
- async getProposerInCurrentOrNextSlot(): Promise<{
189
- currentProposer: EthAddress;
190
- nextProposer: EthAddress;
191
- currentSlot: bigint;
192
- nextSlot: bigint;
257
+ public async getProposerAttesterAddressInCurrentOrNextSlot(): Promise<{
258
+ currentSlot: SlotNumber;
259
+ nextSlot: SlotNumber;
260
+ currentProposer: EthAddress | undefined;
261
+ nextProposer: EthAddress | undefined;
193
262
  }> {
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
- );
263
+ const current = this.getEpochAndSlotNow();
264
+ const next = this.getEpochAndSlotInNextL1Slot();
206
265
 
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
- );
266
+ return {
267
+ currentProposer: await this.getProposerAttesterAddressAt(current),
268
+ nextProposer: await this.getProposerAttesterAddressAt(next),
269
+ currentSlot: current.slot,
270
+ nextSlot: next.slot,
271
+ };
272
+ }
217
273
 
218
- const currentProposer = committee[Number(proposerIndex)];
219
- const nextProposer = committee[Number(nextProposerIndex)];
274
+ /**
275
+ * Get the proposer attester address in the given L2 slot
276
+ * @returns The proposer attester address. If the committee does not exist, we throw a NoCommitteeError.
277
+ * If the committee is empty (i.e. target committee size is 0, and anyone can propose), we return undefined.
278
+ */
279
+ public getProposerAttesterAddressInSlot(slot: SlotNumber): Promise<EthAddress | undefined> {
280
+ const epochAndSlot = this.getEpochAndSlotAtSlot(slot);
281
+ return this.getProposerAttesterAddressAt(epochAndSlot);
282
+ }
220
283
 
221
- return { currentProposer, nextProposer, currentSlot, nextSlot };
284
+ /**
285
+ * Get the proposer attester address in the next slot
286
+ * @returns The proposer attester address. If the committee does not exist, we throw a NoCommitteeError.
287
+ * If the committee is empty (i.e. target committee size is 0, and anyone can propose), we return undefined.
288
+ */
289
+ public getProposerAttesterAddressInNextSlot(): Promise<EthAddress | undefined> {
290
+ const epochAndSlot = this.getEpochAndSlotInNextL1Slot();
291
+ return this.getProposerAttesterAddressAt(epochAndSlot);
222
292
  }
223
293
 
224
294
  /**
225
- * Check if a validator is in the current epoch's committee
295
+ * Get the proposer attester address at a given epoch and slot
296
+ * @param when - The epoch and slot to get the proposer attester address at
297
+ * @returns The proposer attester address. If the committee does not exist, we throw a NoCommitteeError.
298
+ * If the committee is empty (i.e. target committee size is 0, and anyone can propose), we return undefined.
226
299
  */
227
- async isInCommittee(validator: EthAddress): Promise<boolean> {
228
- const committee = await this.getCommittee();
300
+ private async getProposerAttesterAddressAt(when: EpochAndSlot) {
301
+ const { epoch, slot } = when;
302
+ const { committee, seed } = await this.getCommittee(slot);
303
+ if (!committee) {
304
+ throw new NoCommitteeError();
305
+ } else if (committee.length === 0) {
306
+ return undefined;
307
+ }
308
+
309
+ const proposerIndex = this.computeProposerIndex(slot, epoch, seed, BigInt(committee.length));
310
+ return committee[Number(proposerIndex)];
311
+ }
312
+
313
+ public getProposerFromEpochCommittee(
314
+ epochCommitteeInfo: EpochCommitteeInfo,
315
+ slot: SlotNumber,
316
+ ): EthAddress | undefined {
317
+ if (!epochCommitteeInfo.committee || epochCommitteeInfo.committee.length === 0) {
318
+ return undefined;
319
+ }
320
+ const proposerIndex = this.computeProposerIndex(
321
+ slot,
322
+ epochCommitteeInfo.epoch,
323
+ epochCommitteeInfo.seed,
324
+ BigInt(epochCommitteeInfo.committee.length),
325
+ );
326
+
327
+ return epochCommitteeInfo.committee[Number(proposerIndex)];
328
+ }
329
+
330
+ /** Check if a validator is in the given slot's committee */
331
+ async isInCommittee(slot: SlotTag, validator: EthAddress): Promise<boolean> {
332
+ const { committee } = await this.getCommittee(slot);
333
+ if (!committee) {
334
+ return false;
335
+ }
229
336
  return committee.some(v => v.equals(validator));
230
337
  }
338
+
339
+ /** From the set of given addresses, return all that are on the committee for the given slot */
340
+ async filterInCommittee(slot: SlotTag, validators: EthAddress[]): Promise<EthAddress[]> {
341
+ const { committee } = await this.getCommittee(slot);
342
+ if (!committee) {
343
+ return [];
344
+ }
345
+ const committeeSet = new Set(committee.map(v => v.toString()));
346
+ return validators.filter(v => committeeSet.has(v.toString()));
347
+ }
348
+
349
+ async getRegisteredValidators(): Promise<EthAddress[]> {
350
+ const validatorRefreshIntervalMs = this.config.validatorRefreshIntervalSeconds * 1000;
351
+ const validatorRefreshTime = this.lastValidatorRefresh + validatorRefreshIntervalMs;
352
+ if (validatorRefreshTime < this.dateProvider.now()) {
353
+ const currentSet = await this.rollup.getAttesters();
354
+ this.allValidators = new Set(currentSet);
355
+ this.lastValidatorRefresh = this.dateProvider.now();
356
+ }
357
+ return Array.from(this.allValidators.keys().map(v => EthAddress.fromString(v)));
358
+ }
231
359
  }
@@ -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