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