@@ -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 ] ) ,
@@ -1362,7 +1372,14 @@ impl<State> FundingBuilderInner<State> {
13621372 ) ?;
13631373
13641374 let mut value_removed = Amount :: ZERO ;
1365- for output in self . outputs . iter ( ) {
1375+ for ( idx, output) in self . outputs . iter ( ) . enumerate ( ) {
1376+ if self . outputs [ ..idx]
1377+ . iter ( )
1378+ . any ( |existing_output| existing_output. script_pubkey == output. script_pubkey )
1379+ {
1380+ return Err ( FundingContributionError :: InvalidSpliceValue ) ;
1381+ }
1382+
13661383 value_removed = match value_removed. checked_add ( output. value ) {
13671384 Some ( sum) if sum <= Amount :: MAX_MONEY => sum,
13681385 _ => return Err ( FundingContributionError :: InvalidSpliceValue ) ,
@@ -2325,6 +2342,36 @@ mod tests {
23252342 ) ;
23262343 }
23272344
2345+ #[ test]
2346+ fn test_funding_builder_rejects_duplicate_inputs ( ) {
2347+ let feerate = FeeRate :: from_sat_per_kwu ( 2000 ) ;
2348+ let input = funding_input_sats ( 100_000 ) ;
2349+
2350+ let result = FundingTemplate :: new ( None , None , None , Amount :: ZERO )
2351+ . without_prior_contribution ( feerate, FeeRate :: MAX )
2352+ . add_inputs ( vec ! [ input. clone( ) , input] )
2353+ . unwrap ( )
2354+ . build ( ) ;
2355+
2356+ assert ! ( matches!( result, Err ( FundingContributionError :: InvalidSpliceValue ) , ) ) ;
2357+ }
2358+
2359+ #[ test]
2360+ fn test_funding_builder_rejects_duplicate_outputs ( ) {
2361+ let feerate = FeeRate :: from_sat_per_kwu ( 2000 ) ;
2362+ let first_output = funding_output_sats ( 25_000 ) ;
2363+ let second_output = funding_output_sats ( 30_000 ) ;
2364+ assert_ne ! ( first_output, second_output) ;
2365+ assert_eq ! ( first_output. script_pubkey, second_output. script_pubkey) ;
2366+
2367+ let result = FundingTemplate :: new ( None , None , None , Amount :: MAX_MONEY )
2368+ . without_prior_contribution ( feerate, FeeRate :: MAX )
2369+ . add_outputs ( vec ! [ first_output, second_output] )
2370+ . build ( ) ;
2371+
2372+ assert ! ( matches!( result, Err ( FundingContributionError :: InvalidSpliceValue ) , ) ) ;
2373+ }
2374+
23282375 #[ test]
23292376 fn test_funding_builder_remove_input_updates_manual_input_request ( ) {
23302377 let feerate = FeeRate :: from_sat_per_kwu ( 2000 ) ;
0 commit comments