|
1 |
| -use bitcoin::secp256k1::PublicKey; |
2 |
| -use lightning::ln::PaymentHash; |
| 1 | +use crate::{LightningError, LightningNode, NodeInfo, PaymentOutcome, PaymentResult}; |
| 2 | +use async_trait::async_trait; |
| 3 | +use bitcoin::Network; |
| 4 | +use bitcoin::{ |
| 5 | + hashes::{sha256::Hash as Sha256, Hash}, |
| 6 | + secp256k1::PublicKey, |
| 7 | +}; |
| 8 | +use lightning::ln::features::NodeFeatures; |
| 9 | +use lightning::ln::msgs::LightningError as LdkError; |
| 10 | +use lightning::ln::{PaymentHash, PaymentPreimage}; |
| 11 | +use lightning::routing::gossip::NetworkGraph; |
| 12 | +use lightning::routing::router::{find_route, PaymentParameters, Route, RouteParameters}; |
| 13 | +use lightning::routing::scoring::ProbabilisticScorer; |
| 14 | +use lightning::util::logger::{Level, Logger, Record}; |
| 15 | +use std::collections::hash_map::Entry; |
3 | 16 | use std::collections::HashMap;
|
| 17 | +use std::sync::Arc; |
| 18 | +use tokio::select; |
| 19 | +use tokio::sync::oneshot::{channel, Receiver, Sender}; |
| 20 | +use tokio::sync::Mutex; |
| 21 | +use triggered::Listener; |
4 | 22 |
|
5 | 23 | use crate::ShortChannelID;
|
6 | 24 |
|
@@ -338,3 +356,224 @@ impl SimulatedChannel {
|
338 | 356 | .check_htlc_forward(cltv_delta, amount_msat, fee_msat)
|
339 | 357 | }
|
340 | 358 | }
|
| 359 | + |
| 360 | +/// SimNetwork represents a high level network coordinator that is responsible for the task of actually propagating |
| 361 | +/// payments through the simulated network. |
| 362 | +#[async_trait] |
| 363 | +trait SimNetwork: Send + Sync { |
| 364 | + /// Sends payments over the route provided through the network, reporting the final payment outcome to the sender |
| 365 | + /// channel provided. |
| 366 | + fn dispatch_payment( |
| 367 | + &mut self, |
| 368 | + source: PublicKey, |
| 369 | + route: Route, |
| 370 | + payment_hash: PaymentHash, |
| 371 | + sender: Sender<Result<PaymentResult, LightningError>>, |
| 372 | + ); |
| 373 | + |
| 374 | + /// Looks up a node in the simulated network and a list of its channel capacities. |
| 375 | + async fn lookup_node(&self, node: &PublicKey) -> Result<(NodeInfo, Vec<u64>), LightningError>; |
| 376 | +} |
| 377 | + |
| 378 | +/// A wrapper struct used to implement the LightningNode trait (can be thought of as "the" lightning node). Passes |
| 379 | +/// all functionality through to a coordinating simulation network. This implementation contains both the [`SimNetwork`] |
| 380 | +/// implementation that will allow us to dispatch payments and a read-only NetworkGraph that is used for pathfinding. |
| 381 | +/// While these two could be combined, we re-use the LDK-native struct to allow re-use of their pathfinding logic. |
| 382 | +struct SimNode<'a, T: SimNetwork> { |
| 383 | + info: NodeInfo, |
| 384 | + /// The underlying execution network that will be responsible for dispatching payments. |
| 385 | + network: Arc<Mutex<T>>, |
| 386 | + /// Tracks the channel that will provide updates for payments by hash. |
| 387 | + in_flight: HashMap<PaymentHash, Receiver<Result<PaymentResult, LightningError>>>, |
| 388 | + /// A read-only graph used for pathfinding. |
| 389 | + pathfinding_graph: Arc<NetworkGraph<&'a WrappedLog>>, |
| 390 | +} |
| 391 | + |
| 392 | +impl<'a, T: SimNetwork> SimNode<'a, T> { |
| 393 | + /// Creates a new simulation node that refers to the high level network coordinator provided to process payments |
| 394 | + /// on its behalf. The pathfinding graph is provided separately so that each node can handle its own pathfinding. |
| 395 | + pub fn new( |
| 396 | + pubkey: PublicKey, |
| 397 | + payment_network: Arc<Mutex<T>>, |
| 398 | + pathfinding_graph: Arc<NetworkGraph<&'a WrappedLog>>, |
| 399 | + ) -> Self { |
| 400 | + SimNode { |
| 401 | + info: node_info(pubkey), |
| 402 | + network: payment_network, |
| 403 | + in_flight: HashMap::new(), |
| 404 | + pathfinding_graph, |
| 405 | + } |
| 406 | + } |
| 407 | +} |
| 408 | + |
| 409 | +/// Produces the node info for a mocked node, filling in the features that the simulator requires. |
| 410 | +fn node_info(pubkey: PublicKey) -> NodeInfo { |
| 411 | + // Set any features that the simulator requires here. |
| 412 | + let mut features = NodeFeatures::empty(); |
| 413 | + features.set_keysend_optional(); |
| 414 | + |
| 415 | + NodeInfo { |
| 416 | + pubkey, |
| 417 | + alias: "".to_string(), // TODO: store alias? |
| 418 | + features, |
| 419 | + } |
| 420 | +} |
| 421 | + |
| 422 | +/// Uses LDK's pathfinding algorithm with default parameters to find a path from source to destination, with no |
| 423 | +/// restrictions on fee budget. |
| 424 | +fn find_payment_route( |
| 425 | + source: &PublicKey, |
| 426 | + dest: PublicKey, |
| 427 | + amount_msat: u64, |
| 428 | + pathfinding_graph: &NetworkGraph<&WrappedLog>, |
| 429 | +) -> Result<Route, LdkError> { |
| 430 | + let scorer = ProbabilisticScorer::new(Default::default(), pathfinding_graph, &WrappedLog {}); |
| 431 | + |
| 432 | + find_route( |
| 433 | + source, |
| 434 | + &RouteParameters { |
| 435 | + payment_params: PaymentParameters::from_node_id(dest, 0) |
| 436 | + .with_max_total_cltv_expiry_delta(u32::MAX) |
| 437 | + // TODO: set non-zero value to support MPP. |
| 438 | + .with_max_path_count(1) |
| 439 | + // Allow sending htlcs up to 50% of the channel's capacity. |
| 440 | + .with_max_channel_saturation_power_of_half(1), |
| 441 | + final_value_msat: amount_msat, |
| 442 | + max_total_routing_fee_msat: None, |
| 443 | + }, |
| 444 | + pathfinding_graph, |
| 445 | + None, |
| 446 | + &WrappedLog {}, |
| 447 | + &scorer, |
| 448 | + &Default::default(), |
| 449 | + &[0; 32], |
| 450 | + ) |
| 451 | +} |
| 452 | + |
| 453 | +#[async_trait] |
| 454 | +impl<T: SimNetwork> LightningNode for SimNode<'_, T> { |
| 455 | + fn get_info(&self) -> &NodeInfo { |
| 456 | + &self.info |
| 457 | + } |
| 458 | + |
| 459 | + async fn get_network(&mut self) -> Result<Network, LightningError> { |
| 460 | + Ok(Network::Regtest) |
| 461 | + } |
| 462 | + |
| 463 | + /// send_payment picks a random preimage for a payment, dispatches it in the network and adds a tracking channel |
| 464 | + /// to our node state to be used for subsequent track_payment calls. |
| 465 | + async fn send_payment( |
| 466 | + &mut self, |
| 467 | + dest: PublicKey, |
| 468 | + amount_msat: u64, |
| 469 | + ) -> Result<PaymentHash, LightningError> { |
| 470 | + // Create a sender and receiver pair that will be used to report the results of the payment and add them to |
| 471 | + // our internal tracking state along with the chosen payment hash. |
| 472 | + let (sender, receiver) = channel(); |
| 473 | + let preimage = PaymentPreimage(rand::random()); |
| 474 | + let payment_hash = PaymentHash(Sha256::hash(&preimage.0).to_byte_array()); |
| 475 | + |
| 476 | + // Check for payment hash collision, failing the payment if we happen to repeat one. |
| 477 | + match self.in_flight.entry(payment_hash) { |
| 478 | + Entry::Occupied(_) => { |
| 479 | + return Err(LightningError::SendPaymentError( |
| 480 | + "payment hash exists".to_string(), |
| 481 | + )); |
| 482 | + }, |
| 483 | + Entry::Vacant(vacant) => { |
| 484 | + vacant.insert(receiver); |
| 485 | + }, |
| 486 | + } |
| 487 | + |
| 488 | + let route = match find_payment_route( |
| 489 | + &self.info.pubkey, |
| 490 | + dest, |
| 491 | + amount_msat, |
| 492 | + &self.pathfinding_graph, |
| 493 | + ) { |
| 494 | + Ok(path) => path, |
| 495 | + // In the case that we can't find a route for the payment, we still report a successful payment *api call* |
| 496 | + // and report RouteNotFound to the tracking channel. This mimics the behavior of real nodes. |
| 497 | + Err(e) => { |
| 498 | + log::trace!("Could not find path for payment: {:?}.", e); |
| 499 | + |
| 500 | + if let Err(e) = sender.send(Ok(PaymentResult { |
| 501 | + htlc_count: 0, |
| 502 | + payment_outcome: PaymentOutcome::RouteNotFound, |
| 503 | + })) { |
| 504 | + log::error!("Could not send payment result: {:?}.", e); |
| 505 | + } |
| 506 | + |
| 507 | + return Ok(payment_hash); |
| 508 | + }, |
| 509 | + }; |
| 510 | + |
| 511 | + // If we did successfully obtain a route, dispatch the payment through the network and then report success. |
| 512 | + self.network |
| 513 | + .lock() |
| 514 | + .await |
| 515 | + .dispatch_payment(self.info.pubkey, route, payment_hash, sender); |
| 516 | + |
| 517 | + Ok(payment_hash) |
| 518 | + } |
| 519 | + |
| 520 | + /// track_payment blocks until a payment outcome is returned for the payment hash provided, or the shutdown listener |
| 521 | + /// provided is triggered. This call will fail if the hash provided was not obtained by calling send_payment first. |
| 522 | + async fn track_payment( |
| 523 | + &mut self, |
| 524 | + hash: PaymentHash, |
| 525 | + listener: Listener, |
| 526 | + ) -> Result<PaymentResult, LightningError> { |
| 527 | + match self.in_flight.remove(&hash) { |
| 528 | + Some(receiver) => { |
| 529 | + select! { |
| 530 | + biased; |
| 531 | + _ = listener => Err( |
| 532 | + LightningError::TrackPaymentError("shutdown during payment tracking".to_string()), |
| 533 | + ), |
| 534 | + |
| 535 | + // If we get a payment result back, remove from our in flight set of payments and return the result. |
| 536 | + res = receiver => { |
| 537 | + res.map_err(|e| LightningError::TrackPaymentError(format!("channel receive err: {}", e)))? |
| 538 | + }, |
| 539 | + } |
| 540 | + }, |
| 541 | + None => Err(LightningError::TrackPaymentError(format!( |
| 542 | + "payment hash {} not found", |
| 543 | + hex::encode(hash.0), |
| 544 | + ))), |
| 545 | + } |
| 546 | + } |
| 547 | + |
| 548 | + async fn get_node_info(&mut self, node_id: &PublicKey) -> Result<NodeInfo, LightningError> { |
| 549 | + Ok(self.network.lock().await.lookup_node(node_id).await?.0) |
| 550 | + } |
| 551 | + |
| 552 | + async fn list_channels(&mut self) -> Result<Vec<u64>, LightningError> { |
| 553 | + Ok(self |
| 554 | + .network |
| 555 | + .lock() |
| 556 | + .await |
| 557 | + .lookup_node(&self.info.pubkey) |
| 558 | + .await? |
| 559 | + .1) |
| 560 | + } |
| 561 | +} |
| 562 | + |
| 563 | +/// WrappedLog implements LDK's logging trait so that we can provide pathfinding with a logger that uses our existing |
| 564 | +/// logger. |
| 565 | +pub struct WrappedLog {} |
| 566 | + |
| 567 | +impl Logger for WrappedLog { |
| 568 | + fn log(&self, record: Record) { |
| 569 | + match record.level { |
| 570 | + Level::Gossip => log::trace!("{}", record.args), |
| 571 | + Level::Trace => log::trace!("{}", record.args), |
| 572 | + Level::Debug => log::debug!("{}", record.args), |
| 573 | + // LDK has quite noisy info logging for pathfinding, so we downgrade their info logging to our debug level. |
| 574 | + Level::Info => log::debug!("{}", record.args), |
| 575 | + Level::Warn => log::warn!("{}", record.args), |
| 576 | + Level::Error => log::error!("{}", record.args), |
| 577 | + } |
| 578 | + } |
| 579 | +} |
0 commit comments