Skip to content

fix: address twoeths' review on cached PTCs#12

Closed
lodekeeper wants to merge 10 commits intonflaig/cached-ptcsfrom
fix/cached-ptc-review-feedback
Closed

fix: address twoeths' review on cached PTCs#12
lodekeeper wants to merge 10 commits intonflaig/cached-ptcsfrom
fix/cached-ptc-review-feedback

Conversation

@lodekeeper
Copy link
Copy Markdown
Owner

Address twoeths' review feedback on ChainSafe#9047.

Changes

1. Replace getHeadStateAtSlot with getHeadStateAtEpoch (comments 1, 3, 4)

  • Gossip only validates current-slot attestations, so epoch-level state is sufficient
  • getHeadStateAtEpoch reuses checkpoint state cache (cheaper than per-slot regen)
  • Removes getHeadStateAtSlot entirely to prevent DOS via arbitrary slot lookups

2. Use epochCtx for PTC reads (comment 2)

  • Gossip validation: state.epochCtx.getPayloadTimelinessCommittee(data.slot) returns Uint32Array directly (no state tree traversal)
  • Block processing: getPayloadTimelinessCommittee() uses epochCtx for current epoch, falls back to state tree previousPtc for epoch boundary case (previous epoch's last slot)
  • Removes PtcCommitteeView abstraction, uses Uint32Array throughout

3. BLS verification priority (comment 5)

  • API path: {batchable: true, priority: true} (higher priority, like attestations)
  • Gossip path: {batchable: true, priority: false}

4. Documentation (comment 6)

  • Added comments on rotatePayloadTimelinessCommittees calls explaining that payloadTimelinessCommittees are computed in finalProcessEpoch

Verification

  • pnpm build
  • pnpm lint
  • pnpm check-types ✅ (all 19 packages)
  • pnpm vitest run upgradeState.test.ts

lodekeeper and others added 2 commits March 18, 2026 17:01
- Use getHeadStateAtEpoch instead of getHeadStateAtSlot for gossip
  validation (cheaper, avoids per-slot state regen)
- Remove getHeadStateAtSlot to prevent DOS via arbitrary slot lookups
- Use epochCtx.getPayloadTimelinessCommittee() for PTC reads in gossip
  and current-epoch block processing (avoids state tree traversal)
- Keep state tree fallback for previous-epoch PTC at epoch boundaries
- Add batchable + priority BLS verification (API gets priority)
- Add documentation comments on PTC rotation in state transition
- Remove PtcCommitteeView type, use Uint32Array directly

Co-authored-by: Nico Flaig <nflaig@users.noreply.github.com>
- Remove compute_ptc#gloas exception (function was renamed to get_ptc in spec)
- Remove compute_ptc#gloas entry from functions.yml (no longer in gloas spec)
- Update get_ptc#gloas spec hash and body to match v1.7.0-alpha.2 pyspec
- Add get_ptc_assignment#gloas entry from new spec function
for (let i = 0; i < ptc.length; i++) {
if (payloadAttestation.aggregationBits.get(i)) {
attestingIndices.push(ptc.get(i));
attestingIndices.push(ptc[i]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

can we change this to use intersectValues again? see ChainSafe#9047 (comment)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Done — switched back to intersectValues, matches the pattern used everywhere else in the codebase.

Comment on lines +218 to +219
export function getPtcCommitteeIndex(ptc: Uint32Array, validatorIndex: ValidatorIndex): number {
return ptc.indexOf(validatorIndex);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

this fucntion can be remove, no reason to wrap ptc.indexOf

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Removed — callers now use ptc.indexOf(validatorIndex) directly.

payloadAttestationMessage: gloas.PayloadAttestationMessage
): Promise<PayloadAttestationValidationResult> {
return validatePayloadAttestationMessage(chain, payloadAttestationMessage);
return validatePayloadAttestationMessage(chain, payloadAttestationMessage, true);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

can you pass this as opts or assign a const before function call for readablity, having a magic boolean valeu like this that just says true/false is not that readable

see how we do it in other places like htis

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed — API path uses const prioritizeBls = true, gossip path omits the argument (defaults to false).

Comment on lines +208 to +212
const ptc = new Uint32Array(state.previousPtc.length);
for (let i = 0; i < ptc.length; i++) {
ptc[i] = state.previousPtc.get(i);
}
return ptc;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

isn't it better to use getAll() here, please see which approach is more performance cpu/memory wise

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Switched to Uint32Array.from(state.previousPtc.getAll()) — single allocation, no manual loop.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

please ignore specrefs for now, we need a new spec release for this first, revert changes to this file

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Reverted — specrefs restored to base branch state.

- Use named const for prioritizeBls (readability)
- Use getAll() for previousPtc fallback instead of manual loop
- Remove getPtcCommitteeIndex wrapper (just use ptc.indexOf directly)
- Use intersectValues for getIndexedPayloadAttestation (matches existing pattern)
- Revert specrefs changes (needs new spec release first)
Add previousEpochLastSlotPtc to epochCache to eliminate mixed
access pattern (epochCtx + state tree). At epoch transition, the
last slot's PTC is saved before overwriting. This is the only
previous-epoch PTC ever needed (processPayloadAttestation enforces
data.slot + 1 === state.slot).

Now all PTC reads go through epochCtx.getPayloadTimelinessCommittee()
- current epoch: payloadTimelinessCommittees[slot % SLOTS_PER_EPOCH]
- previous epoch last slot: previousEpochLastSlotPtc
- no state tree traversal needed for reads
Comment on lines +198 to +199
export function getPayloadTimelinessCommittee(state: CachedBeaconStateGloas, slot: Slot): Uint32Array {
return state.epochCtx.getPayloadTimelinessCommittee(slot);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

no need for getPayloadTimelinessCommittee now as it's just a simple wrapper function

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Removed — callers now use state.epochCtx.getPayloadTimelinessCommittee(slot) directly.


return this.payloadTimelinessCommittees[slot % SLOTS_PER_EPOCH];
// Previous epoch: only the last slot is cached (for epoch-boundary payload attestation processing)
if (epoch === this.epoch - 1 && slot % SLOTS_PER_EPOCH === SLOTS_PER_EPOCH - 1 && this.previousEpochLastSlotPtc) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

this.previousEpochLastSlotPtc should never be undefined, we need to initialize it from state when we create the cache

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed — previousEpochLastSlotPtc is now initialized from state.previousPtc.getAll() in createFromState, so it's always available after checkpoint sync / restarts.

// PTC for current epoch, computed eagerly at epoch transition
payloadTimelinessCommittees: Uint32Array[];
// PTC for last slot of previous epoch, needed at epoch boundary for payload attestation processing
previousEpochLastSlotPtc: Uint32Array | null;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
previousEpochLastSlotPtc: Uint32Array | null;
previousEpochLastSlotPtc: Uint32Array;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Done — changed to Uint32Array (non-nullable). Pre-gloas epochs get an empty new Uint32Array(0).

…t_ptc_assignment

compute_ptc was renamed to get_ptc in pyspec v1.7.0-alpha.2.
The stale compute_ptc#gloas entry caused check-specrefs to fail.

Also updates get_ptc spec body (slot parameter added) and adds
get_ptc_assignment#gloas (sources: [] — not yet implemented in TS).
- Remove getPayloadTimelinessCommittee wrapper (callers use epochCtx directly)
- Initialize previousEpochLastSlotPtc from state.previousPtc in createFromState
- Change type to non-nullable Uint32Array (always initialized)
const state = (await chain.getHeadStateAtEpoch(
epoch,
RegenCaller.validateGossipPayloadAttestation
)) as CachedBeaconStateGloas;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

as CachedBeaconStateGloas can be removed

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Removed — epochCtx is available on all cached states, no Gloas-specific cast needed.

Comment on lines +540 to +543
previousEpochLastSlotPtc:
currentEpoch >= config.GLOAS_FORK_EPOCH
? Uint32Array.from((state as CachedBeaconStateGloas).previousPtc.getAll())
: new Uint32Array(0),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ah maybe allowing it to be nullable was better

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Reverted to Uint32Array | null, initialized as null in createFromState. afterProcessEpoch populates it at epoch transition before it's needed.

…tPtc

- Remove 'as CachedBeaconStateGloas' cast (epochCtx available on all forks)
- Revert previousEpochLastSlotPtc to nullable (Uint32Array | null)
- Remove state-init in createFromState (afterProcessEpoch handles it)
compute_ptc was renamed to get_ptc in pyspec v1.7.0-alpha.2.
The stale compute_ptc#gloas exception entry caused ethspecify 0.3.9
to error on load (invalid key validation).

Changes:
- Remove stale compute_ptc#gloas from exceptions and functions.yml
- Align get_ptc#gloas spec body + hash with unstable base (ae15f761)
- Align get_ptc#gloas sources to epochCache.ts (matches unstable)
- Add get_ptc_assignment#gloas to exceptions (matches unstable)

All spec refs now pass ethspecify process + check locally.
@nflaig
Copy link
Copy Markdown

nflaig commented Mar 27, 2026

@lodekeeper feel free to close this, I have closed the PR from the target branch since a different approach was merged on the spec side

@lodekeeper
Copy link
Copy Markdown
Owner Author

Closing as requested — the target branch (nflaig/cached-ptcs) has been closed on the spec side since a different approach was merged. Thanks!

@lodekeeper lodekeeper closed this Mar 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants