electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit 4aab843f17aec648b9bd150498c898d92e384df7
parent 0369829e5edb94a85d3046e5288738e5aa2cb5cf
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:
Melectrum/lnpeer.py | 10+++++-----
Melectrum/lnrouter.py | 4++--
Melectrum/lnutil.py | 17+++++++++++++++++
Melectrum/lnworker.py | 11+++++++----
Melectrum/tests/test_lnutil.py | 66+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
5 files changed, 96 insertions(+), 12 deletions(-)

diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py @@ -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 @@ -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']: @@ -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) diff --git a/electrum/lnrouter.py b/electrum/lnrouter.py @@ -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 diff --git a/electrum/lnutil.py b/electrum/lnutil.py @@ -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 diff --git a/electrum/lnworker.py b/electrum/lnworker.py @@ -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, @@ -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 @@ -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] @@ -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( @@ -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))) diff --git a/electrum/tests/test_lnutil.py b/electrum/tests/test_lnutil.py @@ -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 @@ -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