URI: 
       tinterface.py - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
       tinterface.py (37799B)
       ---
            1 #!/usr/bin/env python
            2 #
            3 # Electrum - lightweight Bitcoin client
            4 # Copyright (C) 2011 thomasv@gitorious
            5 # Copyright (C) 2021 Ivan J. <parazyd@dyne.org>
            6 #
            7 # Permission is hereby granted, free of charge, to any person
            8 # obtaining a copy of this software and associated documentation files
            9 # (the "Software"), to deal in the Software without restriction,
           10 # including without limitation the rights to use, copy, modify, merge,
           11 # publish, distribute, sublicense, and/or sell copies of the Software,
           12 # and to permit persons to whom the Software is furnished to do so,
           13 # subject to the following conditions:
           14 #
           15 # The above copyright notice and this permission notice shall be
           16 # included in all copies or substantial portions of the Software.
           17 #
           18 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
           19 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
           20 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
           21 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
           22 # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
           23 # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
           24 # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
           25 # SOFTWARE.
           26 import os
           27 import sys
           28 import asyncio
           29 from typing import (Tuple, Union, List, TYPE_CHECKING, Optional, Set,
           30                     NamedTuple, Any, Sequence)
           31 from collections import defaultdict
           32 from ipaddress import (IPv4Network, IPv6Network, ip_address, IPv6Address,
           33                        IPv4Address)
           34 from binascii import hexlify, unhexlify
           35 import logging
           36 
           37 from aiorpcx import NetAddress
           38 import certifi
           39 
           40 from .util import (ignore_exceptions, log_exceptions, bfh, SilentTaskGroup,
           41                    MySocksProxy, is_integer, is_non_negative_integer,
           42                    is_hash256_str, is_hex_str, is_int_or_float,
           43                    is_non_negative_int_or_float)
           44 from . import util
           45 from . import version
           46 from . import blockchain
           47 from .blockchain import Blockchain, HEADER_SIZE
           48 from . import bitcoin
           49 from . import constants
           50 from . import zeromq
           51 from .i18n import _
           52 from .logging import Logger
           53 from .transaction import Transaction
           54 from .merkle import merkle_branch
           55 
           56 if TYPE_CHECKING:
           57     from .network import Network
           58     from .simple_config import SimpleConfig
           59 
           60 
           61 ca_path = certifi.where()
           62 
           63 BUCKET_NAME_OF_ONION_SERVERS = 'onion'
           64 
           65 MAX_INCOMING_MSG_SIZE = 1_000_000  # in bytes
           66 
           67 _KNOWN_NETWORK_PROTOCOLS = {'t', 's'}
           68 PREFERRED_NETWORK_PROTOCOL = 's'
           69 assert PREFERRED_NETWORK_PROTOCOL in _KNOWN_NETWORK_PROTOCOLS
           70 
           71 
           72 class NetworkTimeout:
           73     # seconds
           74     class Generic:
           75         NORMAL = 30
           76         RELAXED = 45
           77         MOST_RELAXED = 600
           78 
           79     class Urgent(Generic):
           80         NORMAL = 10
           81         RELAXED = 20
           82         MOST_RELAXED = 60
           83 
           84 
           85 def assert_non_negative_integer(val: Any) -> None:
           86     if not is_non_negative_integer(val):
           87         raise RequestCorrupted(f'{val!r} should be a non-negative integer')
           88 
           89 
           90 def assert_integer(val: Any) -> None:
           91     if not is_integer(val):
           92         raise RequestCorrupted(f'{val!r} should be an integer')
           93 
           94 
           95 def assert_int_or_float(val: Any) -> None:
           96     if not is_int_or_float(val):
           97         raise RequestCorrupted(f'{val!r} should be int or float')
           98 
           99 
          100 def assert_non_negative_int_or_float(val: Any) -> None:
          101     if not is_non_negative_int_or_float(val):
          102         raise RequestCorrupted(f'{val!r} should be a non-negative int or float')
          103 
          104 
          105 def assert_hash256_str(val: Any) -> None:
          106     if not is_hash256_str(val):
          107         raise RequestCorrupted(f'{val!r} should be a hash256 str')
          108 
          109 
          110 def assert_hex_str(val: Any) -> None:
          111     if not is_hex_str(val):
          112         raise RequestCorrupted(f'{val!r} should be a hex str')
          113 
          114 
          115 def assert_dict_contains_field(d: Any, *, field_name: str) -> Any:
          116     if not isinstance(d, dict):
          117         raise RequestCorrupted(f'{d!r} should be a dict')
          118     if field_name not in d:
          119         raise RequestCorrupted(f'required field {field_name!r} missing from dict')
          120     return d[field_name]
          121 
          122 def assert_list_or_tuple(val: Any) -> None:
          123     if not isinstance(val, (list, tuple)):
          124         raise RequestCorrupted(f'{val!r} should be a list or tuple')
          125 
          126 
          127 class NetworkException(Exception): pass
          128 
          129 
          130 class GracefulDisconnect(NetworkException):
          131     log_level = logging.INFO
          132 
          133     def __init__(self, *args, log_level=None, **kwargs):
          134         Exception.__init__(self, *args, **kwargs)
          135         if log_level is not None:
          136             self.log_level = log_level
          137 
          138 
          139 class RequestTimedOut(GracefulDisconnect):
          140     def __str__(self):
          141         return _("Network request timed out.")
          142 
          143 
          144 class RequestCorrupted(Exception): pass
          145 
          146 class ErrorParsingSSLCert(Exception): pass
          147 class ErrorGettingSSLCertFromServer(Exception): pass
          148 class ErrorSSLCertFingerprintMismatch(Exception): pass
          149 class InvalidOptionCombination(Exception): pass
          150 class ConnectError(NetworkException): pass
          151 
          152 
          153 class ServerAddr:
          154 
          155     def __init__(self, host: str, port: Union[int, str], *, protocol: str = None):
          156         assert isinstance(host, str), repr(host)
          157         if protocol is None:
          158             protocol = 's'
          159         if not host:
          160             raise ValueError('host must not be empty')
          161         if host[0] == '[' and host[-1] == ']':  # IPv6
          162             host = host[1:-1]
          163         try:
          164             net_addr = NetAddress(host, port)  # this validates host and port
          165         except Exception as e:
          166             raise ValueError(f"cannot construct ServerAddr: invalid host or port (host={host}, port={port})") from e
          167         if protocol not in _KNOWN_NETWORK_PROTOCOLS:
          168             raise ValueError(f"invalid network protocol: {protocol}")
          169         self.host = str(net_addr.host)  # canonical form (if e.g. IPv6 address)
          170         self.port = int(net_addr.port)
          171         self.protocol = protocol
          172         self._net_addr_str = str(net_addr)
          173 
          174     @classmethod
          175     def from_str(cls, s: str) -> 'ServerAddr':
          176         # host might be IPv6 address, hence do rsplit:
          177         host, port, protocol = str(s).rsplit(':', 2)
          178         return ServerAddr(host=host, port=port, protocol=protocol)
          179 
          180 
          181     @classmethod
          182     def from_str_with_inference(cls, s: str) -> Optional['ServerAddr']:
          183         """Construct ServerAddr from str, guessing missing details.
          184         Ongoing compatibility not guaranteed.
          185         """
          186         if not s:
          187             return None
          188         items = str(s).rsplit(':', 2)
          189         if len(items) < 2:
          190             return None  # although maybe we could guess the port too?
          191         host = items[0]
          192         port = items[1]
          193         if len(items) >= 3:
          194             protocol = items[2]
          195         else:
          196             protocol = PREFERRED_NETWORK_PROTOCOL
          197         return ServerAddr(host=host, port=port, protocol=protocol)
          198 
          199     def to_friendly_name(self) -> str:
          200         # note: this method is closely linked to from_str_with_inference
          201         if self.protocol == 's':  # hide trailing ":s"
          202             return self.net_addr_str()
          203         return str(self)
          204 
          205     def __str__(self):
          206         return '{}:{}'.format(self.net_addr_str(), self.protocol)
          207 
          208     def to_json(self) -> str:
          209         return str(self)
          210 
          211     def __repr__(self):
          212         return f'<ServerAddr host={self.host} port={self.port} protocol={self.protocol}>'
          213 
          214     def net_addr_str(self) -> str:
          215         return self._net_addr_str
          216 
          217     def __eq__(self, other):
          218         if not isinstance(other, ServerAddr):
          219             return False
          220         return (self.host == other.host
          221                 and self.port == other.port
          222                 and self.protocol == other.protocol)
          223 
          224     def __ne__(self, other):
          225         return not self == other
          226 
          227     def __hash__(self):
          228         return hash((self.host, self.port, self.protocol))
          229 
          230 
          231 def _get_cert_path_for_host(*, config: 'SimpleConfig', host: str) -> str:
          232     filename = host
          233     try:
          234         ip = ip_address(host)
          235     except ValueError:
          236         pass
          237     else:
          238         if isinstance(ip, IPv6Address):
          239             filename = f"ipv6_{ip.packed.hex()}"
          240     return os.path.join(config.path, 'certs', filename)
          241 
          242 
          243 from datetime import datetime
          244 def __(msg):
          245     print("***********************")
          246     print("*** DEBUG %s ***: %s" % (datetime.now().strftime("%H:%M:%S"), msg))
          247 
          248 
          249 class Interface(Logger):
          250 
          251     LOGGING_SHORTCUT = 'i'
          252 
          253     def __init__(self, *, network: 'Network', server: ServerAddr, proxy: Optional[dict]):
          254         __("Interface: __init__")
          255         self.ready = asyncio.Future()
          256         self.got_disconnected = asyncio.Event()
          257         self.server = server
          258         Logger.__init__(self)
          259         assert network.config.path
          260         self.cert_path = _get_cert_path_for_host(config=network.config, host=self.host)
          261         self.blockchain = None  # type: Optional[Blockchain]
          262         self._requested_chunks = set()  # type: Set[int]
          263         self.network = network
          264         self.proxy = MySocksProxy.from_proxy_dict(proxy)
          265         self.session = None  # type: Optional[NotificationSession]
          266         self._ipaddr_bucket = None
          267 
          268         # TODO: libbitcoin (these are for testnet2.libbitcoin.net)
          269         # This should be incorporated with ServerAddr somehow.
          270         self.client = None
          271         self.bs = 'testnet2.libbitcoin.net'
          272         self.bsports = {'query': 29091,
          273                         'heartbeat': 29092,
          274                         'block': 29093,
          275                         'tx': 29094}
          276 
          277         # Latest block header and corresponding height, as claimed by the server.
          278         # Note that these values are updated before they are verified.
          279         # Especially during initial header sync, verification can take a long time.
          280         # Failing verification will get the interface closed.
          281         self.tip_header = None
          282         self.tip = 0
          283 
          284         self.fee_estimates_eta = {}
          285 
          286         # Dump network messages (only for this interface).  Set at runtime from the console.
          287         self.debug = False
          288 
          289         self.taskgroup = SilentTaskGroup()
          290 
          291         async def spawn_task():
          292             __("Interface: spawn_task")
          293             task = await self.network.taskgroup.spawn(self.run())
          294             if sys.version_info >= (3, 8):
          295                 task.set_name(f"interface::{str(server)}")
          296         asyncio.run_coroutine_threadsafe(spawn_task(), self.network.asyncio_loop)
          297 
          298     @property
          299     def host(self):
          300         return self.server.host
          301 
          302     @property
          303     def port(self):
          304         return self.server.port
          305 
          306     @property
          307     def protocol(self):
          308         return self.server.protocol
          309 
          310     def diagnostic_name(self):
          311         return self.server.net_addr_str()
          312 
          313     def __str__(self):
          314         return f"<Interface {self.diagnostic_name()}>"
          315 
          316     # @ignore_exceptions  # do not kill network.taskgroup
          317     @log_exceptions
          318     # @handle_disconnect
          319     async def run(self):
          320         __("Interface: run")
          321         self.client = zeromq.Client(self.bs, self.bsports,
          322                                     loop=self.network.asyncio_loop)
          323         async with self.taskgroup as group:
          324             await group.spawn(self.ping)
          325             await group.spawn(self.request_fee_estimates)
          326             await group.spawn(self.run_fetch_blocks)
          327             await group.spawn(self.monitor_connection)
          328 
          329     def _mark_ready(self) -> None:
          330         __("Interface: _mark_ready")
          331         if self.ready.cancelled():
          332             raise GracefulDisconnect('conn establishment was too slow; %s' % '*ready* future was cancelled')
          333         if self.ready.done():
          334             return
          335 
          336         assert self.tip_header
          337         chain = blockchain.check_header(self.tip_header)
          338         if not chain:
          339             self.blockchain = blockchain.get_best_chain()
          340         else:
          341             self.blockchain = chain
          342         assert self.blockchain is not None
          343 
          344         self.logger.info(f"set blockchain with height {self.blockchain.height()}")
          345 
          346         self.ready.set_result(1)
          347 
          348     async def get_block_header(self, height, assert_mode):
          349         __(f"Interface: get_block_header: {height}")
          350         self.logger.info(f'requesting block header {height} in mode {assert_mode}')
          351         # use lower timeout as we usually have network.bhi_lock here
          352         timeout = self.network.get_network_timeout_seconds(NetworkTimeout.Urgent)
          353         # ORIG: res = await self.session.send_request('blockchain.block.header', [height], timeout=timeout)
          354         _ec, res = await self.client.block_header(height)
          355         if _ec is not None and _ec != 0:
          356             raise RequestCorrupted(f'got error {_ec}')
          357         #return blockchain.deserialize_header(bytes.fromhex(res), height)
          358         return blockchain.deserialize_header(res, height)
          359 
          360     async def request_chunk(self, height: int, tip=None, *, can_return_early=False):
          361         __("Interface: request_chunk")
          362         if not is_non_negative_integer(height):
          363             raise Exception(f"{repr(height)} is not a block height")
          364         index = height // 2016
          365         if can_return_early and index in self._requested_chunks:
          366             return
          367         self.logger.info(f"requesting chunk from height {height}")
          368         size = 2016
          369         if tip is not None:
          370             size = min(size, tip - index * 2016 + 1)
          371             size = max(size, 0)
          372         try:
          373             self._requested_chunks.add(index)
          374             #ORIG: res = await self.session.send_request('blockchain.block.headers', [index * 2016, size])
          375             concat = bytearray()
          376             for i in range(size):
          377                 _ec, data = await self.client.block_header(index*2016+i)
          378                 if _ec is not None and _ec != 0:
          379                     # TODO: Don't imply error means we reached tip
          380                     break
          381                 concat.extend(data)
          382         finally:
          383             self._requested_chunks.discard(index)
          384         # TODO: max in case of libbitcoin is unnecessary
          385         res = {
          386             'hex': str(hexlify(concat), 'utf-8'),
          387             'count': len(concat)//80,
          388             'max': 2016,
          389         }
          390         # TODO: cleanup
          391         assert_dict_contains_field(res, field_name='count')
          392         assert_dict_contains_field(res, field_name='hex')
          393         assert_dict_contains_field(res, field_name='max')
          394         assert_non_negative_integer(res['count'])
          395         assert_non_negative_integer(res['max'])
          396         assert_hex_str(res['hex'])
          397         if len(res['hex']) != HEADER_SIZE * 2 * res['count']:
          398             raise RequestCorrupted('inconsistent chunk hex and count')
          399         # we never request more than 2016 headers, but we enforce those fit in a single response
          400         if res['max'] < 2016:
          401             raise RequestCorrupted(f"server uses too low 'max' count for block.headers: {res['max']} < 2016")
          402         if res['count'] != size:
          403             raise RequestCorrupted(f"expected {size} headers but only got {res['count']}")
          404         conn = self.blockchain.connect_chunk(index, res['hex'])
          405         if not conn:
          406             return conn, 0
          407         return conn, res['count']
          408 
          409     def is_main_server(self) -> bool:
          410         # __("Interface: is_main_server")
          411         return (self.network.interface == self or
          412                 self.network.interface is None and self.network.default_server == self.server)
          413 
          414     async def monitor_connection(self):
          415         __("Interface: monitor_connection")
          416         while True:
          417             await asyncio.sleep(1)
          418             if not self.client:
          419                 # TODO: libbitcoin ^ Implement is_closing() in zeromq.Client and check ^
          420                 raise GracefulDisconnect('session was closed')
          421 
          422     async def ping(self):
          423         __("Interface: ping")
          424         while True:
          425             await asyncio.sleep(300)
          426             __("Interface: ping loop iteration")
          427             # TODO: libbitcoin bs heartbeat service here?
          428 
          429     async def request_fee_estimates(self):
          430         __("Interface: request_fee_estimates")
          431         from .simple_config import FEE_ETA_TARGETS
          432         while True:
          433             async with SilentTaskGroup() as group:
          434                 fee_tasks = []
          435                 for i in FEE_ETA_TARGETS:
          436                     fee_tasks.append((i, await group.spawn(self.get_estimatefee(i))))
          437             for nblock_target, task in fee_tasks:
          438                 fee = task.result()
          439                 if fee < 0: continue
          440                 self.fee_estimates_eta[nblock_target] = fee
          441             self.network.update_fee_estimates()
          442             await asyncio.sleep(60)
          443 
          444     async def close(self, *, force_after: int = None):
          445         __("Interface: close")
          446         # TODO: libbitcoin
          447         if self.session:
          448             await self.session.stop()
          449         if self.client:
          450             await self.client.stop()
          451 
          452     async def run_fetch_blocks(self):
          453         __("Interface: run_fetch_blocks")
          454         header_queue = asyncio.Queue()
          455         # ORIG: await self.session.subscribe('blockchain.headers.subscribe', [], header_queue)
          456         await self.client.subscribe_to_blocks(header_queue)
          457         while True:
          458             item = await header_queue.get()
          459             # TODO: block to header
          460             header = item[2]
          461             height = item[1]
          462             header = blockchain.deserialize_header(header, height)
          463             self.tip_header = header
          464             self.tip = height
          465             if self.tip < constants.net.max_checkpoint():
          466                 raise GracefulDisconnect('server tip below max checkpoint')
          467             self._mark_ready()
          468             await self._process_header_at_tip()
          469             # header processing done
          470             util.trigger_callback('blockchain_updated')
          471             util.trigger_callback('network_updated')
          472             await self.network.switch_unwanted_fork_interface()
          473             await self.network.switch_lagging_interface()
          474 
          475     async def _process_header_at_tip(self):
          476         __("Interface: _process_header_at_tip")
          477         height, header = self.tip, self.tip_header
          478         async with self.network.bhi_lock:
          479             if self.blockchain.height() >= height and self.blockchain.check_header(header):
          480                 # another interface amended the blockchain
          481                 self.logger.info(f"skipping header {height}")
          482                 return
          483             _, height = await self.step(height, header)
          484             # in the simple case, height == self.tip+1
          485             if height <= self.tip:
          486                 await self.sync_until(height)
          487 
          488     async def sync_until(self, height, next_height=None):
          489         __("Interface: sync_until")
          490         if next_height is None:
          491             next_height = self.tip
          492         last = None
          493         while last is None or height <= next_height:
          494             prev_last, prev_height = last, height
          495             if next_height > height + 10:
          496                 could_connect, num_headers = await self.request_chunk(height, next_height)
          497                 if not could_connect:
          498                     if height <= constants.net.max_checkpoint():
          499                         raise GracefulDisconnect('server chain conflicts with checkpoints or genesis')
          500                     last, height = await self.step(height)
          501                     continue
          502                 util.trigger_callback('network_updated')
          503                 height = (height // 2016 * 2016) + num_headers
          504                 assert height <= next_height+1, (height, self.tip)
          505                 last = 'catchup'
          506             else:
          507                 last, height = await self.step(height)
          508             assert (prev_last, prev_height) != (last, height), 'had to prevent infinite loop in interface.sync_until'
          509         return last, height
          510 
          511     async def step(self, height, header=None):
          512         __("Interface: step")
          513         assert 0 <= height <= self.tip, (height, self.tip)
          514         if header is None:
          515             header = await self.get_block_header(height, 'catchup')
          516 
          517         chain = blockchain.check_header(header) if 'mock' not in header else header['mock']['check'](header)
          518         if chain:
          519             self.blockchain = chain if isinstance(chain, Blockchain) else self.blockchain
          520             # note: there is an edge case here that is not handled.
          521             # we might know the blockhash (enough for check_header) but
          522             # not have the header itself. e.g. regtest chain with only genesis.
          523             # this situation resolves itself on the next block
          524             return 'catchup', height+1
          525 
          526         can_connect = blockchain.can_connect(header) if 'mock' not in header else header['mock']['connect'](height)
          527         if not can_connect:
          528             self.logger.info(f"can't connect {height}")
          529             height, header, bad, bad_header = await self._search_headers_backwards(height, header)
          530             chain = blockchain.check_header(header) if 'mock' not in header else header['mock']['check'](header)
          531             can_connect = blockchain.can_connect(header) if 'mock' not in header else header['mock']['connect'](height)
          532             assert chain or can_connect
          533         if can_connect:
          534             self.logger.info(f"could connect {height}")
          535             height += 1
          536             if isinstance(can_connect, Blockchain):  # not when mocking
          537                 self.blockchain = can_connect
          538                 self.blockchain.save_header(header)
          539             return 'catchup', height
          540 
          541         good, bad, bad_header = await self._search_headers_binary(height, bad, bad_header, chain)
          542         return await self._resolve_potential_chain_fork_given_forkpoint(good, bad, bad_header)
          543 
          544     async def _search_headers_binary(self, height, bad, bad_header, chain):
          545         __("Interface: _search_headers_binary")
          546         assert bad == bad_header['block_height']
          547         _assert_header_does_not_check_against_any_chain(bad_header)
          548 
          549         self.blockchain = chain if isinstance(chain, Blockchain) else self.blockchain
          550         good = height
          551         while True:
          552             assert good < bad, (good, bad)
          553             height = (good + bad) // 2
          554             self.logger.info(f"binary step. good {good}, bad {bad}, height {height}")
          555             header = await self.get_block_header(height, 'binary')
          556             chain = blockchain.check_header(header) if 'mock' not in header else header['mock']['check'](header)
          557             if chain:
          558                 self.blockchain = chain if isinstance(chain, Blockchain) else self.blockchain
          559                 good = height
          560             else:
          561                 bad = height
          562                 bad_header = header
          563             if good + 1 == bad:
          564                 break
          565 
          566         mock = 'mock' in bad_header and bad_header['mock']['connect'](height)
          567         real = not mock and self.blockchain.can_connect(bad_header, check_height=False)
          568         if not real and not mock:
          569             raise Exception('unexpected bad header during binary: {}'.format(bad_header))
          570         _assert_header_does_not_check_against_any_chain(bad_header)
          571 
          572         self.logger.info(f"binary search exited. good {good}, bad {bad}")
          573         return good, bad, bad_header
          574 
          575     async def _resolve_potential_chain_fork_given_forkpoint(self, good, bad, bad_header):
          576         __("Interface: _resolve_potential_chain_fork_given_forkpoint")
          577         assert good + 1 == bad
          578         assert bad == bad_header['block_height']
          579         _assert_header_does_not_check_against_any_chain(bad_header)
          580         # 'good' is the height of a block 'good_header', somewhere in self.blockchain.
          581         # bad_header connects to good_header; bad_header itself is NOT in self.blockchain.
          582 
          583         bh = self.blockchain.height()
          584         assert bh >= good, (bh, good)
          585         if bh == good:
          586             height = good + 1
          587             self.logger.info(f"catching up from {height}")
          588             return 'no_fork', height
          589 
          590         # this is a new fork we don't yet have
          591         height = bad + 1
          592         self.logger.info(f"new fork at bad height {bad}")
          593         forkfun = self.blockchain.fork if 'mock' not in bad_header else bad_header['mock']['fork']
          594         b = forkfun(bad_header)  # type: Blockchain
          595         self.blockchain = b
          596         assert b.forkpoint == bad
          597         return 'fork', height
          598 
          599     async def _search_headers_backwards(self, height, header):
          600         __("Interface: _search_headers_backwards")
          601         async def iterate():
          602             nonlocal height, header
          603             checkp = False
          604             if height <= constants.net.max_checkpoint():
          605                 height = constants.net.max_checkpoint()
          606                 checkp = True
          607             header = await self.get_block_header(height, 'backward')
          608             chain = blockchain.check_header(header) if 'mock' not in header else header['mock']['check'](header)
          609             can_connect = blockchain.can_connect(header) if 'mock' not in header else header['mock']['connect'](height)
          610             if chain or can_connect:
          611                 return False
          612             if checkp:
          613                 raise GracefulDisconnect("server chain conflicts with checkpoints")
          614             return True
          615 
          616         bad, bad_header = height, header
          617         _assert_header_does_not_check_against_any_chain(bad_header)
          618         with blockchain.blockchains_lock: chains = list(blockchain.blockchains.values())
          619         local_max = max([0] + [x.height() for x in chains]) if 'mock' not in header else float('inf')
          620         height = min(local_max + 1, height - 1)
          621         while await iterate():
          622             bad, bad_header = height, header
          623             delta = self.tip - height
          624             height = self.tip - 2 * delta
          625 
          626         _assert_header_does_not_check_against_any_chain(bad_header)
          627         self.logger.info(f"exiting backward mode at {height}")
          628         return height, header, bad, bad_header
          629 
          630     @classmethod
          631     def client_name(cls) -> str:
          632         __("Interface: client_name")
          633         return f'electrum/{version.ELECTRUM_VERSION}'
          634 
          635     def is_tor(self):
          636         __("Interface: is_tor")
          637         return self.host.endswith('.onion')
          638 
          639     def ip_addr(self) -> Optional[str]:
          640         __("Interface: ip_addr")
          641         return None
          642         # TODO: libbitcoin
          643         # This seems always None upstream since remote_address does not exist?
          644         # session = self.session
          645         # if not session: return None
          646         # peer_addr = session.remote_address()
          647         # if not peer_addr: return None
          648         # return str(peer_addr.host)
          649 
          650     def bucket_based_on_ipaddress(self) -> str:
          651         __("Interface: bucket_based_on_ipaddress")
          652         def do_bucket():
          653             if self.is_tor():
          654                 return BUCKET_NAME_OF_ONION_SERVERS
          655             try:
          656                 ip_addr = ip_address(self.ip_addr())  # type: Union[IPv5Address, IPv6Address]
          657             except ValueError:
          658                 return ''
          659             if not ip_addr:
          660                 return ''
          661             if ip_addr.is_loopback:  # localhost is exempt
          662                 return ''
          663             if ip_addr.version == 4:
          664                 slash16 = IPv4Network(ip_addr).supernet(prefixlen_diff=32-16)
          665                 return str(slash16)
          666             elif ip_addr.version == 6:
          667                 slash48 = IPv6Network(ip_addr).supernet(prefixlen_diff=128-48)
          668                 return str(slash48)
          669             return ''
          670 
          671         if not self._ipaddr_bucket:
          672             self._ipaddr_bucket = do_bucket()
          673         return self._ipaddr_bucket
          674 
          675     async def get_merkle_for_transaction(self, tx_hash: str, tx_height: int) -> dict:
          676         __("Interface: get_merkle_for_transaction")
          677         if not is_hash256_str(tx_hash):
          678             raise Exception(f"{repr(tx_hash)} is not a txid")
          679         if not is_non_negative_integer(tx_height):
          680             raise Exception(f"{repr(tx_height)} is not a block height")
          681         # do request
          682         # ORIG: res = await self.session.send_request('blockchain.transaction.get_merkle', [tx_hash, tx_height])
          683         # TODO: Rework to use txid rather than height with libbitcoin?
          684         _ec, hashes = await self.client.block_transaction_hashes(tx_height)
          685         if _ec is not None and _ec != 0:
          686             raise RequestCorrupted(f'got error {_ec}')
          687         tx_pos = hashes.index(unhexlify(tx_hash)[::-1])
          688         branch = merkle_branch(hashes, tx_pos)
          689         res = {'block_height': tx_height, 'merkle': branch, 'pos': tx_pos}
          690         block_height = assert_dict_contains_field(res, field_name='block_height')
          691         merkle = assert_dict_contains_field(res, field_name='merkle')
          692         pos = assert_dict_contains_field(res, field_name='pos')
          693         # note: tx_height was just a hint to the server, don't enforce the response to match it
          694         assert_non_negative_integer(block_height)
          695         assert_non_negative_integer(pos)
          696         assert_list_or_tuple(merkle)
          697         for item in merkle:
          698             assert_hash256_str(item)
          699         return res
          700 
          701     async def get_transaction(self, tx_hash: str, *, timeout=None) -> str:
          702         __("Interface: get_transaction")
          703         if not is_hash256_str(tx_hash):
          704             raise Exception(f"{repr(tx_hash)} is not a txid")
          705         # ORIG: raw = await self.session.send_request('blockchain.transaction.get', [tx_hash], timeout=timeout)
          706         #_ec, raw = await self.client.transaction(tx_hash)
          707         _ec, raw = await self.client.mempool_transaction(tx_hash)
          708         if _ec is not None and _ec != 0:
          709             raise RequestCorrupted(f"got error: {_ec!r}")
          710         # validate response
          711         if not is_hex_str(raw):
          712             raise RequestCorrupted(f"received garbage (non-hex) as tx data (txid {tx_hash}): {raw!r}")
          713         tx = Transaction(raw)
          714         try:
          715             tx.deserialize()  # see if raises
          716         except Exception as e:
          717             raise RequestCorrupted(f"cannot deserialize received transaction (txid {tx_hash})") from e
          718         if tx.txid() != tx_hash:
          719             raise RequestCorrupted(f"received tx does not match expected txid {tx_hash} (got {tx.txid()})")
          720         return raw
          721 
          722 
          723     async def get_history_for_scripthash(self, sh: str) -> List[dict]:
          724         __(f"Interface: get_history_for_scripthash {sh}")
          725         if not is_hash256_str(sh):
          726             raise Exception(f"{repr(sh)} is not a scripthash")
          727         # do request
          728         # ORIG: res = await self.session.send_request('blockchain.scripthash.get_history', [sh])
          729         _ec, history = await self.client.history4(sh)
          730         if _ec is not None and _ec != 0:
          731             raise RequestCorrupted('got error %d' % _ec)
          732         __("Interface: get_history_for_scripthash: got history: %s" % (history))
          733         res = {}
          734         # check response
          735         assert_list_or_tuple(res)
          736         prev_height = 1
          737         for tx_item in res:
          738             height = assert_dict_contains_field(tx_item, field_name='height')
          739             assert_dict_contains_field(tx_item, field_name='tx_hash')
          740             assert_integer(height)
          741             assert_hash256_str(tx_item['tx_hash'])
          742             if height in (-1, 0):
          743                 assert_dict_contains_field(tx_item, field_name='fee')
          744                 assert_non_negative_integer(tx_item['fee'])
          745                 prev_height = - float("inf")  # this ensures confirmed txs can't follow mempool txs
          746             else:
          747                 # check monotonicity of heights
          748                 if height < prev_height:
          749                     raise RequestCorrupted(f'heights of confirmed txs must be in increasing order')
          750                 prev_height = height
          751         hashes = set(map(lambda item: item['tx_hash'], res))
          752         if len(hashes) != len(res):
          753             # Either server is sending garbage... or maybe if server is race-prone
          754             # a recently mined tx could be included in both last block and mempool?
          755             # Still, it's simplest to just disregard the response.
          756             raise RequestCorrupted(f"server history has non-unique txids for sh={sh}")
          757 
          758         return res
          759 
          760     async def listunspent_for_scripthash(self, sh: str) -> List[dict]:
          761         __(f"Interface: listunspent_for_scripthash {sh}")
          762         if not is_hash256_str(sh):
          763             raise Exception(f"{repr(sh)} is not a scripthash")
          764         # do request
          765         # ORIG: res = await self.session.send_request('blockchain.scripthash.listunspent', [sh])
          766         _ec, unspent = await self.client.unspent(sh)
          767         if _ec is not None and _ec != 0:
          768             raise RequestCorrupted('got error %d' % _ec)
          769         __("Interface: listunspent_for_scripthash: got unspent: %s" % unspent)
          770         res = {}
          771         # check response
          772         assert_list_or_tuple(res)
          773         for utxo_item in res:
          774             assert_dict_contains_field(utxo_item, field_name='tx_pos')
          775             assert_dict_contains_field(utxo_item, field_name='value')
          776             assert_dict_contains_field(utxo_item, field_name='tx_hash')
          777             assert_dict_contains_field(utxo_item, field_name='height')
          778             assert_non_negative_integer(utxo_item['tx_pos'])
          779             assert_non_negative_integer(utxo_item['value'])
          780             assert_non_negative_integer(utxo_item['height'])
          781             assert_hash256_str(utxo_item['tx_hash'])
          782         return res
          783 
          784     async def get_balance_for_scripthash(self, sh: str) -> dict:
          785         __(f"Interface: get_balance_for_scripthash {sh}")
          786         if not is_hash256_str(sh):
          787             raise Exception(f"{repr(sh)} is not a scripthash")
          788         # do request
          789         # ORIG: res = await self.sessions.send_request('blockchains.scripthash.get_balance', [sh])
          790         _ec, balance = await self.client.balance(sh)
          791         if _ec is not None and _ec != 0:
          792             raise RequestCorrupted('got error %d' % _ec)
          793         __("Interface: get_balance_for_scripthash: got balance: %s" % balance)
          794         # TODO: libbitcoin
          795         res = {}
          796         # check response
          797         assert_dict_contains_field(res, field_name='confirmed')
          798         assert_dict_contains_field(res, field_name='unconfirmed')
          799         assert_non_negative_integer(res['confirmed'])
          800         assert_non_negative_integer(res['unconfirmed'])
          801         return res
          802 
          803     async def get_txid_from_txpos(self, tx_height: int, tx_pos: int, merkle: bool):
          804         __("Interface: get_txid_from_txpos")
          805         if not is_non_negative_integer(tx_height):
          806             raise Exception(f"{repr(tx_height)} is not a block height")
          807         if not is_non_negative_integer(tx_pos):
          808             raise Exception(f"{repr(tx_pos)} should be non-negative integer")
          809         # do request
          810         # ORIG: res = await self.session.send_request(
          811             # 'blockchain.transaction.id_from_pos',
          812             # [tx_height, tx_pos, merkle],
          813         # )
          814         _ec, hashes = await self.client.block_transaction_hashes(tx_height)
          815         if _ec is not None and _ec != 0:
          816             raise RequestCorrupted('got error %d' % _ec)
          817         txid = hexlify(hashes[tx_pos][::-1])
          818         # check response
          819         if not merkle:
          820             assert_hash256_str(txid)
          821             return txid
          822         branch = merkle_branch(hashes, tx_pos)
          823         res = {'tx_hash': txid, 'merkle': branch}
          824         assert_dict_contains_field(res, field_name='tx_hash')
          825         assert_dict_contains_field(res, field_name='merkle')
          826         assert_hash256_str(res['tx_hash'])
          827         assert_list_or_tuple(res['merkle'])
          828         for node_hash in res['merkle']:
          829             assert_hash256_str(node_hash)
          830         return res
          831 
          832     async def get_fee_histogram(self) -> Sequence[Tuple[Union[float, int], int]]:
          833         __("Interface: get_fee_histogram")
          834         # do request
          835         # ORIG: res = await self.session.send_request('mempool.get_fee_histogram')
          836         # TODO: libbitcoin
          837         res = [[0, 0]]
          838         # check response
          839         assert_list_or_tuple(res)
          840         prev_fee = float('inf')
          841         for fee, s in res:
          842             assert_non_negative_int_or_float(fee)
          843             assert_non_negative_integer(s)
          844             if fee >= prev_fee:  # check monotonicity
          845                 raise RequestCorrupted(f'fees must be in decreasing order')
          846             prev_fee = fee
          847         return res
          848 
          849     async def get_server_banner(self) -> str:
          850         __("Interface: get_server_banner")
          851         # do request
          852         # ORIG: res = await self.session.send_request('server.banner')
          853         # TODO: libbitcoin
          854         res = 'libbitcoin'
          855         # check response
          856         if not isinstance(res, str):
          857             raise RequestCorrupted(f'{res!r} should be a str')
          858         return res
          859 
          860     async def get_donation_address(self) -> str:
          861         __("Interface: get_donation_address")
          862         # do request
          863         # ORIG: res = await self.session.send_request('server.donation_address')
          864         # TODO: libbitcoin
          865         res = None
          866         # check response
          867         if not res:  # ignore empty string
          868             return ''
          869         if not bitcoin.is_address(res):
          870             # note: do not hard-fail -- allow server to use future-type
          871             #       bitcoin address we do not recognize
          872             self.logger.info(f"invalid donation address from server: {repr(res)}")
          873             res = ''
          874         return res
          875 
          876     async def get_relay_fee(self) -> int:
          877         """Returns the min relay feerate in sat/kbyte."""
          878         __("Interface: get_relay_fee")
          879         # do request
          880         # ORIG: res = await self.session.send_request('blockchain.relayfee')
          881         # TODO: libbitcoin
          882         res = 0.00001
          883         # check response
          884         assert_non_negative_int_or_float(res)
          885         relayfee = int(res * bitcoin.COIN)
          886         relayfee = max(0, relayfee)
          887         return relayfee
          888 
          889     async def get_estimatefee(self, num_blocks: int) -> int:
          890         """Returns a feerate estimtte for getting confirmed within
          891         num_blocks blocks, in sat/kbyte.
          892         """
          893         __("Interface: get_estimatefee")
          894         if not is_non_negative_integer(num_blocks):
          895             raise Exception(f"{repr(num_blocks)} is not a num_blocks")
          896         # do request
          897         # ORIG: res = await self.session.send_request('blockchain.estimatefee', [num_blocks])
          898         # TODO: libbitcoin
          899         res = -1
          900         # check response
          901         if res != -1:
          902             assert_non_negative_int_or_float(res)
          903             res = int(res * bitcoin.COIN)
          904         return res
          905 
          906     async def broadcast_transaction(self, tx, timeout=None):
          907         """Broadcasts given transaction"""
          908         __("Interface: broadcast_transaction")
          909         assert_hex_str(tx)
          910         return await self.client.broadcast_transaction(tx)
          911 
          912 
          913 def _assert_header_does_not_check_against_any_chain(header: dict) -> None:
          914     __("Interface: _assert_header_does_not_check_against_any_chain")
          915     chain_bad = blockchain.check_header(header) if 'mock' not in header else header['mock']['check'](header)
          916     if chain_bad:
          917         raise Exception('bad_header must not check!')