Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion libs/gl-plugin/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
//! sign off actually match the authentic commands by a valid
//! caller.

use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::Mutex;
use serde::{Serialize, Deserialize};

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Request {
Expand Down
10 changes: 6 additions & 4 deletions libs/gl-plugin/src/messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ where
/// `peer_connected` hook.
#[derive(Serialize, Deserialize, Debug)]
pub struct PeerConnectedCall {
pub peer: Peer
pub peer: Peer,
}

#[derive(Serialize, Deserialize, Debug)]
Expand All @@ -295,10 +295,9 @@ pub struct Peer {
#[serde(rename_all = "snake_case")]
pub enum Direction {
In,
Out
Out,
}


#[cfg(test)]
mod test {
use super::*;
Expand All @@ -315,7 +314,10 @@ mod test {
});

let call = serde_json::from_str::<PeerConnectedCall>(&msg.to_string()).unwrap();
assert_eq!(call.peer.id, "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f");
assert_eq!(
call.peer.id,
"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"
);
assert_eq!(call.peer.direction, Direction::In);
assert_eq!(call.peer.addr, "34.239.230.56:9735");
assert_eq!(call.peer.features, "");
Expand Down
151 changes: 125 additions & 26 deletions libs/gl-plugin/src/node/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,8 @@ use anyhow::{Context, Error, Result};
use base64::{engine::general_purpose, Engine as _};
use bytes::BufMut;
use cln_rpc::Notification;
use gl_client::metrics::{savings_percent, signer_state_request_wire_bytes};
use gl_client::persist::{State, StateSketch};
use gl_client::metrics::{
signer_state_request_wire_bytes, savings_percent,
};
use governor::{
clock::MonotonicClock, state::direct::NotKeyed, state::InMemoryState, Quota, RateLimiter,
};
Expand Down Expand Up @@ -230,14 +228,12 @@ impl Node for PluginNodeServer {
// We require capacity + 5% buffer to account for fees and routing.
// Only check for specific amounts (not "any" amount invoices).
if req.amount_msat > 0 {
let receivable = self
.get_receivable_capacity(&mut rpc)
.await
.unwrap_or(0);
let receivable = self.get_receivable_capacity(&mut rpc).await.unwrap_or(0);

// Add 5% buffer: capacity >= amount * 1.05
// Equivalent to: capacity * 100 >= amount * 105
let has_sufficient_capacity = req.amount_msat
let has_sufficient_capacity = req
.amount_msat
.saturating_mul(105)
.checked_div(100)
.map(|required| receivable >= required)
Expand Down Expand Up @@ -274,7 +270,10 @@ impl Node for PluginNodeServer {
bolt11: res.bolt11,
created_index: res.created_index.unwrap_or(0) as u32,
expires_at: res.expires_at as u32,
payment_hash: <cln_rpc::primitives::Sha256 as Borrow<[u8]>>::borrow(&res.payment_hash).to_vec(),
payment_hash: <cln_rpc::primitives::Sha256 as Borrow<[u8]>>::borrow(
&res.payment_hash,
)
.to_vec(),
payment_secret: res.payment_secret.to_vec(),
opening_fee_msat: 0,
}));
Expand All @@ -300,35 +299,42 @@ impl Node for PluginNodeServer {
Status::not_found("Could not retrieve LSPS peers for invoice negotiation.")
})?;

if lsps.len() < 1 {
if lsps.is_empty() {
return Err(Status::not_found(
"Could not find an LSP peer to negotiate the LSPS2 channel for this invoice.",
));
}

let lsp = &lsps[0];
log::info!("Selecting {:?} for invoice negotiation", lsp);
let (lsp_id, param) = select_opening_params(lsps).ok_or_else(|| {
Status::not_found("No opening params returned by any LSP, cannot create invoice.")
})?;

log::info!(
"Selecting LSP {} with params {:?} for invoice negotiation",
lsp_id,
param
);

// Compute the expected opening fee from the LSP's fee parameters.
let opening_fee_msat = lsp.params.first().map_or(0, |p| {
let min_fee: u64 = p.min_fee_msat.parse().unwrap_or(0);
let opening_fee_msat = {
let min_fee: u64 = param.min_fee_msat.parse().unwrap_or(0);
let proportional_fee = req
.amount_msat
.saturating_mul(p.proportional)
.saturating_mul(param.proportional)
.div_ceil(1_000_000);
std::cmp::max(min_fee, proportional_fee)
});
};

// Use the new RPC method name for versions > v25.05gl1
let mut res = if *version > *"v25.05gl1" {
let mut invreq: crate::requests::LspInvoiceRequestV2 = req.into();
invreq.lsp_id = lsp.node_id.to_owned();
invreq.lsp_id = lsp_id.to_owned();
rpc.call_typed(&invreq)
.await
.map_err(|e| Status::new(Code::Internal, e.to_string()))?
} else {
let mut invreq: crate::requests::LspInvoiceRequest = req.into();
invreq.lsp_id = lsp.node_id.to_owned();
invreq.lsp_id = lsp_id.to_owned();
rpc.call_typed(&invreq)
.await
.map_err(|e| Status::new(Code::Internal, e.to_string()))?
Expand Down Expand Up @@ -446,9 +452,8 @@ impl Node for PluginNodeServer {
// the large state with them.

let state_snapshot = signer_state.lock().await.clone();
let state_entries: Vec<gl_client::pb::SignerStateEntry> = state_snapshot
.omit_tombstones()
.into();
let state_entries: Vec<gl_client::pb::SignerStateEntry> =
state_snapshot.omit_tombstones().into();
let state_wire_bytes = signer_state_request_wire_bytes(&state_entries);
let state_entries: Vec<pb::SignerStateEntry> = state_entries
.into_iter()
Expand Down Expand Up @@ -501,7 +506,6 @@ impl Node for PluginNodeServer {
hsm_id
);


let state_snapshot = signer_state.lock().await.clone();
// Estimate the size of the full state to calculate the bandwidth savings of sending diffs
let full_entries: Vec<gl_client::pb::SignerStateEntry> =
Expand Down Expand Up @@ -754,10 +758,7 @@ impl Node for PluginNodeServer {
if let Err(e) = address.require_network(network) {
return Err(Status::new(
Code::Unknown,
format!(
"Network validation failed: {}",
e
),
format!("Network validation failed: {}", e),
));
}
}
Expand Down Expand Up @@ -843,6 +844,22 @@ struct Lsps2Offer {
params: Vec<crate::responses::OpeningFeeParams>,
}

/// Select the LSP and opening fee params to use for an LSPS2 invoice
/// negotiation.
///
/// We flatten the params across all LSPs and pick the first available
/// pair. This way an LSP that got selected but returned an empty param
/// set is skipped in favor of the next LSP that actually returned
/// usable params, rather than ending up without valid opening params.
/// Returns `None` if no LSP returned any params at all.
fn select_opening_params(
lsps: Vec<Lsps2Offer>,
) -> Option<(String, crate::responses::OpeningFeeParams)> {
lsps.into_iter()
.flat_map(|l| l.params.into_iter().map(move |p| (l.node_id.clone(), p)))
.next()
}

impl PluginNodeServer {
pub async fn run(self) -> Result<()> {
let addr = self.grpc_binding.parse().unwrap();
Expand Down Expand Up @@ -1216,3 +1233,85 @@ where

mod rpcwait;
pub use rpcwait::RpcWaitService;

#[cfg(test)]
mod test {
use super::*;
use crate::responses::OpeningFeeParams;

fn param(min_fee_msat: &str) -> OpeningFeeParams {
OpeningFeeParams {
min_fee_msat: min_fee_msat.to_string(),
proportional: 0,
valid_until: "2100-01-01T00:00:00Z".to_string(),
min_lifetime: 144,
max_client_to_self_delay: 1024,
min_payment_size_msat: "0".to_string(),
max_payment_size_msat: "1000000000".to_string(),
promise: "promise".to_string(),
}
}

#[test]
fn test_select_opening_params_empty() {
// No LSPs at all -> nothing to select.
assert!(select_opening_params(vec![]).is_none());
}

#[test]
fn test_select_opening_params_first_lsp_empty() {
// The first LSP got selected but returned an empty param set.
// We must skip it and fall back to the next LSP that actually
// returned usable params.
let lsps = vec![
Lsps2Offer {
node_id: "lsp_empty".to_string(),
params: vec![],
},
Lsps2Offer {
node_id: "lsp_good".to_string(),
params: vec![param("100")],
},
];

let (lsp_id, p) = select_opening_params(lsps).expect("should fall back to second LSP");
assert_eq!(lsp_id, "lsp_good");
assert_eq!(p.min_fee_msat, "100");
}

#[test]
fn test_select_opening_params_all_empty() {
// Every LSP returned an empty param set -> nothing valid to pick.
let lsps = vec![
Lsps2Offer {
node_id: "lsp_empty_1".to_string(),
params: vec![],
},
Lsps2Offer {
node_id: "lsp_empty_2".to_string(),
params: vec![],
},
];

assert!(select_opening_params(lsps).is_none());
}

#[test]
fn test_select_opening_params_prefers_first_nonempty() {
// When the first LSP does return params we keep using it.
let lsps = vec![
Lsps2Offer {
node_id: "lsp_first".to_string(),
params: vec![param("1"), param("2")],
},
Lsps2Offer {
node_id: "lsp_second".to_string(),
params: vec![param("3")],
},
];

let (lsp_id, p) = select_opening_params(lsps).expect("first LSP has params");
assert_eq!(lsp_id, "lsp_first");
assert_eq!(p.min_fee_msat, "1");
}
}
7 changes: 3 additions & 4 deletions libs/gl-plugin/src/responses.rs
Original file line number Diff line number Diff line change
Expand Up @@ -349,8 +349,7 @@ pub struct InvoiceResponse {

#[derive(Debug, Clone, Deserialize)]
pub struct LspGetinfoResponse {
pub opening_fee_params_menu: Vec<OpeningFeeParams>,

pub opening_fee_params_menu: Vec<OpeningFeeParams>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)] // LSPS2 requires the client to fail if a field is unrecognized.
Expand All @@ -360,8 +359,8 @@ pub struct OpeningFeeParams {
pub valid_until: String,
pub min_lifetime: u32,
pub max_client_to_self_delay: u32,
pub min_payment_size_msat: String ,
pub max_payment_size_msat: String ,
pub min_payment_size_msat: String,
pub max_payment_size_msat: String,
pub promise: String, // Max 512 bytes
}

Expand Down
6 changes: 5 additions & 1 deletion libs/gl-plugin/src/stager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,11 @@ impl Stage {
.filter_map(|r| {
let head: [u16; 2] = [r.request.raw[0].into(), r.request.raw[1].into()];
let typ = head[0] << 8 | head[1];
if sticky_types.contains(&typ) { Some(typ) } else { None }
if sticky_types.contains(&typ) {
Some(typ)
} else {
None
}
})
.collect();

Expand Down
Loading