What changed, and why it matters
This commit fixes the Qt 'ServerWidget' in the Electrum Bitcoin wallet so that network connection settings actually match what the user selects. Previously, switching from 'Manual Mode' to 'Auto Connect' could fail to save, picking a server from the list could leave the wallet in auto-connect (so it might switch servers unexpectedly), and invalid server text could persist. The patch also makes the widget refresh every time it is shown and cleans up related server-following logic in the network layer. It is a UI/UX bug-fix commit; there is no direct evidence in the commit that it addresses a security vulnerability, though misconfiguration of server settings can have security-adjacent consequences for a wallet.
Treat as a normal bug-fix commit. Reviewers may want to verify that the new set_server parameter flow cannot be called with inconsistent arguments, and that follow_chain_given_server's KeyError is always caught by callers. No urgent security response is indicated by the commit itself.
Security signals we found
UI state desynchronization between manual/auto-connect/one-server modes could lead to unintended server selection
Invalid server string could persist across dialog opens, potentially confusing users
Server-following logic now explicitly sets auto_connect=True when choosing a random interface, reducing ambiguity
No explicit security framing, CVE, or advisory language in commit message or diff
Evidence from the diff
The patch refactors electrum/gui/qt/network_dialog.py’s ServerWidget and touches electrum/network.py. Key changes: (1) on_server_settings_changed now computes a new ServerAddr while falling back to the current server if the input is invalid, compares it to current net params, and only calls set_server when something changed. (2) set_server now takes explicit server/auto_connect/one_server arguments instead of reading UI state. (3) update() clears the server input in auto-connect mode and repopulates it in manual mode. (4) update_from_config uses a new set_connect_mode helper. (5) showEvent calls update_from_config every time the widget is shown. (6) ‘Use as server’ and ‘Follow this server’ now explicitly switch connect mode to manual. (7) follow_chain_given_id in network.py now forces auto_connect=True/oneserver=False because it picks a random interface. (8) follow_chain_given_server becomes synchronous, raises KeyError if not connected, and the caller in network_dialog.py handles mode switching and set_server. The commit message frames all of this as fixing the widget, not as a security fix.
Changed components
electrum/gui/qt/network_dialog.pyelectrum/network.pyServerWidget Qt UINetwork connection parameter handlingInspect captured patch +79 / −28
diff --git a/electrum/gui/qt/network_dialog.py b/electrum/gui/qt/network_dialog.py
index eb099cc..4d7c680 100644
--- a/electrum/gui/qt/network_dialog.py
+++ b/electrum/gui/qt/network_dialog.py
@@ -435,7 +435,10 @@ class ServerWidget(QWidget, QtEventListener):
def do_set_server(server):
self.server_e.setText(server)
- self.set_server()
+ if self.is_auto_connect():
+ # switch to manual mode as the user manually selected a server
+ self.set_connect_mode(ConnectMode.MANUAL, block_signals=True)
+ self.on_server_settings_changed()
self.nodes_list_widget.setServer.connect(do_set_server)
self.layout().addWidget(self.nodes_list_widget)
@@ -444,6 +447,13 @@ class ServerWidget(QWidget, QtEventListener):
self.register_callbacks()
self.destroyed.connect(lambda: self.unregister_callbacks())
+ def showEvent(self, event):
+ # gets called every time the ServerWidget is shown, when opening it and when
+ # switching between the tabs.
+ super().showEvent(event)
+ _logger.debug(f"showing ServerWidget")
+ # If the user entered garbage the previous time the ServerWidget was open this will restore
+ # it back to the current config
self.update_from_config()
self.update()
@@ -458,17 +468,50 @@ class ServerWidget(QWidget, QtEventListener):
def is_one_server(self):
return self.connect_combo.currentIndex() == ConnectMode.ONESERVER
+ def set_connect_mode(self, connect_mode: ConnectMode, *, block_signals = False):
+ # if block_signals = True the on_server_settings_changed won't get called when changing the index
+ assert isinstance(connect_mode, ConnectMode), connect_mode
+ self.connect_combo.blockSignals(block_signals)
+ self.connect_combo.setCurrentIndex(connect_mode)
+ self.connect_combo.blockSignals(False)
+
def on_server_settings_changed(self):
if not self.network._was_started:
self.update()
return
- server = ServerAddr.from_str_with_inference(self.server_e.text().strip())
- net_params = self.network.get_parameters()
- if server != net_params.server or self.is_auto_connect() != net_params.auto_connect or self.is_one_server() != net_params.oneserver:
- self.set_server()
+
+ current_net_params = self.network.get_parameters()
+ new_server = ServerAddr.from_str_with_inference(self.server_e.text().strip())
+ new_server = new_server or current_net_params.server # keep existing server while input is invalid
+
+ settings_changed = False
+ if new_server != current_net_params.server:
+ settings_changed = True
+ if self.is_auto_connect() != current_net_params.auto_connect:
+ settings_changed = True
+ if self.is_one_server() != current_net_params.oneserver:
+ settings_changed = True
+
+ if settings_changed:
+ _logger.debug(
+ f"ServerWidget.on_server_settings_changed:\n"
+ f"[server: {current_net_params.server} -> {new_server}]\n"
+ f"[auto_connect: {current_net_params.auto_connect} -> {self.is_auto_connect()}]\n"
+ f"[oneserver: {current_net_params.oneserver} -> {self.is_one_server()}]"
+ )
+ self.set_server(
+ new_server,
+ auto_connect=self.is_auto_connect(),
+ one_server=self.is_one_server(),
+ )
+ self.update()
def update(self):
self.server_e.setEnabled(self.config.cv.NETWORK_SERVER.is_modifiable() and not self.is_auto_connect())
+ if self.is_auto_connect():
+ self.server_e.clear()
+ elif not self.server_e.text():
+ self.server_e.setText(self.config.NETWORK_SERVER or "")
for item in [
self.status_label_header, self.status_label, self.status_label_helpbutton,
self.height_label_header, self.height_label, self.height_label_helpbutton]:
@@ -497,37 +540,49 @@ class ServerWidget(QWidget, QtEventListener):
auto_connect = self.config.NETWORK_AUTO_CONNECT
one_server = self.config.NETWORK_ONESERVER
v = ConnectMode.AUTOCONNECT if auto_connect else ConnectMode.ONESERVER if one_server else ConnectMode.MANUAL
- self.connect_combo.setCurrentIndex(v)
+ self.set_connect_mode(v)
server = self.config.NETWORK_SERVER
self.server_e.setText(server)
self.server_e.setEnabled(self.config.cv.NETWORK_SERVER.is_modifiable() and not auto_connect)
self.nodes_list_widget.setEnabled(self.config.cv.NETWORK_SERVER.is_modifiable())
+ _logger.debug(f"update from config: done")
def follow_branch(self, chain_id):
self.network.run_from_another_thread(self.network.follow_chain_given_id(chain_id))
+ # follow_chain_given_id connects to random interface, so set connect_mode back to AUTOCONNECT
+ self.set_connect_mode(ConnectMode.AUTOCONNECT, block_signals=True)
self.update()
def follow_server(self, server: ServerAddr):
+ try:
+ self.network.follow_chain_given_server(server)
+ except KeyError:
+ _logger.debug(f"follow_server: cannot follow, not connected to {server.net_addr_str()}.")
+ return
+
self.server_e.setText(str(server))
- self.network.run_from_another_thread(self.network.follow_chain_given_server(server))
+ if self.is_auto_connect():
+ # the user manually selected a server, so the ConnectMode gets set to MANUAL
+ self.set_connect_mode(ConnectMode.MANUAL, block_signals=True)
+
+ self.set_server(
+ server=server,
+ auto_connect=False,
+ one_server=self.is_one_server(),
+ )
self.update()
- def set_server(self):
- net_params = self.network.get_parameters()
- try:
- server = ServerAddr.from_str_with_inference(str(self.server_e.text()))
- if not server:
- raise Exception("failed to parse server")
- except Exception:
- return
- net_params = net_params._replace(
+ def set_server(self, server: ServerAddr, *, auto_connect: bool, one_server: bool):
+ current_net_params = self.network.get_parameters()
+ new_net_params = current_net_params._replace(
server=server,
- auto_connect=self.is_auto_connect(),
- oneserver=self.is_one_server(),
+ auto_connect=auto_connect,
+ oneserver=one_server,
)
- self.network.run_from_another_thread(self.network.set_parameters(net_params))
+ _logger.debug(f"set_server: {new_net_params=}")
+ self.network.run_from_another_thread(self.network.set_parameters(new_net_params))
class NostrWidget(QWidget, QtEventListener):
diff --git a/electrum/network.py b/electrum/network.py
index fb60074..ea38ef0 100644
--- a/electrum/network.py
+++ b/electrum/network.py
@@ -1155,19 +1155,15 @@ class Network(Logger, NetworkRetryManager[ServerAddr]):
chosen_iface = random.choice(interfaces_on_selected_chain) # type: Interface
# switch to server (and save to config)
net_params = self.get_parameters()
- net_params = net_params._replace(server=chosen_iface.server)
+ # we select a random interface, so set connection mode back to autoconnect
+ net_params = net_params._replace(server=chosen_iface.server, auto_connect=True, oneserver=False)
await self.set_parameters(net_params)
- async def follow_chain_given_server(self, server: ServerAddr) -> None:
+ def follow_chain_given_server(self, server: ServerAddr) -> None:
# note that server_str should correspond to a connected interface
- iface = self.interfaces.get(server)
- if iface is None:
- return
+ iface = self.interfaces[server]
self._set_preferred_chain(iface.blockchain)
- # switch to server (and save to config)
- net_params = self.get_parameters()
- net_params = net_params._replace(server=server)
- await self.set_parameters(net_params)
+ self.logger.debug(f"following {self.config.BLOCKCHAIN_PREFERRED_BLOCK=}")
def get_server_height(self) -> int:
"""Length of header chain, as claimed by main interface."""
Why this scored 23/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.