From 6861fa51b11a5dbae70047a2027d58053ecc3c29 Mon Sep 17 00:00:00 2001 From: pasta Date: Wed, 7 Oct 2026 02:05:07 -0500 Subject: [PATCH] fix: preserve wallet permissions when changing passphrases Co-Authored-By: Claude Opus 5.5 (1M context) --- src/wallet/wallet.cpp | 12 ++++++------ test/functional/wallet_encryption.py | 25 +++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index cd5871a2520e..3432f93f7d8a 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -576,10 +576,13 @@ void CWallet::UpgradeDescriptorCache() bool CWallet::ChangeWalletPassphrase(const SecureString& strOldWalletPassphrase, const SecureString& strNewWalletPassphrase) { - bool fWasLocked = IsLocked(true); - { LOCK2(m_relock_mutex, cs_wallet); + const bool was_locked = IsLocked(true); + const bool was_mixing_only = fOnlyMixingAllowed; + const auto relock = interfaces::MakeCleanupHandler([this, was_locked] { + if (was_locked) Lock(); + }); Lock(); CCrypter crypter; @@ -590,7 +593,7 @@ bool CWallet::ChangeWalletPassphrase(const SecureString& strOldWalletPassphrase, return false; if (!crypter.Decrypt(pMasterKey.second.vchCryptedKey, _vMasterKey)) return false; - if (Unlock(_vMasterKey)) + if (Unlock(_vMasterKey, was_mixing_only)) { constexpr MillisecondsDouble target{100}; auto start{SteadyClock::now()}; @@ -611,9 +614,6 @@ bool CWallet::ChangeWalletPassphrase(const SecureString& strOldWalletPassphrase, if (!crypter.Encrypt(_vMasterKey, pMasterKey.second.vchCryptedKey)) return false; WalletBatch(GetDatabase()).WriteMasterKey(pMasterKey.first, pMasterKey.second); - if (fWasLocked) - Lock(); - return true; } } diff --git a/test/functional/wallet_encryption.py b/test/functional/wallet_encryption.py index 57ab68b0c59e..6eec2e2efa24 100755 --- a/test/functional/wallet_encryption.py +++ b/test/functional/wallet_encryption.py @@ -24,6 +24,29 @@ def set_test_params(self): def skip_test_if_missing_module(self): self.skip_if_no_wallet() + def test_passphrase_change_permissions(self): + self.log.info("Passphrase changes preserve locked, mixing-only and full permissions") + node = self.nodes[0] + for mode in ("locked", "mixing", "full"): + node.createwallet("change-" + mode, descriptors=self.options.descriptors) + wallet = node.get_wallet_rpc("change-" + mode) + address = wallet.getnewaddress() + wallet.encryptwallet("old passphrase") + if mode != "locked": + wallet.walletpassphrase("old passphrase", 100, mode == "mixing") + wallet.walletpassphrasechange("old passphrase", "new passphrase") + if mode == "full": + signature = wallet.signmessage(address, "passphrase change") + assert node.verifymessage(address, signature, "passphrase change") + else: + assert_raises_rpc_error(-13, "Please enter the wallet passphrase", wallet.signmessage, address, "passphrase change") + if mode == "locked": + assert_raises_rpc_error(-13, "Please unlock wallet for mixing", wallet.coinjoin, "start") + else: + assert_equal(wallet.coinjoin("start"), "Mixing requested") + wallet.coinjoin("stop") + wallet.unloadwallet() + def run_test(self): passphrase = "WalletPassphrase" passphrase2 = "SecondWalletPassphrase" @@ -109,6 +132,8 @@ def run_test(self): assert_equal(self.nodes[0].getwalletinfo()['unlocked_until'], unlocked_until) self.nodes[0].walletlock() + self.test_passphrase_change_permissions() + if __name__ == '__main__': WalletEncryptionTest().main()