Merge bitcoin/bitcoin#30697: Bugfix: Ensure Atomicity in Wallet Settings Updates from Chain Interface

1b41d45d46 wallet: bugfix: ensure atomicity in settings updates (ismaelsadeeq)

Pull request description:

  This PR fixes #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 1b41d45d46
  furszy:
    self-code-ACK 1b41d45d46

Tree-SHA512: 50cda612b782aeb5e03e2cf63cc44779a013de1c535b883b57af4de22f24b0de80b4edecbcda235413baec0a12bdf0e5750fb6731c9e67d32e742d8c63f08c13
This commit is contained in:
Ava Chow
2024-08-27 12:29:20 -04:00
5 changed files with 100 additions and 25 deletions

View File

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

View File

@@ -329,6 +329,40 @@ BOOST_FIXTURE_TEST_CASE(importwallet_rescan, TestChain100Setup)
}
}
// This test verifies that wallet settings can be added and removed
// concurrently, ensuring no race conditions occur during either process.
BOOST_FIXTURE_TEST_CASE(write_wallet_settings_concurrently, TestingSetup)
{
WalletContext context;
context.chain = m_node.chain.get();
const auto NUM_WALLETS{5};
// Since we're counting the number of wallets, ensure we start without any.
BOOST_REQUIRE(context.chain->getRwSetting("wallet").isNull());
const auto& check_concurrent_wallet = [&](const auto& settings_function, int num_expected_wallets) {
std::vector<std::thread> threads;
threads.reserve(NUM_WALLETS);
for (auto i{0}; i < NUM_WALLETS; ++i) threads.emplace_back(settings_function, i);
for (auto& t : threads) t.join();
auto wallets = context.chain->getRwSetting("wallet");
BOOST_CHECK_EQUAL(wallets.getValues().size(), num_expected_wallets);
};
// Add NUM_WALLETS wallets concurrently, ensure we end up with NUM_WALLETS stored.
check_concurrent_wallet([&context](int i) {
Assert(AddWalletSetting(*context.chain, strprintf("wallet_%d", i)));
},
/*num_expected_wallets=*/NUM_WALLETS);
// Remove NUM_WALLETS wallets concurrently, ensure we end up with 0 wallets.
check_concurrent_wallet([&context](int i) {
Assert(RemoveWalletSetting(*context.chain, strprintf("wallet_%d", i)));
},
/*num_expected_wallets=*/0);
}
// Check that GetImmatureCredit() returns a newly calculated value instead of
// the cached value after a MarkDirty() call.
//

View File

@@ -93,25 +93,30 @@ namespace wallet {
bool AddWalletSetting(interfaces::Chain& chain, const std::string& wallet_name)
{
common::SettingsValue setting_value = chain.getRwSetting("wallet");
if (!setting_value.isArray()) setting_value.setArray();
for (const common::SettingsValue& value : setting_value.getValues()) {
if (value.isStr() && value.get_str() == wallet_name) return true;
}
setting_value.push_back(wallet_name);
return chain.updateRwSetting("wallet", setting_value);
const auto update_function = [&wallet_name](common::SettingsValue& setting_value) {
if (!setting_value.isArray()) setting_value.setArray();
for (const auto& value : setting_value.getValues()) {
if (value.isStr() && value.get_str() == wallet_name) return interfaces::SettingsAction::SKIP_WRITE;
}
setting_value.push_back(wallet_name);
return interfaces::SettingsAction::WRITE;
};
return chain.updateRwSetting("wallet", update_function);
}
bool RemoveWalletSetting(interfaces::Chain& chain, const std::string& wallet_name)
{
common::SettingsValue setting_value = chain.getRwSetting("wallet");
if (!setting_value.isArray()) return true;
common::SettingsValue new_value(common::SettingsValue::VARR);
for (const common::SettingsValue& value : setting_value.getValues()) {
if (!value.isStr() || value.get_str() != wallet_name) new_value.push_back(value);
}
if (new_value.size() == setting_value.size()) return true;
return chain.updateRwSetting("wallet", new_value);
const auto update_function = [&wallet_name](common::SettingsValue& setting_value) {
if (!setting_value.isArray()) return interfaces::SettingsAction::SKIP_WRITE;
common::SettingsValue new_value(common::SettingsValue::VARR);
for (const auto& value : setting_value.getValues()) {
if (!value.isStr() || value.get_str() != wallet_name) new_value.push_back(value);
}
if (new_value.size() == setting_value.size()) return interfaces::SettingsAction::SKIP_WRITE;
setting_value = std::move(new_value);
return interfaces::SettingsAction::WRITE;
};
return chain.updateRwSetting("wallet", update_function);
}
static void UpdateWalletSetting(interfaces::Chain& chain,