@aztec/epoch-cache 0.0.0-test.0

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,4 @@
1
+ import { type L1ContractsConfig, type L1ReaderConfig } from '@aztec/ethereum';
2
+ export type EpochCacheConfig = Pick<L1ReaderConfig & L1ContractsConfig, 'l1RpcUrls' | 'l1ChainId' | 'viemPollingIntervalMS' | 'aztecSlotDuration' | 'ethereumSlotDuration' | 'aztecEpochDuration'>;
3
+ export declare function getEpochCacheConfigEnvVars(): EpochCacheConfig;
4
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +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"}
package/dest/config.js ADDED
@@ -0,0 +1,7 @@
1
+ import { getL1ContractsConfigEnvVars, getL1ReaderConfigFromEnv } from '@aztec/ethereum';
2
+ export function getEpochCacheConfigEnvVars() {
3
+ return {
4
+ ...getL1ReaderConfigFromEnv(),
5
+ ...getL1ContractsConfigEnvVars()
6
+ };
7
+ }
@@ -0,0 +1,87 @@
1
+ /// <reference types="node" resolution-mode="require"/>
2
+ import { RollupContract } from '@aztec/ethereum';
3
+ import { EthAddress } from '@aztec/foundation/eth-address';
4
+ import { DateProvider } from '@aztec/foundation/timer';
5
+ import { type L1RollupConstants } from '@aztec/stdlib/epoch-helpers';
6
+ import { EventEmitter } from 'node:events';
7
+ import { type EpochCacheConfig } from './config.js';
8
+ type EpochAndSlot = {
9
+ epoch: bigint;
10
+ slot: bigint;
11
+ ts: bigint;
12
+ };
13
+ export interface EpochCacheInterface {
14
+ getCommittee(nextSlot: boolean): Promise<EthAddress[]>;
15
+ 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;
23
+ }>;
24
+ isInCommittee(validator: EthAddress): Promise<boolean>;
25
+ }
26
+ /**
27
+ * Epoch cache
28
+ *
29
+ * This class is responsible for managing traffic to the l1 node, by caching the validator set.
30
+ * It also provides a method to get the current or next proposer, and to check who is in the current slot.
31
+ *
32
+ * If the epoch changes, then we update the stored validator set.
33
+ *
34
+ * Note: This class is very dependent on the system clock being in sync.
35
+ */
36
+ export declare class EpochCache extends EventEmitter<{
37
+ committeeChanged: [EthAddress[], bigint];
38
+ }> implements EpochCacheInterface {
39
+ private rollup;
40
+ private readonly l1constants;
41
+ private readonly dateProvider;
42
+ private committee;
43
+ private cachedEpoch;
44
+ private cachedSampleSeed;
45
+ private readonly log;
46
+ constructor(rollup: RollupContract, initialValidators?: EthAddress[], initialSampleSeed?: bigint, l1constants?: L1RollupConstants, dateProvider?: DateProvider);
47
+ static create(rollupAddress: EthAddress, config?: EpochCacheConfig, deps?: {
48
+ dateProvider?: DateProvider;
49
+ }): Promise<EpochCache>;
50
+ private nowInSeconds;
51
+ getEpochAndSlotNow(): EpochAndSlot;
52
+ private getEpochAndSlotInNextSlot;
53
+ private getEpochAndSlotAtTimestamp;
54
+ /**
55
+ * Get the current validator set
56
+ *
57
+ * @param nextSlot - If true, get the validator set for the next slot.
58
+ * @returns The current validator set.
59
+ */
60
+ getCommittee(nextSlot?: boolean): Promise<EthAddress[]>;
61
+ /**
62
+ * Get the ABI encoding of the proposer index - see ValidatorSelectionLib.sol computeProposerIndex
63
+ */
64
+ getProposerIndexEncoding(epoch: bigint, slot: bigint, seed: bigint): `0x${string}`;
65
+ computeProposerIndex(slot: bigint, epoch: bigint, seed: bigint, size: bigint): bigint;
66
+ /**
67
+ * Returns the current and next proposer
68
+ *
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.
74
+ */
75
+ getProposerInCurrentOrNextSlot(): Promise<{
76
+ currentProposer: EthAddress;
77
+ nextProposer: EthAddress;
78
+ currentSlot: bigint;
79
+ nextSlot: bigint;
80
+ }>;
81
+ /**
82
+ * Check if a validator is in the current epoch's committee
83
+ */
84
+ isInCommittee(validator: EthAddress): Promise<boolean>;
85
+ }
86
+ export {};
87
+ //# sourceMappingURL=epoch_cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"epoch_cache.d.ts","sourceRoot":"","sources":["../src/epoch_cache.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,164 @@
1
+ import { RollupContract, createEthereumChain } from '@aztec/ethereum';
2
+ import { EthAddress } from '@aztec/foundation/eth-address';
3
+ import { createLogger } from '@aztec/foundation/log';
4
+ import { DateProvider } from '@aztec/foundation/timer';
5
+ import { EmptyL1RollupConstants, getEpochNumberAtTimestamp, getSlotAtTimestamp } from '@aztec/stdlib/epoch-helpers';
6
+ import { EventEmitter } from 'node:events';
7
+ import { createPublicClient, encodeAbiParameters, fallback, http, keccak256 } from 'viem';
8
+ import { getEpochCacheConfigEnvVars } from './config.js';
9
+ /**
10
+ * Epoch cache
11
+ *
12
+ * This class is responsible for managing traffic to the l1 node, by caching the validator set.
13
+ * It also provides a method to get the current or next proposer, and to check who is in the current slot.
14
+ *
15
+ * If the epoch changes, then we update the stored validator set.
16
+ *
17
+ * Note: This class is very dependent on the system clock being in sync.
18
+ */ export class EpochCache extends EventEmitter {
19
+ rollup;
20
+ l1constants;
21
+ dateProvider;
22
+ committee;
23
+ cachedEpoch;
24
+ cachedSampleSeed;
25
+ 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
33
+ });
34
+ this.cachedEpoch = getEpochNumberAtTimestamp(this.nowInSeconds(), this.l1constants);
35
+ }
36
+ static async create(rollupAddress, config, deps = {}) {
37
+ 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([
46
+ rollup.getL1StartBlock(),
47
+ rollup.getL1GenesisTime(),
48
+ rollup.getCurrentEpochCommittee(),
49
+ rollup.getCurrentSampleSeed()
50
+ ]);
51
+ const l1RollupConstants = {
52
+ l1StartBlock,
53
+ l1GenesisTime,
54
+ slotDuration: config.aztecSlotDuration,
55
+ epochDuration: config.aztecEpochDuration,
56
+ ethereumSlotDuration: config.ethereumSlotDuration
57
+ };
58
+ return new EpochCache(rollup, initialValidators.map((v)=>EthAddress.fromString(v)), sampleSeed, l1RollupConstants, deps.dateProvider);
59
+ }
60
+ nowInSeconds() {
61
+ return BigInt(Math.floor(this.dateProvider.now() / 1000));
62
+ }
63
+ getEpochAndSlotNow() {
64
+ return this.getEpochAndSlotAtTimestamp(this.nowInSeconds());
65
+ }
66
+ getEpochAndSlotInNextSlot() {
67
+ const nextSlotTs = this.nowInSeconds() + BigInt(this.l1constants.slotDuration);
68
+ return this.getEpochAndSlotAtTimestamp(nextSlotTs);
69
+ }
70
+ getEpochAndSlotAtTimestamp(ts) {
71
+ return {
72
+ epoch: getEpochNumberAtTimestamp(ts, this.l1constants),
73
+ slot: getSlotAtTimestamp(ts, this.l1constants),
74
+ ts
75
+ };
76
+ }
77
+ /**
78
+ * Get the current validator set
79
+ *
80
+ * @param nextSlot - If true, get the validator set for the next slot.
81
+ * @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);
101
+ }
102
+ return this.committee;
103
+ }
104
+ /**
105
+ * Get the ABI encoding of the proposer index - see ValidatorSelectionLib.sol computeProposerIndex
106
+ */ getProposerIndexEncoding(epoch, slot, seed) {
107
+ return encodeAbiParameters([
108
+ {
109
+ type: 'uint256',
110
+ name: 'epoch'
111
+ },
112
+ {
113
+ type: 'uint256',
114
+ name: 'slot'
115
+ },
116
+ {
117
+ type: 'uint256',
118
+ name: 'seed'
119
+ }
120
+ ], [
121
+ epoch,
122
+ slot,
123
+ seed
124
+ ]);
125
+ }
126
+ computeProposerIndex(slot, epoch, seed, size) {
127
+ return BigInt(keccak256(this.getProposerIndexEncoding(epoch, slot, seed))) % size;
128
+ }
129
+ /**
130
+ * Returns the current and next proposer
131
+ *
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)];
151
+ return {
152
+ currentProposer,
153
+ nextProposer,
154
+ currentSlot,
155
+ nextSlot
156
+ };
157
+ }
158
+ /**
159
+ * Check if a validator is in the current epoch's committee
160
+ */ async isInCommittee(validator) {
161
+ const committee = await this.getCommittee();
162
+ return committee.some((v)=>v.equals(validator));
163
+ }
164
+ }
@@ -0,0 +1,3 @@
1
+ export * from './epoch_cache.js';
2
+ export * from './config.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,aAAa,CAAC"}
package/dest/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from './epoch_cache.js';
2
+ export * from './config.js';
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=timestamp_provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timestamp_provider.d.ts","sourceRoot":"","sources":["../src/timestamp_provider.ts"],"names":[],"mappings":""}
File without changes
package/package.json ADDED
@@ -0,0 +1,93 @@
1
+ {
2
+ "name": "@aztec/epoch-cache",
3
+ "version": "0.0.0-test.0",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": "./dest/index.js",
7
+ "./test": "./dest/test/index.js",
8
+ "./contracts": "./dest/contracts/index.js"
9
+ },
10
+ "typedocOptions": {
11
+ "entryPoints": [
12
+ "./src/index.ts"
13
+ ],
14
+ "name": "Epoch Cache",
15
+ "tsconfig": "./tsconfig.json"
16
+ },
17
+ "scripts": {
18
+ "build": "yarn clean && tsc -b",
19
+ "build:dev": "tsc -b --watch",
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'",
24
+ "start": "node ./dest/index.js",
25
+ "test": "NODE_NO_WARNINGS=1 node --experimental-vm-modules ../node_modules/.bin/jest --passWithNoTests --maxWorkers=${JEST_MAX_WORKERS:-8}"
26
+ },
27
+ "inherits": [
28
+ "../package.common.json"
29
+ ],
30
+ "dependencies": {
31
+ "@aztec/ethereum": "0.0.0-test.0",
32
+ "@aztec/foundation": "0.0.0-test.0",
33
+ "@aztec/l1-artifacts": "0.0.0-test.0",
34
+ "@aztec/stdlib": "0.0.0-test.0",
35
+ "@viem/anvil": "^0.0.10",
36
+ "dotenv": "^16.0.3",
37
+ "get-port": "^7.1.0",
38
+ "jest-mock-extended": "^3.0.7",
39
+ "tslib": "^2.4.0",
40
+ "viem": "2.22.8",
41
+ "zod": "^3.23.8"
42
+ },
43
+ "devDependencies": {
44
+ "@jest/globals": "^29.5.0",
45
+ "@types/jest": "^29.5.0",
46
+ "@types/node": "^18.14.6",
47
+ "jest": "^29.5.0",
48
+ "ts-node": "^10.9.1",
49
+ "typescript": "^5.0.4"
50
+ },
51
+ "files": [
52
+ "dest",
53
+ "src",
54
+ "!*.test.*"
55
+ ],
56
+ "types": "./dest/index.d.ts",
57
+ "jest": {
58
+ "moduleNameMapper": {
59
+ "^(\\.{1,2}/.*)\\.[cm]?js$": "$1"
60
+ },
61
+ "testRegex": "./src/.*\\.test\\.(js|mjs|ts)$",
62
+ "rootDir": "./src",
63
+ "transform": {
64
+ "^.+\\.tsx?$": [
65
+ "@swc/jest",
66
+ {
67
+ "jsc": {
68
+ "parser": {
69
+ "syntax": "typescript",
70
+ "decorators": true
71
+ },
72
+ "transform": {
73
+ "decoratorVersion": "2022-03"
74
+ }
75
+ }
76
+ }
77
+ ]
78
+ },
79
+ "extensionsToTreatAsEsm": [
80
+ ".ts"
81
+ ],
82
+ "reporters": [
83
+ "default"
84
+ ],
85
+ "testTimeout": 120000,
86
+ "setupFiles": [
87
+ "../../foundation/src/jest/setup.mjs"
88
+ ]
89
+ },
90
+ "engines": {
91
+ "node": ">=18"
92
+ }
93
+ }
package/src/config.ts ADDED
@@ -0,0 +1,20 @@
1
+ import {
2
+ type L1ContractsConfig,
3
+ type L1ReaderConfig,
4
+ getL1ContractsConfigEnvVars,
5
+ getL1ReaderConfigFromEnv,
6
+ } from '@aztec/ethereum';
7
+
8
+ export type EpochCacheConfig = Pick<
9
+ L1ReaderConfig & L1ContractsConfig,
10
+ | 'l1RpcUrls'
11
+ | 'l1ChainId'
12
+ | 'viemPollingIntervalMS'
13
+ | 'aztecSlotDuration'
14
+ | 'ethereumSlotDuration'
15
+ | 'aztecEpochDuration'
16
+ >;
17
+
18
+ export function getEpochCacheConfigEnvVars(): EpochCacheConfig {
19
+ return { ...getL1ReaderConfigFromEnv(), ...getL1ContractsConfigEnvVars() };
20
+ }
@@ -0,0 +1,231 @@
1
+ import { RollupContract, createEthereumChain } from '@aztec/ethereum';
2
+ import { EthAddress } from '@aztec/foundation/eth-address';
3
+ import { type Logger, createLogger } from '@aztec/foundation/log';
4
+ import { DateProvider } from '@aztec/foundation/timer';
5
+ import {
6
+ EmptyL1RollupConstants,
7
+ type L1RollupConstants,
8
+ getEpochNumberAtTimestamp,
9
+ getSlotAtTimestamp,
10
+ } from '@aztec/stdlib/epoch-helpers';
11
+
12
+ import { EventEmitter } from 'node:events';
13
+ import { createPublicClient, encodeAbiParameters, fallback, http, keccak256 } from 'viem';
14
+
15
+ import { type EpochCacheConfig, getEpochCacheConfigEnvVars } from './config.js';
16
+
17
+ type EpochAndSlot = {
18
+ epoch: bigint;
19
+ slot: bigint;
20
+ ts: bigint;
21
+ };
22
+
23
+ export interface EpochCacheInterface {
24
+ getCommittee(nextSlot: boolean): Promise<EthAddress[]>;
25
+ 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;
33
+ }>;
34
+ isInCommittee(validator: EthAddress): Promise<boolean>;
35
+ }
36
+
37
+ /**
38
+ * Epoch cache
39
+ *
40
+ * This class is responsible for managing traffic to the l1 node, by caching the validator set.
41
+ * It also provides a method to get the current or next proposer, and to check who is in the current slot.
42
+ *
43
+ * If the epoch changes, then we update the stored validator set.
44
+ *
45
+ * Note: This class is very dependent on the system clock being in sync.
46
+ */
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;
54
+ private readonly log: Logger = createLogger('epoch-cache');
55
+
56
+ constructor(
57
+ private rollup: RollupContract,
58
+ initialValidators: EthAddress[] = [],
59
+ initialSampleSeed: bigint = 0n,
60
+ private readonly l1constants: L1RollupConstants = EmptyL1RollupConstants,
61
+ private readonly dateProvider: DateProvider = new DateProvider(),
62
+ ) {
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);
70
+ }
71
+
72
+ static async create(
73
+ rollupAddress: EthAddress,
74
+ config?: EpochCacheConfig,
75
+ deps: { dateProvider?: DateProvider } = {},
76
+ ) {
77
+ config = config ?? getEpochCacheConfigEnvVars();
78
+
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
+ });
85
+
86
+ const rollup = new RollupContract(publicClient, rollupAddress.toString());
87
+ const [l1StartBlock, l1GenesisTime, initialValidators, sampleSeed] = await Promise.all([
88
+ rollup.getL1StartBlock(),
89
+ rollup.getL1GenesisTime(),
90
+ rollup.getCurrentEpochCommittee(),
91
+ rollup.getCurrentSampleSeed(),
92
+ ] as const);
93
+
94
+ const l1RollupConstants: L1RollupConstants = {
95
+ l1StartBlock,
96
+ l1GenesisTime,
97
+ slotDuration: config.aztecSlotDuration,
98
+ epochDuration: config.aztecEpochDuration,
99
+ ethereumSlotDuration: config.ethereumSlotDuration,
100
+ };
101
+
102
+ return new EpochCache(
103
+ rollup,
104
+ initialValidators.map(v => EthAddress.fromString(v)),
105
+ sampleSeed,
106
+ l1RollupConstants,
107
+ deps.dateProvider,
108
+ );
109
+ }
110
+
111
+ private nowInSeconds(): bigint {
112
+ return BigInt(Math.floor(this.dateProvider.now() / 1000));
113
+ }
114
+
115
+ getEpochAndSlotNow(): EpochAndSlot {
116
+ return this.getEpochAndSlotAtTimestamp(this.nowInSeconds());
117
+ }
118
+
119
+ private getEpochAndSlotInNextSlot(): EpochAndSlot {
120
+ const nextSlotTs = this.nowInSeconds() + BigInt(this.l1constants.slotDuration);
121
+ return this.getEpochAndSlotAtTimestamp(nextSlotTs);
122
+ }
123
+
124
+ private getEpochAndSlotAtTimestamp(ts: bigint): EpochAndSlot {
125
+ return {
126
+ epoch: getEpochNumberAtTimestamp(ts, this.l1constants),
127
+ slot: getSlotAtTimestamp(ts, this.l1constants),
128
+ ts,
129
+ };
130
+ }
131
+
132
+ /**
133
+ * Get the current validator set
134
+ *
135
+ * @param nextSlot - If true, get the validator set for the next slot.
136
+ * @returns The current validator set.
137
+ */
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);
156
+ }
157
+
158
+ return this.committee;
159
+ }
160
+
161
+ /**
162
+ * Get the ABI encoding of the proposer index - see ValidatorSelectionLib.sol computeProposerIndex
163
+ */
164
+ getProposerIndexEncoding(epoch: bigint, slot: bigint, seed: bigint): `0x${string}` {
165
+ return encodeAbiParameters(
166
+ [
167
+ { type: 'uint256', name: 'epoch' },
168
+ { type: 'uint256', name: 'slot' },
169
+ { type: 'uint256', name: 'seed' },
170
+ ],
171
+ [epoch, slot, seed],
172
+ );
173
+ }
174
+
175
+ computeProposerIndex(slot: bigint, epoch: bigint, seed: bigint, size: bigint): bigint {
176
+ return BigInt(keccak256(this.getProposerIndexEncoding(epoch, slot, seed))) % size;
177
+ }
178
+
179
+ /**
180
+ * Returns the current and next proposer
181
+ *
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.
187
+ */
188
+ async getProposerInCurrentOrNextSlot(): Promise<{
189
+ currentProposer: EthAddress;
190
+ nextProposer: EthAddress;
191
+ currentSlot: bigint;
192
+ nextSlot: bigint;
193
+ }> {
194
+ // Validators are sorted by their index in the committee, and getValidatorSet will cache
195
+ const committee = await this.getCommittee();
196
+ const { slot: currentSlot, epoch: currentEpoch } = this.getEpochAndSlotNow();
197
+ const { slot: nextSlot, epoch: nextEpoch } = this.getEpochAndSlotInNextSlot();
198
+
199
+ // Compute the proposer in this and the next slot
200
+ const proposerIndex = this.computeProposerIndex(
201
+ currentSlot,
202
+ this.cachedEpoch,
203
+ this.cachedSampleSeed,
204
+ BigInt(committee.length),
205
+ );
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
+
218
+ const currentProposer = committee[Number(proposerIndex)];
219
+ const nextProposer = committee[Number(nextProposerIndex)];
220
+
221
+ return { currentProposer, nextProposer, currentSlot, nextSlot };
222
+ }
223
+
224
+ /**
225
+ * Check if a validator is in the current epoch's committee
226
+ */
227
+ async isInCommittee(validator: EthAddress): Promise<boolean> {
228
+ const committee = await this.getCommittee();
229
+ return committee.some(v => v.equals(validator));
230
+ }
231
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './epoch_cache.js';
2
+ export * from './config.js';
File without changes