Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
85ba3e4
plugins(lsps2): add session FSM for LSPS2 payment collection
nepet Mar 18, 2026
2d79a6c
plugins(lsps2): add session actor with action executor boundary
nepet Mar 18, 2026
6fe7ff7
plugins(lsps2): add session manager and unify HTLC handling
nepet Mar 18, 2026
d4657ab
plugins(lsps2): add integration tests for session lifecycle
nepet Mar 18, 2026
3b9804f
plugins(lsps2): add restart recovery for session persistence
nepet Mar 18, 2026
d7ee591
plugins(lsps2): simplify DatastoreProvider to focused trait
nepet Mar 18, 2026
95454d2
plugins(lsps2): add EventSink trait for session event notification
nepet Mar 18, 2026
4d32656
plugins(lsps2): decouple lsps2 crate from CLN-specific types
nepet Mar 18, 2026
5213d99
plugins(lsps2): simplify actor and session manager
nepet Mar 18, 2026
eb52a1d
plugins(lsps2): make collect timeout dev config
nepet Mar 19, 2026
ddeb9de
plugins(lsps2): route recovered sessions through forward_event path
nepet Mar 19, 2026
f140c32
lsps2: adding list_finalized_sessions to the DatastoreProvider
Apr 24, 2026
072d16f
plugins(lsps2): fix onion failure codes sent when failing HTLCs
nepet Jul 3, 2026
9dddf74
plugins(lsps2): key HTLCs by (incoming channel, htlc id)
nepet Jul 3, 2026
0bfa3af
plugins(lsps2): forward replayed HTLCs to recovered sessions
nepet Jul 3, 2026
203be33
plugins(lsps2): prevent double channel funding for one payment hash
nepet Jul 3, 2026
126cef4
plugins(lsps2): fail held HTLCs before cltv expiry, not after
nepet Jul 3, 2026
62f31ad
plugins(lsps2): spawn only one channel-poll task per session
nepet Jul 3, 2026
da807f4
plugins(lsps2): harden recovery loop and fee-overflow handling
nepet Jul 3, 2026
1ca6efd
plugins(lsps2): advertise client_trusts_lsp and validate payment size…
nepet Jul 3, 2026
7f3b805
plugins(lsps2): fix opening fee base and zero extra_fee TLV
nepet Jul 3, 2026
438354c
plugins(lsps2): make channel reserve opt-in and pin datastore compat
nepet Jul 3, 2026
06ad8f1
plugins(lsps2): only abandon session once no forwarded parts remain
nepet Jul 3, 2026
8ded411
plugins(lsps2): cleanups from review
nepet Jul 3, 2026
c7bbb47
plugins(lsps2): make finalize_session idempotent
nepet Jul 29, 2026
0d3c433
plugins(lsps2): only expire offers when starting a new session
nepet Jul 29, 2026
c4fb1fb
plugins(lsps2): always release funding inputs when abandoning
nepet Jul 29, 2026
8549d0a
pytest: cover finalize_session over an existing finalized entry
nepet Jul 29, 2026
6aa8794
docs: add experimental lsps options to config
nepet Jul 31, 2026
69d9795
plugins(lsps2): Add u16 zero len to TEMPORARY_CHANNEL_FAILURE
nepet Aug 18, 2026
45033b6
tests: wait for mempool on lsps2 tests
nepet Aug 18, 2026
c2ec421
tests: relax collect timeout on lsps2 plugin tests
nepet Aug 19, 2026
98becca
tests: shorten long test name
nepet Aug 19, 2026
ab7539b
plugins(lsps2): route parts by scid, not payment hash alone
nepet Aug 20, 2026
4acf561
plugins(lsps2): fund at most one channel per jit scid
nepet Aug 20, 2026
be970b5
plugins(lsps2): monitor recovered sessions for channel death
nepet Aug 20, 2026
a9a44cc
plugins(lsps2): release the channel when recovery finds all forwards …
nepet Aug 20, 2026
31e43da
plugins(lsps2): bound variable-amount payments by the policy size range
nepet Aug 20, 2026
85fb96b
tests: channel_ready wait is bounded
nepet Aug 19, 2026
8449fad
tests: fix flaky reserved-utxo check in channel_ready wait test
nepet Aug 26, 2026
afe8d68
tests: tolerate late channel_update in force-close-abandoned test
nepet Aug 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions doc/lightningd-config.5.md
Original file line number Diff line number Diff line change
Expand Up @@ -896,6 +896,12 @@ of the prior channel balance and the new one.
subprotocol describing the creation of just-in-time-channel (JIT-channels)
between a LSP and this client.

* **experimental-lsps-client-zero-reserve** [plugin `cln-lsps-client`]

Specifying this stops requiring the LSP to maintain a channel reserve on the
JIT-channels it opens to this client. This is left unset by default, as some
LSPs can not handle a zero reserve.

* **experimental-lsps2-service** [plugin `cln-lsps-service`]

Specifying this enables a LSP JIT-Channel service according to the lsps
Expand All @@ -908,6 +914,12 @@ a *experimental-lsps2-promise-secret* to be set.
string that acts as the secret for promises according to ([blip][blip] #52).
Is required if *experimental-lsps2-service* is set.

* **experimental-lsps2-zero-reserve** [plugin `cln-lsps-service`]

Specifying this requires no channel reserve from the client on the
JIT-channels this service opens. This is left unset by default, as some
implementations can not handle an explicit zero reserve.

BUGS
----

Expand Down
15 changes: 12 additions & 3 deletions plugins/lsps-plugin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,31 @@ rust-version.workspace = true
[[bin]]
name = "cln-lsps-client"
path = "src/client.rs"
required-features = ["cln"]

[[bin]]
name = "cln-lsps-service"
path = "src/service.rs"
required-features = ["cln"]

[features]
default = ["cln"]
cln = ["cln-plugin", "cln-rpc"]

[dependencies]
anyhow = "1.0"
async-trait = "0.1"
bitcoin = "0.32.2"
bitcoin = { version = "0.32", features = ["serde"] }
chrono = { version = "0.4.42", features = ["serde"] }
cln-plugin = { workspace = true }
cln-rpc = { workspace = true }
cln-plugin = { workspace = true, optional = true }
cln-rpc = { workspace = true, optional = true }
hex = "0.4"
log = "0.4"
rand = "0.10"
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0", features = ["raw_value"] }
thiserror = "2.0"
tokio = { version = "1.44", features = ["full"] }

[dev-dependencies]
tokio = { version = "1.44", features = ["full", "test-util"] }
51 changes: 40 additions & 11 deletions plugins/lsps-plugin/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,28 @@ const OPTION_ENABLED: options::FlagConfigOption = options::ConfigOption::new_fla
"Enables an LSPS client on the node.",
);

/// Opt-in: do not require the LSP to maintain a channel reserve on JIT
/// channels. Left unset by default as some LSPs (e.g. Megalithic) cannot
/// handle a zero reserve.
const OPTION_ZERO_RESERVE: options::FlagConfigOption = options::ConfigOption::new_flag(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there no way to identify a peer that is unable to set a zero-reserve and reply based on that, rather than setting the degraded mode as default, and asking users to configure an extra flag to get the desired default behavior? Also this means a node is either fully non-0-reserver or fully 0-reserve, no mixing possible.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, do not amend this PR, create a new follow up one. experimental changes are not logged in the changelog anyway.

"experimental-lsps-client-zero-reserve",
"Do not require the LSP to maintain a channel reserve on JIT channels.",
);

const DEFAULT_REQUEST_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(60);

/// Hook response accepting a zero-conf JIT channel from the LSP.
fn openchannel_jit_response(zero_reserve: bool) -> serde_json::Value {
let mut resp = serde_json::json!({
"result": "continue",
"mindepth": 0,
});
if zero_reserve {
resp["reserve"] = 0.into();
}
resp
}

#[derive(Clone)]
pub struct State {
sender: ClnSender,
Expand Down Expand Up @@ -87,6 +107,7 @@ async fn main() -> Result<(), anyhow::Error> {
.filters(vec![HookFilter::Int(i64::from(LSPS0_MESSAGE_TYPE))]),
)
.option(OPTION_ENABLED)
.option(OPTION_ZERO_RESERVE)
.rpcmethod(
"lsps-listprotocols",
"list protocols supported by lsp",
Expand Down Expand Up @@ -269,7 +290,7 @@ async fn on_lsps_lsps2_approve(
) -> Result<serde_json::Value, anyhow::Error> {
let req: ClnRpcLsps2Approve = serde_json::from_value(v)?;
let ds_rec = DatastoreRecord {
jit_channel_scid: req.jit_channel_scid.clone(),
jit_channel_scid: req.jit_channel_scid,
client_trusts_lsp: req.client_trusts_lsp.unwrap_or_default(),
};
let ds_rec_json = serde_json::to_string(&ds_rec)?;
Expand Down Expand Up @@ -485,7 +506,7 @@ async fn on_lsps_lsps2_invoice(
// 5. Approve jit_channel_scid for a jit channel opening.
let appr_req = ClnRpcLsps2Approve {
lsp_id: req.lsp_id,
jit_channel_scid: buy_res.jit_channel_scid,
jit_channel_scid: buy_res.jit_channel_scid.into(),
payment_hash: public_inv.payment_hash.to_string(),
client_trusts_lsp: Some(buy_res.client_trusts_lsp),
};
Expand Down Expand Up @@ -711,10 +732,7 @@ async fn on_openchannel(
}
// Fixme: Check that we actually use client-trusts-LSP mode - can be
// found in the ds record.
return Ok(serde_json::json!({
"result": "continue",
"mindepth": 0,
}));
Ok(openchannel_jit_response(p.option(&OPTION_ZERO_RESERVE)?))
} else {
// Not a requested JIT-channel opening, continue.
Ok(serde_json::json!({"result": "continue"}))
Expand Down Expand Up @@ -857,11 +875,6 @@ macro_rules! ok_or_continue {
};
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
struct LspsBuyJitChannelResponse {
bolt11: String,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct InvoiceRequest {
pub amount_msat: cln_rpc::primitives::AmountOrAny,
Expand Down Expand Up @@ -921,3 +934,19 @@ struct DatastoreRecord {
jit_channel_scid: ShortChannelId,
client_trusts_lsp: bool,
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn openchannel_jit_response_reserve_is_opt_in() {
let resp = openchannel_jit_response(false);
assert_eq!(resp["result"], "continue");
assert_eq!(resp["mindepth"], 0);
assert!(resp.get("reserve").is_none());

let resp = openchannel_jit_response(true);
assert_eq!(resp["reserve"], 0);
}
}
4 changes: 2 additions & 2 deletions plugins/lsps-plugin/src/cln_adapters/hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ where
plugin.state().pending().complete(&id, hook.payload).await;
}

return Ok(serde_json::json!({
Ok(serde_json::json!({
"result": "continue"
}));
}))
}

pub async fn service_custommsg_hook<S>(plugin: Plugin<S>, v: Value) -> Result<Value>
Expand Down
5 changes: 5 additions & 0 deletions plugins/lsps-plugin/src/cln_adapters/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,8 @@ pub mod rpc;
pub mod sender;
pub mod state;
pub mod types;

pub use rpc::{
ClnActionExecutor, ClnDatastore, ClnPolicyProvider, ClnRecoveryProvider,
ClnRpcClient,
};
Loading
Loading