diff options
author | ismaelsadeeq <ask4ismailsadiq@gmail.com> | 2024-08-26 10:32:56 +0100 |
---|---|---|
committer | ismaelsadeeq <ask4ismailsadiq@gmail.com> | 2024-08-26 13:41:56 +0100 |
commit | 1b41d45d462d856a9d0b44ae0039bbb2cd78407c (patch) | |
tree | 3823c9a9d5787595c956015c589412624faeed21 /src | |
parent | ee367170cb2acf82b6ff8e0ccdbc1cce09730662 (diff) |
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 <matiasfurszyfer@protonmail.com>
Diffstat (limited to 'src')
-rw-r--r-- | src/interfaces/chain.h | 24 | ||||
-rw-r--r-- | src/node/interfaces.cpp | 30 | ||||
-rw-r--r-- | src/wallet/load.cpp | 2 | ||||
-rw-r--r-- | src/wallet/test/wallet_tests.cpp | 34 | ||||
-rw-r--r-- | src/wallet/wallet.cpp | 35 |
5 files changed, 100 insertions, 25 deletions
diff --git a/src/interfaces/chain.h b/src/interfaces/chain.h index af45f81f95..be596b1765 100644 --- a/src/interfaces/chain.h +++ b/src/interfaces/chain.h @@ -96,6 +96,17 @@ struct BlockInfo { BlockInfo(const uint256& hash LIFETIMEBOUND) : hash(hash) {} }; +//! The action to be taken after updating a settings value. +//! WRITE indicates that the updated value must be written to disk, +//! while SKIP_WRITE indicates that the change will be kept in memory-only +//! without persisting it. +enum class SettingsAction { + WRITE, + SKIP_WRITE +}; + +using SettingsUpdate = std::function<std::optional<interfaces::SettingsAction>(common::SettingsValue&)>; + //! Interface giving clients (wallet processes, maybe other analysis tools in //! the future) ability to access to the chain state, receive notifications, //! estimate fees, and submit transactions. @@ -344,9 +355,16 @@ public: //! Return <datadir>/settings.json setting value. virtual common::SettingsValue getRwSetting(const std::string& name) = 0; - //! Write a setting to <datadir>/settings.json. Optionally just update the - //! setting in memory and do not write the file. - virtual bool updateRwSetting(const std::string& name, const common::SettingsValue& value, bool write=true) = 0; + //! Updates a setting in <datadir>/settings.json. + //! Depending on the action returned by the update function, this will either + //! update the setting in memory or write the updated settings to disk. + virtual bool updateRwSetting(const std::string& name, const SettingsUpdate& update_function) = 0; + + //! Replace a setting in <datadir>/settings.json with a new value. + virtual bool overwriteRwSetting(const std::string& name, common::SettingsValue& value, bool write = true) = 0; + + //! Delete a given setting in <datadir>/settings.json. + virtual bool deleteRwSettings(const std::string& name, bool write = true) = 0; //! Synchronously send transactionAddedToMempool notifications about all //! current mempool transactions to the specified handler and return after diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp index 9fe08eb3dd..54b986c926 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -814,14 +814,32 @@ public: }); return result; } - bool updateRwSetting(const std::string& name, const common::SettingsValue& value, bool write) override + bool updateRwSetting(const std::string& name, + const interfaces::SettingsUpdate& update_settings_func) override { + std::optional<interfaces::SettingsAction> action; args().LockSettings([&](common::Settings& settings) { - if (value.isNull()) { - settings.rw_settings.erase(name); - } else { - settings.rw_settings[name] = value; - } + auto* ptr_value = common::FindKey(settings.rw_settings, name); + // Create value if it doesn't exist + auto& value = ptr_value ? *ptr_value : settings.rw_settings[name]; + action = update_settings_func(value); + }); + if (!action) return false; + // Now dump value to disk if requested + return *action == interfaces::SettingsAction::SKIP_WRITE || args().WriteSettingsFile(); + } + bool overwriteRwSetting(const std::string& name, common::SettingsValue& value, bool write) override + { + if (value.isNull()) return deleteRwSettings(name, write); + return updateRwSetting(name, [&](common::SettingsValue& settings) { + settings = std::move(value); + return write ? interfaces::SettingsAction::WRITE : interfaces::SettingsAction::SKIP_WRITE; + }); + } + bool deleteRwSettings(const std::string& name, bool write) override + { + args().LockSettings([&](common::Settings& settings) { + settings.rw_settings.erase(name); }); return !write || args().WriteSettingsFile(); } diff --git a/src/wallet/load.cpp b/src/wallet/load.cpp index e26347d437..129b5c7c2a 100644 --- a/src/wallet/load.cpp +++ b/src/wallet/load.cpp @@ -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); } } diff --git a/src/wallet/test/wallet_tests.cpp b/src/wallet/test/wallet_tests.cpp index 44ffddb168..12d5a3b3eb 100644 --- a/src/wallet/test/wallet_tests.cpp +++ b/src/wallet/test/wallet_tests.cpp @@ -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. // diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 5584b43520..e2d7429f94 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -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, |