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