forked from LBRYCommunity/lbry-sdk
PEP-257 docstrings
Signed-off-by: binaryflesh <logan.campos123@gmail.com>
This commit is contained in:
parent
f0c2d16749
commit
6788e09ae9
22 changed files with 644 additions and 644 deletions
|
@ -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):
|
||||
|
|
|
@ -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()]
|
||||
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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('<H', tx_idx)
|
||||
cache_value = self.utxo_cache.pop(tx_hash + idx_packed, None)
|
||||
|
@ -599,7 +599,7 @@ class BlockProcessor:
|
|||
.format(hash_to_hex_str(tx_hash), tx_idx))
|
||||
|
||||
async def _process_prefetched_blocks(self):
|
||||
'''Loop forever processing blocks as they arrive.'''
|
||||
"""Loop forever processing blocks as they arrive."""
|
||||
while True:
|
||||
if self.height == self.daemon.cached_height():
|
||||
if not self._caught_up_event.is_set():
|
||||
|
@ -635,7 +635,7 @@ class BlockProcessor:
|
|||
# --- External API
|
||||
|
||||
async def fetch_and_process_blocks(self, caught_up_event):
|
||||
'''Fetch, process and index blocks from the daemon.
|
||||
"""Fetch, process and index blocks from the daemon.
|
||||
|
||||
Sets caught_up_event when first caught up. Flushes to disk
|
||||
and shuts down cleanly if cancelled.
|
||||
|
@ -645,7 +645,7 @@ class BlockProcessor:
|
|||
processed but not written to disk, it should write those to
|
||||
disk before exiting, as otherwise a significant amount of work
|
||||
could be lost.
|
||||
'''
|
||||
"""
|
||||
self._caught_up_event = caught_up_event
|
||||
await self._first_open_dbs()
|
||||
try:
|
||||
|
@ -660,10 +660,10 @@ class BlockProcessor:
|
|||
self.db.close()
|
||||
|
||||
def force_chain_reorg(self, count):
|
||||
'''Force a reorg of the given number of blocks.
|
||||
"""Force a reorg of the given number of blocks.
|
||||
|
||||
Returns True if a reorg is queued, false if not caught up.
|
||||
'''
|
||||
"""
|
||||
if self._caught_up_event.is_set():
|
||||
self.reorg_count = count
|
||||
self.blocks_event.set()
|
||||
|
|
|
@ -24,11 +24,11 @@
|
|||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
'''Module providing coin abstraction.
|
||||
"""Module providing coin abstraction.
|
||||
|
||||
Anything coin-specific should go in this file and be subclassed where
|
||||
necessary for appropriate handling.
|
||||
'''
|
||||
"""
|
||||
|
||||
from collections import namedtuple
|
||||
import re
|
||||
|
@ -55,11 +55,11 @@ OP_RETURN = OpCodes.OP_RETURN
|
|||
|
||||
|
||||
class CoinError(Exception):
|
||||
'''Exception raised for coin-related errors.'''
|
||||
"""Exception raised for coin-related errors."""
|
||||
|
||||
|
||||
class Coin:
|
||||
'''Base class of coin hierarchy.'''
|
||||
"""Base class of coin hierarchy."""
|
||||
|
||||
REORG_LIMIT = 200
|
||||
# Not sure if these are coin-specific
|
||||
|
@ -88,9 +88,9 @@ class Coin:
|
|||
|
||||
@classmethod
|
||||
def lookup_coin_class(cls, name, net):
|
||||
'''Return a coin class given name and network.
|
||||
"""Return a coin class given name and network.
|
||||
|
||||
Raise an exception if unrecognised.'''
|
||||
Raise an exception if unrecognised."""
|
||||
req_attrs = ['TX_COUNT', 'TX_COUNT_HEIGHT', 'TX_PER_BLOCK']
|
||||
for coin in util.subclasses(Coin):
|
||||
if (coin.NAME.lower() == name.lower() and
|
||||
|
@ -120,10 +120,10 @@ class Coin:
|
|||
|
||||
@classmethod
|
||||
def genesis_block(cls, block):
|
||||
'''Check the Genesis block is the right one for this coin.
|
||||
"""Check the Genesis block is the right one for this coin.
|
||||
|
||||
Return the block less its unspendable coinbase.
|
||||
'''
|
||||
"""
|
||||
header = cls.block_header(block, 0)
|
||||
header_hex_hash = hash_to_hex_str(cls.header_hash(header))
|
||||
if header_hex_hash != cls.GENESIS_HASH:
|
||||
|
@ -134,16 +134,16 @@ class Coin:
|
|||
|
||||
@classmethod
|
||||
def hashX_from_script(cls, script):
|
||||
'''Returns a hashX from a script, or None if the script is provably
|
||||
"""Returns a hashX from a script, or None if the script is provably
|
||||
unspendable so the output can be dropped.
|
||||
'''
|
||||
"""
|
||||
if script and script[0] == OP_RETURN:
|
||||
return None
|
||||
return sha256(script).digest()[:HASHX_LEN]
|
||||
|
||||
@staticmethod
|
||||
def lookup_xverbytes(verbytes):
|
||||
'''Return a (is_xpub, coin_class) pair given xpub/xprv verbytes.'''
|
||||
"""Return a (is_xpub, coin_class) pair given xpub/xprv verbytes."""
|
||||
# Order means BTC testnet will override NMC testnet
|
||||
for coin in util.subclasses(Coin):
|
||||
if verbytes == coin.XPUB_VERBYTES:
|
||||
|
@ -154,23 +154,23 @@ class Coin:
|
|||
|
||||
@classmethod
|
||||
def address_to_hashX(cls, address):
|
||||
'''Return a hashX given a coin address.'''
|
||||
"""Return a hashX given a coin address."""
|
||||
return cls.hashX_from_script(cls.pay_to_address_script(address))
|
||||
|
||||
@classmethod
|
||||
def P2PKH_address_from_hash160(cls, hash160):
|
||||
'''Return a P2PKH address given a public key.'''
|
||||
"""Return a P2PKH address given a public key."""
|
||||
assert len(hash160) == 20
|
||||
return cls.ENCODE_CHECK(cls.P2PKH_VERBYTE + hash160)
|
||||
|
||||
@classmethod
|
||||
def P2PKH_address_from_pubkey(cls, pubkey):
|
||||
'''Return a coin address given a public key.'''
|
||||
"""Return a coin address given a public key."""
|
||||
return cls.P2PKH_address_from_hash160(hash160(pubkey))
|
||||
|
||||
@classmethod
|
||||
def P2SH_address_from_hash160(cls, hash160):
|
||||
'''Return a coin address given a hash160.'''
|
||||
"""Return a coin address given a hash160."""
|
||||
assert len(hash160) == 20
|
||||
return cls.ENCODE_CHECK(cls.P2SH_VERBYTES[0] + hash160)
|
||||
|
||||
|
@ -184,10 +184,10 @@ class Coin:
|
|||
|
||||
@classmethod
|
||||
def pay_to_address_script(cls, address):
|
||||
'''Return a pubkey script that pays to a pubkey hash.
|
||||
"""Return a pubkey script that pays to a pubkey hash.
|
||||
|
||||
Pass the address (either P2PKH or P2SH) in base58 form.
|
||||
'''
|
||||
"""
|
||||
raw = cls.DECODE_CHECK(address)
|
||||
|
||||
# Require version byte(s) plus hash160.
|
||||
|
@ -205,7 +205,7 @@ class Coin:
|
|||
|
||||
@classmethod
|
||||
def privkey_WIF(cls, privkey_bytes, compressed):
|
||||
'''Return the private key encoded in Wallet Import Format.'''
|
||||
"""Return the private key encoded in Wallet Import Format."""
|
||||
payload = bytearray(cls.WIF_BYTE) + privkey_bytes
|
||||
if compressed:
|
||||
payload.append(0x01)
|
||||
|
@ -213,48 +213,48 @@ class Coin:
|
|||
|
||||
@classmethod
|
||||
def header_hash(cls, header):
|
||||
'''Given a header return hash'''
|
||||
"""Given a header return hash"""
|
||||
return double_sha256(header)
|
||||
|
||||
@classmethod
|
||||
def header_prevhash(cls, header):
|
||||
'''Given a header return previous hash'''
|
||||
"""Given a header return previous hash"""
|
||||
return header[4:36]
|
||||
|
||||
@classmethod
|
||||
def static_header_offset(cls, height):
|
||||
'''Given a header height return its offset in the headers file.
|
||||
"""Given a header height return its offset in the headers file.
|
||||
|
||||
If header sizes change at some point, this is the only code
|
||||
that needs updating.'''
|
||||
that needs updating."""
|
||||
assert cls.STATIC_BLOCK_HEADERS
|
||||
return height * cls.BASIC_HEADER_SIZE
|
||||
|
||||
@classmethod
|
||||
def static_header_len(cls, height):
|
||||
'''Given a header height return its length.'''
|
||||
"""Given a header height return its length."""
|
||||
return (cls.static_header_offset(height + 1)
|
||||
- cls.static_header_offset(height))
|
||||
|
||||
@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."""
|
||||
return block[:cls.static_header_len(height)]
|
||||
|
||||
@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."""
|
||||
header = cls.block_header(raw_block, height)
|
||||
txs = cls.DESERIALIZER(raw_block, start=len(header)).read_tx_block()
|
||||
return Block(raw_block, header, txs)
|
||||
|
||||
@classmethod
|
||||
def decimal_value(cls, value):
|
||||
'''Return the number of standard coin units as a Decimal given a
|
||||
"""Return the number of standard coin units as a Decimal given a
|
||||
quantity of smallest units.
|
||||
|
||||
For example 1 BTC is returned for 100 million satoshis.
|
||||
'''
|
||||
"""
|
||||
return Decimal(value) / cls.VALUE_PER_COIN
|
||||
|
||||
@classmethod
|
||||
|
@ -274,12 +274,12 @@ class AuxPowMixin:
|
|||
|
||||
@classmethod
|
||||
def header_hash(cls, header):
|
||||
'''Given a header return hash'''
|
||||
"""Given a header return hash"""
|
||||
return double_sha256(header[:cls.BASIC_HEADER_SIZE])
|
||||
|
||||
@classmethod
|
||||
def block_header(cls, block, height):
|
||||
'''Return the AuxPow block header bytes'''
|
||||
"""Return the AuxPow block header bytes"""
|
||||
deserializer = cls.DESERIALIZER(block)
|
||||
return deserializer.read_header(height, cls.BASIC_HEADER_SIZE)
|
||||
|
||||
|
@ -306,7 +306,7 @@ class EquihashMixin:
|
|||
|
||||
@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)
|
||||
|
||||
|
@ -318,7 +318,7 @@ class ScryptMixin:
|
|||
|
||||
@classmethod
|
||||
def header_hash(cls, header):
|
||||
'''Given a header return the hash.'''
|
||||
"""Given a header return the hash."""
|
||||
if cls.HEADER_HASH is None:
|
||||
import scrypt
|
||||
cls.HEADER_HASH = lambda x: scrypt.hash(x, x, 1024, 1, 1, 32)
|
||||
|
@ -432,7 +432,7 @@ class BitcoinGold(EquihashMixin, BitcoinMixin, Coin):
|
|||
|
||||
@classmethod
|
||||
def header_hash(cls, header):
|
||||
'''Given a header return hash'''
|
||||
"""Given a header return hash"""
|
||||
height, = util.unpack_le_uint32_from(header, 68)
|
||||
if height >= 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)
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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('<H', tx_idx)
|
||||
|
||||
|
|
|
@ -5,10 +5,10 @@
|
|||
# See the file "LICENCE" for information about the copyright
|
||||
# and warranty status of this software.
|
||||
|
||||
'''An enum-like type with reverse lookup.
|
||||
"""An enum-like type with reverse lookup.
|
||||
|
||||
Source: Python Cookbook, http://code.activestate.com/recipes/67107/
|
||||
'''
|
||||
"""
|
||||
|
||||
|
||||
class EnumError(Exception):
|
||||
|
|
|
@ -139,13 +139,13 @@ class Env:
|
|||
raise self.Error('unknown event loop policy "{}"'.format(policy))
|
||||
|
||||
def cs_host(self, *, for_rpc):
|
||||
'''Returns the 'host' argument to pass to asyncio's create_server
|
||||
"""Returns the 'host' argument to pass to asyncio's create_server
|
||||
call. The result can be a single host name string, a list of
|
||||
host name strings, or an empty string to bind to all interfaces.
|
||||
|
||||
If rpc is True the host to use for the RPC server is returned.
|
||||
Otherwise the host to use for SSL/TCP servers is returned.
|
||||
'''
|
||||
"""
|
||||
host = self.rpc_host if for_rpc else self.host
|
||||
result = [part.strip() for part in host.split(',')]
|
||||
if len(result) == 1:
|
||||
|
@ -161,9 +161,9 @@ class Env:
|
|||
return result
|
||||
|
||||
def sane_max_sessions(self):
|
||||
'''Return the maximum number of sessions to permit. Normally this
|
||||
"""Return the maximum number of sessions to permit. Normally this
|
||||
is MAX_SESSIONS. However, to prevent open file exhaustion, ajdust
|
||||
downwards if running with a small open file rlimit.'''
|
||||
downwards if running with a small open file rlimit."""
|
||||
env_value = self.integer('MAX_SESSIONS', 1000)
|
||||
nofile_limit = resource.getrlimit(resource.RLIMIT_NOFILE)[0]
|
||||
# We give the DB 250 files; allow ElectrumX 100 for itself
|
||||
|
@ -209,8 +209,8 @@ class Env:
|
|||
.format(host))
|
||||
|
||||
def port(port_kind):
|
||||
'''Returns the clearnet identity port, if any and not zero,
|
||||
otherwise the listening port.'''
|
||||
"""Returns the clearnet identity port, if any and not zero,
|
||||
otherwise the listening port."""
|
||||
result = 0
|
||||
if clearnet:
|
||||
result = getattr(clearnet, port_kind)
|
||||
|
|
|
@ -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.
|
||||
|
||||
'''Cryptograph hash functions and related classes.'''
|
||||
"""Cryptograph hash functions and related classes."""
|
||||
|
||||
|
||||
import hashlib
|
||||
|
@ -39,53 +39,53 @@ HASHX_LEN = 11
|
|||
|
||||
|
||||
def sha256(x):
|
||||
'''Simple wrapper of hashlib sha256.'''
|
||||
"""Simple wrapper of hashlib sha256."""
|
||||
return _sha256(x).digest()
|
||||
|
||||
|
||||
def ripemd160(x):
|
||||
'''Simple wrapper of hashlib ripemd160.'''
|
||||
"""Simple wrapper of hashlib ripemd160."""
|
||||
h = _new_hash('ripemd160')
|
||||
h.update(x)
|
||||
return h.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))
|
||||
|
||||
|
||||
def hmac_sha512(key, msg):
|
||||
'''Use SHA-512 to provide an HMAC.'''
|
||||
"""Use SHA-512 to provide an HMAC."""
|
||||
return _new_hmac(key, msg, _sha512).digest()
|
||||
|
||||
|
||||
def hash160(x):
|
||||
'''RIPEMD-160 of SHA-256.
|
||||
"""RIPEMD-160 of SHA-256.
|
||||
|
||||
Used to make bitcoin addresses from pubkeys.'''
|
||||
Used to make bitcoin addresses from pubkeys."""
|
||||
return ripemd160(sha256(x))
|
||||
|
||||
|
||||
def hash_to_hex_str(x):
|
||||
'''Convert a big-endian binary hash to displayed hex string.
|
||||
"""Convert a big-endian binary hash to displayed hex string.
|
||||
|
||||
Display form of a binary hash is reversed and converted to hex.
|
||||
'''
|
||||
"""
|
||||
return bytes(reversed(x)).hex()
|
||||
|
||||
|
||||
def hex_str_to_hash(x):
|
||||
'''Convert a displayed hex string to a binary hash.'''
|
||||
"""Convert a displayed hex string to a binary hash."""
|
||||
return bytes(reversed(hex_to_bytes(x)))
|
||||
|
||||
|
||||
class Base58Error(Exception):
|
||||
'''Exception used for Base58 errors.'''
|
||||
"""Exception used for Base58 errors."""
|
||||
|
||||
|
||||
class Base58:
|
||||
'''Class providing base 58 functionality.'''
|
||||
"""Class providing base 58 functionality."""
|
||||
|
||||
chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
|
||||
assert len(chars) == 58
|
||||
|
@ -143,8 +143,8 @@ class Base58:
|
|||
|
||||
@staticmethod
|
||||
def decode_check(txt, *, hash_fn=double_sha256):
|
||||
'''Decodes a Base58Check-encoded string to a payload. The version
|
||||
prefixes it.'''
|
||||
"""Decodes a Base58Check-encoded string to a payload. The version
|
||||
prefixes it."""
|
||||
be_bytes = Base58.decode(txt)
|
||||
result, check = be_bytes[:-4], be_bytes[-4:]
|
||||
if check != hash_fn(result)[:4]:
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
# See the file "LICENCE" for information about the copyright
|
||||
# and warranty status of this software.
|
||||
|
||||
'''History by script hash (address).'''
|
||||
"""History by script hash (address)."""
|
||||
|
||||
import array
|
||||
import ast
|
||||
|
@ -96,7 +96,7 @@ class History:
|
|||
self.logger.info('deleted excess history entries')
|
||||
|
||||
def write_state(self, batch):
|
||||
'''Write state to the history DB.'''
|
||||
"""Write state to the history DB."""
|
||||
state = {
|
||||
'flush_count': self.flush_count,
|
||||
'comp_flush_count': self.comp_flush_count,
|
||||
|
@ -174,10 +174,10 @@ class History:
|
|||
self.logger.info(f'backing up removed {nremoves:,d} history entries')
|
||||
|
||||
def get_txnums(self, hashX, limit=1000):
|
||||
'''Generator that returns an unpruned, sorted list of tx_nums in the
|
||||
"""Generator that returns an unpruned, sorted list of tx_nums in the
|
||||
history of a hashX. Includes both spending and receiving
|
||||
transactions. By default yields at most 1000 entries. Set
|
||||
limit to None to get them all. '''
|
||||
limit to None to get them all. """
|
||||
limit = util.resolve_limit(limit)
|
||||
for key, hist in self.db.iterator(prefix=hashX):
|
||||
a = array.array('I')
|
||||
|
@ -208,7 +208,7 @@ class History:
|
|||
# flush_count is reset to comp_flush_count, and comp_flush_count to -1
|
||||
|
||||
def _flush_compaction(self, cursor, write_items, keys_to_delete):
|
||||
'''Flush a single compaction pass as a batch.'''
|
||||
"""Flush a single compaction pass as a batch."""
|
||||
# Update compaction state
|
||||
if cursor == 65536:
|
||||
self.flush_count = self.comp_flush_count
|
||||
|
@ -228,8 +228,8 @@ class History:
|
|||
|
||||
def _compact_hashX(self, hashX, hist_map, hist_list,
|
||||
write_items, keys_to_delete):
|
||||
'''Compres history for a hashX. hist_list is an ordered list of
|
||||
the histories to be compressed.'''
|
||||
"""Compres history for a hashX. hist_list is an ordered list of
|
||||
the histories to be compressed."""
|
||||
# History entries (tx numbers) are 4 bytes each. Distribute
|
||||
# over rows of up to 50KB in size. A fixed row size means
|
||||
# future compactions will not need to update the first N - 1
|
||||
|
@ -263,8 +263,8 @@ class History:
|
|||
return write_size
|
||||
|
||||
def _compact_prefix(self, prefix, write_items, keys_to_delete):
|
||||
'''Compact all history entries for hashXs beginning with the
|
||||
given prefix. Update keys_to_delete and write.'''
|
||||
"""Compact all history entries for hashXs beginning with the
|
||||
given prefix. Update keys_to_delete and write."""
|
||||
prior_hashX = None
|
||||
hist_map = {}
|
||||
hist_list = []
|
||||
|
@ -292,9 +292,9 @@ class History:
|
|||
return write_size
|
||||
|
||||
def _compact_history(self, limit):
|
||||
'''Inner loop of history compaction. Loops until limit bytes have
|
||||
"""Inner loop of history compaction. Loops until limit bytes have
|
||||
been processed.
|
||||
'''
|
||||
"""
|
||||
keys_to_delete = set()
|
||||
write_items = [] # A list of (key, value) pairs
|
||||
write_size = 0
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
# See the file "LICENCE" for information about the copyright
|
||||
# and warranty status of this software.
|
||||
|
||||
'''Mempool handling.'''
|
||||
"""Mempool handling."""
|
||||
|
||||
import asyncio
|
||||
import itertools
|
||||
|
@ -39,48 +39,48 @@ class MemPoolTxSummary:
|
|||
|
||||
|
||||
class MemPoolAPI(ABC):
|
||||
'''A concrete instance of this class is passed to the MemPool object
|
||||
and used by it to query DB and blockchain state.'''
|
||||
"""A concrete instance of this class is passed to the MemPool object
|
||||
and used by it to query DB and blockchain state."""
|
||||
|
||||
@abstractmethod
|
||||
async def height(self):
|
||||
'''Query bitcoind for its height.'''
|
||||
"""Query bitcoind for its height."""
|
||||
|
||||
@abstractmethod
|
||||
def cached_height(self):
|
||||
'''Return the height of bitcoind the last time it was queried,
|
||||
"""Return the height of bitcoind the last time it was queried,
|
||||
for any reason, without actually querying it.
|
||||
'''
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
async def mempool_hashes(self):
|
||||
'''Query bitcoind for the hashes of all transactions in its
|
||||
mempool, returned as a list.'''
|
||||
"""Query bitcoind for the hashes of all transactions in its
|
||||
mempool, returned as a list."""
|
||||
|
||||
@abstractmethod
|
||||
async def raw_transactions(self, hex_hashes):
|
||||
'''Query bitcoind for the serialized raw transactions with the given
|
||||
"""Query bitcoind for the serialized raw transactions with the given
|
||||
hashes. Missing transactions are returned as None.
|
||||
|
||||
hex_hashes is an iterable of hexadecimal hash strings.'''
|
||||
hex_hashes is an iterable of hexadecimal hash strings."""
|
||||
|
||||
@abstractmethod
|
||||
async def lookup_utxos(self, prevouts):
|
||||
'''Return a list of (hashX, value) pairs each prevout if unspent,
|
||||
"""Return a list of (hashX, value) pairs each prevout if unspent,
|
||||
otherwise return None if spent or not found.
|
||||
|
||||
prevouts - an iterable of (hash, index) pairs
|
||||
'''
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
async def on_mempool(self, touched, height):
|
||||
'''Called each time the mempool is synchronized. touched is a set of
|
||||
"""Called each time the mempool is synchronized. touched is a set of
|
||||
hashXs touched since the previous call. height is the
|
||||
daemon's height at the time the mempool was obtained.'''
|
||||
daemon's height at the time the mempool was obtained."""
|
||||
|
||||
|
||||
class MemPool:
|
||||
'''Representation of the daemon's mempool.
|
||||
"""Representation of the daemon's mempool.
|
||||
|
||||
coin - a coin class from coins.py
|
||||
api - an object implementing MemPoolAPI
|
||||
|
@ -91,7 +91,7 @@ class MemPool:
|
|||
|
||||
tx: tx_hash -> 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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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 = {}
|
||||
|
|
|
@ -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']
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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())]
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue