@aztec/epoch-cache 0.75.0-commit.c03ba01a2a4122e43e90d5133ba017e54b90e9d2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dest/config.js +7 -0
- package/dest/epoch_cache.js +164 -0
- package/dest/index.js +2 -0
- package/dest/timestamp_provider.js +0 -0
- package/package.json +93 -0
- package/src/config.ts +20 -0
- package/src/epoch_cache.ts +214 -0
- package/src/index.ts +2 -0
- package/src/timestamp_provider.ts +0 -0
package/dest/config.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { EmptyL1RollupConstants, getEpochNumberAtTimestamp, getSlotAtTimestamp } from '@aztec/circuit-types';
|
|
2
|
+
import { RollupContract, createEthereumChain } from '@aztec/ethereum';
|
|
3
|
+
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
4
|
+
import { createLogger } from '@aztec/foundation/log';
|
|
5
|
+
import { DateProvider } from '@aztec/foundation/timer';
|
|
6
|
+
import { EventEmitter } from 'node:events';
|
|
7
|
+
import { createPublicClient, encodeAbiParameters, http, keccak256 } from 'viem';
|
|
8
|
+
import { getEpochCacheConfigEnvVars } from './config.js';
|
|
9
|
+
/**
|
|
10
|
+
* Epoch cache
|
|
11
|
+
*
|
|
12
|
+
* This class is responsible for managing traffic to the l1 node, by caching the validator set.
|
|
13
|
+
* It also provides a method to get the current or next proposer, and to check who is in the current slot.
|
|
14
|
+
*
|
|
15
|
+
* If the epoch changes, then we update the stored validator set.
|
|
16
|
+
*
|
|
17
|
+
* Note: This class is very dependent on the system clock being in sync.
|
|
18
|
+
*/ export class EpochCache extends EventEmitter {
|
|
19
|
+
rollup;
|
|
20
|
+
l1constants;
|
|
21
|
+
dateProvider;
|
|
22
|
+
committee;
|
|
23
|
+
cachedEpoch;
|
|
24
|
+
cachedSampleSeed;
|
|
25
|
+
log;
|
|
26
|
+
constructor(rollup, initialValidators = [], initialSampleSeed = 0n, l1constants = EmptyL1RollupConstants, dateProvider = new DateProvider()){
|
|
27
|
+
super(), this.rollup = rollup, this.l1constants = l1constants, this.dateProvider = dateProvider, this.log = createLogger('epoch-cache');
|
|
28
|
+
this.committee = initialValidators;
|
|
29
|
+
this.cachedSampleSeed = initialSampleSeed;
|
|
30
|
+
this.log.debug(`Initialized EpochCache with constants and validators`, {
|
|
31
|
+
l1constants,
|
|
32
|
+
initialValidators
|
|
33
|
+
});
|
|
34
|
+
this.cachedEpoch = getEpochNumberAtTimestamp(this.nowInSeconds(), this.l1constants);
|
|
35
|
+
}
|
|
36
|
+
static async create(rollupAddress, config, deps = {}) {
|
|
37
|
+
config = config ?? getEpochCacheConfigEnvVars();
|
|
38
|
+
const chain = createEthereumChain(config.l1RpcUrl, config.l1ChainId);
|
|
39
|
+
const publicClient = createPublicClient({
|
|
40
|
+
chain: chain.chainInfo,
|
|
41
|
+
transport: http(chain.rpcUrl),
|
|
42
|
+
pollingInterval: config.viemPollingIntervalMS
|
|
43
|
+
});
|
|
44
|
+
const rollup = new RollupContract(publicClient, rollupAddress.toString());
|
|
45
|
+
const [l1StartBlock, l1GenesisTime, initialValidators, sampleSeed] = await Promise.all([
|
|
46
|
+
rollup.getL1StartBlock(),
|
|
47
|
+
rollup.getL1GenesisTime(),
|
|
48
|
+
rollup.getCurrentEpochCommittee(),
|
|
49
|
+
rollup.getCurrentSampleSeed()
|
|
50
|
+
]);
|
|
51
|
+
const l1RollupConstants = {
|
|
52
|
+
l1StartBlock,
|
|
53
|
+
l1GenesisTime,
|
|
54
|
+
slotDuration: config.aztecSlotDuration,
|
|
55
|
+
epochDuration: config.aztecEpochDuration,
|
|
56
|
+
ethereumSlotDuration: config.ethereumSlotDuration
|
|
57
|
+
};
|
|
58
|
+
return new EpochCache(rollup, initialValidators.map((v)=>EthAddress.fromString(v)), sampleSeed, l1RollupConstants, deps.dateProvider);
|
|
59
|
+
}
|
|
60
|
+
nowInSeconds() {
|
|
61
|
+
return BigInt(Math.floor(this.dateProvider.now() / 1000));
|
|
62
|
+
}
|
|
63
|
+
getEpochAndSlotNow() {
|
|
64
|
+
return this.getEpochAndSlotAtTimestamp(this.nowInSeconds());
|
|
65
|
+
}
|
|
66
|
+
getEpochAndSlotInNextSlot() {
|
|
67
|
+
const nextSlotTs = this.nowInSeconds() + BigInt(this.l1constants.slotDuration);
|
|
68
|
+
return this.getEpochAndSlotAtTimestamp(nextSlotTs);
|
|
69
|
+
}
|
|
70
|
+
getEpochAndSlotAtTimestamp(ts) {
|
|
71
|
+
return {
|
|
72
|
+
epoch: getEpochNumberAtTimestamp(ts, this.l1constants),
|
|
73
|
+
slot: getSlotAtTimestamp(ts, this.l1constants),
|
|
74
|
+
ts
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Get the current validator set
|
|
79
|
+
*
|
|
80
|
+
* @param nextSlot - If true, get the validator set for the next slot.
|
|
81
|
+
* @returns The current validator set.
|
|
82
|
+
*/ async getCommittee(nextSlot = false) {
|
|
83
|
+
// If the current epoch has changed, then we need to make a request to update the validator set
|
|
84
|
+
const { epoch: calculatedEpoch, ts } = nextSlot ? this.getEpochAndSlotInNextSlot() : this.getEpochAndSlotNow();
|
|
85
|
+
if (calculatedEpoch !== this.cachedEpoch) {
|
|
86
|
+
this.log.debug(`Updating validator set for new epoch ${calculatedEpoch}`, {
|
|
87
|
+
epoch: calculatedEpoch,
|
|
88
|
+
previousEpoch: this.cachedEpoch
|
|
89
|
+
});
|
|
90
|
+
const [committeeAtTs, sampleSeedAtTs] = await Promise.all([
|
|
91
|
+
this.rollup.getCommitteeAt(ts),
|
|
92
|
+
this.rollup.getSampleSeedAt(ts)
|
|
93
|
+
]);
|
|
94
|
+
this.committee = committeeAtTs.map((v)=>EthAddress.fromString(v));
|
|
95
|
+
this.cachedEpoch = calculatedEpoch;
|
|
96
|
+
this.cachedSampleSeed = sampleSeedAtTs;
|
|
97
|
+
this.log.debug(`Updated validator set for epoch ${calculatedEpoch}`, {
|
|
98
|
+
commitee: this.committee
|
|
99
|
+
});
|
|
100
|
+
this.emit('committeeChanged', this.committee, calculatedEpoch);
|
|
101
|
+
}
|
|
102
|
+
return this.committee;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Get the ABI encoding of the proposer index - see ValidatorSelectionLib.sol computeProposerIndex
|
|
106
|
+
*/ getProposerIndexEncoding(epoch, slot, seed) {
|
|
107
|
+
return encodeAbiParameters([
|
|
108
|
+
{
|
|
109
|
+
type: 'uint256',
|
|
110
|
+
name: 'epoch'
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
type: 'uint256',
|
|
114
|
+
name: 'slot'
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
type: 'uint256',
|
|
118
|
+
name: 'seed'
|
|
119
|
+
}
|
|
120
|
+
], [
|
|
121
|
+
epoch,
|
|
122
|
+
slot,
|
|
123
|
+
seed
|
|
124
|
+
]);
|
|
125
|
+
}
|
|
126
|
+
computeProposerIndex(slot, epoch, seed, size) {
|
|
127
|
+
return BigInt(keccak256(this.getProposerIndexEncoding(epoch, slot, seed))) % size;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Returns the current and next proposer
|
|
131
|
+
*
|
|
132
|
+
* We return the next proposer as the node will check if it is the proposer at the next ethereum block, which
|
|
133
|
+
* can be the next slot. If this is the case, then it will send proposals early.
|
|
134
|
+
*
|
|
135
|
+
* If we are at an epoch boundary, then we can update the cache for the next epoch, this is the last check
|
|
136
|
+
* we do in the validator client, so we can update the cache here.
|
|
137
|
+
*/ async getProposerInCurrentOrNextSlot() {
|
|
138
|
+
// Validators are sorted by their index in the committee, and getValidatorSet will cache
|
|
139
|
+
const committee = await this.getCommittee();
|
|
140
|
+
const { slot: currentSlot, epoch: currentEpoch } = this.getEpochAndSlotNow();
|
|
141
|
+
const { slot: nextSlot, epoch: nextEpoch } = this.getEpochAndSlotInNextSlot();
|
|
142
|
+
// Compute the proposer in this and the next slot
|
|
143
|
+
const proposerIndex = this.computeProposerIndex(currentSlot, this.cachedEpoch, this.cachedSampleSeed, BigInt(committee.length));
|
|
144
|
+
// Check if the next proposer is in the next epoch
|
|
145
|
+
if (nextEpoch !== currentEpoch) {
|
|
146
|
+
await this.getCommittee(/*next slot*/ true);
|
|
147
|
+
}
|
|
148
|
+
const nextProposerIndex = this.computeProposerIndex(nextSlot, this.cachedEpoch, this.cachedSampleSeed, BigInt(committee.length));
|
|
149
|
+
const currentProposer = committee[Number(proposerIndex)];
|
|
150
|
+
const nextProposer = committee[Number(nextProposerIndex)];
|
|
151
|
+
return {
|
|
152
|
+
currentProposer,
|
|
153
|
+
nextProposer,
|
|
154
|
+
currentSlot,
|
|
155
|
+
nextSlot
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Check if a validator is in the current epoch's committee
|
|
160
|
+
*/ async isInCommittee(validator) {
|
|
161
|
+
const committee = await this.getCommittee();
|
|
162
|
+
return committee.some((v)=>v.equals(validator));
|
|
163
|
+
}
|
|
164
|
+
}
|
package/dest/index.js
ADDED
|
File without changes
|
package/package.json
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aztec/epoch-cache",
|
|
3
|
+
"version": "0.75.0-commit.c03ba01a2a4122e43e90d5133ba017e54b90e9d2",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": "./dest/index.js",
|
|
7
|
+
"./test": "./dest/test/index.js",
|
|
8
|
+
"./contracts": "./dest/contracts/index.js"
|
|
9
|
+
},
|
|
10
|
+
"typedocOptions": {
|
|
11
|
+
"entryPoints": [
|
|
12
|
+
"./src/index.ts"
|
|
13
|
+
],
|
|
14
|
+
"name": "Epoch Cache",
|
|
15
|
+
"tsconfig": "./tsconfig.json"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "yarn clean && tsc -b",
|
|
19
|
+
"build:dev": "tsc -b --watch",
|
|
20
|
+
"clean": "rm -rf ./dest .tsbuildinfo",
|
|
21
|
+
"formatting": "run -T prettier --check ./src && run -T eslint ./src",
|
|
22
|
+
"formatting:fix": "run -T eslint --fix ./src && run -T prettier -w ./src",
|
|
23
|
+
"start:dev": "tsc-watch -p tsconfig.json --onSuccess 'yarn start'",
|
|
24
|
+
"start": "node ./dest/index.js",
|
|
25
|
+
"test": "NODE_NO_WARNINGS=1 node --experimental-vm-modules ../node_modules/.bin/jest --passWithNoTests --maxWorkers=${JEST_MAX_WORKERS:-8}"
|
|
26
|
+
},
|
|
27
|
+
"inherits": [
|
|
28
|
+
"../package.common.json"
|
|
29
|
+
],
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@aztec/circuit-types": "0.75.0-commit.c03ba01a2a4122e43e90d5133ba017e54b90e9d2",
|
|
32
|
+
"@aztec/ethereum": "0.75.0-commit.c03ba01a2a4122e43e90d5133ba017e54b90e9d2",
|
|
33
|
+
"@aztec/foundation": "0.75.0-commit.c03ba01a2a4122e43e90d5133ba017e54b90e9d2",
|
|
34
|
+
"@aztec/l1-artifacts": "0.75.0-commit.c03ba01a2a4122e43e90d5133ba017e54b90e9d2",
|
|
35
|
+
"@viem/anvil": "^0.0.10",
|
|
36
|
+
"dotenv": "^16.0.3",
|
|
37
|
+
"get-port": "^7.1.0",
|
|
38
|
+
"jest-mock-extended": "^3.0.7",
|
|
39
|
+
"tslib": "^2.4.0",
|
|
40
|
+
"viem": "2.22.8",
|
|
41
|
+
"zod": "^3.23.8"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@jest/globals": "^29.5.0",
|
|
45
|
+
"@types/jest": "^29.5.0",
|
|
46
|
+
"@types/node": "^18.14.6",
|
|
47
|
+
"jest": "^29.5.0",
|
|
48
|
+
"ts-node": "^10.9.1",
|
|
49
|
+
"typescript": "^5.0.4"
|
|
50
|
+
},
|
|
51
|
+
"files": [
|
|
52
|
+
"dest",
|
|
53
|
+
"src",
|
|
54
|
+
"!*.test.*"
|
|
55
|
+
],
|
|
56
|
+
"types": "./dest/index.d.ts",
|
|
57
|
+
"jest": {
|
|
58
|
+
"moduleNameMapper": {
|
|
59
|
+
"^(\\.{1,2}/.*)\\.[cm]?js$": "$1"
|
|
60
|
+
},
|
|
61
|
+
"testRegex": "./src/.*\\.test\\.(js|mjs|ts)$",
|
|
62
|
+
"rootDir": "./src",
|
|
63
|
+
"transform": {
|
|
64
|
+
"^.+\\.tsx?$": [
|
|
65
|
+
"@swc/jest",
|
|
66
|
+
{
|
|
67
|
+
"jsc": {
|
|
68
|
+
"parser": {
|
|
69
|
+
"syntax": "typescript",
|
|
70
|
+
"decorators": true
|
|
71
|
+
},
|
|
72
|
+
"transform": {
|
|
73
|
+
"decoratorVersion": "2022-03"
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
},
|
|
79
|
+
"extensionsToTreatAsEsm": [
|
|
80
|
+
".ts"
|
|
81
|
+
],
|
|
82
|
+
"reporters": [
|
|
83
|
+
"default"
|
|
84
|
+
],
|
|
85
|
+
"testTimeout": 30000,
|
|
86
|
+
"setupFiles": [
|
|
87
|
+
"../../foundation/src/jest/setup.mjs"
|
|
88
|
+
]
|
|
89
|
+
},
|
|
90
|
+
"engines": {
|
|
91
|
+
"node": ">=18"
|
|
92
|
+
}
|
|
93
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type L1ContractsConfig,
|
|
3
|
+
type L1ReaderConfig,
|
|
4
|
+
getL1ContractsConfigEnvVars,
|
|
5
|
+
getL1ReaderConfigFromEnv,
|
|
6
|
+
} from '@aztec/ethereum';
|
|
7
|
+
|
|
8
|
+
export type EpochCacheConfig = Pick<
|
|
9
|
+
L1ReaderConfig & L1ContractsConfig,
|
|
10
|
+
| 'l1RpcUrl'
|
|
11
|
+
| 'l1ChainId'
|
|
12
|
+
| 'viemPollingIntervalMS'
|
|
13
|
+
| 'aztecSlotDuration'
|
|
14
|
+
| 'ethereumSlotDuration'
|
|
15
|
+
| 'aztecEpochDuration'
|
|
16
|
+
>;
|
|
17
|
+
|
|
18
|
+
export function getEpochCacheConfigEnvVars(): EpochCacheConfig {
|
|
19
|
+
return { ...getL1ReaderConfigFromEnv(), ...getL1ContractsConfigEnvVars() };
|
|
20
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EmptyL1RollupConstants,
|
|
3
|
+
type L1RollupConstants,
|
|
4
|
+
getEpochNumberAtTimestamp,
|
|
5
|
+
getSlotAtTimestamp,
|
|
6
|
+
} from '@aztec/circuit-types';
|
|
7
|
+
import { RollupContract, createEthereumChain } from '@aztec/ethereum';
|
|
8
|
+
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
9
|
+
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
10
|
+
import { DateProvider } from '@aztec/foundation/timer';
|
|
11
|
+
|
|
12
|
+
import { EventEmitter } from 'node:events';
|
|
13
|
+
import { createPublicClient, encodeAbiParameters, http, keccak256 } from 'viem';
|
|
14
|
+
|
|
15
|
+
import { type EpochCacheConfig, getEpochCacheConfigEnvVars } from './config.js';
|
|
16
|
+
|
|
17
|
+
type EpochAndSlot = {
|
|
18
|
+
epoch: bigint;
|
|
19
|
+
slot: bigint;
|
|
20
|
+
ts: bigint;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Epoch cache
|
|
25
|
+
*
|
|
26
|
+
* This class is responsible for managing traffic to the l1 node, by caching the validator set.
|
|
27
|
+
* It also provides a method to get the current or next proposer, and to check who is in the current slot.
|
|
28
|
+
*
|
|
29
|
+
* If the epoch changes, then we update the stored validator set.
|
|
30
|
+
*
|
|
31
|
+
* Note: This class is very dependent on the system clock being in sync.
|
|
32
|
+
*/
|
|
33
|
+
export class EpochCache extends EventEmitter<{ committeeChanged: [EthAddress[], bigint] }> {
|
|
34
|
+
private committee: EthAddress[];
|
|
35
|
+
private cachedEpoch: bigint;
|
|
36
|
+
private cachedSampleSeed: bigint;
|
|
37
|
+
private readonly log: Logger = createLogger('epoch-cache');
|
|
38
|
+
|
|
39
|
+
constructor(
|
|
40
|
+
private rollup: RollupContract,
|
|
41
|
+
initialValidators: EthAddress[] = [],
|
|
42
|
+
initialSampleSeed: bigint = 0n,
|
|
43
|
+
private readonly l1constants: L1RollupConstants = EmptyL1RollupConstants,
|
|
44
|
+
private readonly dateProvider: DateProvider = new DateProvider(),
|
|
45
|
+
) {
|
|
46
|
+
super();
|
|
47
|
+
this.committee = initialValidators;
|
|
48
|
+
this.cachedSampleSeed = initialSampleSeed;
|
|
49
|
+
|
|
50
|
+
this.log.debug(`Initialized EpochCache with constants and validators`, { l1constants, initialValidators });
|
|
51
|
+
|
|
52
|
+
this.cachedEpoch = getEpochNumberAtTimestamp(this.nowInSeconds(), this.l1constants);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
static async create(
|
|
56
|
+
rollupAddress: EthAddress,
|
|
57
|
+
config?: EpochCacheConfig,
|
|
58
|
+
deps: { dateProvider?: DateProvider } = {},
|
|
59
|
+
) {
|
|
60
|
+
config = config ?? getEpochCacheConfigEnvVars();
|
|
61
|
+
|
|
62
|
+
const chain = createEthereumChain(config.l1RpcUrl, config.l1ChainId);
|
|
63
|
+
const publicClient = createPublicClient({
|
|
64
|
+
chain: chain.chainInfo,
|
|
65
|
+
transport: http(chain.rpcUrl),
|
|
66
|
+
pollingInterval: config.viemPollingIntervalMS,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const rollup = new RollupContract(publicClient, rollupAddress.toString());
|
|
70
|
+
const [l1StartBlock, l1GenesisTime, initialValidators, sampleSeed] = await Promise.all([
|
|
71
|
+
rollup.getL1StartBlock(),
|
|
72
|
+
rollup.getL1GenesisTime(),
|
|
73
|
+
rollup.getCurrentEpochCommittee(),
|
|
74
|
+
rollup.getCurrentSampleSeed(),
|
|
75
|
+
] as const);
|
|
76
|
+
|
|
77
|
+
const l1RollupConstants: L1RollupConstants = {
|
|
78
|
+
l1StartBlock,
|
|
79
|
+
l1GenesisTime,
|
|
80
|
+
slotDuration: config.aztecSlotDuration,
|
|
81
|
+
epochDuration: config.aztecEpochDuration,
|
|
82
|
+
ethereumSlotDuration: config.ethereumSlotDuration,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return new EpochCache(
|
|
86
|
+
rollup,
|
|
87
|
+
initialValidators.map(v => EthAddress.fromString(v)),
|
|
88
|
+
sampleSeed,
|
|
89
|
+
l1RollupConstants,
|
|
90
|
+
deps.dateProvider,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private nowInSeconds(): bigint {
|
|
95
|
+
return BigInt(Math.floor(this.dateProvider.now() / 1000));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
getEpochAndSlotNow(): EpochAndSlot {
|
|
99
|
+
return this.getEpochAndSlotAtTimestamp(this.nowInSeconds());
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
getEpochAndSlotInNextSlot(): EpochAndSlot {
|
|
103
|
+
const nextSlotTs = this.nowInSeconds() + BigInt(this.l1constants.slotDuration);
|
|
104
|
+
return this.getEpochAndSlotAtTimestamp(nextSlotTs);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
getEpochAndSlotAtTimestamp(ts: bigint): EpochAndSlot {
|
|
108
|
+
return {
|
|
109
|
+
epoch: getEpochNumberAtTimestamp(ts, this.l1constants),
|
|
110
|
+
slot: getSlotAtTimestamp(ts, this.l1constants),
|
|
111
|
+
ts,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get the current validator set
|
|
117
|
+
*
|
|
118
|
+
* @param nextSlot - If true, get the validator set for the next slot.
|
|
119
|
+
* @returns The current validator set.
|
|
120
|
+
*/
|
|
121
|
+
async getCommittee(nextSlot: boolean = false): Promise<EthAddress[]> {
|
|
122
|
+
// If the current epoch has changed, then we need to make a request to update the validator set
|
|
123
|
+
const { epoch: calculatedEpoch, ts } = nextSlot ? this.getEpochAndSlotInNextSlot() : this.getEpochAndSlotNow();
|
|
124
|
+
|
|
125
|
+
if (calculatedEpoch !== this.cachedEpoch) {
|
|
126
|
+
this.log.debug(`Updating validator set for new epoch ${calculatedEpoch}`, {
|
|
127
|
+
epoch: calculatedEpoch,
|
|
128
|
+
previousEpoch: this.cachedEpoch,
|
|
129
|
+
});
|
|
130
|
+
const [committeeAtTs, sampleSeedAtTs] = await Promise.all([
|
|
131
|
+
this.rollup.getCommitteeAt(ts),
|
|
132
|
+
this.rollup.getSampleSeedAt(ts),
|
|
133
|
+
]);
|
|
134
|
+
this.committee = committeeAtTs.map((v: `0x${string}`) => EthAddress.fromString(v));
|
|
135
|
+
this.cachedEpoch = calculatedEpoch;
|
|
136
|
+
this.cachedSampleSeed = sampleSeedAtTs;
|
|
137
|
+
this.log.debug(`Updated validator set for epoch ${calculatedEpoch}`, { commitee: this.committee });
|
|
138
|
+
this.emit('committeeChanged', this.committee, calculatedEpoch);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return this.committee;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get the ABI encoding of the proposer index - see ValidatorSelectionLib.sol computeProposerIndex
|
|
146
|
+
*/
|
|
147
|
+
getProposerIndexEncoding(epoch: bigint, slot: bigint, seed: bigint): `0x${string}` {
|
|
148
|
+
return encodeAbiParameters(
|
|
149
|
+
[
|
|
150
|
+
{ type: 'uint256', name: 'epoch' },
|
|
151
|
+
{ type: 'uint256', name: 'slot' },
|
|
152
|
+
{ type: 'uint256', name: 'seed' },
|
|
153
|
+
],
|
|
154
|
+
[epoch, slot, seed],
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
computeProposerIndex(slot: bigint, epoch: bigint, seed: bigint, size: bigint): bigint {
|
|
159
|
+
return BigInt(keccak256(this.getProposerIndexEncoding(epoch, slot, seed))) % size;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Returns the current and next proposer
|
|
164
|
+
*
|
|
165
|
+
* We return the next proposer as the node will check if it is the proposer at the next ethereum block, which
|
|
166
|
+
* can be the next slot. If this is the case, then it will send proposals early.
|
|
167
|
+
*
|
|
168
|
+
* If we are at an epoch boundary, then we can update the cache for the next epoch, this is the last check
|
|
169
|
+
* we do in the validator client, so we can update the cache here.
|
|
170
|
+
*/
|
|
171
|
+
async getProposerInCurrentOrNextSlot(): Promise<{
|
|
172
|
+
currentProposer: EthAddress;
|
|
173
|
+
nextProposer: EthAddress;
|
|
174
|
+
currentSlot: bigint;
|
|
175
|
+
nextSlot: bigint;
|
|
176
|
+
}> {
|
|
177
|
+
// Validators are sorted by their index in the committee, and getValidatorSet will cache
|
|
178
|
+
const committee = await this.getCommittee();
|
|
179
|
+
const { slot: currentSlot, epoch: currentEpoch } = this.getEpochAndSlotNow();
|
|
180
|
+
const { slot: nextSlot, epoch: nextEpoch } = this.getEpochAndSlotInNextSlot();
|
|
181
|
+
|
|
182
|
+
// Compute the proposer in this and the next slot
|
|
183
|
+
const proposerIndex = this.computeProposerIndex(
|
|
184
|
+
currentSlot,
|
|
185
|
+
this.cachedEpoch,
|
|
186
|
+
this.cachedSampleSeed,
|
|
187
|
+
BigInt(committee.length),
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
// Check if the next proposer is in the next epoch
|
|
191
|
+
if (nextEpoch !== currentEpoch) {
|
|
192
|
+
await this.getCommittee(/*next slot*/ true);
|
|
193
|
+
}
|
|
194
|
+
const nextProposerIndex = this.computeProposerIndex(
|
|
195
|
+
nextSlot,
|
|
196
|
+
this.cachedEpoch,
|
|
197
|
+
this.cachedSampleSeed,
|
|
198
|
+
BigInt(committee.length),
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const currentProposer = committee[Number(proposerIndex)];
|
|
202
|
+
const nextProposer = committee[Number(nextProposerIndex)];
|
|
203
|
+
|
|
204
|
+
return { currentProposer, nextProposer, currentSlot, nextSlot };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Check if a validator is in the current epoch's committee
|
|
209
|
+
*/
|
|
210
|
+
async isInCommittee(validator: EthAddress): Promise<boolean> {
|
|
211
|
+
const committee = await this.getCommittee();
|
|
212
|
+
return committee.some(v => v.equals(validator));
|
|
213
|
+
}
|
|
214
|
+
}
|
package/src/index.ts
ADDED
|
File without changes
|