@aztec/epoch-cache 0.82.2-alpha-testnet.4 → 0.82.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dest/epoch_cache.d.ts +20 -18
- package/dest/epoch_cache.d.ts.map +1 -1
- package/dest/epoch_cache.js +88 -62
- package/package.json +5 -5
- package/src/epoch_cache.ts +82 -71
- package/dest/timestamp_provider.d.ts +0 -2
- package/dest/timestamp_provider.d.ts.map +0 -1
- package/dest/timestamp_provider.js +0 -0
- package/src/timestamp_provider.ts +0 -0
package/dest/epoch_cache.d.ts
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
/// <reference types="node" resolution-mode="require"/>
|
|
2
1
|
import { RollupContract } from '@aztec/ethereum';
|
|
3
2
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
4
3
|
import { DateProvider } from '@aztec/foundation/timer';
|
|
5
4
|
import { type L1RollupConstants } from '@aztec/stdlib/epoch-helpers';
|
|
6
|
-
import { EventEmitter } from 'node:events';
|
|
7
5
|
import { type EpochCacheConfig } from './config.js';
|
|
8
6
|
type EpochAndSlot = {
|
|
9
7
|
epoch: bigint;
|
|
10
8
|
slot: bigint;
|
|
11
9
|
ts: bigint;
|
|
12
10
|
};
|
|
11
|
+
export type EpochCommitteeInfo = {
|
|
12
|
+
committee: EthAddress[];
|
|
13
|
+
seed: bigint;
|
|
14
|
+
epoch: bigint;
|
|
15
|
+
};
|
|
13
16
|
export interface EpochCacheInterface {
|
|
14
|
-
getCommittee(
|
|
17
|
+
getCommittee(slot: 'now' | 'next' | bigint | undefined): Promise<EpochCommitteeInfo>;
|
|
15
18
|
getEpochAndSlotNow(): EpochAndSlot;
|
|
16
19
|
getProposerIndexEncoding(epoch: bigint, slot: bigint, seed: bigint): `0x${string}`;
|
|
17
20
|
computeProposerIndex(slot: bigint, epoch: bigint, seed: bigint, size: bigint): bigint;
|
|
@@ -27,37 +30,38 @@ export interface EpochCacheInterface {
|
|
|
27
30
|
* Epoch cache
|
|
28
31
|
*
|
|
29
32
|
* This class is responsible for managing traffic to the l1 node, by caching the validator set.
|
|
33
|
+
* Keeps the last N epochs in cache.
|
|
30
34
|
* It also provides a method to get the current or next proposer, and to check who is in the current slot.
|
|
31
35
|
*
|
|
32
|
-
* If the epoch changes, then we update the stored validator set.
|
|
33
|
-
*
|
|
34
36
|
* Note: This class is very dependent on the system clock being in sync.
|
|
35
37
|
*/
|
|
36
|
-
export declare class EpochCache
|
|
37
|
-
committeeChanged: [EthAddress[], bigint];
|
|
38
|
-
}> implements EpochCacheInterface {
|
|
38
|
+
export declare class EpochCache implements EpochCacheInterface {
|
|
39
39
|
private rollup;
|
|
40
40
|
private readonly l1constants;
|
|
41
41
|
private readonly dateProvider;
|
|
42
|
-
private
|
|
43
|
-
private
|
|
44
|
-
private cachedSampleSeed;
|
|
42
|
+
private readonly config;
|
|
43
|
+
private cache;
|
|
45
44
|
private readonly log;
|
|
46
|
-
constructor(rollup: RollupContract, initialValidators?: EthAddress[], initialSampleSeed?: bigint, l1constants?: L1RollupConstants, dateProvider?: DateProvider
|
|
45
|
+
constructor(rollup: RollupContract, initialEpoch?: bigint, initialValidators?: EthAddress[], initialSampleSeed?: bigint, l1constants?: L1RollupConstants, dateProvider?: DateProvider, config?: {
|
|
46
|
+
cacheSize: number;
|
|
47
|
+
});
|
|
47
48
|
static create(rollupAddress: EthAddress, config?: EpochCacheConfig, deps?: {
|
|
48
49
|
dateProvider?: DateProvider;
|
|
49
50
|
}): Promise<EpochCache>;
|
|
50
|
-
|
|
51
|
+
getL1Constants(): L1RollupConstants;
|
|
51
52
|
getEpochAndSlotNow(): EpochAndSlot;
|
|
53
|
+
private nowInSeconds;
|
|
54
|
+
private getEpochAndSlotAtSlot;
|
|
52
55
|
private getEpochAndSlotInNextSlot;
|
|
53
56
|
private getEpochAndSlotAtTimestamp;
|
|
54
57
|
/**
|
|
55
58
|
* Get the current validator set
|
|
56
|
-
*
|
|
57
59
|
* @param nextSlot - If true, get the validator set for the next slot.
|
|
58
60
|
* @returns The current validator set.
|
|
59
61
|
*/
|
|
60
|
-
getCommittee(
|
|
62
|
+
getCommittee(slot?: 'now' | 'next' | bigint): Promise<EpochCommitteeInfo>;
|
|
63
|
+
private getEpochAndTimestamp;
|
|
64
|
+
private computeCommittee;
|
|
61
65
|
/**
|
|
62
66
|
* Get the ABI encoding of the proposer index - see ValidatorSelectionLib.sol computeProposerIndex
|
|
63
67
|
*/
|
|
@@ -68,9 +72,6 @@ export declare class EpochCache extends EventEmitter<{
|
|
|
68
72
|
*
|
|
69
73
|
* We return the next proposer as the node will check if it is the proposer at the next ethereum block, which
|
|
70
74
|
* can be the next slot. If this is the case, then it will send proposals early.
|
|
71
|
-
*
|
|
72
|
-
* If we are at an epoch boundary, then we can update the cache for the next epoch, this is the last check
|
|
73
|
-
* we do in the validator client, so we can update the cache here.
|
|
74
75
|
*/
|
|
75
76
|
getProposerInCurrentOrNextSlot(): Promise<{
|
|
76
77
|
currentProposer: EthAddress;
|
|
@@ -78,6 +79,7 @@ export declare class EpochCache extends EventEmitter<{
|
|
|
78
79
|
currentSlot: bigint;
|
|
79
80
|
nextSlot: bigint;
|
|
80
81
|
}>;
|
|
82
|
+
private getProposerAt;
|
|
81
83
|
/**
|
|
82
84
|
* Check if a validator is in the current epoch's committee
|
|
83
85
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"epoch_cache.d.ts","sourceRoot":"","sources":["../src/epoch_cache.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"epoch_cache.d.ts","sourceRoot":"","sources":["../src/epoch_cache.ts"],"names":[],"mappings":"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"}
|
package/dest/epoch_cache.js
CHANGED
|
@@ -2,36 +2,44 @@ import { RollupContract, createEthereumChain } from '@aztec/ethereum';
|
|
|
2
2
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
3
3
|
import { createLogger } from '@aztec/foundation/log';
|
|
4
4
|
import { DateProvider } from '@aztec/foundation/timer';
|
|
5
|
-
import { EmptyL1RollupConstants, getEpochNumberAtTimestamp, getSlotAtTimestamp } from '@aztec/stdlib/epoch-helpers';
|
|
6
|
-
import { EventEmitter } from 'node:events';
|
|
5
|
+
import { EmptyL1RollupConstants, getEpochAtSlot, getEpochNumberAtTimestamp, getSlotAtTimestamp, getTimestampRangeForEpoch } from '@aztec/stdlib/epoch-helpers';
|
|
7
6
|
import { createPublicClient, encodeAbiParameters, fallback, http, keccak256 } from 'viem';
|
|
8
7
|
import { getEpochCacheConfigEnvVars } from './config.js';
|
|
9
8
|
/**
|
|
10
9
|
* Epoch cache
|
|
11
10
|
*
|
|
12
11
|
* This class is responsible for managing traffic to the l1 node, by caching the validator set.
|
|
12
|
+
* Keeps the last N epochs in cache.
|
|
13
13
|
* It also provides a method to get the current or next proposer, and to check who is in the current slot.
|
|
14
14
|
*
|
|
15
|
-
* If the epoch changes, then we update the stored validator set.
|
|
16
|
-
*
|
|
17
15
|
* Note: This class is very dependent on the system clock being in sync.
|
|
18
|
-
*/ export class EpochCache
|
|
16
|
+
*/ export class EpochCache {
|
|
19
17
|
rollup;
|
|
20
18
|
l1constants;
|
|
21
19
|
dateProvider;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
cachedSampleSeed;
|
|
20
|
+
config;
|
|
21
|
+
cache;
|
|
25
22
|
log;
|
|
26
|
-
constructor(rollup, initialValidators = [], initialSampleSeed = 0n, l1constants = EmptyL1RollupConstants, dateProvider = new DateProvider()
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
this.
|
|
30
|
-
this.
|
|
23
|
+
constructor(rollup, initialEpoch = 0n, initialValidators = [], initialSampleSeed = 0n, l1constants = EmptyL1RollupConstants, dateProvider = new DateProvider(), config = {
|
|
24
|
+
cacheSize: 12
|
|
25
|
+
}){
|
|
26
|
+
this.rollup = rollup;
|
|
27
|
+
this.l1constants = l1constants;
|
|
28
|
+
this.dateProvider = dateProvider;
|
|
29
|
+
this.config = config;
|
|
30
|
+
this.cache = new Map();
|
|
31
|
+
this.log = createLogger('epoch-cache');
|
|
32
|
+
this.cache.set(initialEpoch, {
|
|
33
|
+
epoch: initialEpoch,
|
|
34
|
+
committee: initialValidators,
|
|
35
|
+
seed: initialSampleSeed
|
|
36
|
+
});
|
|
37
|
+
this.log.debug(`Initialized EpochCache with ${initialValidators.length} validators`, {
|
|
31
38
|
l1constants,
|
|
32
|
-
initialValidators
|
|
39
|
+
initialValidators,
|
|
40
|
+
initialSampleSeed,
|
|
41
|
+
initialEpoch
|
|
33
42
|
});
|
|
34
|
-
this.cachedEpoch = getEpochNumberAtTimestamp(this.nowInSeconds(), this.l1constants);
|
|
35
43
|
}
|
|
36
44
|
static async create(rollupAddress, config, deps = {}) {
|
|
37
45
|
config = config ?? getEpochCacheConfigEnvVars();
|
|
@@ -42,11 +50,12 @@ import { getEpochCacheConfigEnvVars } from './config.js';
|
|
|
42
50
|
pollingInterval: config.viemPollingIntervalMS
|
|
43
51
|
});
|
|
44
52
|
const rollup = new RollupContract(publicClient, rollupAddress.toString());
|
|
45
|
-
const [l1StartBlock, l1GenesisTime, initialValidators, sampleSeed] = await Promise.all([
|
|
53
|
+
const [l1StartBlock, l1GenesisTime, initialValidators, sampleSeed, epochNumber] = await Promise.all([
|
|
46
54
|
rollup.getL1StartBlock(),
|
|
47
55
|
rollup.getL1GenesisTime(),
|
|
48
56
|
rollup.getCurrentEpochCommittee(),
|
|
49
|
-
rollup.getCurrentSampleSeed()
|
|
57
|
+
rollup.getCurrentSampleSeed(),
|
|
58
|
+
rollup.getEpochNumber()
|
|
50
59
|
]);
|
|
51
60
|
const l1RollupConstants = {
|
|
52
61
|
l1StartBlock,
|
|
@@ -55,14 +64,26 @@ import { getEpochCacheConfigEnvVars } from './config.js';
|
|
|
55
64
|
epochDuration: config.aztecEpochDuration,
|
|
56
65
|
ethereumSlotDuration: config.ethereumSlotDuration
|
|
57
66
|
};
|
|
58
|
-
return new EpochCache(rollup, initialValidators.map((v)=>EthAddress.fromString(v)), sampleSeed, l1RollupConstants, deps.dateProvider);
|
|
67
|
+
return new EpochCache(rollup, epochNumber, initialValidators.map((v)=>EthAddress.fromString(v)), sampleSeed, l1RollupConstants, deps.dateProvider);
|
|
59
68
|
}
|
|
60
|
-
|
|
61
|
-
return
|
|
69
|
+
getL1Constants() {
|
|
70
|
+
return this.l1constants;
|
|
62
71
|
}
|
|
63
72
|
getEpochAndSlotNow() {
|
|
64
73
|
return this.getEpochAndSlotAtTimestamp(this.nowInSeconds());
|
|
65
74
|
}
|
|
75
|
+
nowInSeconds() {
|
|
76
|
+
return BigInt(Math.floor(this.dateProvider.now() / 1000));
|
|
77
|
+
}
|
|
78
|
+
getEpochAndSlotAtSlot(slot) {
|
|
79
|
+
const epoch = getEpochAtSlot(slot, this.l1constants);
|
|
80
|
+
const ts = getTimestampRangeForEpoch(slot, this.l1constants)[0];
|
|
81
|
+
return {
|
|
82
|
+
epoch,
|
|
83
|
+
ts,
|
|
84
|
+
slot
|
|
85
|
+
};
|
|
86
|
+
}
|
|
66
87
|
getEpochAndSlotInNextSlot() {
|
|
67
88
|
const nextSlotTs = this.nowInSeconds() + BigInt(this.l1constants.slotDuration);
|
|
68
89
|
return this.getEpochAndSlotAtTimestamp(nextSlotTs);
|
|
@@ -76,30 +97,43 @@ import { getEpochCacheConfigEnvVars } from './config.js';
|
|
|
76
97
|
}
|
|
77
98
|
/**
|
|
78
99
|
* Get the current validator set
|
|
79
|
-
*
|
|
80
100
|
* @param nextSlot - If true, get the validator set for the next slot.
|
|
81
101
|
* @returns The current validator set.
|
|
82
|
-
*/ async getCommittee(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
this.log.debug(`Updating validator set for new epoch ${calculatedEpoch}`, {
|
|
87
|
-
epoch: calculatedEpoch,
|
|
88
|
-
previousEpoch: this.cachedEpoch
|
|
89
|
-
});
|
|
90
|
-
const [committeeAtTs, sampleSeedAtTs] = await Promise.all([
|
|
91
|
-
this.rollup.getCommitteeAt(ts),
|
|
92
|
-
this.rollup.getSampleSeedAt(ts)
|
|
93
|
-
]);
|
|
94
|
-
this.committee = committeeAtTs.map((v)=>EthAddress.fromString(v));
|
|
95
|
-
this.cachedEpoch = calculatedEpoch;
|
|
96
|
-
this.cachedSampleSeed = sampleSeedAtTs;
|
|
97
|
-
this.log.debug(`Updated validator set for epoch ${calculatedEpoch}`, {
|
|
98
|
-
commitee: this.committee
|
|
99
|
-
});
|
|
100
|
-
this.emit('committeeChanged', this.committee, calculatedEpoch);
|
|
102
|
+
*/ async getCommittee(slot = 'now') {
|
|
103
|
+
const { epoch, ts } = this.getEpochAndTimestamp(slot);
|
|
104
|
+
if (this.cache.has(epoch)) {
|
|
105
|
+
return this.cache.get(epoch);
|
|
101
106
|
}
|
|
102
|
-
|
|
107
|
+
const epochData = await this.computeCommittee({
|
|
108
|
+
epoch,
|
|
109
|
+
ts
|
|
110
|
+
});
|
|
111
|
+
this.cache.set(epoch, epochData);
|
|
112
|
+
const toPurge = Array.from(this.cache.keys()).sort((a, b)=>Number(b - a)).slice(this.config.cacheSize);
|
|
113
|
+
toPurge.forEach((key)=>this.cache.delete(key));
|
|
114
|
+
return epochData;
|
|
115
|
+
}
|
|
116
|
+
getEpochAndTimestamp(slot = 'now') {
|
|
117
|
+
if (slot === 'now') {
|
|
118
|
+
return this.getEpochAndSlotNow();
|
|
119
|
+
} else if (slot === 'next') {
|
|
120
|
+
return this.getEpochAndSlotInNextSlot();
|
|
121
|
+
} else {
|
|
122
|
+
return this.getEpochAndSlotAtSlot(slot);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async computeCommittee(when) {
|
|
126
|
+
const { ts, epoch } = when;
|
|
127
|
+
const [committeeHex, seed] = await Promise.all([
|
|
128
|
+
this.rollup.getCommitteeAt(ts),
|
|
129
|
+
this.rollup.getSampleSeedAt(ts)
|
|
130
|
+
]);
|
|
131
|
+
const committee = committeeHex.map((v)=>EthAddress.fromString(v));
|
|
132
|
+
return {
|
|
133
|
+
committee,
|
|
134
|
+
seed,
|
|
135
|
+
epoch
|
|
136
|
+
};
|
|
103
137
|
}
|
|
104
138
|
/**
|
|
105
139
|
* Get the ABI encoding of the proposer index - see ValidatorSelectionLib.sol computeProposerIndex
|
|
@@ -131,34 +165,26 @@ import { getEpochCacheConfigEnvVars } from './config.js';
|
|
|
131
165
|
*
|
|
132
166
|
* We return the next proposer as the node will check if it is the proposer at the next ethereum block, which
|
|
133
167
|
* can be the next slot. If this is the case, then it will send proposals early.
|
|
134
|
-
*
|
|
135
|
-
* If we are at an epoch boundary, then we can update the cache for the next epoch, this is the last check
|
|
136
|
-
* we do in the validator client, so we can update the cache here.
|
|
137
168
|
*/ async getProposerInCurrentOrNextSlot() {
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
const { slot: currentSlot, epoch: currentEpoch } = this.getEpochAndSlotNow();
|
|
141
|
-
const { slot: nextSlot, epoch: nextEpoch } = this.getEpochAndSlotInNextSlot();
|
|
142
|
-
// Compute the proposer in this and the next slot
|
|
143
|
-
const proposerIndex = this.computeProposerIndex(currentSlot, this.cachedEpoch, this.cachedSampleSeed, BigInt(committee.length));
|
|
144
|
-
// Check if the next proposer is in the next epoch
|
|
145
|
-
if (nextEpoch !== currentEpoch) {
|
|
146
|
-
await this.getCommittee(/*next slot*/ true);
|
|
147
|
-
}
|
|
148
|
-
const nextProposerIndex = this.computeProposerIndex(nextSlot, this.cachedEpoch, this.cachedSampleSeed, BigInt(committee.length));
|
|
149
|
-
const currentProposer = committee[Number(proposerIndex)];
|
|
150
|
-
const nextProposer = committee[Number(nextProposerIndex)];
|
|
169
|
+
const current = this.getEpochAndSlotNow();
|
|
170
|
+
const next = this.getEpochAndSlotInNextSlot();
|
|
151
171
|
return {
|
|
152
|
-
currentProposer,
|
|
153
|
-
nextProposer,
|
|
154
|
-
currentSlot,
|
|
155
|
-
nextSlot
|
|
172
|
+
currentProposer: await this.getProposerAt(current),
|
|
173
|
+
nextProposer: await this.getProposerAt(next),
|
|
174
|
+
currentSlot: current.slot,
|
|
175
|
+
nextSlot: next.slot
|
|
156
176
|
};
|
|
157
177
|
}
|
|
178
|
+
async getProposerAt(when) {
|
|
179
|
+
const { epoch, slot } = when;
|
|
180
|
+
const { seed, committee } = await this.getCommittee(slot);
|
|
181
|
+
const proposerIndex = this.computeProposerIndex(slot, epoch, seed, BigInt(committee.length));
|
|
182
|
+
return committee[Number(proposerIndex)];
|
|
183
|
+
}
|
|
158
184
|
/**
|
|
159
185
|
* Check if a validator is in the current epoch's committee
|
|
160
186
|
*/ async isInCommittee(validator) {
|
|
161
|
-
const committee = await this.getCommittee();
|
|
187
|
+
const { committee } = await this.getCommittee();
|
|
162
188
|
return committee.some((v)=>v.equals(validator));
|
|
163
189
|
}
|
|
164
190
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aztec/epoch-cache",
|
|
3
|
-
"version": "0.82.
|
|
3
|
+
"version": "0.82.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./dest/index.js",
|
|
@@ -28,10 +28,10 @@
|
|
|
28
28
|
"../package.common.json"
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@aztec/ethereum": "0.82.
|
|
32
|
-
"@aztec/foundation": "0.82.
|
|
33
|
-
"@aztec/l1-artifacts": "0.82.
|
|
34
|
-
"@aztec/stdlib": "0.82.
|
|
31
|
+
"@aztec/ethereum": "0.82.3",
|
|
32
|
+
"@aztec/foundation": "0.82.3",
|
|
33
|
+
"@aztec/l1-artifacts": "0.82.3",
|
|
34
|
+
"@aztec/stdlib": "0.82.3",
|
|
35
35
|
"@viem/anvil": "^0.0.10",
|
|
36
36
|
"dotenv": "^16.0.3",
|
|
37
37
|
"get-port": "^7.1.0",
|
package/src/epoch_cache.ts
CHANGED
|
@@ -5,11 +5,12 @@ import { DateProvider } from '@aztec/foundation/timer';
|
|
|
5
5
|
import {
|
|
6
6
|
EmptyL1RollupConstants,
|
|
7
7
|
type L1RollupConstants,
|
|
8
|
+
getEpochAtSlot,
|
|
8
9
|
getEpochNumberAtTimestamp,
|
|
9
10
|
getSlotAtTimestamp,
|
|
11
|
+
getTimestampRangeForEpoch,
|
|
10
12
|
} from '@aztec/stdlib/epoch-helpers';
|
|
11
13
|
|
|
12
|
-
import { EventEmitter } from 'node:events';
|
|
13
14
|
import { createPublicClient, encodeAbiParameters, fallback, http, keccak256 } from 'viem';
|
|
14
15
|
|
|
15
16
|
import { type EpochCacheConfig, getEpochCacheConfigEnvVars } from './config.js';
|
|
@@ -20,8 +21,14 @@ type EpochAndSlot = {
|
|
|
20
21
|
ts: bigint;
|
|
21
22
|
};
|
|
22
23
|
|
|
24
|
+
export type EpochCommitteeInfo = {
|
|
25
|
+
committee: EthAddress[];
|
|
26
|
+
seed: bigint;
|
|
27
|
+
epoch: bigint;
|
|
28
|
+
};
|
|
29
|
+
|
|
23
30
|
export interface EpochCacheInterface {
|
|
24
|
-
getCommittee(
|
|
31
|
+
getCommittee(slot: 'now' | 'next' | bigint | undefined): Promise<EpochCommitteeInfo>;
|
|
25
32
|
getEpochAndSlotNow(): EpochAndSlot;
|
|
26
33
|
getProposerIndexEncoding(epoch: bigint, slot: bigint, seed: bigint): `0x${string}`;
|
|
27
34
|
computeProposerIndex(slot: bigint, epoch: bigint, seed: bigint, size: bigint): bigint;
|
|
@@ -38,35 +45,31 @@ export interface EpochCacheInterface {
|
|
|
38
45
|
* Epoch cache
|
|
39
46
|
*
|
|
40
47
|
* This class is responsible for managing traffic to the l1 node, by caching the validator set.
|
|
48
|
+
* Keeps the last N epochs in cache.
|
|
41
49
|
* It also provides a method to get the current or next proposer, and to check who is in the current slot.
|
|
42
50
|
*
|
|
43
|
-
* If the epoch changes, then we update the stored validator set.
|
|
44
|
-
*
|
|
45
51
|
* Note: This class is very dependent on the system clock being in sync.
|
|
46
52
|
*/
|
|
47
|
-
export class EpochCache
|
|
48
|
-
|
|
49
|
-
implements EpochCacheInterface
|
|
50
|
-
{
|
|
51
|
-
private committee: EthAddress[];
|
|
52
|
-
private cachedEpoch: bigint;
|
|
53
|
-
private cachedSampleSeed: bigint;
|
|
53
|
+
export class EpochCache implements EpochCacheInterface {
|
|
54
|
+
private cache: Map<bigint, EpochCommitteeInfo> = new Map();
|
|
54
55
|
private readonly log: Logger = createLogger('epoch-cache');
|
|
55
56
|
|
|
56
57
|
constructor(
|
|
57
58
|
private rollup: RollupContract,
|
|
59
|
+
initialEpoch: bigint = 0n,
|
|
58
60
|
initialValidators: EthAddress[] = [],
|
|
59
61
|
initialSampleSeed: bigint = 0n,
|
|
60
62
|
private readonly l1constants: L1RollupConstants = EmptyL1RollupConstants,
|
|
61
63
|
private readonly dateProvider: DateProvider = new DateProvider(),
|
|
64
|
+
private readonly config = { cacheSize: 12 },
|
|
62
65
|
) {
|
|
63
|
-
|
|
64
|
-
this.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
this.cache.set(initialEpoch, { epoch: initialEpoch, committee: initialValidators, seed: initialSampleSeed });
|
|
67
|
+
this.log.debug(`Initialized EpochCache with ${initialValidators.length} validators`, {
|
|
68
|
+
l1constants,
|
|
69
|
+
initialValidators,
|
|
70
|
+
initialSampleSeed,
|
|
71
|
+
initialEpoch,
|
|
72
|
+
});
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
static async create(
|
|
@@ -84,11 +87,12 @@ export class EpochCache
|
|
|
84
87
|
});
|
|
85
88
|
|
|
86
89
|
const rollup = new RollupContract(publicClient, rollupAddress.toString());
|
|
87
|
-
const [l1StartBlock, l1GenesisTime, initialValidators, sampleSeed] = await Promise.all([
|
|
90
|
+
const [l1StartBlock, l1GenesisTime, initialValidators, sampleSeed, epochNumber] = await Promise.all([
|
|
88
91
|
rollup.getL1StartBlock(),
|
|
89
92
|
rollup.getL1GenesisTime(),
|
|
90
93
|
rollup.getCurrentEpochCommittee(),
|
|
91
94
|
rollup.getCurrentSampleSeed(),
|
|
95
|
+
rollup.getEpochNumber(),
|
|
92
96
|
] as const);
|
|
93
97
|
|
|
94
98
|
const l1RollupConstants: L1RollupConstants = {
|
|
@@ -101,6 +105,7 @@ export class EpochCache
|
|
|
101
105
|
|
|
102
106
|
return new EpochCache(
|
|
103
107
|
rollup,
|
|
108
|
+
epochNumber,
|
|
104
109
|
initialValidators.map(v => EthAddress.fromString(v)),
|
|
105
110
|
sampleSeed,
|
|
106
111
|
l1RollupConstants,
|
|
@@ -108,12 +113,22 @@ export class EpochCache
|
|
|
108
113
|
);
|
|
109
114
|
}
|
|
110
115
|
|
|
116
|
+
public getL1Constants(): L1RollupConstants {
|
|
117
|
+
return this.l1constants;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
public getEpochAndSlotNow(): EpochAndSlot {
|
|
121
|
+
return this.getEpochAndSlotAtTimestamp(this.nowInSeconds());
|
|
122
|
+
}
|
|
123
|
+
|
|
111
124
|
private nowInSeconds(): bigint {
|
|
112
125
|
return BigInt(Math.floor(this.dateProvider.now() / 1000));
|
|
113
126
|
}
|
|
114
127
|
|
|
115
|
-
|
|
116
|
-
|
|
128
|
+
private getEpochAndSlotAtSlot(slot: bigint): EpochAndSlot {
|
|
129
|
+
const epoch = getEpochAtSlot(slot, this.l1constants);
|
|
130
|
+
const ts = getTimestampRangeForEpoch(slot, this.l1constants)[0];
|
|
131
|
+
return { epoch, ts, slot };
|
|
117
132
|
}
|
|
118
133
|
|
|
119
134
|
private getEpochAndSlotInNextSlot(): EpochAndSlot {
|
|
@@ -131,31 +146,42 @@ export class EpochCache
|
|
|
131
146
|
|
|
132
147
|
/**
|
|
133
148
|
* Get the current validator set
|
|
134
|
-
*
|
|
135
149
|
* @param nextSlot - If true, get the validator set for the next slot.
|
|
136
150
|
* @returns The current validator set.
|
|
137
151
|
*/
|
|
138
|
-
async getCommittee(
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
this.log.debug(`Updating validator set for new epoch ${calculatedEpoch}`, {
|
|
144
|
-
epoch: calculatedEpoch,
|
|
145
|
-
previousEpoch: this.cachedEpoch,
|
|
146
|
-
});
|
|
147
|
-
const [committeeAtTs, sampleSeedAtTs] = await Promise.all([
|
|
148
|
-
this.rollup.getCommitteeAt(ts),
|
|
149
|
-
this.rollup.getSampleSeedAt(ts),
|
|
150
|
-
]);
|
|
151
|
-
this.committee = committeeAtTs.map((v: `0x${string}`) => EthAddress.fromString(v));
|
|
152
|
-
this.cachedEpoch = calculatedEpoch;
|
|
153
|
-
this.cachedSampleSeed = sampleSeedAtTs;
|
|
154
|
-
this.log.debug(`Updated validator set for epoch ${calculatedEpoch}`, { commitee: this.committee });
|
|
155
|
-
this.emit('committeeChanged', this.committee, calculatedEpoch);
|
|
152
|
+
public async getCommittee(slot: 'now' | 'next' | bigint = 'now'): Promise<EpochCommitteeInfo> {
|
|
153
|
+
const { epoch, ts } = this.getEpochAndTimestamp(slot);
|
|
154
|
+
|
|
155
|
+
if (this.cache.has(epoch)) {
|
|
156
|
+
return this.cache.get(epoch)!;
|
|
156
157
|
}
|
|
157
158
|
|
|
158
|
-
|
|
159
|
+
const epochData = await this.computeCommittee({ epoch, ts });
|
|
160
|
+
this.cache.set(epoch, epochData);
|
|
161
|
+
|
|
162
|
+
const toPurge = Array.from(this.cache.keys())
|
|
163
|
+
.sort((a, b) => Number(b - a))
|
|
164
|
+
.slice(this.config.cacheSize);
|
|
165
|
+
toPurge.forEach(key => this.cache.delete(key));
|
|
166
|
+
|
|
167
|
+
return epochData;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private getEpochAndTimestamp(slot: 'now' | 'next' | bigint = 'now') {
|
|
171
|
+
if (slot === 'now') {
|
|
172
|
+
return this.getEpochAndSlotNow();
|
|
173
|
+
} else if (slot === 'next') {
|
|
174
|
+
return this.getEpochAndSlotInNextSlot();
|
|
175
|
+
} else {
|
|
176
|
+
return this.getEpochAndSlotAtSlot(slot);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private async computeCommittee(when: { epoch: bigint; ts: bigint }): Promise<EpochCommitteeInfo> {
|
|
181
|
+
const { ts, epoch } = when;
|
|
182
|
+
const [committeeHex, seed] = await Promise.all([this.rollup.getCommitteeAt(ts), this.rollup.getSampleSeedAt(ts)]);
|
|
183
|
+
const committee = committeeHex.map((v: `0x${string}`) => EthAddress.fromString(v));
|
|
184
|
+
return { committee, seed, epoch };
|
|
159
185
|
}
|
|
160
186
|
|
|
161
187
|
/**
|
|
@@ -181,9 +207,6 @@ export class EpochCache
|
|
|
181
207
|
*
|
|
182
208
|
* We return the next proposer as the node will check if it is the proposer at the next ethereum block, which
|
|
183
209
|
* can be the next slot. If this is the case, then it will send proposals early.
|
|
184
|
-
*
|
|
185
|
-
* If we are at an epoch boundary, then we can update the cache for the next epoch, this is the last check
|
|
186
|
-
* we do in the validator client, so we can update the cache here.
|
|
187
210
|
*/
|
|
188
211
|
async getProposerInCurrentOrNextSlot(): Promise<{
|
|
189
212
|
currentProposer: EthAddress;
|
|
@@ -191,41 +214,29 @@ export class EpochCache
|
|
|
191
214
|
currentSlot: bigint;
|
|
192
215
|
nextSlot: bigint;
|
|
193
216
|
}> {
|
|
194
|
-
|
|
195
|
-
const
|
|
196
|
-
const { slot: currentSlot, epoch: currentEpoch } = this.getEpochAndSlotNow();
|
|
197
|
-
const { slot: nextSlot, epoch: nextEpoch } = this.getEpochAndSlotInNextSlot();
|
|
198
|
-
|
|
199
|
-
// Compute the proposer in this and the next slot
|
|
200
|
-
const proposerIndex = this.computeProposerIndex(
|
|
201
|
-
currentSlot,
|
|
202
|
-
this.cachedEpoch,
|
|
203
|
-
this.cachedSampleSeed,
|
|
204
|
-
BigInt(committee.length),
|
|
205
|
-
);
|
|
206
|
-
|
|
207
|
-
// Check if the next proposer is in the next epoch
|
|
208
|
-
if (nextEpoch !== currentEpoch) {
|
|
209
|
-
await this.getCommittee(/*next slot*/ true);
|
|
210
|
-
}
|
|
211
|
-
const nextProposerIndex = this.computeProposerIndex(
|
|
212
|
-
nextSlot,
|
|
213
|
-
this.cachedEpoch,
|
|
214
|
-
this.cachedSampleSeed,
|
|
215
|
-
BigInt(committee.length),
|
|
216
|
-
);
|
|
217
|
+
const current = this.getEpochAndSlotNow();
|
|
218
|
+
const next = this.getEpochAndSlotInNextSlot();
|
|
217
219
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
+
return {
|
|
221
|
+
currentProposer: await this.getProposerAt(current),
|
|
222
|
+
nextProposer: await this.getProposerAt(next),
|
|
223
|
+
currentSlot: current.slot,
|
|
224
|
+
nextSlot: next.slot,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
220
227
|
|
|
221
|
-
|
|
228
|
+
private async getProposerAt(when: EpochAndSlot) {
|
|
229
|
+
const { epoch, slot } = when;
|
|
230
|
+
const { seed, committee } = await this.getCommittee(slot);
|
|
231
|
+
const proposerIndex = this.computeProposerIndex(slot, epoch, seed, BigInt(committee.length));
|
|
232
|
+
return committee[Number(proposerIndex)];
|
|
222
233
|
}
|
|
223
234
|
|
|
224
235
|
/**
|
|
225
236
|
* Check if a validator is in the current epoch's committee
|
|
226
237
|
*/
|
|
227
238
|
async isInCommittee(validator: EthAddress): Promise<boolean> {
|
|
228
|
-
const committee = await this.getCommittee();
|
|
239
|
+
const { committee } = await this.getCommittee();
|
|
229
240
|
return committee.some(v => v.equals(validator));
|
|
230
241
|
}
|
|
231
242
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"timestamp_provider.d.ts","sourceRoot":"","sources":["../src/timestamp_provider.ts"],"names":[],"mappings":""}
|
|
File without changes
|
|
File without changes
|