@lodestar/fork-choice 1.47.0-dev.f591cb177e → 1.47.0

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 {DataAvailabilityStatus} from "@lodestar/state-transition";
2
- import {Epoch, RootHex, Slot, UintNum64} from "@lodestar/types";
2
+ import {Epoch, RootHex, Slot, UintNum64, ValidatorIndex} from "@lodestar/types";
3
3
 
4
4
  // RootHex is a root as a hex string
5
5
  // Used for lightweight and easy comparison
@@ -144,6 +144,13 @@ export type ProtoBlock = BlockExtraMeta & {
144
144
  // Indicate whether block arrives in a timely manner ie. before the 4 second mark
145
145
  timeliness: boolean;
146
146
 
147
+ // Indicate whether block arrives before the PTC deadline
148
+ // Spec: gloas/fork-choice.md#modified-record_block_timeliness (block_timeliness[PTC_TIMELINESS_INDEX])
149
+ ptcTimeliness: boolean;
150
+
151
+ // The index of the block proposer. Used by should_apply_proposer_boost to detect proposer equivocations
152
+ proposerIndex: ValidatorIndex;
153
+
147
154
  /** Payload status for this node (Gloas fork). Always FULL in pre-gloas */
148
155
  payloadStatus: PayloadStatus;
149
156
 
@@ -160,13 +167,13 @@ export type ProtoBlock = BlockExtraMeta & {
160
167
  */
161
168
  export type ProtoNode = ProtoBlock & {
162
169
  parent?: number;
163
- /** Total weight, ie. attestationScore plus the proposer boost credited to this node */
164
- weight: number;
170
+ /** Total weight in Gwei, ie. attestationScore plus the proposer boost credited to this node */
171
+ weight: bigint;
165
172
  /**
166
- * Weight from attester votes only, excluding proposer boost.
173
+ * Weight in Gwei from attester votes only, excluding proposer boost.
167
174
  * Spec: get_attestation_score
168
175
  */
169
- attestationScore: number;
176
+ attestationScore: bigint;
170
177
  bestChild?: number;
171
178
  bestDescendant?: number;
172
179
  };
@@ -1,7 +1,7 @@
1
1
  import {BitArray} from "@chainsafe/ssz";
2
- import {GENESIS_EPOCH, GENESIS_SLOT, PTC_SIZE} from "@lodestar/params";
2
+ import {EFFECTIVE_BALANCE_INCREMENT, GENESIS_EPOCH, GENESIS_SLOT, PTC_SIZE} from "@lodestar/params";
3
3
  import {DataAvailabilityStatus, computeEpochAtSlot, computeStartSlotAtEpoch} from "@lodestar/state-transition";
4
- import {Epoch, RootHex, Slot} from "@lodestar/types";
4
+ import {Epoch, RootHex, Slot, ValidatorIndex} from "@lodestar/types";
5
5
  import {bitCount, toRootHex} from "@lodestar/utils";
6
6
  import {ForkChoiceError, ForkChoiceErrorCode} from "../forkChoice/errors.js";
7
7
  import {LVHExecError, LVHExecErrorCode, ProtoArrayError, ProtoArrayErrorCode} from "./errors.js";
@@ -31,7 +31,8 @@ const DATA_AVAILABILITY_TIMELY_THRESHOLD = Math.floor(PTC_SIZE / 2);
31
31
  * Proposer boost deltas, back-propagated to the boosted node's ancestors in applyScoreChanges().
32
32
  * Reuse the array to avoid memory reallocation and gc, as computeDeltas does for attestation deltas.
33
33
  */
34
- const boostDeltas = new Array<number>();
34
+ const boostDeltas = new Array<bigint>();
35
+ const EFFECTIVE_BALANCE_INCREMENT_BIGINT = BigInt(EFFECTIVE_BALANCE_INCREMENT);
35
36
 
36
37
  /**
37
38
  * popcount(attended AND NOT yes) — explicit False-vote count.
@@ -52,7 +53,7 @@ export function countNoVotes(attended: BitArray, yes: BitArray): number {
52
53
  }
53
54
 
54
55
  export const DEFAULT_PRUNE_THRESHOLD = 0;
55
- type ProposerBoost = {root: RootHex; score: number};
56
+ type ProposerBoost = {root: RootHex; score: bigint};
56
57
 
57
58
  const ZERO_HASH_HEX = toRootHex(Buffer.alloc(32, 0));
58
59
 
@@ -379,7 +380,7 @@ export class ProtoArray {
379
380
  }
380
381
 
381
382
  boostDeltas.length = this.nodes.length;
382
- boostDeltas.fill(0);
383
+ boostDeltas.fill(0n);
383
384
 
384
385
  if (
385
386
  justifiedEpoch !== this.justifiedEpoch ||
@@ -416,17 +417,20 @@ export class ProtoArray {
416
417
  // the boost neutral with respect to EMPTY vs FULL selection.
417
418
  const isBoostVariant = isGloasBlock(node) ? node.payloadStatus === PayloadStatus.PENDING : true; // pre-Gloas has only FULL, always boost
418
419
  const currentBoost =
419
- proposerBoost && proposerBoost.root === node.blockRoot && isBoostVariant ? proposerBoost.score : 0;
420
+ proposerBoost && proposerBoost.root === node.blockRoot && isBoostVariant ? proposerBoost.score : 0n;
420
421
  const previousBoost =
421
422
  this.previousProposerBoost && this.previousProposerBoost.root === node.blockRoot && isBoostVariant
422
423
  ? this.previousProposerBoost.score
423
- : 0;
424
+ : 0n;
424
425
 
425
426
  // If this node's execution status has been marked invalid, then the weight of the node
426
427
  // needs to be taken out of consideration after which the node weight will become 0
427
428
  // for subsequent iterations of applyScoreChanges
428
429
  const isInvalid = node.executionStatus === ExecutionStatus.Invalid;
429
- const attestationDelta = isInvalid ? -node.attestationScore : attestationDeltas[nodeIndex];
430
+ const attestationDelta = isInvalid
431
+ ? -Number(node.attestationScore / EFFECTIVE_BALANCE_INCREMENT_BIGINT)
432
+ : attestationDeltas[nodeIndex];
433
+ const attestationDeltaGwei = BigInt(attestationDelta) * EFFECTIVE_BALANCE_INCREMENT_BIGINT;
430
434
  const boostDelta = isInvalid
431
435
  ? // old boost = weight - attestationScore
432
436
  -(node.weight - node.attestationScore)
@@ -434,8 +438,8 @@ export class ProtoArray {
434
438
 
435
439
  // Apply the deltas to the node. Their sum is the node's total delta, so weight is unaffected
436
440
  // by tracking the two scores apart.
437
- node.attestationScore += attestationDelta;
438
- node.weight += attestationDelta + boostDelta;
441
+ node.attestationScore += attestationDeltaGwei;
442
+ node.weight += attestationDeltaGwei + boostDelta;
439
443
 
440
444
  // Update the parent deltas (if any)
441
445
  const parentIndex = node.parent;
@@ -527,8 +531,8 @@ export class ProtoArray {
527
531
  ...block,
528
532
  parent: parentIndex, // Points to parent's EMPTY/FULL or FULL (for transition)
529
533
  payloadStatus: PayloadStatus.PENDING,
530
- weight: 0,
531
- attestationScore: 0,
534
+ weight: 0n,
535
+ attestationScore: 0n,
532
536
  bestChild: undefined,
533
537
  bestDescendant: undefined,
534
538
  };
@@ -541,8 +545,8 @@ export class ProtoArray {
541
545
  ...block,
542
546
  parent: pendingIndex, // Points to own PENDING
543
547
  payloadStatus: PayloadStatus.EMPTY,
544
- weight: 0,
545
- attestationScore: 0,
548
+ weight: 0n,
549
+ attestationScore: 0n,
546
550
  bestChild: undefined,
547
551
  bestDescendant: undefined,
548
552
  };
@@ -577,8 +581,8 @@ export class ProtoArray {
577
581
  ...block,
578
582
  parent: this.getNodeIndexByRootAndStatus(block.parentRoot, PayloadStatus.FULL),
579
583
  payloadStatus: PayloadStatus.FULL,
580
- weight: 0,
581
- attestationScore: 0,
584
+ weight: 0n,
585
+ attestationScore: 0n,
582
586
  bestChild: undefined,
583
587
  bestDescendant: undefined,
584
588
  };
@@ -663,8 +667,8 @@ export class ProtoArray {
663
667
  ...pendingNode,
664
668
  parent: pendingIndex, // Points to own PENDING (same as EMPTY)
665
669
  payloadStatus: PayloadStatus.FULL,
666
- weight: 0,
667
- attestationScore: 0,
670
+ weight: 0n,
671
+ attestationScore: 0n,
668
672
  bestChild: undefined,
669
673
  bestDescendant: undefined,
670
674
  executionStatus,
@@ -1496,13 +1500,13 @@ export class ProtoArray {
1496
1500
  childNode.payloadStatus === PayloadStatus.PENDING ||
1497
1501
  childNode.slot + 1 !== currentSlot
1498
1502
  ? childNode.weight
1499
- : 0;
1503
+ : 0n;
1500
1504
  const bestChildEffectiveWeight =
1501
1505
  !isGloasBlock(bestChildNode) ||
1502
1506
  bestChildNode.payloadStatus === PayloadStatus.PENDING ||
1503
1507
  bestChildNode.slot + 1 !== currentSlot
1504
1508
  ? bestChildNode.weight
1505
- : 0;
1509
+ : 0n;
1506
1510
 
1507
1511
  if (childEffectiveWeight !== bestChildEffectiveWeight) {
1508
1512
  // Different effective weights, choose the winner by weight
@@ -1597,8 +1601,8 @@ export class ProtoArray {
1597
1601
  return correctJustified && correctFinalized;
1598
1602
  }
1599
1603
 
1600
- /** Weights are in EFFECTIVE_BALANCE_INCREMENT units (NOT Gwei); callers scale as needed. */
1601
- getViableHeads(currentSlot: Slot): {root: RootHex; payloadStatus: PayloadStatus; weight: number}[] {
1604
+ /** Returns exact Gwei weights for the compliance test boundary. */
1605
+ getViableHeads(currentSlot: Slot): {root: RootHex; payloadStatus: PayloadStatus; weight: bigint}[] {
1602
1606
  // Mirror the spec's `get_filtered_block_tree`, which is rooted at the store's justified
1603
1607
  // checkpoint: a viable head is a leaf (no viable descendant, i.e. `bestChild === undefined`)
1604
1608
  // that descends from the justified checkpoint block AND is itself viable for head. Iterating
@@ -1607,7 +1611,7 @@ export class ProtoArray {
1607
1611
  const justifiedVariant = this.getDefaultVariant(this.justifiedRoot);
1608
1612
  // Gloas payload-status variants of one blockRoot are distinct nodes in the spec's filtered
1609
1613
  // tree, identified by (root, payload_status, weight) — emit one entry per variant.
1610
- const heads: {root: RootHex; payloadStatus: PayloadStatus; weight: number}[] = [];
1614
+ const heads: {root: RootHex; payloadStatus: PayloadStatus; weight: bigint}[] = [];
1611
1615
  for (const node of this.nodes) {
1612
1616
  if (node.bestChild !== undefined || !this.nodeIsViableForHead(node, currentSlot)) {
1613
1617
  continue;
@@ -1624,7 +1628,11 @@ export class ProtoArray {
1624
1628
  continue;
1625
1629
  }
1626
1630
  }
1627
- heads.push({root: node.blockRoot, payloadStatus: node.payloadStatus, weight: node.weight});
1631
+ heads.push({
1632
+ root: node.blockRoot,
1633
+ payloadStatus: node.payloadStatus,
1634
+ weight: node.weight,
1635
+ });
1628
1636
  }
1629
1637
  return heads;
1630
1638
  }
@@ -1999,6 +2007,32 @@ export class ProtoArray {
1999
2007
  return this.getNodeByIndex(nodeIndex);
2000
2008
  }
2001
2009
 
2010
+ /**
2011
+ * Return true if a block other than `excludeRoot` at `slot` was proposed by `proposerIndex` and
2012
+ * is PTC-timely. Used by `should_apply_proposer_boost` to detect proposer equivocations.
2013
+ *
2014
+ * https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.14/specs/gloas/fork-choice.md#new-should_apply_proposer_boost
2015
+ *
2016
+ * Iterates unique block roots (via the canonical variant) since `slot`, `proposerIndex` and
2017
+ * `ptcTimeliness` are block-level properties identical across payload-status variants.
2018
+ */
2019
+ hasEquivocatingBlock(proposerIndex: ValidatorIndex, slot: Slot, excludeRoot: RootHex): boolean {
2020
+ for (const root of this.indices.keys()) {
2021
+ if (root === excludeRoot) {
2022
+ continue;
2023
+ }
2024
+ const nodeIndex = this.getDefaultNodeIndex(root);
2025
+ if (nodeIndex === undefined) {
2026
+ continue;
2027
+ }
2028
+ const node = this.nodes[nodeIndex];
2029
+ if (node !== undefined && node.slot === slot && node.proposerIndex === proposerIndex && node.ptcTimeliness) {
2030
+ return true;
2031
+ }
2032
+ }
2033
+ return false;
2034
+ }
2035
+
2002
2036
  /**
2003
2037
  * Return MUTABLE ProtoBlock for blockRoot with explicit payload status
2004
2038
  *