Skip to content

Commit 1eb0ffd

Browse files
committed
lsp_plugin: check that featurebit is set and that
the client is connected to the lsp before sending a request Signed-off-by: Peter Neuroth <pet.v.ne@gmail.com>
1 parent 43b0782 commit 1eb0ffd

File tree

4 files changed

+176
-2
lines changed

4 files changed

+176
-2
lines changed

plugins/lsps-plugin/src/client.rs

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
1-
use anyhow::Context;
1+
use anyhow::{anyhow, Context};
22
use cln_lsps::jsonrpc::client::JsonRpcClient;
33
use cln_lsps::lsps0::{
44
self,
55
transport::{Bolt8Transport, CustomMessageHookManager, WithCustomMessageHookManager},
66
};
7+
use cln_lsps::util;
8+
use cln_lsps::LSP_FEATURE_BIT;
79
use cln_plugin::options;
10+
use cln_rpc::model::requests::ListpeersRequest;
11+
use cln_rpc::primitives::PublicKey;
12+
use cln_rpc::ClnRpc;
813
use log::debug;
914
use serde::Deserialize;
1015
use std::path::Path;
16+
use std::str::FromStr as _;
1117

1218
/// An option to enable this service.
1319
const OPTION_ENABLED: options::FlagConfigOption = options::ConfigOption::new_flag(
@@ -66,9 +72,14 @@ async fn on_lsps_listprotocols(
6672
}
6773
let dir = p.configuration().lightning_dir;
6874
let rpc_path = Path::new(&dir).join(&p.configuration().rpc_file);
75+
let mut cln_client = cln_rpc::ClnRpc::new(rpc_path.clone()).await?;
6976

7077
let req: Request = serde_json::from_value(v).context("Failed to parse request JSON")?;
7178

79+
// Fail early: Check that we are connected to the peer and that it has the
80+
// LSP feature bit set.
81+
ensure_lsp_connected(&mut cln_client, &req.lsp_id).await?;
82+
7283
// Create the transport first and handle potential errors
7384
let transport = Bolt8Transport::new(
7485
&req.lsp_id,
@@ -90,3 +101,43 @@ async fn on_lsps_listprotocols(
90101
debug!("Received lsps0.list_protocols response: {:?}", res);
91102
Ok(serde_json::to_value(res)?)
92103
}
104+
105+
/// Checks that the node is connected to the peer and that it has the LSP
106+
/// feature bit set.
107+
async fn ensure_lsp_connected(cln_client: &mut ClnRpc, lsp_id: &str) -> Result<(), anyhow::Error> {
108+
let res = cln_client
109+
.call_typed(&ListpeersRequest {
110+
id: Some(PublicKey::from_str(lsp_id)?),
111+
level: None,
112+
})
113+
.await?;
114+
115+
// unwrap in next line is safe as we checked that an item exists before.
116+
if res.peers.is_empty() || !res.peers.first().unwrap().connected {
117+
debug!("Node isn't connected to lsp {lsp_id}");
118+
return Err(anyhow!("not connected to lsp"));
119+
}
120+
121+
res.peers
122+
.first()
123+
.filter(|peer| {
124+
// Check that feature bit is set
125+
peer.features.as_deref().map_or(false, |f_str| {
126+
if let Some(feature_bits) = hex::decode(f_str).ok() {
127+
let mut fb = feature_bits.clone();
128+
fb.reverse();
129+
util::is_feature_bit_set(&fb, LSP_FEATURE_BIT)
130+
} else {
131+
false
132+
}
133+
})
134+
})
135+
.ok_or_else(|| {
136+
anyhow!(
137+
"peer is not an lsp, feature bit {} is missing",
138+
LSP_FEATURE_BIT,
139+
)
140+
})?;
141+
142+
Ok(())
143+
}

plugins/lsps-plugin/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
pub mod jsonrpc;
22
pub mod lsps0;
33
pub mod util;
4+
5+
pub const LSP_FEATURE_BIT: usize = 729;

plugins/lsps-plugin/src/service.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ use async_trait::async_trait;
33
use cln_lsps::jsonrpc::server::{JsonRpcResponseWriter, RequestHandler};
44
use cln_lsps::jsonrpc::{server::JsonRpcServer, JsonRpcRequest};
55
use cln_lsps::jsonrpc::{JsonRpcResponse, RequestObject, RpcError, TransportError};
6-
use cln_lsps::lsps0;
76
use cln_lsps::lsps0::model::{Lsps0listProtocolsRequest, Lsps0listProtocolsResponse};
87
use cln_lsps::lsps0::transport::{self, CustomMsg};
98
use cln_lsps::util::wrap_payload_with_peer_id;
9+
use cln_lsps::{lsps0, util, LSP_FEATURE_BIT};
1010
use cln_plugin::options::ConfigOption;
1111
use cln_plugin::{options, Plugin};
1212
use cln_rpc::notifications::CustomMsgNotification;
@@ -31,6 +31,14 @@ struct State {
3131
async fn main() -> Result<(), anyhow::Error> {
3232
if let Some(plugin) = cln_plugin::Builder::new(tokio::io::stdin(), tokio::io::stdout())
3333
.option(OPTION_ENABLED)
34+
.featurebits(
35+
cln_plugin::FeatureBitsKind::Node,
36+
util::feature_bit_to_hex(LSP_FEATURE_BIT),
37+
)
38+
.featurebits(
39+
cln_plugin::FeatureBitsKind::Init,
40+
util::feature_bit_to_hex(LSP_FEATURE_BIT),
41+
)
3442
.hook("custommsg", on_custommsg)
3543
.configure()
3644
.await?

plugins/lsps-plugin/src/util.rs

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,42 @@ use core::fmt;
55
use serde_json::Value;
66
use std::str::FromStr;
77

8+
/// Checks if the feature bit is set in the provided bitmap.
9+
/// Returns true if the `feature_bit` is set in the `bitmap`. Returns false if
10+
/// the `feature_bit` is unset or our ouf bounds.
11+
///
12+
/// # Arguments
13+
///
14+
/// * `bitmap`: A slice of bytes representing the feature bitmap.
15+
/// * `feature_bit`: The 0-based index of the bit to check across the bitmap.
16+
///
17+
pub fn is_feature_bit_set(bitmap: &[u8], feature_bit: usize) -> bool {
18+
let byte_index = feature_bit >> 3; // Equivalent to feature_bit / 8
19+
let bit_index = feature_bit & 7; // Equivalent to feature_bit % 8
20+
21+
if let Some(&target_byte) = bitmap.get(byte_index) {
22+
let mask = 1 << bit_index;
23+
(target_byte & mask) != 0
24+
} else {
25+
false
26+
}
27+
}
28+
29+
/// Returns a single feature_bit in hex representation, least-significant bit
30+
/// first.
31+
///
32+
/// # Arguments
33+
///
34+
/// * `feature_bit`: The 0-based index of the bit to check across the bitmap.
35+
///
36+
pub fn feature_bit_to_hex(feature_bit: usize) -> String {
37+
let byte_index = feature_bit >> 3; // Equivalent to feature_bit / 8
38+
let mask = 1 << (feature_bit & 7); // Equivalent to feature_bit % 8
39+
let mut map = vec![0u8; byte_index + 1];
40+
map[0] |= mask; // least-significant bit first ordering.
41+
hex::encode(&map)
42+
}
43+
844
/// Errors that can occur when unwrapping payload data
945
#[derive(Debug, Clone, PartialEq)]
1046
pub enum UnwrapError {
@@ -131,4 +167,81 @@ mod tests {
131167
Some(UnwrapError::InvalidPublicKey(_))
132168
));
133169
}
170+
171+
#[test]
172+
fn test_basic_bit_checks() {
173+
// Example bitmap:
174+
// Byte 0: 0b10100101 (165) -> Bits 0, 2, 5, 7 set
175+
// Byte 1: 0b01101010 (106) -> Bits 1, 3, 5, 6 set (indices 9, 11, 13, 14)
176+
let bitmap: &[u8] = &[0b10100101, 0b01101010];
177+
178+
// Check bits in byte 0 (indices 0-7)
179+
assert_eq!(is_feature_bit_set(bitmap, 0), true); // Bit 0
180+
assert_eq!(is_feature_bit_set(bitmap, 1), false); // Bit 1
181+
assert_eq!(is_feature_bit_set(bitmap, 2), true); // Bit 2
182+
assert_eq!(is_feature_bit_set(bitmap, 3), false); // Bit 3
183+
assert_eq!(is_feature_bit_set(bitmap, 4), false); // Bit 4
184+
assert_eq!(is_feature_bit_set(bitmap, 5), true); // Bit 5
185+
assert_eq!(is_feature_bit_set(bitmap, 6), false); // Bit 6
186+
assert_eq!(is_feature_bit_set(bitmap, 7), true); // Bit 7
187+
188+
// Check bits in byte 1 (indices 8-15)
189+
assert_eq!(is_feature_bit_set(bitmap, 8), false); // Bit 8 (Byte 1, bit 0)
190+
assert_eq!(is_feature_bit_set(bitmap, 9), true); // Bit 9 (Byte 1, bit 1)
191+
assert_eq!(is_feature_bit_set(bitmap, 10), false); // Bit 10 (Byte 1, bit 2)
192+
assert_eq!(is_feature_bit_set(bitmap, 11), true); // Bit 11 (Byte 1, bit 3)
193+
assert_eq!(is_feature_bit_set(bitmap, 12), false); // Bit 12 (Byte 1, bit 4)
194+
assert_eq!(is_feature_bit_set(bitmap, 13), true); // Bit 13 (Byte 1, bit 5)
195+
assert_eq!(is_feature_bit_set(bitmap, 14), true); // Bit 14 (Byte 1, bit 6)
196+
assert_eq!(is_feature_bit_set(bitmap, 15), false); // Bit 15 (Byte 1, bit 7)
197+
}
198+
199+
#[test]
200+
fn test_out_of_bounds() {
201+
let bitmap: &[u8] = &[0b11111111, 0b00000000]; // 16 bits total
202+
203+
assert_eq!(is_feature_bit_set(bitmap, 15), false); // Last valid bit (is 0)
204+
assert_eq!(is_feature_bit_set(bitmap, 16), false); // Out of bounds
205+
assert_eq!(is_feature_bit_set(bitmap, 100), false); // Way out of bounds
206+
}
207+
208+
#[test]
209+
fn test_empty_bitmap() {
210+
let bitmap: &[u8] = &[];
211+
assert_eq!(is_feature_bit_set(bitmap, 0), false);
212+
assert_eq!(is_feature_bit_set(bitmap, 8), false);
213+
}
214+
215+
#[test]
216+
fn test_feature_to_hex_bit_0_be() {
217+
// Bit 0 is in Byte 0 (LE index). num_bytes=1. BE index = 1-1-0=0.
218+
// Expected map: [0x01]
219+
let feature_hex = feature_bit_to_hex(0);
220+
assert_eq!(feature_hex, "01");
221+
assert!(is_feature_bit_set(&hex::decode(feature_hex).unwrap(), 0));
222+
}
223+
224+
#[test]
225+
fn test_feature_to_hex_bit_8_be() {
226+
// Bit 8 is in Byte 1 (LE index). num_bytes=2. BE index = 2-1-1=0.
227+
// Mask is 0x01 for bit 0 within its byte.
228+
// Expected map: [0x01, 0x00] (Byte for 8-15 first, then 0-7)
229+
let feature_hex = feature_bit_to_hex(8);
230+
let mut decoded = hex::decode(&feature_hex).unwrap();
231+
decoded.reverse();
232+
assert_eq!(feature_hex, "0100");
233+
assert!(is_feature_bit_set(&decoded, 8));
234+
}
235+
236+
#[test]
237+
fn test_feature_to_hex_bit_27_be() {
238+
// Bit 27 is in Byte 3 (LE index). num_bytes=4. BE index = 4-1-3=0.
239+
// Mask is 0x08 for bit 3 within its byte.
240+
// Expected map: [0x08, 0x00, 0x00, 0x00] (Byte for 24-31 first)
241+
let feature_hex = feature_bit_to_hex(27);
242+
let mut decoded = hex::decode(&feature_hex).unwrap();
243+
decoded.reverse();
244+
assert_eq!(feature_hex, "08000000");
245+
assert!(is_feature_bit_set(&decoded, 27));
246+
}
134247
}

0 commit comments

Comments
 (0)