@@ -540,161 +540,6 @@ pub struct ProbabilisticScoringParameters {
540540 	pub  considered_impossible_penalty_msat :  u64 , 
541541} 
542542
543- /// Tracks the historical state of a distribution as a weighted average of how much time was spent 
544- /// in each of 8 buckets. 
545- #[ derive( Clone ,  Copy ) ]  
546- struct  HistoricalBucketRangeTracker  { 
547- 	buckets :  [ u16 ;  8 ] , 
548- } 
549- 
550- impl  HistoricalBucketRangeTracker  { 
551- 	fn  new ( )  -> Self  {  Self  {  buckets :  [ 0 ;  8 ]  }  } 
552- 	fn  track_datapoint ( & mut  self ,  liquidity_offset_msat :  u64 ,  capacity_msat :  u64 )  { 
553- 		// We have 8 leaky buckets for min and max liquidity. Each bucket tracks the amount of time 
554- 		// we spend in each bucket as a 16-bit fixed-point number with a 5 bit fractional part. 
555- 		// 
556- 		// Each time we update our liquidity estimate, we add 32 (1.0 in our fixed-point system) to 
557- 		// the buckets for the current min and max liquidity offset positions. 
558- 		// 
559- 		// We then decay each bucket by multiplying by 2047/2048 (avoiding dividing by a 
560- 		// non-power-of-two). This ensures we can't actually overflow the u16 - when we get to 
561- 		// 63,457 adding 32 and decaying by 2047/2048 leaves us back at 63,457. 
562- 		// 
563- 		// In total, this allows us to track data for the last 8,000 or so payments across a given 
564- 		// channel. 
565- 		// 
566- 		// These constants are a balance - we try to fit in 2 bytes per bucket to reduce overhead, 
567- 		// and need to balance having more bits in the decimal part (to ensure decay isn't too 
568- 		// non-linear) with having too few bits in the mantissa, causing us to not store very many 
569- 		// datapoints. 
570- 		// 
571- 		// The constants were picked experimentally, selecting a decay amount that restricts us 
572- 		// from overflowing buckets without having to cap them manually. 
573- 
574- 		// Ensure the bucket index is in the range [0, 7], even if the liquidity offset is zero or 
575- 		// the channel's capacity, though the second should generally never happen. 
576- 		debug_assert ! ( liquidity_offset_msat <= capacity_msat) ; 
577- 		let  bucket_idx:  u8  = ( liquidity_offset_msat *  8  / capacity_msat. saturating_add ( 1 ) ) 
578- 			. try_into ( ) . unwrap_or ( 32 ) ;  // 32 is bogus for 8 buckets, and will be ignored 
579- 		debug_assert ! ( bucket_idx < 8 ) ; 
580- 		if  bucket_idx < 8  { 
581- 			for  e in  self . buckets . iter_mut ( )  { 
582- 				* e = ( ( * e as  u32 )  *  2047  / 2048 )  as  u16 ; 
583- 			} 
584- 			self . buckets [ bucket_idx as  usize ]  = self . buckets [ bucket_idx as  usize ] . saturating_add ( 32 ) ; 
585- 		} 
586- 	} 
587- 	/// Decay all buckets by the given number of half-lives. Used to more aggressively remove old 
588-  	/// datapoints as we receive newer information. 
589-  	fn  time_decay_data ( & mut  self ,  half_lives :  u32 )  { 
590- 		for  e in  self . buckets . iter_mut ( )  { 
591- 			* e = e. checked_shr ( half_lives) . unwrap_or ( 0 ) ; 
592- 		} 
593- 	} 
594- } 
595- 
596- impl_writeable_tlv_based ! ( HistoricalBucketRangeTracker ,  {  ( 0 ,  buckets,  required)  } ) ; 
597- 
598- struct  HistoricalMinMaxBuckets < ' a >  { 
599- 	min_liquidity_offset_history :  & ' a  HistoricalBucketRangeTracker , 
600- 	max_liquidity_offset_history :  & ' a  HistoricalBucketRangeTracker , 
601- } 
602- 
603- impl  HistoricalMinMaxBuckets < ' _ >  { 
604- 	#[ inline]  
605- 	fn  get_decayed_buckets < T :  Time > ( & self ,  now :  T ,  last_updated :  T ,  half_life :  Duration ) 
606- 	-> ( [ u16 ;  8 ] ,  [ u16 ;  8 ] ,  u32 )  { 
607- 		let  required_decays = now. duration_since ( last_updated) . as_secs ( ) 
608- 			. checked_div ( half_life. as_secs ( ) ) 
609- 			. map_or ( u32:: max_value ( ) ,  |decays| cmp:: min ( decays,  u32:: max_value ( )  as  u64 )  as  u32 ) ; 
610- 		let  mut  min_buckets = * self . min_liquidity_offset_history ; 
611- 		min_buckets. time_decay_data ( required_decays) ; 
612- 		let  mut  max_buckets = * self . max_liquidity_offset_history ; 
613- 		max_buckets. time_decay_data ( required_decays) ; 
614- 		( min_buckets. buckets ,  max_buckets. buckets ,  required_decays) 
615- 	} 
616- 
617- 	#[ inline]  
618- 	fn  calculate_success_probability_times_billion < T :  Time > ( 
619- 		& self ,  now :  T ,  last_updated :  T ,  half_life :  Duration ,  amount_msat :  u64 ,  capacity_msat :  u64 ) 
620- 	-> Option < u64 >  { 
621- 		// If historical penalties are enabled, calculate the penalty by walking the set of 
622- 		// historical liquidity bucket (min, max) combinations (where min_idx < max_idx) and, for 
623- 		// each, calculate the probability of success given our payment amount, then total the 
624- 		// weighted average probability of success. 
625- 		// 
626- 		// We use a sliding scale to decide which point within a given bucket will be compared to 
627- 		// the amount being sent - for lower-bounds, the amount being sent is compared to the lower 
628- 		// edge of the first bucket (i.e. zero), but compared to the upper 7/8ths of the last 
629- 		// bucket (i.e. 9 times the index, or 63), with each bucket in between increasing the 
630- 		// comparison point by 1/64th. For upper-bounds, the same applies, however with an offset 
631- 		// of 1/64th (i.e. starting at one and ending at 64). This avoids failing to assign 
632- 		// penalties to channels at the edges. 
633- 		// 
634- 		// If we used the bottom edge of buckets, we'd end up never assigning any penalty at all to 
635- 		// such a channel when sending less than ~0.19% of the channel's capacity (e.g. ~200k sats 
636- 		// for a 1 BTC channel!). 
637- 		// 
638- 		// If we used the middle of each bucket we'd never assign any penalty at all when sending 
639- 		// less than 1/16th of a channel's capacity, or 1/8th if we used the top of the bucket. 
640- 		let  mut  total_valid_points_tracked = 0 ; 
641- 
642- 		let  payment_amt_64th_bucket:  u8  = if  amount_msat < u64:: max_value ( )  / 64  { 
643- 			( amount_msat *  64  / capacity_msat. saturating_add ( 1 ) ) 
644- 				. try_into ( ) . unwrap_or ( 65 ) 
645- 		}  else  { 
646- 			// Only use 128-bit arithmetic when multiplication will overflow to avoid 128-bit 
647- 			// division. This branch should only be hit in fuzz testing since the amount would 
648- 			// need to be over 2.88 million BTC in practice. 
649- 			( ( amount_msat as  u128 )  *  64  / ( capacity_msat as  u128 ) . saturating_add ( 1 ) ) 
650- 				. try_into ( ) . unwrap_or ( 65 ) 
651- 		} ; 
652- 		#[ cfg( not( fuzzing) ) ]  
653- 		debug_assert ! ( payment_amt_64th_bucket <= 64 ) ; 
654- 		if  payment_amt_64th_bucket >= 64  {  return  None ;  } 
655- 
656- 		// Check if all our buckets are zero, once decayed and treat it as if we had no data. We 
657- 		// don't actually use the decayed buckets, though, as that would lose precision. 
658- 		let  ( decayed_min_buckets,  decayed_max_buckets,  required_decays)  =
659- 			self . get_decayed_buckets ( now,  last_updated,  half_life) ; 
660- 		if  decayed_min_buckets. iter ( ) . all ( |v| * v == 0 )  || decayed_max_buckets. iter ( ) . all ( |v| * v == 0 )  { 
661- 			return  None ; 
662- 		} 
663- 
664- 		for  ( min_idx,  min_bucket)  in  self . min_liquidity_offset_history . buckets . iter ( ) . enumerate ( )  { 
665- 			for  max_bucket in  self . max_liquidity_offset_history . buckets . iter ( ) . take ( 8  - min_idx)  { 
666- 				total_valid_points_tracked += ( * min_bucket as  u64 )  *  ( * max_bucket as  u64 ) ; 
667- 			} 
668- 		} 
669- 		// If the total valid points is smaller than 1.0 (i.e. 32 in our fixed-point scheme), treat 
670- 		// it as if we were fully decayed. 
671- 		if  total_valid_points_tracked. checked_shr ( required_decays) . unwrap_or ( 0 )  < 32 * 32  { 
672- 			return  None ; 
673- 		} 
674- 
675- 		let  mut  cumulative_success_prob_times_billion = 0 ; 
676- 		for  ( min_idx,  min_bucket)  in  self . min_liquidity_offset_history . buckets . iter ( ) . enumerate ( )  { 
677- 			for  ( max_idx,  max_bucket)  in  self . max_liquidity_offset_history . buckets . iter ( ) . enumerate ( ) . take ( 8  - min_idx)  { 
678- 				let  bucket_prob_times_million = ( * min_bucket as  u64 )  *  ( * max_bucket as  u64 ) 
679- 					*  1024  *  1024  / total_valid_points_tracked; 
680- 				let  min_64th_bucket = min_idx as  u8  *  9 ; 
681- 				let  max_64th_bucket = ( 7  - max_idx as  u8 )  *  9  + 1 ; 
682- 				if  payment_amt_64th_bucket > max_64th_bucket { 
683- 					// Success probability 0, the payment amount is above the max liquidity 
684- 				}  else  if  payment_amt_64th_bucket <= min_64th_bucket { 
685- 					cumulative_success_prob_times_billion += bucket_prob_times_million *  1024 ; 
686- 				}  else  { 
687- 					cumulative_success_prob_times_billion += bucket_prob_times_million * 
688- 						( ( max_64th_bucket - payment_amt_64th_bucket)  as  u64 )  *  1024  /
689- 						( ( max_64th_bucket - min_64th_bucket)  as  u64 ) ; 
690- 				} 
691- 			} 
692- 		} 
693- 
694- 		Some ( cumulative_success_prob_times_billion) 
695- 	} 
696- } 
697- 
698543/// Accounting for channel liquidity balance uncertainty. 
699544/// 
700545/// Direction is defined in terms of [`NodeId`] partial ordering, where the source node is the 
@@ -1666,6 +1511,166 @@ mod approx {
16661511	} 
16671512} 
16681513
1514+ mod  bucketed_history { 
1515+ 	use  super :: * ; 
1516+ 
1517+ 	/// Tracks the historical state of a distribution as a weighted average of how much time was spent 
1518+  	/// in each of 8 buckets. 
1519+  	#[ derive( Clone ,  Copy ) ]  
1520+ 	pub ( super )  struct  HistoricalBucketRangeTracker  { 
1521+ 		buckets :  [ u16 ;  8 ] , 
1522+ 	} 
1523+ 
1524+ 	impl  HistoricalBucketRangeTracker  { 
1525+ 		pub ( super )  fn  new ( )  -> Self  {  Self  {  buckets :  [ 0 ;  8 ]  }  } 
1526+ 		pub ( super )  fn  track_datapoint ( & mut  self ,  liquidity_offset_msat :  u64 ,  capacity_msat :  u64 )  { 
1527+ 			// We have 8 leaky buckets for min and max liquidity. Each bucket tracks the amount of time 
1528+ 			// we spend in each bucket as a 16-bit fixed-point number with a 5 bit fractional part. 
1529+ 			// 
1530+ 			// Each time we update our liquidity estimate, we add 32 (1.0 in our fixed-point system) to 
1531+ 			// the buckets for the current min and max liquidity offset positions. 
1532+ 			// 
1533+ 			// We then decay each bucket by multiplying by 2047/2048 (avoiding dividing by a 
1534+ 			// non-power-of-two). This ensures we can't actually overflow the u16 - when we get to 
1535+ 			// 63,457 adding 32 and decaying by 2047/2048 leaves us back at 63,457. 
1536+ 			// 
1537+ 			// In total, this allows us to track data for the last 8,000 or so payments across a given 
1538+ 			// channel. 
1539+ 			// 
1540+ 			// These constants are a balance - we try to fit in 2 bytes per bucket to reduce overhead, 
1541+ 			// and need to balance having more bits in the decimal part (to ensure decay isn't too 
1542+ 			// non-linear) with having too few bits in the mantissa, causing us to not store very many 
1543+ 			// datapoints. 
1544+ 			// 
1545+ 			// The constants were picked experimentally, selecting a decay amount that restricts us 
1546+ 			// from overflowing buckets without having to cap them manually. 
1547+ 
1548+ 			// Ensure the bucket index is in the range [0, 7], even if the liquidity offset is zero or 
1549+ 			// the channel's capacity, though the second should generally never happen. 
1550+ 			debug_assert ! ( liquidity_offset_msat <= capacity_msat) ; 
1551+ 			let  bucket_idx:  u8  = ( liquidity_offset_msat *  32  / capacity_msat. saturating_add ( 1 ) ) 
1552+ 				. try_into ( ) . unwrap_or ( 32 ) ;  // 32 is bogus for 32 buckets, and will be ignored 
1553+ 			debug_assert ! ( bucket_idx < 8 ) ; 
1554+ 			if  bucket_idx < 8  { 
1555+ 				for  e in  self . buckets . iter_mut ( )  { 
1556+ 					* e = ( ( * e as  u32 )  *  2047  / 2048 )  as  u16 ; 
1557+ 				} 
1558+ 				self . buckets [ bucket_idx as  usize ]  = self . buckets [ bucket_idx as  usize ] . saturating_add ( 32 ) ; 
1559+ 			} 
1560+ 		} 
1561+ 		/// Decay all buckets by the given number of half-lives. Used to more aggressively remove old 
1562+  		/// datapoints as we receive newer information. 
1563+  		pub ( super )  fn  time_decay_data ( & mut  self ,  half_lives :  u32 )  { 
1564+ 			for  e in  self . buckets . iter_mut ( )  { 
1565+ 				* e = e. checked_shr ( half_lives) . unwrap_or ( 0 ) ; 
1566+ 			} 
1567+ 		} 
1568+ 	} 
1569+ 
1570+ 	impl_writeable_tlv_based ! ( HistoricalBucketRangeTracker ,  {  ( 0 ,  buckets,  required)  } ) ; 
1571+ 
1572+ 	pub ( super )  struct  HistoricalMinMaxBuckets < ' a >  { 
1573+ 		pub ( super )  min_liquidity_offset_history :  & ' a  HistoricalBucketRangeTracker , 
1574+ 		pub ( super )  max_liquidity_offset_history :  & ' a  HistoricalBucketRangeTracker , 
1575+ 	} 
1576+ 
1577+ 	impl  HistoricalMinMaxBuckets < ' _ >  { 
1578+ 		#[ inline]  
1579+ 		pub ( super )  fn  get_decayed_buckets < T :  Time > ( & self ,  now :  T ,  last_updated :  T ,  half_life :  Duration ) 
1580+ 		-> ( [ u16 ;  8 ] ,  [ u16 ;  8 ] ,  u32 )  { 
1581+ 			let  required_decays = now. duration_since ( last_updated) . as_secs ( ) 
1582+ 				. checked_div ( half_life. as_secs ( ) ) 
1583+ 				. map_or ( u32:: max_value ( ) ,  |decays| cmp:: min ( decays,  u32:: max_value ( )  as  u64 )  as  u32 ) ; 
1584+ 			let  mut  min_buckets = * self . min_liquidity_offset_history ; 
1585+ 			min_buckets. time_decay_data ( required_decays) ; 
1586+ 			let  mut  max_buckets = * self . max_liquidity_offset_history ; 
1587+ 			max_buckets. time_decay_data ( required_decays) ; 
1588+ 			( min_buckets. buckets ,  max_buckets. buckets ,  required_decays) 
1589+ 		} 
1590+ 
1591+ 		#[ inline]  
1592+ 		pub ( super )  fn  calculate_success_probability_times_billion < T :  Time > ( 
1593+ 			& self ,  now :  T ,  last_updated :  T ,  half_life :  Duration ,  amount_msat :  u64 ,  capacity_msat :  u64 ) 
1594+ 		-> Option < u64 >  { 
1595+ 			// If historical penalties are enabled, calculate the penalty by walking the set of 
1596+ 			// historical liquidity bucket (min, max) combinations (where min_idx < max_idx) and, for 
1597+ 			// each, calculate the probability of success given our payment amount, then total the 
1598+ 			// weighted average probability of success. 
1599+ 			// 
1600+ 			// We use a sliding scale to decide which point within a given bucket will be compared to 
1601+ 			// the amount being sent - for lower-bounds, the amount being sent is compared to the lower 
1602+ 			// edge of the first bucket (i.e. zero), but compared to the upper 7/8ths of the last 
1603+ 			// bucket (i.e. 9 times the index, or 63), with each bucket in between increasing the 
1604+ 			// comparison point by 1/64th. For upper-bounds, the same applies, however with an offset 
1605+ 			// of 1/64th (i.e. starting at one and ending at 64). This avoids failing to assign 
1606+ 			// penalties to channels at the edges. 
1607+ 			// 
1608+ 			// If we used the bottom edge of buckets, we'd end up never assigning any penalty at all to 
1609+ 			// such a channel when sending less than ~0.19% of the channel's capacity (e.g. ~200k sats 
1610+ 			// for a 1 BTC channel!). 
1611+ 			// 
1612+ 			// If we used the middle of each bucket we'd never assign any penalty at all when sending 
1613+ 			// less than 1/16th of a channel's capacity, or 1/8th if we used the top of the bucket. 
1614+ 			let  mut  total_valid_points_tracked = 0 ; 
1615+ 
1616+ 			let  payment_amt_64th_bucket:  u16  = if  amount_msat < u64:: max_value ( )  / 1024  { 
1617+ 				( amount_msat *  1024  / capacity_msat. saturating_add ( 1 ) ) 
1618+ 					. try_into ( ) . unwrap_or ( 65 ) 
1619+ 			}  else  { 
1620+ 				// Only use 128-bit arithmetic when multiplication will overflow to avoid 128-bit 
1621+ 				// division. This branch should only be hit in fuzz testing since the amount would 
1622+ 				// need to be over 2.88 million BTC in practice. 
1623+ 				( ( amount_msat as  u128 )  *  1024  / ( capacity_msat as  u128 ) . saturating_add ( 1 ) ) 
1624+ 					. try_into ( ) . unwrap_or ( 65 ) 
1625+ 			} ; 
1626+ 			#[ cfg( not( fuzzing) ) ]  
1627+ 			debug_assert ! ( payment_amt_64th_bucket <= 64 ) ; 
1628+ 			if  payment_amt_64th_bucket >= 64  {  return  None ;  } 
1629+ 
1630+ 			// Check if all our buckets are zero, once decayed and treat it as if we had no data. We 
1631+ 			// don't actually use the decayed buckets, though, as that would lose precision. 
1632+ 			let  ( decayed_min_buckets,  decayed_max_buckets,  required_decays)  =
1633+ 				self . get_decayed_buckets ( now,  last_updated,  half_life) ; 
1634+ 			if  decayed_min_buckets. iter ( ) . all ( |v| * v == 0 )  || decayed_max_buckets. iter ( ) . all ( |v| * v == 0 )  { 
1635+ 				return  None ; 
1636+ 			} 
1637+ 
1638+ 			for  ( min_idx,  min_bucket)  in  self . min_liquidity_offset_history . buckets . iter ( ) . enumerate ( )  { 
1639+ 				for  max_bucket in  self . max_liquidity_offset_history . buckets . iter ( ) . take ( 8  - min_idx)  { 
1640+ 					total_valid_points_tracked += ( * min_bucket as  u64 )  *  ( * max_bucket as  u64 ) ; 
1641+ 				} 
1642+ 			} 
1643+ 			// If the total valid points is smaller than 1.0 (i.e. 32 in our fixed-point scheme), treat 
1644+ 			// it as if we were fully decayed. 
1645+ 			if  total_valid_points_tracked. checked_shr ( required_decays) . unwrap_or ( 0 )  < 32 * 32  { 
1646+ 				return  None ; 
1647+ 			} 
1648+ 
1649+ 			let  mut  cumulative_success_prob_times_billion = 0 ; 
1650+ 			for  ( min_idx,  min_bucket)  in  self . min_liquidity_offset_history . buckets . iter ( ) . enumerate ( )  { 
1651+ 				for  ( max_idx,  max_bucket)  in  self . max_liquidity_offset_history . buckets . iter ( ) . enumerate ( ) . take ( 8  - min_idx)  { 
1652+ 					let  min_64th_bucket = min_idx as  u16  *  33 ; 
1653+ 					let  max_64th_bucket = ( 31  - max_idx as  u16 )  *  33  + 1 ; 
1654+ 					let  bucket_prob_times_million = ( * min_bucket as  u64 )  *  ( * max_bucket as  u64 ) 
1655+ 						*  1024  *  1024  / total_valid_points_tracked; 
1656+ 					if  payment_amt_64th_bucket > max_64th_bucket { 
1657+ 						// Success probability 0, the payment amount is above the max liquidity 
1658+ 					}  else  if  payment_amt_64th_bucket <= min_64th_bucket { 
1659+ 						cumulative_success_prob_times_billion += bucket_prob_times_million *  1024 ; 
1660+ 					}  else  { 
1661+ 						cumulative_success_prob_times_billion += bucket_prob_times_million * 
1662+ 							( ( max_64th_bucket - payment_amt_64th_bucket)  as  u64 )  *  1024  /
1663+ 							( ( max_64th_bucket - min_64th_bucket)  as  u64 ) ; 
1664+ 					} 
1665+ 				} 
1666+ 			} 
1667+ 
1668+ 			Some ( cumulative_success_prob_times_billion) 
1669+ 		} 
1670+ 	} 
1671+ } 
1672+ use  bucketed_history:: { HistoricalBucketRangeTracker ,  HistoricalMinMaxBuckets } ; 
1673+ 
16691674impl < G :  Deref < Target  = NetworkGraph < L > > ,  L :  Deref ,  T :  Time >  Writeable  for  ProbabilisticScorerUsingTime < G ,  L ,  T >  where  L :: Target :  Logger  { 
16701675	#[ inline]  
16711676	fn  write < W :  Writer > ( & self ,  w :  & mut  W )  -> Result < ( ) ,  io:: Error >  { 
0 commit comments