Skip to content

Add Electrum protocol version negotiation#62

Open
eynhaender wants to merge 1 commit into
cryptoadvance:masterfrom
eynhaender:master
Open

Add Electrum protocol version negotiation#62
eynhaender wants to merge 1 commit into
cryptoadvance:masterfrom
eynhaender:master

Conversation

@eynhaender

@eynhaender eynhaender commented Jun 15, 2026

Copy link
Copy Markdown

What

This introduces proper protocol version negotiation to the Electrum Interface.

Changes

Min/Max Versions: Mininum Supported Version is 1.3 since we make use of blockchain.block.header. Maximum is 1.4 since we don't implement any 1.5 features yet.

Result stored: The server's response [server_software_version, negotiated_protocol] is stored on the socket instance and logged at INFO level on every connection and reconnect.

Fail/reconnect on all error paths: RPCError (e.g. libbitcoin code 19, ElectrumX code 1), timeout, unexpected response format, and a negotiated version below our minimum all trigger an immediate reconnect via the existing broken_killing_threads path. Previously, failures were logged as warnings and the connection continued without a negotiated version.

Architecture: The handshake is moved out of _ping_loop into a new handshaking state in the monitor loop state machine, between creating_threads and execute_recreation_callback. The recv, write and notify threads are started first (so self.call() works), the handshake runs synchronously in the monitor loop, and the ping thread is only started after a successful handshake. This guarantees that status == ok is never set before version negotiation has completed, and that server.version always precedes server.ping by construction.

Tested against

Server valid range unsupported range below-minimum range
libbitcoin 4.0.0 ✓ 1.4 RPCError 19 → reconnect returns 1.2 → _ver check catches it
ElectrumX 1.10–1.16 ✓ 1.4 RPCError 1 → reconnect RPCError or 1.2 → reconnect
Fulcrum 2.1.1 ✓ 1.4 RPCError 1 → reconnect RPCError → reconnect
electrs-esplora (Blockstream) ✓ 1.4 lenient, returns 1.4 lenient, returns 1.4
emzy.de (ElectrumX) ✓ 1.4 closes connection → Exception → reconnect dto.

Limitations

  • Protocol range is hardcoded (["1.3", "1.4"]). There is no mechanism to negotiate a different range at runtime or per-instance.

@al-munazzim

Copy link
Copy Markdown
Contributor

The direction looks right: server.version should be sent before the first server.ping, especially for stricter Electrum servers.

One concern: the advertised protocol range is ["1.2", "1.4"], but Spectrum later calls blockchain.block.header, which was added in protocol 1.3. If a server negotiates 1.2, Spectrum may successfully handshake and then fail on methods it assumes exist.

Can we either advertise ["1.3", "1.4"] or "1.4" here? Also, it would be useful to store/log the returned [server_software_version, negotiated_protocol_version] so we can see what was negotiated and fail/reconnect explicitly if negotiation did not succeed.

@eynhaender

Copy link
Copy Markdown
Author

Yes, absolutely. There are so many versions of the protocol by now that we should generally agree on one version and then manage it properly. I’m happy to take another pass at it and update the PR.

Moves server.version handling out of _ping_loop into a dedicated
`handshaking` state in the monitor loop state machine.

The handshake runs synchronously after recv/write/notify threads
are started but before the ping thread is launched, ensuring the
protocol ordering constraint (server.version before server.ping,
required since protocol 1.2) is enforced by the architecture, not
just by call order within a thread.

Changes:
- New `handshaking` state between `creating_threads` and
  `execute_recreation_callback`; ping thread only starts after a
  successful handshake
- Protocol range raised from ["1.2", "1.4"] to ["1.3", "1.4"];
  blockchain.block.header, used throughout spectrum.py, was added
  in 1.3
- Negotiated server software version and protocol version stored
  as `server_software_version` and `negotiated_protocol` on the
  socket instance and logged at INFO level
- All failure paths (RPCError, timeout, unexpected response format,
  sub-minimum negotiated version) trigger reconnect via
  `broken_killing_threads`
- Fix logger.exception() missing argument in _create_threads

Tested against libbitcoin, ElectrumX, Fulcrum and electrs-esplora.
@eynhaender eynhaender changed the title Send server.version before first ping in _ping_loop Add Electrum protocol version negotiation Jun 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants