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