@@ -27,6 +27,7 @@ use common::{
2727 wait_for_tx, TestChainSource , TestConfig , TestStoreType , TestSyncStore ,
2828} ;
2929use electrsd:: corepc_node:: Node as BitcoinD ;
30+ use electrsd:: electrum_client:: ElectrumApi ;
3031use electrsd:: ElectrsD ;
3132use ldk_node:: config:: { AsyncPaymentsRole , EsploraSyncConfig } ;
3233use ldk_node:: entropy:: NodeEntropy ;
@@ -169,6 +170,181 @@ async fn channel_full_cycle_0conf_0reserve() {
169170 . await ;
170171}
171172
173+ // Demonstrates that `create_funding_transaction` never applies the built-and-signed funding
174+ // transaction back to the on-chain (BDK) wallet, so the inputs it selected remain selectable
175+ // until the next chain/mempool sync round-trip. Consequently, two channel opens that happen
176+ // between two wallet syncs build funding transactions that double-spend the same UTXO(s).
177+ //
178+ // With 0-conf channels this is especially nasty: both channels go `ChannelReady` immediately,
179+ // but only one funding transaction can ever enter the mempool. The other one is rejected by
180+ // bitcoind at broadcast time (HTTP 400, `txn-mempool-conflict`), which the Esplora broadcaster
181+ // logs at `trace` level and otherwise ignores. The losing channel remains "ready" and usable
182+ // while being completely unbacked on-chain: a phantom channel whose eventual commitment
183+ // transaction can never confirm (`bad-txns-inputs-missingorspent`).
184+ //
185+ // This test asserts the *current* (buggy) behavior to prove the issue exists. Once funding
186+ // inputs are properly marked as spent at build time (e.g. via `Wallet::apply_unconfirmed_txs`),
187+ // the second `open_channel` below should fail with `Error::InsufficientFunds` instead, and
188+ // this test will need to be inverted.
189+ #[ tokio:: test( flavor = "multi_thread" , worker_threads = 1 ) ]
190+ async fn zero_conf_channel_funding_tx_double_spend ( ) {
191+ let ( bitcoind, electrsd) = setup_bitcoind_and_electrsd ( ) ;
192+ let chain_source = TestChainSource :: Esplora ( & electrsd) ;
193+
194+ // Node A is the funder. Nodes B and C trust node A for 0-conf channels.
195+ println ! ( "== Node A ==" ) ;
196+ let config_a = random_config ( false ) ;
197+ let node_a = setup_node ( & chain_source, config_a) ;
198+
199+ println ! ( "\n == Node B ==" ) ;
200+ let mut config_b = random_config ( false ) ;
201+ config_b. node_config . trusted_peers_0conf . push ( node_a. node_id ( ) ) ;
202+ let node_b = setup_node ( & chain_source, config_b) ;
203+
204+ println ! ( "\n == Node C ==" ) ;
205+ let mut config_c = random_config ( false ) ;
206+ config_c. node_config . trusted_peers_0conf . push ( node_a. node_id ( ) ) ;
207+ let node_c = setup_node ( & chain_source, config_c) ;
208+
209+ // Give node A exactly ONE UTXO. Every funding transaction node A builds *must* spend it,
210+ // so any two funding transactions built from the same wallet state are double-spends of
211+ // each other by construction.
212+ let premine_amount_sat = 100_000 ;
213+ let addr_a = node_a. onchain_payment ( ) . new_address ( ) . unwrap ( ) ;
214+ premine_blocks ( & bitcoind. client , & electrsd. client ) . await ;
215+ let utxo_txid = distribute_funds_unconfirmed (
216+ & bitcoind. client ,
217+ & electrsd. client ,
218+ vec ! [ addr_a. clone( ) ] ,
219+ Amount :: from_sat ( premine_amount_sat) ,
220+ )
221+ . await ;
222+ generate_blocks_and_wait ( & bitcoind. client , & electrsd. client , 1 ) . await ;
223+ node_a. sync_wallets ( ) . unwrap ( ) ;
224+ assert_eq ! ( node_a. list_balances( ) . spendable_onchain_balance_sats, premine_amount_sat) ;
225+
226+ let utxo_tx = electrsd. client . transaction_get ( & utxo_txid) . unwrap ( ) ;
227+ let utxo_vout = utxo_tx
228+ . output
229+ . iter ( )
230+ . position ( |o| o. script_pubkey == addr_a. script_pubkey ( ) )
231+ . expect ( "premined output to node A not found" ) ;
232+ let utxo_outpoint = bitcoin:: OutPoint { txid : utxo_txid, vout : utxo_vout as u32 } ;
233+
234+ // Two channels of 70k sats each require 140k sats total, but node A only owns 100k sats.
235+ // No two valid, non-conflicting funding transactions can exist for these amounts.
236+ let funding_amount_sat = 70_000 ;
237+
238+ // Open the first 0-conf channel A -> B and wait until its funding transaction is in the
239+ // mempool. There is no race here: the second funding transaction below is built strictly
240+ // *after* the first one was broadcast and accepted.
241+ println ! ( "\n A -- open_channel -> B" ) ;
242+ node_a
243+ . open_channel (
244+ node_b. node_id ( ) ,
245+ node_b. listening_addresses ( ) . unwrap ( ) . first ( ) . unwrap ( ) . clone ( ) ,
246+ funding_amount_sat,
247+ None ,
248+ None ,
249+ )
250+ . unwrap ( ) ;
251+ let funding_txo_b = expect_channel_pending_event ! ( node_a, node_b. node_id( ) ) ;
252+ expect_channel_pending_event ! ( node_b, node_a. node_id( ) ) ;
253+ wait_for_tx ( & electrsd. client , funding_txo_b. txid ) . await ;
254+
255+ // 0-conf: the channel is ready without a single confirmation.
256+ expect_channel_ready_event ! ( node_a, node_b. node_id( ) ) ;
257+ expect_channel_ready_event ! ( node_b, node_a. node_id( ) ) ;
258+
259+ // The first funding transaction spends node A's only UTXO.
260+ let funding_tx_b = electrsd. client . transaction_get ( & funding_txo_b. txid ) . unwrap ( ) ;
261+ assert_eq ! ( funding_tx_b. input. len( ) , 1 ) ;
262+ assert_eq ! ( funding_tx_b. input[ 0 ] . previous_output, utxo_outpoint) ;
263+
264+ // NOTE: We deliberately do NOT call `node_a.sync_wallets()` here. Nothing else marks the
265+ // just-spent UTXO as spent: `create_funding_transaction` doesn't apply the transaction to
266+ // the wallet, so the wallet only learns about the spend when a sync pulls the transaction
267+ // back from the chain source. In production this window lasts until the next background
268+ // sync completes.
269+
270+ // Open the second 0-conf channel A -> C. Node A has already committed its only UTXO to the
271+ // first channel, so with correct input accounting this MUST fail with
272+ // `Error::InsufficientFunds` (only ~30k sats of unconfirmed change remain). Instead, the
273+ // stale wallet still reports 100k sats spendable and happily double-spends the same UTXO.
274+ println ! ( "\n A -- open_channel -> C" ) ;
275+ node_a
276+ . open_channel (
277+ node_c. node_id ( ) ,
278+ node_c. listening_addresses ( ) . unwrap ( ) . first ( ) . unwrap ( ) . clone ( ) ,
279+ funding_amount_sat,
280+ None ,
281+ None ,
282+ )
283+ . unwrap ( ) ;
284+ let funding_txo_c = expect_channel_pending_event ! ( node_a, node_c. node_id( ) ) ;
285+ expect_channel_pending_event ! ( node_c, node_a. node_id( ) ) ;
286+ assert_ne ! ( funding_txo_b. txid, funding_txo_c. txid) ;
287+
288+ // The second channel also goes `ChannelReady` at 0-conf, although its funding transaction
289+ // is a double-spend that can never confirm.
290+ expect_channel_ready_event ! ( node_a, node_c. node_id( ) ) ;
291+ expect_channel_ready_event ! ( node_c, node_a. node_id( ) ) ;
292+
293+ // Node A now has channels worth more than everything it ever owned on-chain.
294+ let total_channel_value_sats: u64 =
295+ node_a. list_channels ( ) . iter ( ) . map ( |c| c. channel_value_sats ) . sum ( ) ;
296+ assert_eq ! ( total_channel_value_sats, 2 * funding_amount_sat) ;
297+ assert ! ( total_channel_value_sats > premine_amount_sat) ;
298+
299+ // The second funding transaction was handed to the broadcaster, but bitcoind rejects it as
300+ // a mempool conflict. The Esplora broadcaster swallows the HTTP 400 at `trace` level, so
301+ // the transaction silently evaporates: it never enters the mempool.
302+ tokio:: time:: sleep ( std:: time:: Duration :: from_secs ( 3 ) ) . await ;
303+ let mempool = bitcoind. client . get_raw_mempool ( ) . unwrap ( ) . into_model ( ) . unwrap ( ) ;
304+ assert ! (
305+ mempool. 0 . iter( ) . any( |txid| * txid == funding_txo_b. txid) ,
306+ "First funding tx should be in the mempool"
307+ ) ;
308+ assert ! (
309+ !mempool. 0 . iter( ) . any( |txid| * txid == funding_txo_c. txid) ,
310+ "Second funding tx should have been rejected as a double-spend"
311+ ) ;
312+
313+ // Mine blocks: the first channel confirms, the second one never can.
314+ generate_blocks_and_wait ( & bitcoind. client , & electrsd. client , 6 ) . await ;
315+ node_a. sync_wallets ( ) . unwrap ( ) ;
316+ node_b. sync_wallets ( ) . unwrap ( ) ;
317+ node_c. sync_wallets ( ) . unwrap ( ) ;
318+
319+ assert ! (
320+ electrsd. client. transaction_get( & funding_txo_c. txid) . is_err( ) ,
321+ "Second funding tx must not exist on-chain"
322+ ) ;
323+
324+ let channel_a_b = node_a
325+ . list_channels ( )
326+ . into_iter ( )
327+ . find ( |c| c. counterparty_node_id == node_b. node_id ( ) )
328+ . unwrap ( ) ;
329+ assert ! ( channel_a_b. confirmations. unwrap( ) >= 6 ) ;
330+
331+ // The losing channel is a phantom: still "ready" and usable, holding 70k sats that are
332+ // backed by nothing on-chain. Its funding outpoint does not and will never exist, so any
333+ // commitment transaction spending it bounces with `bad-txns-inputs-missingorspent`.
334+ let channel_a_c = node_a
335+ . list_channels ( )
336+ . into_iter ( )
337+ . find ( |c| c. counterparty_node_id == node_c. node_id ( ) )
338+ . unwrap ( ) ;
339+ assert ! ( channel_a_c. is_channel_ready) ;
340+ assert ! ( channel_a_c. is_usable) ;
341+ assert_eq ! ( channel_a_c. confirmations, Some ( 0 ) ) ;
342+
343+ node_a. stop ( ) . unwrap ( ) ;
344+ node_b. stop ( ) . unwrap ( ) ;
345+ node_c. stop ( ) . unwrap ( ) ;
346+ }
347+
172348#[ tokio:: test( flavor = "multi_thread" , worker_threads = 1 ) ]
173349async fn channel_open_fails_when_funds_insufficient ( ) {
174350 let ( bitcoind, electrsd) = setup_bitcoind_and_electrsd ( ) ;
0 commit comments