@@ -576,6 +576,16 @@ impl<ChannelSigner: EcdsaChannelSigner> OnchainTxHandler<ChannelSigner> {
576576 self . pending_claim_requests . len ( ) != 0
577577 }
578578
579+ fn is_outpoint_spend_waiting_threshold_conf ( & self , outpoint : & BitcoinOutPoint ) -> bool {
580+ self . onchain_events_awaiting_threshold_conf . iter ( ) . any ( |entry| {
581+ if let OnchainEvent :: ContentiousOutpoint { ref package } = entry. event {
582+ package. contains_outpoint ( outpoint)
583+ } else {
584+ false
585+ }
586+ } )
587+ }
588+
579589 /// Lightning security model (i.e being able to redeem/timeout HTLC or penalize counterparty
580590 /// onchain) lays on the assumption of claim transactions getting confirmed before timelock
581591 /// expiration (CSV or CLTV following cases). In case of high-fee spikes, claim tx may get stuck
@@ -802,7 +812,15 @@ impl<ChannelSigner: EcdsaChannelSigner> OnchainTxHandler<ChannelSigner> {
802812 // First drop any duplicate claims.
803813 requests. retain ( |req| {
804814 let outpoint = req. outpoint ( ) ;
805- if self . claimable_outpoints . get ( outpoint) . is_some ( ) {
815+ if self . is_outpoint_spend_waiting_threshold_conf ( outpoint) {
816+ // This is a package-layer guard. ChannelMonitor filters regenerated
817+ // HTLC claims using HTLC resolution state, while this keeps outpoints
818+ // split from an existing package from being re-added during the reorg
819+ // window.
820+ log_info ! ( logger, "Ignoring claim for outpoint {}:{}, it is already spent by a transaction awaiting anti-reorg confirmation" ,
821+ outpoint. txid, outpoint. vout) ;
822+ false
823+ } else if self . claimable_outpoints . get ( outpoint) . is_some ( ) {
806824 log_info ! ( logger, "Ignoring second claim for outpoint {}:{}, already registered its claiming request" ,
807825 outpoint. txid, outpoint. vout) ;
808826 false
@@ -1276,11 +1294,14 @@ impl<ChannelSigner: EcdsaChannelSigner> OnchainTxHandler<ChannelSigner> {
12761294
12771295#[ cfg( test) ]
12781296mod tests {
1279- use bitcoin:: hash_types:: Txid ;
1297+ use bitcoin:: hash_types:: { BlockHash , Txid } ;
12801298 use bitcoin:: hashes:: sha256:: Hash as Sha256 ;
12811299 use bitcoin:: hashes:: Hash ;
1300+ use bitcoin:: locktime:: absolute:: LockTime ;
1301+ use bitcoin:: transaction:: { OutPoint as BitcoinOutPoint , Version } ;
12821302 use bitcoin:: Network ;
1283- use bitcoin:: { key:: Secp256k1 , secp256k1:: PublicKey , secp256k1:: SecretKey , ScriptBuf } ;
1303+ use bitcoin:: { key:: Secp256k1 , secp256k1:: PublicKey , secp256k1:: SecretKey } ;
1304+ use bitcoin:: { Amount , ScriptBuf , Transaction , TxIn , TxOut } ;
12841305 use types:: features:: ChannelTypeFeatures ;
12851306
12861307 use crate :: chain:: chaininterface:: { ConfirmationTarget , LowerBoundedFeeEstimator } ;
@@ -1404,6 +1425,18 @@ mod tests {
14041425 requests
14051426 }
14061427
1428+ fn locked_outpoints (
1429+ tx_handler : & OnchainTxHandler < InMemorySigner > , locktime : u32 ,
1430+ ) -> Vec < BitcoinOutPoint > {
1431+ tx_handler
1432+ . locktimed_packages
1433+ . get ( & locktime)
1434+ . into_iter ( )
1435+ . flat_map ( |packages| packages. iter ( ) )
1436+ . flat_map ( |package| package. outpoints ( ) . into_iter ( ) . map ( |outpoint| * outpoint) )
1437+ . collect ( )
1438+ }
1439+
14071440 // Test that all claims with locktime equal to or less than the current height are broadcast
14081441 // immediately while claims with locktime greater than the current height are only broadcast
14091442 // once the locktime is reached.
@@ -1569,4 +1602,104 @@ mod tests {
15691602 _ => panic ! ( "expected a single HTLC bump event" ) ,
15701603 }
15711604 }
1605+
1606+ #[ test]
1607+ fn test_replayed_claim_ignored_for_pending_spent_outpoint ( ) {
1608+ let claim_height = 21 ;
1609+ let spend_height = 22 ;
1610+ let locktime = 42 ;
1611+ let mut nondust_htlcs = Vec :: new ( ) ;
1612+ for i in 0 ..2 {
1613+ let preimage = PaymentPreimage ( [ i + 1 ; 32 ] ) ;
1614+ let hash = PaymentHash ( Sha256 :: hash ( & preimage. 0 [ ..] ) . to_byte_array ( ) ) ;
1615+ nondust_htlcs. push ( HTLCOutputInCommitment {
1616+ offered : true ,
1617+ amount_msat : 10000 ,
1618+ cltv_expiry : locktime,
1619+ payment_hash : hash,
1620+ transaction_output_index : Some ( i as u32 ) ,
1621+ } ) ;
1622+ }
1623+
1624+ let mut tx_handler = new_test_tx_handler (
1625+ ChannelTypeFeatures :: anchors_zero_htlc_fee_and_dependencies ( ) ,
1626+ nondust_htlcs,
1627+ ) ;
1628+ let requests = build_offered_holder_htlc_requests ( & tx_handler) ;
1629+ let spent_outpoint = * requests[ 0 ] . outpoint ( ) ;
1630+ let still_delayed_outpoint = * requests[ 1 ] . outpoint ( ) ;
1631+ let destination_script = ScriptBuf :: new ( ) ;
1632+ let broadcaster = TestBroadcaster :: new ( Network :: Testnet ) ;
1633+ let fee_estimator = TestFeeEstimator :: new ( 253 ) ;
1634+ let fee_estimator = LowerBoundedFeeEstimator :: new ( & fee_estimator) ;
1635+ let logger = TestLogger :: new ( ) ;
1636+
1637+ // Register both holder HTLC claims as one delayed package before any
1638+ // individual outpoint spends are observed.
1639+ tx_handler. update_claims_view_from_requests (
1640+ requests. clone ( ) ,
1641+ claim_height,
1642+ claim_height,
1643+ & & broadcaster,
1644+ ConfirmationTarget :: UrgentOnChainSweep ,
1645+ & destination_script,
1646+ & fee_estimator,
1647+ & logger,
1648+ ) ;
1649+ assert_eq ! ( locked_outpoints( & tx_handler, locktime) . len( ) , 2 ) ;
1650+
1651+ // Spend one outpoint before the package matures. The handler should split
1652+ // it into a ContentiousOutpoint until the anti-reorg threshold passes.
1653+ let spend_tx = Transaction {
1654+ version : Version :: TWO ,
1655+ lock_time : LockTime :: ZERO ,
1656+ input : vec ! [ TxIn { previous_output: spent_outpoint, ..Default :: default ( ) } ] ,
1657+ output : vec ! [ TxOut { value: Amount :: from_sat( 1000 ) , script_pubkey: ScriptBuf :: new( ) } ] ,
1658+ } ;
1659+ tx_handler. update_claims_view_from_matched_txn (
1660+ & [ & spend_tx] ,
1661+ spend_height,
1662+ BlockHash :: all_zeros ( ) ,
1663+ spend_height,
1664+ & & broadcaster,
1665+ ConfirmationTarget :: UrgentOnChainSweep ,
1666+ & destination_script,
1667+ & fee_estimator,
1668+ & logger,
1669+ ) ;
1670+ let locked = locked_outpoints ( & tx_handler, locktime) ;
1671+ assert_eq ! ( locked, vec![ still_delayed_outpoint] ) ;
1672+
1673+ // Replaying both original claim requests during that window must not
1674+ // re-add the already-spent outpoint to the delayed package.
1675+ tx_handler. update_claims_view_from_requests (
1676+ requests,
1677+ spend_height,
1678+ spend_height,
1679+ & & broadcaster,
1680+ ConfirmationTarget :: UrgentOnChainSweep ,
1681+ & destination_script,
1682+ & fee_estimator,
1683+ & logger,
1684+ ) ;
1685+ let locked = locked_outpoints ( & tx_handler, locktime) ;
1686+ assert_eq ! ( locked, vec![ still_delayed_outpoint] ) ;
1687+ assert ! ( tx_handler. pending_claim_requests. is_empty( ) ) ;
1688+ assert ! ( tx_handler. claimable_outpoints. is_empty( ) ) ;
1689+
1690+ // If the spend reorgs out, the contentious outpoint is resurrected into
1691+ // the delayed package.
1692+ tx_handler. blocks_disconnected (
1693+ spend_height - 1 ,
1694+ & & broadcaster,
1695+ ConfirmationTarget :: UrgentOnChainSweep ,
1696+ & destination_script,
1697+ & fee_estimator,
1698+ & logger,
1699+ ) ;
1700+ let locked = locked_outpoints ( & tx_handler, locktime) ;
1701+ assert_eq ! ( locked. len( ) , 2 ) ;
1702+ assert ! ( locked. contains( & spent_outpoint) ) ;
1703+ assert ! ( locked. contains( & still_delayed_outpoint) ) ;
1704+ }
15721705}
0 commit comments