Skip to content

Commit 008c728

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 e4bb615 commit 008c728

File tree

4 files changed

+185
-10
lines changed

4 files changed

+185
-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 update_lsps2_service_config_partial(
970+
&self, config_update: liquidity::LSPS2ServiceConfigUpdate,
971+
) -> Result<(), Error> {
972+
match self.liquidity_source.as_ref() {
973+
Some(ls) => ls.update_lsps2_service_config_partial(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: 108 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ pub(crate) struct LSPS2ClientConfig {
8888
}
8989

9090
struct LSPS2Service {
91-
service_config: LSPS2ServiceConfig,
91+
service_config: Arc<RwLock<LSPS2ServiceConfig>>,
9292
ldk_service_config: LdkLSPS2ServiceConfig,
9393
}
9494

@@ -130,6 +130,65 @@ pub struct LSPS2ServiceConfig {
130130
pub max_payment_size_msat: u64,
131131
}
132132

133+
/// Represents a partial update to the LSPS2 service configuration.
134+
///
135+
/// Only fields set to `Some` will be updated; fields set to `None` will be ignored.
136+
#[derive(Debug, Clone, Default)]
137+
pub struct LSPS2ServiceConfigUpdate {
138+
/// Update the required token (None to remove, Some to set).
139+
pub require_token: Option<Option<String>>,
140+
/// Update whether the service should be advertised.
141+
pub advertise_service: Option<bool>,
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(advertise_service) = update.advertise_service {
166+
self.advertise_service = advertise_service;
167+
}
168+
if let Some(channel_opening_fee_ppm) = update.channel_opening_fee_ppm {
169+
self.channel_opening_fee_ppm = channel_opening_fee_ppm;
170+
}
171+
if let Some(channel_over_provisioning_ppm) = update.channel_over_provisioning_ppm {
172+
self.channel_over_provisioning_ppm = channel_over_provisioning_ppm;
173+
}
174+
if let Some(min_channel_opening_fee_msat) = update.min_channel_opening_fee_msat {
175+
self.min_channel_opening_fee_msat = min_channel_opening_fee_msat;
176+
}
177+
if let Some(min_channel_lifetime) = update.min_channel_lifetime {
178+
self.min_channel_lifetime = min_channel_lifetime;
179+
}
180+
if let Some(max_client_to_self_delay) = update.max_client_to_self_delay {
181+
self.max_client_to_self_delay = max_client_to_self_delay;
182+
}
183+
if let Some(min_payment_size_msat) = update.min_payment_size_msat {
184+
self.min_payment_size_msat = min_payment_size_msat;
185+
}
186+
if let Some(max_payment_size_msat) = update.max_payment_size_msat {
187+
self.max_payment_size_msat = max_payment_size_msat;
188+
}
189+
}
190+
}
191+
133192
pub(crate) struct LiquiditySourceBuilder<L: Deref>
134193
where
135194
L::Target: LdkLogger,
@@ -212,16 +271,29 @@ where
212271
&mut self, promise_secret: [u8; 32], service_config: LSPS2ServiceConfig,
213272
) -> &mut Self {
214273
let ldk_service_config = LdkLSPS2ServiceConfig { promise_secret };
215-
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+
});
216278
self
217279
}
218280

219281
pub(crate) async fn build(self) -> Result<LiquiditySource<L>, BuildError> {
220-
let liquidity_service_config = self.lsps2_service.as_ref().map(|s| {
282+
let liquidity_service_config = self.lsps2_service.as_ref().and_then(|s| {
221283
let lsps2_service_config = Some(s.ldk_service_config.clone());
222284
let lsps5_service_config = None;
223-
let advertise_service = s.service_config.advertise_service;
224-
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+
})
225297
});
226298

227299
let lsps1_client_config = self.lsps1_client.as_ref().map(|s| s.ldk_client_config.clone());
@@ -299,6 +371,34 @@ where
299371
self.lsps2_client.as_ref().map(|s| (s.lsp_node_id, s.lsp_address.clone()))
300372
}
301373

374+
pub(crate) fn update_lsps2_service_config_partial(
375+
&self, update: LSPS2ServiceConfigUpdate,
376+
) -> Result<(), Error> {
377+
if let Some(lsps2_service) = self.lsps2_service.as_ref() {
378+
if let Ok(mut config) = lsps2_service.service_config.write() {
379+
config.apply_update(update);
380+
log_info!(self.logger, "Partially updated LSPS2 service configuration.");
381+
Ok(())
382+
} else {
383+
log_error!(self.logger, "Failed to acquire LSPS2 service config lock.");
384+
Err(Error::InvalidLSP2Config)
385+
}
386+
} else {
387+
log_error!(
388+
self.logger,
389+
"Failed to update LSPS2 service config as LSPS2 service was not configured."
390+
);
391+
Err(Error::LSP2ServiceNotConfigured)
392+
}
393+
}
394+
395+
fn get_lsps2_service_config(&self) -> Option<LSPS2ServiceConfig> {
396+
self.lsps2_service
397+
.as_ref()
398+
.and_then(|s| s.service_config.read().ok())
399+
.map(|cfg| cfg.clone()) // Clone to release lock
400+
}
401+
302402
pub(crate) async fn handle_next_event(&self) {
303403
match self.liquidity_manager.next_event_async().await {
304404
LiquidityEvent::LSPS1Client(LSPS1ClientEvent::SupportedOptionsReady {
@@ -479,7 +579,7 @@ where
479579
self.liquidity_manager.lsps2_service_handler().as_ref()
480580
{
481581
let service_config = if let Some(service_config) =
482-
self.lsps2_service.as_ref().map(|s| s.service_config.clone())
582+
self.get_lsps2_service_config()
483583
{
484584
service_config
485585
} else {
@@ -548,7 +648,7 @@ where
548648
self.liquidity_manager.lsps2_service_handler().as_ref()
549649
{
550650
let service_config = if let Some(service_config) =
551-
self.lsps2_service.as_ref().map(|s| s.service_config.clone())
651+
self.get_lsps2_service_config()
552652
{
553653
service_config
554654
} else {
@@ -620,9 +720,7 @@ where
620720
return;
621721
};
622722

623-
let service_config = if let Some(service_config) =
624-
self.lsps2_service.as_ref().map(|s| s.service_config.clone())
625-
{
723+
let service_config = if let Some(service_config) = self.get_lsps2_service_config() {
626724
service_config
627725
} else {
628726
log_error!(self.logger, "Failed to handle LSPS2ServiceEvent as LSPS2 liquidity service was not configured.",);

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+
.update_lsps2_service_config_partial(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)