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/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
},
"dependencies": {
"@chainsafe/persistent-merkle-tree": "^1.2.1",
"@chainsafe/ssz": "^1.2.2",
"@chainsafe/ssz": "^1.4.0",
"@lodestar/config": "workspace:^",
"@lodestar/params": "workspace:^",
"@lodestar/types": "workspace:^",
Expand Down
3 changes: 2 additions & 1 deletion packages/api/src/beacon/routes/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -780,7 +780,8 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions<Endpoi
(data as BlockContents).block as BeaconBlock<ForkPreDeneb> // <- tranformation
);
},
deserialize(data, {executionPayloadBlinded, version}) {
deserialize(data, meta) {
const {executionPayloadBlinded, version} = meta as ProduceBlockV3Meta;
return executionPayloadBlinded
? getPostBellatrixForkTypes(version).BlindedBeaconBlock.deserialize(data)
: isForkPostDeneb(version)
Expand Down
5 changes: 4 additions & 1 deletion packages/api/src/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,10 @@ export type ResponseDataCodec<T, M> = {
toJson: (data: T, meta: M) => unknown; // server
fromJson: (data: unknown, meta: M) => T; // client
serialize: (data: T, meta: M) => Uint8Array; // server
deserialize: (data: Uint8Array, meta: M) => T; // client
// `meta` is typed as `any` so SSZ `Type<T>.deserialize(data, opts?)` satisfies the shape.
// Route-specific deserializers (e.g. `WithVersion`) cast to their expected meta internally.
// biome-ignore lint/suspicious/noExplicitAny: contravariant workaround for ssz Type.deserialize(opts?)
deserialize: (data: Uint8Array, meta: any) => T; // client
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.

this is due to ChainSafe/ssz#503 which we aren't even making use of but this is not a big deal, we can revisit this later cc @wemeetagain

};

export type ResponseMetadataCodec<T> = {
Expand Down
2 changes: 1 addition & 1 deletion packages/beacon-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@
"@chainsafe/prometheus-gc-stats": "^1.0.0",
"@chainsafe/pubkey-index-map": "^3.0.0",
"@chainsafe/snappy-wasm": "^0.5.0",
"@chainsafe/ssz": "^1.2.2",
"@chainsafe/ssz": "^1.4.0",
"@chainsafe/threads": "^1.11.3",
"@crate-crypto/node-eth-kzg": "0.9.1",
"@fastify/bearer-auth": "^10.0.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ export async function validateApiPayloadAttestationMessage(
chain: IBeaconChain,
payloadAttestationMessage: gloas.PayloadAttestationMessage
): Promise<PayloadAttestationValidationResult> {
return validatePayloadAttestationMessage(chain, payloadAttestationMessage);
const prioritizeBls = true;
return validatePayloadAttestationMessage(chain, payloadAttestationMessage, prioritizeBls);
}

export async function validateGossipPayloadAttestationMessage(
Expand All @@ -30,7 +31,8 @@ export async function validateGossipPayloadAttestationMessage(

async function validatePayloadAttestationMessage(
chain: IBeaconChain,
payloadAttestationMessage: gloas.PayloadAttestationMessage
payloadAttestationMessage: gloas.PayloadAttestationMessage,
prioritizeBls = false
): Promise<PayloadAttestationValidationResult> {
const {data, validatorIndex} = payloadAttestationMessage;
const epoch = computeEpochAtSlot(data.slot);
Expand Down Expand Up @@ -102,7 +104,7 @@ async function validatePayloadAttestationMessage(
payloadAttestationMessage.signature
);

if (!(await chain.bls.verifySignatureSets([signatureSet]))) {
if (!(await chain.bls.verifySignatureSets([signatureSet], {batchable: true, priority: prioritizeBls}))) {
Comment thread
twoeths marked this conversation as resolved.
throw new PayloadAttestationError(GossipAction.REJECT, {
code: PayloadAttestationErrorCode.INVALID_SIGNATURE,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
CachedBeaconStateAllForks,
CachedBeaconStateAltair,
CachedBeaconStateFulu,
CachedBeaconStateGloas,
EpochTransitionCache,
beforeProcessEpoch,
} from "@lodestar/state-transition";
Expand Down Expand Up @@ -51,6 +52,9 @@ const epochTransitionFns: Record<string, EpochTransitionFn> = {
const fork = state.config.getForkSeq(state.slot);
epochFns.processProposerLookahead(fork, state as CachedBeaconStateFulu, epochTransitionCache);
},
ptc_window: (state, epochTransitionCache) => {
epochFns.processPtcWindow(state as CachedBeaconStateGloas, epochTransitionCache);
},
builder_pending_payments: epochFns.processBuilderPendingPayments as EpochTransitionFn,
};

Expand Down
8 changes: 4 additions & 4 deletions packages/beacon-node/test/spec/presets/fork_choice.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -588,11 +588,11 @@ const forkChoiceTest =
name.includes("voting_source_beyond_two_epoch") ||
name.includes("justified_update_always_if_better") ||
name.includes("justified_update_not_realized_finality") ||
// TODO GLOAS: Requires should_apply_proposer_boost (gloas/fork-choice.md#new-should_apply_proposer_boost)
// which conditionally suppresses proposer boost when the parent is weak and from the previous slot.
// Pre-Gloas forks always apply boost; Gloas adds is_head_weak + equivocation checks.
// TODO GLOAS: These tests will be unskipped by https://github.com/ChainSafe/lodestar/pull/9233
(name.includes("gloas") &&
name.includes("include_votes_another_empty_chain_with_enough_ffg_votes_previous_epoch")) ||
(name.includes("simple_attempted_reorg_without_enough_ffg_votes") ||
name.includes("include_votes_another_empty_chain_with_enough_ffg_votes_current_epoch") ||
name.includes("include_votes_another_empty_chain_without_enough_ffg_votes_current_epoch"))) ||
// TODO GLOAS: These two tests are affected by the wrong proposer boost cutoff time from the
// consensus-specs and thus have wrong expectation of proposer boost. Our implementation
// should pass these two tests after https://github.com/ethereum/consensus-specs/pull/5095
Expand Down
10 changes: 5 additions & 5 deletions packages/beacon-node/test/spec/utils/specTestIterator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,12 @@ export const defaultSkipOpts: SkipOpts = {
/^fulu\/light_client\/single_merkle_proof\/BeaconBlockBody.*/,
/^.+\/light_client\/data_collection\/.*/,
/^gloas\/ssz_static\/ForkChoiceNode.*$/,
// Ignore the partial data column container additions for now. Unskip them when
// cell level DAS is ready
/^fulu\/ssz_static\/PartialDataColumn(Header|PartsMetadata|Sidecar)\/.*$/,
/^gloas\/ssz_static\/PartialDataColumn(Header|PartsMetadata|Sidecar)\/.*$/,
],
skippedTests: [
// TODO GLOAS: broken in v1.7.0-alpha.3 due to missing voluntary_exit.ssz_snappy input file
// Fixed by https://github.com/ethereum/consensus-specs/pull/5005 in v1.7.0-alpha.4
/^gloas\/operations\/voluntary_exit\/pyspec_tests\/builder_voluntary_exit__success$/,
],
skippedTests: [],
skippedRunners: [],
};

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
"@chainsafe/enr": "^6.0.1",
"@chainsafe/persistent-merkle-tree": "^1.2.1",
"@chainsafe/pubkey-index-map": "^3.0.0",
"@chainsafe/ssz": "^1.2.2",
"@chainsafe/ssz": "^1.4.0",
"@chainsafe/threads": "^1.11.3",
"@libp2p/crypto": "^5.1.13",
"@libp2p/interface": "^3.1.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/config/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
],
"dependencies": {
"@chainsafe/as-sha256": "^1.2.0",
"@chainsafe/ssz": "^1.2.2",
"@chainsafe/ssz": "^1.4.0",
"@lodestar/params": "workspace:^",
"@lodestar/spec-test-util": "workspace:^",
"@lodestar/types": "workspace:^",
Expand Down
2 changes: 1 addition & 1 deletion packages/db/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"check-readme": "pnpm exec ts-node ../../scripts/check_readme.ts"
},
"dependencies": {
"@chainsafe/ssz": "^1.2.2",
"@chainsafe/ssz": "^1.4.0",
"@lodestar/config": "workspace:^",
"@lodestar/utils": "workspace:^",
"classic-level": "^1.4.1"
Expand Down
2 changes: 1 addition & 1 deletion packages/fork-choice/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"check-readme": "pnpm exec ts-node ../../scripts/check_readme.ts"
},
"dependencies": {
"@chainsafe/ssz": "^1.2.2",
"@chainsafe/ssz": "^1.4.0",
"@lodestar/config": "workspace:^",
"@lodestar/params": "workspace:^",
"@lodestar/state-transition": "workspace:^",
Expand Down
2 changes: 1 addition & 1 deletion packages/light-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
"@chainsafe/bls": "8.2.0",
"@chainsafe/blst": "^2.2.0",
"@chainsafe/persistent-merkle-tree": "^1.2.1",
"@chainsafe/ssz": "^1.2.2",
"@chainsafe/ssz": "^1.4.0",
"@lodestar/api": "workspace:^",
"@lodestar/config": "workspace:^",
"@lodestar/params": "workspace:^",
Expand Down
2 changes: 1 addition & 1 deletion packages/logger/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
"winston-transport": "^4.5.0"
},
"devDependencies": {
"@chainsafe/ssz": "^1.2.2",
"@chainsafe/ssz": "^1.4.0",
"@chainsafe/threads": "^1.11.3",
"@lodestar/test-utils": "workspace:^",
"@types/triple-beam": "^1.3.2"
Expand Down
2 changes: 1 addition & 1 deletion packages/reqresp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
"uint8arraylist": "^2.4.7"
},
"devDependencies": {
"@chainsafe/ssz": "^1.2.2",
"@chainsafe/ssz": "^1.4.0",
"@libp2p/crypto": "^5.1.13",
"@libp2p/logger": "^6.2.2",
"@libp2p/peer-id": "^6.0.4",
Expand Down
2 changes: 1 addition & 1 deletion packages/spec-test-util/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
"vitest": "catalog:"
},
"peerDependencies": {
"@chainsafe/ssz": "^1.2.2",
"@chainsafe/ssz": "^1.4.0",
"@lodestar/types": "workspace:^",
"vitest": "catalog:"
}
Expand Down
2 changes: 1 addition & 1 deletion packages/state-transition/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
"@chainsafe/persistent-merkle-tree": "^1.2.1",
"@chainsafe/persistent-ts": "^1.0.0",
"@chainsafe/pubkey-index-map": "^3.0.0",
"@chainsafe/ssz": "^1.2.2",
"@chainsafe/ssz": "^1.4.0",
"@chainsafe/swap-or-not-shuffle": "^1.2.1",
"@lodestar/config": "workspace:^",
"@lodestar/params": "workspace:^",
Expand Down
62 changes: 32 additions & 30 deletions packages/state-transition/src/cache/epochCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@ import {
calculateShufflingDecisionRoot,
computeEpochShuffling,
} from "../util/epochShuffling.js";
import {getPtcWindowEpochCacheData} from "../util/gloas.js";
import {
computeActivationExitEpoch,
computeEpochAtSlot,
computePayloadTimelinessCommitteesForEpoch,
computeProposers,
computeSyncPeriodAtEpoch,
getActivationChurnLimit,
Expand All @@ -56,7 +56,7 @@ import {sumTargetUnslashedBalanceIncrements} from "../util/targetUnslashedBalanc
import {EffectiveBalanceIncrements, getEffectiveBalanceIncrementsWithLen} from "./effectiveBalanceIncrements.js";
import {EpochTransitionCache} from "./epochTransitionCache.js";
import {PubkeyCache, createPubkeyCache, syncPubkeys} from "./pubkeyCache.js";
import {CachedBeaconStateAllForks, CachedBeaconStateFulu} from "./stateCache.js";
import {CachedBeaconStateAllForks, CachedBeaconStateFulu, CachedBeaconStateGloas} from "./stateCache.js";
import {
SyncCommitteeCache,
SyncCommitteeCacheEmpty,
Expand Down Expand Up @@ -226,11 +226,12 @@ 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[];
// PTC for next epoch, precomputed from the ptc window for future duty serving
nextPayloadTimelinessCommittees: Uint32Array[];
Comment thread
ensi321 marked this conversation as resolved.
Copy link
Copy Markdown
Contributor

@ensi321 ensi321 Apr 19, 2026

Choose a reason for hiding this comment

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

I think this is a fragile approach because spec says we now store 2 + MIN_SEED_LOOKAHEAD epochs of ptc committees in the state. If MIN_SEED_LOOKAHEAD increases in the future, we would need to add more fields here.

But for now, it is fine.

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.

yeah was thinking about that too, it seems fine as is for now, at least in the state-transition we should handle it like we do in processProposerLookahead but I don't wanna deal with that now


// TODO: Helper stats
syncPeriod: SyncPeriod;
Expand Down Expand Up @@ -270,6 +271,7 @@ export class EpochCache {
nextSyncCommitteeIndexed: SyncCommitteeCache;
previousPayloadTimelinessCommittees: Uint32Array[];
payloadTimelinessCommittees: Uint32Array[];
nextPayloadTimelinessCommittees: Uint32Array[];
epoch: Epoch;
syncPeriod: SyncPeriod;
}) {
Expand Down Expand Up @@ -301,6 +303,7 @@ export class EpochCache {
this.nextSyncCommitteeIndexed = data.nextSyncCommitteeIndexed;
this.previousPayloadTimelinessCommittees = data.previousPayloadTimelinessCommittees;
this.payloadTimelinessCommittees = data.payloadTimelinessCommittees;
this.nextPayloadTimelinessCommittees = data.nextPayloadTimelinessCommittees;
this.epoch = data.epoch;
this.syncPeriod = data.syncPeriod;
}
Expand Down Expand Up @@ -451,25 +454,13 @@ export class EpochCache {
nextSyncCommitteeIndexed = new SyncCommitteeCacheEmpty();
}

// Compute PTC for all slots in the prev/current epoch
// Copy previous/current epoch PTC slices from state.ptcWindow once, then serve hot-path lookups from epochCtx.
let previousPayloadTimelinessCommittees: Uint32Array[] = [];
let payloadTimelinessCommittees: Uint32Array[] = [];
let nextPayloadTimelinessCommittees: Uint32Array[] = [];
if (currentEpoch >= config.GLOAS_FORK_EPOCH) {
payloadTimelinessCommittees = computePayloadTimelinessCommitteesForEpoch(
state,
currentEpoch,
currentShuffling.committees,
effectiveBalanceIncrements
);

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

// Precompute churnLimit for efficient initiateValidatorExit() during block proposing MUST be recompute everytime the
Expand Down Expand Up @@ -546,6 +537,7 @@ export class EpochCache {
nextSyncCommitteeIndexed,
previousPayloadTimelinessCommittees,
payloadTimelinessCommittees,
nextPayloadTimelinessCommittees,
epoch: currentEpoch,
syncPeriod: computeSyncPeriodAtEpoch(currentEpoch),
});
Expand Down Expand Up @@ -592,6 +584,7 @@ export class EpochCache {
nextSyncCommitteeIndexed: this.nextSyncCommitteeIndexed,
previousPayloadTimelinessCommittees: this.previousPayloadTimelinessCommittees,
payloadTimelinessCommittees: this.payloadTimelinessCommittees,
nextPayloadTimelinessCommittees: this.nextPayloadTimelinessCommittees,
epoch: this.epoch,
syncPeriod: this.syncPeriod,
});
Expand Down Expand Up @@ -695,21 +688,26 @@ export class EpochCache {
/**
* At fork boundary, this runs post-fork logic and it happens after `upgradeState*` is called.
*/
finalProcessEpoch(state: CachedBeaconStateAllForks): void {
finalProcessEpoch(state: CachedBeaconStateAllForks, epochTransitionCache: EpochTransitionCache): void {
// this.epoch was updated at the end of afterProcessEpoch()
const upcomingEpoch = this.epoch;
const epochAfterUpcoming = upcomingEpoch + 1;

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,
this.currentShuffling.committees,
this.effectiveBalanceIncrements
);
if (epochTransitionCache.nextEpochPayloadTimelinessCommittees) {
// shift arrays from transition cache
this.previousPayloadTimelinessCommittees = this.payloadTimelinessCommittees;
this.payloadTimelinessCommittees = this.nextPayloadTimelinessCommittees;
this.nextPayloadTimelinessCommittees = epochTransitionCache.nextEpochPayloadTimelinessCommittees;
} else {
// Fork boundary: processPtcWindow didn't run, read from freshly initialized state.ptcWindow
({
previousPayloadTimelinessCommittees: this.previousPayloadTimelinessCommittees,
payloadTimelinessCommittees: this.payloadTimelinessCommittees,
nextPayloadTimelinessCommittees: this.nextPayloadTimelinessCommittees,
} = getPtcWindowEpochCacheData(state as CachedBeaconStateGloas));
}
}
if (upcomingEpoch >= this.config.FULU_FORK_EPOCH) {
// Populate proposer cache with lookahead from state
Expand Down Expand Up @@ -1034,12 +1032,16 @@ export class EpochCache {
throw new Error("Payload Timeliness Committee is not available before gloas fork");
}

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

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 + 1) {
return this.nextPayloadTimelinessCommittees[slot % SLOTS_PER_EPOCH];
}

throw new Error(`Payload Timeliness Committee is not available for slot=${slot}`);
Expand Down
7 changes: 7 additions & 0 deletions packages/state-transition/src/cache/epochTransitionCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,12 @@ export interface EpochTransitionCache {
*/
nextShuffling: EpochShuffling | null;

/**
* Pre-computed PTC for epoch N + MIN_SEED_LOOKAHEAD + 1, populated by processPtcWindow (Gloas+).
* Used by finalProcessEpoch to shift PTC arrays in epoch cache without reading from state.
*/
nextEpochPayloadTimelinessCommittees: Uint32Array[] | null;

/**
* Altair specific, this is total active balances for the next epoch.
* This is only used in `afterProcessEpoch` to compute base reward and sync participant reward.
Expand Down Expand Up @@ -502,6 +508,7 @@ export function beforeProcessEpoch(
indicesToEject,
nextShufflingActiveIndices,
nextShuffling: null,
nextEpochPayloadTimelinessCommittees: null,
// to be updated in processEffectiveBalanceUpdates
nextEpochTotalActiveBalanceByIncrement: 0,
isActivePrevEpoch,
Expand Down
Loading
Loading