spesmilo / spesmilo/electrum-protocol

Extend `server.version` for feature negotiation

Open
#7 4 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
No language data
Stars
18
Forks
8
PR merge metrics
No merged PRs in 30d

Description

Currently server.version is very naive: the client sends a range of protocol versions it supports, and the server selects a single version from that range and replies with that selected version.

This implies that the client supports a continuous range of protocol versions (given there being protocol 1.2, 1.3, 1.4, the client can't proclaim they want either 1.2 or 1.4).
Interestingly, the server is allowed to only implement support for a non-continuous range of versions:
imagine the client sends [1.2, 1.4], that means the client must support {1.2, 1.3, 1.4}, but it is fine for a server to implement {1.2, 1.4} (excluding 1.3), as it is ultimately the server that chooses.

I think it would be useful to extend server.version, in a generic way, to be used for negotiating certain features.

For example, electrumx 1.18 added some basic countermeasures against traffic analysis in https://github.com/spesmilo/electrumx/pull/301, which can result in messages being delayed by up to 1 second.
Some clients might want to opt-out of (or opt-in to) such delays.


Idea:
RPC signature:
server.version(client_name="", protocol_version="1.4", options={})

where options is a dictionary, keyed by feature_name strings.
The values could be lists: only the first item of the list is defined: "force_on", "prefer_on", "prefer_off", "force_off".
(making this a 4-state might seem like over-engineering but - depending on the features - each state makes sense)
The rest of the items in the list are specific to the given feature, to allow general customisation, and should be ignored by servers in case of unknown features.

If a server sees an unknown feature in the options dict with "force_on" or "force_off", they should send back an RPCError and disconnect.

RPC result:
[server_software_version, protocol_version, options]

where options is a dictionary, keyed by feature_name strings, filtered to only contain the keys the server understands. (i.e. features unknown to the server got removed)
The values could be lists: only the first item of the list is defined: "on" or "off".
The rest of the items in the list are specific to the given feature, to allow general customisation.


Concrete example:

We could add a feature named "delays". This feature could potentially be customisable, so that the client can say they want delays to never be longer than e.g. 250 ms.

The client could then send:

<-- server.version("electrum/4.6.2", "1.7", {"delays": ["prefer_on", 250]})

To which the server might reply, if they know the feature:

--> ["ElectrumX 1.18.0", "1.7", {"delays": ["on"]}]

Or if the server does not know the feature:

--> ["ElectrumX 1.18.0", "1.7", {}]

Or the client could send:

<-- server.version("electrum/4.6.2", "1.7", {"delays": ["force_off"]})

To which the server might reply, if they know the feature:

--> ["ElectrumX 1.18.0", "1.7", {"delays": ["off"]}]

Or if the server does not know the feature, the server would send back and error and disconnect:

--> RPCError(-32603, "unknown required feature: 'delays'")

Unfortunately, with current server implementations, adding a new arg to server.version seems to be a breaking change. Ideally servers should ignore the extraneous arg, but that's often not the case.

I think we should add a requirement to the protocol spec to relax the server behaviour re parsing server.version args: extraneous unknown args should be tolerated and ignored. Also, in case of json-rpc 2.0, named args should be supported.

  • server.version arg list, len 2 <<< current widespread client behaviour
    • electrs OK
      1.27 | D | interface.[blockstream.info:700] | <-- ('server.version', ['electrum/4.6.2', '1.4']) {} (id: 1)
      1.47 | D | interface.[blockstream.info:700] | --> ['electrs-esplora 0.4.1', '1.4'] (id: 1)
      
    • fulcrum OK
      119.14 | D | interface.[blackie.c3-soft.com:57002] | <-- ('server.version', ['electrum/4.6.2', '1.4']) {} (id: 1)
      119.35 | D | interface.[blackie.c3-soft.com:57002] | --> ['Fulcrum 2.0', '1.4'] (id: 1)
      
    • electrumx OK
      155.17 | D | interface.[fortress.qtornado.com:443] | <-- ('server.version', ['electrum/4.6.2', '1.4']) {} (id: 1)
      156.31 | D | interface.[fortress.qtornado.com:443] | --> ['ElectrumX 1.18.0', '1.4'] (id: 1)
      
  • server.version arg list, len 3
    • electrs OK
      50.49 | D | interface.[blockstream.info:700] | <-- ('server.version', ['electrum/4.6.2', '1.4', {'no_delays': True}]) {} (id: 1)
      50.69 | D | interface.[blockstream.info:700] | --> ['electrs-esplora 0.4.1', '1.4'] (id: 1)
      
    • fulcrum RPCError
      39.87 | D | interface.[blackie.c3-soft.com:57002] | <-- ('server.version', ['electrum/4.6.2', '1.4', {'no_delays': True}]) {} (id: 1)
      40.09 | D | interface.[blackie.c3-soft.com:57002] | --> RPCError(-32602, 'Expected at most 2 parameters for server.version, got 3 instead') (id: 1)
      
    • electrumx RPCError
      3.74 | D | interface.[fortress.qtornado.com:443] | <-- ('server.version', ['electrum/4.6.2', '1.4', {'no_delays': True}]) {} (id: 1)
      4.73 | D | interface.[fortress.qtornado.com:443] | --> RPCError(-32602, '3 arguments passed to method server.version taking at most 2') (id: 1)
      
  • server.version arg dict, len 2
    • electrs REMOTE_DC
      44.55 | D | interface.[blockstream.info:700] | <-- ('server.version', {'client_name': 'electrum/4.6.2', 'protocol_version': '1.4'}) {} (id: 1)
      44.75 | D | interface.[blockstream.info:700] | --> CancelledError() (id: 1)
      
    • fulcrum RPCError
      10.24 | D | interface.[blackie.c3-soft.com:57002] | <-- ('server.version', {'client_name': 'electrum/4.6.2', 'protocol_version': '1.4'}) {} (id: 1)
      10.45 | D | interface.[blackie.c3-soft.com:57002] | --> RPCError(-32602, 'Named params are not supported for this method') (id: 1)
      
    • electrumx OK
      1.57 | D | interface.[fortress.qtornado.com:443] | <-- ('server.version', {'client_name': 'electrum/4.6.2', 'protocol_version': '1.4'}) {} (id: 1)
      2.78 | D | interface.[fortress.qtornado.com:443] | --> ['ElectrumX 1.18.0', '1.4'] (id: 1)
      
  • server.version arg dict, len 3
    • electrs REMOTE_DC
      18.36 | D | interface.[blockstream.info:700] | <-- ('server.version', {'client_name': 'electrum/4.6.2', 'protocol_version': '1.4', 'options': {'no_delays': True}}) {} (id: 1)
      18.56 | D | interface.[blockstream.info:700] | --> CancelledError() (id: 1)
      
    • fulcrum RPCError
      63.11 | D | interface.[blackie.c3-soft.com:57002] | <-- ('server.version', {'client_name': 'electrum/4.6.2', 'protocol_version': '1.4', 'options': {'no_delays': True}}) {} (id: 1)
      63.31 | D | interface.[blackie.c3-soft.com:57002] | --> RPCError(-32602, 'Named params are not supported for this method') (id: 1)
      
    • electrumx RPCError
      90.77 | D | interface.[fortress.qtornado.com:443] | <-- ('server.version', {'client_name': 'electrum/4.6.2', 'protocol_version': '1.4', 'options': {'no_delays': True}}) {} (id: 1)
      91.78 | D | interface.[fortress.qtornado.com:443] | --> RPCError(-32602, 'method "server.version" does not take parameter "options"') (id: 1)
      

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the server.version RPC entry point and the existing protocol requirements described in this issue. Done means the protocol specifies feature-option negotiation, unknown-feature handling, tolerant extra arguments, and JSON-RPC named-argument behavior, with compatibility expectations clear for existing servers.

Written by the indexing model from the issue text.

Assessment

Tech stack
json
Domain
api
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Needs clarification
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.