plugins: lsps: move id generation into jsonrpcrequest
What changed, and why it matters
This commit is a routine code cleanup in Core Lightning's LSPS (Lightning Service Provider Specification) plugin. It moves the generation of random request IDs from the transport/client layer down into the JSON-RPC request construction layer. There is no security-relevant change: the same secure random number generator is still used, and request IDs are still generated for normal requests while notifications remain without IDs. The change simply removes duplicated ID-generation logic and aligns the code with the LSPS0 specification.
No security action required. This is a non-security refactoring. Standard code review and regression testing are sufficient.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch refactors ID generation for JSON-RPC requests in the LSPS plugin. Previously, JsonRpcClient in transport.rs generated a random ID via generate_random_id() and passed it into RequestObject. Now, the JsonRpcRequest trait in proto/jsonrpc.rs provides a default into_request() method that calls into_request_with_id(Some(generate_random_id())). The random ID generator itself is moved unchanged from transport.rs to proto/jsonrpc.rs. Tests are updated to use into_request() instead of manually supplying IDs, except for one serialization test that explicitly uses into_request_with_id(). Debug logging and unused imports are also removed. No cryptographic or protocol behavior changes.
Changed components
plugins/lsps-plugin/src/core/transport.rsplugins/lsps-plugin/src/proto/jsonrpc.rsplugins/lsps-plugin/src/lsps0/handler.rsplugins/lsps-plugin/src/lsps2/handler.rsInspect captured patch +52 / −93
diff --git a/plugins/lsps-plugin/src/core/transport.rs b/plugins/lsps-plugin/src/core/transport.rs
index 0d86afbf..96aaea1d 100644
--- a/plugins/lsps-plugin/src/core/transport.rs
+++ b/plugins/lsps-plugin/src/core/transport.rs
@@ -2,9 +2,6 @@ use crate::proto::jsonrpc::{JsonRpcRequest, JsonRpcResponse, RequestObject};
use async_trait::async_trait;
use bitcoin::secp256k1::PublicKey;
use core::fmt::Debug;
-use log::{debug, error};
-use rand::rngs::OsRng;
-use rand::TryRngCore;
use serde::{de::DeserializeOwned, Serialize};
use serde_json::Value;
use std::sync::Arc;
@@ -23,6 +20,8 @@ pub enum Error {
#[source]
source: serde_json::Error,
},
+ #[error("request is missing id")]
+ MissingId,
}
impl From<serde_json::Error> for Error {
@@ -42,7 +41,6 @@ pub trait Transport: Send + Sync {
async fn send(&self, peer: &PublicKey, request: &str) -> Result<String>;
async fn notify(&self, peer: &PublicKey, request: &str) -> Result<()>;
}
-}
/// A typed JSON-RPC client that works with any transport implementation.
///
@@ -67,19 +65,16 @@ impl<T: Transport> JsonRpcClient<T> {
peer_id: &PublicKey,
method: &str,
params: Option<Value>,
+ id: Option<String>,
) -> Result<JsonRpcResponse<Value>> {
- let id = generate_random_id();
-
- debug!("Preparing request: method={}, id={}", method, id);
let request = RequestObject {
jsonrpc: "2.0".into(),
method: method.into(),
params,
- id: Some(id.clone().into()),
+ id,
};
- let response: JsonRpcResponse<Value> =
- self.send_request(peer_id, method, &request, id).await?;
+ let response: JsonRpcResponse<Value> = self.send_request(peer_id, &request).await?;
Ok(response)
}
@@ -97,13 +92,8 @@ impl<T: Transport> JsonRpcClient<T> {
RQ: JsonRpcRequest + Serialize + Send + Sync,
RS: DeserializeOwned + Serialize + Debug + Send + Sync,
{
- let method = RQ::METHOD;
- let id = generate_random_id();
-
- debug!("Preparing request: method={}, id={}", method, id);
- let request = request.into_request(Some(id.clone().into()));
- let response: JsonRpcResponse<RS> =
- self.send_request(peer_id, method, &request, id).await?;
+ let request = request.into_request();
+ let response: JsonRpcResponse<RS> = self.send_request(peer_id, &request).await?;
Ok(response)
}
@@ -114,14 +104,13 @@ impl<T: Transport> JsonRpcClient<T> {
method: &str,
params: Option<Value>,
) -> Result<()> {
- debug!("Preparing notification: method={}", method);
let request = RequestObject {
jsonrpc: "2.0".into(),
method: method.into(),
params,
id: None,
};
- Ok(self.send_notification(peer_id, method, &request).await?)
+ Ok(self.send_notification(peer_id, &request).await?)
}
/// Sends a typed notification (no response expected).
@@ -129,88 +118,34 @@ impl<T: Transport> JsonRpcClient<T> {
where
RQ: JsonRpcRequest + Serialize + Send + Sync,
{
- let method = RQ::METHOD;
-
- debug!("Preparing notification: method={}", method);
- let request = request.into_request(None);
- Ok(self.send_notification(peer_id, method, &request).await?)
+ let request = request.into_request();
+ Ok(self.send_notification(peer_id, &request).await?)
}
async fn send_request<RS, RP>(
&self,
peer: &PublicKey,
- method: &str,
payload: &RP,
- id: String,
) -> Result<JsonRpcResponse<RS>>
where
RP: Serialize + Send + Sync,
RS: DeserializeOwned + Serialize + Debug + Send + Sync,
{
let request_json = serde_json::to_string(&payload)?;
- debug!(
- "Sending request: method={}, id={}, request={:?}",
- method, id, &request_json
- );
- let start = tokio::time::Instant::now();
let res_str = self.transport.send(peer, &request_json).await?;
- let elapsed = start.elapsed();
- debug!(
- "Received response: method={}, id={}, response={}, elapsed={}ms",
- method,
- id,
- &res_str,
- elapsed.as_millis()
- );
Ok(serde_json::from_str(&res_str)?)
}
- async fn send_notification<RP>(
- &self,
- peer_id: &PublicKey,
- method: &str,
- payload: &RP,
- ) -> Result<()>
+ async fn send_notification<RP>(&self, peer_id: &PublicKey, payload: &RP) -> Result<()>
where
RP: Serialize + Send + Sync,
{
let request_json = serde_json::to_string(&payload)?;
- debug!("Sending notification: method={}", method);
- let start = tokio::time::Instant::now();
self.transport.notify(peer_id, &request_json).await?;
- let elapsed = start.elapsed();
- debug!(
- "Sent notification: method={}, elapsed={}ms",
- method,
- elapsed.as_millis()
- );
Ok(())
}
}
-/// Generates a random ID for JSON-RPC requests.
-///
-/// Uses a secure random number generator to create a hex-encoded ID. Falls back
-/// to a timestamp-based ID if random generation fails.
-fn generate_random_id() -> String {
- let mut bytes = [0u8; 10];
- match OsRng.try_fill_bytes(&mut bytes) {
- Ok(_) => hex::encode(bytes),
- Err(e) => {
- // Fallback to a timestamp-based ID if random generation fails
- error!(
- "Failed to generate random ID: {}, falling back to timestamp",
- e
- );
- let timestamp = std::time::SystemTime::now()
- .duration_since(std::time::UNIX_EPOCH)
- .unwrap_or_default()
- .as_nanos();
- format!("fallback-{}", timestamp)
- }
- }
-}
-
#[cfg(test)]
mod test_json_rpc {
diff --git a/plugins/lsps-plugin/src/lsps0/handler.rs b/plugins/lsps-plugin/src/lsps0/handler.rs
index e6a5a838..286c0188 100644
--- a/plugins/lsps-plugin/src/lsps0/handler.rs
+++ b/plugins/lsps-plugin/src/lsps0/handler.rs
@@ -60,7 +60,7 @@ mod test {
lsps2_enabled: false,
};
- let request = Lsps0listProtocolsRequest {}.into_request(Some("test-id".to_string()));
+ let request = Lsps0listProtocolsRequest {}.into_request();
let payload = create_wrapped_request(&request);
let result = handler.handle(&payload).await.unwrap();
@@ -77,7 +77,7 @@ mod test {
lsps2_enabled: true,
};
- let request = Lsps0listProtocolsRequest {}.into_request(Some("test-id".to_string()));
+ let request = Lsps0listProtocolsRequest {}.into_request();
let payload = create_wrapped_request(&request);
let result = handler.handle(&payload).await.unwrap();
diff --git a/plugins/lsps-plugin/src/lsps2/handler.rs b/plugins/lsps-plugin/src/lsps2/handler.rs
index 250fd8b0..cea15f2d 100644
--- a/plugins/lsps-plugin/src/lsps2/handler.rs
+++ b/plugins/lsps-plugin/src/lsps2/handler.rs
@@ -1022,7 +1022,7 @@ mod tests {
*fake.lsps2_getpolicy_response.lock().unwrap() = Some(params);
let handler = Lsps2GetInfoHandler::new(fake, promise_secret);
- let request = Lsps2GetInfoRequest { token: None }.into_request(Some("test-id".to_string()));
+ let request = Lsps2GetInfoRequest { token: None }.into_request();
let payload = create_wrapped_request(&request);
let result = handler.handle(&payload).await.unwrap();
@@ -1053,7 +1053,7 @@ mod tests {
data: None,
});
let handler = Lsps2GetInfoHandler::new(fake, [0; 32]);
- let request = Lsps2GetInfoRequest { token: None }.into_request(Some("test-id".to_string()));
+ let request = Lsps2GetInfoRequest { token: None }.into_request();
let payload = create_wrapped_request(&request);
let result = handler.handle(&payload).await;
@@ -1084,7 +1084,7 @@ mod tests {
opening_fee_params: buy,
payment_size_msat: Some(Msat(2_000_000)),
}
- .into_request(Some("ok-fixed".into()));
+ .into_request();
let payload = create_wrapped_request(&req);
let out = handler.handle(&payload).await.unwrap();
@@ -1116,7 +1116,7 @@ mod tests {
opening_fee_params: buy,
payment_size_msat: None,
}
- .into_request(Some("ok-var".into()));
+ .into_request();
let payload = create_wrapped_request(&req);
let out = handler.handle(&payload).await.unwrap();
@@ -1136,7 +1136,7 @@ mod tests {
opening_fee_params: buy_wrong,
payment_size_msat: Some(Msat(2_000_000)),
}
- .into_request(Some("bad-promise".into()));
+ .into_request();
let err1 = handler
.handle(&create_wrapped_request(&req_wrong))
.await
@@ -1150,7 +1150,7 @@ mod tests {
opening_fee_params: buy_past,
payment_size_msat: Some(Msat(2_000_000)),
}
- .into_request(Some("past-valid".into()));
+ .into_request();
let err2 = handler
.handle(&create_wrapped_request(&req_past))
.await
@@ -1190,7 +1190,7 @@ mod tests {
opening_fee_params: buy,
payment_size_msat: Some(Msat(9_999)), // strictly less than min_fee => opening_fee >= payment_size
}
- .into_request(Some("too-small".into()));
+ .into_request();
let err = handler
.handle(&create_wrapped_request(&req))
@@ -1232,7 +1232,7 @@ mod tests {
opening_fee_params: buy,
payment_size_msat: Some(Msat(u64::MAX / 2)),
}
- .into_request(Some("overflow".into()));
+ .into_request();
let err = handler
.handle(&create_wrapped_request(&req))
diff --git a/plugins/lsps-plugin/src/proto/jsonrpc.rs b/plugins/lsps-plugin/src/proto/jsonrpc.rs
index 7c4f94fc..24b30713 100644
--- a/plugins/lsps-plugin/src/proto/jsonrpc.rs
+++ b/plugins/lsps-plugin/src/proto/jsonrpc.rs
@@ -1,3 +1,4 @@
+use rand::{rngs::OsRng, TryRngCore as _};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_json::{self, Value};
use std::fmt;
@@ -16,7 +17,14 @@ pub const INTERNAL_ERROR: i64 = -32603;
/// request format.
pub trait JsonRpcRequest: Serialize {
const METHOD: &'static str;
- fn into_request(self, id: Option<String>) -> RequestObject<Self>
+ fn into_request(self) -> RequestObject<Self>
+ where
+ Self: Sized,
+ {
+ Self::into_request_with_id(self, Some(generate_random_id()))
+ }
+
+ fn into_request_with_id(self, id: Option<String>) -> RequestObject<Self>
where
Self: Sized,
{
@@ -29,6 +37,25 @@ pub trait JsonRpcRequest: Serialize {
}
}
+/// Generates a random ID for JSON-RPC requests.
+///
+/// Uses a secure random number generator to create a hex-encoded ID. Falls back
+/// to a timestamp-based ID if random generation fails.
+fn generate_random_id() -> String {
+ let mut bytes = [0u8; 10];
+ match OsRng.try_fill_bytes(&mut bytes) {
+ Ok(_) => hex::encode(bytes),
+ Err(_) => {
+ // Fallback to a timestamp-based ID if random generation fails
+ let timestamp = std::time::SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .unwrap_or_default()
+ .as_nanos();
+ format!("fallback-{}", timestamp)
+ }
+ }
+}
+
/// # RequestObject
///
/// Represents a JSON-RPC 2.0 Request object, as defined in section 4 of the
@@ -41,10 +68,7 @@ pub trait JsonRpcRequest: Serialize {
/// to allow it to be encoded as JSON. Typically this will be a struct
/// implementing the `JsonRpcRequest` trait.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
-pub struct RequestObject<T>
-where
- T: Serialize,
-{
+pub struct RequestObject<T> {
/// **REQUIRED**. MUST be `"2.0"`.
pub jsonrpc: String,
/// **REQUIRED**. The method to be invoked.
@@ -395,7 +419,7 @@ mod test_message_serialization {
impl JsonRpcRequest for SayHelloRequest {
const METHOD: &'static str = "say_hello";
}
- let rpc_request = SayHelloRequest.into_request(Some("unique-id-123".into()));
+ let rpc_request = SayHelloRequest.into_request();
assert!(!serde_json::to_string(&rpc_request)
.expect("could not convert to json")
.contains("\"params\""));
@@ -416,7 +440,7 @@ mod test_message_serialization {
name: "Satoshi".to_string(),
age: 99,
}
- .into_request(Some("unique-id-123".into()));
+ .into_request_with_id(Some("unique-id-123".into()));
let json_value: serde_json::Value = serde_json::to_value(&rpc_request).unwrap();
let expected_value: serde_json::Value = serde_json::json!({
Why this scored 12/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.