diff --git a/electrum/channel_db.py b/electrum/channel_db.py index 4331a56e5..e7b6e5f9e 100644 --- a/electrum/channel_db.py +++ b/electrum/channel_db.py @@ -68,7 +68,7 @@ class ChannelInfo(NamedTuple): @staticmethod def from_msg(payload: dict) -> 'ChannelInfo': features = int.from_bytes(payload['features'], 'big') - validate_features(features) + features = validate_features(features) channel_id = payload['short_channel_id'] node_id_1 = payload['node_id_1'] node_id_2 = payload['node_id_2'] @@ -164,7 +164,7 @@ class NodeInfo(NamedTuple): def from_msg(payload) -> Tuple['NodeInfo', Sequence['LNPeerAddr']]: node_id = payload['node_id'] features = int.from_bytes(payload['features'], "big") - validate_features(features) + features = validate_features(features) addresses = NodeInfo.parse_addresses_field(payload['addresses']) peer_addrs = [] for host, port in addresses: diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index 2dd1092cb..d143476da 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -42,7 +42,7 @@ from .lnutil import (Outpoint, LocalConfig, RECEIVED, UpdateAddHtlc, ChannelConf LightningPeerConnectionClosed, HandshakeFailed, RemoteMisbehaving, ShortChannelID, IncompatibleLightningFeatures, derive_payment_secret_from_payment_preimage, - ChannelType, LNProtocolWarning) + ChannelType, LNProtocolWarning, validate_features, IncompatibleOrInsaneFeatures) from .lnutil import FeeUpdate, channel_id_from_funding_tx from .lntransport import LNTransport, LNTransportBase from .lnmsg import encode_msg, decode_msg, UnknownOptionalMsgType, FailedToParseMsg @@ -352,12 +352,12 @@ class Peer(Logger): if self._received_init: self.logger.info("ALREADY INITIALIZED BUT RECEIVED INIT") return - self.their_features = LnFeatures(int.from_bytes(payload['features'], byteorder="big")) - their_globalfeatures = int.from_bytes(payload['globalfeatures'], byteorder="big") - self.their_features |= their_globalfeatures - # check transitive dependencies for received features - if not self.their_features.validate_transitive_dependencies(): - raise GracefulDisconnect("remote did not set all dependencies for the features they sent") + _their_features = int.from_bytes(payload['features'], byteorder="big") + _their_features |= int.from_bytes(payload['globalfeatures'], byteorder="big") + try: + self.their_features = validate_features(_their_features) + except IncompatibleOrInsaneFeatures as e: + raise GracefulDisconnect(f"remote sent insane features: {repr(e)}") # check if features are compatible, and set self.features to what we negotiated try: self.features = ln_compare_features(self.features, self.their_features) diff --git a/electrum/lnutil.py b/electrum/lnutil.py index e348659af..4ab8e3949 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -8,6 +8,8 @@ import json from collections import namedtuple, defaultdict from typing import NamedTuple, List, Tuple, Mapping, Optional, TYPE_CHECKING, Union, Dict, Set, Sequence import re +import sys + import attr from aiorpcx import NetAddress @@ -1211,6 +1213,18 @@ class LnFeatures(IntFlag): r.append(feature_name or f"bit_{flag}") return r + if hasattr(IntFlag, "_numeric_repr_"): # python 3.11+ + # performance improvement (avoid base2<->base10), see #8403 + _numeric_repr_ = hex + + def __repr__(self): + # performance improvement (avoid base2<->base10), see #8403 + return f"<{self._name_}: {hex(self._value_)}>" + + def __str__(self): + # performance improvement (avoid base2<->base10), see #8403 + return hex(self._value_) + class ChannelType(IntFlag): OPTION_LEGACY_CHANNEL = 0 @@ -1332,11 +1346,22 @@ def ln_compare_features(our_features: 'LnFeatures', their_features: int) -> 'LnF return our_features -def validate_features(features: int) -> None: +if hasattr(sys, "get_int_max_str_digits"): + # check that the user or other library has not lowered the limit (from default) + assert sys.get_int_max_str_digits() >= 4300, f"sys.get_int_max_str_digits() too low: {sys.get_int_max_str_digits()}" + + +def validate_features(features: int) -> LnFeatures: """Raises IncompatibleOrInsaneFeatures if - a mandatory feature is listed that we don't recognize, or - the features are inconsistent + For convenience, returns the parsed features. """ + if features.bit_length() > 10_000: + # This is an implementation-specific limit for how high feature bits we allow. + # Needed as LnFeatures subclasses IntFlag, and uses ints internally. + # See https://docs.python.org/3/library/stdtypes.html#integer-string-conversion-length-limitation + raise IncompatibleOrInsaneFeatures(f"features bitvector too large: {features.bit_length()=} > 10_000") features = LnFeatures(features) enabled_features = list_enabled_bits(features) for fbit in enabled_features: @@ -1345,6 +1370,7 @@ def validate_features(features: int) -> None: if not features.validate_transitive_dependencies(): raise IncompatibleOrInsaneFeatures(f"not all transitive dependencies are set. " f"features={features}") + return features def derive_payment_secret_from_payment_preimage(payment_preimage: bytes) -> bytes: