Skip to content

Commit 2cef200

Browse files
committed
Merge bitcoin/bitcoin#28944: wallet, rpc: add anti-fee-sniping to send and sendall
aac0b6d test: test sendall and send do anti-fee-sniping (ishaanam) 20802c7 wallet, rpc: add anti-fee-sniping to `send` and `sendall` (ishaanam) Pull request description: Currently, `send` and `sendall` don't do anti-fee-sniping because they don't use `CreateTransaction`. This PR adds anti-fee-sniping to these RPCs by calling `DiscourageFeeSniping` from `FinishTransaction` when the user does not specify a locktime. ACKs for top commit: achow101: ACK aac0b6d murchandamus: ACK aac0b6d glozow: ACK aac0b6d Tree-SHA512: d4f1b43b5bda489bdba46b0af60e50bff0de604a35670e6ea6e1de2b539f16b3f68805492f51d6d2078d421b63432ca22a561a5721d1a37686f2e48284e1e646
2 parents 932e993 + aac0b6d commit 2cef200

File tree

6 files changed

+55
-10
lines changed

6 files changed

+55
-10
lines changed

src/wallet/rpc/spend.cpp

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,21 @@ std::set<int> InterpretSubtractFeeFromOutputInstructions(const UniValue& sffo_in
9292
return sffo_set;
9393
}
9494

95-
static UniValue FinishTransaction(const std::shared_ptr<CWallet> pwallet, const UniValue& options, const CMutableTransaction& rawTx)
95+
static UniValue FinishTransaction(const std::shared_ptr<CWallet> pwallet, const UniValue& options, CMutableTransaction& rawTx)
9696
{
97+
bool can_anti_fee_snipe = !options.exists("locktime");
98+
99+
for (const CTxIn& tx_in : rawTx.vin) {
100+
// Checks sequence values consistent with DiscourageFeeSniping
101+
can_anti_fee_snipe = can_anti_fee_snipe && (tx_in.nSequence == CTxIn::MAX_SEQUENCE_NONFINAL || tx_in.nSequence == MAX_BIP125_RBF_SEQUENCE);
102+
}
103+
104+
if (can_anti_fee_snipe) {
105+
LOCK(pwallet->cs_wallet);
106+
FastRandomContext rng_fast;
107+
DiscourageFeeSniping(rawTx, rng_fast, pwallet->chain(), pwallet->GetLastBlockHash(), pwallet->GetLastBlockHeight());
108+
}
109+
97110
// Make a blank psbt
98111
PartiallySignedTransaction psbtx(rawTx);
99112

@@ -1240,7 +1253,7 @@ RPCHelpMan send()
12401253
}},
12411254
},
12421255
},
1243-
{"locktime", RPCArg::Type::NUM, RPCArg::Default{0}, "Raw locktime. Non-0 value also locktime-activates inputs"},
1256+
{"locktime", RPCArg::Type::NUM, RPCArg::DefaultHint{"locktime close to block height to prevent fee sniping"}, "Raw locktime. Non-0 value also locktime-activates inputs"},
12441257
{"lock_unspents", RPCArg::Type::BOOL, RPCArg::Default{false}, "Lock selected unspent outputs"},
12451258
{"psbt", RPCArg::Type::BOOL, RPCArg::DefaultHint{"automatic"}, "Always return a PSBT, implies add_to_wallet=false."},
12461259
{"subtract_fee_from_outputs", RPCArg::Type::ARR, RPCArg::Default{UniValue::VARR}, "Outputs to subtract the fee from, specified as integer indices.\n"
@@ -1309,7 +1322,8 @@ RPCHelpMan send()
13091322
rawTx.vout.clear();
13101323
auto txr = FundTransaction(*pwallet, rawTx, recipients, options, coin_control, /*override_min_fee=*/false);
13111324

1312-
return FinishTransaction(pwallet, options, CMutableTransaction(*txr.tx));
1325+
CMutableTransaction tx = CMutableTransaction(*txr.tx);
1326+
return FinishTransaction(pwallet, options, tx);
13131327
}
13141328
};
13151329
}
@@ -1355,7 +1369,7 @@ RPCHelpMan sendall()
13551369
},
13561370
},
13571371
},
1358-
{"locktime", RPCArg::Type::NUM, RPCArg::Default{0}, "Raw locktime. Non-0 value also locktime-activates inputs"},
1372+
{"locktime", RPCArg::Type::NUM, RPCArg::DefaultHint{"locktime close to block height to prevent fee sniping"}, "Raw locktime. Non-0 value also locktime-activates inputs"},
13591373
{"lock_unspents", RPCArg::Type::BOOL, RPCArg::Default{false}, "Lock selected unspent outputs"},
13601374
{"psbt", RPCArg::Type::BOOL, RPCArg::DefaultHint{"automatic"}, "Always return a PSBT, implies add_to_wallet=false."},
13611375
{"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."},

src/wallet/spend.cpp

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -941,11 +941,7 @@ static bool IsCurrentForAntiFeeSniping(interfaces::Chain& chain, const uint256&
941941
return true;
942942
}
943943

944-
/**
945-
* Set a height-based locktime for new transactions (uses the height of the
946-
* current chain tip unless we are not synced with the current chain
947-
*/
948-
static void DiscourageFeeSniping(CMutableTransaction& tx, FastRandomContext& rng_fast,
944+
void DiscourageFeeSniping(CMutableTransaction& tx, FastRandomContext& rng_fast,
949945
interfaces::Chain& chain, const uint256& block_hash, int block_height)
950946
{
951947
// All inputs must be added by now

src/wallet/spend.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,12 @@ struct CreatedTransactionResult
218218
: tx(_tx), fee(_fee), fee_calc(_fee_calc), change_pos(_change_pos) {}
219219
};
220220

221+
/**
222+
* Set a height-based locktime for new transactions (uses the height of the
223+
* current chain tip unless we are not synced with the current chain
224+
*/
225+
void DiscourageFeeSniping(CMutableTransaction& tx, FastRandomContext& rng_fast, interfaces::Chain& chain, const uint256& block_hash, int block_height);
226+
221227
/**
222228
* Create a new transaction paying the recipients with a set of coins
223229
* selected by SelectCoins(); Also create the change output, when needed

test/functional/wallet_rescan_unconfirmed.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def run_test(self):
5353
# The only UTXO available to spend is tx_parent_to_reorg.
5454
assert_equal(len(w0_utxos), 1)
5555
assert_equal(w0_utxos[0]["txid"], tx_parent_to_reorg["txid"])
56-
tx_child_unconfirmed_sweep = w0.sendall([ADDRESS_BCRT1_UNSPENDABLE])
56+
tx_child_unconfirmed_sweep = w0.sendall(recipients=[ADDRESS_BCRT1_UNSPENDABLE], options={"locktime":0})
5757
assert tx_child_unconfirmed_sweep["txid"] in node.getrawmempool()
5858
node.syncwithvalidationinterfacequeue()
5959

test/functional/wallet_send.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,12 @@ def test_send(self, from_wallet, to_wallet=None, amount=None, data=None,
144144
return
145145

146146
if locktime:
147+
assert_equal(from_wallet.gettransaction(txid=res["txid"], verbose=True)["decoded"]["locktime"], locktime)
147148
return res
149+
else:
150+
if add_to_wallet:
151+
decoded_tx = from_wallet.gettransaction(txid=res["txid"], verbose=True)["decoded"]
152+
assert_greater_than(decoded_tx["locktime"], from_wallet.getblockcount() - 100)
148153

149154
if expect_sign:
150155
assert_equal(res["complete"], True)

test/functional/wallet_sendall.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from decimal import Decimal, getcontext
88

9+
from test_framework.messages import SEQUENCE_FINAL
910
from test_framework.test_framework import BitcoinTestFramework
1011
from test_framework.util import (
1112
assert_equal,
@@ -430,6 +431,26 @@ def sendall_does_ancestor_aware_funding(self):
430431

431432
assert_greater_than(higher_parent_feerate_amount, lower_parent_feerate_amount)
432433

434+
@cleanup
435+
def sendall_anti_fee_sniping(self):
436+
self.log.info("Testing sendall does anti-fee-sniping when locktime is not specified")
437+
self.add_utxos([10,11])
438+
tx_from_wallet = self.test_sendall_success(sendall_args = [self.remainder_target])
439+
440+
assert_greater_than(tx_from_wallet["decoded"]["locktime"], tx_from_wallet["blockheight"] - 100)
441+
442+
self.log.info("Testing sendall does not do anti-fee-sniping when locktime is specified")
443+
self.add_utxos([10,11])
444+
txid = self.wallet.sendall(recipients=[self.remainder_target], options={"locktime":0})["txid"]
445+
assert_equal(self.wallet.gettransaction(txid=txid, verbose=True)["decoded"]["locktime"], 0)
446+
447+
self.log.info("Testing sendall does not do anti-fee-sniping when even one of the sequences is final")
448+
self.add_utxos([10, 11])
449+
utxos = self.wallet.listunspent()
450+
utxos[0]["sequence"] = SEQUENCE_FINAL
451+
txid = self.wallet.sendall(recipients=[self.remainder_target], inputs=utxos)["txid"]
452+
assert_equal(self.wallet.gettransaction(txid=txid, verbose=True)["decoded"]["locktime"], 0)
453+
433454
# This tests needs to be the last one otherwise @cleanup will fail with "Transaction too large" error
434455
def sendall_fails_with_transaction_too_large(self):
435456
self.log.info("Test that sendall fails if resulting transaction is too large")
@@ -511,6 +532,9 @@ def run_test(self):
511532
# Sendall only uses outputs with less than a given number of confirmation when using minconf
512533
self.sendall_with_maxconf()
513534

535+
# Sendall discourages fee-sniping when a locktime is not specified
536+
self.sendall_anti_fee_sniping()
537+
514538
# Sendall spends unconfirmed change
515539
self.sendall_spends_unconfirmed_change()
516540

0 commit comments

Comments
 (0)