-
Notifications
You must be signed in to change notification settings - Fork 37.7k
wallet: guard and alert about a wallet invalid state during chain sync #25272
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
d964c9d
to
712627a
Compare
Update: |
712627a
to
d15e9f7
Compare
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. |
Concept ACK |
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.
Code review ACK d15e9f7. Nice find and test, and clear description of the problem. This change will raise an explicit error in a failure case that was previously ignored, so in theory it could lead to worse outcomes in some cases. But status quo results in inconsistent in-memory state, and the error seems pretty unlikely to happen to begin with so this is probably the right move.
class FailDatabase : public WalletDatabase | ||
{ | ||
public: | ||
bool m_pass{true}; // false when this db should fail |
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.
In commit "test: add case for wallet invalid state where the inputs (prev-txs) of a new arriving transaction are not marked dirty, while the transaction that spends them exist inside the in-memory wallet tx map (not stored on db due a db write failure)." (742cd0b)
Default is true here, but false in the other class. Would suggest sticking choosing one default just to avoid confusion and having to remember which class has which default.
(Also maybe use a shorter commit subject, https://cbea.ms/git-commit/ has nice guidelines)
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.
Default is true here, but false in the other class. Would suggest sticking choosing one default just to avoid confusion and having to remember which class has which default.
yeah, good catch. I wrote the test differently first, this is a remnant of the first approach.
(Also maybe use a shorter commit subject, https://cbea.ms/git-commit/ has nice guidelines)
awesome reading :), thanks!
097e911
to
4bd0d01
Compare
Thanks for the feedback! Applied the following changes:
|
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.
It's preferable that each commit passes the test suite. Currently the first commit fails as the code currently fails the test that you have added. So it would be preferable if that test were added after the fix is implemented.
Ok. I actually did that on purpose so it was easier for reviewers to replicate/understand the problem (I wrote it in the PR description); just compiling the first commit, see/verify the failure, then compile the fix and see it passing (Test Driven Approach). At least for me, it make more sense (and feels more natural) than have tests always passing when we actually have problems in the sources. But.. that is probably not a discussion for this PR, will just re-order the commits :). |
4bd0d01
to
7027e94
Compare
Updated per achow101 feedback:
|
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.
Code review ACK 7027e94. Only changes since last review are internal changes to commits, and making suggested static_cast and m_pass default changes
ACK 7027e94 |
-Context: If `AddToWallet` db write fails, the method returns a wtx nullptr without removing the recently added transaction from the wallet's map. -Problem: When a db write error occurs, `AddToWalletIfInvolvingMe` return false even when the tx is on the wallet's map already --> which makes `SyncTransaction` skip the `MarkInputsDirty` call --> which leads to a wallet invalid state where the inputs of this new transaction are not marked dirty, while the transaction that spends them still exist on the in-memory wallet tx map. Plus, as we only store arriving transaction inside `AddToWalletIfInvolvingMe` when we synchronize/scan blocks from the chain and nowhere else, it makes sense to treat the tx db write error as a runtime error to notify the user about the problem. Otherwise, the user will lose all the not stored transactions after a wallet shutdown (without be able to recover them automatically on the next startup because the chain sync would be above the block where the txs arrived).
When a transaction arrives, the wallet mark its inputs (prev-txs) as dirty. Clearing the wallet transaction cache, triggering a balance recalculation. If this does not happen due a db write error during `AddToWallet`, the wallet will be in an invalid state: The transaction that spends certain wallet UTXO will exist inside the in-memory wallet tx map, having the credit/debit calculated, while its inputs will still have the old cached data (like if them were never spent).
7027e94
to
9e04cfa
Compare
Sadly had to rebase it on master, got a silent merge conflict with the Ready to go again. |
re-ACK 9e04cfa |
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.
Quick review of the patch only LGTM with one question. Did not build, nor review/verify the passing/failing test yet, will do.
if (!wtx) { | ||
// Can only be nullptr if there was a db write error (missing db, read-only db or a db engine internal writing error). | ||
// As we only store arriving transaction in this process, and we don't want an inconsistent state, let's throw an error. | ||
throw std::runtime_error("DB error adding transaction to wallet, write failed"); |
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.
Would it make sense to follow this existing error message format in src/wallet/wallet.cpp:2157
, e.g. provide the function name?
throw std::runtime_error(std::string(__func__) + ": Wallet db error, transaction commit failed");
(modulo s/db/DB/
)
ACK 9e04cfa modulo #25272 (comment) regarding the error message |
Hey, thanks for the review. about your point @jonatack:
Probably would make more sense to go into the opposite direction and change line 2157 to describe the context where the runtime error occurred properly. Something like So if this edge case happens, and the runtime error string gets presented to the user before shutdown, the text is more descriptive than having the function name on it (which, looking it at the user's perspective, provides zero information). Still, we could probably merge this PR as is now and tackle it in a short follow-up PR. |
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.
Post-merge ACK 9e04cfa
Follow-up work to my comment in #25239.
Guarding and alerting the user about a wallet invalid state during chain synchronization.
Explanation
if the
AddToWallet
tx write fails, the method returns a wtxnullptr
without removing the recently added transaction from the wallet's map.Which makes that
AddToWalletIfInvolvingMe
return false (even when the tx is on the wallet's map already), --> which makesSyncTransaction
skip theMarkInputsDirty
call --> which leads to a wallet invalid state where the inputs of this new transaction are not marked dirty, while the transaction that spends them still exist on the in-memory wallet tx map.Plus, as we only store the arriving transaction inside
AddToWalletIfInvolvingMe
when we synchronize/scan block/s from the chain and nowhere else, it makes sense to treat the transaction db write error as a runtime error to notify the user about the problem. Otherwise, the user will lose all the not stored transactions after a wallet shutdown (without be able to recover them automatically on the next startup because the chain sync would be above the block where the txs arrived).Note:
On purpose, the first commit adds test coverage for it. Showing how the wallet can end up in an invalid state. The second commit corrects it with the proposed solution.