Skip to content
Merged
119 changes: 94 additions & 25 deletions packages/fork-choice/src/forkChoice/forkChoice.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {ChainForkConfig} from "@lodestar/config";
import {MIN_SEED_LOOKAHEAD, SLOTS_PER_EPOCH} from "@lodestar/params";
import {MIN_SEED_LOOKAHEAD, SLOTS_PER_EPOCH, isForkPostGloas} from "@lodestar/params";
import {
DataAvailabilityStatus,
EffectiveBalanceIncrements,
Expand Down Expand Up @@ -455,30 +455,12 @@ export class ForkChoice implements IForkChoice {
}

// No reorg if headBlock is "not weak" ie. headBlock's weight exceeds (REORG_HEAD_WEIGHT_THRESHOLD = 20)% of total attester weight
// https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.4/specs/phase0/fork-choice.md#is_head_weak
const reorgThreshold = getCommitteeFraction(this.fcStore.justified.totalBalance, {
slotsPerEpoch: SLOTS_PER_EPOCH,
committeePercent: this.config.REORG_HEAD_WEIGHT_THRESHOLD,
});
const headNode = this.protoArray.getNode(headBlock.blockRoot, headBlock.payloadStatus);
// If headNode is unavailable, give up reorg
if (headNode === undefined || headNode.weight >= reorgThreshold) {
if (!this.isHeadWeak(headBlock.blockRoot)) {
return {proposerHead, isHeadTimely, notReorgedReason: NotReorgedReason.HeadBlockNotWeak};
}

// 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
// https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.11/specs/phase0/fork-choice.md#is_parent_strong
// For Gloas: measure support for the parent beacon block root regardless of its payload status by
// looking up the PENDING variant.
// https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.11/specs/gloas/fork-choice.md#modified-is_parent_strong
const parentThreshold = getCommitteeFraction(this.fcStore.justified.totalBalance, {
slotsPerEpoch: SLOTS_PER_EPOCH,
committeePercent: this.config.REORG_PARENT_WEIGHT_THRESHOLD,
});
const parentStrongVariant = isGloasBlock(parentBlock) ? PayloadStatus.PENDING : PayloadStatus.FULL;
const parentNode = this.protoArray.getNode(parentBlock.blockRoot, parentStrongVariant);
// If parentNode is unavailable, give up reorg
if (parentNode === undefined || parentNode.weight <= parentThreshold) {
if (!this.isParentStrong(parentBlock.blockRoot)) {
return {proposerHead, isHeadTimely, notReorgedReason: NotReorgedReason.ParentBlockNotStrong};
}

Expand Down Expand Up @@ -521,7 +503,7 @@ export class ForkChoice implements IForkChoice {

const timer = computeDeltasMetrics?.duration.startTimer();
const {
deltas,
attestationDeltas,
equivocatingValidators,
oldInactiveValidators,
newInactiveValidators,
Expand All @@ -537,8 +519,8 @@ export class ForkChoice implements IForkChoice {
);
timer?.();

computeDeltasMetrics?.deltasCount.set(deltas.length);
computeDeltasMetrics?.zeroDeltasCount.set(deltas.filter((d) => d === 0).length);
computeDeltasMetrics?.deltasCount.set(attestationDeltas.length);
computeDeltasMetrics?.zeroDeltasCount.set(attestationDeltas.filter((d) => d === 0).length);
computeDeltasMetrics?.equivocatingValidators.set(equivocatingValidators);
computeDeltasMetrics?.oldInactiveValidators.set(oldInactiveValidators);
computeDeltasMetrics?.newInactiveValidators.set(newInactiveValidators);
Expand All @@ -564,7 +546,7 @@ export class ForkChoice implements IForkChoice {

const currentSlot = this.fcStore.currentSlot;
this.protoArray.applyScoreChanges({
deltas,
attestationDeltas,
proposerBoost,
justifiedEpoch: this.fcStore.justified.checkpoint.epoch,
justifiedRoot: this.fcStore.justified.checkpoint.rootHex,
Expand Down Expand Up @@ -1546,6 +1528,93 @@ export class ForkChoice implements IForkChoice {
return headDependentRoot === blockDependentRoot;
}

/**
* Return true if the block is "weak" ie. its weight is below REORG_HEAD_WEIGHT_THRESHOLD of the
* total attester weight per slot.
*
* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.11/specs/phase0/fork-choice.md#is_head_weak
* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.11/specs/gloas/fork-choice.md#modified-is_head_weak
*/
private isHeadWeak(blockRoot: RootHex): boolean {
// The default variant is PENDING for gloas, FULL pre-gloas. PENDING is the variant gloas measures
// support on, ie. support for the beacon block root regardless of its payload status.
// Only ever called on a block already in fork choice, so a miss is a broken invariant.
const node = this.protoArray.getNodeDefaultStatus(blockRoot);
Comment thread
ensi321 marked this conversation as resolved.
if (node === undefined) {
// this is called for head so we should always have this in forkchoice, otherwise we have a serious error
throw new ForkChoiceError({code: ForkChoiceErrorCode.MISSING_PROTO_ARRAY_BLOCK, root: blockRoot});
Comment thread
ensi321 marked this conversation as resolved.
}

const reorgThreshold = getCommitteeFraction(this.fcStore.justified.totalBalance, {
slotsPerEpoch: SLOTS_PER_EPOCH,
committeePercent: this.config.REORG_HEAD_WEIGHT_THRESHOLD,
});

if (!isForkPostGloas(this.config.getForkName(node.slot))) {
return node.weight < reorgThreshold;
}

let headWeight = node.attestationScore;

const {equivocatingIndices} = this.fcStore;
// Equivocators are extremely rare (none in normal operation), and with none the added weight is
// always 0. Return before fetching the state and walking the block's committees.
if (equivocatingIndices.size > 0) {
const state = this.fcStore.stateGetter({stateRoot: node.stateRoot});
// Only ever called on the head, so the state is always cached.
// A miss is a broken invariant, not a recoverable state.
if (state === null) {
throw new ForkChoiceError({
code: ForkChoiceErrorCode.BEACON_STATE_ERROR,
error: new Error(`Missing state for isHeadWeak, blockRoot=${blockRoot} stateRoot=${node.stateRoot}`),
});
}

const epoch = computeEpochAtSlot(node.slot);
for (let index = 0; index < state.getBeaconCommitteeCountPerSlot(epoch); index++) {
for (const validatorIndex of state.getBeaconCommittee(node.slot, index)) {
if (equivocatingIndices.has(validatorIndex)) {
// the spec specifies to use effective_balance of the justified state
let balance = this.fcStore.justified.balances[validatorIndex];
if (!balance) {
// 0 (zeroed by getEffectiveBalanceIncrementsZeroInactive) or undefined (validator not in
// the justified state) - fall back to the head state's effective balance
balance = state.effectiveBalanceIncrements[validatorIndex];
}
headWeight += balance;
}
}
}
}

return headWeight < reorgThreshold;
}

/**
* Return true if the parent block is "strong" ie. its weight exceeds REORG_PARENT_WEIGHT_THRESHOLD
* of the total attester weight per slot.
*
* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.12/specs/phase0/fork-choice.md#is_parent_strong
* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.12/specs/gloas/fork-choice.md#modified-is_parent_strong
*/
private isParentStrong(parentRoot: RootHex): boolean {
const node = this.protoArray.getNodeDefaultStatus(parentRoot);
// If parentNode is unavailable, give up reorg
if (node === undefined) {
return false;
}

const parentThreshold = getCommitteeFraction(this.fcStore.justified.totalBalance, {
slotsPerEpoch: SLOTS_PER_EPOCH,
committeePercent: this.config.REORG_PARENT_WEIGHT_THRESHOLD,
});

// pre-gloas uses get_weight() (boost-inclusive), gloas uses get_attestation_score() (boost-excluded)
const parentWeight = isForkPostGloas(this.config.getForkName(node.slot)) ? node.attestationScore : node.weight;

return parentWeight > parentThreshold;
}

/**
* Return true if the block is timely for the current slot.
* Child class can overwrite this for testing purpose.
Expand Down
26 changes: 13 additions & 13 deletions packages/fork-choice/src/protoArray/computeDeltas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import {ProtoArrayError, ProtoArrayErrorCode} from "./errors.js";
import {NULL_VOTE_INDEX, VoteIndex} from "./interface.js";

// reuse arrays to avoid memory reallocation and gc
const deltas = new Array<number>();
const attestationDeltas = new Array<number>();

export type DeltasResult = {
deltas: number[];
attestationDeltas: number[];
equivocatingValidators: number;
// inactive validators before beacon node started
oldInactiveValidators: number;
Expand All @@ -19,9 +19,9 @@ export type DeltasResult = {
};

/**
* Returns a list of `deltas`, where there is one delta for each of the indices in `indices`
* Returns a list of `attestationDeltas`, where there is one delta for each of the indices in `indices`
*
* The deltas are formed by a change between `oldBalances` and `newBalances`, and/or a change of vote in `votes`.
* The attestationDeltas are formed by a change between `oldBalances` and `newBalances`, and/or a change of vote in `votes`.
*
* ## Errors
*
Expand All @@ -46,8 +46,8 @@ export function computeDeltas(
throw new Error(`numProtoNodes must be less than NULL_VOTE_INDEX: ${numProtoNodes} >= ${NULL_VOTE_INDEX}`);
}

deltas.length = numProtoNodes;
deltas.fill(0);
attestationDeltas.length = numProtoNodes;
attestationDeltas.fill(0);

// avoid creating new variables in the loop to potentially reduce GC pressure
let oldBalance: number, newBalance: number;
Expand Down Expand Up @@ -80,7 +80,7 @@ export function computeDeltas(
});
}
oldBalance = oldBalances[vIndex] ?? 0;
deltas[currentIndex] -= oldBalance;
attestationDeltas[currentIndex] -= oldBalance;
}
voteCurrentIndices[vIndex] = NULL_VOTE_INDEX;
equivocatingIndex++;
Expand Down Expand Up @@ -125,7 +125,7 @@ export function computeDeltas(
});
}

deltas[currentIndex] -= oldBalance;
attestationDeltas[currentIndex] -= oldBalance;
}

// We ignore the vote if it is not known in `indices .
Expand All @@ -138,7 +138,7 @@ export function computeDeltas(
});
}

deltas[nextIndex] += newBalance;
attestationDeltas[nextIndex] += newBalance;
}
voteCurrentIndices[vIndex] = nextIndex;
newVoteValidators++;
Expand All @@ -147,13 +147,13 @@ export function computeDeltas(
}
} // end validator loop

if (deltas.length !== numProtoNodes) {
// deltas array could be growed in the loop, especially if we mistakenly set the [NULL_VOTE_INDEX] to it , just to be safe
throw new Error(`deltas length mismatch: expected ${numProtoNodes}, got ${deltas.length}`);
if (attestationDeltas.length !== numProtoNodes) {
// attestationDeltas array could be growed in the loop, especially if we mistakenly set the [NULL_VOTE_INDEX] to it , just to be safe
throw new Error(`attestationDeltas length mismatch: expected ${numProtoNodes}, got ${attestationDeltas.length}`);
}

return {
deltas,
attestationDeltas,
equivocatingValidators,
oldInactiveValidators,
newInactiveValidators,
Expand Down
6 changes: 6 additions & 0 deletions packages/fork-choice/src/protoArray/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,13 @@ export type ProtoBlock = BlockExtraMeta & {
*/
export type ProtoNode = ProtoBlock & {
parent?: number;
/** Total weight, ie. attestationScore plus the proposer boost credited to this node */
weight: number;
/**
* Weight from attester votes only, excluding proposer boost.
* Spec: get_attestation_score
*/
attestationScore: number;
bestChild?: number;
bestDescendant?: number;
};
Loading
Loading