Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace findAttesterDependentRoot with getDependantRoot #4555

Merged
merged 5 commits into from
Sep 16, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import {
getBlockRootAtSlot,
} from "@lodestar/state-transition";
import {toHexString} from "@chainsafe/ssz";
import {IForkChoice} from "@lodestar/fork-choice";
import {IForkChoice, EpochDifference} from "@lodestar/fork-choice";
import {toHex} from "@lodestar/utils";
import {MapDef} from "../../util/map.js";
import {intersectUint8Arrays, IntersectResult} from "../../util/bitArray.js";
import {pruneBySlot} from "./utils.js";
Expand Down Expand Up @@ -433,14 +434,20 @@ export function isValidAttestationData(
// Otherwise the shuffling is determined by the block at the end of the target epoch
// minus the shuffling lookahead (usually 2). We call this the "pivot".
const pivotSlot = computeStartSlotAtEpoch(targetEpoch - 1) - 1;
nazarhussain marked this conversation as resolved.
Show resolved Hide resolved
const statePivotBlockRoot = toHexString(getBlockRootAtSlot(state, pivotSlot));
const stateDependantRoot = toHexString(getBlockRootAtSlot(state, pivotSlot));

// Use fork choice's view of the block DAG to quickly evaluate whether the attestation's
// pivot block is the same as the current state's pivot block. If it is, then the
// attestation's shuffling is the same as the current state's.
// To account for skipped slots, find the first block at *or before* the pivot slot.
const pivotBlockRoot = forkChoice.findAttesterDependentRoot(data.beaconBlockRoot);
return pivotBlockRoot === statePivotBlockRoot;
const beaconBlockRootHex = toHex(data.beaconBlockRoot);
const beaconBlock = forkChoice.getBlockHex(beaconBlockRootHex);
if (!beaconBlock) {
throw Error(`Attestation data.beaconBlockRoot ${beaconBlockRootHex} not found in forkchoice`);
}

const attestationDependantRoot = forkChoice.getDependantRoot(beaconBlock, EpochDifference.previous);
return attestationDependantRoot === stateDependantRoot;
}

function flagIsTimelySource(flag: number): boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,15 +71,14 @@ describe("AggregatedAttestationPool", function () {
it(name, function () {
const aggregationBits = new BitArray(new Uint8Array(attestingBits), 8);
pool.add({...attestation, aggregationBits}, aggregationBits.getTrueBitIndexes().length, committee);
forkchoiceStub.findAttesterDependentRoot.returns(ZERO_HASH_HEX);
forkchoiceStub.getDependantRoot.returns(ZERO_HASH_HEX);
expect(pool.getAttestationsForBlock(forkchoiceStub, altairState).length > 0).to.equal(
isReturned,
"Wrong attestation isReturned"
);
expect(
forkchoiceStub.findAttesterDependentRoot.calledOnce,
"forkchoice should be called to check pivot block"
).to.equal(true);
expect(forkchoiceStub.getDependantRoot.calledOnce, "forkchoice should be called to check pivot block").to.equal(
true
);
});
}

Expand All @@ -99,13 +98,12 @@ describe("AggregatedAttestationPool", function () {
// all attesters are not seen
const attestingIndices = [2, 3];
pool.add(attestation, attestingIndices.length, committee);
forkchoiceStub.findAttesterDependentRoot.returns("0xWeird");
forkchoiceStub.getDependantRoot.returns("0xWeird");
expect(pool.getAttestationsForBlock(forkchoiceStub, altairState)).to.be.deep.equal(
[],
"no attestation since incorrect pivot block root"
);
expect(forkchoiceStub.findAttesterDependentRoot.calledOnce, "forkchoice should be called to check pivot block").to
.be.true;
expect(forkchoiceStub.getDependantRoot.calledOnce, "forkchoice should be called to check pivot block").to.be.true;
});
});

Expand Down
2 changes: 1 addition & 1 deletion packages/beacon-node/test/utils/mocks/chain/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ function mockForkChoice(): IForkChoice {
getBlockSummariesAtSlot: () => [block],
getCommonAncestorDistance: () => null,
validateLatestHash: () => {},
findAttesterDependentRoot: () => block.blockRoot,
getDependantRoot: () => rootHex,
};
}

Expand Down
84 changes: 50 additions & 34 deletions packages/fork-choice/src/forkChoice/forkChoice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import {ProtoArray} from "../protoArray/protoArray.js";
import {ProtoArrayError, ProtoArrayErrorCode} from "../protoArray/errors.js";

import {ForkChoiceError, ForkChoiceErrorCode, InvalidBlockCode, InvalidAttestationCode} from "./errors.js";
import {IForkChoice, LatestMessage, QueuedAttestation, PowBlockHex} from "./interface.js";
import {IForkChoice, LatestMessage, QueuedAttestation, PowBlockHex, EpochDifference} from "./interface.js";
import {IForkChoiceStore, CheckpointWithHex, toCheckpointWithHex, JustifiedBalances} from "./store.js";

/* eslint-disable max-len */
Expand Down Expand Up @@ -773,40 +773,56 @@ export class ForkChoice implements IForkChoice {
}

/**
* Find dependent root of a head block
* epoch - 2 | epoch - 1 | epoch
* pivotSlot | - | blockSlot
* A dependant root is the block root of the last block before the state transition that decided a specific shuffling
*
* For proposer shuffling with 0 epochs of lookahead = previous immediate epoch transition
* For attester shuffling with 1 epochs of lookahead = last epoch's epoch transition
*
* ```
* epoch: 0 1 2 3 4
* |-------|-------|=======|-------|
* dependant root A -------------^
* dependant root B -----^
* ```
* - proposer shuffling for a block in epoch 2: dependant root A (EpochDifference = 0)
* - attester shuffling for a block in epoch 2: dependant root B (EpochDifference = 1)
*/
findAttesterDependentRoot(headBlockHash: Root): RootHex | null {
let block = this.getBlock(headBlockHash);
if (!block) return null;
const {slot} = block;
// The shuffling is determined by the block at the end of the target epoch
// minus the shuffling lookahead (usually 2). We call this the "pivot".
const pivotSlot = computeStartSlotAtEpoch(computeEpochAtSlot(slot) - 1) - 1;

// 1st hop: target block
block = this.getBlockHex(block.targetRoot);
if (!block) return null;
if (block.slot <= pivotSlot) return block.blockRoot;
// or parent of target block
block = this.getBlockHex(block.parentRoot);
if (!block) return null;
if (block.slot <= pivotSlot) return block.blockRoot;

// 2nd hop: go back 1 more epoch, target block
block = this.getBlockHex(block.targetRoot);
if (!block) return null;
if (block.slot <= pivotSlot) return block.blockRoot;
// or parent of target block
block = this.getBlockHex(block.parentRoot);
if (!block) return null;
// most of the time, in a stable network, it reaches here
if (block.slot <= pivotSlot) return block.blockRoot;

throw Error(
`Not able to find attester dependent root for head ${headBlockHash}, slot ${slot}, pivotSlot ${pivotSlot}, last tried slot ${block.slot}`
);
getDependantRoot(block: ProtoBlock, epochDifference: EpochDifference): RootHex {
// The navigation at the end of the while loop will always progress backwards,
// jumping to a block with a strictly less slot number. So the condition `blockEpoch < atEpoch`
// is guaranteed to happen. Given the use of target blocks for faster navigation, it will take
// at most `2 * (blockEpoch - atEpoch + 1)` iterations to find the dependant root.

const beforeSlot = block.slot - (block.slot % SLOTS_PER_EPOCH) - epochDifference * SLOTS_PER_EPOCH;
nazarhussain marked this conversation as resolved.
Show resolved Hide resolved

// Special case close to genesis block, return the genesis block root
if (beforeSlot <= 0) {
const genesisBlock = this.protoArray.nodes[0];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If that's the usage may be we make the getNodeFromIndex function public and then use it here.

Copy link
Contributor Author

@dapplion dapplion Sep 16, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but getNodeFromIndex would throw PROTO_ARRAY_ERROR_INVALID_NODE_INDEX, while this implementation would throw Genesis block not available, which would be much easier to debug if a user reports. Would prefer to stick to this one-line re-implementation, as it's quite peculiar use of the nodes array

if (genesisBlock === undefined || genesisBlock.slot !== 0) {
throw Error("Genesis block not available");
}
return genesisBlock.blockRoot;
}

// eslint-disable-next-line no-constant-condition
while (true) {
nazarhussain marked this conversation as resolved.
Show resolved Hide resolved
// Dependant root must be in epoch less than `beforeSlot`
if (block.slot < beforeSlot) {
return block.blockRoot;
}

// Skip one last jump if there's no skipped slot at first slot of the epoch
if (block.slot === beforeSlot) {
return block.parentRoot;
}

block =
block.blockRoot === block.targetRoot
? // For the first slot of the epoch, a block is it's own target
this.protoArray.getBlockReadonly(block.parentRoot)
: // else we can navigate much faster jumping to the target block
this.protoArray.getBlockReadonly(block.targetRoot);
}
}

private getPreMergeExecStatus(executionStatus: MaybeValidExecutionStatus): ExecutionStatus.PreMerge {
Expand Down
25 changes: 22 additions & 3 deletions packages/fork-choice/src/forkChoice/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ export type CheckpointHexWithBalance = {
balances: EffectiveBalanceIncrements;
};

export enum EpochDifference {
current = 0,
previous = 1,
}

export interface IForkChoice {
irrecoverableError?: Error;
/**
Expand Down Expand Up @@ -157,9 +162,23 @@ export interface IForkChoice {
* Optimistic sync validate till validated latest hash, invalidate any decendant branch if invalidated branch decendant provided
*/
validateLatestHash(execResponse: LVHExecResponse): void;
/** Find attester dependent root of a block */
findAttesterDependentRoot(headBlockHash: Root): RootHex | null;
/** Get critical error from forkChoice */

/**
* A dependant root is the block root of the last block before the state transition that decided a specific shuffling
*
* For proposer shuffling with 0 epochs of lookahead = previous immediate epoch transition
* For attester shuffling with 1 epochs of lookahead = last epoch's epoch transition
*
* ```
* epoch: 0 1 2 3 4
* |-------|-------|=======|-------|
* dependant root A -------------^
* dependant root B -----^
* ```
* - proposer shuffling for a block in epoch 2: dependant root A (EpochDifference = 0)
* - attester shuffling for a block in epoch 2: dependant root B (EpochDifference = 1)
*/
getDependantRoot(block: ProtoBlock, atEpochDiff: EpochDifference): RootHex;
}

/** Same to the PowBlock but we want RootHex to work with forkchoice conveniently */
Expand Down
2 changes: 1 addition & 1 deletion packages/fork-choice/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export {
} from "./protoArray/interface.js";

export {ForkChoice, ForkChoiceOpts, assertValidTerminalPowBlock} from "./forkChoice/forkChoice.js";
export {IForkChoice, PowBlockHex} from "./forkChoice/interface.js";
export {IForkChoice, PowBlockHex, EpochDifference} from "./forkChoice/interface.js";
export {ForkChoiceStore, IForkChoiceStore, CheckpointWithHex, JustifiedBalancesGetter} from "./forkChoice/store.js";
export {
InvalidAttestation,
Expand Down
10 changes: 10 additions & 0 deletions packages/fork-choice/src/protoArray/protoArray.ts
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,7 @@ export class ProtoArray {
return this.getNodeByIndex(blockIndex);
}

/** Return MUTABLE ProtoBlock for blockRoot (spreads properties) */
getBlock(blockRoot: RootHex): ProtoBlock | undefined {
const node = this.getNode(blockRoot);
if (!node) {
Expand All @@ -877,6 +878,15 @@ export class ProtoArray {
};
}

/** Return NON-MUTABLE ProtoBlock for blockRoot (does not spread properties) */
getBlockReadonly(blockRoot: RootHex): ProtoBlock {
const node = this.getNode(blockRoot);
if (!node) {
throw Error(`No block for root ${blockRoot}`);
}
return node;
}

/**
* Returns `true` if the `descendantRoot` has an ancestor with `ancestorRoot`.
* Always returns `false` if either input roots are unknown.
Expand Down
20 changes: 17 additions & 3 deletions packages/fork-choice/test/unit/forkChoice/forkChoice.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@ import {fromHexString} from "@chainsafe/ssz";
import {RootHex} from "@lodestar/types";
import {computeEpochAtSlot} from "@lodestar/state-transition";
import {SLOTS_PER_EPOCH} from "@lodestar/params";
import {ForkChoice, IForkChoiceStore, ProtoBlock, ProtoArray, ExecutionStatus} from "../../../src/index.js";
import {
ForkChoice,
IForkChoiceStore,
ProtoBlock,
ProtoArray,
ExecutionStatus,
EpochDifference,
} from "../../../src/index.js";

describe("Forkchoice", function () {
const genesisSlot = 0;
Expand Down Expand Up @@ -139,13 +146,20 @@ describe("Forkchoice", function () {
];

for (const {name, skippedSlots, pivotSlot} of dependentRootTestCases) {
it(`findAttesterDependentRoot - ${name}`, () => {
it(`getDependantRoot (EpochDifference.previous) - ${name}`, () => {
const slot = 2 * 32 + 5;
populateProtoArray(slot, skippedSlots);
const forkchoice = new ForkChoice(config, fcStore, protoArr);

// Initial point
const blockRoot = getBlockRoot(slot);
const block = forkchoice.getBlockHex(blockRoot);
if (!block) throw Error(`No block for blockRoot ${blockRoot}`);

// Expected
const pivotRoot = getBlockRoot(pivotSlot);
expect(forkchoice.findAttesterDependentRoot(fromHexString(blockRoot))).to.be.equal(

expect(forkchoice.getDependantRoot(block, EpochDifference.previous)).to.be.equal(
pivotRoot,
"incorrect attester dependent root"
);
Expand Down