Repository navigation
Compute proposer boost score using Gwei instead of ETH #9694
Description
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:
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
- 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. - 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.
Metadata
Metadata
Assignees
Labels
Type
Fields
Priority
Activity
chore: link issue #9694 in proposer boost normalization TODO
wemeetagain commentedon Jul 24, 2026
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.twoeths commentedon Jul 27, 2026
yeah we should try it, NodeJS improved a lot since we found the bigint performance issue
GrapeBaBa commentedon Aug 14, 2026
#9804 used scaled increment approach mentioned by @twoeths in previous PR , #9821 tried option 2 of this issue but keep increment for
computeDeltasfor not lost perf too muchfix(fork-choice): preserve exact Gwei weights (ChainSafe#9821)
test: re-triage gloas fork choice compliance skips for v1.7.0-alpha.14