Browse Source

Merge pull request #7636 from bitromortac/2201-channel-type

lightning: implement channel types
patch-4
ThomasV 3 years ago
committed by GitHub
parent
commit
b268877d53
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      electrum/gui/kivy/uix/dialogs/lightning_channels.py
  2. 1
      electrum/gui/qt/channel_details.py
  3. 2
      electrum/json_db.py
  4. 8
      electrum/lnchannel.py
  5. 85
      electrum/lnpeer.py
  6. 58
      electrum/lnutil.py
  7. 4
      electrum/lnwire/peer_wire.csv
  8. 5
      electrum/lnworker.py
  9. 1
      electrum/tests/test_lnchannel.py
  10. 1
      electrum/tests/test_lnpeer.py
  11. 14
      electrum/tests/test_lnutil.py
  12. 20
      electrum/wallet_db.py

5
electrum/gui/kivy/uix/dialogs/lightning_channels.py

@ -246,6 +246,7 @@ Builder.load_string(r'''
warning: '' warning: ''
is_frozen_for_sending: False is_frozen_for_sending: False
is_frozen_for_receiving: False is_frozen_for_receiving: False
channel_type:''
BoxLayout: BoxLayout:
padding: '12dp', '12dp', '12dp', '12dp' padding: '12dp', '12dp', '12dp', '12dp'
spacing: '12dp' spacing: '12dp'
@ -294,6 +295,9 @@ Builder.load_string(r'''
BoxLabel: BoxLabel:
text: _('Frozen (for receiving)') text: _('Frozen (for receiving)')
value: str(root.is_frozen_for_receiving) value: str(root.is_frozen_for_receiving)
BoxLabel:
text: _('Channel type')
value: str(root.channel_type)
Widget: Widget:
size_hint: 1, 0.1 size_hint: 1, 0.1
TopLabel: TopLabel:
@ -484,6 +488,7 @@ class ChannelDetailsPopup(Popup, Logger):
self.warning = '' if self.app.wallet.lnworker.channel_db or self.app.wallet.lnworker.is_trampoline_peer(chan.node_id) else _('Warning') + ': ' + msg self.warning = '' if self.app.wallet.lnworker.channel_db or self.app.wallet.lnworker.is_trampoline_peer(chan.node_id) else _('Warning') + ': ' + msg
self.is_frozen_for_sending = chan.is_frozen_for_sending() self.is_frozen_for_sending = chan.is_frozen_for_sending()
self.is_frozen_for_receiving = chan.is_frozen_for_receiving() self.is_frozen_for_receiving = chan.is_frozen_for_receiving()
self.channel_type = chan.storage['channel_type'].name_minimal
self.update_action_dropdown() self.update_action_dropdown()
def update_action_dropdown(self): def update_action_dropdown(self):

1
electrum/gui/qt/channel_details.py

@ -193,6 +193,7 @@ class ChannelDetailsDialog(QtWidgets.QDialog, MessageBoxMixin):
form_layout.addRow(_('Remote dust limit:'), self.dust_limit) form_layout.addRow(_('Remote dust limit:'), self.dust_limit)
self.remote_reserve = self.window.format_amount_and_units(chan.config[REMOTE].reserve_sat) self.remote_reserve = self.window.format_amount_and_units(chan.config[REMOTE].reserve_sat)
form_layout.addRow(_('Remote reserve:'), SelectableLabel(self.remote_reserve)) form_layout.addRow(_('Remote reserve:'), SelectableLabel(self.remote_reserve))
form_layout.addRow(_('Channel type:'), SelectableLabel(chan.storage['channel_type'].name_minimal))
vbox.addLayout(form_layout) vbox.addLayout(form_layout)
# add htlc tree view to vbox (wouldn't scale correctly in QFormLayout) # add htlc tree view to vbox (wouldn't scale correctly in QFormLayout)

2
electrum/json_db.py

@ -98,7 +98,7 @@ class StoredDict(dict):
if not self.db or self.db._should_convert_to_stored_dict(key): if not self.db or self.db._should_convert_to_stored_dict(key):
v = StoredDict(v, self.db, self.path + [key]) v = StoredDict(v, self.db, self.path + [key])
# convert_value is called depth-first # convert_value is called depth-first
if isinstance(v, dict) or isinstance(v, str): if isinstance(v, dict) or isinstance(v, str) or isinstance(v, int):
if self.db: if self.db:
v = self.db._convert_value(self.path, key, v) v = self.db._convert_value(self.path, key, v)
# set parent of StoredObject # set parent of StoredObject

8
electrum/lnchannel.py

@ -52,7 +52,8 @@ from .lnutil import (Outpoint, LocalConfig, RemoteConfig, Keypair, OnlyPubkeyKey
ScriptHtlc, PaymentFailure, calc_fees_for_commitment_tx, RemoteMisbehaving, make_htlc_output_witness_script, ScriptHtlc, PaymentFailure, calc_fees_for_commitment_tx, RemoteMisbehaving, make_htlc_output_witness_script,
ShortChannelID, map_htlcs_to_ctx_output_idxs, LNPeerAddr, ShortChannelID, map_htlcs_to_ctx_output_idxs, LNPeerAddr,
fee_for_htlc_output, offered_htlc_trim_threshold_sat, fee_for_htlc_output, offered_htlc_trim_threshold_sat,
received_htlc_trim_threshold_sat, make_commitment_output_to_remote_address) received_htlc_trim_threshold_sat, make_commitment_output_to_remote_address,
ChannelType)
from .lnsweep import create_sweeptxs_for_our_ctx, create_sweeptxs_for_their_ctx from .lnsweep import create_sweeptxs_for_our_ctx, create_sweeptxs_for_their_ctx
from .lnsweep import create_sweeptx_for_their_revoked_htlc, SweepInfo from .lnsweep import create_sweeptx_for_their_revoked_htlc, SweepInfo
from .lnhtlc import HTLCManager from .lnhtlc import HTLCManager
@ -711,7 +712,8 @@ class Channel(AbstractChannel):
return chan_ann return chan_ann
def is_static_remotekey_enabled(self) -> bool: def is_static_remotekey_enabled(self) -> bool:
return bool(self.storage.get('static_remotekey_enabled')) channel_type = ChannelType(self.storage.get('channel_type'))
return bool(channel_type & ChannelType.OPTION_STATIC_REMOTEKEY)
def get_wallet_addresses_channel_might_want_reserved(self) -> Sequence[str]: def get_wallet_addresses_channel_might_want_reserved(self) -> Sequence[str]:
ret = [] ret = []
@ -927,6 +929,7 @@ class Channel(AbstractChannel):
Action must be initiated by LOCAL. Action must be initiated by LOCAL.
Finally, the next remote ctx becomes the latest remote ctx. Finally, the next remote ctx becomes the latest remote ctx.
""" """
# TODO: when more channel types are supported, this method should depend on channel type
next_remote_ctn = self.get_next_ctn(REMOTE) next_remote_ctn = self.get_next_ctn(REMOTE)
self.logger.info(f"sign_next_commitment {next_remote_ctn}") self.logger.info(f"sign_next_commitment {next_remote_ctn}")
@ -968,6 +971,7 @@ class Channel(AbstractChannel):
If all checks pass, the next local ctx becomes the latest local ctx. If all checks pass, the next local ctx becomes the latest local ctx.
""" """
# TODO in many failure cases below, we should "fail" the channel (force-close) # TODO in many failure cases below, we should "fail" the channel (force-close)
# TODO: when more channel types are supported, this method should depend on channel type
next_local_ctn = self.get_next_ctn(LOCAL) next_local_ctn = self.get_next_ctn(LOCAL)
self.logger.info(f"receive_new_commitment. ctn={next_local_ctn}, len(htlc_sigs)={len(htlc_sigs)}") self.logger.info(f"receive_new_commitment. ctn={next_local_ctn}, len(htlc_sigs)={len(htlc_sigs)}")

85
electrum/lnpeer.py

@ -42,7 +42,7 @@ from .lnutil import (Outpoint, LocalConfig, RECEIVED, UpdateAddHtlc, ChannelConf
LightningPeerConnectionClosed, HandshakeFailed, LightningPeerConnectionClosed, HandshakeFailed,
RemoteMisbehaving, ShortChannelID, RemoteMisbehaving, ShortChannelID,
IncompatibleLightningFeatures, derive_payment_secret_from_payment_preimage, IncompatibleLightningFeatures, derive_payment_secret_from_payment_preimage,
UpfrontShutdownScriptViolation) UpfrontShutdownScriptViolation, ChannelType)
from .lnutil import FeeUpdate, channel_id_from_funding_tx from .lnutil import FeeUpdate, channel_id_from_funding_tx
from .lntransport import LNTransport, LNTransportBase from .lntransport import LNTransport, LNTransportBase
from .lnmsg import encode_msg, decode_msg, UnknownOptionalMsgType from .lnmsg import encode_msg, decode_msg, UnknownOptionalMsgType
@ -511,6 +511,9 @@ class Peer(Logger):
def is_static_remotekey(self): def is_static_remotekey(self):
return self.features.supports(LnFeatures.OPTION_STATIC_REMOTEKEY_OPT) return self.features.supports(LnFeatures.OPTION_STATIC_REMOTEKEY_OPT)
def is_channel_type(self):
return self.features.supports(LnFeatures.OPTION_CHANNEL_TYPE_OPT)
def is_upfront_shutdown_script(self): def is_upfront_shutdown_script(self):
return self.features.supports(LnFeatures.OPTION_UPFRONT_SHUTDOWN_SCRIPT_OPT) return self.features.supports(LnFeatures.OPTION_UPFRONT_SHUTDOWN_SCRIPT_OPT)
@ -528,16 +531,15 @@ class Peer(Logger):
self.logger.info(f"upfront shutdown script received: {upfront_shutdown_script}") self.logger.info(f"upfront shutdown script received: {upfront_shutdown_script}")
return upfront_shutdown_script return upfront_shutdown_script
def make_local_config(self, funding_sat: int, push_msat: int, initiator: HTLCOwner) -> LocalConfig: def make_local_config(self, funding_sat: int, push_msat: int, initiator: HTLCOwner, channel_type: ChannelType) -> LocalConfig:
channel_seed = os.urandom(32) channel_seed = os.urandom(32)
initial_msat = funding_sat * 1000 - push_msat if initiator == LOCAL else push_msat initial_msat = funding_sat * 1000 - push_msat if initiator == LOCAL else push_msat
static_remotekey = None
# sending empty bytes as the upfront_shutdown_script will give us the # sending empty bytes as the upfront_shutdown_script will give us the
# flexibility to decide an address at closing time # flexibility to decide an address at closing time
upfront_shutdown_script = b'' upfront_shutdown_script = b''
if self.is_static_remotekey(): if channel_type & channel_type.OPTION_STATIC_REMOTEKEY:
wallet = self.lnworker.wallet wallet = self.lnworker.wallet
assert wallet.txin_type == 'p2wpkh' assert wallet.txin_type == 'p2wpkh'
addr = wallet.get_new_sweep_address_for_channel() addr = wallet.get_new_sweep_address_for_channel()
@ -613,7 +615,24 @@ class Peer(Logger):
raise Exception('Not a trampoline node: ' + str(self.their_features)) raise Exception('Not a trampoline node: ' + str(self.their_features))
feerate = self.lnworker.current_feerate_per_kw() feerate = self.lnworker.current_feerate_per_kw()
local_config = self.make_local_config(funding_sat, push_msat, LOCAL) # we set a channel type for internal bookkeeping
open_channel_tlvs = {}
if self.their_features.supports(LnFeatures.OPTION_STATIC_REMOTEKEY_OPT):
our_channel_type = ChannelType(ChannelType.OPTION_STATIC_REMOTEKEY)
else:
our_channel_type = ChannelType(0)
# if option_channel_type is negotiated: MUST set channel_type
if self.is_channel_type():
# if it includes channel_type: MUST set it to a defined type representing the type it wants.
open_channel_tlvs['channel_type'] = {
'type': our_channel_type.to_bytes_minimal()
}
local_config = self.make_local_config(funding_sat, push_msat, LOCAL, our_channel_type)
# if it includes open_channel_tlvs: MUST include upfront_shutdown_script.
open_channel_tlvs['upfront_shutdown_script'] = {
'shutdown_scriptpubkey': local_config.upfront_shutdown_script
}
# for the first commitment transaction # for the first commitment transaction
per_commitment_secret_first = get_per_commitment_secret_from_seed( per_commitment_secret_first = get_per_commitment_secret_from_seed(
@ -642,10 +661,7 @@ class Peer(Logger):
channel_flags=0x00, # not willing to announce channel channel_flags=0x00, # not willing to announce channel
channel_reserve_satoshis=local_config.reserve_sat, channel_reserve_satoshis=local_config.reserve_sat,
htlc_minimum_msat=local_config.htlc_minimum_msat, htlc_minimum_msat=local_config.htlc_minimum_msat,
open_channel_tlvs={ open_channel_tlvs=open_channel_tlvs,
'upfront_shutdown_script':
{'shutdown_scriptpubkey': local_config.upfront_shutdown_script}
}
) )
# <- accept_channel # <- accept_channel
@ -660,6 +676,15 @@ class Peer(Logger):
upfront_shutdown_script = self.upfront_shutdown_script_from_payload( upfront_shutdown_script = self.upfront_shutdown_script_from_payload(
payload, 'accept') payload, 'accept')
accept_channel_tlvs = payload.get('accept_channel_tlvs')
their_channel_type = accept_channel_tlvs.get('channel_type') if accept_channel_tlvs else None
if their_channel_type:
their_channel_type = ChannelType.from_bytes(their_channel_type['type'], byteorder='big').discard_unknown_and_check()
# if channel_type is set, and channel_type was set in open_channel,
# and they are not equal types: MUST reject the channel.
if open_channel_tlvs.get('channel_type') is not None and their_channel_type != our_channel_type:
raise Exception("Channel type is not the one that we sent.")
remote_config = RemoteConfig( remote_config = RemoteConfig(
payment_basepoint=OnlyPubkeyKeypair(payload['payment_basepoint']), payment_basepoint=OnlyPubkeyKeypair(payload['payment_basepoint']),
multisig_key=OnlyPubkeyKeypair(payload["funding_pubkey"]), multisig_key=OnlyPubkeyKeypair(payload["funding_pubkey"]),
@ -675,7 +700,7 @@ class Peer(Logger):
htlc_minimum_msat=payload['htlc_minimum_msat'], htlc_minimum_msat=payload['htlc_minimum_msat'],
next_per_commitment_point=remote_per_commitment_point, next_per_commitment_point=remote_per_commitment_point,
current_per_commitment_point=None, current_per_commitment_point=None,
upfront_shutdown_script=upfront_shutdown_script upfront_shutdown_script=upfront_shutdown_script,
) )
ChannelConfig.cross_validate_params( ChannelConfig.cross_validate_params(
local_config=local_config, local_config=local_config,
@ -724,7 +749,7 @@ class Peer(Logger):
funding_txn_minimum_depth=funding_txn_minimum_depth funding_txn_minimum_depth=funding_txn_minimum_depth
) )
storage = self.create_channel_storage( storage = self.create_channel_storage(
channel_id, outpoint, local_config, remote_config, constraints) channel_id, outpoint, local_config, remote_config, constraints, our_channel_type)
chan = Channel( chan = Channel(
storage, storage,
sweep_address=self.lnworker.sweep_address, sweep_address=self.lnworker.sweep_address,
@ -755,7 +780,7 @@ class Peer(Logger):
self.lnworker.add_new_channel(chan) self.lnworker.add_new_channel(chan)
return chan, funding_tx return chan, funding_tx
def create_channel_storage(self, channel_id, outpoint, local_config, remote_config, constraints): def create_channel_storage(self, channel_id, outpoint, local_config, remote_config, constraints, channel_type):
chan_dict = { chan_dict = {
"node_id": self.pubkey.hex(), "node_id": self.pubkey.hex(),
"channel_id": channel_id.hex(), "channel_id": channel_id.hex(),
@ -772,7 +797,7 @@ class Peer(Logger):
"fail_htlc_reasons": {}, # htlc_id -> onion_packet "fail_htlc_reasons": {}, # htlc_id -> onion_packet
"unfulfilled_htlcs": {}, # htlc_id -> error_bytes, failure_message "unfulfilled_htlcs": {}, # htlc_id -> error_bytes, failure_message
"revocation_store": {}, "revocation_store": {},
"static_remotekey_enabled": self.is_static_remotekey(), # stored because it cannot be "downgraded", per BOLT2 "channel_type": channel_type,
} }
return StoredDict(chan_dict, self.lnworker.db if self.lnworker else None, []) return StoredDict(chan_dict, self.lnworker.db if self.lnworker else None, [])
@ -796,7 +821,21 @@ class Peer(Logger):
push_msat = payload['push_msat'] push_msat = payload['push_msat']
feerate = payload['feerate_per_kw'] # note: we are not validating this feerate = payload['feerate_per_kw'] # note: we are not validating this
temp_chan_id = payload['temporary_channel_id'] temp_chan_id = payload['temporary_channel_id']
local_config = self.make_local_config(funding_sat, push_msat, REMOTE)
open_channel_tlvs = payload.get('open_channel_tlvs')
channel_type = open_channel_tlvs.get('channel_type') if open_channel_tlvs else None
# The receiving node MAY fail the channel if:
# option_channel_type was negotiated but the message doesn't include a channel_type
if self.is_channel_type() and channel_type is None:
raise Exception("sender has advertized option_channel_type, but hasn't sent the channel type")
# MUST fail the channel if it supports channel_type,
# channel_type was set, and the type is not suitable.
elif self.is_channel_type() and channel_type is not None:
channel_type = ChannelType.from_bytes(channel_type['type'], byteorder='big').discard_unknown_and_check()
if not channel_type.complies_with_features(self.features):
raise Exception("sender has sent a channel type we don't support")
local_config = self.make_local_config(funding_sat, push_msat, REMOTE, channel_type)
upfront_shutdown_script = self.upfront_shutdown_script_from_payload( upfront_shutdown_script = self.upfront_shutdown_script_from_payload(
payload, 'open') payload, 'open')
@ -839,6 +878,17 @@ class Peer(Logger):
per_commitment_point_first = secret_to_pubkey( per_commitment_point_first = secret_to_pubkey(
int.from_bytes(per_commitment_secret_first, 'big')) int.from_bytes(per_commitment_secret_first, 'big'))
min_depth = 3 min_depth = 3
accept_channel_tlvs = {
'upfront_shutdown_script': {
'shutdown_scriptpubkey': local_config.upfront_shutdown_script
},
}
# The sender: if it sets channel_type: MUST set it to the channel_type from open_channel
if self.is_channel_type():
accept_channel_tlvs['channel_type'] = {
'type': channel_type.to_bytes_minimal()
}
self.send_message( self.send_message(
'accept_channel', 'accept_channel',
temporary_channel_id=temp_chan_id, temporary_channel_id=temp_chan_id,
@ -855,10 +905,7 @@ class Peer(Logger):
delayed_payment_basepoint=local_config.delayed_basepoint.pubkey, delayed_payment_basepoint=local_config.delayed_basepoint.pubkey,
htlc_basepoint=local_config.htlc_basepoint.pubkey, htlc_basepoint=local_config.htlc_basepoint.pubkey,
first_per_commitment_point=per_commitment_point_first, first_per_commitment_point=per_commitment_point_first,
accept_channel_tlvs={ accept_channel_tlvs=accept_channel_tlvs,
'upfront_shutdown_script':
{'shutdown_scriptpubkey': local_config.upfront_shutdown_script}
}
) )
# <- funding created # <- funding created
@ -875,7 +922,7 @@ class Peer(Logger):
) )
outpoint = Outpoint(funding_txid, funding_idx) outpoint = Outpoint(funding_txid, funding_idx)
chan_dict = self.create_channel_storage( chan_dict = self.create_channel_storage(
channel_id, outpoint, local_config, remote_config, constraints) channel_id, outpoint, local_config, remote_config, constraints, channel_type)
chan = Channel( chan = Channel(
chan_dict, chan_dict,
sweep_address=self.lnworker.sweep_address, sweep_address=self.lnworker.sweep_address,

58
electrum/lnutil.py

@ -8,7 +8,6 @@ import json
from collections import namedtuple, defaultdict from collections import namedtuple, defaultdict
from typing import NamedTuple, List, Tuple, Mapping, Optional, TYPE_CHECKING, Union, Dict, Set, Sequence from typing import NamedTuple, List, Tuple, Mapping, Optional, TYPE_CHECKING, Union, Dict, Set, Sequence
import re import re
import time
import attr import attr
from aiorpcx import NetAddress from aiorpcx import NetAddress
@ -1030,6 +1029,12 @@ class LnFeatures(IntFlag):
_ln_feature_contexts[OPTION_SHUTDOWN_ANYSEGWIT_REQ] = (LNFC.INIT | LNFC.NODE_ANN) _ln_feature_contexts[OPTION_SHUTDOWN_ANYSEGWIT_REQ] = (LNFC.INIT | LNFC.NODE_ANN)
_ln_feature_contexts[OPTION_SHUTDOWN_ANYSEGWIT_OPT] = (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 # temporary
OPTION_TRAMPOLINE_ROUTING_REQ_ECLAIR = 1 << 50 OPTION_TRAMPOLINE_ROUTING_REQ_ECLAIR = 1 << 50
OPTION_TRAMPOLINE_ROUTING_OPT_ECLAIR = 1 << 51 OPTION_TRAMPOLINE_ROUTING_OPT_ECLAIR = 1 << 51
@ -1104,6 +1109,56 @@ class LnFeatures(IntFlag):
or get_ln_flag_pair_of_bit(flag) in our_flags) 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')
@property
def name_minimal(self):
return self.name.replace('OPTION_', '')
del LNFC # name is ambiguous without context del LNFC # name is ambiguous without context
# features that are actually implemented and understood in our codebase: # features that are actually implemented and understood in our codebase:
@ -1119,6 +1174,7 @@ LN_FEATURES_IMPLEMENTED = (
| LnFeatures.BASIC_MPP_OPT | LnFeatures.BASIC_MPP_REQ | LnFeatures.BASIC_MPP_OPT | LnFeatures.BASIC_MPP_REQ
| LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT | LnFeatures.OPTION_TRAMPOLINE_ROUTING_REQ | LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT | LnFeatures.OPTION_TRAMPOLINE_ROUTING_REQ
| LnFeatures.OPTION_SHUTDOWN_ANYSEGWIT_OPT | LnFeatures.OPTION_SHUTDOWN_ANYSEGWIT_REQ | LnFeatures.OPTION_SHUTDOWN_ANYSEGWIT_OPT | LnFeatures.OPTION_SHUTDOWN_ANYSEGWIT_REQ
| LnFeatures.OPTION_CHANNEL_TYPE_OPT | LnFeatures.OPTION_CHANNEL_TYPE_REQ
) )

4
electrum/lnwire/peer_wire.csv

@ -53,6 +53,8 @@ msgdata,open_channel,channel_flags,byte,
msgdata,open_channel,tlvs,open_channel_tlvs, msgdata,open_channel,tlvs,open_channel_tlvs,
tlvtype,open_channel_tlvs,upfront_shutdown_script,0 tlvtype,open_channel_tlvs,upfront_shutdown_script,0
tlvdata,open_channel_tlvs,upfront_shutdown_script,shutdown_scriptpubkey,byte,... 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 msgtype,accept_channel,33
msgdata,accept_channel,temporary_channel_id,byte,32 msgdata,accept_channel,temporary_channel_id,byte,32
msgdata,accept_channel,dust_limit_satoshis,u64, 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, msgdata,accept_channel,tlvs,accept_channel_tlvs,
tlvtype,accept_channel_tlvs,upfront_shutdown_script,0 tlvtype,accept_channel_tlvs,upfront_shutdown_script,0
tlvdata,accept_channel_tlvs,upfront_shutdown_script,shutdown_scriptpubkey,byte,... 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 msgtype,funding_created,34
msgdata,funding_created,temporary_channel_id,byte,32 msgdata,funding_created,temporary_channel_id,byte,32
msgdata,funding_created,funding_txid,sha256, msgdata,funding_created,funding_txid,sha256,

Can't render this file because it has a wrong number of fields in line 2.

5
electrum/lnworker.py

@ -167,7 +167,7 @@ BASE_FEATURES = LnFeatures(0)\
| LnFeatures.OPTION_STATIC_REMOTEKEY_OPT\ | LnFeatures.OPTION_STATIC_REMOTEKEY_OPT\
| LnFeatures.VAR_ONION_OPT\ | LnFeatures.VAR_ONION_OPT\
| LnFeatures.PAYMENT_SECRET_OPT\ | LnFeatures.PAYMENT_SECRET_OPT\
| LnFeatures.OPTION_UPFRONT_SHUTDOWN_SCRIPT_OPT | LnFeatures.OPTION_UPFRONT_SHUTDOWN_SCRIPT_OPT\
# we do not want to receive unrequested gossip (see lnpeer.maybe_save_remote_update) # we do not want to receive unrequested gossip (see lnpeer.maybe_save_remote_update)
LNWALLET_FEATURES = BASE_FEATURES\ LNWALLET_FEATURES = BASE_FEATURES\
@ -177,10 +177,11 @@ LNWALLET_FEATURES = BASE_FEATURES\
| LnFeatures.BASIC_MPP_OPT\ | LnFeatures.BASIC_MPP_OPT\
| LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT\ | LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT\
| LnFeatures.OPTION_SHUTDOWN_ANYSEGWIT_OPT\ | LnFeatures.OPTION_SHUTDOWN_ANYSEGWIT_OPT\
| LnFeatures.OPTION_CHANNEL_TYPE_OPT\
LNGOSSIP_FEATURES = BASE_FEATURES\ LNGOSSIP_FEATURES = BASE_FEATURES\
| LnFeatures.GOSSIP_QUERIES_OPT\ | LnFeatures.GOSSIP_QUERIES_OPT\
| LnFeatures.GOSSIP_QUERIES_REQ | LnFeatures.GOSSIP_QUERIES_REQ\
class LNWorker(Logger, NetworkRetryManager[LNPeerAddr]): class LNWorker(Logger, NetworkRetryManager[LNPeerAddr]):

1
electrum/tests/test_lnchannel.py

@ -107,6 +107,7 @@ def create_channel_state(funding_txid, funding_index, funding_sat, is_initiator,
'fail_htlc_reasons': {}, 'fail_htlc_reasons': {},
'unfulfilled_htlcs': {}, 'unfulfilled_htlcs': {},
'revocation_store': {}, 'revocation_store': {},
'channel_type': lnutil.ChannelType.OPTION_STATIC_REMOTEKEY
} }
return StoredDict(state, None, []) return StoredDict(state, None, [])

1
electrum/tests/test_lnpeer.py

@ -138,6 +138,7 @@ class MockLNWallet(Logger, NetworkRetryManager[LNPeerAddr]):
self.features |= LnFeatures.VAR_ONION_OPT self.features |= LnFeatures.VAR_ONION_OPT
self.features |= LnFeatures.PAYMENT_SECRET_OPT self.features |= LnFeatures.PAYMENT_SECRET_OPT
self.features |= LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT self.features |= LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT
self.features |= LnFeatures.OPTION_CHANNEL_TYPE_OPT
self.pending_payments = defaultdict(asyncio.Future) self.pending_payments = defaultdict(asyncio.Future)
for chan in chans: for chan in chans:
chan.lnworker = self chan.lnworker = self

14
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, 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) ln_compare_features, IncompatibleLightningFeatures, ChannelType)
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
@ -890,3 +890,15 @@ class TestLNUtil(ElectrumTestCase):
self.assertEqual( self.assertEqual(
None, None,
LNWallet._decode_channel_update_msg(bytes.fromhex("0101") + msg_without_prefix)) 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)

20
electrum/wallet_db.py

@ -37,7 +37,7 @@ from .invoices import Invoice
from .keystore import bip44_derivation from .keystore import bip44_derivation
from .transaction import Transaction, TxOutpoint, tx_from_any, PartialTransaction, PartialTxOutput from .transaction import Transaction, TxOutpoint, tx_from_any, PartialTransaction, PartialTxOutput
from .logging import Logger from .logging import Logger
from .lnutil import LOCAL, REMOTE, FeeUpdate, UpdateAddHtlc, LocalConfig, RemoteConfig, Keypair, OnlyPubkeyKeypair, RevocationStore from .lnutil import LOCAL, REMOTE, FeeUpdate, UpdateAddHtlc, LocalConfig, RemoteConfig, ChannelType
from .lnutil import ImportedChannelBackupStorage, OnchainChannelBackupStorage from .lnutil import ImportedChannelBackupStorage, OnchainChannelBackupStorage
from .lnutil import ChannelConstraints, Outpoint, ShachainElement from .lnutil import ChannelConstraints, Outpoint, ShachainElement
from .json_db import StoredDict, JsonDB, locked, modifier from .json_db import StoredDict, JsonDB, locked, modifier
@ -53,7 +53,7 @@ if TYPE_CHECKING:
OLD_SEED_VERSION = 4 # electrum versions < 2.0 OLD_SEED_VERSION = 4 # electrum versions < 2.0
NEW_SEED_VERSION = 11 # electrum versions >= 2.0 NEW_SEED_VERSION = 11 # electrum versions >= 2.0
FINAL_SEED_VERSION = 43 # electrum >= 2.7 will set this to prevent FINAL_SEED_VERSION = 44 # electrum >= 2.7 will set this to prevent
# old versions from overwriting new format # old versions from overwriting new format
@ -192,6 +192,7 @@ class WalletDB(JsonDB):
self._convert_version_41() self._convert_version_41()
self._convert_version_42() self._convert_version_42()
self._convert_version_43() self._convert_version_43()
self._convert_version_44()
self.put('seed_version', FINAL_SEED_VERSION) # just to be sure self.put('seed_version', FINAL_SEED_VERSION) # just to be sure
self._after_upgrade_tasks() self._after_upgrade_tasks()
@ -850,6 +851,19 @@ class WalletDB(JsonDB):
self.data['channels'] = channels self.data['channels'] = channels
self.data['seed_version'] = 43 self.data['seed_version'] = 43
def _convert_version_44(self):
if not self._is_upgrade_method_needed(43, 43):
return
channels = self.data.get('channels', {})
for key, item in channels.items():
if item['static_remotekey_enabled']:
channel_type = ChannelType.OPTION_STATIC_REMOTEKEY
else:
channel_type = ChannelType(0)
del item['static_remotekey_enabled']
item['channel_type'] = channel_type
self.data['seed_version'] = 44
def _convert_imported(self): def _convert_imported(self):
if not self._is_upgrade_method_needed(0, 13): if not self._is_upgrade_method_needed(0, 13):
return return
@ -1377,6 +1391,8 @@ class WalletDB(JsonDB):
v = ChannelConstraints(**v) v = ChannelConstraints(**v)
elif key == 'funding_outpoint': elif key == 'funding_outpoint':
v = Outpoint(**v) v = Outpoint(**v)
elif key == 'channel_type':
v = ChannelType(v)
return v return v
def _should_convert_to_stored_dict(self, key) -> bool: def _should_convert_to_stored_dict(self, key) -> bool:

Loading…
Cancel
Save