diff --git a/src/wallet/rpc/spend.cpp b/src/wallet/rpc/spend.cpp index 27bcbc3b94c..ceedec59266 100644 --- a/src/wallet/rpc/spend.cpp +++ b/src/wallet/rpc/spend.cpp @@ -92,8 +92,21 @@ std::set InterpretSubtractFeeFromOutputInstructions(const UniValue& sffo_in return sffo_set; } -static UniValue FinishTransaction(const std::shared_ptr pwallet, const UniValue& options, const CMutableTransaction& rawTx) +static UniValue FinishTransaction(const std::shared_ptr pwallet, const UniValue& options, CMutableTransaction& rawTx) { + bool can_anti_fee_snipe = !options.exists("locktime"); + + for (const CTxIn& tx_in : rawTx.vin) { + // Checks sequence values consistent with DiscourageFeeSniping + can_anti_fee_snipe = can_anti_fee_snipe && (tx_in.nSequence == CTxIn::MAX_SEQUENCE_NONFINAL || tx_in.nSequence == MAX_BIP125_RBF_SEQUENCE); + } + + if (can_anti_fee_snipe) { + LOCK(pwallet->cs_wallet); + FastRandomContext rng_fast; + DiscourageFeeSniping(rawTx, rng_fast, pwallet->chain(), pwallet->GetLastBlockHash(), pwallet->GetLastBlockHeight()); + } + // Make a blank psbt PartiallySignedTransaction psbtx(rawTx); @@ -1257,7 +1270,7 @@ RPCHelpMan send() }}, }, }, - {"locktime", RPCArg::Type::NUM, RPCArg::Default{0}, "Raw locktime. Non-0 value also locktime-activates inputs"}, + {"locktime", RPCArg::Type::NUM, RPCArg::DefaultHint{"locktime close to block height to prevent fee sniping"}, "Raw locktime. Non-0 value also locktime-activates inputs"}, {"lock_unspents", RPCArg::Type::BOOL, RPCArg::Default{false}, "Lock selected unspent outputs"}, {"psbt", RPCArg::Type::BOOL, RPCArg::DefaultHint{"automatic"}, "Always return a PSBT, implies add_to_wallet=false."}, {"subtract_fee_from_outputs", RPCArg::Type::ARR, RPCArg::Default{UniValue::VARR}, "Outputs to subtract the fee from, specified as integer indices.\n" @@ -1326,7 +1339,8 @@ RPCHelpMan send() rawTx.vout.clear(); auto txr = FundTransaction(*pwallet, rawTx, recipients, options, coin_control, /*override_min_fee=*/false); - return FinishTransaction(pwallet, options, CMutableTransaction(*txr.tx)); + CMutableTransaction tx = CMutableTransaction(*txr.tx); + return FinishTransaction(pwallet, options, tx); } }; } @@ -1374,7 +1388,7 @@ RPCHelpMan sendall() }, }, }, - {"locktime", RPCArg::Type::NUM, RPCArg::Default{0}, "Raw locktime. Non-0 value also locktime-activates inputs"}, + {"locktime", RPCArg::Type::NUM, RPCArg::DefaultHint{"locktime close to block height to prevent fee sniping"}, "Raw locktime. Non-0 value also locktime-activates inputs"}, {"lock_unspents", RPCArg::Type::BOOL, RPCArg::Default{false}, "Lock selected unspent outputs"}, {"psbt", RPCArg::Type::BOOL, RPCArg::DefaultHint{"automatic"}, "Always return a PSBT, implies add_to_wallet=false."}, {"send_max", RPCArg::Type::BOOL, RPCArg::Default{false}, "When true, only use UTXOs that can pay for their own fees to maximize the output amount. When 'false' (default), no UTXO is left behind. send_max is incompatible with providing specific inputs."}, diff --git a/src/wallet/spend.cpp b/src/wallet/spend.cpp index d96946e7b89..ed89b4c3705 100644 --- a/src/wallet/spend.cpp +++ b/src/wallet/spend.cpp @@ -936,11 +936,7 @@ static bool IsCurrentForAntiFeeSniping(interfaces::Chain& chain, const uint256& return true; } -/** - * Set a height-based locktime for new transactions (uses the height of the - * current chain tip unless we are not synced with the current chain - */ -static void DiscourageFeeSniping(CMutableTransaction& tx, FastRandomContext& rng_fast, +void DiscourageFeeSniping(CMutableTransaction& tx, FastRandomContext& rng_fast, interfaces::Chain& chain, const uint256& block_hash, int block_height) { // All inputs must be added by now diff --git a/src/wallet/spend.h b/src/wallet/spend.h index bdbfd750954..c8e7737e1f5 100644 --- a/src/wallet/spend.h +++ b/src/wallet/spend.h @@ -218,6 +218,12 @@ struct CreatedTransactionResult : tx(_tx), fee(_fee), fee_calc(_fee_calc), change_pos(_change_pos) {} }; +/** + * Set a height-based locktime for new transactions (uses the height of the + * current chain tip unless we are not synced with the current chain + */ +void DiscourageFeeSniping(CMutableTransaction& tx, FastRandomContext& rng_fast, interfaces::Chain& chain, const uint256& block_hash, int block_height); + /** * Create a new transaction paying the recipients with a set of coins * selected by SelectCoins(); Also create the change output, when needed diff --git a/test/functional/wallet_rescan_unconfirmed.py b/test/functional/wallet_rescan_unconfirmed.py index 77cf8befd29..bb863bb7449 100755 --- a/test/functional/wallet_rescan_unconfirmed.py +++ b/test/functional/wallet_rescan_unconfirmed.py @@ -53,7 +53,7 @@ class WalletRescanUnconfirmed(BitcoinTestFramework): # The only UTXO available to spend is tx_parent_to_reorg. assert_equal(len(w0_utxos), 1) assert_equal(w0_utxos[0]["txid"], tx_parent_to_reorg["txid"]) - tx_child_unconfirmed_sweep = w0.sendall([ADDRESS_BCRT1_UNSPENDABLE]) + tx_child_unconfirmed_sweep = w0.sendall(recipients=[ADDRESS_BCRT1_UNSPENDABLE], options={"locktime":0}) assert tx_child_unconfirmed_sweep["txid"] in node.getrawmempool() node.syncwithvalidationinterfacequeue()