Skip to content

Commit 59b13e4

Browse files
committed
interface: protocol 1.7: some response types changed: array->dict
This was motivated by wanting to include the chaintip in responses but that part was reverted and postponed to later. ref spesmilo/electrum-protocol#17
1 parent 09aa14d commit 59b13e4

3 files changed

Lines changed: 53 additions & 31 deletions

File tree

electrum/interface.py

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,13 @@ def assert_hex_str(val: Any, *, allow_odd_len: bool = False) -> None:
131131
raise RequestCorrupted(f'{val!r} should be a hex str')
132132

133133

134-
def assert_dict_contains_field(d: Any, *, field_name: str) -> Any:
134+
def assert_dict(d: Any) -> None:
135135
if not isinstance(d, dict):
136136
raise RequestCorrupted(f'{d!r} should be a dict')
137+
138+
139+
def assert_dict_contains_field(d: Any, *, field_name: str) -> Any:
140+
assert_dict(d)
137141
if field_name not in d:
138142
raise RequestCorrupted(f'required field {field_name!r} missing from dict')
139143
return d[field_name]
@@ -235,7 +239,7 @@ def set_default_timeout(self, timeout):
235239
assert hasattr(self, "max_send_delay") # in base class
236240
self.max_send_delay = timeout
237241

238-
async def subscribe(self, method: str, params: List, queue: asyncio.Queue):
242+
async def subscribe(self, method: str, params: List, queue: asyncio.Queue) -> None:
239243
# note: until the cache is written for the first time,
240244
# each 'subscribe' call might make a request on the network.
241245
notif_params = params[:]
@@ -1471,9 +1475,10 @@ async def get_history_for_spk(self, spk: str) -> List[dict]:
14711475
# do request
14721476
res = await self.session.send_request('blockchain.scriptpubkey.get_history', [spk])
14731477
# check response
1474-
assert_list_or_tuple(res)
1478+
hist = assert_dict_contains_field(res, field_name='history')
1479+
assert_list_or_tuple(hist)
14751480
prev_height = 1
1476-
for tx_item in res:
1481+
for tx_item in hist:
14771482
height = assert_dict_contains_field(tx_item, field_name='height')
14781483
assert_dict_contains_field(tx_item, field_name='tx_hash')
14791484
assert_integer(height)
@@ -1491,23 +1496,24 @@ async def get_history_for_spk(self, spk: str) -> List[dict]:
14911496
prev_height = height
14921497
if self.active_protocol_tuple >= (1, 6):
14931498
# enforce order of mempool txs
1494-
mempool_txs = [tx_item for tx_item in res if tx_item['height'] <= 0]
1499+
mempool_txs = [tx_item for tx_item in hist if tx_item['height'] <= 0]
14951500
if mempool_txs != sorted(mempool_txs, key=lambda x: (-x['height'], bytes.fromhex(x['tx_hash']))):
14961501
raise RequestCorrupted(f'mempool txs not in canonical order')
1497-
hashes = set(map(lambda item: item['tx_hash'], res))
1498-
if len(hashes) != len(res):
1502+
hashes = set(map(lambda item: item['tx_hash'], hist))
1503+
if len(hashes) != len(hist):
14991504
# Either server is sending garbage... or maybe if server is race-prone
15001505
# a recently mined tx could be included in both last block and mempool?
15011506
# Still, it's simplest to just disregard the response.
15021507
raise RequestCorrupted(f"server history has non-unique txids for spk={spk}")
1503-
return res
1508+
return hist
15041509

15051510
async def listunspent_for_spk(self, spk: str) -> List[dict]:
15061511
# do request
15071512
res = await self.session.send_request('blockchain.scriptpubkey.listunspent', [spk])
15081513
# check response
1509-
assert_list_or_tuple(res)
1510-
for utxo_item in res:
1514+
utxos = assert_dict_contains_field(res, field_name='utxos')
1515+
assert_list_or_tuple(utxos)
1516+
for utxo_item in utxos:
15111517
assert_dict_contains_field(utxo_item, field_name='tx_pos')
15121518
assert_dict_contains_field(utxo_item, field_name='value')
15131519
assert_dict_contains_field(utxo_item, field_name='tx_hash')
@@ -1516,7 +1522,7 @@ async def listunspent_for_spk(self, spk: str) -> List[dict]:
15161522
assert_non_negative_integer(utxo_item['value'])
15171523
assert_non_negative_integer(utxo_item['height'])
15181524
assert_hash256_str(utxo_item['tx_hash'])
1519-
return res
1525+
return utxos
15201526

15211527
async def get_balance_for_spk(self, spk: str) -> dict:
15221528
# do request

electrum/synchronizer.py

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@
3535
from .util import make_aiohttp_session, NetworkJobOnDefaultServer, random_shuffled_copy, OldTaskGroup, log_exceptions
3636
from .bitcoin import address_to_script, script_to_scripthash, is_address, neuter_bitcoin_address
3737
from .logging import Logger
38-
from .interface import GracefulDisconnect, NetworkTimeout, HistoryTooLong, assert_hash256_str
38+
from .interface import (
39+
GracefulDisconnect, NetworkTimeout, HistoryTooLong, assert_hash256_str, assert_non_negative_integer,
40+
assert_dict_contains_field, assert_dict, assert_integer,
41+
)
3942

4043
if TYPE_CHECKING:
4144
from .network import Network
@@ -120,10 +123,10 @@ async def _on_address_status(self, addr: str, status: Optional[str]):
120123
"""
121124
raise NotImplementedError() # implemented by subclasses
122125

123-
async def _on_outpoint_status(self, txid: str, index:int, status: dict):
126+
async def _on_outpoint_status(self, txid: str, index: int, status: dict):
124127
raise NotImplementedError() # implemented by subclasses
125128

126-
async def _subscribe_to_address(self, addr):
129+
async def _subscribe_to_address(self, addr: str):
127130
spk = address_to_script(addr)
128131
h = script_to_scripthash(spk)
129132
self.scripthash_to_address[h] = addr
@@ -153,8 +156,13 @@ async def _subscribe_to_outpoint(self, outpoint: str, *, spk_hint: str) -> None:
153156
@log_exceptions
154157
async def handle_address_status(self):
155158
while True:
156-
h, status = await self.address_status_queue.get()
157-
addr = self.scripthash_to_address[h]
159+
sh, status = await self.address_status_queue.get()
160+
# basic checks for response
161+
assert_hash256_str(sh)
162+
if status is not None:
163+
assert_hash256_str(status)
164+
# process status
165+
addr = self.scripthash_to_address[sh]
158166
self._handling_addr_statuses.add(addr)
159167
self.requested_addrs.discard(addr) # ok for addr not to be present
160168
await self.taskgroup.spawn(self._on_address_status, addr, status)
@@ -164,7 +172,12 @@ async def handle_address_status(self):
164172
async def handle_outpoint_status(self):
165173
while True:
166174
txid, index, status = await self.outpoint_status_queue.get()
167-
outpoint = txid + ':%d'%index
175+
# basic checks for response. more checks done in _on_outpoint_status
176+
assert_hash256_str(txid)
177+
assert_non_negative_integer(index)
178+
assert_dict(status)
179+
# process status
180+
outpoint = f"{txid}:{index}"
168181
self._handling_outpoint_statuses.add(outpoint)
169182
self.requested_outpoints.discard(outpoint) # ok for addr not to be present
170183
await self.taskgroup.spawn(self._on_outpoint_status, txid, index, status)
@@ -284,26 +297,28 @@ async def disconnect_if_still_stale():
284297

285298
async def _on_outpoint_status(self, txid: str, index: int, status: dict):
286299
try:
287-
assert isinstance(status, dict)
300+
# check inputs (coming from server)
288301
assert_hash256_str(txid)
302+
assert_non_negative_integer(index)
303+
#
289304
txs = set()
290305
txs.add(txid)
291-
height = status.get('funder_height')
292-
if height is not None:
293-
assert isinstance(height, int)
306+
funder_height = status.get('funder_height')
307+
if funder_height is not None:
308+
assert_integer(funder_height)
294309
# spv the input
295-
self.adb.add_unverified_or_unconfirmed_tx(txid, height)
310+
self.adb.add_unverified_or_unconfirmed_tx(txid, funder_height)
296311
# fetch the output
297312
spender_txid = status.get('spender_txhash')
298313
if spender_txid is not None:
299314
assert_hash256_str(spender_txid)
300-
spender_height = status.get('spender_height')
301-
assert isinstance(spender_height, int)
315+
spender_height = assert_dict_contains_field(status, field_name='spender_height')
316+
assert_integer(spender_height)
302317
self.adb.add_unverified_or_unconfirmed_tx(spender_txid, spender_height)
303318
txs.add(spender_txid)
304319
await self._request_missing_txs(txs, allow_server_not_finding_tx=False)
305320
finally:
306-
outpoint = txid + ':%d'%index
321+
outpoint = f"{txid}:{index}"
307322
self._handling_outpoint_statuses.discard(outpoint)
308323

309324

tests/toyserver/toyserver.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import collections
77
from dataclasses import dataclass
88
from functools import partial
9-
from typing import Optional, Sequence, Iterable, List, Set, Callable, TypeVar
9+
from typing import Optional, Sequence, Iterable, List, Set, Callable, TypeVar, Any
1010

1111
import aiorpcx
1212
from aiorpcx import RPCError
@@ -490,12 +490,11 @@ async def _handle_server_version(self, client_name='', protocol_version=None, *a
490490
async def _handle_server_features(self) -> dict:
491491
return {
492492
'genesis_hash': constants.net.GENESIS,
493-
'hosts': {"14.3.140.101": {"tcp_port": 51001, "ssl_port": 51002}},
493+
'hosts': {"127.0.0.1": {"tcp_port": 51001, "ssl_port": 51002}},
494494
'protocol_max': '1.7',
495495
'protocol_min': '1.7',
496496
'pruning': None,
497-
'server_version': 'ElectrumX 1.19.0',
498-
'hash_function': 'sha256',
497+
'server_version': 'toyserver 0.1',
499498
}
500499

501500
async def _handle_estimatefee(self, number, mode=None):
@@ -574,13 +573,15 @@ async def _handle_spk_subscribe(self, spk: str) -> Optional[str]:
574573
hist = self.svr.calc_spk_history(spk)
575574
return history_status(hist)
576575

577-
async def _handle_spk_get_history(self, spk: str) -> Sequence[dict]:
576+
async def _handle_spk_get_history(self, spk: str) -> dict[str, Any]:
577+
res = dict()
578578
hist_tuples = self.svr.calc_spk_history(spk)
579579
hist_dicts = [{"height": height, "tx_hash": txid} for (txid, height) in hist_tuples]
580580
for hist_dict in hist_dicts: # add "fee" key for mempool txs
581581
if hist_dict["height"] in (0, -1,):
582582
hist_dict["fee"] = 0
583-
return hist_dicts
583+
res["history"] = hist_dicts
584+
return res
584585

585586
async def server_send_notifications(self, *, touched_spks: Iterable[str], height_changed: bool = False) -> None:
586587
if height_changed and self.subbed_headers and self.notified_height != self.svr.cur_height:

0 commit comments

Comments
 (0)