diff --git a/rust-lightning b/rust-lightning index e0e7aabe..dd15b538 160000 --- a/rust-lightning +++ b/rust-lightning @@ -1 +1 @@ -Subproject commit e0e7aabe5ce000c247340055b66a146f34c6471c +Subproject commit dd15b5385b3c46f5f9d3ef167dfe972fde966dfd diff --git a/src/ldk.rs b/src/ldk.rs index addd4dcf..f9e6ff9a 100644 --- a/src/ldk.rs +++ b/src/ldk.rs @@ -130,6 +130,12 @@ pub(crate) static HOLD_PAYMENT_CLAIMABLE_ON_NODE: Mutex> = Mut #[cfg(test)] pub(crate) static HELD_PAYMENT_CLAIMABLE_COUNT: AtomicUsize = AtomicUsize::new(0); +// Test-only: the node with this pubkey emits a `push_asset_amount` greater than the channel asset +// amount on the wire in `open_channel`, regardless of the value validated by its REST layer. Used +// to model a channel counterparty whose wire client is not bound by the sender-side clamp. +#[cfg(test)] +pub(crate) static FORCE_PUSH_ASSET_AMOUNT_ON_NODE: Mutex> = Mutex::new(None); + pub(crate) struct LdkBackgroundServices { stop_processing: Arc, peer_manager: Arc, diff --git a/src/routes.rs b/src/routes.rs index 2ff56fd9..58146985 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -79,6 +79,8 @@ use tokio::{ sync::MutexGuard as TokioMutexGuard, }; +#[cfg(test)] +use crate::ldk::FORCE_PUSH_ASSET_AMOUNT_ON_NODE; use crate::swap::{SwapData, SwapInfo, SwapString}; use crate::utils::{ check_already_initialized, check_channel_id, check_password_strength, check_password_validity, @@ -3495,7 +3497,16 @@ pub(crate) async fn open_channel( let schema = unlocked_state .rgb_get_asset_metadata(*contract_id)? .asset_schema; - (Some((*contract_id, payload.push_asset_amount)), Some(schema)) + #[cfg(not(test))] + let wire_push_asset_amount = payload.push_asset_amount; + #[cfg(test)] + let wire_push_asset_amount = match *FORCE_PUSH_ASSET_AMOUNT_ON_NODE.lock().unwrap() { + Some(node) if node == unlocked_state.channel_manager.get_our_node_id() => { + Some(*asset_amount + 1) + } + _ => payload.push_asset_amount, + }; + (Some((*contract_id, wire_push_asset_amount)), Some(schema)) } else { let balance = unlocked_state.rgb_get_btc_balance(true)?; if payload.capacity_sat > balance.vanilla.spendable { diff --git a/src/test/mod.rs b/src/test/mod.rs index 1f441edd..ffc88ccf 100644 --- a/src/test/mod.rs +++ b/src/test/mod.rs @@ -29,8 +29,8 @@ use tracing_test::traced_test; use crate::disk::LDK_LOGS_FILE; use crate::error::APIErrorResponse; use crate::ldk::{ - FEE_RATE, HELD_PAYMENT_CLAIMABLE_COUNT, HOLD_PAYMENT_CLAIMABLE_ON_NODE, - IGNORE_INBOUND_CHANNELS_ON_NODE, + FEE_RATE, FORCE_PUSH_ASSET_AMOUNT_ON_NODE, HELD_PAYMENT_CLAIMABLE_COUNT, + HOLD_PAYMENT_CLAIMABLE_ON_NODE, IGNORE_INBOUND_CHANNELS_ON_NODE, }; use crate::routes::{ AddressResponse, AssetBalanceRequest, AssetBalanceResponse, AssetCFA, AssetIFA, AssetNIA, @@ -2281,6 +2281,7 @@ mod openchannel_optional_addr; mod openchannel_push_asset_amount; mod out_of_band; mod payment; +mod push_asset_amount_above_chan_amt; mod refuse_high_fees; mod restart; mod send_receive; diff --git a/src/test/push_asset_amount_above_chan_amt.rs b/src/test/push_asset_amount_above_chan_amt.rs new file mode 100644 index 00000000..39924124 --- /dev/null +++ b/src/test/push_asset_amount_above_chan_amt.rs @@ -0,0 +1,64 @@ +use super::*; + +const TEST_DIR_BASE: &str = "tmp/push_asset_amount_above_chan_amt/"; + +/// A counterparty sending a `push_asset_amount` greater than the channel asset amount used to +/// underflow `remote_rgb_amount` on the acceptor, panicking its event handler. The acceptor must +/// reject the funding and stay alive. +#[serial_test::serial] +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[traced_test] +async fn push_asset_amount_above_chan_amt() { + 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 node1_pubkey = node_info(node1_addr).await.pubkey; + let node2_pubkey = node_info(node2_addr).await.pubkey; + + let asset_id = issue_asset_nia(node1_addr).await.asset_id; + + // node1 puts more than the channel asset amount on the wire, bypassing the REST clamp that the + // push_asset_amount below satisfies + let _force_guard = NodeOverrideGuard::set(&FORCE_PUSH_ASSET_AMOUNT_ON_NODE, &node1_pubkey); + + open_channel_raw( + node1_addr, + &node2_pubkey, + Some(NODE2_PEER_PORT), + Some(100_000), + None, + Some(100), + Some(&asset_id), + Some(0), + None, + None, + None, + true, + true, + ) + .await + .unwrap(); + + // node2 rejects the funding, so node1's pending channel is discarded + let t_0 = OffsetDateTime::now_utc(); + loop { + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + if list_channels(node1_addr).await.is_empty() { + break; + } + if (OffsetDateTime::now_utc() - t_0).as_seconds_f32() > 30.0 { + panic!("initiator channel was not discarded"); + } + } + + // with the underflow node2 would have panicked in its event handler + node_info(node2_addr).await; + assert!(list_channels(node2_addr).await.is_empty()); +}