@aztec/epoch-cache 0.75.0-commit.c03ba01a2a4122e43e90d5133ba017e54b90e9d2

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.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,164 @@
1
+ import { EmptyL1RollupConstants, getEpochNumberAtTimestamp, getSlotAtTimestamp } from '@aztec/circuit-types';
2
+ import { RollupContract, createEthereumChain } from '@aztec/ethereum';
3
+ import { EthAddress } from '@aztec/foundation/eth-address';
4
+ import { createLogger } from '@aztec/foundation/log';
5
+ import { DateProvider } from '@aztec/foundation/timer';
6
+ import { EventEmitter } from 'node:events';
7
+ import { createPublicClient, encodeAbiParameters, 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.l1RpcUrl, config.l1ChainId);
39
+ const publicClient = createPublicClient({
40
+ chain: chain.chainInfo,
41
+ transport: http(chain.rpcUrl),
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
+ }
package/dest/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from './epoch_cache.js';
2
+ export * from './config.js';
File without changes
package/package.json ADDED
@@ -0,0 +1,93 @@
1
+ {
2
+ "name": "@aztec/epoch-cache",
3
+ "version": "0.75.0-commit.c03ba01a2a4122e43e90d5133ba017e54b90e9d2",
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/circuit-types": "0.75.0-commit.c03ba01a2a4122e43e90d5133ba017e54b90e9d2",
32
+ "@aztec/ethereum": "0.75.0-commit.c03ba01a2a4122e43e90d5133ba017e54b90e9d2",
33
+ "@aztec/foundation": "0.75.0-commit.c03ba01a2a4122e43e90d5133ba017e54b90e9d2",
34
+ "@aztec/l1-artifacts": "0.75.0-commit.c03ba01a2a4122e43e90d5133ba017e54b90e9d2",
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": 30000,
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
+ | 'l1RpcUrl'
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,214 @@
1
+ import {
2
+ EmptyL1RollupConstants,
3
+ type L1RollupConstants,
4
+ getEpochNumberAtTimestamp,
5
+ getSlotAtTimestamp,
6
+ } from '@aztec/circuit-types';
7
+ import { RollupContract, createEthereumChain } from '@aztec/ethereum';
8
+ import { EthAddress } from '@aztec/foundation/eth-address';
9
+ import { type Logger, createLogger } from '@aztec/foundation/log';
10
+ import { DateProvider } from '@aztec/foundation/timer';
11
+
12
+ import { EventEmitter } from 'node:events';
13
+ import { createPublicClient, encodeAbiParameters, 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
+ /**
24
+ * Epoch cache
25
+ *
26
+ * This class is responsible for managing traffic to the l1 node, by caching the validator set.
27
+ * It also provides a method to get the current or next proposer, and to check who is in the current slot.
28
+ *
29
+ * If the epoch changes, then we update the stored validator set.
30
+ *
31
+ * Note: This class is very dependent on the system clock being in sync.
32
+ */
33
+ export class EpochCache extends EventEmitter<{ committeeChanged: [EthAddress[], bigint] }> {
34
+ private committee: EthAddress[];
35
+ private cachedEpoch: bigint;
36
+ private cachedSampleSeed: bigint;
37
+ private readonly log: Logger = createLogger('epoch-cache');
38
+
39
+ constructor(
40
+ private rollup: RollupContract,
41
+ initialValidators: EthAddress[] = [],
42
+ initialSampleSeed: bigint = 0n,
43
+ private readonly l1constants: L1RollupConstants = EmptyL1RollupConstants,
44
+ private readonly dateProvider: DateProvider = new DateProvider(),
45
+ ) {
46
+ super();
47
+ this.committee = initialValidators;
48
+ this.cachedSampleSeed = initialSampleSeed;
49
+
50
+ this.log.debug(`Initialized EpochCache with constants and validators`, { l1constants, initialValidators });
51
+
52
+ this.cachedEpoch = getEpochNumberAtTimestamp(this.nowInSeconds(), this.l1constants);
53
+ }
54
+
55
+ static async create(
56
+ rollupAddress: EthAddress,
57
+ config?: EpochCacheConfig,
58
+ deps: { dateProvider?: DateProvider } = {},
59
+ ) {
60
+ config = config ?? getEpochCacheConfigEnvVars();
61
+
62
+ const chain = createEthereumChain(config.l1RpcUrl, config.l1ChainId);
63
+ const publicClient = createPublicClient({
64
+ chain: chain.chainInfo,
65
+ transport: http(chain.rpcUrl),
66
+ pollingInterval: config.viemPollingIntervalMS,
67
+ });
68
+
69
+ const rollup = new RollupContract(publicClient, rollupAddress.toString());
70
+ const [l1StartBlock, l1GenesisTime, initialValidators, sampleSeed] = await Promise.all([
71
+ rollup.getL1StartBlock(),
72
+ rollup.getL1GenesisTime(),
73
+ rollup.getCurrentEpochCommittee(),
74
+ rollup.getCurrentSampleSeed(),
75
+ ] as const);
76
+
77
+ const l1RollupConstants: L1RollupConstants = {
78
+ l1StartBlock,
79
+ l1GenesisTime,
80
+ slotDuration: config.aztecSlotDuration,
81
+ epochDuration: config.aztecEpochDuration,
82
+ ethereumSlotDuration: config.ethereumSlotDuration,
83
+ };
84
+
85
+ return new EpochCache(
86
+ rollup,
87
+ initialValidators.map(v => EthAddress.fromString(v)),
88
+ sampleSeed,
89
+ l1RollupConstants,
90
+ deps.dateProvider,
91
+ );
92
+ }
93
+
94
+ private nowInSeconds(): bigint {
95
+ return BigInt(Math.floor(this.dateProvider.now() / 1000));
96
+ }
97
+
98
+ getEpochAndSlotNow(): EpochAndSlot {
99
+ return this.getEpochAndSlotAtTimestamp(this.nowInSeconds());
100
+ }
101
+
102
+ getEpochAndSlotInNextSlot(): EpochAndSlot {
103
+ const nextSlotTs = this.nowInSeconds() + BigInt(this.l1constants.slotDuration);
104
+ return this.getEpochAndSlotAtTimestamp(nextSlotTs);
105
+ }
106
+
107
+ getEpochAndSlotAtTimestamp(ts: bigint): EpochAndSlot {
108
+ return {
109
+ epoch: getEpochNumberAtTimestamp(ts, this.l1constants),
110
+ slot: getSlotAtTimestamp(ts, this.l1constants),
111
+ ts,
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Get the current validator set
117
+ *
118
+ * @param nextSlot - If true, get the validator set for the next slot.
119
+ * @returns The current validator set.
120
+ */
121
+ async getCommittee(nextSlot: boolean = false): Promise<EthAddress[]> {
122
+ // If the current epoch has changed, then we need to make a request to update the validator set
123
+ const { epoch: calculatedEpoch, ts } = nextSlot ? this.getEpochAndSlotInNextSlot() : this.getEpochAndSlotNow();
124
+
125
+ if (calculatedEpoch !== this.cachedEpoch) {
126
+ this.log.debug(`Updating validator set for new epoch ${calculatedEpoch}`, {
127
+ epoch: calculatedEpoch,
128
+ previousEpoch: this.cachedEpoch,
129
+ });
130
+ const [committeeAtTs, sampleSeedAtTs] = await Promise.all([
131
+ this.rollup.getCommitteeAt(ts),
132
+ this.rollup.getSampleSeedAt(ts),
133
+ ]);
134
+ this.committee = committeeAtTs.map((v: `0x${string}`) => EthAddress.fromString(v));
135
+ this.cachedEpoch = calculatedEpoch;
136
+ this.cachedSampleSeed = sampleSeedAtTs;
137
+ this.log.debug(`Updated validator set for epoch ${calculatedEpoch}`, { commitee: this.committee });
138
+ this.emit('committeeChanged', this.committee, calculatedEpoch);
139
+ }
140
+
141
+ return this.committee;
142
+ }
143
+
144
+ /**
145
+ * Get the ABI encoding of the proposer index - see ValidatorSelectionLib.sol computeProposerIndex
146
+ */
147
+ getProposerIndexEncoding(epoch: bigint, slot: bigint, seed: bigint): `0x${string}` {
148
+ return encodeAbiParameters(
149
+ [
150
+ { type: 'uint256', name: 'epoch' },
151
+ { type: 'uint256', name: 'slot' },
152
+ { type: 'uint256', name: 'seed' },
153
+ ],
154
+ [epoch, slot, seed],
155
+ );
156
+ }
157
+
158
+ computeProposerIndex(slot: bigint, epoch: bigint, seed: bigint, size: bigint): bigint {
159
+ return BigInt(keccak256(this.getProposerIndexEncoding(epoch, slot, seed))) % size;
160
+ }
161
+
162
+ /**
163
+ * Returns the current and next proposer
164
+ *
165
+ * We return the next proposer as the node will check if it is the proposer at the next ethereum block, which
166
+ * can be the next slot. If this is the case, then it will send proposals early.
167
+ *
168
+ * If we are at an epoch boundary, then we can update the cache for the next epoch, this is the last check
169
+ * we do in the validator client, so we can update the cache here.
170
+ */
171
+ async getProposerInCurrentOrNextSlot(): Promise<{
172
+ currentProposer: EthAddress;
173
+ nextProposer: EthAddress;
174
+ currentSlot: bigint;
175
+ nextSlot: bigint;
176
+ }> {
177
+ // Validators are sorted by their index in the committee, and getValidatorSet will cache
178
+ const committee = await this.getCommittee();
179
+ const { slot: currentSlot, epoch: currentEpoch } = this.getEpochAndSlotNow();
180
+ const { slot: nextSlot, epoch: nextEpoch } = this.getEpochAndSlotInNextSlot();
181
+
182
+ // Compute the proposer in this and the next slot
183
+ const proposerIndex = this.computeProposerIndex(
184
+ currentSlot,
185
+ this.cachedEpoch,
186
+ this.cachedSampleSeed,
187
+ BigInt(committee.length),
188
+ );
189
+
190
+ // Check if the next proposer is in the next epoch
191
+ if (nextEpoch !== currentEpoch) {
192
+ await this.getCommittee(/*next slot*/ true);
193
+ }
194
+ const nextProposerIndex = this.computeProposerIndex(
195
+ nextSlot,
196
+ this.cachedEpoch,
197
+ this.cachedSampleSeed,
198
+ BigInt(committee.length),
199
+ );
200
+
201
+ const currentProposer = committee[Number(proposerIndex)];
202
+ const nextProposer = committee[Number(nextProposerIndex)];
203
+
204
+ return { currentProposer, nextProposer, currentSlot, nextSlot };
205
+ }
206
+
207
+ /**
208
+ * Check if a validator is in the current epoch's committee
209
+ */
210
+ async isInCommittee(validator: EthAddress): Promise<boolean> {
211
+ const committee = await this.getCommittee();
212
+ return committee.some(v => v.equals(validator));
213
+ }
214
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './epoch_cache.js';
2
+ export * from './config.js';
File without changes