@lodestar/fork-choice 1.36.0-dev.f657221d88 → 1.36.0-dev.faac5550fb

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,11 +1,23 @@
1
1
  import {EffectiveBalanceIncrements} from "@lodestar/state-transition";
2
2
  import {ValidatorIndex} from "@lodestar/types";
3
3
  import {ProtoArrayError, ProtoArrayErrorCode} from "./errors.js";
4
- import {VoteTracker} from "./interface.js";
4
+ import {NULL_VOTE_INDEX, VoteIndex} from "./interface.js";
5
5
 
6
6
  // reuse arrays to avoid memory reallocation and gc
7
7
  const deltas = new Array<number>();
8
8
 
9
+ export type DeltasResult = {
10
+ deltas: number[];
11
+ equivocatingValidators: number;
12
+ // inactive validators before beacon node started
13
+ oldInactiveValidators: number;
14
+ // new inactive validators after beacon node started
15
+ newInactiveValidators: number;
16
+ // below is for active validators
17
+ unchangedVoteValidators: number;
18
+ newVoteValidators: number;
19
+ };
20
+
9
21
  /**
10
22
  * Returns a list of `deltas`, where there is one delta for each of the indices in `indices`
11
23
  *
@@ -17,27 +29,49 @@ const deltas = new Array<number>();
17
29
  */
18
30
  export function computeDeltas(
19
31
  numProtoNodes: number,
20
- votes: VoteTracker[],
32
+ voteCurrentIndices: VoteIndex[],
33
+ voteNextIndices: VoteIndex[],
21
34
  oldBalances: EffectiveBalanceIncrements,
22
35
  newBalances: EffectiveBalanceIncrements,
23
36
  equivocatingIndices: Set<ValidatorIndex>
24
- ): number[] {
37
+ ): DeltasResult {
38
+ if (voteCurrentIndices.length !== voteNextIndices.length) {
39
+ throw new Error(
40
+ `voteCurrentIndices and voteNextIndices must have the same length: ${voteCurrentIndices.length} !== ${voteNextIndices.length}`
41
+ );
42
+ }
43
+
44
+ if (numProtoNodes >= NULL_VOTE_INDEX) {
45
+ // this never happen in practice, but we check to be safe
46
+ throw new Error(`numProtoNodes must be less than NULL_VOTE_INDEX: ${numProtoNodes} >= ${NULL_VOTE_INDEX}`);
47
+ }
48
+
25
49
  deltas.length = numProtoNodes;
26
50
  deltas.fill(0);
27
51
 
28
52
  // avoid creating new variables in the loop to potentially reduce GC pressure
29
53
  let oldBalance: number, newBalance: number;
30
- let currentIndex: number | null, nextIndex: number | null;
54
+ let currentIndex: VoteIndex, nextIndex: VoteIndex;
55
+ // sort equivocating indices to avoid Set.has() in the loop
56
+ const equivocatingArray = Array.from(equivocatingIndices).sort((a, b) => a - b);
57
+ let equivocatingIndex = 0;
58
+ let equivocatingValidatorIndex = equivocatingArray[equivocatingIndex];
31
59
 
32
- for (let vIndex = 0; vIndex < votes.length; vIndex++) {
33
- const vote = votes[vIndex];
60
+ const equivocatingValidators = equivocatingIndices.size;
61
+ let oldInactiveValidators = 0;
62
+ let newInactiveValidators = 0;
63
+ let unchangedVoteValidators = 0;
64
+ let newVoteValidators = 0;
65
+
66
+ for (let vIndex = 0; vIndex < voteNextIndices.length; vIndex++) {
67
+ currentIndex = voteCurrentIndices[vIndex];
68
+ nextIndex = voteNextIndices[vIndex];
34
69
  // There is no need to create a score change if the validator has never voted or both of their
35
70
  // votes are for the zero hash (genesis block)
36
- if (vote === undefined) {
71
+ if (currentIndex === NULL_VOTE_INDEX && nextIndex === NULL_VOTE_INDEX) {
72
+ oldInactiveValidators++;
37
73
  continue;
38
74
  }
39
- currentIndex = vote.currentIndex;
40
- nextIndex = vote.nextIndex;
41
75
 
42
76
  // IF the validator was not included in the _old_ balances (i.e. it did not exist yet)
43
77
  // then say its balance was 0
@@ -50,9 +84,9 @@ export function computeDeltas(
50
84
  // on-boarded fewer validators than the prior fork.
51
85
  newBalance = newBalances === oldBalances ? oldBalance : (newBalances[vIndex] ?? 0);
52
86
 
53
- if (equivocatingIndices.size > 0 && equivocatingIndices.has(vIndex)) {
87
+ if (vIndex === equivocatingValidatorIndex) {
54
88
  // this function could be called multiple times but we only want to process slashing validator for 1 time
55
- if (currentIndex !== null) {
89
+ if (currentIndex !== NULL_VOTE_INDEX) {
56
90
  if (currentIndex >= numProtoNodes) {
57
91
  throw new ProtoArrayError({
58
92
  code: ProtoArrayErrorCode.INVALID_NODE_DELTA,
@@ -61,14 +95,21 @@ export function computeDeltas(
61
95
  }
62
96
  deltas[currentIndex] -= oldBalance;
63
97
  }
64
- vote.currentIndex = null;
98
+ voteCurrentIndices[vIndex] = NULL_VOTE_INDEX;
99
+ equivocatingIndex++;
100
+ equivocatingValidatorIndex = equivocatingArray[equivocatingIndex];
101
+ continue;
102
+ }
103
+
104
+ if (oldBalance === 0 && newBalance === 0) {
105
+ newInactiveValidators++;
65
106
  continue;
66
107
  }
67
108
 
68
109
  if (currentIndex !== nextIndex || oldBalance !== newBalance) {
69
110
  // We ignore the vote if it is not known in `indices .
70
111
  // We assume that it is outside of our tree (ie: pre-finalization) and therefore not interesting
71
- if (currentIndex !== null) {
112
+ if (currentIndex !== NULL_VOTE_INDEX) {
72
113
  if (currentIndex >= numProtoNodes) {
73
114
  throw new ProtoArrayError({
74
115
  code: ProtoArrayErrorCode.INVALID_NODE_DELTA,
@@ -80,7 +121,7 @@ export function computeDeltas(
80
121
 
81
122
  // We ignore the vote if it is not known in `indices .
82
123
  // We assume that it is outside of our tree (ie: pre-finalization) and therefore not interesting
83
- if (nextIndex !== null) {
124
+ if (nextIndex !== NULL_VOTE_INDEX) {
84
125
  if (nextIndex >= numProtoNodes) {
85
126
  throw new ProtoArrayError({
86
127
  code: ProtoArrayErrorCode.INVALID_NODE_DELTA,
@@ -89,9 +130,24 @@ export function computeDeltas(
89
130
  }
90
131
  deltas[nextIndex] += newBalance;
91
132
  }
133
+ voteCurrentIndices[vIndex] = nextIndex;
134
+ newVoteValidators++;
135
+ } else {
136
+ unchangedVoteValidators++;
92
137
  }
93
- vote.currentIndex = nextIndex;
138
+ } // end validator loop
139
+
140
+ if (deltas.length !== numProtoNodes) {
141
+ // deltas array could be growed in the loop, especially if we mistakenly set the [NULL_VOTE_INDEX] to it , just to be safe
142
+ throw new Error(`deltas length mismatch: expected ${numProtoNodes}, got ${deltas.length}`);
94
143
  }
95
144
 
96
- return deltas;
145
+ return {
146
+ deltas,
147
+ equivocatingValidators,
148
+ oldInactiveValidators,
149
+ newInactiveValidators,
150
+ unchangedVoteValidators,
151
+ newVoteValidators,
152
+ };
97
153
  }
@@ -6,15 +6,16 @@ import {Epoch, RootHex, Slot, UintNum64} from "@lodestar/types";
6
6
  export const HEX_ZERO_HASH = "0x0000000000000000000000000000000000000000000000000000000000000000";
7
7
 
8
8
  /**
9
- * Simplified 'latest message' with previous message
10
- * The index is relative to ProtoArray indices
9
+ * The null vote index indicates that a validator votes to a non-existent block. This usually happens when
10
+ * we prune the proto array and the validator's latest message is in the pruned part.
11
+ * The number of proto nodes will never exceed this value because it represents (0xffffffff / 365 / 24 / 60 / 5), ie > 1634 years of non-finalized network.
11
12
  */
12
- export type VoteTracker = {
13
- currentIndex: number | null;
14
- // if a vode is out of date (the voted index was in the past while proto array is pruned), it will be set to null
15
- nextIndex: number | null;
16
- nextEpoch: Epoch;
17
- };
13
+ export const NULL_VOTE_INDEX = 0xffffffff;
14
+
15
+ /**
16
+ * A vote index is a non-negative integer from 0 to NULL_VOTE_INDEX inclusive, and it will never be undefined.
17
+ */
18
+ export type VoteIndex = number;
18
19
 
19
20
  export enum ExecutionStatus {
20
21
  Valid = "Valid",