Skip to content

Extend server.version for feature negotiation #7

@SomberNight

Description

@SomberNight

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 spesmilo/electrumx#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)
      

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions