electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

interface.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!')