@@ -99,6 +99,7 @@ def run_test(self):
9999 self .test_subtract_fee_with_presets ()
100100 self .test_transaction_too_large ()
101101 self .test_include_unsafe ()
102+ self .test_22670 ()
102103
103104 def test_change_position (self ):
104105 """Ensure setting changePosition in fundraw with an exact match is handled properly."""
@@ -969,6 +970,62 @@ def test_include_unsafe(self):
969970 signedtx = wallet .signrawtransactionwithwallet (fundedtx ['hex' ])
970971 wallet .sendrawtransaction (signedtx ['hex' ])
971972
973+ def test_22670 (self ):
974+ # In issue #22670, it was observed that ApproximateBestSubset may
975+ # choose enough value to cover the target amount but not enough to cover the transaction fees.
976+ # This leads to a transaction whose actual transaction feerate is lower than expected.
977+ # However at normal feerates, the difference between the effective value and the real value
978+ # that this bug is not detected because the transaction fee must be at least 0.01 BTC (the minimum change value).
979+ # Otherwise the targeted minimum change value will be enough to cover the transaction fees that were not
980+ # being accounted for. So the minimum relay fee is set to 0.1 BTC/kvB in this test.
981+ self .log .info ("Test issue 22670 ApproximateBestSubset bug" )
982+ # Make sure the default wallet will not be loaded when restarted with a high minrelaytxfee
983+ self .nodes [0 ].unloadwallet (self .default_wallet_name , False )
984+ feerate = Decimal ("0.1" )
985+ self .restart_node (0 , [f"-minrelaytxfee={ feerate } " , "-discardfee=0" ]) # Set high minrelayfee, set discardfee to 0 for easier calculation
986+
987+ self .nodes [0 ].loadwallet (self .default_wallet_name , True )
988+ funds = self .nodes [0 ].get_wallet_rpc (self .default_wallet_name )
989+ self .nodes [0 ].createwallet (wallet_name = "tester" )
990+ tester = self .nodes [0 ].get_wallet_rpc ("tester" )
991+
992+ # Because this test is specifically for ApproximateBestSubset, the target value must be greater
993+ # than any single input available, and require more than 1 input. So we make 3 outputs
994+ for i in range (0 , 3 ):
995+ funds .sendtoaddress (tester .getnewaddress (address_type = "bech32" ), 1 )
996+ self .nodes [0 ].generate (1 )
997+
998+ # Create transactions in order to calculate fees for the target bounds that can trigger this bug
999+ change_tx = tester .fundrawtransaction (tester .createrawtransaction ([], [{funds .getnewaddress (): 1.5 }]))
1000+ tx = tester .createrawtransaction ([], [{funds .getnewaddress (): 2 }])
1001+ no_change_tx = tester .fundrawtransaction (tx , {"subtractFeeFromOutputs" : [0 ]})
1002+
1003+ overhead_fees = feerate * len (tx ) / 2 / 1000
1004+ cost_of_change = change_tx ["fee" ] - no_change_tx ["fee" ]
1005+ fees = no_change_tx ["fee" ]
1006+ assert_greater_than (fees , 0.01 )
1007+
1008+ def do_fund_send (target ):
1009+ create_tx = tester .createrawtransaction ([], [{funds .getnewaddress (): lower_bound }])
1010+ funded_tx = tester .fundrawtransaction (create_tx )
1011+ signed_tx = tester .signrawtransactionwithwallet (funded_tx ["hex" ])
1012+ assert signed_tx ["complete" ]
1013+ decoded_tx = tester .decoderawtransaction (signed_tx ["hex" ])
1014+ assert_equal (len (decoded_tx ["vin" ]), 3 )
1015+ assert tester .testmempoolaccept ([signed_tx ["hex" ]])[0 ]["allowed" ]
1016+
1017+ # We want to choose more value than is available in 2 inputs when considering the fee,
1018+ # but not enough to need 3 inputs when not considering the fee.
1019+ # So the target value must be at least 2.00000001 - fee.
1020+ lower_bound = Decimal ("2.00000001" ) - fees
1021+ # The target value must be at most 2 - cost_of_change - not_input_fees - min_change (these are all
1022+ # included in the target before ApproximateBestSubset).
1023+ upper_bound = Decimal ("2.0" ) - cost_of_change - overhead_fees - Decimal ("0.01" )
1024+ assert_greater_than_or_equal (upper_bound , lower_bound )
1025+ do_fund_send (lower_bound )
1026+ do_fund_send (upper_bound )
1027+
1028+ self .restart_node (0 )
9721029
9731030if __name__ == '__main__' :
9741031 RawTransactionsTest ().main ()
0 commit comments