@aztec/epoch-cache 0.0.1-commit.6d3c34e → 0.0.1-commit.6ebe706df
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 +3 -2
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +3 -1
- package/dest/epoch_cache.d.ts +64 -21
- package/dest/epoch_cache.d.ts.map +1 -1
- package/dest/epoch_cache.js +110 -46
- package/dest/test/index.d.ts +2 -0
- package/dest/test/index.d.ts.map +1 -0
- package/dest/test/index.js +1 -0
- package/dest/test/test_epoch_cache.d.ts +91 -0
- package/dest/test/test_epoch_cache.d.ts.map +1 -0
- package/dest/test/test_epoch_cache.js +195 -0
- package/package.json +7 -8
- package/src/config.ts +9 -3
- package/src/epoch_cache.ts +139 -50
- package/src/test/index.ts +1 -0
- package/src/test/test_epoch_cache.ts +238 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { SlotNumber } from '@aztec/foundation/branded-types';
|
|
2
|
+
import { getEpochAtSlot, getSlotAtTimestamp, getTimestampRangeForEpoch } from '@aztec/stdlib/epoch-helpers';
|
|
3
|
+
import { PROPOSER_PIPELINING_SLOT_OFFSET } from '../epoch_cache.js';
|
|
4
|
+
/** Default L1 constants for testing. */ const DEFAULT_L1_CONSTANTS = {
|
|
5
|
+
l1StartBlock: 0n,
|
|
6
|
+
l1GenesisTime: 0n,
|
|
7
|
+
slotDuration: 24,
|
|
8
|
+
epochDuration: 16,
|
|
9
|
+
ethereumSlotDuration: 12,
|
|
10
|
+
proofSubmissionEpochs: 2,
|
|
11
|
+
targetCommitteeSize: 48,
|
|
12
|
+
rollupManaLimit: Number.MAX_SAFE_INTEGER
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* A test implementation of EpochCacheInterface that allows manual configuration
|
|
16
|
+
* of committee, proposer, slot, and escape hatch state for use in tests.
|
|
17
|
+
*
|
|
18
|
+
* Unlike the real EpochCache, this class doesn't require any RPC connections
|
|
19
|
+
* or mock setup. Simply use the setter methods to configure the test state.
|
|
20
|
+
*/ export class TestEpochCache {
|
|
21
|
+
committee = [];
|
|
22
|
+
proposerAddress;
|
|
23
|
+
currentSlot = SlotNumber(0);
|
|
24
|
+
escapeHatchOpen = false;
|
|
25
|
+
seed = 0n;
|
|
26
|
+
registeredValidators = [];
|
|
27
|
+
l1Constants;
|
|
28
|
+
proposerPipeliningEnabled = false;
|
|
29
|
+
constructor(l1Constants = {}){
|
|
30
|
+
this.l1Constants = {
|
|
31
|
+
...DEFAULT_L1_CONSTANTS,
|
|
32
|
+
...l1Constants
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Sets the committee members. Used in validation and attestation flows.
|
|
37
|
+
* @param committee - Array of committee member addresses.
|
|
38
|
+
*/ setCommittee(committee) {
|
|
39
|
+
this.committee = committee;
|
|
40
|
+
return this;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Sets the proposer address returned by getProposerAttesterAddressInSlot.
|
|
44
|
+
* @param proposer - The address of the current proposer.
|
|
45
|
+
*/ setProposer(proposer) {
|
|
46
|
+
this.proposerAddress = proposer;
|
|
47
|
+
return this;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Sets the current slot number.
|
|
51
|
+
* @param slot - The slot number to set.
|
|
52
|
+
*/ setCurrentSlot(slot) {
|
|
53
|
+
this.currentSlot = slot;
|
|
54
|
+
return this;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Sets whether the escape hatch is open.
|
|
58
|
+
* @param open - True if escape hatch should be open.
|
|
59
|
+
*/ setEscapeHatchOpen(open) {
|
|
60
|
+
this.escapeHatchOpen = open;
|
|
61
|
+
return this;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Sets the randomness seed used for proposer selection.
|
|
65
|
+
* @param seed - The seed value.
|
|
66
|
+
*/ setSeed(seed) {
|
|
67
|
+
this.seed = seed;
|
|
68
|
+
return this;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Sets the list of registered validators (all validators, not just committee).
|
|
72
|
+
* @param validators - Array of validator addresses.
|
|
73
|
+
*/ setRegisteredValidators(validators) {
|
|
74
|
+
this.registeredValidators = validators;
|
|
75
|
+
return this;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Sets the L1 constants used for epoch/slot calculations.
|
|
79
|
+
* @param constants - Partial constants to override defaults.
|
|
80
|
+
*/ setL1Constants(constants) {
|
|
81
|
+
this.l1Constants = {
|
|
82
|
+
...this.l1Constants,
|
|
83
|
+
...constants
|
|
84
|
+
};
|
|
85
|
+
return this;
|
|
86
|
+
}
|
|
87
|
+
getL1Constants() {
|
|
88
|
+
return this.l1Constants;
|
|
89
|
+
}
|
|
90
|
+
setProposerPipeliningEnabled(enabled) {
|
|
91
|
+
this.proposerPipeliningEnabled = enabled;
|
|
92
|
+
}
|
|
93
|
+
getCommittee(_slot) {
|
|
94
|
+
const epoch = getEpochAtSlot(this.currentSlot, this.l1Constants);
|
|
95
|
+
return Promise.resolve({
|
|
96
|
+
committee: this.committee,
|
|
97
|
+
epoch,
|
|
98
|
+
seed: this.seed,
|
|
99
|
+
isEscapeHatchOpen: this.escapeHatchOpen
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
getSlotNow() {
|
|
103
|
+
return this.currentSlot;
|
|
104
|
+
}
|
|
105
|
+
getTargetSlot() {
|
|
106
|
+
return this.proposerPipeliningEnabled ? SlotNumber(this.currentSlot + PROPOSER_PIPELINING_SLOT_OFFSET) : this.currentSlot;
|
|
107
|
+
}
|
|
108
|
+
getEpochNow() {
|
|
109
|
+
return getEpochAtSlot(this.currentSlot, this.l1Constants);
|
|
110
|
+
}
|
|
111
|
+
getTargetEpoch() {
|
|
112
|
+
return getEpochAtSlot(this.getTargetSlot(), this.l1Constants);
|
|
113
|
+
}
|
|
114
|
+
isProposerPipeliningEnabled() {
|
|
115
|
+
return this.proposerPipeliningEnabled;
|
|
116
|
+
}
|
|
117
|
+
getEpochAndSlotNow() {
|
|
118
|
+
const epochNow = getEpochAtSlot(this.currentSlot, this.l1Constants);
|
|
119
|
+
const ts = getTimestampRangeForEpoch(epochNow, this.l1Constants)[0];
|
|
120
|
+
return {
|
|
121
|
+
epoch: epochNow,
|
|
122
|
+
slot: this.currentSlot,
|
|
123
|
+
ts,
|
|
124
|
+
nowMs: ts * 1000n
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
getEpochAndSlotInNextL1Slot() {
|
|
128
|
+
const nowTs = getTimestampRangeForEpoch(getEpochAtSlot(this.currentSlot, this.l1Constants), this.l1Constants)[0];
|
|
129
|
+
const nextSlotTs = nowTs + BigInt(this.l1Constants.ethereumSlotDuration);
|
|
130
|
+
const nextSlot = getSlotAtTimestamp(nextSlotTs, this.l1Constants);
|
|
131
|
+
const epochNow = getEpochAtSlot(nextSlot, this.l1Constants);
|
|
132
|
+
const ts = getTimestampRangeForEpoch(epochNow, this.l1Constants)[0];
|
|
133
|
+
return {
|
|
134
|
+
epoch: epochNow,
|
|
135
|
+
slot: nextSlot,
|
|
136
|
+
ts,
|
|
137
|
+
nowSeconds: nowTs
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
getTargetEpochAndSlotInNextL1Slot() {
|
|
141
|
+
const result = this.getEpochAndSlotInNextL1Slot();
|
|
142
|
+
const offset = this.isProposerPipeliningEnabled() ? PROPOSER_PIPELINING_SLOT_OFFSET : 0;
|
|
143
|
+
const targetSlot = SlotNumber(result.slot + offset);
|
|
144
|
+
return {
|
|
145
|
+
...result,
|
|
146
|
+
slot: targetSlot,
|
|
147
|
+
epoch: getEpochAtSlot(targetSlot, this.l1Constants)
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
getProposerIndexEncoding(epoch, slot, seed) {
|
|
151
|
+
// Simple encoding for testing purposes
|
|
152
|
+
return `0x${epoch.toString(16).padStart(64, '0')}${slot.toString(16).padStart(64, '0')}${seed.toString(16).padStart(64, '0')}`;
|
|
153
|
+
}
|
|
154
|
+
computeProposerIndex(slot, _epoch, _seed, size) {
|
|
155
|
+
if (size === 0n) {
|
|
156
|
+
return 0n;
|
|
157
|
+
}
|
|
158
|
+
return BigInt(slot) % size;
|
|
159
|
+
}
|
|
160
|
+
getCurrentAndNextSlot() {
|
|
161
|
+
const currentSlot = this.getSlotNow();
|
|
162
|
+
const next = this.getEpochAndSlotInNextL1Slot();
|
|
163
|
+
return {
|
|
164
|
+
currentSlot,
|
|
165
|
+
nextSlot: next.slot
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
getTargetAndNextSlot() {
|
|
169
|
+
const targetSlot = this.getTargetSlot();
|
|
170
|
+
const next = this.getTargetEpochAndSlotInNextL1Slot();
|
|
171
|
+
return {
|
|
172
|
+
targetSlot,
|
|
173
|
+
nextSlot: next.slot
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
getProposerAttesterAddressInSlot(_slot) {
|
|
177
|
+
return Promise.resolve(this.proposerAddress);
|
|
178
|
+
}
|
|
179
|
+
getRegisteredValidators() {
|
|
180
|
+
return Promise.resolve(this.registeredValidators);
|
|
181
|
+
}
|
|
182
|
+
isInCommittee(_slot, validator) {
|
|
183
|
+
return Promise.resolve(this.committee.some((v)=>v.equals(validator)));
|
|
184
|
+
}
|
|
185
|
+
filterInCommittee(_slot, validators) {
|
|
186
|
+
const committeeSet = new Set(this.committee.map((v)=>v.toString()));
|
|
187
|
+
return Promise.resolve(validators.filter((v)=>committeeSet.has(v.toString())));
|
|
188
|
+
}
|
|
189
|
+
isEscapeHatchOpen(_epoch) {
|
|
190
|
+
return Promise.resolve(this.escapeHatchOpen);
|
|
191
|
+
}
|
|
192
|
+
isEscapeHatchOpenAtSlot(_slot) {
|
|
193
|
+
return Promise.resolve(this.escapeHatchOpen);
|
|
194
|
+
}
|
|
195
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aztec/epoch-cache",
|
|
3
|
-
"version": "0.0.1-commit.
|
|
3
|
+
"version": "0.0.1-commit.6ebe706df",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./dest/index.js",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"build": "yarn clean && ../scripts/tsc.sh",
|
|
19
19
|
"build:dev": "../scripts/tsc.sh --watch",
|
|
20
20
|
"clean": "rm -rf ./dest .tsbuildinfo",
|
|
21
|
-
"start:dev": "concurrently -k \"
|
|
21
|
+
"start:dev": "concurrently -k \"../scripts/tsc.sh --watch\" \"nodemon --watch dest --exec yarn start\"",
|
|
22
22
|
"start": "node ./dest/index.js",
|
|
23
23
|
"test": "NODE_NO_WARNINGS=1 node --experimental-vm-modules ../node_modules/.bin/jest --passWithNoTests --maxWorkers=${JEST_MAX_WORKERS:-8}"
|
|
24
24
|
},
|
|
@@ -26,11 +26,10 @@
|
|
|
26
26
|
"../package.common.json"
|
|
27
27
|
],
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@aztec/ethereum": "0.0.1-commit.
|
|
30
|
-
"@aztec/foundation": "0.0.1-commit.
|
|
31
|
-
"@aztec/l1-artifacts": "0.0.1-commit.
|
|
32
|
-
"@aztec/stdlib": "0.0.1-commit.
|
|
33
|
-
"@viem/anvil": "^0.0.10",
|
|
29
|
+
"@aztec/ethereum": "0.0.1-commit.6ebe706df",
|
|
30
|
+
"@aztec/foundation": "0.0.1-commit.6ebe706df",
|
|
31
|
+
"@aztec/l1-artifacts": "0.0.1-commit.6ebe706df",
|
|
32
|
+
"@aztec/stdlib": "0.0.1-commit.6ebe706df",
|
|
34
33
|
"dotenv": "^16.0.3",
|
|
35
34
|
"get-port": "^7.1.0",
|
|
36
35
|
"jest-mock-extended": "^4.0.0",
|
|
@@ -42,7 +41,7 @@
|
|
|
42
41
|
"@jest/globals": "^30.0.0",
|
|
43
42
|
"@types/jest": "^30.0.0",
|
|
44
43
|
"@types/node": "^22.15.17",
|
|
45
|
-
"@typescript/native-preview": "7.0.0-dev.
|
|
44
|
+
"@typescript/native-preview": "7.0.0-dev.20260113.1",
|
|
46
45
|
"jest": "^30.0.0",
|
|
47
46
|
"ts-node": "^10.9.1",
|
|
48
47
|
"typescript": "^5.3.3"
|
package/src/config.ts
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { type L1ContractsConfig, getL1ContractsConfigEnvVars } from '@aztec/ethereum/config';
|
|
2
2
|
import { type L1ReaderConfig, getL1ReaderConfigFromEnv } from '@aztec/ethereum/l1-reader';
|
|
3
|
+
import { type PipelineConfig, getPipelineConfigEnvVars } from '@aztec/stdlib/config';
|
|
3
4
|
|
|
4
5
|
export type EpochCacheConfig = Pick<
|
|
5
|
-
L1ReaderConfig & L1ContractsConfig,
|
|
6
|
-
|
|
6
|
+
L1ReaderConfig & L1ContractsConfig & PipelineConfig,
|
|
7
|
+
| 'l1RpcUrls'
|
|
8
|
+
| 'l1ChainId'
|
|
9
|
+
| 'viemPollingIntervalMS'
|
|
10
|
+
| 'ethereumSlotDuration'
|
|
11
|
+
| 'l1HttpTimeoutMS'
|
|
12
|
+
| 'enableProposerPipelining'
|
|
7
13
|
>;
|
|
8
14
|
|
|
9
15
|
export function getEpochCacheConfigEnvVars(): EpochCacheConfig {
|
|
10
|
-
return { ...getL1ReaderConfigFromEnv(), ...getL1ContractsConfigEnvVars() };
|
|
16
|
+
return { ...getL1ReaderConfigFromEnv(), ...getL1ContractsConfigEnvVars(), ...getPipelineConfigEnvVars() };
|
|
11
17
|
}
|
package/src/epoch_cache.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createEthereumChain } from '@aztec/ethereum/chain';
|
|
2
|
+
import { makeL1HttpTransport } from '@aztec/ethereum/client';
|
|
2
3
|
import { NoCommitteeError, RollupContract } from '@aztec/ethereum/contracts';
|
|
3
4
|
import { EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
4
5
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
@@ -8,19 +9,23 @@ import {
|
|
|
8
9
|
type L1RollupConstants,
|
|
9
10
|
getEpochAtSlot,
|
|
10
11
|
getEpochNumberAtTimestamp,
|
|
12
|
+
getNextL1SlotTimestamp,
|
|
11
13
|
getSlotAtTimestamp,
|
|
12
14
|
getSlotRangeForEpoch,
|
|
13
15
|
getTimestampForSlot,
|
|
14
|
-
getTimestampRangeForEpoch,
|
|
15
16
|
} from '@aztec/stdlib/epoch-helpers';
|
|
16
17
|
|
|
17
|
-
import { createPublicClient, encodeAbiParameters,
|
|
18
|
+
import { createPublicClient, encodeAbiParameters, keccak256 } from 'viem';
|
|
18
19
|
|
|
19
20
|
import { type EpochCacheConfig, getEpochCacheConfigEnvVars } from './config.js';
|
|
20
21
|
|
|
22
|
+
/** When proposer pipelining is enabled, the proposer builds one slot ahead. */
|
|
23
|
+
export const PROPOSER_PIPELINING_SLOT_OFFSET = 1;
|
|
24
|
+
|
|
25
|
+
/** Flat return type for compound epoch/slot getters. */
|
|
21
26
|
export type EpochAndSlot = {
|
|
22
|
-
epoch: EpochNumber;
|
|
23
27
|
slot: SlotNumber;
|
|
28
|
+
epoch: EpochNumber;
|
|
24
29
|
ts: bigint;
|
|
25
30
|
};
|
|
26
31
|
|
|
@@ -28,26 +33,34 @@ export type EpochCommitteeInfo = {
|
|
|
28
33
|
committee: EthAddress[] | undefined;
|
|
29
34
|
seed: bigint;
|
|
30
35
|
epoch: EpochNumber;
|
|
36
|
+
/** True if the epoch is within an open escape hatch window. */
|
|
37
|
+
isEscapeHatchOpen: boolean;
|
|
31
38
|
};
|
|
32
39
|
|
|
33
40
|
export type SlotTag = 'now' | 'next' | SlotNumber;
|
|
34
41
|
|
|
35
42
|
export interface EpochCacheInterface {
|
|
36
43
|
getCommittee(slot: SlotTag | undefined): Promise<EpochCommitteeInfo>;
|
|
37
|
-
|
|
38
|
-
|
|
44
|
+
getSlotNow(): SlotNumber;
|
|
45
|
+
getTargetSlot(): SlotNumber;
|
|
46
|
+
getEpochNow(): EpochNumber;
|
|
47
|
+
getTargetEpoch(): EpochNumber;
|
|
48
|
+
getEpochAndSlotNow(): EpochAndSlot & { nowMs: bigint };
|
|
49
|
+
getEpochAndSlotInNextL1Slot(): EpochAndSlot & { nowSeconds: bigint };
|
|
50
|
+
/** Returns epoch/slot info for the next L1 slot with pipeline offset applied. */
|
|
51
|
+
getTargetEpochAndSlotInNextL1Slot(): EpochAndSlot & { nowSeconds: bigint };
|
|
52
|
+
isProposerPipeliningEnabled(): boolean;
|
|
53
|
+
isEscapeHatchOpen(epoch: EpochNumber): Promise<boolean>;
|
|
54
|
+
isEscapeHatchOpenAtSlot(slot: SlotTag): Promise<boolean>;
|
|
39
55
|
getProposerIndexEncoding(epoch: EpochNumber, slot: SlotNumber, seed: bigint): `0x${string}`;
|
|
40
56
|
computeProposerIndex(slot: SlotNumber, epoch: EpochNumber, seed: bigint, size: bigint): bigint;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
nextProposer: EthAddress | undefined;
|
|
44
|
-
currentSlot: SlotNumber;
|
|
45
|
-
nextSlot: SlotNumber;
|
|
46
|
-
}>;
|
|
57
|
+
getCurrentAndNextSlot(): { currentSlot: SlotNumber; nextSlot: SlotNumber };
|
|
58
|
+
getTargetAndNextSlot(): { targetSlot: SlotNumber; nextSlot: SlotNumber };
|
|
47
59
|
getProposerAttesterAddressInSlot(slot: SlotNumber): Promise<EthAddress | undefined>;
|
|
48
60
|
getRegisteredValidators(): Promise<EthAddress[]>;
|
|
49
61
|
isInCommittee(slot: SlotTag, validator: EthAddress): Promise<boolean>;
|
|
50
62
|
filterInCommittee(slot: SlotTag, validators: EthAddress[]): Promise<EthAddress[]>;
|
|
63
|
+
getL1Constants(): L1RollupConstants;
|
|
51
64
|
}
|
|
52
65
|
|
|
53
66
|
/**
|
|
@@ -66,6 +79,8 @@ export class EpochCache implements EpochCacheInterface {
|
|
|
66
79
|
private lastValidatorRefresh = 0;
|
|
67
80
|
private readonly log: Logger = createLogger('epoch-cache');
|
|
68
81
|
|
|
82
|
+
protected enableProposerPipelining: boolean;
|
|
83
|
+
|
|
69
84
|
constructor(
|
|
70
85
|
private rollup: RollupContract,
|
|
71
86
|
private readonly l1constants: L1RollupConstants & {
|
|
@@ -73,10 +88,12 @@ export class EpochCache implements EpochCacheInterface {
|
|
|
73
88
|
lagInEpochsForRandao: number;
|
|
74
89
|
},
|
|
75
90
|
private readonly dateProvider: DateProvider = new DateProvider(),
|
|
76
|
-
protected readonly config = { cacheSize: 12, validatorRefreshIntervalSeconds: 60 },
|
|
91
|
+
protected readonly config = { cacheSize: 12, validatorRefreshIntervalSeconds: 60, enableProposerPipelining: false },
|
|
77
92
|
) {
|
|
93
|
+
this.enableProposerPipelining = this.config.enableProposerPipelining;
|
|
78
94
|
this.log.debug(`Initialized EpochCache`, {
|
|
79
95
|
l1constants,
|
|
96
|
+
enableProposerPipelining: this.enableProposerPipelining,
|
|
80
97
|
});
|
|
81
98
|
}
|
|
82
99
|
|
|
@@ -95,7 +112,7 @@ export class EpochCache implements EpochCacheInterface {
|
|
|
95
112
|
const chain = createEthereumChain(config.l1RpcUrls, config.l1ChainId);
|
|
96
113
|
const publicClient = createPublicClient({
|
|
97
114
|
chain: chain.chainInfo,
|
|
98
|
-
transport:
|
|
115
|
+
transport: makeL1HttpTransport(config.l1RpcUrls, { timeout: config.l1HttpTimeoutMS }),
|
|
99
116
|
pollingInterval: config.viemPollingIntervalMS,
|
|
100
117
|
});
|
|
101
118
|
rollup = new RollupContract(publicClient, rollupOrAddress.toString());
|
|
@@ -109,6 +126,8 @@ export class EpochCache implements EpochCacheInterface {
|
|
|
109
126
|
epochDuration,
|
|
110
127
|
lagInEpochsForValidatorSet,
|
|
111
128
|
lagInEpochsForRandao,
|
|
129
|
+
targetCommitteeSize,
|
|
130
|
+
rollupManaLimit,
|
|
112
131
|
] = await Promise.all([
|
|
113
132
|
rollup.getL1StartBlock(),
|
|
114
133
|
rollup.getL1GenesisTime(),
|
|
@@ -117,6 +136,8 @@ export class EpochCache implements EpochCacheInterface {
|
|
|
117
136
|
rollup.getEpochDuration(),
|
|
118
137
|
rollup.getLagInEpochsForValidatorSet(),
|
|
119
138
|
rollup.getLagInEpochsForRandao(),
|
|
139
|
+
rollup.getTargetCommitteeSize(),
|
|
140
|
+
rollup.getManaLimit(),
|
|
120
141
|
] as const);
|
|
121
142
|
|
|
122
143
|
const l1RollupConstants = {
|
|
@@ -128,42 +149,77 @@ export class EpochCache implements EpochCacheInterface {
|
|
|
128
149
|
ethereumSlotDuration: config.ethereumSlotDuration,
|
|
129
150
|
lagInEpochsForValidatorSet: Number(lagInEpochsForValidatorSet),
|
|
130
151
|
lagInEpochsForRandao: Number(lagInEpochsForRandao),
|
|
152
|
+
targetCommitteeSize: Number(targetCommitteeSize),
|
|
153
|
+
rollupManaLimit: Number(rollupManaLimit),
|
|
131
154
|
};
|
|
132
155
|
|
|
133
|
-
return new EpochCache(rollup, l1RollupConstants, deps.dateProvider
|
|
156
|
+
return new EpochCache(rollup, l1RollupConstants, deps.dateProvider, {
|
|
157
|
+
cacheSize: 12,
|
|
158
|
+
validatorRefreshIntervalSeconds: 60,
|
|
159
|
+
enableProposerPipelining: config.enableProposerPipelining,
|
|
160
|
+
});
|
|
134
161
|
}
|
|
135
162
|
|
|
136
163
|
public getL1Constants(): L1RollupConstants {
|
|
137
164
|
return this.l1constants;
|
|
138
165
|
}
|
|
139
166
|
|
|
140
|
-
public
|
|
141
|
-
|
|
142
|
-
|
|
167
|
+
public isProposerPipeliningEnabled(): boolean {
|
|
168
|
+
return this.enableProposerPipelining;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
public getSlotNow(): SlotNumber {
|
|
172
|
+
return this.getEpochAndSlotNow().slot;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
public getTargetSlot(): SlotNumber {
|
|
176
|
+
const slotNow = this.getSlotNow();
|
|
177
|
+
const offset = this.isProposerPipeliningEnabled() ? PROPOSER_PIPELINING_SLOT_OFFSET : 0;
|
|
178
|
+
return SlotNumber(slotNow + offset);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
public getEpochNow(): EpochNumber {
|
|
182
|
+
return this.getEpochAndSlotNow().epoch;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
public getTargetEpoch(): EpochNumber {
|
|
186
|
+
return getEpochAtSlot(this.getTargetSlot(), this.l1constants);
|
|
143
187
|
}
|
|
144
188
|
|
|
145
|
-
public
|
|
146
|
-
|
|
189
|
+
public getEpochAndSlotNow(): EpochAndSlot & { nowMs: bigint } {
|
|
190
|
+
const nowMs = BigInt(this.dateProvider.now());
|
|
191
|
+
const nowSeconds = nowMs / 1000n;
|
|
192
|
+
return { ...this.getEpochAndSlotAtTimestamp(nowSeconds), nowMs };
|
|
147
193
|
}
|
|
148
194
|
|
|
149
195
|
private getEpochAndSlotAtSlot(slot: SlotNumber): EpochAndSlot {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
196
|
+
return this.getEpochAndSlotAtTimestamp(getTimestampForSlot(slot, this.l1constants));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
public getEpochAndSlotInNextL1Slot(): EpochAndSlot & { nowSeconds: bigint } {
|
|
200
|
+
const nowSeconds = this.dateProvider.nowInSeconds();
|
|
201
|
+
const nextSlotTs = getNextL1SlotTimestamp(nowSeconds, this.l1constants);
|
|
202
|
+
return { ...this.getEpochAndSlotAtTimestamp(nextSlotTs), nowSeconds: BigInt(nowSeconds) };
|
|
153
203
|
}
|
|
154
204
|
|
|
155
|
-
public
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
205
|
+
public getTargetEpochAndSlotInNextL1Slot(): EpochAndSlot & { nowSeconds: bigint } {
|
|
206
|
+
if (!this.isProposerPipeliningEnabled()) {
|
|
207
|
+
return this.getEpochAndSlotInNextL1Slot();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const result = this.getEpochAndSlotInNextL1Slot();
|
|
211
|
+
const offset = PROPOSER_PIPELINING_SLOT_OFFSET;
|
|
212
|
+
const targetSlot = SlotNumber(result.slot + offset);
|
|
213
|
+
return { ...result, slot: targetSlot, epoch: getEpochAtSlot(targetSlot, this.l1constants) };
|
|
159
214
|
}
|
|
160
215
|
|
|
161
216
|
private getEpochAndSlotAtTimestamp(ts: bigint): EpochAndSlot {
|
|
162
217
|
const slot = getSlotAtTimestamp(ts, this.l1constants);
|
|
218
|
+
const epoch = getEpochNumberAtTimestamp(ts, this.l1constants);
|
|
163
219
|
return {
|
|
164
|
-
epoch: getEpochNumberAtTimestamp(ts, this.l1constants),
|
|
165
|
-
ts: getTimestampForSlot(slot, this.l1constants),
|
|
166
220
|
slot,
|
|
221
|
+
epoch,
|
|
222
|
+
ts: getTimestampForSlot(slot, this.l1constants),
|
|
167
223
|
};
|
|
168
224
|
}
|
|
169
225
|
|
|
@@ -172,6 +228,38 @@ export class EpochCache implements EpochCacheInterface {
|
|
|
172
228
|
return this.getCommittee(startSlot);
|
|
173
229
|
}
|
|
174
230
|
|
|
231
|
+
/**
|
|
232
|
+
* Returns whether the escape hatch is open for the given epoch.
|
|
233
|
+
*
|
|
234
|
+
* Uses the already-cached EpochCommitteeInfo when available. If not cached, it will fetch
|
|
235
|
+
* the epoch committee info (which includes the escape hatch flag) and return it.
|
|
236
|
+
*/
|
|
237
|
+
public async isEscapeHatchOpen(epoch: EpochNumber): Promise<boolean> {
|
|
238
|
+
const cached = this.cache.get(epoch);
|
|
239
|
+
if (cached) {
|
|
240
|
+
return cached.isEscapeHatchOpen;
|
|
241
|
+
}
|
|
242
|
+
const info = await this.getCommitteeForEpoch(epoch);
|
|
243
|
+
return info.isEscapeHatchOpen;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Returns whether the escape hatch is open for the epoch containing the given slot.
|
|
248
|
+
*
|
|
249
|
+
* This is a lightweight helper intended for callers that already have a slot number and only
|
|
250
|
+
* need the escape hatch flag (without pulling full committee info).
|
|
251
|
+
*/
|
|
252
|
+
public async isEscapeHatchOpenAtSlot(slot: SlotTag = 'now'): Promise<boolean> {
|
|
253
|
+
const epoch =
|
|
254
|
+
slot === 'now'
|
|
255
|
+
? this.getEpochNow()
|
|
256
|
+
: slot === 'next'
|
|
257
|
+
? this.getEpochAndSlotInNextL1Slot().epoch
|
|
258
|
+
: getEpochAtSlot(slot, this.l1constants);
|
|
259
|
+
|
|
260
|
+
return await this.isEscapeHatchOpen(epoch);
|
|
261
|
+
}
|
|
262
|
+
|
|
175
263
|
/**
|
|
176
264
|
* Get the current validator set
|
|
177
265
|
* @param nextSlot - If true, get the validator set for the next slot.
|
|
@@ -199,7 +287,7 @@ export class EpochCache implements EpochCacheInterface {
|
|
|
199
287
|
return epochData;
|
|
200
288
|
}
|
|
201
289
|
|
|
202
|
-
private getEpochAndTimestamp(slot: SlotTag = 'now') {
|
|
290
|
+
private getEpochAndTimestamp(slot: SlotTag = 'now'): { epoch: EpochNumber; ts: bigint } {
|
|
203
291
|
if (slot === 'now') {
|
|
204
292
|
return this.getEpochAndSlotNow();
|
|
205
293
|
} else if (slot === 'next') {
|
|
@@ -211,10 +299,11 @@ export class EpochCache implements EpochCacheInterface {
|
|
|
211
299
|
|
|
212
300
|
private async computeCommittee(when: { epoch: EpochNumber; ts: bigint }): Promise<EpochCommitteeInfo> {
|
|
213
301
|
const { ts, epoch } = when;
|
|
214
|
-
const [committee, seedBuffer, l1Timestamp] = await Promise.all([
|
|
302
|
+
const [committee, seedBuffer, l1Timestamp, isEscapeHatchOpen] = await Promise.all([
|
|
215
303
|
this.rollup.getCommitteeAt(ts),
|
|
216
304
|
this.rollup.getSampleSeedAt(ts),
|
|
217
305
|
this.rollup.client.getBlock({ includeTransactions: false }).then(b => b.timestamp),
|
|
306
|
+
this.rollup.isEscapeHatchOpen(epoch),
|
|
218
307
|
]);
|
|
219
308
|
const { lagInEpochsForValidatorSet, epochDuration, slotDuration } = this.l1constants;
|
|
220
309
|
const sub = BigInt(lagInEpochsForValidatorSet) * BigInt(epochDuration) * BigInt(slotDuration);
|
|
@@ -223,7 +312,7 @@ export class EpochCache implements EpochCacheInterface {
|
|
|
223
312
|
`Cannot query committee for future epoch ${epoch} with timestamp ${ts} (current L1 time is ${l1Timestamp}). Check your Ethereum node is synced.`,
|
|
224
313
|
);
|
|
225
314
|
}
|
|
226
|
-
return { committee, seed: seedBuffer.toBigInt(), epoch };
|
|
315
|
+
return { committee, seed: seedBuffer.toBigInt(), epoch, isEscapeHatchOpen };
|
|
227
316
|
}
|
|
228
317
|
|
|
229
318
|
/**
|
|
@@ -248,25 +337,24 @@ export class EpochCache implements EpochCacheInterface {
|
|
|
248
337
|
return BigInt(keccak256(this.getProposerIndexEncoding(epoch, slot, seed))) % size;
|
|
249
338
|
}
|
|
250
339
|
|
|
251
|
-
/**
|
|
252
|
-
|
|
253
|
-
|
|
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.
|
|
256
|
-
*/
|
|
257
|
-
public async getProposerAttesterAddressInCurrentOrNextSlot(): Promise<{
|
|
258
|
-
currentSlot: SlotNumber;
|
|
259
|
-
nextSlot: SlotNumber;
|
|
260
|
-
currentProposer: EthAddress | undefined;
|
|
261
|
-
nextProposer: EthAddress | undefined;
|
|
262
|
-
}> {
|
|
263
|
-
const current = this.getEpochAndSlotNow();
|
|
340
|
+
/** Returns the current and next L2 slot in next eth L1 Slot. */
|
|
341
|
+
public getCurrentAndNextSlot(): { currentSlot: SlotNumber; nextSlot: SlotNumber } {
|
|
342
|
+
const currentSlot = this.getSlotNow();
|
|
264
343
|
const next = this.getEpochAndSlotInNextL1Slot();
|
|
265
344
|
|
|
266
345
|
return {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
346
|
+
currentSlot,
|
|
347
|
+
nextSlot: next.slot,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** Returns the taget and next L2 slot in the next L1 slot */
|
|
352
|
+
public getTargetAndNextSlot(): { targetSlot: SlotNumber; nextSlot: SlotNumber } {
|
|
353
|
+
const targetSlot = this.getTargetSlot();
|
|
354
|
+
const next = this.getTargetEpochAndSlotInNextL1Slot();
|
|
355
|
+
|
|
356
|
+
return {
|
|
357
|
+
targetSlot,
|
|
270
358
|
nextSlot: next.slot,
|
|
271
359
|
};
|
|
272
360
|
}
|
|
@@ -349,10 +437,11 @@ export class EpochCache implements EpochCacheInterface {
|
|
|
349
437
|
async getRegisteredValidators(): Promise<EthAddress[]> {
|
|
350
438
|
const validatorRefreshIntervalMs = this.config.validatorRefreshIntervalSeconds * 1000;
|
|
351
439
|
const validatorRefreshTime = this.lastValidatorRefresh + validatorRefreshIntervalMs;
|
|
352
|
-
|
|
353
|
-
|
|
440
|
+
const now = this.dateProvider.now();
|
|
441
|
+
if (validatorRefreshTime < now) {
|
|
442
|
+
const currentSet = await this.rollup.getAttesters(BigInt(Math.floor(now / 1000)));
|
|
354
443
|
this.allValidators = new Set(currentSet.map(v => v.toString()));
|
|
355
|
-
this.lastValidatorRefresh =
|
|
444
|
+
this.lastValidatorRefresh = now;
|
|
356
445
|
}
|
|
357
446
|
return Array.from(this.allValidators.keys()).map(v => EthAddress.fromString(v));
|
|
358
447
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './test_epoch_cache.js';
|