Skip to content

lsps-plugin: add cln-lsps-policy plugin for LSPS2 fee policy management - #9349

Open
Andezion wants to merge 3 commits into
ElementsProject:masterfrom
Andezion:lsps-policy-rpc-master
Open

lsps-plugin: add cln-lsps-policy plugin for LSPS2 fee policy management#9349
Andezion wants to merge 3 commits into
ElementsProject:masterfrom
Andezion:lsps-policy-rpc-master

Conversation

@Andezion

Copy link
Copy Markdown
Collaborator

Summary

cln-lsps-service calls out to two internal RPC methods to make LSPS2 policy
decisions - lsps2-policy-getpolicy and lsps2-policy-getchannelcapacity -
but no plugin in the repo registers them, so the service can't run standalone

This adds a new cln-lsps-policy binary that implements both:

  • lsps2-policy-getpolicy - returns the LSP's fee parameter menu
    (Lsps2PolicyGetInfoResponse), built from configurable CLN options.
  • lsps2-policy-getchannelcapacity - returns the channel capacity to open
    for a given JIT-channel buy request (Lsps2PolicyGetChannelCapacityResponse)

All policy knods are exposed as plugin options with sensible defaults, so an
LSP operator can tune fees/limits without touching code:

Option Default
lsps2-policy-min-fee-msat 1000
lsps2-policy-proportional-ppm 0
lsps2-policy-valid-until-hours 1
lsps2-policy-min-lifetime 144
lsps2-policy-max-client-to-self-delay 2016
lsps2-policy-min-payment-size-msat 1000
lsps2-policy-max-payment-size-msat 10_000_000_000
lsps2-policy-channel-capacity-msat 10_000_000_000

Values are validated at startup (non-negative, max > min, ppm in
[0, 1_000_000]) so misconfiguration fails fast instead of silently
producing bad fee params

Test plan

  • cargo build - all three binaries (cln-lsps-client,
    cln-lsps-service, cln-lsps-policy) compile cleanly
  • cargo test - 84 passed, 0 failed (no regressions in existing
    lsps-plugin test suite)
  • Manual end-to-end test: run cln-lsps-service + cln-lsps-policy
    together against a client and confirm lsps2.get_info /
    lsps2.buy succeed

Important

26.06 FREEZE April 30th: Non-bugfix PRs not ready by this date will wait for 26.09.

RC1 is scheduled on May 14th

The final release is scheduled for June 1st.

Checklist

Before submitting the PR, ensure the following tasks are completed. If an item is not applicable to your PR, please mark it as checked:

  • The changelog has been updated in the relevant commit(s) according to the guidelines.
  • Tests have been added or modified to reflect the changes.
  • Documentation has been reviewed and updated as needed.
  • Related issues have been listed and linked, including any that this PR closes.
  • Important All PRs must consider how to reverse any persistent changes for tools/lightning-downgrade

Changelog-None

@Andezion
Andezion requested a review from nepet July 23, 2026 21:41
@Andezion Andezion self-assigned this Jul 23, 2026
@Andezion
Andezion force-pushed the lsps-policy-rpc-master branch 2 times, most recently from 9833ed2 to 7553558 Compare August 3, 2026 06:25
@Andezion Andezion added the Status::Ready for Review The work has been completed and is now awaiting evaluation or approval. label Aug 14, 2026
@Andezion
Andezion force-pushed the lsps-policy-rpc-master branch from 7553558 to 2a4087f Compare August 19, 2026 13:43
@Andezion
Andezion force-pushed the lsps-policy-rpc-master branch from 2a4087f to 8c133c1 Compare August 20, 2026 13:14
@clemenslosbichler-cloud

Copy link
Copy Markdown

hey, think theres a name clash here

cln-lsps-policy gets added to RUST_PLUGIN_NAMES and DEFAULT_TARGETS so it loads as a builtin by default. it registers lsps2-policy-getpolicy and lsps2-policy-getchannelcapacity, but tests/plugins/lsps2_policy.py already registers those exact same two names. so anyone supplying their own policy plugin (or just that fixture) would hit a duplicate rpc registration afaict

is the default policy meant to be opt-in? or should it back off when another policy plugin is already loaded

@Andezion
Andezion force-pushed the lsps-policy-rpc-master branch from 8c133c1 to a49c382 Compare August 20, 2026 15:39
@madelinevibes madelinevibes added this to the v26.09 milestone Aug 24, 2026

@niklasgruener niklasgruener left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i found these while running the PR through Context Goblin, a code review tool we’re currently developing. the main concerns are around configuration validation and how malformed or unauthenticated requests interact with the default policy behavior. hope the comments are useful.

proportional
);
}
if valid_until_hours <= 0 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this checks that the value is positive, but not that it can actually be converted below. a very large positive i64 would be accepted at startup, then Duration::try_hours would fail on every getpolicy call.

could we validate the upper bound here as well so an invalid config fails during startup?

Comment thread plugins/lsps-plugin/src/policy.rs Outdated
v: serde_json::Value,
) -> Result<serde_json::Value, anyhow::Error> {
let _req: Lsps2PolicyGetInfoRequest =
serde_json::from_value(v).unwrap_or(Lsps2PolicyGetInfoRequest { token: None });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could this propagate the deserialization error with serde_json::from_value(v)?, like on_getchannelcapacity does below?

right now malformed parameters are silently converted into { token: None }, so an invalid request becomes indistinguishable from a valid anonymous request. that could also become a fail-open path if token validation is added later.

Comment thread plugins/lsps-plugin/src/policy.rs Outdated
let _req: Lsps2PolicyGetChannelCapacityRequest = serde_json::from_value(v)?;

let res = Lsps2PolicyGetChannelCapacityResponse {
channel_capacity_msat: Some(p.state().channel_capacity_msat),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the request includes init_payment_size, but the response always uses the configured capacity.

with the defaults, this returns a 10,000,000,000 msat (0.1 BTC) channel regardless of the payment size, while the opening policy charges a flat 1,000 msat fee and client_rejected is always false.

could the capacity be derived or capped based on init_payment_size, or should the fixed-capacity and no-client-gating behavior be documented more explicitly?

@Andezion
Andezion force-pushed the lsps-policy-rpc-master branch from a49c382 to 9bbd070 Compare August 24, 2026 11:24
@Andezion
Andezion force-pushed the lsps-policy-rpc-master branch from 9bbd070 to d203303 Compare August 24, 2026 13:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Status::Ready for Review The work has been completed and is now awaiting evaluation or approval.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants