Skip to content
Closed
20 changes: 19 additions & 1 deletion packages/state-transition/src/block/processPayloadAttestation.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {SLOTS_PER_EPOCH} from "@lodestar/params";
import {gloas} from "@lodestar/types";
import {byteArrayEquals} from "@lodestar/utils";
import {CachedBeaconStateGloas} from "../types.js";
import {computeEpochAtSlot} from "../util/epoch.js";
import {isValidIndexedPayloadAttestation} from "./isValidIndexedPayloadAttestation.js";

export function processPayloadAttestation(
Expand All @@ -17,7 +19,23 @@ export function processPayloadAttestation(
throw Error("Payload attestation is not from previous slot");
}

const indexedPayloadAttestation = state.epochCtx.getIndexedPayloadAttestation(data.slot, payloadAttestation);
// At epoch boundary, the PTC for the last slot of the previous epoch must be read
// from state.previousEpochLastPtc (effective balances may have changed during epoch processing).
// For all other slots, the epochCtx cache has the current epoch's PTCs.
const isEpochBoundary =
computeEpochAtSlot(data.slot) !== computeEpochAtSlot(state.slot) &&
data.slot % SLOTS_PER_EPOCH === SLOTS_PER_EPOCH - 1;

const ptc = isEpochBoundary
? new Uint32Array(state.previousEpochLastPtc.getAll())
: state.epochCtx.getPayloadTimelinessCommittee(data.slot);

const attestingIndices = payloadAttestation.aggregationBits.intersectValues(ptc);
const indexedPayloadAttestation: gloas.IndexedPayloadAttestation = {
attestingIndices: attestingIndices.sort((a, b) => a - b),
data: payloadAttestation.data,
signature: payloadAttestation.signature,
};

if (!isValidIndexedPayloadAttestation(state, indexedPayloadAttestation, true)) {
throw Error("Invalid payload attestation");
Expand Down
33 changes: 6 additions & 27 deletions packages/state-transition/src/cache/epochCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,9 +226,6 @@ export class EpochCache {
/** TODO: Indexed SyncCommitteeCache */
nextSyncCommitteeIndexed: SyncCommitteeCache;

// TODO GLOAS: See if we need to cache PTC for next epoch
// PTC for previous epoch, required for slot N block validating slot N-1 attestations
previousPayloadTimelinessCommittees: Uint32Array[];
// PTC for current epoch, computed eagerly at epoch transition
payloadTimelinessCommittees: Uint32Array[];

Expand Down Expand Up @@ -268,7 +265,6 @@ export class EpochCache {
previousTargetUnslashedBalanceIncrements: number;
currentSyncCommitteeIndexed: SyncCommitteeCache;
nextSyncCommitteeIndexed: SyncCommitteeCache;
previousPayloadTimelinessCommittees: Uint32Array[];
payloadTimelinessCommittees: Uint32Array[];
epoch: Epoch;
syncPeriod: SyncPeriod;
Expand Down Expand Up @@ -299,7 +295,6 @@ export class EpochCache {
this.previousTargetUnslashedBalanceIncrements = data.previousTargetUnslashedBalanceIncrements;
this.currentSyncCommitteeIndexed = data.currentSyncCommitteeIndexed;
this.nextSyncCommitteeIndexed = data.nextSyncCommitteeIndexed;
this.previousPayloadTimelinessCommittees = data.previousPayloadTimelinessCommittees;
this.payloadTimelinessCommittees = data.payloadTimelinessCommittees;
this.epoch = data.epoch;
this.syncPeriod = data.syncPeriod;
Expand Down Expand Up @@ -451,8 +446,7 @@ export class EpochCache {
nextSyncCommitteeIndexed = new SyncCommitteeCacheEmpty();
}

// Compute PTC for all slots in the prev/current epoch
let previousPayloadTimelinessCommittees: Uint32Array[] = [];
// Compute PTC for current epoch, load previous epoch last-slot PTC from state
let payloadTimelinessCommittees: Uint32Array[] = [];
if (currentEpoch >= config.GLOAS_FORK_EPOCH) {
payloadTimelinessCommittees = computePayloadTimelinessCommitteesForEpoch(
Expand All @@ -461,15 +455,6 @@ export class EpochCache {
currentShuffling.committees,
effectiveBalanceIncrements
);

if (!isGenesis && previousEpoch >= config.GLOAS_FORK_EPOCH) {
previousPayloadTimelinessCommittees = computePayloadTimelinessCommitteesForEpoch(
state,
previousEpoch,
previousShuffling.committees,
effectiveBalanceIncrements
);
}
}

// Precompute churnLimit for efficient initiateValidatorExit() during block proposing MUST be recompute everytime the
Expand Down Expand Up @@ -544,7 +529,6 @@ export class EpochCache {
currentTargetUnslashedBalanceIncrements,
currentSyncCommitteeIndexed,
nextSyncCommitteeIndexed,
previousPayloadTimelinessCommittees,
payloadTimelinessCommittees,
epoch: currentEpoch,
syncPeriod: computeSyncPeriodAtEpoch(currentEpoch),
Expand Down Expand Up @@ -590,7 +574,6 @@ export class EpochCache {
currentTargetUnslashedBalanceIncrements: this.currentTargetUnslashedBalanceIncrements,
currentSyncCommitteeIndexed: this.currentSyncCommitteeIndexed,
nextSyncCommitteeIndexed: this.nextSyncCommitteeIndexed,
previousPayloadTimelinessCommittees: this.previousPayloadTimelinessCommittees,
payloadTimelinessCommittees: this.payloadTimelinessCommittees,
epoch: this.epoch,
syncPeriod: this.syncPeriod,
Expand Down Expand Up @@ -702,8 +685,6 @@ export class EpochCache {

this.proposersPrevEpoch = this.proposers;
if (upcomingEpoch >= this.config.GLOAS_FORK_EPOCH) {
// Shift and compute current epoch PTC eagerly for all slots
this.previousPayloadTimelinessCommittees = this.payloadTimelinessCommittees;
this.payloadTimelinessCommittees = computePayloadTimelinessCommitteesForEpoch(
state,
upcomingEpoch,
Expand Down Expand Up @@ -1034,15 +1015,13 @@ export class EpochCache {
throw new Error("Payload Timeliness Committee is not available before gloas fork");
}

if (epoch === this.epoch) {
return this.payloadTimelinessCommittees[slot % SLOTS_PER_EPOCH];
}

if (epoch === this.epoch - 1 && this.previousPayloadTimelinessCommittees.length > 0) {
return this.previousPayloadTimelinessCommittees[slot % SLOTS_PER_EPOCH];
if (epoch !== this.epoch) {
throw new Error(
`Payload Timeliness Committee is only available for current epoch, slot=${slot} epoch=${epoch} cache_epoch=${this.epoch}`
);
}

throw new Error(`Payload Timeliness Committee is not available for slot=${slot}`);
return this.payloadTimelinessCommittees[slot % SLOTS_PER_EPOCH];
}

getIndexedPayloadAttestation(
Expand Down
6 changes: 6 additions & 0 deletions packages/state-transition/src/epoch/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
CachedBeaconStatePhase0,
EpochTransitionCache,
} from "../types.js";
import {processPtcUpdate} from "../util/gloas.js";
import {processBuilderPendingPayments} from "./processBuilderPendingPayments.js";
import {processEffectiveBalanceUpdates} from "./processEffectiveBalanceUpdates.js";
import {processEth1DataReset} from "./processEth1DataReset.js";
Expand Down Expand Up @@ -98,6 +99,11 @@ export function processEpoch(
throw new Error("Lodestar does not support this network, parameters don't fit number value inside state.slashings");
}

// [New in Gloas:EIP7732] Cache last-slot PTC before effective balance updates
if (fork >= ForkSeq.gloas) {
processPtcUpdate(state as CachedBeaconStateGloas);
}

{
const timer = metrics?.epochTransitionStepTime.startTimer({
step: EpochTransitionStep.processJustificationAndFinalization,
Expand Down
5 changes: 4 additions & 1 deletion packages/state-transition/src/slot/upgradeStateToGloas.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params";
import {PTC_SIZE, SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params";
import {ssz} from "@lodestar/types";
import {toHex} from "@lodestar/utils";
import {isValidDepositSignature} from "../block/processDeposit.js";
Expand Down Expand Up @@ -69,6 +69,9 @@ export function upgradeStateToGloas(stateFulu: CachedBeaconStateFulu): CachedBea

const stateGloas = getCachedBeaconState(stateGloasView, stateFulu);

// Initialize previousEpochLastPtc to zeros (no previous epoch PTC at fork boundary)
stateGloas.previousEpochLastPtc = ssz.gloas.PayloadTimelinessCommittee.toViewDU(new Array(PTC_SIZE).fill(0));

// Process pending builder deposits at the fork boundary
onboardBuildersFromPendingDeposits(stateGloas);

Expand Down
3 changes: 3 additions & 0 deletions packages/state-transition/src/util/genesis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
GENESIS_EPOCH,
GENESIS_SLOT,
MAX_EFFECTIVE_BALANCE,
PTC_SIZE,
UNSET_DEPOSIT_REQUESTS_START_INDEX,
} from "@lodestar/params";
import {Bytes32, Root, TimeSeconds, phase0, ssz} from "@lodestar/types";
Expand Down Expand Up @@ -332,6 +333,8 @@ export function initializeBeaconStateFromEth1(
const stateGloas = state as CompositeViewDU<typeof ssz.gloas.BeaconState>;
stateGloas.fork.previousVersion = config.GLOAS_FORK_VERSION;
stateGloas.fork.currentVersion = config.GLOAS_FORK_VERSION;
// Initialize previousEpochLastPtc to zeros (no previous epoch PTC at genesis)
stateGloas.previousEpochLastPtc = ssz.gloas.PayloadTimelinessCommittee.toViewDU(new Array(PTC_SIZE).fill(0));
}

state.commit();
Expand Down
16 changes: 15 additions & 1 deletion packages/state-transition/src/util/gloas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ import {
MIN_DEPOSIT_AMOUNT,
SLOTS_PER_EPOCH,
} from "@lodestar/params";
import {BuilderIndex, Epoch, ValidatorIndex, gloas} from "@lodestar/types";
import {BuilderIndex, Epoch, ValidatorIndex, gloas, ssz} from "@lodestar/types";
import {AttestationData} from "@lodestar/types/phase0";
import {byteArrayEquals} from "@lodestar/utils";
import {CachedBeaconStateGloas} from "../types.js";
import {getBlockRootAtSlot} from "./blockRoot.js";
import {computeEpochAtSlot} from "./epoch.js";
import {RootCache} from "./rootCache.js";
import {computePayloadTimelinessCommitteeAtSlot} from "./seed.js";

export function isBuilderWithdrawalCredential(withdrawalCredentials: Uint8Array): boolean {
return withdrawalCredentials[0] === BUILDER_WITHDRAWAL_PREFIX;
Expand All @@ -28,6 +29,19 @@ export function getBuilderPaymentQuorumThreshold(state: CachedBeaconStateGloas):
return Math.floor(quorum / BUILDER_PAYMENT_THRESHOLD_DENOMINATOR);
}

export function processPtcUpdate(state: CachedBeaconStateGloas): void {
const slot = state.slot;
const slotInEpoch = slot % SLOTS_PER_EPOCH;
const ptc = computePayloadTimelinessCommitteeAtSlot(
state,
slot,
state.epochCtx.currentShuffling.committees[slotInEpoch],
state.epochCtx.effectiveBalanceIncrements
);

state.previousEpochLastPtc = ssz.gloas.PayloadTimelinessCommittee.toViewDU(Array.from(ptc));
}

function hasBuilderIndexFlag(index: number): boolean {
// Equivalent to `(index & BUILDER_INDEX_FLAG) != 0`
return Math.floor(index / BUILDER_INDEX_FLAG) % 2 === 1;
Expand Down
27 changes: 26 additions & 1 deletion packages/state-transition/src/util/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
SLOTS_PER_EPOCH,
SYNC_COMMITTEE_SIZE,
} from "@lodestar/params";
import {Bytes32, DomainType, Epoch, ValidatorIndex} from "@lodestar/types";
import {Bytes32, DomainType, Epoch, Slot, ValidatorIndex} from "@lodestar/types";
import {assert, bytesToBigInt, bytesToInt, intToBytes} from "@lodestar/utils";
import {EffectiveBalanceIncrements} from "../cache/effectiveBalanceIncrements.js";
import {BeaconStateAllForks, CachedBeaconStateAllForks} from "../types.js";
Expand Down Expand Up @@ -268,6 +268,31 @@ export function getNextSyncCommitteeIndices(
);
}

/**
* Compute PTC for a single slot using the state's current effective balances.
*/
export function computePayloadTimelinessCommitteeAtSlot(
state: BeaconStateAllForks,
slot: Slot,
committees: Uint32Array[],
effectiveBalanceIncrements: EffectiveBalanceIncrements
): Uint32Array {
const epoch = computeEpochAtSlot(slot);
const stateEpoch = computeEpochAtSlot(state.slot);
if (epoch > stateEpoch) {
throw new Error(`compute_ptc: epoch ${epoch} > current epoch ${stateEpoch}`);
}
const epochSeed = getSeed(state, epoch, DOMAIN_PTC_ATTESTER);
const slotSeedInput = new Uint8Array(epochSeed.length + 8);
slotSeedInput.set(epochSeed, 0);
const slotSeedView = new DataView(slotSeedInput.buffer, slotSeedInput.byteOffset, slotSeedInput.byteLength);

slotSeedView.setUint32(epochSeed.length, slot, true);
slotSeedView.setUint32(epochSeed.length + 4, 0, true);

return computePayloadTimelinessCommitteeForSlot(digest(slotSeedInput), committees, effectiveBalanceIncrements);
}

/**
* Compute PTC for all slots in an epoch eagerly.
*/
Expand Down
12 changes: 11 additions & 1 deletion packages/types/src/gloas/sszTypes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import {BitVectorType, ContainerType, ListBasicType, ListCompositeType, VectorCompositeType} from "@chainsafe/ssz";
import {
BitVectorType,
ContainerType,
ListBasicType,
ListCompositeType,
VectorBasicType,
VectorCompositeType,
} from "@chainsafe/ssz";
import {
BUILDER_PENDING_WITHDRAWALS_LIMIT,
BUILDER_REGISTRY_LIMIT,
Expand Down Expand Up @@ -94,6 +101,8 @@ export const PayloadAttestationMessage = new ContainerType(
{typeName: "PayloadAttestationMessage", jsonCase: "eth2"}
);

export const PayloadTimelinessCommittee = new VectorBasicType(ValidatorIndex, PTC_SIZE);

export const IndexedPayloadAttestation = new ContainerType(
{
attestingIndices: new ListBasicType(ValidatorIndex, PTC_SIZE),
Expand Down Expand Up @@ -263,6 +272,7 @@ export const BeaconState = new ContainerType(
builderPendingWithdrawals: new ListCompositeType(BuilderPendingWithdrawal, BUILDER_PENDING_WITHDRAWALS_LIMIT), // New in GLOAS:EIP7732
latestBlockHash: Bytes32, // New in GLOAS:EIP7732
payloadExpectedWithdrawals: capellaSsz.Withdrawals, // New in GLOAS:EIP7732
previousEpochLastPtc: PayloadTimelinessCommittee, // New in GLOAS:EIP7732
},
{typeName: "BeaconState", jsonCase: "eth2"}
);
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/gloas/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export type BuilderPendingPayment = ValueOf<typeof ssz.BuilderPendingPayment>;
export type PayloadAttestationData = ValueOf<typeof ssz.PayloadAttestationData>;
export type PayloadAttestation = ValueOf<typeof ssz.PayloadAttestation>;
export type PayloadAttestationMessage = ValueOf<typeof ssz.PayloadAttestationMessage>;
export type PayloadTimelinessCommittee = ValueOf<typeof ssz.PayloadTimelinessCommittee>;
export type IndexedPayloadAttestation = ValueOf<typeof ssz.IndexedPayloadAttestation>;
export type ProposerPreferences = ValueOf<typeof ssz.ProposerPreferences>;
export type SignedProposerPreferences = ValueOf<typeof ssz.SignedProposerPreferences>;
Expand Down
35 changes: 35 additions & 0 deletions specrefs/functions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1015,6 +1015,28 @@
return (committee_weight * PROPOSER_SCORE_BOOST) // 100
</spec>

- name: compute_ptc#gloas
sources:
- file: packages/state-transition/src/util/seed.ts
search: '^export function computePayloadTimelinessCommitteeAtSlot\('
regex: true
spec: |
def compute_ptc(state: BeaconState, slot: Slot) -> Vector[ValidatorIndex, PTC_SIZE]:
"""
Compute the payload timeliness committee for the given ``slot``
using the state's current effective balances.
"""
epoch = compute_epoch_at_slot(slot)
seed = hash(get_seed(state, epoch, DOMAIN_PTC_ATTESTER) + uint_to_bytes(slot))
indices: List[ValidatorIndex] = []
committees_per_slot = get_committee_count_per_slot(state, epoch)
for i in range(committees_per_slot):
committee = get_beacon_committee(state, slot, CommitteeIndex(i))
indices.extend(committee)
return compute_balance_weighted_selection(
state, indices, seed, size=PTC_SIZE, shuffle_indices=False
)

- name: compute_pulled_up_tip#phase0
sources:
- file: packages/fork-choice/src/forkChoice/forkChoice.ts
Expand Down Expand Up @@ -8705,6 +8727,19 @@
slash_validator(state, header_1.proposer_index)
</spec>

- name: process_ptc_update#gloas
sources:
- file: packages/state-transition/src/util/gloas.ts
search: '^export function processPtcUpdate\('
regex: true
spec: |
def process_ptc_update(state: BeaconState) -> None:
"""
Cache the PTC for the current slot (last slot of the ending epoch)
before effective balance updates alter the weighted selection.
"""
state.previous_epoch_last_ptc = compute_ptc(state, Slot(state.slot))

- name: process_randao#phase0
sources:
- file: packages/state-transition/src/block/processRandao.ts
Expand Down
Loading