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.
This commit is contained in:
@@ -76,8 +76,8 @@ class Peer(Logger):
|
|||||||
self.pubkey = pubkey # remote pubkey
|
self.pubkey = pubkey # remote pubkey
|
||||||
self.lnworker = lnworker
|
self.lnworker = lnworker
|
||||||
self.privkey = self.transport.privkey # local privkey
|
self.privkey = self.transport.privkey # local privkey
|
||||||
self.features = self.lnworker.features
|
self.features = self.lnworker.features # type: LnFeatures
|
||||||
self.their_features = 0
|
self.their_features = LnFeatures(0) # type: LnFeatures
|
||||||
self.node_ids = [self.pubkey, privkey_to_pubkey(self.privkey)]
|
self.node_ids = [self.pubkey, privkey_to_pubkey(self.privkey)]
|
||||||
assert self.node_ids[0] != self.node_ids[1]
|
assert self.node_ids[0] != self.node_ids[1]
|
||||||
self.network = lnworker.network
|
self.network = lnworker.network
|
||||||
@@ -491,10 +491,10 @@ class Peer(Logger):
|
|||||||
self.lnworker.peer_closed(self)
|
self.lnworker.peer_closed(self)
|
||||||
|
|
||||||
def is_static_remotekey(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):
|
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]:
|
def upfront_shutdown_script_from_payload(self, payload, msg_identifier: str) -> Optional[bytes]:
|
||||||
if msg_identifier not in ['accept', 'open']:
|
if msg_identifier not in ['accept', 'open']:
|
||||||
@@ -917,7 +917,7 @@ class Peer(Logger):
|
|||||||
oldest_unrevoked_remote_ctn = chan.get_oldest_unrevoked_ctn(REMOTE)
|
oldest_unrevoked_remote_ctn = chan.get_oldest_unrevoked_ctn(REMOTE)
|
||||||
latest_remote_ctn = chan.get_latest_ctn(REMOTE)
|
latest_remote_ctn = chan.get_latest_ctn(REMOTE)
|
||||||
next_remote_ctn = chan.get_next_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
|
# send message
|
||||||
if chan.is_static_remotekey_enabled():
|
if chan.is_static_remotekey_enabled():
|
||||||
latest_secret, latest_point = chan.get_secret_and_point(LOCAL, 0)
|
latest_secret, latest_point = chan.get_secret_and_point(LOCAL, 0)
|
||||||
|
|||||||
@@ -96,8 +96,8 @@ class RouteEdge(PathEdge):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def has_feature_varonion(self) -> bool:
|
def has_feature_varonion(self) -> bool:
|
||||||
features = self.node_features
|
features = LnFeatures(self.node_features)
|
||||||
return bool(features & LnFeatures.VAR_ONION_REQ or features & LnFeatures.VAR_ONION_OPT)
|
return features.supports(LnFeatures.VAR_ONION_OPT)
|
||||||
|
|
||||||
def is_trampoline(self):
|
def is_trampoline(self):
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -1013,6 +1013,23 @@ class LnFeatures(IntFlag):
|
|||||||
features |= (1 << flag)
|
features |= (1 << flag)
|
||||||
return features
|
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
|
del LNFC # name is ambiguous without context
|
||||||
|
|
||||||
|
|||||||
@@ -228,7 +228,7 @@ LNGOSSIP_FEATURES = BASE_FEATURES\
|
|||||||
|
|
||||||
class LNWorker(Logger, NetworkRetryManager[LNPeerAddr]):
|
class LNWorker(Logger, NetworkRetryManager[LNPeerAddr]):
|
||||||
|
|
||||||
def __init__(self, xprv, features):
|
def __init__(self, xprv, features: LnFeatures):
|
||||||
Logger.__init__(self)
|
Logger.__init__(self)
|
||||||
NetworkRetryManager.__init__(
|
NetworkRetryManager.__init__(
|
||||||
self,
|
self,
|
||||||
@@ -1264,7 +1264,7 @@ class LNWallet(LNWorker):
|
|||||||
if is_hardcoded_trampoline(node_id):
|
if is_hardcoded_trampoline(node_id):
|
||||||
return True
|
return True
|
||||||
peer = self._peers.get(node_id)
|
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 True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -1279,9 +1279,11 @@ class LNWallet(LNWorker):
|
|||||||
r_tags, t_tags) -> LNPaymentRoute:
|
r_tags, t_tags) -> LNPaymentRoute:
|
||||||
""" return the route that leads to trampoline, and the trampoline fake edge"""
|
""" 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
|
# 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
|
# 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
|
is_legacy = False
|
||||||
if len(r_tags) > 0 and len(r_tags[0]) == 1:
|
if len(r_tags) > 0 and len(r_tags[0]) == 1:
|
||||||
pubkey, scid, feebase, feerate, cltv = r_tags[0][0]
|
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
|
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."""
|
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 to send over a single channel
|
||||||
try:
|
try:
|
||||||
routes = [self.create_route_for_payment(
|
routes = [self.create_route_for_payment(
|
||||||
@@ -1402,7 +1405,7 @@ class LNWallet(LNWorker):
|
|||||||
full_path=full_path
|
full_path=full_path
|
||||||
)]
|
)]
|
||||||
except NoPathFound:
|
except NoPathFound:
|
||||||
if not invoice_features & LnFeatures.BASIC_MPP_OPT:
|
if not invoice_features.supports(LnFeatures.BASIC_MPP_OPT):
|
||||||
raise
|
raise
|
||||||
channels_with_funds = dict([
|
channels_with_funds = dict([
|
||||||
(cid, int(chan.available_to_spend(HTLCOwner.LOCAL)))
|
(cid, int(chan.available_to_spend(HTLCOwner.LOCAL)))
|
||||||
|
|||||||
@@ -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,
|
make_htlc_tx_inputs, secret_to_pubkey, derive_blinded_pubkey, derive_privkey,
|
||||||
derive_pubkey, make_htlc_tx, extract_ctn_from_tx, UnableToDeriveSecret,
|
derive_pubkey, make_htlc_tx, extract_ctn_from_tx, UnableToDeriveSecret,
|
||||||
get_compressed_pubkey_from_bech32, split_host_port, ConnStringFormatError,
|
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.util import bh2u, bfh, MyEncoder
|
||||||
from electrum.transaction import Transaction, PartialTransaction
|
from electrum.transaction import Transaction, PartialTransaction
|
||||||
from electrum.lnworker import LNWallet
|
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
|
features = LnFeatures.BASIC_MPP_OPT | LnFeatures.PAYMENT_SECRET_REQ | LnFeatures.VAR_ONION_REQ
|
||||||
self.assertEqual(features, features.for_invoice())
|
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):
|
def test_lnworker_decode_channel_update_msg(self):
|
||||||
msg_without_prefix = bytes.fromhex("439b71c8ddeff63004e4ff1f9764a57dcf20232b79d9d669aef0e31c42be8e44208f7d868d0133acb334047f30e9399dece226ccd98e5df5330adf7f356290516fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d619000000000008762700054a00005ef2cf9c0101009000000000000003e80000000000000001000000002367b880")
|
msg_without_prefix = bytes.fromhex("439b71c8ddeff63004e4ff1f9764a57dcf20232b79d9d669aef0e31c42be8e44208f7d868d0133acb334047f30e9399dece226ccd98e5df5330adf7f356290516fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d619000000000008762700054a00005ef2cf9c0101009000000000000003e80000000000000001000000002367b880")
|
||||||
# good messages
|
# good messages
|
||||||
|
|||||||
Reference in New Issue
Block a user