diff --git a/src/ldk.rs b/src/ldk.rs index 9dfd0261..13cccf45 100644 --- a/src/ldk.rs +++ b/src/ldk.rs @@ -111,6 +111,13 @@ pub(crate) const FEE_RATE: u64 = 7; pub(crate) const UTXO_SIZE_SAT: u32 = 32000; pub(crate) const MIN_CHANNEL_CONFIRMATIONS: u8 = 6; +#[cfg(test)] +pub(crate) static FORCE_NEXT_INTERCEPTED_SWAP_RGB_FORWARD_FAILURE: AtomicBool = + AtomicBool::new(false); + +#[cfg(test)] +const TEST_RGB_FORWARD_FAILURE_EXCESS_AMOUNT: u64 = 1; + pub(crate) struct LdkBackgroundServices { stop_processing: Arc, peer_manager: Arc, @@ -1276,6 +1283,23 @@ async fn handle_ldk_events( tracing::debug!("Swap is whitelisted, forwarding the htlc..."); unlocked_state.update_taker_swap_status(&payment_hash, SwapStatus::Pending); + let outbound_rgb_payment = expected_outbound_rgb_payment; + #[cfg(test)] + let outbound_rgb_payment = { + let mut outbound_rgb_payment = outbound_rgb_payment; + if FORCE_NEXT_INTERCEPTED_SWAP_RGB_FORWARD_FAILURE.swap(false, Ordering::SeqCst) { + if let (Some((contract_id, _)), Some((_, local_rgb_amount, _))) = + (outbound_rgb_payment, outbound_rgb_info) + { + outbound_rgb_payment = Some(( + contract_id, + local_rgb_amount.saturating_add(TEST_RGB_FORWARD_FAILURE_EXCESS_AMOUNT), + )); + } + } + outbound_rgb_payment + }; + unlocked_state .channel_manager .forward_intercepted_htlc( @@ -1283,7 +1307,7 @@ async fn handle_ldk_events( channelmanager::NextHopForward::ShortChannelId(requested_next_hop_scid), outbound_channel.counterparty.node_id, expected_outbound_amount_msat, - expected_outbound_rgb_payment, + outbound_rgb_payment, ) .expect("Forward should be valid"); } diff --git a/src/routes.rs b/src/routes.rs index e614385b..fd066605 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -3092,9 +3092,9 @@ pub(crate) async fn node_info( let pending_payments_map = |b| match b { &Balance::MaybeTimeoutClaimableHTLC { amount_satoshis, - outbound_payment, + outbound_payment: true, .. - } if outbound_payment => amount_satoshis, + } => amount_satoshis, _ => 0, }; let pending_outbound_payments_sat = balances.iter().map(pending_payments_map).sum::(); diff --git a/src/test/mod.rs b/src/test/mod.rs index 0db01e5b..f9bb12e5 100644 --- a/src/test/mod.rs +++ b/src/test/mod.rs @@ -2029,6 +2029,7 @@ mod swap_roundtrip_buy_same_channel; mod swap_roundtrip_fail_amount_maker; mod swap_roundtrip_fail_amount_taker; mod swap_roundtrip_fail_btc2btc; +mod swap_roundtrip_fail_forward; mod swap_roundtrip_fail_invalid_asset_from; mod swap_roundtrip_fail_invalid_asset_to; mod swap_roundtrip_fail_same_asset; diff --git a/src/test/swap_roundtrip_fail_forward.rs b/src/test/swap_roundtrip_fail_forward.rs new file mode 100644 index 00000000..71fd1a76 --- /dev/null +++ b/src/test/swap_roundtrip_fail_forward.rs @@ -0,0 +1,99 @@ +use super::*; + +const TEST_DIR_BASE: &str = "tmp/swap_roundtrip_fail_forward/"; + +#[serial_test::serial] +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[traced_test] +async fn swap_fail_forward_marks_taker_failed() { + initialize(); + + let test_dir_node1 = format!("{TEST_DIR_BASE}node1"); + let test_dir_node2 = format!("{TEST_DIR_BASE}node2"); + let (node1_addr, _) = start_node(&test_dir_node1, NODE1_PEER_PORT, false).await; + let (node2_addr, _) = start_node(&test_dir_node2, NODE2_PEER_PORT, false).await; + + fund_and_create_utxos(node1_addr, None).await; + fund_and_create_utxos(node2_addr, None).await; + + let asset_id = issue_asset_nia(node2_addr).await.asset_id; + + let node1_pubkey = node_info(node1_addr).await.pubkey; + let node2_pubkey = node_info(node2_addr).await.pubkey; + + open_channel( + node1_addr, + &node2_pubkey, + Some(NODE2_PEER_PORT), + Some(5000000), + Some(546000), + None, + None, + ) + .await; + open_channel( + node2_addr, + &node1_pubkey, + Some(NODE1_PEER_PORT), + None, + None, + Some(600), + Some(&asset_id), + ) + .await; + wait_for_usable_channels(node1_addr, 2).await; + wait_for_usable_channels(node2_addr, 2).await; + + println!("\nsetup swap"); + let maker_addr = node1_addr; + let taker_addr = node2_addr; + let qty_from = 10; + let qty_to = 50000; + let maker_init_response = + maker_init(maker_addr, qty_from, Some(&asset_id), qty_to, None, 3600).await; + taker(taker_addr, maker_init_response.swapstring.clone()).await; + + let swaps_maker = list_swaps(maker_addr).await; + assert!(swaps_maker.taker.is_empty()); + assert_eq!(swaps_maker.maker.len(), 1); + assert_eq!( + swaps_maker.maker.first().unwrap().status, + SwapStatus::Waiting + ); + + let swaps_taker = list_swaps(taker_addr).await; + assert!(swaps_taker.maker.is_empty()); + assert_eq!(swaps_taker.taker.len(), 1); + assert_eq!( + swaps_taker.taker.first().unwrap().status, + SwapStatus::Waiting + ); + + crate::ldk::FORCE_NEXT_INTERCEPTED_SWAP_RGB_FORWARD_FAILURE + .store(true, std::sync::atomic::Ordering::SeqCst); + + println!("\nexecute swap"); + maker_execute( + maker_addr, + maker_init_response.swapstring, + maker_init_response.payment_secret, + node2_pubkey, + ) + .await; + + wait_for_swap_status( + maker_addr, + &maker_init_response.payment_hash, + SwapStatus::Failed, + ) + .await; + + // This assertion documents the expected behavior and currently fails because the + // HTLCHandlingFailed event is ignored, leaving the taker swap stuck as Pending. + wait_for_swap_status( + taker_addr, + &maker_init_response.payment_hash, + SwapStatus::Failed, + ) + .await; +}