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
26 changes: 25 additions & 1 deletion src/ldk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<AtomicBool>,
peer_manager: Arc<PeerManager>,
Expand Down Expand Up @@ -1276,14 +1283,31 @@ 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(
intercept_id,
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");
}
Expand Down
4 changes: 2 additions & 2 deletions src/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<u64>();
Expand Down
1 change: 1 addition & 0 deletions src/test/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
99 changes: 99 additions & 0 deletions src/test/swap_roundtrip_fail_forward.rs
Original file line number Diff line number Diff line change
@@ -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);

Comment on lines +72 to +74
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The global FORCE_NEXT_INTERCEPTED_SWAP_RGB_FORWARD_FAILURE flag is set here but only reset when an intercepted HTLC is processed (via swap(false, ...)). If this test fails/panics before that event occurs, the flag can remain true and potentially affect later tests in the same run. Consider resetting it back to false with a small RAII drop-guard (or an explicit cleanup in a finally-style block) to prevent cross-test contamination.

Copilot uses AI. Check for mistakes.
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;
Comment on lines +91 to +98
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test currently asserts taker reaches SwapStatus::Failed, but the comment indicates this is known to fail (taker stays Pending). As written it will make cargo test hang up to 70s and then fail CI. If the fix isn’t included in this PR, mark the test #[ignore] (or otherwise gate it) so the suite stays green; alternatively, include the production fix so the assertion can pass reliably.

Copilot uses AI. Check for mistakes.
}
Loading