interface: extend client to be able to support a range of protocols
What changed, and why it matters
This commit refactors how the Electrum wallet negotiates the server protocol version. Previously the client requested a single fixed protocol version; now it advertises a supported range (currently still set to just 1.4 to 1.4, so behavior is unchanged in practice). It also updates the QML GUI to display a range string and changes a server-list filter to use the minimum protocol version. There is no direct security fix here, but it lays groundwork for future protocol-version flexibility and slightly hardens version-string parsing by falling back to version 0 on malformed input.
No immediate action required. Treat as routine refactoring. When reviewing future commits that actually widen PROTOCOL_VERSION_MAX, verify that new protocol features are authenticated and that downgrade attacks cannot force a weaker protocol version.
Security signals we found
Protocol-version negotiation logic changed from single value to range
New protocol_tuple() helper normalizes version strings and defaults to (0,) on parse failure
Server protocol-version response is now range-validated before use
No explicit vulnerability or CVE referenced in commit or supplied materials
Evidence from the diff
The patch replaces the scalar version.PROTOCOL_VERSION with PROTOCOL_VERSION_MIN and PROTOCOL_VERSION_MAX. Interface.server_version now sends [client_name, [min, max]] instead of [client_name, ‘1.4’], validates the returned server protocol version lies within that range using tuple comparison, and stores it as active_protocol_tuple. A new protocol_tuple() helper converts version strings to tuples, returning (0,) on any parsing failure. network.filter_version now compares against PROTOCOL_VERSION_MIN. version.py currently sets both min and max to ‘1.4’, so the negotiated protocol remains identical to before; the change is therefore preparatory.
Changed components
electrum/interface.pyelectrum/network.pyelectrum/version.pyelectrum/gui/qml/qeapp.pyInspect captured patch +27 / −8
diff --git a/electrum/gui/qml/qeapp.py b/electrum/gui/qml/qeapp.py
index c14c0d4..a4df18b 100644
--- a/electrum/gui/qml/qeapp.py
+++ b/electrum/gui/qml/qeapp.py
@@ -546,7 +546,7 @@ class ElectrumQmlApplication(QGuiApplication):
self.context.setContextProperty('QRIP', self.qr_ip_h)
self.context.setContextProperty('BUILD', {
'electrum_version': version.ELECTRUM_VERSION,
- 'protocol_version': version.PROTOCOL_VERSION,
+ 'protocol_version': f"[{version.PROTOCOL_VERSION_MIN}, {version.PROTOCOL_VERSION_MAX}]",
'qt_version': QT_VERSION_STR,
'pyqt_version': PYQT_VERSION_STR
})
diff --git a/electrum/interface.py b/electrum/interface.py
index ecd6f99..a092e35 100644
--- a/electrum/interface.py
+++ b/electrum/interface.py
@@ -50,7 +50,7 @@ import certifi
from .util import (ignore_exceptions, log_exceptions, bfh, ESocksProxy,
is_integer, is_non_negative_integer, is_hash256_str, is_hex_str,
is_int_or_float, is_non_negative_int_or_float, OldTaskGroup,
- send_exception_to_crash_reporter, error_text_str_to_safe_str)
+ send_exception_to_crash_reporter, error_text_str_to_safe_str, versiontuple)
from . import util
from . import x509
from . import pem
@@ -139,6 +139,18 @@ def assert_list_or_tuple(val: Any) -> None:
raise RequestCorrupted(f'{val!r} should be a list or tuple')
+def protocol_tuple(s: Any) -> tuple[int, ...]:
+ """Converts a protocol version number, such as "1.0" to a tuple (1, 0).
+
+ If the version number is bad, (0, ) indicating version 0 is returned.
+ """
+ try:
+ assert isinstance(s, str)
+ return versiontuple(s)
+ except Exception:
+ return (0, )
+
+
class ChainResolutionMode(enum.Enum):
CATCHUP = enum.auto()
BACKWARD = enum.auto()
@@ -574,6 +586,8 @@ class Interface(Logger):
self.fee_estimates_eta = {} # type: Dict[int, int]
+ self.active_protocol_tuple = (0,) # type: Optional[tuple[int, ...]]
+
# Dump network messages (only for this interface). Set at runtime from the console.
self.debug = False
@@ -964,15 +978,19 @@ class Interface(Logger):
start = time.perf_counter()
self.session = session # type: NotificationSession
self.session.set_default_timeout(self.network.get_network_timeout_seconds(NetworkTimeout.Generic))
+ client_prange = [version.PROTOCOL_VERSION_MIN, version.PROTOCOL_VERSION_MAX]
try:
- ver = await session.send_request('server.version', [self.client_name(), version.PROTOCOL_VERSION])
+ ver = await session.send_request('server.version', [self.client_name(), client_prange])
except aiorpcx.jsonrpc.RPCError as e:
raise GracefulDisconnect(e) # probably 'unsupported protocol version'
if exit_early:
return
- if ver[1] != version.PROTOCOL_VERSION:
+ self.active_protocol_tuple = protocol_tuple(ver[1])
+ client_pmin = protocol_tuple(client_prange[0])
+ client_pmax = protocol_tuple(client_prange[1])
+ if not (client_pmin <= self.active_protocol_tuple <= client_pmax):
raise GracefulDisconnect(f'server violated protocol-version-negotiation. '
- f'we asked for {version.PROTOCOL_VERSION!r}, they sent {ver[1]!r}')
+ f'we asked for {client_prange!r}, they sent {ver[1]!r}')
if not self.network.check_interface_against_healthy_spread_of_connected_servers(self):
raise GracefulDisconnect(f'too many connected servers already '
f'in bucket {self.bucket_based_on_ipaddress()}')
diff --git a/electrum/network.py b/electrum/network.py
index c49b48f..966a31e 100644
--- a/electrum/network.py
+++ b/electrum/network.py
@@ -55,7 +55,7 @@ from .interface import (
Interface, PREFERRED_NETWORK_PROTOCOL, RequestTimedOut, NetworkTimeout, BUCKET_NAME_OF_ONION_SERVERS,
NetworkException, RequestCorrupted, ServerAddr, TxBroadcastError,
)
-from .version import PROTOCOL_VERSION
+from .version import PROTOCOL_VERSION_MIN
from .i18n import _
from .logging import get_logger, Logger
from .fee_policy import FeeHistogram, FeeTimeEstimates, FEE_ETA_TARGETS
@@ -118,7 +118,7 @@ def parse_servers(result: Sequence[Tuple[str, str, List[str]]]) -> Dict[str, dic
def filter_version(servers):
def is_recent(version):
try:
- return util.versiontuple(version) >= util.versiontuple(PROTOCOL_VERSION)
+ return util.versiontuple(version) >= util.versiontuple(PROTOCOL_VERSION_MIN)
except Exception as e:
return False
return {k: v for k, v in servers.items() if is_recent(v.get('version'))}
diff --git a/electrum/version.py b/electrum/version.py
index 40ec211..49ad233 100644
--- a/electrum/version.py
+++ b/electrum/version.py
@@ -1,6 +1,7 @@
ELECTRUM_VERSION = '4.6.2' # version of the client package
-PROTOCOL_VERSION = '1.4' # protocol version requested
+PROTOCOL_VERSION_MIN = '1.4' # electrum protocol
+PROTOCOL_VERSION_MAX = '1.4'
# The hash of the mnemonic seed must begin with this
SEED_PREFIX = '01' # Standard wallet
Why this scored 19/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.