5050from .util import (ignore_exceptions , log_exceptions , bfh , ESocksProxy ,
5151 is_integer , is_non_negative_integer , is_hash256_str , is_hex_str ,
5252 is_int_or_float , is_non_negative_int_or_float , OldTaskGroup ,
53- send_exception_to_crash_reporter , error_text_str_to_safe_str )
53+ send_exception_to_crash_reporter , error_text_str_to_safe_str , versiontuple )
5454from . import util
5555from . import x509
5656from . import pem
@@ -139,6 +139,18 @@ def assert_list_or_tuple(val: Any) -> None:
139139 raise RequestCorrupted (f'{ val !r} should be a list or tuple' )
140140
141141
142+ def protocol_tuple (s : Any ) -> tuple [int , ...]:
143+ """Converts a protocol version number, such as "1.0" to a tuple (1, 0).
144+
145+ If the version number is bad, (0, ) indicating version 0 is returned.
146+ """
147+ try :
148+ assert isinstance (s , str )
149+ return versiontuple (s )
150+ except Exception :
151+ return (0 , )
152+
153+
142154class ChainResolutionMode (enum .Enum ):
143155 CATCHUP = enum .auto ()
144156 BACKWARD = enum .auto ()
@@ -574,6 +586,8 @@ def __init__(self, *, network: 'Network', server: ServerAddr):
574586
575587 self .fee_estimates_eta = {} # type: Dict[int, int]
576588
589+ self .active_protocol_tuple = (0 ,) # type: Optional[tuple[int, ...]]
590+
577591 # Dump network messages (only for this interface). Set at runtime from the console.
578592 self .debug = False
579593
@@ -854,13 +868,25 @@ async def get_block_headers(
854868 res = await self .session .send_request ('blockchain.block.headers' , [start_height , count ], timeout = timeout )
855869 # check response
856870 assert_dict_contains_field (res , field_name = 'count' )
857- assert_dict_contains_field (res , field_name = 'hex' )
858871 assert_dict_contains_field (res , field_name = 'max' )
859872 assert_non_negative_integer (res ['count' ])
860873 assert_non_negative_integer (res ['max' ])
861- assert_hex_str (res ['hex' ])
862- if len (res ['hex' ]) != HEADER_SIZE * 2 * res ['count' ]:
863- raise RequestCorrupted ('inconsistent chunk hex and count' )
874+ if self .active_protocol_tuple >= (1 , 6 ):
875+ hex_headers_list = assert_dict_contains_field (res , field_name = 'headers' )
876+ assert_list_or_tuple (hex_headers_list )
877+ for item in hex_headers_list :
878+ assert_hex_str (item )
879+ if len (item ) != HEADER_SIZE * 2 :
880+ raise RequestCorrupted (f"invalid header size. got { len (item )// 2 } , expected { HEADER_SIZE } " )
881+ if len (hex_headers_list ) != res ['count' ]:
882+ raise RequestCorrupted (f"{ len (hex_headers_list )= } != { res ['count' ]= } " )
883+ headers = list (bfh (hex_header ) for hex_header in hex_headers_list )
884+ else : # proto 1.4
885+ hex_headers_concat = assert_dict_contains_field (res , field_name = 'hex' )
886+ assert_hex_str (hex_headers_concat )
887+ if len (hex_headers_concat ) != HEADER_SIZE * 2 * res ['count' ]:
888+ raise RequestCorrupted ('inconsistent chunk hex and count' )
889+ headers = list (util .chunks (bfh (hex_headers_concat ), size = HEADER_SIZE ))
864890 # we never request more than MAX_NUM_HEADERS_IN_REQUEST headers, but we enforce those fit in a single response
865891 if res ['max' ] < MAX_NUM_HEADERS_PER_REQUEST :
866892 raise RequestCorrupted (f"server uses too low 'max' count for block.headers: { res ['max' ]} < { MAX_NUM_HEADERS_PER_REQUEST } " )
@@ -873,7 +899,6 @@ async def get_block_headers(
873899 raise RequestCorrupted (
874900 f"asked for { count } headers but got fewer: { res ['count' ]} . ({ start_height = } , { self .tip = } )" )
875901 # checks done.
876- headers = list (util .chunks (bfh (res ['hex' ]), size = HEADER_SIZE ))
877902 return headers
878903
879904 async def request_chunk_below_max_checkpoint (
@@ -964,15 +989,19 @@ async def open_session(
964989 start = time .perf_counter ()
965990 self .session = session # type: NotificationSession
966991 self .session .set_default_timeout (self .network .get_network_timeout_seconds (NetworkTimeout .Generic ))
992+ client_prange = [version .PROTOCOL_VERSION_MIN , version .PROTOCOL_VERSION_MAX ]
967993 try :
968- ver = await session .send_request ('server.version' , [self .client_name (), version . PROTOCOL_VERSION ])
994+ ver = await session .send_request ('server.version' , [self .client_name (), client_prange ])
969995 except aiorpcx .jsonrpc .RPCError as e :
970996 raise GracefulDisconnect (e ) # probably 'unsupported protocol version'
971997 if exit_early :
972998 return
973- if ver [1 ] != version .PROTOCOL_VERSION :
999+ self .active_protocol_tuple = protocol_tuple (ver [1 ])
1000+ client_pmin = protocol_tuple (client_prange [0 ])
1001+ client_pmax = protocol_tuple (client_prange [1 ])
1002+ if not (client_pmin <= self .active_protocol_tuple <= client_pmax ):
9741003 raise GracefulDisconnect (f'server violated protocol-version-negotiation. '
975- f'we asked for { version . PROTOCOL_VERSION !r} , they sent { ver [1 ]!r} ' )
1004+ f'we asked for { client_prange !r} , they sent { ver [1 ]!r} ' )
9761005 if not self .network .check_interface_against_healthy_spread_of_connected_servers (self ):
9771006 raise GracefulDisconnect (f'too many connected servers already '
9781007 f'in bucket { self .bucket_based_on_ipaddress ()} ' )
@@ -1387,6 +1416,33 @@ async def broadcast_transaction(self, tx: 'Transaction', *, timeout=None) -> Non
13871416 # the status of a scripthash we are subscribed to. Caching here will save a future get_transaction RPC.
13881417 self ._rawtx_cache [txid_calc ] = bytes .fromhex (rawtx )
13891418
1419+ async def broadcast_txpackage (self , txs : Sequence ['Transaction' ]) -> bool :
1420+ assert self .active_protocol_tuple >= (1 , 6 ), f"server using old protocol: { self .active_protocol_tuple } "
1421+ rawtxs = [tx .serialize () for tx in txs ]
1422+ assert all (is_hex_str (rawtx ) for rawtx in rawtxs )
1423+ assert all (tx .txid () is not None for tx in txs )
1424+ timeout = self .network .get_network_timeout_seconds (NetworkTimeout .Urgent )
1425+ for tx in txs :
1426+ if any (DummyAddress .is_dummy_address (txout .address ) for txout in tx .outputs ()):
1427+ raise DummyAddressUsedInTxException ("tried to broadcast tx with dummy address!" )
1428+ try :
1429+ res = await self .session .send_request ('blockchain.transaction.broadcast_package' , [rawtxs ], timeout = timeout )
1430+ except aiorpcx .jsonrpc .CodeMessageError as e :
1431+ self .logger .info (f"broadcast_txpackage error [DO NOT TRUST THIS MESSAGE]: { error_text_str_to_safe_str (repr (e ))} . { rawtxs = } " )
1432+ return False
1433+ success = assert_dict_contains_field (res , field_name = 'success' )
1434+ if not success :
1435+ errors = assert_dict_contains_field (res , field_name = 'errors' )
1436+ self .logger .info (f"broadcast_txpackage error [DO NOT TRUST THIS MESSAGE]: { error_text_str_to_safe_str (repr (errors ))} . { rawtxs = } " )
1437+ return False
1438+ assert success
1439+ # broadcast succeeded.
1440+ # We now cache the rawtx, for *this interface only*. The tx likely touches some ismine addresses, affecting
1441+ # the status of a scripthash we are subscribed to. Caching here will save a future get_transaction RPC.
1442+ for tx , rawtx in zip (txs , rawtxs ):
1443+ self ._rawtx_cache [tx .txid ()] = bytes .fromhex (rawtx )
1444+ return True
1445+
13901446 async def get_history_for_scripthash (self , sh : str ) -> List [dict ]:
13911447 if not is_hash256_str (sh ):
13921448 raise Exception (f"{ repr (sh )} is not a scripthash" )
@@ -1399,6 +1455,8 @@ async def get_history_for_scripthash(self, sh: str) -> List[dict]:
13991455 height = assert_dict_contains_field (tx_item , field_name = 'height' )
14001456 assert_dict_contains_field (tx_item , field_name = 'tx_hash' )
14011457 assert_integer (height )
1458+ if height < - 1 :
1459+ raise RequestCorrupted (f'{ height !r} is not a valid block height' )
14021460 assert_hash256_str (tx_item ['tx_hash' ])
14031461 if height in (- 1 , 0 ):
14041462 assert_dict_contains_field (tx_item , field_name = 'fee' )
@@ -1409,6 +1467,11 @@ async def get_history_for_scripthash(self, sh: str) -> List[dict]:
14091467 if height < prev_height :
14101468 raise RequestCorrupted (f'heights of confirmed txs must be in increasing order' )
14111469 prev_height = height
1470+ if self .active_protocol_tuple >= (1 , 6 ):
1471+ # enforce order of mempool txs
1472+ mempool_txs = [tx_item for tx_item in res if tx_item ['height' ] <= 0 ]
1473+ if mempool_txs != sorted (mempool_txs , key = lambda x : (- x ['height' ], bytes .fromhex (x ['tx_hash' ]))):
1474+ raise RequestCorrupted (f'mempool txs not in canonical order' )
14121475 hashes = set (map (lambda item : item ['tx_hash' ], res ))
14131476 if len (hashes ) != len (res ):
14141477 # Either server is sending garbage... or maybe if server is race-prone
@@ -1507,10 +1570,14 @@ async def get_donation_address(self) -> str:
15071570 async def get_relay_fee (self ) -> int :
15081571 """Returns the min relay feerate in sat/kbyte."""
15091572 # do request
1510- res = await self .session .send_request ('blockchain.relayfee' )
1573+ if self .active_protocol_tuple >= (1 , 6 ):
1574+ res = await self .session .send_request ('mempool.get_info' )
1575+ minrelaytxfee = assert_dict_contains_field (res , field_name = 'minrelaytxfee' )
1576+ else :
1577+ minrelaytxfee = await self .session .send_request ('blockchain.relayfee' )
15111578 # check response
1512- assert_non_negative_int_or_float (res )
1513- relayfee = int (res * bitcoin .COIN )
1579+ assert_non_negative_int_or_float (minrelaytxfee )
1580+ relayfee = int (minrelaytxfee * bitcoin .COIN )
15141581 relayfee = max (0 , relayfee )
15151582 return relayfee
15161583
0 commit comments