use crate::ln::msgs::DecodeError;
use crate::routing::gossip::{DirectedChannelInfo, EffectiveCapacity, NetworkGraph, NodeId};
use crate::routing::router::{Path, CandidateRouteHop, PublicHopCandidate};
use crate::routing::log_approx;
use crate::util::ser::{Readable, ReadableArgs, Writeable, Writer};
use crate::util::logger::Logger;
use crate::prelude::*;
use core::{cmp, fmt};
use core::ops::{Deref, DerefMut};
use core::time::Duration;
use crate::io::{self, Read};
use crate::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
#[cfg(not(c_bindings))]
use {
core::cell::{RefCell, RefMut, Ref},
crate::sync::{Mutex, MutexGuard},
};
macro_rules! define_score { ($($supertrait: path)*) => {
pub trait ScoreLookUp {
type ScoreParams;
fn channel_penalty_msat(
&self, candidate: &CandidateRouteHop, usage: ChannelUsage, score_params: &Self::ScoreParams
) -> u64;
}
pub trait ScoreUpdate {
fn payment_path_failed(&mut self, path: &Path, short_channel_id: u64, duration_since_epoch: Duration);
fn payment_path_successful(&mut self, path: &Path, duration_since_epoch: Duration);
fn probe_failed(&mut self, path: &Path, short_channel_id: u64, duration_since_epoch: Duration);
fn probe_successful(&mut self, path: &Path, duration_since_epoch: Duration);
fn time_passed(&mut self, duration_since_epoch: Duration);
}
pub trait Score : ScoreLookUp + ScoreUpdate $(+ $supertrait)* {}
#[cfg(not(c_bindings))]
impl<T: ScoreLookUp + ScoreUpdate $(+ $supertrait)*> Score for T {}
#[cfg(not(c_bindings))]
impl<S: ScoreLookUp, T: Deref<Target=S>> ScoreLookUp for T {
type ScoreParams = S::ScoreParams;
fn channel_penalty_msat(
&self, candidate: &CandidateRouteHop, usage: ChannelUsage, score_params: &Self::ScoreParams
) -> u64 {
self.deref().channel_penalty_msat(candidate, usage, score_params)
}
}
#[cfg(not(c_bindings))]
impl<S: ScoreUpdate, T: DerefMut<Target=S>> ScoreUpdate for T {
fn payment_path_failed(&mut self, path: &Path, short_channel_id: u64, duration_since_epoch: Duration) {
self.deref_mut().payment_path_failed(path, short_channel_id, duration_since_epoch)
}
fn payment_path_successful(&mut self, path: &Path, duration_since_epoch: Duration) {
self.deref_mut().payment_path_successful(path, duration_since_epoch)
}
fn probe_failed(&mut self, path: &Path, short_channel_id: u64, duration_since_epoch: Duration) {
self.deref_mut().probe_failed(path, short_channel_id, duration_since_epoch)
}
fn probe_successful(&mut self, path: &Path, duration_since_epoch: Duration) {
self.deref_mut().probe_successful(path, duration_since_epoch)
}
fn time_passed(&mut self, duration_since_epoch: Duration) {
self.deref_mut().time_passed(duration_since_epoch)
}
}
} }
#[cfg(c_bindings)]
define_score!(Writeable);
#[cfg(not(c_bindings))]
define_score!();
pub trait LockableScore<'a> {
type ScoreUpdate: 'a + ScoreUpdate;
type ScoreLookUp: 'a + ScoreLookUp;
type WriteLocked: DerefMut<Target = Self::ScoreUpdate> + Sized;
type ReadLocked: Deref<Target = Self::ScoreLookUp> + Sized;
fn read_lock(&'a self) -> Self::ReadLocked;
fn write_lock(&'a self) -> Self::WriteLocked;
}
pub trait WriteableScore<'a>: LockableScore<'a> + Writeable {}
#[cfg(not(c_bindings))]
impl<'a, T> WriteableScore<'a> for T where T: LockableScore<'a> + Writeable {}
#[cfg(not(c_bindings))]
impl<'a, T: Score + 'a> LockableScore<'a> for Mutex<T> {
type ScoreUpdate = T;
type ScoreLookUp = T;
type WriteLocked = MutexGuard<'a, Self::ScoreUpdate>;
type ReadLocked = MutexGuard<'a, Self::ScoreLookUp>;
fn read_lock(&'a self) -> Self::ReadLocked {
Mutex::lock(self).unwrap()
}
fn write_lock(&'a self) -> Self::WriteLocked {
Mutex::lock(self).unwrap()
}
}
#[cfg(not(c_bindings))]
impl<'a, T: Score + 'a> LockableScore<'a> for RefCell<T> {
type ScoreUpdate = T;
type ScoreLookUp = T;
type WriteLocked = RefMut<'a, Self::ScoreUpdate>;
type ReadLocked = Ref<'a, Self::ScoreLookUp>;
fn write_lock(&'a self) -> Self::WriteLocked {
self.borrow_mut()
}
fn read_lock(&'a self) -> Self::ReadLocked {
self.borrow()
}
}
#[cfg(any(not(c_bindings), feature = "_test_utils", test))]
impl<'a, T: Score + 'a> LockableScore<'a> for RwLock<T> {
type ScoreUpdate = T;
type ScoreLookUp = T;
type WriteLocked = RwLockWriteGuard<'a, Self::ScoreLookUp>;
type ReadLocked = RwLockReadGuard<'a, Self::ScoreUpdate>;
fn read_lock(&'a self) -> Self::ReadLocked {
RwLock::read(self).unwrap()
}
fn write_lock(&'a self) -> Self::WriteLocked {
RwLock::write(self).unwrap()
}
}
#[cfg(c_bindings)]
pub struct MultiThreadedLockableScore<T: Score> {
score: RwLock<T>,
}
#[cfg(c_bindings)]
impl<'a, T: Score + 'a> LockableScore<'a> for MultiThreadedLockableScore<T> {
type ScoreUpdate = T;
type ScoreLookUp = T;
type WriteLocked = MultiThreadedScoreLockWrite<'a, Self::ScoreUpdate>;
type ReadLocked = MultiThreadedScoreLockRead<'a, Self::ScoreLookUp>;
fn read_lock(&'a self) -> Self::ReadLocked {
MultiThreadedScoreLockRead(self.score.read().unwrap())
}
fn write_lock(&'a self) -> Self::WriteLocked {
MultiThreadedScoreLockWrite(self.score.write().unwrap())
}
}
#[cfg(c_bindings)]
impl<T: Score> Writeable for MultiThreadedLockableScore<T> {
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
self.score.read().unwrap().write(writer)
}
}
#[cfg(c_bindings)]
impl<'a, T: Score + 'a> WriteableScore<'a> for MultiThreadedLockableScore<T> {}
#[cfg(c_bindings)]
impl<T: Score> MultiThreadedLockableScore<T> {
pub fn new(score: T) -> Self {
MultiThreadedLockableScore { score: RwLock::new(score) }
}
}
#[cfg(c_bindings)]
pub struct MultiThreadedScoreLockRead<'a, T: Score>(RwLockReadGuard<'a, T>);
#[cfg(c_bindings)]
pub struct MultiThreadedScoreLockWrite<'a, T: Score>(RwLockWriteGuard<'a, T>);
#[cfg(c_bindings)]
impl<'a, T: 'a + Score> Deref for MultiThreadedScoreLockRead<'a, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
self.0.deref()
}
}
#[cfg(c_bindings)]
impl<'a, T: Score> ScoreLookUp for MultiThreadedScoreLockRead<'a, T> {
type ScoreParams = T::ScoreParams;
fn channel_penalty_msat(&self, candidate:&CandidateRouteHop, usage: ChannelUsage, score_params: &Self::ScoreParams
) -> u64 {
self.0.channel_penalty_msat(candidate, usage, score_params)
}
}
#[cfg(c_bindings)]
impl<'a, T: Score> Writeable for MultiThreadedScoreLockWrite<'a, T> {
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
self.0.write(writer)
}
}
#[cfg(c_bindings)]
impl<'a, T: 'a + Score> Deref for MultiThreadedScoreLockWrite<'a, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
self.0.deref()
}
}
#[cfg(c_bindings)]
impl<'a, T: 'a + Score> DerefMut for MultiThreadedScoreLockWrite<'a, T> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.0.deref_mut()
}
}
#[cfg(c_bindings)]
impl<'a, T: Score> ScoreUpdate for MultiThreadedScoreLockWrite<'a, T> {
fn payment_path_failed(&mut self, path: &Path, short_channel_id: u64, duration_since_epoch: Duration) {
self.0.payment_path_failed(path, short_channel_id, duration_since_epoch)
}
fn payment_path_successful(&mut self, path: &Path, duration_since_epoch: Duration) {
self.0.payment_path_successful(path, duration_since_epoch)
}
fn probe_failed(&mut self, path: &Path, short_channel_id: u64, duration_since_epoch: Duration) {
self.0.probe_failed(path, short_channel_id, duration_since_epoch)
}
fn probe_successful(&mut self, path: &Path, duration_since_epoch: Duration) {
self.0.probe_successful(path, duration_since_epoch)
}
fn time_passed(&mut self, duration_since_epoch: Duration) {
self.0.time_passed(duration_since_epoch)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ChannelUsage {
pub amount_msat: u64,
pub inflight_htlc_msat: u64,
pub effective_capacity: EffectiveCapacity,
}
#[derive(Clone)]
pub struct FixedPenaltyScorer {
penalty_msat: u64,
}
impl FixedPenaltyScorer {
pub fn with_penalty(penalty_msat: u64) -> Self {
Self { penalty_msat }
}
}
impl ScoreLookUp for FixedPenaltyScorer {
type ScoreParams = ();
fn channel_penalty_msat(&self, _: &CandidateRouteHop, _: ChannelUsage, _score_params: &Self::ScoreParams) -> u64 {
self.penalty_msat
}
}
impl ScoreUpdate for FixedPenaltyScorer {
fn payment_path_failed(&mut self, _path: &Path, _short_channel_id: u64, _duration_since_epoch: Duration) {}
fn payment_path_successful(&mut self, _path: &Path, _duration_since_epoch: Duration) {}
fn probe_failed(&mut self, _path: &Path, _short_channel_id: u64, _duration_since_epoch: Duration) {}
fn probe_successful(&mut self, _path: &Path, _duration_since_epoch: Duration) {}
fn time_passed(&mut self, _duration_since_epoch: Duration) {}
}
impl Writeable for FixedPenaltyScorer {
#[inline]
fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> {
write_tlv_fields!(w, {});
Ok(())
}
}
impl ReadableArgs<u64> for FixedPenaltyScorer {
#[inline]
fn read<R: Read>(r: &mut R, penalty_msat: u64) -> Result<Self, DecodeError> {
read_tlv_fields!(r, {});
Ok(Self { penalty_msat })
}
}
pub struct ProbabilisticScorer<G: Deref<Target = NetworkGraph<L>>, L: Deref>
where L::Target: Logger {
decay_params: ProbabilisticScoringDecayParameters,
network_graph: G,
logger: L,
channel_liquidities: HashMap<u64, ChannelLiquidity>,
}
#[derive(Clone)]
pub struct ProbabilisticScoringFeeParameters {
pub base_penalty_msat: u64,
pub base_penalty_amount_multiplier_msat: u64,
pub liquidity_penalty_multiplier_msat: u64,
pub liquidity_penalty_amount_multiplier_msat: u64,
pub historical_liquidity_penalty_multiplier_msat: u64,
pub historical_liquidity_penalty_amount_multiplier_msat: u64,
pub manual_node_penalties: HashMap<NodeId, u64>,
pub anti_probing_penalty_msat: u64,
pub considered_impossible_penalty_msat: u64,
pub linear_success_probability: bool,
}
impl Default for ProbabilisticScoringFeeParameters {
fn default() -> Self {
Self {
base_penalty_msat: 1024,
base_penalty_amount_multiplier_msat: 131_072,
liquidity_penalty_multiplier_msat: 0,
liquidity_penalty_amount_multiplier_msat: 0,
manual_node_penalties: new_hash_map(),
anti_probing_penalty_msat: 250,
considered_impossible_penalty_msat: 1_0000_0000_000,
historical_liquidity_penalty_multiplier_msat: 10_000,
historical_liquidity_penalty_amount_multiplier_msat: 1_250,
linear_success_probability: false,
}
}
}
impl ProbabilisticScoringFeeParameters {
pub fn add_banned(&mut self, node_id: &NodeId) {
self.manual_node_penalties.insert(*node_id, u64::max_value());
}
pub fn add_banned_from_list(&mut self, node_ids: Vec<NodeId>) {
for id in node_ids {
self.manual_node_penalties.insert(id, u64::max_value());
}
}
pub fn remove_banned(&mut self, node_id: &NodeId) {
self.manual_node_penalties.remove(node_id);
}
pub fn set_manual_penalty(&mut self, node_id: &NodeId, penalty: u64) {
self.manual_node_penalties.insert(*node_id, penalty);
}
pub fn remove_manual_penalty(&mut self, node_id: &NodeId) {
self.manual_node_penalties.remove(node_id);
}
pub fn clear_manual_penalties(&mut self) {
self.manual_node_penalties = new_hash_map();
}
}
#[cfg(test)]
impl ProbabilisticScoringFeeParameters {
fn zero_penalty() -> Self {
Self {
base_penalty_msat: 0,
base_penalty_amount_multiplier_msat: 0,
liquidity_penalty_multiplier_msat: 0,
liquidity_penalty_amount_multiplier_msat: 0,
historical_liquidity_penalty_multiplier_msat: 0,
historical_liquidity_penalty_amount_multiplier_msat: 0,
manual_node_penalties: new_hash_map(),
anti_probing_penalty_msat: 0,
considered_impossible_penalty_msat: 0,
linear_success_probability: true,
}
}
}
#[derive(Copy, Clone)]
pub struct ProbabilisticScoringDecayParameters {
pub historical_no_updates_half_life: Duration,
pub liquidity_offset_half_life: Duration,
}
impl Default for ProbabilisticScoringDecayParameters {
fn default() -> Self {
Self {
liquidity_offset_half_life: Duration::from_secs(30 * 60),
historical_no_updates_half_life: Duration::from_secs(60 * 60 * 24 * 14),
}
}
}
#[cfg(test)]
impl ProbabilisticScoringDecayParameters {
fn zero_penalty() -> Self {
Self {
liquidity_offset_half_life: Duration::from_secs(30 * 60),
historical_no_updates_half_life: Duration::from_secs(60 * 60 * 24 * 14),
}
}
}
#[repr(C)] struct ChannelLiquidity {
min_liquidity_offset_msat: u64,
max_liquidity_offset_msat: u64,
liquidity_history: HistoricalLiquidityTracker,
last_updated: Duration,
offset_history_last_updated: Duration,
}
const _LIQUIDITY_MAP_SIZING_CHECK: usize = 192 - ::core::mem::size_of::<(u64, ChannelLiquidity)>();
const _LIQUIDITY_MAP_SIZING_CHECK_2: usize = ::core::mem::size_of::<(u64, ChannelLiquidity)>() - 192;
struct DirectedChannelLiquidity<L: Deref<Target = u64>, HT: Deref<Target = HistoricalLiquidityTracker>, T: Deref<Target = Duration>> {
min_liquidity_offset_msat: L,
max_liquidity_offset_msat: L,
liquidity_history: DirectedHistoricalLiquidityTracker<HT>,
capacity_msat: u64,
last_updated: T,
offset_history_last_updated: T,
}
impl<G: Deref<Target = NetworkGraph<L>>, L: Deref> ProbabilisticScorer<G, L> where L::Target: Logger {
pub fn new(decay_params: ProbabilisticScoringDecayParameters, network_graph: G, logger: L) -> Self {
Self {
decay_params,
network_graph,
logger,
channel_liquidities: new_hash_map(),
}
}
#[cfg(test)]
fn with_channel(mut self, short_channel_id: u64, liquidity: ChannelLiquidity) -> Self {
assert!(self.channel_liquidities.insert(short_channel_id, liquidity).is_none());
self
}
pub fn debug_log_liquidity_stats(&self) {
let graph = self.network_graph.read_only();
for (scid, liq) in self.channel_liquidities.iter() {
if let Some(chan_debug) = graph.channels().get(scid) {
let log_direction = |source, target| {
if let Some((directed_info, _)) = chan_debug.as_directed_to(target) {
let amt = directed_info.effective_capacity().as_msat();
let dir_liq = liq.as_directed(source, target, amt);
let min_buckets = &dir_liq.liquidity_history.min_liquidity_offset_history_buckets();
let max_buckets = &dir_liq.liquidity_history.max_liquidity_offset_history_buckets();
log_debug!(self.logger, core::concat!(
"Liquidity from {} to {} via {} is in the range ({}, {}).\n",
"\tHistorical min liquidity bucket relative probabilities:\n",
"\t\t{} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {}\n",
"\tHistorical max liquidity bucket relative probabilities:\n",
"\t\t{} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {}"),
source, target, scid, dir_liq.min_liquidity_msat(), dir_liq.max_liquidity_msat(),
min_buckets[ 0], min_buckets[ 1], min_buckets[ 2], min_buckets[ 3],
min_buckets[ 4], min_buckets[ 5], min_buckets[ 6], min_buckets[ 7],
min_buckets[ 8], min_buckets[ 9], min_buckets[10], min_buckets[11],
min_buckets[12], min_buckets[13], min_buckets[14], min_buckets[15],
min_buckets[16], min_buckets[17], min_buckets[18], min_buckets[19],
min_buckets[20], min_buckets[21], min_buckets[22], min_buckets[23],
min_buckets[24], min_buckets[25], min_buckets[26], min_buckets[27],
min_buckets[28], min_buckets[29], min_buckets[30], min_buckets[31],
max_buckets[31], max_buckets[30], max_buckets[29], max_buckets[28],
max_buckets[27], max_buckets[26], max_buckets[25], max_buckets[24],
max_buckets[23], max_buckets[22], max_buckets[21], max_buckets[20],
max_buckets[19], max_buckets[18], max_buckets[17], max_buckets[16],
max_buckets[15], max_buckets[14], max_buckets[13], max_buckets[12],
max_buckets[11], max_buckets[10], max_buckets[ 9], max_buckets[ 8],
max_buckets[ 7], max_buckets[ 6], max_buckets[ 5], max_buckets[ 4],
max_buckets[ 3], max_buckets[ 2], max_buckets[ 1], max_buckets[ 0]);
} else {
log_debug!(self.logger, "No amount known for SCID {} from {:?} to {:?}", scid, source, target);
}
};
log_direction(&chan_debug.node_one, &chan_debug.node_two);
log_direction(&chan_debug.node_two, &chan_debug.node_one);
} else {
log_debug!(self.logger, "No network graph entry for SCID {}", scid);
}
}
}
pub fn estimated_channel_liquidity_range(&self, scid: u64, target: &NodeId) -> Option<(u64, u64)> {
let graph = self.network_graph.read_only();
if let Some(chan) = graph.channels().get(&scid) {
if let Some(liq) = self.channel_liquidities.get(&scid) {
if let Some((directed_info, source)) = chan.as_directed_to(target) {
let amt = directed_info.effective_capacity().as_msat();
let dir_liq = liq.as_directed(source, target, amt);
return Some((dir_liq.min_liquidity_msat(), dir_liq.max_liquidity_msat()));
}
}
}
None
}
pub fn historical_estimated_channel_liquidity_probabilities(&self, scid: u64, target: &NodeId)
-> Option<([u16; 32], [u16; 32])> {
let graph = self.network_graph.read_only();
if let Some(chan) = graph.channels().get(&scid) {
if let Some(liq) = self.channel_liquidities.get(&scid) {
if let Some((directed_info, source)) = chan.as_directed_to(target) {
let amt = directed_info.effective_capacity().as_msat();
let dir_liq = liq.as_directed(source, target, amt);
let min_buckets = *dir_liq.liquidity_history.min_liquidity_offset_history_buckets();
let mut max_buckets = *dir_liq.liquidity_history.max_liquidity_offset_history_buckets();
max_buckets.reverse();
return Some((min_buckets, max_buckets));
}
}
}
None
}
pub fn historical_estimated_payment_success_probability(
&self, scid: u64, target: &NodeId, amount_msat: u64, params: &ProbabilisticScoringFeeParameters,
allow_fallback_estimation: bool,
) -> Option<f64> {
let graph = self.network_graph.read_only();
if let Some(chan) = graph.channels().get(&scid) {
if let Some((directed_info, source)) = chan.as_directed_to(target) {
if let Some(liq) = self.channel_liquidities.get(&scid) {
let capacity_msat = directed_info.effective_capacity().as_msat();
let dir_liq = liq.as_directed(source, target, capacity_msat);
let res = dir_liq.liquidity_history.calculate_success_probability_times_billion(
¶ms, amount_msat, capacity_msat
).map(|p| p as f64 / (1024 * 1024 * 1024) as f64);
if res.is_some() {
return res;
}
}
if allow_fallback_estimation {
let amt = amount_msat;
return Some(
self.calc_live_prob(scid, source, target, directed_info, amt, params, true)
);
}
}
}
None
}
fn calc_live_prob(
&self, scid: u64, source: &NodeId, target: &NodeId, directed_info: DirectedChannelInfo,
amt: u64, params: &ProbabilisticScoringFeeParameters,
min_zero_penalty: bool,
) -> f64 {
let capacity_msat = directed_info.effective_capacity().as_msat();
let dummy_liq = ChannelLiquidity::new(Duration::ZERO);
let liq = self.channel_liquidities.get(&scid)
.unwrap_or(&dummy_liq)
.as_directed(&source, &target, capacity_msat);
let min_liq = liq.min_liquidity_msat();
let max_liq = liq.max_liquidity_msat();
if amt <= liq.min_liquidity_msat() {
return 1.0;
} else if amt > liq.max_liquidity_msat() {
return 0.0;
}
let (num, den) =
success_probability(amt, min_liq, max_liq, capacity_msat, ¶ms, min_zero_penalty);
num as f64 / den as f64
}
pub fn live_estimated_payment_success_probability(
&self, scid: u64, target: &NodeId, amount_msat: u64, params: &ProbabilisticScoringFeeParameters,
) -> Option<f64> {
let graph = self.network_graph.read_only();
if let Some(chan) = graph.channels().get(&scid) {
if let Some((directed_info, source)) = chan.as_directed_to(target) {
return Some(self.calc_live_prob(scid, source, target, directed_info, amount_msat, params, false));
}
}
None
}
}
impl ChannelLiquidity {
fn new(last_updated: Duration) -> Self {
Self {
min_liquidity_offset_msat: 0,
max_liquidity_offset_msat: 0,
liquidity_history: HistoricalLiquidityTracker::new(),
last_updated,
offset_history_last_updated: last_updated,
}
}
fn as_directed(
&self, source: &NodeId, target: &NodeId, capacity_msat: u64,
) -> DirectedChannelLiquidity<&u64, &HistoricalLiquidityTracker, &Duration> {
let source_less_than_target = source < target;
let (min_liquidity_offset_msat, max_liquidity_offset_msat) =
if source_less_than_target {
(&self.min_liquidity_offset_msat, &self.max_liquidity_offset_msat)
} else {
(&self.max_liquidity_offset_msat, &self.min_liquidity_offset_msat)
};
DirectedChannelLiquidity {
min_liquidity_offset_msat,
max_liquidity_offset_msat,
liquidity_history: self.liquidity_history.as_directed(source_less_than_target),
capacity_msat,
last_updated: &self.last_updated,
offset_history_last_updated: &self.offset_history_last_updated,
}
}
fn as_directed_mut(
&mut self, source: &NodeId, target: &NodeId, capacity_msat: u64,
) -> DirectedChannelLiquidity<&mut u64, &mut HistoricalLiquidityTracker, &mut Duration> {
let source_less_than_target = source < target;
let (min_liquidity_offset_msat, max_liquidity_offset_msat) =
if source_less_than_target {
(&mut self.min_liquidity_offset_msat, &mut self.max_liquidity_offset_msat)
} else {
(&mut self.max_liquidity_offset_msat, &mut self.min_liquidity_offset_msat)
};
DirectedChannelLiquidity {
min_liquidity_offset_msat,
max_liquidity_offset_msat,
liquidity_history: self.liquidity_history.as_directed_mut(source_less_than_target),
capacity_msat,
last_updated: &mut self.last_updated,
offset_history_last_updated: &mut self.offset_history_last_updated,
}
}
fn decayed_offset(
&self, offset: u64, duration_since_epoch: Duration,
decay_params: ProbabilisticScoringDecayParameters,
) -> u64 {
let half_life = decay_params.liquidity_offset_half_life.as_secs_f64();
if half_life != 0.0 {
let elapsed_time = duration_since_epoch.saturating_sub(self.last_updated).as_secs_f64();
((offset as f64) * powf64(0.5, elapsed_time / half_life)) as u64
} else {
0
}
}
}
const NEGATIVE_LOG10_UPPER_BOUND: u64 = 2;
const PRECISION_LOWER_BOUND_DENOMINATOR: u64 = log_approx::LOWER_BITS_BOUND;
const AMOUNT_PENALTY_DIVISOR: u64 = 1 << 20;
const BASE_AMOUNT_PENALTY_DIVISOR: u64 = 1 << 30;
#[inline(always)]
fn three_f64_pow_9(a: f64, b: f64, c: f64) -> (f64, f64, f64) {
let (a2, b2, c2) = (a * a, b * b, c * c);
let (a4, b4, c4) = (a2 * a2, b2 * b2, c2 * c2);
(a * a4 * a4, b * b4 * b4, c * c4 * c4)
}
const MIN_ZERO_IMPLIES_NO_SUCCESSES_PENALTY_ON_64: u64 = 78;
#[inline(always)]
fn linear_success_probability(
total_inflight_amount_msat: u64, min_liquidity_msat: u64, max_liquidity_msat: u64,
min_zero_implies_no_successes: bool,
) -> (u64, u64) {
let (numerator, mut denominator) =
(max_liquidity_msat - total_inflight_amount_msat,
(max_liquidity_msat - min_liquidity_msat).saturating_add(1));
if min_zero_implies_no_successes && min_liquidity_msat == 0 &&
denominator < u64::max_value() / MIN_ZERO_IMPLIES_NO_SUCCESSES_PENALTY_ON_64
{
denominator = denominator * MIN_ZERO_IMPLIES_NO_SUCCESSES_PENALTY_ON_64 / 64
}
(numerator, denominator)
}
#[inline(always)]
fn nonlinear_success_probability(
total_inflight_amount_msat: u64, min_liquidity_msat: u64, max_liquidity_msat: u64,
capacity_msat: u64, min_zero_implies_no_successes: bool,
) -> (f64, f64) {
let capacity = capacity_msat as f64;
let max = (max_liquidity_msat as f64) / capacity;
let min = (min_liquidity_msat as f64) / capacity;
let amount = (total_inflight_amount_msat as f64) / capacity;
let (max_norm, min_norm, amt_norm) = (max - 0.5, min - 0.5, amount - 0.5);
let (max_pow, min_pow, amt_pow) = three_f64_pow_9(max_norm, min_norm, amt_norm);
let (max_v, min_v, amt_v) = (max_pow + max_norm / 256.0, min_pow + min_norm / 256.0, amt_pow + amt_norm / 256.0);
let mut denominator = max_v - min_v;
let numerator = max_v - amt_v;
if min_zero_implies_no_successes && min_liquidity_msat == 0 {
denominator = denominator * (MIN_ZERO_IMPLIES_NO_SUCCESSES_PENALTY_ON_64 as f64) / 64.0;
}
(numerator, denominator)
}
#[inline(always)]
fn success_probability_float(
total_inflight_amount_msat: u64, min_liquidity_msat: u64, max_liquidity_msat: u64,
capacity_msat: u64, params: &ProbabilisticScoringFeeParameters,
min_zero_implies_no_successes: bool,
) -> (f64, f64) {
debug_assert!(min_liquidity_msat <= total_inflight_amount_msat);
debug_assert!(total_inflight_amount_msat < max_liquidity_msat);
debug_assert!(max_liquidity_msat <= capacity_msat);
if params.linear_success_probability {
let (numerator, denominator) = linear_success_probability(total_inflight_amount_msat, min_liquidity_msat, max_liquidity_msat, min_zero_implies_no_successes);
(numerator as f64, denominator as f64)
} else {
nonlinear_success_probability(total_inflight_amount_msat, min_liquidity_msat, max_liquidity_msat, capacity_msat, min_zero_implies_no_successes)
}
}
#[inline(always)]
fn success_probability(
total_inflight_amount_msat: u64, min_liquidity_msat: u64, max_liquidity_msat: u64,
capacity_msat: u64, params: &ProbabilisticScoringFeeParameters,
min_zero_implies_no_successes: bool,
) -> (u64, u64) {
debug_assert!(min_liquidity_msat <= total_inflight_amount_msat);
debug_assert!(total_inflight_amount_msat < max_liquidity_msat);
debug_assert!(max_liquidity_msat <= capacity_msat);
if params.linear_success_probability {
linear_success_probability(total_inflight_amount_msat, min_liquidity_msat, max_liquidity_msat, min_zero_implies_no_successes)
} else {
let (num, den) = nonlinear_success_probability(
total_inflight_amount_msat, min_liquidity_msat, max_liquidity_msat, capacity_msat,
min_zero_implies_no_successes,
);
const BILLIONISH: f64 = 1024.0 * 1024.0 * 1024.0 * 64.0;
let numerator = (num * BILLIONISH) as u64 + 1;
let denominator = (den * BILLIONISH) as u64 + 1;
debug_assert!(numerator <= 1 << 30, "Got large numerator ({}) from float {}.", numerator, num);
debug_assert!(denominator <= 1 << 30, "Got large denominator ({}) from float {}.", denominator, den);
(numerator, denominator)
}
}
impl<L: Deref<Target = u64>, HT: Deref<Target = HistoricalLiquidityTracker>, T: Deref<Target = Duration>>
DirectedChannelLiquidity< L, HT, T> {
fn penalty_msat(
&self, amount_msat: u64, inflight_htlc_msat: u64,
score_params: &ProbabilisticScoringFeeParameters,
) -> u64 {
let total_inflight_amount_msat = amount_msat.saturating_add(inflight_htlc_msat);
let available_capacity = self.capacity_msat;
let max_liquidity_msat = self.max_liquidity_msat();
let min_liquidity_msat = core::cmp::min(self.min_liquidity_msat(), max_liquidity_msat);
let mut res = 0;
if score_params.liquidity_penalty_multiplier_msat != 0 ||
score_params.liquidity_penalty_amount_multiplier_msat != 0 {
if total_inflight_amount_msat <= min_liquidity_msat {
} else if total_inflight_amount_msat >= max_liquidity_msat {
let negative_log10_times_2048 = NEGATIVE_LOG10_UPPER_BOUND * 2048;
res = Self::combined_penalty_msat(amount_msat, negative_log10_times_2048,
score_params.liquidity_penalty_multiplier_msat,
score_params.liquidity_penalty_amount_multiplier_msat);
} else {
let (numerator, denominator) = success_probability(
total_inflight_amount_msat, min_liquidity_msat, max_liquidity_msat,
available_capacity, score_params, false,
);
if denominator - numerator < denominator / PRECISION_LOWER_BOUND_DENOMINATOR {
} else {
let negative_log10_times_2048 =
log_approx::negative_log10_times_2048(numerator, denominator);
res = Self::combined_penalty_msat(amount_msat, negative_log10_times_2048,
score_params.liquidity_penalty_multiplier_msat,
score_params.liquidity_penalty_amount_multiplier_msat);
}
}
}
if total_inflight_amount_msat >= max_liquidity_msat {
res = res.saturating_add(score_params.considered_impossible_penalty_msat);
}
if total_inflight_amount_msat >= available_capacity {
res = res.saturating_add(Self::combined_penalty_msat(amount_msat,
NEGATIVE_LOG10_UPPER_BOUND * 2048,
score_params.historical_liquidity_penalty_multiplier_msat,
score_params.historical_liquidity_penalty_amount_multiplier_msat));
return res;
}
if score_params.historical_liquidity_penalty_multiplier_msat != 0 ||
score_params.historical_liquidity_penalty_amount_multiplier_msat != 0 {
if let Some(cumulative_success_prob_times_billion) = self.liquidity_history
.calculate_success_probability_times_billion(
score_params, total_inflight_amount_msat, self.capacity_msat
)
{
let historical_negative_log10_times_2048 =
log_approx::negative_log10_times_2048(cumulative_success_prob_times_billion + 1, 1024 * 1024 * 1024);
res = res.saturating_add(Self::combined_penalty_msat(amount_msat,
historical_negative_log10_times_2048, score_params.historical_liquidity_penalty_multiplier_msat,
score_params.historical_liquidity_penalty_amount_multiplier_msat));
} else {
let (numerator, denominator) = success_probability(
total_inflight_amount_msat, 0, available_capacity, available_capacity,
score_params, true,
);
let negative_log10_times_2048 =
log_approx::negative_log10_times_2048(numerator, denominator);
res = res.saturating_add(Self::combined_penalty_msat(amount_msat, negative_log10_times_2048,
score_params.historical_liquidity_penalty_multiplier_msat,
score_params.historical_liquidity_penalty_amount_multiplier_msat));
}
}
res
}
#[inline(always)]
fn combined_penalty_msat(amount_msat: u64, mut negative_log10_times_2048: u64,
liquidity_penalty_multiplier_msat: u64, liquidity_penalty_amount_multiplier_msat: u64,
) -> u64 {
negative_log10_times_2048 =
negative_log10_times_2048.min(NEGATIVE_LOG10_UPPER_BOUND * 2048);
let liquidity_penalty_msat = negative_log10_times_2048
.saturating_mul(liquidity_penalty_multiplier_msat) / 2048;
let amount_penalty_msat = negative_log10_times_2048
.saturating_mul(liquidity_penalty_amount_multiplier_msat)
.saturating_mul(amount_msat) / 2048 / AMOUNT_PENALTY_DIVISOR;
liquidity_penalty_msat.saturating_add(amount_penalty_msat)
}
#[inline(always)]
fn min_liquidity_msat(&self) -> u64 {
*self.min_liquidity_offset_msat
}
#[inline(always)]
fn max_liquidity_msat(&self) -> u64 {
self.capacity_msat
.saturating_sub(*self.max_liquidity_offset_msat)
}
}
impl<L: DerefMut<Target = u64>, HT: DerefMut<Target = HistoricalLiquidityTracker>, T: DerefMut<Target = Duration>>
DirectedChannelLiquidity<L, HT, T> {
fn failed_at_channel<Log: Deref>(
&mut self, amount_msat: u64, duration_since_epoch: Duration, chan_descr: fmt::Arguments, logger: &Log
) where Log::Target: Logger {
let existing_max_msat = self.max_liquidity_msat();
if amount_msat < existing_max_msat {
log_debug!(logger, "Setting max liquidity of {} from {} to {}", chan_descr, existing_max_msat, amount_msat);
self.set_max_liquidity_msat(amount_msat, duration_since_epoch);
} else {
log_trace!(logger, "Max liquidity of {} is {} (already less than or equal to {})",
chan_descr, existing_max_msat, amount_msat);
}
self.update_history_buckets(0, duration_since_epoch);
}
fn failed_downstream<Log: Deref>(
&mut self, amount_msat: u64, duration_since_epoch: Duration, chan_descr: fmt::Arguments, logger: &Log
) where Log::Target: Logger {
let existing_min_msat = self.min_liquidity_msat();
if amount_msat > existing_min_msat {
log_debug!(logger, "Setting min liquidity of {} from {} to {}", existing_min_msat, chan_descr, amount_msat);
self.set_min_liquidity_msat(amount_msat, duration_since_epoch);
} else {
log_trace!(logger, "Min liquidity of {} is {} (already greater than or equal to {})",
chan_descr, existing_min_msat, amount_msat);
}
self.update_history_buckets(0, duration_since_epoch);
}
fn successful<Log: Deref>(&mut self,
amount_msat: u64, duration_since_epoch: Duration, chan_descr: fmt::Arguments, logger: &Log
) where Log::Target: Logger {
let max_liquidity_msat = self.max_liquidity_msat().checked_sub(amount_msat).unwrap_or(0);
log_debug!(logger, "Subtracting {} from max liquidity of {} (setting it to {})", amount_msat, chan_descr, max_liquidity_msat);
self.set_max_liquidity_msat(max_liquidity_msat, duration_since_epoch);
self.update_history_buckets(amount_msat, duration_since_epoch);
}
fn update_history_buckets(&mut self, bucket_offset_msat: u64, duration_since_epoch: Duration) {
self.liquidity_history.track_datapoint(
*self.min_liquidity_offset_msat + bucket_offset_msat,
self.max_liquidity_offset_msat.saturating_sub(bucket_offset_msat),
self.capacity_msat,
);
*self.offset_history_last_updated = duration_since_epoch;
}
fn set_min_liquidity_msat(&mut self, amount_msat: u64, duration_since_epoch: Duration) {
*self.min_liquidity_offset_msat = amount_msat;
if amount_msat > self.max_liquidity_msat() {
*self.max_liquidity_offset_msat = 0;
}
*self.last_updated = duration_since_epoch;
}
fn set_max_liquidity_msat(&mut self, amount_msat: u64, duration_since_epoch: Duration) {
*self.max_liquidity_offset_msat = self.capacity_msat.checked_sub(amount_msat).unwrap_or(0);
if amount_msat < *self.min_liquidity_offset_msat {
*self.min_liquidity_offset_msat = 0;
}
*self.last_updated = duration_since_epoch;
}
}
impl<G: Deref<Target = NetworkGraph<L>>, L: Deref> ScoreLookUp for ProbabilisticScorer<G, L> where L::Target: Logger {
type ScoreParams = ProbabilisticScoringFeeParameters;
fn channel_penalty_msat(
&self, candidate: &CandidateRouteHop, usage: ChannelUsage, score_params: &ProbabilisticScoringFeeParameters
) -> u64 {
let (scid, target) = match candidate {
CandidateRouteHop::PublicHop(PublicHopCandidate { info, short_channel_id }) => {
(short_channel_id, info.target())
},
_ => return 0,
};
let source = candidate.source();
if let Some(penalty) = score_params.manual_node_penalties.get(target) {
return *penalty;
}
let base_penalty_msat = score_params.base_penalty_msat.saturating_add(
score_params.base_penalty_amount_multiplier_msat
.saturating_mul(usage.amount_msat) / BASE_AMOUNT_PENALTY_DIVISOR);
let mut anti_probing_penalty_msat = 0;
match usage.effective_capacity {
EffectiveCapacity::ExactLiquidity { liquidity_msat: amount_msat } |
EffectiveCapacity::HintMaxHTLC { amount_msat } =>
{
if usage.amount_msat > amount_msat {
return u64::max_value();
} else {
return base_penalty_msat;
}
},
EffectiveCapacity::Total { capacity_msat, htlc_maximum_msat } => {
if htlc_maximum_msat >= capacity_msat/2 {
anti_probing_penalty_msat = score_params.anti_probing_penalty_msat;
}
},
_ => {},
}
let capacity_msat = usage.effective_capacity.as_msat();
self.channel_liquidities
.get(scid)
.unwrap_or(&ChannelLiquidity::new(Duration::ZERO))
.as_directed(&source, &target, capacity_msat)
.penalty_msat(usage.amount_msat, usage.inflight_htlc_msat, score_params)
.saturating_add(anti_probing_penalty_msat)
.saturating_add(base_penalty_msat)
}
}
impl<G: Deref<Target = NetworkGraph<L>>, L: Deref> ScoreUpdate for ProbabilisticScorer<G, L> where L::Target: Logger {
fn payment_path_failed(&mut self, path: &Path, short_channel_id: u64, duration_since_epoch: Duration) {
let amount_msat = path.final_value_msat();
log_trace!(self.logger, "Scoring path through to SCID {} as having failed at {} msat", short_channel_id, amount_msat);
let network_graph = self.network_graph.read_only();
for (hop_idx, hop) in path.hops.iter().enumerate() {
let target = NodeId::from_pubkey(&hop.pubkey);
let channel_directed_from_source = network_graph.channels()
.get(&hop.short_channel_id)
.and_then(|channel| channel.as_directed_to(&target));
let at_failed_channel = hop.short_channel_id == short_channel_id;
if at_failed_channel && hop_idx == 0 {
log_warn!(self.logger, "Payment failed at the first hop - we do not attempt to learn channel info in such cases as we can directly observe local state.\n\tBecause we know the local state, we should generally not see failures here - this may be an indication that your channel peer on channel {} is broken and you may wish to close the channel.", hop.short_channel_id);
}
if let Some((channel, source)) = channel_directed_from_source {
let capacity_msat = channel.effective_capacity().as_msat();
if at_failed_channel {
self.channel_liquidities
.entry(hop.short_channel_id)
.or_insert_with(|| ChannelLiquidity::new(duration_since_epoch))
.as_directed_mut(source, &target, capacity_msat)
.failed_at_channel(amount_msat, duration_since_epoch,
format_args!("SCID {}, towards {:?}", hop.short_channel_id, target), &self.logger);
} else {
self.channel_liquidities
.entry(hop.short_channel_id)
.or_insert_with(|| ChannelLiquidity::new(duration_since_epoch))
.as_directed_mut(source, &target, capacity_msat)
.failed_downstream(amount_msat, duration_since_epoch,
format_args!("SCID {}, towards {:?}", hop.short_channel_id, target), &self.logger);
}
} else {
log_debug!(self.logger, "Not able to penalize channel with SCID {} as we do not have graph info for it (likely a route-hint last-hop).",
hop.short_channel_id);
}
if at_failed_channel { break; }
}
}
fn payment_path_successful(&mut self, path: &Path, duration_since_epoch: Duration) {
let amount_msat = path.final_value_msat();
log_trace!(self.logger, "Scoring path through SCID {} as having succeeded at {} msat.",
path.hops.split_last().map(|(hop, _)| hop.short_channel_id).unwrap_or(0), amount_msat);
let network_graph = self.network_graph.read_only();
for hop in &path.hops {
let target = NodeId::from_pubkey(&hop.pubkey);
let channel_directed_from_source = network_graph.channels()
.get(&hop.short_channel_id)
.and_then(|channel| channel.as_directed_to(&target));
if let Some((channel, source)) = channel_directed_from_source {
let capacity_msat = channel.effective_capacity().as_msat();
self.channel_liquidities
.entry(hop.short_channel_id)
.or_insert_with(|| ChannelLiquidity::new(duration_since_epoch))
.as_directed_mut(source, &target, capacity_msat)
.successful(amount_msat, duration_since_epoch,
format_args!("SCID {}, towards {:?}", hop.short_channel_id, target), &self.logger);
} else {
log_debug!(self.logger, "Not able to learn for channel with SCID {} as we do not have graph info for it (likely a route-hint last-hop).",
hop.short_channel_id);
}
}
}
fn probe_failed(&mut self, path: &Path, short_channel_id: u64, duration_since_epoch: Duration) {
self.payment_path_failed(path, short_channel_id, duration_since_epoch)
}
fn probe_successful(&mut self, path: &Path, duration_since_epoch: Duration) {
self.payment_path_failed(path, u64::max_value(), duration_since_epoch)
}
fn time_passed(&mut self, duration_since_epoch: Duration) {
let decay_params = self.decay_params;
self.channel_liquidities.retain(|_scid, liquidity| {
liquidity.min_liquidity_offset_msat =
liquidity.decayed_offset(liquidity.min_liquidity_offset_msat, duration_since_epoch, decay_params);
liquidity.max_liquidity_offset_msat =
liquidity.decayed_offset(liquidity.max_liquidity_offset_msat, duration_since_epoch, decay_params);
liquidity.last_updated = duration_since_epoch;
let elapsed_time =
duration_since_epoch.saturating_sub(liquidity.offset_history_last_updated);
if elapsed_time > decay_params.historical_no_updates_half_life {
let half_life = decay_params.historical_no_updates_half_life.as_secs_f64();
if half_life != 0.0 {
liquidity.liquidity_history.decay_buckets(elapsed_time.as_secs_f64() / half_life);
liquidity.offset_history_last_updated = duration_since_epoch;
}
}
liquidity.min_liquidity_offset_msat != 0 || liquidity.max_liquidity_offset_msat != 0 ||
liquidity.liquidity_history.has_datapoints()
});
}
}
#[cfg(c_bindings)]
impl<G: Deref<Target = NetworkGraph<L>>, L: Deref> Score for ProbabilisticScorer<G, L>
where L::Target: Logger {}
#[cfg(feature = "std")]
#[inline]
fn powf64(n: f64, exp: f64) -> f64 {
n.powf(exp)
}
#[cfg(not(feature = "std"))]
fn powf64(n: f64, exp: f64) -> f64 {
libm::powf(n as f32, exp as f32) as f64
}
mod bucketed_history {
use super::*;
#[repr(align(128))]
struct BucketStartPos([u16; 33]);
impl BucketStartPos {
const fn new() -> Self {
Self([
0, 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 3072, 4096, 6144, 8192, 10240, 12288,
13312, 14336, 15360, 15872, 16128, 16256, 16320, 16352, 16368, 16376, 16380, 16382, 16383, 16384,
])
}
}
impl core::ops::Index<usize> for BucketStartPos {
type Output = u16;
#[inline(always)]
fn index(&self, index: usize) -> &u16 { &self.0[index] }
}
const BUCKET_START_POS: BucketStartPos = BucketStartPos::new();
const LEGACY_TO_BUCKET_RANGE: [(u8, u8); 8] = [
(0, 12), (12, 14), (14, 15), (15, 16), (16, 17), (17, 18), (18, 20), (20, 32)
];
const POSITION_TICKS: u16 = 1 << 14;
fn pos_to_bucket(pos: u16) -> usize {
for bucket in 0..32 {
if pos < BUCKET_START_POS[bucket + 1] {
return bucket;
}
}
debug_assert!(false);
return 32;
}
#[cfg(test)]
#[test]
fn check_bucket_maps() {
const BUCKET_WIDTH_IN_16384S: [u16; 32] = [
1, 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 1024, 1024, 2048, 2048,
2048, 2048, 1024, 1024, 1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1, 1];
let mut min_size_iter = 0;
let mut legacy_bucket_iter = 0;
for (bucket, width) in BUCKET_WIDTH_IN_16384S.iter().enumerate() {
assert_eq!(BUCKET_START_POS[bucket], min_size_iter);
for i in 0..*width {
assert_eq!(pos_to_bucket(min_size_iter + i) as usize, bucket);
}
min_size_iter += *width;
if min_size_iter % (POSITION_TICKS / 8) == 0 {
assert_eq!(LEGACY_TO_BUCKET_RANGE[legacy_bucket_iter].1 as usize, bucket + 1);
if legacy_bucket_iter + 1 < 8 {
assert_eq!(LEGACY_TO_BUCKET_RANGE[legacy_bucket_iter + 1].0 as usize, bucket + 1);
}
legacy_bucket_iter += 1;
}
}
assert_eq!(BUCKET_START_POS[32], POSITION_TICKS);
assert_eq!(min_size_iter, POSITION_TICKS);
}
#[inline]
fn amount_to_pos(amount_msat: u64, capacity_msat: u64) -> u16 {
let pos = if amount_msat < u64::max_value() / (POSITION_TICKS as u64) {
(amount_msat * (POSITION_TICKS as u64) / capacity_msat.saturating_add(1))
.try_into().unwrap_or(POSITION_TICKS)
} else {
((amount_msat as u128) * (POSITION_TICKS as u128)
/ (capacity_msat as u128).saturating_add(1))
.try_into().unwrap_or(POSITION_TICKS)
};
#[cfg(test)]
debug_assert!(pos < POSITION_TICKS);
pos
}
pub(super) struct LegacyHistoricalBucketRangeTracker {
buckets: [u16; 8],
}
impl LegacyHistoricalBucketRangeTracker {
pub(crate) fn into_current(self) -> HistoricalBucketRangeTracker {
let mut buckets = [0; 32];
for (idx, legacy_bucket) in self.buckets.iter().enumerate() {
let mut new_val = *legacy_bucket;
let (start, end) = LEGACY_TO_BUCKET_RANGE[idx];
new_val /= (end - start) as u16;
for i in start..end {
buckets[i as usize] = new_val;
}
}
HistoricalBucketRangeTracker { buckets }
}
}
#[derive(Clone, Copy)]
pub(super) struct HistoricalBucketRangeTracker {
buckets: [u16; 32],
}
pub const BUCKET_FIXED_POINT_ONE: u16 = 32;
impl HistoricalBucketRangeTracker {
pub(super) fn new() -> Self { Self { buckets: [0; 32] } }
fn track_datapoint(&mut self, liquidity_offset_msat: u64, capacity_msat: u64) {
let pos: u16 = amount_to_pos(liquidity_offset_msat, capacity_msat);
if pos < POSITION_TICKS {
for e in self.buckets.iter_mut() {
*e = ((*e as u32) * 2047 / 2048) as u16;
}
let bucket = pos_to_bucket(pos);
self.buckets[bucket] = self.buckets[bucket].saturating_add(BUCKET_FIXED_POINT_ONE);
}
}
}
impl_writeable_tlv_based!(HistoricalBucketRangeTracker, { (0, buckets, required) });
impl_writeable_tlv_based!(LegacyHistoricalBucketRangeTracker, { (0, buckets, required) });
#[derive(Clone, Copy)]
#[repr(C)] pub(super) struct HistoricalLiquidityTracker {
total_valid_points_tracked: f64,
min_liquidity_offset_history: HistoricalBucketRangeTracker,
max_liquidity_offset_history: HistoricalBucketRangeTracker,
}
impl HistoricalLiquidityTracker {
pub(super) fn new() -> HistoricalLiquidityTracker {
HistoricalLiquidityTracker {
min_liquidity_offset_history: HistoricalBucketRangeTracker::new(),
max_liquidity_offset_history: HistoricalBucketRangeTracker::new(),
total_valid_points_tracked: 0.0,
}
}
pub(super) fn from_min_max(
min_liquidity_offset_history: HistoricalBucketRangeTracker,
max_liquidity_offset_history: HistoricalBucketRangeTracker,
) -> HistoricalLiquidityTracker {
let mut res = HistoricalLiquidityTracker {
min_liquidity_offset_history,
max_liquidity_offset_history,
total_valid_points_tracked: 0.0,
};
res.recalculate_valid_point_count();
res
}
pub(super) fn has_datapoints(&self) -> bool {
self.min_liquidity_offset_history.buckets != [0; 32] ||
self.max_liquidity_offset_history.buckets != [0; 32]
}
pub(super) fn decay_buckets(&mut self, half_lives: f64) {
let divisor = powf64(2048.0, half_lives) as u64;
for bucket in self.min_liquidity_offset_history.buckets.iter_mut() {
*bucket = ((*bucket as u64) * 1024 / divisor) as u16;
}
for bucket in self.max_liquidity_offset_history.buckets.iter_mut() {
*bucket = ((*bucket as u64) * 1024 / divisor) as u16;
}
self.recalculate_valid_point_count();
}
fn recalculate_valid_point_count(&mut self) {
let mut total_valid_points_tracked = 0;
for (min_idx, min_bucket) in self.min_liquidity_offset_history.buckets.iter().enumerate() {
for max_bucket in self.max_liquidity_offset_history.buckets.iter().take(32 - min_idx) {
let mut bucket_weight = (*min_bucket as u64) * (*max_bucket as u64);
bucket_weight *= bucket_weight;
total_valid_points_tracked += bucket_weight;
}
}
self.total_valid_points_tracked = total_valid_points_tracked as f64;
}
pub(super) fn writeable_min_offset_history(&self) -> &HistoricalBucketRangeTracker {
&self.min_liquidity_offset_history
}
pub(super) fn writeable_max_offset_history(&self) -> &HistoricalBucketRangeTracker {
&self.max_liquidity_offset_history
}
pub(super) fn as_directed<'a>(&'a self, source_less_than_target: bool)
-> DirectedHistoricalLiquidityTracker<&'a HistoricalLiquidityTracker> {
DirectedHistoricalLiquidityTracker { source_less_than_target, tracker: self }
}
pub(super) fn as_directed_mut<'a>(&'a mut self, source_less_than_target: bool)
-> DirectedHistoricalLiquidityTracker<&'a mut HistoricalLiquidityTracker> {
DirectedHistoricalLiquidityTracker { source_less_than_target, tracker: self }
}
}
pub(super) struct DirectedHistoricalLiquidityTracker<D: Deref<Target = HistoricalLiquidityTracker>> {
source_less_than_target: bool,
tracker: D,
}
impl<D: DerefMut<Target = HistoricalLiquidityTracker>> DirectedHistoricalLiquidityTracker<D> {
pub(super) fn track_datapoint(
&mut self, min_offset_msat: u64, max_offset_msat: u64, capacity_msat: u64,
) {
if self.source_less_than_target {
self.tracker.min_liquidity_offset_history.track_datapoint(min_offset_msat, capacity_msat);
self.tracker.max_liquidity_offset_history.track_datapoint(max_offset_msat, capacity_msat);
} else {
self.tracker.max_liquidity_offset_history.track_datapoint(min_offset_msat, capacity_msat);
self.tracker.min_liquidity_offset_history.track_datapoint(max_offset_msat, capacity_msat);
}
self.tracker.recalculate_valid_point_count();
}
}
impl<D: Deref<Target = HistoricalLiquidityTracker>> DirectedHistoricalLiquidityTracker<D> {
pub(super) fn min_liquidity_offset_history_buckets(&self) -> &[u16; 32] {
if self.source_less_than_target {
&self.tracker.min_liquidity_offset_history.buckets
} else {
&self.tracker.max_liquidity_offset_history.buckets
}
}
pub(super) fn max_liquidity_offset_history_buckets(&self) -> &[u16; 32] {
if self.source_less_than_target {
&self.tracker.max_liquidity_offset_history.buckets
} else {
&self.tracker.min_liquidity_offset_history.buckets
}
}
#[inline]
pub(super) fn calculate_success_probability_times_billion(
&self, params: &ProbabilisticScoringFeeParameters, total_inflight_amount_msat: u64,
capacity_msat: u64
) -> Option<u64> {
let payment_pos = amount_to_pos(total_inflight_amount_msat, capacity_msat);
if payment_pos >= POSITION_TICKS { return None; }
let min_liquidity_offset_history_buckets =
self.min_liquidity_offset_history_buckets();
let max_liquidity_offset_history_buckets =
self.max_liquidity_offset_history_buckets();
let total_valid_points_tracked = self.tracker.total_valid_points_tracked;
#[cfg(debug_assertions)] {
let mut actual_valid_points_tracked = 0;
for (min_idx, min_bucket) in min_liquidity_offset_history_buckets.iter().enumerate() {
for max_bucket in max_liquidity_offset_history_buckets.iter().take(32 - min_idx) {
let mut bucket_weight = (*min_bucket as u64) * (*max_bucket as u64);
bucket_weight *= bucket_weight;
actual_valid_points_tracked += bucket_weight;
}
}
assert_eq!(total_valid_points_tracked, actual_valid_points_tracked as f64);
}
const FULLY_DECAYED: f64 = BUCKET_FIXED_POINT_ONE as f64 * BUCKET_FIXED_POINT_ONE as f64 *
BUCKET_FIXED_POINT_ONE as f64 * BUCKET_FIXED_POINT_ONE as f64;
if total_valid_points_tracked < FULLY_DECAYED.into() {
return None;
}
let mut cumulative_success_prob = 0.0f64;
if min_liquidity_offset_history_buckets[0] != 0 {
let mut highest_max_bucket_with_points = 0;
let mut highest_max_bucket_with_full_points = None;
let mut total_weight = 0;
for (max_idx, max_bucket) in max_liquidity_offset_history_buckets.iter().enumerate() {
if *max_bucket >= BUCKET_FIXED_POINT_ONE {
highest_max_bucket_with_full_points = Some(cmp::max(highest_max_bucket_with_full_points.unwrap_or(0), max_idx));
}
if *max_bucket != 0 {
highest_max_bucket_with_points = cmp::max(highest_max_bucket_with_points, max_idx);
}
let bucket_weight = (*max_bucket as u64) * (min_liquidity_offset_history_buckets[0] as u64);
total_weight += bucket_weight * bucket_weight;
}
debug_assert!(total_weight as f64 <= total_valid_points_tracked);
let selected_max = highest_max_bucket_with_full_points.unwrap_or(highest_max_bucket_with_points);
let max_bucket_end_pos = BUCKET_START_POS[32 - selected_max] - 1;
if payment_pos < max_bucket_end_pos {
let (numerator, denominator) = success_probability_float(payment_pos as u64, 0,
max_bucket_end_pos as u64, POSITION_TICKS as u64 - 1, params, true);
let bucket_prob = total_weight as f64 / total_valid_points_tracked;
cumulative_success_prob += bucket_prob * numerator / denominator;
}
}
for (min_idx, min_bucket) in min_liquidity_offset_history_buckets.iter().enumerate().skip(1) {
let min_bucket_start_pos = BUCKET_START_POS[min_idx];
for (max_idx, max_bucket) in max_liquidity_offset_history_buckets.iter().enumerate().take(32 - min_idx) {
let max_bucket_end_pos = BUCKET_START_POS[32 - max_idx] - 1;
if payment_pos >= max_bucket_end_pos {
break;
}
let mut bucket_weight = (*min_bucket as u64) * (*max_bucket as u64);
bucket_weight *= bucket_weight;
debug_assert!(bucket_weight as f64 <= total_valid_points_tracked);
let bucket_prob = bucket_weight as f64 / total_valid_points_tracked;
if payment_pos < min_bucket_start_pos {
cumulative_success_prob += bucket_prob;
} else {
let (numerator, denominator) = success_probability_float(payment_pos as u64,
min_bucket_start_pos as u64, max_bucket_end_pos as u64,
POSITION_TICKS as u64 - 1, params, true);
cumulative_success_prob += bucket_prob * numerator / denominator;
}
}
}
Some((cumulative_success_prob * (1024.0 * 1024.0 * 1024.0)) as u64)
}
}
}
use bucketed_history::{LegacyHistoricalBucketRangeTracker, HistoricalBucketRangeTracker, DirectedHistoricalLiquidityTracker, HistoricalLiquidityTracker};
impl<G: Deref<Target = NetworkGraph<L>>, L: Deref> Writeable for ProbabilisticScorer<G, L> where L::Target: Logger {
#[inline]
fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> {
write_tlv_fields!(w, {
(0, self.channel_liquidities, required),
});
Ok(())
}
}
impl<G: Deref<Target = NetworkGraph<L>>, L: Deref>
ReadableArgs<(ProbabilisticScoringDecayParameters, G, L)> for ProbabilisticScorer<G, L> where L::Target: Logger {
#[inline]
fn read<R: Read>(
r: &mut R, args: (ProbabilisticScoringDecayParameters, G, L)
) -> Result<Self, DecodeError> {
let (decay_params, network_graph, logger) = args;
let mut channel_liquidities = new_hash_map();
read_tlv_fields!(r, {
(0, channel_liquidities, required),
});
Ok(Self {
decay_params,
network_graph,
logger,
channel_liquidities,
})
}
}
impl Writeable for ChannelLiquidity {
#[inline]
fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> {
write_tlv_fields!(w, {
(0, self.min_liquidity_offset_msat, required),
(2, self.max_liquidity_offset_msat, required),
(4, self.last_updated, required),
(5, self.liquidity_history.writeable_min_offset_history(), required),
(7, self.liquidity_history.writeable_max_offset_history(), required),
(9, self.offset_history_last_updated, required),
});
Ok(())
}
}
impl Readable for ChannelLiquidity {
#[inline]
fn read<R: Read>(r: &mut R) -> Result<Self, DecodeError> {
let mut min_liquidity_offset_msat = 0;
let mut max_liquidity_offset_msat = 0;
let mut legacy_min_liq_offset_history: Option<LegacyHistoricalBucketRangeTracker> = None;
let mut legacy_max_liq_offset_history: Option<LegacyHistoricalBucketRangeTracker> = None;
let mut min_liquidity_offset_history: Option<HistoricalBucketRangeTracker> = None;
let mut max_liquidity_offset_history: Option<HistoricalBucketRangeTracker> = None;
let mut last_updated = Duration::from_secs(0);
let mut offset_history_last_updated = None;
read_tlv_fields!(r, {
(0, min_liquidity_offset_msat, required),
(1, legacy_min_liq_offset_history, option),
(2, max_liquidity_offset_msat, required),
(3, legacy_max_liq_offset_history, option),
(4, last_updated, required),
(5, min_liquidity_offset_history, option),
(7, max_liquidity_offset_history, option),
(9, offset_history_last_updated, option),
});
if min_liquidity_offset_history.is_none() {
if let Some(legacy_buckets) = legacy_min_liq_offset_history {
min_liquidity_offset_history = Some(legacy_buckets.into_current());
} else {
min_liquidity_offset_history = Some(HistoricalBucketRangeTracker::new());
}
}
if max_liquidity_offset_history.is_none() {
if let Some(legacy_buckets) = legacy_max_liq_offset_history {
max_liquidity_offset_history = Some(legacy_buckets.into_current());
} else {
max_liquidity_offset_history = Some(HistoricalBucketRangeTracker::new());
}
}
Ok(Self {
min_liquidity_offset_msat,
max_liquidity_offset_msat,
liquidity_history: HistoricalLiquidityTracker::from_min_max(
min_liquidity_offset_history.unwrap(), max_liquidity_offset_history.unwrap()
),
last_updated,
offset_history_last_updated: offset_history_last_updated.unwrap_or(last_updated),
})
}
}
#[cfg(test)]
mod tests {
use super::{ChannelLiquidity, HistoricalLiquidityTracker, ProbabilisticScoringFeeParameters, ProbabilisticScoringDecayParameters, ProbabilisticScorer};
use crate::blinded_path::BlindedHop;
use crate::util::config::UserConfig;
use crate::ln::channelmanager;
use crate::ln::msgs::{ChannelAnnouncement, ChannelUpdate, UnsignedChannelAnnouncement, UnsignedChannelUpdate};
use crate::routing::gossip::{EffectiveCapacity, NetworkGraph, NodeId};
use crate::routing::router::{BlindedTail, Path, RouteHop, CandidateRouteHop, PublicHopCandidate};
use crate::routing::scoring::{ChannelUsage, ScoreLookUp, ScoreUpdate};
use crate::util::ser::{ReadableArgs, Writeable};
use crate::util::test_utils::{self, TestLogger};
use bitcoin::constants::ChainHash;
use bitcoin::hashes::Hash;
use bitcoin::hashes::sha256d::Hash as Sha256dHash;
use bitcoin::network::Network;
use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey};
use core::time::Duration;
use crate::io;
fn source_privkey() -> SecretKey {
SecretKey::from_slice(&[42; 32]).unwrap()
}
fn target_privkey() -> SecretKey {
SecretKey::from_slice(&[43; 32]).unwrap()
}
fn source_pubkey() -> PublicKey {
let secp_ctx = Secp256k1::new();
PublicKey::from_secret_key(&secp_ctx, &source_privkey())
}
fn target_pubkey() -> PublicKey {
let secp_ctx = Secp256k1::new();
PublicKey::from_secret_key(&secp_ctx, &target_privkey())
}
fn source_node_id() -> NodeId {
NodeId::from_pubkey(&source_pubkey())
}
fn target_node_id() -> NodeId {
NodeId::from_pubkey(&target_pubkey())
}
fn sender_privkey() -> SecretKey {
SecretKey::from_slice(&[41; 32]).unwrap()
}
fn recipient_privkey() -> SecretKey {
SecretKey::from_slice(&[45; 32]).unwrap()
}
fn sender_pubkey() -> PublicKey {
let secp_ctx = Secp256k1::new();
PublicKey::from_secret_key(&secp_ctx, &sender_privkey())
}
fn recipient_pubkey() -> PublicKey {
let secp_ctx = Secp256k1::new();
PublicKey::from_secret_key(&secp_ctx, &recipient_privkey())
}
fn recipient_node_id() -> NodeId {
NodeId::from_pubkey(&recipient_pubkey())
}
fn network_graph(logger: &TestLogger) -> NetworkGraph<&TestLogger> {
let mut network_graph = NetworkGraph::new(Network::Testnet, logger);
add_channel(&mut network_graph, 42, source_privkey(), target_privkey());
add_channel(&mut network_graph, 43, target_privkey(), recipient_privkey());
network_graph
}
fn add_channel(
network_graph: &mut NetworkGraph<&TestLogger>, short_channel_id: u64, node_1_key: SecretKey,
node_2_key: SecretKey
) {
let genesis_hash = ChainHash::using_genesis_block(Network::Testnet);
let node_1_secret = &SecretKey::from_slice(&[39; 32]).unwrap();
let node_2_secret = &SecretKey::from_slice(&[40; 32]).unwrap();
let secp_ctx = Secp256k1::new();
let unsigned_announcement = UnsignedChannelAnnouncement {
features: channelmanager::provided_channel_features(&UserConfig::default()),
chain_hash: genesis_hash,
short_channel_id,
node_id_1: NodeId::from_pubkey(&PublicKey::from_secret_key(&secp_ctx, &node_1_key)),
node_id_2: NodeId::from_pubkey(&PublicKey::from_secret_key(&secp_ctx, &node_2_key)),
bitcoin_key_1: NodeId::from_pubkey(&PublicKey::from_secret_key(&secp_ctx, &node_1_secret)),
bitcoin_key_2: NodeId::from_pubkey(&PublicKey::from_secret_key(&secp_ctx, &node_2_secret)),
excess_data: Vec::new(),
};
let msghash = hash_to_message!(&Sha256dHash::hash(&unsigned_announcement.encode()[..])[..]);
let signed_announcement = ChannelAnnouncement {
node_signature_1: secp_ctx.sign_ecdsa(&msghash, &node_1_key),
node_signature_2: secp_ctx.sign_ecdsa(&msghash, &node_2_key),
bitcoin_signature_1: secp_ctx.sign_ecdsa(&msghash, &node_1_secret),
bitcoin_signature_2: secp_ctx.sign_ecdsa(&msghash, &node_2_secret),
contents: unsigned_announcement,
};
let chain_source: Option<&crate::util::test_utils::TestChainSource> = None;
network_graph.update_channel_from_announcement(
&signed_announcement, &chain_source).unwrap();
update_channel(network_graph, short_channel_id, node_1_key, 0, 1_000, 100);
update_channel(network_graph, short_channel_id, node_2_key, 1, 0, 100);
}
fn update_channel(
network_graph: &mut NetworkGraph<&TestLogger>, short_channel_id: u64, node_key: SecretKey,
channel_flags: u8, htlc_maximum_msat: u64, timestamp: u32,
) {
let genesis_hash = ChainHash::using_genesis_block(Network::Testnet);
let secp_ctx = Secp256k1::new();
let unsigned_update = UnsignedChannelUpdate {
chain_hash: genesis_hash,
short_channel_id,
timestamp,
message_flags: 1, channel_flags,
cltv_expiry_delta: 18,
htlc_minimum_msat: 0,
htlc_maximum_msat,
fee_base_msat: 1,
fee_proportional_millionths: 0,
excess_data: Vec::new(),
};
let msghash = hash_to_message!(&Sha256dHash::hash(&unsigned_update.encode()[..])[..]);
let signed_update = ChannelUpdate {
signature: secp_ctx.sign_ecdsa(&msghash, &node_key),
contents: unsigned_update,
};
network_graph.update_channel(&signed_update).unwrap();
}
fn path_hop(pubkey: PublicKey, short_channel_id: u64, fee_msat: u64) -> RouteHop {
let config = UserConfig::default();
RouteHop {
pubkey,
node_features: channelmanager::provided_node_features(&config),
short_channel_id,
channel_features: channelmanager::provided_channel_features(&config),
fee_msat,
cltv_expiry_delta: 18,
maybe_announced_channel: true,
}
}
fn payment_path_for_amount(amount_msat: u64) -> Path {
Path {
hops: vec![
path_hop(source_pubkey(), 41, 1),
path_hop(target_pubkey(), 42, 2),
path_hop(recipient_pubkey(), 43, amount_msat),
], blinded_tail: None,
}
}
#[test]
fn liquidity_bounds_directed_from_lowest_node_id() {
let logger = TestLogger::new();
let last_updated = Duration::ZERO;
let offset_history_last_updated = Duration::ZERO;
let network_graph = network_graph(&logger);
let decay_params = ProbabilisticScoringDecayParameters::default();
let mut scorer = ProbabilisticScorer::new(decay_params, &network_graph, &logger)
.with_channel(42,
ChannelLiquidity {
min_liquidity_offset_msat: 700, max_liquidity_offset_msat: 100,
last_updated, offset_history_last_updated,
liquidity_history: HistoricalLiquidityTracker::new(),
})
.with_channel(43,
ChannelLiquidity {
min_liquidity_offset_msat: 700, max_liquidity_offset_msat: 100,
last_updated, offset_history_last_updated,
liquidity_history: HistoricalLiquidityTracker::new(),
});
let source = source_node_id();
let target = target_node_id();
let recipient = recipient_node_id();
assert!(source > target);
assert!(target < recipient);
let liquidity = scorer.channel_liquidities.get(&42).unwrap()
.as_directed(&source, &target, 1_000);
assert_eq!(liquidity.min_liquidity_msat(), 100);
assert_eq!(liquidity.max_liquidity_msat(), 300);
let liquidity = scorer.channel_liquidities.get(&42).unwrap()
.as_directed(&target, &source, 1_000);
assert_eq!(liquidity.min_liquidity_msat(), 700);
assert_eq!(liquidity.max_liquidity_msat(), 900);
scorer.channel_liquidities.get_mut(&42).unwrap()
.as_directed_mut(&source, &target, 1_000)
.set_min_liquidity_msat(200, Duration::ZERO);
let liquidity = scorer.channel_liquidities.get(&42).unwrap()
.as_directed(&source, &target, 1_000);
assert_eq!(liquidity.min_liquidity_msat(), 200);
assert_eq!(liquidity.max_liquidity_msat(), 300);
let liquidity = scorer.channel_liquidities.get(&42).unwrap()
.as_directed(&target, &source, 1_000);
assert_eq!(liquidity.min_liquidity_msat(), 700);
assert_eq!(liquidity.max_liquidity_msat(), 800);
let liquidity = scorer.channel_liquidities.get(&43).unwrap()
.as_directed(&target, &recipient, 1_000);
assert_eq!(liquidity.min_liquidity_msat(), 700);
assert_eq!(liquidity.max_liquidity_msat(), 900);
let liquidity = scorer.channel_liquidities.get(&43).unwrap()
.as_directed(&recipient, &target, 1_000);
assert_eq!(liquidity.min_liquidity_msat(), 100);
assert_eq!(liquidity.max_liquidity_msat(), 300);
scorer.channel_liquidities.get_mut(&43).unwrap()
.as_directed_mut(&target, &recipient, 1_000)
.set_max_liquidity_msat(200, Duration::ZERO);
let liquidity = scorer.channel_liquidities.get(&43).unwrap()
.as_directed(&target, &recipient, 1_000);
assert_eq!(liquidity.min_liquidity_msat(), 0);
assert_eq!(liquidity.max_liquidity_msat(), 200);
let liquidity = scorer.channel_liquidities.get(&43).unwrap()
.as_directed(&recipient, &target, 1_000);
assert_eq!(liquidity.min_liquidity_msat(), 800);
assert_eq!(liquidity.max_liquidity_msat(), 1000);
}
#[test]
fn resets_liquidity_upper_bound_when_crossed_by_lower_bound() {
let logger = TestLogger::new();
let last_updated = Duration::ZERO;
let offset_history_last_updated = Duration::ZERO;
let network_graph = network_graph(&logger);
let decay_params = ProbabilisticScoringDecayParameters::default();
let mut scorer = ProbabilisticScorer::new(decay_params, &network_graph, &logger)
.with_channel(42,
ChannelLiquidity {
min_liquidity_offset_msat: 200, max_liquidity_offset_msat: 400,
last_updated, offset_history_last_updated,
liquidity_history: HistoricalLiquidityTracker::new(),
});
let source = source_node_id();
let target = target_node_id();
assert!(source > target);
let liquidity = scorer.channel_liquidities.get(&42).unwrap()
.as_directed(&source, &target, 1_000);
assert_eq!(liquidity.min_liquidity_msat(), 400);
assert_eq!(liquidity.max_liquidity_msat(), 800);
let liquidity = scorer.channel_liquidities.get(&42).unwrap()
.as_directed(&target, &source, 1_000);
assert_eq!(liquidity.min_liquidity_msat(), 200);
assert_eq!(liquidity.max_liquidity_msat(), 600);
scorer.channel_liquidities.get_mut(&42).unwrap()
.as_directed_mut(&source, &target, 1_000)
.set_min_liquidity_msat(900, Duration::ZERO);
let liquidity = scorer.channel_liquidities.get(&42).unwrap()
.as_directed(&source, &target, 1_000);
assert_eq!(liquidity.min_liquidity_msat(), 900);
assert_eq!(liquidity.max_liquidity_msat(), 1_000);
let liquidity = scorer.channel_liquidities.get(&42).unwrap()
.as_directed(&target, &source, 1_000);
assert_eq!(liquidity.min_liquidity_msat(), 0);
assert_eq!(liquidity.max_liquidity_msat(), 100);
scorer.channel_liquidities.get_mut(&42).unwrap()
.as_directed_mut(&target, &source, 1_000)
.set_min_liquidity_msat(400, Duration::ZERO);
let liquidity = scorer.channel_liquidities.get(&42).unwrap()
.as_directed(&source, &target, 1_000);
assert_eq!(liquidity.min_liquidity_msat(), 0);
assert_eq!(liquidity.max_liquidity_msat(), 600);
let liquidity = scorer.channel_liquidities.get(&42).unwrap()
.as_directed(&target, &source, 1_000);
assert_eq!(liquidity.min_liquidity_msat(), 400);
assert_eq!(liquidity.max_liquidity_msat(), 1_000);
}
#[test]
fn resets_liquidity_lower_bound_when_crossed_by_upper_bound() {
let logger = TestLogger::new();
let last_updated = Duration::ZERO;
let offset_history_last_updated = Duration::ZERO;
let network_graph = network_graph(&logger);
let decay_params = ProbabilisticScoringDecayParameters::default();
let mut scorer = ProbabilisticScorer::new(decay_params, &network_graph, &logger)
.with_channel(42,
ChannelLiquidity {
min_liquidity_offset_msat: 200, max_liquidity_offset_msat: 400,
last_updated, offset_history_last_updated,
liquidity_history: HistoricalLiquidityTracker::new(),
});
let source = source_node_id();
let target = target_node_id();
assert!(source > target);
let liquidity = scorer.channel_liquidities.get(&42).unwrap()
.as_directed(&source, &target, 1_000);
assert_eq!(liquidity.min_liquidity_msat(), 400);
assert_eq!(liquidity.max_liquidity_msat(), 800);
let liquidity = scorer.channel_liquidities.get(&42).unwrap()
.as_directed(&target, &source, 1_000);
assert_eq!(liquidity.min_liquidity_msat(), 200);
assert_eq!(liquidity.max_liquidity_msat(), 600);
scorer.channel_liquidities.get_mut(&42).unwrap()
.as_directed_mut(&source, &target, 1_000)
.set_max_liquidity_msat(300, Duration::ZERO);
let liquidity = scorer.channel_liquidities.get(&42).unwrap()
.as_directed(&source, &target, 1_000);
assert_eq!(liquidity.min_liquidity_msat(), 0);
assert_eq!(liquidity.max_liquidity_msat(), 300);
let liquidity = scorer.channel_liquidities.get(&42).unwrap()
.as_directed(&target, &source, 1_000);
assert_eq!(liquidity.min_liquidity_msat(), 700);
assert_eq!(liquidity.max_liquidity_msat(), 1_000);
scorer.channel_liquidities.get_mut(&42).unwrap()
.as_directed_mut(&target, &source, 1_000)
.set_max_liquidity_msat(600, Duration::ZERO);
let liquidity = scorer.channel_liquidities.get(&42).unwrap()
.as_directed(&source, &target, 1_000);
assert_eq!(liquidity.min_liquidity_msat(), 400);
assert_eq!(liquidity.max_liquidity_msat(), 1_000);
let liquidity = scorer.channel_liquidities.get(&42).unwrap()
.as_directed(&target, &source, 1_000);
assert_eq!(liquidity.min_liquidity_msat(), 0);
assert_eq!(liquidity.max_liquidity_msat(), 600);
}
#[test]
fn increased_penalty_nearing_liquidity_upper_bound() {
let logger = TestLogger::new();
let network_graph = network_graph(&logger);
let params = ProbabilisticScoringFeeParameters {
liquidity_penalty_multiplier_msat: 1_000,
..ProbabilisticScoringFeeParameters::zero_penalty()
};
let decay_params = ProbabilisticScoringDecayParameters::default();
let scorer = ProbabilisticScorer::new(decay_params, &network_graph, &logger);
let source = source_node_id();
let usage = ChannelUsage {
amount_msat: 1_024,
inflight_htlc_msat: 0,
effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024_000, htlc_maximum_msat: 1_000 },
};
let network_graph = network_graph.read_only();
let channel = network_graph.channel(42).unwrap();
let (info, _) = channel.as_directed_from(&source).unwrap();
let candidate = CandidateRouteHop::PublicHop(PublicHopCandidate {
info,
short_channel_id: 42,
});
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 0);
let usage = ChannelUsage { amount_msat: 10_240, ..usage };
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 0);
let usage = ChannelUsage { amount_msat: 102_400, ..usage };
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 47);
let usage = ChannelUsage { amount_msat: 1_023_999, ..usage };
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 2_000);
let usage = ChannelUsage {
amount_msat: 128,
inflight_htlc_msat: 0,
effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024, htlc_maximum_msat: 1_000 },
};
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 58);
let usage = ChannelUsage { amount_msat: 256, ..usage };
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 125);
let usage = ChannelUsage { amount_msat: 374, ..usage };
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 198);
let usage = ChannelUsage { amount_msat: 512, ..usage };
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 300);
let usage = ChannelUsage { amount_msat: 640, ..usage };
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 425);
let usage = ChannelUsage { amount_msat: 768, ..usage };
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 602);
let usage = ChannelUsage { amount_msat: 896, ..usage };
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 902);
}
#[test]
fn constant_penalty_outside_liquidity_bounds() {
let logger = TestLogger::new();
let last_updated = Duration::ZERO;
let offset_history_last_updated = Duration::ZERO;
let network_graph = network_graph(&logger);
let params = ProbabilisticScoringFeeParameters {
liquidity_penalty_multiplier_msat: 1_000,
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringFeeParameters::zero_penalty()
};
let decay_params = ProbabilisticScoringDecayParameters {
..ProbabilisticScoringDecayParameters::zero_penalty()
};
let scorer = ProbabilisticScorer::new(decay_params, &network_graph, &logger)
.with_channel(42,
ChannelLiquidity {
min_liquidity_offset_msat: 40, max_liquidity_offset_msat: 40,
last_updated, offset_history_last_updated,
liquidity_history: HistoricalLiquidityTracker::new(),
});
let source = source_node_id();
let usage = ChannelUsage {
amount_msat: 39,
inflight_htlc_msat: 0,
effective_capacity: EffectiveCapacity::Total { capacity_msat: 100, htlc_maximum_msat: 1_000 },
};
let channel = network_graph.read_only().channel(42).unwrap().to_owned();
let (info, _) = channel.as_directed_from(&source).unwrap();
let candidate = CandidateRouteHop::PublicHop(PublicHopCandidate {
info,
short_channel_id: 42,
});
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 0);
let usage = ChannelUsage { amount_msat: 50, ..usage };
assert_ne!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 0);
assert_ne!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), u64::max_value());
let usage = ChannelUsage { amount_msat: 61, ..usage };
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), u64::max_value());
}
#[test]
fn does_not_further_penalize_own_channel() {
let logger = TestLogger::new();
let network_graph = network_graph(&logger);
let params = ProbabilisticScoringFeeParameters {
liquidity_penalty_multiplier_msat: 1_000,
..ProbabilisticScoringFeeParameters::zero_penalty()
};
let mut scorer = ProbabilisticScorer::new(ProbabilisticScoringDecayParameters::default(), &network_graph, &logger);
let source = source_node_id();
let usage = ChannelUsage {
amount_msat: 500,
inflight_htlc_msat: 0,
effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_000, htlc_maximum_msat: 1_000 },
};
let failed_path = payment_path_for_amount(500);
let successful_path = payment_path_for_amount(200);
let channel = &network_graph.read_only().channel(42).unwrap().to_owned();
let (info, _) = channel.as_directed_from(&source).unwrap();
let candidate = CandidateRouteHop::PublicHop(PublicHopCandidate {
info,
short_channel_id: 41,
});
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 301);
scorer.payment_path_failed(&failed_path, 41, Duration::ZERO);
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 301);
scorer.payment_path_successful(&successful_path, Duration::ZERO);
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 301);
}
#[test]
fn sets_liquidity_lower_bound_on_downstream_failure() {
let logger = TestLogger::new();
let network_graph = network_graph(&logger);
let params = ProbabilisticScoringFeeParameters {
liquidity_penalty_multiplier_msat: 1_000,
..ProbabilisticScoringFeeParameters::zero_penalty()
};
let mut scorer = ProbabilisticScorer::new(ProbabilisticScoringDecayParameters::default(), &network_graph, &logger);
let source = source_node_id();
let path = payment_path_for_amount(500);
let usage = ChannelUsage {
amount_msat: 250,
inflight_htlc_msat: 0,
effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_000, htlc_maximum_msat: 1_000 },
};
let channel = network_graph.read_only().channel(42).unwrap().to_owned();
let (info, _) = channel.as_directed_from(&source).unwrap();
let candidate = CandidateRouteHop::PublicHop(PublicHopCandidate {
info,
short_channel_id: 42,
});
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 128);
let usage = ChannelUsage { amount_msat: 500, ..usage };
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 301);
let usage = ChannelUsage { amount_msat: 750, ..usage };
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 602);
scorer.payment_path_failed(&path, 43, Duration::ZERO);
let usage = ChannelUsage { amount_msat: 250, ..usage };
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 0);
let usage = ChannelUsage { amount_msat: 500, ..usage };
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 0);
let usage = ChannelUsage { amount_msat: 750, ..usage };
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 300);
}
#[test]
fn sets_liquidity_upper_bound_on_failure() {
let logger = TestLogger::new();
let network_graph = network_graph(&logger);
let params = ProbabilisticScoringFeeParameters {
liquidity_penalty_multiplier_msat: 1_000,
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringFeeParameters::zero_penalty()
};
let mut scorer = ProbabilisticScorer::new(ProbabilisticScoringDecayParameters::default(), &network_graph, &logger);
let source = source_node_id();
let path = payment_path_for_amount(500);
let usage = ChannelUsage {
amount_msat: 250,
inflight_htlc_msat: 0,
effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_000, htlc_maximum_msat: 1_000 },
};
let channel = network_graph.read_only().channel(42).unwrap().to_owned();
let (info, _) = channel.as_directed_from(&source).unwrap();
let candidate = CandidateRouteHop::PublicHop(PublicHopCandidate {
info,
short_channel_id: 42,
});
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 128);
let usage = ChannelUsage { amount_msat: 500, ..usage };
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 301);
let usage = ChannelUsage { amount_msat: 750, ..usage };
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 602);
scorer.payment_path_failed(&path, 42, Duration::ZERO);
let usage = ChannelUsage { amount_msat: 250, ..usage };
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 300);
let usage = ChannelUsage { amount_msat: 500, ..usage };
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), u64::max_value());
let usage = ChannelUsage { amount_msat: 750, ..usage };
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), u64::max_value());
}
#[test]
fn ignores_channels_after_removed_failed_channel() {
let secp_ctx = Secp256k1::new();
let logger = TestLogger::new();
let mut network_graph = NetworkGraph::new(Network::Testnet, &logger);
let secret_a = SecretKey::from_slice(&[42; 32]).unwrap();
let secret_b = SecretKey::from_slice(&[43; 32]).unwrap();
let secret_c = SecretKey::from_slice(&[44; 32]).unwrap();
let secret_d = SecretKey::from_slice(&[45; 32]).unwrap();
add_channel(&mut network_graph, 42, secret_a, secret_b);
add_channel(&mut network_graph, 44, secret_c, secret_d);
let pub_a = PublicKey::from_secret_key(&secp_ctx, &secret_a);
let pub_b = PublicKey::from_secret_key(&secp_ctx, &secret_b);
let pub_c = PublicKey::from_secret_key(&secp_ctx, &secret_c);
let pub_d = PublicKey::from_secret_key(&secp_ctx, &secret_d);
let path = vec![
path_hop(pub_b, 42, 1),
path_hop(pub_c, 43, 2),
path_hop(pub_d, 44, 100),
];
let node_a = NodeId::from_pubkey(&pub_a);
let node_b = NodeId::from_pubkey(&pub_b);
let node_c = NodeId::from_pubkey(&pub_c);
let params = ProbabilisticScoringFeeParameters {
liquidity_penalty_multiplier_msat: 1_000,
..ProbabilisticScoringFeeParameters::zero_penalty()
};
let mut scorer = ProbabilisticScorer::new(ProbabilisticScoringDecayParameters::default(), &network_graph, &logger);
let usage = ChannelUsage {
amount_msat: 250,
inflight_htlc_msat: 0,
effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_000, htlc_maximum_msat: 1_000 },
};
let channel = network_graph.read_only().channel(42).unwrap().to_owned();
let (info, _) = channel.as_directed_from(&node_a).unwrap();
let candidate = CandidateRouteHop::PublicHop(PublicHopCandidate {
info,
short_channel_id: 42,
});
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 128);
let channel = network_graph.read_only().channel(42).unwrap().to_owned();
let (info, _) = channel.as_directed_from(&node_b).unwrap();
let candidate = CandidateRouteHop::PublicHop(PublicHopCandidate {
info,
short_channel_id: 43,
});
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 128);
let channel = network_graph.read_only().channel(44).unwrap().to_owned();
let (info, _) = channel.as_directed_from(&node_c).unwrap();
let candidate = CandidateRouteHop::PublicHop(PublicHopCandidate {
info,
short_channel_id: 44,
});
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 128);
scorer.payment_path_failed(&Path { hops: path, blinded_tail: None }, 43, Duration::ZERO);
let channel = network_graph.read_only().channel(42).unwrap().to_owned();
let (info, _) = channel.as_directed_from(&node_a).unwrap();
let candidate = CandidateRouteHop::PublicHop(PublicHopCandidate {
info,
short_channel_id: 42,
});
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 80);
let channel = network_graph.read_only().channel(42).unwrap().to_owned();
let (info, _) = channel.as_directed_from(&node_b).unwrap();
let candidate = CandidateRouteHop::PublicHop(PublicHopCandidate {
info,
short_channel_id: 43,
});
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 128);
let channel = network_graph.read_only().channel(44).unwrap().to_owned();
let (info, _) = channel.as_directed_from(&node_c).unwrap();
let candidate = CandidateRouteHop::PublicHop(PublicHopCandidate {
info,
short_channel_id: 44,
});
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 128);
}
#[test]
fn reduces_liquidity_upper_bound_along_path_on_success() {
let logger = TestLogger::new();
let network_graph = network_graph(&logger);
let params = ProbabilisticScoringFeeParameters {
liquidity_penalty_multiplier_msat: 1_000,
..ProbabilisticScoringFeeParameters::zero_penalty()
};
let mut scorer = ProbabilisticScorer::new(ProbabilisticScoringDecayParameters::default(), &network_graph, &logger);
let source = source_node_id();
let usage = ChannelUsage {
amount_msat: 250,
inflight_htlc_msat: 0,
effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_000, htlc_maximum_msat: 1_000 },
};
let network_graph = network_graph.read_only().channels().clone();
let channel_42 = network_graph.get(&42).unwrap();
let channel_43 = network_graph.get(&43).unwrap();
let (info, _) = channel_42.as_directed_from(&source).unwrap();
let candidate_41 = CandidateRouteHop::PublicHop(PublicHopCandidate {
info,
short_channel_id: 41,
});
let (info, target) = channel_42.as_directed_from(&source).unwrap();
let candidate_42 = CandidateRouteHop::PublicHop(PublicHopCandidate {
info,
short_channel_id: 42,
});
let (info, _) = channel_43.as_directed_from(&target).unwrap();
let candidate_43 = CandidateRouteHop::PublicHop(PublicHopCandidate {
info,
short_channel_id: 43,
});
assert_eq!(scorer.channel_penalty_msat(&candidate_41, usage, ¶ms), 128);
assert_eq!(scorer.channel_penalty_msat(&candidate_42, usage, ¶ms), 128);
assert_eq!(scorer.channel_penalty_msat(&candidate_43, usage, ¶ms), 128);
scorer.payment_path_successful(&payment_path_for_amount(500), Duration::ZERO);
assert_eq!(scorer.channel_penalty_msat(&candidate_41, usage, ¶ms), 128);
assert_eq!(scorer.channel_penalty_msat(&candidate_42, usage, ¶ms), 300);
assert_eq!(scorer.channel_penalty_msat(&candidate_43, usage, ¶ms), 300);
}
#[test]
fn decays_liquidity_bounds_over_time() {
let logger = TestLogger::new();
let network_graph = network_graph(&logger);
let params = ProbabilisticScoringFeeParameters {
liquidity_penalty_multiplier_msat: 1_000,
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringFeeParameters::zero_penalty()
};
let decay_params = ProbabilisticScoringDecayParameters {
liquidity_offset_half_life: Duration::from_secs(10),
..ProbabilisticScoringDecayParameters::zero_penalty()
};
let mut scorer = ProbabilisticScorer::new(decay_params, &network_graph, &logger);
let source = source_node_id();
let usage = ChannelUsage {
amount_msat: 0,
inflight_htlc_msat: 0,
effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024, htlc_maximum_msat: 1_024 },
};
let channel = network_graph.read_only().channel(42).unwrap().to_owned();
let (info, _) = channel.as_directed_from(&source).unwrap();
let candidate = CandidateRouteHop::PublicHop(PublicHopCandidate {
info,
short_channel_id: 42,
});
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 0);
let usage = ChannelUsage { amount_msat: 1_023, ..usage };
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 2_000);
scorer.payment_path_failed(&payment_path_for_amount(768), 42, Duration::ZERO);
scorer.payment_path_failed(&payment_path_for_amount(128), 43, Duration::ZERO);
let usage = ChannelUsage { amount_msat: 128, ..usage };
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 0);
let usage = ChannelUsage { amount_msat: 256, ..usage };
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 93);
let usage = ChannelUsage { amount_msat: 768, ..usage };
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 1_479);
let usage = ChannelUsage { amount_msat: 896, ..usage };
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), u64::max_value());
scorer.time_passed(Duration::from_secs(5));
let usage = ChannelUsage { amount_msat: 128, ..usage };
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 22);
let usage = ChannelUsage { amount_msat: 256, ..usage };
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 106);
let usage = ChannelUsage { amount_msat: 768, ..usage };
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 921);
let usage = ChannelUsage { amount_msat: 896, ..usage };
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), u64::max_value());
scorer.time_passed(Duration::from_secs(10));
let usage = ChannelUsage { amount_msat: 64, ..usage };
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 0);
let usage = ChannelUsage { amount_msat: 128, ..usage };
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 34);
let usage = ChannelUsage { amount_msat: 896, ..usage };
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 1_970);
let usage = ChannelUsage { amount_msat: 960, ..usage };
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), u64::max_value());
scorer.time_passed(Duration::from_secs(10 * 8));
let usage = ChannelUsage { amount_msat: 0, ..usage };
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 0);
let usage = ChannelUsage { amount_msat: 1, ..usage };
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 0);
let usage = ChannelUsage { amount_msat: 1_023, ..usage };
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 2_000);
let usage = ChannelUsage { amount_msat: 1_024, ..usage };
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), u64::max_value());
scorer.time_passed(Duration::from_secs(10 * 9));
let usage = ChannelUsage { amount_msat: 0, ..usage };
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 0);
let usage = ChannelUsage { amount_msat: 1_024, ..usage };
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), u64::max_value());
scorer.time_passed(Duration::from_secs(10 * 10));
let usage = ChannelUsage { amount_msat: 0, ..usage };
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 0);
let usage = ChannelUsage { amount_msat: 1_024, ..usage };
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), u64::max_value());
}
#[test]
fn restricts_liquidity_bounds_after_decay() {
let logger = TestLogger::new();
let network_graph = network_graph(&logger);
let params = ProbabilisticScoringFeeParameters {
liquidity_penalty_multiplier_msat: 1_000,
..ProbabilisticScoringFeeParameters::zero_penalty()
};
let decay_params = ProbabilisticScoringDecayParameters {
liquidity_offset_half_life: Duration::from_secs(10),
..ProbabilisticScoringDecayParameters::default()
};
let mut scorer = ProbabilisticScorer::new(decay_params, &network_graph, &logger);
let source = source_node_id();
let usage = ChannelUsage {
amount_msat: 512,
inflight_htlc_msat: 0,
effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024, htlc_maximum_msat: 1_000 },
};
let channel = network_graph.read_only().channel(42).unwrap().to_owned();
let (info, _) = channel.as_directed_from(&source).unwrap();
let candidate = CandidateRouteHop::PublicHop(PublicHopCandidate {
info,
short_channel_id: 42,
});
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 300);
scorer.payment_path_failed(&payment_path_for_amount(768), 42, Duration::ZERO);
scorer.payment_path_failed(&payment_path_for_amount(256), 43, Duration::ZERO);
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 281);
scorer.time_passed(Duration::from_secs(10));
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 291);
scorer.payment_path_successful(&payment_path_for_amount(64), Duration::from_secs(10));
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 331);
scorer.payment_path_failed(&payment_path_for_amount(256), 43, Duration::from_secs(10));
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 245);
scorer.time_passed(Duration::from_secs(20));
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 280);
}
#[test]
fn restores_persisted_liquidity_bounds() {
let logger = TestLogger::new();
let network_graph = network_graph(&logger);
let params = ProbabilisticScoringFeeParameters {
liquidity_penalty_multiplier_msat: 1_000,
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringFeeParameters::zero_penalty()
};
let decay_params = ProbabilisticScoringDecayParameters {
liquidity_offset_half_life: Duration::from_secs(10),
..ProbabilisticScoringDecayParameters::default()
};
let mut scorer = ProbabilisticScorer::new(decay_params, &network_graph, &logger);
let source = source_node_id();
let usage = ChannelUsage {
amount_msat: 500,
inflight_htlc_msat: 0,
effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_000, htlc_maximum_msat: 1_000 },
};
scorer.payment_path_failed(&payment_path_for_amount(500), 42, Duration::ZERO);
let channel = network_graph.read_only().channel(42).unwrap().to_owned();
let (info, _) = channel.as_directed_from(&source).unwrap();
let candidate = CandidateRouteHop::PublicHop(PublicHopCandidate {
info,
short_channel_id: 42,
});
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), u64::max_value());
scorer.time_passed(Duration::from_secs(10));
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 473);
scorer.payment_path_failed(&payment_path_for_amount(250), 43, Duration::from_secs(10));
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 300);
let mut serialized_scorer = Vec::new();
scorer.write(&mut serialized_scorer).unwrap();
let mut serialized_scorer = io::Cursor::new(&serialized_scorer);
let deserialized_scorer =
<ProbabilisticScorer<_, _>>::read(&mut serialized_scorer, (decay_params, &network_graph, &logger)).unwrap();
assert_eq!(deserialized_scorer.channel_penalty_msat(&candidate, usage, ¶ms), 300);
}
fn do_decays_persisted_liquidity_bounds(decay_before_reload: bool) {
let logger = TestLogger::new();
let network_graph = network_graph(&logger);
let params = ProbabilisticScoringFeeParameters {
liquidity_penalty_multiplier_msat: 1_000,
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringFeeParameters::zero_penalty()
};
let decay_params = ProbabilisticScoringDecayParameters {
liquidity_offset_half_life: Duration::from_secs(10),
..ProbabilisticScoringDecayParameters::zero_penalty()
};
let mut scorer = ProbabilisticScorer::new(decay_params, &network_graph, &logger);
let source = source_node_id();
let usage = ChannelUsage {
amount_msat: 500,
inflight_htlc_msat: 0,
effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_000, htlc_maximum_msat: 1_000 },
};
scorer.payment_path_failed(&payment_path_for_amount(500), 42, Duration::ZERO);
let channel = network_graph.read_only().channel(42).unwrap().to_owned();
let (info, _) = channel.as_directed_from(&source).unwrap();
let candidate = CandidateRouteHop::PublicHop(PublicHopCandidate {
info,
short_channel_id: 42,
});
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), u64::max_value());
if decay_before_reload {
scorer.time_passed(Duration::from_secs(10));
}
let mut serialized_scorer = Vec::new();
scorer.write(&mut serialized_scorer).unwrap();
let mut serialized_scorer = io::Cursor::new(&serialized_scorer);
let mut deserialized_scorer =
<ProbabilisticScorer<_, _>>::read(&mut serialized_scorer, (decay_params, &network_graph, &logger)).unwrap();
if !decay_before_reload {
scorer.time_passed(Duration::from_secs(10));
deserialized_scorer.time_passed(Duration::from_secs(10));
}
assert_eq!(deserialized_scorer.channel_penalty_msat(&candidate, usage, ¶ms), 473);
scorer.payment_path_failed(&payment_path_for_amount(250), 43, Duration::from_secs(10));
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 300);
deserialized_scorer.time_passed(Duration::from_secs(20));
assert_eq!(deserialized_scorer.channel_penalty_msat(&candidate, usage, ¶ms), 370);
}
#[test]
fn decays_persisted_liquidity_bounds() {
do_decays_persisted_liquidity_bounds(false);
do_decays_persisted_liquidity_bounds(true);
}
#[test]
fn scores_realistic_payments() {
let logger = TestLogger::new();
let network_graph = network_graph(&logger);
let params = ProbabilisticScoringFeeParameters::default();
let scorer = ProbabilisticScorer::new(ProbabilisticScoringDecayParameters::default(), &network_graph, &logger);
let source = source_node_id();
let usage = ChannelUsage {
amount_msat: 100_000_000,
inflight_htlc_msat: 0,
effective_capacity: EffectiveCapacity::Total { capacity_msat: 950_000_000, htlc_maximum_msat: 1_000 },
};
let channel = network_graph.read_only().channel(42).unwrap().to_owned();
let (info, _) = channel.as_directed_from(&source).unwrap();
let candidate = CandidateRouteHop::PublicHop(PublicHopCandidate {
info,
short_channel_id: 42,
});
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 42_252);
let usage = ChannelUsage {
effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_950_000_000, htlc_maximum_msat: 1_000 }, ..usage
};
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 36_005);
let usage = ChannelUsage {
effective_capacity: EffectiveCapacity::Total { capacity_msat: 2_950_000_000, htlc_maximum_msat: 1_000 }, ..usage
};
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 32_851);
let usage = ChannelUsage {
effective_capacity: EffectiveCapacity::Total { capacity_msat: 3_950_000_000, htlc_maximum_msat: 1_000 }, ..usage
};
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 30_832);
let usage = ChannelUsage {
effective_capacity: EffectiveCapacity::Total { capacity_msat: 4_950_000_000, htlc_maximum_msat: 1_000 }, ..usage
};
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 29_886);
let usage = ChannelUsage {
effective_capacity: EffectiveCapacity::Total { capacity_msat: 5_950_000_000, htlc_maximum_msat: 1_000 }, ..usage
};
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 28_939);
let usage = ChannelUsage {
effective_capacity: EffectiveCapacity::Total { capacity_msat: 6_950_000_000, htlc_maximum_msat: 1_000 }, ..usage
};
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 28_435);
let usage = ChannelUsage {
effective_capacity: EffectiveCapacity::Total { capacity_msat: 7_450_000_000, htlc_maximum_msat: 1_000 }, ..usage
};
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 27_993);
let usage = ChannelUsage {
effective_capacity: EffectiveCapacity::Total { capacity_msat: 7_950_000_000, htlc_maximum_msat: 1_000 }, ..usage
};
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 27_993);
let usage = ChannelUsage {
effective_capacity: EffectiveCapacity::Total { capacity_msat: 8_950_000_000, htlc_maximum_msat: 1_000 }, ..usage
};
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 27_488);
let usage = ChannelUsage {
effective_capacity: EffectiveCapacity::Total { capacity_msat: 9_950_000_000, htlc_maximum_msat: 1_000 }, ..usage
};
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 27_047);
}
#[test]
fn adds_base_penalty_to_liquidity_penalty() {
let logger = TestLogger::new();
let network_graph = network_graph(&logger);
let source = source_node_id();
let usage = ChannelUsage {
amount_msat: 128,
inflight_htlc_msat: 0,
effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024, htlc_maximum_msat: 1_000 },
};
let params = ProbabilisticScoringFeeParameters {
liquidity_penalty_multiplier_msat: 1_000,
..ProbabilisticScoringFeeParameters::zero_penalty()
};
let scorer = ProbabilisticScorer::new(ProbabilisticScoringDecayParameters::default(), &network_graph, &logger);
let channel = network_graph.read_only().channel(42).unwrap().to_owned();
let (info, _) = channel.as_directed_from(&source).unwrap();
let candidate = CandidateRouteHop::PublicHop(PublicHopCandidate {
info,
short_channel_id: 42,
});
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 58);
let params = ProbabilisticScoringFeeParameters {
base_penalty_msat: 500, liquidity_penalty_multiplier_msat: 1_000,
anti_probing_penalty_msat: 0, ..ProbabilisticScoringFeeParameters::zero_penalty()
};
let scorer = ProbabilisticScorer::new(ProbabilisticScoringDecayParameters::default(), &network_graph, &logger);
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 558);
let params = ProbabilisticScoringFeeParameters {
base_penalty_msat: 500, liquidity_penalty_multiplier_msat: 1_000,
base_penalty_amount_multiplier_msat: (1 << 30),
anti_probing_penalty_msat: 0, ..ProbabilisticScoringFeeParameters::zero_penalty()
};
let scorer = ProbabilisticScorer::new(ProbabilisticScoringDecayParameters::default(), &network_graph, &logger);
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 558 + 128);
}
#[test]
fn adds_amount_penalty_to_liquidity_penalty() {
let logger = TestLogger::new();
let network_graph = network_graph(&logger);
let source = source_node_id();
let usage = ChannelUsage {
amount_msat: 512_000,
inflight_htlc_msat: 0,
effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024_000, htlc_maximum_msat: 1_000 },
};
let params = ProbabilisticScoringFeeParameters {
liquidity_penalty_multiplier_msat: 1_000,
liquidity_penalty_amount_multiplier_msat: 0,
..ProbabilisticScoringFeeParameters::zero_penalty()
};
let scorer = ProbabilisticScorer::new(ProbabilisticScoringDecayParameters::default(), &network_graph, &logger);
let channel = network_graph.read_only().channel(42).unwrap().to_owned();
let (info, _) = channel.as_directed_from(&source).unwrap();
let candidate = CandidateRouteHop::PublicHop(PublicHopCandidate {
info,
short_channel_id: 42,
});
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 300);
let params = ProbabilisticScoringFeeParameters {
liquidity_penalty_multiplier_msat: 1_000,
liquidity_penalty_amount_multiplier_msat: 256,
..ProbabilisticScoringFeeParameters::zero_penalty()
};
let scorer = ProbabilisticScorer::new(ProbabilisticScoringDecayParameters::default(), &network_graph, &logger);
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 337);
}
#[test]
fn calculates_log10_without_overflowing_u64_max_value() {
let logger = TestLogger::new();
let network_graph = network_graph(&logger);
let source = source_node_id();
let usage = ChannelUsage {
amount_msat: u64::max_value(),
inflight_htlc_msat: 0,
effective_capacity: EffectiveCapacity::Infinite,
};
let params = ProbabilisticScoringFeeParameters {
liquidity_penalty_multiplier_msat: 40_000,
..ProbabilisticScoringFeeParameters::zero_penalty()
};
let decay_params = ProbabilisticScoringDecayParameters::zero_penalty();
let channel = network_graph.read_only().channel(42).unwrap().to_owned();
let (info, _) = channel.as_directed_from(&source).unwrap();
let candidate = CandidateRouteHop::PublicHop(PublicHopCandidate {
info,
short_channel_id: 42,
});
let scorer = ProbabilisticScorer::new(decay_params, &network_graph, &logger);
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 80_000);
}
#[test]
fn accounts_for_inflight_htlc_usage() {
let logger = TestLogger::new();
let network_graph = network_graph(&logger);
let params = ProbabilisticScoringFeeParameters {
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringFeeParameters::zero_penalty()
};
let scorer = ProbabilisticScorer::new(ProbabilisticScoringDecayParameters::default(), &network_graph, &logger);
let source = source_node_id();
let usage = ChannelUsage {
amount_msat: 750,
inflight_htlc_msat: 0,
effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_000, htlc_maximum_msat: 1_000 },
};
let network_graph = network_graph.read_only();
let channel = network_graph.channel(42).unwrap();
let (info, _) = channel.as_directed_from(&source).unwrap();
let candidate = CandidateRouteHop::PublicHop(PublicHopCandidate {
info,
short_channel_id: 42,
});
assert_ne!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), u64::max_value());
let usage = ChannelUsage { inflight_htlc_msat: 251, ..usage };
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), u64::max_value());
}
#[test]
fn removes_uncertainity_when_exact_liquidity_known() {
let logger = TestLogger::new();
let network_graph = network_graph(&logger);
let params = ProbabilisticScoringFeeParameters::default();
let scorer = ProbabilisticScorer::new(ProbabilisticScoringDecayParameters::default(), &network_graph, &logger);
let source = source_node_id();
let base_penalty_msat = params.base_penalty_msat;
let usage = ChannelUsage {
amount_msat: 750,
inflight_htlc_msat: 0,
effective_capacity: EffectiveCapacity::ExactLiquidity { liquidity_msat: 1_000 },
};
let network_graph = network_graph.read_only();
let channel = network_graph.channel(42).unwrap();
let (info, _) = channel.as_directed_from(&source).unwrap();
let candidate = CandidateRouteHop::PublicHop(PublicHopCandidate {
info,
short_channel_id: 42,
});
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), base_penalty_msat);
let usage = ChannelUsage { amount_msat: 1_000, ..usage };
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), base_penalty_msat);
let usage = ChannelUsage { amount_msat: 1_001, ..usage };
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), u64::max_value());
}
#[test]
fn remembers_historical_failures() {
let logger = TestLogger::new();
let network_graph = network_graph(&logger);
let params = ProbabilisticScoringFeeParameters {
historical_liquidity_penalty_multiplier_msat: 1024,
historical_liquidity_penalty_amount_multiplier_msat: 1024,
..ProbabilisticScoringFeeParameters::zero_penalty()
};
let decay_params = ProbabilisticScoringDecayParameters {
liquidity_offset_half_life: Duration::from_secs(60 * 60),
historical_no_updates_half_life: Duration::from_secs(10),
};
let mut scorer = ProbabilisticScorer::new(decay_params, &network_graph, &logger);
let source = source_node_id();
let target = target_node_id();
let usage = ChannelUsage {
amount_msat: 100,
inflight_htlc_msat: 0,
effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024, htlc_maximum_msat: 1_024 },
};
let usage_1 = ChannelUsage {
amount_msat: 1,
inflight_htlc_msat: 0,
effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024, htlc_maximum_msat: 1_024 },
};
{
let network_graph = network_graph.read_only();
let channel = network_graph.channel(42).unwrap();
let (info, _) = channel.as_directed_from(&source).unwrap();
let candidate = CandidateRouteHop::PublicHop(PublicHopCandidate {
info,
short_channel_id: 42,
});
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 135);
}
assert_eq!(scorer.historical_estimated_channel_liquidity_probabilities(42, &target),
None);
assert_eq!(scorer.historical_estimated_payment_success_probability(42, &target, 42, ¶ms, false),
None);
scorer.payment_path_failed(&payment_path_for_amount(1), 42, Duration::ZERO);
{
let network_graph = network_graph.read_only();
let channel = network_graph.channel(42).unwrap();
let (info, _) = channel.as_directed_from(&source).unwrap();
let candidate = CandidateRouteHop::PublicHop(PublicHopCandidate {
info,
short_channel_id: 42,
});
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 2048);
assert_eq!(scorer.channel_penalty_msat(&candidate, usage_1, ¶ms), 220);
}
assert_eq!(scorer.historical_estimated_channel_liquidity_probabilities(42, &target),
Some(([32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])));
assert!(scorer.historical_estimated_payment_success_probability(42, &target, 1, ¶ms, false)
.unwrap() > 0.35);
assert_eq!(scorer.historical_estimated_payment_success_probability(42, &target, 500, ¶ms, false),
Some(0.0));
scorer.payment_path_failed(&payment_path_for_amount(1000), 43, Duration::ZERO);
{
let network_graph = network_graph.read_only();
let channel = network_graph.channel(42).unwrap();
let (info, _) = channel.as_directed_from(&source).unwrap();
let candidate = CandidateRouteHop::PublicHop(PublicHopCandidate {
info,
short_channel_id: 42,
});
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 83);
}
assert_eq!(scorer.historical_estimated_channel_liquidity_probabilities(42, &target),
Some(([31, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 31, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32])));
let five_hundred_prob =
scorer.historical_estimated_payment_success_probability(42, &target, 500, ¶ms, false).unwrap();
assert!(five_hundred_prob > 0.61, "{}", five_hundred_prob);
assert!(five_hundred_prob < 0.62, "{}", five_hundred_prob);
let one_prob =
scorer.historical_estimated_payment_success_probability(42, &target, 1, ¶ms, false).unwrap();
assert!(one_prob < 0.89, "{}", one_prob);
assert!(one_prob > 0.88, "{}", one_prob);
scorer.time_passed(Duration::from_secs(10 * 16));
{
let network_graph = network_graph.read_only();
let channel = network_graph.channel(42).unwrap();
let (info, _) = channel.as_directed_from(&source).unwrap();
let candidate = CandidateRouteHop::PublicHop(PublicHopCandidate {
info,
short_channel_id: 42,
});
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 135);
}
assert_eq!(scorer.historical_estimated_channel_liquidity_probabilities(42, &target),
Some(([0; 32], [0; 32])));
assert_eq!(scorer.historical_estimated_payment_success_probability(42, &target, 1, ¶ms, false), None);
let usage = ChannelUsage {
amount_msat: 100,
inflight_htlc_msat: 1024,
effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024, htlc_maximum_msat: 1_024 },
};
scorer.payment_path_failed(&payment_path_for_amount(1), 42, Duration::from_secs(10 * 16));
{
let network_graph = network_graph.read_only();
let channel = network_graph.channel(42).unwrap();
let (info, _) = channel.as_directed_from(&source).unwrap();
let candidate = CandidateRouteHop::PublicHop(PublicHopCandidate {
info,
short_channel_id: 42,
});
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 2048);
let usage = ChannelUsage {
amount_msat: 1,
inflight_htlc_msat: 0,
effective_capacity: EffectiveCapacity::AdvertisedMaxHTLC { amount_msat: 0 },
};
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 2048);
}
scorer.time_passed(Duration::from_secs(10 * (16 + 60 * 60)));
assert_eq!(scorer.historical_estimated_channel_liquidity_probabilities(42, &target),
None);
let path = vec![
path_hop(target_pubkey(), 43, 2),
path_hop(source_pubkey(), 42, 1),
path_hop(sender_pubkey(), 41, 0),
];
scorer.payment_path_failed(&Path { hops: path, blinded_tail: None }, 42, Duration::from_secs(10 * (16 + 60 * 60)));
}
#[test]
fn adds_anti_probing_penalty() {
let logger = TestLogger::new();
let network_graph = network_graph(&logger);
let source = source_node_id();
let params = ProbabilisticScoringFeeParameters {
anti_probing_penalty_msat: 500,
..ProbabilisticScoringFeeParameters::zero_penalty()
};
let scorer = ProbabilisticScorer::new(ProbabilisticScoringDecayParameters::default(), &network_graph, &logger);
let usage = ChannelUsage {
amount_msat: 512_000,
inflight_htlc_msat: 0,
effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024_000, htlc_maximum_msat: 1_000 },
};
let network_graph = network_graph.read_only();
let channel = network_graph.channel(42).unwrap();
let (info, _) = channel.as_directed_from(&source).unwrap();
let candidate = CandidateRouteHop::PublicHop(PublicHopCandidate {
info,
short_channel_id: 42,
});
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 0);
let usage = ChannelUsage {
amount_msat: 512_000,
inflight_htlc_msat: 0,
effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024_000, htlc_maximum_msat: 1_024_000 },
};
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 500);
let usage = ChannelUsage {
amount_msat: 512_000,
inflight_htlc_msat: 0,
effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024_000, htlc_maximum_msat: 512_000 },
};
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 500);
let usage = ChannelUsage {
amount_msat: 512_000,
inflight_htlc_msat: 0,
effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024_000, htlc_maximum_msat: 511_999 },
};
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 0);
}
#[test]
fn scores_with_blinded_path() {
let logger = TestLogger::new();
let network_graph = network_graph(&logger);
let params = ProbabilisticScoringFeeParameters {
liquidity_penalty_multiplier_msat: 1_000,
..ProbabilisticScoringFeeParameters::zero_penalty()
};
let decay_params = ProbabilisticScoringDecayParameters::default();
let mut scorer = ProbabilisticScorer::new(decay_params, &network_graph, &logger);
let source = source_node_id();
let usage = ChannelUsage {
amount_msat: 512,
inflight_htlc_msat: 0,
effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024, htlc_maximum_msat: 1_000 },
};
let channel = network_graph.read_only().channel(42).unwrap().to_owned();
let (info, target) = channel.as_directed_from(&source).unwrap();
let candidate = CandidateRouteHop::PublicHop(PublicHopCandidate {
info,
short_channel_id: 42,
});
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 300);
let mut path = payment_path_for_amount(768);
let recipient_hop = path.hops.pop().unwrap();
path.blinded_tail = Some(BlindedTail {
hops: vec![BlindedHop { blinded_node_id: test_utils::pubkey(44), encrypted_payload: Vec::new() }],
blinding_point: test_utils::pubkey(42),
excess_final_cltv_expiry_delta: recipient_hop.cltv_expiry_delta,
final_value_msat: recipient_hop.fee_msat,
});
assert!(scorer.channel_liquidities.get(&42).is_none());
scorer.payment_path_failed(&path, 42, Duration::ZERO);
path.blinded_tail.as_mut().unwrap().final_value_msat = 256;
scorer.payment_path_failed(&path, 43, Duration::ZERO);
let liquidity = scorer.channel_liquidities.get(&42).unwrap()
.as_directed(&source, &target, 1_000);
assert_eq!(liquidity.min_liquidity_msat(), 256);
assert_eq!(liquidity.max_liquidity_msat(), 768);
}
#[test]
fn realistic_historical_failures() {
let logger = TestLogger::new();
let mut network_graph = network_graph(&logger);
let params = ProbabilisticScoringFeeParameters {
historical_liquidity_penalty_multiplier_msat: 1024,
historical_liquidity_penalty_amount_multiplier_msat: 1024,
..ProbabilisticScoringFeeParameters::zero_penalty()
};
let decay_params = ProbabilisticScoringDecayParameters {
liquidity_offset_half_life: Duration::from_secs(60 * 60),
historical_no_updates_half_life: Duration::from_secs(10),
..ProbabilisticScoringDecayParameters::default()
};
let capacity_msat = 100_000_000_000;
update_channel(&mut network_graph, 42, source_privkey(), 0, capacity_msat, 200);
update_channel(&mut network_graph, 42, target_privkey(), 1, capacity_msat, 200);
let mut scorer = ProbabilisticScorer::new(decay_params, &network_graph, &logger);
let source = source_node_id();
let mut amount_msat = 10_000_000;
let usage = ChannelUsage {
amount_msat,
inflight_htlc_msat: 0,
effective_capacity: EffectiveCapacity::Total { capacity_msat, htlc_maximum_msat: capacity_msat },
};
let channel = network_graph.read_only().channel(42).unwrap().to_owned();
let (info, target) = channel.as_directed_from(&source).unwrap();
let candidate = CandidateRouteHop::PublicHop(PublicHopCandidate {
info,
short_channel_id: 42,
});
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 910);
assert_eq!(scorer.historical_estimated_channel_liquidity_probabilities(42, &target),
None);
assert_eq!(scorer.historical_estimated_payment_success_probability(42, &target, 42, ¶ms, false),
None);
scorer.payment_path_failed(&payment_path_for_amount(amount_msat), 42, Duration::ZERO);
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms),
2048 + 2048 * amount_msat / super::AMOUNT_PENALTY_DIVISOR);
assert_eq!(scorer.historical_estimated_channel_liquidity_probabilities(42, &target),
Some(([32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])));
assert_eq!(scorer.historical_estimated_payment_success_probability(42, &target, amount_msat, ¶ms, false),
Some(0.0));
amount_msat /= 2;
assert_eq!(scorer.historical_estimated_payment_success_probability(42, &target, amount_msat, ¶ms, false),
Some(0.5));
scorer.payment_path_failed(&payment_path_for_amount(amount_msat), 42, Duration::ZERO);
assert_eq!(scorer.historical_estimated_channel_liquidity_probabilities(42, &target),
Some(([63, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[32, 31, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])));
assert_eq!(scorer.historical_estimated_payment_success_probability(42, &target, amount_msat, ¶ms, false),
Some(0.0));
}
}
#[cfg(ldk_bench)]
pub mod benches {
use super::*;
use criterion::Criterion;
use crate::routing::router::{bench_utils, RouteHop};
use crate::util::test_utils::TestLogger;
use crate::types::features::{ChannelFeatures, NodeFeatures};
pub fn decay_100k_channel_bounds(bench: &mut Criterion) {
let logger = TestLogger::new();
let (network_graph, mut scorer) = bench_utils::read_graph_scorer(&logger).unwrap();
let mut cur_time = Duration::ZERO;
cur_time += Duration::from_millis(1);
scorer.time_passed(cur_time);
bench.bench_function("decay_100k_channel_bounds", |b| b.iter(|| {
cur_time += Duration::from_millis(1);
scorer.time_passed(cur_time);
}));
}
}