Skip to content

Compute proposer boost score using Gwei instead of ETH #9694

Closed

Description

@ensi321

looks good to me as an infrastructure for compliance test
right now the getCommitteeFranction() doesn't follow forkchoice spec because we store weight by ETH, not by gwei. I suspect we did that even before proposer boost was landed, and we did not revisit.
one way to do that is to let forkchoice store weight in ETH by a scale of SLOTS_PER_EPOCH * 100, then we can keep it as integer without an overflow issue. But this is a separate issue.

Originally posted by @twoeths in #9670 (review)

Problem

getCommitteeFraction() computes the proposer boost score in EFFECTIVE_BALANCE_INCREMENT
(1 ETH) units and floors twice in those units:

https://github.com/ChainSafe/lodestar/blob/unstable/packages/fork-choice/src/forkChoice/forkChoice.ts#L2083-L2091

export function getCommitteeFraction(
  justifiedTotalActiveBalanceByIncrement: number,
  config: {slotsPerEpoch: number; committeePercent: number}
): number {
  const committeeWeight = Math.floor(justifiedTotalActiveBalanceByIncrement / config.slotsPerEpoch);
  return Math.floor((committeeWeight * config.committeePercent) / 100);
}

The spec's get_proposer_score (https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.12/specs/phase0/fork-choice.md#get_proposer_score)
(and calculate_committee_fraction (https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.12/specs/phase0/fork-choice.md#calculate_committee_fraction)
for the reorg thresholds) performs the same computation in Gwei:

committee_weight = get_total_active_balance(justified_state) // SLOTS_PER_EPOCH
return (committee_weight * config.PROPOSER_SCORE_BOOST) // 100

Because we floor in 1-ETH units instead of Gwei, our boost score is below the spec value by up to
(100 - gcd(PROPOSER_SCORE_BOOST, 100) + PROPOSER_SCORE_BOOST) / 100 = 1.2 ETH
(for PROPOSER_SCORE_BOOST = 40).

Impact

  • Head selection can diverge from spec (and other clients) when competing branch weights are
    within ~1.2 ETH of each other — exactly the boost-decided tie scenarios.
  • Same imprecision applies to the proposer-boost-reorg thresholds (reorgThreshold /
    parentThreshold, forkChoice.ts:459/474).
  • The fork-choice compliance test runner (feat: add fork choice compliance test for pre-gloas forks #9670) cannot compare weights exactly against the
    vectors and has to emulate our flooring to normalize the expected boosted weight
    (see TODO in test/spec/utils/forkChoiceTestRunner.ts). Fixing this removes that workaround.

Constraint

Protoarray weights are number in increment units because Gwei-scale totals overflow
Number.MAX_SAFE_INTEGER (2^53 ≈ 9.0e15; mainnet total active stake is ~3e16 Gwei).
A Gwei-precision boost therefore can't just be dropped into the current weight representation.

Options

  1. Compute boost in Gwei (bigint), floor once to increments. Minimal change; shrinks the
    divergence from ≤1.2 ETH to <1 ETH but still not exact, and the compliance-runner
    normalization must stay.
  2. Track protoarray weights in Gwei as bigint. Exact spec match; attestation weights stay
    exact too (effective balances are increment multiples). Cost: bigint arithmetic in
    computeDeltas/applyScoreChanges hot paths — needs

Option 2 is the only one that fully closes the gap and lets the compliance runner assert
weights against vectors byte-for-byte.

Activity

  1. added a commit that references this issue on Jul 23, 2026
  2. wemeetagain commented on Jul 24, 2026

    @wemeetagain
    Member

    We can benchmark bigints for protoarray weights. I suspect it will be too expensive, but worth a shot to actually measure it.
    Another option, might be messy, is using a {hi: number, lo: number} per weight.

  3. twoeths commented on Jul 27, 2026

    @twoeths
    Member

    We can benchmark bigints for protoarray weights. I suspect it will be too expensive

    yeah we should try it, NodeJS improved a lot since we found the bigint performance issue

  4. GrapeBaBa commented on Aug 14, 2026

    @GrapeBaBa
    Contributor

    #9804 used scaled increment approach mentioned by @twoeths in previous PR , #9821 tried option 2 of this issue but keep increment for computeDeltas for not lost perf too much

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions