lsp_plugin: add basic lsps2 mpp support to client
This includes a mocked lsps2 service plugin, tests and some changes on the client side. The client now can accept mpp payments for a jit-channel opening from a connected LSP. Changelog-Added: Lsps2 `fixed-invoice-mpp` mode for the lsps2 client Signed-off-by: Peter Neuroth <pet.v.ne@gmail.com>
This commit is contained in:
committed by
Rusty Russell
parent
3b05a81317
commit
be015898ae
@@ -18,12 +18,14 @@ use cln_lsps::util;
|
||||
use cln_lsps::LSP_FEATURE_BIT;
|
||||
use cln_plugin::options;
|
||||
use cln_rpc::model::requests::{
|
||||
DatastoreMode, DatastoreRequest, DeldatastoreRequest, ListdatastoreRequest, ListpeersRequest,
|
||||
DatastoreMode, DatastoreRequest, DeldatastoreRequest, DelinvoiceRequest, DelinvoiceStatus,
|
||||
ListdatastoreRequest, ListinvoicesRequest, ListpeersRequest,
|
||||
};
|
||||
use cln_rpc::model::responses::InvoiceResponse;
|
||||
use cln_rpc::primitives::{AmountOrAny, PublicKey, ShortChannelId};
|
||||
use cln_rpc::primitives::{Amount, AmountOrAny, PublicKey, ShortChannelId};
|
||||
use cln_rpc::ClnRpc;
|
||||
use log::{debug, info, warn};
|
||||
use rand::{CryptoRng, Rng};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
use std::str::FromStr as _;
|
||||
@@ -263,7 +265,15 @@ async fn on_lsps_lsps2_approve(
|
||||
hex: None,
|
||||
mode: Some(DatastoreMode::CREATE_OR_REPLACE),
|
||||
string: Some(ds_rec_json),
|
||||
key: vec!["lsps".to_string(), "client".to_string(), req.lsp_id],
|
||||
key: vec!["lsps".to_string(), "client".to_string(), req.lsp_id.clone()],
|
||||
};
|
||||
let _ds_res = cln_client.call_typed(&ds_req).await?;
|
||||
let ds_req = DatastoreRequest {
|
||||
generation: None,
|
||||
hex: None,
|
||||
mode: Some(DatastoreMode::CREATE_OR_REPLACE),
|
||||
string: Some(req.lsp_id),
|
||||
key: vec!["lsps".to_string(), "invoice".to_string(), req.payment_hash],
|
||||
};
|
||||
let _ds_res = cln_client.call_typed(&ds_req).await?;
|
||||
Ok(serde_json::Value::default())
|
||||
@@ -338,6 +348,30 @@ async fn on_lsps_jitchannel(
|
||||
AmountOrAny::Any => None,
|
||||
};
|
||||
|
||||
// Check that the amount is big enough to cover the fee and a single HTLC.
|
||||
let reduced_amount_msat = if let Some(payment_msat) = payment_size_msat {
|
||||
match compute_opening_fee(
|
||||
payment_msat.msat(),
|
||||
selected_params.min_fee_msat.msat(),
|
||||
selected_params.proportional.ppm() as u64,
|
||||
) {
|
||||
Some(fee_msat) => {
|
||||
if payment_msat.msat() - fee_msat < 1000 {
|
||||
bail!(
|
||||
"amount_msat {}msat is too small, needs to be at least {}msat: opening fee is {}msat",
|
||||
payment_msat,
|
||||
1000 + fee_msat,
|
||||
fee_msat
|
||||
);
|
||||
}
|
||||
Some(payment_msat.msat() - fee_msat)
|
||||
}
|
||||
None => bail!("failed to compute opening fee"),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// 3. Request channel from LSP.
|
||||
let buy_res: Lsps2BuyResponse = cln_client
|
||||
.call_raw(
|
||||
@@ -372,50 +406,91 @@ async fn on_lsps_jitchannel(
|
||||
cltv_expiry_delta: u16::try_from(buy_res.lsp_cltv_expiry_delta)?,
|
||||
};
|
||||
|
||||
let inv: cln_rpc::model::responses::InvoiceResponse = cln_client
|
||||
// Generate a preimage if we have an amount specified.
|
||||
let preimage = if payment_size_msat.is_some() {
|
||||
Some(gen_rand_preimage_hex(&mut rand::rng()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let public_inv: cln_rpc::model::responses::InvoiceResponse = cln_client
|
||||
.call_raw(
|
||||
"invoice",
|
||||
&InvoiceRequest {
|
||||
amount_msat: req.amount_msat,
|
||||
dev_routes: Some(vec![vec![hint]]),
|
||||
description: req.description,
|
||||
label: req.label,
|
||||
dev_routes: Some(vec![vec![hint.clone()]]),
|
||||
description: req.description.clone(),
|
||||
label: req.label.clone(),
|
||||
expiry: Some(expiry as u64),
|
||||
cltv: Some(u32::try_from(6 + 2)?), // TODO: FETCH REAL VALUE!
|
||||
cltv: None,
|
||||
deschashonly: None,
|
||||
preimage: None,
|
||||
preimage: preimage.clone(),
|
||||
exposeprivatechannels: None,
|
||||
fallbacks: None,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
// We need to reduce the expected amount if the invoice has an amount set
|
||||
if let Some(amount_msat) = reduced_amount_msat {
|
||||
debug!(
|
||||
"amount_msat is specified: create new invoice with reduced amount {}msat",
|
||||
amount_msat,
|
||||
);
|
||||
let _ = cln_client
|
||||
.call_typed(&DelinvoiceRequest {
|
||||
desconly: None,
|
||||
status: DelinvoiceStatus::UNPAID,
|
||||
label: req.label.clone(),
|
||||
})
|
||||
.await?;
|
||||
|
||||
let _: cln_rpc::model::responses::InvoiceResponse = cln_client
|
||||
.call_raw(
|
||||
"invoice",
|
||||
&InvoiceRequest {
|
||||
amount_msat: AmountOrAny::Amount(Amount::from_msat(amount_msat)),
|
||||
dev_routes: Some(vec![vec![hint]]),
|
||||
description: req.description,
|
||||
label: req.label,
|
||||
expiry: Some(expiry as u64),
|
||||
cltv: None,
|
||||
deschashonly: None,
|
||||
preimage,
|
||||
exposeprivatechannels: None,
|
||||
fallbacks: None,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// 5. Approve jit_channel_scid for a jit channel opening.
|
||||
let appr_req = ClnRpcLsps2Approve {
|
||||
lsp_id: req.lsp_id,
|
||||
jit_channel_scid: buy_res.jit_channel_scid,
|
||||
payment_hash: public_inv.payment_hash.to_string(),
|
||||
client_trusts_lsp: Some(buy_res.client_trusts_lsp),
|
||||
};
|
||||
let _: serde_json::Value = cln_client.call_raw("lsps-lsps2-approve", &appr_req).await?;
|
||||
|
||||
// 6. Return invoice.
|
||||
let out = InvoiceResponse {
|
||||
bolt11: inv.bolt11,
|
||||
created_index: inv.created_index,
|
||||
warning_capacity: inv.warning_capacity,
|
||||
warning_deadends: inv.warning_deadends,
|
||||
warning_mpp: inv.warning_mpp,
|
||||
warning_offline: inv.warning_offline,
|
||||
warning_private_unused: inv.warning_private_unused,
|
||||
expires_at: inv.expires_at,
|
||||
payment_hash: inv.payment_hash,
|
||||
payment_secret: inv.payment_secret,
|
||||
bolt11: public_inv.bolt11,
|
||||
created_index: public_inv.created_index,
|
||||
warning_capacity: public_inv.warning_capacity,
|
||||
warning_deadends: public_inv.warning_deadends,
|
||||
warning_mpp: public_inv.warning_mpp,
|
||||
warning_offline: public_inv.warning_offline,
|
||||
warning_private_unused: public_inv.warning_private_unused,
|
||||
expires_at: public_inv.expires_at,
|
||||
payment_hash: public_inv.payment_hash,
|
||||
payment_secret: public_inv.payment_secret,
|
||||
};
|
||||
Ok(serde_json::to_value(out)?)
|
||||
}
|
||||
|
||||
async fn on_htlc_accepted(
|
||||
_p: cln_plugin::Plugin<State>,
|
||||
p: cln_plugin::Plugin<State>,
|
||||
v: serde_json::Value,
|
||||
) -> Result<serde_json::Value, anyhow::Error> {
|
||||
let req: HtlcAcceptedRequest = serde_json::from_value(v)?;
|
||||
@@ -424,38 +499,87 @@ async fn on_htlc_accepted(
|
||||
let onion_amt = match req.onion.forward_msat {
|
||||
Some(a) => a,
|
||||
None => {
|
||||
debug!("onion is missing forward_msat");
|
||||
debug!("onion is missing forward_msat, continue");
|
||||
let value = serde_json::to_value(HtlcAcceptedResponse::continue_(None, None, None))?;
|
||||
return Ok(value);
|
||||
}
|
||||
};
|
||||
|
||||
let is_lsp_payment = req
|
||||
let Some(payment_data) = req.onion.payload.get(TLV_PAYMENT_SECRET) else {
|
||||
debug!("payment is a forward, continue");
|
||||
let value = serde_json::to_value(HtlcAcceptedResponse::continue_(None, None, None))?;
|
||||
return Ok(value);
|
||||
};
|
||||
|
||||
let extra_fee_msat = req
|
||||
.htlc
|
||||
.extra_tlvs
|
||||
.as_ref()
|
||||
.map_or(false, |tlv| tlv.contains(65537));
|
||||
.map(|tlvs| tlvs.get_u64(65537))
|
||||
.transpose()?
|
||||
.flatten();
|
||||
if let Some(amt) = extra_fee_msat {
|
||||
debug!("lsp htlc is deducted by an extra_fee={amt}");
|
||||
}
|
||||
|
||||
if !is_lsp_payment || htlc_amt.msat() >= onion_amt.msat() {
|
||||
// Not an Lsp payment.
|
||||
// Check that the htlc belongs to a jit-channel request.
|
||||
let dir = p.configuration().lightning_dir;
|
||||
let rpc_path = Path::new(&dir).join(&p.configuration().rpc_file);
|
||||
let mut cln_client = cln_rpc::ClnRpc::new(rpc_path.clone()).await?;
|
||||
let lsp_data = cln_client
|
||||
.call_typed(&ListdatastoreRequest {
|
||||
key: Some(vec![
|
||||
"lsps".to_string(),
|
||||
"invoice".to_string(),
|
||||
hex::encode(&req.htlc.payment_hash),
|
||||
]),
|
||||
})
|
||||
.await?;
|
||||
|
||||
if lsp_data.datastore.first().is_none() {
|
||||
// Not an LSP payment, just continue
|
||||
debug!("payment is a not a jit-channel-opening, continue");
|
||||
let value = serde_json::to_value(HtlcAcceptedResponse::continue_(None, None, None))?;
|
||||
return Ok(value);
|
||||
}
|
||||
debug!("incoming jit-channel htlc");
|
||||
};
|
||||
|
||||
// Safe unwrap(): we already checked that `extra_tlvs` exists.
|
||||
let extra_tlvs = req.htlc.extra_tlvs.unwrap();
|
||||
let deducted_amt = match extra_tlvs.get_u64(65537)? {
|
||||
Some(amt) => amt,
|
||||
debug!(
|
||||
"incoming jit-channel htlc with htlc_amt={} and onion_amt={}",
|
||||
htlc_amt.msat(),
|
||||
onion_amt.msat()
|
||||
);
|
||||
|
||||
let inv_res = cln_client
|
||||
.call_typed(&ListinvoicesRequest {
|
||||
index: None,
|
||||
invstring: None,
|
||||
label: None,
|
||||
limit: None,
|
||||
offer_id: None,
|
||||
payment_hash: Some(hex::encode(&req.htlc.payment_hash)),
|
||||
start: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
let Some(invoice) = inv_res.invoices.first() else {
|
||||
debug!(
|
||||
"no invoice found for jit-channel opening with payment_hash={}",
|
||||
hex::encode(&req.htlc.payment_hash)
|
||||
);
|
||||
let value = serde_json::to_value(HtlcAcceptedResponse::continue_(None, None, None))?;
|
||||
return Ok(value);
|
||||
};
|
||||
|
||||
let total_amt = match invoice.amount_msat {
|
||||
Some(a) => {
|
||||
debug!("invoice has total_amt={}msat", &a.msat());
|
||||
a.msat()
|
||||
}
|
||||
None => {
|
||||
warn!("htlc is missing the extra_fee amount");
|
||||
let value = serde_json::to_value(HtlcAcceptedResponse::continue_(None, None, None))?;
|
||||
return Ok(value);
|
||||
debug!("invoice has no total amount, only accept single htlc");
|
||||
htlc_amt.msat()
|
||||
}
|
||||
};
|
||||
debug!("lsp htlc is deducted by an extra_fee={}", deducted_amt);
|
||||
|
||||
// Fixme: Check that it is not a forward (has payment_secret) before rpc_calls.
|
||||
|
||||
// Fixme: Check that we did not already pay for this channel.
|
||||
// - via datastore or invoice label.
|
||||
@@ -465,18 +589,9 @@ async fn on_htlc_accepted(
|
||||
|
||||
let mut payload = req.onion.payload.clone();
|
||||
payload.set_tu64(TLV_FORWARD_AMT, htlc_amt.msat());
|
||||
let payment_secret = match payload.get(TLV_PAYMENT_SECRET) {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
debug!("can't decode tlv payment_secret {:?}", payload);
|
||||
let value = serde_json::to_value(HtlcAcceptedResponse::continue_(None, None, None))?;
|
||||
return Ok(value);
|
||||
}
|
||||
};
|
||||
|
||||
let total_amt = htlc_amt.msat();
|
||||
let mut ps = Vec::new();
|
||||
ps.extend_from_slice(&payment_secret[0..32]);
|
||||
ps.extend_from_slice(&payment_data[0..32]);
|
||||
ps.extend(encode_tu64(total_amt));
|
||||
payload.insert(TLV_PAYMENT_SECRET, ps);
|
||||
let payload_bytes = match payload.to_bytes() {
|
||||
@@ -640,6 +755,12 @@ async fn check_peer_lsp_status(
|
||||
})
|
||||
}
|
||||
|
||||
pub fn gen_rand_preimage_hex<R: Rng + CryptoRng>(rng: &mut R) -> String {
|
||||
let mut pre = [0u8; 32];
|
||||
rng.fill_bytes(&mut pre);
|
||||
hex::encode(&pre)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
struct LspsBuyJitChannelResponse {
|
||||
bolt11: String,
|
||||
@@ -694,6 +815,7 @@ struct ClnRpcLsps2GetinfoRequest {
|
||||
struct ClnRpcLsps2Approve {
|
||||
lsp_id: String,
|
||||
jit_channel_scid: ShortChannelId,
|
||||
payment_hash: String,
|
||||
#[serde(default)]
|
||||
client_trusts_lsp: Option<bool>,
|
||||
}
|
||||
|
||||
@@ -402,12 +402,15 @@ impl<A: ClnApi> HtlcAcceptedHookHandler<A> {
|
||||
// ---
|
||||
|
||||
// Fixme: We only accept no-mpp for now, mpp and other flows will be added later on
|
||||
// Fixme: We continue mpp for now to let the test mock handle the htlc, as we need
|
||||
// to test the client implementation for mpp payments.
|
||||
if ds_rec.expected_payment_size.is_some() {
|
||||
warn!("mpp payments are not implemented yet");
|
||||
return Ok(HtlcAcceptedResponse::fail(
|
||||
Some(UNKNOWN_NEXT_PEER.to_string()),
|
||||
None,
|
||||
));
|
||||
return Ok(HtlcAcceptedResponse::continue_(None, None, None));
|
||||
// return Ok(HtlcAcceptedResponse::fail(
|
||||
// Some(UNKNOWN_NEXT_PEER.to_string()),
|
||||
// None,
|
||||
// ));
|
||||
}
|
||||
|
||||
// B) Is the fee option menu still valid?
|
||||
@@ -1558,6 +1561,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // We deactivate the mpp check on the experimental server for
|
||||
// client side checks.
|
||||
async fn test_htlc_mpp_not_implemented() {
|
||||
let fake = FakeCln::default();
|
||||
let handler = HtlcAcceptedHookHandler::new(fake.clone(), 1000);
|
||||
|
||||
205
tests/plugins/lsps2_service_mock.py
Executable file
205
tests/plugins/lsps2_service_mock.py
Executable file
@@ -0,0 +1,205 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Zero‑conf LSPS2 mock
|
||||
====================
|
||||
|
||||
• On the **first incoming HTLC**, call `connect` and `fundchannel` with **zeroconf** to a configured peer.
|
||||
• **Hold all HTLCs** until the channel reports `CHANNELD_NORMAL`, then **continue** them all.
|
||||
• After the channel is ready, future HTLCs are continued immediately.
|
||||
"""
|
||||
|
||||
import threading
|
||||
import time
|
||||
import struct
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Dict, Optional
|
||||
from pyln.client import Plugin
|
||||
from pyln.proto.onion import TlvPayload
|
||||
|
||||
|
||||
plugin = Plugin()
|
||||
|
||||
|
||||
@plugin.method("lsps2-policy-getpolicy")
|
||||
def lsps2_policy_getpolicy(request):
|
||||
"""Returns an opening fee menu for the LSPS2 plugin."""
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
# Is ISO 8601 format "YYYY-MM-DDThh:mm:ss.uuuZ"
|
||||
valid_until = (now + timedelta(hours=1)).isoformat().replace("+00:00", "Z")
|
||||
|
||||
return {
|
||||
"policy_opening_fee_params_menu": [
|
||||
{
|
||||
"min_fee_msat": "1000000",
|
||||
"proportional": 0,
|
||||
"valid_until": valid_until,
|
||||
"min_lifetime": 2000,
|
||||
"max_client_to_self_delay": 2016,
|
||||
"min_payment_size_msat": "1000",
|
||||
"max_payment_size_msat": "100000000",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@plugin.method("lsps2-policy-getchannelcapacity")
|
||||
def lsps2_policy_getchannelcapacity(
|
||||
request, init_payment_size, scid, opening_fee_params
|
||||
):
|
||||
"""Returns an opening fee menu for the LSPS2 plugin."""
|
||||
return {"channel_capacity_msat": 100000000}
|
||||
|
||||
|
||||
TLV_OPENING_FEE = 65537
|
||||
|
||||
|
||||
@dataclass
|
||||
class Held:
|
||||
htlc: dict
|
||||
onion: dict
|
||||
event: threading.Event = field(default_factory=threading.Event)
|
||||
response: Optional[dict] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class State:
|
||||
target_peer: Optional[str] = None
|
||||
channel_cap: Optional[int] = None
|
||||
opening_fee_msat: Optional[int] = None
|
||||
pending: Dict[str, Held] = field(default_factory=dict)
|
||||
funding_started: bool = False
|
||||
channel_ready: bool = False
|
||||
channel_id_hex: Optional[str] = None
|
||||
fee_remaining_msat: int = 0
|
||||
worker_thread: Optional[threading.Thread] = None
|
||||
lock: threading.Lock = field(default_factory=threading.Lock)
|
||||
|
||||
|
||||
state = State()
|
||||
|
||||
|
||||
def _key(h: dict) -> str:
|
||||
return f"{h.get('id', '?')}:{h.get('payment_hash', '?')}"
|
||||
|
||||
|
||||
def _ensure_zero_conf_channel(peer_id: str, capacity: int) -> bool:
|
||||
plugin.log(f"fundchannel zero-conf to {peer_id} for {capacity} sat...")
|
||||
res = plugin.rpc.fundchannel(
|
||||
peer_id,
|
||||
capacity,
|
||||
announce=False,
|
||||
mindepth=0,
|
||||
channel_type=[12, 46, 50],
|
||||
)
|
||||
plugin.log(f"got channel response {res}")
|
||||
state.channel_id_hex = res["channel_id"]
|
||||
|
||||
for _ in range(120):
|
||||
channels = plugin.rpc.listpeerchannels(peer_id)["channels"]
|
||||
for c in channels:
|
||||
if c.get("state") == "CHANNELD_NORMAL":
|
||||
plugin.log("zero-conf channel is NORMAL; releaseing HTLCs")
|
||||
return True
|
||||
time.sleep(1)
|
||||
return False
|
||||
|
||||
|
||||
def _modify_payload_and_build_response(held: Held):
|
||||
amt_msat = int(held.htlc.get("amount_msat", 0))
|
||||
fee_applied = 0
|
||||
if state.fee_remaining_msat > 0:
|
||||
fee_applied = min(state.fee_remaining_msat, max(amt_msat - 1, 0))
|
||||
state.fee_remaining_msat -= fee_applied
|
||||
forward_msat = max(1, amt_msat - fee_applied)
|
||||
|
||||
payload = None
|
||||
extra = None
|
||||
if amt_msat != forward_msat:
|
||||
amt_byte = struct.pack("!Q", forward_msat)
|
||||
while len(amt_byte) > 1 and amt_byte[0] == 0:
|
||||
amt_byte = amt_byte[1:]
|
||||
payload = TlvPayload().from_hex(held.onion["payload"])
|
||||
p = TlvPayload()
|
||||
p.add_field(2, amt_byte)
|
||||
p.add_field(4, payload.get(4).value)
|
||||
p.add_field(6, payload.get(6).value)
|
||||
payload = p.to_bytes(include_prefix=False)
|
||||
|
||||
amt_byte = fee_applied.to_bytes(8, "big")
|
||||
e = TlvPayload()
|
||||
e.add_field(TLV_OPENING_FEE, amt_byte)
|
||||
extra = e.to_bytes(include_prefix=False)
|
||||
|
||||
resp = {"result": "continue"}
|
||||
if payload:
|
||||
resp["payload"] = payload.hex()
|
||||
if extra:
|
||||
resp["extra_tlvs"] = extra.hex()
|
||||
if state.channel_id_hex:
|
||||
resp["forward_to"] = state.channel_id_hex
|
||||
return resp
|
||||
|
||||
|
||||
def _release_all_locked():
|
||||
# called with state.lock held
|
||||
items = list(state.pending.items())
|
||||
state.pending.clear()
|
||||
for _k, held in items:
|
||||
if held.response is None:
|
||||
held.response = _modify_payload_and_build_response(held)
|
||||
held.event.set()
|
||||
|
||||
|
||||
def _worker():
|
||||
plugin.log("collecting htlcs and fund channel...")
|
||||
with state.lock:
|
||||
peer = state.target_peer
|
||||
cap = state.channel_cap
|
||||
fee = state.opening_fee_msat
|
||||
if not peer or not cap or not fee:
|
||||
with state.lock:
|
||||
_release_all_locked()
|
||||
return
|
||||
|
||||
ok = _ensure_zero_conf_channel(peer, cap)
|
||||
with state.lock:
|
||||
state.channel_ready = ok
|
||||
state.fee_remaining_msat = fee if ok else 0
|
||||
_release_all_locked()
|
||||
|
||||
|
||||
@plugin.method("setuplsps2service")
|
||||
def setuplsps2service(plugin, peer_id, channel_cap, opening_fee_msat):
|
||||
state.target_peer = peer_id
|
||||
state.channel_cap = channel_cap
|
||||
state.opening_fee_msat = opening_fee_msat
|
||||
|
||||
|
||||
@plugin.async_hook("htlc_accepted")
|
||||
def on_htlc_accepted(htlc, onion, request, plugin, **kwargs):
|
||||
key = _key(htlc)
|
||||
|
||||
with state.lock:
|
||||
if state.channel_ready:
|
||||
held_now = Held(htlc=htlc, onion=onion)
|
||||
resp = _modify_payload_and_build_response(held_now)
|
||||
request.set_result(resp)
|
||||
return
|
||||
|
||||
if not state.funding_started:
|
||||
state.funding_started = True
|
||||
state.worker_thread = threading.Thread(target=_worker, daemon=True)
|
||||
state.worker_thread.start()
|
||||
|
||||
# enqueue and block until the worker releases us
|
||||
held = Held(htlc=htlc, onion=onion)
|
||||
state.pending[key] = held
|
||||
|
||||
held.event.wait()
|
||||
request.set_result(held.response)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
plugin.run()
|
||||
@@ -192,6 +192,106 @@ def test_lsps2_buyjitchannel_no_mpp_var_invoice(node_factory, bitcoind):
|
||||
assert len(chs) == 1
|
||||
|
||||
|
||||
|
||||
def test_lsps2_buyjitchannel_mpp_fixed_invoice(node_factory, bitcoind):
|
||||
"""Tests the creation of a "Just-In-Time-Channel" (jit-channel).
|
||||
|
||||
At the beginning we have the following situation where l2 acts as the LSP
|
||||
(LSP)
|
||||
l1 l2----l3
|
||||
|
||||
l1 now wants to get a channel from l2 via the lsps2 jit-channel protocol:
|
||||
- l1 requests a new jit channel form l2
|
||||
- l1 creates an invoice based on the opening fee parameters it got from l2
|
||||
- l3 pays the invoice
|
||||
- l2 opens a channel to l1 and forwards the payment (deducted by a fee)
|
||||
|
||||
eventualy this will result in the following situation
|
||||
(LSP)
|
||||
l1----l2----l3
|
||||
"""
|
||||
# A mock for lsps2 mpp payments, contains the policy plugin as well.
|
||||
plugin = os.path.join(os.path.dirname(__file__), "plugins/lsps2_service_mock.py")
|
||||
|
||||
l1, l2, l3 = node_factory.get_nodes(
|
||||
3,
|
||||
opts=[
|
||||
{"experimental-lsps-client": None},
|
||||
{
|
||||
"experimental-lsps2-service": None,
|
||||
"experimental-lsps2-promise-secret": "0" * 64,
|
||||
"plugin": plugin,
|
||||
"fee-base": 0, # We are going to deduct our fee anyways,
|
||||
"fee-per-satoshi": 0, # We are going to deduct our fee anyways,
|
||||
},
|
||||
{},
|
||||
],
|
||||
)
|
||||
|
||||
# Give the LSP some funds to open jit-channels
|
||||
addr = l2.rpc.newaddr()["bech32"]
|
||||
bitcoind.rpc.sendtoaddress(addr, 1)
|
||||
bitcoind.generate_block(1)
|
||||
|
||||
node_factory.join_nodes([l3, l2], fundchannel=True, wait_for_announce=True)
|
||||
node_factory.join_nodes([l1, l2], fundchannel=False)
|
||||
|
||||
chanid = only_one(l3.rpc.listpeerchannels(l2.info["id"])["channels"])[
|
||||
"short_channel_id"
|
||||
]
|
||||
|
||||
amt = 10_000_000
|
||||
inv = l1.rpc.lsps_jitchannel(
|
||||
lsp_id=l2.info["id"],
|
||||
amount_msat=f"{amt}msat",
|
||||
description="lsp-jit-channel-0",
|
||||
label="lsp-jit-channel-0",
|
||||
)
|
||||
dec = l3.rpc.decode(inv["bolt11"])
|
||||
|
||||
l2.rpc.setuplsps2service(
|
||||
peer_id=l1.info["id"], channel_cap=100_000, opening_fee_msat=1000_000
|
||||
)
|
||||
|
||||
routehint = only_one(only_one(dec["routes"]))
|
||||
|
||||
parts = 10
|
||||
route_part = [
|
||||
{
|
||||
"amount_msat": amt // parts,
|
||||
"id": l2.info["id"],
|
||||
"delay": routehint["cltv_expiry_delta"] + 6,
|
||||
"channel": chanid,
|
||||
},
|
||||
{
|
||||
"amount_msat": amt // parts,
|
||||
"id": l1.info["id"],
|
||||
"delay": 6,
|
||||
"channel": routehint["short_channel_id"],
|
||||
},
|
||||
]
|
||||
|
||||
# MPP-payment of fixed amount
|
||||
for partid in range(1, parts + 1):
|
||||
r = l3.rpc.sendpay(
|
||||
route_part,
|
||||
dec["payment_hash"],
|
||||
payment_secret=inv["payment_secret"],
|
||||
bolt11=inv["bolt11"],
|
||||
amount_msat=f"{amt}msat",
|
||||
groupid=1,
|
||||
partid=partid,
|
||||
)
|
||||
assert r
|
||||
|
||||
res = l3.rpc.waitsendpay(dec["payment_hash"], partid=parts, groupid=1)
|
||||
assert res["payment_preimage"]
|
||||
|
||||
# l1 should have gotten a jit-channel.
|
||||
chs = l1.rpc.listpeerchannels()["channels"]
|
||||
assert len(chs) == 1
|
||||
|
||||
|
||||
def test_lsps2_non_approved_zero_conf(node_factory, bitcoind):
|
||||
"""Checks that we don't allow zerof_conf channels from an LSP if we did
|
||||
not approve it first.
|
||||
|
||||
Reference in New Issue
Block a user