Skip to content

Commit

Permalink
feat: add proposer boost reorg flag (#6652)
Browse files Browse the repository at this point in the history
* Second batch of changes

* Wire proposer boost related code to block production

* Update test

* Update metrics

* Update packages/beacon-node/test/e2e/chain/proposerBoostReorg.test.ts

Co-authored-by: twoeths <tuyen@chainsafe.io>

* Address comment

* Update packages/beacon-node/src/metrics/metrics/beacon.ts

Co-authored-by: Nico Flaig <nflaig@protonmail.com>

* Compute hash treet root of updatedPrepareState

* computeStateHashTreeRoot after prepareExecutionPayload

* fix build issue

* Fix spec test

* lint

* Remove Enabled suffix

* Fix merge

* Add alias

* Update packages/cli/src/options/beaconNodeOptions/chain.ts

Co-authored-by: Nico Flaig <nflaig@protonmail.com>

* chore: add predictProposerHead regen enum

---------

Co-authored-by: twoeths <tuyen@chainsafe.io>
Co-authored-by: Nico Flaig <nflaig@protonmail.com>
Co-authored-by: Tuyen Nguyen <vutuyen2636@gmail.com>
  • Loading branch information
4 people authored Jun 8, 2024
1 parent c153277 commit f6d3bce
Show file tree
Hide file tree
Showing 24 changed files with 398 additions and 58 deletions.
6 changes: 3 additions & 3 deletions packages/beacon-node/src/api/impl/validator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ export function getValidatorApi({
// forkChoice.updateTime() might have already been called by the onSlot clock
// handler, in which case this should just return.
chain.forkChoice.updateTime(slot);
parentBlockRoot = fromHexString(chain.recomputeForkChoiceHead().blockRoot);
parentBlockRoot = fromHexString(chain.getProposerHead(slot).blockRoot);
} else {
parentBlockRoot = inParentBlockRoot;
}
Expand Down Expand Up @@ -430,7 +430,7 @@ export function getValidatorApi({
// forkChoice.updateTime() might have already been called by the onSlot clock
// handler, in which case this should just return.
chain.forkChoice.updateTime(slot);
parentBlockRoot = fromHexString(chain.recomputeForkChoiceHead().blockRoot);
parentBlockRoot = fromHexString(chain.getProposerHead(slot).blockRoot);
} else {
parentBlockRoot = inParentBlockRoot;
}
Expand Down Expand Up @@ -508,7 +508,7 @@ export function getValidatorApi({
// forkChoice.updateTime() might have already been called by the onSlot clock
// handler, in which case this should just return.
chain.forkChoice.updateTime(slot);
const parentBlockRoot = fromHexString(chain.recomputeForkChoiceHead().blockRoot);
const parentBlockRoot = fromHexString(chain.getProposerHead(slot).blockRoot);

const fork = config.getForkName(slot);
// set some sensible opts
Expand Down
58 changes: 52 additions & 6 deletions packages/beacon-node/src/chain/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import {
bellatrix,
isBlindedBeaconBlock,
} from "@lodestar/types";
import {CheckpointWithHex, ExecutionStatus, IForkChoice, ProtoBlock} from "@lodestar/fork-choice";
import {CheckpointWithHex, ExecutionStatus, IForkChoice, ProtoBlock, UpdateHeadOpt} from "@lodestar/fork-choice";
import {ProcessShutdownCallback} from "@lodestar/validator";
import {Logger, gweiToWei, isErrorAborted, pruneSetToMax, sleep, toHex} from "@lodestar/utils";
import {ForkSeq, SLOTS_PER_EPOCH} from "@lodestar/params";
Expand All @@ -45,7 +45,14 @@ import {isOptimisticBlock} from "../util/forkChoice.js";
import {BufferPool} from "../util/bufferPool.js";
import {BlockProcessor, ImportBlockOpts} from "./blocks/index.js";
import {ChainEventEmitter, ChainEvent} from "./emitter.js";
import {IBeaconChain, ProposerPreparationData, BlockHash, StateGetOpts, CommonBlockBody} from "./interface.js";
import {
IBeaconChain,
ProposerPreparationData,
BlockHash,
StateGetOpts,
CommonBlockBody,
FindHeadFnName,
} from "./interface.js";
import {IChainOptions} from "./options.js";
import {QueuedStateRegenerator, RegenCaller} from "./regen/index.js";
import {initializeForkChoice} from "./forkChoice/index.js";
Expand Down Expand Up @@ -279,7 +286,8 @@ export class BeaconChain implements IBeaconChain {
clock.currentSlot,
cachedState,
opts,
this.justifiedBalancesGetter.bind(this)
this.justifiedBalancesGetter.bind(this),
logger
);
const regen = new QueuedStateRegenerator({
config,
Expand Down Expand Up @@ -703,12 +711,50 @@ export class BeaconChain implements IBeaconChain {

recomputeForkChoiceHead(): ProtoBlock {
this.metrics?.forkChoice.requests.inc();
const timer = this.metrics?.forkChoice.findHead.startTimer();
const timer = this.metrics?.forkChoice.findHead.startTimer({entrypoint: FindHeadFnName.recomputeForkChoiceHead});

try {
return this.forkChoice.updateHead();
return this.forkChoice.updateAndGetHead({mode: UpdateHeadOpt.GetCanonicialHead}).head;
} catch (e) {
this.metrics?.forkChoice.errors.inc({entrypoint: UpdateHeadOpt.GetCanonicialHead});
throw e;
} finally {
timer?.();
}
}

predictProposerHead(slot: Slot): ProtoBlock {
this.metrics?.forkChoice.requests.inc();
const timer = this.metrics?.forkChoice.findHead.startTimer({entrypoint: FindHeadFnName.predictProposerHead});

try {
return this.forkChoice.updateAndGetHead({mode: UpdateHeadOpt.GetPredictedProposerHead, slot}).head;
} catch (e) {
this.metrics?.forkChoice.errors.inc({entrypoint: UpdateHeadOpt.GetPredictedProposerHead});
throw e;
} finally {
timer?.();
}
}

getProposerHead(slot: Slot): ProtoBlock {
this.metrics?.forkChoice.requests.inc();
const timer = this.metrics?.forkChoice.findHead.startTimer({entrypoint: FindHeadFnName.getProposerHead});
const secFromSlot = this.clock.secFromSlot(slot);

try {
const {head, isHeadTimely, notReorgedReason} = this.forkChoice.updateAndGetHead({
mode: UpdateHeadOpt.GetProposerHead,
secFromSlot,
slot,
});

if (isHeadTimely && notReorgedReason !== undefined) {
this.metrics?.forkChoice.notReorgedReason.inc({reason: notReorgedReason});
}
return head;
} catch (e) {
this.metrics?.forkChoice.errors.inc();
this.metrics?.forkChoice.errors.inc({entrypoint: UpdateHeadOpt.GetProposerHead});
throw e;
} finally {
timer?.();
Expand Down
12 changes: 12 additions & 0 deletions packages/beacon-node/src/chain/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ export type StateGetOpts = {
allowRegen: boolean;
};

export enum FindHeadFnName {
recomputeForkChoiceHead = "recomputeForkChoiceHead",
predictProposerHead = "predictProposerHead",
getProposerHead = "getProposerHead",
}

/**
* The IBeaconChain service deals with processing incoming blocks, advancing a state transition
* and applying the fork choice rule to update the chain head
Expand Down Expand Up @@ -188,6 +194,12 @@ export interface IBeaconChain {

recomputeForkChoiceHead(): ProtoBlock;

/** When proposerBoostReorg is enabled, this is called at slot n-1 to predict the head block to build on if we are proposing at slot n */
predictProposerHead(slot: Slot): ProtoBlock;

/** When proposerBoostReorg is enabled and we are proposing a block, this is called to determine which head block to build on */
getProposerHead(slot: Slot): ProtoBlock;

waitForBlock(slot: Slot, root: RootHex): Promise<boolean>;

updateBeaconProposerData(epoch: Epoch, proposers: ProposerPreparationData[]): Promise<void>;
Expand Down
3 changes: 2 additions & 1 deletion packages/beacon-node/src/chain/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@ export const defaultChainOptions: IChainOptions = {
blsVerifyAllMainThread: false,
blsVerifyAllMultiThread: false,
disableBlsBatchVerify: false,
proposerBoostEnabled: true,
proposerBoost: true,
proposerBoostReorg: false,
computeUnrealized: true,
safeSlotsToImportOptimistically: SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY,
suggestedFeeRecipient: defaultValidatorOptions.suggestedFeeRecipient,
Expand Down
54 changes: 43 additions & 11 deletions packages/beacon-node/src/chain/prepareNextSlot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import {
computeEpochAtSlot,
isExecutionStateType,
computeTimeAtSlot,
CachedBeaconStateExecutions,
StateHashTreeRootSource,
CachedBeaconStateAllForks,
} from "@lodestar/state-transition";
import {ChainForkConfig} from "@lodestar/config";
import {ForkSeq, SLOTS_PER_EPOCH, ForkExecution} from "@lodestar/params";
Expand Down Expand Up @@ -113,14 +115,6 @@ export class PrepareNextSlotScheduler {
RegenCaller.precomputeEpoch
);

// cache HashObjects for faster hashTreeRoot() later, especially for computeNewStateRoot() if we need to produce a block at slot 0 of epoch
// see https://github.com/ChainSafe/lodestar/issues/6194
const hashTreeRootTimer = this.metrics?.stateHashTreeRootTime.startTimer({
source: StateHashTreeRootSource.prepareNextSlot,
});
prepareState.hashTreeRoot();
hashTreeRootTimer?.();

// assuming there is no reorg, it caches the checkpoint state & helps avoid doing a full state transition in the next slot
// + when gossip block comes, we need to validate and run state transition
// + if next slot is a skipped slot, it'd help getting target checkpoint state faster to validate attestations
Expand All @@ -144,7 +138,31 @@ export class PrepareNextSlotScheduler {
if (isExecutionStateType(prepareState)) {
const proposerIndex = prepareState.epochCtx.getBeaconProposer(prepareSlot);
const feeRecipient = this.chain.beaconProposerCache.get(proposerIndex);
let updatedPrepareState = prepareState;
let updatedHeadRoot = headRoot;

if (feeRecipient) {
// If we are proposing next slot, we need to predict if we can proposer-boost-reorg or not
const {slot: proposerHeadSlot, blockRoot: proposerHeadRoot} = this.chain.predictProposerHead(clockSlot);

// If we predict we can reorg, update prepareState with proposer head block
if (proposerHeadRoot !== headRoot || proposerHeadSlot !== headSlot) {
this.logger.verbose("Weak head detected. May build on this block instead:", {
proposerHeadSlot,
proposerHeadRoot,
headSlot,
headRoot,
});
this.metrics?.weakHeadDetected.inc();
updatedPrepareState = (await this.chain.regen.getBlockSlotState(
proposerHeadRoot,
prepareSlot,
{dontTransferCache: !isEpochTransition},
RegenCaller.predictProposerHead
)) as CachedBeaconStateExecutions;
updatedHeadRoot = proposerHeadRoot;
}

// Update the builder status, if enabled shoot an api call to check status
this.chain.updateBuilderStatus(clockSlot);
if (this.chain.executionBuilder?.status) {
Expand All @@ -167,10 +185,10 @@ export class PrepareNextSlotScheduler {
this.chain,
this.logger,
fork as ForkExecution, // State is of execution type
fromHex(headRoot),
fromHex(updatedHeadRoot),
safeBlockHash,
finalizedBlockHash,
prepareState,
updatedPrepareState,
feeRecipient
);
this.logger.verbose("PrepareNextSlotScheduler prepared new payload", {
Expand All @@ -180,10 +198,12 @@ export class PrepareNextSlotScheduler {
});
}

this.computeStateHashTreeRoot(updatedPrepareState);

// If emitPayloadAttributes is true emit a SSE payloadAttributes event
if (this.chain.opts.emitPayloadAttributes === true) {
const data = await getPayloadAttributesForSSE(fork as ForkExecution, this.chain, {
prepareState,
prepareState: updatedPrepareState,
prepareSlot,
parentBlockRoot: fromHex(headRoot),
// The likely consumers of this API are builders and will anyway ignore the
Expand All @@ -192,6 +212,8 @@ export class PrepareNextSlotScheduler {
});
this.chain.emitter.emit(routes.events.EventType.payloadAttributes, {data, version: fork});
}
} else {
this.computeStateHashTreeRoot(prepareState);
}
} catch (e) {
if (!isErrorAborted(e) && !isQueueErrorAborted(e)) {
Expand All @@ -200,4 +222,14 @@ export class PrepareNextSlotScheduler {
}
}
};

computeStateHashTreeRoot(state: CachedBeaconStateAllForks): void {
// cache HashObjects for faster hashTreeRoot() later, especially for computeNewStateRoot() if we need to produce a block at slot 0 of epoch
// see https://github.com/ChainSafe/lodestar/issues/6194
const hashTreeRootTimer = this.metrics?.stateHashTreeRootTime.startTimer({
source: StateHashTreeRootSource.prepareNextSlot,
});
state.hashTreeRoot();
hashTreeRootTimer?.();
}
}
1 change: 1 addition & 0 deletions packages/beacon-node/src/chain/regen/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export enum RegenCaller {
validateGossipBlock = "validateGossipBlock",
validateGossipBlob = "validateGossipBlob",
precomputeEpoch = "precomputeEpoch",
predictProposerHead = "predictProposerHead",
produceAttestationData = "produceAttestationData",
processBlocksInEpoch = "processBlocksInEpoch",
validateGossipAggregateAndProof = "validateGossipAggregateAndProof",
Expand Down
18 changes: 16 additions & 2 deletions packages/beacon-node/src/metrics/metrics/beacon.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import {ProducedBlockSource} from "@lodestar/types";
import {NotReorgedReason} from "@lodestar/fork-choice/lib/forkChoice/interface.js";
import {UpdateHeadOpt} from "@lodestar/fork-choice";
import {RegistryMetricCreator} from "../utils/registryMetricCreator.js";
import {BlockProductionStep, PayloadPreparationType} from "../../chain/produceBlock/index.js";

Expand Down Expand Up @@ -57,18 +59,20 @@ export function createBeaconMetrics(register: RegistryMetricCreator) {
// Non-spec'ed

forkChoice: {
findHead: register.histogram({
findHead: register.histogram<{entrypoint: string}>({
name: "beacon_fork_choice_find_head_seconds",
help: "Time taken to find head in seconds",
buckets: [0.1, 1, 10],
labelNames: ["entrypoint"],
}),
requests: register.gauge({
name: "beacon_fork_choice_requests_total",
help: "Count of occasions where fork choice has tried to find a head",
}),
errors: register.gauge({
errors: register.gauge<{entrypoint: UpdateHeadOpt}>({
name: "beacon_fork_choice_errors_total",
help: "Count of occasions where fork choice has returned an error when trying to find a head",
labelNames: ["entrypoint"],
}),
changedHead: register.gauge({
name: "beacon_fork_choice_changed_head_total",
Expand Down Expand Up @@ -109,6 +113,11 @@ export function createBeaconMetrics(register: RegistryMetricCreator) {
name: "beacon_fork_choice_indices_count",
help: "Current count of indices in fork choice data structures",
}),
notReorgedReason: register.gauge<{reason: NotReorgedReason}>({
name: "beacon_fork_choice_not_reorged_reason_total",
help: "Reason why the current head is not re-orged out",
labelNames: ["reason"],
}),
},

parentBlockDistance: register.histogram({
Expand Down Expand Up @@ -198,5 +207,10 @@ export function createBeaconMetrics(register: RegistryMetricCreator) {
name: "beacon_clock_epoch",
help: "Current clock epoch",
}),

weakHeadDetected: register.gauge({
name: "beacon_weak_head_detected_total",
help: "Detected current head block is weak. May reorg it out when proposing next slot. See proposer boost reorg for more",
}),
};
}
Loading

0 comments on commit f6d3bce

Please sign in to comment.