fix: address twoeths' review on cached PTCs#12
fix: address twoeths' review on cached PTCs#12lodekeeper wants to merge 10 commits intonflaig/cached-ptcsfrom
Conversation
- 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]); |
There was a problem hiding this comment.
can we change this to use intersectValues again? see ChainSafe#9047 (comment)
There was a problem hiding this comment.
Done — switched back to intersectValues, matches the pattern used everywhere else in the codebase.
| export function getPtcCommitteeIndex(ptc: Uint32Array, validatorIndex: ValidatorIndex): number { | ||
| return ptc.indexOf(validatorIndex); |
There was a problem hiding this comment.
this fucntion can be remove, no reason to wrap ptc.indexOf
There was a problem hiding this comment.
Removed — callers now use ptc.indexOf(validatorIndex) directly.
| payloadAttestationMessage: gloas.PayloadAttestationMessage | ||
| ): Promise<PayloadAttestationValidationResult> { | ||
| return validatePayloadAttestationMessage(chain, payloadAttestationMessage); | ||
| return validatePayloadAttestationMessage(chain, payloadAttestationMessage, true); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Fixed — API path uses const prioritizeBls = true, gossip path omits the argument (defaults to false).
| const ptc = new Uint32Array(state.previousPtc.length); | ||
| for (let i = 0; i < ptc.length; i++) { | ||
| ptc[i] = state.previousPtc.get(i); | ||
| } | ||
| return ptc; |
There was a problem hiding this comment.
isn't it better to use getAll() here, please see which approach is more performance cpu/memory wise
There was a problem hiding this comment.
Switched to Uint32Array.from(state.previousPtc.getAll()) — single allocation, no manual loop.
There was a problem hiding this comment.
please ignore specrefs for now, we need a new spec release for this first, revert changes to this file
There was a problem hiding this comment.
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
| export function getPayloadTimelinessCommittee(state: CachedBeaconStateGloas, slot: Slot): Uint32Array { | ||
| return state.epochCtx.getPayloadTimelinessCommittee(slot); |
There was a problem hiding this comment.
no need for getPayloadTimelinessCommittee now as it's just a simple wrapper function
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
this.previousEpochLastSlotPtc should never be undefined, we need to initialize it from state when we create the cache
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
| previousEpochLastSlotPtc: Uint32Array | null; | |
| previousEpochLastSlotPtc: Uint32Array; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
as CachedBeaconStateGloas can be removed
There was a problem hiding this comment.
Removed — epochCtx is available on all cached states, no Gloas-specific cast needed.
| previousEpochLastSlotPtc: | ||
| currentEpoch >= config.GLOAS_FORK_EPOCH | ||
| ? Uint32Array.from((state as CachedBeaconStateGloas).previousPtc.getAll()) | ||
| : new Uint32Array(0), |
There was a problem hiding this comment.
ah maybe allowing it to be nullable was better
There was a problem hiding this comment.
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.
|
@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 |
|
Closing as requested — the target branch (nflaig/cached-ptcs) has been closed on the spec side since a different approach was merged. Thanks! |
Address twoeths' review feedback on ChainSafe#9047.
Changes
1. Replace
getHeadStateAtSlotwithgetHeadStateAtEpoch(comments 1, 3, 4)getHeadStateAtEpochreuses checkpoint state cache (cheaper than per-slot regen)getHeadStateAtSlotentirely to prevent DOS via arbitrary slot lookups2. Use
epochCtxfor PTC reads (comment 2)state.epochCtx.getPayloadTimelinessCommittee(data.slot)returnsUint32Arraydirectly (no state tree traversal)getPayloadTimelinessCommittee()uses epochCtx for current epoch, falls back to state treepreviousPtcfor epoch boundary case (previous epoch's last slot)PtcCommitteeViewabstraction, usesUint32Arraythroughout3. BLS verification priority (comment 5)
{batchable: true, priority: true}(higher priority, like attestations){batchable: true, priority: false}4. Documentation (comment 6)
rotatePayloadTimelinessCommitteescalls explaining thatpayloadTimelinessCommitteesare computed infinalProcessEpochVerification
pnpm build✅pnpm lint✅pnpm check-types✅ (all 19 packages)pnpm vitest run upgradeState.test.ts✅