@lodestar/fork-choice 1.51.0-dev.c99cf49add → 1.51.0-dev.d2440f7e84

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.
@@ -86,8 +86,8 @@ export enum UpdateHeadOpt {
86
86
 
87
87
  export type UpdateAndGetHeadOpt =
88
88
  | {mode: UpdateHeadOpt.GetCanonicalHead}
89
- | {mode: UpdateHeadOpt.GetProposerHead; secFromSlot: number; slot: Slot}
90
- | {mode: UpdateHeadOpt.GetPredictedProposerHead; secFromSlot: number; slot: Slot};
89
+ | {mode: UpdateHeadOpt.GetProposerHead; msFromSlot: number; slot: Slot}
90
+ | {mode: UpdateHeadOpt.GetPredictedProposerHead; msFromSlot: number; slot: Slot};
91
91
 
92
92
  // the initial vote epoch for all validators
93
93
  const INIT_VOTE_SLOT: Slot = 0;
@@ -315,13 +315,13 @@ export class ForkChoice implements IForkChoice {
315
315
  const canonicalHeadBlock = mode === UpdateHeadOpt.GetPredictedProposerHead ? this.getHead() : this.updateHead();
316
316
  switch (mode) {
317
317
  case UpdateHeadOpt.GetPredictedProposerHead:
318
- return {head: this.predictProposerHead(canonicalHeadBlock, opt.secFromSlot, opt.slot)};
318
+ return {head: this.predictProposerHead(canonicalHeadBlock, opt.msFromSlot, opt.slot)};
319
319
  case UpdateHeadOpt.GetProposerHead: {
320
320
  const {
321
321
  proposerHead: head,
322
322
  isHeadTimely,
323
323
  notReorgedReason,
324
- } = this.getProposerHead(canonicalHeadBlock, opt.secFromSlot, opt.slot);
324
+ } = this.getProposerHead(canonicalHeadBlock, opt.msFromSlot, opt.slot);
325
325
  return {head, isHeadTimely, notReorgedReason};
326
326
  }
327
327
  case UpdateHeadOpt.GetCanonicalHead:
@@ -338,7 +338,7 @@ export class ForkChoice implements IForkChoice {
338
338
  // Note when proposer boost reorg is disabled, it always returns false
339
339
  shouldOverrideForkChoiceUpdate(
340
340
  headBlock: ProtoBlock,
341
- secFromSlot: number,
341
+ msFromSlot: number,
342
342
  currentSlot: Slot
343
343
  ): ShouldOverrideForkChoiceUpdateResult {
344
344
  if (headBlock === null) {
@@ -369,7 +369,7 @@ export class ForkChoice implements IForkChoice {
369
369
 
370
370
  const currentTimeOk =
371
371
  headBlock.slot === currentSlot ||
372
- (proposalSlot === currentSlot && this.isProposingOnTime(secFromSlot, currentSlot));
372
+ (proposalSlot === currentSlot && this.isProposingOnTime(msFromSlot, currentSlot));
373
373
 
374
374
  // Mirror the proposer equivocation branch of getProposerHead(). The head slot's attestations are
375
375
  // still queued at this point so the head is assumed weak, same as for the regular branch below.
@@ -422,6 +422,24 @@ export class ForkChoice implements IForkChoice {
422
422
  return this.protoArray.shouldExtendPayload(blockRoot, this.proposerBoostRoot);
423
423
  }
424
424
 
425
+ /** Inclusion list verdict for the payload of `blockRoot`, as reported by the execution engine */
426
+ recordPayloadInclusionListSatisfaction(blockRoot: RootHex, satisfied: boolean): boolean {
427
+ const changed = this.protoArray.recordPayloadInclusionListSatisfaction(blockRoot, satisfied);
428
+ if (changed && !satisfied) {
429
+ this.metrics?.forkChoice.unsatisfiedInclusionListBlocks.inc();
430
+ this.logger?.verbose("Payload does not satisfy the inclusion list constraints, not extending it", {blockRoot});
431
+ }
432
+ return changed;
433
+ }
434
+
435
+ isPayloadInclusionListSatisfied(blockRoot: RootHex): boolean {
436
+ return this.protoArray.isPayloadInclusionListSatisfied(blockRoot);
437
+ }
438
+
439
+ getPayloadInclusionListSatisfaction(blockRoot: RootHex): boolean | null {
440
+ return this.protoArray.getPayloadInclusionListSatisfaction(blockRoot);
441
+ }
442
+
425
443
  /** Spec: should_build_on_full(store, head, slot) */
426
444
  shouldBuildOnFull(head: ProtoBlock, slot: Slot): boolean {
427
445
  return this.protoArray.shouldBuildOnFull(head, slot);
@@ -437,7 +455,7 @@ export class ForkChoice implements IForkChoice {
437
455
  * By calling this function, we assume we are the proposer of next slot
438
456
  *
439
457
  */
440
- predictProposerHead(headBlock: ProtoBlock, secFromSlot: number, currentSlot: Slot): ProtoBlock {
458
+ predictProposerHead(headBlock: ProtoBlock, msFromSlot: number, currentSlot: Slot): ProtoBlock {
441
459
  const {proposerBoost, proposerBoostReorg} = this.opts ?? {};
442
460
  // Skip re-org attempt if proposer boost (reorg) are disabled
443
461
  if (!proposerBoost || !proposerBoostReorg) {
@@ -450,7 +468,7 @@ export class ForkChoice implements IForkChoice {
450
468
  }
451
469
 
452
470
  const blockRoot = headBlock.blockRoot;
453
- const result = this.shouldOverrideForkChoiceUpdate(headBlock, secFromSlot, currentSlot);
471
+ const result = this.shouldOverrideForkChoiceUpdate(headBlock, msFromSlot, currentSlot);
454
472
 
455
473
  if (result.shouldOverrideFcu) {
456
474
  this.logger?.verbose("Current head is weak. Predicting next block to be built on parent of head.", {
@@ -481,7 +499,7 @@ export class ForkChoice implements IForkChoice {
481
499
  */
482
500
  getProposerHead(
483
501
  headBlock: ProtoBlock,
484
- secFromSlot: number,
502
+ msFromSlot: number,
485
503
  slot: Slot
486
504
  ): {proposerHead: ProtoBlock; isHeadTimely: boolean; notReorgedReason?: NotReorgedReason} {
487
505
  const isHeadTimely = headBlock.timeliness;
@@ -538,7 +556,7 @@ export class ForkChoice implements IForkChoice {
538
556
  }
539
557
 
540
558
  // Only re-org if we are proposing on-time
541
- if (!this.isProposingOnTime(secFromSlot, slot)) {
559
+ if (!this.isProposingOnTime(msFromSlot, slot)) {
542
560
  return {proposerHead, isHeadTimely, notReorgedReason: NotReorgedReason.NotProposingOnTime};
543
561
  }
544
562
 
@@ -739,8 +757,8 @@ export class ForkChoice implements IForkChoice {
739
757
  onBlock(
740
758
  block: BeaconBlock,
741
759
  state: IBeaconStateView,
742
- receiveDelaySec: number,
743
- importDelaySec: number,
760
+ receiveDelayMs: number,
761
+ importDelayMs: number,
744
762
  currentSlot: Slot,
745
763
  executionStatus: BlockExecutionStatus,
746
764
  dataAvailabilityStatus: DataAvailabilityStatus
@@ -812,7 +830,7 @@ export class ForkChoice implements IForkChoice {
812
830
  // Decide whether this block should receive proposer boost, and whether the block is timely.
813
831
  // The store field `this.proposerBoostRoot` and `updateCheckpoints()` are mutated only after
814
832
  // `protoArray.onBlock()` succeeds
815
- const isTimely = this.isBlockReceivedTimely(block, receiveDelaySec);
833
+ const isTimely = this.isBlockReceivedTimely(block, receiveDelayMs);
816
834
  const isProposerBoostBlock =
817
835
  this.opts?.proposerBoost === true &&
818
836
  isTimely &&
@@ -831,14 +849,17 @@ export class ForkChoice implements IForkChoice {
831
849
  const blockEpoch = computeEpochAtSlot(slot);
832
850
 
833
851
  // same logic to compute_pulled_up_tip in the spec, making it inline because of reusing variables
834
- // If the parent checkpoints are already at the same epoch as the block being imported,
835
- // it's impossible for the unrealized checkpoints to differ from the parent's. This
836
- // holds true because:
852
+ // If the block has no slashings and the parent checkpoints are already at the same epoch as the
853
+ // block being imported, it's impossible for the unrealized checkpoints to differ from the parent's.
854
+ // This holds true because:
837
855
  //
838
856
  // 1. A child block cannot have lower FFG checkpoints than its parent.
839
857
  // 2. A block in epoch `N` cannot contain attestations which would justify an epoch higher than `N`.
840
858
  // 3. A block in epoch `N` cannot contain attestations which would finalize an epoch higher than `N - 1`.
841
859
  //
860
+ // Slashings are excluded because they can reduce the unslashed target balance of the child state
861
+ // and therefore lower its unrealized checkpoints.
862
+ //
842
863
  // This is an optimization. It should reduce the amount of times we run
843
864
  // `process_justification_and_finalization` by approximately 1/3rd when the chain is
844
865
  // performing optimally.
@@ -846,6 +867,8 @@ export class ForkChoice implements IForkChoice {
846
867
  let unrealizedFinalizedCheckpoint: CheckpointWithHex;
847
868
  if (this.opts?.computeUnrealized) {
848
869
  if (
870
+ block.body.proposerSlashings.length === 0 &&
871
+ block.body.attesterSlashings.length === 0 &&
849
872
  parentBlock.unrealizedJustifiedEpoch === blockEpoch &&
850
873
  parentBlock.unrealizedFinalizedEpoch + 1 >= blockEpoch
851
874
  ) {
@@ -883,8 +906,8 @@ export class ForkChoice implements IForkChoice {
883
906
  targetRoot: toRootHex(targetRoot),
884
907
  stateRoot: toRootHex(block.stateRoot),
885
908
  timeliness: isTimely,
886
- ptcTimeliness: this.isBlockPtcTimely(block, receiveDelaySec),
887
- importedTimely: this.isBlockImportedTimely(block, importDelaySec),
909
+ ptcTimeliness: this.isBlockPtcTimely(block, receiveDelayMs),
910
+ importedTimely: this.isBlockImportedTimely(block, importDelayMs),
888
911
  proposerIndex: block.proposerIndex,
889
912
 
890
913
  justifiedEpoch: stateJustifiedEpoch,
@@ -980,14 +1003,8 @@ export class ForkChoice implements IForkChoice {
980
1003
  *
981
1004
  * https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/fork-choice.md#on_attestation
982
1005
  *
983
- * It only approximates the specification since it does not perform
984
- * `is_valid_indexed_attestation` since that should already have been called upstream and it's
985
- * too expensive to call again.
986
- *
987
- * ## Notes:
988
- *
989
- * The supplied `attestation` **must** pass the `in_valid_indexed_attestation` function as it
990
- * will not be run here.
1006
+ * Does not run `is_valid_indexed_attestation`. The caller must ensure all of its
1007
+ * validity requirements except index ordering: `attestingIndices` may be unsorted.
991
1008
  */
992
1009
  onAttestation(attestation: IndexedAttestation, attDataRoot: string, forceImport?: boolean): void {
993
1010
  // Ignore any attestations to the zero hash.
@@ -1414,7 +1431,9 @@ export class ForkChoice implements IForkChoice {
1414
1431
  *
1415
1432
  * `ancestors` is the raw walk and includes the previous finalized block as its last element —
1416
1433
  * callers that don't want the boundary should slice it off themselves.
1417
- * Post-gloas for each block root, it returns exactly one variant of it.
1434
+ * For Gloas parent blocks, `ancestors` contains the selected EMPTY or FULL variant.
1435
+ * The other variant, if present in the traversed range, goes into `nonAncestors` with the same root.
1436
+ * Exclude roots present in `ancestors` before pruning by block root.
1418
1437
  */
1419
1438
  getAllAncestorAndNonAncestorBlocks(
1420
1439
  blockRoot: RootHex,
@@ -1522,7 +1541,9 @@ export class ForkChoice implements IForkChoice {
1522
1541
 
1523
1542
  /** Very expensive function, iterates the entire ProtoArray. TODO: Is this function even necessary? */
1524
1543
  getBlockSummariesByParentRoot(parentRoot: RootHex): ProtoBlock[] {
1525
- return this.protoArray.nodes.filter((node) => node.parentRoot === parentRoot);
1544
+ return this.protoArray.nodes.filter(
1545
+ (node) => node.parentRoot === parentRoot && (!isGloasBlock(node) || node.payloadStatus === PayloadStatus.PENDING)
1546
+ );
1526
1547
  }
1527
1548
 
1528
1549
  /** Very expensive function, iterates the entire ProtoArray. TODO: Is this function even necessary? */
@@ -1531,7 +1552,7 @@ export class ForkChoice implements IForkChoice {
1531
1552
  const blocksAtSlot: ProtoBlock[] = [];
1532
1553
  for (let i = 0, len = nodes.length; i < len; i++) {
1533
1554
  const node = nodes[i];
1534
- if (node.slot === slot) {
1555
+ if (node.slot === slot && (!isGloasBlock(node) || node.payloadStatus === PayloadStatus.PENDING)) {
1535
1556
  blocksAtSlot.push(node);
1536
1557
  }
1537
1558
  }
@@ -1757,9 +1778,9 @@ export class ForkChoice implements IForkChoice {
1757
1778
  * Return true if the block is timely for the current slot.
1758
1779
  * Child class can overwrite this for testing purpose.
1759
1780
  */
1760
- protected isBlockReceivedTimely(block: BeaconBlock, blockDelaySec: number): boolean {
1781
+ protected isBlockReceivedTimely(block: BeaconBlock, blockDelayMs: number): boolean {
1761
1782
  const fork = this.config.getForkName(block.slot);
1762
- const isBeforeLateBlockCutoff = blockDelaySec * 1000 < this.config.getAttestationDueMs(fork);
1783
+ const isBeforeLateBlockCutoff = blockDelayMs < this.config.getAttestationDueMs(fork);
1763
1784
  return this.fcStore.currentSlot === block.slot && isBeforeLateBlockCutoff;
1764
1785
  }
1765
1786
 
@@ -1773,12 +1794,12 @@ export class ForkChoice implements IForkChoice {
1773
1794
  *
1774
1795
  * Child class can overwrite this for testing purpose.
1775
1796
  */
1776
- protected isBlockPtcTimely(block: BeaconBlock, blockDelaySec: number): boolean {
1797
+ protected isBlockPtcTimely(block: BeaconBlock, blockDelayMs: number): boolean {
1777
1798
  const ptcThresholdMs = this.config.getSlotComponentDurationMs(
1778
1799
  this.config.getForkName(block.slot),
1779
1800
  this.config.PAYLOAD_ATTESTATION_DUE_BPS
1780
1801
  );
1781
- return this.fcStore.currentSlot === block.slot && blockDelaySec * 1000 < ptcThresholdMs;
1802
+ return this.fcStore.currentSlot === block.slot && blockDelayMs < ptcThresholdMs;
1782
1803
  }
1783
1804
 
1784
1805
  /**
@@ -1786,9 +1807,9 @@ export class ForkChoice implements IForkChoice {
1786
1807
  * block was imported in a timely manner for its own slot.
1787
1808
  * This is not part of the spec, we use this to determine late canonical blocks.
1788
1809
  */
1789
- protected isBlockImportedTimely(block: BeaconBlock, importDelaySec: number): boolean {
1810
+ protected isBlockImportedTimely(block: BeaconBlock, importDelayMs: number): boolean {
1790
1811
  const fork = this.config.getForkName(block.slot);
1791
- const isBeforeLateBlockCutoff = importDelaySec * 1000 < this.config.getAttestationDueMs(fork);
1812
+ const isBeforeLateBlockCutoff = importDelayMs < this.config.getAttestationDueMs(fork);
1792
1813
  return this.fcStore.currentSlot === block.slot && isBeforeLateBlockCutoff;
1793
1814
  }
1794
1815
 
@@ -1873,10 +1894,10 @@ export class ForkChoice implements IForkChoice {
1873
1894
  /**
1874
1895
  * https://github.com/ethereum/consensus-specs/blob/v1.5.0/specs/phase0/fork-choice.md#is_proposing_on_time
1875
1896
  */
1876
- private isProposingOnTime(secFromSlot: number, slot: Slot): boolean {
1897
+ private isProposingOnTime(msFromSlot: number, slot: Slot): boolean {
1877
1898
  const fork = this.config.getForkName(slot);
1878
1899
  const proposerReorgCutoff = this.config.getProposerReorgCutoffMs(fork);
1879
- return secFromSlot * 1000 <= proposerReorgCutoff;
1900
+ return msFromSlot <= proposerReorgCutoff;
1880
1901
  }
1881
1902
 
1882
1903
  private getPreMergeExecStatus(executionStatus: BlockExecutionStatus): ExecutionStatus.PreMerge {
@@ -123,7 +123,7 @@ export interface IForkChoice {
123
123
  */
124
124
  shouldOverrideForkChoiceUpdate(
125
125
  headBlock: ProtoBlock,
126
- secFromSlot: number,
126
+ msFromSlot: number,
127
127
  currentSlot: Slot
128
128
  ): ShouldOverrideForkChoiceUpdateResult;
129
129
  /**
@@ -159,8 +159,8 @@ export interface IForkChoice {
159
159
  onBlock(
160
160
  block: BeaconBlock,
161
161
  state: IBeaconStateView,
162
- receiveDelaySec: number,
163
- importDelaySec: number,
162
+ receiveDelayMs: number,
163
+ importDelayMs: number,
164
164
  currentSlot: Slot,
165
165
  executionStatus: BlockExecutionStatus,
166
166
  dataAvailabilityStatus: DataAvailabilityStatus
@@ -279,6 +279,12 @@ export interface IForkChoice {
279
279
  shouldExtendPayload(blockRoot: RootHex): boolean;
280
280
  /** Spec: should_build_on_full(store, head, slot) */
281
281
  shouldBuildOnFull(head: ProtoBlock, slot: Slot): boolean;
282
+ /** Inclusion list verdict for the payload of `blockRoot`, as reported by the execution engine. True if it changed */
283
+ recordPayloadInclusionListSatisfaction(blockRoot: RootHex, satisfied: boolean): boolean;
284
+ /** True once the payload is delivered and not recorded unsatisfied */
285
+ isPayloadInclusionListSatisfied(blockRoot: RootHex): boolean;
286
+ /** Engine verdict recorded for the payload of `blockRoot`, null when none was recorded */
287
+ getPayloadInclusionListSatisfaction(blockRoot: RootHex): boolean | null;
282
288
  getFinalizedBlock(): ProtoBlock;
283
289
  getJustifiedBlock(): ProtoBlock;
284
290
  getFinalizedCheckpointSlot(): Slot;
@@ -344,7 +350,9 @@ export interface IForkChoice {
344
350
  * `CheckpointWithHex` / finalized root without a specific payload-status variant in mind.
345
351
  */
346
352
  forwardIterateDescendantsDefaultStatus(blockRoot: RootHex): IterableIterator<ProtoBlock>;
353
+ /** One summary per block, its default variant (FULL pre-Gloas, PENDING for Gloas) */
347
354
  getBlockSummariesByParentRoot(parentRoot: RootHex): ProtoBlock[];
355
+ /** One summary per block, its default variant (FULL pre-Gloas, PENDING for Gloas) */
348
356
  getBlockSummariesAtSlot(slot: Slot): ProtoBlock[];
349
357
  /** Returns the distance of common ancestor of nodes to the max of the newNode and the prevNode. */
350
358
  getCommonAncestorDepth(prevBlock: ProtoBlock, newBlock: ProtoBlock): AncestorResult;
package/src/metrics.ts CHANGED
@@ -19,6 +19,10 @@ export function getForkChoiceMetrics(register: MetricsRegisterExtra) {
19
19
  name: "beacon_fork_choice_requests_total",
20
20
  help: "Count of occasions where fork choice has tried to find a head",
21
21
  }),
22
+ unsatisfiedInclusionListBlocks: register.gauge({
23
+ name: "beacon_fork_choice_unsatisfied_inclusion_list_blocks_total",
24
+ help: "Count of payloads not extended because they did not satisfy the inclusion list constraints",
25
+ }),
22
26
  errors: register.gauge<{entrypoint: UpdateHeadOpt}>({
23
27
  name: "beacon_fork_choice_errors_total",
24
28
  help: "Count of occasions where fork choice has returned an error when trying to find a head",
@@ -122,6 +122,11 @@ export class ProtoArray {
122
122
  * when called with the negative parameter value.
123
123
  */
124
124
  private ptcAttested = new Map<RootHex, BitArray>();
125
+ /**
126
+ * Execution engine inclusion list verdict per block, recorded on payload import from heze.
127
+ * Spec: heze/fork-choice.md#modified-store (payload_inclusion_list_satisfaction)
128
+ */
129
+ private payloadInclusionListSatisfaction = new Map<RootHex, boolean>();
125
130
 
126
131
  constructor({
127
132
  pruneThreshold,
@@ -938,6 +943,27 @@ export class ProtoArray {
938
943
  return this.getParentPayloadStatus(block) === PayloadStatus.FULL;
939
944
  }
940
945
 
946
+ /** Spec: heze/fork-choice.md#new-record_payload_inclusion_list_satisfaction. Returns whether the verdict changed */
947
+ recordPayloadInclusionListSatisfaction(blockRoot: RootHex, satisfied: boolean): boolean {
948
+ const previous = this.payloadInclusionListSatisfaction.get(blockRoot);
949
+ this.payloadInclusionListSatisfaction.set(blockRoot, satisfied);
950
+ return previous !== satisfied;
951
+ }
952
+
953
+ /**
954
+ * Spec: heze/fork-choice.md#new-is_payload_inclusion_list_satisfied
955
+ *
956
+ * False until the payload is delivered. A delivered payload without a verdict is pre-heze, where
957
+ * no inclusion list constraints apply.
958
+ */
959
+ isPayloadInclusionListSatisfied(blockRoot: RootHex): boolean {
960
+ return this.hasPayload(blockRoot) && this.payloadInclusionListSatisfaction.get(blockRoot) !== false;
961
+ }
962
+
963
+ getPayloadInclusionListSatisfaction(blockRoot: RootHex): boolean | null {
964
+ return this.payloadInclusionListSatisfaction.get(blockRoot) ?? null;
965
+ }
966
+
941
967
  /**
942
968
  * Determine if we should extend the payload (prefer FULL over EMPTY)
943
969
  * Spec: gloas/fork-choice.md#new-should_extend_payload
@@ -956,6 +982,10 @@ export class ProtoArray {
956
982
  return false;
957
983
  }
958
984
 
985
+ if (this.payloadInclusionListSatisfaction.get(blockRoot) === false) {
986
+ return false;
987
+ }
988
+
959
989
  // Condition 1: Payload is timely AND data is available
960
990
  if (this.isPayloadTimely(blockRoot) && this.isPayloadDataAvailable(blockRoot)) {
961
991
  return true;
@@ -1265,6 +1295,15 @@ export class ProtoArray {
1265
1295
 
1266
1296
  // For Gloas: check if from previous slot
1267
1297
  if (node.slot + 1 !== currentSlot) {
1298
+ // During the block's own slot a payload known to not satisfy the inclusion list
1299
+ // constraints ranks below its EMPTY variant, as it will from the next slot on
1300
+ if (
1301
+ node.slot === currentSlot &&
1302
+ node.payloadStatus === PayloadStatus.FULL &&
1303
+ this.payloadInclusionListSatisfaction.get(node.blockRoot) === false
1304
+ ) {
1305
+ return PayloadStatus.PENDING;
1306
+ }
1268
1307
  return node.payloadStatus;
1269
1308
  }
1270
1309
 
@@ -1399,6 +1438,7 @@ export class ProtoArray {
1399
1438
  this.payloadTimelinessVotes.delete(root);
1400
1439
  this.ptcAttested.delete(root);
1401
1440
  this.payloadDataAvailabilityVotes.delete(root);
1441
+ this.payloadInclusionListSatisfaction.delete(root);
1402
1442
  }
1403
1443
 
1404
1444
  // Store nodes prior to finalization