@@ -1948,6 +1948,166 @@ async fn splice_payment_reorged_to_unconfirmed() {
19481948 node_b. stop ( ) . unwrap ( ) ;
19491949}
19501950
1951+ #[ tokio:: test( flavor = "multi_thread" , worker_threads = 1 ) ]
1952+ async fn rbf_splice_payment_reverts_after_deep_reorg ( ) {
1953+ // A graduated RBF splice payment is anchored to the FIRST candidate's id but stamped with the
1954+ // CONFIRMED (RBF) candidate's txid. Graduation removes its pending-store entry, so a later deep
1955+ // reorg (deeper than ANTI_REORG_DELAY) that returns the confirmed candidate to the mempool must
1956+ // still map the event back to the original payment and revert it — not create a duplicate
1957+ // generic on-chain payment under the confirmed candidate's id.
1958+
1959+ // Lower incrementalrelayfee so the RBF feerate bump is relayable (as run_rbf_splice_channel_test).
1960+ let bitcoind_exe = std:: env:: var ( "BITCOIND_EXE" )
1961+ . ok ( )
1962+ . or_else ( || corepc_node:: downloaded_exe_path ( ) . ok ( ) )
1963+ . expect (
1964+ "you need to provide an env var BITCOIND_EXE or specify a bitcoind version feature" ,
1965+ ) ;
1966+ let mut bitcoind_conf = corepc_node:: Conf :: default ( ) ;
1967+ bitcoind_conf. network = "regtest" ;
1968+ bitcoind_conf. args . push ( "-rest" ) ;
1969+ bitcoind_conf. args . push ( "-incrementalrelayfee=0.00000100" ) ;
1970+ let bitcoind = BitcoinD :: with_conf ( bitcoind_exe, & bitcoind_conf) . unwrap ( ) ;
1971+
1972+ let electrs_exe = std:: env:: var ( "ELECTRS_EXE" )
1973+ . ok ( )
1974+ . or_else ( electrsd:: downloaded_exe_path)
1975+ . expect ( "you need to provide env var ELECTRS_EXE or specify an electrsd version feature" ) ;
1976+ let mut electrsd_conf = electrsd:: Conf :: default ( ) ;
1977+ electrsd_conf. http_enabled = true ;
1978+ electrsd_conf. network = "regtest" ;
1979+ let electrsd = ElectrsD :: with_conf ( electrs_exe, & bitcoind, & electrsd_conf) . unwrap ( ) ;
1980+ let chain_source = random_chain_source ( & bitcoind, & electrsd) ;
1981+
1982+ let ( node_a, node_b) = setup_two_nodes ( & chain_source, false , true , false ) ;
1983+
1984+ let address_a = node_a. onchain_payment ( ) . new_address ( ) . unwrap ( ) ;
1985+ let address_b = node_b. onchain_payment ( ) . new_address ( ) . unwrap ( ) ;
1986+ let premine_amount_sat = 5_000_000 ;
1987+ premine_and_distribute_funds (
1988+ & bitcoind. client ,
1989+ & electrsd. client ,
1990+ vec ! [ address_a, address_b] ,
1991+ Amount :: from_sat ( premine_amount_sat) ,
1992+ )
1993+ . await ;
1994+ node_a. sync_wallets ( ) . unwrap ( ) ;
1995+ node_b. sync_wallets ( ) . unwrap ( ) ;
1996+
1997+ open_channel ( & node_a, & node_b, 4_000_000 , false , & electrsd) . await ;
1998+ generate_blocks_and_wait ( & bitcoind. client , & electrsd. client , 6 ) . await ;
1999+ node_a. sync_wallets ( ) . unwrap ( ) ;
2000+ node_b. sync_wallets ( ) . unwrap ( ) ;
2001+ let _user_channel_id_a = expect_channel_ready_event ! ( node_a, node_b. node_id( ) ) ;
2002+ let user_channel_id_b = expect_channel_ready_event ! ( node_b, node_a. node_id( ) ) ;
2003+
2004+ // node_b splices in, then RBF-bumps it: the funding payment spans two candidates, its id
2005+ // anchored to the first (original) candidate's txid.
2006+ node_b. splice_in ( & user_channel_id_b, node_a. node_id ( ) , 1_000_000 ) . unwrap ( ) ;
2007+ let original_txo = expect_splice_negotiated_event ! ( node_a, node_b. node_id( ) ) ;
2008+ expect_splice_negotiated_event ! ( node_b, node_a. node_id( ) ) ;
2009+ wait_for_tx ( & electrsd. client , original_txo. txid ) . await ;
2010+ wait_for_classified_funding_payment ( & node_b, original_txo. txid ) . await ;
2011+ node_a. sync_wallets ( ) . unwrap ( ) ;
2012+ node_b. sync_wallets ( ) . unwrap ( ) ;
2013+
2014+ node_b. bump_channel_funding_fee ( & user_channel_id_b, node_a. node_id ( ) ) . unwrap ( ) ;
2015+ let rbf_txo = expect_splice_negotiated_event ! ( node_a, node_b. node_id( ) ) ;
2016+ expect_splice_negotiated_event ! ( node_b, node_a. node_id( ) ) ;
2017+ assert_ne ! ( original_txo, rbf_txo, "RBF should produce a different funding txo" ) ;
2018+ wait_for_tx ( & electrsd. client , rbf_txo. txid ) . await ;
2019+ wait_for_classified_funding_payment ( & node_b, rbf_txo. txid ) . await ;
2020+ node_a. sync_wallets ( ) . unwrap ( ) ;
2021+ node_b. sync_wallets ( ) . unwrap ( ) ;
2022+
2023+ // Confirm the RBF candidate and graduate it past ANTI_REORG_DELAY (6 confirmations), which
2024+ // removes the pending-store entry.
2025+ generate_blocks_and_wait ( & bitcoind. client , & electrsd. client , 6 ) . await ;
2026+ node_a. sync_wallets ( ) . unwrap ( ) ;
2027+ node_b. sync_wallets ( ) . unwrap ( ) ;
2028+ expect_channel_ready_event ! ( node_a, node_b. node_id( ) ) ;
2029+ expect_channel_ready_event ! ( node_b, node_a. node_id( ) ) ;
2030+
2031+ let payment_id = PaymentId ( original_txo. txid . to_byte_array ( ) ) ;
2032+ let rbf_payment_id = PaymentId ( rbf_txo. txid . to_byte_array ( ) ) ;
2033+
2034+ // Graduated: anchored to the original candidate's id, stamped with the confirmed RBF
2035+ // candidate's txid, with no separate record under the RBF candidate's id.
2036+ let payment = node_b. payment ( & payment_id) . expect ( "splice payment graduated" ) ;
2037+ assert_eq ! ( payment. status, PaymentStatus :: Succeeded ) ;
2038+ assert ! ( matches!(
2039+ payment. kind,
2040+ PaymentKind :: Onchain {
2041+ txid,
2042+ status: ConfirmationStatus :: Confirmed { .. } ,
2043+ tx_type: Some ( TransactionType :: InteractiveFunding { .. } ) ,
2044+ } if txid == rbf_txo. txid
2045+ ) ) ;
2046+ assert ! (
2047+ node_b. payment( & rbf_payment_id) . is_none( ) ,
2048+ "the graduated splice payment must not be duplicated under the RBF candidate's id" ,
2049+ ) ;
2050+
2051+ // Deep reorg (deeper than ANTI_REORG_DELAY): drop the 6 graduation blocks and build a longer,
2052+ // transaction-free chain, returning the confirmed RBF candidate to the mempool.
2053+ let original_height =
2054+ bitcoind. client . get_blockchain_info ( ) . expect ( "failed to get blockchain info" ) . blocks ;
2055+ invalidate_blocks ( & bitcoind. client , 6 ) ;
2056+ let replacement_address = bitcoind. client . new_address ( ) . expect ( "failed to get new address" ) ;
2057+ for _ in 0 ..7 {
2058+ let _res: serde_json:: Value = bitcoind
2059+ . client
2060+ . call ( "generateblock" , & [ json ! ( replacement_address. to_string( ) ) , json ! ( [ ] ) ] )
2061+ . expect ( "failed to generate empty block" ) ;
2062+ }
2063+ wait_for_block ( & electrsd. client , original_height as usize + 1 ) . await ;
2064+ // Wait for the reorged-out RBF candidate to reappear in the mempool before syncing, so the sync
2065+ // reliably observes its TxUnconfirmed event rather than racing electrs's reindex.
2066+ wait_for_tx ( & electrsd. client , rbf_txo. txid ) . await ;
2067+ node_b. sync_wallets ( ) . unwrap ( ) ;
2068+
2069+ // The reorg event for the confirmed RBF candidate's txid must map back to the original payment
2070+ // and revert it to Pending/Unconfirmed, rather than creating a duplicate generic on-chain
2071+ // payment under the RBF candidate's id.
2072+ assert ! (
2073+ node_b. payment( & rbf_payment_id) . is_none( ) ,
2074+ "a reorged-out RBF splice must not produce a duplicate generic on-chain payment" ,
2075+ ) ;
2076+ let payment = node_b. payment ( & payment_id) . expect ( "splice payment still exists after the reorg" ) ;
2077+ assert_eq ! ( payment. status, PaymentStatus :: Pending ) ;
2078+ assert ! ( matches!(
2079+ payment. kind,
2080+ PaymentKind :: Onchain {
2081+ status: ConfirmationStatus :: Unconfirmed ,
2082+ tx_type: Some ( TransactionType :: InteractiveFunding { .. } ) ,
2083+ ..
2084+ }
2085+ ) ) ;
2086+
2087+ // The revert re-established the pending-store entry, so once the RBF candidate (still in the
2088+ // mempool) reconfirms past ANTI_REORG_DELAY the payment re-graduates to Succeeded in place —
2089+ // without leaving a duplicate behind.
2090+ generate_blocks_and_wait ( & bitcoind. client , & electrsd. client , 6 ) . await ;
2091+ node_b. sync_wallets ( ) . unwrap ( ) ;
2092+ let payment = node_b. payment ( & payment_id) . expect ( "splice payment re-graduated" ) ;
2093+ assert_eq ! ( payment. status, PaymentStatus :: Succeeded ) ;
2094+ assert ! ( matches!(
2095+ payment. kind,
2096+ PaymentKind :: Onchain {
2097+ txid,
2098+ status: ConfirmationStatus :: Confirmed { .. } ,
2099+ tx_type: Some ( TransactionType :: InteractiveFunding { .. } ) ,
2100+ } if txid == rbf_txo. txid
2101+ ) ) ;
2102+ assert ! (
2103+ node_b. payment( & rbf_payment_id) . is_none( ) ,
2104+ "re-graduation must not create a duplicate payment under the RBF candidate's id" ,
2105+ ) ;
2106+
2107+ node_a. stop ( ) . unwrap ( ) ;
2108+ node_b. stop ( ) . unwrap ( ) ;
2109+ }
2110+
19512111#[ tokio:: test( flavor = "multi_thread" , worker_threads = 1 ) ]
19522112async fn splice_in_rbf_joins_counterparty_splice ( ) {
19532113 let ( bitcoind, electrsd) = setup_bitcoind_and_electrsd ( ) ;
0 commit comments