From b7113e6f42c220792c2a9808943b714eb72c4f3e Mon Sep 17 00:00:00 2001 From: Robert Hamilton Date: Sat, 5 Sep 2026 19:58:40 -0500 Subject: [PATCH 1/2] test: characterize disabled wallet settings Wallet RPCs report errors with -nosettings after changing wallet state. Check these results alongside wallet usability, unchanged settings.json, and restored startup preferences when settings are enabled again. Reuse an existing wallet for loading and check explicit unloading last, so the sequence does not depend on the skipped unload completion wait. --- test/functional/wallet_startup.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/test/functional/wallet_startup.py b/test/functional/wallet_startup.py index 2c5fb259059..bc281c23b50 100755 --- a/test/functional/wallet_startup.py +++ b/test/functional/wallet_startup.py @@ -64,6 +64,35 @@ class WalletStartupTest(BitcoinTestFramework): # Reset directory permissions for cleanup dir_path.chmod(original_dir_perms) + def test_disabled_settings(self, node): + self.log.info("Test wallet startup preferences with dynamic settings disabled") + load_message = "Attempt to write settings file when dynamic settings are disabled." + + settings_path = node.chain_path / "settings.json" + settings_before = settings_path.read_bytes() + self.restart_node(0, extra_args=["-nosettings"]) + assert_equal(node.listwallets(), ['']) + + assert_raises_rpc_error(-1, load_message, node.createwallet, wallet_name="no_settings", load_on_startup=True) # TODO: Report a warning after creating the wallet + assert_equal(set(node.listwallets()), {'', 'no_settings'}) + + # Leaving the startup preference unchanged does not warn, and the wallet remains usable. + assert_equal(node.unloadwallet(wallet_name="no_settings"), {}) + assert_equal(node.loadwallet(filename="no_settings"), {"name": "no_settings"}) + assert_equal(node.get_wallet_rpc("no_settings").getwalletinfo()["walletname"], "no_settings") + + assert_raises_rpc_error(-1, load_message, node.loadwallet, filename="w2", load_on_startup=True) # TODO: Report a warning after loading the wallet + assert_equal(set(node.listwallets()), {'', 'no_settings', 'w2'}) + + assert_raises_rpc_error(-1, "Attempt to write settings file when dynamic settings are disabled.", node.unloadwallet, wallet_name="no_settings", load_on_startup=False) # TODO: Report a warning after unloading the wallet + assert_equal(set(node.listwallets()), {'', 'w2'}) + self.stop_node(0) + assert_equal(settings_path.read_bytes(), settings_before) + + # Re-enabling settings restores the original startup preferences. + self.start_node(0) + assert_equal(set(node.listwallets()), {'w2', 'w3'}) + def run_test(self): self.log.info('Should start without any wallets') assert_equal(self.nodes[0].listwallets(), []) @@ -94,6 +123,7 @@ class WalletStartupTest(BitcoinTestFramework): assert_equal(set(self.nodes[0].listwallets()), set(('w2', 'w3'))) self.test_load_unwritable_wallet(self.nodes[0]) + self.test_disabled_settings(self.nodes[0]) if __name__ == '__main__': WalletStartupTest(__file__).main() From a34fc8b11a420c1ed9877a5dd73aab57aba17cd0 Mon Sep 17 00:00:00 2001 From: Robert Hamilton Date: Sat, 5 Sep 2026 19:58:40 -0500 Subject: [PATCH 2/2] wallet: handle disabled startup settings Return a persistence failure when dynamic settings are disabled so wallet operations finish with their existing startup-setting warning. This avoids an uncaught exception in Qt and RPC errors after the wallet state has already changed. Keep in-memory and no-op updates unchanged. --- src/interfaces/chain.h | 3 +++ src/node/interfaces.cpp | 2 +- test/functional/wallet_startup.py | 8 ++++---- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/interfaces/chain.h b/src/interfaces/chain.h index d713ee7c0a8..1ee433a02a6 100644 --- a/src/interfaces/chain.h +++ b/src/interfaces/chain.h @@ -358,6 +358,9 @@ public: //! support for writing null values to 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. + //! Returns false if the update function returned no action, or if the + //! settings could not be written to disk, including when settings are + //! disabled with -nosettings. In-memory changes are kept either way. virtual bool updateRwSetting(const std::string& name, const SettingsUpdate& update_function) = 0; //! Replace a setting in /settings.json with a new value. diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp index 130f9602bc6..c1dd8e7a4dd 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -843,7 +843,7 @@ public: }); if (!action) return false; // Now dump value to disk if requested - return *action != interfaces::SettingsAction::WRITE || args().WriteSettingsFile(); + return *action != interfaces::SettingsAction::WRITE || (args().GetSettingsPath() && args().WriteSettingsFile()); } bool overwriteRwSetting(const std::string& name, common::SettingsValue value, interfaces::SettingsAction action) override { diff --git a/test/functional/wallet_startup.py b/test/functional/wallet_startup.py index bc281c23b50..ed07bb044a4 100755 --- a/test/functional/wallet_startup.py +++ b/test/functional/wallet_startup.py @@ -66,14 +66,14 @@ class WalletStartupTest(BitcoinTestFramework): def test_disabled_settings(self, node): self.log.info("Test wallet startup preferences with dynamic settings disabled") - load_message = "Attempt to write settings file when dynamic settings are disabled." + load_message = "Wallet load on startup setting could not be updated, so wallet may not be loaded next node startup." settings_path = node.chain_path / "settings.json" settings_before = settings_path.read_bytes() self.restart_node(0, extra_args=["-nosettings"]) assert_equal(node.listwallets(), ['']) - assert_raises_rpc_error(-1, load_message, node.createwallet, wallet_name="no_settings", load_on_startup=True) # TODO: Report a warning after creating the wallet + assert_equal(node.createwallet(wallet_name="no_settings", load_on_startup=True), {"name": "no_settings", "warnings": [load_message]}) assert_equal(set(node.listwallets()), {'', 'no_settings'}) # Leaving the startup preference unchanged does not warn, and the wallet remains usable. @@ -81,10 +81,10 @@ class WalletStartupTest(BitcoinTestFramework): assert_equal(node.loadwallet(filename="no_settings"), {"name": "no_settings"}) assert_equal(node.get_wallet_rpc("no_settings").getwalletinfo()["walletname"], "no_settings") - assert_raises_rpc_error(-1, load_message, node.loadwallet, filename="w2", load_on_startup=True) # TODO: Report a warning after loading the wallet + assert_equal(node.loadwallet(filename="w2", load_on_startup=True), {"name": "w2", "warnings": [load_message]}) assert_equal(set(node.listwallets()), {'', 'no_settings', 'w2'}) - assert_raises_rpc_error(-1, "Attempt to write settings file when dynamic settings are disabled.", node.unloadwallet, wallet_name="no_settings", load_on_startup=False) # TODO: Report a warning after unloading the wallet + assert_equal(node.unloadwallet(wallet_name="no_settings", load_on_startup=False), {"warnings": ["Wallet load on startup setting could not be updated, so wallet may still be loaded next node startup."]}) assert_equal(set(node.listwallets()), {'', 'w2'}) self.stop_node(0) assert_equal(settings_path.read_bytes(), settings_before)