electrum

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

commit 2adbbee5fe2da2bcf753a4ef27ddb4a86cf6dfae
parent 680502cfb8b22b4daa8697701c3f672e2eca0763
Author: ThomasV <thomasv@electrum.org>
Date:   Fri, 29 May 2020 11:30:08 +0200

Add extra state to distinguish shutdown negotiation from post-
negotiation, where channel should not be reestablished. See #6182

Diffstat:
Melectrum/lnchannel.py | 48+++++++++++++++++++++++++++---------------------
Melectrum/lnpeer.py | 24+++++++++++++++---------
Melectrum/tests/test_lnpeer.py | 7+++++++
3 files changed, 49 insertions(+), 30 deletions(-)

diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py @@ -77,10 +77,11 @@ class ChannelState(IntEnum): # - Non-funding node: has sent the funding_signed message. FUNDED = 2 # Funding tx was mined (requires min_depth and tx verification) OPEN = 3 # both parties have sent funding_locked - CLOSING = 4 # shutdown has been sent, and closing tx is unconfirmed. - FORCE_CLOSING = 5 # we force-closed, and closing tx is unconfirmed. (otherwise we remain OPEN) - CLOSED = 6 # closing tx has been mined - REDEEMED = 7 # we can stop watching + SHUTDOWN = 4 # shutdown has been sent. + CLOSING = 5 # closing negotiation done. we have a fully signed tx. + FORCE_CLOSING = 6 # we force-closed, and closing tx is unconfirmed. (otherwise we remain OPEN) + CLOSED = 7 # closing tx has been mined + REDEEMED = 8 # we can stop watching class PeerState(IntEnum): @@ -95,18 +96,25 @@ state_transitions = [ (cs.PREOPENING, cs.OPENING), (cs.OPENING, cs.FUNDED), (cs.FUNDED, cs.OPEN), - (cs.OPENING, cs.CLOSING), - (cs.FUNDED, cs.CLOSING), - (cs.OPEN, cs.CLOSING), - (cs.OPENING, cs.FORCE_CLOSING), - (cs.FUNDED, cs.FORCE_CLOSING), - (cs.OPEN, cs.FORCE_CLOSING), - (cs.CLOSING, cs.FORCE_CLOSING), - (cs.OPENING, cs.CLOSED), - (cs.FUNDED, cs.CLOSED), - (cs.OPEN, cs.CLOSED), - (cs.CLOSING, cs.CLOSING), # if we reestablish - (cs.CLOSING, cs.CLOSED), + (cs.OPENING, cs.SHUTDOWN), + (cs.FUNDED, cs.SHUTDOWN), + (cs.OPEN, cs.SHUTDOWN), + (cs.SHUTDOWN, cs.SHUTDOWN), # if we reestablish + (cs.SHUTDOWN, cs.CLOSING), + (cs.CLOSING, cs.CLOSING), + # we can force close almost any time + (cs.OPENING, cs.FORCE_CLOSING), + (cs.FUNDED, cs.FORCE_CLOSING), + (cs.OPEN, cs.FORCE_CLOSING), + (cs.SHUTDOWN, cs.FORCE_CLOSING), + (cs.CLOSING, cs.FORCE_CLOSING), + # we can get force closed almost any time + (cs.OPENING, cs.CLOSED), + (cs.FUNDED, cs.CLOSED), + (cs.OPEN, cs.CLOSED), + (cs.SHUTDOWN, cs.CLOSED), + (cs.CLOSING, cs.CLOSED), + # (cs.FORCE_CLOSING, cs.FORCE_CLOSING), # allow multiple attempts (cs.FORCE_CLOSING, cs.CLOSED), (cs.FORCE_CLOSING, cs.REDEEMED), @@ -174,11 +182,11 @@ class AbstractChannel(Logger, ABC): return self.get_state() == ChannelState.OPEN def is_closing(self): - return self.get_state() in [ChannelState.CLOSING, ChannelState.FORCE_CLOSING] + return ChannelState.SHUTDOWN <= self.get_state() <= ChannelState.FORCE_CLOSING def is_closed(self): # the closing txid has been saved - return self.get_state() >= ChannelState.CLOSED + return self.get_state() >= ChannelState.CLOSING def is_redeemed(self): return self.get_state() == ChannelState.REDEEMED @@ -707,8 +715,6 @@ class Channel(AbstractChannel): # and the constraints are the ones imposed by their config ctn = self.get_next_ctn(htlc_receiver) chan_config = self.config[htlc_receiver] - if self.is_closed(): - raise PaymentFailure('Channel closed') if self.get_state() != ChannelState.OPEN: raise PaymentFailure('Channel not open', self.get_state()) if htlc_proposer == LOCAL: @@ -777,7 +783,7 @@ class Channel(AbstractChannel): return True def should_try_to_reestablish_peer(self) -> bool: - return ChannelState.PREOPENING < self._state < ChannelState.FORCE_CLOSING and self.peer_state == PeerState.DISCONNECTED + return ChannelState.PREOPENING < self._state < ChannelState.CLOSING and self.peer_state == PeerState.DISCONNECTED def get_funding_address(self): script = funding_output_script(self.config[LOCAL], self.config[REMOTE]) diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py @@ -87,7 +87,7 @@ class Peer(Logger): self.temp_id_to_id = {} # to forward error messages self.funding_created_sent = set() # for channels in PREOPENING self.funding_signed_sent = set() # for channels in PREOPENING - self.shutdown_received = {} + self.shutdown_received = {} # chan_id -> asyncio.Future() self.announcement_signatures = defaultdict(asyncio.Queue) self.orphan_channel_updates = OrderedDict() Logger.__init__(self) @@ -933,7 +933,8 @@ class Peer(Logger): if chan.is_funded() and chan.config[LOCAL].funding_locked_received: self.mark_open(chan) util.trigger_callback('channel', chan) - if chan.get_state() == ChannelState.CLOSING: + # if we have sent a previous shutdown, it must be retransmitted (Bolt2) + if chan.get_state() == ChannelState.SHUTDOWN: await self.send_shutdown(chan) def send_funding_locked(self, chan: Channel): @@ -1429,7 +1430,7 @@ class Peer(Logger): while chan.has_pending_changes(REMOTE): await asyncio.sleep(0.1) self.send_message('shutdown', channel_id=chan.channel_id, len=len(scriptpubkey), scriptpubkey=scriptpubkey) - chan.set_state(ChannelState.CLOSING) + chan.set_state(ChannelState.SHUTDOWN) # can fullfill or fail htlcs. cannot add htlcs, because of CLOSING state chan.set_can_send_ctx_updates(True) @@ -1492,12 +1493,17 @@ class Peer(Logger): if not chan.constraints.is_initiator: send_closing_signed() # add signatures - closing_tx.add_signature_to_txin(txin_idx=0, - signing_pubkey=chan.config[LOCAL].multisig_key.pubkey.hex(), - sig=bh2u(der_sig_from_sig_string(our_sig) + b'\x01')) - closing_tx.add_signature_to_txin(txin_idx=0, - signing_pubkey=chan.config[REMOTE].multisig_key.pubkey.hex(), - sig=bh2u(der_sig_from_sig_string(their_sig) + b'\x01')) + closing_tx.add_signature_to_txin( + txin_idx=0, + signing_pubkey=chan.config[LOCAL].multisig_key.pubkey.hex(), + sig=bh2u(der_sig_from_sig_string(our_sig) + b'\x01')) + closing_tx.add_signature_to_txin( + txin_idx=0, + signing_pubkey=chan.config[REMOTE].multisig_key.pubkey.hex(), + sig=bh2u(der_sig_from_sig_string(their_sig) + b'\x01')) + # save local transaction and set state + self.lnworker.wallet.add_transaction(closing_tx) + chan.set_state(ChannelState.CLOSING) # broadcast await self.network.try_broadcasting(closing_tx, 'closing') return closing_tx.txid() diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py @@ -90,13 +90,20 @@ class MockBlockchain: class MockWallet: + def set_label(self, x, y): pass + def save_db(self): pass + + def add_transaction(self, tx): + pass + def is_lightning_backup(self): return False + class MockLNWallet(Logger, NetworkRetryManager[LNPeerAddr]): def __init__(self, *, local_keypair: Keypair, chans: Iterable['Channel'], tx_queue): Logger.__init__(self)