URI: 
       tadd lnchannel.can_send_ctx_updates. just drop illegal updates for now - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit e54c69b861c2990adf9cf618b68c6f1c7dd3ebea
   DIR parent 9d1fa4cc99d1d93badb4f5ad20b68f67ad9aa4b9
  HTML Author: SomberNight <somber.night@protonmail.com>
       Date:   Wed, 26 Feb 2020 20:35:46 +0100
       
       add lnchannel.can_send_ctx_updates. just drop illegal updates for now
       
       Diffstat:
         M electrum/lnchannel.py               |      28 ++++++++++++++++++++++++----
         M electrum/lnpeer.py                  |      30 +++++++++++++++++++++++-------
         M electrum/tests/test_lnchannel.py    |       9 ++++-----
         M electrum/tests/test_lnpeer.py       |       4 ++++
       
       4 files changed, 55 insertions(+), 16 deletions(-)
       ---
   DIR diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py
       t@@ -151,6 +151,7 @@ class Channel(Logger):
                self._outgoing_channel_update = None  # type: Optional[bytes]
                self._chan_ann_without_sigs = None  # type: Optional[bytes]
                self.revocation_store = RevocationStore(state["revocation_store"])
       +        self._can_send_ctx_updates = True  # type: bool
        
            def get_id_for_log(self) -> str:
                scid = self.short_channel_id
       t@@ -287,11 +288,11 @@ class Channel(Logger):
                            out[rhash] = (self.channel_id, htlc, direction)
                return out
        
       -    def open_with_first_pcp(self, remote_pcp, remote_sig):
       +    def open_with_first_pcp(self, remote_pcp: bytes, remote_sig: bytes) -> None:
                with self.db_lock:
       -            self.config[REMOTE].current_per_commitment_point=remote_pcp
       -            self.config[REMOTE].next_per_commitment_point=None
       -            self.config[LOCAL].current_commitment_signature=remote_sig
       +            self.config[REMOTE].current_per_commitment_point = remote_pcp
       +            self.config[REMOTE].next_per_commitment_point = None
       +            self.config[LOCAL].current_commitment_signature = remote_sig
                    self.hm.channel_open_finished()
                    self.peer_state = peer_states.GOOD
        
       t@@ -321,6 +322,19 @@ class Channel(Logger):
                # the closing txid has been saved
                return self.get_state() >= channel_states.CLOSED
        
       +    def set_can_send_ctx_updates(self, b: bool) -> None:
       +        self._can_send_ctx_updates = b
       +
       +    def can_send_ctx_updates(self) -> bool:
       +        """Whether we can send update_fee, update_*_htlc changes to the remote."""
       +        if not self.is_open():
       +            return False
       +        if self.peer_state != peer_states.GOOD:
       +            return False
       +        if not self._can_send_ctx_updates:
       +            return False
       +        return True
       +
            def save_funding_height(self, txid, height, timestamp):
                self.storage['funding_height'] = txid, height, timestamp
        
       t@@ -345,6 +359,8 @@ class Channel(Logger):
                    raise PaymentFailure('Channel closed')
                if self.get_state() != channel_states.OPEN:
                    raise PaymentFailure('Channel not open', self.get_state())
       +        if not self.can_send_ctx_updates():
       +            raise PaymentFailure('Channel cannot send ctx updates')
                if self.available_to_spend(LOCAL) < amount_msat:
                    raise PaymentFailure(f'Not enough local balance. Have: {self.available_to_spend(LOCAL)}, Need: {amount_msat}')
                if len(self.hm.htlcs(LOCAL)) + 1 > self.config[REMOTE].max_accepted_htlcs:
       t@@ -377,6 +393,7 @@ class Channel(Logger):
        
                This docstring is from LND.
                """
       +        assert self.can_send_ctx_updates(), f"cannot update channel. {self.get_state()!r} {self.peer_state!r}"
                if isinstance(htlc, dict):  # legacy conversion  # FIXME remove
                    htlc = UpdateAddHtlc(**htlc)
                assert isinstance(htlc, UpdateAddHtlc)
       t@@ -704,6 +721,7 @@ class Channel(Logger):
                SettleHTLC attempts to settle an existing outstanding received HTLC.
                """
                self.logger.info("settle_htlc")
       +        assert self.can_send_ctx_updates(), f"cannot update channel. {self.get_state()!r} {self.peer_state!r}"
                log = self.hm.log[REMOTE]
                htlc = log['adds'][htlc_id]
                assert htlc.payment_hash == sha256(preimage)
       t@@ -733,6 +751,7 @@ class Channel(Logger):
        
            def fail_htlc(self, htlc_id):
                self.logger.info("fail_htlc")
       +        assert self.can_send_ctx_updates(), f"cannot update channel. {self.get_state()!r} {self.peer_state!r}"
                with self.db_lock:
                    self.hm.send_fail(htlc_id)
        
       t@@ -753,6 +772,7 @@ class Channel(Logger):
                    raise Exception(f"Cannot update_fee: wrong initiator. us: {from_us}")
                with self.db_lock:
                    if from_us:
       +                assert self.can_send_ctx_updates(), f"cannot update channel. {self.get_state()!r} {self.peer_state!r}"
                        self.hm.send_update_fee(feerate)
                    else:
                        self.hm.recv_update_fee(feerate)
   DIR diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py
       t@@ -712,8 +712,8 @@ class Peer(Logger):
                chan_id = chan.channel_id
                assert channel_states.PREOPENING < chan.get_state() < channel_states.CLOSED
                if chan.peer_state != peer_states.DISCONNECTED:
       -            self.logger.info('reestablish_channel was called but channel {} already in state {}'
       -                             .format(chan_id, chan.get_state()))
       +            self.logger.info(f'reestablish_channel was called but channel {chan.get_id_for_log()} '
       +                             f'already in peer_state {chan.peer_state}')
                    return
                chan.peer_state = peer_states.REESTABLISHING
                self.network.trigger_callback('channel', chan)
       t@@ -890,7 +890,6 @@ class Peer(Logger):
                if not chan:
                    raise Exception("Got unknown funding_locked", channel_id)
                if not chan.config[LOCAL].funding_locked_received:
       -            our_next_point = chan.config[REMOTE].next_per_commitment_point
                    their_next_point = payload["next_per_commitment_point"]
                    chan.config[REMOTE].next_per_commitment_point = their_next_point
                    chan.config[LOCAL].funding_locked_received = True
       t@@ -1041,6 +1040,9 @@ class Peer(Logger):
                    raise PaymentFailure('Channel not open')
                assert amount_msat > 0, "amount_msat is not greater zero"
                await asyncio.wait_for(self.initialized, LN_P2P_NETWORK_TIMEOUT)
       +        # TODO also wait for channel reestablish to finish. (combine timeout with waiting for init?)
       +        if not chan.can_send_ctx_updates():
       +            raise PaymentFailure("Channel cannot send updates")
                # create onion packet
                final_cltv = self.network.get_local_height() + min_final_cltv_expiry
                hops_data, amount_msat, cltv = calc_hops_data_for_payment(route, amount_msat, final_cltv)
       t@@ -1051,7 +1053,7 @@ class Peer(Logger):
                htlc = UpdateAddHtlc(amount_msat=amount_msat, payment_hash=payment_hash, cltv_expiry=cltv, timestamp=int(time.time()))
                htlc = chan.add_htlc(htlc)
                remote_ctn = chan.get_latest_ctn(REMOTE)
       -        chan.onion_keys[htlc.htlc_id] = secret_key
       +        chan.set_onion_key(htlc.htlc_id, secret_key)
                self.logger.info(f"starting payment. len(route)={len(route)}. route: {route}. htlc: {htlc}")
                self.send_message("update_add_htlc",
                                  channel_id=chan.channel_id,
       t@@ -1136,6 +1138,9 @@ class Peer(Logger):
                                     timestamp=int(time.time()),
                                     htlc_id=htlc_id)
                htlc = chan.receive_htlc(htlc)
       +        # TODO: fulfilling/failing/forwarding of htlcs should be robust to going offline.
       +        #       instead of storing state implicitly in coroutines, we could decouple it from receiving the htlc.
       +        #       maybe persist the required details, and have a long-running task that makes these decisions.
                local_ctn = chan.get_latest_ctn(LOCAL)
                remote_ctn = chan.get_latest_ctn(REMOTE)
                if processed_onion.are_we_final:
       t@@ -1179,8 +1184,9 @@ class Peer(Logger):
                    return
                outgoing_chan_upd = next_chan.get_outgoing_gossip_channel_update()[2:]
                outgoing_chan_upd_len = len(outgoing_chan_upd).to_bytes(2, byteorder="big")
       -        if next_chan.get_state() != channel_states.OPEN:
       -            self.logger.info(f"cannot forward htlc. next_chan not OPEN: {next_chan_scid} in state {next_chan.get_state()}")
       +        if not next_chan.can_send_ctx_updates():
       +            self.logger.info(f"cannot forward htlc. next_chan {next_chan_scid} cannot send ctx updates. "
       +                             f"chan state {next_chan.get_state()}, peer state: {next_chan.peer_state}")
                    reason = OnionRoutingFailureMessage(code=OnionFailureCode.TEMPORARY_CHANNEL_FAILURE,
                                                        data=outgoing_chan_upd_len+outgoing_chan_upd)
                    await self.fail_htlc(chan, htlc.htlc_id, onion_packet, reason)
       t@@ -1277,6 +1283,10 @@ class Peer(Logger):
        
            async def _fulfill_htlc(self, chan: Channel, htlc_id: int, preimage: bytes):
                self.logger.info(f"_fulfill_htlc. chan {chan.short_channel_id}. htlc_id {htlc_id}")
       +        if not chan.can_send_ctx_updates():
       +            self.logger.info(f"dropping chan update (fulfill htlc {htlc_id}) for {chan.short_channel_id}. "
       +                             f"cannot send updates")
       +            return
                chan.settle_htlc(preimage, htlc_id)
                payment_hash = sha256(preimage)
                self.lnworker.payment_received(payment_hash)
       t@@ -1290,6 +1300,10 @@ class Peer(Logger):
            async def fail_htlc(self, chan: Channel, htlc_id: int, onion_packet: OnionPacket,
                                reason: OnionRoutingFailureMessage):
                self.logger.info(f"fail_htlc. chan {chan.short_channel_id}. htlc_id {htlc_id}. reason: {reason}")
       +        if not chan.can_send_ctx_updates():
       +            self.logger.info(f"dropping chan update (fail htlc {htlc_id}) for {chan.short_channel_id}. "
       +                             f"cannot send updates")
       +            return
                chan.fail_htlc(htlc_id)
                remote_ctn = chan.get_latest_ctn(REMOTE)
                error_packet = construct_onion_error(reason, onion_packet, our_onion_private_key=self.privkey)
       t@@ -1323,6 +1337,8 @@ class Peer(Logger):
                """
                called when our fee estimates change
                """
       +        if not chan.can_send_ctx_updates():
       +            return
                if not chan.constraints.is_initiator:
                    # TODO force close if initiator does not update_fee enough
                    return
       t@@ -1372,7 +1388,7 @@ class Peer(Logger):
            async def send_shutdown(self, chan: Channel):
                scriptpubkey = bfh(bitcoin.address_to_script(chan.sweep_address))
                # wait until no more pending updates (bolt2)
       -        # TODO: stop sending updates during that time
       +        chan.set_can_send_ctx_updates(False)
                ctn = chan.get_latest_ctn(REMOTE)
                if chan.has_pending_changes(REMOTE):
                    await self.await_remote(chan, ctn)
   DIR diff --git a/electrum/tests/test_lnchannel.py b/electrum/tests/test_lnchannel.py
       t@@ -157,13 +157,12 @@ def create_test_channels(feerate=6000, local=None, remote=None):
            alice_second = lnutil.secret_to_pubkey(int.from_bytes(lnutil.get_per_commitment_secret_from_seed(alice_seed, lnutil.RevocationStore.START_INDEX - 1), "big"))
            bob_second = lnutil.secret_to_pubkey(int.from_bytes(lnutil.get_per_commitment_secret_from_seed(bob_seed, lnutil.RevocationStore.START_INDEX - 1), "big"))
        
       +    alice.open_with_first_pcp(bob_first, sig_from_bob)
       +    bob.open_with_first_pcp(alice_first, sig_from_alice)
       +
       +    # from funding_locked:
            alice.config[REMOTE].next_per_commitment_point = bob_second
       -    alice.config[REMOTE].current_per_commitment_point = bob_first
            bob.config[REMOTE].next_per_commitment_point = alice_second
       -    bob.config[REMOTE].current_per_commitment_point = alice_first
       -
       -    alice.hm.channel_open_finished()
       -    bob.hm.channel_open_finished()
        
            # TODO: sweep_address in lnchannel.py should use static_remotekey
            alice.sweep_address = bitcoin.pubkey_to_address('p2wpkh', alice.config[LOCAL].payment_basepoint.pubkey.hex())
   DIR diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py
       t@@ -225,6 +225,8 @@ class TestPeer(ElectrumTestCase):
            def test_reestablish(self):
                alice_channel, bob_channel = create_test_channels()
                p1, p2, w1, w2, _q1, _q2 = self.prepare_peers(alice_channel, bob_channel)
       +        for chan in (alice_channel, bob_channel):
       +            chan.peer_state = peer_states.DISCONNECTED
                async def reestablish():
                    await asyncio.gather(
                        p1.reestablish_channel(alice_channel),
       t@@ -254,6 +256,8 @@ class TestPeer(ElectrumTestCase):
                    run(f())
        
                p1, p2, w1, w2, _q1, _q2 = self.prepare_peers(alice_channel_0, bob_channel)
       +        for chan in (alice_channel_0, bob_channel):
       +            chan.peer_state = peer_states.DISCONNECTED
                async def reestablish():
                    await asyncio.gather(
                        p1.reestablish_channel(alice_channel_0),