use core::fmt;
use std::fmt::Display;
use thiserror::Error;
use bitcoin::secp256k1::PublicKey;
use rand_distr::{Distribution, Exp, LogNormal, WeightedIndex};
use std::time::Duration;
use crate::{
DestinationGenerationError, DestinationGenerator, MutRng, NodeInfo, PaymentGenerationError,
PaymentGenerator,
};
const HOURS_PER_MONTH: u64 = 30 * 24;
const SECONDS_PER_MONTH: u64 = HOURS_PER_MONTH * 60 * 60;
#[derive(Debug, Error)]
pub enum RandomActivityError {
#[error("Value error: {0}")]
ValueError(String),
#[error("InsufficientCapacity: {0}")]
InsufficientCapacity(String),
}
pub struct NetworkGraphView {
node_picker: WeightedIndex<u64>,
nodes: Vec<(NodeInfo, u64)>,
rng: MutRng,
}
impl NetworkGraphView {
pub fn new(nodes: Vec<(NodeInfo, u64)>, rng: MutRng) -> Result<Self, RandomActivityError> {
if nodes.len() < 2 {
return Err(RandomActivityError::ValueError(
"at least two nodes required for activity generation".to_string(),
));
}
if nodes.iter().any(|(_, v)| *v == 0) {
return Err(RandomActivityError::InsufficientCapacity(
"network generator created with zero capacity node".to_string(),
));
}
let node_picker = WeightedIndex::new(nodes.iter().map(|(_, v)| *v).collect::<Vec<u64>>())
.map_err(|e| RandomActivityError::ValueError(e.to_string()))?;
Ok(NetworkGraphView {
node_picker,
nodes,
rng,
})
}
}
impl DestinationGenerator for NetworkGraphView {
fn choose_destination(
&self,
source: PublicKey,
) -> Result<(NodeInfo, Option<u64>), DestinationGenerationError> {
let mut rng = self
.rng
.0
.lock()
.map_err(|e| DestinationGenerationError(e.to_string()))?;
let mut i = 1;
loop {
let index = self.node_picker.sample(&mut *rng);
let (node_info, capacity) = self.nodes.get(index).unwrap();
if node_info.pubkey != source {
return Ok((node_info.clone(), Some(*capacity)));
}
if i % 50 == 0 {
log::warn!("Unable to select a destination for: {source} after {i} attempts. Please report a bug!")
}
i += 1
}
}
}
impl Display for NetworkGraphView {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "network graph view with: {} channels", self.nodes.len())
}
}
pub struct RandomPaymentActivity {
multiplier: f64,
expected_payment_amt: u64,
source_capacity: u64,
event_dist: Exp<f64>,
rng: MutRng,
}
impl RandomPaymentActivity {
pub fn new(
source_capacity_msat: u64,
expected_payment_amt: u64,
multiplier: f64,
rng: MutRng,
) -> Result<Self, RandomActivityError> {
if source_capacity_msat == 0 {
return Err(RandomActivityError::ValueError(
"source_capacity_msat cannot be zero".into(),
));
}
if expected_payment_amt == 0 {
return Err(RandomActivityError::ValueError(
"expected_payment_amt cannot be zero".into(),
));
}
if multiplier == 0.0 {
return Err(RandomActivityError::ValueError(
"multiplier cannot be zero".into(),
));
}
RandomPaymentActivity::validate_capacity(source_capacity_msat, expected_payment_amt)?;
let lamda = events_per_month(source_capacity_msat, multiplier, expected_payment_amt)
/ (SECONDS_PER_MONTH as f64);
let event_dist =
Exp::new(lamda).map_err(|e| RandomActivityError::ValueError(e.to_string()))?;
Ok(RandomPaymentActivity {
multiplier,
expected_payment_amt,
source_capacity: source_capacity_msat,
event_dist,
rng,
})
}
pub fn validate_capacity(
node_capacity_msat: u64,
expected_payment_amt: u64,
) -> Result<(), RandomActivityError> {
let min_required_capacity = 2 * expected_payment_amt;
if node_capacity_msat < min_required_capacity {
return Err(RandomActivityError::InsufficientCapacity(format!(
"node needs at least {} capacity (has: {}) to process expected payment amount: {}",
min_required_capacity, node_capacity_msat, expected_payment_amt
)));
}
Ok(())
}
}
fn events_per_month(source_capacity_msat: u64, multiplier: f64, expected_payment_amt: u64) -> f64 {
(source_capacity_msat as f64 * multiplier) / expected_payment_amt as f64
}
impl PaymentGenerator for RandomPaymentActivity {
fn payment_start(&self) -> Option<Duration> {
None
}
fn payment_count(&self) -> Option<u64> {
None
}
fn next_payment_wait(&self) -> Result<Duration, PaymentGenerationError> {
let mut rng = self
.rng
.0
.lock()
.map_err(|e| PaymentGenerationError(e.to_string()))?;
let duration_in_secs = self.event_dist.sample(&mut *rng) as u64;
Ok(Duration::from_secs(duration_in_secs))
}
fn payment_amount(
&self,
destination_capacity: Option<u64>,
) -> Result<u64, PaymentGenerationError> {
let destination_capacity = destination_capacity.ok_or(PaymentGenerationError(
"destination amount required for payment activity generator".to_string(),
))?;
let payment_limit = std::cmp::min(self.source_capacity, destination_capacity) / 2;
let ln_pmt_amt = (self.expected_payment_amt as f64).ln();
let ln_limit = (payment_limit as f64).ln();
let mu = 2.0 * ln_pmt_amt - ln_limit;
let sigma_square = 2.0 * (ln_limit - ln_pmt_amt);
if sigma_square < 0.0 {
return Err(PaymentGenerationError(format!(
"payment amount not possible for limit: {payment_limit}, sigma squared: {sigma_square}"
)));
}
let log_normal = LogNormal::new(mu, sigma_square.sqrt())
.map_err(|e| PaymentGenerationError(e.to_string()))?;
let mut rng = self
.rng
.0
.lock()
.map_err(|e| PaymentGenerationError(e.to_string()))?;
let payment_amount = log_normal.sample(&mut *rng) as u64;
Ok(payment_amount)
}
}
impl Display for RandomPaymentActivity {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let monthly_events = events_per_month(
self.source_capacity,
self.multiplier,
self.expected_payment_amt,
);
write!(
f,
"activity generator for capacity: {} with multiplier {}: {} payments per month ({} per hour)",
self.source_capacity,
self.multiplier,
monthly_events,
monthly_events / HOURS_PER_MONTH as f64
)
}
}
#[cfg(test)]
mod tests {
mod test_network_graph_view {
use ntest::timeout;
use super::super::*;
use crate::test_utils::create_nodes;
#[test]
fn test_new() {
let rng = MutRng::new(Some(u64::MAX));
for i in 0..2 {
assert!(matches!(
NetworkGraphView::new(create_nodes(i, 42 * (i as u64 + 1)), rng.clone()),
Err(RandomActivityError::ValueError { .. })
));
}
let mut nodes = create_nodes(1, 0);
nodes.extend(create_nodes(1, 21));
assert!(matches!(
NetworkGraphView::new(nodes, rng.clone()),
Err(RandomActivityError::InsufficientCapacity { .. })
));
assert!(matches!(
NetworkGraphView::new(create_nodes(2, 0), rng.clone()),
Err(RandomActivityError::InsufficientCapacity { .. })
));
assert!(NetworkGraphView::new(create_nodes(2, 42), rng).is_ok());
}
#[test]
#[timeout(5000)]
fn test_sample_node_by_capacity() {
let small_node_count = 999;
let big_node_count = 1;
let small_node_capacity = 1_000;
let big_node_capacity = small_node_capacity * small_node_count as u64;
let mut nodes = create_nodes(small_node_count, small_node_capacity);
nodes.extend(create_nodes(big_node_count, big_node_capacity));
let big_node = nodes.last().unwrap().0.pubkey;
let rng = MutRng::new(Some(u64::MAX));
let view = NetworkGraphView::new(nodes, rng).unwrap();
for _ in 0..10 {
view.choose_destination(big_node).unwrap();
}
}
}
mod payment_activity_generator {
use super::super::*;
use crate::test_utils::get_random_int;
#[test]
fn test_new() {
let rng = MutRng::new(Some(u64::MAX));
let expected_payment = get_random_int(1, 100);
assert!(RandomPaymentActivity::new(
2 * expected_payment,
expected_payment,
1.0,
rng.clone()
)
.is_ok());
assert!(matches!(
RandomPaymentActivity::new(
2 * expected_payment,
expected_payment + 1,
1.0,
rng.clone()
),
Err(RandomActivityError::InsufficientCapacity { .. })
));
assert!(matches!(
RandomPaymentActivity::new(
0,
get_random_int(1, 10),
get_random_int(1, 10) as f64,
rng.clone()
),
Err(RandomActivityError::ValueError { .. })
));
assert!(matches!(
RandomPaymentActivity::new(
get_random_int(1, 10),
0,
get_random_int(1, 10) as f64,
rng.clone()
),
Err(RandomActivityError::ValueError { .. })
));
assert!(matches!(
RandomPaymentActivity::new(get_random_int(1, 10), get_random_int(1, 10), 0.0, rng),
Err(RandomActivityError::ValueError { .. })
));
}
#[test]
fn test_validate_capacity() {
for _ in 0..=get_random_int(20, 100) {
let capacity = get_random_int(0, 100);
let payment_amt = get_random_int(0, 100);
let r = RandomPaymentActivity::validate_capacity(capacity, payment_amt);
if capacity < 2 * payment_amt {
assert!(matches!(
r,
Err(RandomActivityError::InsufficientCapacity { .. })
));
} else {
assert!(r.is_ok());
}
}
}
#[test]
fn test_payment_amount() {
let expected_payment = get_random_int(1, 100);
let source_capacity = 2 * expected_payment;
let rng = MutRng::new(Some(u64::MAX));
let pag =
RandomPaymentActivity::new(source_capacity, expected_payment, 1.0, rng).unwrap();
for i in 0..source_capacity {
assert!(matches!(
pag.payment_amount(Some(i)),
Err(PaymentGenerationError(..))
))
}
for i in source_capacity + 1..100 * source_capacity {
assert!(pag.payment_amount(Some(i)).is_ok())
}
for i in u64::MAX - 10000..u64::MAX {
assert!(pag.payment_amount(Some(i)).is_ok())
}
assert!(matches!(
pag.payment_amount(None),
Err(PaymentGenerationError(..))
));
}
}
}