-
Notifications
You must be signed in to change notification settings - Fork 37.7k
wallet: Properly rebroadcast unconfirmed transaction chains #25768
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers. ConflictsReviewers, 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Concept ACK
cb4131c
to
b11ccea
Compare
Concept ACK |
901b864
to
e40ddf3
Compare
Would you explain what is the difference between |
"Loading" as in when the wallet is read from disk and loaded into memory. |
So what is the difference between rebroadcasting when loading and in normal cases? |
There are differences in which transactions will be considered for rebroadcasting. On load, all non-abandoned transactions will be rebroadcast. The periodic rebroadcast will only rebroadcast those that are more than 5 minutes older than the most recent block. |
Is it possible to shuffle transactions before |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Concept + tACK e40ddf3
- Verified that the tests are intermittently failing.
- Failure occurring at debug log assertion
ResendWalletTransactions: resubmit 2 unconfirmed transactions
Below are just one nit comment and a conceptual question.
No. Shuffling is the reason we have this issue in the first place (there is no order in an unsorted map so iterating it is basically the same as iterating a shuffled list). I don't think the insertion order is a privacy leak since it's the same order as when the transactions were originally broadcast which is public information to anyone that's connected to the network. Additionally, this is not a unilateral broadcast - it is up to the mempool relay code to determine when, how, and which transactions are actually rebroadcast. Only transactions that are not already in the mempool will be rebroadcast. |
I was suggesting to shuffle the sorted list before rebroadcast. So we have all the transactions we need but do not relay in that order.
In that case, feel free to ignore my suggestion. |
Concept ACK |
Shuffling after sorting would just undo the sorting. Sorting is not used for filtering; it is used because we need to have the transactions in order. Specifically parent transactions must be rebroadcast before child transactions, and that specific order can be obtained through the insertion order. While we could do something more complicated like actually building packages so that the actually order dependent things are grouped together, and then shuffling the packages, but I don't see the benefit in doing that. That might actually harm privacy because we would be broadcasting transactions that are related to each other at the same time, but only those transactions that are related to our wallet. Since a package that the wallet builds may exclude other transactions in the package that the mempool would know about, we could essentially be telling other nodes some of the scripts that our wallet is watching for as they could then look at what the rebroadcast package has that the rest of the package in the mempool does not. |
e40ddf3
to
85be5fb
Compare
Based on some discussions during the PR review club today, I've made several changes to this PR. The first is that Additionally, Lastly I've updated the test to reliably fail on master. I found that |
85be5fb
to
fee0dea
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ACK fee0dea
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ACK fee0dea
Now the test consistently fails on master, as it should.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ACK 748d5c7
Both of these functions do almost the exact same thing. They can be deduplicated so that their behavior matches except for the filtering aspect. As this function will now always be called on wallet loading, nNextResend will also always be initialized, so wallet_resendwallettransactions.py is updated to account for that. This also resolves a bug where ResendWalletTransactions would fail to rebroadcast txs in insertion order thereby potentially rebroadcasting a child transaction before its parent and causing the child to not actually get rebroadcast. Also names the combined function to ResubmitWalletTransactions as the function just submits the transactions to the mempool rather than doing any sending by itself.
The test checks that parent txs are broadcast before child txs. The previous behavior is that the rebroadcasting would simply iterate mapWallet. As mapWallet is a std::unsorted_map, the child can sometimes come before the parent and thus be rebroadcast in the wrong order and fail the test.
748d5c7
to
3405f3e
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
reACK 3405f3e
utACK 3405f3e |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Late code review ACK 3405f3e
The only orthogonal question that arises from my side is why are we trying to resubmit txes after every import RPC command. Can anything change at the, already created, wallet txes level after an import?
// During reindex, importing and IBD, old wallet transactions become | ||
// unconfirmed. Don't resend them as that would spam other nodes. | ||
if (!chain().isReadyToBroadcast()) return; | ||
// We only allow forcing mempool submission when not relaying to avoid this spam. | ||
if (!force && relay && !chain().isReadyToBroadcast()) return; | ||
|
||
// Do this infrequently and randomly to avoid giving away | ||
// that these are our transactions. | ||
if (GetTime() < nNextResend || !fBroadcastTransactions) return; | ||
bool fFirst = (nNextResend == 0); | ||
if (!force && GetTime() < nNextResend) return; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At this point of the review, this is just a minor nit but code would be a bit clearer in this way:
if (!force) {
// During reindex, importing and IBD, old wallet transactions become
// unconfirmed. Don't resend them as that would spam other nodes.
// We only allow forcing mempool submission when not relaying to avoid this spam.
if (relay && !chain().isReadyToBroadcast()) return;
// Do this infrequently and randomly to avoid giving away
// that these are our transactions.
if (GetTime() < nNextResend) return;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think both these checks would be more appropriate in MaybeResendWalletTxs()
since they're more scheduling-like in nature. It would also address furszy's comment them since we don't need to check for force
anymore (it's always True there). If you do this, you probably also want to duplicate if (!fBroadcastTransactions)
there since it's a very cheap check and we could avoid iterating over the wallets.
Unfortunately, we still need the force
parameter when iterating over the transactions, otherwise ResubmitWalletTransactions()
's signature could have been simplified.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
left some minor nits, which can be addressed the next time this code is touched
evict_time = block_time + 60 * 60 * DEFAULT_MEMPOOL_EXPIRY_HOURS + 5 | ||
node.setmocktime(evict_time) | ||
indep_send = node.send(outputs=[{node.getnewaddress(): 1}], options={"inputs": [indep_utxo]}) | ||
node.syncwithvalidationinterfacequeue() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you explain why this is needed? Generally this is only needed when forcing the wallet to receive all in-flight mempool notifications. However, there is no wallet call that follows. Also, there is no m_best_block_time
update.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, there is no
m_best_block_time
update.
Why not? IIRC this was done in order to update m_best_block_time
so that the transaction would be properly evicted later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The transaction is evicted from the mempool, not the wallet. (Only the wallet has an m_best_block_time
)
Also, there is no new best block, so m_best_block_time
won't be updated either way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, wrong line. I think this syncwithvalidationinterfacequeue
is probably unneeded.
block = create_block(int(node.getbestblockhash(), 16), create_coinbase(node.getblockcount() + 1), block_time) | ||
block.solve() | ||
node.submitblock(block.serialize().hex()) | ||
node.syncwithvalidationinterfacequeue() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you explain why this is needed? Maybe duplicate the comment "Set correct m_best_block_time" from above?
// The `force` option results in all unconfirmed transactions being submitted to | ||
// the mempool. This does not necessarily result in those transactions being relayed, | ||
// that depends on the `relay` option. Periodic rebroadcast uses the pattern | ||
// relay=true force=false (also the default values), while loading into the mempool |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// relay=true force=false (also the default values), while loading into the mempool | |
// relay=true force=false, while loading into the mempool |
// Only rebroadcast unconfirmed txs | ||
if (!wtx.isUnconfirmed()) continue; | ||
|
||
// attempt to rebroadcast all txes more than 5 minutes older than |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// attempt to rebroadcast all txes more than 5 minutes older than | |
// Attempt to rebroadcast all txes more than 5 minutes older than |
Any reason that this A
changed into a
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ACK 3405f3e
// Resubmit transactions from the wallet to the mempool, optionally asking the | ||
// mempool to relay them. On startup, we will do this for all unconfirmed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: perhaps this first sentence should live in wallet.h
?
// During reindex, importing and IBD, old wallet transactions become | ||
// unconfirmed. Don't resend them as that would spam other nodes. | ||
if (!chain().isReadyToBroadcast()) return; | ||
// We only allow forcing mempool submission when not relaying to avoid this spam. | ||
if (!force && relay && !chain().isReadyToBroadcast()) return; | ||
|
||
// Do this infrequently and randomly to avoid giving away | ||
// that these are our transactions. | ||
if (GetTime() < nNextResend || !fBroadcastTransactions) return; | ||
bool fFirst = (nNextResend == 0); | ||
if (!force && GetTime() < nNextResend) return; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think both these checks would be more appropriate in MaybeResendWalletTxs()
since they're more scheduling-like in nature. It would also address furszy's comment them since we don't need to check for force
anymore (it's always True there). If you do this, you probably also want to duplicate if (!fBroadcastTransactions)
there since it's a very cheap check and we could avoid iterating over the wallets.
Unfortunately, we still need the force
parameter when iterating over the transactions, otherwise ResubmitWalletTransactions()
's signature could have been simplified.
See bitcoin#25768 (comment) Also fix doc typo from bitcoin#25768 (comment)
See bitcoin#25768 (comment) Also fix doc typo from bitcoin#25768 (comment)
@@ -295,7 +295,7 @@ RPCHelpMan importaddress() | |||
RescanWallet(*pwallet, reserver); | |||
{ | |||
LOCK(pwallet->cs_wallet); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The locks are no longer needed and can be removed
If a user calls |
I addressed that in #26205 |
b01682a refactor: revert m_next_resend to not be std::atomic (stickies-v) 9245f45 wallet: only update m_next_resend when actually resending (stickies-v) 7fbde8a refactor: carve out tx resend timer logic into ShouldResend (stickies-v) 01f3534 refactor: remove unused locks for ResubmitWalletTransactions (stickies-v) c6e8e11 wallet: fix capitalization in docstring (stickies-v) Pull request description: This PR addresses the outstanding comments/issues from #25768: - capitalization [typo](bitcoin/bitcoin#25768 (comment)) in docstring - remove [unused locks](bitcoin/bitcoin@01f3534) that we previously needed for `ReacceptWalletTransactions()` - before #25768, only `ResendWalletTransactions()` would reset `m_next_resend` (formerly called `nNextResend`). By unifying it with `ReacceptWalletTransactions()` into `ResubmitWalletTransactions()`, the number of callsites that would reset the `m_next_resend` timer increased - since `m_next_resend` is only used in case of `relay=true` (formerly `ResendWalletTransactions()`), this is unintuitive - it leads to [unexpected behaviour](bitcoin/bitcoin#25768 (comment)) such as transactions potentially never being rebroadcasted. - it makes the ResubmitWalletTransactions()` logic [more complicated than strictly necessary](bitcoin/bitcoin#25768 (comment)) - since #25768, we relied on an earlier call of `ResubmitWalletTransactions(relay=false, force=true)` to initialize `m_next_resend()`, I think we can more elegantly do that by just providing `m_next_resend` with a default value - just to highlight: this commit introduces behaviour change Note: the `if (!fBroadcastTransactions)` in `CWallet:ShouldResend()` is duplicated on purpose, since it potentially avoids the slightly more expensive `if (!chain().isReadyToBroadcast())` check afterwards. I don't have a strong view on it, so happy to remove that additional check to reduce the diff, too. ACKs for top commit: aureleoules: ACK b01682a achow101: ACK b01682a Tree-SHA512: ac5f1d8858f8dd736dd1480f385984d660c1916b62a42562317020e8f9fd6a30bd8f23d973d47e4c9480d744c5ba39fdbefd69568a5eb0589a8422d7e5971c1c
b01682a refactor: revert m_next_resend to not be std::atomic (stickies-v) 9245f45 wallet: only update m_next_resend when actually resending (stickies-v) 7fbde8a refactor: carve out tx resend timer logic into ShouldResend (stickies-v) 01f3534 refactor: remove unused locks for ResubmitWalletTransactions (stickies-v) c6e8e11 wallet: fix capitalization in docstring (stickies-v) Pull request description: This PR addresses the outstanding comments/issues from bitcoin#25768: - capitalization [typo](bitcoin#25768 (comment)) in docstring - remove [unused locks](bitcoin@01f3534) that we previously needed for `ReacceptWalletTransactions()` - before bitcoin#25768, only `ResendWalletTransactions()` would reset `m_next_resend` (formerly called `nNextResend`). By unifying it with `ReacceptWalletTransactions()` into `ResubmitWalletTransactions()`, the number of callsites that would reset the `m_next_resend` timer increased - since `m_next_resend` is only used in case of `relay=true` (formerly `ResendWalletTransactions()`), this is unintuitive - it leads to [unexpected behaviour](bitcoin#25768 (comment)) such as transactions potentially never being rebroadcasted. - it makes the ResubmitWalletTransactions()` logic [more complicated than strictly necessary](bitcoin#25768 (comment)) - since bitcoin#25768, we relied on an earlier call of `ResubmitWalletTransactions(relay=false, force=true)` to initialize `m_next_resend()`, I think we can more elegantly do that by just providing `m_next_resend` with a default value - just to highlight: this commit introduces behaviour change Note: the `if (!fBroadcastTransactions)` in `CWallet:ShouldResend()` is duplicated on purpose, since it potentially avoids the slightly more expensive `if (!chain().isReadyToBroadcast())` check afterwards. I don't have a strong view on it, so happy to remove that additional check to reduce the diff, too. ACKs for top commit: aureleoules: ACK b01682a achow101: ACK b01682a Tree-SHA512: ac5f1d8858f8dd736dd1480f385984d660c1916b62a42562317020e8f9fd6a30bd8f23d973d47e4c9480d744c5ba39fdbefd69568a5eb0589a8422d7e5971c1c
Currently
ResendWalletTransactions
(used for normal rebroadcasts) will attempt to rebroadcast all of the transactions in the wallet in the order they are stored inmapWallet
. This ends up being random asmapWallet
is astd::unordered_map
. HoweverReacceptWalletTransactions
(used for adding to the mempool on loading) first sorts the txs by wallet insertion order, then submits them. The result is thatResendWalletTranactions
will fail to rebroadcast child transactions if their txids happen to be lexicographically less than their parent's txid. This PR resolves this issue by combiningReacceptWalletTransactions
andResendWalletTransactions
into a newResubmitWalletTransactions
so that the iteration code and basic checks are shared.A test has also been added that checks that such transaction chains are rebroadcast correctly.