Eunovo
commented at 9:23 am on March 20, 2024:
none
This PR implements a fix for the issue described in #29435.
The problem is that the wallet is unable to abandon transactions that have unrelated parent conflicts. The solution implemented here, augments the mempool transaction REPLACED signal with the double-spending transaction which the wallet stores and watches for in Block notifications. A map is added to the wallet to track conflicting tx ids and their child transactions. The entry is erased when the double-spending tx is removed from MemPool.
DrahtBot
commented at 9:23 am on March 20, 2024:
contributor
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.
If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update.
Conflicts
No conflicts as of last run.
DrahtBot added the label
Wallet
on Mar 20, 2024
DrahtBot added the label
CI failed
on Mar 20, 2024
DrahtBot
commented at 10:20 am on March 20, 2024:
contributor
🚧 At least one of the CI tasks failed. Make sure to run all tests locally, according to the
documentation.
Possibly this is due to a silent merge conflict (the changes in this pull request being
incompatible with the current code in the target branch). If so, make sure to rebase on the latest
commit of the target branch.
Leave a comment here, if you need help tracking down a confusing failure.
DrahtBot added the label
Needs rebase
on Mar 27, 2024
achow101
commented at 5:50 pm on March 27, 2024:
member
I’m actually wondering now if it would be sufficient to just have the MemPoolRejectReason include the replacement txid for replacements, and the conflicting block hash for conflicts. If our tx is in the mempool, we should get these notifications when it is removed which I think would be enough without having to be looking for the parents or looking to see if we have any descendants?
Eunovo
commented at 6:24 am on March 28, 2024:
none
I’m actually wondering now if it would be sufficient to just have the MemPoolRejectReason include the replacement txid for replacements, and the conflicting block hash for conflicts. If our tx is in the mempool, we should get these notifications when it is removed which I think would be enough without having to be looking for the parents or looking to see if we have any descendants?
@achow101 I think this makes sense. Once we add conflicting block hash for conflicts then we can safely mark wallet tx as conflicted which should solve the issue. What would we still need the replacement txid for?
EDIT
On second thought, I have realized that the wallet might not get the Conflict MemPoolRemovalReason for its tx. The wallet tx may be kicked out of the Mempool early due to replacement so when CtxMemPool::removeForBlock is called, the wallet tx will no longer be in the Mempool. Therefore, the issue is not completely solved by adding conflicting block hash to Conflict MemPoolRemovalReason, But adding it is still useful for handling cases where a new block is received that conflicts with wallet tx.
It appears to also be watching for replacements of those replacements too, and I think that is unnecessary.
Eunovo
commented at 7:44 pm on March 28, 2024:
none
It appears to also be watching for replacements of those replacements too, and I think that is unnecessary.
Thanks @achow101. Yes, I added that when I realized that if the replacement is also replaced then the check in the BlockConnected callback will fail to mark the wallet tx as conflicted. Unless for some reason, the replacement cannot be replaced?
Eunovo force-pushed
on Apr 9, 2024
Eunovo
commented at 4:52 pm on April 9, 2024:
none
It appears to also be watching for replacements of those replacements too, and I think that is unnecessary.
@achow101 I took this out because the new replacement is not guaranteed to conflict with the original wallet transaction
I had to use RecursiveUpdateTxState directly in https://github.com/bitcoin/bitcoin/pull/29680/commits/8ee9629b51eed250f7b5a8a6644b40ca725f3990 because CWallet::MarkConflicted checks that the conflicting block height is not more than the m_last_block_processed by the wallet but transactionRemovedFromMempool is triggered before blockConnected so the wallet hasn’t had a chance to process the block causing the conflict notification. I had to force the wallet txs update. I wonder what the repercussions for doing this are.
DrahtBot removed the label
Needs rebase
on Apr 9, 2024
Eunovo force-pushed
on Apr 9, 2024
DrahtBot added the label
CI failed
on Apr 9, 2024
DrahtBot
commented at 6:43 pm on April 9, 2024:
contributor
🚧 At least one of the CI tasks failed. Make sure to run all tests locally, according to the
documentation.
Possibly this is due to a silent merge conflict (the changes in this pull request being
incompatible with the current code in the target branch). If so, make sure to rebase on the latest
commit of the target branch.
Leave a comment here, if you need help tracking down a confusing failure.
DrahtBot removed the label
CI failed
on Apr 10, 2024
Eunovo marked this as ready for review
on Apr 10, 2024
Eunovo
commented at 3:43 am on April 21, 2024:
none
Putting in draft while I fix falling test
Eunovo marked this as a draft
on Apr 21, 2024
Eunovo force-pushed
on Apr 22, 2024
DrahtBot added the label
CI failed
on Apr 22, 2024
Eunovo force-pushed
on Apr 23, 2024
DrahtBot removed the label
CI failed
on Apr 23, 2024
Eunovo marked this as ready for review
on Apr 23, 2024
DrahtBot added the label
CI failed
on May 4, 2024
Eunovo force-pushed
on May 7, 2024
Eunovo force-pushed
on May 9, 2024
DrahtBot removed the label
CI failed
on May 11, 2024
josibake
commented at 1:05 pm on May 13, 2024:
member
Concept ACK
Seems like a reasonable approach to the problem described in #29435 , will dig in more. Using a variant as a replacement for the Enum seems a bit odd at first glance and requires a lot of duplicate code for each struct. Perhaps another approach could be a class for RemovalReason, which contains the Enum as a field and then uses a std::variant to hold the data needed by the wallet, if any. Would be more extensible, and then the to string method could be defined once on the class. Special logic per reason can also be handled all in the same place by switching on the Enum value and taking appropriate action per reason.
EDIT: this approach might also make it easier to do the first commit as a scripted-diff
Perhaps another approach could be a class for RemovalReason, which contains the Enum as a field and then uses a std::variant to hold the data needed by the wallet, if any. Would be more extensible, and then the to string method could be defined once on the class. Special logic per reason can also be handled all in the same place by switching on the Enum value and taking appropriate action per reason.
Thanks for the review @josibake. Will this approach still require the definition of structs to hold each data for each reason?
josibake
commented at 2:39 pm on May 13, 2024:
member
Will this approach still require the definition of structs to hold each data for each reason?
I don’t think so: only the Conflict and Replace reasons need extra data, and from what I understand it can only be one reason at any given time. Given that, you could have a field on the new class for removal data which is a std::variant of the possible data types. For example, std::variant<CTransactionRef, BlockData>, where block data holds the block hash and block number.
I don’t think so: only the Conflict and Replace reasons need extra data, and from what I understand it can only be one reason at any given time. Given that, you could have a field on the new class for removal data which is a std::variant of the possible data types. For example, std::variant<CTransactionRef, BlockData>, where block data holds the block hash and block number.
@josibake Makes sense but I’m skeptical about how this affects the removal reason usage. For example, you can just define SizeLimitReason() right now, but with this approach, you’d have to do RemovalReason(RemovalReasons::SIZE_LIMIT) and this new approach still requires the use of std::variant, so, I’m worried that the current code in https://github.com/bitcoin/bitcoin/pull/29680/commits/59ee08e43255bf0e96d1fab8eb243a4b8b7864c2 will still be more clear.
WDYT?
josibake
commented at 3:54 pm on May 13, 2024:
member
The benefits I see are we can keep the existing Enum and code for converting the Enum to a string. This would also allow updating the function signatures with a scripted-diff, which makes larger refactors like this easier to verify as a reviewer.
It also feels like a more natural fit to me and my gut feeling is that having a class to encapsulate the removal reasons and necessary data will be more maintainable and extensible going forward.
The benefits I see are we can keep the existing Enum and code for converting the Enum to a string. This would also allow updating the function signatures with a scripted-diff, which makes larger refactors like this easier to verify as a reviewer.
It also feels like a more natural fit to me and my gut feeling is that having a class to encapsulate the removal reasons and necessary data will be more maintainable and extensible going forward.
@josibake Makes sense. I’ll create a RemovalReason class
ryanofsky
commented at 8:10 pm on May 13, 2024:
contributor
Given that, you could have a field on the new class for removal data which is a std::variant of the possible data types. For example, std::variant<CTransactionRef, BlockData>, where block data holds the block hash and block number.
The drawback of a separate data field is that there is no longer a guarantee that data accompanying the reason is always present when the reason is set, and always absent when it is not set. So it is possible for code to only partially initialize the RemovalReason class or initialize it in an inconsistent state.
In general I like the idea of replacing enums with variants for more safety, and to be able to express rules about state in type definitions. But variants in c++ are more awkward than sum types in other languages, and I did not look into this specific case, so maybe the tradeoffs in this case are not worth it.
josibake
commented at 8:21 am on May 14, 2024:
member
The drawback of a separate data field is that there is no longer a guarantee that data accompanying the reason is always present when the reason is set, and always absent when it is not set. So it is possible for code to only partially initialize the RemovalReason class or initialize it in an inconsistent state.
Seems easily addressed with a constructor, no? Something like:
0classRemovedReason {
1public: 2 MemPoolRemovalReason m_reason;
3 std::variant<std::monostate, CTxReference, BlockData> m_extra_data;
4 5// Constructor for reasons that don't require extra data
6 RemovedReason(MemPoolRemovalReason r) : reason(r) {
7if (requiresExtraData(r)) {
8throw std::invalid_argument("reason X requires data Y etc");
9 }
10 }
1112// Constructor needing CTxRef
13 RemovedReason(RemovalReason r, const CTxRef& data) : reason(r), extra_data(data) {
14if (!IsA(r)) {
15throw std::invalid_argument("CTxRef is required for reason A, got bla");
16 }
17 }
1819// Constructor needing BlockData
20 RemovedReason(RemovalReason r, const BlockData& data) : reason(r), extra_data(data) {
21if (!IsB(r)) {
22throw std::invalid_argument("BlockData is required for reason B, got bla");
23 }
24 }
Maybe this is starting to be more complicated than just having a struct per each reason? But I’d still argue this is a better approach in that it keeps all the logic for mempool removal reasons in one place and avoids duplicating code on each struct.
Maybe this is starting to be more complicated than just having a struct per each reason? But I’d still argue this is a better approach in that it keeps all the logic for mempool removal reasons in one place and avoids duplicating code on each struct.
Same thing I was thinking. Using the class right now looks like it will make things more complicated. Maybe we should leave the class for a future change where it becomes necessary? @josibake@ryanofsky
ryanofsky
commented at 2:45 pm on May 14, 2024:
contributor
Seems easily addressed with a constructor, no? Something like:
Yes, but those are manual constraints that you are writing by hand rather than automatic constraints expressed implicitly in the data definition. Depending on the constructors it may also only provide runtime checking rather than compile-time checking like in your example. And if the struct is mutable could allow invalid representations of state after construction.
I don’t know what is best in this particular case, I would just stand up for:
0enumclassMyState {
1 STATE1,
2 STATE2,
3 STATE3,
4};
56classMyData {
7 MyState m_state,
8// ... more data and methods...
9};
in many cases.
Maybe we should leave the class for a future change where it becomes necessary?
I’m not sure the answer to this, but it is probably worth experimenting and choosing the approach that seems simplest.
josibake
commented at 2:57 pm on May 14, 2024:
member
Yes, but those are manual constraints that you are writing by hand rather than automatic constraints expressed implicitly in the data definition
Fair point, in that bugs could be introduced by someone not writing these pre-checks correctly / efficiently. I’ll admit I’m not fully convinced that the struct per state approach isn’t going to be harder to maintain / extend in the future, but given that I haven’t convinced you guys on that point and you have convinced me there is an advantage per the struct per state approach, I’ll retract my suggestion we change it :smile:
DrahtBot added the label
Needs rebase
on May 15, 2024
Eunovo force-pushed
on May 20, 2024
DrahtBot removed the label
Needs rebase
on May 20, 2024
DrahtBot added the label
CI failed
on May 20, 2024
in
test/functional/wallet_conflicts.py:486
in
7da8f981eboutdated
DrahtBot removed the label
CI failed
on May 22, 2024
in
src/kernel/mempool_removal_reason.h:14
in
7debac0b09outdated
10+
11 #include <string>
12+#include <variant>
1314-/** Reason why a transaction was removed from the mempool,
15+/** Reasons why a transaction was removed from the mempool,
nit: I think it’s more correct to leave this as “Reason.” “Reasons” implies a single transaction can have multiple reasons for being removed at the same time.
in
src/validationinterface.cpp:22
in
7debac0b09outdated
Don’t we also need to #include kernel/mempool_removal_reasons.h , for Include What You Use (IWYU)? I’ll admit, I’m totally sure what our conventions are on this. cc @TheCharlatan or @maflcko
Was a bit surprised that pthisBlock->GetHash() isn’t returning a member variable and instead is calculating the hash each time its called. Worth mentioning we are adding an extra hash to ConnectTip. Probably negligible but wanted to mention it.
There might be some gain in caching GetHash() but I think that has to be addressed on its own. I’ll have to measure the runtimes and see its worth a PR.
Any reason why conflicting_block_hash is passed by reference but replacement_tx isn’t?
Would also be nice if we prevented these objects from be constructed with nullptrs. I’m not sure if we have a convention in our codebase around this or other examples to point to, but would be worth looking into.
Would also be nice if we prevented these objects from be constructed with nullptrs. I’m not sure if we have a convention in our codebase around this or other examples to point to, but would be worth looking into.
It looks like I may be able to do this by deleting the constructor
I haven’t finished reviewing the later commits yet, but seems odd to pass both conflicting_block_hash and conflicting_block_height. Seems like we should be able to only use conflicting_block_hash?
The TxStateBlockConflicted requires both that’s why I added the both of them
josibake
commented at 10:02 am on May 22, 2024:
member
Still reviewing the later commits, but had some initial feedback/questions for the first commit.
DrahtBot added the label
Needs rebase
on May 24, 2024
Eunovo force-pushed
on Jun 2, 2024
DrahtBot removed the label
Needs rebase
on Jun 3, 2024
in
src/wallet/wallet.cpp:1451
in
b67c82a6f2outdated
1448 RefreshMempoolStatus(it->second, chain());
1449 }
1450 // Handle transactions that were removed from the mempool because they
1451 // conflict with transactions in a newly connected block.
1452- if (reason == MemPoolRemovalReason::CONFLICT) {
1453+ if (std::get_if<ConflictReason>(&reason)) {
This commit does not compile, shouldn’t have changed m_all_conflicts for m_all_conflicting.
Eunovo force-pushed
on Jun 3, 2024
Eunovo
commented at 11:24 pm on June 3, 2024:
none
Thanks for the reviews @josibake and @furszy I have implemented your suggested changes
DrahtBot added the label
CI failed
on Jun 4, 2024
DrahtBot
commented at 0:41 am on June 4, 2024:
contributor
🚧 At least one of the CI tasks failed. Make sure to run all tests locally, according to the
documentation.
Possibly this is due to a silent merge conflict (the changes in this pull request being
incompatible with the current code in the target branch). If so, make sure to rebase on the latest
commit of the target branch.
Leave a comment here, if you need help tracking down a confusing failure.
427@@ -428,6 +428,11 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
428 //! Cache of descriptor ScriptPubKeys used for IsMine. Maps ScriptPubKey to set of spkms
429 std::unordered_map<CScript, std::vector<ScriptPubKeyMan*>, SaltedSipHasher> m_cached_spks;
430431+ /**
432+ * Tracks txids of unrelated double-spending txs to wallet transactions
achow101
commented at 10:24 pm on November 4, 2024:
In 7eba56accbd1b4ce3c09656c8d69b5aa437a706f “Handle double-spending of unrelated parents to wallet txs”
nit: whitespace.
Also, I think it would be better to clarify that the key is the txid of the replacing transaction which may be unrelated to the wallet, and the value is the replaced transaction which is related to this wallet.
This PR implements a fix for the issue described in #29435.
The problem is that the wallet is unable to abandon transactions that have unrelated parent conflicts. The solution implemented here augments the mempool transaction REPLACED signal with the double-spending transaction which the wallet stores and watches for in Block notifications. A map is added to the wallet to track conflicting tx ids and their child transactions. The entry is erased when the double-spending tx is removed from MemPool.
#30072 introduced Mempool Changesets for packages and single transactions. Previously, I captured the replacement_tx when a tx was being kicked out of the Mempool, but with the addition of packages, any of the tx in the package could be the reason for the conflict.
I’ve changed the replacement_tx to be the last child tx in the package so the wallet doesn’t have to watch the entire replacement package.
This solution assumes that all transactions in a package will be added to a Block. Suppose the first transaction in a 1P1C package is the actual replacement tx and the package only partially gets added to a Block. In that case, the wallet will not abandon the conflicted wallet tx because it’s watching the child of the package.
DrahtBot removed the label
CI failed
on Dec 1, 2024
Eunovo force-pushed
on Dec 2, 2024
DrahtBot added the label
CI failed
on Dec 31, 2024
DrahtBot removed the label
CI failed
on Jan 2, 2025
DrahtBot
commented at 1:43 pm on January 2, 2025:
contributor
Maybe turn into a draft for now, while CI is failing intermittently?
Eunovo
commented at 12:12 pm on January 5, 2025:
none
Putting in draft while I fix failing CI
Eunovo marked this as a draft
on Jan 5, 2025
Eunovo force-pushed
on Jan 15, 2025
DrahtBot added the label
CI failed
on Jan 15, 2025
Change MemPoolRemovalReason to variant type
This allows the mempool to send additional data with TransactionRemovedFromMempool event.
Now, we can send conflicting_block_hash and conflicting_block_height for Conflicts and replacement_tx for Replacements.
4c64dd5a3c
Handle double-spending of unrelated parents to wallet txs
Detect replacement of wallet txs and wait for confirmation of replacement tx before marking wallet tx as conflicted
71200b72ab
test: Conflict with unconfirmed parent not in walletc162984fbc
Handle new blocks with unrelated parents conflicts
Watch for wallet transaction conflicts triggered by adding conflicting blocks
6db2961f60
Eunovo force-pushed
on Jan 16, 2025
in
src/interfaces/chain.h:7
in
4c64dd5a3coutdated
6@@ -7,6 +7,7 @@
7
ismaelsadeeq
commented at 6:18 pm on January 17, 2025:
ismaelsadeeq
commented at 6:19 pm on January 17, 2025:
In 4c64dd5a3cc4a48e5470fff26c99dde20f81c7e0 “Change MemPoolRemovalReason to variant type”
nit: this line is too long, please break.
in
src/wallet/wallet.cpp:1493
in
71200b72aboutdated
1488+ if (iter != m_unrelated_conflict_tx_watchlist.end()) {
1489+ // The replacement tx was removed from the mempool, remove it from map
1490+ // This new replacement tx may not conflict with the original tx
1491+ // so leave wallet tx to remain as TxStateInactive
1492+ m_unrelated_conflict_tx_watchlist.erase(iter);
1493+ }
ismaelsadeeq
commented at 6:22 pm on January 17, 2025:
In 71200b72ab848c3066e5123dc0badc24c9648f47 “Handle double-spending of unrelated parents to wallet txs”
0 auto* replaced_reason = std::get_if<ReplacedReason>(&reason);
1 if (IsFromMe(*tx) && replaced_reason != nullptr) {
2 3 // Check if wallet transaction is being replaced by a parent transaction which is not from this wallet
4 if (replaced_reason->replacement_tx.has_value() && !IsFromMe(*replaced_reason->replacement_tx.value())) {
5 m_unrelated_conflict_tx_watchlist.insert(std::make_pair(replaced_reason->replacement_tx.value()->GetHash(), tx));
6 }
7 // Remove the replacement tx was removed from the mempool, remove it from map
8 // This new replacement tx may not conflict with the original tx
9 // so leave wallet tx to remain as TxStateInactive
10 m_unrelated_conflict_tx_watchlist.erase(tx->GetHash());
11 }
in
src/wallet/wallet.cpp:1491
in
71200b72aboutdated
1486+
1487+ auto iter = m_unrelated_conflict_tx_watchlist.find(tx->GetHash());
1488+ if (iter != m_unrelated_conflict_tx_watchlist.end()) {
1489+ // The replacement tx was removed from the mempool, remove it from map
1490+ // This new replacement tx may not conflict with the original tx
1491+ // so leave wallet tx to remain as TxStateInactive
ismaelsadeeq
commented at 6:23 pm on January 17, 2025:
In 71200b72ab848c3066e5123dc0badc24c9648f47 “Handle double-spending of unrelated parents to wallet txs”
How do you know that without checking ?
ismaelsadeeq
commented at 6:33 pm on January 17, 2025:
member
Concept ACK
Left some initial comments
Previously, I captured the replacement_tx when a tx was being kicked out of the Mempool, but with the addition of packages, any of the tx in the package could be the reason for the conflict.
I’ve changed the replacement_tx to be the last child tx in the package so the wallet doesn’t have to watch the entire replacement package.
Can you elaborate on how that work, and the wallet detects that, what if the conflict is not from the child, it’s from a parent transaction?
This solution assumes that all transactions in a package will be added to a Block.
This is Incorrect I think, you can have a package and not all the transaction will be added in the block.
Suppose the first transaction in a 1P1C package is the actual replacement tx and the package only partially gets added to a Block.
In that case, the wallet will not abandon the conflicted wallet tx because it’s watching the child of the package.
How is this not an issue?
Eunovo
commented at 3:41 pm on January 18, 2025:
none
@ismaelsadeeq I wanted to get other opinions on this. With the addition of changesets, the problem is more complicated.
Can you elaborate on how that work, and the wallet detects that, what if the conflict is not from the child, it’s from a parent transaction?
It does work because it’s guaranteed that the parent will be confirmed before the child. The wallet can mark the replaced wallet tx as conflicted when the child of the package gets confirmed. The problem though is that the child may never be confirmed even if the parent has been confirmed.
This is Incorrect I think, you can have a package and not all the transaction will be added in the block.
How is this not an issue?
This is a metadata mirror of the GitHub repository
bitcoin/bitcoin.
This site is not affiliated with GitHub.
Content is generated from a GitHub metadata backup.
generated: 2025-01-21 06:12 UTC
This site is hosted by @0xB10C More mirrored repositories can be found on mirror.b10c.me