Browse Source

lnpeer+wallet: use channel type for channel open

* channel_type is put into storage, serialized as int and
  deserialized as ChannelType
* check for static_remotekey is done via channel type
master
bitromortac 4 years ago
parent
commit
6915e3cb10
No known key found for this signature in database
GPG Key ID: 1965063FC13BEBE2
  1. 2
      electrum/json_db.py
  2. 8
      electrum/lnchannel.py
  3. 85
      electrum/lnpeer.py
  4. 1
      electrum/lnutil.py
  5. 5
      electrum/lnworker.py
  6. 1
      electrum/tests/test_lnchannel.py
  7. 1
      electrum/tests/test_lnpeer.py
  8. 2
      electrum/wallet_db.py

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
@ -709,7 +710,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 = []
@ -925,6 +927,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}")
@ -966,6 +969,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
@ -508,6 +508,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)
@ -525,16 +528,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()
@ -610,7 +612,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(
@ -639,10 +658,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
@ -657,6 +673,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"]),
@ -672,7 +697,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,
@ -721,7 +746,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,
@ -752,7 +777,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(),
@ -769,7 +794,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, [])
@ -793,7 +818,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')
@ -836,6 +875,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,
@ -852,10 +902,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
@ -872,7 +919,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,

1
electrum/lnutil.py

@ -1169,6 +1169,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
) )

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

2
electrum/wallet_db.py

@ -1377,6 +1377,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