Skip to content

Commit 47a12c9

Browse files
authored
Merge pull request #2 from SomberNight/202502_protocol_next
protocol 1.7: outpoint subs
2 parents 22deba8 + 5a702d4 commit 47a12c9

5 files changed

Lines changed: 1144 additions & 160 deletions

File tree

docs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import os
1616
import sys
1717
sys.path.insert(0, os.path.abspath('..'))
18-
VERSION = "Electrum Protocol 1.6.x"
18+
VERSION = "Electrum Protocol 1.7.x"
1919

2020
# -- Project information -----------------------------------------------------
2121

docs/protocol-basics.rst

Lines changed: 166 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ within strings, so no confusion is possible there. However it does permit newli
3232
extraneous whitespace between elements; client and server MUST NOT use newlines in such a
3333
way.
3434

35+
Messages SHOULD be :ref:`padded <padding_messages>` to bucketed lengths,
36+
and COULD be buffered (to introduce delays) to protect against traffic analysis.
37+
3538
If using JSON RPC 2.0's feature of parameter passing by name, the
3639
names shown in the description of the method or notification in
3740
question MUST be used.
@@ -76,15 +79,15 @@ All responses received in the stream from and including the server's
7679
response to this call will use its negotiated protocol version.
7780

7881

82+
.. _scriptpubkeys:
7983
.. _script hashes:
8084

8185
Script Hashes
8286
-------------
8387

8488
A :dfn:`script hash` is the hash of the binary bytes of the locking
85-
script (ScriptPubKey), expressed as a hexadecimal string. The hash
86-
function to use is given by the "hash_function" member of
87-
:func:`server.features` (currently :func:`sha256` only). Like for
89+
script (scriptPubKey), expressed as a hexadecimal string. The hash
90+
function to use is :func:`sha256`. Like for
8891
block and transaction hashes, when converting the big-endian binary
8992
hash to a hexadecimal string the least-significant byte appears first,
9093
and the most-significant byte last.
@@ -93,27 +96,28 @@ For example, the legacy Bitcoin address from the genesis block::
9396

9497
1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa
9598

96-
has P2PKH script::
99+
has P2PKH script (scriptPubKey)::
97100

98101
76a91462e907b15cbf27d5425399ebf6f0fb50ebb88f1888ac
99102

100103
with SHA256 hash::
101104

102105
6191c3b590bfcfa0475e877c302da1e323497acf3b42c08d8fa28e364edf018b
103106

104-
which is sent to the server reversed as::
107+
the scripthash is defined as the reverse of that::
105108

106109
8b01df4e368ea28f8dc0423bcf7a4923e3a12d307c875e47a0cfbf90b5c39161
107110

108-
By subscribing to this hash you can find P2PKH payments to that address.
111+
By subscribing to the scriptPubKey (or the scripthash in older protocol versions),
112+
you can find P2PKH payments to that address.
109113

110114
One public key, the genesis block public key, among the trillions for
111115
that address is::
112116

113117
04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb
114118
649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f
115119

116-
which has P2PK script::
120+
which has P2PK script (scriptPubKey)::
117121

118122
4104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb
119123
649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac
@@ -122,11 +126,12 @@ with SHA256 hash::
122126

123127
3318537dfb3135df9f3d950dbdf8a7ae68dd7c7dfef61ed17963ff80f3850474
124128

125-
which is sent to the server reversed as::
129+
the scripthash is defined as the reverse of that::
126130

127131
740485f380ff6379d11ef6fe7d7cdd68aea7f8bd0d953d9fdf3531fb7d531833
128132

129-
By subscribing to this hash you can find P2PK payments to the genesis
133+
By subscribing to the scriptPubKey (or the scripthash in older protocol versions),
134+
you can find P2PK payments to the genesis
130135
block public key.
131136

132137
.. note:: The Genesis block coinbase is uniquely unspendable and
@@ -139,10 +144,10 @@ block public key.
139144
Status
140145
------
141146

142-
To calculate the `status` of a :ref:`script hash <script hashes>` (or
143-
address):
147+
To calculate the `status` of a :ref:`scriptPubKey <scriptpubkeys>`
148+
(or :ref:`script hash <script hashes>` or address):
144149

145-
1. Consider all transactions touching the script hash (both those spending
150+
1. Consider all transactions touching the scriptPubKey (both those spending
146151
from it, and those funding it), both confirmed and unconfirmed (in mempool).
147152

148153
2. Order confirmed transactions by increasing height (and position in the
@@ -167,21 +172,22 @@ to arrive at a canonical ordering.
167172
6. Next, with mempool transactions in the specified order, append a similar
168173
string.
169174

170-
7. The :dfn:`status` of the script hash is the :func:`sha256` hash of the
175+
7. The :dfn:`status` of the scriptPubKey is the :func:`sha256` hash of the
171176
full string expressed as a hexadecimal string, or :const:`null` if the
172177
string is empty because there are no transactions.
173178

174179

175180
**Status Example**
176181

177-
Consider the following BTC regtest address: `bcrt1qn7d2x7272lznt5hhk9s07q3cqnrqljnwladrd3`,
178-
which has script hash: `f04bf26fc16a27abaa2f10540da7a24e369e6ec408f2d901202a7c0cd4a6112d`.
182+
Consider the following BTC regtest address: `bcrt1qn7d2x7272lznt5hhk9s07q3cqnrqljnwladrd3`.
183+
That maps to scriptPubKey `00149f9aa3795e57c535d2f7b160ff023804c60fca6e`.
184+
(and btw has script hash: `f04bf26fc16a27abaa2f10540da7a24e369e6ec408f2d901202a7c0cd4a6112d`)
179185

180186
Example RPC traffic::
181187

182-
<-- ('blockchain.scripthash.subscribe', ['f04bf26fc16a27abaa2f10540da7a24e369e6ec408f2d901202a7c0cd4a6112d']) {} (id: 15)
188+
<-- ('blockchain.scriptpubkey.subscribe', ['00149f9aa3795e57c535d2f7b160ff023804c60fca6e']) {} (id: 15)
183189
--> 78e96c6562cafa71c115503b9411fdfdc595a45031e2ab76ff75162fe1b0590d (id: 15)
184-
<-- ('blockchain.scripthash.get_history', ['f04bf26fc16a27abaa2f10540da7a24e369e6ec408f2d901202a7c0cd4a6112d']) {} (id: 16)
190+
<-- ('blockchain.scriptpubkey.get_history', ['00149f9aa3795e57c535d2f7b160ff023804c60fca6e']) {} (id: 16)
185191
--> [
186192
{'tx_hash': 'a6c9c361bd0bc536d6a22648efbf8f9b200e425ef6c3a7a9669dc444c532a347', 'height': 2472},
187193
{'tx_hash': '9c42f84b2fcdaff676ba25d9d4941741cc0d1a01cce0c23fdc4c0b2afa38431c', 'height': 2473},
@@ -218,6 +224,29 @@ those two txs are sorted by txid (`80` comes before `e0`).
218224
Finally, the status is `78e96c6562cafa71c115503b9411fdfdc595a45031e2ab76ff75162fe1b0590d`.
219225

220226

227+
.. _blockref:
228+
229+
Block Ref
230+
---------
231+
232+
To refer to a block in an unambiguous way, the blockhash can be used.
233+
However, it helps the client to also provide the corresponding height,
234+
otherwise the client might need to maintain some kind of blockhash->blockheight map,
235+
which (naively) for a million blocks might take ~50-100 MB of RAM (or disk space).
236+
237+
To save some bandwidth, we strip the leading zeroes (which result from mining difficulty)
238+
from the hex representation of the block hash. As the length of the full blockhash
239+
is a known constant, this leaves the compressed representation unambiguous.
240+
241+
The `blockref` is a `(block_height, compressed_blockhash)` pair, serialized as a json array.
242+
`block_height` is an integer, the corresponding height for `compressed_blockhash`.
243+
`compressed_blockhash` is a hex string (but can be odd-length).
244+
245+
Example from Bitcoin mainnet::
246+
247+
[600000, "7316856900e76b4f7a9139cfbfba89842c8d196cd5f91"]
248+
249+
221250
Block Headers
222251
-------------
223252

@@ -256,3 +285,123 @@ and confirm the returned roots match.
256285
implementation would require hashing approximately 88MB of data to
257286
provide a single merkle proof. ElectrumX implements an optimization
258287
such that it hashes only approximately 180KB of data per proof.
288+
289+
290+
.. _padding_messages:
291+
292+
Traffic analysis
293+
----------------
294+
295+
The goal is to defend against a passive network Man-in-the-Middle, such as an ISP
296+
or a Tor exit node, observing the encrypted TLS stream, and making educated guesses
297+
of the message contents based on TCP packet flow: timing, direction, and sizes of TCP packets.
298+
299+
.. note:: When using raw cleartext TCP as transport for the JSON-RPC payloads, without encryption,
300+
a passive network observer can see all the plaintext messages.
301+
302+
As a generic mitigation, implementations (both client and server)
303+
304+
- SHOULD pad messages to bucketed lengths (e.g. powers of 2, with a min size), and
305+
306+
- COULD introduce small timing delays, ideally by buffering messages.
307+
308+
(A future protocol version will allow the client to opt-in/opt-out of this server-behaviour,
309+
but this is not done yet. Opt-out would be useful at least for the timing delays, to avoid
310+
slowing down CLI scripts, or non-regtest CI functional tests.)
311+
312+
We can fully backwards-compatibly add padding to the JSON-RPC messages by adding extra
313+
whitespaces inside the JSON objects in a way that parsers ignore.
314+
This can be done at any protocol version.
315+
316+
For example, instead of sending::
317+
318+
{"jsonrpc":"2.0","method":"server.version","id":0,"params":["electrum/4.5.8","1.4"]}\n
319+
320+
321+
the client could send::
322+
323+
{"jsonrpc":"2.0","method":"server.version","id":0,"params":["electrum/4.5.8","1.4"] }\n
324+
325+
326+
For better results, both the client and the server SHOULD implement logic to pad the messages
327+
that they send. So that requests and responses (and notifications) SHOULD all be padded.
328+
This does not have to be rolled out simultaneously: it is ok for only a client to pad what
329+
they send and not the server (or the other way around),
330+
that just limits the effectiveness of the defense against traffic analysis.
331+
332+
Note when the JSON-RPC messages are sent in the TLS stream, they are sometimes batched together.
333+
That is, a single TCP packet might contain multiple small JSON-RPC messages,
334+
e.g. if the client tries to send multiple messages in a short burst.
335+
Also, many protocol requests are <100 bytes, so it would be wasteful to pad all to e.g. 1 kbyte.
336+
337+
To save bandwidth, instead of padding individual JSON-RPC messages,
338+
participants (the client and the server) COULD implement an application-level buffer,
339+
write the messages into that buffer, periodically empty the buffer into the TLS stream
340+
and only add the padding into e.g. the last JSON-RPC message when emptying the buffer.
341+
342+
.. note:: Example implementation
343+
in the `Electrum client <https://github.com/spesmilo/electrum/pull/9875>`_
344+
and in the `electrumx server <https://github.com/spesmilo/electrumx/pull/301>`_:
345+
346+
Both the client and the server write raw JSON-RPC protocol messages into a buffer,
347+
which is then occasionally flushed to the wire. When it is flushed, padding is added
348+
to round up the total length to 1 KB, or to the next power of 2.
349+
The buffer is flushed if it reaches 1 KB, plus there is extra logic that periodically polls
350+
if the oldest message in the buffer is older than 1 second, in which case it is also flushed.
351+
The worst-case 1 second delay is a performance hit that might in cases be felt by the user.
352+
This is a tradeoff to make the flow of packets harder to analyse for an observer.
353+
354+
Implementations COULD make the buffer size and the max time delay configurable.
355+
356+
.. note:: Many protocol requests are <100 bytes. Contrast that with broadcasting a transaction,
357+
which could potentially be several megabytes of data.
358+
(max consensus-valid tx is 4 MB, times 2 for hex-encoding)
359+
Hence padding to a constant size is not practical.
360+
Instead it is recommended to pad to bucketed lengths, e.g. to powers of 2.
361+
362+
The specific details of the size of the buffer, how often it is flushed, and how the padding
363+
is done is not specified by the protocol at the moment. Consequently, neither the client nor
364+
the server can enforce the other to pad and much less to delay messages.
365+
366+
.. note:: Some server implementations do not deal with TLS at all,
367+
they only implement the raw cleartext TCP protocol, and just recommend operators
368+
to put a reverse proxy in front that does TLS termination.
369+
Such protocol implementations (both client and server) nevertheless still
370+
SHOULD implement traffic analysis protections.
371+
That way, if the operator tunnels the traffic over TLS externally,
372+
the resulting stream meaningfully receives the protections.
373+
374+
.. note:: An alternative approach to the buffer-based delaying and padding
375+
could be to more aggressively use JSON-RPC batching to batch messages, and to use the
376+
optional Record Padding of
377+
`TLS 1.3 <https://www.rfc-editor.org/rfc/rfc8446#section-5.4>`_.
378+
See `SSL_CONF_cmd RecordPadding` in openssl. However this only allows
379+
padding to multiples of a constant, while above the recommendation was to pad to powers of 2.
380+
The implementation might still want to delay messages a bit
381+
to accumulate them into a larger JSON-RPC batch. Also note if users
382+
(e.g. server operators) are expected to do their own TLS termination
383+
(e.g. using a reverse proxy), configuring TLS RecordPadding would become an externality
384+
for them to do, which they might forget.
385+
386+
.. note:: Buffering the messages to introduce timing delays
387+
and padding to ~bucketed sizes is a good baseline.
388+
However even approximate timing and direction of TCP packets
389+
can leak too much information in some scenarios.
390+
391+
To combat timing analysis, both the client and the server
392+
COULD send dummy RPCs with a random timer, but more importantly at strategically selected events.
393+
For example, when the client receives a new block header notification,
394+
it COULD probabilistically send a random number of "server.ping" messages
395+
with small random sleeps in-between.
396+
397+
Protocol version 1.7 extends "server.ping" to make it more useful for mimicking other traffic
398+
and adding noise. Now the client can send it either as a JSON-RPC "Request"
399+
or JSON-RPC "Notification", and the server can send it as a JSON-RPC "Notification".
400+
It can be sent as a notification at any time, without any corresponding prior subscription.
401+
If sent as a notification, the receiver is expected not to respond.
402+
403+
When the server sends a block header notification to the client,
404+
it COULD also probabilistically send noise ("server.ping") notifications to the client,
405+
perhaps conditioned on whether it will also send
406+
:func:`blockchain.scriptpubkey.subscribe` notifications.
407+
(so server could send noise if there are no status notifications to be sent)

docs/protocol-changes.rst

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,3 +216,50 @@ Removed methods
216216

217217
* :func:`blockchain.relayfee` is removed. The `minrelaytxfee` field
218218
of the new :func:`mempool.get_info` RPC is a direct replacement.
219+
220+
221+
.. _version 1.7:
222+
223+
Version 1.7
224+
===========
225+
226+
Changes
227+
-------
228+
229+
* :func:`server.ping` can now also be sent as an unrequested notification, by both the client
230+
and the server. The request/response signature also changed.
231+
* The `blockchain.scripthash.*` methods are all replaced with `blockchain.scriptpubkey.*`
232+
methods. Note: `sha256(scriptPubKey) == scripthash`. Besides taking a scriptPubKey instead
233+
of a scripthash as input param, the new methods also have some minor functional changes.
234+
- the notifications for :func:`blockchain.scriptpubkey.subscribe` still contain the scripthash
235+
- the protocol now guarantees that a notification is sent even if the `status` of the scriptPubKey
236+
did not change but there was a reorg affecting a relevant tx
237+
- many RPC results now contain the `chaintip` (and hence return dict instead of array)
238+
* The `hash_function` field is removed from the :func:`server.features` response.
239+
* Standardize "history too large" error code for `blockchain.scriptpubkey.*` methods.
240+
241+
New methods
242+
-----------
243+
244+
* :func:`blockchain.scriptpubkey.get_balance`
245+
* :func:`blockchain.scriptpubkey.get_history`
246+
* :func:`blockchain.scriptpubkey.get_mempool`
247+
* :func:`blockchain.scriptpubkey.listunspent`
248+
* :func:`blockchain.scriptpubkey.subscribe`
249+
* :func:`blockchain.scriptpubkey.unsubscribe`
250+
* :func:`blockchain.outpoint.subscribe` to subscribe to a transaction
251+
outpoint, and get a notification when it gets spent.
252+
* :func:`blockchain.outpoint.unsubscribe` to unsubscribe from a TXO.
253+
* :func:`blockchain.outpoint.get_status` to get current status of a TXO, without subscribing to changes.
254+
* :func:`blockchain.transaction.testmempoolaccept`
255+
* :func:`mempool.recent` to list a few of the latest txs just added to the mempool.
256+
257+
Removed methods
258+
---------------
259+
260+
* :func:`blockchain.scripthash.get_balance`
261+
* :func:`blockchain.scripthash.get_history`
262+
* :func:`blockchain.scripthash.get_mempool`
263+
* :func:`blockchain.scripthash.listunspent`
264+
* :func:`blockchain.scripthash.subscribe`
265+
* :func:`blockchain.scripthash.unsubscribe`

0 commit comments

Comments
 (0)