@lodestar/fork-choice 1.45.0-rc.0 → 1.46.0-dev.8fc4e297d2
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.
- package/lib/forkChoice/fastConfirmation/metrics.d.ts +1 -0
- package/lib/forkChoice/fastConfirmation/metrics.d.ts.map +1 -1
- package/lib/forkChoice/fastConfirmation/metrics.js +4 -0
- package/lib/forkChoice/fastConfirmation/metrics.js.map +1 -1
- package/lib/forkChoice/forkChoice.d.ts +35 -0
- package/lib/forkChoice/forkChoice.d.ts.map +1 -1
- package/lib/forkChoice/forkChoice.js +157 -37
- package/lib/forkChoice/forkChoice.js.map +1 -1
- package/lib/forkChoice/interface.d.ts +4 -0
- package/lib/forkChoice/interface.d.ts.map +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -1
- package/lib/index.js.map +1 -1
- package/lib/metrics.d.ts +1 -0
- package/lib/metrics.d.ts.map +1 -1
- package/lib/protoArray/computeDeltas.d.ts +3 -3
- package/lib/protoArray/computeDeltas.d.ts.map +1 -1
- package/lib/protoArray/computeDeltas.js +12 -12
- package/lib/protoArray/computeDeltas.js.map +1 -1
- package/lib/protoArray/interface.d.ts +6 -0
- package/lib/protoArray/interface.d.ts.map +1 -1
- package/lib/protoArray/protoArray.d.ts +14 -2
- package/lib/protoArray/protoArray.d.ts.map +1 -1
- package/lib/protoArray/protoArray.js +76 -24
- package/lib/protoArray/protoArray.js.map +1 -1
- package/package.json +8 -8
- package/src/forkChoice/fastConfirmation/metrics.ts +4 -0
- package/src/forkChoice/forkChoice.ts +171 -38
- package/src/forkChoice/interface.ts +4 -0
- package/src/index.ts +1 -1
- package/src/protoArray/computeDeltas.ts +13 -13
- package/src/protoArray/interface.ts +6 -0
- package/src/protoArray/protoArray.ts +84 -28
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {ChainForkConfig} from "@lodestar/config";
|
|
2
|
-
import {MIN_SEED_LOOKAHEAD, SLOTS_PER_EPOCH} from "@lodestar/params";
|
|
2
|
+
import {MIN_SEED_LOOKAHEAD, SLOTS_PER_EPOCH, isForkPostGloas} from "@lodestar/params";
|
|
3
3
|
import {
|
|
4
4
|
DataAvailabilityStatus,
|
|
5
5
|
EffectiveBalanceIncrements,
|
|
@@ -152,6 +152,7 @@ export class ForkChoice implements IForkChoice {
|
|
|
152
152
|
/** Optional fast confirmation rule implementation */
|
|
153
153
|
private readonly fastConfirmationRule?: IFastConfirmationRule;
|
|
154
154
|
private readonly fastConfirmationContext?: FastConfirmationContext;
|
|
155
|
+
private fastConfirmationPaused = false;
|
|
155
156
|
/**
|
|
156
157
|
* Instantiates a Fork Choice from some existing components
|
|
157
158
|
*
|
|
@@ -180,6 +181,7 @@ export class ForkChoice implements IForkChoice {
|
|
|
180
181
|
if (this.opts?.fastConfirmation) {
|
|
181
182
|
this.fastConfirmationRule = new FastConfirmationRule(this.fcStore, metrics, this.logger);
|
|
182
183
|
this.fastConfirmationContext = this.createFastConfirmationContext();
|
|
184
|
+
metrics?.fastConfirmation.paused.set(0);
|
|
183
185
|
}
|
|
184
186
|
|
|
185
187
|
metrics?.forkChoice.votes.addCollect(() => {
|
|
@@ -230,6 +232,47 @@ export class ForkChoice implements IForkChoice {
|
|
|
230
232
|
return this.getBlockHexDefaultStatus(this.getConfirmedRoot());
|
|
231
233
|
}
|
|
232
234
|
|
|
235
|
+
resumeFastConfirmation(): void {
|
|
236
|
+
this.toggleFastConfirmation(false);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
pauseFastConfirmation(): void {
|
|
240
|
+
this.toggleFastConfirmation(true);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private toggleFastConfirmation(paused: boolean): void {
|
|
244
|
+
if (!this.fastConfirmationRule) return;
|
|
245
|
+
if (paused === this.fastConfirmationPaused) return;
|
|
246
|
+
this.fastConfirmationPaused = paused;
|
|
247
|
+
if (paused) {
|
|
248
|
+
// Pin immediately: block imports report the safe block hash to the EL before the next slot tick
|
|
249
|
+
this.fcStore.confirmedRoot = this.fcStore.finalizedCheckpoint.rootHex;
|
|
250
|
+
try {
|
|
251
|
+
this.notifyConfirmedRoot();
|
|
252
|
+
} catch (err) {
|
|
253
|
+
// Callers run in clock/network handler context with no catch above
|
|
254
|
+
this.logger?.debug("Fast confirmation notify failed", {slot: this.fcStore.currentSlot}, err as Error);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
this.metrics?.fastConfirmation.paused.set(paused ? 1 : 0);
|
|
258
|
+
this.logger?.info(paused ? "Paused fast confirmation" : "Resumed fast confirmation", {
|
|
259
|
+
slot: this.fcStore.currentSlot,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private notifyConfirmedRoot(): void {
|
|
264
|
+
const confirmedRoot = this.fcStore.confirmedRoot;
|
|
265
|
+
const confirmedBlock = this.getBlockHexDefaultStatus(confirmedRoot);
|
|
266
|
+
if (confirmedBlock === null) {
|
|
267
|
+
throw new Error(`Fast confirmation produced root not in protoArray: ${confirmedRoot}`);
|
|
268
|
+
}
|
|
269
|
+
this.fcStore.notifyFastConfirmation?.({
|
|
270
|
+
block: confirmedRoot,
|
|
271
|
+
slot: confirmedBlock.slot,
|
|
272
|
+
currentSlot: this.fcStore.currentSlot,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
233
276
|
/**
|
|
234
277
|
*
|
|
235
278
|
* A multiplexer to wrap around the traditional `updateHead()` according to the scenario
|
|
@@ -455,30 +498,12 @@ export class ForkChoice implements IForkChoice {
|
|
|
455
498
|
}
|
|
456
499
|
|
|
457
500
|
// No reorg if headBlock is "not weak" ie. headBlock's weight exceeds (REORG_HEAD_WEIGHT_THRESHOLD = 20)% of total attester weight
|
|
458
|
-
|
|
459
|
-
const reorgThreshold = getCommitteeFraction(this.fcStore.justified.totalBalance, {
|
|
460
|
-
slotsPerEpoch: SLOTS_PER_EPOCH,
|
|
461
|
-
committeePercent: this.config.REORG_HEAD_WEIGHT_THRESHOLD,
|
|
462
|
-
});
|
|
463
|
-
const headNode = this.protoArray.getNode(headBlock.blockRoot, headBlock.payloadStatus);
|
|
464
|
-
// If headNode is unavailable, give up reorg
|
|
465
|
-
if (headNode === undefined || headNode.weight >= reorgThreshold) {
|
|
501
|
+
if (!this.isHeadWeak(headBlock.blockRoot)) {
|
|
466
502
|
return {proposerHead, isHeadTimely, notReorgedReason: NotReorgedReason.HeadBlockNotWeak};
|
|
467
503
|
}
|
|
468
504
|
|
|
469
505
|
// No reorg if parentBlock is "not strong" ie. parentBlock's weight is less than or equal to (REORG_PARENT_WEIGHT_THRESHOLD = 160)% of total attester weight
|
|
470
|
-
|
|
471
|
-
// For Gloas: measure support for the parent beacon block root regardless of its payload status by
|
|
472
|
-
// looking up the PENDING variant.
|
|
473
|
-
// https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.11/specs/gloas/fork-choice.md#modified-is_parent_strong
|
|
474
|
-
const parentThreshold = getCommitteeFraction(this.fcStore.justified.totalBalance, {
|
|
475
|
-
slotsPerEpoch: SLOTS_PER_EPOCH,
|
|
476
|
-
committeePercent: this.config.REORG_PARENT_WEIGHT_THRESHOLD,
|
|
477
|
-
});
|
|
478
|
-
const parentStrongVariant = isGloasBlock(parentBlock) ? PayloadStatus.PENDING : PayloadStatus.FULL;
|
|
479
|
-
const parentNode = this.protoArray.getNode(parentBlock.blockRoot, parentStrongVariant);
|
|
480
|
-
// If parentNode is unavailable, give up reorg
|
|
481
|
-
if (parentNode === undefined || parentNode.weight <= parentThreshold) {
|
|
506
|
+
if (!this.isParentStrong(parentBlock.blockRoot)) {
|
|
482
507
|
return {proposerHead, isHeadTimely, notReorgedReason: NotReorgedReason.ParentBlockNotStrong};
|
|
483
508
|
}
|
|
484
509
|
|
|
@@ -521,7 +546,7 @@ export class ForkChoice implements IForkChoice {
|
|
|
521
546
|
|
|
522
547
|
const timer = computeDeltasMetrics?.duration.startTimer();
|
|
523
548
|
const {
|
|
524
|
-
|
|
549
|
+
attestationDeltas,
|
|
525
550
|
equivocatingValidators,
|
|
526
551
|
oldInactiveValidators,
|
|
527
552
|
newInactiveValidators,
|
|
@@ -537,8 +562,8 @@ export class ForkChoice implements IForkChoice {
|
|
|
537
562
|
);
|
|
538
563
|
timer?.();
|
|
539
564
|
|
|
540
|
-
computeDeltasMetrics?.deltasCount.set(
|
|
541
|
-
computeDeltasMetrics?.zeroDeltasCount.set(
|
|
565
|
+
computeDeltasMetrics?.deltasCount.set(attestationDeltas.length);
|
|
566
|
+
computeDeltasMetrics?.zeroDeltasCount.set(attestationDeltas.filter((d) => d === 0).length);
|
|
542
567
|
computeDeltasMetrics?.equivocatingValidators.set(equivocatingValidators);
|
|
543
568
|
computeDeltasMetrics?.oldInactiveValidators.set(oldInactiveValidators);
|
|
544
569
|
computeDeltasMetrics?.newInactiveValidators.set(newInactiveValidators);
|
|
@@ -564,7 +589,7 @@ export class ForkChoice implements IForkChoice {
|
|
|
564
589
|
|
|
565
590
|
const currentSlot = this.fcStore.currentSlot;
|
|
566
591
|
this.protoArray.applyScoreChanges({
|
|
567
|
-
|
|
592
|
+
attestationDeltas,
|
|
568
593
|
proposerBoost,
|
|
569
594
|
justifiedEpoch: this.fcStore.justified.checkpoint.epoch,
|
|
570
595
|
justifiedRoot: this.fcStore.justified.checkpoint.rootHex,
|
|
@@ -598,6 +623,22 @@ export class ForkChoice implements IForkChoice {
|
|
|
598
623
|
return this.protoArray.nodes.filter((node) => node.bestChild === undefined);
|
|
599
624
|
}
|
|
600
625
|
|
|
626
|
+
/**
|
|
627
|
+
* weight is in EFFECTIVE_BALANCE_INCREMENTS not gwei.
|
|
628
|
+
* For compliance test use only
|
|
629
|
+
*/
|
|
630
|
+
getViableHeads(): {root: RootHex; payloadStatus: PayloadStatus; weight: number}[] {
|
|
631
|
+
return this.protoArray.getViableHeads(this.fcStore.currentSlot);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* The cached justified total active balance, in EFFECTIVE_BALANCE_INCREMENT units.
|
|
636
|
+
* For compliance test use only
|
|
637
|
+
*/
|
|
638
|
+
getJustifiedTotalActiveBalanceByIncrement(): number {
|
|
639
|
+
return this.fcStore.justified.totalBalance;
|
|
640
|
+
}
|
|
641
|
+
|
|
601
642
|
/** This is for the debug API only */
|
|
602
643
|
getAllNodes(): ProtoNode[] {
|
|
603
644
|
return this.protoArray.nodes;
|
|
@@ -1530,6 +1571,93 @@ export class ForkChoice implements IForkChoice {
|
|
|
1530
1571
|
return headDependentRoot === blockDependentRoot;
|
|
1531
1572
|
}
|
|
1532
1573
|
|
|
1574
|
+
/**
|
|
1575
|
+
* Return true if the block is "weak" ie. its weight is below REORG_HEAD_WEIGHT_THRESHOLD of the
|
|
1576
|
+
* total attester weight per slot.
|
|
1577
|
+
*
|
|
1578
|
+
* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.11/specs/phase0/fork-choice.md#is_head_weak
|
|
1579
|
+
* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.11/specs/gloas/fork-choice.md#modified-is_head_weak
|
|
1580
|
+
*/
|
|
1581
|
+
private isHeadWeak(blockRoot: RootHex): boolean {
|
|
1582
|
+
// The default variant is PENDING for gloas, FULL pre-gloas. PENDING is the variant gloas measures
|
|
1583
|
+
// support on, ie. support for the beacon block root regardless of its payload status.
|
|
1584
|
+
// Only ever called on a block already in fork choice, so a miss is a broken invariant.
|
|
1585
|
+
const node = this.protoArray.getNodeDefaultStatus(blockRoot);
|
|
1586
|
+
if (node === undefined) {
|
|
1587
|
+
// this is called for head so we should always have this in forkchoice, otherwise we have a serious error
|
|
1588
|
+
throw new ForkChoiceError({code: ForkChoiceErrorCode.MISSING_PROTO_ARRAY_BLOCK, root: blockRoot});
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
const reorgThreshold = getCommitteeFraction(this.fcStore.justified.totalBalance, {
|
|
1592
|
+
slotsPerEpoch: SLOTS_PER_EPOCH,
|
|
1593
|
+
committeePercent: this.config.REORG_HEAD_WEIGHT_THRESHOLD,
|
|
1594
|
+
});
|
|
1595
|
+
|
|
1596
|
+
if (!isForkPostGloas(this.config.getForkName(node.slot))) {
|
|
1597
|
+
return node.weight < reorgThreshold;
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
let headWeight = node.attestationScore;
|
|
1601
|
+
|
|
1602
|
+
const {equivocatingIndices} = this.fcStore;
|
|
1603
|
+
// Equivocators are extremely rare (none in normal operation), and with none the added weight is
|
|
1604
|
+
// always 0. Return before fetching the state and walking the block's committees.
|
|
1605
|
+
if (equivocatingIndices.size > 0) {
|
|
1606
|
+
const state = this.fcStore.stateGetter({stateRoot: node.stateRoot});
|
|
1607
|
+
// Only ever called on the head, so the state is always cached.
|
|
1608
|
+
// A miss is a broken invariant, not a recoverable state.
|
|
1609
|
+
if (state === null) {
|
|
1610
|
+
throw new ForkChoiceError({
|
|
1611
|
+
code: ForkChoiceErrorCode.BEACON_STATE_ERROR,
|
|
1612
|
+
error: new Error(`Missing state for isHeadWeak, blockRoot=${blockRoot} stateRoot=${node.stateRoot}`),
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
const epoch = computeEpochAtSlot(node.slot);
|
|
1617
|
+
for (let index = 0; index < state.getBeaconCommitteeCountPerSlot(epoch); index++) {
|
|
1618
|
+
for (const validatorIndex of state.getBeaconCommittee(node.slot, index)) {
|
|
1619
|
+
if (equivocatingIndices.has(validatorIndex)) {
|
|
1620
|
+
// the spec specifies to use effective_balance of the justified state
|
|
1621
|
+
let balance = this.fcStore.justified.balances[validatorIndex];
|
|
1622
|
+
if (!balance) {
|
|
1623
|
+
// 0 (zeroed by getEffectiveBalanceIncrementsZeroInactive) or undefined (validator not in
|
|
1624
|
+
// the justified state) - fall back to the head state's effective balance
|
|
1625
|
+
balance = state.effectiveBalanceIncrements[validatorIndex];
|
|
1626
|
+
}
|
|
1627
|
+
headWeight += balance;
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
return headWeight < reorgThreshold;
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
/**
|
|
1637
|
+
* Return true if the parent block is "strong" ie. its weight exceeds REORG_PARENT_WEIGHT_THRESHOLD
|
|
1638
|
+
* of the total attester weight per slot.
|
|
1639
|
+
*
|
|
1640
|
+
* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.12/specs/phase0/fork-choice.md#is_parent_strong
|
|
1641
|
+
* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.12/specs/gloas/fork-choice.md#modified-is_parent_strong
|
|
1642
|
+
*/
|
|
1643
|
+
private isParentStrong(parentRoot: RootHex): boolean {
|
|
1644
|
+
const node = this.protoArray.getNodeDefaultStatus(parentRoot);
|
|
1645
|
+
// If parentNode is unavailable, give up reorg
|
|
1646
|
+
if (node === undefined) {
|
|
1647
|
+
return false;
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
const parentThreshold = getCommitteeFraction(this.fcStore.justified.totalBalance, {
|
|
1651
|
+
slotsPerEpoch: SLOTS_PER_EPOCH,
|
|
1652
|
+
committeePercent: this.config.REORG_PARENT_WEIGHT_THRESHOLD,
|
|
1653
|
+
});
|
|
1654
|
+
|
|
1655
|
+
// pre-gloas uses get_weight() (boost-inclusive), gloas uses get_attestation_score() (boost-excluded)
|
|
1656
|
+
const parentWeight = isForkPostGloas(this.config.getForkName(node.slot)) ? node.attestationScore : node.weight;
|
|
1657
|
+
|
|
1658
|
+
return parentWeight > parentThreshold;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1533
1661
|
/**
|
|
1534
1662
|
* Return true if the block is timely for the current slot.
|
|
1535
1663
|
* Child class can overwrite this for testing purpose.
|
|
@@ -1982,9 +2110,23 @@ export class ForkChoice implements IForkChoice {
|
|
|
1982
2110
|
}
|
|
1983
2111
|
|
|
1984
2112
|
private runFastConfirmation(): void {
|
|
1985
|
-
|
|
1986
|
-
|
|
2113
|
+
const fastConfirmationRule = this.fastConfirmationRule;
|
|
2114
|
+
const fastConfirmationContext = this.fastConfirmationContext;
|
|
2115
|
+
if (!fastConfirmationRule || !fastConfirmationContext) return;
|
|
1987
2116
|
|
|
2117
|
+
if (this.fastConfirmationPaused) {
|
|
2118
|
+
// Keep consumers on a safe, available root while the rule is paused
|
|
2119
|
+
this.fcStore.confirmedRoot = this.fcStore.finalizedCheckpoint.rootHex;
|
|
2120
|
+
try {
|
|
2121
|
+
this.notifyConfirmedRoot();
|
|
2122
|
+
} catch (err) {
|
|
2123
|
+
// Runs outside the timed try/catch below; a throw would escape to the clock listener
|
|
2124
|
+
this.logger?.debug("Fast confirmation notify failed", {slot: this.fcStore.currentSlot}, err as Error);
|
|
2125
|
+
}
|
|
2126
|
+
return;
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
withObservedDuration(this.metrics?.fastConfirmation.totalDuration.startTimer(), () => {
|
|
1988
2130
|
try {
|
|
1989
2131
|
withObservedDuration(
|
|
1990
2132
|
this.metrics?.fastConfirmation.stepsDuration.startTimer({
|
|
@@ -1993,18 +2135,9 @@ export class ForkChoice implements IForkChoice {
|
|
|
1993
2135
|
() => this.updateHead()
|
|
1994
2136
|
);
|
|
1995
2137
|
|
|
1996
|
-
const result =
|
|
2138
|
+
const result = fastConfirmationRule.onSlotStartAfterPastAttestationsApplied(fastConfirmationContext);
|
|
1997
2139
|
this.fcStore.confirmedRoot = result.confirmedRoot;
|
|
1998
|
-
|
|
1999
|
-
const confirmedBlock = this.getBlockHexDefaultStatus(result.confirmedRoot);
|
|
2000
|
-
if (confirmedBlock === null) {
|
|
2001
|
-
throw new Error(`Fast confirmation produced root not in protoArray: ${result.confirmedRoot}`);
|
|
2002
|
-
}
|
|
2003
|
-
this.fcStore.notifyFastConfirmation?.({
|
|
2004
|
-
block: result.confirmedRoot,
|
|
2005
|
-
slot: confirmedBlock.slot,
|
|
2006
|
-
currentSlot: this.fcStore.currentSlot,
|
|
2007
|
-
});
|
|
2140
|
+
this.notifyConfirmedRoot();
|
|
2008
2141
|
} catch (err) {
|
|
2009
2142
|
this.logger?.debug(
|
|
2010
2143
|
"Fast confirmation failed",
|
|
@@ -100,6 +100,10 @@ export interface IForkChoice {
|
|
|
100
100
|
getHead(): ProtoBlock;
|
|
101
101
|
getConfirmedRoot(): RootHex;
|
|
102
102
|
getConfirmedBlock(): ProtoBlock | null;
|
|
103
|
+
/** Resume the fast confirmation rule; restarts from the finalized root on the next slot tick */
|
|
104
|
+
resumeFastConfirmation(): void;
|
|
105
|
+
/** Pause the fast confirmation rule (e.g. while syncing); pins the confirmed root to the finalized root */
|
|
106
|
+
pauseFastConfirmation(): void;
|
|
103
107
|
updateAndGetHead(mode: UpdateAndGetHeadOpt): {
|
|
104
108
|
head: ProtoBlock;
|
|
105
109
|
isHeadTimely?: boolean;
|
package/src/index.ts
CHANGED
|
@@ -17,7 +17,7 @@ export {
|
|
|
17
17
|
type IFastConfirmationStore,
|
|
18
18
|
getFastConfirmationMetrics,
|
|
19
19
|
} from "./forkChoice/fastConfirmation/fastConfirmationRule.ts";
|
|
20
|
-
export {ForkChoice, type ForkChoiceOpts, UpdateHeadOpt} from "./forkChoice/forkChoice.js";
|
|
20
|
+
export {ForkChoice, type ForkChoiceOpts, UpdateHeadOpt, getCommitteeFraction} from "./forkChoice/forkChoice.js";
|
|
21
21
|
export {
|
|
22
22
|
type AncestorResult,
|
|
23
23
|
AncestorStatus,
|
|
@@ -4,10 +4,10 @@ import {ProtoArrayError, ProtoArrayErrorCode} from "./errors.js";
|
|
|
4
4
|
import {NULL_VOTE_INDEX, VoteIndex} from "./interface.js";
|
|
5
5
|
|
|
6
6
|
// reuse arrays to avoid memory reallocation and gc
|
|
7
|
-
const
|
|
7
|
+
const attestationDeltas = new Array<number>();
|
|
8
8
|
|
|
9
9
|
export type DeltasResult = {
|
|
10
|
-
|
|
10
|
+
attestationDeltas: number[];
|
|
11
11
|
equivocatingValidators: number;
|
|
12
12
|
// inactive validators before beacon node started
|
|
13
13
|
oldInactiveValidators: number;
|
|
@@ -19,9 +19,9 @@ export type DeltasResult = {
|
|
|
19
19
|
};
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
* Returns a list of `
|
|
22
|
+
* Returns a list of `attestationDeltas`, where there is one delta for each of the indices in `indices`
|
|
23
23
|
*
|
|
24
|
-
* The
|
|
24
|
+
* The attestationDeltas are formed by a change between `oldBalances` and `newBalances`, and/or a change of vote in `votes`.
|
|
25
25
|
*
|
|
26
26
|
* ## Errors
|
|
27
27
|
*
|
|
@@ -46,8 +46,8 @@ export function computeDeltas(
|
|
|
46
46
|
throw new Error(`numProtoNodes must be less than NULL_VOTE_INDEX: ${numProtoNodes} >= ${NULL_VOTE_INDEX}`);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
attestationDeltas.length = numProtoNodes;
|
|
50
|
+
attestationDeltas.fill(0);
|
|
51
51
|
|
|
52
52
|
// avoid creating new variables in the loop to potentially reduce GC pressure
|
|
53
53
|
let oldBalance: number, newBalance: number;
|
|
@@ -80,7 +80,7 @@ export function computeDeltas(
|
|
|
80
80
|
});
|
|
81
81
|
}
|
|
82
82
|
oldBalance = oldBalances[vIndex] ?? 0;
|
|
83
|
-
|
|
83
|
+
attestationDeltas[currentIndex] -= oldBalance;
|
|
84
84
|
}
|
|
85
85
|
voteCurrentIndices[vIndex] = NULL_VOTE_INDEX;
|
|
86
86
|
equivocatingIndex++;
|
|
@@ -125,7 +125,7 @@ export function computeDeltas(
|
|
|
125
125
|
});
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
|
|
128
|
+
attestationDeltas[currentIndex] -= oldBalance;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
// We ignore the vote if it is not known in `indices .
|
|
@@ -138,7 +138,7 @@ export function computeDeltas(
|
|
|
138
138
|
});
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
|
|
141
|
+
attestationDeltas[nextIndex] += newBalance;
|
|
142
142
|
}
|
|
143
143
|
voteCurrentIndices[vIndex] = nextIndex;
|
|
144
144
|
newVoteValidators++;
|
|
@@ -147,13 +147,13 @@ export function computeDeltas(
|
|
|
147
147
|
}
|
|
148
148
|
} // end validator loop
|
|
149
149
|
|
|
150
|
-
if (
|
|
151
|
-
//
|
|
152
|
-
throw new Error(`
|
|
150
|
+
if (attestationDeltas.length !== numProtoNodes) {
|
|
151
|
+
// attestationDeltas array could be growed in the loop, especially if we mistakenly set the [NULL_VOTE_INDEX] to it , just to be safe
|
|
152
|
+
throw new Error(`attestationDeltas length mismatch: expected ${numProtoNodes}, got ${attestationDeltas.length}`);
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
return {
|
|
156
|
-
|
|
156
|
+
attestationDeltas,
|
|
157
157
|
equivocatingValidators,
|
|
158
158
|
oldInactiveValidators,
|
|
159
159
|
newInactiveValidators,
|
|
@@ -160,7 +160,13 @@ export type ProtoBlock = BlockExtraMeta & {
|
|
|
160
160
|
*/
|
|
161
161
|
export type ProtoNode = ProtoBlock & {
|
|
162
162
|
parent?: number;
|
|
163
|
+
/** Total weight, ie. attestationScore plus the proposer boost credited to this node */
|
|
163
164
|
weight: number;
|
|
165
|
+
/**
|
|
166
|
+
* Weight from attester votes only, excluding proposer boost.
|
|
167
|
+
* Spec: get_attestation_score
|
|
168
|
+
*/
|
|
169
|
+
attestationScore: number;
|
|
164
170
|
bestChild?: number;
|
|
165
171
|
bestDescendant?: number;
|
|
166
172
|
};
|
|
@@ -27,6 +27,12 @@ const PAYLOAD_TIMELY_THRESHOLD = Math.floor(PTC_SIZE / 2);
|
|
|
27
27
|
*/
|
|
28
28
|
const DATA_AVAILABILITY_TIMELY_THRESHOLD = Math.floor(PTC_SIZE / 2);
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Proposer boost deltas, back-propagated to the boosted node's ancestors in applyScoreChanges().
|
|
32
|
+
* Reuse the array to avoid memory reallocation and gc, as computeDeltas does for attestation deltas.
|
|
33
|
+
*/
|
|
34
|
+
const boostDeltas = new Array<number>();
|
|
35
|
+
|
|
30
36
|
/**
|
|
31
37
|
* popcount(attended AND NOT yes) — explicit False-vote count.
|
|
32
38
|
* Excludes PTC members who didn't attest (the None state).
|
|
@@ -348,7 +354,7 @@ export class ProtoArray {
|
|
|
348
354
|
* - If required, update the parents best-descendant with the current node or its best-descendant.
|
|
349
355
|
*/
|
|
350
356
|
applyScoreChanges({
|
|
351
|
-
|
|
357
|
+
attestationDeltas,
|
|
352
358
|
proposerBoost,
|
|
353
359
|
justifiedEpoch,
|
|
354
360
|
justifiedRoot,
|
|
@@ -356,7 +362,7 @@ export class ProtoArray {
|
|
|
356
362
|
finalizedRoot,
|
|
357
363
|
currentSlot,
|
|
358
364
|
}: {
|
|
359
|
-
|
|
365
|
+
attestationDeltas: number[];
|
|
360
366
|
proposerBoost: ProposerBoost | null;
|
|
361
367
|
justifiedEpoch: Epoch;
|
|
362
368
|
justifiedRoot: RootHex;
|
|
@@ -364,14 +370,17 @@ export class ProtoArray {
|
|
|
364
370
|
finalizedRoot: RootHex;
|
|
365
371
|
currentSlot: Slot;
|
|
366
372
|
}): void {
|
|
367
|
-
if (
|
|
373
|
+
if (attestationDeltas.length !== this.nodes.length) {
|
|
368
374
|
throw new ProtoArrayError({
|
|
369
375
|
code: ProtoArrayErrorCode.INVALID_DELTA_LEN,
|
|
370
|
-
deltas:
|
|
376
|
+
deltas: attestationDeltas.length,
|
|
371
377
|
indices: this.nodes.length,
|
|
372
378
|
});
|
|
373
379
|
}
|
|
374
380
|
|
|
381
|
+
boostDeltas.length = this.nodes.length;
|
|
382
|
+
boostDeltas.fill(0);
|
|
383
|
+
|
|
375
384
|
if (
|
|
376
385
|
justifiedEpoch !== this.justifiedEpoch ||
|
|
377
386
|
finalizedEpoch !== this.finalizedEpoch ||
|
|
@@ -416,18 +425,22 @@ export class ProtoArray {
|
|
|
416
425
|
// If this node's execution status has been marked invalid, then the weight of the node
|
|
417
426
|
// needs to be taken out of consideration after which the node weight will become 0
|
|
418
427
|
// for subsequent iterations of applyScoreChanges
|
|
419
|
-
const
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
//
|
|
428
|
+
const isInvalid = node.executionStatus === ExecutionStatus.Invalid;
|
|
429
|
+
const attestationDelta = isInvalid ? -node.attestationScore : attestationDeltas[nodeIndex];
|
|
430
|
+
const boostDelta = isInvalid
|
|
431
|
+
? // old boost = weight - attestationScore
|
|
432
|
+
-(node.weight - node.attestationScore)
|
|
433
|
+
: boostDeltas[nodeIndex] + currentBoost - previousBoost;
|
|
434
|
+
|
|
435
|
+
// Apply the deltas to the node. Their sum is the node's total delta, so weight is unaffected
|
|
436
|
+
// by tracking the two scores apart.
|
|
437
|
+
node.attestationScore += attestationDelta;
|
|
438
|
+
node.weight += attestationDelta + boostDelta;
|
|
439
|
+
|
|
440
|
+
// Update the parent deltas (if any)
|
|
428
441
|
const parentIndex = node.parent;
|
|
429
442
|
if (parentIndex !== undefined) {
|
|
430
|
-
const parentDelta =
|
|
443
|
+
const parentDelta = attestationDeltas[parentIndex];
|
|
431
444
|
if (parentDelta === undefined) {
|
|
432
445
|
throw new ProtoArrayError({
|
|
433
446
|
code: ProtoArrayErrorCode.INVALID_PARENT_DELTA,
|
|
@@ -435,8 +448,9 @@ export class ProtoArray {
|
|
|
435
448
|
});
|
|
436
449
|
}
|
|
437
450
|
|
|
438
|
-
// back-propagate the nodes
|
|
439
|
-
|
|
451
|
+
// back-propagate the nodes deltas to its parent
|
|
452
|
+
attestationDeltas[parentIndex] += attestationDelta;
|
|
453
|
+
boostDeltas[parentIndex] += boostDelta;
|
|
440
454
|
}
|
|
441
455
|
}
|
|
442
456
|
|
|
@@ -514,6 +528,7 @@ export class ProtoArray {
|
|
|
514
528
|
parent: parentIndex, // Points to parent's EMPTY/FULL or FULL (for transition)
|
|
515
529
|
payloadStatus: PayloadStatus.PENDING,
|
|
516
530
|
weight: 0,
|
|
531
|
+
attestationScore: 0,
|
|
517
532
|
bestChild: undefined,
|
|
518
533
|
bestDescendant: undefined,
|
|
519
534
|
};
|
|
@@ -527,6 +542,7 @@ export class ProtoArray {
|
|
|
527
542
|
parent: pendingIndex, // Points to own PENDING
|
|
528
543
|
payloadStatus: PayloadStatus.EMPTY,
|
|
529
544
|
weight: 0,
|
|
545
|
+
attestationScore: 0,
|
|
530
546
|
bestChild: undefined,
|
|
531
547
|
bestDescendant: undefined,
|
|
532
548
|
};
|
|
@@ -562,6 +578,7 @@ export class ProtoArray {
|
|
|
562
578
|
parent: this.getNodeIndexByRootAndStatus(block.parentRoot, PayloadStatus.FULL),
|
|
563
579
|
payloadStatus: PayloadStatus.FULL,
|
|
564
580
|
weight: 0,
|
|
581
|
+
attestationScore: 0,
|
|
565
582
|
bestChild: undefined,
|
|
566
583
|
bestDescendant: undefined,
|
|
567
584
|
};
|
|
@@ -647,6 +664,7 @@ export class ProtoArray {
|
|
|
647
664
|
parent: pendingIndex, // Points to own PENDING (same as EMPTY)
|
|
648
665
|
payloadStatus: PayloadStatus.FULL,
|
|
649
666
|
weight: 0,
|
|
667
|
+
attestationScore: 0,
|
|
650
668
|
bestChild: undefined,
|
|
651
669
|
bestDescendant: undefined,
|
|
652
670
|
executionStatus,
|
|
@@ -679,18 +697,11 @@ export class ProtoArray {
|
|
|
679
697
|
getPayloadRevealCounts(fromSlot: Slot, toSlot: Slot): {blocksPresent: number; payloadsRevealed: number} {
|
|
680
698
|
let blocksPresent = 0;
|
|
681
699
|
let payloadsRevealed = 0;
|
|
682
|
-
//
|
|
683
|
-
//
|
|
684
|
-
for (
|
|
685
|
-
const node = this.nodes[i];
|
|
686
|
-
if (node.slot < fromSlot) {
|
|
687
|
-
if (isGloasBlock(node) && node.payloadStatus === PayloadStatus.FULL) {
|
|
688
|
-
continue;
|
|
689
|
-
}
|
|
690
|
-
break;
|
|
691
|
-
}
|
|
700
|
+
// Full scan, nodes are in import order not slot order (an old block can be imported after newer
|
|
701
|
+
// ones during sync or reorg resolution), so we cannot stop early on an out-of-window slot
|
|
702
|
+
for (const node of this.nodes) {
|
|
692
703
|
// Count each gloas block once via its PENDING variant, pre-gloas nodes are FULL only
|
|
693
|
-
if (node.slot > toSlot || node.payloadStatus !== PayloadStatus.PENDING) {
|
|
704
|
+
if (node.slot < fromSlot || node.slot > toSlot || node.payloadStatus !== PayloadStatus.PENDING) {
|
|
694
705
|
continue;
|
|
695
706
|
}
|
|
696
707
|
blocksPresent++;
|
|
@@ -1059,7 +1070,7 @@ export class ProtoArray {
|
|
|
1059
1070
|
|
|
1060
1071
|
// update the forkchoice as the invalidation can change the entire forkchoice DAG
|
|
1061
1072
|
this.applyScoreChanges({
|
|
1062
|
-
|
|
1073
|
+
attestationDeltas: Array.from({length: this.nodes.length}, () => 0),
|
|
1063
1074
|
proposerBoost: this.previousProposerBoost,
|
|
1064
1075
|
justifiedEpoch: this.justifiedEpoch,
|
|
1065
1076
|
justifiedRoot: this.justifiedRoot,
|
|
@@ -1570,6 +1581,38 @@ export class ProtoArray {
|
|
|
1570
1581
|
return correctJustified && correctFinalized;
|
|
1571
1582
|
}
|
|
1572
1583
|
|
|
1584
|
+
/** Weights are in EFFECTIVE_BALANCE_INCREMENT units (NOT Gwei); callers scale as needed. */
|
|
1585
|
+
getViableHeads(currentSlot: Slot): {root: RootHex; payloadStatus: PayloadStatus; weight: number}[] {
|
|
1586
|
+
// Mirror the spec's `get_filtered_block_tree`, which is rooted at the store's justified
|
|
1587
|
+
// checkpoint: a viable head is a leaf (no viable descendant, i.e. `bestChild === undefined`)
|
|
1588
|
+
// that descends from the justified checkpoint block AND is itself viable for head. Iterating
|
|
1589
|
+
// all nodes without the justified-descendant filter would wrongly include FFG-viable leaves
|
|
1590
|
+
// that hang off the finalized checkpoint on a branch not under the current justified checkpoint.
|
|
1591
|
+
const justifiedVariant = this.getDefaultVariant(this.justifiedRoot);
|
|
1592
|
+
// Gloas payload-status variants of one blockRoot are distinct nodes in the spec's filtered
|
|
1593
|
+
// tree, identified by (root, payload_status, weight) — emit one entry per variant.
|
|
1594
|
+
const heads: {root: RootHex; payloadStatus: PayloadStatus; weight: number}[] = [];
|
|
1595
|
+
for (const node of this.nodes) {
|
|
1596
|
+
if (node.bestChild !== undefined || !this.nodeIsViableForHead(node, currentSlot)) {
|
|
1597
|
+
continue;
|
|
1598
|
+
}
|
|
1599
|
+
if (this.justifiedEpoch !== GENESIS_EPOCH) {
|
|
1600
|
+
// Same-root short-circuit: every payload-status variant of the justified block itself is
|
|
1601
|
+
// in the filtered tree, but `isDescendant` from the default (PENDING) variant does not
|
|
1602
|
+
// reach the sibling EMPTY/FULL variants of the same root.
|
|
1603
|
+
const descendsFromJustified =
|
|
1604
|
+
node.blockRoot === this.justifiedRoot ||
|
|
1605
|
+
(justifiedVariant !== undefined &&
|
|
1606
|
+
this.isDescendant(this.justifiedRoot, justifiedVariant, node.blockRoot, node.payloadStatus));
|
|
1607
|
+
if (!descendsFromJustified) {
|
|
1608
|
+
continue;
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
heads.push({root: node.blockRoot, payloadStatus: node.payloadStatus, weight: node.weight});
|
|
1612
|
+
}
|
|
1613
|
+
return heads;
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1573
1616
|
/**
|
|
1574
1617
|
* Return `true` if `node` is equal to or a descendant of the finalized node.
|
|
1575
1618
|
* This function helps improve performance of nodeIsViableForHead a lot by avoiding
|
|
@@ -1927,6 +1970,19 @@ export class ProtoArray {
|
|
|
1927
1970
|
return this.getNodeByIndex(blockIndex);
|
|
1928
1971
|
}
|
|
1929
1972
|
|
|
1973
|
+
/**
|
|
1974
|
+
* Return ProtoNode for the default/canonical variant in a single hash lookup
|
|
1975
|
+
* - Pre-Gloas blocks: the FULL variant
|
|
1976
|
+
* - Gloas blocks: the PENDING variant
|
|
1977
|
+
*/
|
|
1978
|
+
getNodeDefaultStatus(blockRoot: RootHex): ProtoNode | undefined {
|
|
1979
|
+
const nodeIndex = this.getDefaultNodeIndex(blockRoot);
|
|
1980
|
+
if (nodeIndex === undefined) {
|
|
1981
|
+
return undefined;
|
|
1982
|
+
}
|
|
1983
|
+
return this.getNodeByIndex(nodeIndex);
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1930
1986
|
/**
|
|
1931
1987
|
* Return MUTABLE ProtoBlock for blockRoot with explicit payload status
|
|
1932
1988
|
*
|