cln-plugin: add a hook builder to make use of before, after and filters options
What changed, and why it matters
This commit is a routine feature addition to the Rust plugin library used with Core Lightning. It lets plugin authors describe hook ordering and message filters when registering hooks with the lightning node. There is no indication this fixes a security bug or introduces a vulnerability; it is an API enhancement.
No security action required; review as normal feature/API change.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change adds a HookBuilder API to cln-plugin so hooks can carry before/after ordering constraints and optional filters. It updates the manifest’s hooks field from a Vec
Changed components
plugins/src/lib.rsplugins/src/messages.rsplugins/examples/cln-plugin-startup.rsplugins/lsps-plugin/src/client.rsplugins/lsps-plugin/src/service.rsInspect captured patch +123 / −10
diff --git a/plugins/examples/cln-plugin-startup.rs b/plugins/examples/cln-plugin-startup.rs
index d8500f93..12578fe4 100644
--- a/plugins/examples/cln-plugin-startup.rs
+++ b/plugins/examples/cln-plugin-startup.rs
@@ -7,7 +7,7 @@ use cln_plugin::options::{
DefaultStringArrayConfigOption, IntegerArrayConfigOption, IntegerConfigOption,
StringArrayConfigOption,
};
-use cln_plugin::{messages, Builder, Error, Plugin};
+use cln_plugin::{messages, Builder, Error, HookBuilder, Plugin};
const TEST_NOTIF_TAG: &str = "test_custom_notification";
@@ -79,7 +79,12 @@ async fn main() -> Result<(), anyhow::Error> {
.rpcmethod("test-log-levels", "send on all log levels", test_log_levels)
.subscribe("connect", connect_handler)
.subscribe("test_custom_notification", test_receive_custom_notification)
- .hook("peer_connected", peer_connected_handler)
+ .hook_from_builder(
+ HookBuilder::new("peer_connected", peer_connected_handler)
+ .after(Vec::new())
+ .before(Vec::new())
+ .filters(Vec::new()),
+ )
.notification(messages::NotificationTopic::new(TEST_NOTIF_TAG))
.start(state)
.await?
diff --git a/plugins/lsps-plugin/src/client.rs b/plugins/lsps-plugin/src/client.rs
index df648ee5..bdf3475f 100644
--- a/plugins/lsps-plugin/src/client.rs
+++ b/plugins/lsps-plugin/src/client.rs
@@ -17,11 +17,11 @@ use cln_lsps::{
transport::{MultiplexedTransport, PendingRequests},
},
proto::{
- lsps0::{Msat, LSP_FEATURE_BIT},
+ lsps0::{Msat, LSPS0_MESSAGE_TYPE, LSP_FEATURE_BIT},
lsps2::{compute_opening_fee, Lsps2BuyResponse, Lsps2GetInfoResponse, OpeningFeeParams},
},
};
-use cln_plugin::options;
+use cln_plugin::{options, HookBuilder, HookFilter};
use cln_rpc::{
model::{
requests::{
@@ -82,7 +82,10 @@ impl ClientState for State {
#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
if let Some(plugin) = cln_plugin::Builder::new(tokio::io::stdin(), tokio::io::stdout())
- .hook("custommsg", hooks::client_custommsg_hook)
+ .hook_from_builder(
+ HookBuilder::new("custommsg", hooks::client_custommsg_hook)
+ .filters(vec![HookFilter::Int(i64::from(LSPS0_MESSAGE_TYPE))]),
+ )
.option(OPTION_ENABLED)
.rpcmethod(
"lsps-listprotocols",
diff --git a/plugins/lsps-plugin/src/service.rs b/plugins/lsps-plugin/src/service.rs
index bc59ba51..2e9ae10c 100644
--- a/plugins/lsps-plugin/src/service.rs
+++ b/plugins/lsps-plugin/src/service.rs
@@ -12,9 +12,9 @@ use cln_lsps::{
},
server::LspsService,
},
- proto::lsps0::Msat,
+ proto::lsps0::{Msat, LSPS0_MESSAGE_TYPE},
};
-use cln_plugin::{options, Plugin};
+use cln_plugin::{options, HookBuilder, HookFilter, Plugin};
use log::{debug, error, trace};
use std::path::{Path, PathBuf};
use std::sync::Arc;
@@ -78,7 +78,10 @@ async fn main() -> Result<(), anyhow::Error> {
// cln_plugin::FeatureBitsKind::Init,
// util::feature_bit_to_hex(LSP_FEATURE_BIT),
// )
- .hook("custommsg", service_custommsg_hook)
+ .hook_from_builder(
+ HookBuilder::new("custommsg", service_custommsg_hook)
+ .filters(vec![HookFilter::Int(i64::from(LSPS0_MESSAGE_TYPE))]),
+ )
.hook("htlc_accepted", on_htlc_accepted)
.configure()
.await?
diff --git a/plugins/src/lib.rs b/plugins/src/lib.rs
index dc9cf1ab..e9a3a10d 100644
--- a/plugins/src/lib.rs
+++ b/plugins/src/lib.rs
@@ -2,6 +2,7 @@ use crate::codec::{JsonCodec, JsonRpcCodec};
pub use anyhow::anyhow;
use anyhow::{Context, Result};
use futures::sink::SinkExt;
+use serde::Serialize;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
extern crate log;
use log::trace;
@@ -204,12 +205,21 @@ where
self.hooks.insert(
hookname.to_string(),
Hook {
+ name: hookname.to_string(),
callback: Box::new(move |p, r| Box::pin(callback(p, r))),
+ before: Vec::new(),
+ after: Vec::new(),
+ filters: None,
},
);
self
}
+ pub fn hook_from_builder(mut self, hook: HookBuilder<S>) -> Builder<S, I, O> {
+ self.hooks.insert(hook.name.clone(), hook.build());
+ self
+ }
+
/// Register a custom RPC method for the RPC passthrough from the
/// main daemon
pub fn rpcmethod<C, F>(mut self, name: &str, description: &str, callback: C) -> Builder<S, I, O>
@@ -411,10 +421,21 @@ where
.chain(self.wildcard_subscription.iter().map(|_| String::from("*")))
.collect();
+ let hooks: Vec<messages::Hook> = self
+ .hooks
+ .values()
+ .map(|v| messages::Hook {
+ name: v.name.clone(),
+ before: v.before.clone(),
+ after: v.after.clone(),
+ filters: v.filters.clone(),
+ })
+ .collect();
+
messages::GetManifestResponse {
options: self.options.values().cloned().collect(),
subscriptions,
- hooks: self.hooks.keys().map(|s| s.clone()).collect(),
+ hooks,
rpcmethods,
notifications: self.notifications.clone(),
featurebits: self.featurebits.clone(),
@@ -458,6 +479,56 @@ where
}
}
+impl<S> HookBuilder<S>
+where
+ S: Send + Clone,
+{
+ pub fn new<C, F>(name: &str, callback: C) -> Self
+ where
+ C: Send + Sync + 'static,
+ C: Fn(Plugin<S>, Request) -> F + 'static,
+ F: Future<Output = Response> + Send + 'static,
+ {
+ Self {
+ name: name.to_string(),
+ callback: Box::new(move |p, r| Box::pin(callback(p, r))),
+ before: Vec::new(),
+ after: Vec::new(),
+ filters: None,
+ }
+ }
+
+ pub fn before(mut self, before: Vec<String>) -> Self {
+ self.before = before;
+ self
+ }
+
+ pub fn after(mut self, after: Vec<String>) -> Self {
+ self.after = after;
+ self
+ }
+
+ pub fn filters(mut self, filters: Vec<HookFilter>) -> Self {
+ // Empty Vec would filter everything, must be None instead to not get serialized
+ if filters.is_empty() {
+ self.filters = None;
+ } else {
+ self.filters = Some(filters);
+ }
+ self
+ }
+
+ fn build(self) -> Hook<S> {
+ Hook {
+ callback: self.callback,
+ name: self.name,
+ before: self.before,
+ after: self.after,
+ filters: self.filters,
+ }
+ }
+}
+
impl<S> RpcMethodBuilder<S>
where
S: Send + Clone,
@@ -542,7 +613,29 @@ struct Hook<S>
where
S: Clone + Send,
{
+ name: String,
+ callback: AsyncCallback<S>,
+ before: Vec<String>,
+ after: Vec<String>,
+ filters: Option<Vec<HookFilter>>,
+}
+
+pub struct HookBuilder<S>
+where
+ S: Clone + Send,
+{
+ name: String,
callback: AsyncCallback<S>,
+ before: Vec<String>,
+ after: Vec<String>,
+ filters: Option<Vec<HookFilter>>,
+}
+
+#[derive(Debug, Clone, Serialize)]
+#[serde(untagged)]
+pub enum HookFilter {
+ Str(String),
+ Int(i64),
}
impl<S> Plugin<S>
diff --git a/plugins/src/messages.rs b/plugins/src/messages.rs
index 75402bb2..4ed65c27 100644
--- a/plugins/src/messages.rs
+++ b/plugins/src/messages.rs
@@ -1,4 +1,5 @@
use crate::options::UntypedConfigOption;
+use crate::HookFilter;
use serde::de::{self, Deserializer};
use serde::{Deserialize, Serialize};
use serde_json::Value;
@@ -150,6 +151,14 @@ pub(crate) struct RpcMethod {
pub(crate) usage: String,
}
+#[derive(Serialize, Default, Debug)]
+pub(crate) struct Hook {
+ pub(crate) name: String,
+ pub(crate) before: Vec<String>,
+ pub(crate) after: Vec<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub(crate) filters: Option<Vec<HookFilter>>,
+}
#[derive(Serialize, Default, Debug, Clone)]
pub struct NotificationTopic {
pub method: String,
@@ -175,7 +184,7 @@ pub(crate) struct GetManifestResponse {
pub(crate) rpcmethods: Vec<RpcMethod>,
pub(crate) subscriptions: Vec<String>,
pub(crate) notifications: Vec<NotificationTopic>,
- pub(crate) hooks: Vec<String>,
+ pub(crate) hooks: Vec<Hook>,
pub(crate) dynamic: bool,
pub(crate) featurebits: FeatureBits,
pub(crate) nonnumericids: bool,
Why this scored 15/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.