Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
import {
CachedBeaconStateAllForks,
CachedBeaconStateAltair,
CachedBeaconStateGloas,
CachedBeaconStatePhase0,
EffectiveBalanceIncrements,
RootCache,
Expand Down Expand Up @@ -486,7 +487,10 @@ export class AggregatedAttestationPool {
consolidation.attData,
inclusionDistance,
stateEpoch,
rootCache
rootCache,
ForkSeq[fork] >= ForkSeq.gloas
? (state as CachedBeaconStateGloas).executionPayloadAvailability.toBoolArray()
: null
);

const weight =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const epochTransitionFns: Record<string, EpochTransitionFn> = {
const fork = state.config.getForkSeq(state.slot);
epochFns.processProposerLookahead(fork, state as CachedBeaconStateFulu, epochTransitionCache);
},
builder_pending_payments: epochFns.processBuilderPendingPayments as EpochTransitionFn,
};

/**
Expand Down
41 changes: 32 additions & 9 deletions packages/beacon-node/test/spec/presets/operations.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import path from "node:path";
import {ACTIVE_PRESET, ForkName} from "@lodestar/params";
import {ACTIVE_PRESET, ForkName, ForkSeq} from "@lodestar/params";
import {InputType} from "@lodestar/spec-test-util";
import {
BeaconStateAllForks,
CachedBeaconStateAllForks,
CachedBeaconStateBellatrix,
CachedBeaconStateCapella,
CachedBeaconStateElectra,
CachedBeaconStateGloas,
ExecutionPayloadStatus,
getBlockRootAtSlot,
} from "@lodestar/state-transition";
import * as blockFns from "@lodestar/state-transition/block";
import {AttesterSlashing, altair, bellatrix, capella, electra, phase0, ssz, sszTypesFor} from "@lodestar/types";
import {AttesterSlashing, altair, bellatrix, capella, electra, gloas, phase0, ssz, sszTypesFor} from "@lodestar/types";
import {createCachedBeaconStateTest} from "../../utils/cachedBeaconState.js";
import {getConfig} from "../../utils/config.js";
import {ethereumConsensusSpecsTests} from "../specTestVersioning.js";
Expand Down Expand Up @@ -67,13 +68,24 @@ const operationFns: Record<string, BlockProcessFn<CachedBeaconStateAllForks>> =
blockFns.processVoluntaryExit(fork, state, testCase.voluntary_exit);
},

execution_payload: (state, testCase: {body: bellatrix.BeaconBlockBody; execution: {execution_valid: boolean}}) => {
execution_payload: (
state,
testCase: {
body: bellatrix.BeaconBlockBody | gloas.BeaconBlockBody;
signed_envelope: gloas.SignedExecutionPayloadEnvelope;
execution: {execution_valid: boolean};
}
) => {
const fork = state.config.getForkSeq(state.slot);
blockFns.processExecutionPayload(fork, state as CachedBeaconStateBellatrix, testCase.body, {
executionPayloadStatus: testCase.execution.execution_valid
? ExecutionPayloadStatus.valid
: ExecutionPayloadStatus.invalid,
});
if (fork >= ForkSeq.gloas) {
blockFns.processExecutionPayloadEnvelope(state as CachedBeaconStateGloas, testCase.signed_envelope, true);
} else {
blockFns.processExecutionPayload(fork, state as CachedBeaconStateBellatrix, testCase.body, {
executionPayloadStatus: testCase.execution.execution_valid
? ExecutionPayloadStatus.valid
: ExecutionPayloadStatus.invalid,
});
}
},

bls_to_execution_change: (state, testCase: {address_change: capella.SignedBLSToExecutionChange}) => {
Expand All @@ -95,7 +107,16 @@ const operationFns: Record<string, BlockProcessFn<CachedBeaconStateAllForks>> =
},

consolidation_request: (state, testCase: {consolidation_request: electra.ConsolidationRequest}) => {
blockFns.processConsolidationRequest(state as CachedBeaconStateElectra, testCase.consolidation_request);
const fork = state.config.getForkSeq(state.slot);
blockFns.processConsolidationRequest(fork, state as CachedBeaconStateElectra, testCase.consolidation_request);
},

execution_payload_bid: (state, testCase: {block: gloas.BeaconBlock}) => {
blockFns.processExecutionPayloadBid(state as CachedBeaconStateGloas, testCase.block);
},

payload_attestation: (state, testCase: {payload_attestation: gloas.PayloadAttestation}) => {
blockFns.processPayloadAttestation(state as CachedBeaconStateGloas, testCase.payload_attestation);
},
};

Expand Down Expand Up @@ -149,6 +170,8 @@ const operations: TestRunnerFn<OperationsTestCase, BeaconStateAllForks> = (fork,
withdrawal_request: ssz.electra.WithdrawalRequest,
deposit_request: ssz.electra.DepositRequest,
consolidation_request: ssz.electra.ConsolidationRequest,
payload_attestation: ssz.gloas.PayloadAttestation,
signed_envelope: ssz.gloas.SignedExecutionPayloadEnvelope,
},
shouldError: (testCase) => testCase.post === undefined,
getExpected: (testCase) => testCase.post,
Expand Down
1 change: 1 addition & 0 deletions packages/fork-choice/src/forkChoice/forkChoice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1617,6 +1617,7 @@ export class ForkChoice implements IForkChoice {
* the specs mandates validating terminal conditions on the previously
* imported merge block.
*/
// TODO GLOAS: See if we need to update this for gloas
export function assertValidTerminalPowBlock(
config: ChainConfig,
block: bellatrix.BeaconBlock,
Expand Down
33 changes: 28 additions & 5 deletions packages/state-transition/src/block/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
import {ForkSeq} from "@lodestar/params";
import {BeaconBlock, BlindedBeaconBlock, altair, capella} from "@lodestar/types";
import {BeaconBlock, BlindedBeaconBlock, altair, capella, gloas} from "@lodestar/types";
import {BeaconStateTransitionMetrics} from "../metrics.js";
import {CachedBeaconStateAllForks, CachedBeaconStateBellatrix, CachedBeaconStateCapella} from "../types.js";
import {
CachedBeaconStateAllForks,
CachedBeaconStateBellatrix,
CachedBeaconStateCapella,
CachedBeaconStateGloas,
} from "../types.js";
import {getFullOrBlindedPayload, isExecutionEnabled} from "../util/execution.js";
import {BlockExternalData, DataAvailabilityStatus} from "./externalData.js";
import {processBlobKzgCommitments} from "./processBlobKzgCommitments.js";
import {processBlockHeader} from "./processBlockHeader.js";
import {processEth1Data} from "./processEth1Data.js";
import {processExecutionPayload} from "./processExecutionPayload.js";
import {processExecutionPayloadBid} from "./processExecutionPayloadBid.ts";
import {processExecutionPayloadEnvelope} from "./processExecutionPayloadEnvelope.ts";
import {processOperations} from "./processOperations.js";
import {processPayloadAttestation} from "./processPayloadAttestation.ts";
import {processRandao} from "./processRandao.js";
import {processSyncAggregate} from "./processSyncCommittee.js";
import {processWithdrawals} from "./processWithdrawals.js";
Expand All @@ -22,6 +30,9 @@ export {
processEth1Data,
processSyncAggregate,
processWithdrawals,
processExecutionPayloadBid,
processPayloadAttestation,
processExecutionPayloadEnvelope,
};

export * from "./externalData.js";
Expand All @@ -41,23 +52,35 @@ export function processBlock(

processBlockHeader(state, block);

// The call to the process_execution_payload must happen before the call to the process_randao as the former depends
// on the randao_mix computed with the reveal of the previous block.
if (fork >= ForkSeq.bellatrix && isExecutionEnabled(state as CachedBeaconStateBellatrix, block)) {
if (fork >= ForkSeq.capella) {
const fullOrBlindedPayload = getFullOrBlindedPayload(block);
// TODO Deneb: Allow to disable withdrawals for interop testing
// https://github.com/ethereum/consensus-specs/blob/b62c9e877990242d63aa17a2a59a49bc649a2f2e/specs/eip4844/beacon-chain.md#disabling-withdrawals
if (fork >= ForkSeq.capella) {
// TODO
processWithdrawals(
fork,
state as CachedBeaconStateCapella,
fullOrBlindedPayload as capella.FullOrBlindedExecutionPayload
);
}
}

// The call to the process_execution_payload must happen before the call to the process_randao as the former depends
// on the randao_mix computed with the reveal of the previous block.
// We call processExecutionPayload somehwere else post-gloas
if (
fork >= ForkSeq.bellatrix &&
fork < ForkSeq.gloas &&
isExecutionEnabled(state as CachedBeaconStateBellatrix, block)
) {
processExecutionPayload(fork, state as CachedBeaconStateBellatrix, block.body, externalData);
}

if (fork >= ForkSeq.gloas) {
processExecutionPayloadBid(state as CachedBeaconStateGloas, block as gloas.BeaconBlock);
}

processRandao(state, block, verifySignatures);
processEth1Data(state, block.body.eth1Data);
processOperations(fork, state, block.body, opts, metrics);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {gloas} from "@lodestar/types";
import {getIndexedPayloadAttestationSignatureSet} from "../signatureSets/index.ts";
import {CachedBeaconStateGloas} from "../types.js";
import {verifySignatureSet} from "../util/index.ts";

export function isValidIndexedPayloadAttestation(
state: CachedBeaconStateGloas,
indexedPayloadAttestation: gloas.IndexedPayloadAttestation,
verifySignature: boolean
): boolean {
const indices = indexedPayloadAttestation.attestingIndices;
const isSorted = indices.every((val, i, arr) => i === 0 || arr[i - 1] <= val);
Copy link
Contributor

Choose a reason for hiding this comment

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

critical

The check for sorted indices arr[i - 1] <= val allows for duplicate validator indices, but the specification requires indices to be strictly increasing, which also implies uniqueness. This could lead to invalid attestations being considered valid.

According to the is_sorted function in the consensus specs, the check should be for strict inequality (<).

Suggested change
const isSorted = indices.every((val, i, arr) => i === 0 || arr[i - 1] <= val);
const isSorted = indices.every((val, i, arr) => i === 0 || arr[i - 1] < val);


if (indices.length === 0 || !isSorted) {
return false;
}

if (verifySignature) {
return verifySignatureSet(getIndexedPayloadAttestationSignatureSet(state, indexedPayloadAttestation));
}

return true;
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,11 @@ export function validateAttestation(fork: ForkSeq, state: CachedBeaconStateAllFo
}

if (fork >= ForkSeq.electra) {
assert.equal(data.index, 0, `AttestationData.index must be zero: index=${data.index}`);
if (fork >= ForkSeq.gloas) {
assert.lt(data.index, 2, `AttestationData.index must be 0 or 1: index=${data.index}`);
} else {
assert.equal(data.index, 0, `AttestationData.index must be zero: index=${data.index}`);
}
const attestationElectra = attestation as electra.Attestation;
const committeeIndices = attestationElectra.committeeBits.getTrueBitIndexes();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import {byteArrayEquals} from "@chainsafe/ssz";
import {
EFFECTIVE_BALANCE_INCREMENT,
ForkSeq,
MIN_ATTESTATION_INCLUSION_DELAY,
PROPOSER_WEIGHT,
SLOTS_PER_EPOCH,
SLOTS_PER_HISTORICAL_ROOT,
TIMELY_HEAD_FLAG_INDEX,
TIMELY_HEAD_WEIGHT,
TIMELY_SOURCE_FLAG_INDEX,
Expand All @@ -16,7 +18,8 @@ import {Attestation, Epoch, phase0} from "@lodestar/types";
import {intSqrt} from "@lodestar/utils";
import {BeaconStateTransitionMetrics} from "../metrics.js";
import {getAttestationWithIndicesSignatureSet} from "../signatureSets/indexedAttestation.js";
import {CachedBeaconStateAltair} from "../types.js";
import {CachedBeaconStateAltair, CachedBeaconStateGloas} from "../types.js";
import {isAttestationSameSlot, isAttestationSameSlotRootCache} from "../util/gloas.ts";
import {increaseBalance, verifySignatureSet} from "../util/index.js";
import {RootCache} from "../util/rootCache.js";
import {checkpointToStr, isTimelyTarget, validateAttestation} from "./processAttestationPhase0.js";
Expand All @@ -31,7 +34,7 @@ const SLOTS_PER_EPOCH_SQRT = intSqrt(SLOTS_PER_EPOCH);

export function processAttestationsAltair(
fork: ForkSeq,
state: CachedBeaconStateAltair,
state: CachedBeaconStateAltair | CachedBeaconStateGloas,
attestations: Attestation[],
verifySignature = true,
metrics?: BeaconStateTransitionMetrics | null
Expand All @@ -46,6 +49,9 @@ export function processAttestationsAltair(
let proposerReward = 0;
let newSeenAttesters = 0;
let newSeenAttestersEffectiveBalance = 0;

const builderWeightMap: Map<number, number> = new Map();

for (const attestation of attestations) {
const data = attestation.data;

Expand All @@ -66,13 +72,16 @@ export function processAttestationsAltair(

const inCurrentEpoch = data.target.epoch === currentEpoch;
const epochParticipation = inCurrentEpoch ? state.currentEpochParticipation : state.previousEpochParticipation;
// Count how much additional weight added to current or previous epoch's builder pending payment (in ETH increment)
let paymentWeightToAdd = 0;

const flagsAttestation = getAttestationParticipationStatus(
fork,
data,
stateSlot - data.slot,
epochCtx.epoch,
rootCache
rootCache,
fork >= ForkSeq.gloas ? (state as CachedBeaconStateGloas).executionPayloadAvailability.toBoolArray() : null
);

// For each participant, update their participation
Expand Down Expand Up @@ -121,12 +130,35 @@ export function processAttestationsAltair(
}
}
}

if (fork >= ForkSeq.gloas && flagsNewSet !== 0 && isAttestationSameSlot(state as CachedBeaconStateGloas, data)) {
paymentWeightToAdd += effectiveBalanceIncrements[validatorIndex];
}
}

// Do the discrete math inside the loop to ensure a deterministic result
const totalIncrements = totalBalanceIncrementsWithWeight;
const proposerRewardNumerator = totalIncrements * state.epochCtx.baseRewardPerIncrement;
proposerReward += Math.floor(proposerRewardNumerator / PROPOSER_REWARD_DOMINATOR);

if (fork >= ForkSeq.gloas) {
const builderPendingPaymentIndex = inCurrentEpoch
? SLOTS_PER_EPOCH + (data.slot % SLOTS_PER_EPOCH)
: data.slot % SLOTS_PER_EPOCH;

const existingWeight =
builderWeightMap.get(builderPendingPaymentIndex) ??
(state as CachedBeaconStateGloas).builderPendingPayments.get(builderPendingPaymentIndex).weight;
const updatedWeight = existingWeight + paymentWeightToAdd * EFFECTIVE_BALANCE_INCREMENT;
builderWeightMap.set(builderPendingPaymentIndex, updatedWeight);
}
}

for (const [index, weight] of builderWeightMap) {
const payment = (state as CachedBeaconStateGloas).builderPendingPayments.get(index);
if (payment.withdrawal.amount > 0) {
payment.weight = weight;
}
}

metrics?.newSeenAttestersPerBlock.set(newSeenAttesters);
Expand All @@ -145,7 +177,8 @@ export function getAttestationParticipationStatus(
data: phase0.AttestationData,
inclusionDelay: number,
currentEpoch: Epoch,
rootCache: RootCache
rootCache: RootCache,
executionPayloadAvailability: boolean[] | null
): number {
const justifiedCheckpoint =
data.target.epoch === currentEpoch ? rootCache.currentJustifiedCheckpoint : rootCache.previousJustifiedCheckpoint;
Expand All @@ -168,9 +201,33 @@ export function getAttestationParticipationStatus(
const isMatchingTarget = byteArrayEquals(data.target.root, rootCache.getBlockRoot(data.target.epoch));

// a timely head is only be set if the target is _also_ matching
const isMatchingHead =
// In gloas, this is called `is_matching_blockroot`
let isMatchingHead =
isMatchingTarget && byteArrayEquals(data.beaconBlockRoot, rootCache.getBlockRootAtSlot(data.slot));

if (fork >= ForkSeq.gloas) {
let isMatchingPayload = false;

if (isAttestationSameSlotRootCache(rootCache, data)) {
if (data.index !== 0) {
throw new Error("Attesting same slot must indicate empty payload");
}
isMatchingPayload = true;
} else {
if (executionPayloadAvailability === null) {
throw new Error("Must supply executionPayloadAvailability post-gloas");
}

if (data.index !== 0 && data.index !== 1) {
throw new Error(`data index must be 0 or 1 index=${data.index}`);
}

isMatchingPayload = Boolean(data.index) === executionPayloadAvailability[data.slot % SLOTS_PER_HISTORICAL_ROOT];
}

isMatchingHead = isMatchingHead && isMatchingPayload;
}

let flags = 0;
if (isMatchingSource && inclusionDelay <= SLOTS_PER_EPOCH_SQRT) flags |= TIMELY_SOURCE;
if (isMatchingTarget && isTimelyTarget(fork, inclusionDelay)) flags |= TIMELY_TARGET;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {FAR_FUTURE_EPOCH, MIN_ACTIVATION_BALANCE, PENDING_CONSOLIDATIONS_LIMIT} from "@lodestar/params";
import {FAR_FUTURE_EPOCH, ForkSeq, MIN_ACTIVATION_BALANCE, PENDING_CONSOLIDATIONS_LIMIT} from "@lodestar/params";
import {electra, ssz} from "@lodestar/types";
import {CachedBeaconStateElectra} from "../types.js";
import {hasEth1WithdrawalCredential} from "../util/capella.js";
Expand All @@ -13,6 +13,7 @@ import {getConsolidationChurnLimit, getPendingBalanceToWithdraw, isActiveValidat

// TODO Electra: Clean up necessary as there is a lot of overlap with isValidSwitchToCompoundRequest
export function processConsolidationRequest(
fork: ForkSeq,
state: CachedBeaconStateElectra,
consolidationRequest: electra.ConsolidationRequest
): void {
Expand Down Expand Up @@ -82,7 +83,7 @@ export function processConsolidationRequest(
}

// Verify the source has no pending withdrawals in the queue
if (getPendingBalanceToWithdraw(state, sourceIndex) > 0) {
if (getPendingBalanceToWithdraw(fork, state, sourceIndex) > 0) {
return;
}

Expand Down
Loading
Loading