diff --git a/torba/rpc/framing.py b/torba/rpc/framing.py index 6a5c2b9be..754d68139 100644 --- a/torba/rpc/framing.py +++ b/torba/rpc/framing.py @@ -23,7 +23,7 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -'''RPC message framing in a byte stream.''' +"""RPC message framing in a byte stream.""" __all__ = ('FramerBase', 'NewlineFramer', 'BinaryFramer', 'BitcoinFramer', 'OversizedPayloadError', 'BadChecksumError', 'BadMagicError') @@ -34,38 +34,38 @@ from asyncio import Queue class FramerBase(object): - '''Abstract base class for a framer. + """Abstract base class for a framer. A framer breaks an incoming byte stream into protocol messages, buffering if necesary. It also frames outgoing messages into a byte stream. - ''' + """ def frame(self, message): - '''Return the framed message.''' + """Return the framed message.""" raise NotImplementedError def received_bytes(self, data): - '''Pass incoming network bytes.''' + """Pass incoming network bytes.""" raise NotImplementedError async def receive_message(self): - '''Wait for a complete unframed message to arrive, and return it.''' + """Wait for a complete unframed message to arrive, and return it.""" raise NotImplementedError class NewlineFramer(FramerBase): - '''A framer for a protocol where messages are separated by newlines.''' + """A framer for a protocol where messages are separated by newlines.""" # The default max_size value is motivated by JSONRPC, where a # normal request will be 250 bytes or less, and a reasonable # batch may contain 4000 requests. def __init__(self, max_size=250 * 4000): - '''max_size - an anti-DoS measure. If, after processing an incoming + """max_size - an anti-DoS measure. If, after processing an incoming message, buffered data would exceed max_size bytes, that buffered data is dropped entirely and the framer waits for a newline character to re-synchronize the stream. - ''' + """ self.max_size = max_size self.queue = Queue() self.received_bytes = self.queue.put_nowait @@ -105,9 +105,9 @@ class NewlineFramer(FramerBase): class ByteQueue(object): - '''A producer-comsumer queue. Incoming network data is put as it + """A producer-comsumer queue. Incoming network data is put as it arrives, and the consumer calls an async method waiting for data of - a specific length.''' + a specific length.""" def __init__(self): self.queue = Queue() @@ -127,7 +127,7 @@ class ByteQueue(object): class BinaryFramer(object): - '''A framer for binary messaging protocols.''' + """A framer for binary messaging protocols.""" def __init__(self): self.byte_queue = ByteQueue() @@ -165,12 +165,12 @@ pack_le_uint32 = struct_le_I.pack def sha256(x): - '''Simple wrapper of hashlib sha256.''' + """Simple wrapper of hashlib sha256.""" return _sha256(x).digest() def double_sha256(x): - '''SHA-256 of SHA-256, as used extensively in bitcoin.''' + """SHA-256 of SHA-256, as used extensively in bitcoin.""" return sha256(sha256(x)) @@ -187,7 +187,7 @@ class OversizedPayloadError(Exception): class BitcoinFramer(BinaryFramer): - '''Provides a framer of binary message payloads in the style of the + """Provides a framer of binary message payloads in the style of the Bitcoin network protocol. Each binary message has the following elements, in order: @@ -201,7 +201,7 @@ class BitcoinFramer(BinaryFramer): Call frame(command, payload) to get a framed message. Pass incoming network bytes to received_bytes(). Wait on receive_message() to get incoming (command, payload) pairs. - ''' + """ def __init__(self, magic, max_block_size): def pad_command(command): diff --git a/torba/rpc/jsonrpc.py b/torba/rpc/jsonrpc.py index 3aaa95e83..84830ecd2 100644 --- a/torba/rpc/jsonrpc.py +++ b/torba/rpc/jsonrpc.py @@ -23,7 +23,7 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -'''Classes for JSONRPC versions 1.0 and 2.0, and a loose interpretation.''' +"""Classes for JSONRPC versions 1.0 and 2.0, and a loose interpretation.""" __all__ = ('JSONRPC', 'JSONRPCv1', 'JSONRPCv2', 'JSONRPCLoose', 'JSONRPCAutoDetect', 'Request', 'Notification', 'Batch', @@ -156,7 +156,7 @@ class ProtocolError(CodeMessageError): class JSONRPC(object): - '''Abstract base class that interprets and constructs JSON RPC messages.''' + """Abstract base class that interprets and constructs JSON RPC messages.""" # Error codes. See http://www.jsonrpc.org/specification PARSE_ERROR = -32700 @@ -172,24 +172,24 @@ class JSONRPC(object): @classmethod def _message_id(cls, message, require_id): - '''Validate the message is a dictionary and return its ID. + """Validate the message is a dictionary and return its ID. Raise an error if the message is invalid or the ID is of an invalid type. If it has no ID, raise an error if require_id is True, otherwise return None. - ''' + """ raise NotImplementedError @classmethod def _validate_message(cls, message): - '''Validate other parts of the message other than those - done in _message_id.''' + """Validate other parts of the message other than those + done in _message_id.""" pass @classmethod def _request_args(cls, request): - '''Validate the existence and type of the arguments passed - in the request dictionary.''' + """Validate the existence and type of the arguments passed + in the request dictionary.""" raise NotImplementedError @classmethod @@ -221,7 +221,7 @@ class JSONRPC(object): @classmethod def _message_to_payload(cls, message): - '''Returns a Python object or a ProtocolError.''' + """Returns a Python object or a ProtocolError.""" try: return json.loads(message.decode()) except UnicodeDecodeError: @@ -245,7 +245,7 @@ class JSONRPC(object): @classmethod def message_to_item(cls, message): - '''Translate an unframed received message and return an + """Translate an unframed received message and return an (item, request_id) pair. The item can be a Request, Notification, Response or a list. @@ -264,7 +264,7 @@ class JSONRPC(object): the response was bad. raises: ProtocolError - ''' + """ payload = cls._message_to_payload(message) if isinstance(payload, dict): if 'method' in payload: @@ -282,19 +282,19 @@ class JSONRPC(object): # Message formation @classmethod def request_message(cls, item, request_id): - '''Convert an RPCRequest item to a message.''' + """Convert an RPCRequest item to a message.""" assert isinstance(item, Request) return cls.encode_payload(cls.request_payload(item, request_id)) @classmethod def notification_message(cls, item): - '''Convert an RPCRequest item to a message.''' + """Convert an RPCRequest item to a message.""" assert isinstance(item, Notification) return cls.encode_payload(cls.request_payload(item, None)) @classmethod def response_message(cls, result, request_id): - '''Convert a response result (or RPCError) to a message.''' + """Convert a response result (or RPCError) to a message.""" if isinstance(result, CodeMessageError): payload = cls.error_payload(result, request_id) else: @@ -303,7 +303,7 @@ class JSONRPC(object): @classmethod def batch_message(cls, batch, request_ids): - '''Convert a request Batch to a message.''' + """Convert a request Batch to a message.""" assert isinstance(batch, Batch) if not cls.allow_batches: raise ProtocolError.invalid_request( @@ -317,9 +317,9 @@ class JSONRPC(object): @classmethod def batch_message_from_parts(cls, messages): - '''Convert messages, one per batch item, into a batch message. At + """Convert messages, one per batch item, into a batch message. At least one message must be passed. - ''' + """ # Comma-separate the messages and wrap the lot in square brackets middle = b', '.join(messages) if not middle: @@ -328,7 +328,7 @@ class JSONRPC(object): @classmethod def encode_payload(cls, payload): - '''Encode a Python object as JSON and convert it to bytes.''' + """Encode a Python object as JSON and convert it to bytes.""" try: return json.dumps(payload).encode() except TypeError: @@ -337,7 +337,7 @@ class JSONRPC(object): class JSONRPCv1(JSONRPC): - '''JSON RPC version 1.0.''' + """JSON RPC version 1.0.""" allow_batches = False @@ -392,7 +392,7 @@ class JSONRPCv1(JSONRPC): @classmethod def request_payload(cls, request, request_id): - '''JSON v1 request (or notification) payload.''' + """JSON v1 request (or notification) payload.""" if isinstance(request.args, dict): raise ProtocolError.invalid_args( 'JSONRPCv1 does not support named arguments') @@ -404,7 +404,7 @@ class JSONRPCv1(JSONRPC): @classmethod def response_payload(cls, result, request_id): - '''JSON v1 response payload.''' + """JSON v1 response payload.""" return { 'result': result, 'error': None, @@ -421,7 +421,7 @@ class JSONRPCv1(JSONRPC): class JSONRPCv2(JSONRPC): - '''JSON RPC version 2.0.''' + """JSON RPC version 2.0.""" @classmethod def _message_id(cls, message, require_id): @@ -477,7 +477,7 @@ class JSONRPCv2(JSONRPC): @classmethod def request_payload(cls, request, request_id): - '''JSON v2 request (or notification) payload.''' + """JSON v2 request (or notification) payload.""" payload = { 'jsonrpc': '2.0', 'method': request.method, @@ -492,7 +492,7 @@ class JSONRPCv2(JSONRPC): @classmethod def response_payload(cls, result, request_id): - '''JSON v2 response payload.''' + """JSON v2 response payload.""" return { 'jsonrpc': '2.0', 'result': result, @@ -509,7 +509,7 @@ class JSONRPCv2(JSONRPC): class JSONRPCLoose(JSONRPC): - '''A relaxed versin of JSON RPC.''' + """A relaxed versin of JSON RPC.""" # Don't be so loose we accept any old message ID _message_id = JSONRPCv2._message_id @@ -546,7 +546,7 @@ class JSONRPCAutoDetect(JSONRPCv2): @classmethod def detect_protocol(cls, message): - '''Attempt to detect the protocol from the message.''' + """Attempt to detect the protocol from the message.""" main = cls._message_to_payload(message) def protocol_for_payload(payload): @@ -581,13 +581,13 @@ class JSONRPCAutoDetect(JSONRPCv2): class JSONRPCConnection(object): - '''Maintains state of a JSON RPC connection, in particular + """Maintains state of a JSON RPC connection, in particular encapsulating the handling of request IDs. protocol - the JSON RPC protocol to follow max_response_size - responses over this size send an error response instead. - ''' + """ _id_counter = itertools.count() @@ -684,7 +684,7 @@ class JSONRPCConnection(object): # External API # def send_request(self, request): - '''Send a Request. Return a (message, event) pair. + """Send a Request. Return a (message, event) pair. The message is an unframed message to send over the network. Wait on the event for the response; which will be in the @@ -692,7 +692,7 @@ class JSONRPCConnection(object): Raises: ProtocolError if the request violates the protocol in some way.. - ''' + """ request_id = next(self._id_counter) message = self._protocol.request_message(request, request_id) return message, self._event(request, request_id) @@ -708,13 +708,13 @@ class JSONRPCConnection(object): return message, event def receive_message(self, message): - '''Call with an unframed message received from the network. + """Call with an unframed message received from the network. Raises: ProtocolError if the message violates the protocol in some way. However, if it happened in a response that can be paired with a request, the ProtocolError is instead set in the result attribute of the send_request() that caused the error. - ''' + """ try: item, request_id = self._protocol.message_to_item(message) except ProtocolError as e: @@ -743,7 +743,7 @@ class JSONRPCConnection(object): return self.receive_message(message) def cancel_pending_requests(self): - '''Cancel all pending requests.''' + """Cancel all pending requests.""" exception = CancelledError() for request, event in self._requests.values(): event.result = exception @@ -751,7 +751,7 @@ class JSONRPCConnection(object): self._requests.clear() def pending_requests(self): - '''All sent requests that have not received a response.''' + """All sent requests that have not received a response.""" return [request for request, event in self._requests.values()] diff --git a/torba/rpc/session.py b/torba/rpc/session.py index e23f78b47..c8b8c6945 100644 --- a/torba/rpc/session.py +++ b/torba/rpc/session.py @@ -54,7 +54,7 @@ class Connector: self.kwargs = kwargs async def create_connection(self): - '''Initiate a connection.''' + """Initiate a connection.""" connector = self.proxy or self.loop return await connector.create_connection( self.session_factory, self.host, self.port, **self.kwargs) @@ -70,7 +70,7 @@ class Connector: class SessionBase(asyncio.Protocol): - '''Base class of networking sessions. + """Base class of networking sessions. There is no client / server distinction other than who initiated the connection. @@ -81,7 +81,7 @@ class SessionBase(asyncio.Protocol): Alternatively if used in a with statement, the connection is made on entry to the block, and closed on exit from the block. - ''' + """ max_errors = 10 @@ -138,7 +138,7 @@ class SessionBase(asyncio.Protocol): await self._concurrency.set_max_concurrent(target) def _using_bandwidth(self, size): - '''Called when sending or receiving size bytes.''' + """Called when sending or receiving size bytes.""" self.bw_charge += size async def _limited_wait(self, secs): @@ -173,7 +173,7 @@ class SessionBase(asyncio.Protocol): # asyncio framework def data_received(self, framed_message): - '''Called by asyncio when a message comes in.''' + """Called by asyncio when a message comes in.""" if self.verbosity >= 4: self.logger.debug(f'Received framed message {framed_message}') self.recv_size += len(framed_message) @@ -181,21 +181,21 @@ class SessionBase(asyncio.Protocol): self.framer.received_bytes(framed_message) def pause_writing(self): - '''Transport calls when the send buffer is full.''' + """Transport calls when the send buffer is full.""" if not self.is_closing(): self._can_send.clear() self.transport.pause_reading() def resume_writing(self): - '''Transport calls when the send buffer has room.''' + """Transport calls when the send buffer has room.""" if not self._can_send.is_set(): self._can_send.set() self.transport.resume_reading() def connection_made(self, transport): - '''Called by asyncio when a connection is established. + """Called by asyncio when a connection is established. - Derived classes overriding this method must call this first.''' + Derived classes overriding this method must call this first.""" self.transport = transport # This would throw if called on a closed SSL transport. Fixed # in asyncio in Python 3.6.1 and 3.5.4 @@ -209,9 +209,9 @@ class SessionBase(asyncio.Protocol): self._pm_task = self.loop.create_task(self._receive_messages()) def connection_lost(self, exc): - '''Called by asyncio when the connection closes. + """Called by asyncio when the connection closes. - Tear down things done in connection_made.''' + Tear down things done in connection_made.""" self._address = None self.transport = None self._task_group.cancel() @@ -221,21 +221,21 @@ class SessionBase(asyncio.Protocol): # External API def default_framer(self): - '''Return a default framer.''' + """Return a default framer.""" raise NotImplementedError def peer_address(self): - '''Returns the peer's address (Python networking address), or None if + """Returns the peer's address (Python networking address), or None if no connection or an error. This is the result of socket.getpeername() when the connection was made. - ''' + """ return self._address def peer_address_str(self): - '''Returns the peer's IP address and port as a human-readable - string.''' + """Returns the peer's IP address and port as a human-readable + string.""" if not self._address: return 'unknown' ip_addr_str, port = self._address[:2] @@ -245,16 +245,16 @@ class SessionBase(asyncio.Protocol): return f'{ip_addr_str}:{port}' def is_closing(self): - '''Return True if the connection is closing.''' + """Return True if the connection is closing.""" return not self.transport or self.transport.is_closing() def abort(self): - '''Forcefully close the connection.''' + """Forcefully close the connection.""" if self.transport: self.transport.abort() async def close(self, *, force_after=30): - '''Close the connection and return when closed.''' + """Close the connection and return when closed.""" self._close() if self._pm_task: with suppress(CancelledError): @@ -264,12 +264,12 @@ class SessionBase(asyncio.Protocol): class MessageSession(SessionBase): - '''Session class for protocols where messages are not tied to responses, + """Session class for protocols where messages are not tied to responses, such as the Bitcoin protocol. To use as a client (connection-opening) session, pass host, port and perhaps a proxy. - ''' + """ async def _receive_messages(self): while not self.is_closing(): try: @@ -303,7 +303,7 @@ class MessageSession(SessionBase): await self._task_group.add(self._throttled_message(message)) async def _throttled_message(self, message): - '''Process a single request, respecting the concurrency limit.''' + """Process a single request, respecting the concurrency limit.""" async with self._concurrency.semaphore: try: await self.handle_message(message) @@ -318,15 +318,15 @@ class MessageSession(SessionBase): # External API def default_framer(self): - '''Return a bitcoin framer.''' + """Return a bitcoin framer.""" return BitcoinFramer(bytes.fromhex('e3e1f3e8'), 128_000_000) async def handle_message(self, message): - '''message is a (command, payload) pair.''' + """message is a (command, payload) pair.""" pass async def send_message(self, message): - '''Send a message (command, payload) over the network.''' + """Send a message (command, payload) over the network.""" await self._send_message(message) @@ -337,7 +337,7 @@ class BatchError(Exception): class BatchRequest(object): - '''Used to build a batch request to send to the server. Stores + """Used to build a batch request to send to the server. Stores the Attributes batch and results are initially None. @@ -367,7 +367,7 @@ class BatchRequest(object): RPC error response, or violated the protocol in some way, a BatchError exception is raised. Otherwise the caller can be certain each request returned a standard result. - ''' + """ def __init__(self, session, raise_errors): self._session = session @@ -401,8 +401,8 @@ class BatchRequest(object): class RPCSession(SessionBase): - '''Base class for protocols where a message can lead to a response, - for example JSON RPC.''' + """Base class for protocols where a message can lead to a response, + for example JSON RPC.""" def __init__(self, *, framer=None, loop=None, connection=None): super().__init__(framer=framer, loop=loop) @@ -435,7 +435,7 @@ class RPCSession(SessionBase): await self._task_group.add(self._throttled_request(request)) async def _throttled_request(self, request): - '''Process a single request, respecting the concurrency limit.''' + """Process a single request, respecting the concurrency limit.""" async with self._concurrency.semaphore: try: result = await self.handle_request(request) @@ -461,18 +461,18 @@ class RPCSession(SessionBase): # External API def default_connection(self): - '''Return a default connection if the user provides none.''' + """Return a default connection if the user provides none.""" return JSONRPCConnection(JSONRPCv2) def default_framer(self): - '''Return a default framer.''' + """Return a default framer.""" return NewlineFramer() async def handle_request(self, request): pass async def send_request(self, method, args=()): - '''Send an RPC request over the network.''' + """Send an RPC request over the network.""" message, event = self.connection.send_request(Request(method, args)) await self._send_message(message) await event.wait() @@ -482,12 +482,12 @@ class RPCSession(SessionBase): return result async def send_notification(self, method, args=()): - '''Send an RPC notification over the network.''' + """Send an RPC notification over the network.""" message = self.connection.send_notification(Notification(method, args)) await self._send_message(message) def send_batch(self, raise_errors=False): - '''Return a BatchRequest. Intended to be used like so: + """Return a BatchRequest. Intended to be used like so: async with session.send_batch() as batch: batch.add_request("method1") @@ -499,12 +499,12 @@ class RPCSession(SessionBase): Note that in some circumstances exceptions can be raised; see BatchRequest doc string. - ''' + """ return BatchRequest(self, raise_errors) class Server(object): - '''A simple wrapper around an asyncio.Server object.''' + """A simple wrapper around an asyncio.Server object.""" def __init__(self, session_factory, host=None, port=None, *, loop=None, **kwargs): @@ -520,9 +520,9 @@ class Server(object): self._session_factory, self.host, self.port, **self._kwargs) async def close(self): - '''Close the listening socket. This does not close any ServerSession + """Close the listening socket. This does not close any ServerSession objects created to handle incoming connections. - ''' + """ if self.server: self.server.close() await self.server.wait_closed() diff --git a/torba/rpc/socks.py b/torba/rpc/socks.py index cc4b63f13..776f86a29 100644 --- a/torba/rpc/socks.py +++ b/torba/rpc/socks.py @@ -23,7 +23,7 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -'''SOCKS proxying.''' +"""SOCKS proxying.""" import sys import asyncio @@ -42,16 +42,16 @@ SOCKSUserAuth = collections.namedtuple("SOCKSUserAuth", "username password") class SOCKSError(Exception): - '''Base class for SOCKS exceptions. Each raised exception will be - an instance of a derived class.''' + """Base class for SOCKS exceptions. Each raised exception will be + an instance of a derived class.""" class SOCKSProtocolError(SOCKSError): - '''Raised when the proxy does not follow the SOCKS protocol''' + """Raised when the proxy does not follow the SOCKS protocol""" class SOCKSFailure(SOCKSError): - '''Raised when the proxy refuses or fails to make a connection''' + """Raised when the proxy refuses or fails to make a connection""" class NeedData(Exception): @@ -83,7 +83,7 @@ class SOCKSBase(object): class SOCKS4(SOCKSBase): - '''SOCKS4 protocol wrapper.''' + """SOCKS4 protocol wrapper.""" # See http://ftp.icm.edu.pl/packages/socks/socks4/SOCKS4.protocol REPLY_CODES = { @@ -159,7 +159,7 @@ class SOCKS4a(SOCKS4): class SOCKS5(SOCKSBase): - '''SOCKS protocol wrapper.''' + """SOCKS protocol wrapper.""" # See https://tools.ietf.org/html/rfc1928 ERROR_CODES = { @@ -269,12 +269,12 @@ class SOCKS5(SOCKSBase): class SOCKSProxy(object): def __init__(self, address, protocol, auth): - '''A SOCKS proxy at an address following a SOCKS protocol. auth is an + """A SOCKS proxy at an address following a SOCKS protocol. auth is an authentication method to use when connecting, or None. address is a (host, port) pair; for IPv6 it can instead be a (host, port, flowinfo, scopeid) 4-tuple. - ''' + """ self.address = address self.protocol = protocol self.auth = auth @@ -305,11 +305,11 @@ class SOCKSProxy(object): client.receive_data(data) async def _connect_one(self, host, port): - '''Connect to the proxy and perform a handshake requesting a + """Connect to the proxy and perform a handshake requesting a connection to (host, port). Return the open socket on success, or the exception on failure. - ''' + """ client = self.protocol(host, port, self.auth) sock = socket.socket() loop = asyncio.get_event_loop() @@ -327,11 +327,11 @@ class SOCKSProxy(object): return e async def _connect(self, addresses): - '''Connect to the proxy and perform a handshake requesting a + """Connect to the proxy and perform a handshake requesting a connection to each address in addresses. Return an (open_socket, address) pair on success. - ''' + """ assert len(addresses) > 0 exceptions = [] @@ -347,9 +347,9 @@ class SOCKSProxy(object): OSError(f'multiple exceptions: {", ".join(strings)}')) async def _detect_proxy(self): - '''Return True if it appears we can connect to a SOCKS proxy, + """Return True if it appears we can connect to a SOCKS proxy, otherwise False. - ''' + """ if self.protocol is SOCKS4a: host, port = 'www.apple.com', 80 else: @@ -366,7 +366,7 @@ class SOCKSProxy(object): @classmethod async def auto_detect_address(cls, address, auth): - '''Try to detect a SOCKS proxy at address using the authentication + """Try to detect a SOCKS proxy at address using the authentication method (or None). SOCKS5, SOCKS4a and SOCKS are tried in order. If a SOCKS proxy is detected a SOCKSProxy object is returned. @@ -375,7 +375,7 @@ class SOCKSProxy(object): example, it may have no network connectivity. If no proxy is detected return None. - ''' + """ for protocol in (SOCKS5, SOCKS4a, SOCKS4): proxy = cls(address, protocol, auth) if await proxy._detect_proxy(): @@ -384,7 +384,7 @@ class SOCKSProxy(object): @classmethod async def auto_detect_host(cls, host, ports, auth): - '''Try to detect a SOCKS proxy on a host on one of the ports. + """Try to detect a SOCKS proxy on a host on one of the ports. Calls auto_detect for the ports in order. Returns SOCKS are tried in order; a SOCKSProxy object for the first detected @@ -394,7 +394,7 @@ class SOCKSProxy(object): example, it may have no network connectivity. If no proxy is detected return None. - ''' + """ for port in ports: address = (host, port) proxy = await cls.auto_detect_address(address, auth) @@ -406,7 +406,7 @@ class SOCKSProxy(object): async def create_connection(self, protocol_factory, host, port, *, resolve=False, ssl=None, family=0, proto=0, flags=0): - '''Set up a connection to (host, port) through the proxy. + """Set up a connection to (host, port) through the proxy. If resolve is True then host is resolved locally with getaddrinfo using family, proto and flags, otherwise the proxy @@ -417,7 +417,7 @@ class SOCKSProxy(object): protocol to the address of the successful remote connection. Additionally raises SOCKSError if something goes wrong with the proxy handshake. - ''' + """ loop = asyncio.get_event_loop() if resolve: infos = await loop.getaddrinfo(host, port, family=family, diff --git a/torba/server/block_processor.py b/torba/server/block_processor.py index 93925b1ed..b575d9123 100644 --- a/torba/server/block_processor.py +++ b/torba/server/block_processor.py @@ -6,7 +6,7 @@ # See the file "LICENCE" for information about the copyright # and warranty status of this software. -'''Block prefetcher and chain processor.''' +"""Block prefetcher and chain processor.""" import asyncio @@ -21,7 +21,7 @@ from torba.server.db import FlushData class Prefetcher: - '''Prefetches blocks (in the forward direction only).''' + """Prefetches blocks (in the forward direction only).""" def __init__(self, daemon, coin, blocks_event): self.logger = class_logger(__name__, self.__class__.__name__) @@ -43,7 +43,7 @@ class Prefetcher: self.polling_delay = 5 async def main_loop(self, bp_height): - '''Loop forever polling for more blocks.''' + """Loop forever polling for more blocks.""" await self.reset_height(bp_height) while True: try: @@ -55,7 +55,7 @@ class Prefetcher: self.logger.info(f'ignoring daemon error: {e}') def get_prefetched_blocks(self): - '''Called by block processor when it is processing queued blocks.''' + """Called by block processor when it is processing queued blocks.""" blocks = self.blocks self.blocks = [] self.cache_size = 0 @@ -63,12 +63,12 @@ class Prefetcher: return blocks async def reset_height(self, height): - '''Reset to prefetch blocks from the block processor's height. + """Reset to prefetch blocks from the block processor's height. Used in blockchain reorganisations. This coroutine can be called asynchronously to the _prefetch_blocks coroutine so we must synchronize with a semaphore. - ''' + """ async with self.semaphore: self.blocks.clear() self.cache_size = 0 @@ -86,10 +86,10 @@ class Prefetcher: .format(daemon_height)) async def _prefetch_blocks(self): - '''Prefetch some blocks and put them on the queue. + """Prefetch some blocks and put them on the queue. Repeats until the queue is full or caught up. - ''' + """ daemon = self.daemon daemon_height = await daemon.height() async with self.semaphore: @@ -136,15 +136,15 @@ class Prefetcher: class ChainError(Exception): - '''Raised on error processing blocks.''' + """Raised on error processing blocks.""" class BlockProcessor: - '''Process blocks and update the DB state to match. + """Process blocks and update the DB state to match. Employ a prefetcher to prefetch blocks in batches for processing. Coordinate backing up in case of chain reorganisations. - ''' + """ def __init__(self, env, db, daemon, notifications): self.env = env @@ -187,9 +187,9 @@ class BlockProcessor: return await asyncio.shield(run_in_thread_locked()) async def check_and_advance_blocks(self, raw_blocks): - '''Process the list of raw blocks passed. Detects and handles + """Process the list of raw blocks passed. Detects and handles reorgs. - ''' + """ if not raw_blocks: return first = self.height + 1 @@ -224,10 +224,10 @@ class BlockProcessor: await self.prefetcher.reset_height(self.height) async def reorg_chain(self, count=None): - '''Handle a chain reorganisation. + """Handle a chain reorganisation. Count is the number of blocks to simulate a reorg, or None for - a real reorg.''' + a real reorg.""" if count is None: self.logger.info('chain reorg detected') else: @@ -260,12 +260,12 @@ class BlockProcessor: await self.prefetcher.reset_height(self.height) async def reorg_hashes(self, count): - '''Return a pair (start, last, hashes) of blocks to back up during a + """Return a pair (start, last, hashes) of blocks to back up during a reorg. The hashes are returned in order of increasing height. Start is the height of the first hash, last of the last. - ''' + """ start, count = await self.calc_reorg_range(count) last = start + count - 1 s = '' if count == 1 else 's' @@ -275,11 +275,11 @@ class BlockProcessor: return start, last, await self.db.fs_block_hashes(start, count) async def calc_reorg_range(self, count): - '''Calculate the reorg range''' + """Calculate the reorg range""" def diff_pos(hashes1, hashes2): - '''Returns the index of the first difference in the hash lists. - If both lists match returns their length.''' + """Returns the index of the first difference in the hash lists. + If both lists match returns their length.""" for n, (hash1, hash2) in enumerate(zip(hashes1, hashes2)): if hash1 != hash2: return n @@ -318,7 +318,7 @@ class BlockProcessor: # - Flushing def flush_data(self): - '''The data for a flush. The lock must be taken.''' + """The data for a flush. The lock must be taken.""" assert self.state_lock.locked() return FlushData(self.height, self.tx_count, self.headers, self.tx_hashes, self.undo_infos, self.utxo_cache, @@ -342,7 +342,7 @@ class BlockProcessor: self.next_cache_check = time.time() + 30 def check_cache_size(self): - '''Flush a cache if it gets too big.''' + """Flush a cache if it gets too big.""" # Good average estimates based on traversal of subobjects and # requesting size from Python (see deep_getsizeof). one_MB = 1000*1000 @@ -368,10 +368,10 @@ class BlockProcessor: return None def advance_blocks(self, blocks): - '''Synchronously advance the blocks. + """Synchronously advance the blocks. It is already verified they correctly connect onto our tip. - ''' + """ min_height = self.db.min_undo_height(self.daemon.cached_height()) height = self.height @@ -436,11 +436,11 @@ class BlockProcessor: return undo_info def backup_blocks(self, raw_blocks): - '''Backup the raw blocks and flush. + """Backup the raw blocks and flush. The blocks should be in order of decreasing height, starting at. self.height. A flush is performed once the blocks are backed up. - ''' + """ self.db.assert_flushed(self.flush_data()) assert self.height >= len(raw_blocks) @@ -500,7 +500,7 @@ class BlockProcessor: assert n == 0 self.tx_count -= len(txs) - '''An in-memory UTXO cache, representing all changes to UTXO state + """An in-memory UTXO cache, representing all changes to UTXO state since the last DB flush. We want to store millions of these in memory for optimal @@ -552,15 +552,15 @@ class BlockProcessor: looking up a UTXO the prefix space of the compressed hash needs to be searched and resolved if necessary with the tx_num. The collision rate is low (<0.1%). - ''' + """ def spend_utxo(self, tx_hash, tx_idx): - '''Spend a UTXO and return the 33-byte value. + """Spend a UTXO and return the 33-byte value. If the UTXO is not in the cache it must be on disk. We store all UTXOs so not finding one indicates a logic error or DB corruption. - ''' + """ # Fast track is it being in the cache idx_packed = pack('= cls.FORK_HEIGHT: return double_sha256(header) @@ -511,7 +511,7 @@ class Emercoin(Coin): @classmethod def block_header(cls, block, height): - '''Returns the block header given a block and its height.''' + """Returns the block header given a block and its height.""" deserializer = cls.DESERIALIZER(block) if deserializer.is_merged_block(): @@ -520,7 +520,7 @@ class Emercoin(Coin): @classmethod def header_hash(cls, header): - '''Given a header return hash''' + """Given a header return hash""" return double_sha256(header[:cls.BASIC_HEADER_SIZE]) @@ -543,7 +543,7 @@ class BitcoinTestnetMixin: class BitcoinCashTestnet(BitcoinTestnetMixin, Coin): - '''Bitcoin Testnet for Bitcoin Cash daemons.''' + """Bitcoin Testnet for Bitcoin Cash daemons.""" NAME = "BitcoinCash" PEERS = [ 'electrum-testnet-abc.criptolayer.net s50112', @@ -563,7 +563,7 @@ class BitcoinCashRegtest(BitcoinCashTestnet): class BitcoinSegwitTestnet(BitcoinTestnetMixin, Coin): - '''Bitcoin Testnet for Core bitcoind >= 0.13.1.''' + """Bitcoin Testnet for Core bitcoind >= 0.13.1.""" NAME = "BitcoinSegwit" DESERIALIZER = lib_tx.DeserializerSegWit PEERS = [ @@ -588,7 +588,7 @@ class BitcoinSegwitRegtest(BitcoinSegwitTestnet): class BitcoinNolnet(BitcoinCash): - '''Bitcoin Unlimited nolimit testnet.''' + """Bitcoin Unlimited nolimit testnet.""" NET = "nolnet" GENESIS_HASH = ('0000000057e31bd2066c939a63b7b862' '3bd0f10d8c001304bdfc1a7902ae6d35') @@ -878,7 +878,7 @@ class Motion(Coin): @classmethod def header_hash(cls, header): - '''Given a header return the hash.''' + """Given a header return the hash.""" import x16r_hash return x16r_hash.getPoWHash(header) @@ -912,7 +912,7 @@ class Dash(Coin): @classmethod def header_hash(cls, header): - '''Given a header return the hash.''' + """Given a header return the hash.""" import x11_hash return x11_hash.getPoWHash(header) @@ -1014,7 +1014,7 @@ class FairCoin(Coin): @classmethod def block(cls, raw_block, height): - '''Return a Block namedtuple given a raw block and its height.''' + """Return a Block namedtuple given a raw block and its height.""" if height > 0: return super().block(raw_block, height) else: @@ -1465,7 +1465,7 @@ class Bitzeny(Coin): @classmethod def header_hash(cls, header): - '''Given a header return the hash.''' + """Given a header return the hash.""" import zny_yescrypt return zny_yescrypt.getPoWHash(header) @@ -1513,7 +1513,7 @@ class Denarius(Coin): @classmethod def header_hash(cls, header): - '''Given a header return the hash.''' + """Given a header return the hash.""" import tribus_hash return tribus_hash.getPoWHash(header) @@ -1552,11 +1552,11 @@ class Sibcoin(Dash): @classmethod def header_hash(cls, header): - ''' + """ Given a header return the hash for sibcoin. Need to download `x11_gost_hash` module Source code: https://github.com/ivansib/x11_gost_hash - ''' + """ import x11_gost_hash return x11_gost_hash.getPoWHash(header) @@ -1724,7 +1724,7 @@ class BitcoinAtom(Coin): @classmethod def header_hash(cls, header): - '''Given a header return hash''' + """Given a header return hash""" header_to_be_hashed = header[:cls.BASIC_HEADER_SIZE] # New block header format has some extra flags in the end if len(header) == cls.HEADER_SIZE_POST_FORK: @@ -1737,7 +1737,7 @@ class BitcoinAtom(Coin): @classmethod def block_header(cls, block, height): - '''Return the block header bytes''' + """Return the block header bytes""" deserializer = cls.DESERIALIZER(block) return deserializer.read_header(height, cls.BASIC_HEADER_SIZE) @@ -1777,12 +1777,12 @@ class Decred(Coin): @classmethod def header_hash(cls, header): - '''Given a header return the hash.''' + """Given a header return the hash.""" return cls.HEADER_HASH(header) @classmethod def block(cls, raw_block, height): - '''Return a Block namedtuple given a raw block and its height.''' + """Return a Block namedtuple given a raw block and its height.""" if height > 0: return super().block(raw_block, height) else: @@ -1837,11 +1837,11 @@ class Axe(Dash): @classmethod def header_hash(cls, header): - ''' + """ Given a header return the hash for AXE. Need to download `axe_hash` module Source code: https://github.com/AXErunners/axe_hash - ''' + """ import x11_hash return x11_hash.getPoWHash(header) @@ -1867,11 +1867,11 @@ class Xuez(Coin): @classmethod def header_hash(cls, header): - ''' + """ Given a header return the hash for Xuez. Need to download `xevan_hash` module Source code: https://github.com/xuez/xuez - ''' + """ version, = util.unpack_le_uint32_from(header) import xevan_hash @@ -1915,7 +1915,7 @@ class Pac(Coin): @classmethod def header_hash(cls, header): - '''Given a header return the hash.''' + """Given a header return the hash.""" import x11_hash return x11_hash.getPoWHash(header) @@ -1960,7 +1960,7 @@ class Polis(Coin): @classmethod def header_hash(cls, header): - '''Given a header return the hash.''' + """Given a header return the hash.""" import x11_hash return x11_hash.getPoWHash(header) @@ -1989,7 +1989,7 @@ class ColossusXT(Coin): @classmethod def header_hash(cls, header): - '''Given a header return the hash.''' + """Given a header return the hash.""" import quark_hash return quark_hash.getPoWHash(header) @@ -2018,7 +2018,7 @@ class GoByte(Coin): @classmethod def header_hash(cls, header): - '''Given a header return the hash.''' + """Given a header return the hash.""" import neoscrypt return neoscrypt.getPoWHash(header) @@ -2047,7 +2047,7 @@ class Monoeci(Coin): @classmethod def header_hash(cls, header): - '''Given a header return the hash.''' + """Given a header return the hash.""" import x11_hash return x11_hash.getPoWHash(header) @@ -2082,7 +2082,7 @@ class Minexcoin(EquihashMixin, Coin): @classmethod def block_header(cls, block, height): - '''Return the block header bytes''' + """Return the block header bytes""" deserializer = cls.DESERIALIZER(block) return deserializer.read_header(height, cls.HEADER_SIZE_NO_SOLUTION) @@ -2116,7 +2116,7 @@ class Groestlcoin(Coin): @classmethod def header_hash(cls, header): - '''Given a header return the hash.''' + """Given a header return the hash.""" return cls.grshash(header) ENCODE_CHECK = partial(Base58.encode_check, hash_fn=grshash) @@ -2224,7 +2224,7 @@ class Bitg(Coin): @classmethod def header_hash(cls, header): - '''Given a header return the hash.''' + """Given a header return the hash.""" import quark_hash return quark_hash.getPoWHash(header) diff --git a/torba/server/daemon.py b/torba/server/daemon.py index dba43abd8..5a89bb1f8 100644 --- a/torba/server/daemon.py +++ b/torba/server/daemon.py @@ -5,8 +5,8 @@ # See the file "LICENCE" for information about the copyright # and warranty status of this software. -'''Class for handling asynchronous connections to a blockchain -daemon.''' +"""Class for handling asynchronous connections to a blockchain +daemon.""" import asyncio import itertools @@ -26,19 +26,19 @@ from torba.rpc import JSONRPC class DaemonError(Exception): - '''Raised when the daemon returns an error in its results.''' + """Raised when the daemon returns an error in its results.""" class WarmingUpError(Exception): - '''Internal - when the daemon is warming up.''' + """Internal - when the daemon is warming up.""" class WorkQueueFullError(Exception): - '''Internal - when the daemon's work queue is full.''' + """Internal - when the daemon's work queue is full.""" class Daemon: - '''Handles connections to a daemon at the given URL.''' + """Handles connections to a daemon at the given URL.""" WARMING_UP = -28 id_counter = itertools.count() @@ -57,7 +57,7 @@ class Daemon: self.available_rpcs = {} def set_url(self, url): - '''Set the URLS to the given list, and switch to the first one.''' + """Set the URLS to the given list, and switch to the first one.""" urls = url.split(',') urls = [self.coin.sanitize_url(url) for url in urls] for n, url in enumerate(urls): @@ -68,19 +68,19 @@ class Daemon: self.urls = urls def current_url(self): - '''Returns the current daemon URL.''' + """Returns the current daemon URL.""" return self.urls[self.url_index] def logged_url(self, url=None): - '''The host and port part, for logging.''' + """The host and port part, for logging.""" url = url or self.current_url() return url[url.rindex('@') + 1:] def failover(self): - '''Call to fail-over to the next daemon URL. + """Call to fail-over to the next daemon URL. Returns False if there is only one, otherwise True. - ''' + """ if len(self.urls) > 1: self.url_index = (self.url_index + 1) % len(self.urls) self.logger.info(f'failing over to {self.logged_url()}') @@ -88,7 +88,7 @@ class Daemon: return False def client_session(self): - '''An aiohttp client session.''' + """An aiohttp client session.""" return aiohttp.ClientSession() async def _send_data(self, data): @@ -107,11 +107,11 @@ class Daemon: raise DaemonError(text) async def _send(self, payload, processor): - '''Send a payload to be converted to JSON. + """Send a payload to be converted to JSON. Handles temporary connection issues. Daemon reponse errors are raise through DaemonError. - ''' + """ def log_error(error): nonlocal last_error_log, retry now = time.time() @@ -154,7 +154,7 @@ class Daemon: retry = max(min(self.max_retry, retry * 2), self.init_retry) async def _send_single(self, method, params=None): - '''Send a single request to the daemon.''' + """Send a single request to the daemon.""" def processor(result): err = result['error'] if not err: @@ -169,11 +169,11 @@ class Daemon: return await self._send(payload, processor) async def _send_vector(self, method, params_iterable, replace_errs=False): - '''Send several requests of the same method. + """Send several requests of the same method. The result will be an array of the same length as params_iterable. If replace_errs is true, any item with an error is returned as None, - otherwise an exception is raised.''' + otherwise an exception is raised.""" def processor(result): errs = [item['error'] for item in result if item['error']] if any(err.get('code') == self.WARMING_UP for err in errs): @@ -189,10 +189,10 @@ class Daemon: return [] async def _is_rpc_available(self, method): - '''Return whether given RPC method is available in the daemon. + """Return whether given RPC method is available in the daemon. Results are cached and the daemon will generally not be queried with - the same method more than once.''' + the same method more than once.""" available = self.available_rpcs.get(method) if available is None: available = True @@ -206,30 +206,30 @@ class Daemon: return available async def block_hex_hashes(self, first, count): - '''Return the hex hashes of count block starting at height first.''' + """Return the hex hashes of count block starting at height first.""" params_iterable = ((h, ) for h in range(first, first + count)) return await self._send_vector('getblockhash', params_iterable) async def deserialised_block(self, hex_hash): - '''Return the deserialised block with the given hex hash.''' + """Return the deserialised block with the given hex hash.""" return await self._send_single('getblock', (hex_hash, True)) async def raw_blocks(self, hex_hashes): - '''Return the raw binary blocks with the given hex hashes.''' + """Return the raw binary blocks with the given hex hashes.""" params_iterable = ((h, False) for h in hex_hashes) blocks = await self._send_vector('getblock', params_iterable) # Convert hex string to bytes return [hex_to_bytes(block) for block in blocks] async def mempool_hashes(self): - '''Update our record of the daemon's mempool hashes.''' + """Update our record of the daemon's mempool hashes.""" return await self._send_single('getrawmempool') async def estimatefee(self, block_count): - '''Return the fee estimate for the block count. Units are whole + """Return the fee estimate for the block count. Units are whole currency units per KB, e.g. 0.00000995, or -1 if no estimate is available. - ''' + """ args = (block_count, ) if await self._is_rpc_available('estimatesmartfee'): estimate = await self._send_single('estimatesmartfee', args) @@ -237,25 +237,25 @@ class Daemon: return await self._send_single('estimatefee', args) async def getnetworkinfo(self): - '''Return the result of the 'getnetworkinfo' RPC call.''' + """Return the result of the 'getnetworkinfo' RPC call.""" return await self._send_single('getnetworkinfo') async def relayfee(self): - '''The minimum fee a low-priority tx must pay in order to be accepted - to the daemon's memory pool.''' + """The minimum fee a low-priority tx must pay in order to be accepted + to the daemon's memory pool.""" network_info = await self.getnetworkinfo() return network_info['relayfee'] async def getrawtransaction(self, hex_hash, verbose=False): - '''Return the serialized raw transaction with the given hash.''' + """Return the serialized raw transaction with the given hash.""" # Cast to int because some coin daemons are old and require it return await self._send_single('getrawtransaction', (hex_hash, int(verbose))) async def getrawtransactions(self, hex_hashes, replace_errs=True): - '''Return the serialized raw transactions with the given hashes. + """Return the serialized raw transactions with the given hashes. - Replaces errors with None by default.''' + Replaces errors with None by default.""" params_iterable = ((hex_hash, 0) for hex_hash in hex_hashes) txs = await self._send_vector('getrawtransaction', params_iterable, replace_errs=replace_errs) @@ -263,57 +263,57 @@ class Daemon: return [hex_to_bytes(tx) if tx else None for tx in txs] async def broadcast_transaction(self, raw_tx): - '''Broadcast a transaction to the network.''' + """Broadcast a transaction to the network.""" return await self._send_single('sendrawtransaction', (raw_tx, )) async def height(self): - '''Query the daemon for its current height.''' + """Query the daemon for its current height.""" self._height = await self._send_single('getblockcount') return self._height def cached_height(self): - '''Return the cached daemon height. + """Return the cached daemon height. - If the daemon has not been queried yet this returns None.''' + If the daemon has not been queried yet this returns None.""" return self._height class DashDaemon(Daemon): async def masternode_broadcast(self, params): - '''Broadcast a transaction to the network.''' + """Broadcast a transaction to the network.""" return await self._send_single('masternodebroadcast', params) async def masternode_list(self, params): - '''Return the masternode status.''' + """Return the masternode status.""" return await self._send_single('masternodelist', params) class FakeEstimateFeeDaemon(Daemon): - '''Daemon that simulates estimatefee and relayfee RPC calls. Coin that - wants to use this daemon must define ESTIMATE_FEE & RELAY_FEE''' + """Daemon that simulates estimatefee and relayfee RPC calls. Coin that + wants to use this daemon must define ESTIMATE_FEE & RELAY_FEE""" async def estimatefee(self, block_count): - '''Return the fee estimate for the given parameters.''' + """Return the fee estimate for the given parameters.""" return self.coin.ESTIMATE_FEE async def relayfee(self): - '''The minimum fee a low-priority tx must pay in order to be accepted - to the daemon's memory pool.''' + """The minimum fee a low-priority tx must pay in order to be accepted + to the daemon's memory pool.""" return self.coin.RELAY_FEE class LegacyRPCDaemon(Daemon): - '''Handles connections to a daemon at the given URL. + """Handles connections to a daemon at the given URL. This class is useful for daemons that don't have the new 'getblock' RPC call that returns the block in hex, the workaround is to manually recreate the block bytes. The recreated block bytes may not be the exact as in the underlying blockchain but it is good enough for our indexing - purposes.''' + purposes.""" async def raw_blocks(self, hex_hashes): - '''Return the raw binary blocks with the given hex hashes.''' + """Return the raw binary blocks with the given hex hashes.""" params_iterable = ((h, ) for h in hex_hashes) block_info = await self._send_vector('getblock', params_iterable) @@ -339,7 +339,7 @@ class LegacyRPCDaemon(Daemon): ]) async def make_raw_block(self, b): - '''Construct a raw block''' + """Construct a raw block""" header = await self.make_raw_header(b) @@ -365,7 +365,7 @@ class LegacyRPCDaemon(Daemon): class DecredDaemon(Daemon): async def raw_blocks(self, hex_hashes): - '''Return the raw binary blocks with the given hex hashes.''' + """Return the raw binary blocks with the given hex hashes.""" params_iterable = ((h, False) for h in hex_hashes) blocks = await self._send_vector('getblock', params_iterable) @@ -448,12 +448,12 @@ class DecredDaemon(Daemon): class PreLegacyRPCDaemon(LegacyRPCDaemon): - '''Handles connections to a daemon at the given URL. + """Handles connections to a daemon at the given URL. This class is useful for daemons that don't have the new 'getblock' RPC call that returns the block in hex, and need the False parameter - for the getblock''' + for the getblock""" async def deserialised_block(self, hex_hash): - '''Return the deserialised block with the given hex hash.''' + """Return the deserialised block with the given hex hash.""" return await self._send_single('getblock', (hex_hash, False)) diff --git a/torba/server/db.py b/torba/server/db.py index 16e165bc9..8fff9dadf 100644 --- a/torba/server/db.py +++ b/torba/server/db.py @@ -6,7 +6,7 @@ # See the file "LICENCE" for information about the copyright # and warranty status of this software. -'''Interface to the blockchain database.''' +"""Interface to the blockchain database.""" import asyncio @@ -47,16 +47,16 @@ class FlushData: class DB: - '''Simple wrapper of the backend database for querying. + """Simple wrapper of the backend database for querying. Performs no DB update, though the DB will be cleaned on opening if it was shutdown uncleanly. - ''' + """ DB_VERSIONS = [6] class DBError(Exception): - '''Raised on general DB errors generally indicating corruption.''' + """Raised on general DB errors generally indicating corruption.""" def __init__(self, env): self.logger = util.class_logger(__name__, self.__class__.__name__) @@ -142,18 +142,18 @@ class DB: await self._open_dbs(True, True) async def open_for_sync(self): - '''Open the databases to sync to the daemon. + """Open the databases to sync to the daemon. When syncing we want to reserve a lot of open files for the synchronization. When serving clients we want the open files for serving network connections. - ''' + """ await self._open_dbs(True, False) async def open_for_serving(self): - '''Open the databases for serving. If they are already open they are + """Open the databases for serving. If they are already open they are closed first. - ''' + """ if self.utxo_db: self.logger.info('closing DBs to re-open for serving') self.utxo_db.close() @@ -176,7 +176,7 @@ class DB: # Flushing def assert_flushed(self, flush_data): - '''Asserts state is fully flushed.''' + """Asserts state is fully flushed.""" assert flush_data.tx_count == self.fs_tx_count == self.db_tx_count assert flush_data.height == self.fs_height == self.db_height assert flush_data.tip == self.db_tip @@ -188,8 +188,8 @@ class DB: self.history.assert_flushed() def flush_dbs(self, flush_data, flush_utxos, estimate_txs_remaining): - '''Flush out cached state. History is always flushed; UTXOs are - flushed if flush_utxos.''' + """Flush out cached state. History is always flushed; UTXOs are + flushed if flush_utxos.""" if flush_data.height == self.db_height: self.assert_flushed(flush_data) return @@ -231,12 +231,12 @@ class DB: f'ETA: {formatted_time(eta)}') def flush_fs(self, flush_data): - '''Write headers, tx counts and block tx hashes to the filesystem. + """Write headers, tx counts and block tx hashes to the filesystem. The first height to write is self.fs_height + 1. The FS metadata is all append-only, so in a crash we just pick up again from the height stored in the DB. - ''' + """ prior_tx_count = (self.tx_counts[self.fs_height] if self.fs_height >= 0 else 0) assert len(flush_data.block_tx_hashes) == len(flush_data.headers) @@ -274,7 +274,7 @@ class DB: self.history.flush() def flush_utxo_db(self, batch, flush_data): - '''Flush the cached DB writes and UTXO set to the batch.''' + """Flush the cached DB writes and UTXO set to the batch.""" # Care is needed because the writes generated by flushing the # UTXO state may have keys in common with our write cache or # may be in the DB already. @@ -317,7 +317,7 @@ class DB: self.db_tip = flush_data.tip def flush_state(self, batch): - '''Flush chain state to the batch.''' + """Flush chain state to the batch.""" now = time.time() self.wall_time += now - self.last_flush self.last_flush = now @@ -325,7 +325,7 @@ class DB: self.write_utxo_state(batch) def flush_backup(self, flush_data, touched): - '''Like flush_dbs() but when backing up. All UTXOs are flushed.''' + """Like flush_dbs() but when backing up. All UTXOs are flushed.""" assert not flush_data.headers assert not flush_data.block_tx_hashes assert flush_data.height < self.db_height @@ -369,28 +369,28 @@ class DB: - self.dynamic_header_offset(height) def backup_fs(self, height, tx_count): - '''Back up during a reorg. This just updates our pointers.''' + """Back up during a reorg. This just updates our pointers.""" self.fs_height = height self.fs_tx_count = tx_count # Truncate header_mc: header count is 1 more than the height. self.header_mc.truncate(height + 1) async def raw_header(self, height): - '''Return the binary header at the given height.''' + """Return the binary header at the given height.""" header, n = await self.read_headers(height, 1) if n != 1: raise IndexError(f'height {height:,d} out of range') return header async def read_headers(self, start_height, count): - '''Requires start_height >= 0, count >= 0. Reads as many headers as + """Requires start_height >= 0, count >= 0. Reads as many headers as are available starting at start_height up to count. This would be zero if start_height is beyond self.db_height, for example. Returns a (binary, n) pair where binary is the concatenated binary headers, and n is the count of headers returned. - ''' + """ if start_height < 0 or count < 0: raise self.DBError(f'{count:,d} headers starting at ' f'{start_height:,d} not on disk') @@ -407,9 +407,9 @@ class DB: return await asyncio.get_event_loop().run_in_executor(None, read_headers) def fs_tx_hash(self, tx_num): - '''Return a par (tx_hash, tx_height) for the given tx number. + """Return a par (tx_hash, tx_height) for the given tx number. - If the tx_height is not on disk, returns (None, tx_height).''' + If the tx_height is not on disk, returns (None, tx_height).""" tx_height = bisect_right(self.tx_counts, tx_num) if tx_height > self.db_height: tx_hash = None @@ -432,12 +432,12 @@ class DB: return [self.coin.header_hash(header) for header in headers] async def limited_history(self, hashX, *, limit=1000): - '''Return an unpruned, sorted list of (tx_hash, height) tuples of + """Return an unpruned, sorted list of (tx_hash, height) tuples of confirmed transactions that touched the address, earliest in the blockchain first. Includes both spending and receiving transactions. By default returns at most 1000 entries. Set limit to None to get them all. - ''' + """ def read_history(): tx_nums = list(self.history.get_txnums(hashX, limit)) fs_tx_hash = self.fs_tx_hash @@ -454,19 +454,19 @@ class DB: # -- Undo information def min_undo_height(self, max_height): - '''Returns a height from which we should store undo info.''' + """Returns a height from which we should store undo info.""" return max_height - self.env.reorg_limit + 1 def undo_key(self, height): - '''DB key for undo information at the given height.''' + """DB key for undo information at the given height.""" return b'U' + pack('>I', height) def read_undo_info(self, height): - '''Read undo information from a file for the current height.''' + """Read undo information from a file for the current height.""" return self.utxo_db.get(self.undo_key(height)) def flush_undo_infos(self, batch_put, undo_infos): - '''undo_infos is a list of (undo_info, height) pairs.''' + """undo_infos is a list of (undo_info, height) pairs.""" for undo_info, height in undo_infos: batch_put(self.undo_key(height), b''.join(undo_info)) @@ -477,13 +477,13 @@ class DB: return f'{self.raw_block_prefix()}{height:d}' def read_raw_block(self, height): - '''Returns a raw block read from disk. Raises FileNotFoundError - if the block isn't on-disk.''' + """Returns a raw block read from disk. Raises FileNotFoundError + if the block isn't on-disk.""" with util.open_file(self.raw_block_path(height)) as f: return f.read(-1) def write_raw_block(self, block, height): - '''Write a raw block to disk.''' + """Write a raw block to disk.""" with util.open_truncate(self.raw_block_path(height)) as f: f.write(block) # Delete old blocks to prevent them accumulating @@ -494,7 +494,7 @@ class DB: pass def clear_excess_undo_info(self): - '''Clear excess undo info. Only most recent N are kept.''' + """Clear excess undo info. Only most recent N are kept.""" prefix = b'U' min_height = self.min_undo_height(self.db_height) keys = [] @@ -578,7 +578,7 @@ class DB: .format(util.formatted_time(self.wall_time))) def write_utxo_state(self, batch): - '''Write (UTXO) state to the batch.''' + """Write (UTXO) state to the batch.""" state = { 'genesis': self.coin.GENESIS_HASH, 'height': self.db_height, @@ -597,7 +597,7 @@ class DB: self.write_utxo_state(batch) async def all_utxos(self, hashX): - '''Return all UTXOs for an address sorted in no particular order.''' + """Return all UTXOs for an address sorted in no particular order.""" def read_utxos(): utxos = [] utxos_append = utxos.append @@ -621,15 +621,15 @@ class DB: await sleep(0.25) async def lookup_utxos(self, prevouts): - '''For each prevout, lookup it up in the DB and return a (hashX, + """For each prevout, lookup it up in the DB and return a (hashX, value) pair or None if not found. Used by the mempool code. - ''' + """ def lookup_hashXs(): - '''Return (hashX, suffix) pairs, or None if not found, + """Return (hashX, suffix) pairs, or None if not found, for each prevout. - ''' + """ def lookup_hashX(tx_hash, tx_idx): idx_packed = pack(' MemPoolTx hashXs: hashX -> set of all hashes of txs touching the hashX - ''' + """ def __init__(self, coin, api, refresh_secs=5.0, log_status_secs=120.0): assert isinstance(api, MemPoolAPI) @@ -107,7 +107,7 @@ class MemPool: self.lock = Lock() async def _logging(self, synchronized_event): - '''Print regular logs of mempool stats.''' + """Print regular logs of mempool stats.""" self.logger.info('beginning processing of daemon mempool. ' 'This can take some time...') start = time.time() @@ -156,12 +156,12 @@ class MemPool: self.cached_compact_histogram = compact def _accept_transactions(self, tx_map, utxo_map, touched): - '''Accept transactions in tx_map to the mempool if all their inputs + """Accept transactions in tx_map to the mempool if all their inputs can be found in the existing mempool or a utxo_map from the DB. Returns an (unprocessed tx_map, unspent utxo_map) pair. - ''' + """ hashXs = self.hashXs txs = self.txs @@ -200,7 +200,7 @@ class MemPool: return deferred, {prevout: utxo_map[prevout] for prevout in unspent} async def _refresh_hashes(self, synchronized_event): - '''Refresh our view of the daemon's mempool.''' + """Refresh our view of the daemon's mempool.""" while True: height = self.api.cached_height() hex_hashes = await self.api.mempool_hashes() @@ -256,7 +256,7 @@ class MemPool: return touched async def _fetch_and_accept(self, hashes, all_hashes, touched): - '''Fetch a list of mempool transactions.''' + """Fetch a list of mempool transactions.""" hex_hashes_iter = (hash_to_hex_str(hash) for hash in hashes) raw_txs = await self.api.raw_transactions(hex_hashes_iter) @@ -303,7 +303,7 @@ class MemPool: # async def keep_synchronized(self, synchronized_event): - '''Keep the mempool synchronized with the daemon.''' + """Keep the mempool synchronized with the daemon.""" await asyncio.wait([ self._refresh_hashes(synchronized_event), self._refresh_histogram(synchronized_event), @@ -311,10 +311,10 @@ class MemPool: ]) async def balance_delta(self, hashX): - '''Return the unconfirmed amount in the mempool for hashX. + """Return the unconfirmed amount in the mempool for hashX. Can be positive or negative. - ''' + """ value = 0 if hashX in self.hashXs: for hash in self.hashXs[hashX]: @@ -324,16 +324,16 @@ class MemPool: return value async def compact_fee_histogram(self): - '''Return a compact fee histogram of the current mempool.''' + """Return a compact fee histogram of the current mempool.""" return self.cached_compact_histogram async def potential_spends(self, hashX): - '''Return a set of (prev_hash, prev_idx) pairs from mempool + """Return a set of (prev_hash, prev_idx) pairs from mempool transactions that touch hashX. None, some or all of these may be spends of the hashX, but all actual spends of it (in the DB or mempool) will be included. - ''' + """ result = set() for tx_hash in self.hashXs.get(hashX, ()): tx = self.txs[tx_hash] @@ -341,7 +341,7 @@ class MemPool: return result async def transaction_summaries(self, hashX): - '''Return a list of MemPoolTxSummary objects for the hashX.''' + """Return a list of MemPoolTxSummary objects for the hashX.""" result = [] for tx_hash in self.hashXs.get(hashX, ()): tx = self.txs[tx_hash] @@ -350,12 +350,12 @@ class MemPool: return result async def unordered_UTXOs(self, hashX): - '''Return an unordered list of UTXO named tuples from mempool + """Return an unordered list of UTXO named tuples from mempool transactions that pay to hashX. This does not consider if any other mempool transactions spend the outputs. - ''' + """ utxos = [] for tx_hash in self.hashXs.get(hashX, ()): tx = self.txs.get(tx_hash) diff --git a/torba/server/merkle.py b/torba/server/merkle.py index e8e54a06c..8cfa89c68 100644 --- a/torba/server/merkle.py +++ b/torba/server/merkle.py @@ -24,7 +24,7 @@ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # and warranty status of this software. -'''Merkle trees, branches, proofs and roots.''' +"""Merkle trees, branches, proofs and roots.""" from asyncio import Event from math import ceil, log @@ -33,12 +33,12 @@ from torba.server.hash import double_sha256 class Merkle: - '''Perform merkle tree calculations on binary hashes using a given hash + """Perform merkle tree calculations on binary hashes using a given hash function. If the hash count is not even, the final hash is repeated when calculating the next merkle layer up the tree. - ''' + """ def __init__(self, hash_func=double_sha256): self.hash_func = hash_func @@ -47,7 +47,7 @@ class Merkle: return self.branch_length(hash_count) + 1 def branch_length(self, hash_count): - '''Return the length of a merkle branch given the number of hashes.''' + """Return the length of a merkle branch given the number of hashes.""" if not isinstance(hash_count, int): raise TypeError('hash_count must be an integer') if hash_count < 1: @@ -55,9 +55,9 @@ class Merkle: return ceil(log(hash_count, 2)) def branch_and_root(self, hashes, index, length=None): - '''Return a (merkle branch, merkle_root) pair given hashes, and the + """Return a (merkle branch, merkle_root) pair given hashes, and the index of one of those hashes. - ''' + """ hashes = list(hashes) if not isinstance(index, int): raise TypeError('index must be an integer') @@ -86,12 +86,12 @@ class Merkle: return branch, hashes[0] def root(self, hashes, length=None): - '''Return the merkle root of a non-empty iterable of binary hashes.''' + """Return the merkle root of a non-empty iterable of binary hashes.""" branch, root = self.branch_and_root(hashes, 0, length) return root def root_from_proof(self, hash, branch, index): - '''Return the merkle root given a hash, a merkle branch to it, and + """Return the merkle root given a hash, a merkle branch to it, and its index in the hashes array. branch is an iterable sorted deepest to shallowest. If the @@ -102,7 +102,7 @@ class Merkle: branch_length(). Unfortunately this is not easily done for bitcoin transactions as the number of transactions in a block is unknown to an SPV client. - ''' + """ hash_func = self.hash_func for elt in branch: if index & 1: @@ -115,8 +115,8 @@ class Merkle: return hash def level(self, hashes, depth_higher): - '''Return a level of the merkle tree of hashes the given depth - higher than the bottom row of the original tree.''' + """Return a level of the merkle tree of hashes the given depth + higher than the bottom row of the original tree.""" size = 1 << depth_higher root = self.root return [root(hashes[n: n + size], depth_higher) @@ -124,7 +124,7 @@ class Merkle: def branch_and_root_from_level(self, level, leaf_hashes, index, depth_higher): - '''Return a (merkle branch, merkle_root) pair when a merkle-tree has a + """Return a (merkle branch, merkle_root) pair when a merkle-tree has a level cached. To maximally reduce the amount of data hashed in computing a @@ -140,7 +140,7 @@ class Merkle: index is the index in the full list of hashes of the hash whose merkle branch we want. - ''' + """ if not isinstance(level, list): raise TypeError("level must be a list") if not isinstance(leaf_hashes, list): @@ -157,14 +157,14 @@ class Merkle: class MerkleCache: - '''A cache to calculate merkle branches efficiently.''' + """A cache to calculate merkle branches efficiently.""" def __init__(self, merkle, source_func): - '''Initialise a cache hashes taken from source_func: + """Initialise a cache hashes taken from source_func: async def source_func(index, count): ... - ''' + """ self.merkle = merkle self.source_func = source_func self.length = 0 @@ -175,9 +175,9 @@ class MerkleCache: return 1 << self.depth_higher def _leaf_start(self, index): - '''Given a level's depth higher and a hash index, return the leaf + """Given a level's depth higher and a hash index, return the leaf index and leaf hash count needed to calculate a merkle branch. - ''' + """ depth_higher = self.depth_higher return (index >> depth_higher) << depth_higher @@ -185,7 +185,7 @@ class MerkleCache: return self.merkle.level(hashes, self.depth_higher) async def _extend_to(self, length): - '''Extend the length of the cache if necessary.''' + """Extend the length of the cache if necessary.""" if length <= self.length: return # Start from the beginning of any final partial segment. @@ -196,8 +196,8 @@ class MerkleCache: self.length = length async def _level_for(self, length): - '''Return a (level_length, final_hash) pair for a truncation - of the hashes to the given length.''' + """Return a (level_length, final_hash) pair for a truncation + of the hashes to the given length.""" if length == self.length: return self.level level = self.level[:length >> self.depth_higher] @@ -208,15 +208,15 @@ class MerkleCache: return level async def initialize(self, length): - '''Call to initialize the cache to a source of given length.''' + """Call to initialize the cache to a source of given length.""" self.length = length self.depth_higher = self.merkle.tree_depth(length) // 2 self.level = self._level(await self.source_func(0, length)) self.initialized.set() def truncate(self, length): - '''Truncate the cache so it covers no more than length underlying - hashes.''' + """Truncate the cache so it covers no more than length underlying + hashes.""" if not isinstance(length, int): raise TypeError('length must be an integer') if length <= 0: @@ -228,11 +228,11 @@ class MerkleCache: self.level[length >> self.depth_higher:] = [] async def branch_and_root(self, length, index): - '''Return a merkle branch and root. Length is the number of + """Return a merkle branch and root. Length is the number of hashes used to calculate the merkle root, index is the position of the hash to calculate the branch of. - index must be less than length, which must be at least 1.''' + index must be less than length, which must be at least 1.""" if not isinstance(length, int): raise TypeError('length must be an integer') if not isinstance(index, int): diff --git a/torba/server/peer.py b/torba/server/peer.py index c59c84a34..a3f268da3 100644 --- a/torba/server/peer.py +++ b/torba/server/peer.py @@ -23,7 +23,7 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -'''Representation of a peer server.''' +"""Representation of a peer server.""" from ipaddress import ip_address @@ -47,8 +47,8 @@ class Peer: def __init__(self, host, features, source='unknown', ip_addr=None, last_good=0, last_try=0, try_count=0): - '''Create a peer given a host name (or IP address as a string), - a dictionary of features, and a record of the source.''' + """Create a peer given a host name (or IP address as a string), + a dictionary of features, and a record of the source.""" assert isinstance(host, str) assert isinstance(features, dict) assert host in features.get('hosts', {}) @@ -83,14 +83,14 @@ class Peer: @classmethod def deserialize(cls, item): - '''Deserialize from a dictionary.''' + """Deserialize from a dictionary.""" return cls(**item) def matches(self, peers): - '''Return peers whose host matches our hostname or IP address. + """Return peers whose host matches our hostname or IP address. Additionally include all peers whose IP address matches our hostname if that is an IP address. - ''' + """ candidates = (self.host.lower(), self.ip_addr) return [peer for peer in peers if peer.host.lower() in candidates @@ -100,7 +100,7 @@ class Peer: return self.host def update_features(self, features): - '''Update features in-place.''' + """Update features in-place.""" try: tmp = Peer(self.host, features) except Exception: @@ -115,8 +115,8 @@ class Peer: setattr(self, feature, getattr(peer, feature)) def connection_port_pairs(self): - '''Return a list of (kind, port) pairs to try when making a - connection.''' + """Return a list of (kind, port) pairs to try when making a + connection.""" # Use a list not a set - it's important to try the registered # ports first. pairs = [('SSL', self.ssl_port), ('TCP', self.tcp_port)] @@ -125,13 +125,13 @@ class Peer: return [pair for pair in pairs if pair[1]] def mark_bad(self): - '''Mark as bad to avoid reconnects but also to remember for a - while.''' + """Mark as bad to avoid reconnects but also to remember for a + while.""" self.bad = True def check_ports(self, other): - '''Remember differing ports in case server operator changed them - or removed one.''' + """Remember differing ports in case server operator changed them + or removed one.""" if other.ssl_port != self.ssl_port: self.other_port_pairs.add(('SSL', other.ssl_port)) if other.tcp_port != self.tcp_port: @@ -160,7 +160,7 @@ class Peer: @cachedproperty def ip_address(self): - '''The host as a python ip_address object, or None.''' + """The host as a python ip_address object, or None.""" try: return ip_address(self.host) except ValueError: @@ -174,7 +174,7 @@ class Peer: return tuple(self.ip_addr.split('.')[:2]) def serialize(self): - '''Serialize to a dictionary.''' + """Serialize to a dictionary.""" return {attr: getattr(self, attr) for attr in self.ATTRS} def _port(self, key): @@ -202,28 +202,28 @@ class Peer: @cachedproperty def genesis_hash(self): - '''Returns None if no SSL port, otherwise the port as an integer.''' + """Returns None if no SSL port, otherwise the port as an integer.""" return self._string('genesis_hash') @cachedproperty def ssl_port(self): - '''Returns None if no SSL port, otherwise the port as an integer.''' + """Returns None if no SSL port, otherwise the port as an integer.""" return self._port('ssl_port') @cachedproperty def tcp_port(self): - '''Returns None if no TCP port, otherwise the port as an integer.''' + """Returns None if no TCP port, otherwise the port as an integer.""" return self._port('tcp_port') @cachedproperty def server_version(self): - '''Returns the server version as a string if known, otherwise None.''' + """Returns the server version as a string if known, otherwise None.""" return self._string('server_version') @cachedproperty def pruning(self): - '''Returns the pruning level as an integer. None indicates no - pruning.''' + """Returns the pruning level as an integer. None indicates no + pruning.""" pruning = self._integer('pruning') if pruning and pruning > 0: return pruning @@ -236,22 +236,22 @@ class Peer: @cachedproperty def protocol_min(self): - '''Minimum protocol version as a string, e.g., 1.0''' + """Minimum protocol version as a string, e.g., 1.0""" return self._protocol_version_string('protocol_min') @cachedproperty def protocol_max(self): - '''Maximum protocol version as a string, e.g., 1.1''' + """Maximum protocol version as a string, e.g., 1.1""" return self._protocol_version_string('protocol_max') def to_tuple(self): - '''The tuple ((ip, host, details) expected in response - to a peers subscription.''' + """The tuple ((ip, host, details) expected in response + to a peers subscription.""" details = self.real_name().split()[1:] return (self.ip_addr or self.host, self.host, details) def real_name(self): - '''Real name of this peer as used on IRC.''' + """Real name of this peer as used on IRC.""" def port_text(letter, port): if port == self.DEFAULT_PORTS.get(letter): return letter @@ -268,12 +268,12 @@ class Peer: @classmethod def from_real_name(cls, real_name, source): - '''Real name is a real name as on IRC, such as + """Real name is a real name as on IRC, such as "erbium1.sytes.net v1.0 s t" Returns an instance of this Peer class. - ''' + """ host = 'nohost' features = {} ports = {} diff --git a/torba/server/peers.py b/torba/server/peers.py index 842111466..1ea4ebc91 100644 --- a/torba/server/peers.py +++ b/torba/server/peers.py @@ -5,7 +5,7 @@ # See the file "LICENCE" for information about the copyright # and warranty status of this software. -'''Peer management.''' +"""Peer management.""" import asyncio import random @@ -39,7 +39,7 @@ def assert_good(message, result, instance): class PeerSession(RPCSession): - '''An outgoing session to a peer.''' + """An outgoing session to a peer.""" async def handle_request(self, request): # We subscribe so might be unlucky enough to get a notification... @@ -51,11 +51,11 @@ class PeerSession(RPCSession): class PeerManager: - '''Looks after the DB of peer network servers. + """Looks after the DB of peer network servers. Attempts to maintain a connection with up to 8 peers. Issues a 'peers.subscribe' RPC to them and tells them our data. - ''' + """ def __init__(self, env, db): self.logger = class_logger(__name__, self.__class__.__name__) # Initialise the Peer class @@ -78,12 +78,12 @@ class PeerManager: self.group = TaskGroup() def _my_clearnet_peer(self): - '''Returns the clearnet peer representing this server, if any.''' + """Returns the clearnet peer representing this server, if any.""" clearnet = [peer for peer in self.myselves if not peer.is_tor] return clearnet[0] if clearnet else None def _set_peer_statuses(self): - '''Set peer statuses.''' + """Set peer statuses.""" cutoff = time.time() - STALE_SECS for peer in self.peers: if peer.bad: @@ -96,10 +96,10 @@ class PeerManager: peer.status = PEER_NEVER def _features_to_register(self, peer, remote_peers): - '''If we should register ourselves to the remote peer, which has + """If we should register ourselves to the remote peer, which has reported the given list of known peers, return the clearnet identity features to register, otherwise None. - ''' + """ # Announce ourself if not present. Don't if disabled, we # are a non-public IP address, or to ourselves. if not self.env.peer_announce or peer in self.myselves: @@ -114,7 +114,7 @@ class PeerManager: return my.features def _permit_new_onion_peer(self): - '''Accept a new onion peer only once per random time interval.''' + """Accept a new onion peer only once per random time interval.""" now = time.time() if now < self.permit_onion_peer_time: return False @@ -122,7 +122,7 @@ class PeerManager: return True async def _import_peers(self): - '''Import hard-coded peers from a file or the coin defaults.''' + """Import hard-coded peers from a file or the coin defaults.""" imported_peers = self.myselves.copy() # Add the hard-coded ones unless only reporting ourself if self.env.peer_discovery != self.env.PD_SELF: @@ -131,12 +131,12 @@ class PeerManager: await self._note_peers(imported_peers, limit=None) async def _detect_proxy(self): - '''Detect a proxy if we don't have one and some time has passed since + """Detect a proxy if we don't have one and some time has passed since the last attempt. If found self.proxy is set to a SOCKSProxy instance, otherwise None. - ''' + """ host = self.env.tor_proxy_host if self.env.tor_proxy_port is None: ports = [9050, 9150, 1080] @@ -155,7 +155,7 @@ class PeerManager: async def _note_peers(self, peers, limit=2, check_ports=False, source=None): - '''Add a limited number of peers that are not already present.''' + """Add a limited number of peers that are not already present.""" new_peers = [] for peer in peers: if not peer.is_public or (peer.is_tor and not self.proxy): @@ -378,12 +378,12 @@ class PeerManager: # External interface # async def discover_peers(self): - '''Perform peer maintenance. This includes + """Perform peer maintenance. This includes 1) Forgetting unreachable peers. 2) Verifying connectivity of new peers. 3) Retrying old peers at regular intervals. - ''' + """ if self.env.peer_discovery != self.env.PD_ON: self.logger.info('peer discovery is disabled') return @@ -395,7 +395,7 @@ class PeerManager: self.group.add(self._import_peers()) def info(self): - '''The number of peers.''' + """The number of peers.""" self._set_peer_statuses() counter = Counter(peer.status for peer in self.peers) return { @@ -407,11 +407,11 @@ class PeerManager: } async def add_localRPC_peer(self, real_name): - '''Add a peer passed by the admin over LocalRPC.''' + """Add a peer passed by the admin over LocalRPC.""" await self._note_peers([Peer.from_real_name(real_name, 'RPC')]) async def on_add_peer(self, features, source_info): - '''Add a peer (but only if the peer resolves to the source).''' + """Add a peer (but only if the peer resolves to the source).""" if not source_info: self.logger.info('ignored add_peer request: no source info') return False @@ -449,12 +449,12 @@ class PeerManager: return permit def on_peers_subscribe(self, is_tor): - '''Returns the server peers as a list of (ip, host, details) tuples. + """Returns the server peers as a list of (ip, host, details) tuples. We return all peers we've connected to in the last day. Additionally, if we don't have onion routing, we return a few hard-coded onion servers. - ''' + """ cutoff = time.time() - STALE_SECS recent = [peer for peer in self.peers if peer.last_good > cutoff and @@ -485,12 +485,12 @@ class PeerManager: return [peer.to_tuple() for peer in peers] def proxy_peername(self): - '''Return the peername of the proxy, if there is a proxy, otherwise - None.''' + """Return the peername of the proxy, if there is a proxy, otherwise + None.""" return self.proxy.peername if self.proxy else None def rpc_data(self): - '''Peer data for the peers RPC method.''' + """Peer data for the peers RPC method.""" self._set_peer_statuses() descs = ['good', 'stale', 'never', 'bad'] diff --git a/torba/server/script.py b/torba/server/script.py index 00f7472b6..d47811eb2 100644 --- a/torba/server/script.py +++ b/torba/server/script.py @@ -24,7 +24,7 @@ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # and warranty status of this software. -'''Script-related classes and functions.''' +"""Script-related classes and functions.""" import struct @@ -37,7 +37,7 @@ from torba.server.util import unpack_le_uint16_from, unpack_le_uint32_from, \ class ScriptError(Exception): - '''Exception used for script errors.''' + """Exception used for script errors.""" OpCodes = Enumeration("Opcodes", [ @@ -92,9 +92,9 @@ def _match_ops(ops, pattern): class ScriptPubKey: - '''A class for handling a tx output script that gives conditions + """A class for handling a tx output script that gives conditions necessary for spending. - ''' + """ TO_ADDRESS_OPS = [OpCodes.OP_DUP, OpCodes.OP_HASH160, -1, OpCodes.OP_EQUALVERIFY, OpCodes.OP_CHECKSIG] @@ -106,7 +106,7 @@ class ScriptPubKey: @classmethod def pay_to(cls, handlers, script): - '''Parse a script, invoke the appropriate handler and + """Parse a script, invoke the appropriate handler and return the result. One of the following handlers is invoked: @@ -115,7 +115,7 @@ class ScriptPubKey: handlers.pubkey(pubkey) handlers.unspendable() handlers.strange(script) - ''' + """ try: ops = Script.get_ops(script) except ScriptError: @@ -163,7 +163,7 @@ class ScriptPubKey: @classmethod def multisig_script(cls, m, pubkeys): - '''Returns the script for a pay-to-multisig transaction.''' + """Returns the script for a pay-to-multisig transaction.""" n = len(pubkeys) if not 1 <= m <= n <= 15: raise ScriptError('{:d} of {:d} multisig script not possible' @@ -218,7 +218,7 @@ class Script: @classmethod def push_data(cls, data): - '''Returns the opcodes to push the data on the stack.''' + """Returns the opcodes to push the data on the stack.""" assert isinstance(data, (bytes, bytearray)) n = len(data) diff --git a/torba/server/session.py b/torba/server/session.py index c5b063ec5..47d611484 100644 --- a/torba/server/session.py +++ b/torba/server/session.py @@ -5,7 +5,7 @@ # See the file "LICENCE" for information about the copyright # and warranty status of this software. -'''Classes for local RPC server and remote client TCP/SSL servers.''' +"""Classes for local RPC server and remote client TCP/SSL servers.""" import asyncio import codecs @@ -48,8 +48,8 @@ def scripthash_to_hashX(scripthash): def non_negative_integer(value): - '''Return param value it is or can be converted to a non-negative - integer, otherwise raise an RPCError.''' + """Return param value it is or can be converted to a non-negative + integer, otherwise raise an RPCError.""" try: value = int(value) if value >= 0: @@ -61,15 +61,15 @@ def non_negative_integer(value): def assert_boolean(value): - '''Return param value it is boolean otherwise raise an RPCError.''' + """Return param value it is boolean otherwise raise an RPCError.""" if value in (False, True): return value raise RPCError(BAD_REQUEST, f'{value} should be a boolean value') def assert_tx_hash(value): - '''Raise an RPCError if the value is not a valid transaction - hash.''' + """Raise an RPCError if the value is not a valid transaction + hash.""" try: if len(util.hex_to_bytes(value)) == 32: return @@ -79,7 +79,7 @@ def assert_tx_hash(value): class Semaphores: - '''For aiorpcX's semaphore handling.''' + """For aiorpcX's semaphore handling.""" def __init__(self, semaphores): self.semaphores = semaphores @@ -104,7 +104,7 @@ class SessionGroup: class SessionManager: - '''Holds global state about all sessions.''' + """Holds global state about all sessions.""" def __init__(self, env, db, bp, daemon, mempool, shutdown_event): env.max_send = max(350000, env.max_send) @@ -159,9 +159,9 @@ class SessionManager: self.logger.info(f'{kind} server listening on {host}:{port:d}') async def _start_external_servers(self): - '''Start listening on TCP and SSL ports, but only if the respective + """Start listening on TCP and SSL ports, but only if the respective port was given in the environment. - ''' + """ env = self.env host = env.cs_host(for_rpc=False) if env.tcp_port is not None: @@ -172,7 +172,7 @@ class SessionManager: await self._start_server('SSL', host, env.ssl_port, ssl=sslc) async def _close_servers(self, kinds): - '''Close the servers of the given kinds (TCP etc.).''' + """Close the servers of the given kinds (TCP etc.).""" if kinds: self.logger.info('closing down {} listening servers' .format(', '.join(kinds))) @@ -203,7 +203,7 @@ class SessionManager: paused = False async def _log_sessions(self): - '''Periodically log sessions.''' + """Periodically log sessions.""" log_interval = self.env.log_sessions if log_interval: while True: @@ -247,7 +247,7 @@ class SessionManager: return result async def _clear_stale_sessions(self): - '''Cut off sessions that haven't done anything for 10 minutes.''' + """Cut off sessions that haven't done anything for 10 minutes.""" while True: await sleep(60) stale_cutoff = time.time() - self.env.session_timeout @@ -276,7 +276,7 @@ class SessionManager: session.group = new_group def _get_info(self): - '''A summary of server state.''' + """A summary of server state.""" group_map = self._group_map() return { 'closing': len([s for s in self.sessions if s.is_closing()]), @@ -298,7 +298,7 @@ class SessionManager: } def _session_data(self, for_log): - '''Returned to the RPC 'sessions' call.''' + """Returned to the RPC 'sessions' call.""" now = time.time() sessions = sorted(self.sessions, key=lambda s: s.start_time) return [(session.session_id, @@ -315,7 +315,7 @@ class SessionManager: for session in sessions] def _group_data(self): - '''Returned to the RPC 'groups' call.''' + """Returned to the RPC 'groups' call.""" result = [] group_map = self._group_map() for group, sessions in group_map.items(): @@ -338,9 +338,9 @@ class SessionManager: return electrum_header, raw_header async def _refresh_hsub_results(self, height): - '''Refresh the cached header subscription responses to be for height, + """Refresh the cached header subscription responses to be for height, and record that as notified_height. - ''' + """ # Paranoia: a reorg could race and leave db_height lower height = min(height, self.db.db_height) electrum, raw = await self._electrum_and_raw_headers(height) @@ -350,39 +350,39 @@ class SessionManager: # --- LocalRPC command handlers async def rpc_add_peer(self, real_name): - '''Add a peer. + """Add a peer. real_name: "bch.electrumx.cash t50001 s50002" for example - ''' + """ await self.peer_mgr.add_localRPC_peer(real_name) return "peer '{}' added".format(real_name) async def rpc_disconnect(self, session_ids): - '''Disconnect sesssions. + """Disconnect sesssions. session_ids: array of session IDs - ''' + """ async def close(session): - '''Close the session's transport.''' + """Close the session's transport.""" await session.close(force_after=2) return f'disconnected {session.session_id}' return await self._for_each_session(session_ids, close) async def rpc_log(self, session_ids): - '''Toggle logging of sesssions. + """Toggle logging of sesssions. session_ids: array of session IDs - ''' + """ async def toggle_logging(session): - '''Toggle logging of the session.''' + """Toggle logging of the session.""" session.toggle_logging() return f'log {session.session_id}: {session.log_me}' return await self._for_each_session(session_ids, toggle_logging) async def rpc_daemon_url(self, daemon_url): - '''Replace the daemon URL.''' + """Replace the daemon URL.""" daemon_url = daemon_url or self.env.daemon_url try: self.daemon.set_url(daemon_url) @@ -391,24 +391,24 @@ class SessionManager: return f'now using daemon at {self.daemon.logged_url()}' async def rpc_stop(self): - '''Shut down the server cleanly.''' + """Shut down the server cleanly.""" self.shutdown_event.set() return 'stopping' async def rpc_getinfo(self): - '''Return summary information about the server process.''' + """Return summary information about the server process.""" return self._get_info() async def rpc_groups(self): - '''Return statistics about the session groups.''' + """Return statistics about the session groups.""" return self._group_data() async def rpc_peers(self): - '''Return a list of data about server peers.''' + """Return a list of data about server peers.""" return self.peer_mgr.rpc_data() async def rpc_query(self, items, limit): - '''Return a list of data about server peers.''' + """Return a list of data about server peers.""" coin = self.env.coin db = self.db lines = [] @@ -459,14 +459,14 @@ class SessionManager: return lines async def rpc_sessions(self): - '''Return statistics about connected sessions.''' + """Return statistics about connected sessions.""" return self._session_data(for_log=False) async def rpc_reorg(self, count): - '''Force a reorg of the given number of blocks. + """Force a reorg of the given number of blocks. count: number of blocks to reorg - ''' + """ count = non_negative_integer(count) if not self.bp.force_chain_reorg(count): raise RPCError(BAD_REQUEST, 'still catching up with daemon') @@ -475,8 +475,8 @@ class SessionManager: # --- External Interface async def serve(self, notifications, server_listening_event): - '''Start the RPC server if enabled. When the event is triggered, - start TCP and SSL servers.''' + """Start the RPC server if enabled. When the event is triggered, + start TCP and SSL servers.""" try: if self.env.rpc_port is not None: await self._start_server('RPC', self.env.cs_host(for_rpc=True), @@ -515,18 +515,18 @@ class SessionManager: ]) def session_count(self): - '''The number of connections that we've sent something to.''' + """The number of connections that we've sent something to.""" return len(self.sessions) async def daemon_request(self, method, *args): - '''Catch a DaemonError and convert it to an RPCError.''' + """Catch a DaemonError and convert it to an RPCError.""" try: return await getattr(self.daemon, method)(*args) except DaemonError as e: raise RPCError(DAEMON_ERROR, f'daemon error: {e!r}') from None async def raw_header(self, height): - '''Return the binary header at the given height.''' + """Return the binary header at the given height.""" try: return await self.db.raw_header(height) except IndexError: @@ -534,7 +534,7 @@ class SessionManager: 'out of range') from None async def electrum_header(self, height): - '''Return the deserialized header at the given height.''' + """Return the deserialized header at the given height.""" electrum_header, _ = await self._electrum_and_raw_headers(height) return electrum_header @@ -544,7 +544,7 @@ class SessionManager: return hex_hash async def limited_history(self, hashX): - '''A caching layer.''' + """A caching layer.""" hc = self.history_cache if hashX not in hc: # History DoS limit. Each element of history is about 99 @@ -556,7 +556,7 @@ class SessionManager: return hc[hashX] async def _notify_sessions(self, height, touched): - '''Notify sessions about height changes and touched addresses.''' + """Notify sessions about height changes and touched addresses.""" height_changed = height != self.notified_height if height_changed: await self._refresh_hsub_results(height) @@ -579,7 +579,7 @@ class SessionManager: return self.cur_group def remove_session(self, session): - '''Remove a session from our sessions list if there.''' + """Remove a session from our sessions list if there.""" self.sessions.remove(session) self.session_event.set() @@ -593,11 +593,11 @@ class SessionManager: class SessionBase(RPCSession): - '''Base class of ElectrumX JSON sessions. + """Base class of ElectrumX JSON sessions. Each session runs its tasks in asynchronous parallelism with other sessions. - ''' + """ MAX_CHUNK_SIZE = 2016 session_counter = itertools.count() @@ -627,8 +627,8 @@ class SessionBase(RPCSession): pass def peer_address_str(self, *, for_log=True): - '''Returns the peer's IP address and port as a human-readable - string, respecting anon logs if the output is for a log.''' + """Returns the peer's IP address and port as a human-readable + string, respecting anon logs if the output is for a log.""" if for_log and self.anon_logs: return 'xx.xx.xx.xx:xx' return super().peer_address_str() @@ -642,7 +642,7 @@ class SessionBase(RPCSession): self.log_me = not self.log_me def flags(self): - '''Status flags.''' + """Status flags.""" status = self.kind[0] if self.is_closing(): status += 'C' @@ -652,7 +652,7 @@ class SessionBase(RPCSession): return status def connection_made(self, transport): - '''Handle an incoming client connection.''' + """Handle an incoming client connection.""" super().connection_made(transport) self.session_id = next(self.session_counter) context = {'conn_id': f'{self.session_id}'} @@ -662,7 +662,7 @@ class SessionBase(RPCSession): f'{self.session_mgr.session_count():,d} total') def connection_lost(self, exc): - '''Handle client disconnection.''' + """Handle client disconnection.""" super().connection_lost(exc) self.session_mgr.remove_session(self) msg = '' @@ -687,9 +687,9 @@ class SessionBase(RPCSession): return 0 async def handle_request(self, request): - '''Handle an incoming request. ElectrumX doesn't receive + """Handle an incoming request. ElectrumX doesn't receive notifications from client sessions. - ''' + """ if isinstance(request, Request): handler = self.request_handlers.get(request.method) else: @@ -699,7 +699,7 @@ class SessionBase(RPCSession): class ElectrumX(SessionBase): - '''A TCP server that handles incoming Electrum connections.''' + """A TCP server that handles incoming Electrum connections.""" PROTOCOL_MIN = (1, 1) PROTOCOL_MAX = (1, 4) @@ -722,7 +722,7 @@ class ElectrumX(SessionBase): @classmethod def server_features(cls, env): - '''Return the server features dictionary.''' + """Return the server features dictionary.""" min_str, max_str = cls.protocol_min_max_strings() return { 'hosts': env.hosts_dict(), @@ -739,7 +739,7 @@ class ElectrumX(SessionBase): @classmethod def server_version_args(cls): - '''The arguments to a server.version RPC call to a peer.''' + """The arguments to a server.version RPC call to a peer.""" return [torba.__version__, cls.protocol_min_max_strings()] def protocol_version_string(self): @@ -749,9 +749,9 @@ class ElectrumX(SessionBase): return len(self.hashX_subs) async def notify(self, touched, height_changed): - '''Notify the client about changes to touched addresses (from mempool + """Notify the client about changes to touched addresses (from mempool updates or new blocks) and height. - ''' + """ if height_changed and self.subscribe_headers: args = (await self.subscribe_headers_result(), ) await self.send_notification('blockchain.headers.subscribe', args) @@ -789,40 +789,40 @@ class ElectrumX(SessionBase): self.logger.info(f'notified of {len(changed):,d} address{es}') async def subscribe_headers_result(self): - '''The result of a header subscription or notification.''' + """The result of a header subscription or notification.""" return self.session_mgr.hsub_results[self.subscribe_headers_raw] async def _headers_subscribe(self, raw): - '''Subscribe to get headers of new blocks.''' + """Subscribe to get headers of new blocks.""" self.subscribe_headers_raw = assert_boolean(raw) self.subscribe_headers = True return await self.subscribe_headers_result() async def headers_subscribe(self): - '''Subscribe to get raw headers of new blocks.''' + """Subscribe to get raw headers of new blocks.""" return await self._headers_subscribe(True) async def headers_subscribe_True(self, raw=True): - '''Subscribe to get headers of new blocks.''' + """Subscribe to get headers of new blocks.""" return await self._headers_subscribe(raw) async def headers_subscribe_False(self, raw=False): - '''Subscribe to get headers of new blocks.''' + """Subscribe to get headers of new blocks.""" return await self._headers_subscribe(raw) async def add_peer(self, features): - '''Add a peer (but only if the peer resolves to the source).''' + """Add a peer (but only if the peer resolves to the source).""" return await self.peer_mgr.on_add_peer(features, self.peer_address()) async def peers_subscribe(self): - '''Return the server peers as a list of (ip, host, details) tuples.''' + """Return the server peers as a list of (ip, host, details) tuples.""" return self.peer_mgr.on_peers_subscribe(self.is_tor()) async def address_status(self, hashX): - '''Returns an address status. + """Returns an address status. Status is a hex string, but must be None if there is no history. - ''' + """ # Note history is ordered and mempool unordered in electrum-server # For mempool, height is -1 if it has unconfirmed inputs, otherwise 0 db_history = await self.session_mgr.limited_history(hashX) @@ -847,8 +847,8 @@ class ElectrumX(SessionBase): return status async def hashX_listunspent(self, hashX): - '''Return the list of UTXOs of a script hash, including mempool - effects.''' + """Return the list of UTXOs of a script hash, including mempool + effects.""" utxos = await self.db.all_utxos(hashX) utxos = sorted(utxos) utxos.extend(await self.mempool.unordered_UTXOs(hashX)) @@ -879,29 +879,29 @@ class ElectrumX(SessionBase): raise RPCError(BAD_REQUEST, f'{address} is not a valid address') async def address_get_balance(self, address): - '''Return the confirmed and unconfirmed balance of an address.''' + """Return the confirmed and unconfirmed balance of an address.""" hashX = self.address_to_hashX(address) return await self.get_balance(hashX) async def address_get_history(self, address): - '''Return the confirmed and unconfirmed history of an address.''' + """Return the confirmed and unconfirmed history of an address.""" hashX = self.address_to_hashX(address) return await self.confirmed_and_unconfirmed_history(hashX) async def address_get_mempool(self, address): - '''Return the mempool transactions touching an address.''' + """Return the mempool transactions touching an address.""" hashX = self.address_to_hashX(address) return await self.unconfirmed_history(hashX) async def address_listunspent(self, address): - '''Return the list of UTXOs of an address.''' + """Return the list of UTXOs of an address.""" hashX = self.address_to_hashX(address) return await self.hashX_listunspent(hashX) async def address_subscribe(self, address): - '''Subscribe to an address. + """Subscribe to an address. - address: the address to subscribe to''' + address: the address to subscribe to""" hashX = self.address_to_hashX(address) return await self.hashX_subscribe(hashX, address) @@ -912,7 +912,7 @@ class ElectrumX(SessionBase): return {'confirmed': confirmed, 'unconfirmed': unconfirmed} async def scripthash_get_balance(self, scripthash): - '''Return the confirmed and unconfirmed balance of a scripthash.''' + """Return the confirmed and unconfirmed balance of a scripthash.""" hashX = scripthash_to_hashX(scripthash) return await self.get_balance(hashX) @@ -932,24 +932,24 @@ class ElectrumX(SessionBase): return conf + await self.unconfirmed_history(hashX) async def scripthash_get_history(self, scripthash): - '''Return the confirmed and unconfirmed history of a scripthash.''' + """Return the confirmed and unconfirmed history of a scripthash.""" hashX = scripthash_to_hashX(scripthash) return await self.confirmed_and_unconfirmed_history(hashX) async def scripthash_get_mempool(self, scripthash): - '''Return the mempool transactions touching a scripthash.''' + """Return the mempool transactions touching a scripthash.""" hashX = scripthash_to_hashX(scripthash) return await self.unconfirmed_history(hashX) async def scripthash_listunspent(self, scripthash): - '''Return the list of UTXOs of a scripthash.''' + """Return the list of UTXOs of a scripthash.""" hashX = scripthash_to_hashX(scripthash) return await self.hashX_listunspent(hashX) async def scripthash_subscribe(self, scripthash): - '''Subscribe to a script hash. + """Subscribe to a script hash. - scripthash: the SHA256 hash of the script to subscribe to''' + scripthash: the SHA256 hash of the script to subscribe to""" hashX = scripthash_to_hashX(scripthash) return await self.hashX_subscribe(hashX, scripthash) @@ -968,8 +968,8 @@ class ElectrumX(SessionBase): } async def block_header(self, height, cp_height=0): - '''Return a raw block header as a hexadecimal string, or as a - dictionary with a merkle proof.''' + """Return a raw block header as a hexadecimal string, or as a + dictionary with a merkle proof.""" height = non_negative_integer(height) cp_height = non_negative_integer(cp_height) raw_header_hex = (await self.session_mgr.raw_header(height)).hex() @@ -980,18 +980,18 @@ class ElectrumX(SessionBase): return result async def block_header_13(self, height): - '''Return a raw block header as a hexadecimal string. + """Return a raw block header as a hexadecimal string. - height: the header's height''' + height: the header's height""" return await self.block_header(height) async def block_headers(self, start_height, count, cp_height=0): - '''Return count concatenated block headers as hex for the main chain; + """Return count concatenated block headers as hex for the main chain; starting at start_height. start_height and count must be non-negative integers. At most MAX_CHUNK_SIZE headers will be returned. - ''' + """ start_height = non_negative_integer(start_height) count = non_negative_integer(count) cp_height = non_negative_integer(cp_height) @@ -1009,9 +1009,9 @@ class ElectrumX(SessionBase): return await self.block_headers(start_height, count) async def block_get_chunk(self, index): - '''Return a chunk of block headers as a hexadecimal string. + """Return a chunk of block headers as a hexadecimal string. - index: the chunk index''' + index: the chunk index""" index = non_negative_integer(index) size = self.coin.CHUNK_SIZE start_height = index * size @@ -1019,15 +1019,15 @@ class ElectrumX(SessionBase): return headers.hex() async def block_get_header(self, height): - '''The deserialized header at a given height. + """The deserialized header at a given height. - height: the header's height''' + height: the header's height""" height = non_negative_integer(height) return await self.session_mgr.electrum_header(height) def is_tor(self): - '''Try to detect if the connection is to a tor hidden service we are - running.''' + """Try to detect if the connection is to a tor hidden service we are + running.""" peername = self.peer_mgr.proxy_peername() if not peername: return False @@ -1051,11 +1051,11 @@ class ElectrumX(SessionBase): return banner async def donation_address(self): - '''Return the donation address as a string, empty if there is none.''' + """Return the donation address as a string, empty if there is none.""" return self.env.donation_address async def banner(self): - '''Return the server banner text.''' + """Return the server banner text.""" banner = f'You are connected to an {torba.__version__} server.' if self.is_tor(): @@ -1074,31 +1074,31 @@ class ElectrumX(SessionBase): return banner async def relayfee(self): - '''The minimum fee a low-priority tx must pay in order to be accepted - to the daemon's memory pool.''' + """The minimum fee a low-priority tx must pay in order to be accepted + to the daemon's memory pool.""" return await self.daemon_request('relayfee') async def estimatefee(self, number): - '''The estimated transaction fee per kilobyte to be paid for a + """The estimated transaction fee per kilobyte to be paid for a transaction to be included within a certain number of blocks. number: the number of blocks - ''' + """ number = non_negative_integer(number) return await self.daemon_request('estimatefee', number) async def ping(self): - '''Serves as a connection keep-alive mechanism and for the client to + """Serves as a connection keep-alive mechanism and for the client to confirm the server is still responding. - ''' + """ return None async def server_version(self, client_name='', protocol_version=None): - '''Returns the server version as a string. + """Returns the server version as a string. client_name: a string identifying the client protocol_version: the protocol version spoken by the client - ''' + """ if self.sv_seen and self.protocol_tuple >= (1, 4): raise RPCError(BAD_REQUEST, f'server.version already sent') self.sv_seen = True @@ -1129,9 +1129,9 @@ class ElectrumX(SessionBase): return torba.__version__, self.protocol_version_string() async def transaction_broadcast(self, raw_tx): - '''Broadcast a raw transaction to the network. + """Broadcast a raw transaction to the network. - raw_tx: the raw transaction as a hexadecimal string''' + raw_tx: the raw transaction as a hexadecimal string""" # This returns errors as JSON RPC errors, as is natural try: hex_hash = await self.session_mgr.broadcast_transaction(raw_tx) @@ -1146,11 +1146,11 @@ class ElectrumX(SessionBase): f'network rules.\n\n{message}\n[{raw_tx}]') async def transaction_get(self, tx_hash, verbose=False): - '''Return the serialized raw transaction given its hash + """Return the serialized raw transaction given its hash tx_hash: the transaction hash as a hexadecimal string verbose: passed on to the daemon - ''' + """ assert_tx_hash(tx_hash) if verbose not in (True, False): raise RPCError(BAD_REQUEST, f'"verbose" must be a boolean') @@ -1158,12 +1158,12 @@ class ElectrumX(SessionBase): return await self.daemon_request('getrawtransaction', tx_hash, verbose) async def _block_hash_and_tx_hashes(self, height): - '''Returns a pair (block_hash, tx_hashes) for the main chain block at + """Returns a pair (block_hash, tx_hashes) for the main chain block at the given height. block_hash is a hexadecimal string, and tx_hashes is an ordered list of hexadecimal strings. - ''' + """ height = non_negative_integer(height) hex_hashes = await self.daemon_request('block_hex_hashes', height, 1) block_hash = hex_hashes[0] @@ -1171,23 +1171,23 @@ class ElectrumX(SessionBase): return block_hash, block['tx'] def _get_merkle_branch(self, tx_hashes, tx_pos): - '''Return a merkle branch to a transaction. + """Return a merkle branch to a transaction. tx_hashes: ordered list of hex strings of tx hashes in a block tx_pos: index of transaction in tx_hashes to create branch for - ''' + """ hashes = [hex_str_to_hash(hash) for hash in tx_hashes] branch, root = self.db.merkle.branch_and_root(hashes, tx_pos) branch = [hash_to_hex_str(hash) for hash in branch] return branch async def transaction_merkle(self, tx_hash, height): - '''Return the markle branch to a confirmed transaction given its hash + """Return the markle branch to a confirmed transaction given its hash and height. tx_hash: the transaction hash as a hexadecimal string height: the height of the block it is in - ''' + """ assert_tx_hash(tx_hash) block_hash, tx_hashes = await self._block_hash_and_tx_hashes(height) try: @@ -1199,9 +1199,9 @@ class ElectrumX(SessionBase): return {"block_height": height, "merkle": branch, "pos": pos} async def transaction_id_from_pos(self, height, tx_pos, merkle=False): - '''Return the txid and optionally a merkle proof, given + """Return the txid and optionally a merkle proof, given a block height and position in the block. - ''' + """ tx_pos = non_negative_integer(tx_pos) if merkle not in (True, False): raise RPCError(BAD_REQUEST, f'"merkle" must be a boolean') @@ -1279,7 +1279,7 @@ class ElectrumX(SessionBase): class LocalRPC(SessionBase): - '''A local TCP RPC server session.''' + """A local TCP RPC server session.""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -1291,7 +1291,7 @@ class LocalRPC(SessionBase): class DashElectrumX(ElectrumX): - '''A TCP server that handles incoming Electrum Dash connections.''' + """A TCP server that handles incoming Electrum Dash connections.""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -1307,7 +1307,7 @@ class DashElectrumX(ElectrumX): }) async def notify(self, touched, height_changed): - '''Notify the client about changes in masternode list.''' + """Notify the client about changes in masternode list.""" await super().notify(touched, height_changed) for mn in self.mns: status = await self.daemon_request('masternode_list', @@ -1317,10 +1317,10 @@ class DashElectrumX(ElectrumX): # Masternode command handlers async def masternode_announce_broadcast(self, signmnb): - '''Pass through the masternode announce message to be broadcast + """Pass through the masternode announce message to be broadcast by the daemon. - signmnb: signed masternode broadcast message.''' + signmnb: signed masternode broadcast message.""" try: return await self.daemon_request('masternode_broadcast', ['relay', signmnb]) @@ -1332,10 +1332,10 @@ class DashElectrumX(ElectrumX): f'rejected.\n\n{message}\n[{signmnb}]') async def masternode_subscribe(self, collateral): - '''Returns the status of masternode. + """Returns the status of masternode. collateral: masternode collateral. - ''' + """ result = await self.daemon_request('masternode_list', ['status', collateral]) if result is not None: @@ -1344,20 +1344,20 @@ class DashElectrumX(ElectrumX): return None async def masternode_list(self, payees): - ''' + """ Returns the list of masternodes. payees: a list of masternode payee addresses. - ''' + """ if not isinstance(payees, list): raise RPCError(BAD_REQUEST, 'expected a list of payees') def get_masternode_payment_queue(mns): - '''Returns the calculated position in the payment queue for all the + """Returns the calculated position in the payment queue for all the valid masterernodes in the given mns list. mns: a list of masternodes information. - ''' + """ now = int(datetime.datetime.utcnow().strftime("%s")) mn_queue = [] @@ -1383,12 +1383,12 @@ class DashElectrumX(ElectrumX): return mn_queue def get_payment_position(payment_queue, address): - ''' + """ Returns the position of the payment list for the given address. payment_queue: position in the payment queue for the masternode. address: masternode payee address. - ''' + """ position = -1 for pos, mn in enumerate(payment_queue, start=1): if mn[2] == address: diff --git a/torba/server/storage.py b/torba/server/storage.py index f4c92f90d..d52d2eba7 100644 --- a/torba/server/storage.py +++ b/torba/server/storage.py @@ -5,7 +5,7 @@ # See the file "LICENCE" for information about the copyright # and warranty status of this software. -'''Backend database abstraction.''' +"""Backend database abstraction.""" import os from functools import partial @@ -14,7 +14,7 @@ from torba.server import util def db_class(name): - '''Returns a DB engine class.''' + """Returns a DB engine class.""" for db_class in util.subclasses(Storage): if db_class.__name__.lower() == name.lower(): db_class.import_module() @@ -23,7 +23,7 @@ def db_class(name): class Storage: - '''Abstract base class of the DB backend abstraction.''' + """Abstract base class of the DB backend abstraction.""" def __init__(self, name, for_sync): self.is_new = not os.path.exists(name) @@ -32,15 +32,15 @@ class Storage: @classmethod def import_module(cls): - '''Import the DB engine module.''' + """Import the DB engine module.""" raise NotImplementedError def open(self, name, create): - '''Open an existing database or create a new one.''' + """Open an existing database or create a new one.""" raise NotImplementedError def close(self): - '''Close an existing database.''' + """Close an existing database.""" raise NotImplementedError def get(self, key): @@ -50,26 +50,26 @@ class Storage: raise NotImplementedError def write_batch(self): - '''Return a context manager that provides `put` and `delete`. + """Return a context manager that provides `put` and `delete`. Changes should only be committed when the context manager closes without an exception. - ''' + """ raise NotImplementedError def iterator(self, prefix=b'', reverse=False): - '''Return an iterator that yields (key, value) pairs from the + """Return an iterator that yields (key, value) pairs from the database sorted by key. If `prefix` is set, only keys starting with `prefix` will be included. If `reverse` is True the items are returned in reverse order. - ''' + """ raise NotImplementedError class LevelDB(Storage): - '''LevelDB database engine.''' + """LevelDB database engine.""" @classmethod def import_module(cls): @@ -90,7 +90,7 @@ class LevelDB(Storage): class RocksDB(Storage): - '''RocksDB database engine.''' + """RocksDB database engine.""" @classmethod def import_module(cls): @@ -122,7 +122,7 @@ class RocksDB(Storage): class RocksDBWriteBatch: - '''A write batch for RocksDB.''' + """A write batch for RocksDB.""" def __init__(self, db): self.batch = RocksDB.module.WriteBatch() @@ -137,7 +137,7 @@ class RocksDBWriteBatch: class RocksDBIterator: - '''An iterator for RocksDB.''' + """An iterator for RocksDB.""" def __init__(self, db, prefix, reverse): self.prefix = prefix diff --git a/torba/server/text.py b/torba/server/text.py index 7677353ca..800ba0be3 100644 --- a/torba/server/text.py +++ b/torba/server/text.py @@ -4,9 +4,9 @@ from torba.server import util def sessions_lines(data): - '''A generator returning lines for a list of sessions. + """A generator returning lines for a list of sessions. - data is the return value of rpc_sessions().''' + data is the return value of rpc_sessions().""" fmt = ('{:<6} {:<5} {:>17} {:>5} {:>5} {:>5} ' '{:>7} {:>7} {:>7} {:>7} {:>7} {:>9} {:>21}') yield fmt.format('ID', 'Flags', 'Client', 'Proto', @@ -26,9 +26,9 @@ def sessions_lines(data): def groups_lines(data): - '''A generator returning lines for a list of groups. + """A generator returning lines for a list of groups. - data is the return value of rpc_groups().''' + data is the return value of rpc_groups().""" fmt = ('{:<6} {:>9} {:>9} {:>6} {:>6} {:>8}' '{:>7} {:>9} {:>7} {:>9}') @@ -49,9 +49,9 @@ def groups_lines(data): def peers_lines(data): - '''A generator returning lines for a list of peers. + """A generator returning lines for a list of peers. - data is the return value of rpc_peers().''' + data is the return value of rpc_peers().""" def time_fmt(t): if not t: return 'Never' diff --git a/torba/server/tx.py b/torba/server/tx.py index a85a2ccdb..212589d54 100644 --- a/torba/server/tx.py +++ b/torba/server/tx.py @@ -25,7 +25,7 @@ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # and warranty status of this software. -'''Transaction-related classes and functions.''' +"""Transaction-related classes and functions.""" from collections import namedtuple @@ -42,7 +42,7 @@ MINUS_1 = 4294967295 class Tx(namedtuple("Tx", "version inputs outputs locktime")): - '''Class representing a transaction.''' + """Class representing a transaction.""" def serialize(self): return b''.join(( @@ -56,7 +56,7 @@ class Tx(namedtuple("Tx", "version inputs outputs locktime")): class TxInput(namedtuple("TxInput", "prev_hash prev_idx script sequence")): - '''Class representing a transaction input.''' + """Class representing a transaction input.""" def __str__(self): script = self.script.hex() prev_hash = hash_to_hex_str(self.prev_hash) @@ -64,7 +64,7 @@ class TxInput(namedtuple("TxInput", "prev_hash prev_idx script sequence")): .format(prev_hash, self.prev_idx, script, self.sequence)) def is_generation(self): - '''Test if an input is generation/coinbase like''' + """Test if an input is generation/coinbase like""" return self.prev_idx == MINUS_1 and self.prev_hash == ZERO def serialize(self): @@ -86,14 +86,14 @@ class TxOutput(namedtuple("TxOutput", "value pk_script")): class Deserializer: - '''Deserializes blocks into transactions. + """Deserializes blocks into transactions. External entry points are read_tx(), read_tx_and_hash(), read_tx_and_vsize() and read_block(). This code is performance sensitive as it is executed 100s of millions of times during sync. - ''' + """ TX_HASH_FN = staticmethod(double_sha256) @@ -104,7 +104,7 @@ class Deserializer: self.cursor = start def read_tx(self): - '''Return a deserialized transaction.''' + """Return a deserialized transaction.""" return Tx( self._read_le_int32(), # version self._read_inputs(), # inputs @@ -113,20 +113,20 @@ class Deserializer: ) def read_tx_and_hash(self): - '''Return a (deserialized TX, tx_hash) pair. + """Return a (deserialized TX, tx_hash) pair. The hash needs to be reversed for human display; for efficiency we process it in the natural serialized order. - ''' + """ start = self.cursor return self.read_tx(), self.TX_HASH_FN(self.binary[start:self.cursor]) def read_tx_and_vsize(self): - '''Return a (deserialized TX, vsize) pair.''' + """Return a (deserialized TX, vsize) pair.""" return self.read_tx(), self.binary_length def read_tx_block(self): - '''Returns a list of (deserialized_tx, tx_hash) pairs.''' + """Returns a list of (deserialized_tx, tx_hash) pairs.""" read = self.read_tx_and_hash # Some coins have excess data beyond the end of the transactions return [read() for _ in range(self._read_varint())] @@ -206,7 +206,7 @@ class Deserializer: class TxSegWit(namedtuple("Tx", "version marker flag inputs outputs " "witness locktime")): - '''Class representing a SegWit transaction.''' + """Class representing a SegWit transaction.""" class DeserializerSegWit(Deserializer): @@ -222,7 +222,7 @@ class DeserializerSegWit(Deserializer): return [read_varbytes() for i in range(self._read_varint())] def _read_tx_parts(self): - '''Return a (deserialized TX, tx_hash, vsize) tuple.''' + """Return a (deserialized TX, tx_hash, vsize) tuple.""" start = self.cursor marker = self.binary[self.cursor + 4] if marker: @@ -269,7 +269,7 @@ class DeserializerAuxPow(Deserializer): VERSION_AUXPOW = (1 << 8) def read_header(self, height, static_header_size): - '''Return the AuxPow block header bytes''' + """Return the AuxPow block header bytes""" start = self.cursor version = self._read_le_uint32() if version & self.VERSION_AUXPOW: @@ -298,7 +298,7 @@ class DeserializerAuxPowSegWit(DeserializerSegWit, DeserializerAuxPow): class DeserializerEquihash(Deserializer): def read_header(self, height, static_header_size): - '''Return the block header bytes''' + """Return the block header bytes""" start = self.cursor # We are going to calculate the block size then read it as bytes self.cursor += static_header_size @@ -314,7 +314,7 @@ class DeserializerEquihashSegWit(DeserializerSegWit, DeserializerEquihash): class TxJoinSplit(namedtuple("Tx", "version inputs outputs locktime")): - '''Class representing a JoinSplit transaction.''' + """Class representing a JoinSplit transaction.""" class DeserializerZcash(DeserializerEquihash): @@ -365,7 +365,7 @@ class DeserializerZcash(DeserializerEquihash): class TxTime(namedtuple("Tx", "version time inputs outputs locktime")): - '''Class representing transaction that has a time field.''' + """Class representing transaction that has a time field.""" class DeserializerTxTime(Deserializer): @@ -406,7 +406,7 @@ class DeserializerTxTimeAuxPow(DeserializerTxTime): return False def read_header(self, height, static_header_size): - '''Return the AuxPow block header bytes''' + """Return the AuxPow block header bytes""" start = self.cursor version = self._read_le_uint32() if version & self.VERSION_AUXPOW: @@ -433,7 +433,7 @@ class DeserializerBitcoinAtom(DeserializerSegWit): FORK_BLOCK_HEIGHT = 505888 def read_header(self, height, static_header_size): - '''Return the block header bytes''' + """Return the block header bytes""" header_len = static_header_size if height >= self.FORK_BLOCK_HEIGHT: header_len += 4 # flags @@ -445,7 +445,7 @@ class DeserializerGroestlcoin(DeserializerSegWit): class TxInputTokenPay(TxInput): - '''Class representing a TokenPay transaction input.''' + """Class representing a TokenPay transaction input.""" OP_ANON_MARKER = 0xb9 # 2byte marker (cpubkey + sigc + sigr) @@ -468,7 +468,7 @@ class TxInputTokenPay(TxInput): class TxInputTokenPayStealth( namedtuple("TxInput", "keyimage ringsize script sequence")): - '''Class representing a TokenPay stealth transaction input.''' + """Class representing a TokenPay stealth transaction input.""" def __str__(self): script = self.script.hex() @@ -514,7 +514,7 @@ class DeserializerTokenPay(DeserializerTxTime): # Decred class TxInputDcr(namedtuple("TxInput", "prev_hash prev_idx tree sequence")): - '''Class representing a Decred transaction input.''' + """Class representing a Decred transaction input.""" def __str__(self): prev_hash = hash_to_hex_str(self.prev_hash) @@ -522,18 +522,18 @@ class TxInputDcr(namedtuple("TxInput", "prev_hash prev_idx tree sequence")): .format(prev_hash, self.prev_idx, self.tree, self.sequence)) def is_generation(self): - '''Test if an input is generation/coinbase like''' + """Test if an input is generation/coinbase like""" return self.prev_idx == MINUS_1 and self.prev_hash == ZERO class TxOutputDcr(namedtuple("TxOutput", "value version pk_script")): - '''Class representing a Decred transaction output.''' + """Class representing a Decred transaction output.""" pass class TxDcr(namedtuple("Tx", "version inputs outputs locktime expiry " "witness")): - '''Class representing a Decred transaction.''' + """Class representing a Decred transaction.""" class DeserializerDecred(Deserializer): @@ -559,14 +559,14 @@ class DeserializerDecred(Deserializer): return tx, vsize def read_tx_block(self): - '''Returns a list of (deserialized_tx, tx_hash) pairs.''' + """Returns a list of (deserialized_tx, tx_hash) pairs.""" read = self.read_tx_and_hash txs = [read() for _ in range(self._read_varint())] stxs = [read() for _ in range(self._read_varint())] return txs + stxs def read_tx_tree(self): - '''Returns a list of deserialized_tx without tx hashes.''' + """Returns a list of deserialized_tx without tx hashes.""" read_tx = self.read_tx return [read_tx() for _ in range(self._read_varint())] diff --git a/torba/server/util.py b/torba/server/util.py index 7f36d8cd7..74dfb9566 100644 --- a/torba/server/util.py +++ b/torba/server/util.py @@ -24,7 +24,7 @@ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # and warranty status of this software. -'''Miscellaneous utility classes and functions.''' +"""Miscellaneous utility classes and functions.""" import array @@ -40,21 +40,21 @@ from struct import pack, Struct class ConnectionLogger(logging.LoggerAdapter): - '''Prepends a connection identifier to a logging message.''' + """Prepends a connection identifier to a logging message.""" def process(self, msg, kwargs): conn_id = self.extra.get('conn_id', 'unknown') return f'[{conn_id}] {msg}', kwargs class CompactFormatter(logging.Formatter): - '''Strips the module from the logger name to leave the class only.''' + """Strips the module from the logger name to leave the class only.""" def format(self, record): record.name = record.name.rpartition('.')[-1] return super().format(record) def make_logger(name, *, handler, level): - '''Return the root ElectrumX logger.''' + """Return the root ElectrumX logger.""" logger = logging.getLogger(name) logger.addHandler(handler) logger.setLevel(logging.INFO) @@ -63,7 +63,7 @@ def make_logger(name, *, handler, level): def class_logger(path, classname): - '''Return a hierarchical logger for a class.''' + """Return a hierarchical logger for a class.""" return logging.getLogger(path).getChild(classname) @@ -83,8 +83,8 @@ class cachedproperty: def formatted_time(t, sep=' '): - '''Return a number of seconds as a string in days, hours, mins and - maybe secs.''' + """Return a number of seconds as a string in days, hours, mins and + maybe secs.""" t = int(t) fmts = (('{:d}d', 86400), ('{:02d}h', 3600), ('{:02d}m', 60)) parts = [] @@ -136,7 +136,7 @@ def deep_getsizeof(obj): def subclasses(base_class, strict=True): - '''Return a list of subclasses of base_class in its module.''' + """Return a list of subclasses of base_class in its module.""" def select(obj): return (inspect.isclass(obj) and issubclass(obj, base_class) and (not strict or obj != base_class)) @@ -146,7 +146,7 @@ def subclasses(base_class, strict=True): def chunks(items, size): - '''Break up items, an iterable, into chunks of length size.''' + """Break up items, an iterable, into chunks of length size.""" for i in range(0, len(items), size): yield items[i: i + size] @@ -159,19 +159,19 @@ def resolve_limit(limit): def bytes_to_int(be_bytes): - '''Interprets a big-endian sequence of bytes as an integer''' + """Interprets a big-endian sequence of bytes as an integer""" return int.from_bytes(be_bytes, 'big') def int_to_bytes(value): - '''Converts an integer to a big-endian sequence of bytes''' + """Converts an integer to a big-endian sequence of bytes""" return value.to_bytes((value.bit_length() + 7) // 8, 'big') def increment_byte_string(bs): - '''Return the lexicographically next byte string of the same length. + """Return the lexicographically next byte string of the same length. - Return None if there is none (when the input is all 0xff bytes).''' + Return None if there is none (when the input is all 0xff bytes).""" for n in range(1, len(bs) + 1): if bs[-n] != 0xff: return bs[:-n] + bytes([bs[-n] + 1]) + bytes(n - 1) @@ -179,7 +179,7 @@ def increment_byte_string(bs): class LogicalFile: - '''A logical binary file split across several separate files on disk.''' + """A logical binary file split across several separate files on disk.""" def __init__(self, prefix, digits, file_size): digit_fmt = '{' + ':0{:d}d'.format(digits) + '}' @@ -187,10 +187,10 @@ class LogicalFile: self.file_size = file_size def read(self, start, size=-1): - '''Read up to size bytes from the virtual file, starting at offset + """Read up to size bytes from the virtual file, starting at offset start, and return them. - If size is -1 all bytes are read.''' + If size is -1 all bytes are read.""" parts = [] while size != 0: try: @@ -207,7 +207,7 @@ class LogicalFile: return b''.join(parts) def write(self, start, b): - '''Write the bytes-like object, b, to the underlying virtual file.''' + """Write the bytes-like object, b, to the underlying virtual file.""" while b: size = min(len(b), self.file_size - (start % self.file_size)) with self.open_file(start, True) as f: @@ -216,10 +216,10 @@ class LogicalFile: start += size def open_file(self, start, create): - '''Open the virtual file and seek to start. Return a file handle. + """Open the virtual file and seek to start. Return a file handle. Raise FileNotFoundError if the file does not exist and create is False. - ''' + """ file_num, offset = divmod(start, self.file_size) filename = self.filename_fmt.format(file_num) f = open_file(filename, create) @@ -228,7 +228,7 @@ class LogicalFile: def open_file(filename, create=False): - '''Open the file name. Return its handle.''' + """Open the file name. Return its handle.""" try: return open(filename, 'rb+') except FileNotFoundError: @@ -238,12 +238,12 @@ def open_file(filename, create=False): def open_truncate(filename): - '''Open the file name. Return its handle.''' + """Open the file name. Return its handle.""" return open(filename, 'wb+') def address_string(address): - '''Return an address as a correctly formatted string.''' + """Return an address as a correctly formatted string.""" fmt = '{}:{:d}' host, port = address try: @@ -273,9 +273,9 @@ def is_valid_hostname(hostname): def protocol_tuple(s): - '''Converts a protocol version number, such as "1.0" to a tuple (1, 0). + """Converts a protocol version number, such as "1.0" to a tuple (1, 0). - If the version number is bad, (0, ) indicating version 0 is returned.''' + If the version number is bad, (0, ) indicating version 0 is returned.""" try: return tuple(int(part) for part in s.split('.')) except Exception: @@ -283,22 +283,22 @@ def protocol_tuple(s): def version_string(ptuple): - '''Convert a version tuple such as (1, 2) to "1.2". - There is always at least one dot, so (1, ) becomes "1.0".''' + """Convert a version tuple such as (1, 2) to "1.2". + There is always at least one dot, so (1, ) becomes "1.0".""" while len(ptuple) < 2: ptuple += (0, ) return '.'.join(str(p) for p in ptuple) def protocol_version(client_req, min_tuple, max_tuple): - '''Given a client's protocol version string, return a pair of + """Given a client's protocol version string, return a pair of protocol tuples: (negotiated version, client min request) If the request is unsupported, the negotiated protocol tuple is None. - ''' + """ if client_req is None: client_min = client_max = min_tuple else: