@lodestar/validator 1.35.0-rc.0 → 1.35.0-rc.1

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.
@@ -1,5 +1,5 @@
1
1
  import {ApiClient, routes} from "@lodestar/api";
2
- import {ChainConfig} from "@lodestar/config";
2
+ import {ChainForkConfig} from "@lodestar/config";
3
3
  import {computeEpochAtSlot, computeStartSlotAtEpoch} from "@lodestar/state-transition";
4
4
  import {BLSPubkey, Epoch, RootHex, Slot} from "@lodestar/types";
5
5
  import {sleep, toPubkeyHex} from "@lodestar/utils";
@@ -9,10 +9,11 @@ import {IClock, LoggerVc, differenceHex} from "../util/index.js";
9
9
  import {ValidatorStore} from "./validatorStore.js";
10
10
 
11
11
  /** This polls block duties 1s before the next epoch */
12
- // TODO: change to 6 to do it 2s before the next epoch
12
+ // TODO: change to 8333 (5/6 of slot) to do it 2s before the next epoch
13
13
  // once we have some improvement on epoch transition time
14
14
  // see https://github.com/ChainSafe/lodestar/issues/5792#issuecomment-1647457442
15
- const BLOCK_DUTIES_LOOKAHEAD_FACTOR = 12;
15
+ // TODO GLOAS: re-evaluate timing
16
+ const BLOCK_DUTIES_LOOKAHEAD_BPS = 9167;
16
17
  /** Only retain `HISTORICAL_DUTIES_EPOCHS` duties prior to the current epoch */
17
18
  const HISTORICAL_DUTIES_EPOCHS = 2;
18
19
  // Re-declaring to not have to depend on `lodestar-params` just for this 0
@@ -30,7 +31,7 @@ export class BlockDutiesService {
30
31
  private readonly proposers = new Map<Epoch, BlockDutyAtEpoch>();
31
32
 
32
33
  constructor(
33
- private readonly config: ChainConfig,
34
+ private readonly config: ChainForkConfig,
34
35
  private readonly logger: LoggerVc,
35
36
  private readonly api: ApiClient,
36
37
  private readonly clock: IClock,
@@ -169,7 +170,8 @@ export class BlockDutiesService {
169
170
  */
170
171
  private async pollBeaconProposersNextEpoch(currentSlot: Slot, nextEpoch: Epoch, signal: AbortSignal): Promise<void> {
171
172
  const nextSlot = currentSlot + 1;
172
- const lookAheadMs = (this.config.SECONDS_PER_SLOT * 1000) / BLOCK_DUTIES_LOOKAHEAD_FACTOR;
173
+ const lookAheadMs =
174
+ this.config.SLOT_DURATION_MS - this.config.getSlotComponentDurationMs(BLOCK_DUTIES_LOOKAHEAD_BPS);
173
175
  await sleep(this.clock.msToSlot(nextSlot) - lookAheadMs, signal);
174
176
  this.logger.debug("Polling proposers for next epoch", {nextEpoch, nextSlot});
175
177
  // Poll proposers for the next epoch
@@ -1,6 +1,7 @@
1
1
  import {ApiClient, routes} from "@lodestar/api";
2
2
  import {ChainForkConfig} from "@lodestar/config";
3
- import {computeEpochAtSlot, isSyncCommitteeAggregator} from "@lodestar/state-transition";
3
+ import {ForkName, isForkPostAltair} from "@lodestar/params";
4
+ import {isSyncCommitteeAggregator} from "@lodestar/state-transition";
4
5
  import {BLSSignature, CommitteeIndex, Root, Slot, altair} from "@lodestar/types";
5
6
  import {sleep} from "@lodestar/utils";
6
7
  import {Metrics} from "../metrics.js";
@@ -58,9 +59,11 @@ export class SyncCommitteeService {
58
59
  }
59
60
 
60
61
  private runSyncCommitteeTasks = async (slot: Slot, signal: AbortSignal): Promise<void> => {
62
+ const fork = this.config.getForkName(slot);
63
+
61
64
  try {
62
65
  // Before altair fork no need to check duties
63
- if (computeEpochAtSlot(slot) < this.config.ALTAIR_FORK_EPOCH) {
66
+ if (!isForkPostAltair(fork)) {
64
67
  return;
65
68
  }
66
69
 
@@ -77,25 +80,32 @@ export class SyncCommitteeService {
77
80
  // This will run in parallel to other sync committee tasks but must be finished before starting
78
81
  // sync committee contributions as it is required to correctly determine if validator is aggregator
79
82
  // and to produce a ContributionAndProof that can be threshold aggregated by the middleware client.
80
- this.runDistributedAggregationSelectionTasks(dutiesAtSlot, slot, signal).catch((e) =>
83
+ this.runDistributedAggregationSelectionTasks(fork, dutiesAtSlot, slot, signal).catch((e) =>
81
84
  this.logger.error("Error on sync committee aggregation selection", {slot}, e)
82
85
  );
83
86
  }
84
87
 
85
88
  // unlike Attestation, SyncCommitteeSignature could be published asap
86
- // especially with lodestar, it's very busy at 1/3 of slot
89
+ // especially with lodestar, it's very busy at ATTESTATION_DUE_BPS of the slot
87
90
  // see https://github.com/ChainSafe/lodestar/issues/4608
88
- await Promise.race([sleep(this.clock.msToSlot(slot + 1 / 3), signal), this.emitter.waitForBlockSlot(slot)]);
89
- this.metrics?.syncCommitteeStepCallProduceMessage.observe(this.clock.secFromSlot(slot + 1 / 3));
91
+ const syncMessageDueMs = this.config.getSyncMessageDueMs(fork);
92
+ await Promise.race([
93
+ sleep(syncMessageDueMs - this.clock.msFromSlot(slot), signal),
94
+ this.emitter.waitForBlockSlot(slot),
95
+ ]);
96
+ this.metrics?.syncCommitteeStepCallProduceMessage.observe(this.clock.secFromSlot(slot) - syncMessageDueMs / 1000);
90
97
 
91
98
  // Step 1. Download, sign and publish an `SyncCommitteeMessage` for each validator.
92
99
  // Differs from AttestationService, `SyncCommitteeMessage` are equal for all
93
- const beaconBlockRoot = await this.produceAndPublishSyncCommittees(slot, dutiesAtSlot);
100
+ const beaconBlockRoot = await this.produceAndPublishSyncCommittees(fork, slot, dutiesAtSlot);
94
101
 
95
102
  // Step 2. If an attestation was produced, make an aggregate.
96
- // First, wait until the `aggregation_production_instant` (2/3rds of the way though the slot)
97
- await sleep(this.clock.msToSlot(slot + 2 / 3), signal);
98
- this.metrics?.syncCommitteeStepCallProduceAggregate.observe(this.clock.secFromSlot(slot + 2 / 3));
103
+ // First, wait until the `CONTRIBUTION_DUE_BPS` of the slot
104
+ const syncContributionDueMs = this.config.getSyncContributionDueMs(fork);
105
+ await sleep(syncContributionDueMs - this.clock.msFromSlot(slot), signal);
106
+ this.metrics?.syncCommitteeStepCallProduceAggregate.observe(
107
+ this.clock.secFromSlot(slot) - syncContributionDueMs / 1000
108
+ );
99
109
 
100
110
  // await for all so if the Beacon node is overloaded it auto-throttles
101
111
  // TODO: This approach is conservative to reduce the node's load, review
@@ -105,9 +115,11 @@ export class SyncCommitteeService {
105
115
  if (duties.length === 0) return;
106
116
  // Then download, sign and publish a `SignedAggregateAndProof` for each
107
117
  // validator that is elected to aggregate for this `slot` and `subcommitteeIndex`.
108
- await this.produceAndPublishAggregates(slot, subcommitteeIndex, beaconBlockRoot, duties).catch((e: Error) => {
109
- this.logger.error("Error on SyncCommitteeContribution", {slot, index: subcommitteeIndex}, e);
110
- });
118
+ await this.produceAndPublishAggregates(fork, slot, subcommitteeIndex, beaconBlockRoot, duties).catch(
119
+ (e: Error) => {
120
+ this.logger.error("Error on SyncCommitteeContribution", {slot, index: subcommitteeIndex}, e);
121
+ }
122
+ );
111
123
  })
112
124
  );
113
125
  } catch (e) {
@@ -124,7 +136,11 @@ export class SyncCommitteeService {
124
136
  * Only one `SyncCommittee` is downloaded from the BN. It is then signed by each
125
137
  * validator and the list of individually-signed `SyncCommittee` objects is returned to the BN.
126
138
  */
127
- private async produceAndPublishSyncCommittees(slot: Slot, duties: SyncDutyAndProofs[]): Promise<Root> {
139
+ private async produceAndPublishSyncCommittees(
140
+ fork: ForkName,
141
+ slot: Slot,
142
+ duties: SyncDutyAndProofs[]
143
+ ): Promise<Root> {
128
144
  const logCtx = {slot};
129
145
 
130
146
  // /eth/v1/beacon/blocks/:blockId/root -> at slot -1
@@ -156,14 +172,15 @@ export class SyncCommitteeService {
156
172
  // by default we want to submit SyncCommitteeSignature asap after we receive block
157
173
  // provide a delay option just in case any client implementation validate the existence of block in
158
174
  // SyncCommitteeSignature gossip validation.
159
- const msToOneThirdSlot = this.clock.msToSlot(slot + 1 / 3);
175
+ const syncMessageDueMs = this.config.getSyncMessageDueMs(fork);
176
+ const msToCutoffTime = syncMessageDueMs - this.clock.msFromSlot(slot);
160
177
  const afterBlockDelayMs = 1000 * this.clock.secondsPerSlot * (this.opts?.scAfterBlockDelaySlotFraction ?? 0);
161
- const toDelayMs = Math.min(msToOneThirdSlot, afterBlockDelayMs);
178
+ const toDelayMs = Math.min(msToCutoffTime, afterBlockDelayMs);
162
179
  if (toDelayMs > 0) {
163
180
  await sleep(toDelayMs);
164
181
  }
165
182
 
166
- this.metrics?.syncCommitteeStepCallPublishMessage.observe(this.clock.secFromSlot(slot + 1 / 3));
183
+ this.metrics?.syncCommitteeStepCallPublishMessage.observe(this.clock.secFromSlot(slot) - syncMessageDueMs / 1000);
167
184
 
168
185
  if (signatures.length > 0) {
169
186
  try {
@@ -189,6 +206,7 @@ export class SyncCommitteeService {
189
206
  * returned to the BN.
190
207
  */
191
208
  private async produceAndPublishAggregates(
209
+ fork: ForkName,
192
210
  slot: Slot,
193
211
  subcommitteeIndex: CommitteeIndex,
194
212
  beaconBlockRoot: Root,
@@ -223,7 +241,9 @@ export class SyncCommitteeService {
223
241
  })
224
242
  );
225
243
 
226
- this.metrics?.syncCommitteeStepCallPublishAggregate.observe(this.clock.secFromSlot(slot + 2 / 3));
244
+ this.metrics?.syncCommitteeStepCallPublishAggregate.observe(
245
+ this.clock.secFromSlot(slot) - this.config.getSyncContributionDueMs(fork) / 1000
246
+ );
227
247
 
228
248
  if (signedContributions.length > 0) {
229
249
  try {
@@ -248,6 +268,7 @@ export class SyncCommitteeService {
248
268
  * See https://docs.google.com/document/d/1q9jOTPcYQa-3L8luRvQJ-M0eegtba4Nmon3dpO79TMk/mobilebasic
249
269
  */
250
270
  private async runDistributedAggregationSelectionTasks(
271
+ fork: ForkName,
251
272
  duties: SyncDutyAndProofs[],
252
273
  slot: number,
253
274
  signal: AbortSignal
@@ -270,18 +291,18 @@ export class SyncCommitteeService {
270
291
 
271
292
  const res = await Promise.race([
272
293
  this.api.validator.submitSyncCommitteeSelections({selections: partialSelections}),
273
- // Exit sync committee contributions flow if there is no response after 2/3 of slot.
274
- // This is in contrast to attestations aggregations flow which is already exited at 1/3 of the slot
294
+ // Exit sync committee contributions flow if there is no response after CONTRIBUTION_DUE_BPS of the slot.
295
+ // This is in contrast to attestations aggregations flow which is already exited at ATTESTATION_DUE_BPS of the slot
275
296
  // because for sync committee is not required to resubscribe to subnets as beacon node will assume
276
297
  // validator always aggregates. This allows us to wait until we have to produce sync committee contributions.
277
298
  // Note that the sync committee contributions flow is not explicitly exited but rather will be skipped
278
299
  // due to the fact that calculation of `is_sync_committee_aggregator` in SyncCommitteeDutiesService is not done
279
300
  // and selectionProof is set to null, meaning no validator will be considered an aggregator.
280
- sleep(this.clock.msToSlot(slot + 2 / 3), signal),
301
+ sleep(this.config.getSyncContributionDueMs(fork) - this.clock.msFromSlot(slot), signal),
281
302
  ]);
282
303
 
283
304
  if (!res) {
284
- throw new Error("Failed to receive combined selection proofs before 2/3 of slot");
305
+ throw new Error("Failed to receive combined selection proofs before CONTRIBUTION_DUE_BPS of the slot");
285
306
  }
286
307
 
287
308
  const combinedSelections = res.value();
package/src/util/clock.ts CHANGED
@@ -16,6 +16,7 @@ export interface IClock {
16
16
  runEverySlot(fn: (slot: Slot, signal: AbortSignal) => Promise<void>): void;
17
17
  runEveryEpoch(fn: (epoch: Epoch, signal: AbortSignal) => Promise<void>): void;
18
18
  msToSlot(slot: Slot): number;
19
+ msFromSlot(slot: Slot): number;
19
20
  secFromSlot(slot: Slot): number;
20
21
  getCurrentSlot(): Slot;
21
22
  getCurrentEpoch(): Epoch;
@@ -76,6 +77,11 @@ export class Clock implements IClock {
76
77
  return timeAt * 1000 - Date.now();
77
78
  }
78
79
 
80
+ /** Milliseconds elapsed from a specific slot to now */
81
+ msFromSlot(slot: Slot): number {
82
+ return Date.now() - (this.genesisTime * 1000 + this.config.SLOT_DURATION_MS * slot);
83
+ }
84
+
79
85
  /** Seconds elapsed from a specific slot to now */
80
86
  secFromSlot(slot: Slot): number {
81
87
  return Date.now() / 1000 - (this.genesisTime + this.config.SECONDS_PER_SLOT * slot);
@@ -146,11 +146,18 @@ function getSpecCriticalParams(localConfig: ChainConfig): Record<keyof ConfigWit
146
146
  GLOAS_FORK_EPOCH: gloasForkRelevant,
147
147
 
148
148
  // Time parameters
149
- SECONDS_PER_SLOT: true,
149
+ SECONDS_PER_SLOT: true, // Deprecated
150
+ SLOT_DURATION_MS: true,
150
151
  SECONDS_PER_ETH1_BLOCK: false, // Legacy
151
152
  MIN_VALIDATOR_WITHDRAWABILITY_DELAY: true,
152
153
  SHARD_COMMITTEE_PERIOD: true,
153
154
  ETH1_FOLLOW_DISTANCE: true,
155
+ PROPOSER_REORG_CUTOFF_BPS: true,
156
+ ATTESTATION_DUE_BPS: true,
157
+ AGGREGATE_DUE_BPS: true,
158
+ // Altair
159
+ SYNC_MESSAGE_DUE_BPS: altairForkRelevant,
160
+ CONTRIBUTION_DUE_BPS: altairForkRelevant,
154
161
 
155
162
  // Validator cycle
156
163
  INACTIVITY_SCORE_BIAS: true,