Skip to content
Merged
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
2 changes: 1 addition & 1 deletion packages/api/test/unit/beacon/testData/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ export const eventTestData: EventData = {
proposal_slot: "10",
validator_index: "42",
fee_recipient: "0x0000000000000000000000000000000000000000",
gas_limit: "30000000",
target_gas_limit: "30000000",
},
signature:
"0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505",
Expand Down
10 changes: 9 additions & 1 deletion packages/beacon-node/src/api/impl/validator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1067,7 +1067,15 @@ export function getValidatorApi(

const blockIsForSlot = block.slot === slot;
const payloadInput = chain.seenPayloadEnvelopeInputCache.get(block.blockRoot);
const payloadPresent = blockIsForSlot && (payloadInput?.hasPayloadEnvelope() ?? false);
// Spec: set payload_present only if the envelope was seen before get_payload_due_ms()
// into the slot. Use the envelope's own arrival time (getPayloadEnvelopeSource), not
// the input's creation time.
const payloadDueSec = config.getPayloadDueMs() / 1000;
const payloadPresent =
blockIsForSlot &&
payloadInput !== undefined &&
payloadInput.hasPayloadEnvelope() &&
chain.clock.secFromSlot(slot, payloadInput.getPayloadEnvelopeSource().seenTimestampSec) < payloadDueSec;
const blobDataAvailable = blockIsForSlot && (payloadInput?.hasAllData() ?? false);

return {
Expand Down
59 changes: 59 additions & 0 deletions packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
G2_POINT_AT_INFINITY,
IBeaconStateView,
type IBeaconStateViewBellatrix,
type IBeaconStateViewGloas,
computeEpochAtSlot,
Comment thread
twoeths marked this conversation as resolved.
computeTimeAtSlot,
isStatePostBellatrix,
isStatePostCapella,
Expand Down Expand Up @@ -58,10 +60,12 @@ import {
PayloadId,
getExpectedGasLimit,
} from "../../execution/index.js";
import {getShufflingDependentRoot} from "../../util/dependentRoot.js";
import {fromGraffitiBytes} from "../../util/graffiti.js";
import {kzg} from "../../util/kzg.js";
import type {BeaconChain} from "../chain.js";
import {CommonBlockBody} from "../interface.js";
import {ProposerPreferencesPool} from "../opPools/index.js";
import {validateBlobsAndKzgCommitments, validateCellsAndKzgCommitments} from "./validateBlobsAndKzgCommitments.js";

// Time to provide the EL to generate a payload from new payload id
Expand Down Expand Up @@ -204,6 +208,9 @@ export async function produceBlockBody<T extends BlockType>(
// this into a completely separate function and have pre/post gloas more separated
const safeBlockHash = getSafeExecutionBlockHash(this.forkChoice);
const finalizedBlockHash = this.forkChoice.getFinalizedBlock().executionPayloadBlockHash ?? ZERO_HASH_HEX;
// TODO GLOAS: post-Gloas, proposer feeRecipient is also carried (signed) in
// ProposerPreferencesPool. Consider using this unified cache instead
// see https://github.com/ChainSafe/lodestar/issues/9379
const feeRecipient = requestedFeeRecipient ?? this.beaconProposerCache.getOrDefault(proposerIndex);

const endExecutionPayload = this.metrics?.executionBlockProductionTimeSteps.startTimer();
Expand Down Expand Up @@ -633,6 +640,8 @@ export async function prepareExecutionPayload(
chain: {
executionEngine: IExecutionEngine;
config: ChainForkConfig;
forkChoice: IForkChoice;
proposerPreferencesPool: ProposerPreferencesPool;
},
logger: Logger,
fork: ForkPostBellatrix,
Expand Down Expand Up @@ -733,6 +742,7 @@ export function getPayloadAttributesForSSE(
chain: {
config: ChainForkConfig;
forkChoice: IForkChoice;
proposerPreferencesPool: ProposerPreferencesPool;
},
{
prepareState,
Expand Down Expand Up @@ -789,6 +799,8 @@ function preparePayloadAttributes(
fork: ForkPostBellatrix,
chain: {
config: ChainForkConfig;
forkChoice: IForkChoice;
proposerPreferencesPool: ProposerPreferencesPool;
},
{
prepareState,
Expand Down Expand Up @@ -851,12 +863,59 @@ function preparePayloadAttributes(
}

if (ForkSeq[fork] >= ForkSeq.gloas) {
if (!isStatePostGloas(prepareState)) {
throw new Error("Expected Gloas state for Gloas payload attributes");
}
(payloadAttributes as gloas.SSEPayloadAttributes["payloadAttributes"]).slotNumber = prepareSlot;
(payloadAttributes as gloas.SSEPayloadAttributes["payloadAttributes"]).targetGasLimit = getProposerTargetGasLimit(
chain,
prepareState,
prepareSlot,
parentBlockRoot
);
}

return payloadAttributes;
}

/**
* Resolve the proposer's preferred (target) gas limit for the Gloas `PayloadAttributesV4`
* `targetGasLimit` field (consensus-specs#5235, execution-apis#796).
*
* Sourced from the `SignedProposerPreferences` the proposer's VC submitted to the pool
* (same `(slot, dependent_root)` lookup as gossip bid validation). When no matching
* preferences are pooled, target the parent payload's gas limit so the gas limit stays
* unchanged (`is_gas_limit_target_compatible` then requires `gas_limit == parent_gas_limit`).
*/
function getProposerTargetGasLimit(
chain: {forkChoice: IForkChoice; proposerPreferencesPool: ProposerPreferencesPool},
state: IBeaconStateViewGloas,
prepareSlot: Slot,
parentBlockRoot: Root
): number {
const parentBlock = chain.forkChoice.getBlockHexDefaultStatus(toRootHex(parentBlockRoot));
const dependentRootHex = (() => {
if (parentBlock === null) {
return null;
}
try {
return getShufflingDependentRoot(
chain.forkChoice,
computeEpochAtSlot(prepareSlot),
computeEpochAtSlot(parentBlock.slot),
parentBlock
);
} catch {
return null;
}
})();

const pref = dependentRootHex !== null ? chain.proposerPreferencesPool.get(prepareSlot, dependentRootHex) : null;
// TODO GLOAS: state.latestExecutionPayloadBid is the latest *bid*, not the latest *executed*
// payload — for EMPTY parents this drifts. Consider having a default value like Prysm's DefaultBuilderGasLimit.
return Number(pref ? pref.message.targetGasLimit : state.latestExecutionPayloadBid.gasLimit);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Derive fallback target gas limit from parent payload

When no proposer preferences are found, this fallback uses state.latestExecutionPayloadBid.gasLimit, but that field is the previous block’s bid gas limit, not necessarily the latest executed payload gas limit. In Gloas, latestExecutionPayloadBid is overwritten for every block (including EMPTY-parent cases), so after an EMPTY block with a custom proposer target, the next slot can inherit that target even though no payload changed, violating the stated “keep parent payload gas limit unchanged” behavior and producing incorrect targetGasLimit in PayloadAttributesV4.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

we need to come up with chain.targetGasLimit option
Prysm has something like DefaultBuilderGasLimit for that

}

export async function produceCommonBlockBody<T extends BlockType>(
this: BeaconChain,
blockType: T,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,14 +128,14 @@ async function validateExecutionPayloadBid(
});
}

// [REJECT] `bid.gas_limit == proposer_preferences.gas_limit`.
// [REJECT] `bid.gas_limit == proposer_preferences.target_gas_limit`.
const bidGasLimit = Number(bid.gasLimit);
if (bidGasLimit !== proposerPreferences.message.gasLimit) {
if (bidGasLimit !== proposerPreferences.message.targetGasLimit) {
throw new ExecutionPayloadBidError(GossipAction.REJECT, {
code: ExecutionPayloadBidErrorCode.PROPOSER_PREFERENCES_GAS_LIMIT_MISMATCH,
builderIndex: bid.builderIndex,
bidGasLimit,
expectedGasLimit: proposerPreferences.message.gasLimit,
expectedGasLimit: proposerPreferences.message.targetGasLimit,
});
}

Expand Down
1 change: 1 addition & 0 deletions packages/beacon-node/src/execution/engine/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export type PayloadAttributes = {
withdrawals?: capella.Withdrawal[];
parentBeaconBlockRoot?: Uint8Array;
slotNumber?: number; // EIP-7843
targetGasLimit?: number; // GLOAS (PayloadAttributesV4, execution-apis#796)
};

export type VersionedHashes = Uint8Array[];
Expand Down
4 changes: 4 additions & 0 deletions packages/beacon-node/src/execution/engine/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,8 @@ export type PayloadAttributesRpc = {
parentBeaconBlockRoot?: DATA;
/** QUANTITY, 64 Bits - value for the slot number field of the new payload (EIP-7843) */
slotNumber?: QUANTITY;
/** QUANTITY, 64 Bits - target value for the gasLimit field of the new payload (GLOAS, execution-apis#796) */
targetGasLimit?: QUANTITY;
};

export type ClientVersionRpc = {
Expand Down Expand Up @@ -425,6 +427,7 @@ export function serializePayloadAttributes(data: PayloadAttributes): PayloadAttr
withdrawals: data.withdrawals?.map(serializeWithdrawal),
parentBeaconBlockRoot: data.parentBeaconBlockRoot ? bytesToData(data.parentBeaconBlockRoot) : undefined,
slotNumber: data.slotNumber !== undefined ? numToQuantity(data.slotNumber) : undefined,
targetGasLimit: data.targetGasLimit !== undefined ? numToQuantity(data.targetGasLimit) : undefined,
};
}

Expand All @@ -442,6 +445,7 @@ export function deserializePayloadAttributes(data: PayloadAttributesRpc): Payloa
withdrawals: data.withdrawals?.map((withdrawal) => deserializeWithdrawal(withdrawal)),
parentBeaconBlockRoot: data.parentBeaconBlockRoot ? dataToBytes(data.parentBeaconBlockRoot, 32) : undefined,
slotNumber: data.slotNumber !== undefined ? quantityToNum(data.slotNumber) : undefined,
targetGasLimit: data.targetGasLimit !== undefined ? quantityToNum(data.targetGasLimit) : undefined,
};
}

Expand Down
7 changes: 4 additions & 3 deletions packages/beacon-node/test/spec/utils/specTestIterator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,14 @@ export const defaultSkipOpts: SkipOpts = {
// TODO-GLOAS: re-enable after Gloas light client is implemented
/^gloas\/light_client\/.*/,
/^gloas\/ssz_static\/LightClient(Bootstrap|FinalityUpdate|Header|OptimisticUpdate|Update)\/.*/,
// TODO-GLOAS: re-enable after on_payload_attestation_message (PTC) fork choice is implemented.
// New test suite added in v1.7.0-alpha.8 (consensus-specs #5206); gloas PTC fork choice
// handling is not yet implemented in Lodestar.
/^gloas\/fork_choice\/on_payload_attestation_message\/.*$/,
],
skippedTests: [
// TODO-GLOAS: re-enable after gloas light client is implemented
/\/gloas_fork$/,
// TODOGLOAS: re-enable after upgrading to v1.7.0-alpha.8
// (which includes #5254). https://github.com/ethereum/consensus-specs/pull/5254
/^gloas\/fork\/fork\/pyspec_tests\/fork_invalid_validator_deposit_followed_by_builder_credentials$/,
],
// TODO GLOAS: Investigate why networking tests are failing since alpha.5
skippedRunners: ["fast_confirmation", "networking"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import {ProtoBlock} from "@lodestar/fork-choice";
import {toRootHex} from "@lodestar/utils";
import {getValidatorApi} from "../../../../../src/api/impl/validator/index.js";
import {defaultApiOptions} from "../../../../../src/api/options.js";
import {PayloadEnvelopeInput} from "../../../../../src/chain/blocks/payloadEnvelopeInput/index.js";
import {
PayloadEnvelopeInput,
PayloadEnvelopeInputSource,
} from "../../../../../src/chain/blocks/payloadEnvelopeInput/index.js";
import {ZERO_HASH_HEX} from "../../../../../src/constants/index.js";
import {SyncState} from "../../../../../src/sync/interface.js";
import {ApiTestModules, getApiTestModules} from "../../../../utils/api.js";
Expand Down Expand Up @@ -66,8 +69,10 @@ describe("api - validator - produceAttestationData", () => {
slot: 0,
blockRoot: ZERO_HASH_HEX,
} as ProtoBlock);
vi.mocked(modules.chain.clock.secFromSlot).mockReturnValue(0);
vi.mocked(modules.chain.seenPayloadEnvelopeInputCache.get).mockReturnValue({
hasPayloadEnvelope: () => true,
getPayloadEnvelopeSource: () => ({source: PayloadEnvelopeInputSource.gossip, seenTimestampSec: 0}),
hasAllData: () => true,
} as PayloadEnvelopeInput);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ describe("chain / opPools / ProposerPreferencesPool", () => {
proposalSlot,
validatorIndex,
feeRecipient: Buffer.alloc(20, 0xab),
gasLimit: 30_000_000,
targetGasLimit: 30_000_000,
},
signature: Buffer.alloc(96, 0),
});
Expand Down
6 changes: 4 additions & 2 deletions packages/config/src/chainConfig/configs/mainnet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ export const chainConfig: ChainConfig = {
SECONDS_PER_ETH1_BLOCK: 14,
// 2**8 (= 256) epochs ~27 hours
MIN_VALIDATOR_WITHDRAWABILITY_DELAY: 256,
// 2**6 (= 64) epochs
MIN_BUILDER_WITHDRAWABILITY_DELAY: 64,
// 2**13 (= 8,192) epochs ~36 days
MIN_BUILDER_WITHDRAWABILITY_DELAY: 8192,
// 2**8 (= 256) epochs ~27 hours
SHARD_COMMITTEE_PERIOD: 256,
// 2**11 (= 2,048) Eth1 blocks ~8 hours
Expand Down Expand Up @@ -99,6 +99,8 @@ export const chainConfig: ChainConfig = {
CONTRIBUTION_DUE_BPS_GLOAS: 5000,
// 75% of SLOT_DURATION_MS
PAYLOAD_ATTESTATION_DUE_BPS: 7500,
// 75% of SLOT_DURATION_MS
PAYLOAD_DUE_BPS: 7500,

// Validator cycle
// ---------------------------------------------------------------
Expand Down
2 changes: 2 additions & 0 deletions packages/config/src/chainConfig/configs/minimal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ export const chainConfig: ChainConfig = {
CONTRIBUTION_DUE_BPS_GLOAS: 5000,
// 75% of SLOT_DURATION_MS
PAYLOAD_ATTESTATION_DUE_BPS: 7500,
// 75% of SLOT_DURATION_MS
PAYLOAD_DUE_BPS: 7500,

// Validator cycle
// ---------------------------------------------------------------
Expand Down
2 changes: 2 additions & 0 deletions packages/config/src/chainConfig/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export type ChainConfig = {
SYNC_MESSAGE_DUE_BPS_GLOAS: number;
CONTRIBUTION_DUE_BPS_GLOAS: number;
PAYLOAD_ATTESTATION_DUE_BPS: number;
PAYLOAD_DUE_BPS: number;

// Validator cycle
INACTIVITY_SCORE_BIAS: number;
Expand Down Expand Up @@ -191,6 +192,7 @@ export const chainConfigTypes: SpecTypes<ChainConfig> = {
SYNC_MESSAGE_DUE_BPS_GLOAS: "number",
CONTRIBUTION_DUE_BPS_GLOAS: "number",
PAYLOAD_ATTESTATION_DUE_BPS: "number",
PAYLOAD_DUE_BPS: "number",

// Validator cycle
INACTIVITY_SCORE_BIAS: "number",
Expand Down
3 changes: 3 additions & 0 deletions packages/config/src/forkConfig/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,9 @@ export function createForkConfig(config: ChainConfig): ForkConfig {
getProposerReorgCutoffMs(_fork: ForkName): number {
return this.getSlotComponentDurationMs(config.PROPOSER_REORG_CUTOFF_BPS);
},
getPayloadDueMs(): number {
return this.getSlotComponentDurationMs(config.PAYLOAD_DUE_BPS);
},

getSlotComponentDurationMs(basisPoints: number): number {
return Math.round((basisPoints * config.SLOT_DURATION_MS) / BASIS_POINTS);
Expand Down
1 change: 1 addition & 0 deletions packages/config/src/forkConfig/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export type ForkConfig = {
getSyncMessageDueMs(fork: ForkName): number;
getSyncContributionDueMs(fork: ForkName): number;
getProposerReorgCutoffMs(fork: ForkName): number;
getPayloadDueMs(): number;

/** Convert basis points to milliseconds into the slot */
getSlotComponentDurationMs(basisPoints: number): number;
Expand Down
3 changes: 2 additions & 1 deletion packages/state-transition/src/slot/upgradeStateToGloas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export function upgradeStateToGloas(stateFulu: CachedBeaconStateFulu): CachedBea
stateGloasView.currentSyncCommittee = stateGloasCloned.currentSyncCommittee;
stateGloasView.nextSyncCommittee = stateGloasCloned.nextSyncCommittee;
stateGloasView.latestExecutionPayloadBid.blockHash = stateFulu.latestExecutionPayloadHeader.blockHash;
stateGloasView.latestExecutionPayloadBid.gasLimit = BigInt(stateFulu.latestExecutionPayloadHeader.gasLimit);
stateGloasView.latestExecutionPayloadBid.executionRequestsRoot = ssz.electra.ExecutionRequests.hashTreeRoot(
ssz.electra.ExecutionRequests.defaultValue()
);
Expand Down Expand Up @@ -86,7 +87,7 @@ export function upgradeStateToGloas(stateFulu: CachedBeaconStateFulu): CachedBea

/**
* Applies any pending deposits for builders to onboard builders during the fork transition
* Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.2/specs/gloas/fork.md#new-onboard_builders_from_pending_deposits
* Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.8/specs/gloas/fork.md#new-onboard_builders_from_pending_deposits
*/
function onboardBuildersFromPendingDeposits(state: CachedBeaconStateGloas): void {
// Track pubkeys of new builders added when applying deposits
Expand Down
3 changes: 2 additions & 1 deletion packages/types/src/gloas/sszTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export const ProposerPreferences = new ContainerType(
proposalSlot: Slot,
validatorIndex: ValidatorIndex,
feeRecipient: ExecutionAddress,
gasLimit: UintNum64,
targetGasLimit: UintNum64,
},
{typeName: "ProposerPreferences", jsonCase: "eth2"}
);
Expand Down Expand Up @@ -324,6 +324,7 @@ export const PayloadAttributes = new ContainerType(
{
...denebSsz.PayloadAttributes.fields,
slotNumber: Slot,
targetGasLimit: UintNum64,
},
{typeName: "PayloadAttributes", jsonCase: "eth2"}
);
Expand Down
2 changes: 1 addition & 1 deletion packages/validator/src/services/validatorStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -723,7 +723,7 @@ export class ValidatorStore {
proposalSlot: duty.slot,
validatorIndex: duty.validatorIndex,
feeRecipient: fromHex(feeRecipient),
gasLimit,
targetGasLimit: gasLimit,
};

const signingSlot = duty.slot;
Expand Down
1 change: 1 addition & 0 deletions packages/validator/src/util/params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,7 @@ function getSpecCriticalParams(localConfig: ChainConfig): Record<keyof ConfigWit
SYNC_MESSAGE_DUE_BPS_GLOAS: gloasForkRelevant,
CONTRIBUTION_DUE_BPS_GLOAS: gloasForkRelevant,
PAYLOAD_ATTESTATION_DUE_BPS: gloasForkRelevant,
PAYLOAD_DUE_BPS: gloasForkRelevant,
PTC_SIZE: gloasForkRelevant,
MAX_PAYLOAD_ATTESTATIONS: gloasForkRelevant,
BUILDER_REGISTRY_LIMIT: gloasForkRelevant,
Expand Down
2 changes: 1 addition & 1 deletion spec-tests-version.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"ethereumConsensusSpecsTests": {
"specVersion": "v1.7.0-alpha.7",
"specVersion": "v1.7.0-alpha.8",
"specTestsRepoUrl": "https://github.com/ethereum/consensus-specs",
"outputDirBase": "spec-tests",
"testsToDownload": [
Expand Down
Loading
Loading