electrum

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

commit 6b90a2d36c52c577a786e8080e3c09531795b16d
parent e5458bb5348be6d73ff834f7db3cd4a993c7ed6a
Author: bitromortac <bitromortac@protonmail.com>
Date:   Fri, 18 Dec 2020 11:06:42 +0100

lnpeer: implement upfront shutdown script logic

Upfront shutdown script is a script provided on channel opening,
which will be used by the peer to enforce us closing to this script
on collaborative channel close.

Diffstat:
Melectrum/lnchannel.py | 3+++
Melectrum/lnpeer.py | 110++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Melectrum/lnutil.py | 2++
Melectrum/tests/test_lnchannel.py | 2++
4 files changed, 110 insertions(+), 7 deletions(-)

diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py @@ -649,6 +649,9 @@ class Channel(AbstractChannel): def is_static_remotekey_enabled(self) -> bool: return bool(self.storage.get('static_remotekey_enabled')) + def is_upfront_shutdown_script_enabled(self) -> bool: + return bool(self.storage.get('upfront_shutdown_script_enabled')) + def get_wallet_addresses_channel_might_want_reserved(self) -> Sequence[str]: ret = [] if self.is_static_remotekey_enabled(): diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py @@ -44,7 +44,8 @@ from .lnutil import (Outpoint, LocalConfig, RECEIVED, UpdateAddHtlc, RemoteMisbehaving, NBLOCK_OUR_CLTV_EXPIRY_DELTA, format_short_channel_id, ShortChannelID, IncompatibleLightningFeatures, derive_payment_secret_from_payment_preimage, - LN_MAX_FUNDING_SAT, calc_fees_for_commitment_tx) + LN_MAX_FUNDING_SAT, calc_fees_for_commitment_tx, + UpfrontShutdownScriptViolation) from .lnutil import FeeUpdate, channel_id_from_funding_tx from .lntransport import LNTransport, LNTransportBase from .lnmsg import encode_msg, decode_msg @@ -486,12 +487,33 @@ class Peer(Logger): def is_static_remotekey(self): return bool(self.features & LnFeatures.OPTION_STATIC_REMOTEKEY_OPT) + def is_upfront_shutdown_script(self): + return bool(self.features & 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']: + raise ValueError("msg_identifier must be either 'accept' or 'open'") + + uss_tlv = payload[msg_identifier + '_channel_tlvs'].get( + 'upfront_shutdown_script') + + if uss_tlv and self.is_upfront_shutdown_script(): + upfront_shutdown_script = uss_tlv['shutdown_scriptpubkey'] + else: + upfront_shutdown_script = b'' + self.logger.info(f"upfront shutdown script received: {upfront_shutdown_script}") + return upfront_shutdown_script + def make_local_config(self, funding_sat: int, push_msat: int, initiator: HTLCOwner) -> LocalConfig: channel_seed = os.urandom(32) 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 + # flexibility to decide an address at closing time + upfront_shutdown_script = b'' + if self.is_static_remotekey(): - # Note: in the future, if a CSV delay is added, - # we will want to derive that key wallet = self.lnworker.wallet assert wallet.txin_type == 'p2wpkh' addr = wallet.get_new_sweep_address_for_channel() @@ -503,6 +525,7 @@ class Peer(Logger): local_config = LocalConfig.from_seed( channel_seed=channel_seed, static_remotekey=static_remotekey, + upfront_shutdown_script=upfront_shutdown_script, to_self_delay=self.network.config.get('lightning_to_self_delay', 7 * 144), dust_limit_sat=dust_limit_sat, max_htlc_value_in_flight_msat=funding_sat * 1000, @@ -546,15 +569,27 @@ class Peer(Logger): push_msat: int, temp_channel_id: bytes ) -> Tuple[Channel, 'PartialTransaction']: + """Implements the channel opening flow. + + -> open_channel message + <- accept_channel message + -> funding_created message + <- funding_signed message + + Channel configurations are initialized in this method. + """ await asyncio.wait_for(self.initialized, LN_P2P_NETWORK_TIMEOUT) + feerate = self.lnworker.current_feerate_per_kw() local_config = self.make_local_config(funding_sat, push_msat, LOCAL) + if funding_sat > LN_MAX_FUNDING_SAT: raise Exception(f"MUST set funding_satoshis to less than 2^24 satoshi. {funding_sat} sat > {LN_MAX_FUNDING_SAT}") if push_msat > 1000 * funding_sat: raise Exception(f"MUST set push_msat to equal or less than 1000 * funding_satoshis: {push_msat} msat > {1000 * funding_sat} msat") if funding_sat < lnutil.MIN_FUNDING_SAT: raise Exception(f"funding_sat too low: {funding_sat} < {lnutil.MIN_FUNDING_SAT}") + # for the first commitment transaction per_commitment_secret_first = get_per_commitment_secret_from_seed(local_config.per_commitment_secret_seed, RevocationStore.START_INDEX) @@ -579,7 +614,13 @@ class Peer(Logger): channel_flags=0x00, # not willing to announce channel channel_reserve_satoshis=local_config.reserve_sat, htlc_minimum_msat=local_config.htlc_minimum_msat, + open_channel_tlvs={ + 'upfront_shutdown_script': + {'shutdown_scriptpubkey': local_config.upfront_shutdown_script} + } ) + + # <- accept_channel payload = await self.wait_for_message('accept_channel', temp_channel_id) remote_per_commitment_point = payload['first_per_commitment_point'] funding_txn_minimum_depth = payload['minimum_depth'] @@ -587,6 +628,10 @@ class Peer(Logger): raise Exception(f"minimum depth too low, {funding_txn_minimum_depth}") if funding_txn_minimum_depth > 30: raise Exception(f"minimum depth too high, {funding_txn_minimum_depth}") + + upfront_shutdown_script = self.upfront_shutdown_script_from_payload( + payload, 'accept') + remote_config = RemoteConfig( payment_basepoint=OnlyPubkeyKeypair(payload['payment_basepoint']), multisig_key=OnlyPubkeyKeypair(payload["funding_pubkey"]), @@ -602,6 +647,7 @@ class Peer(Logger): htlc_minimum_msat=payload['htlc_minimum_msat'], next_per_commitment_point=remote_per_commitment_point, current_per_commitment_point=None, + upfront_shutdown_script=upfront_shutdown_script ) remote_config.validate_params(funding_sat=funding_sat) # if channel_reserve_satoshis is less than dust_limit_satoshis within the open_channel message: @@ -612,6 +658,8 @@ class Peer(Logger): # MUST reject the channel. if local_config.reserve_sat < remote_config.dust_limit_sat: raise Exception("violated constraint: local_config.reserve_sat < remote_config.dust_limit_sat") + + # -> funding created # replace dummy output in funding tx redeem_script = funding_output_script(local_config, remote_config) funding_address = bitcoin.redeem_script_to_address('p2wsh', redeem_script) @@ -626,7 +674,7 @@ class Peer(Logger): funding_txid = funding_tx.txid() assert funding_txid funding_index = funding_tx.outputs().index(funding_output) - # remote commitment transaction + # build remote commitment transaction channel_id, funding_txid_bytes = channel_id_from_funding_tx(funding_txid, funding_index) outpoint = Outpoint(funding_txid, funding_index) constraints = ChannelConstraints(capacity=funding_sat, is_initiator=True, funding_txn_minimum_depth=funding_txn_minimum_depth) @@ -640,12 +688,15 @@ class Peer(Logger): chan.add_or_update_peer_addr(self.transport.peer_addr) sig_64, _ = chan.sign_next_commitment() self.temp_id_to_id[temp_channel_id] = channel_id + self.send_message("funding_created", temporary_channel_id=temp_channel_id, funding_txid=funding_txid_bytes, funding_output_index=funding_index, signature=sig_64) self.funding_created_sent.add(channel_id) + + # <- funding signed payload = await self.wait_for_message('funding_signed', channel_id) self.logger.info('received funding_signed') remote_sig = payload['signature'] @@ -675,6 +726,16 @@ class Peer(Logger): return StoredDict(chan_dict, None, []) async def on_open_channel(self, payload): + """Implements the channel acceptance flow. + + <- open_channel message + -> accept_channel message + <- funding_created message + -> funding_signed message + + Channel configurations are initialized in this method. + """ + # <- open_channel if payload['chain_hash'] != constants.net.rev_genesis_bytes(): raise Exception('wrong chain_hash') funding_sat = payload['funding_satoshis'] @@ -688,6 +749,10 @@ class Peer(Logger): raise Exception(f"MUST set push_msat to equal or less than 1000 * funding_satoshis: {push_msat} msat > {1000 * funding_sat} msat") if funding_sat < lnutil.MIN_FUNDING_SAT: raise Exception(f"funding_sat too low: {funding_sat} < {lnutil.MIN_FUNDING_SAT}") + + upfront_shutdown_script = self.upfront_shutdown_script_from_payload( + payload, 'open') + remote_config = RemoteConfig( payment_basepoint=OnlyPubkeyKeypair(payload['payment_basepoint']), multisig_key=OnlyPubkeyKeypair(payload['funding_pubkey']), @@ -703,7 +768,9 @@ class Peer(Logger): htlc_minimum_msat=payload['htlc_minimum_msat'], next_per_commitment_point=payload['first_per_commitment_point'], current_per_commitment_point=None, + upfront_shutdown_script=upfront_shutdown_script, ) + remote_config.validate_params(funding_sat=funding_sat) # The receiving node MUST fail the channel if: # the funder's amount for the initial commitment transaction is not sufficient for full fee payment. @@ -720,12 +787,15 @@ class Peer(Logger): # note: we ignore payload['channel_flags'], which e.g. contains 'announce_channel'. # Notably if the remote sets 'announce_channel' to True, we will ignore that too, # but we will not play along with actually announcing the channel (so we keep it private). + + # -> accept channel # for the first commitment transaction per_commitment_secret_first = get_per_commitment_secret_from_seed(local_config.per_commitment_secret_seed, RevocationStore.START_INDEX) per_commitment_point_first = secret_to_pubkey(int.from_bytes(per_commitment_secret_first, 'big')) min_depth = 3 - self.send_message('accept_channel', + self.send_message( + 'accept_channel', temporary_channel_id=temp_chan_id, dust_limit_satoshis=local_config.dust_limit_sat, max_htlc_value_in_flight_msat=local_config.max_htlc_value_in_flight_msat, @@ -740,8 +810,16 @@ class Peer(Logger): delayed_payment_basepoint=local_config.delayed_basepoint.pubkey, htlc_basepoint=local_config.htlc_basepoint.pubkey, first_per_commitment_point=per_commitment_point_first, + accept_channel_tlvs={ + 'upfront_shutdown_script': + {'shutdown_scriptpubkey': local_config.upfront_shutdown_script} + } ) + + # <- funding created funding_created = await self.wait_for_message('funding_created', temp_chan_id) + + # -> funding signed funding_idx = funding_created['funding_output_index'] funding_txid = bh2u(funding_created['funding_txid'][::-1]) channel_id, funding_txid_bytes = channel_id_from_funding_tx(funding_txid, funding_idx) @@ -1407,6 +1485,13 @@ class Peer(Logger): async def on_shutdown(self, chan: Channel, payload): their_scriptpubkey = payload['scriptpubkey'] + their_upfront_scriptpubkey = chan.config[REMOTE].upfront_shutdown_script + + # BOLT-02 check if they use the upfront shutdown script they advertized + if their_upfront_scriptpubkey: + if not (their_scriptpubkey == their_upfront_scriptpubkey): + raise UpfrontShutdownScriptViolation("remote didn't use upfront shutdown script it commited to in channel opening") + # BOLT-02 restrict the scriptpubkey to some templates: if not (match_script_against_template(their_scriptpubkey, transaction.SCRIPTPUBKEY_TEMPLATE_WITNESS_V0) or match_script_against_template(their_scriptpubkey, transaction.SCRIPTPUBKEY_TEMPLATE_P2SH) @@ -1433,7 +1518,13 @@ class Peer(Logger): async def send_shutdown(self, chan: Channel): if not self.can_send_shutdown(chan): raise Exception('cannot send shutdown') - scriptpubkey = bfh(bitcoin.address_to_script(chan.sweep_address)) + + if chan.config[LOCAL].upfront_shutdown_script: + scriptpubkey = chan.config[LOCAL].upfront_shutdown_script + else: + scriptpubkey = bfh(bitcoin.address_to_script(chan.sweep_address)) + assert scriptpubkey + # wait until no more pending updates (bolt2) chan.set_can_send_ctx_updates(False) while chan.has_pending_changes(REMOTE): @@ -1452,7 +1543,12 @@ class Peer(Logger): # if no HTLCs remain, we must not send updates chan.set_can_send_ctx_updates(False) their_scriptpubkey = payload['scriptpubkey'] - our_scriptpubkey = bfh(bitcoin.address_to_script(chan.sweep_address)) + if chan.config[LOCAL].upfront_shutdown_script: + our_scriptpubkey = chan.config[LOCAL].upfront_shutdown_script + else: + our_scriptpubkey = bfh(bitcoin.address_to_script(chan.sweep_address)) + assert our_scriptpubkey + # estimate fee of closing tx our_sig, closing_tx = chan.make_closing_tx(our_scriptpubkey, their_scriptpubkey, fee_sat=0) fee_rate = self.network.config.fee_per_kb() diff --git a/electrum/lnutil.py b/electrum/lnutil.py @@ -81,6 +81,7 @@ class Config(StoredObject): initial_msat = attr.ib(type=int) reserve_sat = attr.ib(type=int) # applies to OTHER ctx htlc_minimum_msat = attr.ib(type=int) # smallest value for INCOMING htlc + upfront_shutdown_script = attr.ib(type=bytes, converter=hex_to_bytes) def validate_params(self, *, funding_sat: int) -> None: conf_name = type(self).__name__ @@ -300,6 +301,7 @@ class UnableToDeriveSecret(LightningError): pass class HandshakeFailed(LightningError): pass class ConnStringFormatError(LightningError): pass class RemoteMisbehaving(LightningError): pass +class UpfrontShutdownScriptViolation(RemoteMisbehaving): pass class NotFoundChanAnnouncementForUpdate(Exception): pass diff --git a/electrum/tests/test_lnchannel.py b/electrum/tests/test_lnchannel.py @@ -69,6 +69,7 @@ def create_channel_state(funding_txid, funding_index, funding_sat, is_initiator, htlc_minimum_msat=1, next_per_commitment_point=nex, current_per_commitment_point=cur, + upfront_shutdown_script=b'', ), "local_config":lnpeer.LocalConfig( channel_seed = None, @@ -89,6 +90,7 @@ def create_channel_state(funding_txid, funding_index, funding_sat, is_initiator, current_commitment_signature=None, current_htlc_signatures=None, htlc_minimum_msat=1, + upfront_shutdown_script=b'', ), "constraints":lnpeer.ChannelConstraints( capacity=funding_sat,