Skip to content

Commit 7a0a7fd

Browse files
Add support for partial updates to LSPS2 service configuration
- Introduced `LSPS2ServiceConfigUpdate` struct for partial updates. - Updated `LSPS2Service` to use `Arc<RwLock<LSPS2ServiceConfig>>` for thread-safe configuration. - Implemented `update_lsps2_service_config_partial` method for applying updates. - Enhanced error handling for unconfigured services and invalid configurations. - Added integration tests for partial configuration updates.
1 parent 4cbd1bb commit 7a0a7fd

File tree

4 files changed

+180
-10
lines changed

4 files changed

+180
-10
lines changed

src/error.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,12 @@ pub enum Error {
121121
LiquiditySourceUnavailable,
122122
/// The given operation failed due to the LSP's required opening fee being too high.
123123
LiquidityFeeTooHigh,
124+
/// The given LSP2 configuration is invalid.
125+
InvalidLSP2Config,
126+
/// The LSP2 service is not configured.
127+
LSP2ServiceNotConfigured,
128+
/// No liquidity source is configured.
129+
NoLiquiditySourceConfigured,
124130
/// The given blinded paths are invalid.
125131
InvalidBlindedPaths,
126132
/// Asynchronous payment services are disabled.
@@ -198,6 +204,9 @@ impl fmt::Display for Error {
198204
Self::LiquidityFeeTooHigh => {
199205
write!(f, "The given operation failed due to the LSP's required opening fee being too high.")
200206
},
207+
Self::InvalidLSP2Config => write!(f, "The given LSP2 configuration is invalid."),
208+
Self::LSP2ServiceNotConfigured => write!(f, "The LSP2 service is not configured."),
209+
Self::NoLiquiditySourceConfigured => write!(f, "No liquidity source is configured."),
201210
Self::InvalidBlindedPaths => write!(f, "The given blinded paths are invalid."),
202211
Self::AsyncPaymentServicesDisabled => {
203212
write!(f, "Asynchronous payment services are disabled.")

src/lib.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -963,6 +963,18 @@ impl Node {
963963
))
964964
}
965965

966+
/// Updates the LSPS2 service configuration with the provided partial update.
967+
///
968+
/// Returns an error if no liquidity source is configured.
969+
pub fn lsps2_update_service_config(
970+
&self, config_update: liquidity::LSPS2ServiceConfigUpdate,
971+
) -> Result<(), Error> {
972+
match self.liquidity_source.as_ref() {
973+
Some(ls) => ls.lsps2_update_service_config(config_update),
974+
None => Err(Error::NoLiquiditySourceConfigured),
975+
}
976+
}
977+
966978
/// Retrieve a list of known channels.
967979
pub fn list_channels(&self) -> Vec<ChannelDetails> {
968980
self.channel_manager.list_channels().into_iter().map(|c| c.into()).collect()

src/liquidity.rs

Lines changed: 103 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ pub(crate) struct LSPS2ClientConfig {
9090
}
9191

9292
struct LSPS2Service {
93-
service_config: LSPS2ServiceConfig,
93+
service_config: Arc<RwLock<LSPS2ServiceConfig>>,
9494
ldk_service_config: LdkLSPS2ServiceConfig,
9595
}
9696

@@ -132,6 +132,60 @@ pub struct LSPS2ServiceConfig {
132132
pub max_payment_size_msat: u64,
133133
}
134134

135+
/// Represents a partial update to the LSPS2 service configuration.
136+
///
137+
/// Only fields set to `Some` will be updated; fields set to `None` will be ignored.
138+
#[derive(Debug, Clone, Default)]
139+
pub struct LSPS2ServiceConfigUpdate {
140+
/// Update the required token (None to remove, Some to set).
141+
pub require_token: Option<Option<String>>,
142+
/// Update the channel opening fee in parts-per-million.
143+
pub channel_opening_fee_ppm: Option<u32>,
144+
/// Update the proportional overprovisioning for the channel.
145+
pub channel_over_provisioning_ppm: Option<u32>,
146+
/// Update the minimum fee required for opening a channel.
147+
pub min_channel_opening_fee_msat: Option<u64>,
148+
/// Update the minimum number of blocks to keep the channel open.
149+
pub min_channel_lifetime: Option<u32>,
150+
/// Update the maximum client to_self_delay.
151+
pub max_client_to_self_delay: Option<u32>,
152+
/// Update the minimum payment size.
153+
pub min_payment_size_msat: Option<u64>,
154+
/// Update the maximum payment size.
155+
pub max_payment_size_msat: Option<u64>,
156+
}
157+
158+
impl LSPS2ServiceConfig {
159+
/// Applies a partial update to the configuration.
160+
/// Only fields present in `update` (i.e., `Some`) will be changed.
161+
pub fn apply_update(&mut self, update: LSPS2ServiceConfigUpdate) {
162+
if let Some(require_token) = update.require_token {
163+
self.require_token = require_token;
164+
}
165+
if let Some(channel_opening_fee_ppm) = update.channel_opening_fee_ppm {
166+
self.channel_opening_fee_ppm = channel_opening_fee_ppm;
167+
}
168+
if let Some(channel_over_provisioning_ppm) = update.channel_over_provisioning_ppm {
169+
self.channel_over_provisioning_ppm = channel_over_provisioning_ppm;
170+
}
171+
if let Some(min_channel_opening_fee_msat) = update.min_channel_opening_fee_msat {
172+
self.min_channel_opening_fee_msat = min_channel_opening_fee_msat;
173+
}
174+
if let Some(min_channel_lifetime) = update.min_channel_lifetime {
175+
self.min_channel_lifetime = min_channel_lifetime;
176+
}
177+
if let Some(max_client_to_self_delay) = update.max_client_to_self_delay {
178+
self.max_client_to_self_delay = max_client_to_self_delay;
179+
}
180+
if let Some(min_payment_size_msat) = update.min_payment_size_msat {
181+
self.min_payment_size_msat = min_payment_size_msat;
182+
}
183+
if let Some(max_payment_size_msat) = update.max_payment_size_msat {
184+
self.max_payment_size_msat = max_payment_size_msat;
185+
}
186+
}
187+
}
188+
135189
pub(crate) struct LiquiditySourceBuilder<L: Deref>
136190
where
137191
L::Target: LdkLogger,
@@ -217,16 +271,29 @@ where
217271
&mut self, promise_secret: [u8; 32], service_config: LSPS2ServiceConfig,
218272
) -> &mut Self {
219273
let ldk_service_config = LdkLSPS2ServiceConfig { promise_secret };
220-
self.lsps2_service = Some(LSPS2Service { service_config, ldk_service_config });
274+
self.lsps2_service = Some(LSPS2Service {
275+
service_config: Arc::new(RwLock::new(service_config)),
276+
ldk_service_config,
277+
});
221278
self
222279
}
223280

224281
pub(crate) async fn build(self) -> Result<LiquiditySource<L>, BuildError> {
225-
let liquidity_service_config = self.lsps2_service.as_ref().map(|s| {
282+
let liquidity_service_config = self.lsps2_service.as_ref().and_then(|s| {
226283
let lsps2_service_config = Some(s.ldk_service_config.clone());
227284
let lsps5_service_config = None;
228-
let advertise_service = s.service_config.advertise_service;
229-
LiquidityServiceConfig { lsps2_service_config, lsps5_service_config, advertise_service }
285+
let advertise_service = match s.service_config.read() {
286+
Ok(cfg) => cfg.advertise_service,
287+
Err(e) => {
288+
log_error!(self.logger, "Failed to acquire LSPS2 service config lock: {:?}", e);
289+
return None;
290+
},
291+
};
292+
Some(LiquidityServiceConfig {
293+
lsps2_service_config,
294+
lsps5_service_config,
295+
advertise_service,
296+
})
230297
});
231298

232299
let lsps1_client_config = self.lsps1_client.as_ref().map(|s| s.ldk_client_config.clone());
@@ -305,6 +372,13 @@ where
305372
self.lsps2_client.as_ref().map(|s| (s.lsp_node_id, s.lsp_address.clone()))
306373
}
307374

375+
fn get_lsps2_service_config(&self) -> Option<LSPS2ServiceConfig> {
376+
self.lsps2_service
377+
.as_ref()
378+
.and_then(|s| s.service_config.read().ok())
379+
.map(|cfg| cfg.clone()) // Clone to release lock
380+
}
381+
308382
pub(crate) async fn handle_next_event(&self) {
309383
match self.liquidity_manager.next_event_async().await {
310384
LiquidityEvent::LSPS1Client(LSPS1ClientEvent::SupportedOptionsReady {
@@ -485,7 +559,7 @@ where
485559
self.liquidity_manager.lsps2_service_handler().as_ref()
486560
{
487561
let service_config = if let Some(service_config) =
488-
self.lsps2_service.as_ref().map(|s| s.service_config.clone())
562+
self.get_lsps2_service_config()
489563
{
490564
service_config
491565
} else {
@@ -554,7 +628,7 @@ where
554628
self.liquidity_manager.lsps2_service_handler().as_ref()
555629
{
556630
let service_config = if let Some(service_config) =
557-
self.lsps2_service.as_ref().map(|s| s.service_config.clone())
631+
self.get_lsps2_service_config()
558632
{
559633
service_config
560634
} else {
@@ -626,9 +700,7 @@ where
626700
return;
627701
};
628702

629-
let service_config = if let Some(service_config) =
630-
self.lsps2_service.as_ref().map(|s| s.service_config.clone())
631-
{
703+
let service_config = if let Some(service_config) = self.get_lsps2_service_config() {
632704
service_config
633705
} else {
634706
log_error!(self.logger, "Failed to handle LSPS2ServiceEvent as LSPS2 liquidity service was not configured.",);
@@ -1000,6 +1072,27 @@ where
10001072
Ok(response)
10011073
}
10021074

1075+
pub(crate) fn lsps2_update_service_config(
1076+
&self, update: LSPS2ServiceConfigUpdate,
1077+
) -> Result<(), Error> {
1078+
if let Some(lsps2_service) = self.lsps2_service.as_ref() {
1079+
if let Ok(mut config) = lsps2_service.service_config.write() {
1080+
config.apply_update(update);
1081+
log_info!(self.logger, "Partially updated LSPS2 service configuration.");
1082+
Ok(())
1083+
} else {
1084+
log_error!(self.logger, "Failed to acquire LSPS2 service config lock.");
1085+
Err(Error::InvalidLSP2Config)
1086+
}
1087+
} else {
1088+
log_error!(
1089+
self.logger,
1090+
"Failed to update LSPS2 service config as LSPS2 service was not configured."
1091+
);
1092+
Err(Error::LSP2ServiceNotConfigured)
1093+
}
1094+
}
1095+
10031096
pub(crate) async fn lsps2_receive_to_jit_channel(
10041097
&self, amount_msat: u64, description: &Bolt11InvoiceDescription, expiry_secs: u32,
10051098
max_total_lsp_fee_limit_msat: Option<u64>, payment_hash: Option<PaymentHash>,

tests/integration_tests_rust.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1741,6 +1741,62 @@ fn lsps2_client_service_integration() {
17411741

17421742
expect_event!(payer_node, PaymentFailed);
17431743
assert_eq!(client_node.payment(&payment_id).unwrap().status, PaymentStatus::Failed);
1744+
1745+
////////////////////////////////////////////////////////////////////////////
1746+
// Test partial configuration updates on the provider side
1747+
////////////////////////////////////////////////////////////////////////////
1748+
// Update the channel opening fee to a new value
1749+
let new_channel_opening_fee_ppm = 20_000;
1750+
let new_channel_over_provisioning_ppm = 150_000; // Increase overprovisioning to 15%
1751+
service_node
1752+
.lsps2_update_service_config(ldk_node::liquidity::LSPS2ServiceConfigUpdate {
1753+
channel_opening_fee_ppm: Some(new_channel_opening_fee_ppm),
1754+
channel_over_provisioning_ppm: Some(new_channel_over_provisioning_ppm),
1755+
..Default::default()
1756+
})
1757+
.unwrap();
1758+
1759+
// Generate a new JIT invoice to test the updated fees
1760+
let jit_amount_msat = 150_000_000;
1761+
let jit_invoice = client_node
1762+
.bolt11_payment()
1763+
.receive_via_jit_channel(jit_amount_msat, &invoice_description.into(), 1024, None)
1764+
.unwrap();
1765+
1766+
// Pay the invoice and check if the new fee is applied
1767+
let payment_id = payer_node.bolt11_payment().send(&jit_invoice, None).unwrap();
1768+
let funding_txo = expect_channel_pending_event!(service_node, client_node.node_id());
1769+
expect_channel_ready_event!(service_node, client_node.node_id());
1770+
expect_event!(service_node, PaymentForwarded);
1771+
expect_channel_pending_event!(client_node, service_node.node_id());
1772+
expect_channel_ready_event!(client_node, service_node.node_id());
1773+
1774+
// Calculate expected fee with the new rate
1775+
let new_service_fee_msat = (jit_amount_msat * new_channel_opening_fee_ppm as u64) / 1_000_000;
1776+
let expected_received_amount_msat = jit_amount_msat - new_service_fee_msat;
1777+
expect_payment_successful_event!(payer_node, Some(payment_id), None);
1778+
let client_payment_id =
1779+
expect_payment_received_event!(client_node, expected_received_amount_msat).unwrap();
1780+
let client_payment = client_node.payment(&client_payment_id).unwrap();
1781+
match client_payment.kind {
1782+
PaymentKind::Bolt11Jit { counterparty_skimmed_fee_msat, .. } => {
1783+
assert_eq!(counterparty_skimmed_fee_msat, Some(new_service_fee_msat));
1784+
},
1785+
_ => panic!("Unexpected payment kind"),
1786+
}
1787+
1788+
// Check if the new overprovisioning is applied
1789+
let expected_channel_overprovisioning_msat =
1790+
(expected_received_amount_msat * new_channel_over_provisioning_ppm as u64) / 1_000_000;
1791+
let expected_channel_size_sat =
1792+
(expected_received_amount_msat + expected_channel_overprovisioning_msat) / 1000;
1793+
let channel_value_sats = client_node
1794+
.list_channels()
1795+
.iter()
1796+
.find(|c| c.funding_txo == Some(funding_txo))
1797+
.unwrap()
1798+
.channel_value_sats;
1799+
assert_eq!(channel_value_sats, expected_channel_size_sat);
17441800
}
17451801

17461802
#[test]

0 commit comments

Comments
 (0)