Skip to content
Open
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
13 changes: 9 additions & 4 deletions plugins/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -155,16 +155,19 @@ $(shell test -d plugins/wss-proxy && $(RM) -r plugins/wss-proxy || true)
ifneq ($(RUST),0)
RUST_PLUGIN_NAMES := cln-grpc clnrest cln-lsps-client cln-lsps-service wss-proxy cln-bip353 cln-currencyrate

RUST_OPTIONAL_PLUGIN_NAMES := cln-lsps-policy

# Builtin plugins must be in this plugins dir to work when we're executed
# *without* make install.
RUST_PLUGINS := $(addprefix plugins/,$(RUST_PLUGIN_NAMES))
$(RUST_PLUGINS): plugins/%: $(RUST_TARGET_DIR)/%
RUST_OPTIONAL_PLUGINS := $(addprefix plugins/,$(RUST_OPTIONAL_PLUGIN_NAMES))
$(RUST_PLUGINS) $(RUST_OPTIONAL_PLUGINS): plugins/%: $(RUST_TARGET_DIR)/%
@cp $< $@

PLUGINS += $(RUST_PLUGINS)

# You don't need the originals if you have the final ones.
.INTERMEDIATE: $(RUST_PLUGIN_NAMES:%=$(RUST_TARGET_DIR)/%)
.INTERMEDIATE: $(RUST_PLUGIN_NAMES:%=$(RUST_TARGET_DIR)/%) $(RUST_OPTIONAL_PLUGIN_NAMES:%=$(RUST_TARGET_DIR)/%)
endif # RUST

include plugins/askrene/Makefile
Expand Down Expand Up @@ -287,6 +290,8 @@ $(RUST_TARGET_DIR)/cln-lsps-client: ${CLN_LSPS_PLUGIN_SRC} ${CLN_RPC_SOURCES} ${
$(CARGO) build ${CARGO_OPTS} --bin cln-lsps-client
$(RUST_TARGET_DIR)/cln-lsps-service: ${CLN_LSPS_PLUGIN_SRC} ${CLN_RPC_SOURCES} ${CLN_PLUGIN_SRC}
$(CARGO) build ${CARGO_OPTS} --bin cln-lsps-service
$(RUST_TARGET_DIR)/cln-lsps-policy: ${CLN_LSPS_PLUGIN_SRC} ${CLN_RPC_SOURCES} ${CLN_PLUGIN_SRC}
$(CARGO) build ${CARGO_OPTS} --bin cln-lsps-policy
$(RUST_TARGET_DIR)/wss-proxy: ${CLN_WSS_PROXY_PLUGIN_SRC} ${CLN_RPC_SOURCES} ${CLN_PLUGIN_SRC}
$(CARGO) build ${CARGO_OPTS} --bin wss-proxy
$(RUST_TARGET_DIR)/cln-bip353: ${CLN_BIP353_PLUGIN_SRC} ${CLN_PLUGIN_SRC}
Expand All @@ -297,12 +302,12 @@ $(RUST_TARGET_DIR)/cln-currencyrate: ${CLN_CURRENCYRATE_PLUGIN_SRC} ${CLN_RPC_SO
ifneq ($(RUST),0)
include plugins/rest-plugin/Makefile
include plugins/wss-proxy-plugin/Makefile
DEFAULT_TARGETS += $(CLN_PLUGIN_EXAMPLES) plugins/cln-grpc plugins/clnrest plugins/cln-lsps-client plugins/cln-lsps-service plugins/wss-proxy plugins/cln-bip353 plugins/cln-currencyrate
DEFAULT_TARGETS += $(CLN_PLUGIN_EXAMPLES) plugins/cln-grpc plugins/clnrest plugins/cln-lsps-client plugins/cln-lsps-service plugins/cln-lsps-policy plugins/wss-proxy plugins/cln-bip353 plugins/cln-currencyrate
endif

clean: plugins-clean
plugins-clean:
$(RM) $(PLUGINS) $(CLN_PLUGIN_EXAMPLES)
$(RM) $(PLUGINS) $(CLN_PLUGIN_EXAMPLES) $(RUST_OPTIONAL_PLUGINS)
$(RM) plugins/sql-schema_gen.h

include plugins/test/Makefile
4 changes: 4 additions & 0 deletions plugins/lsps-plugin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ path = "src/client.rs"
name = "cln-lsps-service"
path = "src/service.rs"

[[bin]]
name = "cln-lsps-policy"
path = "src/policy.rs"

[dependencies]
anyhow = "1.0"
async-trait = "0.1"
Expand Down
250 changes: 250 additions & 0 deletions plugins/lsps-plugin/src/policy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
use anyhow::{anyhow, bail};
use chrono::{Duration, Utc};
use cln_lsps::proto::lsps0::{Msat, Ppm};
use cln_lsps::proto::lsps2::{
Lsps2PolicyGetChannelCapacityRequest, Lsps2PolicyGetChannelCapacityResponse,
Lsps2PolicyGetInfoRequest, Lsps2PolicyGetInfoResponse, PolicyOpeningFeeParams,
};
use cln_plugin::options::{self, DefaultIntegerConfigOption};

const OPTION_MIN_FEE_MSAT: DefaultIntegerConfigOption = options::ConfigOption::new_i64_with_default(
"lsps2-policy-min-fee-msat",
1_000,
"Minimum opening fee in millisatoshis charged by the LSP",
);

const OPTION_PROPORTIONAL_PPM: DefaultIntegerConfigOption =
options::ConfigOption::new_i64_with_default(
"lsps2-policy-proportional-ppm",
0,
"Proportional opening fee in parts-per-million of the payment size",
);

const OPTION_VALID_UNTIL_HOURS: DefaultIntegerConfigOption =
options::ConfigOption::new_i64_with_default(
"lsps2-policy-valid-until-hours",
1,
"Number of hours for which the offered fee parameters are valid",
);

const OPTION_MIN_LIFETIME: DefaultIntegerConfigOption = options::ConfigOption::new_i64_with_default(
"lsps2-policy-min-lifetime",
144,
"Minimum channel lifetime in blocks that the LSP guarantees",
);

const OPTION_MAX_CLIENT_TO_SELF_DELAY: DefaultIntegerConfigOption =
options::ConfigOption::new_i64_with_default(
"lsps2-policy-max-client-to-self-delay",
2016,
"Maximum to_self_delay (in blocks) accepted from the client",
);

const OPTION_MIN_PAYMENT_SIZE_MSAT: DefaultIntegerConfigOption =
options::ConfigOption::new_i64_with_default(
"lsps2-policy-min-payment-size-msat",
1_000,
"Minimum payment size in millisatoshis that will trigger a JIT channel opening",
);

const OPTION_MAX_PAYMENT_SIZE_MSAT: DefaultIntegerConfigOption =
options::ConfigOption::new_i64_with_default(
"lsps2-policy-max-payment-size-msat",
10_000_000_000,
"Maximum payment size in millisatoshis that will trigger a JIT channel opening",
);

const OPTION_CHANNEL_CAPACITY_MSAT: DefaultIntegerConfigOption =
options::ConfigOption::new_i64_with_default(
"lsps2-policy-channel-capacity-msat",
10_000_000_000,
"Channel capacity in millisatoshis to open for JIT channel requests",
);

#[derive(Clone)]
struct State {
min_fee_msat: Msat,
proportional: Ppm,
valid_until_hours: i64,
min_lifetime: u32,
max_client_to_self_delay: u32,
min_payment_size_msat: Msat,
max_payment_size_msat: Msat,
channel_capacity_msat: u64,
}

#[allow(clippy::too_many_arguments)]
fn build_state(
min_fee_msat: i64,
proportional: i64,
valid_until_hours: i64,
min_lifetime: i64,
max_client_to_self_delay: i64,
min_payment_size_msat: i64,
max_payment_size_msat: i64,
channel_capacity_msat: i64,
) -> Result<State, anyhow::Error> {
if min_fee_msat < 0 {
bail!(
"`{}` must be non-negative, got {}",
OPTION_MIN_FEE_MSAT.name,
min_fee_msat
);
}
if proportional < 0 || proportional > 1_000_000 {
bail!(
"`{}` must be between 0 and 1000000, got {}",
OPTION_PROPORTIONAL_PPM.name,
proportional
);
}
if valid_until_hours <= 0 {
bail!(
"`{}` must be positive, got {}",
OPTION_VALID_UNTIL_HOURS.name,
valid_until_hours
);
}
if min_lifetime < 0 || min_lifetime > i64::from(u32::MAX) {
bail!(
"`{}` must be between 0 and {}, got {}",
OPTION_MIN_LIFETIME.name,
u32::MAX,
min_lifetime
);
}
if max_client_to_self_delay < 0 || max_client_to_self_delay > i64::from(u32::MAX) {
bail!(
"`{}` must be between 0 and {}, got {}",
OPTION_MAX_CLIENT_TO_SELF_DELAY.name,
u32::MAX,
max_client_to_self_delay
);
}
if min_payment_size_msat < 0 {
bail!(
"`{}` must be non-negative, got {}",
OPTION_MIN_PAYMENT_SIZE_MSAT.name,
min_payment_size_msat
);
}
if max_payment_size_msat <= min_payment_size_msat {
bail!(
"`{}` must be greater than `{}`, got {} <= {}",
OPTION_MAX_PAYMENT_SIZE_MSAT.name,
OPTION_MIN_PAYMENT_SIZE_MSAT.name,
max_payment_size_msat,
min_payment_size_msat
);
}
if channel_capacity_msat <= 0 {
bail!(
"`{}` must be positive, got {}",
OPTION_CHANNEL_CAPACITY_MSAT.name,
channel_capacity_msat
);
}
if channel_capacity_msat % 1_000 != 0 {
bail!(
"`{}` must be divisible by 1000 (whole satoshis), got {}",
OPTION_CHANNEL_CAPACITY_MSAT.name,
channel_capacity_msat
);
}

Ok(State {
min_fee_msat: Msat::from_msat(min_fee_msat as u64),
proportional: Ppm::from_ppm(proportional as u32),
valid_until_hours,
min_lifetime: min_lifetime as u32,
max_client_to_self_delay: max_client_to_self_delay as u32,
min_payment_size_msat: Msat::from_msat(min_payment_size_msat as u64),
max_payment_size_msat: Msat::from_msat(max_payment_size_msat as u64),
channel_capacity_msat: channel_capacity_msat as u64,
})
}

#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
if let Some(plugin) = cln_plugin::Builder::new(tokio::io::stdin(), tokio::io::stdout())
.option(OPTION_MIN_FEE_MSAT)
.option(OPTION_PROPORTIONAL_PPM)
.option(OPTION_VALID_UNTIL_HOURS)
.option(OPTION_MIN_LIFETIME)
.option(OPTION_MAX_CLIENT_TO_SELF_DELAY)
.option(OPTION_MIN_PAYMENT_SIZE_MSAT)
.option(OPTION_MAX_PAYMENT_SIZE_MSAT)
.option(OPTION_CHANNEL_CAPACITY_MSAT)
.rpcmethod(
"lsps2-policy-getpolicy",
"Returns the LSP fee policy used for LSPS2 JIT channel requests",
on_getpolicy,
)
.rpcmethod(
"lsps2-policy-getchannelcapacity",
"Returns the channel capacity the LSP will open for a given JIT channel request",
on_getchannelcapacity,
)
.configure()
.await?
{
let state = build_state(
plugin.option(&OPTION_MIN_FEE_MSAT)?,
plugin.option(&OPTION_PROPORTIONAL_PPM)?,
plugin.option(&OPTION_VALID_UNTIL_HOURS)?,
plugin.option(&OPTION_MIN_LIFETIME)?,
plugin.option(&OPTION_MAX_CLIENT_TO_SELF_DELAY)?,
plugin.option(&OPTION_MIN_PAYMENT_SIZE_MSAT)?,
plugin.option(&OPTION_MAX_PAYMENT_SIZE_MSAT)?,
plugin.option(&OPTION_CHANNEL_CAPACITY_MSAT)?,
)?;

let plugin = plugin.start(state).await?;
plugin.join().await
} else {
Ok(())
}
}

async fn on_getpolicy(
p: cln_plugin::Plugin<State>,
v: serde_json::Value,
) -> Result<serde_json::Value, anyhow::Error> {
let _req: Lsps2PolicyGetInfoRequest =
serde_json::from_value(v).unwrap_or(Lsps2PolicyGetInfoRequest { token: None });

let s = p.state();
let offset = Duration::try_hours(s.valid_until_hours)
.ok_or_else(|| anyhow!("`{}` is out of range", OPTION_VALID_UNTIL_HOURS.name))?;
let valid_until = Utc::now()
.checked_add_signed(offset)
.ok_or_else(|| anyhow!("`{}` is out of range", OPTION_VALID_UNTIL_HOURS.name))?;

let params = PolicyOpeningFeeParams {
min_fee_msat: s.min_fee_msat,
proportional: s.proportional,
valid_until,
min_lifetime: s.min_lifetime,
max_client_to_self_delay: s.max_client_to_self_delay,
min_payment_size_msat: s.min_payment_size_msat,
max_payment_size_msat: s.max_payment_size_msat,
};

let res = Lsps2PolicyGetInfoResponse {
policy_opening_fee_params_menu: vec![params],
client_rejected: false,
};
Ok(serde_json::to_value(res)?)
}

async fn on_getchannelcapacity(
p: cln_plugin::Plugin<State>,
v: serde_json::Value,
) -> Result<serde_json::Value, anyhow::Error> {
let _req: Lsps2PolicyGetChannelCapacityRequest = serde_json::from_value(v)?;

let res = Lsps2PolicyGetChannelCapacityResponse {
channel_capacity_msat: Some(p.state().channel_capacity_msat),
};
Ok(serde_json::to_value(res)?)
}
Loading