Repository navigation
wallet: close races around CWallet::EncryptWallet - #183
Merged
Merged
Conversation
EncryptWallet decided whether the wallet could be encrypted before it took the wallet locks, and the key derivation that follows is tuned to take a noticeable amount of time. A second EncryptWallet caller in that window, such as the encryptwallet RPC overlapping the GUI worker added by #178, passed the same check, encrypted the already encrypted keys and hit the assert in the encryption loop. Move the already-encrypted and PQC-validation checks under the wallet locks, after the key derivation. The derivation still runs unlocked so it does not stall other wallet users, while the check and the encryption now happen under one lock acquisition, so the losing caller returns false without touching the wallet. The encryptwallet RPC takes the same locks before its own already-encrypted check, so an overlapping RPC call reports the wallet as already encrypted once the other encryption has finished, instead of a generic failure. The new master key was inserted into mapMasterKeys before the database transaction began, so a failed TxnBegin left the wallet reporting IsCrypted() with nothing written. Insert it only once the transaction is open and the master key record has been written; a failed write aborts the transaction. Post-commit failures are unchanged. IsHDEnabled and GetActiveScriptPubKeyMans read the active manager maps without cs_wallet, which EncryptWallet replaces under the lock. GetActiveScriptPubKeyMans, IsActiveScriptPubKeyMan and ConnectScriptPubKeyManNotifiers now require cs_wallet; IsHDEnabled and the two callers that did not hold it take the lock. A test hook parks one EncryptWallet caller between key derivation and the locks while another completes. Further unit tests cover the failed TxnBegin and the failed master key write, and wallet_encryption.py runs two overlapping encryptwallet calls. Fixes #179 Co-Authored-By: Claude <noreply@anthropic.com>
When the encryptwallet RPC encrypts the wallet while the GUI's request is under way, the GUI's request fails, but the wallet is encrypted with the RPC's passphrase. The Encrypt Wallet dialog still reported that the wallet was not encrypted. After a failed encryption the dialog now reads the encryption status, which the model refreshes once the worker has returned, and says the wallet was already encrypted by another request and the entered passphrase was not used. A genuinely unencrypted wallet keeps the existing message. The synthetic Qt wallet now refuses an already encrypted wallet as CWallet::EncryptWallet does, and a Qt test covers the new message. ConcurrentEncryptWalletLoserFailsCleanly hung instead of failing when the winning encryption's BOOST_REQUIRE failed: the loser's std::async future was destroyed, and waited for the parked loser, before anything released it. A scope guard declared after the future now releases the loser on unwind. The EncryptWallet comment said the key derivation does not stall other wallet users, but the encryptwallet RPC holds the wallet locks for the whole call; only callers that do not hold them, such as the GUI worker, avoid the stall. Say so. Fixes #179 Co-Authored-By: Claude <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
21 of 30 tasks
6 of 15 tasks
kiwidream
added a commit
that referenced
this pull request
Oct 27, 2026
Cover every pull request merged to 1.x.x since the signed v1.0.0 tag, through #252. List the behavior changes first: the -fallbackfee default (#151), fee-only send entries (#150), and the encryptwallet overlap error code moving from -16 to -15 (#183). Record the release's known limitations: the unsigned-platform waiver (#70), the depth of the fuzz evidence (#173), and scheduled-workflow deployment (#152, #158). The required P2MR v1 conformance statement names the signed tag target and the oracle report digest, so the in-tree notes point to the release page, and the publish-time notes file carries the exact statement. Fold the #150 and #151 fragments into these notes and remove them. Refs #174 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7 of 30 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This closes three races around
CWallet::EncryptWalletfrom #179. They predate #178, but #178's asynchronous GUI encryption makes it easier for a GUI encryption and anencryptwalletRPC to overlap.LOCK2. AnencryptwalletRPC overlapping the GUI worker could pass them too, double-encrypt and hitassert(false).EncryptWalletnow makes both checks underLOCK2(m_relock_mutex, cs_wallet), after the key derivation, which still runs unlocked on the GUI and interfaces path.-15RPC_WALLET_WRONG_ENC_STATE("running with an encrypted wallet") instead of-16"Failed to encrypt".-16now means a real database failure. This is a user-visible RPC error-code change.TxnBeginfailure. The key now entersmapMasterKeysandnMasterKeyMaxIDonly afterTxnBeginandWriteMasterKeyboth succeed. A failedWriteMasterKeynow aborts the transaction. Post-commit behaviour is unchanged.GetActiveScriptPubKeyMans,IsActiveScriptPubKeyManandConnectScriptPubKeyManNotifiersnow requirecs_wallet, viaEXCLUSIVE_LOCKS_REQUIREDplusAssertLockHeld.IsHDEnablednow locks internally.AddWalletandCWallet::Create, are fixed.hdEnabledis already skipped while encrypting (qt: keep Encrypt Wallet responsive during existing-wallet encryption #178).There are no wallet format, encryption semantics or consensus changes.
Fixes #179.
Testing
Built locally.
Ran focused unit or functional tests for the changed area.
Ran lint or formatting checks relevant to this change.
Not run. Reason:
New
wallet_encryption_testssuite, 3 deterministic cases using a test-only hook between key derivation and locking, plus the mock database:EncryptWalletcalls;TxnBeginfailure;WriteMasterKeyfailure.Against the base production code these fail with SIGABRT at
assert(false), withIsCrypted/mapMasterKeysleft set, and with a fatal flag-write error respectively.wallet_encryption.pyadds a two-thread overlappingencryptwalletcase. It got-16on 6/6 base runs and gets-15with the fix.New Qt test
TestEncryptWalletAlreadyEncryptedByAnotherRequestcovers the dialog message and fails without the fix. The race test now fails instead of hanging if its first assertion fails; verified with a forced failure.Other suites:
test_qbitwallet suites,wallet_encryption.py(also with--usecli),wallet_keypool.pyandtest_qbit-qt(offscreen, including the qt: keep Encrypt Wallet responsive during existing-wallet encryption #178 tests) pass.clang 20 with
-Wthread-safety -Wthread-safety-precise -Werror=thread-safetyandDEBUG_LOCKORDER: all 65 wallet and wallet-test objects rebuild clean. Its wallet, spend, psbt and feebumper suites pass, andwallet_encryption_testspasses 15/15 repeats.Adversarial review: an independent review traced every caller of the changed accessors for lock-order inversions and GUI freezes, checked every
EncryptWalletfailure path and the RPC lock scope, and reproduced the revert proofs. It found no blocker; its nits are addressed in 91f83c5.Not run: TSan, Debug-config, macOS/Windows, the Qt translation units under clang thread-safety (the clang build is GUI-off), and a manual GUI+RPC overlap on a populated wallet.
.tstranslation files weren't regenerated for the new string.Follow-up (not in this PR):
GetAllScriptPubKeyMans,GetScriptPubKeyManandIsCryptedremain unannotated unlocked readers.Target Branch
mainor a maintainer-requested release branch such as0.1.x.Targets
1.x.xfor v1.0.1 (#174). It merges cleanly with #182.Risk / Review Notes
Notes: this changes wallet encryption locking and failure handling, plus lock requirements on SPKM accessors. The RPC error code for an overlapping or repeated
encryptwalletchanges from-16to-15, and the v1.0.1 release notes should mention it.Docs / Process Impact
Choose exactly one:
-15code for an already-encrypted wallet, will be added to the v1.0.1 release notes in release: prepare v1.0.1 #181.🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.