Skip to content

Commit 78567b0

Browse files
committed
Merge bitcoin#30697: Bugfix: Ensure Atomicity in Wallet Settings Updates from Chain Interface
1b41d45 wallet: bugfix: ensure atomicity in settings updates (ismaelsadeeq) Pull request description: This PR fixes bitcoin#30620. As outlined in the issue, creating two wallets with `load_on_startup=true` simultaneously results in only one wallet being added to the startup file. The current issue arises because the wallet settings update process involves: 1. Obtaining the settings value while acquiring the settings lock. 2. Modifying the settings value. 3. Overwriting the settings value while acquiring the settings lock again. This sequence is not thread-safe. Different threads could modify the same base value simultaneously, overwriting data from other workers without realizing it. The PR attempts to fix this by modifying the chain interface's `updateRwSetting` method to accept a function that will be called with the settings reference. This function will either update or delete the setting and return an enum indicating whether the settings need to be overwritten in this or not. Additionally, this PR introduces two new methods to the chain interface: - `overwriteRwSetting`: This method replaces the setting with a new value. Used in `VerifyWallets` - `deleteRwSettings`: This method completely erases a specified setting. This method is currently used only in `overwriteRwSetting`. These changes ensure that updates are race-free across all clients. ACKs for top commit: achow101: ACK 1b41d45 furszy: self-code-ACK bitcoin@1b41d45 Tree-SHA512: 50cda612b782aeb5e03e2cf63cc44779a013de1c535b883b57af4de22f24b0de80b4edecbcda235413baec0a12bdf0e5750fb6731c9e67d32e742d8c63f08c13
2 parents c6d2d1c + 1b41d45 commit 78567b0

File tree

5 files changed

+100
-25
lines changed

5 files changed

+100
-25
lines changed

src/interfaces/chain.h

+21-3
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,17 @@ struct BlockInfo {
9696
BlockInfo(const uint256& hash LIFETIMEBOUND) : hash(hash) {}
9797
};
9898

99+
//! The action to be taken after updating a settings value.
100+
//! WRITE indicates that the updated value must be written to disk,
101+
//! while SKIP_WRITE indicates that the change will be kept in memory-only
102+
//! without persisting it.
103+
enum class SettingsAction {
104+
WRITE,
105+
SKIP_WRITE
106+
};
107+
108+
using SettingsUpdate = std::function<std::optional<interfaces::SettingsAction>(common::SettingsValue&)>;
109+
99110
//! Interface giving clients (wallet processes, maybe other analysis tools in
100111
//! the future) ability to access to the chain state, receive notifications,
101112
//! estimate fees, and submit transactions.
@@ -344,9 +355,16 @@ class Chain
344355
//! Return <datadir>/settings.json setting value.
345356
virtual common::SettingsValue getRwSetting(const std::string& name) = 0;
346357

347-
//! Write a setting to <datadir>/settings.json. Optionally just update the
348-
//! setting in memory and do not write the file.
349-
virtual bool updateRwSetting(const std::string& name, const common::SettingsValue& value, bool write=true) = 0;
358+
//! Updates a setting in <datadir>/settings.json.
359+
//! Depending on the action returned by the update function, this will either
360+
//! update the setting in memory or write the updated settings to disk.
361+
virtual bool updateRwSetting(const std::string& name, const SettingsUpdate& update_function) = 0;
362+
363+
//! Replace a setting in <datadir>/settings.json with a new value.
364+
virtual bool overwriteRwSetting(const std::string& name, common::SettingsValue& value, bool write = true) = 0;
365+
366+
//! Delete a given setting in <datadir>/settings.json.
367+
virtual bool deleteRwSettings(const std::string& name, bool write = true) = 0;
350368

351369
//! Synchronously send transactionAddedToMempool notifications about all
352370
//! current mempool transactions to the specified handler and return after

src/node/interfaces.cpp

+24-6
Original file line numberDiff line numberDiff line change
@@ -814,14 +814,32 @@ class ChainImpl : public Chain
814814
});
815815
return result;
816816
}
817-
bool updateRwSetting(const std::string& name, const common::SettingsValue& value, bool write) override
817+
bool updateRwSetting(const std::string& name,
818+
const interfaces::SettingsUpdate& update_settings_func) override
818819
{
820+
std::optional<interfaces::SettingsAction> action;
819821
args().LockSettings([&](common::Settings& settings) {
820-
if (value.isNull()) {
821-
settings.rw_settings.erase(name);
822-
} else {
823-
settings.rw_settings[name] = value;
824-
}
822+
auto* ptr_value = common::FindKey(settings.rw_settings, name);
823+
// Create value if it doesn't exist
824+
auto& value = ptr_value ? *ptr_value : settings.rw_settings[name];
825+
action = update_settings_func(value);
826+
});
827+
if (!action) return false;
828+
// Now dump value to disk if requested
829+
return *action == interfaces::SettingsAction::SKIP_WRITE || args().WriteSettingsFile();
830+
}
831+
bool overwriteRwSetting(const std::string& name, common::SettingsValue& value, bool write) override
832+
{
833+
if (value.isNull()) return deleteRwSettings(name, write);
834+
return updateRwSetting(name, [&](common::SettingsValue& settings) {
835+
settings = std::move(value);
836+
return write ? interfaces::SettingsAction::WRITE : interfaces::SettingsAction::SKIP_WRITE;
837+
});
838+
}
839+
bool deleteRwSettings(const std::string& name, bool write) override
840+
{
841+
args().LockSettings([&](common::Settings& settings) {
842+
settings.rw_settings.erase(name);
825843
});
826844
return !write || args().WriteSettingsFile();
827845
}

src/wallet/load.cpp

+1-1
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ bool VerifyWallets(WalletContext& context)
6969
// Pass write=false because no need to write file and probably
7070
// better not to. If unnamed wallet needs to be added next startup
7171
// and the setting is empty, this code will just run again.
72-
chain.updateRwSetting("wallet", wallets, /* write= */ false);
72+
chain.overwriteRwSetting("wallet", wallets, /*write=*/false);
7373
}
7474
}
7575

src/wallet/test/wallet_tests.cpp

+34
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,40 @@ BOOST_FIXTURE_TEST_CASE(importwallet_rescan, TestChain100Setup)
329329
}
330330
}
331331

332+
// This test verifies that wallet settings can be added and removed
333+
// concurrently, ensuring no race conditions occur during either process.
334+
BOOST_FIXTURE_TEST_CASE(write_wallet_settings_concurrently, TestingSetup)
335+
{
336+
WalletContext context;
337+
context.chain = m_node.chain.get();
338+
const auto NUM_WALLETS{5};
339+
340+
// Since we're counting the number of wallets, ensure we start without any.
341+
BOOST_REQUIRE(context.chain->getRwSetting("wallet").isNull());
342+
343+
const auto& check_concurrent_wallet = [&](const auto& settings_function, int num_expected_wallets) {
344+
std::vector<std::thread> threads;
345+
threads.reserve(NUM_WALLETS);
346+
for (auto i{0}; i < NUM_WALLETS; ++i) threads.emplace_back(settings_function, i);
347+
for (auto& t : threads) t.join();
348+
349+
auto wallets = context.chain->getRwSetting("wallet");
350+
BOOST_CHECK_EQUAL(wallets.getValues().size(), num_expected_wallets);
351+
};
352+
353+
// Add NUM_WALLETS wallets concurrently, ensure we end up with NUM_WALLETS stored.
354+
check_concurrent_wallet([&context](int i) {
355+
Assert(AddWalletSetting(*context.chain, strprintf("wallet_%d", i)));
356+
},
357+
/*num_expected_wallets=*/NUM_WALLETS);
358+
359+
// Remove NUM_WALLETS wallets concurrently, ensure we end up with 0 wallets.
360+
check_concurrent_wallet([&context](int i) {
361+
Assert(RemoveWalletSetting(*context.chain, strprintf("wallet_%d", i)));
362+
},
363+
/*num_expected_wallets=*/0);
364+
}
365+
332366
// Check that GetImmatureCredit() returns a newly calculated value instead of
333367
// the cached value after a MarkDirty() call.
334368
//

src/wallet/wallet.cpp

+20-15
Original file line numberDiff line numberDiff line change
@@ -93,25 +93,30 @@ namespace wallet {
9393

9494
bool AddWalletSetting(interfaces::Chain& chain, const std::string& wallet_name)
9595
{
96-
common::SettingsValue setting_value = chain.getRwSetting("wallet");
97-
if (!setting_value.isArray()) setting_value.setArray();
98-
for (const common::SettingsValue& value : setting_value.getValues()) {
99-
if (value.isStr() && value.get_str() == wallet_name) return true;
100-
}
101-
setting_value.push_back(wallet_name);
102-
return chain.updateRwSetting("wallet", setting_value);
96+
const auto update_function = [&wallet_name](common::SettingsValue& setting_value) {
97+
if (!setting_value.isArray()) setting_value.setArray();
98+
for (const auto& value : setting_value.getValues()) {
99+
if (value.isStr() && value.get_str() == wallet_name) return interfaces::SettingsAction::SKIP_WRITE;
100+
}
101+
setting_value.push_back(wallet_name);
102+
return interfaces::SettingsAction::WRITE;
103+
};
104+
return chain.updateRwSetting("wallet", update_function);
103105
}
104106

105107
bool RemoveWalletSetting(interfaces::Chain& chain, const std::string& wallet_name)
106108
{
107-
common::SettingsValue setting_value = chain.getRwSetting("wallet");
108-
if (!setting_value.isArray()) return true;
109-
common::SettingsValue new_value(common::SettingsValue::VARR);
110-
for (const common::SettingsValue& value : setting_value.getValues()) {
111-
if (!value.isStr() || value.get_str() != wallet_name) new_value.push_back(value);
112-
}
113-
if (new_value.size() == setting_value.size()) return true;
114-
return chain.updateRwSetting("wallet", new_value);
109+
const auto update_function = [&wallet_name](common::SettingsValue& setting_value) {
110+
if (!setting_value.isArray()) return interfaces::SettingsAction::SKIP_WRITE;
111+
common::SettingsValue new_value(common::SettingsValue::VARR);
112+
for (const auto& value : setting_value.getValues()) {
113+
if (!value.isStr() || value.get_str() != wallet_name) new_value.push_back(value);
114+
}
115+
if (new_value.size() == setting_value.size()) return interfaces::SettingsAction::SKIP_WRITE;
116+
setting_value = std::move(new_value);
117+
return interfaces::SettingsAction::WRITE;
118+
};
119+
return chain.updateRwSetting("wallet", update_function);
115120
}
116121

117122
static void UpdateWalletSetting(interfaces::Chain& chain,

0 commit comments

Comments
 (0)