wallet: persist set_daemon params across wallet loads. - allow set_daemon without a wallet loaded
What changed, and why it matters
This Monero commit changes how the wallet remembers which daemon (server) it should connect to. Previously, if you told the wallet RPC server to use a specific daemon before any wallet was open, that setting was lost as soon as you opened or created a wallet. Now that setting is saved and automatically applied to wallets opened or created later. The change also lets you call set_daemon even when no wallet is loaded. There is one small security-relevant side effect: because the daemon settings now persist, a test helper that temporarily switches to a wrong daemon had to explicitly restore the 'trusted' flag afterward, otherwise the restored daemon would be treated as untrusted. The commit itself is a feature/fix for configuration persistence, not an exploit.
Review as a normal feature/fix commit. Verify that the wallet RPC server's set_daemon endpoint properly validates req.address and SSL options when no wallet is loaded, since a stored invalid or malicious pending daemon config could be applied to subsequently opened wallets. Confirm that the trusted-daemon persistence does not inadvertently downgrade security for wallets that expect an untrusted default. No immediate security patch is indicated by the diff alone.
Security signals we found
Daemon connection parameters now persist across wallet creation/opening via m_pending_daemon
set_daemon can now be invoked before any wallet is loaded, storing config server-side
Trusted-daemon flag is now part of persisted daemon config and can change implicitly across wallet reloads
Functional test updated to explicitly restore trusted=True after daemon switch, indicating trust state is now sticky
Evidence from the diff
The patch introduces a wallet2::daemon_config struct and an optional daemon_override argument to wallet2::make_new/make_from_file/make_basic. The wallet RPC server stores the last set_daemon request in m_pending_daemon and passes it when creating or opening wallets, so daemon address, login, proxy, trusted flag, and SSL options persist across wallet loads. It also allows set_daemon to be called when m_wallet is null by validating the request and storing it for later use. A minor ordering change in http_client.h moves set_ssl before setting host/port/auth. The functional test WrongDaemonGuard now explicitly passes trusted=True when restoring the correct daemon because the persisted config would otherwise leave the daemon untrusted.
Changed components
src/wallet/wallet2.cppsrc/wallet/wallet2.hsrc/wallet/wallet_rpc_server.cppsrc/wallet/wallet_rpc_server.hcontrib/epee/include/net/http_client.htests/functional_tests/multisig.pyInspect captured patch +101 / −31
diff --git a/contrib/epee/include/net/http_client.h b/contrib/epee/include/net/http_client.h
index fc1360a..ff3b0ce 100644
--- a/contrib/epee/include/net/http_client.h
+++ b/contrib/epee/include/net/http_client.h
@@ -124,10 +124,10 @@ namespace net_utils
{
CRITICAL_REGION_LOCAL(m_lock);
disconnect();
+ m_net_client.set_ssl(std::move(ssl_options));
m_host_buff = std::move(host);
m_port = std::move(port);
m_auth = user ? http_client_auth{std::move(*user)} : http_client_auth{};
- m_net_client.set_ssl(std::move(ssl_options));
}
void set_auto_connect(bool auto_connect) override
diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp
index 760e2fa..d2ca4b6 100644
--- a/src/wallet/wallet2.cpp
+++ b/src/wallet/wallet2.cpp
@@ -326,7 +326,7 @@ std::string get_weight_string(const cryptonote::transaction &tx, size_t blob_siz
return get_weight_string(get_transaction_weight(tx, blob_size));
}
-std::unique_ptr<tools::wallet2> make_basic(const boost::program_options::variables_map& vm, bool unattended, const options& opts, const std::function<boost::optional<tools::password_container>(const char *, bool)> &password_prompter)
+std::unique_ptr<tools::wallet2> make_basic(const boost::program_options::variables_map& vm, bool unattended, const options& opts, const std::function<boost::optional<tools::password_container>(const char *, bool)> &password_prompter, const boost::optional<tools::wallet2::daemon_config>& daemon_override = boost::none)
{
const bool testnet = command_line::get_arg(vm, opts.testnet);
const bool stagenet = command_line::get_arg(vm, opts.stagenet);
@@ -455,8 +455,20 @@ std::unique_ptr<tools::wallet2> make_basic(const boost::program_options::variabl
THROW_WALLET_EXCEPTION_IF(!command_line::is_arg_defaulted(vm, opts.trusted_daemon) && !command_line::is_arg_defaulted(vm, opts.untrusted_daemon),
tools::error::wallet_internal_error, tools::wallet2::tr("--trusted-daemon and --untrusted-daemon are both seen, assuming untrusted"));
- // set --trusted-daemon if local and not overridden
- if (!trusted_daemon)
+ // a set_daemon issued before this wallet existed replaces the daemon connection wholesale
+ if (daemon_override)
+ {
+ daemon_address = daemon_override->address;
+ login = boost::none;
+ if (!daemon_override->username.empty() || !daemon_override->password.empty())
+ login.emplace(daemon_override->username, daemon_override->password);
+ proxy = daemon_override->proxy;
+ trusted_daemon = daemon_override->trusted;
+ ssl_options = daemon_override->ssl_options;
+ }
+
+ // set --trusted-daemon if local and not overridden by command line or set_daemon
+ if (!trusted_daemon.is_initialized())
{
try
{
@@ -1349,7 +1361,7 @@ std::pair<std::unique_ptr<wallet2>, tools::password_container> wallet2::make_fro
}
std::pair<std::unique_ptr<wallet2>, password_container> wallet2::make_from_file(
- const boost::program_options::variables_map& vm, bool unattended, const std::string& wallet_file, const std::function<boost::optional<tools::password_container>(const char *, bool)> &password_prompter)
+ const boost::program_options::variables_map& vm, bool unattended, const std::string& wallet_file, const std::function<boost::optional<tools::password_container>(const char *, bool)> &password_prompter, const boost::optional<daemon_config>& daemon_override)
{
const options opts{};
auto pwd = get_password(vm, opts, password_prompter, false);
@@ -1357,7 +1369,7 @@ std::pair<std::unique_ptr<wallet2>, password_container> wallet2::make_from_file(
{
return {nullptr, password_container{}};
}
- auto wallet = make_basic(vm, unattended, opts, password_prompter);
+ auto wallet = make_basic(vm, unattended, opts, password_prompter, daemon_override);
if (wallet && !wallet_file.empty())
{
wallet->load(wallet_file, pwd->password());
@@ -1365,7 +1377,7 @@ std::pair<std::unique_ptr<wallet2>, password_container> wallet2::make_from_file(
return {std::move(wallet), std::move(*pwd)};
}
-std::pair<std::unique_ptr<wallet2>, password_container> wallet2::make_new(const boost::program_options::variables_map& vm, bool unattended, const std::function<boost::optional<password_container>(const char *, bool)> &password_prompter)
+std::pair<std::unique_ptr<wallet2>, password_container> wallet2::make_new(const boost::program_options::variables_map& vm, bool unattended, const std::function<boost::optional<password_container>(const char *, bool)> &password_prompter, const boost::optional<daemon_config>& daemon_override)
{
const options opts{};
auto pwd = get_password(vm, opts, password_prompter, true);
@@ -1373,7 +1385,7 @@ std::pair<std::unique_ptr<wallet2>, password_container> wallet2::make_new(const
{
return {nullptr, password_container{}};
}
- return {make_basic(vm, unattended, opts, password_prompter), std::move(*pwd)};
+ return {make_basic(vm, unattended, opts, password_prompter, daemon_override), std::move(*pwd)};
}
std::unique_ptr<wallet2> wallet2::make_dummy(const boost::program_options::variables_map& vm, bool unattended, const std::function<boost::optional<tools::password_container>(const char *, bool)> &password_prompter)
@@ -1393,6 +1405,22 @@ bool wallet2::set_daemon(std::string daemon_address, boost::optional<epee::net_u
if(m_http_client->is_connected())
m_http_client->disconnect();
+
+ if (proxy.empty())
+ MINFO("setting daemon to " << daemon_address);
+ else
+ MINFO("setting daemon to " << daemon_address << ". Connecting via proxy @ " << proxy);
+ try
+ {
+ if (!m_http_client->set_server(daemon_address, daemon_login, std::move(ssl_options)))
+ return false;
+ }
+ catch (const std::exception &e)
+ {
+ LOG_ERROR("failed to set daemon to " << daemon_address << ": " << e.what());
+ return false;
+ }
+
CHECK_AND_ASSERT_MES(set_proxy(proxy), false, "failed to set proxy address");
m_proxy = proxy;
const bool changed = m_daemon_address != daemon_address;
@@ -1406,15 +1434,11 @@ bool wallet2::set_daemon(std::string daemon_address, boost::optional<epee::net_u
m_pool_info_query_time = 0;
}
- const std::string address = get_daemon_address();
- MINFO("setting daemon to " << address);
- bool ret = m_http_client->set_server(address, get_daemon_login(), std::move(ssl_options));
- if (ret)
{
CRITICAL_REGION_LOCAL(default_daemon_address_lock);
- default_daemon_address = address;
+ default_daemon_address = m_daemon_address;
}
- return ret;
+ return true;
}
//----------------------------------------------------------------------------------------------------
bool wallet2::set_proxy(const std::string &address)
diff --git a/src/wallet/wallet2.h b/src/wallet/wallet2.h
index c15e5df..df6d478 100644
--- a/src/wallet/wallet2.h
+++ b/src/wallet/wallet2.h
@@ -221,15 +221,26 @@ private:
static std::string device_derivation_path_option(const boost::program_options::variables_map &vm);
static void init_options(boost::program_options::options_description& desc_params);
+ //! Daemon connection settings that override the ones from the command line when passed to make_new/make_from_file.
+ struct daemon_config
+ {
+ std::string address;
+ std::string username;
+ std::string password;
+ std::string proxy;
+ bool trusted = false;
+ epee::net_utils::ssl_options_t ssl_options = epee::net_utils::ssl_support_t::e_ssl_support_autodetect;
+ };
+
//! Uses stdin and stdout. Returns a wallet2 if no errors.
static std::pair<std::unique_ptr<wallet2>, password_container> make_from_json(const boost::program_options::variables_map& vm, bool unattended, const std::string& json_file, const std::function<boost::optional<password_container>(const char *, bool)> &password_prompter);
//! Uses stdin and stdout. Returns a wallet2 and password for `wallet_file` if no errors.
static std::pair<std::unique_ptr<wallet2>, password_container>
- make_from_file(const boost::program_options::variables_map& vm, bool unattended, const std::string& wallet_file, const std::function<boost::optional<password_container>(const char *, bool)> &password_prompter);
+ make_from_file(const boost::program_options::variables_map& vm, bool unattended, const std::string& wallet_file, const std::function<boost::optional<password_container>(const char *, bool)> &password_prompter, const boost::optional<daemon_config>& daemon_override = boost::none);
//! Uses stdin and stdout. Returns a wallet2 and password for wallet with no file if no errors.
- static std::pair<std::unique_ptr<wallet2>, password_container> make_new(const boost::program_options::variables_map& vm, bool unattended, const std::function<boost::optional<password_container>(const char *, bool)> &password_prompter);
+ static std::pair<std::unique_ptr<wallet2>, password_container> make_new(const boost::program_options::variables_map& vm, bool unattended, const std::function<boost::optional<password_container>(const char *, bool)> &password_prompter, const boost::optional<daemon_config>& daemon_override = boost::none);
//! Just parses variables.
static std::unique_ptr<wallet2> make_dummy(const boost::program_options::variables_map& vm, bool unattended, const std::function<boost::optional<password_container>(const char *, bool)> &password_prompter);
diff --git a/src/wallet/wallet_rpc_server.cpp b/src/wallet/wallet_rpc_server.cpp
index 8b51019..0bc516b 100644
--- a/src/wallet/wallet_rpc_server.cpp
+++ b/src/wallet/wallet_rpc_server.cpp
@@ -3643,7 +3643,7 @@ namespace tools
command_line::add_arg(desc, arg_password);
po::store(po::parse_command_line(argc, argv, desc), vm2);
}
- std::unique_ptr<tools::wallet2> wal = tools::wallet2::make_new(vm2, true, nullptr).first;
+ std::unique_ptr<tools::wallet2> wal = tools::wallet2::make_new(vm2, true, nullptr, m_pending_daemon).first;
if (!wal)
{
er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR;
@@ -3747,7 +3747,7 @@ namespace tools
}
std::unique_ptr<tools::wallet2> wal = nullptr;
try {
- wal = tools::wallet2::make_from_file(vm2, true, wallet_file, nullptr).first;
+ wal = tools::wallet2::make_from_file(vm2, true, wallet_file, nullptr, m_pending_daemon).first;
}
catch (const std::exception& e)
{
@@ -3981,8 +3981,7 @@ namespace tools
command_line::add_arg(desc, arg_password);
po::store(po::parse_command_line(argc, argv, desc), vm2);
}
-
- auto rc = tools::wallet2::make_new(vm2, true, nullptr);
+ auto rc = tools::wallet2::make_new(vm2, true, nullptr, m_pending_daemon);
std::unique_ptr<wallet2> wal;
wal = std::move(rc.first);
if (!wal)
@@ -4203,8 +4202,7 @@ namespace tools
command_line::add_arg(desc, arg_password);
po::store(po::parse_command_line(argc, argv, desc), vm2);
}
-
- auto rc = tools::wallet2::make_new(vm2, true, nullptr);
+ auto rc = tools::wallet2::make_new(vm2, true, nullptr, m_pending_daemon);
std::unique_ptr<wallet2> wal;
wal = std::move(rc.first);
if (!wal)
@@ -4795,7 +4793,14 @@ namespace tools
er.message = "Command unavailable in restricted mode.";
return false;
}
- if (!m_wallet) return not_open(er);
+
+ epee::net_utils::http::url_content parsed{};
+ if (!req.address.empty() && !epee::net_utils::parse_url(req.address, parsed))
+ {
+ er.code = WALLET_RPC_ERROR_CODE_NO_DAEMON_CONNECTION;
+ er.message = "Failed to parse daemon address";
+ return false;
+ }
std::vector<std::vector<uint8_t>> ssl_allowed_fingerprints;
ssl_allowed_fingerprints.reserve(req.ssl_allowed_fingerprints.size());
@@ -4853,16 +4858,43 @@ namespace tools
return false;
}
- boost::optional<epee::net_utils::http::login> daemon_login{};
- if (!req.username.empty() || !req.password.empty())
- daemon_login.emplace(req.username, req.password);
+ wallet2::daemon_config cfg;
+ cfg.address = req.address;
+ cfg.username = req.username;
+ cfg.password = req.password;
+ cfg.proxy = req.proxy;
+ cfg.trusted = req.trusted;
+ cfg.ssl_options = ssl_options;
- if (!m_wallet->set_daemon(req.address, daemon_login, req.trusted, std::move(ssl_options), req.proxy))
+ // apply to the open wallet now; the config also seeds wallets opened/created later
+ if (m_wallet)
{
- er.code = WALLET_RPC_ERROR_CODE_NO_DAEMON_CONNECTION;
- er.message = std::string("Unable to set daemon");
- return false;
+ boost::optional<epee::net_utils::http::login> daemon_login{};
+ if (!req.username.empty() || !req.password.empty())
+ daemon_login.emplace(req.username, req.password);
+
+ if (!m_wallet->set_daemon(req.address, daemon_login, req.trusted, std::move(ssl_options), req.proxy))
+ {
+ er.code = WALLET_RPC_ERROR_CODE_NO_DAEMON_CONNECTION;
+ er.message = std::string("Unable to set daemon");
+ return false;
+ }
+ }
+ else
+ {
+ try
+ {
+ ssl_options.create_context();
+ }
+ catch (const std::exception &e)
+ {
+ er.code = WALLET_RPC_ERROR_CODE_NO_DAEMON_CONNECTION;
+ er.message = std::string("Failed to set up SSL: ") + e.what();
+ return false;
+ }
}
+
+ m_pending_daemon = std::move(cfg);
return true;
}
//------------------------------------------------------------------------------------------------------------------------------
diff --git a/src/wallet/wallet_rpc_server.h b/src/wallet/wallet_rpc_server.h
index 5e87d54..62cc60b 100644
--- a/src/wallet/wallet_rpc_server.h
+++ b/src/wallet/wallet_rpc_server.h
@@ -284,6 +284,8 @@ namespace tools
void check_background_mining();
wallet2 *m_wallet;
+ // set_daemon settings reused for wallets opened/created later
+ boost::optional<wallet2::daemon_config> m_pending_daemon;
std::string m_wallet_dir;
tools::private_file rpc_login_file;
std::atomic<bool> m_stop;
diff --git a/tests/functional_tests/multisig.py b/tests/functional_tests/multisig.py
index 17c94c8..b76f971 100755
--- a/tests/functional_tests/multisig.py
+++ b/tests/functional_tests/multisig.py
@@ -683,7 +683,8 @@ class WrongDaemonGuard:
def __enter__(self):
Wallet(idx = self.idx).set_daemon("localhost:0")
def __exit__(self, exc_type, exc_value, traceback):
- Wallet(idx = self.idx).set_daemon("localhost:" + str(self.correct_port))
+ # set_daemon settings persist across wallet loads, so restore trust explicitly
+ Wallet(idx = self.idx).set_daemon("localhost:" + str(self.correct_port), trusted = True)
if __name__ == '__main__':
with AutoRefreshGuard() as arguard:
Why this scored 27/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.