diff --git a/electrum/lnutil.py b/electrum/lnutil.py index c22e3815b..014a37a66 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -8,7 +8,6 @@ import json from collections import namedtuple, defaultdict from typing import NamedTuple, List, Tuple, Mapping, Optional, TYPE_CHECKING, Union, Dict, Set, Sequence import re -import time import attr from aiorpcx import NetAddress @@ -1029,6 +1028,12 @@ class LnFeatures(IntFlag): _ln_feature_contexts[OPTION_SHUTDOWN_ANYSEGWIT_REQ] = (LNFC.INIT | LNFC.NODE_ANN) _ln_feature_contexts[OPTION_SHUTDOWN_ANYSEGWIT_OPT] = (LNFC.INIT | LNFC.NODE_ANN) + OPTION_CHANNEL_TYPE_REQ = 1 << 44 + OPTION_CHANNEL_TYPE_OPT = 1 << 45 + + _ln_feature_contexts[OPTION_CHANNEL_TYPE_REQ] = (LNFC.INIT | LNFC.NODE_ANN) + _ln_feature_contexts[OPTION_CHANNEL_TYPE_OPT] = (LNFC.INIT | LNFC.NODE_ANN) + # temporary OPTION_TRAMPOLINE_ROUTING_REQ_ECLAIR = 1 << 50 OPTION_TRAMPOLINE_ROUTING_OPT_ECLAIR = 1 << 51 @@ -1103,6 +1108,52 @@ class LnFeatures(IntFlag): or get_ln_flag_pair_of_bit(flag) in our_flags) +class ChannelType(IntFlag): + OPTION_LEGACY_CHANNEL = 0 + OPTION_STATIC_REMOTEKEY = 1 << 12 + OPTION_ANCHOR_OUTPUTS = 1 << 20 + OPTION_ANCHORS_ZERO_FEE_HTLC_TX = 1 << 22 + + def discard_unknown_and_check(self): + """Discards unknown flags and checks flag combination.""" + flags = list_enabled_bits(self) + known_channel_types = [] + for flag in flags: + channel_type = ChannelType(1 << flag) + if channel_type.name: + known_channel_types.append(channel_type) + final_channel_type = known_channel_types[0] + for channel_type in known_channel_types[1:]: + final_channel_type |= channel_type + + final_channel_type.check_combinations() + return final_channel_type + + def check_combinations(self): + if self == ChannelType.OPTION_STATIC_REMOTEKEY: + pass + elif self == ChannelType.OPTION_ANCHOR_OUTPUTS | ChannelType.OPTION_STATIC_REMOTEKEY: + pass + elif self == ChannelType.OPTION_ANCHORS_ZERO_FEE_HTLC_TX | ChannelType.OPTION_STATIC_REMOTEKEY: + pass + else: + raise ValueError("Channel type is not a valid flag combination.") + + def complies_with_features(self, features: LnFeatures) -> bool: + flags = list_enabled_bits(self) + complies = True + for flag in flags: + feature = LnFeatures(1 << flag) + complies &= features.supports(feature) + return complies + + def to_bytes_minimal(self): + # MUST use the smallest bitmap possible to represent the channel type. + bit_length =self.value.bit_length() + byte_length = bit_length // 8 + int(bool(bit_length % 8)) + return self.to_bytes(byte_length, byteorder='big') + + del LNFC # name is ambiguous without context # features that are actually implemented and understood in our codebase: diff --git a/electrum/lnwire/peer_wire.csv b/electrum/lnwire/peer_wire.csv index d6eb9ccc2..17b8a103d 100644 --- a/electrum/lnwire/peer_wire.csv +++ b/electrum/lnwire/peer_wire.csv @@ -53,6 +53,8 @@ msgdata,open_channel,channel_flags,byte, msgdata,open_channel,tlvs,open_channel_tlvs, tlvtype,open_channel_tlvs,upfront_shutdown_script,0 tlvdata,open_channel_tlvs,upfront_shutdown_script,shutdown_scriptpubkey,byte,... +tlvtype,open_channel_tlvs,channel_type,1 +tlvdata,open_channel_tlvs,channel_type,type,byte,... msgtype,accept_channel,33 msgdata,accept_channel,temporary_channel_id,byte,32 msgdata,accept_channel,dust_limit_satoshis,u64, @@ -71,6 +73,8 @@ msgdata,accept_channel,first_per_commitment_point,point, msgdata,accept_channel,tlvs,accept_channel_tlvs, tlvtype,accept_channel_tlvs,upfront_shutdown_script,0 tlvdata,accept_channel_tlvs,upfront_shutdown_script,shutdown_scriptpubkey,byte,... +tlvtype,accept_channel_tlvs,channel_type,1 +tlvdata,accept_channel_tlvs,channel_type,type,byte,... msgtype,funding_created,34 msgdata,funding_created,temporary_channel_id,byte,32 msgdata,funding_created,funding_txid,sha256, diff --git a/electrum/tests/test_lnutil.py b/electrum/tests/test_lnutil.py index 116cbe8cd..0483e8b8e 100644 --- a/electrum/tests/test_lnutil.py +++ b/electrum/tests/test_lnutil.py @@ -9,7 +9,7 @@ from electrum.lnutil import (RevocationStore, get_per_commitment_secret_from_see 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, - ln_compare_features, IncompatibleLightningFeatures) + ln_compare_features, IncompatibleLightningFeatures, ChannelType) from electrum.util import bh2u, bfh, MyEncoder from electrum.transaction import Transaction, PartialTransaction from electrum.lnworker import LNWallet @@ -890,3 +890,15 @@ class TestLNUtil(ElectrumTestCase): self.assertEqual( None, LNWallet._decode_channel_update_msg(bytes.fromhex("0101") + msg_without_prefix)) + + def test_channel_type(self): + # test compliance and non compliance with LN features + features = LnFeatures(LnFeatures.BASIC_MPP_OPT | LnFeatures.OPTION_STATIC_REMOTEKEY_OPT) + self.assertTrue(ChannelType.OPTION_STATIC_REMOTEKEY.complies_with_features(features)) + + features = LnFeatures(LnFeatures.BASIC_MPP_OPT | LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT) + self.assertFalse(ChannelType.OPTION_STATIC_REMOTEKEY.complies_with_features(features)) + + # ignore unknown channel types + channel_type = ChannelType(0b10000000001000000000010).discard_unknown_and_check() + self.assertEqual(ChannelType(0b10000000001000000000000), channel_type) \ No newline at end of file