Skip to content

Commit 1b41d45

Browse files
ismaelsadeeqfurszy
andcommitted
wallet: bugfix: ensure atomicity in settings updates
- Settings updates were not thread-safe, as they were executed in three separate steps: 1) Obtain settings value while acquiring the settings lock. 2) Modify settings value. 3) Overwrite settings value while acquiring the settings lock. This approach allowed concurrent threads to modify the same base value simultaneously, leading to data loss. When this occurred, the final settings state would only reflect the changes from the last thread that completed the operation, overwriting updates from other threads. Fix this by making the settings update operation atomic. - Add test coverage for this behavior. Co-authored-by: furszy <[email protected]>
1 parent ee36717 commit 1b41d45

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)