Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 59 additions & 8 deletions packages/fork-choice/src/forkChoice/forkChoice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,21 @@ export class ForkChoice implements IForkChoice {
return {shouldOverrideFcu: false, reason: NotReorgedReason.ParentBlockNotAvailable};
}

const currentTimeOk =
headBlock.slot === currentSlot ||
(proposalSlot === currentSlot && this.isProposingOnTime(secFromSlot, currentSlot));

// Mirror the proposer equivocation branch of getProposerHead(). The head slot's attestations are
// still queued at this point so the head is assumed weak, same as for the regular branch below.
if (currentTimeOk && this.isProposerEquivocation(headBlock)) {
this.logger?.verbose("Head proposer equivocated. Should override forkchoice update", {
blockRoot: headBlock.blockRoot,
slot: currentSlot,
proposerIndex: headBlock.proposerIndex,
});
return {shouldOverrideFcu: true, parentBlock};
}

const {prelimProposerHead, prelimNotReorgedReason} = this.getPreliminaryProposerHead(
headBlock,
parentBlock,
Expand All @@ -375,9 +390,6 @@ export class ForkChoice implements IForkChoice {
return {shouldOverrideFcu: false, reason: prelimNotReorgedReason ?? NotReorgedReason.Unknown};
}

const currentTimeOk =
headBlock.slot === currentSlot ||
(proposalSlot === currentSlot && this.isProposingOnTime(secFromSlot, currentSlot));
if (!currentTimeOk) {
return {shouldOverrideFcu: false, reason: NotReorgedReason.ReorgMoreThanOneSlot};
}
Expand Down Expand Up @@ -462,7 +474,8 @@ export class ForkChoice implements IForkChoice {
* https://github.com/ethereum/consensus-specs/pull/3034 for info about proposer boost reorg
* This function should only be called during block proposal and only be called after `updateHead()` in `updateAndGetHead()`
*
* Same as https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.4/specs/phase0/fork-choice.md#get_proposer_head
* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.14/specs/phase0/fork-choice.md#get_proposer_head
* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.14/specs/gloas/fork-choice.md#modified-get_proposer_head
*/
getProposerHead(
headBlock: ProtoBlock,
Expand Down Expand Up @@ -493,6 +506,29 @@ export class ForkChoice implements IForkChoice {
return {proposerHead, isHeadTimely, notReorgedReason: NotReorgedReason.ParentBlockNotAvailable};
}

// Half of single_slot_reorg check in the spec is done in getPreliminaryProposerHead()
const currentTimeOk = headBlock.slot + 1 === slot;
const isProposerBoostWornOff = this.proposerBoostRoot !== headBlock.blockRoot;

// Re-org more aggressively if there is a proposer equivocation in the previous slot, skipping the
// regular reorg conditions. Any known equivocation counts here, timely or not.
if (
currentTimeOk &&
isProposerBoostWornOff &&
this.isProposerEquivocation(headBlock) &&
this.isHeadWeak(headBlock.blockRoot)
Comment thread
nflaig marked this conversation as resolved.
) {
this.logger?.verbose("Performing single-slot reorg to remove weak head of equivocating proposer", {
slot,
proposerHead: parentBlock.blockRoot,
weakHead: headBlock.blockRoot,
proposerIndex: headBlock.proposerIndex,
});
proposerHead = parentBlock;

return {proposerHead, isHeadTimely};
}

const {prelimProposerHead, prelimNotReorgedReason} = this.getPreliminaryProposerHead(headBlock, parentBlock, slot);

if (prelimProposerHead === headBlock && prelimNotReorgedReason !== undefined) {
Expand All @@ -505,14 +541,11 @@ export class ForkChoice implements IForkChoice {
}

// No reorg if attempted reorg is more than a single slot
// Half of single_slot_reorg check in the spec is done in getPreliminaryProposerHead()
const currentTimeOk = headBlock.slot + 1 === slot;
if (!currentTimeOk) {
return {proposerHead, isHeadTimely, notReorgedReason: NotReorgedReason.ReorgMoreThanOneSlot};
}

// No reorg if proposer boost is still in effect
const isProposerBoostWornOff = this.proposerBoostRoot !== headBlock.blockRoot;
if (!isProposerBoostWornOff) {
return {proposerHead, isHeadTimely, notReorgedReason: NotReorgedReason.ProposerBoostNotWornOff};
}
Expand Down Expand Up @@ -1756,7 +1789,25 @@ export class ForkChoice implements IForkChoice {

// Parent is weak and from the previous slot: apply boost only if there are no early
// equivocations, ie. no other PTC-timely block at the parent's slot from the same proposer.
return !this.protoArray.hasEquivocatingBlock(parentBlock.proposerIndex, parentBlock.slot, parentBlock.blockRoot);
return !this.protoArray.hasEquivocatingBlock(
parentBlock.proposerIndex,
parentBlock.slot,
parentBlock.blockRoot,
// Only a sibling seen before the PTC deadline counts. A late released one might not have been
// seen by the proposer, so it cannot be expected to reorg it and is not denied the boost for it.
true
);
}

/**
* Return true if another block at the same slot from the same proposer is known to fork choice.
*
* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.14/specs/phase0/fork-choice.md#is_proposer_equivocation
*/
private isProposerEquivocation(block: ProtoBlock): boolean {
// Any known sibling counts, timely or not. Timeliness only matters for withholding the boost from
// the next proposer, the reorg itself is safe to attempt whenever the equivocation is visible.
return this.protoArray.hasEquivocatingBlock(block.proposerIndex, block.slot, block.blockRoot, false);
}

/**
Expand Down
21 changes: 15 additions & 6 deletions packages/fork-choice/src/protoArray/protoArray.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2008,15 +2008,19 @@ export class ProtoArray {
}

/**
* Return true if a block other than `excludeRoot` at `slot` was proposed by `proposerIndex` and
* is PTC-timely. Used by `should_apply_proposer_boost` to detect proposer equivocations.
*
* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.14/specs/gloas/fork-choice.md#new-should_apply_proposer_boost
* Return true if a block other than `excludeRoot` at `slot` was proposed by `proposerIndex`.
* `should_apply_proposer_boost` only counts PTC-timely blocks (`ptcTimelyOnly`), `is_proposer_equivocation`
* counts any known block.
*
* Iterates unique block roots (via the canonical variant) since `slot`, `proposerIndex` and
* `ptcTimeliness` are block-level properties identical across payload-status variants.
*/
hasEquivocatingBlock(proposerIndex: ValidatorIndex, slot: Slot, excludeRoot: RootHex): boolean {
hasEquivocatingBlock(
proposerIndex: ValidatorIndex,
slot: Slot,
excludeRoot: RootHex,
ptcTimelyOnly: boolean
): boolean {
for (const root of this.indices.keys()) {
if (root === excludeRoot) {
continue;
Expand All @@ -2026,7 +2030,12 @@ export class ProtoArray {
continue;
}
const node = this.nodes[nodeIndex];
if (node !== undefined && node.slot === slot && node.proposerIndex === proposerIndex && node.ptcTimeliness) {
if (
node !== undefined &&
node.slot === slot &&
node.proposerIndex === proposerIndex &&
(node.ptcTimeliness || !ptcTimelyOnly)
) {
return true;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,11 +195,22 @@ describe("Forkchoice / GetProposerHead", () => {
stateGetter: () => null,
};

/** Another block at the head slot from the same proposer, ie. a proposer equivocation */
const equivocatingHeadBlock: ProtoBlockWithWeight = {
...baseHeadBlock,
stateRoot: getStateRoot(headSlot + 100),
blockRoot: getBlockRoot(headSlot + 100),
targetRoot: getBlockRoot(headSlot + 100),
weight: 0,
};

// head block's weight < 30 is considered weak. parent block's total weight > 240 is considered strong
const testCases: {
id: string;
parentBlock: ProtoBlockWithWeight;
headBlock: ProtoBlockWithWeight;
/** Imported alongside the head, to simulate an equivocation */
siblingBlock?: ProtoBlockWithWeight;
expectReorg: boolean;
currentSlot?: Slot;
secFromSlot?: number;
Expand Down Expand Up @@ -294,6 +305,60 @@ describe("Forkchoice / GetProposerHead", () => {
secFromSlot: config.getProposerReorgCutoffMs(ForkName.phase0) / 1000 + 1,
expectedNotReorgedReason: NotReorgedReason.NotProposingOnTime,
},
{
id: "Reorg weak equivocating head even if head is timely",
parentBlock: {...baseParentHeadBlock},
headBlock: {...baseHeadBlock, timeliness: true},
siblingBlock: equivocatingHeadBlock,
expectReorg: true,
},
{
id: "Reorg weak equivocating head even if parent is weak",
parentBlock: {...baseParentHeadBlock, weight: 211},
headBlock: {...baseHeadBlock},
siblingBlock: equivocatingHeadBlock,
expectReorg: true,
},
{
id: "Reorg weak equivocating head even if not proposing on time",
parentBlock: {...baseParentHeadBlock},
headBlock: {...baseHeadBlock},
siblingBlock: equivocatingHeadBlock,
expectReorg: true,
secFromSlot: config.getProposerReorgCutoffMs(ForkName.phase0) / 1000 + 1,
},
{
id: "Reorg weak equivocating head regardless of the equivocating block's PTC timeliness",
parentBlock: {...baseParentHeadBlock},
headBlock: {...baseHeadBlock, timeliness: true},
siblingBlock: {...equivocatingHeadBlock, ptcTimeliness: true},
expectReorg: true,
},
{
id: "No equivocation reorg if head is strong",
parentBlock: {...baseParentHeadBlock},
headBlock: {...baseHeadBlock, timeliness: true, weight: 30},
siblingBlock: equivocatingHeadBlock,
expectReorg: false,
expectedNotReorgedReason: NotReorgedReason.HeadBlockIsTimely,
},
{
id: "No equivocation reorg if current slot is more than one slot from head block",
parentBlock: {...baseParentHeadBlock},
headBlock: {...baseHeadBlock},
siblingBlock: equivocatingHeadBlock,
expectReorg: false,
currentSlot: headSlot + 2,
expectedNotReorgedReason: NotReorgedReason.ReorgMoreThanOneSlot,
},
{
id: "No equivocation reorg if the other block at the head slot is from a different proposer",
parentBlock: {...baseParentHeadBlock},
headBlock: {...baseHeadBlock, timeliness: true},
siblingBlock: {...equivocatingHeadBlock, proposerIndex: 1},
expectReorg: false,
expectedNotReorgedReason: NotReorgedReason.HeadBlockIsTimely,
},
];

beforeEach(() => {
Expand All @@ -304,6 +369,7 @@ describe("Forkchoice / GetProposerHead", () => {
id,
parentBlock,
headBlock,
siblingBlock,
expectReorg,
currentSlot: proposalSlot,
secFromSlot,
Expand All @@ -313,11 +379,14 @@ describe("Forkchoice / GetProposerHead", () => {
it(`${id}`, async () => {
protoArr.onBlock(parentBlock, parentBlock.slot, null);
protoArr.onBlock(headBlock, headBlock.slot, null);
if (siblingBlock) {
protoArr.onBlock(siblingBlock, siblingBlock.slot, null);
}

const currentSlot = proposalSlot ?? headBlock.slot + 1;
const currentSecFromSlot = secFromSlot ?? 0;
protoArr.applyScoreChanges({
attestationDeltas: [0, parentBlock.weight, headBlock.weight],
attestationDeltas: [0, parentBlock.weight, headBlock.weight, ...(siblingBlock ? [siblingBlock.weight] : [])],
proposerBoost: null,
justifiedEpoch: genesisEpoch,
justifiedRoot: genesisRoot,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,10 +195,21 @@ describe("Forkchoice / shouldOverrideForkChoiceUpdate", () => {
stateGetter: () => null,
};

/** Another block at the head slot from the same proposer, ie. a proposer equivocation */
const equivocatingHeadBlock: ProtoBlockWithWeight = {
...baseHeadBlock,
stateRoot: getStateRoot(headSlot + 100),
blockRoot: getBlockRoot(headSlot + 100),
targetRoot: getBlockRoot(headSlot + 100),
weight: 0,
};

const testCases: {
id: string;
parentBlock: ProtoBlockWithWeight;
headBlock: ProtoBlockWithWeight;
/** Imported alongside the head, to simulate an equivocation */
siblingBlock?: ProtoBlockWithWeight;
expectReorg: boolean;
currentSlot?: Slot;
expectedNotReorgedReason?: NotReorgedReason;
Expand Down Expand Up @@ -260,6 +271,37 @@ describe("Forkchoice / shouldOverrideForkChoiceUpdate", () => {
expectReorg: false,
expectedNotReorgedReason: NotReorgedReason.ParentBlockDistanceMoreThanOneSlot,
},
{
id: "Reorg equivocating head even if head is timely",
parentBlock: {...baseParentHeadBlock},
headBlock: {...baseHeadBlock, timeliness: true},
siblingBlock: equivocatingHeadBlock,
expectReorg: true,
},
{
id: "Reorg equivocating head even if reorg spans more than a single slot",
parentBlock: {...baseParentHeadBlock},
headBlock: {...baseHeadBlock, slot: headSlot + 1},
siblingBlock: {...equivocatingHeadBlock, slot: headSlot + 1},
expectReorg: true,
},
{
id: "No equivocation reorg if current slot is more than one slot from head block",
parentBlock: {...baseParentHeadBlock},
headBlock: {...baseHeadBlock},
siblingBlock: equivocatingHeadBlock,
expectReorg: false,
currentSlot: headSlot + 2,
expectedNotReorgedReason: NotReorgedReason.ReorgMoreThanOneSlot,
},
{
id: "No equivocation reorg if the other block at the head slot is from a different proposer",
parentBlock: {...baseParentHeadBlock},
headBlock: {...baseHeadBlock, timeliness: true},
siblingBlock: {...equivocatingHeadBlock, proposerIndex: 1},
expectReorg: false,
expectedNotReorgedReason: NotReorgedReason.HeadBlockIsTimely,
},
];

beforeEach(() => {
Expand All @@ -270,6 +312,7 @@ describe("Forkchoice / shouldOverrideForkChoiceUpdate", () => {
id,
parentBlock,
headBlock,
siblingBlock,
expectReorg,
currentSlot: blockSeenSlot,
expectedNotReorgedReason,
Expand All @@ -278,6 +321,9 @@ describe("Forkchoice / shouldOverrideForkChoiceUpdate", () => {
it(id, async () => {
protoArr.onBlock(parentBlock, parentBlock.slot, null);
protoArr.onBlock(headBlock, headBlock.slot, null);
if (siblingBlock) {
protoArr.onBlock(siblingBlock, siblingBlock.slot, null);
}

const secFromSlot = 0;
const currentSlot = blockSeenSlot ?? headBlock.slot;
Expand Down
3 changes: 0 additions & 3 deletions specrefs/.ethspecify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,6 @@ exceptions:
- get_weight#phase0
- is_candidate_block#phase0
- is_proposer#phase0
- is_proposer_equivocation#phase0
- max_compressed_len#phase0
- max_message_size#phase0
- record_block_timeliness#phase0
Expand Down Expand Up @@ -313,8 +312,6 @@ exceptions:
- process_proposer_slashing#gloas
- process_slot#gloas
- process_withdrawals#gloas
- record_block_timeliness#gloas
- should_apply_proposer_boost#gloas
- update_builder_pending_withdrawals#gloas
- update_latest_messages#gloas
- update_next_withdrawal_builder_index#gloas
Expand Down
Loading
Loading