@@ -60,7 +60,22 @@ macro_rules! build_funding_contribution {
6060 let shared_input: Option <Input > = $shared_input;
6161 let feerate: FeeRate = $feerate;
6262
63- let value_removed = outputs. iter( ) . map( |txout| txout. value) . sum( ) ;
63+ // Validate user-provided amounts are within MAX_MONEY before coin selection to
64+ // ensure FundingContribution::net_value() arithmetic cannot overflow. With all
65+ // amounts bounded by MAX_MONEY (~2.1e15 sat), the worst-case net_value()
66+ // computation is -2 * MAX_MONEY (~-4.2e15), well within i64::MIN (~-9.2e18).
67+ if value_added > Amount :: MAX_MONEY {
68+ return Err ( ( ) ) ;
69+ }
70+
71+ let mut value_removed = Amount :: ZERO ;
72+ for txout in outputs. iter( ) {
73+ value_removed = match value_removed. checked_add( txout. value) {
74+ Some ( sum) if sum <= Amount :: MAX_MONEY => sum,
75+ _ => return Err ( ( ) ) ,
76+ } ;
77+ }
78+
6479 let is_splice = shared_input. is_some( ) ;
6580
6681 let coin_selection = if value_added == Amount :: ZERO {
@@ -102,6 +117,7 @@ macro_rules! build_funding_contribution {
102117 // purposes — this is conservative, overestimating rather than underestimating fees if
103118 // the node ends up as the acceptor.
104119 let estimated_fee = estimate_transaction_fee( & inputs, & outputs, true , is_splice, feerate) ;
120+ debug_assert!( estimated_fee <= Amount :: MAX_MONEY ) ;
105121
106122 let contribution = FundingContribution {
107123 value_added,
@@ -305,10 +321,9 @@ impl FundingContribution {
305321 ( inputs. into_iter ( ) . map ( |input| input. utxo . outpoint ) . collect ( ) , outputs)
306322 }
307323
308- /// The net value contributed to a channel by the splice. If negative, more value will be
309- /// spliced out than spliced in. Fees will be deducted from the expected splice-out amount
310- /// if no inputs were included.
311- pub fn net_value ( & self ) -> Result < SignedAmount , String > {
324+ /// Validates that the funding inputs are suitable for use in the interactive transaction
325+ /// protocol, checking prevtx sizes and input sufficiency.
326+ pub fn validate ( & self ) -> Result < ( ) , String > {
312327 for FundingTxInput { utxo, prevtx, .. } in self . inputs . iter ( ) {
313328 use crate :: util:: ser:: Writeable ;
314329 const MESSAGE_TEMPLATE : msgs:: TxAddInput = msgs:: TxAddInput {
@@ -361,26 +376,32 @@ impl FundingContribution {
361376 }
362377 }
363378
379+ Ok ( ( ) )
380+ }
381+
382+ /// The net value contributed to a channel by the splice. If negative, more value will be
383+ /// spliced out than spliced in. Fees will be deducted from the expected splice-out amount
384+ /// if no inputs were included.
385+ pub fn net_value ( & self ) -> SignedAmount {
364386 let unpaid_fees = if self . inputs . is_empty ( ) { self . estimated_fee } else { Amount :: ZERO }
365387 . to_signed ( )
366- . expect ( "fees should never exceed Amount::MAX_MONEY" ) ;
367- let value_added = self . value_added . to_signed ( ) . map_err ( |_| "Value added too large" ) ?;
388+ . expect ( "estimated_fee is validated to not exceed Amount::MAX_MONEY" ) ;
389+ let value_added = self
390+ . value_added
391+ . to_signed ( )
392+ . expect ( "value_added is validated to not exceed Amount::MAX_MONEY" ) ;
368393 let value_removed = self
369394 . outputs
370395 . iter ( )
371396 . map ( |txout| txout. value )
372397 . sum :: < Amount > ( )
373398 . to_signed ( )
374- . map_err ( |_| "Value removed too large" ) ? ;
399+ . expect ( "value_removed is validated to not exceed Amount::MAX_MONEY" ) ;
375400
376401 let contribution_amount = value_added - value_removed;
377- let adjusted_contribution = contribution_amount. checked_sub ( unpaid_fees) . ok_or ( format ! (
378- "{} splice-out amount plus {} fee estimate exceeds the total bitcoin supply" ,
379- contribution_amount. unsigned_abs( ) ,
380- self . estimated_fee,
381- ) ) ?;
382-
383- Ok ( adjusted_contribution)
402+ contribution_amount
403+ . checked_sub ( unpaid_fees)
404+ . expect ( "all amounts are validated to not exceed Amount::MAX_MONEY" )
384405 }
385406}
386407
@@ -390,10 +411,12 @@ pub type FundingTxInput = crate::util::wallet_utils::ConfirmedUtxo;
390411
391412#[ cfg( test) ]
392413mod tests {
393- use super :: { estimate_transaction_fee, FundingContribution , FundingTxInput } ;
414+ use super :: { estimate_transaction_fee, FundingContribution , FundingTemplate , FundingTxInput } ;
415+ use crate :: chain:: ClaimId ;
416+ use crate :: util:: wallet_utils:: { CoinSelection , CoinSelectionSourceSync , Input } ;
394417 use bitcoin:: hashes:: Hash ;
395418 use bitcoin:: transaction:: { Transaction , TxOut , Version } ;
396- use bitcoin:: { Amount , FeeRate , ScriptBuf , SignedAmount , WPubkeyHash } ;
419+ use bitcoin:: { Amount , FeeRate , Psbt , ScriptBuf , SignedAmount , WPubkeyHash } ;
397420
398421 #[ test]
399422 #[ rustfmt:: skip]
@@ -483,7 +506,8 @@ mod tests {
483506 is_splice : true ,
484507 feerate : FeeRate :: from_sat_per_kwu ( 2000 ) ,
485508 } ;
486- assert_eq ! ( contribution. net_value( ) , Ok ( contribution. value_added. to_signed( ) . unwrap( ) ) ) ;
509+ assert ! ( contribution. validate( ) . is_ok( ) ) ;
510+ assert_eq ! ( contribution. net_value( ) , contribution. value_added. to_signed( ) . unwrap( ) ) ;
487511 }
488512
489513 // Net splice-in
@@ -503,7 +527,8 @@ mod tests {
503527 is_splice : true ,
504528 feerate : FeeRate :: from_sat_per_kwu ( 2000 ) ,
505529 } ;
506- assert_eq ! ( contribution. net_value( ) , Ok ( SignedAmount :: from_sat( 220_000 - 200_000 ) ) ) ;
530+ assert ! ( contribution. validate( ) . is_ok( ) ) ;
531+ assert_eq ! ( contribution. net_value( ) , SignedAmount :: from_sat( 220_000 - 200_000 ) ) ;
507532 }
508533
509534 // Net splice-out
@@ -523,7 +548,8 @@ mod tests {
523548 is_splice : true ,
524549 feerate : FeeRate :: from_sat_per_kwu ( 2000 ) ,
525550 } ;
526- assert_eq ! ( contribution. net_value( ) , Ok ( SignedAmount :: from_sat( 220_000 - 400_000 ) ) ) ;
551+ assert ! ( contribution. validate( ) . is_ok( ) ) ;
552+ assert_eq ! ( contribution. net_value( ) , SignedAmount :: from_sat( 220_000 - 400_000 ) ) ;
527553 }
528554
529555 // Net splice-out, inputs insufficient to cover fees
@@ -544,7 +570,7 @@ mod tests {
544570 feerate : FeeRate :: from_sat_per_kwu ( 90000 ) ,
545571 } ;
546572 assert_eq ! (
547- contribution. net_value ( ) ,
573+ contribution. validate ( ) ,
548574 Err ( format!(
549575 "Total input amount 0.00300000 BTC is lower than needed for splice-in contribution 0.00220000 BTC, considering fees of {}. Need more inputs." ,
550576 Amount :: from_sat( expected_fee) ,
@@ -567,7 +593,7 @@ mod tests {
567593 feerate : FeeRate :: from_sat_per_kwu ( 2000 ) ,
568594 } ;
569595 assert_eq ! (
570- contribution. net_value ( ) ,
596+ contribution. validate ( ) ,
571597 Err ( format!(
572598 "Total input amount 0.00100000 BTC is lower than needed for splice-in contribution 0.00220000 BTC, considering fees of {}. Need more inputs." ,
573599 Amount :: from_sat( expected_fee) ,
@@ -590,7 +616,8 @@ mod tests {
590616 is_splice : true ,
591617 feerate : FeeRate :: from_sat_per_kwu ( 2000 ) ,
592618 } ;
593- assert_eq ! ( contribution. net_value( ) , Ok ( contribution. value_added. to_signed( ) . unwrap( ) ) ) ;
619+ assert ! ( contribution. validate( ) . is_ok( ) ) ;
620+ assert_eq ! ( contribution. net_value( ) , contribution. value_added. to_signed( ) . unwrap( ) ) ;
594621 }
595622
596623 // higher fee rate, does not cover
@@ -609,7 +636,7 @@ mod tests {
609636 feerate : FeeRate :: from_sat_per_kwu ( 2200 ) ,
610637 } ;
611638 assert_eq ! (
612- contribution. net_value ( ) ,
639+ contribution. validate ( ) ,
613640 Err ( format!(
614641 "Total input amount 0.00300000 BTC is lower than needed for splice-in contribution 0.00298032 BTC, considering fees of {}. Need more inputs." ,
615642 Amount :: from_sat( expected_fee) ,
@@ -632,7 +659,68 @@ mod tests {
632659 is_splice : false ,
633660 feerate : FeeRate :: from_sat_per_kwu ( 2000 ) ,
634661 } ;
635- assert_eq ! ( contribution. net_value( ) , Ok ( contribution. value_added. to_signed( ) . unwrap( ) ) ) ;
662+ assert ! ( contribution. validate( ) . is_ok( ) ) ;
663+ assert_eq ! ( contribution. net_value( ) , contribution. value_added. to_signed( ) . unwrap( ) ) ;
664+ }
665+ }
666+
667+ struct UnreachableWallet ;
668+
669+ impl CoinSelectionSourceSync for UnreachableWallet {
670+ fn select_confirmed_utxos (
671+ & self , _claim_id : Option < ClaimId > , _must_spend : Vec < Input > , _must_pay_to : & [ TxOut ] ,
672+ _target_feerate_sat_per_1000_weight : u32 , _max_tx_weight : u64 ,
673+ ) -> Result < CoinSelection , ( ) > {
674+ unreachable ! ( "should not reach coin selection" )
675+ }
676+ fn sign_psbt ( & self , _psbt : Psbt ) -> Result < Transaction , ( ) > {
677+ unreachable ! ( "should not reach signing" )
678+ }
679+ }
680+
681+ #[ test]
682+ fn test_build_funding_contribution_validates_max_money ( ) {
683+ let over_max = Amount :: MAX_MONEY + Amount :: from_sat ( 1 ) ;
684+ let feerate = FeeRate :: from_sat_per_kwu ( 2000 ) ;
685+
686+ // splice_in_sync with value_added > MAX_MONEY
687+ {
688+ let template = FundingTemplate :: new ( None , feerate) ;
689+ assert ! ( template. splice_in_sync( over_max, UnreachableWallet ) . is_err( ) ) ;
690+ }
691+
692+ // splice_out_sync with single output value > MAX_MONEY
693+ {
694+ let template = FundingTemplate :: new ( None , feerate) ;
695+ let outputs = vec ! [ funding_output_sats( over_max. to_sat( ) ) ] ;
696+ assert ! ( template. splice_out_sync( outputs, UnreachableWallet ) . is_err( ) ) ;
697+ }
698+
699+ // splice_out_sync with multiple outputs summing > MAX_MONEY
700+ {
701+ let template = FundingTemplate :: new ( None , feerate) ;
702+ let half_over = Amount :: MAX_MONEY / 2 + Amount :: from_sat ( 1 ) ;
703+ let outputs = vec ! [
704+ funding_output_sats( half_over. to_sat( ) ) ,
705+ funding_output_sats( half_over. to_sat( ) ) ,
706+ ] ;
707+ assert ! ( template. splice_out_sync( outputs, UnreachableWallet ) . is_err( ) ) ;
708+ }
709+
710+ // splice_in_and_out_sync with value_added > MAX_MONEY
711+ {
712+ let template = FundingTemplate :: new ( None , feerate) ;
713+ let outputs = vec ! [ funding_output_sats( 1_000 ) ] ;
714+ assert ! ( template. splice_in_and_out_sync( over_max, outputs, UnreachableWallet ) . is_err( ) ) ;
715+ }
716+
717+ // splice_in_and_out_sync with output sum > MAX_MONEY
718+ {
719+ let template = FundingTemplate :: new ( None , feerate) ;
720+ let outputs = vec ! [ funding_output_sats( over_max. to_sat( ) ) ] ;
721+ assert ! ( template
722+ . splice_in_and_out_sync( Amount :: from_sat( 1_000 ) , outputs, UnreachableWallet )
723+ . is_err( ) ) ;
636724 }
637725 }
638726}
0 commit comments