URI: 
       tlnutil.LnFeatures: impl and use "supports" method for feature-bit-tests - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 4aab843f17aec648b9bd150498c898d92e384df7
   DIR parent 0369829e5edb94a85d3046e5288738e5aa2cb5cf
  HTML Author: SomberNight <somber.night@protonmail.com>
       Date:   Mon, 22 Feb 2021 19:53:01 +0100
       
       lnutil.LnFeatures: impl and use "supports" method for feature-bit-tests
       
       Note that for a required feature, BOLT-09 allows setting either:
       - only the REQ bit
       - both the REQ bit and the OPT bit
       
       Hence, when checking if a feature is supported by e.g. an invoice, both
       bits should be checked.
       
       Note that in lnpeer.py, in self.features specifically, REQ implies OPT,
       as it is set by ln_compare_features.
       
       Diffstat:
         M electrum/lnpeer.py                  |      10 +++++-----
         M electrum/lnrouter.py                |       4 ++--
         M electrum/lnutil.py                  |      17 +++++++++++++++++
         M electrum/lnworker.py                |      11 +++++++----
         M electrum/tests/test_lnutil.py       |      66 ++++++++++++++++++++++++++++++-
       
       5 files changed, 96 insertions(+), 12 deletions(-)
       ---
   DIR diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py
       t@@ -76,8 +76,8 @@ class Peer(Logger):
                self.pubkey = pubkey  # remote pubkey
                self.lnworker = lnworker
                self.privkey = self.transport.privkey  # local privkey
       -        self.features = self.lnworker.features
       -        self.their_features = 0
       +        self.features = self.lnworker.features  # type: LnFeatures
       +        self.their_features = LnFeatures(0)  # type: LnFeatures
                self.node_ids = [self.pubkey, privkey_to_pubkey(self.privkey)]
                assert self.node_ids[0] != self.node_ids[1]
                self.network = lnworker.network
       t@@ -491,10 +491,10 @@ class Peer(Logger):
                self.lnworker.peer_closed(self)
        
            def is_static_remotekey(self):
       -        return bool(self.features & LnFeatures.OPTION_STATIC_REMOTEKEY_OPT)
       +        return self.features.supports(LnFeatures.OPTION_STATIC_REMOTEKEY_OPT)
        
            def is_upfront_shutdown_script(self):
       -        return bool(self.features & LnFeatures.OPTION_UPFRONT_SHUTDOWN_SCRIPT_OPT)
       +        return self.features.supports(LnFeatures.OPTION_UPFRONT_SHUTDOWN_SCRIPT_OPT)
        
            def upfront_shutdown_script_from_payload(self, payload, msg_identifier: str) -> Optional[bytes]:
                if msg_identifier not in ['accept', 'open']:
       t@@ -917,7 +917,7 @@ class Peer(Logger):
                oldest_unrevoked_remote_ctn = chan.get_oldest_unrevoked_ctn(REMOTE)
                latest_remote_ctn = chan.get_latest_ctn(REMOTE)
                next_remote_ctn = chan.get_next_ctn(REMOTE)
       -        assert self.features & LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT
       +        assert self.features.supports(LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT)
                # send message
                if chan.is_static_remotekey_enabled():
                    latest_secret, latest_point = chan.get_secret_and_point(LOCAL, 0)
   DIR diff --git a/electrum/lnrouter.py b/electrum/lnrouter.py
       t@@ -96,8 +96,8 @@ class RouteEdge(PathEdge):
                return True
        
            def has_feature_varonion(self) -> bool:
       -        features = self.node_features
       -        return bool(features & LnFeatures.VAR_ONION_REQ or features & LnFeatures.VAR_ONION_OPT)
       +        features = LnFeatures(self.node_features)
       +        return features.supports(LnFeatures.VAR_ONION_OPT)
        
            def is_trampoline(self):
                return False
   DIR diff --git a/electrum/lnutil.py b/electrum/lnutil.py
       t@@ -1013,6 +1013,23 @@ class LnFeatures(IntFlag):
                        features |= (1 << flag)
                return features
        
       +    def supports(self, feature: 'LnFeatures') -> bool:
       +        """Returns whether given feature is enabled.
       +
       +        Helper function that tries to hide the complexity of even/odd bits.
       +        For example, instead of:
       +          bool(myfeatures & LnFeatures.VAR_ONION_OPT or myfeatures & LnFeatures.VAR_ONION_REQ)
       +        you can do:
       +          myfeatures.supports(LnFeatures.VAR_ONION_OPT)
       +        """
       +        enabled_bits = list_enabled_bits(feature)
       +        if len(enabled_bits) != 1:
       +            raise ValueError(f"'feature' cannot be a combination of features: {feature}")
       +        flag = enabled_bits[0]
       +        our_flags = set(list_enabled_bits(self))
       +        return (flag in our_flags
       +                or get_ln_flag_pair_of_bit(flag) in our_flags)
       +
        
        del LNFC  # name is ambiguous without context
        
   DIR diff --git a/electrum/lnworker.py b/electrum/lnworker.py
       t@@ -228,7 +228,7 @@ LNGOSSIP_FEATURES = BASE_FEATURES\
        
        class LNWorker(Logger, NetworkRetryManager[LNPeerAddr]):
        
       -    def __init__(self, xprv, features):
       +    def __init__(self, xprv, features: LnFeatures):
                Logger.__init__(self)
                NetworkRetryManager.__init__(
                    self,
       t@@ -1264,7 +1264,7 @@ class LNWallet(LNWorker):
                if is_hardcoded_trampoline(node_id):
                    return True
                peer = self._peers.get(node_id)
       -        if peer and bool(peer.their_features & LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT):
       +        if peer and peer.their_features.supports(LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT):
                    return True
                return False
        
       t@@ -1279,9 +1279,11 @@ class LNWallet(LNWorker):
                    r_tags, t_tags) -> LNPaymentRoute:
                """ return the route that leads to trampoline, and the trampoline fake edge"""
        
       +        invoice_features = LnFeatures(invoice_features)
       +
                # We do not set trampoline_routing_opt in our invoices, because the spec is not ready
                # Do not use t_tags if the flag is set, because we the format is not decided yet
       -        if invoice_features & LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT:
       +        if invoice_features.supports(LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT):
                    is_legacy = False
                    if len(r_tags) > 0 and len(r_tags[0]) == 1:
                        pubkey, scid, feebase, feerate, cltv = r_tags[0][0]
       t@@ -1390,6 +1392,7 @@ class LNWallet(LNWorker):
        
                We first try to conduct the payment over a single channel. If that fails
                and mpp is supported by the receiver, we will split the payment."""
       +        invoice_features = LnFeatures(invoice_features)
                # try to send over a single channel
                try:
                    routes = [self.create_route_for_payment(
       t@@ -1402,7 +1405,7 @@ class LNWallet(LNWorker):
                        full_path=full_path
                    )]
                except NoPathFound:
       -            if not invoice_features & LnFeatures.BASIC_MPP_OPT:
       +            if not invoice_features.supports(LnFeatures.BASIC_MPP_OPT):
                        raise
                    channels_with_funds = dict([
                        (cid, int(chan.available_to_spend(HTLCOwner.LOCAL)))
   DIR diff --git a/electrum/tests/test_lnutil.py b/electrum/tests/test_lnutil.py
       t@@ -8,7 +8,8 @@ from electrum.lnutil import (RevocationStore, get_per_commitment_secret_from_see
                                     make_htlc_tx_inputs, secret_to_pubkey, derive_blinded_pubkey, derive_privkey,
                                     derive_pubkey, make_htlc_tx, extract_ctn_from_tx, UnableToDeriveSecret,
                                     get_compressed_pubkey_from_bech32, split_host_port, ConnStringFormatError,
       -                             ScriptHtlc, extract_nodeid, calc_fees_for_commitment_tx, UpdateAddHtlc, LnFeatures)
       +                             ScriptHtlc, extract_nodeid, calc_fees_for_commitment_tx, UpdateAddHtlc, LnFeatures,
       +                             ln_compare_features, IncompatibleLightningFeatures)
        from electrum.util import bh2u, bfh, MyEncoder
        from electrum.transaction import Transaction, PartialTransaction
        from electrum.lnworker import LNWallet
       t@@ -807,6 +808,69 @@ class TestLNUtil(ElectrumTestCase):
                features = LnFeatures.BASIC_MPP_OPT | LnFeatures.PAYMENT_SECRET_REQ | LnFeatures.VAR_ONION_REQ
                self.assertEqual(features, features.for_invoice())
        
       +    def test_ln_compare_features(self):
       +        f1 = LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ | LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT
       +        f2 = LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ
       +        self.assertEqual(LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ | LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT,
       +                         ln_compare_features(f1, f2))
       +        self.assertEqual(LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ | LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT,
       +                         ln_compare_features(f2, f1))
       +        # note that the args are not commutative; if we (first arg) REQ a feature, OPT will get auto-set
       +        f1 = LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT
       +        f2 = LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ
       +        self.assertEqual(LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT,
       +                         ln_compare_features(f1, f2))
       +        self.assertEqual(LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ | LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT,
       +                         ln_compare_features(f2, f1))
       +
       +        f1 = LnFeatures(0)
       +        f2 = LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT
       +        self.assertEqual(LnFeatures(0), ln_compare_features(f1, f2))
       +        self.assertEqual(LnFeatures(0), ln_compare_features(f2, f1))
       +
       +        f1 = LnFeatures(0)
       +        f2 = LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ
       +        with self.assertRaises(IncompatibleLightningFeatures):
       +            ln_compare_features(f1, f2)
       +        with self.assertRaises(IncompatibleLightningFeatures):
       +            ln_compare_features(f2, f1)
       +
       +        f1 = LnFeatures.BASIC_MPP_OPT | LnFeatures.PAYMENT_SECRET_REQ | LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT | LnFeatures.VAR_ONION_OPT
       +        f2 = LnFeatures.PAYMENT_SECRET_OPT | LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ | LnFeatures.VAR_ONION_OPT
       +        self.assertEqual(LnFeatures.PAYMENT_SECRET_OPT |
       +                         LnFeatures.PAYMENT_SECRET_REQ |
       +                         LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT |
       +                         LnFeatures.VAR_ONION_OPT,
       +                         ln_compare_features(f1, f2))
       +        self.assertEqual(LnFeatures.PAYMENT_SECRET_OPT |
       +                         LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT |
       +                         LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ |
       +                         LnFeatures.VAR_ONION_OPT,
       +                         ln_compare_features(f2, f1))
       +
       +    def test_ln_features_supports(self):
       +        f_null = LnFeatures(0)
       +        f_opt = LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT
       +        f_req = LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ
       +        f_optreq = LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ | LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT
       +        self.assertFalse(f_null.supports(LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT))
       +        self.assertFalse(f_null.supports(LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ))
       +        self.assertTrue(f_opt.supports(LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT))
       +        self.assertTrue(f_opt.supports(LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ))
       +        self.assertTrue(f_req.supports(LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT))
       +        self.assertTrue(f_req.supports(LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ))
       +        self.assertTrue(f_optreq.supports(LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT))
       +        self.assertTrue(f_optreq.supports(LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ))
       +        with self.assertRaises(ValueError):
       +            f_opt.supports(f_optreq)
       +        with self.assertRaises(ValueError):
       +            f_optreq.supports(f_optreq)
       +        f1 = LnFeatures.BASIC_MPP_OPT | LnFeatures.PAYMENT_SECRET_REQ | LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT | LnFeatures.VAR_ONION_OPT
       +        self.assertTrue(f1.supports(LnFeatures.PAYMENT_SECRET_OPT))
       +        self.assertTrue(f1.supports(LnFeatures.BASIC_MPP_REQ))
       +        self.assertFalse(f1.supports(LnFeatures.OPTION_STATIC_REMOTEKEY_OPT))
       +        self.assertFalse(f1.supports(LnFeatures.OPTION_TRAMPOLINE_ROUTING_REQ))
       +
            def test_lnworker_decode_channel_update_msg(self):
                msg_without_prefix = bytes.fromhex("439b71c8ddeff63004e4ff1f9764a57dcf20232b79d9d669aef0e31c42be8e44208f7d868d0133acb334047f30e9399dece226ccd98e5df5330adf7f356290516fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d619000000000008762700054a00005ef2cf9c0101009000000000000003e80000000000000001000000002367b880")
                # good messages