@@ -99,6 +99,7 @@ def run_test(self):
9999 self .test_option_subtract_fee_from_outputs ()
100100 self .test_subtract_fee_with_presets ()
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."""
@@ -932,6 +933,60 @@ def test_include_unsafe(self):
932933 signedtx = wallet .signrawtransactionwithwallet (fundedtx ['hex' ])
933934 wallet .sendrawtransaction (signedtx ['hex' ])
934935
936+ def test_22670 (self ):
937+ # In issue #22670, it was observed that ApproximateBestSubset may
938+ # choose enough value to cover the target amount but not enough to cover the transaction fees.
939+ # This leads to a transaction whose actual transaction feerate is lower than expected.
940+ # However at normal feerates, the difference between the effective value and the real value
941+ # that this bug is not detected because the transaction fee must be at least 0.01 BTC (the minimum change value).
942+ # Otherwise the targeted minimum change value will be enough to cover the transaction fees that were not
943+ # being accounted for. So the minimum relay fee is set to 0.1 BTC/kvB in this test.
944+ self .log .info ("Test issue 22670 ApproximateBestSubset bug" )
945+ # Make sure the default wallet will not be loaded when restarted with a high minrelaytxfee
946+ self .nodes [0 ].unloadwallet (self .default_wallet_name , False )
947+ feerate = Decimal ("0.1" )
948+ self .restart_node (0 , [f"-minrelaytxfee={ feerate } " , "-discardfee=0" ]) # Set high minrelayfee, set discardfee to 0 for easier calculation
949+
950+ self .nodes [0 ].loadwallet (self .default_wallet_name , True )
951+ funds = self .nodes [0 ].get_wallet_rpc (self .default_wallet_name )
952+ self .nodes [0 ].createwallet (wallet_name = "tester" )
953+ tester = self .nodes [0 ].get_wallet_rpc ("tester" )
954+
955+ # Because this test is specifically for ApproximateBestSubset, the target value must be greater
956+ # than any single input available, and require more than 1 input. So we make 3 outputs
957+ for i in range (0 , 3 ):
958+ funds .sendtoaddress (tester .getnewaddress (), 1 )
959+ self .generate (self .nodes [0 ], 1 , sync_fun = self .no_op )
960+
961+ # Create transactions in order to calculate fees for the target bounds that can trigger this bug
962+ change_tx = tester .fundrawtransaction (tester .createrawtransaction ([], [{funds .getnewaddress (): 1.5 }]))
963+ tx = tester .createrawtransaction ([], [{funds .getnewaddress (): 2 }])
964+ no_change_tx = tester .fundrawtransaction (tx , {"subtractFeeFromOutputs" : [0 ]})
965+
966+ overhead_fees = feerate * len (tx ) / 2 / 1000
967+ cost_of_change = change_tx ["fee" ] - no_change_tx ["fee" ]
968+ fees = no_change_tx ["fee" ]
969+ assert_greater_than (fees , 0.01 )
970+
971+ def do_fund_send (target ):
972+ create_tx = tester .createrawtransaction ([], [{funds .getnewaddress (): lower_bound }])
973+ funded_tx = tester .fundrawtransaction (create_tx )
974+ signed_tx = tester .signrawtransactionwithwallet (funded_tx ["hex" ])
975+ assert signed_tx ["complete" ]
976+ decoded_tx = tester .decoderawtransaction (signed_tx ["hex" ])
977+ assert_equal (len (decoded_tx ["vin" ]), 3 )
978+ assert tester .testmempoolaccept ([signed_tx ["hex" ]])[0 ]["allowed" ]
979+
980+ # We want to choose more value than is available in 2 inputs when considering the fee,
981+ # but not enough to need 3 inputs when not considering the fee.
982+ # So the target value must be at least 2.00000001 - fee.
983+ lower_bound = Decimal ("2.00000001" ) - fees
984+ # The target value must be at most 2 - cost_of_change - not_input_fees - min_change (these are all
985+ # included in the target before ApproximateBestSubset).
986+ upper_bound = Decimal ("2.0" ) - cost_of_change - overhead_fees - Decimal ("0.01" )
987+ assert_greater_than_or_equal (upper_bound , lower_bound )
988+ do_fund_send (lower_bound )
989+ do_fund_send (upper_bound )
935990
936991if __name__ == '__main__' :
937992 RawTransactionsTest ().main ()
0 commit comments