+ assert_eq!(scorer.channel_penalty_msat(42, 1_000, 1_024_000, &source, &target), 0);
+ assert_eq!(scorer.channel_penalty_msat(42, 256_999, 1_024_000, &source, &target), 0);
+ assert_eq!(scorer.channel_penalty_msat(42, 257_000, 1_024_000, &source, &target), 100);
+ assert_eq!(scorer.channel_penalty_msat(42, 258_000, 1_024_000, &source, &target), 200);
+ assert_eq!(scorer.channel_penalty_msat(42, 512_000, 1_024_000, &source, &target), 256 * 100);
+ }
+
+ // `ProbabilisticScorer` tests
+
+ /// A probabilistic scorer for testing with time that can be manually advanced.
+ type ProbabilisticScorer<'a> = ProbabilisticScorerUsingTime::<&'a NetworkGraph, SinceEpoch>;
+
+ 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 sender_node_id() -> NodeId {
+ NodeId::from_pubkey(&sender_pubkey())
+ }
+
+ fn recipient_node_id() -> NodeId {
+ NodeId::from_pubkey(&recipient_pubkey())
+ }
+
+ fn network_graph() -> NetworkGraph {
+ let genesis_hash = genesis_block(Network::Testnet).header.block_hash();
+ let mut network_graph = NetworkGraph::new(genesis_hash);
+ 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, short_channel_id: u64, node_1_key: SecretKey,
+ node_2_key: SecretKey
+ ) {
+ let genesis_hash = genesis_block(Network::Testnet).header.block_hash();
+ 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: ChannelFeatures::known(),
+ chain_hash: genesis_hash,
+ short_channel_id,
+ node_id_1: PublicKey::from_secret_key(&secp_ctx, &node_1_key),
+ node_id_2: PublicKey::from_secret_key(&secp_ctx, &node_2_key),
+ bitcoin_key_1: PublicKey::from_secret_key(&secp_ctx, &node_1_secret),
+ bitcoin_key_2: 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(&msghash, &node_1_key),
+ node_signature_2: secp_ctx.sign(&msghash, &node_2_key),
+ bitcoin_signature_1: secp_ctx.sign(&msghash, &node_1_secret),
+ bitcoin_signature_2: secp_ctx.sign(&msghash, &node_2_secret),
+ contents: unsigned_announcement,
+ };
+ let chain_source: Option<&::util::test_utils::TestChainSource> = None;
+ network_graph.update_channel_from_announcement(
+ &signed_announcement, &chain_source, &secp_ctx).unwrap();
+ update_channel(network_graph, short_channel_id, node_1_key, 0);
+ update_channel(network_graph, short_channel_id, node_2_key, 1);
+ }
+
+ fn update_channel(
+ network_graph: &mut NetworkGraph, short_channel_id: u64, node_key: SecretKey, flags: u8
+ ) {
+ let genesis_hash = genesis_block(Network::Testnet).header.block_hash();
+ let secp_ctx = Secp256k1::new();
+ let unsigned_update = UnsignedChannelUpdate {
+ chain_hash: genesis_hash,
+ short_channel_id,
+ timestamp: 100,
+ flags,
+ cltv_expiry_delta: 18,
+ htlc_minimum_msat: 0,
+ htlc_maximum_msat: OptionalField::Present(1_000),
+ 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(&msghash, &node_key),
+ contents: unsigned_update,
+ };
+ network_graph.update_channel(&signed_update, &secp_ctx).unwrap();
+ }
+
+ fn payment_path_for_amount(amount_msat: u64) -> Vec<RouteHop> {
+ vec![
+ RouteHop {
+ pubkey: source_pubkey(),
+ node_features: NodeFeatures::known(),
+ short_channel_id: 41,
+ channel_features: ChannelFeatures::known(),
+ fee_msat: 1,
+ cltv_expiry_delta: 18,
+ },
+ RouteHop {
+ pubkey: target_pubkey(),
+ node_features: NodeFeatures::known(),
+ short_channel_id: 42,
+ channel_features: ChannelFeatures::known(),
+ fee_msat: 2,
+ cltv_expiry_delta: 18,
+ },
+ RouteHop {
+ pubkey: recipient_pubkey(),
+ node_features: NodeFeatures::known(),
+ short_channel_id: 43,
+ channel_features: ChannelFeatures::known(),
+ fee_msat: amount_msat,
+ cltv_expiry_delta: 18,
+ },
+ ]
+ }
+
+ #[test]
+ fn liquidity_bounds_directed_from_lowest_node_id() {
+ let last_updated = SinceEpoch::now();
+ let network_graph = network_graph();
+ let params = ProbabilisticScoringParameters::default();
+ let mut scorer = ProbabilisticScorer::new(params, &network_graph)
+ .with_channel(42,
+ ChannelLiquidity {
+ min_liquidity_offset_msat: 700, max_liquidity_offset_msat: 100, last_updated
+ })
+ .with_channel(43,
+ ChannelLiquidity {
+ min_liquidity_offset_msat: 700, max_liquidity_offset_msat: 100, last_updated
+ });
+ let source = source_node_id();
+ let target = target_node_id();
+ let recipient = recipient_node_id();
+ assert!(source > target);
+ assert!(target < recipient);
+
+ // Update minimum liquidity.
+
+ let liquidity_offset_half_life = scorer.params.liquidity_offset_half_life;
+ let liquidity = scorer.channel_liquidities.get(&42).unwrap()
+ .as_directed(&source, &target, 1_000, liquidity_offset_half_life);
+ 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, liquidity_offset_half_life);
+ 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, liquidity_offset_half_life)
+ .set_min_liquidity_msat(200);
+
+ let liquidity = scorer.channel_liquidities.get(&42).unwrap()
+ .as_directed(&source, &target, 1_000, liquidity_offset_half_life);
+ 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, liquidity_offset_half_life);
+ assert_eq!(liquidity.min_liquidity_msat(), 700);
+ assert_eq!(liquidity.max_liquidity_msat(), 800);
+
+ // Update maximum liquidity.
+
+ let liquidity = scorer.channel_liquidities.get(&43).unwrap()
+ .as_directed(&target, &recipient, 1_000, liquidity_offset_half_life);
+ 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, liquidity_offset_half_life);
+ 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, liquidity_offset_half_life)
+ .set_max_liquidity_msat(200);
+
+ let liquidity = scorer.channel_liquidities.get(&43).unwrap()
+ .as_directed(&target, &recipient, 1_000, liquidity_offset_half_life);
+ 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, liquidity_offset_half_life);
+ 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 last_updated = SinceEpoch::now();
+ let network_graph = network_graph();
+ let params = ProbabilisticScoringParameters::default();
+ let mut scorer = ProbabilisticScorer::new(params, &network_graph)
+ .with_channel(42,
+ ChannelLiquidity {
+ min_liquidity_offset_msat: 200, max_liquidity_offset_msat: 400, last_updated
+ });
+ let source = source_node_id();
+ let target = target_node_id();
+ assert!(source > target);
+
+ // Check initial bounds.
+ let liquidity_offset_half_life = scorer.params.liquidity_offset_half_life;
+ let liquidity = scorer.channel_liquidities.get(&42).unwrap()
+ .as_directed(&source, &target, 1_000, liquidity_offset_half_life);
+ 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, liquidity_offset_half_life);
+ assert_eq!(liquidity.min_liquidity_msat(), 200);
+ assert_eq!(liquidity.max_liquidity_msat(), 600);
+
+ // Reset from source to target.
+ scorer.channel_liquidities.get_mut(&42).unwrap()
+ .as_directed_mut(&source, &target, 1_000, liquidity_offset_half_life)
+ .set_min_liquidity_msat(900);
+
+ let liquidity = scorer.channel_liquidities.get(&42).unwrap()
+ .as_directed(&source, &target, 1_000, liquidity_offset_half_life);
+ 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, liquidity_offset_half_life);
+ assert_eq!(liquidity.min_liquidity_msat(), 0);
+ assert_eq!(liquidity.max_liquidity_msat(), 100);
+
+ // Reset from target to source.
+ scorer.channel_liquidities.get_mut(&42).unwrap()
+ .as_directed_mut(&target, &source, 1_000, liquidity_offset_half_life)
+ .set_min_liquidity_msat(400);
+
+ let liquidity = scorer.channel_liquidities.get(&42).unwrap()
+ .as_directed(&source, &target, 1_000, liquidity_offset_half_life);
+ 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, liquidity_offset_half_life);
+ 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 last_updated = SinceEpoch::now();
+ let network_graph = network_graph();
+ let params = ProbabilisticScoringParameters::default();
+ let mut scorer = ProbabilisticScorer::new(params, &network_graph)
+ .with_channel(42,
+ ChannelLiquidity {
+ min_liquidity_offset_msat: 200, max_liquidity_offset_msat: 400, last_updated
+ });
+ let source = source_node_id();
+ let target = target_node_id();
+ assert!(source > target);
+
+ // Check initial bounds.
+ let liquidity_offset_half_life = scorer.params.liquidity_offset_half_life;
+ let liquidity = scorer.channel_liquidities.get(&42).unwrap()
+ .as_directed(&source, &target, 1_000, liquidity_offset_half_life);
+ 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, liquidity_offset_half_life);
+ assert_eq!(liquidity.min_liquidity_msat(), 200);
+ assert_eq!(liquidity.max_liquidity_msat(), 600);
+
+ // Reset from source to target.
+ scorer.channel_liquidities.get_mut(&42).unwrap()
+ .as_directed_mut(&source, &target, 1_000, liquidity_offset_half_life)
+ .set_max_liquidity_msat(300);
+
+ let liquidity = scorer.channel_liquidities.get(&42).unwrap()
+ .as_directed(&source, &target, 1_000, liquidity_offset_half_life);
+ 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, liquidity_offset_half_life);
+ assert_eq!(liquidity.min_liquidity_msat(), 700);
+ assert_eq!(liquidity.max_liquidity_msat(), 1_000);
+
+ // Reset from target to source.
+ scorer.channel_liquidities.get_mut(&42).unwrap()
+ .as_directed_mut(&target, &source, 1_000, liquidity_offset_half_life)
+ .set_max_liquidity_msat(600);
+
+ let liquidity = scorer.channel_liquidities.get(&42).unwrap()
+ .as_directed(&source, &target, 1_000, liquidity_offset_half_life);
+ 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, liquidity_offset_half_life);
+ assert_eq!(liquidity.min_liquidity_msat(), 0);
+ assert_eq!(liquidity.max_liquidity_msat(), 600);
+ }
+
+ #[test]
+ fn increased_penalty_nearing_liquidity_upper_bound() {
+ let network_graph = network_graph();
+ let params = ProbabilisticScoringParameters {
+ liquidity_penalty_multiplier_msat: 1_000, ..Default::default()
+ };
+ let scorer = ProbabilisticScorer::new(params, &network_graph);
+ let source = source_node_id();
+ let target = target_node_id();
+
+ assert_eq!(scorer.channel_penalty_msat(42, 100, 100_000, &source, &target), 0);
+ assert_eq!(scorer.channel_penalty_msat(42, 1_000, 100_000, &source, &target), 4);
+ assert_eq!(scorer.channel_penalty_msat(42, 10_000, 100_000, &source, &target), 45);
+ assert_eq!(scorer.channel_penalty_msat(42, 100_000, 100_000, &source, &target), 2_000);
+
+ assert_eq!(scorer.channel_penalty_msat(42, 125, 1_000, &source, &target), 57);
+ assert_eq!(scorer.channel_penalty_msat(42, 250, 1_000, &source, &target), 124);
+ assert_eq!(scorer.channel_penalty_msat(42, 375, 1_000, &source, &target), 203);
+ assert_eq!(scorer.channel_penalty_msat(42, 500, 1_000, &source, &target), 300);
+ assert_eq!(scorer.channel_penalty_msat(42, 625, 1_000, &source, &target), 425);
+ assert_eq!(scorer.channel_penalty_msat(42, 750, 1_000, &source, &target), 600);
+ assert_eq!(scorer.channel_penalty_msat(42, 875, 1_000, &source, &target), 900);
+ }
+
+ #[test]
+ fn constant_penalty_outside_liquidity_bounds() {
+ let last_updated = SinceEpoch::now();
+ let network_graph = network_graph();
+ let params = ProbabilisticScoringParameters {
+ liquidity_penalty_multiplier_msat: 1_000, ..Default::default()
+ };
+ let scorer = ProbabilisticScorer::new(params, &network_graph)
+ .with_channel(42,
+ ChannelLiquidity {
+ min_liquidity_offset_msat: 40, max_liquidity_offset_msat: 40, last_updated
+ });
+ let source = source_node_id();
+ let target = target_node_id();
+
+ assert_eq!(scorer.channel_penalty_msat(42, 39, 100, &source, &target), 0);
+ assert_ne!(scorer.channel_penalty_msat(42, 50, 100, &source, &target), 0);
+ assert_ne!(scorer.channel_penalty_msat(42, 50, 100, &source, &target), 2_000);
+ assert_eq!(scorer.channel_penalty_msat(42, 61, 100, &source, &target), 2_000);
+ }
+
+ #[test]
+ fn does_not_further_penalize_own_channel() {
+ let network_graph = network_graph();
+ let params = ProbabilisticScoringParameters {
+ liquidity_penalty_multiplier_msat: 1_000, ..Default::default()
+ };
+ let mut scorer = ProbabilisticScorer::new(params, &network_graph);
+ let sender = sender_node_id();
+ let source = source_node_id();
+ let failed_path = payment_path_for_amount(500);
+ let successful_path = payment_path_for_amount(200);
+
+ assert_eq!(scorer.channel_penalty_msat(41, 500, 1_000, &sender, &source), 300);
+
+ scorer.payment_path_failed(&failed_path.iter().collect::<Vec<_>>(), 41);
+ assert_eq!(scorer.channel_penalty_msat(41, 500, 1_000, &sender, &source), 300);
+
+ scorer.payment_path_successful(&successful_path.iter().collect::<Vec<_>>());
+ assert_eq!(scorer.channel_penalty_msat(41, 500, 1_000, &sender, &source), 300);
+ }
+
+ #[test]
+ fn sets_liquidity_lower_bound_on_downstream_failure() {
+ let network_graph = network_graph();
+ let params = ProbabilisticScoringParameters {
+ liquidity_penalty_multiplier_msat: 1_000, ..Default::default()
+ };
+ let mut scorer = ProbabilisticScorer::new(params, &network_graph);
+ let source = source_node_id();
+ let target = target_node_id();
+ let path = payment_path_for_amount(500);
+
+ assert_eq!(scorer.channel_penalty_msat(42, 250, 1_000, &source, &target), 124);
+ assert_eq!(scorer.channel_penalty_msat(42, 500, 1_000, &source, &target), 300);
+ assert_eq!(scorer.channel_penalty_msat(42, 750, 1_000, &source, &target), 600);
+
+ scorer.payment_path_failed(&path.iter().collect::<Vec<_>>(), 43);
+
+ assert_eq!(scorer.channel_penalty_msat(42, 250, 1_000, &source, &target), 0);
+ assert_eq!(scorer.channel_penalty_msat(42, 500, 1_000, &source, &target), 0);
+ assert_eq!(scorer.channel_penalty_msat(42, 750, 1_000, &source, &target), 300);
+ }
+
+ #[test]
+ fn sets_liquidity_upper_bound_on_failure() {
+ let network_graph = network_graph();
+ let params = ProbabilisticScoringParameters {
+ liquidity_penalty_multiplier_msat: 1_000, ..Default::default()
+ };
+ let mut scorer = ProbabilisticScorer::new(params, &network_graph);
+ let source = source_node_id();
+ let target = target_node_id();
+ let path = payment_path_for_amount(500);
+
+ assert_eq!(scorer.channel_penalty_msat(42, 250, 1_000, &source, &target), 124);
+ assert_eq!(scorer.channel_penalty_msat(42, 500, 1_000, &source, &target), 300);
+ assert_eq!(scorer.channel_penalty_msat(42, 750, 1_000, &source, &target), 600);
+
+ scorer.payment_path_failed(&path.iter().collect::<Vec<_>>(), 42);
+
+ assert_eq!(scorer.channel_penalty_msat(42, 250, 1_000, &source, &target), 300);
+ assert_eq!(scorer.channel_penalty_msat(42, 500, 1_000, &source, &target), 2_000);
+ assert_eq!(scorer.channel_penalty_msat(42, 750, 1_000, &source, &target), 2_000);
+ }
+
+ #[test]
+ fn reduces_liquidity_upper_bound_along_path_on_success() {
+ let network_graph = network_graph();
+ let params = ProbabilisticScoringParameters {
+ liquidity_penalty_multiplier_msat: 1_000, ..Default::default()
+ };
+ let mut scorer = ProbabilisticScorer::new(params, &network_graph);
+ let sender = sender_node_id();
+ let source = source_node_id();
+ let target = target_node_id();
+ let recipient = recipient_node_id();
+ let path = payment_path_for_amount(500);
+
+ assert_eq!(scorer.channel_penalty_msat(41, 250, 1_000, &sender, &source), 124);
+ assert_eq!(scorer.channel_penalty_msat(42, 250, 1_000, &source, &target), 124);
+ assert_eq!(scorer.channel_penalty_msat(43, 250, 1_000, &target, &recipient), 124);
+
+ scorer.payment_path_successful(&path.iter().collect::<Vec<_>>());
+
+ assert_eq!(scorer.channel_penalty_msat(41, 250, 1_000, &sender, &source), 124);
+ assert_eq!(scorer.channel_penalty_msat(42, 250, 1_000, &source, &target), 300);
+ assert_eq!(scorer.channel_penalty_msat(43, 250, 1_000, &target, &recipient), 300);
+ }
+
+ #[test]
+ fn decays_liquidity_bounds_over_time() {
+ let network_graph = network_graph();
+ let params = ProbabilisticScoringParameters {
+ liquidity_penalty_multiplier_msat: 1_000,
+ liquidity_offset_half_life: Duration::from_secs(10),
+ };
+ let mut scorer = ProbabilisticScorer::new(params, &network_graph);
+ let source = source_node_id();
+ let target = target_node_id();
+
+ assert_eq!(scorer.channel_penalty_msat(42, 0, 1_024, &source, &target), 0);
+ assert_eq!(scorer.channel_penalty_msat(42, 1_024, 1_024, &source, &target), 2_000);
+
+ scorer.payment_path_failed(&payment_path_for_amount(768).iter().collect::<Vec<_>>(), 42);
+ scorer.payment_path_failed(&payment_path_for_amount(128).iter().collect::<Vec<_>>(), 43);
+
+ assert_eq!(scorer.channel_penalty_msat(42, 128, 1_024, &source, &target), 0);
+ assert_eq!(scorer.channel_penalty_msat(42, 256, 1_024, &source, &target), 92);
+ assert_eq!(scorer.channel_penalty_msat(42, 768, 1_024, &source, &target), 1_424);
+ assert_eq!(scorer.channel_penalty_msat(42, 896, 1_024, &source, &target), 2_000);
+
+ SinceEpoch::advance(Duration::from_secs(9));
+ assert_eq!(scorer.channel_penalty_msat(42, 128, 1_024, &source, &target), 0);
+ assert_eq!(scorer.channel_penalty_msat(42, 256, 1_024, &source, &target), 92);
+ assert_eq!(scorer.channel_penalty_msat(42, 768, 1_024, &source, &target), 1_424);
+ assert_eq!(scorer.channel_penalty_msat(42, 896, 1_024, &source, &target), 2_000);
+
+ SinceEpoch::advance(Duration::from_secs(1));
+ assert_eq!(scorer.channel_penalty_msat(42, 64, 1_024, &source, &target), 0);
+ assert_eq!(scorer.channel_penalty_msat(42, 128, 1_024, &source, &target), 34);
+ assert_eq!(scorer.channel_penalty_msat(42, 896, 1_024, &source, &target), 1_812);
+ assert_eq!(scorer.channel_penalty_msat(42, 960, 1_024, &source, &target), 2_000);
+
+ // Fully decay liquidity lower bound.
+ SinceEpoch::advance(Duration::from_secs(10 * 7));
+ assert_eq!(scorer.channel_penalty_msat(42, 0, 1_024, &source, &target), 0);
+ assert_eq!(scorer.channel_penalty_msat(42, 1, 1_024, &source, &target), 0);
+ assert_eq!(scorer.channel_penalty_msat(42, 1_023, 1_024, &source, &target), 2_000);
+ assert_eq!(scorer.channel_penalty_msat(42, 1_024, 1_024, &source, &target), 2_000);
+
+ // Fully decay liquidity upper bound.
+ SinceEpoch::advance(Duration::from_secs(10));
+ assert_eq!(scorer.channel_penalty_msat(42, 0, 1_024, &source, &target), 0);
+ assert_eq!(scorer.channel_penalty_msat(42, 1_024, 1_024, &source, &target), 2_000);
+
+ SinceEpoch::advance(Duration::from_secs(10));
+ assert_eq!(scorer.channel_penalty_msat(42, 0, 1_024, &source, &target), 0);
+ assert_eq!(scorer.channel_penalty_msat(42, 1_024, 1_024, &source, &target), 2_000);
+ }
+
+ #[test]
+ fn decays_liquidity_bounds_without_shift_overflow() {
+ let network_graph = network_graph();
+ let params = ProbabilisticScoringParameters {
+ liquidity_penalty_multiplier_msat: 1_000,
+ liquidity_offset_half_life: Duration::from_secs(10),
+ };
+ let mut scorer = ProbabilisticScorer::new(params, &network_graph);
+ let source = source_node_id();
+ let target = target_node_id();
+ assert_eq!(scorer.channel_penalty_msat(42, 256, 1_024, &source, &target), 124);
+
+ scorer.payment_path_failed(&payment_path_for_amount(512).iter().collect::<Vec<_>>(), 42);
+ assert_eq!(scorer.channel_penalty_msat(42, 256, 1_024, &source, &target), 281);
+
+ // An unchecked right shift 64 bits or more in DirectedChannelLiquidity::decayed_offset_msat
+ // would cause an overflow.
+ SinceEpoch::advance(Duration::from_secs(10 * 64));
+ assert_eq!(scorer.channel_penalty_msat(42, 256, 1_024, &source, &target), 124);
+
+ SinceEpoch::advance(Duration::from_secs(10));
+ assert_eq!(scorer.channel_penalty_msat(42, 256, 1_024, &source, &target), 124);
+ }
+
+ #[test]
+ fn restricts_liquidity_bounds_after_decay() {
+ let network_graph = network_graph();
+ let params = ProbabilisticScoringParameters {
+ liquidity_penalty_multiplier_msat: 1_000,
+ liquidity_offset_half_life: Duration::from_secs(10),
+ };
+ let mut scorer = ProbabilisticScorer::new(params, &network_graph);
+ let source = source_node_id();
+ let target = target_node_id();
+
+ assert_eq!(scorer.channel_penalty_msat(42, 512, 1_024, &source, &target), 300);
+
+ // More knowledge gives higher confidence (256, 768), meaning a lower penalty.
+ scorer.payment_path_failed(&payment_path_for_amount(768).iter().collect::<Vec<_>>(), 42);
+ scorer.payment_path_failed(&payment_path_for_amount(256).iter().collect::<Vec<_>>(), 43);
+ assert_eq!(scorer.channel_penalty_msat(42, 512, 1_024, &source, &target), 281);
+
+ // Decaying knowledge gives less confidence (128, 896), meaning a higher penalty.
+ SinceEpoch::advance(Duration::from_secs(10));
+ assert_eq!(scorer.channel_penalty_msat(42, 512, 1_024, &source, &target), 293);
+
+ // Reducing the upper bound gives more confidence (128, 832) that the payment amount (512)
+ // is closer to the upper bound, meaning a higher penalty.
+ scorer.payment_path_successful(&payment_path_for_amount(64).iter().collect::<Vec<_>>());
+ assert_eq!(scorer.channel_penalty_msat(42, 512, 1_024, &source, &target), 333);
+
+ // Increasing the lower bound gives more confidence (256, 832) that the payment amount (512)
+ // is closer to the lower bound, meaning a lower penalty.
+ scorer.payment_path_failed(&payment_path_for_amount(256).iter().collect::<Vec<_>>(), 43);
+ assert_eq!(scorer.channel_penalty_msat(42, 512, 1_024, &source, &target), 247);
+
+ // Further decaying affects the lower bound more than the upper bound (128, 928).
+ SinceEpoch::advance(Duration::from_secs(10));
+ assert_eq!(scorer.channel_penalty_msat(42, 512, 1_024, &source, &target), 280);
+ }
+
+ #[test]
+ fn restores_persisted_liquidity_bounds() {
+ let network_graph = network_graph();
+ let params = ProbabilisticScoringParameters {
+ liquidity_penalty_multiplier_msat: 1_000,
+ liquidity_offset_half_life: Duration::from_secs(10),
+ };
+ let mut scorer = ProbabilisticScorer::new(params, &network_graph);
+ let source = source_node_id();
+ let target = target_node_id();
+
+ scorer.payment_path_failed(&payment_path_for_amount(500).iter().collect::<Vec<_>>(), 42);
+ assert_eq!(scorer.channel_penalty_msat(42, 500, 1_000, &source, &target), 2_000);
+
+ SinceEpoch::advance(Duration::from_secs(10));
+ assert_eq!(scorer.channel_penalty_msat(42, 500, 1_000, &source, &target), 475);
+
+ scorer.payment_path_failed(&payment_path_for_amount(250).iter().collect::<Vec<_>>(), 43);
+ assert_eq!(scorer.channel_penalty_msat(42, 500, 1_000, &source, &target), 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, (params, &network_graph)).unwrap();
+ assert_eq!(deserialized_scorer.channel_penalty_msat(42, 500, 1_000, &source, &target), 300);
+ }
+
+ #[test]
+ fn decays_persisted_liquidity_bounds() {
+ let network_graph = network_graph();
+ let params = ProbabilisticScoringParameters {
+ liquidity_penalty_multiplier_msat: 1_000,
+ liquidity_offset_half_life: Duration::from_secs(10),
+ };
+ let mut scorer = ProbabilisticScorer::new(params, &network_graph);
+ let source = source_node_id();
+ let target = target_node_id();
+
+ scorer.payment_path_failed(&payment_path_for_amount(500).iter().collect::<Vec<_>>(), 42);
+ assert_eq!(scorer.channel_penalty_msat(42, 500, 1_000, &source, &target), 2_000);
+
+ let mut serialized_scorer = Vec::new();
+ scorer.write(&mut serialized_scorer).unwrap();
+
+ SinceEpoch::advance(Duration::from_secs(10));
+
+ let mut serialized_scorer = io::Cursor::new(&serialized_scorer);
+ let deserialized_scorer =
+ <ProbabilisticScorer>::read(&mut serialized_scorer, (params, &network_graph)).unwrap();
+ assert_eq!(deserialized_scorer.channel_penalty_msat(42, 500, 1_000, &source, &target), 475);
+
+ scorer.payment_path_failed(&payment_path_for_amount(250).iter().collect::<Vec<_>>(), 43);
+ assert_eq!(scorer.channel_penalty_msat(42, 500, 1_000, &source, &target), 300);
+
+ SinceEpoch::advance(Duration::from_secs(10));
+ assert_eq!(deserialized_scorer.channel_penalty_msat(42, 500, 1_000, &source, &target), 367);