Skip to content

Commit c420b7e

Browse files
committed
sim-lib: add SimNode implementation of LightningNode
Add an implementation of the LightningNode trait that represents the underlying lightning node. This implementation is intentionally kept simple, depending on some SimNetwork trait to handle the mechanics of actually simulating the flow of payments through a simulated graph.
1 parent 0e4ccd2 commit c420b7e

File tree

1 file changed

+241
-2
lines changed

1 file changed

+241
-2
lines changed

sim-lib/src/sim_node.rs

Lines changed: 241 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,24 @@
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;
316
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;
422

523
use crate::ShortChannelID;
624

@@ -338,3 +356,224 @@ impl SimulatedChannel {
338356
.check_htlc_forward(cltv_delta, amount_msat, fee_msat)
339357
}
340358
}
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

Comments
 (0)