@@ -11,10 +11,14 @@ import (
11
11
)
12
12
13
13
const (
14
- smallAmount = lnwire .MilliSatoshi (400_000 )
15
- largeAmount = lnwire .MilliSatoshi (5_000_000 )
16
- capacity = lnwire .MilliSatoshi (10_000_000 )
17
- scale = lnwire .MilliSatoshi (400_000 )
14
+ smallAmount = lnwire .MilliSatoshi (400_000_000 )
15
+ largeAmount = lnwire .MilliSatoshi (5_000_000_000 )
16
+ capacity = lnwire .MilliSatoshi (10_000_000_000 )
17
+ scale = lnwire .MilliSatoshi (400_000_000 )
18
+
19
+ // defaultTolerance is the default absolute tolerance for comparing
20
+ // probability calculations to expected values.
21
+ defaultTolerance = 0.001
18
22
)
19
23
20
24
// TestSuccessProbability tests that we get correct probability estimates for
@@ -25,7 +29,6 @@ func TestSuccessProbability(t *testing.T) {
25
29
tests := []struct {
26
30
name string
27
31
expectedProbability float64
28
- tolerance float64
29
32
successAmount lnwire.MilliSatoshi
30
33
failAmount lnwire.MilliSatoshi
31
34
amount lnwire.MilliSatoshi
@@ -78,7 +81,6 @@ func TestSuccessProbability(t *testing.T) {
78
81
failAmount : capacity ,
79
82
amount : smallAmount ,
80
83
expectedProbability : 0.684 ,
81
- tolerance : 0.001 ,
82
84
},
83
85
// If we had an unsettled success, we are sure we can send a
84
86
// lower amount.
@@ -110,7 +112,6 @@ func TestSuccessProbability(t *testing.T) {
110
112
failAmount : capacity ,
111
113
amount : smallAmount ,
112
114
expectedProbability : 0.851 ,
113
- tolerance : 0.001 ,
114
115
},
115
116
// If we had a large unsettled success before, we know we can
116
117
// send even larger payments with high probability.
@@ -122,7 +123,6 @@ func TestSuccessProbability(t *testing.T) {
122
123
failAmount : capacity ,
123
124
amount : largeAmount ,
124
125
expectedProbability : 0.998 ,
125
- tolerance : 0.001 ,
126
126
},
127
127
// If we had a failure before, we can't send with the fail
128
128
// amount.
@@ -151,7 +151,6 @@ func TestSuccessProbability(t *testing.T) {
151
151
failAmount : largeAmount ,
152
152
amount : smallAmount ,
153
153
expectedProbability : 0.368 ,
154
- tolerance : 0.001 ,
155
154
},
156
155
// From here on we deal with mixed previous successes and
157
156
// failures.
@@ -183,7 +182,6 @@ func TestSuccessProbability(t *testing.T) {
183
182
successAmount : smallAmount ,
184
183
amount : smallAmount + largeAmount / 10 ,
185
184
expectedProbability : 0.287 ,
186
- tolerance : 0.001 ,
187
185
},
188
186
// We still can't send the fail amount.
189
187
{
@@ -194,22 +192,45 @@ func TestSuccessProbability(t *testing.T) {
194
192
amount : largeAmount ,
195
193
expectedProbability : 0.0 ,
196
194
},
197
- // Same success and failure amounts (illogical).
195
+ // Same success and failure amounts (illogical), which gets
196
+ // reset to no knowledge.
198
197
{
199
198
name : "previous f/s, same" ,
200
199
capacity : capacity ,
201
200
failAmount : largeAmount ,
202
201
successAmount : largeAmount ,
203
202
amount : largeAmount ,
204
- expectedProbability : 0.0 ,
203
+ expectedProbability : 0.5 ,
205
204
},
206
- // Higher success than failure amount (illogical).
205
+ // Higher success than failure amount (illogical), which gets
206
+ // reset to no knowledge.
207
207
{
208
- name : "previous f/s, higher success " ,
208
+ name : "previous f/s, illogical " ,
209
209
capacity : capacity ,
210
210
failAmount : smallAmount ,
211
211
successAmount : largeAmount ,
212
- expectedProbability : 0.0 ,
212
+ amount : largeAmount ,
213
+ expectedProbability : 0.5 ,
214
+ },
215
+ // Larger success and larger failure than the old capacity are
216
+ // rescaled to still give a very high success rate.
217
+ {
218
+ name : "smaller cap, large success/fail" ,
219
+ capacity : capacity ,
220
+ failAmount : 2 * capacity + 1 ,
221
+ successAmount : 2 * capacity ,
222
+ amount : largeAmount ,
223
+ expectedProbability : 1.0 ,
224
+ },
225
+ // A lower success amount is not rescaled.
226
+ {
227
+ name : "smaller cap, large fail" ,
228
+ capacity : capacity ,
229
+ successAmount : smallAmount / 2 ,
230
+ failAmount : 2 * capacity ,
231
+ amount : smallAmount ,
232
+ // See "previous success, larger amount".
233
+ expectedProbability : 0.851 ,
213
234
},
214
235
}
215
236
@@ -228,7 +249,7 @@ func TestSuccessProbability(t *testing.T) {
228
249
test .failAmount , test .amount ,
229
250
)
230
251
require .InDelta (t , test .expectedProbability , p ,
231
- test . tolerance )
252
+ defaultTolerance )
232
253
require .NoError (t , err )
233
254
})
234
255
}
@@ -244,6 +265,59 @@ func TestSuccessProbability(t *testing.T) {
244
265
})
245
266
}
246
267
268
+ // TestSmallScale tests that the probability formula works with small scale
269
+ // values.
270
+ func TestSmallScale (t * testing.T ) {
271
+ var (
272
+ // We use the smallest possible scale value together with a
273
+ // large capacity. This is an extreme form of a bimodal
274
+ // distribution.
275
+ scale lnwire.MilliSatoshi = 1
276
+ capacity lnwire.MilliSatoshi = 7e+09
277
+
278
+ // Success and failure amounts are chosen such that the expected
279
+ // balance must be somewhere in the middle of the channel, a
280
+ // value not expected when dealing with a bimodal distribution.
281
+ // In this case, the bimodal model fails to give good forecasts
282
+ // due to the numerics of the exponential functions, which get
283
+ // evaluated to exact zero floats.
284
+ successAmount lnwire.MilliSatoshi = 1.0e+09
285
+ failAmount lnwire.MilliSatoshi = 4.0e+09
286
+ )
287
+
288
+ estimator := BimodalEstimator {
289
+ BimodalConfig : BimodalConfig {BimodalScaleMsat : scale },
290
+ }
291
+
292
+ // An amount that's close to the success amount should have a very high
293
+ // probability.
294
+ amtCloseSuccess := successAmount + 1
295
+ p , err := estimator .probabilityFormula (
296
+ capacity , successAmount , failAmount , amtCloseSuccess ,
297
+ )
298
+ require .NoError (t , err )
299
+ require .InDelta (t , 1.0 , p , defaultTolerance )
300
+
301
+ // An amount that's close to the fail amount should have a very low
302
+ // probability.
303
+ amtCloseFail := failAmount - 1
304
+ p , err = estimator .probabilityFormula (
305
+ capacity , successAmount , failAmount , amtCloseFail ,
306
+ )
307
+ require .NoError (t , err )
308
+ require .InDelta (t , 0.0 , p , defaultTolerance )
309
+
310
+ // In the region where the bimodal model doesn't give good forecasts, we
311
+ // fall back to a uniform model, which interpolates probabilities
312
+ // linearly.
313
+ amtLinear := successAmount + (failAmount - successAmount )* 1 / 4
314
+ p , err = estimator .probabilityFormula (
315
+ capacity , successAmount , failAmount , amtLinear ,
316
+ )
317
+ require .NoError (t , err )
318
+ require .InDelta (t , 0.75 , p , defaultTolerance )
319
+ }
320
+
247
321
// TestIntegral tests certain limits of the probability distribution integral.
248
322
func TestIntegral (t * testing.T ) {
249
323
t .Parallel ()
@@ -689,9 +763,24 @@ func TestLocalPairProbability(t *testing.T) {
689
763
// FuzzProbability checks that we don't encounter errors related to NaNs.
690
764
func FuzzProbability (f * testing.F ) {
691
765
estimator := BimodalEstimator {
692
- BimodalConfig : BimodalConfig {BimodalScaleMsat : scale },
766
+ BimodalConfig : BimodalConfig {BimodalScaleMsat : 400_000 },
693
767
}
694
768
769
+ // Predefined seed reported in
770
+ // https://github.com/lightningnetwork/lnd/issues/9085. This test found
771
+ // a case where we could not compute a normalization factor because we
772
+ // learned that the balance lies somewhere in the middle of the channel,
773
+ // a surprising result for the bimodal model, which predicts two
774
+ // distinct modes at the edges and therefore has numerical issues in the
775
+ // middle. Additionally, the scale is small with respect to the values
776
+ // used here.
777
+ f .Add (
778
+ uint64 (1_000_000_000 ),
779
+ uint64 (300_000_000 ),
780
+ uint64 (400_000_000 ),
781
+ uint64 (300_000_000 ),
782
+ )
783
+
695
784
f .Fuzz (func (t * testing.T , capacity , successAmt , failAmt , amt uint64 ) {
696
785
if capacity == 0 {
697
786
return
0 commit comments