Skip to content

Add pathfinder trait #273

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions sim-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ futures = "0.3.30"
console-subscriber = { version = "0.4.0", optional = true}
tokio-util = { version = "0.7.13", features = ["rt"] }
openssl = { version = "0.10", features = ["vendored"] }
lightning = { version = "0.0.123" }

[features]
dev = ["console-subscriber"]
7 changes: 7 additions & 0 deletions sim-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ use simln_lib::{
use simple_logger::SimpleLogger;
use tokio_util::task::TaskTracker;

// Import the pathfinder types
use simln_lib::sim_node::DefaultPathFinder;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Enable tracing if building in developer mode.
Expand All @@ -32,6 +35,9 @@ async fn main() -> anyhow::Result<()> {

let tasks = TaskTracker::new();

// Create the pathfinder instance
let pathfinder = DefaultPathFinder;

let (sim, validated_activities) = if sim_params.sim_network.is_empty() {
create_simulation(&cli, &sim_params, tasks.clone()).await?
} else {
Expand All @@ -47,6 +53,7 @@ async fn main() -> anyhow::Result<()> {
tasks.clone(),
interceptors,
CustomRecords::default(),
pathfinder,
)
.await?
};
Expand Down
221 changes: 218 additions & 3 deletions sim-cli/src/parsing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ use serde::{Deserialize, Serialize};
use simln_lib::clock::SimulationClock;
use simln_lib::sim_node::{
ln_node_from_graph, populate_network_graph, ChannelPolicy, CustomRecords, Interceptor,
SimGraph, SimulatedChannel,
PathFinder, SimGraph, SimulatedChannel,
};

use simln_lib::{
cln, cln::ClnNode, eclair, eclair::EclairNode, lnd, lnd::LndNode, serializers,
ActivityDefinition, Amount, Interval, LightningError, LightningNode, NodeId, NodeInfo,
Expand Down Expand Up @@ -224,12 +225,13 @@ struct NodeMapping {
alias_node_map: HashMap<String, NodeInfo>,
}

pub async fn create_simulation_with_network(
pub async fn create_simulation_with_network<P: PathFinder + Clone + 'static>(
cli: &Cli,
sim_params: &SimParams,
tasks: TaskTracker,
interceptors: Vec<Arc<dyn Interceptor>>,
custom_records: CustomRecords,
pathfinder: P,
) -> Result<(Simulation<SimulationClock>, Vec<ActivityDefinition>), anyhow::Error> {
let cfg: SimulationCfg = SimulationCfg::try_from(cli)?;
let SimParams {
Expand Down Expand Up @@ -276,7 +278,7 @@ pub async fn create_simulation_with_network(
.map_err(|e| SimulationError::SimulatedNetworkError(format!("{:?}", e)))?,
);

let nodes = ln_node_from_graph(simulation_graph.clone(), routing_graph).await;
let nodes = ln_node_from_graph(simulation_graph.clone(), routing_graph, pathfinder).await;
let validated_activities =
get_validated_activities(&nodes, nodes_info, sim_params.activity.clone()).await?;

Expand Down Expand Up @@ -589,3 +591,216 @@ pub async fn get_validated_activities(

validate_activities(activity.to_vec(), activity_validation_params).await
}

#[cfg(test)]
mod tests {
use super::*;
use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey};
use lightning::routing::gossip::NetworkGraph;
use lightning::routing::router::{find_route, PaymentParameters, Route, RouteParameters};
use lightning::routing::scoring::{ProbabilisticScorer, ProbabilisticScoringDecayParameters};
use rand::RngCore;
use simln_lib::clock::SystemClock;
use simln_lib::sim_node::{
ln_node_from_graph, populate_network_graph, PathFinder, SimGraph, WrappedLog,
};
use simln_lib::SimulationError;
use std::sync::Arc;
use tokio::sync::Mutex;
use tokio_util::task::TaskTracker;

/// Gets a key pair generated in a pseudorandom way.
fn get_random_keypair() -> (SecretKey, PublicKey) {
let secp = Secp256k1::new();
let mut rng = rand::thread_rng();
let mut bytes = [0u8; 32];
rng.fill_bytes(&mut bytes);
let secret_key = SecretKey::from_slice(&bytes).expect("Failed to create secret key");
let public_key = PublicKey::from_secret_key(&secp, &secret_key);
(secret_key, public_key)
}

/// Helper function to create simulated channels for testing.
fn create_simulated_channels(num_channels: usize, capacity_msat: u64) -> Vec<SimulatedChannel> {
let mut channels = Vec::new();
for i in 0..num_channels {
let (_node1_sk, node1_pubkey) = get_random_keypair();
let (_node2_sk, node2_pubkey) = get_random_keypair();

let channel = SimulatedChannel::new(
capacity_msat,
ShortChannelID::from(i as u64),
ChannelPolicy {
pubkey: node1_pubkey,
max_htlc_count: 483,
max_in_flight_msat: capacity_msat / 2,
min_htlc_size_msat: 1000,
max_htlc_size_msat: capacity_msat / 2,
cltv_expiry_delta: 144,
base_fee: 1000,
fee_rate_prop: 100,
},
ChannelPolicy {
pubkey: node2_pubkey,
max_htlc_count: 483,
max_in_flight_msat: capacity_msat / 2,
min_htlc_size_msat: 1000,
max_htlc_size_msat: capacity_msat / 2,
cltv_expiry_delta: 144,
base_fee: 1000,
fee_rate_prop: 100,
},
);
channels.push(channel);
}
channels
}

/// A pathfinder that always fails to find a path.
#[derive(Clone)]
pub struct AlwaysFailPathFinder;

impl PathFinder for AlwaysFailPathFinder {
fn find_route(
&self,
_source: &PublicKey,
_dest: PublicKey,
_amount_msat: u64,
_pathfinding_graph: &NetworkGraph<&'static WrappedLog>,
) -> Result<Route, SimulationError> {
Err(SimulationError::SimulatedNetworkError(
"No route found".to_string(),
))
}
}

/// A pathfinder that only returns single-hop paths.
#[derive(Clone)]
pub struct SingleHopOnlyPathFinder;

impl PathFinder for SingleHopOnlyPathFinder {
fn find_route(
&self,
source: &PublicKey,
dest: PublicKey,
amount_msat: u64,
pathfinding_graph: &NetworkGraph<&'static WrappedLog>,
) -> Result<Route, SimulationError> {
let scorer = ProbabilisticScorer::new(
ProbabilisticScoringDecayParameters::default(),
pathfinding_graph,
&WrappedLog {},
);

// Try to find a route - if it fails or has more than one hop, return an error.
match find_route(
source,
&RouteParameters {
payment_params: PaymentParameters::from_node_id(dest, 0)
.with_max_total_cltv_expiry_delta(u32::MAX)
.with_max_path_count(1)
.with_max_channel_saturation_power_of_half(1),
final_value_msat: amount_msat,
max_total_routing_fee_msat: None,
},
pathfinding_graph,
None,
&WrappedLog {},
&scorer,
&Default::default(),
&[0; 32],
) {
Ok(route) => {
// Only allow single-hop routes.
if route.paths.len() == 1 && route.paths[0].hops.len() == 1 {
Ok(route)
} else {
Err(SimulationError::SimulatedNetworkError(
"Only single-hop routes allowed".to_string(),
))
}
},
Err(e) => Err(SimulationError::SimulatedNetworkError(e.err)),
}
}
}

#[tokio::test]
async fn test_always_fail_pathfinder() {
let channels = create_simulated_channels(3, 1_000_000_000);
let routing_graph =
Arc::new(populate_network_graph(channels.clone(), Arc::new(SystemClock {})).unwrap());

let pathfinder = AlwaysFailPathFinder;
let source = channels[0].get_node_1_pubkey();
let dest = channels[2].get_node_2_pubkey();

let result = pathfinder.find_route(&source, dest, 100_000, &routing_graph);

// Should always fail.
assert!(result.is_err());
}

#[tokio::test]
async fn test_single_hop_only_pathfinder() {
let channels = create_simulated_channels(3, 1_000_000_000);
let routing_graph =
Arc::new(populate_network_graph(channels.clone(), Arc::new(SystemClock {})).unwrap());

let pathfinder = SingleHopOnlyPathFinder;
let source = channels[0].get_node_1_pubkey();

// Test direct connection (should work).
let direct_dest = channels[0].get_node_2_pubkey();
let result = pathfinder.find_route(&source, direct_dest, 100_000, &routing_graph);

if result.is_ok() {
let route = result.unwrap();
assert_eq!(route.paths[0].hops.len(), 1); // Only one hop
}

// Test indirect connection (should fail).
let indirect_dest = channels[2].get_node_2_pubkey();
let _result = pathfinder.find_route(&source, indirect_dest, 100_000, &routing_graph);

// May fail because no direct route exists.
// (depends on your test network topology)
}

/// Test that different pathfinders produce different behavior in payments.
#[tokio::test]
async fn test_pathfinder_affects_payment_behavior() {
let channels = create_simulated_channels(3, 1_000_000_000);
let (shutdown_trigger, shutdown_listener) = triggered::trigger();
let sim_graph = Arc::new(Mutex::new(
SimGraph::new(
channels.clone(),
TaskTracker::new(),
Vec::new(),
HashMap::new(), // Empty custom records
(shutdown_trigger.clone(), shutdown_listener.clone()),
)
.unwrap(),
));
let routing_graph =
Arc::new(populate_network_graph(channels.clone(), Arc::new(SystemClock {})).unwrap());

// Create nodes with different pathfinders.
let nodes_default = ln_node_from_graph(
sim_graph.clone(),
routing_graph.clone(),
simln_lib::sim_node::DefaultPathFinder,
)
.await;

let nodes_fail = ln_node_from_graph(
sim_graph.clone(),
routing_graph.clone(),
AlwaysFailPathFinder,
)
.await;

// Both should create the same structure.
assert_eq!(nodes_default.len(), nodes_fail.len());
}
}
Loading