@@ -18,12 +18,14 @@ use cln_lsps::util;
1818use cln_lsps:: LSP_FEATURE_BIT ;
1919use cln_plugin:: options;
2020use cln_rpc:: model:: requests:: {
21- DatastoreMode , DatastoreRequest , DeldatastoreRequest , ListdatastoreRequest , ListpeersRequest ,
21+ DatastoreMode , DatastoreRequest , DeldatastoreRequest , DelinvoiceRequest , DelinvoiceStatus ,
22+ ListdatastoreRequest , ListinvoicesRequest , ListpeersRequest ,
2223} ;
2324use cln_rpc:: model:: responses:: InvoiceResponse ;
24- use cln_rpc:: primitives:: { AmountOrAny , PublicKey , ShortChannelId } ;
25+ use cln_rpc:: primitives:: { Amount , AmountOrAny , PublicKey , ShortChannelId } ;
2526use cln_rpc:: ClnRpc ;
2627use log:: { debug, info, warn} ;
28+ use rand:: { CryptoRng , Rng } ;
2729use serde:: { Deserialize , Serialize } ;
2830use std:: path:: Path ;
2931use std:: str:: FromStr as _;
@@ -263,7 +265,15 @@ async fn on_lsps_lsps2_approve(
263265 hex : None ,
264266 mode : Some ( DatastoreMode :: CREATE_OR_REPLACE ) ,
265267 string : Some ( ds_rec_json) ,
266- key : vec ! [ "lsps" . to_string( ) , "client" . to_string( ) , req. lsp_id] ,
268+ key : vec ! [ "lsps" . to_string( ) , "client" . to_string( ) , req. lsp_id. clone( ) ] ,
269+ } ;
270+ let _ds_res = cln_client. call_typed ( & ds_req) . await ?;
271+ let ds_req = DatastoreRequest {
272+ generation : None ,
273+ hex : None ,
274+ mode : Some ( DatastoreMode :: CREATE_OR_REPLACE ) ,
275+ string : Some ( req. lsp_id ) ,
276+ key : vec ! [ "lsps" . to_string( ) , "invoice" . to_string( ) , req. payment_hash] ,
267277 } ;
268278 let _ds_res = cln_client. call_typed ( & ds_req) . await ?;
269279 Ok ( serde_json:: Value :: default ( ) )
@@ -338,6 +348,30 @@ async fn on_lsps_jitchannel(
338348 AmountOrAny :: Any => None ,
339349 } ;
340350
351+ // Check that the amount is big enough to cover the fee and a single HTLC.
352+ let reduced_amount_msat = if let Some ( payment_msat) = payment_size_msat {
353+ match compute_opening_fee (
354+ payment_msat. msat ( ) ,
355+ selected_params. min_fee_msat . msat ( ) ,
356+ selected_params. proportional . ppm ( ) as u64 ,
357+ ) {
358+ Some ( fee_msat) => {
359+ if payment_msat. msat ( ) - fee_msat < 1000 {
360+ bail ! (
361+ "amount_msat {}msat is too small, needs to be at least {}msat: opening fee is {}msat" ,
362+ payment_msat,
363+ 1000 + fee_msat,
364+ fee_msat
365+ ) ;
366+ }
367+ Some ( payment_msat. msat ( ) - fee_msat)
368+ }
369+ None => bail ! ( "failed to compute opening fee" ) ,
370+ }
371+ } else {
372+ None
373+ } ;
374+
341375 // 3. Request channel from LSP.
342376 let buy_res: Lsps2BuyResponse = cln_client
343377 . call_raw (
@@ -372,50 +406,91 @@ async fn on_lsps_jitchannel(
372406 cltv_expiry_delta : u16:: try_from ( buy_res. lsp_cltv_expiry_delta ) ?,
373407 } ;
374408
375- let inv: cln_rpc:: model:: responses:: InvoiceResponse = cln_client
409+ // Generate a preimage if we have an amount specified.
410+ let preimage = if payment_size_msat. is_some ( ) {
411+ Some ( gen_rand_preimage_hex ( & mut rand:: rng ( ) ) )
412+ } else {
413+ None
414+ } ;
415+
416+ let public_inv: cln_rpc:: model:: responses:: InvoiceResponse = cln_client
376417 . call_raw (
377418 "invoice" ,
378419 & InvoiceRequest {
379420 amount_msat : req. amount_msat ,
380- dev_routes : Some ( vec ! [ vec![ hint] ] ) ,
381- description : req. description ,
382- label : req. label ,
421+ dev_routes : Some ( vec ! [ vec![ hint. clone ( ) ] ] ) ,
422+ description : req. description . clone ( ) ,
423+ label : req. label . clone ( ) ,
383424 expiry : Some ( expiry as u64 ) ,
384- cltv : Some ( u32 :: try_from ( 6 + 2 ) ? ) , // TODO: FETCH REAL VALUE!
425+ cltv : None ,
385426 deschashonly : None ,
386- preimage : None ,
427+ preimage : preimage . clone ( ) ,
387428 exposeprivatechannels : None ,
388429 fallbacks : None ,
389430 } ,
390431 )
391432 . await ?;
392433
434+ // We need to reduce the expected amount if the invoice has an amount set
435+ if let Some ( amount_msat) = reduced_amount_msat {
436+ debug ! (
437+ "amount_msat is specified: create new invoice with reduced amount {}msat" ,
438+ amount_msat,
439+ ) ;
440+ let _ = cln_client
441+ . call_typed ( & DelinvoiceRequest {
442+ desconly : None ,
443+ status : DelinvoiceStatus :: UNPAID ,
444+ label : req. label . clone ( ) ,
445+ } )
446+ . await ?;
447+
448+ let _: cln_rpc:: model:: responses:: InvoiceResponse = cln_client
449+ . call_raw (
450+ "invoice" ,
451+ & InvoiceRequest {
452+ amount_msat : AmountOrAny :: Amount ( Amount :: from_msat ( amount_msat) ) ,
453+ dev_routes : Some ( vec ! [ vec![ hint] ] ) ,
454+ description : req. description ,
455+ label : req. label ,
456+ expiry : Some ( expiry as u64 ) ,
457+ cltv : None ,
458+ deschashonly : None ,
459+ preimage,
460+ exposeprivatechannels : None ,
461+ fallbacks : None ,
462+ } ,
463+ )
464+ . await ?;
465+ }
466+
393467 // 5. Approve jit_channel_scid for a jit channel opening.
394468 let appr_req = ClnRpcLsps2Approve {
395469 lsp_id : req. lsp_id ,
396470 jit_channel_scid : buy_res. jit_channel_scid ,
471+ payment_hash : public_inv. payment_hash . to_string ( ) ,
397472 client_trusts_lsp : Some ( buy_res. client_trusts_lsp ) ,
398473 } ;
399474 let _: serde_json:: Value = cln_client. call_raw ( "lsps-lsps2-approve" , & appr_req) . await ?;
400475
401476 // 6. Return invoice.
402477 let out = InvoiceResponse {
403- bolt11 : inv . bolt11 ,
404- created_index : inv . created_index ,
405- warning_capacity : inv . warning_capacity ,
406- warning_deadends : inv . warning_deadends ,
407- warning_mpp : inv . warning_mpp ,
408- warning_offline : inv . warning_offline ,
409- warning_private_unused : inv . warning_private_unused ,
410- expires_at : inv . expires_at ,
411- payment_hash : inv . payment_hash ,
412- payment_secret : inv . payment_secret ,
478+ bolt11 : public_inv . bolt11 ,
479+ created_index : public_inv . created_index ,
480+ warning_capacity : public_inv . warning_capacity ,
481+ warning_deadends : public_inv . warning_deadends ,
482+ warning_mpp : public_inv . warning_mpp ,
483+ warning_offline : public_inv . warning_offline ,
484+ warning_private_unused : public_inv . warning_private_unused ,
485+ expires_at : public_inv . expires_at ,
486+ payment_hash : public_inv . payment_hash ,
487+ payment_secret : public_inv . payment_secret ,
413488 } ;
414489 Ok ( serde_json:: to_value ( out) ?)
415490}
416491
417492async fn on_htlc_accepted (
418- _p : cln_plugin:: Plugin < State > ,
493+ p : cln_plugin:: Plugin < State > ,
419494 v : serde_json:: Value ,
420495) -> Result < serde_json:: Value , anyhow:: Error > {
421496 let req: HtlcAcceptedRequest = serde_json:: from_value ( v) ?;
@@ -424,38 +499,87 @@ async fn on_htlc_accepted(
424499 let onion_amt = match req. onion . forward_msat {
425500 Some ( a) => a,
426501 None => {
427- debug ! ( "onion is missing forward_msat" ) ;
502+ debug ! ( "onion is missing forward_msat, continue " ) ;
428503 let value = serde_json:: to_value ( HtlcAcceptedResponse :: continue_ ( None , None , None ) ) ?;
429504 return Ok ( value) ;
430505 }
431506 } ;
432507
433- let is_lsp_payment = req
508+ let Some ( payment_data) = req. onion . payload . get ( TLV_PAYMENT_SECRET ) else {
509+ debug ! ( "payment is a forward, continue" ) ;
510+ let value = serde_json:: to_value ( HtlcAcceptedResponse :: continue_ ( None , None , None ) ) ?;
511+ return Ok ( value) ;
512+ } ;
513+
514+ let extra_fee_msat = req
434515 . htlc
435516 . extra_tlvs
436517 . as_ref ( )
437- . map_or ( false , |tlv| tlv. contains ( 65537 ) ) ;
518+ . map ( |tlvs| tlvs. get_u64 ( 65537 ) )
519+ . transpose ( ) ?
520+ . flatten ( ) ;
521+ if let Some ( amt) = extra_fee_msat {
522+ debug ! ( "lsp htlc is deducted by an extra_fee={amt}" ) ;
523+ }
524+
525+ // Check that the htlc belongs to a jit-channel request.
526+ let dir = p. configuration ( ) . lightning_dir ;
527+ let rpc_path = Path :: new ( & dir) . join ( & p. configuration ( ) . rpc_file ) ;
528+ let mut cln_client = cln_rpc:: ClnRpc :: new ( rpc_path. clone ( ) ) . await ?;
529+ let lsp_data = cln_client
530+ . call_typed ( & ListdatastoreRequest {
531+ key : Some ( vec ! [
532+ "lsps" . to_string( ) ,
533+ "invoice" . to_string( ) ,
534+ hex:: encode( & req. htlc. payment_hash) ,
535+ ] ) ,
536+ } )
537+ . await ?;
438538
439- if !is_lsp_payment || htlc_amt. msat ( ) >= onion_amt. msat ( ) {
440- // Not an Lsp payment.
539+ if lsp_data. datastore . first ( ) . is_none ( ) {
540+ // Not an LSP payment, just continue
541+ debug ! ( "payment is a not a jit-channel-opening, continue" ) ;
441542 let value = serde_json:: to_value ( HtlcAcceptedResponse :: continue_ ( None , None , None ) ) ?;
442543 return Ok ( value) ;
443- }
444- debug ! ( "incoming jit-channel htlc" ) ;
544+ } ;
445545
446- // Safe unwrap(): we already checked that `extra_tlvs` exists.
447- let extra_tlvs = req. htlc . extra_tlvs . unwrap ( ) ;
448- let deducted_amt = match extra_tlvs. get_u64 ( 65537 ) ? {
449- Some ( amt) => amt,
546+ debug ! (
547+ "incoming jit-channel htlc with htlc_amt={} and onion_amt={}" ,
548+ htlc_amt. msat( ) ,
549+ onion_amt. msat( )
550+ ) ;
551+
552+ let inv_res = cln_client
553+ . call_typed ( & ListinvoicesRequest {
554+ index : None ,
555+ invstring : None ,
556+ label : None ,
557+ limit : None ,
558+ offer_id : None ,
559+ payment_hash : Some ( hex:: encode ( & req. htlc . payment_hash ) ) ,
560+ start : None ,
561+ } )
562+ . await ?;
563+
564+ let Some ( invoice) = inv_res. invoices . first ( ) else {
565+ debug ! (
566+ "no invoice found for jit-channel opening with payment_hash={}" ,
567+ hex:: encode( & req. htlc. payment_hash)
568+ ) ;
569+ let value = serde_json:: to_value ( HtlcAcceptedResponse :: continue_ ( None , None , None ) ) ?;
570+ return Ok ( value) ;
571+ } ;
572+
573+ let total_amt = match invoice. amount_msat {
574+ Some ( a) => {
575+ debug ! ( "invoice has total_amt={}msat" , & a. msat( ) ) ;
576+ a. msat ( )
577+ }
450578 None => {
451- warn ! ( "htlc is missing the extra_fee amount" ) ;
452- let value = serde_json:: to_value ( HtlcAcceptedResponse :: continue_ ( None , None , None ) ) ?;
453- return Ok ( value) ;
579+ debug ! ( "invoice has no total amount, only accept single htlc" ) ;
580+ htlc_amt. msat ( )
454581 }
455582 } ;
456- debug ! ( "lsp htlc is deducted by an extra_fee={}" , deducted_amt) ;
457-
458- // Fixme: Check that it is not a forward (has payment_secret) before rpc_calls.
459583
460584 // Fixme: Check that we did not already pay for this channel.
461585 // - via datastore or invoice label.
@@ -465,18 +589,9 @@ async fn on_htlc_accepted(
465589
466590 let mut payload = req. onion . payload . clone ( ) ;
467591 payload. set_tu64 ( TLV_FORWARD_AMT , htlc_amt. msat ( ) ) ;
468- let payment_secret = match payload. get ( TLV_PAYMENT_SECRET ) {
469- Some ( s) => s,
470- None => {
471- debug ! ( "can't decode tlv payment_secret {:?}" , payload) ;
472- let value = serde_json:: to_value ( HtlcAcceptedResponse :: continue_ ( None , None , None ) ) ?;
473- return Ok ( value) ;
474- }
475- } ;
476592
477- let total_amt = htlc_amt. msat ( ) ;
478593 let mut ps = Vec :: new ( ) ;
479- ps. extend_from_slice ( & payment_secret [ 0 ..32 ] ) ;
594+ ps. extend_from_slice ( & payment_data [ 0 ..32 ] ) ;
480595 ps. extend ( encode_tu64 ( total_amt) ) ;
481596 payload. insert ( TLV_PAYMENT_SECRET , ps) ;
482597 let payload_bytes = match payload. to_bytes ( ) {
@@ -640,6 +755,12 @@ async fn check_peer_lsp_status(
640755 } )
641756}
642757
758+ pub fn gen_rand_preimage_hex < R : Rng + CryptoRng > ( rng : & mut R ) -> String {
759+ let mut pre = [ 0u8 ; 32 ] ;
760+ rng. fill_bytes ( & mut pre) ;
761+ hex:: encode ( & pre)
762+ }
763+
643764#[ derive( Debug , Clone , PartialEq , Serialize , Deserialize ) ]
644765struct LspsBuyJitChannelResponse {
645766 bolt11 : String ,
@@ -694,6 +815,7 @@ struct ClnRpcLsps2GetinfoRequest {
694815struct ClnRpcLsps2Approve {
695816 lsp_id : String ,
696817 jit_channel_scid : ShortChannelId ,
818+ payment_hash : String ,
697819 #[ serde( default ) ]
698820 client_trusts_lsp : Option < bool > ,
699821}
0 commit comments