electrum

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

commit 06dfe1699cca8d116914138147301b0ec9b289bc
parent 9ca445bd5d8cff6df049f21e374bbb915b77d23b
Author: ThomasV <thomasv@electrum.org>
Date:   Fri,  3 Apr 2020 16:25:42 +0200

LNWatcher: Distinguish between blockchain-triggered channel state
transitions, and actions taken as a result.
- state transitions are performed in lnchannel.update_onchain_state()
- peer actions are in LNWorker.on_channel_update()

Diffstat:
Melectrum/lnchannel.py | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Melectrum/lnwatcher.py | 16++--------------
Melectrum/lnworker.py | 58++++++----------------------------------------------------
3 files changed, 69 insertions(+), 66 deletions(-)

diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py @@ -54,6 +54,7 @@ from .lnsweep import create_sweeptxs_for_our_ctx, create_sweeptxs_for_their_ctx from .lnsweep import create_sweeptx_for_their_revoked_htlc, SweepInfo from .lnhtlc import HTLCManager from .lnmsg import encode_msg, decode_msg +from .address_synchronizer import TX_HEIGHT_LOCAL if TYPE_CHECKING: from .lnworker import LNWallet @@ -1092,3 +1093,63 @@ class Channel(Logger): min_value_worth_closing_channel_over_sat = max(num_htlcs * 10 * self.config[REMOTE].dust_limit_sat, 500_000) return total_value_sat > min_value_worth_closing_channel_over_sat + + def update_onchain_state(self, funding_txid, funding_height, closing_txid, closing_height, keep_watching): + # note: state transitions are irreversible, but + # save_funding_height, save_closing_height are reversible + if funding_height.height == TX_HEIGHT_LOCAL: + self.update_unfunded_state() + elif closing_height.height == TX_HEIGHT_LOCAL: + self.update_funded_state(funding_txid, funding_height) + else: + self.update_closed_state(funding_txid, funding_height, closing_txid, closing_height, keep_watching) + + def update_unfunded_state(self): + self.delete_funding_height() + self.delete_closing_height() + if self.get_state() in [channel_states.PREOPENING, channel_states.OPENING, channel_states.FORCE_CLOSING] and self.lnworker: + if self.constraints.is_initiator: + # set channel state to REDEEMED so that it can be removed manually + # to protect ourselves against a server lying by omission, + # we check that funding_inputs have been double spent and deeply mined + inputs = self.storage.get('funding_inputs', []) + if not inputs: + self.logger.info(f'channel funding inputs are not provided') + self.set_state(channel_states.REDEEMED) + for i in inputs: + spender_txid = self.lnworker.wallet.db.get_spent_outpoint(*i) + if spender_txid is None: + continue + if spender_txid != self.funding_outpoint.txid: + tx_mined_height = self.lnworker.wallet.get_tx_height(spender_txid) + if tx_mined_height.conf > lnutil.REDEEM_AFTER_DOUBLE_SPENT_DELAY: + self.logger.info(f'channel is double spent {inputs}') + self.set_state(channel_states.REDEEMED) + break + else: + now = int(time.time()) + if now - self.storage.get('init_timestamp', 0) > CHANNEL_OPENING_TIMEOUT: + self.lnworker.remove_channel(self.channel_id) + + def update_funded_state(self, funding_txid, funding_height): + self.save_funding_height(funding_txid, funding_height.height, funding_height.timestamp) + self.delete_closing_height() + if self.get_state() == channel_states.OPENING: + if self.short_channel_id is None: + self.lnworker.maybe_save_short_chan_id(self, funding_height) + if self.short_channel_id: + self.set_state(channel_states.FUNDED) + + def update_closed_state(self, funding_txid, funding_height, closing_txid, closing_height, keep_watching): + self.save_funding_height(funding_txid, funding_height.height, funding_height.timestamp) + self.save_closing_height(closing_txid, closing_height.height, closing_height.timestamp) + if self.get_state() < channel_states.CLOSED: + conf = closing_height.conf + if conf > 0: + self.set_state(channel_states.CLOSED) + else: + # we must not trust the server with unconfirmed transactions + # if the remote force closed, we remain OPEN until the closing tx is confirmed + pass + if self.get_state() == channel_states.CLOSED and not keep_watching: + self.set_state(channel_states.REDEEMED) diff --git a/electrum/lnwatcher.py b/electrum/lnwatcher.py @@ -341,23 +341,11 @@ class LNWalletWatcher(LNWatcher): @ignore_exceptions @log_exceptions async def update_channel_state(self, funding_outpoint, funding_txid, funding_height, closing_txid, closing_height, keep_watching): - # note: state transitions are irreversible, but - # save_funding_height, save_closing_height are reversible chan = self.lnworker.channel_by_txo(funding_outpoint) if not chan: return - if funding_height.height == TX_HEIGHT_LOCAL: - chan.delete_funding_height() - chan.delete_closing_height() - await self.lnworker.update_unfunded_channel(chan, funding_txid) - elif closing_height.height == TX_HEIGHT_LOCAL: - chan.save_funding_height(funding_txid, funding_height.height, funding_height.timestamp) - chan.delete_closing_height() - await self.lnworker.update_open_channel(chan, funding_txid, funding_height) - else: - chan.save_funding_height(funding_txid, funding_height.height, funding_height.timestamp) - chan.save_closing_height(closing_txid, closing_height.height, closing_height.timestamp) - await self.lnworker.update_closed_channel(chan, funding_txid, funding_height, closing_txid, closing_height, keep_watching) + chan.update_onchain_state(funding_txid, funding_height, closing_txid, closing_height, keep_watching) + await self.lnworker.on_channel_update(chan) async def do_breach_remedy(self, funding_outpoint, closing_tx, spenders): chan = self.lnworker.channel_by_txo(funding_outpoint) diff --git a/electrum/lnworker.py b/electrum/lnworker.py @@ -696,57 +696,24 @@ class LNWallet(LNWorker): if chan.funding_outpoint.to_str() == txo: return chan - async def update_unfunded_channel(self, chan, funding_txid): - if chan.get_state() in [channel_states.PREOPENING, channel_states.OPENING, channel_states.FORCE_CLOSING]: - if chan.constraints.is_initiator: - # set channel state to REDEEMED so that it can be removed manually - # to protect ourselves against a server lying by omission, - # we check that funding_inputs have been double spent and deeply mined - inputs = chan.storage.get('funding_inputs', []) - if not inputs: - self.logger.info(f'channel funding inputs are not provided') - chan.set_state(channel_states.REDEEMED) - for i in inputs: - spender_txid = self.wallet.db.get_spent_outpoint(*i) - if spender_txid is None: - continue - if spender_txid != funding_txid: - tx_mined_height = self.wallet.get_tx_height(spender_txid) - if tx_mined_height.conf > lnutil.REDEEM_AFTER_DOUBLE_SPENT_DELAY: - self.logger.info(f'channel is double spent {inputs}') - chan.set_state(channel_states.REDEEMED) - break - else: - now = int(time.time()) - if now - chan.storage.get('init_timestamp', 0) > CHANNEL_OPENING_TIMEOUT: - self.remove_channel(chan.channel_id) - async def update_open_channel(self, chan, funding_txid, funding_height): + async def on_channel_update(self, chan): if chan.get_state() == channel_states.OPEN and chan.should_be_closed_due_to_expiring_htlcs(self.network.get_local_height()): self.logger.info(f"force-closing due to expiring htlcs") await self.try_force_closing(chan.channel_id) - return - if chan.get_state() == channel_states.OPENING: - if chan.short_channel_id is None: - self.maybe_save_short_chan_id(chan, funding_height) - if chan.short_channel_id: - chan.set_state(channel_states.FUNDED) - - if chan.get_state() == channel_states.FUNDED: + elif chan.get_state() == channel_states.FUNDED: peer = self.peers.get(chan.node_id) if peer and peer.is_initialized(): peer.send_funding_locked(chan) elif chan.get_state() == channel_states.OPEN: peer = self.peers.get(chan.node_id) - if peer is None: - self.logger.info("peer not found for {}".format(bh2u(chan.node_id))) - return - await peer.maybe_update_fee(chan) - conf = self.lnwatcher.get_tx_height(chan.funding_outpoint.txid).conf - peer.on_network_update(chan, conf) + if peer: + await peer.maybe_update_fee(chan) + conf = self.lnwatcher.get_tx_height(chan.funding_outpoint.txid).conf + peer.on_network_update(chan, conf) elif chan.get_state() == channel_states.FORCE_CLOSING: force_close_tx = chan.force_close_tx() @@ -757,19 +724,6 @@ class LNWallet(LNWorker): await self.network.try_broadcasting(force_close_tx, 'force-close') - async def update_closed_channel(self, chan, funding_txid, funding_height, closing_txid, closing_height, keep_watching): - - if chan.get_state() < channel_states.CLOSED: - conf = closing_height.conf - if conf > 0: - chan.set_state(channel_states.CLOSED) - else: - # we must not trust the server with unconfirmed transactions - # if the remote force closed, we remain OPEN until the closing tx is confirmed - pass - - if chan.get_state() == channel_states.CLOSED and not keep_watching: - chan.set_state(channel_states.REDEEMED) @log_exceptions