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.
send
and sendall
#28944
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.
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.
For detailed information about the code coverage, see the test coverage report.
See the guideline for information on the review process.
Type | Reviewers |
---|---|
Concept ACK | S3RK |
Stale ACK | achow101, murchandamus |
If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update.
Reviewers, this pull request conflicts with the following ones:
If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.
ACK 5c9ac1456b7b4512845270cd850e5c33b643f575
A test for send
would be nice as well.
A test for send would be nice as well.
Done
141@@ -142,7 +142,12 @@ def test_send(self, from_wallet, to_wallet=None, amount=None, data=None,
142 return
143
144 if locktime:
145+ assert_equal(from_wallet.decoderawtransaction(from_wallet.gettransaction(res["txid"])["hex"])["locktime"], locktime)
In 5101f054ce435ac653a3f5e780fca066e2f25088 “test: test sendall and send do anti-fee-sniping”
nit: gettransaction
has a verbose
argument that does the decoding so this doesn’t need to call decoderawtransaction
. Same below.
387+
388+ assert_greater_than(tx_from_wallet["decoded"]["locktime"], tx_from_wallet["blockheight"] - 100)
389+
390+ self.add_utxos([10,11])
391+ txid = self.wallet.sendall(recipients=[self.remainder_target], options={"locktime":0})["txid"]
392+ tx_from_wallet = self.wallet.decoderawtransaction(self.wallet.getrawtransaction(txid))
In 5101f054ce435ac653a3f5e780fca066e2f25088 “test: test sendall and send do anti-fee-sniping”
nit: getrawtransaction
has a verbosity
option that will do the decoding, so no need to call decoderawtransaction
here.
141@@ -142,7 +142,12 @@ def test_send(self, from_wallet, to_wallet=None, amount=None, data=None,
142 return
143
144 if locktime:
145+ assert_equal(from_wallet.decoderawtransaction(from_wallet.gettransaction(res["txid"])["hex"])["locktime"], locktime)
146 return res
147+ else:
148+ if add_to_wallet:
149+ decoded_tx = from_wallet.decoderawtransaction(from_wallet.gettransaction(res["txid"])["hex"])
150+ assert_greater_than(decoded_tx["locktime"], 100)
In 5101f054ce435ac653a3f5e780fca066e2f25088 “test: test sendall and send do anti-fee-sniping”
Shouldn’t this check for getblockcount() - 100
?
212@@ -213,6 +213,7 @@ struct CreatedTransactionResult
213 : tx(_tx), fee(_fee), fee_calc(_fee_calc), change_pos(_change_pos) {}
214 };
215
216+void DiscourageFeeSniping(CMutableTransaction& tx, FastRandomContext& rng_fast, interfaces::Chain& chain, const uint256& block_hash, int block_height);
141@@ -142,7 +142,12 @@ def test_send(self, from_wallet, to_wallet=None, amount=None, data=None,
142 return
143
144 if locktime:
145+ assert_equal(from_wallet.gettransaction(txid=res["txid"], verbose=True)["decoded"]["locktime"], locktime)
146 return res
147+ else:
148+ if add_to_wallet:
149+ decoded_tx = from_wallet.decoderawtransaction(from_wallet.gettransaction(res["txid"])["hex"])
In f348eadadd6099e7763f04eb3d42cfd8bba33146 “test: test sendall and send do anti-fee-sniping”
nit: verbose
here too.
Concept ACK and almost crACK d76f88051524971def5137a07ce2940dce1c33a9 except that there seems to be a type mix-up:
0 wallet/rpc/spend.cpp:1299:20: error: no matching function for call to 'FinishTransaction'
1 return FinishTransaction(pwallet, options, CMutableTransaction(*txr.tx));
2 ^~~~~~~~~~~~~~~~~
3wallet/rpc/spend.cpp:79:17: note: candidate function not viable: expects an lvalue for 3rd argument
4static UniValue FinishTransaction(const std::shared_ptr<CWallet> pwallet, const UniValue& options, CMutableTransaction& rawTx)
5 ^
61 error generated.
0--- a/test/functional/wallet_import_rescan.py
1+++ b/test/functional/wallet_import_rescan.py
2@@ -293,6 +293,7 @@ class ImportRescanTest(BitcoinTestFramework):
3 add_to_wallet=False,
4 inputs=[unspent_txid_map[variant.initial_txid]],
5 outputs=[{ADDRESS_BCRT1_UNSPENDABLE : variant.initial_amount}],
6+ locktime=0,
7 subtract_fee_from_outputs=[0]
8 )
9 variant.child_txid = child["txid"]
93 {
94+ bool can_anti_fee_snipe = !options.exists("locktime");
95+
96+ for (const CTxIn& tx_in : rawTx.vin) {
97+ // Can not be FINAL for locktime to work
98+ can_anti_fee_snipe &= tx_in.nSequence != CTxIn::SEQUENCE_FINAL;
In 8e5778a9fb6fbcbddd4ba87cc3f3caf41118592a (wallet, rpc: add anti-fee-sniping to send
and sendall
):
It seems to me that the nSequence
check here is too restrictive. Locktime is disabled when all sequences are set to final, but it seems to me that can_anti_fee_snipe
would be set to false
here, even if only one input’s sequence is set to final.
I’m not sure this would matter much in practice because most transactions would probably use the same sequence value for all inputs, but can_anti_fee_snipe
should probably end up true
if at least one nSequence
is non-final?
Could you perhaps add a test that a transaction with final nSequence
values does not get its locktime
set, but one with mixed values and one with all non-final nSequence
values both do?
It seems to me that the
nSequence
check here is too restrictive. Locktime is disabled when all sequences are set to final, but it seems to me thatcan_anti_fee_snipe
would be set tofalse
here, even if only one input’s sequence is set to final.
Currently DiscourageFeeSniping
will assert if any sequence is final, so this check is appropriate for now. It also enforces that the sequences are either MAX_SEQUENCE_NONFINAL
or MAX_BIP125_RBF_SEQUENCE
, so this needs to be checking that as well otherwise an assertion will be hit. Or a commit could be added to relax those checks.
Could you perhaps add a test that a transaction with final nSequence values does not get its locktime set, but one with mixed values and one with all non-final nSequence values both do?
I would also like to see tests for other sequence numbers as there is a risk of reaching an assertion here.
It also enforces that the sequences are either MAX_SEQUENCE_NONFINAL or MAX_BIP125_RBF_SEQUENCE, so this needs to be checking that as well otherwise an assertion will be hit. Or a commit could be added to relax those checks.
It seems like this part of the comment wasn’t addressed
It also enforces that the sequences are either MAX_SEQUENCE_NONFINAL or MAX_BIP125_RBF_SEQUENCE, so this needs to be checking that as well otherwise an assertion will be hit. Or a commit could be added to relax those checks
I have added these checks to FinishTransaction
.
Could you perhaps add a test that a transaction with final nSequence values does not get its locktime set, but one with mixed values and one with all non-final nSequence values both do?
I have added a test for this.
0--- a/test/functional/wallet_import_rescan.py 1+++ b/test/functional/wallet_import_rescan.py 2@@ -293,6 +293,7 @@ class ImportRescanTest(BitcoinTestFramework): 3 add_to_wallet=False, 4 inputs=[unspent_txid_map[variant.initial_txid]], 5 outputs=[{ADDRESS_BCRT1_UNSPENDABLE : variant.initial_amount}], 6+ locktime=0, 7 subtract_fee_from_outputs=[0] 8 ) 9 variant.child_txid = child["txid"]
Done
0--- a/test/functional/wallet_import_rescan.py 1+++ b/test/functional/wallet_import_rescan.py 2@@ -293,6 +293,7 @@ class ImportRescanTest(BitcoinTestFramework): 3 add_to_wallet=False, 4 inputs=[unspent_txid_map[variant.initial_txid]], 5 outputs=[{ADDRESS_BCRT1_UNSPENDABLE : variant.initial_amount}], 6+ locktime=0, 7 subtract_fee_from_outputs=[0] 8 ) 9 variant.child_txid = child["txid"]
Done
It seems that you may have been working on an update, but it might not have been pushed. Is that possible?
0--- a/test/functional/wallet_import_rescan.py 1+++ b/test/functional/wallet_import_rescan.py 2@@ -293,6 +293,7 @@ class ImportRescanTest(BitcoinTestFramework): 3 add_to_wallet=False, 4 inputs=[unspent_txid_map[variant.initial_txid]], 5 outputs=[{ADDRESS_BCRT1_UNSPENDABLE : variant.initial_amount}], 6+ locktime=0, 7 subtract_fee_from_outputs=[0] 8 ) 9 variant.child_txid = child["txid"]
Done
It seems that you may have been working on an update, but it might not have been pushed. Is that possible?
I pushed this change, but I had forgotten to comment until later.
97+ // Can not be FINAL for locktime to work
98+ can_anti_fee_snipe &= tx_in.nSequence != CTxIn::SEQUENCE_FINAL;
99+ }
100+
101+ if (can_anti_fee_snipe) {
102+ LOCK(pwallet->cs_wallet);
CWallet::GetLastBlockHash
and CWallet::GetLastBlockHeight
which both require pwallet->cs_wallet
to be locked.