1use std::path::PathBuf;
2use std::str::FromStr;
3use std::time::Duration;
4
5use anyhow::{bail, ensure};
6use bitcoin::hashes::{Hash, sha256};
7use clap::{Parser, Subcommand};
8use devimint::devfed::{DevFed, DevJitFed};
9use devimint::envs::FM_CLIENT_DIR_ENV;
10use devimint::federation::{Client, Federation};
11use devimint::util::{ProcessManager, almost_equal, poll_simple};
12use devimint::version_constants::{
13 VERSION_0_10_0_ALPHA, VERSION_0_11_0_ALPHA, VERSION_0_12_0_ALPHA, VERSION_0_12_2_ALPHA,
14};
15use devimint::{Gatewayd, cmd, util};
16use fedimint_core::core::OperationId;
17use fedimint_core::encoding::Encodable;
18use fedimint_core::task::{self};
19use fedimint_core::util::{backoff_util, retry, write_overwrite_async};
20use fedimint_lnurl::{LnurlResponse, VerifyResponse, parse_lnurl};
21use fedimint_lnv2_client::FinalSendOperationState;
22use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescriptionRef};
23use serde::Deserialize;
24use tokio::try_join;
25use tracing::info;
26
27#[path = "common.rs"]
28mod common;
29
30async fn module_is_present(client: &Client, kind: &str) -> anyhow::Result<bool> {
31 let modules = cmd!(client, "module").out_json().await?;
32
33 let modules = modules["list"].as_array().expect("module list is an array");
34
35 Ok(modules.iter().any(|m| m["kind"].as_str() == Some(kind)))
36}
37
38async fn assert_module_sanity(client: &Client) -> anyhow::Result<()> {
39 if !devimint::util::is_backwards_compatibility_test() {
40 ensure!(
41 !module_is_present(client, "ln").await?,
42 "ln module should not be present"
43 );
44 }
45
46 Ok(())
47}
48
49#[derive(Parser)]
50#[command(name = "lnv2-module-tests")]
51#[command(about = "LNv2 module integration tests", long_about = None)]
52struct Cli {
53 #[command(subcommand)]
54 command: Option<Commands>,
55}
56
57#[derive(Subcommand)]
58enum Commands {
59 GatewayRegistration,
61 Payments,
63 LnurlPay,
65 LnurlRecovery,
67 DuplicatePayment,
69 FederationOutage,
72}
73
74#[tokio::main]
75async fn main() -> anyhow::Result<()> {
76 let cli = Cli::parse();
77
78 let num_feds = if matches!(cli.command, None | Some(Commands::DuplicatePayment)) {
81 2
82 } else {
83 1
84 };
85
86 devimint::run_devfed_test()
87 .num_feds(num_feds)
88 .call(|dev_fed, process_mgr| async move {
89 if !devimint::util::supports_lnv2() {
90 info!("lnv2 is disabled, skipping");
91 return Ok(());
92 }
93
94 match &cli.command {
95 Some(Commands::GatewayRegistration) => {
96 test_gateway_registration(&dev_fed).await?;
97 }
98 Some(Commands::Payments) => {
99 test_payments(&dev_fed).await?;
100 }
101 Some(Commands::LnurlPay) => {
102 pegin_gateways(&dev_fed).await?;
103 test_lnurl_pay(&dev_fed).await?;
104 }
105 Some(Commands::LnurlRecovery) => {
106 pegin_gateways(&dev_fed).await?;
107 test_lnurl_recovery(&dev_fed).await?;
108 }
109 Some(Commands::DuplicatePayment) => {
110 pegin_gateways(&dev_fed).await?;
111 test_duplicate_payment(&dev_fed, &process_mgr).await?;
112 }
113 Some(Commands::FederationOutage) => {
114 pegin_gateways(&dev_fed).await?;
115 let dev_fed = dev_fed.to_dev_fed(&process_mgr).await?;
116 test_federation_outage(dev_fed, &process_mgr).await?;
117 }
118 None => {
119 test_gateway_registration(&dev_fed).await?;
121 test_payments(&dev_fed).await?;
122 test_duplicate_payment(&dev_fed, &process_mgr).await?;
123 test_lnurl_pay(&dev_fed).await?;
124 test_lnurl_recovery(&dev_fed).await?;
125 pegin_gateways(&dev_fed).await?;
128 let dev_fed = dev_fed.to_dev_fed(&process_mgr).await?;
129 test_federation_outage(dev_fed, &process_mgr).await?;
130 }
131 }
132
133 info!("Testing LNV2 is complete!");
134
135 Ok(())
136 })
137 .await
138}
139
140async fn pegin_gateways(dev_fed: &DevJitFed) -> anyhow::Result<()> {
141 info!("Pegging-in gateways...");
142
143 let federation = dev_fed.fed().await?;
144
145 let gw_lnd = dev_fed.gw_lnd().await?;
146 let gw_ldk = dev_fed.gw_ldk().await?;
147
148 federation
149 .pegin_gateways(1_000_000, vec![gw_lnd, gw_ldk])
150 .await?;
151
152 Ok(())
153}
154
155async fn test_gateway_registration(dev_fed: &DevJitFed) -> anyhow::Result<()> {
156 let client = dev_fed
157 .fed()
158 .await?
159 .new_joined_client("lnv2-test-gateway-registration-client")
160 .await?;
161
162 assert_module_sanity(&client).await?;
163
164 let gw_lnd = dev_fed.gw_lnd().await?;
165 let gw_ldk = dev_fed.gw_ldk_connected().await?;
166
167 let gateways = [gw_lnd.addr.clone(), gw_ldk.addr.clone()];
168
169 info!("Removing the gateways devimint added on startup...");
170
171 let peers = (0..dev_fed.fed().await?.members.len()).collect::<Vec<usize>>();
172 remove_all_gateways(&client, &peers).await?;
173
174 info!("Testing registration of gateways...");
175
176 for gateway in &gateways {
177 for peer in 0..dev_fed.fed().await?.members.len() {
178 assert!(add_gateway(&client, peer, gateway).await?);
179 }
180 }
181
182 assert_eq!(
183 cmd!(client, "module", "lnv2", "gateways", "list")
184 .out_json()
185 .await?
186 .as_array()
187 .expect("JSON Value is not an array")
188 .len(),
189 2
190 );
191
192 assert_eq!(
193 cmd!(client, "module", "lnv2", "gateways", "list", "--peer", "0")
194 .out_json()
195 .await?
196 .as_array()
197 .expect("JSON Value is not an array")
198 .len(),
199 2
200 );
201
202 info!("Testing selection of gateways...");
203
204 assert!(
205 gateways.contains(
206 &cmd!(client, "module", "lnv2", "gateways", "select")
207 .out_json()
208 .await?
209 .as_str()
210 .expect("JSON Value is not a string")
211 .to_string()
212 )
213 );
214
215 cmd!(client, "module", "lnv2", "gateways", "map")
216 .out_json()
217 .await?;
218
219 for _ in 0..10 {
220 for gateway in &gateways {
221 let invoice = common::receive(&client, gateway, 1_000_000).await?.0;
222
223 assert_eq!(
224 cmd!(
225 client,
226 "module",
227 "lnv2",
228 "gateways",
229 "select",
230 "--invoice",
231 invoice.to_string()
232 )
233 .out_json()
234 .await?
235 .as_str()
236 .expect("JSON Value is not a string"),
237 gateway
238 )
239 }
240 }
241
242 info!("Testing deregistration of gateways...");
243
244 for gateway in &gateways {
245 for peer in 0..dev_fed.fed().await?.members.len() {
246 assert!(remove_gateway(&client, peer, gateway).await?);
247 }
248 }
249
250 assert!(
251 cmd!(client, "module", "lnv2", "gateways", "list")
252 .out_json()
253 .await?
254 .as_array()
255 .expect("JSON Value is not an array")
256 .is_empty(),
257 );
258
259 assert!(
260 cmd!(client, "module", "lnv2", "gateways", "list", "--peer", "0")
261 .out_json()
262 .await?
263 .as_array()
264 .expect("JSON Value is not an array")
265 .is_empty()
266 );
267
268 Ok(())
269}
270
271async fn test_payments(dev_fed: &DevJitFed) -> anyhow::Result<()> {
272 let federation = dev_fed.fed().await?;
273
274 let client = federation
275 .new_joined_client("lnv2-test-payments-client")
276 .await?;
277
278 assert_module_sanity(&client).await?;
279
280 federation.pegin_client(10_000, &client).await?;
281
282 almost_equal(client.balance().await?, 10_000 * 1000, 500_000).unwrap();
283
284 let gw_lnd = dev_fed.gw_lnd().await?;
285 let gw_ldk = dev_fed.gw_ldk().await?;
286 let lnd = dev_fed.lnd().await?;
287
288 let test_hold_invoice = gw_lnd.gatewayd_version >= *VERSION_0_12_0_ALPHA;
297
298 let hold_invoice = if test_hold_invoice {
299 Some(lnd.create_hold_invoice(60000).await?)
300 } else {
301 info!(
302 gatewayd_version = %gw_lnd.gatewayd_version,
303 "Skipping HOLD invoice payment: gateway cancels HOLD invoices it did not create"
304 );
305
306 None
307 };
308
309 let gateway_pairs = [(gw_lnd, gw_ldk), (gw_ldk, gw_lnd)];
310
311 let gateway_matrix = [
312 (gw_lnd, gw_lnd),
313 (gw_lnd, gw_ldk),
314 (gw_ldk, gw_lnd),
315 (gw_ldk, gw_ldk),
316 ];
317
318 info!("Testing refund of circular payments...");
319
320 for (gw_send, gw_receive) in gateway_matrix {
321 info!(
322 "Testing refund of payment: client -> {} -> {} -> client",
323 gw_send.ln.ln_type(),
324 gw_receive.ln.ln_type()
325 );
326
327 let invoice = common::receive(&client, &gw_receive.addr, 1_000_000)
328 .await?
329 .0;
330
331 let state = common::send(&client, &gw_send.addr, &invoice.to_string()).await?;
332 assert!(matches!(state, FinalSendOperationState::Refunded));
333 }
334
335 pegin_gateways(dev_fed).await?;
336
337 info!("Testing circular payments...");
338
339 for (gw_send, gw_receive) in gateway_matrix {
340 info!(
341 "Testing payment: client -> {} -> {} -> client",
342 gw_send.ln.ln_type(),
343 gw_receive.ln.ln_type()
344 );
345
346 let (invoice, receive_op) = common::receive(&client, &gw_receive.addr, 1_000_000).await?;
347
348 let state = common::send(&client, &gw_send.addr, &invoice.to_string()).await?;
349 assert!(matches!(state, FinalSendOperationState::Success(_)));
350
351 common::await_receive_claimed(&client, receive_op).await?;
352 }
353
354 info!("Testing payments from client to gateways...");
355
356 for (gw_send, gw_receive) in gateway_pairs {
357 info!(
358 "Testing payment: client -> {} -> {}",
359 gw_send.ln.ln_type(),
360 gw_receive.ln.ln_type()
361 );
362
363 let invoice = gw_receive.client().create_invoice(1_000_000).await?;
364
365 let state = common::send(&client, &gw_send.addr, &invoice.to_string()).await?;
366 assert!(matches!(state, FinalSendOperationState::Success(_)));
367 }
368
369 info!("Testing payments from gateways to client...");
370
371 for (gw_send, gw_receive) in gateway_pairs {
372 info!(
373 "Testing payment: {} -> {} -> client",
374 gw_send.ln.ln_type(),
375 gw_receive.ln.ln_type()
376 );
377
378 let (invoice, receive_op) = common::receive(&client, &gw_receive.addr, 1_000_000).await?;
379
380 gw_send.client().pay_invoice(invoice).await?;
381
382 common::await_receive_claimed(&client, receive_op).await?;
383 }
384
385 retry(
386 "Waiting for the full balance to become available to the client".to_string(),
387 backoff_util::background_backoff(),
388 || async {
389 ensure!(client.balance().await? >= 9000 * 1000);
390
391 Ok(())
392 },
393 )
394 .await?;
395
396 if let Some((hold_preimage, hold_invoice, hold_payment_hash)) = hold_invoice {
397 info!("Testing Client can pay LND HOLD invoice via LDK Gateway...");
398
399 let (state, _) = try_join!(
400 common::send(&client, &gw_ldk.addr, &hold_invoice),
401 lnd.settle_hold_invoice(hold_preimage, hold_payment_hash),
402 )?;
403 assert!(matches!(state, FinalSendOperationState::Success(_)));
404 }
405
406 info!("Testing LNv2 lightning fees...");
407
408 let fed_id = federation.calculate_federation_id();
409
410 gw_lnd
411 .client()
412 .set_federation_routing_fee(fed_id.clone(), 0, 0)
413 .await?;
414
415 gw_lnd
416 .client()
417 .set_federation_transaction_fee(fed_id.clone(), 0, 0)
418 .await?;
419
420 test_fees(fed_id, &client, gw_lnd, gw_ldk, 1_000_000 - 1_000).await?;
423
424 let online_peers: Vec<usize> = federation.members.keys().copied().collect();
425
426 test_iroh_payment(&client, gw_lnd, gw_ldk, &online_peers).await?;
427
428 info!("Testing payment summary...");
429
430 let lnd_payment_summary = gw_lnd.client().payment_summary().await?;
431
432 assert_eq!(lnd_payment_summary.outgoing.total_success, 5);
433 assert_eq!(lnd_payment_summary.outgoing.total_failure, 2);
434 assert_eq!(lnd_payment_summary.incoming.total_success, 4);
435 assert_eq!(lnd_payment_summary.incoming.total_failure, 0);
436
437 assert!(lnd_payment_summary.outgoing.median_latency.is_some());
438 assert!(lnd_payment_summary.outgoing.average_latency.is_some());
439 assert!(lnd_payment_summary.incoming.median_latency.is_some());
440 assert!(lnd_payment_summary.incoming.average_latency.is_some());
441
442 let ldk_payment_summary = gw_ldk.client().payment_summary().await?;
443
444 assert_eq!(
445 ldk_payment_summary.outgoing.total_success,
446 if test_hold_invoice { 4 } else { 3 }
447 );
448 assert_eq!(ldk_payment_summary.outgoing.total_failure, 2);
449 assert_eq!(ldk_payment_summary.incoming.total_success, 4);
450 assert_eq!(ldk_payment_summary.incoming.total_failure, 0);
451
452 assert!(ldk_payment_summary.outgoing.median_latency.is_some());
453 assert!(ldk_payment_summary.outgoing.average_latency.is_some());
454 assert!(ldk_payment_summary.incoming.median_latency.is_some());
455 assert!(ldk_payment_summary.incoming.average_latency.is_some());
456
457 Ok(())
458}
459
460async fn join_client(name: &str, invite_code: &str) -> anyhow::Result<Client> {
464 let client = Client::create(name).await?;
465 client.join_federation(invite_code.to_string()).await?;
466 Ok(client)
467}
468
469async fn test_duplicate_payment(
477 dev_fed: &DevJitFed,
478 process_mgr: &ProcessManager,
479) -> anyhow::Result<()> {
480 info!(
481 "Testing three clients across two federations paying the same invoice settle exactly once..."
482 );
483
484 let gw_send = dev_fed.gw_ldk().await?;
487 let gw_receive = dev_fed.gw_lnd().await?;
488
489 if gw_send.gatewayd_version < *VERSION_0_12_0_ALPHA {
492 info!(
493 gatewayd_version = %gw_send.gatewayd_version,
494 "Skipping: gateway predates cross-federation outgoing payment dedup"
495 );
496 return Ok(());
497 }
498
499 let first_federation = dev_fed.fed().await?;
500
501 let first_invite = first_federation.invite_code()?;
506
507 let second_federation = Federation::new(
511 process_mgr,
512 dev_fed.bitcoind().await?.clone(),
513 false,
514 false,
515 false,
516 1,
517 "lnv2-duplicate-payment".to_string(),
518 )
519 .await?;
520 let second_invite = second_federation.invite_code()?;
521
522 assert_ne!(
523 first_federation.calculate_federation_id(),
524 second_federation.calculate_federation_id(),
525 "the second federation must be distinct from the first"
526 );
527
528 let client_dir: PathBuf = std::env::var(FM_CLIENT_DIR_ENV)?.parse()?;
534 write_overwrite_async(client_dir.join("invite-code"), &first_invite).await?;
535
536 gw_send.client().connect_fed(second_invite.clone()).await?;
537 second_federation
538 .pegin_gateways(1_000_000, vec![gw_send])
539 .await?;
540
541 let client_a = join_client("lnv2-duplicate-payment-a", &first_invite).await?;
542 let client_b = join_client("lnv2-duplicate-payment-b", &first_invite).await?;
543 let client_c = join_client("lnv2-duplicate-payment-c", &second_invite).await?;
544
545 first_federation.pegin_client(10_000, &client_a).await?;
546 first_federation.pegin_client(10_000, &client_b).await?;
547 second_federation.pegin_client(10_000, &client_c).await?;
548
549 let second_federation_id = second_federation.calculate_federation_id();
554 let second_balance_before_control = gw_send
555 .client()
556 .ecash_balance(second_federation_id.clone())
557 .await?;
558 let control_invoice = gw_receive
559 .client()
560 .create_invoice(1_000_000)
561 .await?
562 .to_string();
563 let control = common::send(&client_c, &gw_send.addr, &control_invoice).await?;
564 assert!(
565 matches!(control, FinalSendOperationState::Success(_)),
566 "second-federation control payment must succeed, got {control:?}"
567 );
568
569 poll_simple(
573 "Waiting for the gateway to claim the control payment",
574 || async {
575 let balance = gw_send
576 .client()
577 .ecash_balance(second_federation_id.clone())
578 .await?;
579 ensure!(
580 balance > second_balance_before_control,
581 "gateway has not claimed the control payment yet"
582 );
583 Ok(())
584 },
585 )
586 .await?;
587
588 let invoice = gw_receive
589 .client()
590 .create_invoice(1_000_000)
591 .await?
592 .to_string();
593
594 if gw_send.gatewayd_version < *VERSION_0_12_2_ALPHA {
599 return assert_duplicates_refunded(&client_a, &client_b, &client_c, gw_send, &invoice)
600 .await;
601 }
602
603 assert_duplicates_refused(
604 [&client_a, &client_b, &client_c],
605 gw_send,
606 [
607 first_federation.calculate_federation_id(),
608 second_federation_id,
609 ],
610 &invoice,
611 )
612 .await
613}
614
615const REFUSED_SEND_WATCH: Duration = Duration::from_secs(30);
617
618async fn assert_duplicates_refunded(
621 client_a: &Client,
622 client_b: &Client,
623 client_c: &Client,
624 gw_send: &Gatewayd,
625 invoice: &str,
626) -> anyhow::Result<()> {
627 let (state_a, state_b) = try_join!(
630 common::send(client_a, &gw_send.addr, invoice),
631 common::send(client_b, &gw_send.addr, invoice),
632 )?;
633
634 let state_c = common::send(client_c, &gw_send.addr, invoice).await?;
638
639 let states = [state_a, state_b, state_c];
640
641 info!("shared-invoice send states: {states:?}");
642
643 let successes = states
644 .iter()
645 .filter(|state| matches!(state, FinalSendOperationState::Success(_)))
646 .count();
647
648 let refunds = states
649 .iter()
650 .filter(|state| matches!(state, FinalSendOperationState::Refunded))
651 .count();
652
653 assert_eq!(
654 (successes, refunds),
655 (1, 2),
656 "exactly one payment must settle and the other two be refunded, got {states:?}"
657 );
658
659 Ok(())
660}
661
662async fn assert_duplicates_refused(
667 [client_a, client_b, client_c]: [&Client; 3],
668 gw_send: &Gatewayd,
669 [first_federation_id, second_federation_id]: [String; 2],
670 invoice: &str,
671) -> anyhow::Result<()> {
672 let first_balance_before = gw_send
673 .client()
674 .ecash_balance(first_federation_id.clone())
675 .await?;
676 let second_balance_before = gw_send
677 .client()
678 .ecash_balance(second_federation_id.clone())
679 .await?;
680
681 let (op_a, op_b) = try_join!(
684 common::start_send(client_a, &gw_send.addr, invoice),
685 common::start_send(client_b, &gw_send.addr, invoice),
686 )?;
687
688 let (state_a, state_b) = tokio::join!(
689 await_send_within(client_a, op_a, REFUSED_SEND_WATCH),
690 await_send_within(client_b, op_b, REFUSED_SEND_WATCH),
691 );
692
693 let op_c = common::start_send(client_c, &gw_send.addr, invoice).await?;
696 let state_c = await_send_within(client_c, op_c, REFUSED_SEND_WATCH).await;
697
698 let states = [state_a?, state_b?, state_c?];
699
700 info!("shared-invoice send states (None = unresolved): {states:?}");
701
702 let successes = states
703 .iter()
704 .filter(|state| matches!(state, Some(FinalSendOperationState::Success(_))))
705 .count();
706
707 let unresolved = states.iter().filter(|state| state.is_none()).count();
708
709 assert_eq!(
710 (successes, unresolved),
711 (1, 2),
712 "exactly one payment must settle and the other two stay unresolved, got {states:?}"
713 );
714
715 let invoice_msats = Bolt11Invoice::from_str(invoice)?
718 .amount_milli_satoshis()
719 .expect("the invoice has an amount");
720
721 let first_gain = gw_send
722 .client()
723 .ecash_balance(first_federation_id)
724 .await?
725 .checked_sub(first_balance_before)
726 .expect("the gateway's balance in the first federation must not shrink");
727
728 assert!(
729 (invoice_msats..2 * invoice_msats).contains(&first_gain),
730 "the gateway must be reimbursed for exactly one contract, gained {first_gain} msat"
731 );
732
733 assert_eq!(
734 gw_send.client().ecash_balance(second_federation_id).await?,
735 second_balance_before,
736 "the gateway must not claim the second federation's contract"
737 );
738
739 Ok(())
740}
741
742async fn await_send_within(
745 client: &Client,
746 operation_id: OperationId,
747 watch: Duration,
748) -> anyhow::Result<Option<FinalSendOperationState>> {
749 match tokio::time::timeout(watch, common::await_send(client, operation_id)).await {
750 Ok(state) => state.map(Some),
751 Err(_) => Ok(None),
752 }
753}
754
755async fn test_federation_outage(
759 dev_fed: DevFed,
760 process_mgr: &ProcessManager,
761) -> anyhow::Result<()> {
762 info!("Testing that the gateway refuses to fund a receive during a federation outage...");
763
764 let DevFed {
765 mut fed,
766 gw_lnd,
767 gw_ldk,
768 ..
769 } = dev_fed;
770
771 if gw_lnd.gatewayd_version < *VERSION_0_12_2_ALPHA {
772 info!(
773 gatewayd_version = %gw_lnd.gatewayd_version,
774 "Skipping: gateway predates the federation liveness probe"
775 );
776 return Ok(());
777 }
778
779 let client = fed
780 .new_joined_client("lnv2-federation-outage-client")
781 .await?;
782 fed.await_all_peers().await?;
783
784 let federation_id = fed.calculate_federation_id();
785 let gateway = gw_lnd.client().address();
786
787 let (invoice, _) = common::receive(&client, &gateway, 100_000).await?;
790 let balance_before = gw_lnd.client().ecash_balance(federation_id.clone()).await?;
791
792 let fed_size = process_mgr.globals.FM_FED_SIZE;
795 let max_evil = (fed_size - 1) / 3;
796 let stopped: Vec<usize> = (fed_size - max_evil - 1..fed_size).collect();
797 for peer in &stopped {
798 fed.terminate_server(*peer).await?;
799 }
800
801 info!(
802 ?stopped,
803 "Paying invoice while the federation cannot reach consensus"
804 );
805 let payment = fedimint_core::runtime::spawn("lnv2-outage-payment", {
809 let gw_ldk = gw_ldk.client();
810 async move { gw_ldk.pay_invoice(invoice).await }
811 });
812
813 poll_simple("Waiting for the gateway to act on the HTLC", || async {
817 if payment.is_finished() {
818 return Ok(());
819 }
820 let balance = gw_lnd.client().ecash_balance(federation_id.clone()).await?;
821 if balance < balance_before {
822 return Ok(());
823 }
824 bail!("gateway has not acted on the HTLC yet")
825 })
826 .await?;
827
828 for peer in &stopped {
829 fed.start_server(process_mgr, *peer).await?;
830 }
831 fed.await_all_peers().await?;
832
833 let outcome = payment.await?;
837 info!(?outcome, "Payment outcome once the guardians are back");
838 ensure!(
839 outcome.is_err(),
840 "payment must fail: the gateway funded an incoming contract during the outage"
841 );
842 ensure!(
843 gw_lnd.client().ecash_balance(federation_id.clone()).await? == balance_before,
844 "gateway must not fund an incoming contract while the federation cannot reach consensus"
845 );
846
847 info!("Paying a fresh invoice after the guardians are back");
851 poll_simple("Waiting for receives to succeed again", || async {
852 let (invoice, _) = common::receive(&client, &gateway, 100_000).await?;
853 gw_ldk.client().pay_invoice(invoice).await
854 })
855 .await?;
856 ensure!(
857 gw_lnd.client().ecash_balance(federation_id).await? < balance_before,
858 "gateway must fund the incoming contract once the federation is back"
859 );
860
861 info!("Federation outage test complete");
862 Ok(())
863}
864
865async fn test_fees(
866 fed_id: String,
867 client: &Client,
868 gw_lnd: &Gatewayd,
869 gw_ldk: &Gatewayd,
870 expected_addition: u64,
871) -> anyhow::Result<()> {
872 let gw_lnd_ecash_prev = gw_lnd.client().ecash_balance(fed_id.clone()).await?;
873
874 let (invoice, receive_op) = common::receive(client, &gw_ldk.addr, 1_000_000).await?;
875
876 let state = common::send(client, &gw_lnd.addr, &invoice.to_string()).await?;
877 assert!(matches!(state, FinalSendOperationState::Success(_)));
878
879 common::await_receive_claimed(client, receive_op).await?;
880
881 while almost_equal(
885 gw_lnd_ecash_prev + expected_addition,
886 gw_lnd.client().ecash_balance(fed_id.clone()).await?,
887 5000,
888 )
889 .is_err()
890 {
891 info!("Waiting for the sending gateway's outgoing claim to settle...");
892 cmd!(client, "dev", "wait", "1").out_json().await?;
893 }
894
895 Ok(())
896}
897
898async fn add_gateway(client: &Client, peer: usize, gateway: &String) -> anyhow::Result<bool> {
899 cmd!(
900 client,
901 "--our-id",
902 peer.to_string(),
903 "--password",
904 "pass",
905 "module",
906 "lnv2",
907 "gateways",
908 "add",
909 gateway
910 )
911 .out_json()
912 .await?
913 .as_bool()
914 .ok_or(anyhow::anyhow!("JSON Value is not a boolean"))
915}
916
917async fn remove_gateway(client: &Client, peer: usize, gateway: &String) -> anyhow::Result<bool> {
918 cmd!(
919 client,
920 "--our-id",
921 peer.to_string(),
922 "--password",
923 "pass",
924 "module",
925 "lnv2",
926 "gateways",
927 "remove",
928 gateway
929 )
930 .out_json()
931 .await?
932 .as_bool()
933 .ok_or(anyhow::anyhow!("JSON Value is not a boolean"))
934}
935
936async fn remove_all_gateways(client: &Client, peers: &[usize]) -> anyhow::Result<()> {
939 let gateways = cmd!(client, "module", "lnv2", "gateways", "list")
940 .out_json()
941 .await?
942 .as_array()
943 .expect("JSON Value is not an array")
944 .iter()
945 .map(|gateway| {
946 gateway
947 .as_str()
948 .expect("JSON Value is not a string")
949 .to_string()
950 })
951 .collect::<Vec<String>>();
952
953 for gateway in &gateways {
954 for &peer in peers {
955 assert!(remove_gateway(client, peer, gateway).await?);
956 }
957 }
958
959 Ok(())
960}
961
962const LNURL_BALANCE_WAIT_ATTEMPTS: u64 = 120;
965
966async fn wait_for_lnurl_balance(
967 client: &Client,
968 client_name: &str,
969 expected_msats: u64,
970) -> anyhow::Result<()> {
971 let mut last_balance_msats = client.balance().await?;
972
973 for attempt in 1..=LNURL_BALANCE_WAIT_ATTEMPTS {
974 if last_balance_msats < expected_msats {
975 info!(
976 client_name,
977 balance_msats = last_balance_msats,
978 expected_msats,
979 attempt,
980 "Waiting for {client_name} to receive funds via LNURL"
981 );
982 cmd!(client, "dev", "wait", "1").out_json().await?;
983 last_balance_msats = client.balance().await?;
984 } else {
985 info!(
986 client_name,
987 balance_msats = last_balance_msats,
988 expected_msats,
989 attempt,
990 "{client_name} successfully received funds via LNURL"
991 );
992 return Ok(());
993 }
994 }
995
996 if last_balance_msats < expected_msats {
997 anyhow::bail!(
998 "timed out waiting for {client_name} to receive funds via LNURL after {} attempts; last balance: {last_balance_msats} msats; expected balance: {expected_msats} msats",
999 LNURL_BALANCE_WAIT_ATTEMPTS
1000 );
1001 }
1002
1003 info!(
1004 client_name,
1005 balance_msats = last_balance_msats,
1006 expected_msats,
1007 attempt = LNURL_BALANCE_WAIT_ATTEMPTS,
1008 "{client_name} successfully received funds via LNURL"
1009 );
1010
1011 Ok(())
1012}
1013
1014async fn test_lnurl_pay(dev_fed: &DevJitFed) -> anyhow::Result<()> {
1015 if util::FedimintCli::version_or_default().await < *VERSION_0_11_0_ALPHA {
1016 return Ok(());
1017 }
1018
1019 if util::FedimintdCmd::version_or_default().await < *VERSION_0_11_0_ALPHA {
1020 return Ok(());
1021 }
1022
1023 if util::Gatewayd::version_or_default().await < *VERSION_0_11_0_ALPHA {
1024 return Ok(());
1025 }
1026
1027 let federation = dev_fed.fed().await?;
1028
1029 let gw_lnd = dev_fed.gw_lnd().await?;
1030 let gw_ldk = dev_fed.gw_ldk().await?;
1031
1032 let gateway_pairs = [(gw_lnd, gw_ldk), (gw_ldk, gw_lnd)];
1033
1034 let recurringd = dev_fed.recurringdv2().await?.api_url().to_string();
1035
1036 let client_a = federation
1037 .new_joined_client("lnv2-lnurl-test-client-a")
1038 .await?;
1039
1040 assert_module_sanity(&client_a).await?;
1041
1042 let client_b = federation
1043 .new_joined_client("lnv2-lnurl-test-client-b")
1044 .await?;
1045
1046 assert_module_sanity(&client_b).await?;
1047
1048 for (gw_send, gw_receive) in gateway_pairs {
1049 info!(
1050 "Testing lnurl payments: {} -> {} -> client",
1051 gw_send.ln.ln_type(),
1052 gw_receive.ln.ln_type()
1053 );
1054
1055 let lnurl_a = generate_lnurl(&client_a, &recurringd, &gw_receive.addr).await?;
1056 let lnurl_b = generate_lnurl(&client_b, &recurringd, &gw_receive.addr).await?;
1057
1058 let (invoice_a, verify_url_a) = fetch_invoice(lnurl_a.clone(), 500_000).await?;
1059 let (invoice_b, verify_url_b) = fetch_invoice(lnurl_b.clone(), 500_000).await?;
1060
1061 let verify_task_a = task::spawn("verify_task_a", verify_payment_wait(verify_url_a.clone()));
1062 let verify_task_b = task::spawn("verify_task_b", verify_payment_wait(verify_url_b.clone()));
1063
1064 let response_a = verify_payment(&verify_url_a).await?;
1065 let response_b = verify_payment(&verify_url_b).await?;
1066
1067 assert!(!response_a.settled);
1068 assert!(!response_b.settled);
1069
1070 assert!(response_a.preimage.is_none());
1071 assert!(response_b.preimage.is_none());
1072
1073 gw_send.client().pay_invoice(invoice_a.clone()).await?;
1074 gw_send.client().pay_invoice(invoice_b.clone()).await?;
1075
1076 let response_a = verify_payment(&verify_url_a).await?;
1077 let response_b = verify_payment(&verify_url_b).await?;
1078
1079 assert!(response_a.settled);
1080 assert!(response_b.settled);
1081
1082 verify_preimage(&response_a, &invoice_a);
1083 verify_preimage(&response_b, &invoice_b);
1084
1085 assert_eq!(verify_task_a.await??, response_a);
1086 assert_eq!(verify_task_b.await??, response_b);
1087 }
1088
1089 wait_for_lnurl_balance(&client_a, "client A", 950 * 1000).await?;
1090 wait_for_lnurl_balance(&client_b, "client B", 950 * 1000).await?;
1091
1092 Ok(())
1093}
1094
1095async fn test_lnurl_recovery(dev_fed: &DevJitFed) -> anyhow::Result<()> {
1103 if util::FedimintCli::version_or_default().await < *VERSION_0_10_0_ALPHA {
1107 return Ok(());
1108 }
1109
1110 let federation = dev_fed.fed().await?;
1111 let gw_lnd = dev_fed.gw_lnd().await?;
1112 let gw_ldk = dev_fed.gw_ldk().await?;
1113 let recurringd = dev_fed.recurringdv2().await?.api_url().to_string();
1114
1115 const LNURL_AMOUNT_MSAT: u64 = 500_000;
1116 const LNURL_BALANCE_TOLERANCE_MSAT: u64 = 100_000;
1117
1118 info!("Phase 1: Creating client and receiving via LNURL before recovery");
1121
1122 let client = federation
1123 .new_joined_client("lnv2-lnurl-recovery-original")
1124 .await?;
1125
1126 let lnurl = generate_lnurl(&client, &recurringd, &gw_ldk.addr).await?;
1127
1128 for i in 0..3 {
1129 info!("Paying LNURL invoice {}/3", i + 1);
1130 let (invoice, _verify_url) = fetch_invoice(lnurl.clone(), LNURL_AMOUNT_MSAT).await?;
1131 gw_lnd.client().pay_invoice(invoice).await?;
1132 }
1133
1134 while almost_equal(
1135 client.balance().await?,
1136 3 * LNURL_AMOUNT_MSAT,
1137 LNURL_BALANCE_TOLERANCE_MSAT,
1138 )
1139 .is_err()
1140 {
1141 info!("Waiting for pre-recovery LNURL payments to settle...");
1142 cmd!(client, "dev", "wait", "1").out_json().await?;
1143 }
1144
1145 let pre_recovery_balance = client.balance().await?;
1146 info!("Pre-recovery balance: {pre_recovery_balance} msats");
1147
1148 let mnemonic = cmd!(client, "print-secret").out_json().await?["secret"]
1149 .as_str()
1150 .expect("secret is a string")
1151 .to_owned();
1152
1153 info!("Phase 2: Recovering client from seed");
1156
1157 let restored = Client::create("lnv2-lnurl-recovery-restored").await?;
1158 cmd!(
1159 restored,
1160 "restore",
1161 "--invite-code",
1162 federation.invite_code()?,
1163 "--mnemonic",
1164 &mnemonic
1165 )
1166 .run()
1167 .await?;
1168
1169 while restored.balance().await? < pre_recovery_balance {
1170 info!("Waiting for recovery to complete...");
1171 cmd!(restored, "dev", "wait", "1").out_json().await?;
1172 }
1173
1174 let post_recovery_balance = restored.balance().await?;
1175 info!("Post-recovery balance: {post_recovery_balance} msats");
1176
1177 info!("Phase 3: Paying to the original LNURL; restored client must claim");
1180
1181 for i in 0..2 {
1184 info!("Paying pre-recovery LNURL invoice {}/2", i + 1);
1185 let (invoice, _verify_url) = fetch_invoice(lnurl.clone(), LNURL_AMOUNT_MSAT).await?;
1186 gw_lnd.client().pay_invoice(invoice).await?;
1187 }
1188
1189 while almost_equal(
1190 restored.balance().await?,
1191 post_recovery_balance + 2 * LNURL_AMOUNT_MSAT,
1192 LNURL_BALANCE_TOLERANCE_MSAT,
1193 )
1194 .is_err()
1195 {
1196 info!("Waiting for post-recovery LNURL payments to settle...");
1197 cmd!(restored, "dev", "wait", "1").out_json().await?;
1198 }
1199
1200 let final_balance = restored.balance().await?;
1201 info!("Final balance: {final_balance} msats");
1202
1203 let operations = cmd!(restored, "list-operations", "--limit", "100")
1204 .out_json()
1205 .await?;
1206 let lnv2_ops: Vec<_> = operations["operations"]
1207 .as_array()
1208 .expect("operations is an array")
1209 .iter()
1210 .filter(|op| op["operation_kind"].as_str() == Some("lnv2"))
1211 .collect();
1212 assert!(
1213 lnv2_ops.len() >= 2,
1214 "Expected at least 2 LNv2 operations after post-recovery receives, found {}",
1215 lnv2_ops.len()
1216 );
1217
1218 info!(
1219 "LNURL recovery test passed: {} new operations, balance {final_balance} msats",
1220 lnv2_ops.len()
1221 );
1222
1223 Ok(())
1224}
1225
1226async fn generate_lnurl(
1227 client: &Client,
1228 recurringd_base_url: &str,
1229 gw_ldk_addr: &str,
1230) -> anyhow::Result<String> {
1231 cmd!(
1232 client,
1233 "module",
1234 "lnv2",
1235 "lnurl",
1236 "generate",
1237 recurringd_base_url,
1238 "--gateway",
1239 gw_ldk_addr,
1240 )
1241 .out_json()
1242 .await
1243 .map(|s| s.as_str().unwrap().to_owned())
1244}
1245
1246fn verify_preimage(response: &VerifyResponse, invoice: &Bolt11Invoice) {
1247 let preimage = response.preimage.expect("Payment should be settled");
1248
1249 let payment_hash = preimage.consensus_hash::<sha256::Hash>();
1250
1251 assert_eq!(payment_hash, *invoice.payment_hash());
1252}
1253
1254async fn verify_payment(verify_url: &str) -> anyhow::Result<VerifyResponse> {
1255 reqwest::get(verify_url)
1256 .await?
1257 .json::<LnurlResponse<VerifyResponse>>()
1258 .await?
1259 .into_result()
1260 .map_err(anyhow::Error::msg)
1261}
1262
1263async fn verify_payment_wait(verify_url: String) -> anyhow::Result<VerifyResponse> {
1264 reqwest::get(format!("{verify_url}?wait"))
1265 .await?
1266 .json::<LnurlResponse<VerifyResponse>>()
1267 .await?
1268 .into_result()
1269 .map_err(anyhow::Error::msg)
1270}
1271
1272#[derive(Deserialize, Clone)]
1273struct LnUrlPayResponse {
1274 callback: String,
1275 metadata: String,
1276}
1277
1278#[derive(Deserialize, Clone)]
1279struct LnUrlPayInvoiceResponse {
1280 pr: Bolt11Invoice,
1281 routes: Vec<serde_json::Value>,
1283 verify: String,
1284}
1285
1286async fn fetch_invoice(lnurl: String, amount_msat: u64) -> anyhow::Result<(Bolt11Invoice, String)> {
1287 let url = parse_lnurl(&lnurl).ok_or_else(|| anyhow::anyhow!("Invalid LNURL"))?;
1288
1289 let response = reqwest::get(url).await?.json::<LnUrlPayResponse>().await?;
1290
1291 let callback_url = format!("{}?amount={}", response.callback, amount_msat);
1292
1293 let invoice_response = reqwest::get(callback_url)
1294 .await?
1295 .json::<LnUrlPayInvoiceResponse>()
1296 .await?;
1297
1298 ensure!(
1299 invoice_response.pr.amount_milli_satoshis() == Some(amount_msat),
1300 "Invoice amount is not set"
1301 );
1302
1303 ensure!(
1304 invoice_response.routes.is_empty(),
1305 "LUD-06 requires routes to be an empty array"
1306 );
1307
1308 let metadata_hash = sha256::Hash::hash(response.metadata.as_bytes());
1309
1310 ensure!(
1311 matches!(
1312 invoice_response.pr.description(),
1313 Bolt11InvoiceDescriptionRef::Hash(hash) if hash.0 == metadata_hash
1314 ),
1315 "Invoice does not commit to the LNURL metadata via its description hash (LUD-06)"
1316 );
1317
1318 Ok((invoice_response.pr, invoice_response.verify))
1319}
1320
1321async fn test_iroh_payment(
1322 client: &Client,
1323 gw_lnd: &Gatewayd,
1324 gw_ldk: &Gatewayd,
1325 online_peers: &[usize],
1326) -> anyhow::Result<()> {
1327 info!("Testing iroh payment...");
1328 remove_all_gateways(client, online_peers).await?;
1331 for &peer in online_peers {
1332 add_gateway(client, peer, &format!("iroh://{}", gw_lnd.node_id)).await?;
1333 }
1334
1335 if util::FedimintCli::version_or_default().await < *VERSION_0_10_0_ALPHA
1338 || gw_lnd.gatewayd_version < *VERSION_0_10_0_ALPHA
1339 {
1340 for &peer in online_peers {
1341 add_gateway(client, peer, &gw_lnd.addr).await?;
1342 }
1343 }
1344
1345 let invoice = gw_ldk.client().create_invoice(5_000_000).await?;
1346
1347 let send_op = serde_json::from_value::<OperationId>(
1348 cmd!(client, "module", "lnv2", "send", invoice,)
1349 .out_json()
1350 .await?,
1351 )?;
1352
1353 let send_state = common::await_send(client, send_op).await?;
1354 assert!(
1355 matches!(send_state, FinalSendOperationState::Success(_)),
1356 "unexpected send state: {send_state:?}"
1357 );
1358
1359 let (invoice, receive_op) = serde_json::from_value::<(Bolt11Invoice, OperationId)>(
1360 cmd!(client, "module", "lnv2", "receive", "5000000",)
1361 .out_json()
1362 .await?,
1363 )?;
1364
1365 gw_ldk.client().pay_invoice(invoice).await?;
1366 common::await_receive_claimed(client, receive_op).await?;
1367
1368 if util::FedimintCli::version_or_default().await < *VERSION_0_10_0_ALPHA
1369 || gw_lnd.gatewayd_version < *VERSION_0_10_0_ALPHA
1370 {
1371 for &peer in online_peers {
1372 remove_gateway(client, peer, &gw_lnd.addr).await?;
1373 }
1374 }
1375
1376 for &peer in online_peers {
1377 remove_gateway(client, peer, &format!("iroh://{}", gw_lnd.node_id)).await?;
1378 }
1379
1380 Ok(())
1381}