@lodestar/beacon-node 1.47.0-dev.0bcaaaebb5 → 1.47.0-dev.16cb8ec36c

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.
Files changed (96) hide show
  1. package/lib/api/impl/beacon/blocks/index.d.ts.map +1 -1
  2. package/lib/api/impl/beacon/blocks/index.js +3 -6
  3. package/lib/api/impl/beacon/blocks/index.js.map +1 -1
  4. package/lib/api/impl/beacon/pool/index.d.ts.map +1 -1
  5. package/lib/api/impl/beacon/pool/index.js +4 -0
  6. package/lib/api/impl/beacon/pool/index.js.map +1 -1
  7. package/lib/api/impl/events/index.d.ts +1 -1
  8. package/lib/api/impl/events/index.d.ts.map +1 -1
  9. package/lib/api/impl/events/index.js +10 -3
  10. package/lib/api/impl/events/index.js.map +1 -1
  11. package/lib/api/impl/validator/index.js +1 -1
  12. package/lib/api/impl/validator/index.js.map +1 -1
  13. package/lib/chain/archiveStore/historicalState/metrics.d.ts.map +1 -1
  14. package/lib/chain/archiveStore/historicalState/metrics.js +21 -0
  15. package/lib/chain/archiveStore/historicalState/metrics.js.map +1 -1
  16. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  17. package/lib/chain/blocks/importBlock.js +2 -7
  18. package/lib/chain/blocks/importBlock.js.map +1 -1
  19. package/lib/chain/builderCircuitBreaker.d.ts +6 -5
  20. package/lib/chain/builderCircuitBreaker.d.ts.map +1 -1
  21. package/lib/chain/builderCircuitBreaker.js +28 -15
  22. package/lib/chain/builderCircuitBreaker.js.map +1 -1
  23. package/lib/chain/chain.js +1 -1
  24. package/lib/chain/chain.js.map +1 -1
  25. package/lib/chain/errors/proposerPreferences.d.ts +6 -0
  26. package/lib/chain/errors/proposerPreferences.d.ts.map +1 -1
  27. package/lib/chain/errors/proposerPreferences.js +1 -0
  28. package/lib/chain/errors/proposerPreferences.js.map +1 -1
  29. package/lib/chain/opPools/payloadAttestationPool.js +2 -2
  30. package/lib/chain/opPools/payloadAttestationPool.js.map +1 -1
  31. package/lib/chain/options.d.ts +1 -1
  32. package/lib/chain/options.d.ts.map +1 -1
  33. package/lib/chain/prepareNextSlot.js +1 -1
  34. package/lib/chain/prepareNextSlot.js.map +1 -1
  35. package/lib/chain/seenCache/seenAttesters.d.ts +9 -5
  36. package/lib/chain/seenCache/seenAttesters.d.ts.map +1 -1
  37. package/lib/chain/seenCache/seenAttesters.js +25 -4
  38. package/lib/chain/seenCache/seenAttesters.js.map +1 -1
  39. package/lib/chain/validation/payloadAttestationMessage.d.ts.map +1 -1
  40. package/lib/chain/validation/payloadAttestationMessage.js +4 -6
  41. package/lib/chain/validation/payloadAttestationMessage.js.map +1 -1
  42. package/lib/chain/validation/proposerPreferences.d.ts +1 -1
  43. package/lib/chain/validation/proposerPreferences.d.ts.map +1 -1
  44. package/lib/chain/validation/proposerPreferences.js +11 -2
  45. package/lib/chain/validation/proposerPreferences.js.map +1 -1
  46. package/lib/execution/builder/http.d.ts +10 -7
  47. package/lib/execution/builder/http.d.ts.map +1 -1
  48. package/lib/execution/builder/http.js +11 -8
  49. package/lib/execution/builder/http.js.map +1 -1
  50. package/lib/metrics/metrics/beacon.d.ts +3 -3
  51. package/lib/metrics/metrics/beacon.d.ts.map +1 -1
  52. package/lib/metrics/metrics/beacon.js +9 -9
  53. package/lib/metrics/metrics/beacon.js.map +1 -1
  54. package/lib/metrics/metrics/lodestar.d.ts +10 -0
  55. package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
  56. package/lib/metrics/metrics/lodestar.js +16 -0
  57. package/lib/metrics/metrics/lodestar.js.map +1 -1
  58. package/lib/network/libp2p/index.d.ts.map +1 -1
  59. package/lib/network/libp2p/index.js +10 -2
  60. package/lib/network/libp2p/index.js.map +1 -1
  61. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  62. package/lib/network/processor/gossipHandlers.js +7 -2
  63. package/lib/network/processor/gossipHandlers.js.map +1 -1
  64. package/lib/network/reqresp/score.d.ts.map +1 -1
  65. package/lib/network/reqresp/score.js +5 -11
  66. package/lib/network/reqresp/score.js.map +1 -1
  67. package/lib/sync/types.d.ts.map +1 -1
  68. package/lib/sync/types.js.map +1 -1
  69. package/lib/sync/unknownBlock.d.ts +9 -0
  70. package/lib/sync/unknownBlock.d.ts.map +1 -1
  71. package/lib/sync/unknownBlock.js +110 -14
  72. package/lib/sync/unknownBlock.js.map +1 -1
  73. package/package.json +14 -14
  74. package/src/api/impl/beacon/blocks/index.ts +4 -6
  75. package/src/api/impl/beacon/pool/index.ts +5 -0
  76. package/src/api/impl/events/index.ts +10 -4
  77. package/src/api/impl/validator/index.ts +1 -1
  78. package/src/chain/archiveStore/historicalState/metrics.ts +22 -0
  79. package/src/chain/blocks/importBlock.ts +1 -6
  80. package/src/chain/builderCircuitBreaker.ts +31 -17
  81. package/src/chain/chain.ts +1 -1
  82. package/src/chain/errors/proposerPreferences.ts +7 -0
  83. package/src/chain/opPools/payloadAttestationPool.ts +2 -2
  84. package/src/chain/options.ts +1 -1
  85. package/src/chain/prepareNextSlot.ts +1 -1
  86. package/src/chain/seenCache/seenAttesters.ts +31 -5
  87. package/src/chain/validation/payloadAttestationMessage.ts +3 -6
  88. package/src/chain/validation/proposerPreferences.ts +12 -2
  89. package/src/execution/builder/http.ts +11 -11
  90. package/src/metrics/metrics/beacon.ts +9 -9
  91. package/src/metrics/metrics/lodestar.ts +16 -0
  92. package/src/network/libp2p/index.ts +11 -2
  93. package/src/network/processor/gossipHandlers.ts +9 -0
  94. package/src/network/reqresp/score.ts +5 -11
  95. package/src/sync/types.ts +3 -1
  96. package/src/sync/unknownBlock.ts +126 -17
@@ -4,7 +4,8 @@ import {ApiModules} from "../types.js";
4
4
 
5
5
  export function getEventsApi({
6
6
  chain,
7
- }: Pick<ApiModules, "chain" | "config">): ApplicationMethods<routes.events.Endpoints> {
7
+ logger,
8
+ }: Pick<ApiModules, "chain" | "config" | "logger">): ApplicationMethods<routes.events.Endpoints> {
8
9
  return {
9
10
  async eventstream({topics, signal, onEvent}) {
10
11
  const onAbortFns: (() => void)[] = [];
@@ -12,9 +13,14 @@ export function getEventsApi({
12
13
  for (const topic of topics) {
13
14
  // biome-ignore lint/suspicious/noExplicitAny: We need to use `any` type here
14
15
  const handler = (data: any): void => {
15
- // TODO: What happens if this handler throws? Does it break the other chain.emitter listeners?
16
-
17
- onEvent({type: topic, message: data});
16
+ try {
17
+ onEvent({type: topic, message: data});
18
+ } catch (e) {
19
+ // `chain.emitter` emits synchronously, a throwing listener would propagate the error to
20
+ // whoever emitted the event, e.g. a gossip handler, and skip the remaining listeners.
21
+ // A single misbehaving event or client must not affect the node or other subscribers.
22
+ logger.warn("Error sending event to client", {topic}, e as Error);
23
+ }
18
24
  };
19
25
 
20
26
  chain.emitter.on(topic, handler);
@@ -895,7 +895,7 @@ export function getValidatorApi(
895
895
  // TODO GLOAS: add external builder api support when it is implemented
896
896
  const isBuildingOnFull = chain.forkChoice.shouldBuildOnFull(parentBlock, slot);
897
897
  const bidParentBlockHash = isBuildingOnFull ? parentBlock.executionPayloadBlockHash : parentBlock.parentBlockHash;
898
- const circuitBreakerActive = chain.builderCircuitBreaker.isActive(slot);
898
+ const circuitBreakerActive = chain.builderCircuitBreaker.isActive(slot, parentBlock);
899
899
  // Keep a builder bid as fallback unless the circuit breaker is active
900
900
  const builderBid = circuitBreakerActive
901
901
  ? null
@@ -1,3 +1,4 @@
1
+ import {ForkName} from "@lodestar/params";
1
2
  import {
2
3
  BeaconStateTransitionMetrics,
3
4
  EpochTransitionStep,
@@ -45,6 +46,27 @@ export function createHistoricalStateTransitionMetrics(
45
46
  labelNames: ["step"],
46
47
  buckets: [0.01, 0.05, 0.1, 0.2, 0.5, 0.75, 1],
47
48
  }),
49
+ forkUpgradeTime: metricsRegister.histogram<{fork: ForkName}>({
50
+ name: "lodestar_historical_state_stfn_fork_upgrade_seconds",
51
+ help: "Time to upgrade the state at a fork boundary in seconds",
52
+ labelNames: ["fork"],
53
+ buckets: [0.05, 0.1, 0.25, 0.5, 1, 2, 5, 10, 30],
54
+ }),
55
+ onboardBuildersTime: metricsRegister.histogram({
56
+ name: "lodestar_historical_state_stfn_gloas_onboard_builders_seconds",
57
+ help: "Time spent in onboardBuildersFromPendingDeposits at the gloas fork transition",
58
+ buckets: [0.05, 0.1, 0.25, 0.5, 1, 2, 5, 10, 30],
59
+ }),
60
+ onboardBuildersDeposits: metricsRegister.gauge<{outcome: "onboarded" | "topup" | "kept" | "dropped"}>({
61
+ name: "lodestar_historical_state_stfn_gloas_onboard_builders_deposits",
62
+ help: "Pending deposits handled at the gloas fork transition, by outcome",
63
+ labelNames: ["outcome"],
64
+ }),
65
+ onboardBuildersSignatureChecks: metricsRegister.gauge<{source: "cache" | "verified"}>({
66
+ name: "lodestar_historical_state_stfn_gloas_onboard_builders_signature_checks",
67
+ help: "Builder deposit signature checks at the gloas fork transition, by whether the pre-verify cache served them",
68
+ labelNames: ["source"],
69
+ }),
48
70
  processBlockTime: metricsRegister.histogram({
49
71
  name: "lodestar_historical_state_stfn_process_block_seconds",
50
72
  help: "Time to process a single block in seconds",
@@ -22,7 +22,6 @@ import {
22
22
  RootCache,
23
23
  computeEpochAtSlot,
24
24
  computeTimeAtSlot,
25
- isStartSlotOfEpoch,
26
25
  isStatePostAltair,
27
26
  isStatePostBellatrix,
28
27
  } from "@lodestar/state-transition";
@@ -404,11 +403,7 @@ export async function importBlock(
404
403
  notOverrideFcuReason = NotReorgedReason.NotProposerOfNextSlot;
405
404
  }
406
405
  } catch (e) {
407
- if (isStartSlotOfEpoch(proposalSlot)) {
408
- notOverrideFcuReason = NotReorgedReason.AtEpochBoundary;
409
- } else {
410
- this.logger.warn("Unable to get beacon proposer. Do not override fcu.", {proposalSlot}, e as Error);
411
- }
406
+ this.logger.warn("Unable to get beacon proposer. Do not override fcu.", {proposalSlot}, e as Error);
412
407
  }
413
408
 
414
409
  if (shouldOverrideFcu) {
@@ -1,4 +1,4 @@
1
- import {IForkChoice} from "@lodestar/fork-choice";
1
+ import {IForkChoice, ProtoBlock} from "@lodestar/fork-choice";
2
2
  import {Slot} from "@lodestar/types";
3
3
  import {Logger} from "@lodestar/utils";
4
4
  import {getFaultInspectionParams} from "../execution/builder/http.js";
@@ -15,11 +15,15 @@ export type BuilderCircuitBreakerModules = {
15
15
  metrics: Metrics | null;
16
16
  };
17
17
 
18
+ /** Four observations is the minimum useful recovery sample for the default ~25% fault budget */
19
+ const MIN_BLOCKS_TO_DEACTIVATE = 4;
20
+
18
21
  /**
19
22
  * Post-gloas circuit breaker for builder bids. The beacon block is produced by the proposer
20
23
  * regardless of bid source, so missed blocks are not a useful builder health signal. Instead
21
- * count blocks whose payload was never revealed and stop selecting builder bids while the
22
- * non-reveal rate in the fault inspection window is too high.
24
+ * count canonical blocks selected as EMPTY. Activate when the proportion of EMPTY blocks exceeds
25
+ * the fault budget, and resume selecting builder bids only when the observed blocks are within
26
+ * budget and meet the minimum recovery sample size.
23
27
  */
24
28
  export class BuilderCircuitBreaker {
25
29
  readonly faultInspectionWindow: number;
@@ -35,39 +39,49 @@ export class BuilderCircuitBreaker {
35
39
  const {faultInspectionWindow, allowedFaults} = getFaultInspectionParams(opts);
36
40
  this.faultInspectionWindow = faultInspectionWindow;
37
41
  this.allowedFaults = allowedFaults;
42
+ this.modules.logger.info("Builder circuit breaker initialized", {faultInspectionWindow, allowedFaults});
38
43
  }
39
44
 
40
45
  /** Whether builder bids must be ignored for a block produced at clockSlot */
41
- isActive(clockSlot: Slot): boolean {
42
- this.update(clockSlot);
46
+ isActive(clockSlot: Slot, head: ProtoBlock): boolean {
47
+ this.update(clockSlot, head);
43
48
  return this.active;
44
49
  }
45
50
 
46
- update(clockSlot: Slot): void {
51
+ update(clockSlot: Slot, head: ProtoBlock): void {
47
52
  if (clockSlot <= this.lastUpdatedSlot) {
48
53
  return;
49
54
  }
50
55
  this.lastUpdatedSlot = clockSlot;
51
56
 
52
- // Exclude clockSlot itself, its payload reveal may still be in flight
53
- const {blocksPresent, payloadsRevealed} = this.modules.forkChoice.getPayloadRevealCounts(
57
+ // Exclude clockSlot itself, its payload status may still be unresolved
58
+ const {full, empty} = this.modules.forkChoice.getCanonicalPayloadCounts(
54
59
  Math.max(clockSlot - this.faultInspectionWindow, 0),
55
- clockSlot - 1
60
+ clockSlot - 1,
61
+ head
56
62
  );
57
- const faults = blocksPresent - payloadsRevealed;
63
+ const canonicalBlocks = full + empty;
58
64
 
59
65
  const wasActive = this.active;
60
- // Scale the fault budget by blocks present so sparse windows still trigger on high non-reveal rates
61
- this.active = faults * this.faultInspectionWindow > this.allowedFaults * blocksPresent;
66
+ // Scale the fault budget by canonical blocks so sparse windows still trigger on high EMPTY rates
67
+ const exceedsFaultBudget = empty * this.faultInspectionWindow > this.allowedFaults * canonicalBlocks;
68
+ if (exceedsFaultBudget) {
69
+ this.active = true;
70
+ } else if (canonicalBlocks >= MIN_BLOCKS_TO_DEACTIVATE) {
71
+ // Require a minimum sample within the fault budget before accepting builder bids again
72
+ this.active = false;
73
+ }
62
74
 
63
75
  this.modules.metrics?.builderCircuitBreaker.active.set(this.active ? 1 : 0);
64
- this.modules.metrics?.builderCircuitBreaker.faults.set(faults);
65
- this.modules.metrics?.builderCircuitBreaker.blocksPresent.set(blocksPresent);
66
- this.modules.metrics?.builderCircuitBreaker.payloadsRevealed.set(payloadsRevealed);
76
+ this.modules.metrics?.builderCircuitBreaker.canonicalBlocks.set(canonicalBlocks);
77
+ this.modules.metrics?.builderCircuitBreaker.fullBlocks.set(full);
78
+ this.modules.metrics?.builderCircuitBreaker.emptyBlocks.set(empty);
67
79
 
68
80
  const logCtx = {
69
- blocksPresent,
70
- faults,
81
+ clockSlot,
82
+ canonicalBlocks,
83
+ full,
84
+ empty,
71
85
  faultInspectionWindow: this.faultInspectionWindow,
72
86
  allowedFaults: this.allowedFaults,
73
87
  };
@@ -579,7 +579,7 @@ export class BeaconChain implements IBeaconChain {
579
579
  // seenAggregators = single aggregator index, not participants of the aggregate
580
580
  this.seenAggregators.isKnown(epoch, index) ||
581
581
  // seenPayloadAttesters = single signer of payload attestation message
582
- this.seenPayloadAttesters.isKnown(epoch, index) ||
582
+ this.seenPayloadAttesters.seenAtEpoch(epoch, index) ||
583
583
  // seenBlockProposers = single block proposer
584
584
  this.seenBlockProposers.seenAtEpoch(epoch, index)
585
585
  );
@@ -1,7 +1,9 @@
1
+ import {ForkName} from "@lodestar/params";
1
2
  import {Epoch, RootHex, Slot, ValidatorIndex} from "@lodestar/types";
2
3
  import {GossipActionError} from "./gossipValidation.js";
3
4
 
4
5
  export enum ProposerPreferencesErrorCode {
6
+ PRE_GLOAS_PROPOSAL_SLOT = "PROPOSER_PREFERENCES_ERROR_PRE_GLOAS_PROPOSAL_SLOT",
5
7
  INVALID_EPOCH = "PROPOSER_PREFERENCES_ERROR_INVALID_EPOCH",
6
8
  PROPOSAL_SLOT_PASSED = "PROPOSER_PREFERENCES_ERROR_PROPOSAL_SLOT_PASSED",
7
9
  UNKNOWN_DEPENDENT_ROOT = "PROPOSER_PREFERENCES_ERROR_UNKNOWN_DEPENDENT_ROOT",
@@ -13,6 +15,11 @@ export enum ProposerPreferencesErrorCode {
13
15
  }
14
16
 
15
17
  export type ProposerPreferencesErrorType =
18
+ | {
19
+ code: ProposerPreferencesErrorCode.PRE_GLOAS_PROPOSAL_SLOT;
20
+ proposalSlot: Slot;
21
+ proposalFork: ForkName;
22
+ }
16
23
  | {
17
24
  code: ProposerPreferencesErrorCode.INVALID_EPOCH;
18
25
  proposalSlot: Slot;
@@ -178,8 +178,8 @@ function aggregateMessageInto(
178
178
  validatorCommitteeIndices: number[],
179
179
  aggregate: AggregateFast
180
180
  ): InsertOutcome {
181
- // Gossip dedup via `seenPayloadAttesters` is keyed by (epoch, validatorIndex), so the same
182
- // validator's message is never processed twice all of its bits are set together or none.
181
+ // Gossip dedup via `seenPayloadAttesters` is keyed by (slot, validatorIndex), so the same
182
+ // validator's message is never processed twice for this aggregate's slot. All of its bits are set together or none.
183
183
  // Checking the first index is sufficient.
184
184
  if (aggregate.aggregationBits.get(validatorCommitteeIndices[0]) === true) {
185
185
  return InsertOutcome.AlreadyKnown;
@@ -53,7 +53,7 @@ export type IChainOptions = BlockProcessOpts &
53
53
  nativeStateView?: boolean;
54
54
  /** Builder circuit breaker fault inspection window in slots */
55
55
  faultInspectionWindow?: number;
56
- /** Allowed unrevealed payloads within the fault inspection window */
56
+ /** Canonical EMPTY blocks allowed per `faultInspectionWindow` observed blocks */
57
57
  allowedFaults?: number;
58
58
  };
59
59
 
@@ -169,7 +169,7 @@ export class PrepareNextSlotScheduler {
169
169
  }
170
170
 
171
171
  if (isForkPostGloas(fork)) {
172
- this.chain.builderCircuitBreaker.update(clockSlot);
172
+ this.chain.builderCircuitBreaker.update(clockSlot, updatedHead);
173
173
  } else {
174
174
  // Update the builder status, if enabled shoot an api call to check status
175
175
  this.chain.updateBuilderStatus(clockSlot);
@@ -1,4 +1,5 @@
1
- import {Epoch, ValidatorIndex} from "@lodestar/types";
1
+ import {computeEpochAtSlot} from "@lodestar/state-transition";
2
+ import {Epoch, Slot, ValidatorIndex} from "@lodestar/types";
2
3
  import {MapDef} from "@lodestar/utils";
3
4
 
4
5
  // How many *non future* epochs we intend to keep for SeenAttesters.
@@ -57,7 +58,32 @@ export class SeenAttesters {
57
58
  */
58
59
  export class SeenAggregators extends SeenAttesters {}
59
60
 
60
- /**
61
- * Keeps a cache to filter payload attestations from the same attesters in the same epoch
62
- */
63
- export class SeenPayloadAttesters extends SeenAttesters {}
61
+ /** Keeps a cache to filter payload attestations from the same validator in the same slot. */
62
+ export class SeenPayloadAttesters {
63
+ private readonly validatorIndexesBySlot = new MapDef<Slot, Set<ValidatorIndex>>(() => new Set<ValidatorIndex>());
64
+ private readonly validatorIndexesByEpoch = new SeenAttesters();
65
+
66
+ isKnown(slot: Slot, validatorIndex: ValidatorIndex): boolean {
67
+ return this.validatorIndexesBySlot.get(slot)?.has(validatorIndex) === true;
68
+ }
69
+
70
+ add(slot: Slot, validatorIndex: ValidatorIndex): void {
71
+ const epoch = computeEpochAtSlot(slot);
72
+ this.validatorIndexesByEpoch.add(epoch, validatorIndex);
73
+ this.validatorIndexesBySlot.getOrDefault(slot).add(validatorIndex);
74
+ }
75
+
76
+ seenAtEpoch(epoch: Epoch, validatorIndex: ValidatorIndex): boolean {
77
+ return this.validatorIndexesByEpoch.isKnown(epoch, validatorIndex);
78
+ }
79
+
80
+ prune(currentEpoch: Epoch): void {
81
+ const lowestPermissibleEpoch = Math.max(currentEpoch - EPOCH_LOOKBACK_LIMIT, 0);
82
+ for (const slot of this.validatorIndexesBySlot.keys()) {
83
+ if (computeEpochAtSlot(slot) < lowestPermissibleEpoch) {
84
+ this.validatorIndexesBySlot.delete(slot);
85
+ }
86
+ }
87
+ this.validatorIndexesByEpoch.prune(currentEpoch);
88
+ }
89
+ }
@@ -1,5 +1,4 @@
1
1
  import {
2
- computeEpochAtSlot,
3
2
  createSingleSignatureSetFromComponents,
4
3
  getPayloadAttestationDataSigningRoot,
5
4
  isStatePostGloas,
@@ -36,7 +35,6 @@ async function validatePayloadAttestationMessage(
36
35
  prioritizeBls = false
37
36
  ): Promise<PayloadAttestationValidationResult> {
38
37
  const {data, validatorIndex} = payloadAttestationMessage;
39
- const epoch = computeEpochAtSlot(data.slot);
40
38
 
41
39
  // [IGNORE] The message's slot is for the current slot (with a `MAXIMUM_GOSSIP_CLOCK_DISPARITY` allowance), i.e. `data.slot == current_slot`.
42
40
  if (!chain.clock.isCurrentSlotGivenGossipDisparity(data.slot)) {
@@ -48,9 +46,8 @@ async function validatePayloadAttestationMessage(
48
46
  }
49
47
 
50
48
  // [IGNORE] The `payload_attestation_message` is the first valid message received
51
- // from the validator with index `payload_attestation_message.validator_index`.
52
- // A single validator can participate PTC at most once per epoch
53
- if (chain.seenPayloadAttesters.isKnown(epoch, validatorIndex)) {
49
+ // for the slot from the validator with index `payload_attestation_message.validator_index`.
50
+ if (chain.seenPayloadAttesters.isKnown(data.slot, validatorIndex)) {
54
51
  throw new PayloadAttestationError(GossipAction.IGNORE, {
55
52
  code: PayloadAttestationErrorCode.PAYLOAD_ATTESTATION_ALREADY_KNOWN,
56
53
  validatorIndex,
@@ -135,7 +132,7 @@ async function validatePayloadAttestationMessage(
135
132
  }
136
133
 
137
134
  // Valid
138
- chain.seenPayloadAttesters.add(epoch, validatorIndex);
135
+ chain.seenPayloadAttesters.add(data.slot, validatorIndex);
139
136
 
140
137
  return {
141
138
  attDataRootHex: toRootHex(ssz.gloas.PayloadAttestationData.hashTreeRoot(data)),
@@ -1,5 +1,5 @@
1
1
  import {IForkChoice, ProtoBlock} from "@lodestar/fork-choice";
2
- import {GENESIS_EPOCH, GENESIS_SLOT, MIN_SEED_LOOKAHEAD, SLOTS_PER_EPOCH} from "@lodestar/params";
2
+ import {GENESIS_EPOCH, GENESIS_SLOT, MIN_SEED_LOOKAHEAD, SLOTS_PER_EPOCH, isForkPostGloas} from "@lodestar/params";
3
3
  import {
4
4
  computeEpochAtSlot,
5
5
  computeStartSlotAtEpoch,
@@ -13,7 +13,7 @@ import {IBeaconChain} from "../index.js";
13
13
 
14
14
  /**
15
15
  * Validates a gossiped `SignedProposerPreferences` per
16
- * https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.13/specs/gloas/p2p-interface.md#new-proposer_preferences
16
+ * https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.14/specs/gloas/p2p-interface.md#new-proposer_preferences
17
17
  */
18
18
  export async function validateGossipProposerPreferences(
19
19
  chain: IBeaconChain,
@@ -24,6 +24,16 @@ export async function validateGossipProposerPreferences(
24
24
  const dependentRootHex = toRootHex(dependentRoot);
25
25
  const proposalEpoch = computeEpochAtSlot(proposalSlot);
26
26
 
27
+ // [IGNORE] The proposal epoch is after the Gloas upgrade
28
+ const proposalFork = chain.config.getForkName(proposalSlot);
29
+ if (!isForkPostGloas(proposalFork)) {
30
+ throw new ProposerPreferencesError(GossipAction.IGNORE, {
31
+ code: ProposerPreferencesErrorCode.PRE_GLOAS_PROPOSAL_SLOT,
32
+ proposalSlot,
33
+ proposalFork,
34
+ });
35
+ }
36
+
27
37
  // [IGNORE] `preferences.proposal_slot` is within the proposer lookahead,
28
38
  // allowing for `MAXIMUM_GOSSIP_CLOCK_DISPARITY`.
29
39
  const currentEpoch = computeEpochAtSlot(chain.clock.currentSlotWithGossipDisparity);
@@ -71,15 +71,18 @@ export class NoBidReceived extends Error {
71
71
  }
72
72
 
73
73
  /**
74
- * Beacon clients select randomized values from the following ranges when initializing
75
- * the circuit breaker (so at boot time and once for each unique boot).
74
+ * Default circuit breaker parameters:
76
75
  *
77
- * ALLOWED_FAULTS: between 1 and SLOTS_PER_EPOCH // 4
78
- * FAULT_INSPECTION_WINDOW: between SLOTS_PER_EPOCH and 2 * SLOTS_PER_EPOCH
76
+ * SLOTS_PER_EPOCH <= FAULT_INSPECTION_WINDOW < 2 * SLOTS_PER_EPOCH (randomized at initialization)
77
+ * ALLOWED_FAULTS: FAULT_INSPECTION_WINDOW // 4
79
78
  *
80
- * The values are randomized per node so builders cannot predict when a given proposer will
81
- * fall back to local blocks. With fixed thresholds a builder could withhold payloads right up
82
- * to the limit without ever tripping the breaker.
79
+ * e.g. on mainnet SLOTS_PER_EPOCH is 32, so FAULT_INSPECTION_WINDOW is between 32 and 63,
80
+ * and a window of 40 results in ALLOWED_FAULTS = 10.
81
+ *
82
+ * The randomized default window keeps the exact slots under inspection unpredictable per node,
83
+ * so a builder cannot tell when past faults age out of a given proposer's window and time
84
+ * withholding around trip or recovery points. Explicitly configured windows are clamped to at
85
+ * least SLOTS_PER_EPOCH, and configured allowedFaults is capped at the default ~25% budget.
83
86
  */
84
87
  export function getFaultInspectionParams(opts: {faultInspectionWindow?: number; allowedFaults?: number}): {
85
88
  faultInspectionWindow: number;
@@ -90,10 +93,7 @@ export function getFaultInspectionParams(opts: {faultInspectionWindow?: number;
90
93
  SLOTS_PER_EPOCH
91
94
  );
92
95
  // allowedFaults should be < faultInspectionWindow, limiting them to faultInspectionWindow/4
93
- const allowedFaults = Math.min(
94
- opts.allowedFaults ?? Math.floor(faultInspectionWindow / 4),
95
- Math.floor(faultInspectionWindow / 4)
96
- );
96
+ const allowedFaults = Math.min(opts.allowedFaults ?? Infinity, Math.floor(faultInspectionWindow / 4));
97
97
  return {faultInspectionWindow, allowedFaults};
98
98
  }
99
99
 
@@ -143,17 +143,17 @@ export function createBeaconMetrics(register: RegistryMetricCreator) {
143
143
  name: "beacon_builder_circuit_breaker_active",
144
144
  help: "Whether the builder circuit breaker is active (1) causing builder bids to be ignored",
145
145
  }),
146
- faults: register.gauge({
147
- name: "beacon_builder_circuit_breaker_faults",
148
- help: "Count of blocks with unrevealed payloads in the fault inspection window",
146
+ canonicalBlocks: register.gauge({
147
+ name: "beacon_builder_circuit_breaker_canonical_blocks",
148
+ help: "Count of canonical FULL and EMPTY blocks in the fault inspection window",
149
149
  }),
150
- blocksPresent: register.gauge({
151
- name: "beacon_builder_circuit_breaker_blocks_present",
152
- help: "Count of blocks present in the fault inspection window",
150
+ fullBlocks: register.gauge({
151
+ name: "beacon_builder_circuit_breaker_full_blocks",
152
+ help: "Count of canonical FULL blocks in the fault inspection window",
153
153
  }),
154
- payloadsRevealed: register.gauge({
155
- name: "beacon_builder_circuit_breaker_payloads_revealed",
156
- help: "Count of blocks with revealed payloads in the fault inspection window",
154
+ emptyBlocks: register.gauge({
155
+ name: "beacon_builder_circuit_breaker_empty_blocks",
156
+ help: "Count of canonical EMPTY blocks in the fault inspection window",
157
157
  }),
158
158
  },
159
159
 
@@ -908,6 +908,22 @@ export function createLodestarMetrics(
908
908
  labelNames: ["error"],
909
909
  }),
910
910
  },
911
+ gossipExecutionPayloadBid: {
912
+ elapsedTimeTillReceived: register.histogram<{source: OpSource}>({
913
+ name: "lodestar_gossip_execution_payload_bid_elapsed_time_till_received_seconds",
914
+ help: "Time elapsed between the bid's slot start and the time the execution payload bid was received (negative = received before its slot, broadcast in the previous slot; positive = received during its own slot, late)",
915
+ labelNames: ["source"],
916
+ buckets: [-6, -3, -1, 0, 3],
917
+ }),
918
+ },
919
+ gossipPayloadAttestationMessage: {
920
+ elapsedTimeTillReceived: register.histogram<{source: OpSource}>({
921
+ name: "lodestar_gossip_payload_attestation_message_elapsed_time_till_received_seconds",
922
+ help: "Time elapsed between slot time and the time the payload attestation message was received",
923
+ labelNames: ["source"],
924
+ buckets: [3, 6, 9, 12],
925
+ }),
926
+ },
911
927
  // recovery in the case of specific blob rows required
912
928
  recoverBlobSidecars: {
913
929
  blobsReconstructed: register.counter({
@@ -1,3 +1,4 @@
1
+ import {getCiphers} from "node:crypto";
1
2
  import {bootstrap} from "@libp2p/bootstrap";
2
3
  import {identify} from "@libp2p/identify";
3
4
  import type {PrivateKey} from "@libp2p/interface";
@@ -9,12 +10,20 @@ import {Libp2pInit, createLibp2p} from "libp2p";
9
10
  import {Registry} from "prom-client";
10
11
  import {ENR} from "@chainsafe/enr";
11
12
  import {noise} from "@chainsafe/libp2p-noise";
12
- import {defaultCrypto} from "@chainsafe/libp2p-noise/crypto";
13
+ import {asCrypto, defaultCrypto} from "@chainsafe/libp2p-noise/crypto";
13
14
  import {quic} from "@chainsafe/libp2p-quic";
14
15
  import {Libp2p, LodestarComponents} from "../interface.js";
15
16
  import {NetworkOptions, defaultNetworkOptions} from "../options.js";
16
17
  import {Eth2PeerDataStore} from "../peers/datastore.js";
17
18
 
19
+ const noiseCrypto = getCiphers().includes("chacha20-poly1305")
20
+ ? defaultCrypto
21
+ : {
22
+ ...defaultCrypto,
23
+ chaCha20Poly1305Encrypt: asCrypto.chaCha20Poly1305Encrypt,
24
+ chaCha20Poly1305Decrypt: asCrypto.chaCha20Poly1305Decrypt,
25
+ };
26
+
18
27
  export type NodeJsLibp2pOpts = {
19
28
  peerStoreDir?: string;
20
29
  disablePeerDiscovery?: boolean;
@@ -122,7 +131,7 @@ export async function createNodeJsLibp2p(
122
131
  listen: localMultiaddrs,
123
132
  announce: [],
124
133
  },
125
- connectionEncrypters: [noise({crypto: defaultCrypto})],
134
+ connectionEncrypters: [noise({crypto: noiseCrypto})],
126
135
  transports,
127
136
  streamMuxers: [mplex({disconnectThreshold})],
128
137
  peerDiscovery,
@@ -1255,11 +1255,15 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
1255
1255
  [GossipType.payload_attestation_message]: async ({
1256
1256
  gossipData,
1257
1257
  topic,
1258
+ seenTimestampSec,
1258
1259
  }: GossipHandlerParamGeneric<GossipType.payload_attestation_message>) => {
1259
1260
  const {serializedData} = gossipData;
1260
1261
  const payloadAttestationMessage = sszDeserialize(topic, serializedData);
1261
1262
  const validationResult = await validateGossipPayloadAttestationMessage(chain, payloadAttestationMessage);
1262
1263
 
1264
+ const delaySec = chain.clock.secFromSlot(payloadAttestationMessage.data.slot, seenTimestampSec);
1265
+ metrics?.gossipPayloadAttestationMessage.elapsedTimeTillReceived.observe({source: OpSource.gossip}, delaySec);
1266
+
1263
1267
  try {
1264
1268
  const insertOutcome = chain.payloadAttestationPool.add(
1265
1269
  payloadAttestationMessage,
@@ -1286,11 +1290,16 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
1286
1290
  [GossipType.execution_payload_bid]: async ({
1287
1291
  gossipData,
1288
1292
  topic,
1293
+ seenTimestampSec,
1289
1294
  }: GossipHandlerParamGeneric<GossipType.execution_payload_bid>) => {
1290
1295
  const {serializedData} = gossipData;
1291
1296
  const executionPayloadBid = sszDeserialize(topic, serializedData);
1292
1297
  const {proposerIndex} = await validateGossipExecutionPayloadBid(chain, executionPayloadBid);
1293
1298
 
1299
+ // this could be negative, because it's most likely the bid of next slot comes at this clock slot
1300
+ const elapsedSec = chain.clock.secFromSlot(executionPayloadBid.message.slot, seenTimestampSec);
1301
+ metrics?.gossipExecutionPayloadBid.elapsedTimeTillReceived.observe({source: OpSource.gossip}, elapsedSec);
1302
+
1294
1303
  // Handle valid payload bid by storing in a bid pool
1295
1304
  try {
1296
1305
  const insertOutcome = chain.executionPayloadBidPool.add(executionPayloadBid);
@@ -44,17 +44,11 @@ export function onOutgoingReqRespError(e: RequestError, method: ReqRespMethod):
44
44
  // failure, so keep the stronger penalty (Fatal for Ping, as before).
45
45
  return method === ReqRespMethod.Ping ? PeerAction.Fatal : PeerAction.LowToleranceError;
46
46
  }
47
- switch (method) {
48
- // Ping and Status are liveness probes; their dial timeouts are dominated by transient
49
- // network congestion rather than peer misbehavior, so penalize leniently to avoid
50
- // self-inflicted peer starvation (https://github.com/ChainSafe/lodestar/issues/9562),
51
- // while still applying some penalty so genuinely dead peers eventually free the slot.
52
- case ReqRespMethod.Ping:
53
- case ReqRespMethod.Status:
54
- return PeerAction.HighToleranceError;
55
- default:
56
- return PeerAction.LowToleranceError;
57
- }
47
+ // A dial failure means we could not open a stream. That is evidence about the transport, not
48
+ // about the peer's behavior, and it is dominated by transient congestion on our side, so
49
+ // penalize leniently for every method to avoid self-inflicted peer starvation (#9562). A
50
+ // genuinely dead peer still accumulates penalty and is eventually evicted, freeing the slot.
51
+ return PeerAction.HighToleranceError;
58
52
  // TODO: Detect SSZDecodeError and return PeerAction.Fatal
59
53
 
60
54
  case RequestErrorCode.RESP_TIMEOUT:
package/src/sync/types.ts CHANGED
@@ -77,7 +77,9 @@ export type PendingPayloadInput = {
77
77
  export type PendingPayloadRootHex = {
78
78
  status: PendingPayloadInputStatus.pending | PendingPayloadInputStatus.fetching;
79
79
  rootHex: RootHex;
80
- // message slot hint, may be missing when resolving a parent payload
80
+ // Trusted slot only (fork choice / validated data), may be missing until resolved. NOT the gossip
81
+ // message slot from ChainEvent.unknownEnvelopeBlockRoot, which is untrusted and not necessarily the
82
+ // payload/block slot. See BlockInputSync.resolvePayloadSlot.
81
83
  slot?: Slot;
82
84
  timeAddedSec: number;
83
85
  timeSyncedSec?: number;