Summary
Collector.evict() computes its eviction threshold with an unguarded unsigned subtraction:
https://github.com/ssvlabs/ssv/blob/main/operator/dutytracer/collector.go#L139-L147
const slotTTL = 4
func (c *Collector) evict(currentSlot phase0.Slot) {
start := time.Now()
threshold := currentSlot - slotTTL // <-- underflows when currentSlot < slotTTL
...
c.lastEvictedSlot.Store(uint64(threshold))
...
}
phase0.Slot is an unsigned integer. When currentSlot < slotTTL (i.e. the first few slots after genesis), currentSlot - slotTTL wraps around to a near-MaxUint64 value. That wrapped value is then Stored into lastEvictedSlot and passed as the threshold to dumpCommitteeToDBPeriodically / dumpValidatorToDBPeriodically / dumpLinkToDBPeriodically.
Impact
Low in practice — this only triggers in the first ~4 slots of a network's life, which no production exporter will realistically hit. Flagging it for correctness/consistency rather than as an active bug.
Context
This came out of reviewing #2895. That PR adds an analogous retention path (retentionState) that does carefully guard the same shape with a currentSlot <= retainSlots check before subtracting. evict() is the pre-existing sibling one function over that lacks the equivalent guard. Out of scope for #2895, so tracking separately here.
Suggested fix
Add the same underflow guard evict()'s sibling uses — early-return (or clamp the threshold to 0) when currentSlot <= slotTTL — so the two paths are consistent.
Summary
Collector.evict()computes its eviction threshold with an unguarded unsigned subtraction:https://github.com/ssvlabs/ssv/blob/main/operator/dutytracer/collector.go#L139-L147
phase0.Slotis an unsigned integer. WhencurrentSlot < slotTTL(i.e. the first few slots after genesis),currentSlot - slotTTLwraps around to a near-MaxUint64value. That wrapped value is thenStored intolastEvictedSlotand passed as the threshold todumpCommitteeToDBPeriodically/dumpValidatorToDBPeriodically/dumpLinkToDBPeriodically.Impact
Low in practice — this only triggers in the first ~4 slots of a network's life, which no production exporter will realistically hit. Flagging it for correctness/consistency rather than as an active bug.
Context
This came out of reviewing #2895. That PR adds an analogous retention path (
retentionState) that does carefully guard the same shape with acurrentSlot <= retainSlotscheck before subtracting.evict()is the pre-existing sibling one function over that lacks the equivalent guard. Out of scope for #2895, so tracking separately here.Suggested fix
Add the same underflow guard
evict()'s sibling uses — early-return (or clamp the threshold to 0) whencurrentSlot <= slotTTL— so the two paths are consistent.