diff --git a/eel/src/lib.rs b/eel/src/lib.rs index e79a39d8..d51a9633 100644 --- a/eel/src/lib.rs +++ b/eel/src/lib.rs @@ -27,6 +27,7 @@ pub mod invoice; mod logger; mod random; mod rapid_sync_client; +mod router; mod storage_persister; mod task_manager; mod test_utils; @@ -55,6 +56,8 @@ use crate::task_manager::{PeriodConfig, RestartIfFailedPeriod, TaskManager, Task use crate::tx_broadcaster::TxBroadcaster; use crate::types::{ChainMonitor, ChannelManager, PeerManager, RapidGossipSync, Router, TxSync}; +pub use crate::router::MaxRoutingFeeMode; +use crate::router::{FeeLimitingRouter, SimpleMaxRoutingFeeStrategy}; use bitcoin::hashes::hex::ToHex; pub use bitcoin::Network; use cipher::consts::U32; @@ -116,6 +119,7 @@ pub struct LightningNode { peer_manager: Arc, task_manager: Arc>, data_store: Arc>, + max_routing_fee_strategy: Arc, } impl LightningNode { @@ -196,11 +200,15 @@ impl LightningNode { )); // Step 13: Initialize the Router - let router = Arc::new(Router::new( - Arc::clone(&graph), - Arc::clone(&logger), - keys_manager.get_secure_random_bytes(), - Arc::clone(&scorer), + let max_routing_fee_strategy = Arc::new(SimpleMaxRoutingFeeStrategy::new(21_000, 50)); + let router = Arc::new(FeeLimitingRouter::new( + Router::new( + Arc::clone(&graph), + Arc::clone(&logger), + keys_manager.get_secure_random_bytes(), + Arc::clone(&scorer), + ), + Arc::clone(&max_routing_fee_strategy), )); // (needed when using Electrum or BIP 157/158) @@ -339,6 +347,7 @@ impl LightningNode { peer_manager, task_manager, data_store, + max_routing_fee_strategy, }) } @@ -418,6 +427,11 @@ impl LightningNode { invoice::decode_invoice(&invoice, network) } + pub fn get_payment_max_routing_fee_mode(&self, amount_msat: u64) -> MaxRoutingFeeMode { + self.max_routing_fee_strategy + .get_payment_max_fee_mode(amount_msat) + } + pub fn pay_invoice(&self, invoice: String, metadata: String) -> PayResult<()> { let network = self.config.lock().unwrap().network; let invoice = diff --git a/eel/src/router.rs b/eel/src/router.rs new file mode 100644 index 00000000..653f2f71 --- /dev/null +++ b/eel/src/router.rs @@ -0,0 +1,140 @@ +use crate::types::Router; +use lightning::ln::channelmanager::{ChannelDetails, PaymentId}; +use lightning::ln::msgs::{ErrorAction, LightningError}; +use lightning::ln::PaymentHash; +use lightning::routing::router::{InFlightHtlcs, Route, RouteParameters}; +use secp256k1::PublicKey; +use std::sync::Arc; + +pub enum MaxRoutingFeeMode { + Relative { max_fee_permyriad: u16 }, + Absolute { max_fee_msat: u64 }, +} + +pub(crate) struct SimpleMaxRoutingFeeStrategy { + min_max_fee_msat: u64, + max_relative_fee_permyriad: u16, +} + +impl SimpleMaxRoutingFeeStrategy { + pub fn new(min_max_fee_msat: u64, max_relative_fee_permyriad: u16) -> Self { + SimpleMaxRoutingFeeStrategy { + min_max_fee_msat, + max_relative_fee_permyriad, + } + } + + pub fn get_payment_max_fee_mode(&self, payment_amount_msat: u64) -> MaxRoutingFeeMode { + let threshold = self.min_max_fee_msat * 10_000 / self.max_relative_fee_permyriad as u64; + + if payment_amount_msat > threshold { + MaxRoutingFeeMode::Relative { + max_fee_permyriad: self.max_relative_fee_permyriad, + } + } else { + MaxRoutingFeeMode::Absolute { + max_fee_msat: self.min_max_fee_msat, + } + } + } + + pub fn compute_max_fee_msat(&self, payment_amount_msat: u64) -> u64 { + match self.get_payment_max_fee_mode(payment_amount_msat) { + MaxRoutingFeeMode::Relative { max_fee_permyriad } => { + payment_amount_msat * max_fee_permyriad as u64 / 10000 + } + MaxRoutingFeeMode::Absolute { max_fee_msat } => max_fee_msat, + } + } +} + +pub(crate) struct FeeLimitingRouter { + inner: Router, + max_fee_strategy: Arc, +} + +impl FeeLimitingRouter { + pub fn new(router: Router, max_fee_strategy: Arc) -> Self { + FeeLimitingRouter { + inner: router, + max_fee_strategy, + } + } +} + +impl lightning::routing::router::Router for FeeLimitingRouter { + fn find_route( + &self, + payer: &PublicKey, + route_params: &RouteParameters, + first_hops: Option<&[&ChannelDetails]>, + inflight_htlcs: &InFlightHtlcs, + ) -> Result { + let max_fee_msat = self + .max_fee_strategy + .compute_max_fee_msat(route_params.final_value_msat); + + let route = self + .inner + .find_route(payer, route_params, first_hops, inflight_htlcs)?; + let route_fees = route.get_total_fees(); + if route_fees > max_fee_msat { + Err(LightningError { + err: format!("Route's fees exceed maximum allowed - max allowed: {max_fee_msat} - route's fees {route_fees}"), + action: ErrorAction::IgnoreError, + }) + } else { + Ok(route) + } + } + + fn find_route_with_id( + &self, + payer: &PublicKey, + route_params: &RouteParameters, + first_hops: Option<&[&ChannelDetails]>, + inflight_htlcs: &InFlightHtlcs, + payment_hash: PaymentHash, + payment_id: PaymentId, + ) -> Result { + let max_fee_msat = self + .max_fee_strategy + .compute_max_fee_msat(route_params.final_value_msat); + + let route = self.inner.find_route_with_id( + payer, + route_params, + first_hops, + inflight_htlcs, + payment_hash, + payment_id, + )?; + let route_fees = route.get_total_fees(); + if route_fees > max_fee_msat { + Err(LightningError { + err: format!("Route's fees exceed maximum allowed - max allowed: {max_fee_msat} - route's fees {route_fees}"), + action: ErrorAction::IgnoreError, + }) + } else { + Ok(route) + } + } +} + +#[cfg(test)] +mod tests { + use crate::router::SimpleMaxRoutingFeeStrategy; + + #[test] + fn test_simple_max_routing_fee_strategy() { + let max_fee_strategy = SimpleMaxRoutingFeeStrategy::new(21000, 50); + + assert_eq!(max_fee_strategy.compute_max_fee_msat(0), 21_000); + assert_eq!(max_fee_strategy.compute_max_fee_msat(21_000), 21_000); + assert_eq!(max_fee_strategy.compute_max_fee_msat(4199_000), 21_000); + assert_eq!(max_fee_strategy.compute_max_fee_msat(4200_000), 21_000); + assert_eq!(max_fee_strategy.compute_max_fee_msat(4201_000), 21_005); + assert_eq!(max_fee_strategy.compute_max_fee_msat(4399_000), 21_995); + assert_eq!(max_fee_strategy.compute_max_fee_msat(4400_000), 22_000); + } +} diff --git a/eel/src/storage_persister.rs b/eel/src/storage_persister.rs index dd4cc965..b412bd54 100644 --- a/eel/src/storage_persister.rs +++ b/eel/src/storage_persister.rs @@ -1,12 +1,11 @@ use crate::encryption_symmetric::{decrypt, encrypt}; use crate::errors::*; use crate::interfaces::RemoteStorage; -use crate::types::{ - ChainMonitor, ChannelManager, ChannelManagerReadArgs, NetworkGraph, Router, Scorer, -}; +use crate::types::{ChainMonitor, ChannelManager, ChannelManagerReadArgs, NetworkGraph, Scorer}; use crate::LightningLogger; use std::cmp::Ordering; +use crate::router::FeeLimitingRouter; use crate::tx_broadcaster::TxBroadcaster; use bitcoin::hash_types::BlockHash; use bitcoin::hashes::hex::ToHex; @@ -20,7 +19,7 @@ use lightning::chain::keysinterface::{ }; use lightning::chain::transaction::OutPoint; use lightning::chain::{ChannelMonitorUpdateStatus, Watch}; -use lightning::ln::channelmanager::{ChainParameters, SimpleArcChannelManager}; +use lightning::ln::channelmanager::ChainParameters; use lightning::routing::router; use lightning::routing::scoring::{ProbabilisticScoringParameters, WriteableScore}; use lightning::util::config::UserConfig; @@ -253,7 +252,7 @@ impl StoragePersister { keys_manager: Arc, fee_estimator: Arc, logger: Arc, - router: Arc, + router: Arc, channel_monitors: Vec<&mut ChannelMonitor>, user_config: UserConfig, chain_params: ChainParameters, @@ -275,7 +274,7 @@ impl StoragePersister { match local_channel_manager { None => { - let channel_manager = SimpleArcChannelManager::new( + let channel_manager = ChannelManager::new( fee_estimator, chain_monitor, broadcaster, diff --git a/eel/src/types.rs b/eel/src/types.rs index 3611ea84..52ede788 100644 --- a/eel/src/types.rs +++ b/eel/src/types.rs @@ -3,9 +3,9 @@ use crate::logger::LightningLogger; use crate::storage_persister::StoragePersister; use crate::tx_broadcaster::TxBroadcaster; +use crate::router::FeeLimitingRouter; use lightning::chain::chainmonitor::ChainMonitor as LdkChainMonitor; use lightning::chain::keysinterface::{InMemorySigner, KeysManager}; -use lightning::ln::channelmanager::SimpleArcChannelManager; use lightning::ln::peer_handler::IgnoringMessageHandler; use lightning::routing::router::DefaultRouter; use lightning::routing::scoring::ProbabilisticScorer; @@ -24,8 +24,16 @@ pub(crate) type ChainMonitor = LdkChainMonitor< Arc, >; -pub(crate) type ChannelManager = - SimpleArcChannelManager; +pub(crate) type ChannelManager = lightning::ln::channelmanager::ChannelManager< + Arc, + Arc, + Arc, + Arc, + Arc, + Arc, + Arc, + Arc, +>; pub(crate) type ChannelManagerReadArgs<'a> = lightning::ln::channelmanager::ChannelManagerReadArgs< 'a, @@ -35,7 +43,7 @@ pub(crate) type ChannelManagerReadArgs<'a> = lightning::ln::channelmanager::Chan Arc, Arc, Arc, - Arc, + Arc, Arc, >; diff --git a/examples/3l-node/cli.rs b/examples/3l-node/cli.rs index 971b87e6..51c9f445 100644 --- a/examples/3l-node/cli.rs +++ b/examples/3l-node/cli.rs @@ -1,6 +1,6 @@ use crate::hinter::{CommandHint, CommandHinter}; -use uniffi_lipalightninglib::{Amount, TzConfig}; +use uniffi_lipalightninglib::{Amount, MaxRoutingFeeMode, TzConfig}; use bitcoin::secp256k1::PublicKey; use chrono::offset::FixedOffset; @@ -92,6 +92,11 @@ pub(crate) fn poll_for_user_input(node: &LightningNode, log_file_path: &str) { println!("{}", message.red()); } } + "getmaxroutingfeemode" => { + if let Err(message) = get_max_routing_fee_mode(node, &mut words) { + println!("{}", message.red()); + } + } "payinvoice" => { if let Err(message) = pay_invoice(node, &mut words) { println!("{}", message.red()); @@ -158,6 +163,10 @@ fn setup_editor(history_path: &Path) -> Editor { "decodeinvoice ", "decodeinvoice ", )); + hints.insert(CommandHint::new( + "getmaxroutingfeemode ", + "getmaxroutingfeemode ", + )); hints.insert(CommandHint::new("payinvoice ", "payinvoice ")); hints.insert(CommandHint::new( "payopeninvoice ", @@ -189,6 +198,7 @@ fn help() { println!(); println!(" invoice [description]"); println!(" decodeinvoice "); + println!(" getmaxroutingfeemode "); println!(" payinvoice "); println!(" payopeninvoice "); println!(); @@ -388,6 +398,38 @@ fn decode_invoice( Ok(()) } +fn get_max_routing_fee_mode( + node: &LightningNode, + words: &mut dyn Iterator, +) -> Result<(), String> { + let amount_argument = match words.next() { + Some(amount) => match amount.parse::() { + Ok(parsed) => Ok(parsed), + Err(_) => return Err("Error: SAT amount must be an integer".to_string()), + }, + None => Err("The payment amount in SAT is required".to_string()), + }?; + + let max_fee_strategy = node.get_payment_max_routing_fee_mode(amount_argument); + + match max_fee_strategy { + MaxRoutingFeeMode::Relative { max_fee_permyriad } => { + println!( + "Max fee strategy: Relative (<= {} %)", + max_fee_permyriad as f64 / 100.0 + ); + } + MaxRoutingFeeMode::Absolute { max_fee_amount } => { + println!( + "Max fee strategy: Absolute (<= {})", + amount_to_string(max_fee_amount) + ); + } + } + + Ok(()) +} + fn pay_invoice(node: &LightningNode, words: &mut dyn Iterator) -> Result<(), String> { let invoice = words .next() diff --git a/src/lib.rs b/src/lib.rs index 16e87870..34f7bdd8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -100,6 +100,11 @@ pub struct LightningNode { core_node: eel::LightningNode, } +pub enum MaxRoutingFeeMode { + Relative { max_fee_permyriad: u16 }, + Absolute { max_fee_amount: Amount }, +} + impl LightningNode { pub fn new(config: Config, events_callback: Box) -> Result { enable_backtrace(); @@ -213,6 +218,20 @@ impl LightningNode { Ok(InvoiceDetails::from_remote_invoice(invoice, &rate)) } + pub fn get_payment_max_routing_fee_mode(&self, amount_sat: u64) -> MaxRoutingFeeMode { + match self + .core_node + .get_payment_max_routing_fee_mode(amount_sat * 1000) + { + eel::MaxRoutingFeeMode::Relative { max_fee_permyriad } => { + MaxRoutingFeeMode::Relative { max_fee_permyriad } + } + eel::MaxRoutingFeeMode::Absolute { max_fee_msat } => MaxRoutingFeeMode::Absolute { + max_fee_amount: max_fee_msat.to_amount_up(&self.get_exchange_rate()), + }, + } + } + pub fn pay_invoice(&self, invoice: String, metadata: String) -> PayResult<()> { self.core_node.pay_invoice(invoice, metadata) } diff --git a/src/lipalightninglib.udl b/src/lipalightninglib.udl index 5b6f0448..6cd2c6ce 100644 --- a/src/lipalightninglib.udl +++ b/src/lipalightninglib.udl @@ -65,6 +65,9 @@ interface LightningNode { [Throws=DecodeInvoiceError] InvoiceDetails decode_invoice(string invoice); + // Get the max routing fee mode that will be employed to restrict the fees for paying a given amount in sats + MaxRoutingFeeMode get_payment_max_routing_fee_mode(u64 amount_sat); + // Start an attempt to pay an invoice. Can immediately fail, meaning that the payment couldn't be started. // If successful, it doesn't mean that the payment itself was successful (funds received by the payee). // After this method returns, the consumer of this library will learn about a successful/failed payment through the @@ -278,6 +281,13 @@ dictionary InvoiceDetails { timestamp expiry_timestamp; // The moment an invoice expires (UTC) }; +// Indicates the max routing fee mode used to restrict fees of a payment of a given size +[Enum] +interface MaxRoutingFeeMode { + Relative(u16 max_fee_permyriad); // `max_fee_permyriad` Parts per myriad (aka basis points) -> 100 is 1% + Absolute(Amount max_fee_amount); +}; + // Information about an incoming or outgoing payment dictionary Payment { PaymentType payment_type;