|
|
@ -17,12 +17,12 @@ |
|
|
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|
|
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
|
|
|
# THE SOFTWARE. |
|
|
|
|
|
|
|
import enum |
|
|
|
import os |
|
|
|
from collections import namedtuple, defaultdict |
|
|
|
import binascii |
|
|
|
import json |
|
|
|
from enum import IntEnum |
|
|
|
from enum import IntEnum, Enum |
|
|
|
from typing import (Optional, Dict, List, Tuple, NamedTuple, Set, Callable, |
|
|
|
Iterable, Sequence, TYPE_CHECKING, Iterator, Union, Mapping) |
|
|
|
import time |
|
|
@ -82,11 +82,14 @@ class ChannelState(IntEnum): |
|
|
|
OPEN = 3 # both parties have sent funding_locked |
|
|
|
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. Note that if the |
|
|
|
FORCE_CLOSING = 6 # *we* force-closed, and closing tx is unconfirmed. Note that if the |
|
|
|
# remote force-closes then we remain OPEN until it gets mined - |
|
|
|
# the server could be lying to us with a fake tx. |
|
|
|
CLOSED = 7 # closing tx has been mined |
|
|
|
REDEEMED = 8 # we can stop watching |
|
|
|
REQUESTED_FCLOSE = 7 # Chan is open, but we have tried to request the *remote* to force-close |
|
|
|
WE_ARE_TOXIC = 8 # Chan is open, but we have lost state and the remote proved this. |
|
|
|
# The remote must force-close, it is *not* safe for us to do so. |
|
|
|
CLOSED = 9 # closing tx has been mined |
|
|
|
REDEEMED = 10 # we can stop watching |
|
|
|
|
|
|
|
|
|
|
|
class PeerState(IntEnum): |
|
|
@ -113,12 +116,28 @@ state_transitions = [ |
|
|
|
(cs.OPEN, cs.FORCE_CLOSING), |
|
|
|
(cs.SHUTDOWN, cs.FORCE_CLOSING), |
|
|
|
(cs.CLOSING, cs.FORCE_CLOSING), |
|
|
|
(cs.REQUESTED_FCLOSE, cs.FORCE_CLOSING), |
|
|
|
# we can request a force-close almost any time |
|
|
|
(cs.OPENING, cs.REQUESTED_FCLOSE), |
|
|
|
(cs.FUNDED, cs.REQUESTED_FCLOSE), |
|
|
|
(cs.OPEN, cs.REQUESTED_FCLOSE), |
|
|
|
(cs.SHUTDOWN, cs.REQUESTED_FCLOSE), |
|
|
|
(cs.CLOSING, cs.REQUESTED_FCLOSE), |
|
|
|
(cs.REQUESTED_FCLOSE, cs.REQUESTED_FCLOSE), |
|
|
|
# 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.REQUESTED_FCLOSE, cs.CLOSED), |
|
|
|
(cs.WE_ARE_TOXIC, cs.CLOSED), |
|
|
|
# during channel_reestablish, we might realise we have lost state |
|
|
|
(cs.OPENING, cs.WE_ARE_TOXIC), |
|
|
|
(cs.FUNDED, cs.WE_ARE_TOXIC), |
|
|
|
(cs.OPEN, cs.WE_ARE_TOXIC), |
|
|
|
(cs.SHUTDOWN, cs.WE_ARE_TOXIC), |
|
|
|
(cs.REQUESTED_FCLOSE, cs.WE_ARE_TOXIC), |
|
|
|
# |
|
|
|
(cs.FORCE_CLOSING, cs.FORCE_CLOSING), # allow multiple attempts |
|
|
|
(cs.FORCE_CLOSING, cs.CLOSED), |
|
|
@ -130,6 +149,12 @@ state_transitions = [ |
|
|
|
del cs # delete as name is ambiguous without context |
|
|
|
|
|
|
|
|
|
|
|
class ChanCloseOption(Enum): |
|
|
|
COOP_CLOSE = enum.auto() |
|
|
|
LOCAL_FCLOSE = enum.auto() |
|
|
|
REQUEST_REMOTE_FCLOSE = enum.auto() |
|
|
|
|
|
|
|
|
|
|
|
class RevokeAndAck(NamedTuple): |
|
|
|
per_commitment_secret: bytes |
|
|
|
next_per_commitment_point: bytes |
|
|
@ -203,6 +228,10 @@ class AbstractChannel(Logger, ABC): |
|
|
|
def is_redeemed(self): |
|
|
|
return self.get_state() == ChannelState.REDEEMED |
|
|
|
|
|
|
|
@abstractmethod |
|
|
|
def get_close_options(self) -> Sequence[ChanCloseOption]: |
|
|
|
pass |
|
|
|
|
|
|
|
def save_funding_height(self, *, txid: str, height: int, timestamp: Optional[int]) -> None: |
|
|
|
self.storage['funding_height'] = txid, height, timestamp |
|
|
|
|
|
|
@ -545,6 +574,12 @@ class ChannelBackup(AbstractChannel): |
|
|
|
return self.lnworker.node_keypair.pubkey |
|
|
|
raise NotImplementedError(f"unexpected cb type: {type(cb)}") |
|
|
|
|
|
|
|
def get_close_options(self) -> Sequence[ChanCloseOption]: |
|
|
|
ret = [] |
|
|
|
if self.get_state() == ChannelState.FUNDED: |
|
|
|
ret.append(ChanCloseOption.REQUEST_REMOTE_FCLOSE) |
|
|
|
return ret |
|
|
|
|
|
|
|
|
|
|
|
class Channel(AbstractChannel): |
|
|
|
# note: try to avoid naming ctns/ctxs/etc as "current" and "pending". |
|
|
@ -886,7 +921,9 @@ class Channel(AbstractChannel): |
|
|
|
return True |
|
|
|
|
|
|
|
def should_try_to_reestablish_peer(self) -> bool: |
|
|
|
return ChannelState.PREOPENING < self._state < ChannelState.CLOSING and self.peer_state == PeerState.DISCONNECTED |
|
|
|
if self.peer_state != PeerState.DISCONNECTED: |
|
|
|
return False |
|
|
|
return ChannelState.PREOPENING < self._state < ChannelState.CLOSING |
|
|
|
|
|
|
|
def get_funding_address(self): |
|
|
|
script = funding_output_script(self.config[LOCAL], self.config[REMOTE]) |
|
|
@ -1497,6 +1534,16 @@ class Channel(AbstractChannel): |
|
|
|
assert tx.is_complete() |
|
|
|
return tx |
|
|
|
|
|
|
|
def get_close_options(self) -> Sequence[ChanCloseOption]: |
|
|
|
ret = [] |
|
|
|
if not self.is_closed() and self.peer_state == PeerState.GOOD: |
|
|
|
ret.append(ChanCloseOption.COOP_CLOSE) |
|
|
|
ret.append(ChanCloseOption.REQUEST_REMOTE_FCLOSE) |
|
|
|
if not self.is_closed() or self.get_state() == ChannelState.REQUESTED_FCLOSE: |
|
|
|
ret.append(ChanCloseOption.LOCAL_FCLOSE) |
|
|
|
assert not (self.get_state() == ChannelState.WE_ARE_TOXIC and ChanCloseOption.LOCAL_FCLOSE in ret), "local force-close unsafe if we are toxic" |
|
|
|
return ret |
|
|
|
|
|
|
|
def maybe_sweep_revoked_htlc(self, ctx: Transaction, htlc_tx: Transaction) -> Optional[SweepInfo]: |
|
|
|
# look at the output address, check if it matches |
|
|
|
return create_sweeptx_for_their_revoked_htlc(self, ctx, htlc_tx, self.sweep_address) |
|
|
|