Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a70f384
plugins(lsps2): add session FSM for LSPS2 payment collection
nepet Mar 18, 2026
ecdbc64
plugins(lsps2): add session actor with action executor boundary
nepet Mar 18, 2026
a525e13
plugins(lsps2): add session manager and unify HTLC handling
nepet Mar 18, 2026
c45e5cb
plugins(lsps2): add integration tests for session lifecycle
nepet Mar 18, 2026
0659534
plugins(lsps2): add restart recovery for session persistence
nepet Mar 18, 2026
8b35b6a
plugins(lsps2): simplify DatastoreProvider to focused trait
nepet Mar 18, 2026
99322b4
plugins(lsps2): add EventSink trait for session event notification
nepet Mar 18, 2026
5a5cdb0
plugins(lsps2): decouple lsps2 crate from CLN-specific types
nepet Mar 18, 2026
c42b911
plugins(lsps2): simplify actor and session manager
nepet Mar 18, 2026
662fdf0
plugins(lsps2): make collect timeout dev config
nepet Mar 19, 2026
ae56a54
plugins(lsps2): route recovered sessions through forward_event path
nepet Mar 19, 2026
16d662b
lsps2: adding list_finalized_sessions to the DatastoreProvider
Apr 24, 2026
0ff68c8
plugins(lsps2): fix onion failure codes sent when failing HTLCs
nepet Jul 3, 2026
7d45ab2
plugins(lsps2): key HTLCs by (incoming channel, htlc id)
nepet Jul 3, 2026
fbd7e83
plugins(lsps2): forward replayed HTLCs to recovered sessions
nepet Jul 3, 2026
52652ef
plugins(lsps2): prevent double channel funding for one payment hash
nepet Jul 3, 2026
1e4fc6a
plugins(lsps2): fail held HTLCs before cltv expiry, not after
nepet Jul 3, 2026
4869ba6
plugins(lsps2): spawn only one channel-poll task per session
nepet Jul 3, 2026
c8b70c2
plugins(lsps2): harden recovery loop and fee-overflow handling
nepet Jul 3, 2026
7374b88
plugins(lsps2): advertise client_trusts_lsp and validate payment size…
nepet Jul 3, 2026
c1e5fa7
plugins(lsps2): fix opening fee base and zero extra_fee TLV
nepet Jul 3, 2026
bf72c7a
plugins(lsps2): make channel reserve opt-in and pin datastore compat
nepet Jul 3, 2026
c442543
plugins(lsps2): only abandon session once no forwarded parts remain
nepet Jul 3, 2026
4c58bff
plugins(lsps2): cleanups from review
nepet Jul 3, 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
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(
"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