From afeef413d73a2b28dd8bd74b5f234f8f02fe4d5d Mon Sep 17 00:00:00 2001 From: Prem Chaitanya Prathi Date: Thu, 19 Feb 2026 12:10:16 +0530 Subject: [PATCH] truncate exponential delay to practical limits based on probablity --- libp2p/protocols/mix/delay_strategy.nim | 19 +++++++++++++++---- tests/libp2p/mix/test_delay_strategy.nim | 23 ++++++++++++++++++++++- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/libp2p/protocols/mix/delay_strategy.nim b/libp2p/protocols/mix/delay_strategy.nim index ea9a9afd81..0727f7ad29 100644 --- a/libp2p/protocols/mix/delay_strategy.nim +++ b/libp2p/protocols/mix/delay_strategy.nim @@ -38,18 +38,28 @@ method generateForIntermediate*( encodedDelayMs const DefaultMeanDelayMs* = 100 +const DefaultNegligibleProb* = 1e-6 + ## Probability below which the tail of the exponential distribution is truncated. + ## Yields a maximum delay of mean * -ln(negligibleProb) ≈ mean * 13.8. type ExponentialDelayStrategy* = ref object of DelayStrategy ## Recommended strategy: encodes mean delay, samples from exponential distribution. + ## The distribution is truncated at -mean*ln(negligibleProb), discarding the + ## impractically long tail while preserving the mixing properties. meanDelayMs: uint16 + negligibleProb: float64 proc new*( T: typedesc[ExponentialDelayStrategy], meanDelayMs: uint16 = DefaultMeanDelayMs, rng: ref HmacDrbgContext, + negligibleProb: float64 = DefaultNegligibleProb, ): T = doAssert(rng != nil, "random is not set") - T(meanDelayMs: meanDelayMs, rng: rng) + doAssert( + negligibleProb > 0.0 and negligibleProb < 1.0, "negligibleProb must be in (0, 1)" + ) + T(meanDelayMs: meanDelayMs, rng: rng, negligibleProb: negligibleProb) method generateForEntry*( self: ExponentialDelayStrategy @@ -59,11 +69,12 @@ method generateForEntry*( method generateForIntermediate*( self: ExponentialDelayStrategy, meanDelayMs: uint16 ): uint16 {.gcsafe, raises: [].} = - ## Samples from exponential distribution: delay = -mean * ln(U) - ## Fall back to no delay in case of errors + ## Samples from exponential distribution: delay = -mean * ln(U), truncated to + ## -mean*ln(negligibleProb) to discard the impractically long tail. if meanDelayMs == 0: return 0u16 + let maxDelayMs = -float64(meanDelayMs) * ln(self.negligibleProb) let randVal = self.rng[].generate(uint64) let u = (float64(randVal) + 1.0) / (float64(high(uint64)) + 1.0) let delay = -float64(meanDelayMs) * ln(u) - min(delay, float64(high(uint16))).uint16 + min(min(delay, maxDelayMs), float64(high(uint16))).uint16 diff --git a/tests/libp2p/mix/test_delay_strategy.nim b/tests/libp2p/mix/test_delay_strategy.nim index 9ad8e1d781..a95b818656 100644 --- a/tests/libp2p/mix/test_delay_strategy.nim +++ b/tests/libp2p/mix/test_delay_strategy.nim @@ -3,7 +3,7 @@ {.used.} -import std/[sets] +import std/[math, sets] import ../../../libp2p/protocols/mix/delay_strategy import ../../tools/[unittest, crypto] @@ -66,3 +66,24 @@ suite "DelayStrategy": delays.incl(delay) check delays.len > NumSamples div 2 + + test "ExponentialDelayStrategy truncates at negligible probability threshold": + let + meanDelayMs: uint16 = 100 + negligibleProb = DefaultNegligibleProb + strategy = ExponentialDelayStrategy.new(meanDelayMs, rng(), negligibleProb) + # maxDelay = -mean * ln(negligibleProb) + maxDelayMs = uint16(-float64(meanDelayMs) * ln(negligibleProb)) + + for _ in 0 ..< 10000: + check strategy.generateForIntermediate(meanDelayMs) <= maxDelayMs + + test "ExponentialDelayStrategy respects custom negligibleProb": + let + meanDelayMs: uint16 = 100 + negligibleProb = 0.01 # aggressive truncation: max ≈ mean * 4.6 + strategy = ExponentialDelayStrategy.new(meanDelayMs, rng(), negligibleProb) + maxDelayMs = uint16(-float64(meanDelayMs) * ln(negligibleProb)) + + for _ in 0 ..< 10000: + check strategy.generateForIntermediate(meanDelayMs) <= maxDelayMs