Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/wallet/rpc/backup.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1651,10 +1651,14 @@ RPCHelpMan importdescriptors()
}

WalletRescanReserver reserver(*pwallet);
if (!reserver.reserve()) {
if (!reserver.reserve(/*with_passphrase=*/true)) {
throw JSONRPCError(RPC_WALLET_ERROR, "Wallet is currently rescanning. Abort existing rescan or wait.");
}

// Ensure that the wallet is not locked for the remainder of this RPC, as
// the passphrase is used to top up the keypool.
LOCK(pwallet->m_relock_mutex);

const UniValue& requests = main_request.params[0];
const int64_t minimum_timestamp = 1;
int64_t now = 0;
Expand Down
26 changes: 19 additions & 7 deletions src/wallet/rpc/encrypt.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ RPCHelpMan walletpassphrase()
std::weak_ptr<CWallet> weak_wallet = wallet;
pwallet->chain().rpcRunLater(strprintf("lockwallet(%s)", pwallet->GetName()), [weak_wallet, relock_time] {
if (auto shared_wallet = weak_wallet.lock()) {
LOCK(shared_wallet->cs_wallet);
LOCK2(shared_wallet->m_relock_mutex, shared_wallet->cs_wallet);
// Skip if this is not the most recent rpcRunLater callback.
if (shared_wallet->nRelockTime != relock_time) return;
shared_wallet->Lock();
Expand Down Expand Up @@ -122,12 +122,16 @@ RPCHelpMan walletpassphrasechange()
std::shared_ptr<CWallet> const pwallet = GetWalletForJSONRPCRequest(request);
if (!pwallet) return UniValue::VNULL;

LOCK(pwallet->cs_wallet);

if (!pwallet->IsCrypted()) {
throw JSONRPCError(RPC_WALLET_WRONG_ENC_STATE, "Error: running with an unencrypted wallet, but walletpassphrasechange was called.");
}

if (pwallet->IsScanningWithPassphrase()) {
throw JSONRPCError(RPC_WALLET_ERROR, "Error: the wallet is currently being used to rescan the blockchain for related transactions. Please call `abortrescan` before changing the passphrase.");
}

LOCK2(pwallet->m_relock_mutex, pwallet->cs_wallet);

// TODO: get rid of these .c_str() calls by implementing SecureString::operator=(std::string)
// Alternately, find a way to make request.params[0] mlock()'d to begin with.
SecureString strOldWalletPass;
Expand Down Expand Up @@ -175,12 +179,16 @@ RPCHelpMan walletlock()
std::shared_ptr<CWallet> const pwallet = GetWalletForJSONRPCRequest(request);
if (!pwallet) return UniValue::VNULL;

LOCK(pwallet->cs_wallet);

if (!pwallet->IsCrypted()) {
throw JSONRPCError(RPC_WALLET_WRONG_ENC_STATE, "Error: running with an unencrypted wallet, but walletlock was called.");
}

if (pwallet->IsScanningWithPassphrase()) {
throw JSONRPCError(RPC_WALLET_ERROR, "Error: the wallet is currently being used to rescan the blockchain for related transactions. Please call `abortrescan` before locking the wallet.");
}

LOCK2(pwallet->m_relock_mutex, pwallet->cs_wallet);

pwallet->Lock();
pwallet->nRelockTime = 0;

Expand Down Expand Up @@ -219,8 +227,6 @@ RPCHelpMan encryptwallet()
std::shared_ptr<CWallet> const pwallet = GetWalletForJSONRPCRequest(request);
if (!pwallet) return UniValue::VNULL;

LOCK(pwallet->cs_wallet);

if (pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) {
throw JSONRPCError(RPC_WALLET_ENCRYPTION_FAILED, "Error: wallet does not contain private keys, nothing to encrypt.");
}
Expand All @@ -229,6 +235,12 @@ RPCHelpMan encryptwallet()
throw JSONRPCError(RPC_WALLET_WRONG_ENC_STATE, "Error: running with an encrypted wallet, but encryptwallet was called.");
}

if (pwallet->IsScanningWithPassphrase()) {
throw JSONRPCError(RPC_WALLET_ERROR, "Error: the wallet is currently being used to rescan the blockchain for related transactions. Please call `abortrescan` before encrypting the wallet.");
}

LOCK2(pwallet->m_relock_mutex, pwallet->cs_wallet);

// TODO: get rid of this .c_str() by implementing SecureString::operator=(std::string)
// Alternately, find a way to make request.params[0] mlock()'d to begin with.
SecureString strWalletPass;
Expand Down
5 changes: 4 additions & 1 deletion src/wallet/rpc/transactions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -872,15 +872,18 @@ RPCHelpMan rescanblockchain()
wallet.BlockUntilSyncedToCurrentChain();

WalletRescanReserver reserver(*pwallet);
if (!reserver.reserve()) {
if (!reserver.reserve(/*with_passphrase=*/true)) {
throw JSONRPCError(RPC_WALLET_ERROR, "Wallet is currently rescanning. Abort existing rescan or wait.");
}

int start_height = 0;
std::optional<int> stop_height;
uint256 start_block;

LOCK(pwallet->m_relock_mutex);
{
LOCK(pwallet->cs_wallet);
EnsureWalletIsUnlocked(*pwallet);
int tip_height = pwallet->GetLastBlockHeight();

if (!request.params[0].isNull()) {
Expand Down
6 changes: 3 additions & 3 deletions src/wallet/wallet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -551,7 +551,7 @@ bool CWallet::ChangeWalletPassphrase(const SecureString& strOldWalletPassphrase,
bool fWasLocked = IsLocked();

{
LOCK(cs_wallet);
LOCK2(m_relock_mutex, cs_wallet);
Lock();

CCrypter crypter;
Expand Down Expand Up @@ -786,7 +786,7 @@ bool CWallet::EncryptWallet(const SecureString& strWalletPassphrase)
return false;

{
LOCK(cs_wallet);
LOCK2(m_relock_mutex, cs_wallet);
mapMasterKeys[++nMasterKeyMaxID] = kMasterKey;
WalletBatch* encrypted_batch = new WalletBatch(GetDatabase());
if (!encrypted_batch->TxnBegin()) {
Expand Down Expand Up @@ -3407,7 +3407,7 @@ bool CWallet::Lock()
return false;

{
LOCK(cs_wallet);
LOCK2(m_relock_mutex, cs_wallet);
if (!vMasterKey.empty()) {
memory_cleanse(vMasterKey.data(), vMasterKey.size() * sizeof(decltype(vMasterKey)::value_type));
vMasterKey.clear();
Expand Down
9 changes: 8 additions & 1 deletion src/wallet/wallet.h
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
std::atomic<bool> fAbortRescan{false};
std::atomic<bool> fScanningWallet{false}; // controlled by WalletRescanReserver
std::atomic<bool> m_attaching_chain{false};
std::atomic<bool> m_scanning_with_passphrase{false};
std::atomic<int64_t> m_scanning_start{0};
std::atomic<double> m_scanning_progress{0};
friend class WalletRescanReserver;
Expand Down Expand Up @@ -467,6 +468,7 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
void AbortRescan() { fAbortRescan = true; }
bool IsAbortingRescan() const { return fAbortRescan; }
bool IsScanning() const { return fScanningWallet; }
bool IsScanningWithPassphrase() const { return m_scanning_with_passphrase; }
int64_t ScanningDuration() const { return fScanningWallet ? GetTimeMillis() - m_scanning_start : 0; }
double ScanningProgress() const { return fScanningWallet ? (double) m_scanning_progress : 0; }

Expand All @@ -486,6 +488,9 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati

// Used to prevent concurrent calls to walletpassphrase RPC.
Mutex m_unlock_mutex;
// Used to prevent deleting the passphrase from memory when it is still in use.
RecursiveMutex m_relock_mutex;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using Mutex is preferable to RecursiveMutex.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mutex isn't used here because sometimes m_relock_mutex has to be locked multiple time within a scope to prevent deadlocks. See: #26347 (comment)

The new mutex is now grabbed in CWallet::Lock and also in locations where cs_wallet would have been grabbed first in order to prevent a deadlock.


bool Unlock(const SecureString& strWalletPassphrase, bool accept_no_keys = false);
bool ChangeWalletPassphrase(const SecureString& strOldWalletPassphrase, const SecureString& strNewWalletPassphrase);
bool EncryptWallet(const SecureString& strWalletPassphrase);
Expand Down Expand Up @@ -960,12 +965,13 @@ class WalletRescanReserver
public:
explicit WalletRescanReserver(CWallet& w) : m_wallet(w) {}

bool reserve()
bool reserve(bool with_passphrase = false)
{
assert(!m_could_reserve);
if (m_wallet.fScanningWallet.exchange(true)) {
return false;
}
m_wallet.m_scanning_with_passphrase.exchange(with_passphrase);
m_wallet.m_scanning_start = GetTimeMillis();
m_wallet.m_scanning_progress = 0;
m_could_reserve = true;
Expand All @@ -985,6 +991,7 @@ class WalletRescanReserver
{
if (m_could_reserve) {
m_wallet.fScanningWallet = false;
m_wallet.m_scanning_with_passphrase = false;
}
}
};
Expand Down
28 changes: 28 additions & 0 deletions test/functional/wallet_importdescriptors.py
Original file line number Diff line number Diff line change
Expand Up @@ -667,5 +667,33 @@ def run_test(self):
success=True,
warnings=["Unknown output type, cannot set descriptor to active."])

self.log.info("Test importing a descriptor to an encrypted wallet")

descriptor = {"desc": descsum_create("pkh(" + xpriv + "/1h/*h)"),
"timestamp": "now",
"active": True,
"range": [0,4000],
"next_index": 4000}

self.nodes[0].createwallet("temp_wallet", blank=True, descriptors=True)
temp_wallet = self.nodes[0].get_wallet_rpc("temp_wallet")
temp_wallet.importdescriptors([descriptor])
self.generatetoaddress(self.nodes[0], COINBASE_MATURITY + 1, temp_wallet.getnewaddress())
self.generatetoaddress(self.nodes[0], COINBASE_MATURITY + 1, temp_wallet.getnewaddress())

self.nodes[0].createwallet("encrypted_wallet", blank=True, descriptors=True, passphrase="passphrase")
encrypted_wallet = self.nodes[0].get_wallet_rpc("encrypted_wallet")

descriptor["timestamp"] = 0
descriptor["next_index"] = 0

batch = []
batch.append(encrypted_wallet.walletpassphrase.get_request("passphrase", 3))
batch.append(encrypted_wallet.importdescriptors.get_request([descriptor]))

encrypted_wallet.batch(batch)

assert_equal(temp_wallet.getbalance(), encrypted_wallet.getbalance())

if __name__ == '__main__':
ImportDescriptorsTest().main()
39 changes: 39 additions & 0 deletions test/functional/wallet_transactiontime_rescan.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
assert_raises_rpc_error,
set_node_times,
)
from test_framework.wallet_util import (
get_generate_key,
)


class TransactionTimeRescanTest(BitcoinTestFramework):
Expand All @@ -23,6 +26,10 @@ def add_options(self, parser):
def set_test_params(self):
self.setup_clean_chain = False
self.num_nodes = 3
self.extra_args = [["-keypool=400"],
["-keypool=400"],
[]
]

def skip_test_if_missing_module(self):
self.skip_if_no_wallet()
Expand Down Expand Up @@ -167,6 +174,38 @@ def run_test(self):
assert_raises_rpc_error(-8, "Invalid stop_height", restorewo_wallet.rescanblockchain, 1, -1)
assert_raises_rpc_error(-8, "stop_height must be greater than start_height", restorewo_wallet.rescanblockchain, 20, 10)

self.log.info("Test `rescanblockchain` fails when wallet is encrypted and locked")
usernode.createwallet(wallet_name="enc_wallet", passphrase="passphrase")
enc_wallet = usernode.get_wallet_rpc("enc_wallet")
assert_raises_rpc_error(-13, "Error: Please enter the wallet passphrase with walletpassphrase first.", enc_wallet.rescanblockchain)

if not self.options.descriptors:
self.log.info("Test rescanning an encrypted wallet")
hd_seed = get_generate_key().privkey

usernode.createwallet(wallet_name="temp_wallet", blank=True, descriptors=False)
temp_wallet = usernode.get_wallet_rpc("temp_wallet")
temp_wallet.sethdseed(seed=hd_seed)

for i in range(399):
temp_wallet.getnewaddress()

self.generatetoaddress(usernode, COINBASE_MATURITY + 1, temp_wallet.getnewaddress())
self.generatetoaddress(usernode, COINBASE_MATURITY + 1, temp_wallet.getnewaddress())

minernode.createwallet("encrypted_wallet", blank=True, passphrase="passphrase", descriptors=False)
encrypted_wallet = minernode.get_wallet_rpc("encrypted_wallet")

encrypted_wallet.walletpassphrase("passphrase", 1)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fails for me when running in valgrind:

 node0 2023-02-28T19:50:00.090808Z (mocktime: 2023-03-30T19:35:09Z) [httpworker.1] [rpc/request.cpp:179] [parse] [rpc] ThreadRPCServer method=walletpassphrase user=__cookie__ 
 node0 2023-02-28T19:50:08.966047Z (mocktime: 2023-03-30T19:35:09Z) [httpworker.1] [rpc/server.cpp:560] [RPCRunLater] [rpc] queue run of timer lockwallet(encrypted_wallet) in 1 seconds (using HTTP) 
 node0 2023-02-28T19:50:08.995457Z (mocktime: 2023-03-30T19:35:09Z) [http] [httpserver.cpp:239] [http_request_cb] [http] Received a POST request for /wallet/encrypted_wallet from 127.0.0.1:42774 
 node0 2023-02-28T19:50:08.998881Z (mocktime: 2023-03-30T19:35:09Z) [httpworker.2] [rpc/request.cpp:179] [parse] [rpc] ThreadRPCServer method=sethdseed user=__cookie__ 
 node0 2023-02-28T19:51:12.893784Z (mocktime: 2023-03-30T19:35:09Z) [httpworker.2] [wallet/scriptpubkeyman.h:251] [WalletLogPrintf] [encrypted_wallet] keypool added 800 keys (400 internal), size=800 (400 internal) 
 node0 2023-02-28T19:51:12.953236Z (mocktime: 2023-03-30T19:35:09Z) [httpworker.2] [wallet/scriptpubkeyman.h:251] [WalletLogPrintf] [encrypted_wallet] LegacyScriptPubKeyMan::NewKeyPool rewrote keypool 
 node0 2023-02-28T19:51:12.964195Z (mocktime: 2023-03-30T19:35:09Z) [http] [httpserver.cpp:239] [http_request_cb] [http] Received a POST request for /wallet/encrypted_wallet from 127.0.0.1:53062 
 node0 2023-02-28T19:51:12.966738Z (mocktime: 2023-03-30T19:35:09Z) [httpworker.3] [rpc/request.cpp:179] [parse] [rpc] ThreadRPCServer method=sethdseed user=__cookie__ 
 test  2023-02-28T19:51:12.976000Z TestFramework (ERROR): JSONRPC error 
                                   Traceback (most recent call last):
                                     File "/root/bitcoin-core/ci/scratch/build/bitcoin-x86_64-pc-linux-gnu/test/functional/test_framework/test_framework.py", line 134, in main
                                       self.run_test()
                                     File "/root/bitcoin-core/ci/scratch/build/bitcoin-x86_64-pc-linux-gnu/test/functional/wallet_transactiontime_rescan.py", line 200, in run_test
                                       encrypted_wallet.sethdseed(seed=hd_seed)
                                     File "/root/bitcoin-core/ci/scratch/build/bitcoin-x86_64-pc-linux-gnu/test/functional/test_framework/coverage.py", line 49, in __call__
                                       return_val = self.auth_service_proxy_instance.__call__(*args, **kwargs)
                                     File "/root/bitcoin-core/ci/scratch/build/bitcoin-x86_64-pc-linux-gnu/test/functional/test_framework/authproxy.py", line 149, in __call__
                                       raise JSONRPCException(response['error'], status)
                                   test_framework.authproxy.JSONRPCException: Error: Please enter the wallet passphrase with walletpassphrase first. (-13)

Maybe the timeout can be increased to 999999?

Also I wonder why you picked a timeout of 3 below, or why walletpassphrase needs to be called at all twice in a row?

If you want to confirm that the wallet can't be relocked while the rescan is in progress, wouldn't it be better to call walletlock rpc right after the rescan rpc?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe the timeout can be increased to 999999?

Yes, the timeout before sethdseed can be increased to 999999, followed by running walletlock. I can open a follow-up PR for this.

Also I wonder why you picked a timeout of 3 below, or why walletpassphrase needs to be called at all twice in a row?

3 was used because it needed to be a number that was large enough so that the scan would begin, but small enough so that the timeout would occur while the rescan is in progress. However, this number can be too large or too small depending on the speed of the machine.

If you want to confirm that the wallet can't be relocked while the rescan is in progress, wouldn't it be better to call walletlock rpc right after the rescan rpc?

I don't completely understand this suggestion. Do you mean calling walletlock to ensure that the wallet is still unlocked? If so, I think there are two problems with that:

  • walletlock doesn't throw an error if the wallet is already locked
  • The wallet is supposed to re-lock immediately after the rescan, so the wallet might already be locked by the time walletlock is called, even if it did not lock during the rescan. However, using a batched RPC request might help with this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean calling walletlock to ensure that the wallet is still unlocked?

No, I mean calling walletlock to lock the wallet. So the execution would look like:

-> walletpassphrase 99999 (start)
<- walletpassphrase (end)
-> rescanblockchain (start)
-> walletlock (start)
<- rescanblockchain (finish, release mutex)
<- walletlock (take mutex, end)

Without the fix in this pull, walletlock might lock immediately and then likely cause rescanblockchain to fail?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I think I understand what you mean now. I have implemented this using multi-threading in #27199. However, for me this no longer fails without the fix.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, for me this no longer fails without the fix.

For me, nothing fails. Unless I specifically add a manual sleep on the code before the fix:

diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp
index eed40f9462..d5b5af0d4c 100644
--- a/src/wallet/wallet.cpp
+++ b/src/wallet/wallet.cpp
@@ -1851,6 +1851,8 @@ CWallet::ScanResult CWallet::ScanForWalletTransactions(const uint256& start_bloc
     WalletLogPrintf("Rescan started from block %s... (%s)\n", start_block.ToString(),
                     fast_rescan_filter ? "fast variant using block filters" : "slow variant inspecting all blocks");
 
+    UninterruptibleSleep(4s);
+
     fAbortRescan = false;
     ShowProgress(strprintf("%s " + _("Rescanning…").translated, GetDisplayName()), 0); // show rescan progress in GUI as dialog or on splashscreen, if rescan required on startup (e.g. due to corruption)
     uint256 tip_hash = WITH_LOCK(cs_wallet, return GetLastBlockHash());

encrypted_wallet.sethdseed(seed=hd_seed)

batch = []
batch.append(encrypted_wallet.walletpassphrase.get_request("passphrase", 3))
batch.append(encrypted_wallet.rescanblockchain.get_request())

encrypted_wallet.batch(batch)

assert_equal(encrypted_wallet.getbalance(), temp_wallet.getbalance())

if __name__ == '__main__':
TransactionTimeRescanTest().main()