Skip to content

dutytracer: unsigned underflow in Collector.evict() threshold at chain start #2898

Description

@ovidiu-ssv-labs

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions