@@ -2056,3 +2056,277 @@ fn test_splice_with_inflight_htlc_forward_and_resolution() {
20562056 do_test_splice_with_inflight_htlc_forward_and_resolution ( true ) ;
20572057 do_test_splice_with_inflight_htlc_forward_and_resolution ( false ) ;
20582058}
2059+
2060+ #[ test]
2061+ fn test_splice_minimum_depth_for_counterparty_initiated ( ) {
2062+ // When a 0-conf channel is spliced by the counterparty, the LSP (acceptor) should gate
2063+ // splice_locked behind the configured splice_minimum_depth rather than inheriting 0-conf.
2064+ let chanmon_cfgs = create_chanmon_cfgs ( 2 ) ;
2065+ let node_cfgs = create_node_cfgs ( 2 , & chanmon_cfgs) ;
2066+
2067+ // node_a (LSP): accepts 0-conf, but requires 6 confirmations for counterparty-initiated splices.
2068+ let mut lsp_config = test_default_channel_config ( ) ;
2069+ lsp_config. manually_accept_inbound_channels = true ;
2070+ lsp_config. channel_handshake_limits . trust_own_funding_0conf = true ;
2071+ lsp_config. channel_handshake_config . splice_minimum_depth = Some ( 6 ) ;
2072+
2073+ // node_b (client): vanilla 0-conf, no splice_minimum_depth.
2074+ let mut client_config = test_default_channel_config ( ) ;
2075+ client_config. manually_accept_inbound_channels = true ;
2076+ client_config. channel_handshake_limits . trust_own_funding_0conf = true ;
2077+
2078+ let node_chanmgrs =
2079+ create_node_chanmgrs ( 2 , & node_cfgs, & [ Some ( lsp_config) , Some ( client_config) ] ) ;
2080+ let nodes = create_network ( 2 , & node_cfgs, & node_chanmgrs) ;
2081+
2082+ let node_id_a = nodes[ 0 ] . node . get_our_node_id ( ) ;
2083+ let node_id_b = nodes[ 1 ] . node . get_our_node_id ( ) ;
2084+
2085+ // Open a 0-conf channel: node_b initiates, node_a accepts 0-conf.
2086+ let initial_channel_value_sat = 1_000_000 ;
2087+ let ( funding_tx, channel_id) =
2088+ open_zero_conf_channel_with_value ( & nodes[ 1 ] , & nodes[ 0 ] , None , initial_channel_value_sat, 0 ) ;
2089+ mine_transaction ( & nodes[ 0 ] , & funding_tx) ;
2090+ mine_transaction ( & nodes[ 1 ] , & funding_tx) ;
2091+
2092+ // node_b (client) initiates the splice → is_initiator=true on node_b, is_initiator=false on node_a.
2093+ let coinbase_tx = provide_anchor_reserves ( & nodes) ;
2094+ let splice_in_sats = 500_000 ;
2095+ let initiator_contribution = SpliceContribution :: SpliceIn {
2096+ value : Amount :: from_sat ( splice_in_sats) ,
2097+ inputs : vec ! [ FundingTxInput :: new_p2wpkh( coinbase_tx, 1 ) . unwrap( ) ] ,
2098+ change_script : Some ( nodes[ 1 ] . wallet_source . get_change_script ( ) . unwrap ( ) ) ,
2099+ } ;
2100+
2101+ let new_funding_script =
2102+ complete_splice_handshake ( & nodes[ 1 ] , & nodes[ 0 ] , channel_id, initiator_contribution. clone ( ) ) ;
2103+
2104+ let initial_commit_sig = complete_interactive_funding_negotiation (
2105+ & nodes[ 1 ] ,
2106+ & nodes[ 0 ] ,
2107+ channel_id,
2108+ initiator_contribution,
2109+ new_funding_script,
2110+ ) ;
2111+
2112+ // --- Sign the splice tx (custom flow for asymmetric splice_locked) ---
2113+ // The standard sign_interactive_funding_tx helper expects symmetric 0-conf behavior.
2114+ // Here, node_b (initiator) will send splice_locked, but node_a (acceptor) will not.
2115+
2116+ // acceptor (node_a) receives initiator's commitment_signed, responds with commitment_signed + tx_signatures.
2117+ assert ! ( nodes[ 1 ] . node. get_and_clear_pending_msg_events( ) . is_empty( ) ) ;
2118+ nodes[ 0 ] . node . handle_commitment_signed ( node_id_b, & initial_commit_sig) ;
2119+ let msg_events = nodes[ 0 ] . node . get_and_clear_pending_msg_events ( ) ;
2120+ assert_eq ! ( msg_events. len( ) , 2 , "{msg_events:?}" ) ;
2121+ if let MessageSendEvent :: UpdateHTLCs { ref updates, .. } = & msg_events[ 0 ] {
2122+ nodes[ 1 ] . node . handle_commitment_signed ( node_id_a, & updates. commitment_signed [ 0 ] ) ;
2123+ } else {
2124+ panic ! ( "Expected UpdateHTLCs, got {:?}" , & msg_events[ 0 ] ) ;
2125+ }
2126+ if let MessageSendEvent :: SendTxSignatures { ref msg, .. } = & msg_events[ 1 ] {
2127+ nodes[ 1 ] . node . handle_tx_signatures ( node_id_a, msg) ;
2128+ } else {
2129+ panic ! ( "Expected SendTxSignatures, got {:?}" , & msg_events[ 1 ] ) ;
2130+ }
2131+
2132+ // initiator (node_b) signs the funding tx.
2133+ let event = get_event ! ( nodes[ 1 ] , Event :: FundingTransactionReadyForSigning ) ;
2134+ if let Event :: FundingTransactionReadyForSigning {
2135+ channel_id : cid,
2136+ counterparty_node_id,
2137+ unsigned_transaction,
2138+ ..
2139+ } = event
2140+ {
2141+ assert_eq ! ( cid, channel_id) ;
2142+ let signed_tx = nodes[ 1 ] . wallet_source . sign_tx ( unsigned_transaction) . unwrap ( ) ;
2143+ nodes[ 1 ] . node . funding_transaction_signed ( & cid, & counterparty_node_id, signed_tx) . unwrap ( ) ;
2144+ }
2145+
2146+ // node_b (initiator, client) should emit TxSignatures + SpliceLocked (0-conf, no override).
2147+ let mut initiator_msgs = nodes[ 1 ] . node . get_and_clear_pending_msg_events ( ) ;
2148+ assert_eq ! (
2149+ initiator_msgs. len( ) ,
2150+ 2 ,
2151+ "client should send TxSignatures + SpliceLocked: {initiator_msgs:?}"
2152+ ) ;
2153+ let initiator_splice_locked =
2154+ if let MessageSendEvent :: SendTxSignatures { ref msg, .. } = & initiator_msgs[ 0 ] {
2155+ // Forward TxSignatures to acceptor (node_a).
2156+ nodes[ 0 ] . node . handle_tx_signatures ( node_id_b, msg) ;
2157+ match initiator_msgs. remove ( 1 ) {
2158+ MessageSendEvent :: SendSpliceLocked { msg, .. } => msg,
2159+ ref e => panic ! ( "Expected SendSpliceLocked, got {:?}" , e) ,
2160+ }
2161+ } else {
2162+ panic ! ( "Expected SendTxSignatures, got {:?}" , & initiator_msgs[ 0 ] ) ;
2163+ } ;
2164+
2165+ check_added_monitors ( & nodes[ 1 ] , 1 ) ;
2166+ check_added_monitors ( & nodes[ 0 ] , 1 ) ;
2167+
2168+ let splice_tx = {
2169+ let mut txn = nodes[ 1 ] . tx_broadcaster . txn_broadcast ( ) ;
2170+ assert_eq ! ( txn. len( ) , 1 ) ;
2171+ assert_eq ! ( nodes[ 0 ] . tx_broadcaster. txn_broadcast( ) , txn) ;
2172+ txn. remove ( 0 )
2173+ } ;
2174+
2175+ expect_splice_pending_event ( & nodes[ 1 ] , & node_id_a) ;
2176+ expect_splice_pending_event ( & nodes[ 0 ] , & node_id_b) ;
2177+
2178+ // --- Key assertion: node_a (LSP/acceptor) should NOT have emitted SpliceLocked ---
2179+ let acceptor_msgs = nodes[ 0 ] . node . get_and_clear_pending_msg_events ( ) ;
2180+ assert ! (
2181+ acceptor_msgs. is_empty( ) ,
2182+ "LSP should not send splice_locked before required depth, got: {acceptor_msgs:?}"
2183+ ) ;
2184+
2185+ // Confirm the splice tx and mine blocks up to (but not including) the required depth.
2186+ mine_transaction ( & nodes[ 0 ] , & splice_tx) ;
2187+ mine_transaction ( & nodes[ 1 ] , & splice_tx) ;
2188+ // 1 conf from mine_transaction. We need 6 total, so connect 4 more (will be at 5 confs).
2189+ connect_blocks ( & nodes[ 0 ] , 4 ) ;
2190+ connect_blocks ( & nodes[ 1 ] , 4 ) ;
2191+
2192+ // Still not enough depth — node_a should still not have emitted splice_locked.
2193+ // (Other messages like SendAnnouncementSignatures may appear.)
2194+ let acceptor_msgs = nodes[ 0 ] . node . get_and_clear_pending_msg_events ( ) ;
2195+ assert ! (
2196+ !acceptor_msgs. iter( ) . any( |m| matches!( m, MessageSendEvent :: SendSpliceLocked { .. } ) ) ,
2197+ "LSP should not send splice_locked at 5 confs (need 6), got: {acceptor_msgs:?}"
2198+ ) ;
2199+ // Clear any announcement signatures from node_b as well.
2200+ nodes[ 1 ] . node . get_and_clear_pending_msg_events ( ) ;
2201+
2202+ // Mine one more block → 6 confirmations on both. node_a should now emit splice_locked.
2203+ connect_blocks ( & nodes[ 0 ] , 1 ) ;
2204+ connect_blocks ( & nodes[ 1 ] , 1 ) ;
2205+
2206+ let acceptor_splice_locked =
2207+ get_event_msg ! ( nodes[ 0 ] , MessageSendEvent :: SendSpliceLocked , node_id_b) ;
2208+
2209+ // Save old funding info for chain source cleanup BEFORE any splice_locked handling,
2210+ // since handle_splice_locked updates the monitor's funding outpoint.
2211+ let ( prev_funding_outpoint, prev_funding_script) = nodes[ 0 ]
2212+ . chain_monitor
2213+ . chain_monitor
2214+ . get_monitor ( channel_id)
2215+ . map ( |monitor| ( monitor. get_funding_txo ( ) , monitor. get_funding_script ( ) ) )
2216+ . unwrap ( ) ;
2217+
2218+ // Send node_b's splice_locked (from earlier) to node_a.
2219+ nodes[ 0 ] . node . handle_splice_locked ( node_id_b, & initiator_splice_locked) ;
2220+
2221+ // Send node_a's splice_locked to node_b. node_b responds with SendAnnouncementSignatures.
2222+ nodes[ 1 ] . node . handle_splice_locked ( node_id_a, & acceptor_splice_locked) ;
2223+ let mut b_msgs = nodes[ 1 ] . node . get_and_clear_pending_msg_events ( ) ;
2224+ // node_b already sent splice_locked earlier, so it only emits SendAnnouncementSignatures.
2225+ assert_eq ! ( b_msgs. len( ) , 1 , "{b_msgs:?}" ) ;
2226+ if let MessageSendEvent :: SendAnnouncementSignatures { ref msg, .. } = b_msgs. remove ( 0 ) {
2227+ nodes[ 0 ] . node . handle_announcement_signatures ( node_id_b, msg) ;
2228+ } else {
2229+ panic ! ( "Expected SendAnnouncementSignatures, got {:?}" , b_msgs) ;
2230+ }
2231+
2232+ expect_channel_ready_event ( & nodes[ 0 ] , & node_id_b) ;
2233+ check_added_monitors ( & nodes[ 0 ] , 1 ) ;
2234+ expect_channel_ready_event ( & nodes[ 1 ] , & node_id_a) ;
2235+ check_added_monitors ( & nodes[ 1 ] , 1 ) ;
2236+
2237+ // node_a now emits its own announcement_signatures + BroadcastChannelAnnouncement.
2238+ let mut a_msgs = nodes[ 0 ] . node . get_and_clear_pending_msg_events ( ) ;
2239+ assert_eq ! ( a_msgs. len( ) , 2 , "{a_msgs:?}" ) ;
2240+ if let MessageSendEvent :: SendAnnouncementSignatures { ref msg, .. } = a_msgs. remove ( 0 ) {
2241+ nodes[ 1 ] . node . handle_announcement_signatures ( node_id_a, msg) ;
2242+ } else {
2243+ panic ! ( "Expected SendAnnouncementSignatures" ) ;
2244+ }
2245+ assert ! ( matches!( a_msgs. remove( 0 ) , MessageSendEvent :: BroadcastChannelAnnouncement { .. } ) ) ;
2246+
2247+ let mut b_msgs = nodes[ 1 ] . node . get_and_clear_pending_msg_events ( ) ;
2248+ assert_eq ! ( b_msgs. len( ) , 1 , "{b_msgs:?}" ) ;
2249+ assert ! ( matches!( b_msgs. remove( 0 ) , MessageSendEvent :: BroadcastChannelAnnouncement { .. } ) ) ;
2250+
2251+ // Cleanup chain source watches for old funding.
2252+ nodes[ 0 ]
2253+ . chain_source
2254+ . remove_watched_txn_and_outputs ( prev_funding_outpoint, prev_funding_script. clone ( ) ) ;
2255+ nodes[ 1 ]
2256+ . chain_source
2257+ . remove_watched_txn_and_outputs ( prev_funding_outpoint, prev_funding_script) ;
2258+
2259+ // Verify the channel is usable.
2260+ send_payment ( & nodes[ 1 ] , & [ & nodes[ 0 ] ] , 100_000 ) ;
2261+ }
2262+
2263+ #[ test]
2264+ fn test_splice_minimum_depth_not_applied_for_self_initiated ( ) {
2265+ // When the LSP itself initiates a splice, the splice_minimum_depth override should NOT apply
2266+ // (is_initiator=true on LSP side), and the splice should lock immediately (0-conf).
2267+ let chanmon_cfgs = create_chanmon_cfgs ( 2 ) ;
2268+ let node_cfgs = create_node_cfgs ( 2 , & chanmon_cfgs) ;
2269+
2270+ // node_a (LSP): 0-conf + splice_minimum_depth.
2271+ let mut lsp_config = test_default_channel_config ( ) ;
2272+ lsp_config. manually_accept_inbound_channels = true ;
2273+ lsp_config. channel_handshake_limits . trust_own_funding_0conf = true ;
2274+ lsp_config. channel_handshake_config . splice_minimum_depth = Some ( 6 ) ;
2275+
2276+ // node_b (client): vanilla 0-conf.
2277+ let mut client_config = test_default_channel_config ( ) ;
2278+ client_config. manually_accept_inbound_channels = true ;
2279+ client_config. channel_handshake_limits . trust_own_funding_0conf = true ;
2280+
2281+ let node_chanmgrs =
2282+ create_node_chanmgrs ( 2 , & node_cfgs, & [ Some ( lsp_config) , Some ( client_config) ] ) ;
2283+ let nodes = create_network ( 2 , & node_cfgs, & node_chanmgrs) ;
2284+
2285+ let node_id_a = nodes[ 0 ] . node . get_our_node_id ( ) ;
2286+ let node_id_b = nodes[ 1 ] . node . get_our_node_id ( ) ;
2287+
2288+ // Open a 0-conf channel: node_b initiates channel, node_a accepts 0-conf.
2289+ let initial_channel_value_sat = 1_000_000 ;
2290+ let ( funding_tx, channel_id) =
2291+ open_zero_conf_channel_with_value ( & nodes[ 1 ] , & nodes[ 0 ] , None , initial_channel_value_sat, 0 ) ;
2292+ mine_transaction ( & nodes[ 0 ] , & funding_tx) ;
2293+ mine_transaction ( & nodes[ 1 ] , & funding_tx) ;
2294+
2295+ // node_a (LSP) initiates the splice → is_initiator=true on node_a, override should NOT apply.
2296+ let coinbase_tx = provide_anchor_reserves ( & nodes) ;
2297+ let splice_in_sats = 500_000 ;
2298+ let initiator_contribution = SpliceContribution :: SpliceIn {
2299+ value : Amount :: from_sat ( splice_in_sats) ,
2300+ inputs : vec ! [ FundingTxInput :: new_p2wpkh( coinbase_tx, 0 ) . unwrap( ) ] ,
2301+ change_script : Some ( nodes[ 0 ] . wallet_source . get_change_script ( ) . unwrap ( ) ) ,
2302+ } ;
2303+
2304+ let new_funding_script =
2305+ complete_splice_handshake ( & nodes[ 0 ] , & nodes[ 1 ] , channel_id, initiator_contribution. clone ( ) ) ;
2306+
2307+ let initial_commit_sig = complete_interactive_funding_negotiation (
2308+ & nodes[ 0 ] ,
2309+ & nodes[ 1 ] ,
2310+ channel_id,
2311+ initiator_contribution,
2312+ new_funding_script,
2313+ ) ;
2314+
2315+ // Use the standard helper — both sides should be 0-conf since the override doesn't apply
2316+ // to self-initiated splices.
2317+ let ( _splice_tx, splice_locked) =
2318+ sign_interactive_funding_tx ( & nodes[ 0 ] , & nodes[ 1 ] , initial_commit_sig, true ) ;
2319+
2320+ // node_a (LSP, initiator) DOES emit SpliceLocked immediately — the override is not applied
2321+ // because is_initiator=true.
2322+ let ( splice_locked_msg, for_node_id) = splice_locked. unwrap ( ) ;
2323+ assert_eq ! ( for_node_id, node_id_b) ;
2324+
2325+ expect_splice_pending_event ( & nodes[ 0 ] , & node_id_b) ;
2326+ expect_splice_pending_event ( & nodes[ 1 ] , & node_id_a) ;
2327+
2328+ lock_splice ( & nodes[ 0 ] , & nodes[ 1 ] , & splice_locked_msg, true ) ;
2329+
2330+ // Verify the channel is usable.
2331+ send_payment ( & nodes[ 0 ] , & [ & nodes[ 1 ] ] , 100_000 ) ;
2332+ }
0 commit comments