Skip to content

Commit

Permalink
spec 1.1.6 - Applying proposer boost to fork-choice (#3540)
Browse files Browse the repository at this point in the history
* proposer boost to reward a timely block

* fix test type

* enable the proposer boost via --chain.proposerBoostEnabled flag

* cli test fix

* perf type fix

* removing secondsIntoSlot for simplicity

* moved the score eval fn to a more appropriate place

* compute proposer score directly from balances

* cleanup

* test to show boosts are added to weight only once despite multiple calls to applyScoreChanges

* rename blockDelay to blockDelaySec

* boosts now applied in the same way as lighthouse

* eval proposer score atmost once per justified update

* Update chain.ts

Co-authored-by: Lion - dapplion <[email protected]>
  • Loading branch information
g11tech and dapplion authored Jan 22, 2022
1 parent 55da98e commit f9cb593
Show file tree
Hide file tree
Showing 21 changed files with 227 additions and 46 deletions.
7 changes: 6 additions & 1 deletion packages/beacon-state-transition/src/util/slot.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {IChainConfig} from "@chainsafe/lodestar-config";
import {GENESIS_SLOT} from "@chainsafe/lodestar-params";
import {GENESIS_SLOT, INTERVALS_PER_SLOT} from "@chainsafe/lodestar-params";
import {Number64, Slot, Epoch} from "@chainsafe/lodestar-types";
import {computeStartSlotAtEpoch, computeEpochAtSlot} from ".";

Expand All @@ -20,3 +20,8 @@ export function computeSlotsSinceEpochStart(slot: Slot, epoch?: Epoch): number {
export function computeTimeAtSlot(config: IChainConfig, slot: Slot, genesisTime: Number64): Number64 {
return genesisTime + slot * config.SECONDS_PER_SLOT;
}

export function getCurrentInterval(config: IChainConfig, genesisTime: Number64, secondsIntoSlot: number): number {
const timePerInterval = Math.floor(config.SECONDS_PER_SLOT / INTERVALS_PER_SLOT);
return Math.floor(secondsIntoSlot / timePerInterval);
}
9 changes: 9 additions & 0 deletions packages/cli/src/options/beaconNodeOptions/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export interface IChainArgs {
"chain.useSingleThreadVerifier": boolean;
"chain.disableBlsBatchVerify": boolean;
"chain.persistInvalidSszObjects": boolean;
"chain.proposerBoostEnabled": boolean;
// this is defined as part of IBeaconPaths
// "chain.persistInvalidSszObjectsDir": string;
}
Expand All @@ -16,6 +17,7 @@ export function parseArgs(args: IChainArgs): IBeaconNodeOptions["chain"] {
persistInvalidSszObjects: args["chain.persistInvalidSszObjects"],
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
persistInvalidSszObjectsDir: undefined as any,
proposerBoostEnabled: args["chain.proposerBoostEnabled"],
};
}

Expand Down Expand Up @@ -44,4 +46,11 @@ Will double processing times. Use only for debugging purposes.",
description: "Persist invalid ssz objects or not for debugging purpose",
group: "chain",
},

"chain.proposerBoostEnabled": {
type: "boolean",
description: "Enable proposer boost to reward a timely block",
defaultDescription: String(defaultOptions.chain.proposerBoostEnabled),
group: "chain",
},
};
2 changes: 2 additions & 0 deletions packages/cli/test/unit/options/beaconNodeOptions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ describe("options / beaconNodeOptions", () => {
"chain.useSingleThreadVerifier": true,
"chain.disableBlsBatchVerify": true,
"chain.persistInvalidSszObjects": true,
"chain.proposerBoostEnabled": false,

"eth1.enabled": true,
"eth1.providerUrl": "http://my.node:8545",
Expand Down Expand Up @@ -70,6 +71,7 @@ describe("options / beaconNodeOptions", () => {
useSingleThreadVerifier: true,
disableBlsBatchVerify: true,
persistInvalidSszObjects: true,
proposerBoostEnabled: false,
},
eth1: {
enabled: true,
Expand Down
1 change: 1 addition & 0 deletions packages/config/src/chainConfig/presets/mainnet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export const chainConfig: IChainConfig = {
MIN_PER_EPOCH_CHURN_LIMIT: 4,
// 2**16 (= 65,536)
CHURN_LIMIT_QUOTIENT: 65536,
PROPOSER_SCORE_BOOST: 70,

// Deposit contract
// ---------------------------------------------------------------
Expand Down
1 change: 1 addition & 0 deletions packages/config/src/chainConfig/presets/minimal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export const chainConfig: IChainConfig = {
MIN_PER_EPOCH_CHURN_LIMIT: 4,
// [customized] scale queue churn at much lower validator counts for testing
CHURN_LIMIT_QUOTIENT: 32,
PROPOSER_SCORE_BOOST: 70,

// Deposit contract
// ---------------------------------------------------------------
Expand Down
1 change: 1 addition & 0 deletions packages/config/src/chainConfig/sszTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export const ChainConfig = new ContainerType<IChainConfig>({
EJECTION_BALANCE: ssz.Number64,
MIN_PER_EPOCH_CHURN_LIMIT: ssz.Number64,
CHURN_LIMIT_QUOTIENT: ssz.Number64,
PROPOSER_SCORE_BOOST: ssz.Number64,

// Deposit contract
DEPOSIT_CHAIN_ID: ssz.Number64,
Expand Down
1 change: 1 addition & 0 deletions packages/config/src/chainConfig/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export interface IChainConfig {
EJECTION_BALANCE: number;
MIN_PER_EPOCH_CHURN_LIMIT: number;
CHURN_LIMIT_QUOTIENT: number;
PROPOSER_SCORE_BOOST: number;

// Deposit contract
DEPOSIT_CHAIN_ID: number;
Expand Down
102 changes: 92 additions & 10 deletions packages/fork-choice/src/forkChoice/forkChoice.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {isTreeBacked, readonlyValues, toHexString, TreeBacked} from "@chainsafe/ssz";
import {SAFE_SLOTS_TO_UPDATE_JUSTIFIED, SLOTS_PER_HISTORICAL_ROOT} from "@chainsafe/lodestar-params";
import {SAFE_SLOTS_TO_UPDATE_JUSTIFIED, SLOTS_PER_HISTORICAL_ROOT, SLOTS_PER_EPOCH} from "@chainsafe/lodestar-params";
import {Slot, ValidatorIndex, phase0, allForks, ssz, RootHex, Epoch, Root} from "@chainsafe/lodestar-types";
import {
getCurrentInterval,
computeSlotsSinceEpochStart,
computeStartSlotAtEpoch,
computeEpochAtSlot,
Expand Down Expand Up @@ -68,6 +69,10 @@ export class ForkChoice implements IForkChoice {
* Only cache attestation data root hex if it's tree backed since it's available.
**/
private validatedAttestationDatas = new Set<string>();
/** Boost the entire branch with this proposer root as the leaf */
private proposerBoostRoot: RootHex | null = null;
/** Score to use in proposer boost, evaluated lazily from justified balances */
private justifiedProposerBoostScore: number | null = null;
/**
* Instantiates a Fork Choice from some existing components
*
Expand All @@ -85,6 +90,7 @@ export class ForkChoice implements IForkChoice {
* This should be the balances of the state at fcStore.justifiedCheckpoint
*/
private justifiedBalances: number[],
private readonly proposerBoostEnabled: boolean,
private readonly metrics?: IForkChoiceMetrics | null
) {
this.bestJustifiedBalances = justifiedBalances;
Expand Down Expand Up @@ -145,6 +151,13 @@ export class ForkChoice implements IForkChoice {
return this.head;
}

/**
* Get the proposer boost root
*/
getProposerBoostRoot(): RootHex {
return this.proposerBoostRoot ?? HEX_ZERO_HASH;
}

/**
* Run the fork choice rule to determine the head.
* Update the head cache.
Expand All @@ -162,25 +175,40 @@ export class ForkChoice implements IForkChoice {

let timer;
this.metrics?.forkChoiceRequests.inc();

try {
let deltas: number[];

// Check if scores need to be calculated/updated
if (!this.synced) {
timer = this.metrics?.forkChoiceFindHead.startTimer();
const deltas = computeDeltas(
this.protoArray.indices,
this.votes,
this.justifiedBalances,
this.justifiedBalances
);
deltas = computeDeltas(this.protoArray.indices, this.votes, this.justifiedBalances, this.justifiedBalances);
/**
* The structure in line with deltas to propogate boost up the branch
* starting from the proposerIndex
*/
let proposerBoost: {root: RootHex; score: number} | null = null;
if (this.proposerBoostEnabled && this.proposerBoostRoot) {
const proposerBoostScore =
this.justifiedProposerBoostScore ??
computeProposerBoostScoreFromBalances(this.justifiedBalances, {
slotsPerEpoch: SLOTS_PER_EPOCH,
proposerScoreBoost: this.config.PROPOSER_SCORE_BOOST,
});
proposerBoost = {root: this.proposerBoostRoot, score: proposerBoostScore};
this.justifiedProposerBoostScore = proposerBoostScore;
}

this.protoArray.applyScoreChanges({
deltas,
proposerBoost,
justifiedEpoch: this.fcStore.justifiedCheckpoint.epoch,
justifiedRoot: this.fcStore.justifiedCheckpoint.rootHex,
finalizedEpoch: this.fcStore.finalizedCheckpoint.epoch,
finalizedRoot: this.fcStore.finalizedCheckpoint.rootHex,
});
this.synced = true;
}

const headRoot = this.protoArray.findHead(this.fcStore.justifiedCheckpoint.rootHex);
const headIndex = this.protoArray.indices.get(headRoot);
if (headIndex === undefined) {
Expand Down Expand Up @@ -349,15 +377,31 @@ export class ForkChoice implements IForkChoice {
this.updateJustified(currentJustifiedCheckpoint, justifiedBalances);
}

const targetSlot = computeStartSlotAtEpoch(computeEpochAtSlot(slot));
const blockRoot = this.config.getForkTypes(slot).BeaconBlock.hashTreeRoot(block);
const blockRootHex = toHexString(blockRoot);

// Add proposer score boost if the block is timely
if (this.proposerBoostEnabled && slot === this.fcStore.currentSlot) {
const {blockDelaySec} = preCachedData || {};
if (blockDelaySec === undefined) {
throw Error("Missing blockDelaySec info for proposerBoost");
}

const proposerInterval = getCurrentInterval(this.config, state.genesisTime, blockDelaySec);
if (proposerInterval < 1) {
this.proposerBoostRoot = blockRootHex;
this.synced = false;
}
}

const targetSlot = computeStartSlotAtEpoch(computeEpochAtSlot(slot));
const targetRoot = slot === targetSlot ? blockRoot : state.blockRoots[targetSlot % SLOTS_PER_HISTORICAL_ROOT];

// This does not apply a vote to the block, it just makes fork choice aware of the block so
// it can still be identified as the head even if it doesn't have any votes.
this.protoArray.onBlock({
slot: slot,
blockRoot: toHexString(blockRoot),
blockRoot: blockRootHex,
parentRoot: parentRootHex,
targetRoot: toHexString(targetRoot),
stateRoot: toHexString(block.stateRoot),
Expand Down Expand Up @@ -662,6 +706,7 @@ export class ForkChoice implements IForkChoice {
private updateJustified(justifiedCheckpoint: CheckpointWithHex, justifiedBalances: number[]): void {
this.synced = false;
this.justifiedBalances = justifiedBalances;
this.justifiedProposerBoostScore = null;
this.fcStore.justifiedCheckpoint = justifiedCheckpoint;
}

Expand Down Expand Up @@ -934,6 +979,13 @@ export class ForkChoice implements IForkChoice {

// Update store time
this.fcStore.currentSlot = time;
if (this.proposerBoostRoot) {
// Since previous weight was boosted, we need would now need to recalculate the
// scores but without the boost
this.proposerBoostRoot = null;
this.synced = false;
}

const currentSlot = time;
if (computeSlotsSinceEpochStart(currentSlot) !== 0) {
return;
Expand Down Expand Up @@ -985,3 +1037,33 @@ function assertValidTerminalPowBlock(
);
}
}

function computeProposerBoostScore(
{
justifiedTotalActiveBalanceByIncrement,
justifiedActiveValidators,
}: {justifiedTotalActiveBalanceByIncrement: number; justifiedActiveValidators: number},
config: {slotsPerEpoch: number; proposerScoreBoost: number}
): number {
const avgBalanceByIncrement = Math.floor(justifiedTotalActiveBalanceByIncrement / justifiedActiveValidators);
const committeeSize = Math.floor(justifiedActiveValidators / config.slotsPerEpoch);
const committeeWeight = committeeSize * avgBalanceByIncrement;
const proposerScore = Math.floor((committeeWeight * config.proposerScoreBoost) / 100);
return proposerScore;
}

export function computeProposerBoostScoreFromBalances(
justifiedBalances: number[],
config: {slotsPerEpoch: number; proposerScoreBoost: number}
): number {
let justifiedTotalActiveBalanceByIncrement = 0,
justifiedActiveValidators = 0;
for (let i = 0; i < justifiedBalances.length; i++) {
if (justifiedBalances[i] > 0) {
justifiedActiveValidators += 1;
// justified balances here are by increment
justifiedTotalActiveBalanceByIncrement += justifiedBalances[i];
}
}
return computeProposerBoostScore({justifiedTotalActiveBalanceByIncrement, justifiedActiveValidators}, config);
}
2 changes: 2 additions & 0 deletions packages/fork-choice/src/forkChoice/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ export type PowBlockHex = {
export type OnBlockPrecachedData = {
/** `justifiedBalances` balances of justified state which is updated synchronously. */
justifiedBalances?: number[];
/** Time in seconds when the block was received */
blockDelaySec: number;
/**
* POW chain block parent, from getPowBlock() `eth_getBlockByHash` JSON RPC endpoint
* ```ts
Expand Down
16 changes: 14 additions & 2 deletions packages/fork-choice/src/protoArray/protoArray.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {IProtoBlock, IProtoNode, HEX_ZERO_HASH} from "./interface";
import {ProtoArrayError, ProtoArrayErrorCode} from "./errors";

export const DEFAULT_PRUNE_THRESHOLD = 0;
type ProposerBoost = {root: RootHex; score: number};

export class ProtoArray {
// Do not attempt to prune the tree unless it has at least this many nodes.
Expand All @@ -16,6 +17,8 @@ export class ProtoArray {
nodes: IProtoNode[];
indices: Map<RootHex, number>;

private previousProposerBoost?: ProposerBoost | null = null;

constructor({
pruneThreshold,
justifiedEpoch,
Expand Down Expand Up @@ -71,12 +74,14 @@ export class ProtoArray {
*/
applyScoreChanges({
deltas,
proposerBoost,
justifiedEpoch,
justifiedRoot,
finalizedEpoch,
finalizedRoot,
}: {
deltas: number[];
proposerBoost: ProposerBoost | null;
justifiedEpoch: Epoch;
justifiedRoot: RootHex;
finalizedEpoch: Epoch;
Expand Down Expand Up @@ -119,14 +124,19 @@ export class ProtoArray {
continue;
}

const nodeDelta = deltas[nodeIndex];
const currentBoost = proposerBoost && proposerBoost.root === node.blockRoot ? proposerBoost.score : 0;
const previousBoost =
this.previousProposerBoost && this.previousProposerBoost.root === node.blockRoot
? this.previousProposerBoost.score
: 0;
const nodeDelta = deltas[nodeIndex] + currentBoost - previousBoost;

if (nodeDelta === undefined) {
throw new ProtoArrayError({
code: ProtoArrayErrorCode.INVALID_NODE_DELTA,
index: nodeIndex,
});
}

// Apply the delta to the node
node.weight += nodeDelta;

Expand Down Expand Up @@ -166,6 +176,8 @@ export class ProtoArray {
this.maybeUpdateBestChildAndDescendant(parentIndex, nodeIndex);
}
}
// Update the previous proposer boost
this.previousProposerBoost = proposerBoost;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ describe("ForkChoice", () => {
bestJustifiedCheckpoint: {epoch: genesisEpoch, root: fromHexString(finalizedRoot), rootHex: finalizedRoot},
};

forkchoice = new ForkChoice(config, fcStore, protoArr, []);
forkchoice = new ForkChoice(config, fcStore, protoArr, [], false);

let parentBlockRoot = finalizedRoot;
// assume there are 64 unfinalized blocks, this number does not make a difference in term of performance
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {allForks, computeStartSlotAtEpoch} from "@chainsafe/lodestar-beacon-stat
import {generatePerfTestCachedStateAltair} from "@chainsafe/lodestar-beacon-state-transition/test/perf/util";
import {IVoteTracker} from "../../../src/protoArray/interface";
import {computeDeltas} from "../../../src/protoArray/computeDeltas";
import {computeProposerBoostScoreFromBalances} from "../../../src/forkChoice/forkChoice";

describe("computeDeltas", () => {
let originalState: allForks.CachedBeaconState<allForks.BeaconState>;
Expand Down Expand Up @@ -71,4 +72,11 @@ describe("computeDeltas", () => {
computeDeltas(indices, votes, oldBalances, newBalances);
},
});

itBench({
id: "computeProposerBoostScoreFromBalances",
fn: () => {
computeProposerBoostScoreFromBalances(newBalances, {slotsPerEpoch: 32, proposerScoreBoost: 70});
},
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ describe("Forkchoice", function () {

it("getAllAncestorBlocks", function () {
protoArr.onBlock(block);
const forkchoice = new ForkChoice(config, fcStore, protoArr, []);
const forkchoice = new ForkChoice(config, fcStore, protoArr, [], false);
const summaries = forkchoice.getAllAncestorBlocks(finalizedDesc);
// there are 2 blocks in protoArray but iterateAncestorBlocks should only return non-finalized blocks
expect(summaries.length).to.be.equals(1, "should not return the finalized block");
Expand Down
Loading

0 comments on commit f9cb593

Please sign in to comment.