Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ export function processAttestationsAltair(
let newSeenAttesters = 0;
let newSeenAttestersEffectiveBalance = 0;

const builderWeightMap: Map<number, number> = new Map();
// bigint accumulator: per-slot gwei weight can exceed Number.MAX_SAFE_INTEGER at mainnet-scale stake
const builderWeightMap: Map<number, bigint> = new Map();

for (const attestation of attestations) {
const data = attestation.data;
Expand Down Expand Up @@ -150,7 +151,7 @@ export function processAttestationsAltair(
const existingWeight =
builderWeightMap.get(builderPendingPaymentIndex) ??
(state as CachedBeaconStateGloas).builderPendingPayments.get(builderPendingPaymentIndex).weight;
const updatedWeight = existingWeight + paymentWeightToAdd * EFFECTIVE_BALANCE_INCREMENT;
const updatedWeight = existingWeight + BigInt(paymentWeightToAdd) * BigInt(EFFECTIVE_BALANCE_INCREMENT);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Minor: BigInt(EFFECTIVE_BALANCE_INCREMENT) is re-evaluated every iteration of the attestation loop. Consider hoisting to a module-level constant for consistency with how other bigint constants are handled, e.g.:

Suggested change
const updatedWeight = existingWeight + BigInt(paymentWeightToAdd) * BigInt(EFFECTIVE_BALANCE_INCREMENT);
const updatedWeight = existingWeight + BigInt(paymentWeightToAdd) * EFFECTIVE_BALANCE_INCREMENT_BN;

with const EFFECTIVE_BALANCE_INCREMENT_BN = BigInt(EFFECTIVE_BALANCE_INCREMENT); at module scope. Probably negligible at runtime (V8 caches small bigint constants), but cheap and idiomatic.

builderWeightMap.set(builderPendingPaymentIndex, updatedWeight);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export function processExecutionPayloadBid(state: CachedBeaconStateGloas, block:

if (amount > 0) {
const pendingPaymentView = ssz.gloas.BuilderPendingPayment.toViewDU({
weight: 0,
weight: 0n,
withdrawal: ssz.gloas.BuilderPendingWithdrawal.toViewDU({
feeRecipient: bid.feeRecipient,
amount,
Expand Down
14 changes: 8 additions & 6 deletions packages/state-transition/src/util/gloas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,14 @@ export function isBuilderWithdrawalCredential(withdrawalCredentials: Uint8Array)
return withdrawalCredentials[0] === BUILDER_WITHDRAWAL_PREFIX;
}

export function getBuilderPaymentQuorumThreshold(state: CachedBeaconStateGloas): number {
const quorum =
Math.floor((state.epochCtx.totalActiveBalanceIncrements * EFFECTIVE_BALANCE_INCREMENT) / SLOTS_PER_EPOCH) *
BUILDER_PAYMENT_THRESHOLD_NUMERATOR;

return Math.floor(quorum / BUILDER_PAYMENT_THRESHOLD_DENOMINATOR);
export function getBuilderPaymentQuorumThreshold(state: CachedBeaconStateGloas): bigint {
// bigint to avoid f64 precision loss: totalActiveBalanceIncrements * EFFECTIVE_BALANCE_INCREMENT
// exceeds Number.MAX_SAFE_INTEGER once total active stake passes ~9M ETH.
const totalGwei = BigInt(state.epochCtx.totalActiveBalanceIncrements) * BigInt(EFFECTIVE_BALANCE_INCREMENT);
return (
((totalGwei / BigInt(SLOTS_PER_EPOCH)) * BigInt(BUILDER_PAYMENT_THRESHOLD_NUMERATOR)) /
BigInt(BUILDER_PAYMENT_THRESHOLD_DENOMINATOR)
);
}

function hasBuilderIndexFlag(index: number): boolean {
Expand Down
35 changes: 35 additions & 0 deletions packages/state-transition/test/unit/util/gloas.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {describe, expect, it} from "vitest";
import {
BUILDER_PAYMENT_THRESHOLD_DENOMINATOR,
BUILDER_PAYMENT_THRESHOLD_NUMERATOR,
EFFECTIVE_BALANCE_INCREMENT,
SLOTS_PER_EPOCH,
} from "@lodestar/params";
import {CachedBeaconStateGloas} from "../../../src/types.js";
import {getBuilderPaymentQuorumThreshold} from "../../../src/util/gloas.js";

describe("getBuilderPaymentQuorumThreshold", () => {
function refQuorum(totalActiveBalanceIncrements: number): bigint {
const totalGwei = BigInt(totalActiveBalanceIncrements) * BigInt(EFFECTIVE_BALANCE_INCREMENT);
return (
((totalGwei / BigInt(SLOTS_PER_EPOCH)) * BigInt(BUILDER_PAYMENT_THRESHOLD_NUMERATOR)) /
BigInt(BUILDER_PAYMENT_THRESHOLD_DENOMINATOR)
);
}

function makeStateStub(totalActiveBalanceIncrements: number): CachedBeaconStateGloas {
return {epochCtx: {totalActiveBalanceIncrements}} as unknown as CachedBeaconStateGloas;
}

// Stake levels chosen to bracket the f64 precision boundary: 9_007_199 ETH increments
// multiplied by EFFECTIVE_BALANCE_INCREMENT (1e9) equals 2^53 - 1.
it.each([
{label: "tiny devnet (~50k ETH)", totalActiveBalanceIncrements: 50_000},
{label: "below f64 boundary (~9M ETH)", totalActiveBalanceIncrements: 9_000_000},
{label: "mainnet today (~35M ETH)", totalActiveBalanceIncrements: 35_000_000},
{label: "MaxEB worst case (~64M ETH)", totalActiveBalanceIncrements: 64_000_000},
])("matches bigint reference at $label", ({totalActiveBalanceIncrements}) => {
const got = getBuilderPaymentQuorumThreshold(makeStateStub(totalActiveBalanceIncrements));
expect(got).toEqual(refQuorum(totalActiveBalanceIncrements));
});
});
3 changes: 2 additions & 1 deletion packages/types/src/gloas/sszTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ export const BuilderPendingWithdrawal = new ContainerType(

export const BuilderPendingPayment = new ContainerType(
{
weight: UintNum64,
// bigint to avoid f64 precision loss when accumulating gwei weight at mainnet-scale stake
weight: UintBn64,
withdrawal: BuilderPendingWithdrawal,
},
{typeName: "BuilderPendingPayment", jsonCase: "eth2"}
Expand Down
Loading