@@ -132,8 +132,8 @@ pub enum FundingContributionError {
132132 /// The minimum RBF feerate.
133133 min_rbf_feerate : FeeRate ,
134134 } ,
135- /// The splice value is invalid (zero, empty outputs, exceeds the maximum money supply, or
136- /// splices out more than the available channel balance).
135+ /// The splice value is invalid (zero, empty outputs, duplicate inputs or outputs, exceeds the
136+ /// maximum money supply, or splices out more than the available channel balance).
137137 InvalidSpliceValue ,
138138 /// An input's `prevtx` is too large to fit in a `tx_add_input` message.
139139 PrevTxTooLarge ,
@@ -164,7 +164,10 @@ impl core::fmt::Display for FundingContributionError {
164164 write ! ( f, "Feerate {} is below minimum RBF feerate {}" , feerate, min_rbf_feerate)
165165 } ,
166166 FundingContributionError :: InvalidSpliceValue => {
167- write ! ( f, "Invalid splice value (zero, empty, exceeds limit, or overdraws balance)" )
167+ write ! (
168+ f,
169+ "Invalid splice value (zero, empty, duplicate, exceeds limit, or overdraws balance)"
170+ )
168171 } ,
169172 FundingContributionError :: PrevTxTooLarge => {
170173 write ! ( f, "Input prevtx is too large to fit in a tx_add_input message" )
@@ -514,7 +517,14 @@ fn estimate_transaction_fee(
514517
515518fn validate_inputs ( inputs : & [ FundingTxInput ] ) -> Result < ( ) , FundingContributionError > {
516519 let mut total_value = Amount :: ZERO ;
517- for input in inputs {
520+ for ( idx, input) in inputs. iter ( ) . enumerate ( ) {
521+ if inputs[ ..idx]
522+ . iter ( )
523+ . any ( |existing_input| existing_input. utxo . outpoint == input. utxo . outpoint )
524+ {
525+ return Err ( FundingContributionError :: InvalidSpliceValue ) ;
526+ }
527+
518528 use crate :: util:: ser:: Writeable ;
519529 const MESSAGE_TEMPLATE : msgs:: TxAddInput = msgs:: TxAddInput {
520530 channel_id : ChannelId ( [ 0 ; 32 ] ) ,
@@ -1360,7 +1370,14 @@ impl<State> FundingBuilderInner<State> {
13601370 ) ?;
13611371
13621372 let mut value_removed = Amount :: ZERO ;
1363- for output in self . outputs . iter ( ) {
1373+ for ( idx, output) in self . outputs . iter ( ) . enumerate ( ) {
1374+ if self . outputs [ ..idx]
1375+ . iter ( )
1376+ . any ( |existing_output| existing_output. script_pubkey == output. script_pubkey )
1377+ {
1378+ return Err ( FundingContributionError :: InvalidSpliceValue ) ;
1379+ }
1380+
13641381 value_removed = match value_removed. checked_add ( output. value ) {
13651382 Some ( sum) if sum <= Amount :: MAX_MONEY => sum,
13661383 _ => return Err ( FundingContributionError :: InvalidSpliceValue ) ,
@@ -2339,6 +2356,36 @@ mod tests {
23392356 ) ;
23402357 }
23412358
2359+ #[ test]
2360+ fn test_funding_builder_rejects_duplicate_inputs ( ) {
2361+ let feerate = FeeRate :: from_sat_per_kwu ( 2000 ) ;
2362+ let input = funding_input_sats ( 100_000 ) ;
2363+
2364+ let result = FundingTemplate :: new ( None , None , None , Amount :: ZERO )
2365+ . without_prior_contribution ( feerate, FeeRate :: MAX )
2366+ . add_inputs ( vec ! [ input. clone( ) , input] )
2367+ . unwrap ( )
2368+ . build ( ) ;
2369+
2370+ assert ! ( matches!( result, Err ( FundingContributionError :: InvalidSpliceValue ) , ) ) ;
2371+ }
2372+
2373+ #[ test]
2374+ fn test_funding_builder_rejects_duplicate_outputs ( ) {
2375+ let feerate = FeeRate :: from_sat_per_kwu ( 2000 ) ;
2376+ let first_output = funding_output_sats ( 25_000 ) ;
2377+ let second_output = funding_output_sats ( 30_000 ) ;
2378+ assert_ne ! ( first_output, second_output) ;
2379+ assert_eq ! ( first_output. script_pubkey, second_output. script_pubkey) ;
2380+
2381+ let result = FundingTemplate :: new ( None , None , None , Amount :: MAX_MONEY )
2382+ . without_prior_contribution ( feerate, FeeRate :: MAX )
2383+ . add_outputs ( vec ! [ first_output, second_output] )
2384+ . build ( ) ;
2385+
2386+ assert ! ( matches!( result, Err ( FundingContributionError :: InvalidSpliceValue ) , ) ) ;
2387+ }
2388+
23422389 #[ test]
23432390 fn test_funding_builder_remove_input_updates_manual_input_request ( ) {
23442391 let feerate = FeeRate :: from_sat_per_kwu ( 2000 ) ;
0 commit comments