This fixes a bug where if one runs `wallet.clear_history()` they would see exceptions later:
```
Traceback (most recent call last):
File "/home/user/wspace/electrum/electrum/gui/qt/main_window.py", line 866, in timer_actions
self.update_wallet()
File "/home/user/wspace/electrum/electrum/gui/qt/main_window.py", line 1021, in update_wallet
self.update_tabs()
File "/home/user/wspace/electrum/electrum/gui/qt/main_window.py", line 1033, in update_tabs
self.utxo_list.update()
File "/home/user/wspace/electrum/electrum/gui/qt/utxo_list.py", line 103, in update
self.refresh_row(name, idx)
File "/home/user/wspace/electrum/electrum/gui/qt/utxo_list.py", line 124, in refresh_row
parents = self.wallet.get_tx_parents(txid)
File "/home/user/wspace/electrum/electrum/wallet.py", line 885, in get_tx_parents
result.update(self.get_tx_parents(_txid))
File "/home/user/wspace/electrum/electrum/wallet.py", line 881, in get_tx_parents
for i, txin in enumerate(tx.inputs()):
AttributeError: 'NoneType' object has no attribute 'inputs'
```
This is related to the privacy analysis, which assumes that for each tx item in the history list
we should have the raw tx in the db. This is no longer true after wallet.clear_history(), if
the wallet has certain LN channels. E.g. an already redeemed channel that was local-force-closed,
as that closing tx is not related to the wallet directly.
In commit 3541ecb576, we decided not to watch already redeemed channels.
This is potentially good for e.g. privacy, as the server would otherwise see us subscribe to that chan.
However it means that after running wallet.clear_history() txs related to the channel but not to the
wallet won't be re-downloaded.
Instead, now if there are missing txs for a redeemed channel, we start watching it, hence the
synchronizer will re-downloaded the txs.
1666 lines
78 KiB
Python
1666 lines
78 KiB
Python
# Copyright (C) 2018 The Electrum developers
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
# of this software and associated documentation files (the "Software"), to deal
|
|
# in the Software without restriction, including without limitation the rights
|
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
# copies of the Software, and to permit persons to whom the Software is
|
|
# furnished to do so, subject to the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be included in
|
|
# all copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
# 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, Enum
|
|
from typing import (Optional, Dict, List, Tuple, NamedTuple, Set, Callable,
|
|
Iterable, Sequence, TYPE_CHECKING, Iterator, Union, Mapping)
|
|
import time
|
|
import threading
|
|
from abc import ABC, abstractmethod
|
|
import itertools
|
|
|
|
from aiorpcx import NetAddress
|
|
import attr
|
|
|
|
from . import ecc
|
|
from . import constants, util
|
|
from .util import bfh, chunks, TxMinedInfo
|
|
from .invoices import PR_PAID
|
|
from .bitcoin import redeem_script_to_address
|
|
from .crypto import sha256, sha256d
|
|
from .transaction import Transaction, PartialTransaction, TxInput, Sighash
|
|
from .logging import Logger
|
|
from .lnonion import decode_onion_error, OnionFailureCode, OnionRoutingFailure
|
|
from . import lnutil
|
|
from .lnutil import (Outpoint, LocalConfig, RemoteConfig, Keypair, OnlyPubkeyKeypair, ChannelConstraints,
|
|
get_per_commitment_secret_from_seed, secret_to_pubkey, derive_privkey, make_closing_tx,
|
|
sign_and_get_sig_string, RevocationStore, derive_blinded_pubkey, Direction, derive_pubkey,
|
|
make_htlc_tx_with_open_channel, make_commitment, make_received_htlc, make_offered_htlc,
|
|
HTLC_TIMEOUT_WEIGHT, HTLC_SUCCESS_WEIGHT, extract_ctn_from_tx_and_chan, UpdateAddHtlc,
|
|
funding_output_script, SENT, RECEIVED, LOCAL, REMOTE, HTLCOwner, make_commitment_outputs,
|
|
ScriptHtlc, PaymentFailure, calc_fees_for_commitment_tx, RemoteMisbehaving, make_htlc_output_witness_script,
|
|
ShortChannelID, map_htlcs_to_ctx_output_idxs, LNPeerAddr,
|
|
fee_for_htlc_output, offered_htlc_trim_threshold_sat,
|
|
received_htlc_trim_threshold_sat, make_commitment_output_to_remote_address,
|
|
ChannelType, LNProtocolWarning)
|
|
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
|
|
from .lnutil import CHANNEL_OPENING_TIMEOUT
|
|
from .lnutil import ChannelBackupStorage, ImportedChannelBackupStorage, OnchainChannelBackupStorage
|
|
from .lnutil import format_short_channel_id
|
|
from .simple_config import FEERATE_PER_KW_MIN_RELAY_LIGHTNING
|
|
|
|
if TYPE_CHECKING:
|
|
from .lnworker import LNWallet
|
|
from .json_db import StoredDict
|
|
from .lnrouter import RouteEdge
|
|
|
|
|
|
# lightning channel states
|
|
# Note: these states are persisted by name (for a given channel) in the wallet file,
|
|
# so consider doing a wallet db upgrade when changing them.
|
|
class ChannelState(IntEnum):
|
|
PREOPENING = 0 # Initial negotiation. Channel will not be reestablished
|
|
OPENING = 1 # Channel will be reestablished. (per BOLT2)
|
|
# - Funding node: has received funding_signed (can broadcast the funding tx)
|
|
# - 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
|
|
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
|
|
# remote force-closes then we remain OPEN until it gets mined -
|
|
# the server could be lying to us with a fake tx.
|
|
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):
|
|
DISCONNECTED = 0
|
|
REESTABLISHING = 1
|
|
GOOD = 2
|
|
BAD = 3
|
|
|
|
|
|
cs = ChannelState
|
|
state_transitions = [
|
|
(cs.PREOPENING, cs.OPENING),
|
|
(cs.OPENING, cs.FUNDED),
|
|
(cs.FUNDED, cs.OPEN),
|
|
(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),
|
|
(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),
|
|
(cs.FORCE_CLOSING, cs.REDEEMED),
|
|
(cs.CLOSED, cs.REDEEMED),
|
|
(cs.OPENING, cs.REDEEMED), # channel never funded (dropped from mempool)
|
|
(cs.PREOPENING, cs.REDEEMED), # channel never funded
|
|
]
|
|
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
|
|
|
|
|
|
class RemoteCtnTooFarInFuture(Exception): pass
|
|
|
|
|
|
def htlcsum(htlcs: Iterable[UpdateAddHtlc]):
|
|
return sum([x.amount_msat for x in htlcs])
|
|
|
|
|
|
class HTLCWithStatus(NamedTuple):
|
|
channel_id: bytes
|
|
htlc: UpdateAddHtlc
|
|
direction: Direction
|
|
status: str
|
|
|
|
|
|
class AbstractChannel(Logger, ABC):
|
|
storage: Union['StoredDict', dict]
|
|
config: Dict[HTLCOwner, Union[LocalConfig, RemoteConfig]]
|
|
_sweep_info: Dict[str, Dict[str, 'SweepInfo']]
|
|
lnworker: Optional['LNWallet']
|
|
channel_id: bytes
|
|
short_channel_id: Optional[ShortChannelID] = None
|
|
funding_outpoint: Outpoint
|
|
node_id: bytes # note that it might not be the full 33 bytes; for OCB it is only the prefix
|
|
_state: ChannelState
|
|
|
|
def set_short_channel_id(self, short_id: ShortChannelID) -> None:
|
|
self.short_channel_id = short_id
|
|
self.storage["short_channel_id"] = short_id
|
|
|
|
def get_id_for_log(self) -> str:
|
|
scid = self.short_channel_id
|
|
if scid:
|
|
return str(scid)
|
|
return self.channel_id.hex()
|
|
|
|
def short_id_for_GUI(self) -> str:
|
|
return format_short_channel_id(self.short_channel_id)
|
|
|
|
def diagnostic_name(self):
|
|
return self.get_id_for_log()
|
|
|
|
def set_state(self, state: ChannelState, *, force: bool = False) -> None:
|
|
"""Set on-chain state.
|
|
`force` can be set while debugging from the console to allow illegal transitions.
|
|
"""
|
|
old_state = self._state
|
|
if not force and (old_state, state) not in state_transitions:
|
|
raise Exception(f"Transition not allowed: {old_state.name} -> {state.name}")
|
|
self.logger.debug(f'Setting channel state: {old_state.name} -> {state.name}')
|
|
self._state = state
|
|
self.storage['state'] = self._state.name
|
|
if self.lnworker:
|
|
self.lnworker.channel_state_changed(self)
|
|
|
|
def get_state(self) -> ChannelState:
|
|
return self._state
|
|
|
|
def is_funded(self):
|
|
return self.get_state() >= ChannelState.FUNDED
|
|
|
|
def is_open(self):
|
|
return self.get_state() == ChannelState.OPEN
|
|
|
|
def is_closed(self):
|
|
# the closing txid has been saved
|
|
return self.get_state() >= ChannelState.CLOSING
|
|
|
|
def is_redeemed(self):
|
|
return self.get_state() == ChannelState.REDEEMED
|
|
|
|
def need_to_subscribe(self) -> bool:
|
|
"""Whether lnwatcher/synchronizer need to be watching this channel."""
|
|
if not self.is_redeemed():
|
|
return True
|
|
# Chan already deeply closed. Still, if some txs are missing, we should sub.
|
|
# check we have funding tx
|
|
# note: tx might not be directly related to the wallet, e.g. chan opened by remote
|
|
if (funding_item := self.get_funding_height()) is None:
|
|
return True
|
|
if self.lnworker:
|
|
funding_txid, funding_height, funding_timestamp = funding_item
|
|
if self.lnworker.wallet.adb.get_transaction(funding_txid) is None:
|
|
return True
|
|
# check we have closing tx
|
|
# note: tx might not be directly related to the wallet, e.g. local-fclose
|
|
if (closing_item := self.get_closing_height()) is None:
|
|
return True
|
|
if self.lnworker:
|
|
closing_txid, closing_height, closing_timestamp = closing_item
|
|
if self.lnworker.wallet.adb.get_transaction(closing_txid) is None:
|
|
return True
|
|
return False
|
|
|
|
@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
|
|
|
|
def get_funding_height(self):
|
|
return self.storage.get('funding_height')
|
|
|
|
def delete_funding_height(self):
|
|
self.storage.pop('funding_height', None)
|
|
|
|
def save_closing_height(self, *, txid: str, height: int, timestamp: Optional[int]) -> None:
|
|
self.storage['closing_height'] = txid, height, timestamp
|
|
|
|
def get_closing_height(self):
|
|
return self.storage.get('closing_height')
|
|
|
|
def delete_closing_height(self):
|
|
self.storage.pop('closing_height', None)
|
|
|
|
def create_sweeptxs_for_our_ctx(self, ctx):
|
|
return create_sweeptxs_for_our_ctx(chan=self, ctx=ctx, sweep_address=self.sweep_address)
|
|
|
|
def create_sweeptxs_for_their_ctx(self, ctx):
|
|
return create_sweeptxs_for_their_ctx(chan=self, ctx=ctx, sweep_address=self.sweep_address)
|
|
|
|
def is_backup(self):
|
|
return False
|
|
|
|
def sweep_ctx(self, ctx: Transaction) -> Dict[str, SweepInfo]:
|
|
txid = ctx.txid()
|
|
if self._sweep_info.get(txid) is None:
|
|
our_sweep_info = self.create_sweeptxs_for_our_ctx(ctx)
|
|
their_sweep_info = self.create_sweeptxs_for_their_ctx(ctx)
|
|
if our_sweep_info is not None:
|
|
self._sweep_info[txid] = our_sweep_info
|
|
self.logger.info(f'we (local) force closed')
|
|
elif their_sweep_info is not None:
|
|
self._sweep_info[txid] = their_sweep_info
|
|
self.logger.info(f'they (remote) force closed.')
|
|
else:
|
|
self._sweep_info[txid] = {}
|
|
self.logger.info(f'not sure who closed.')
|
|
return self._sweep_info[txid]
|
|
|
|
def update_onchain_state(self, *, funding_txid: str, funding_height: TxMinedInfo,
|
|
closing_txid: str, closing_height: TxMinedInfo, keep_watching: bool) -> None:
|
|
# 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_txid,
|
|
funding_height=funding_height)
|
|
else:
|
|
self.update_closed_state(
|
|
funding_txid=funding_txid,
|
|
funding_height=funding_height,
|
|
closing_txid=closing_txid,
|
|
closing_height=closing_height,
|
|
keep_watching=keep_watching)
|
|
|
|
def update_unfunded_state(self):
|
|
self.delete_funding_height()
|
|
self.delete_closing_height()
|
|
if self.get_state() in [ChannelState.PREOPENING, ChannelState.OPENING, ChannelState.FORCE_CLOSING] and self.lnworker:
|
|
if self.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(ChannelState.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.adb.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(ChannelState.REDEEMED)
|
|
break
|
|
else:
|
|
now = int(time.time())
|
|
if self.lnworker and (now - self.storage.get('init_timestamp', 0) > CHANNEL_OPENING_TIMEOUT):
|
|
self.lnworker.remove_channel(self.channel_id)
|
|
|
|
def update_funded_state(self, *, funding_txid: str, funding_height: TxMinedInfo) -> None:
|
|
self.save_funding_height(txid=funding_txid, height=funding_height.height, timestamp=funding_height.timestamp)
|
|
self.delete_closing_height()
|
|
if funding_height.conf>0:
|
|
self.set_short_channel_id(ShortChannelID.from_components(
|
|
funding_height.height, funding_height.txpos, self.funding_outpoint.output_index))
|
|
if self.get_state() == ChannelState.OPENING:
|
|
if self.is_funding_tx_mined(funding_height):
|
|
self.set_state(ChannelState.FUNDED)
|
|
|
|
def update_closed_state(self, *, funding_txid: str, funding_height: TxMinedInfo,
|
|
closing_txid: str, closing_height: TxMinedInfo, keep_watching: bool) -> None:
|
|
self.save_funding_height(txid=funding_txid, height=funding_height.height, timestamp=funding_height.timestamp)
|
|
self.save_closing_height(txid=closing_txid, height=closing_height.height, timestamp=closing_height.timestamp)
|
|
if funding_height.conf>0:
|
|
self.set_short_channel_id(ShortChannelID.from_components(
|
|
funding_height.height, funding_height.txpos, self.funding_outpoint.output_index))
|
|
if self.get_state() < ChannelState.CLOSED:
|
|
conf = closing_height.conf
|
|
if conf > 0:
|
|
self.set_state(ChannelState.CLOSED)
|
|
else:
|
|
# we must not trust the server with unconfirmed transactions,
|
|
# because the state transition is irreversible. if the remote
|
|
# force closed, we remain OPEN until the closing tx is confirmed
|
|
self.unconfirmed_closing_txid = closing_txid
|
|
if self.lnworker:
|
|
util.trigger_callback('channel', self.lnworker.wallet, self)
|
|
|
|
if self.get_state() == ChannelState.CLOSED and not keep_watching:
|
|
self.set_state(ChannelState.REDEEMED)
|
|
if self.lnworker and self.is_backup():
|
|
# auto-remove redeemed backups
|
|
self.lnworker.remove_channel_backup(self.channel_id)
|
|
|
|
|
|
@abstractmethod
|
|
def is_initiator(self) -> bool:
|
|
pass
|
|
|
|
@abstractmethod
|
|
def is_funding_tx_mined(self, funding_height: TxMinedInfo) -> bool:
|
|
pass
|
|
|
|
@abstractmethod
|
|
def get_funding_address(self) -> str:
|
|
pass
|
|
|
|
def get_state_for_GUI(self) -> str:
|
|
cs = self.get_state()
|
|
if cs <= ChannelState.OPEN and self.unconfirmed_closing_txid:
|
|
return 'FORCE-CLOSING'
|
|
return cs.name
|
|
|
|
@abstractmethod
|
|
def get_oldest_unrevoked_ctn(self, subject: HTLCOwner) -> int:
|
|
pass
|
|
|
|
@abstractmethod
|
|
def included_htlcs(self, subject: HTLCOwner, direction: Direction, ctn: int = None) -> Sequence[UpdateAddHtlc]:
|
|
pass
|
|
|
|
@abstractmethod
|
|
def funding_txn_minimum_depth(self) -> int:
|
|
pass
|
|
|
|
@abstractmethod
|
|
def balance(self, whose: HTLCOwner, *, ctx_owner=HTLCOwner.LOCAL, ctn: int = None) -> int:
|
|
"""This balance (in msat) only considers HTLCs that have been settled by ctn.
|
|
It disregards reserve, fees, and pending HTLCs (in both directions).
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def balance_minus_outgoing_htlcs(self, whose: HTLCOwner, *,
|
|
ctx_owner: HTLCOwner = HTLCOwner.LOCAL,
|
|
ctn: int = None) -> int:
|
|
"""This balance (in msat), which includes the value of
|
|
pending outgoing HTLCs, is used in the UI.
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def is_frozen_for_sending(self) -> bool:
|
|
"""Whether the user has marked this channel as frozen for sending.
|
|
Frozen channels are not supposed to be used for new outgoing payments.
|
|
(note that payment-forwarding ignores this option)
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def is_frozen_for_receiving(self) -> bool:
|
|
"""Whether the user has marked this channel as frozen for receiving.
|
|
Frozen channels are not supposed to be used for new incoming payments.
|
|
(note that payment-forwarding ignores this option)
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def get_local_pubkey(self) -> bytes:
|
|
"""Returns our node ID."""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def get_capacity(self) -> Optional[int]:
|
|
"""Returns channel capacity in satoshis, or None if unknown."""
|
|
pass
|
|
|
|
|
|
class ChannelBackup(AbstractChannel):
|
|
"""
|
|
current capabilities:
|
|
- detect force close
|
|
- request force close
|
|
- sweep my ctx to_local
|
|
future:
|
|
- will need to sweep their ctx to_remote
|
|
"""
|
|
|
|
def __init__(self, cb: ChannelBackupStorage, *, lnworker=None):
|
|
self.name = None
|
|
self.cb = cb
|
|
self.is_imported = isinstance(self.cb, ImportedChannelBackupStorage)
|
|
self._sweep_info = {}
|
|
self.storage = {} # dummy storage
|
|
self._state = ChannelState.OPENING
|
|
self.node_id = cb.node_id if self.is_imported else cb.node_id_prefix
|
|
self.channel_id = cb.channel_id()
|
|
self.funding_outpoint = cb.funding_outpoint()
|
|
self.lnworker = lnworker
|
|
self.short_channel_id = None
|
|
Logger.__init__(self)
|
|
self.config = {}
|
|
if self.is_imported:
|
|
self.init_config(cb)
|
|
self.unconfirmed_closing_txid = None # not a state, only for GUI
|
|
|
|
def init_config(self, cb):
|
|
self.config[LOCAL] = LocalConfig.from_seed(
|
|
channel_seed=cb.channel_seed,
|
|
to_self_delay=cb.local_delay,
|
|
# dummy values
|
|
static_remotekey=None,
|
|
dust_limit_sat=None,
|
|
max_htlc_value_in_flight_msat=None,
|
|
max_accepted_htlcs=None,
|
|
initial_msat=None,
|
|
reserve_sat=None,
|
|
funding_locked_received=False,
|
|
was_announced=False,
|
|
current_commitment_signature=None,
|
|
current_htlc_signatures=b'',
|
|
htlc_minimum_msat=1,
|
|
upfront_shutdown_script='')
|
|
self.config[REMOTE] = RemoteConfig(
|
|
# payment_basepoint needed to deobfuscate ctn in our_ctx
|
|
payment_basepoint=OnlyPubkeyKeypair(cb.remote_payment_pubkey),
|
|
# revocation_basepoint is used to claim to_local in our ctx
|
|
revocation_basepoint=OnlyPubkeyKeypair(cb.remote_revocation_pubkey),
|
|
to_self_delay=cb.remote_delay,
|
|
# dummy values
|
|
multisig_key=OnlyPubkeyKeypair(None),
|
|
htlc_basepoint=OnlyPubkeyKeypair(None),
|
|
delayed_basepoint=OnlyPubkeyKeypair(None),
|
|
dust_limit_sat=None,
|
|
max_htlc_value_in_flight_msat=None,
|
|
max_accepted_htlcs=None,
|
|
initial_msat = None,
|
|
reserve_sat = None,
|
|
htlc_minimum_msat=None,
|
|
next_per_commitment_point=None,
|
|
current_per_commitment_point=None,
|
|
upfront_shutdown_script='')
|
|
|
|
def can_be_deleted(self):
|
|
return self.is_imported or self.is_redeemed()
|
|
|
|
def get_capacity(self):
|
|
lnwatcher = self.lnworker.lnwatcher
|
|
if lnwatcher:
|
|
# fixme: we should probably not call that method here
|
|
return lnwatcher.adb.get_tx_delta(self.funding_outpoint.txid, self.cb.funding_address)
|
|
return None
|
|
|
|
def is_backup(self):
|
|
return True
|
|
|
|
def get_remote_alias(self) -> Optional[bytes]:
|
|
return None
|
|
|
|
def create_sweeptxs_for_their_ctx(self, ctx):
|
|
return {}
|
|
|
|
def create_sweeptxs_for_our_ctx(self, ctx):
|
|
if self.is_imported:
|
|
return create_sweeptxs_for_our_ctx(chan=self, ctx=ctx, sweep_address=self.sweep_address)
|
|
else:
|
|
# backup from op_return
|
|
return {}
|
|
|
|
def get_funding_address(self):
|
|
return self.cb.funding_address
|
|
|
|
def is_initiator(self):
|
|
return self.cb.is_initiator
|
|
|
|
def get_oldest_unrevoked_ctn(self, who):
|
|
return -1
|
|
|
|
def included_htlcs(self, subject, direction, ctn=None):
|
|
return []
|
|
|
|
def funding_txn_minimum_depth(self):
|
|
return 1
|
|
|
|
def is_funding_tx_mined(self, funding_height):
|
|
return funding_height.conf > 1
|
|
|
|
def balance_minus_outgoing_htlcs(self, whose: HTLCOwner, *, ctx_owner: HTLCOwner = HTLCOwner.LOCAL, ctn: int = None):
|
|
return 0
|
|
|
|
def balance(self, whose: HTLCOwner, *, ctx_owner=HTLCOwner.LOCAL, ctn: int = None) -> int:
|
|
return 0
|
|
|
|
def is_frozen_for_sending(self) -> bool:
|
|
return False
|
|
|
|
def is_frozen_for_receiving(self) -> bool:
|
|
return False
|
|
|
|
@property
|
|
def sweep_address(self) -> str:
|
|
# Since channel backups do not save the static_remotekey, payment_basepoint in
|
|
# their local config is not static)
|
|
return self.lnworker.wallet.get_new_sweep_address_for_channel()
|
|
|
|
def get_local_pubkey(self) -> bytes:
|
|
cb = self.cb
|
|
assert isinstance(cb, ChannelBackupStorage)
|
|
if isinstance(cb, ImportedChannelBackupStorage):
|
|
return ecc.ECPrivkey(cb.privkey).get_public_key_bytes(compressed=True)
|
|
if isinstance(cb, OnchainChannelBackupStorage):
|
|
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".
|
|
# they are ambiguous. Use "oldest_unrevoked" or "latest" or "next".
|
|
# TODO enforce this ^
|
|
|
|
# our forwarding parameters for forwarding HTLCs through this channel
|
|
forwarding_cltv_expiry_delta = 144
|
|
forwarding_fee_base_msat = 1000
|
|
forwarding_fee_proportional_millionths = 1
|
|
|
|
def __repr__(self):
|
|
return "Channel(%s)"%self.get_id_for_log()
|
|
|
|
def __init__(self, state: 'StoredDict', *, name=None, lnworker=None, initial_feerate=None):
|
|
self.name = name
|
|
self.channel_id = bfh(state["channel_id"])
|
|
self.short_channel_id = ShortChannelID.normalize(state["short_channel_id"])
|
|
Logger.__init__(self) # should be after short_channel_id is set
|
|
self.lnworker = lnworker
|
|
self.storage = state
|
|
self.db_lock = self.storage.db.lock if self.storage.db else threading.RLock()
|
|
self.config = {}
|
|
self.config[LOCAL] = state["local_config"]
|
|
self.config[REMOTE] = state["remote_config"]
|
|
self.constraints = state["constraints"] # type: ChannelConstraints
|
|
self.funding_outpoint = state["funding_outpoint"]
|
|
self.node_id = bfh(state["node_id"])
|
|
self.onion_keys = state['onion_keys'] # type: Dict[int, bytes]
|
|
self.data_loss_protect_remote_pcp = state['data_loss_protect_remote_pcp']
|
|
self.hm = HTLCManager(log=state['log'], initial_feerate=initial_feerate)
|
|
self.fail_htlc_reasons = state["fail_htlc_reasons"]
|
|
self.unfulfilled_htlcs = state["unfulfilled_htlcs"]
|
|
self._state = ChannelState[state['state']]
|
|
self.peer_state = PeerState.DISCONNECTED
|
|
self._sweep_info = {}
|
|
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
|
|
self._receive_fail_reasons = {} # type: Dict[int, (bytes, OnionRoutingFailure)]
|
|
self.should_request_force_close = False
|
|
self.unconfirmed_closing_txid = None # not a state, only for GUI
|
|
|
|
def get_local_alias(self) -> bytes:
|
|
# deterministic, same secrecy level as wallet master pubkey
|
|
wallet_fingerprint = bytes(self.lnworker.wallet.get_fingerprint(), "utf8")
|
|
return sha256(wallet_fingerprint + self.channel_id)[0:8]
|
|
|
|
def save_remote_alias(self, alias: bytes):
|
|
self.storage['alias'] = alias.hex()
|
|
|
|
def get_remote_alias(self) -> Optional[bytes]:
|
|
alias = self.storage.get('alias')
|
|
return bytes.fromhex(alias) if alias else None
|
|
|
|
def has_onchain_backup(self):
|
|
return self.storage.get('has_onchain_backup', False)
|
|
|
|
def can_be_deleted(self):
|
|
return self.is_redeemed()
|
|
|
|
def get_capacity(self):
|
|
return self.constraints.capacity
|
|
|
|
def is_initiator(self):
|
|
return self.constraints.is_initiator
|
|
|
|
def is_active(self):
|
|
return self.get_state() == ChannelState.OPEN and self.peer_state == PeerState.GOOD
|
|
|
|
def funding_txn_minimum_depth(self):
|
|
return self.constraints.funding_txn_minimum_depth
|
|
|
|
def diagnostic_name(self):
|
|
if self.name:
|
|
return str(self.name)
|
|
return super().diagnostic_name()
|
|
|
|
def set_onion_key(self, key: int, value: bytes):
|
|
self.onion_keys[key] = value
|
|
|
|
def get_onion_key(self, key: int) -> bytes:
|
|
return self.onion_keys.get(key)
|
|
|
|
def set_data_loss_protect_remote_pcp(self, key, value):
|
|
self.data_loss_protect_remote_pcp[key] = value
|
|
|
|
def get_data_loss_protect_remote_pcp(self, key):
|
|
return self.data_loss_protect_remote_pcp.get(key)
|
|
|
|
def get_local_pubkey(self) -> bytes:
|
|
if not self.lnworker:
|
|
raise Exception('lnworker not set for channel!')
|
|
return self.lnworker.node_keypair.pubkey
|
|
|
|
def set_remote_update(self, payload: dict) -> None:
|
|
"""Save the ChannelUpdate message for the incoming direction of this channel.
|
|
This message contains info we need to populate private route hints when
|
|
creating invoices.
|
|
"""
|
|
from .channel_db import ChannelDB
|
|
ChannelDB.verify_channel_update(payload, start_node=self.node_id)
|
|
raw = payload['raw']
|
|
self.storage['remote_update'] = raw.hex()
|
|
|
|
def get_remote_update(self) -> Optional[bytes]:
|
|
return bfh(self.storage.get('remote_update')) if self.storage.get('remote_update') else None
|
|
|
|
def add_or_update_peer_addr(self, peer: LNPeerAddr) -> None:
|
|
if 'peer_network_addresses' not in self.storage:
|
|
self.storage['peer_network_addresses'] = {}
|
|
now = int(time.time())
|
|
self.storage['peer_network_addresses'][peer.net_addr_str()] = now
|
|
|
|
def get_peer_addresses(self) -> Iterator[LNPeerAddr]:
|
|
# sort by timestamp: most recent first
|
|
addrs = sorted(self.storage.get('peer_network_addresses', {}).items(),
|
|
key=lambda x: x[1], reverse=True)
|
|
for net_addr_str, ts in addrs:
|
|
net_addr = NetAddress.from_string(net_addr_str)
|
|
yield LNPeerAddr(host=str(net_addr.host), port=net_addr.port, pubkey=self.node_id)
|
|
|
|
def get_outgoing_gossip_channel_update(self) -> bytes:
|
|
if self._outgoing_channel_update is not None:
|
|
return self._outgoing_channel_update
|
|
if not self.lnworker:
|
|
raise Exception('lnworker not set for channel!')
|
|
sorted_node_ids = list(sorted([self.node_id, self.get_local_pubkey()]))
|
|
channel_flags = b'\x00' if sorted_node_ids[0] == self.get_local_pubkey() else b'\x01'
|
|
now = int(time.time())
|
|
htlc_maximum_msat = min(self.config[REMOTE].max_htlc_value_in_flight_msat, 1000 * self.constraints.capacity)
|
|
|
|
chan_upd = encode_msg(
|
|
"channel_update",
|
|
short_channel_id=self.short_channel_id,
|
|
channel_flags=channel_flags,
|
|
message_flags=b'\x01',
|
|
cltv_expiry_delta=self.forwarding_cltv_expiry_delta,
|
|
htlc_minimum_msat=self.config[REMOTE].htlc_minimum_msat,
|
|
htlc_maximum_msat=htlc_maximum_msat,
|
|
fee_base_msat=self.forwarding_fee_base_msat,
|
|
fee_proportional_millionths=self.forwarding_fee_proportional_millionths,
|
|
chain_hash=constants.net.rev_genesis_bytes(),
|
|
timestamp=now,
|
|
)
|
|
sighash = sha256d(chan_upd[2 + 64:])
|
|
sig = ecc.ECPrivkey(self.lnworker.node_keypair.privkey).sign(sighash, ecc.sig_string_from_r_and_s)
|
|
message_type, payload = decode_msg(chan_upd)
|
|
payload['signature'] = sig
|
|
chan_upd = encode_msg(message_type, **payload)
|
|
|
|
self._outgoing_channel_update = chan_upd
|
|
return chan_upd
|
|
|
|
def construct_channel_announcement_without_sigs(self) -> bytes:
|
|
if self._chan_ann_without_sigs is not None:
|
|
return self._chan_ann_without_sigs
|
|
if not self.lnworker:
|
|
raise Exception('lnworker not set for channel!')
|
|
|
|
bitcoin_keys = [self.config[REMOTE].multisig_key.pubkey,
|
|
self.config[LOCAL].multisig_key.pubkey]
|
|
node_ids = [self.node_id, self.get_local_pubkey()]
|
|
sorted_node_ids = list(sorted(node_ids))
|
|
if sorted_node_ids != node_ids:
|
|
node_ids = sorted_node_ids
|
|
bitcoin_keys.reverse()
|
|
|
|
chan_ann = encode_msg(
|
|
"channel_announcement",
|
|
len=0,
|
|
features=b'',
|
|
chain_hash=constants.net.rev_genesis_bytes(),
|
|
short_channel_id=self.short_channel_id,
|
|
node_id_1=node_ids[0],
|
|
node_id_2=node_ids[1],
|
|
bitcoin_key_1=bitcoin_keys[0],
|
|
bitcoin_key_2=bitcoin_keys[1],
|
|
)
|
|
|
|
self._chan_ann_without_sigs = chan_ann
|
|
return chan_ann
|
|
|
|
def is_static_remotekey_enabled(self) -> bool:
|
|
channel_type = ChannelType(self.storage.get('channel_type'))
|
|
return bool(channel_type & ChannelType.OPTION_STATIC_REMOTEKEY)
|
|
|
|
@property
|
|
def sweep_address(self) -> str:
|
|
# TODO: in case of unilateral close with pending HTLCs, this address will be reused
|
|
addr = None
|
|
assert self.is_static_remotekey_enabled()
|
|
our_payment_pubkey = self.config[LOCAL].payment_basepoint.pubkey
|
|
addr = make_commitment_output_to_remote_address(our_payment_pubkey)
|
|
if self.lnworker:
|
|
assert self.lnworker.wallet.is_mine(addr)
|
|
return addr
|
|
|
|
def get_wallet_addresses_channel_might_want_reserved(self) -> Sequence[str]:
|
|
assert self.is_static_remotekey_enabled()
|
|
our_payment_pubkey = self.config[LOCAL].payment_basepoint.pubkey
|
|
to_remote_address = make_commitment_output_to_remote_address(our_payment_pubkey)
|
|
return [to_remote_address]
|
|
|
|
def get_feerate(self, subject: HTLCOwner, *, ctn: int) -> int:
|
|
# returns feerate in sat/kw
|
|
return self.hm.get_feerate(subject, ctn)
|
|
|
|
def get_oldest_unrevoked_feerate(self, subject: HTLCOwner) -> int:
|
|
return self.hm.get_feerate_in_oldest_unrevoked_ctx(subject)
|
|
|
|
def get_latest_feerate(self, subject: HTLCOwner) -> int:
|
|
return self.hm.get_feerate_in_latest_ctx(subject)
|
|
|
|
def get_next_feerate(self, subject: HTLCOwner) -> int:
|
|
return self.hm.get_feerate_in_next_ctx(subject)
|
|
|
|
def get_payments(self, status=None) -> Mapping[bytes, List[HTLCWithStatus]]:
|
|
out = defaultdict(list)
|
|
for direction, htlc in self.hm.all_htlcs_ever():
|
|
htlc_proposer = LOCAL if direction is SENT else REMOTE
|
|
if self.hm.was_htlc_failed(htlc_id=htlc.htlc_id, htlc_proposer=htlc_proposer):
|
|
_status = 'failed'
|
|
elif self.hm.was_htlc_preimage_released(htlc_id=htlc.htlc_id, htlc_proposer=htlc_proposer):
|
|
_status = 'settled'
|
|
else:
|
|
_status = 'inflight'
|
|
if status and status != _status:
|
|
continue
|
|
htlc_with_status = HTLCWithStatus(
|
|
channel_id=self.channel_id, htlc=htlc, direction=direction, status=_status)
|
|
out[htlc.payment_hash].append(htlc_with_status)
|
|
return out
|
|
|
|
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.hm.channel_open_finished()
|
|
self.peer_state = PeerState.GOOD
|
|
|
|
def get_state_for_GUI(self):
|
|
cs_name = super().get_state_for_GUI()
|
|
if self.is_closed() or self.unconfirmed_closing_txid:
|
|
return cs_name
|
|
ps = self.peer_state
|
|
if ps != PeerState.GOOD:
|
|
return ps.name
|
|
return cs_name
|
|
|
|
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 self.get_state() not in (ChannelState.OPEN, ChannelState.SHUTDOWN):
|
|
return False
|
|
if self.peer_state != PeerState.GOOD:
|
|
return False
|
|
if not self._can_send_ctx_updates:
|
|
return False
|
|
return True
|
|
|
|
def can_send_update_add_htlc(self) -> bool:
|
|
return self.can_send_ctx_updates() and self.is_open()
|
|
|
|
def is_frozen_for_sending(self) -> bool:
|
|
if self.lnworker and self.lnworker.uses_trampoline() and not self.lnworker.is_trampoline_peer(self.node_id):
|
|
return True
|
|
return self.storage.get('frozen_for_sending', False)
|
|
|
|
def set_frozen_for_sending(self, b: bool) -> None:
|
|
self.storage['frozen_for_sending'] = bool(b)
|
|
util.trigger_callback('channel', self.lnworker.wallet, self)
|
|
|
|
def is_frozen_for_receiving(self) -> bool:
|
|
return self.storage.get('frozen_for_receiving', False)
|
|
|
|
def set_frozen_for_receiving(self, b: bool) -> None:
|
|
self.storage['frozen_for_receiving'] = bool(b)
|
|
util.trigger_callback('channel', self.lnworker.wallet, self)
|
|
|
|
def _assert_can_add_htlc(self, *, htlc_proposer: HTLCOwner, amount_msat: int,
|
|
ignore_min_htlc_value: bool = False) -> None:
|
|
"""Raises PaymentFailure if the htlc_proposer cannot add this new HTLC.
|
|
(this is relevant both for forwarding and endpoint)
|
|
"""
|
|
htlc_receiver = htlc_proposer.inverted()
|
|
# note: all these tests are about the *receiver's* *next* commitment transaction,
|
|
# 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.get_state() != ChannelState.OPEN:
|
|
raise PaymentFailure('Channel not open', self.get_state())
|
|
if htlc_proposer == LOCAL:
|
|
if not self.can_send_ctx_updates():
|
|
raise PaymentFailure('Channel cannot send ctx updates')
|
|
if not self.can_send_update_add_htlc():
|
|
raise PaymentFailure('Channel cannot add htlc')
|
|
|
|
# If proposer is LOCAL we apply stricter checks as that is behaviour we can control.
|
|
# This should lead to fewer disagreements (i.e. channels failing).
|
|
strict = (htlc_proposer == LOCAL)
|
|
|
|
# check htlc raw value
|
|
if not ignore_min_htlc_value:
|
|
if amount_msat <= 0:
|
|
raise PaymentFailure("HTLC value must be positive")
|
|
if amount_msat < chan_config.htlc_minimum_msat:
|
|
raise PaymentFailure(f'HTLC value too small: {amount_msat} msat')
|
|
|
|
# check proposer can afford htlc
|
|
max_can_send_msat = self.available_to_spend(htlc_proposer, strict=strict)
|
|
if max_can_send_msat < amount_msat:
|
|
raise PaymentFailure(f'Not enough balance. can send: {max_can_send_msat}, tried: {amount_msat}')
|
|
|
|
# check "max_accepted_htlcs"
|
|
# this is the loose check BOLT-02 specifies:
|
|
if len(self.hm.htlcs_by_direction(htlc_receiver, direction=RECEIVED, ctn=ctn)) + 1 > chan_config.max_accepted_htlcs:
|
|
raise PaymentFailure('Too many HTLCs already in channel')
|
|
# however, c-lightning is a lot stricter, so extra checks:
|
|
# https://github.com/ElementsProject/lightning/blob/4dcd4ca1556b13b6964a10040ba1d5ef82de4788/channeld/full_channel.c#L581
|
|
if strict:
|
|
max_concurrent_htlcs = min(self.config[htlc_proposer].max_accepted_htlcs,
|
|
self.config[htlc_receiver].max_accepted_htlcs)
|
|
if len(self.hm.htlcs(htlc_receiver, ctn=ctn)) + 1 > max_concurrent_htlcs:
|
|
raise PaymentFailure('Too many HTLCs already in channel')
|
|
|
|
# check "max_htlc_value_in_flight_msat"
|
|
current_htlc_sum = htlcsum(self.hm.htlcs_by_direction(htlc_receiver, direction=RECEIVED, ctn=ctn).values())
|
|
if current_htlc_sum + amount_msat > chan_config.max_htlc_value_in_flight_msat:
|
|
raise PaymentFailure(f'HTLC value sum (sum of pending htlcs: {current_htlc_sum/1000} sat '
|
|
f'plus new htlc: {amount_msat/1000} sat) '
|
|
f'would exceed max allowed: {chan_config.max_htlc_value_in_flight_msat/1000} sat')
|
|
|
|
def can_pay(self, amount_msat: int, *, check_frozen=False) -> bool:
|
|
"""Returns whether we can add an HTLC of given value."""
|
|
if check_frozen and self.is_frozen_for_sending():
|
|
return False
|
|
try:
|
|
self._assert_can_add_htlc(htlc_proposer=LOCAL, amount_msat=amount_msat)
|
|
except PaymentFailure:
|
|
return False
|
|
return True
|
|
|
|
def can_receive(self, amount_msat: int, *, check_frozen=False,
|
|
ignore_min_htlc_value: bool = False) -> bool:
|
|
"""Returns whether the remote can add an HTLC of given value."""
|
|
if check_frozen and self.is_frozen_for_receiving():
|
|
return False
|
|
try:
|
|
self._assert_can_add_htlc(htlc_proposer=REMOTE,
|
|
amount_msat=amount_msat,
|
|
ignore_min_htlc_value=ignore_min_htlc_value)
|
|
except PaymentFailure:
|
|
return False
|
|
return True
|
|
|
|
def should_try_to_reestablish_peer(self) -> bool:
|
|
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])
|
|
return redeem_script_to_address('p2wsh', script)
|
|
|
|
def add_htlc(self, htlc: UpdateAddHtlc) -> UpdateAddHtlc:
|
|
"""Adds a new LOCAL HTLC to the channel.
|
|
Action must be initiated by LOCAL.
|
|
"""
|
|
if isinstance(htlc, dict): # legacy conversion # FIXME remove
|
|
htlc = UpdateAddHtlc(**htlc)
|
|
assert isinstance(htlc, UpdateAddHtlc)
|
|
self._assert_can_add_htlc(htlc_proposer=LOCAL, amount_msat=htlc.amount_msat)
|
|
if htlc.htlc_id is None:
|
|
htlc = attr.evolve(htlc, htlc_id=self.hm.get_next_htlc_id(LOCAL))
|
|
with self.db_lock:
|
|
self.hm.send_htlc(htlc)
|
|
self.logger.info("add_htlc")
|
|
return htlc
|
|
|
|
def receive_htlc(self, htlc: UpdateAddHtlc, onion_packet:bytes = None) -> UpdateAddHtlc:
|
|
"""Adds a new REMOTE HTLC to the channel.
|
|
Action must be initiated by REMOTE.
|
|
"""
|
|
if isinstance(htlc, dict): # legacy conversion # FIXME remove
|
|
htlc = UpdateAddHtlc(**htlc)
|
|
assert isinstance(htlc, UpdateAddHtlc)
|
|
try:
|
|
self._assert_can_add_htlc(htlc_proposer=REMOTE, amount_msat=htlc.amount_msat)
|
|
except PaymentFailure as e:
|
|
raise RemoteMisbehaving(e) from e
|
|
if htlc.htlc_id is None: # used in unit tests
|
|
htlc = attr.evolve(htlc, htlc_id=self.hm.get_next_htlc_id(REMOTE))
|
|
with self.db_lock:
|
|
self.hm.recv_htlc(htlc)
|
|
local_ctn = self.get_latest_ctn(LOCAL)
|
|
remote_ctn = self.get_latest_ctn(REMOTE)
|
|
if onion_packet:
|
|
# TODO neither local_ctn nor remote_ctn are used anymore... no point storing them.
|
|
self.unfulfilled_htlcs[htlc.htlc_id] = local_ctn, remote_ctn, onion_packet.hex(), False
|
|
|
|
self.logger.info("receive_htlc")
|
|
return htlc
|
|
|
|
def sign_next_commitment(self) -> Tuple[bytes, Sequence[bytes]]:
|
|
"""Returns signatures for our next remote commitment tx.
|
|
Action must be initiated by LOCAL.
|
|
Finally, the next remote ctx becomes the latest remote ctx.
|
|
"""
|
|
# TODO: when more channel types are supported, this method should depend on channel type
|
|
next_remote_ctn = self.get_next_ctn(REMOTE)
|
|
self.logger.info(f"sign_next_commitment. ctn={next_remote_ctn}")
|
|
|
|
pending_remote_commitment = self.get_next_commitment(REMOTE)
|
|
sig_64 = sign_and_get_sig_string(pending_remote_commitment, self.config[LOCAL], self.config[REMOTE])
|
|
self.logger.debug(f"sign_next_commitment. {pending_remote_commitment.serialize()=}. {sig_64.hex()=}")
|
|
|
|
their_remote_htlc_privkey_number = derive_privkey(
|
|
int.from_bytes(self.config[LOCAL].htlc_basepoint.privkey, 'big'),
|
|
self.config[REMOTE].next_per_commitment_point)
|
|
their_remote_htlc_privkey = their_remote_htlc_privkey_number.to_bytes(32, 'big')
|
|
|
|
htlcsigs = []
|
|
htlc_to_ctx_output_idx_map = map_htlcs_to_ctx_output_idxs(chan=self,
|
|
ctx=pending_remote_commitment,
|
|
pcp=self.config[REMOTE].next_per_commitment_point,
|
|
subject=REMOTE,
|
|
ctn=next_remote_ctn)
|
|
for (direction, htlc), (ctx_output_idx, htlc_relative_idx) in htlc_to_ctx_output_idx_map.items():
|
|
_script, htlc_tx = make_htlc_tx_with_open_channel(chan=self,
|
|
pcp=self.config[REMOTE].next_per_commitment_point,
|
|
subject=REMOTE,
|
|
ctn=next_remote_ctn,
|
|
htlc_direction=direction,
|
|
commit=pending_remote_commitment,
|
|
ctx_output_idx=ctx_output_idx,
|
|
htlc=htlc)
|
|
sig = bfh(htlc_tx.sign_txin(0, their_remote_htlc_privkey))
|
|
htlc_sig = ecc.sig_string_from_der_sig(sig[:-1])
|
|
htlcsigs.append((ctx_output_idx, htlc_sig))
|
|
htlcsigs.sort()
|
|
htlcsigs = [x[1] for x in htlcsigs]
|
|
with self.db_lock:
|
|
self.hm.send_ctx()
|
|
return sig_64, htlcsigs
|
|
|
|
def receive_new_commitment(self, sig: bytes, htlc_sigs: Sequence[bytes]) -> None:
|
|
"""Processes signatures for our next local commitment tx, sent by the REMOTE.
|
|
Action must be initiated by REMOTE.
|
|
If all checks pass, the next local ctx becomes the latest local ctx.
|
|
"""
|
|
# TODO in many failure cases below, we should "fail" the channel (force-close)
|
|
# TODO: when more channel types are supported, this method should depend on channel type
|
|
next_local_ctn = self.get_next_ctn(LOCAL)
|
|
self.logger.info(f"receive_new_commitment. ctn={next_local_ctn}, len(htlc_sigs)={len(htlc_sigs)}")
|
|
|
|
assert len(htlc_sigs) == 0 or type(htlc_sigs[0]) is bytes
|
|
|
|
pending_local_commitment = self.get_next_commitment(LOCAL)
|
|
preimage_hex = pending_local_commitment.serialize_preimage(0)
|
|
pre_hash = sha256d(bfh(preimage_hex))
|
|
if not ecc.verify_signature(self.config[REMOTE].multisig_key.pubkey, sig, pre_hash):
|
|
raise LNProtocolWarning(
|
|
f'failed verifying signature for our updated commitment transaction. '
|
|
f'sig={sig.hex()}. '
|
|
f'pre_hash={pre_hash.hex()}. '
|
|
f'pubkey={self.config[REMOTE].multisig_key.pubkey}. '
|
|
f'ctx={pending_local_commitment.serialize()} '
|
|
)
|
|
|
|
htlc_sigs_string = b''.join(htlc_sigs)
|
|
|
|
_secret, pcp = self.get_secret_and_point(subject=LOCAL, ctn=next_local_ctn)
|
|
|
|
htlc_to_ctx_output_idx_map = map_htlcs_to_ctx_output_idxs(chan=self,
|
|
ctx=pending_local_commitment,
|
|
pcp=pcp,
|
|
subject=LOCAL,
|
|
ctn=next_local_ctn)
|
|
if len(htlc_to_ctx_output_idx_map) != len(htlc_sigs):
|
|
raise LNProtocolWarning(f'htlc sigs failure. recv {len(htlc_sigs)} sigs, expected {len(htlc_to_ctx_output_idx_map)}')
|
|
for (direction, htlc), (ctx_output_idx, htlc_relative_idx) in htlc_to_ctx_output_idx_map.items():
|
|
htlc_sig = htlc_sigs[htlc_relative_idx]
|
|
self._verify_htlc_sig(htlc=htlc,
|
|
htlc_sig=htlc_sig,
|
|
htlc_direction=direction,
|
|
pcp=pcp,
|
|
ctx=pending_local_commitment,
|
|
ctx_output_idx=ctx_output_idx,
|
|
ctn=next_local_ctn)
|
|
with self.db_lock:
|
|
self.hm.recv_ctx()
|
|
self.config[LOCAL].current_commitment_signature=sig
|
|
self.config[LOCAL].current_htlc_signatures=htlc_sigs_string
|
|
|
|
def _verify_htlc_sig(self, *, htlc: UpdateAddHtlc, htlc_sig: bytes, htlc_direction: Direction,
|
|
pcp: bytes, ctx: Transaction, ctx_output_idx: int, ctn: int) -> None:
|
|
_script, htlc_tx = make_htlc_tx_with_open_channel(chan=self,
|
|
pcp=pcp,
|
|
subject=LOCAL,
|
|
ctn=ctn,
|
|
htlc_direction=htlc_direction,
|
|
commit=ctx,
|
|
ctx_output_idx=ctx_output_idx,
|
|
htlc=htlc)
|
|
preimage_hex = htlc_tx.serialize_preimage(0)
|
|
pre_hash = sha256d(bfh(preimage_hex))
|
|
remote_htlc_pubkey = derive_pubkey(self.config[REMOTE].htlc_basepoint.pubkey, pcp)
|
|
if not ecc.verify_signature(remote_htlc_pubkey, htlc_sig, pre_hash):
|
|
raise LNProtocolWarning(
|
|
f'failed verifying HTLC signatures: {htlc=}, {htlc_direction=}. '
|
|
f'htlc_tx={htlc_tx.serialize()}. '
|
|
f'htlc_sig={htlc_sig.hex()}. '
|
|
f'remote_htlc_pubkey={remote_htlc_pubkey.hex()}. '
|
|
f'pre_hash={pre_hash.hex()}. '
|
|
f'ctx={ctx.serialize()}. '
|
|
f'ctx_output_idx={ctx_output_idx}. '
|
|
f'ctn={ctn}. '
|
|
)
|
|
|
|
def get_remote_htlc_sig_for_htlc(self, *, htlc_relative_idx: int) -> bytes:
|
|
data = self.config[LOCAL].current_htlc_signatures
|
|
htlc_sigs = list(chunks(data, 64))
|
|
htlc_sig = htlc_sigs[htlc_relative_idx]
|
|
remote_htlc_sig = ecc.der_sig_from_sig_string(htlc_sig) + Sighash.to_sigbytes(Sighash.ALL)
|
|
return remote_htlc_sig
|
|
|
|
def revoke_current_commitment(self):
|
|
self.logger.info("revoke_current_commitment")
|
|
new_ctn = self.get_latest_ctn(LOCAL)
|
|
new_ctx = self.get_latest_commitment(LOCAL)
|
|
if not self.signature_fits(new_ctx):
|
|
# this should never fail; as receive_new_commitment already did this test
|
|
raise Exception("refusing to revoke as remote sig does not fit")
|
|
with self.db_lock:
|
|
self.hm.send_rev()
|
|
last_secret, last_point = self.get_secret_and_point(LOCAL, new_ctn - 1)
|
|
next_secret, next_point = self.get_secret_and_point(LOCAL, new_ctn + 1)
|
|
return RevokeAndAck(last_secret, next_point)
|
|
|
|
def receive_revocation(self, revocation: RevokeAndAck):
|
|
self.logger.info("receive_revocation")
|
|
new_ctn = self.get_latest_ctn(REMOTE)
|
|
cur_point = self.config[REMOTE].current_per_commitment_point
|
|
derived_point = ecc.ECPrivkey(revocation.per_commitment_secret).get_public_key_bytes(compressed=True)
|
|
if cur_point != derived_point:
|
|
raise Exception('revoked secret not for current point')
|
|
with self.db_lock:
|
|
self.revocation_store.add_next_entry(revocation.per_commitment_secret)
|
|
##### start applying fee/htlc changes
|
|
self.hm.recv_rev()
|
|
self.config[REMOTE].current_per_commitment_point=self.config[REMOTE].next_per_commitment_point
|
|
self.config[REMOTE].next_per_commitment_point=revocation.next_per_commitment_point
|
|
assert new_ctn == self.get_oldest_unrevoked_ctn(REMOTE)
|
|
# lnworker callbacks
|
|
if self.lnworker:
|
|
sent = self.hm.sent_in_ctn(new_ctn)
|
|
for htlc in sent:
|
|
self.lnworker.htlc_fulfilled(self, htlc.payment_hash, htlc.htlc_id)
|
|
failed = self.hm.failed_in_ctn(new_ctn)
|
|
for htlc in failed:
|
|
try:
|
|
error_bytes, failure_message = self._receive_fail_reasons.pop(htlc.htlc_id)
|
|
except KeyError:
|
|
error_bytes, failure_message = None, None
|
|
# if we are forwarding, save error message to disk
|
|
if self.lnworker.get_payment_info(htlc.payment_hash) is None:
|
|
self.save_fail_htlc_reason(htlc.htlc_id, error_bytes, failure_message)
|
|
self.lnworker.htlc_failed(self, htlc.payment_hash, htlc.htlc_id, error_bytes, failure_message)
|
|
|
|
def save_fail_htlc_reason(
|
|
self,
|
|
htlc_id: int,
|
|
error_bytes: Optional[bytes],
|
|
failure_message: Optional['OnionRoutingFailure']):
|
|
error_hex = error_bytes.hex() if error_bytes else None
|
|
failure_hex = failure_message.to_bytes().hex() if failure_message else None
|
|
self.fail_htlc_reasons[htlc_id] = (error_hex, failure_hex)
|
|
|
|
def pop_fail_htlc_reason(self, htlc_id):
|
|
error_hex, failure_hex = self.fail_htlc_reasons.pop(htlc_id, (None, None))
|
|
error_bytes = bytes.fromhex(error_hex) if error_hex else None
|
|
failure_message = OnionRoutingFailure.from_bytes(bytes.fromhex(failure_hex)) if failure_hex else None
|
|
return error_bytes, failure_message
|
|
|
|
def extract_preimage_from_htlc_txin(self, txin: TxInput) -> None:
|
|
witness = txin.witness_elements()
|
|
if len(witness) == 5: # HTLC success tx
|
|
preimage = witness[3]
|
|
elif len(witness) == 3: # spending offered HTLC directly from ctx
|
|
preimage = witness[1]
|
|
else:
|
|
return
|
|
payment_hash = sha256(preimage)
|
|
for direction, htlc in itertools.chain(self.hm.get_htlcs_in_oldest_unrevoked_ctx(REMOTE),
|
|
self.hm.get_htlcs_in_latest_ctx(REMOTE)):
|
|
if htlc.payment_hash == payment_hash:
|
|
is_sent = direction == RECEIVED
|
|
break
|
|
else:
|
|
for direction, htlc in itertools.chain(self.hm.get_htlcs_in_oldest_unrevoked_ctx(LOCAL),
|
|
self.hm.get_htlcs_in_latest_ctx(LOCAL)):
|
|
if htlc.payment_hash == payment_hash:
|
|
is_sent = direction == SENT
|
|
break
|
|
else:
|
|
return
|
|
if self.lnworker.get_preimage(payment_hash) is None:
|
|
self.logger.info(f'found preimage for {payment_hash.hex()} in witness of length {len(witness)}')
|
|
self.lnworker.save_preimage(payment_hash, preimage)
|
|
info = self.lnworker.get_payment_info(payment_hash)
|
|
if info is not None and info.status != PR_PAID:
|
|
if is_sent:
|
|
self.lnworker.htlc_fulfilled(self, payment_hash, htlc.htlc_id)
|
|
else:
|
|
# FIXME
|
|
#self.lnworker.htlc_received(self, payment_hash)
|
|
pass
|
|
|
|
def balance(self, whose: HTLCOwner, *, ctx_owner=HTLCOwner.LOCAL, ctn: int = None) -> int:
|
|
assert type(whose) is HTLCOwner
|
|
initial = self.config[whose].initial_msat
|
|
return self.hm.get_balance_msat(whose=whose,
|
|
ctx_owner=ctx_owner,
|
|
ctn=ctn,
|
|
initial_balance_msat=initial)
|
|
|
|
def balance_minus_outgoing_htlcs(self, whose: HTLCOwner, *, ctx_owner: HTLCOwner = HTLCOwner.LOCAL,
|
|
ctn: int = None) -> int:
|
|
assert type(whose) is HTLCOwner
|
|
if ctn is None:
|
|
ctn = self.get_next_ctn(ctx_owner)
|
|
committed_balance = self.balance(whose, ctx_owner=ctx_owner, ctn=ctn)
|
|
direction = RECEIVED if whose != ctx_owner else SENT
|
|
balance_in_htlcs = self.balance_tied_up_in_htlcs_by_direction(ctx_owner, ctn=ctn, direction=direction)
|
|
return committed_balance - balance_in_htlcs
|
|
|
|
def balance_tied_up_in_htlcs_by_direction(self, ctx_owner: HTLCOwner = LOCAL, *, ctn: int = None,
|
|
direction: Direction):
|
|
# in msat
|
|
if ctn is None:
|
|
ctn = self.get_next_ctn(ctx_owner)
|
|
return htlcsum(self.hm.htlcs_by_direction(ctx_owner, direction, ctn).values())
|
|
|
|
def has_unsettled_htlcs(self) -> bool:
|
|
return len(self.hm.htlcs(LOCAL)) + len(self.hm.htlcs(REMOTE)) > 0
|
|
|
|
def available_to_spend(self, subject: HTLCOwner, *, strict: bool = True) -> int:
|
|
"""The usable balance of 'subject' in msat, after taking reserve and fees into
|
|
consideration. Note that fees (and hence the result) fluctuate even without user interaction.
|
|
"""
|
|
assert type(subject) is HTLCOwner
|
|
sender = subject
|
|
receiver = subject.inverted()
|
|
initiator = LOCAL if self.constraints.is_initiator else REMOTE # the initiator/funder pays on-chain fees
|
|
|
|
def consider_ctx(*, ctx_owner: HTLCOwner, is_htlc_dust: bool) -> int:
|
|
ctn = self.get_next_ctn(ctx_owner)
|
|
sender_balance_msat = self.balance_minus_outgoing_htlcs(whose=sender, ctx_owner=ctx_owner, ctn=ctn)
|
|
receiver_balance_msat = self.balance_minus_outgoing_htlcs(whose=receiver, ctx_owner=ctx_owner, ctn=ctn)
|
|
sender_reserve_msat = self.config[receiver].reserve_sat * 1000
|
|
receiver_reserve_msat = self.config[sender].reserve_sat * 1000
|
|
num_htlcs_in_ctx = len(self.included_htlcs(ctx_owner, SENT, ctn=ctn) + self.included_htlcs(ctx_owner, RECEIVED, ctn=ctn))
|
|
feerate = self.get_feerate(ctx_owner, ctn=ctn)
|
|
ctx_fees_msat = calc_fees_for_commitment_tx(
|
|
num_htlcs=num_htlcs_in_ctx,
|
|
feerate=feerate,
|
|
is_local_initiator=self.constraints.is_initiator,
|
|
round_to_sat=False,
|
|
)
|
|
htlc_fee_msat = fee_for_htlc_output(feerate=feerate)
|
|
htlc_trim_func = received_htlc_trim_threshold_sat if ctx_owner == receiver else offered_htlc_trim_threshold_sat
|
|
htlc_trim_threshold_msat = htlc_trim_func(dust_limit_sat=self.config[ctx_owner].dust_limit_sat, feerate=feerate) * 1000
|
|
if sender == initiator == LOCAL: # see https://github.com/lightningnetwork/lightning-rfc/pull/740
|
|
fee_spike_buffer = calc_fees_for_commitment_tx(
|
|
num_htlcs=num_htlcs_in_ctx + int(not is_htlc_dust) + 1,
|
|
feerate=2 * feerate,
|
|
is_local_initiator=self.constraints.is_initiator,
|
|
round_to_sat=False,
|
|
)[sender]
|
|
max_send_msat = sender_balance_msat - sender_reserve_msat - fee_spike_buffer
|
|
else:
|
|
max_send_msat = sender_balance_msat - sender_reserve_msat - ctx_fees_msat[sender]
|
|
if is_htlc_dust:
|
|
return min(max_send_msat, htlc_trim_threshold_msat - 1)
|
|
else:
|
|
if sender == initiator:
|
|
return max_send_msat - htlc_fee_msat
|
|
else:
|
|
# the receiver is the initiator, so they need to be able to pay tx fees
|
|
if receiver_balance_msat - receiver_reserve_msat - ctx_fees_msat[receiver] - htlc_fee_msat < 0:
|
|
return 0
|
|
return max_send_msat
|
|
|
|
max_send_msat = min(
|
|
max(
|
|
consider_ctx(ctx_owner=receiver, is_htlc_dust=True),
|
|
consider_ctx(ctx_owner=receiver, is_htlc_dust=False),
|
|
),
|
|
max(
|
|
consider_ctx(ctx_owner=sender, is_htlc_dust=True),
|
|
consider_ctx(ctx_owner=sender, is_htlc_dust=False),
|
|
),
|
|
)
|
|
max_send_msat = max(max_send_msat, 0)
|
|
return max_send_msat
|
|
|
|
|
|
def included_htlcs(self, subject: HTLCOwner, direction: Direction, ctn: int = None, *,
|
|
feerate: int = None) -> Sequence[UpdateAddHtlc]:
|
|
"""Returns list of non-dust HTLCs for subject's commitment tx at ctn,
|
|
filtered by direction (of HTLCs).
|
|
"""
|
|
assert type(subject) is HTLCOwner
|
|
assert type(direction) is Direction
|
|
if ctn is None:
|
|
ctn = self.get_oldest_unrevoked_ctn(subject)
|
|
if feerate is None:
|
|
feerate = self.get_feerate(subject, ctn=ctn)
|
|
conf = self.config[subject]
|
|
if direction == RECEIVED:
|
|
threshold_sat = received_htlc_trim_threshold_sat(dust_limit_sat=conf.dust_limit_sat, feerate=feerate)
|
|
else:
|
|
threshold_sat = offered_htlc_trim_threshold_sat(dust_limit_sat=conf.dust_limit_sat, feerate=feerate)
|
|
htlcs = self.hm.htlcs_by_direction(subject, direction, ctn=ctn).values()
|
|
return list(filter(lambda htlc: htlc.amount_msat // 1000 >= threshold_sat, htlcs))
|
|
|
|
def get_secret_and_point(self, subject: HTLCOwner, ctn: int) -> Tuple[Optional[bytes], bytes]:
|
|
assert type(subject) is HTLCOwner
|
|
assert ctn >= 0, ctn
|
|
offset = ctn - self.get_oldest_unrevoked_ctn(subject)
|
|
if subject == REMOTE:
|
|
if offset > 1:
|
|
raise RemoteCtnTooFarInFuture(f"offset: {offset}")
|
|
conf = self.config[REMOTE]
|
|
if offset == 1:
|
|
secret = None
|
|
point = conf.next_per_commitment_point
|
|
elif offset == 0:
|
|
secret = None
|
|
point = conf.current_per_commitment_point
|
|
else:
|
|
secret = self.revocation_store.retrieve_secret(RevocationStore.START_INDEX - ctn)
|
|
point = secret_to_pubkey(int.from_bytes(secret, 'big'))
|
|
else:
|
|
secret = get_per_commitment_secret_from_seed(self.config[LOCAL].per_commitment_secret_seed, RevocationStore.START_INDEX - ctn)
|
|
point = secret_to_pubkey(int.from_bytes(secret, 'big'))
|
|
return secret, point
|
|
|
|
def get_secret_and_commitment(self, subject: HTLCOwner, *, ctn: int) -> Tuple[Optional[bytes], PartialTransaction]:
|
|
secret, point = self.get_secret_and_point(subject, ctn)
|
|
ctx = self.make_commitment(subject, point, ctn)
|
|
return secret, ctx
|
|
|
|
def get_commitment(self, subject: HTLCOwner, *, ctn: int) -> PartialTransaction:
|
|
secret, ctx = self.get_secret_and_commitment(subject, ctn=ctn)
|
|
return ctx
|
|
|
|
def get_next_commitment(self, subject: HTLCOwner) -> PartialTransaction:
|
|
ctn = self.get_next_ctn(subject)
|
|
return self.get_commitment(subject, ctn=ctn)
|
|
|
|
def get_latest_commitment(self, subject: HTLCOwner) -> PartialTransaction:
|
|
ctn = self.get_latest_ctn(subject)
|
|
return self.get_commitment(subject, ctn=ctn)
|
|
|
|
def get_oldest_unrevoked_commitment(self, subject: HTLCOwner) -> PartialTransaction:
|
|
ctn = self.get_oldest_unrevoked_ctn(subject)
|
|
return self.get_commitment(subject, ctn=ctn)
|
|
|
|
def create_sweeptxs(self, ctn: int) -> List[Transaction]:
|
|
from .lnsweep import create_sweeptxs_for_watchtower
|
|
secret, ctx = self.get_secret_and_commitment(REMOTE, ctn=ctn)
|
|
return create_sweeptxs_for_watchtower(self, ctx, secret, self.sweep_address)
|
|
|
|
def get_oldest_unrevoked_ctn(self, subject: HTLCOwner) -> int:
|
|
return self.hm.ctn_oldest_unrevoked(subject)
|
|
|
|
def get_latest_ctn(self, subject: HTLCOwner) -> int:
|
|
return self.hm.ctn_latest(subject)
|
|
|
|
def get_next_ctn(self, subject: HTLCOwner) -> int:
|
|
return self.hm.ctn_latest(subject) + 1
|
|
|
|
def total_msat(self, direction: Direction) -> int:
|
|
"""Return the cumulative total msat amount received/sent so far."""
|
|
assert type(direction) is Direction
|
|
return htlcsum(self.hm.all_settled_htlcs_ever_by_direction(LOCAL, direction))
|
|
|
|
def settle_htlc(self, preimage: bytes, htlc_id: int) -> None:
|
|
"""Settle/fulfill a pending received HTLC.
|
|
Action must be initiated by LOCAL.
|
|
"""
|
|
self.logger.info("settle_htlc")
|
|
assert self.can_send_ctx_updates(), f"cannot update channel. {self.get_state()!r} {self.peer_state!r}"
|
|
htlc = self.hm.get_htlc_by_id(REMOTE, htlc_id)
|
|
assert htlc.payment_hash == sha256(preimage)
|
|
assert htlc_id not in self.hm.log[REMOTE]['settles']
|
|
self.hm.send_settle(htlc_id)
|
|
|
|
def get_payment_hash(self, htlc_id: int) -> bytes:
|
|
htlc = self.hm.get_htlc_by_id(LOCAL, htlc_id)
|
|
return htlc.payment_hash
|
|
|
|
def decode_onion_error(self, reason: bytes, route: Sequence['RouteEdge'],
|
|
htlc_id: int) -> Tuple[OnionRoutingFailure, int]:
|
|
failure_msg, sender_idx = decode_onion_error(
|
|
reason,
|
|
[x.node_id for x in route],
|
|
self.onion_keys[htlc_id])
|
|
return failure_msg, sender_idx
|
|
|
|
def receive_htlc_settle(self, preimage: bytes, htlc_id: int) -> None:
|
|
"""Settle/fulfill a pending offered HTLC.
|
|
Action must be initiated by REMOTE.
|
|
"""
|
|
self.logger.info("receive_htlc_settle")
|
|
htlc = self.hm.get_htlc_by_id(LOCAL, htlc_id)
|
|
assert htlc.payment_hash == sha256(preimage)
|
|
assert htlc_id not in self.hm.log[LOCAL]['settles']
|
|
with self.db_lock:
|
|
self.hm.recv_settle(htlc_id)
|
|
|
|
def fail_htlc(self, htlc_id: int) -> None:
|
|
"""Fail a pending received HTLC.
|
|
Action must be initiated by LOCAL.
|
|
"""
|
|
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)
|
|
|
|
def receive_fail_htlc(self, htlc_id: int, *,
|
|
error_bytes: Optional[bytes],
|
|
reason: Optional[OnionRoutingFailure] = None) -> None:
|
|
"""Fail a pending offered HTLC.
|
|
Action must be initiated by REMOTE.
|
|
"""
|
|
self.logger.info("receive_fail_htlc")
|
|
with self.db_lock:
|
|
self.hm.recv_fail(htlc_id)
|
|
self._receive_fail_reasons[htlc_id] = (error_bytes, reason)
|
|
|
|
def get_next_fee(self, subject: HTLCOwner) -> int:
|
|
return self.constraints.capacity - sum(x.value for x in self.get_next_commitment(subject).outputs())
|
|
|
|
def get_latest_fee(self, subject: HTLCOwner) -> int:
|
|
return self.constraints.capacity - sum(x.value for x in self.get_latest_commitment(subject).outputs())
|
|
|
|
def update_fee(self, feerate: int, from_us: bool) -> None:
|
|
# feerate uses sat/kw
|
|
if self.constraints.is_initiator != from_us:
|
|
raise Exception(f"Cannot update_fee: wrong initiator. us: {from_us}")
|
|
if feerate < FEERATE_PER_KW_MIN_RELAY_LIGHTNING:
|
|
raise Exception(f"Cannot update_fee: feerate lower than min relay fee. {feerate} sat/kw. us: {from_us}")
|
|
sender = LOCAL if from_us else REMOTE
|
|
ctx_owner = -sender
|
|
ctn = self.get_next_ctn(ctx_owner)
|
|
sender_balance_msat = self.balance_minus_outgoing_htlcs(whose=sender, ctx_owner=ctx_owner, ctn=ctn)
|
|
sender_reserve_msat = self.config[-sender].reserve_sat * 1000
|
|
num_htlcs_in_ctx = len(self.included_htlcs(ctx_owner, SENT, ctn=ctn, feerate=feerate) +
|
|
self.included_htlcs(ctx_owner, RECEIVED, ctn=ctn, feerate=feerate))
|
|
ctx_fees_msat = calc_fees_for_commitment_tx(
|
|
num_htlcs=num_htlcs_in_ctx,
|
|
feerate=feerate,
|
|
is_local_initiator=self.constraints.is_initiator,
|
|
)
|
|
remainder = sender_balance_msat - sender_reserve_msat - ctx_fees_msat[sender]
|
|
if remainder < 0:
|
|
raise Exception(f"Cannot update_fee. {sender} tried to update fee but they cannot afford it. "
|
|
f"Their balance would go below reserve: {remainder} msat missing.")
|
|
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)
|
|
|
|
def make_commitment(self, subject: HTLCOwner, this_point: bytes, ctn: int) -> PartialTransaction:
|
|
assert type(subject) is HTLCOwner
|
|
feerate = self.get_feerate(subject, ctn=ctn)
|
|
other = subject.inverted()
|
|
local_msat = self.balance(subject, ctx_owner=subject, ctn=ctn)
|
|
remote_msat = self.balance(other, ctx_owner=subject, ctn=ctn)
|
|
received_htlcs = self.hm.htlcs_by_direction(subject, RECEIVED, ctn).values()
|
|
sent_htlcs = self.hm.htlcs_by_direction(subject, SENT, ctn).values()
|
|
remote_msat -= htlcsum(received_htlcs)
|
|
local_msat -= htlcsum(sent_htlcs)
|
|
assert remote_msat >= 0
|
|
assert local_msat >= 0
|
|
# same htlcs as before, but now without dust.
|
|
received_htlcs = self.included_htlcs(subject, RECEIVED, ctn)
|
|
sent_htlcs = self.included_htlcs(subject, SENT, ctn)
|
|
|
|
this_config = self.config[subject]
|
|
other_config = self.config[-subject]
|
|
other_htlc_pubkey = derive_pubkey(other_config.htlc_basepoint.pubkey, this_point)
|
|
this_htlc_pubkey = derive_pubkey(this_config.htlc_basepoint.pubkey, this_point)
|
|
other_revocation_pubkey = derive_blinded_pubkey(other_config.revocation_basepoint.pubkey, this_point)
|
|
htlcs = [] # type: List[ScriptHtlc]
|
|
for is_received_htlc, htlc_list in zip((True, False), (received_htlcs, sent_htlcs)):
|
|
for htlc in htlc_list:
|
|
htlcs.append(ScriptHtlc(make_htlc_output_witness_script(
|
|
is_received_htlc=is_received_htlc,
|
|
remote_revocation_pubkey=other_revocation_pubkey,
|
|
remote_htlc_pubkey=other_htlc_pubkey,
|
|
local_htlc_pubkey=this_htlc_pubkey,
|
|
payment_hash=htlc.payment_hash,
|
|
cltv_expiry=htlc.cltv_expiry), htlc))
|
|
# note: maybe flip initiator here for fee purposes, we want LOCAL and REMOTE
|
|
# in the resulting dict to correspond to the to_local and to_remote *outputs* of the ctx
|
|
onchain_fees = calc_fees_for_commitment_tx(
|
|
num_htlcs=len(htlcs),
|
|
feerate=feerate,
|
|
is_local_initiator=self.constraints.is_initiator == (subject == LOCAL),
|
|
)
|
|
assert self.is_static_remotekey_enabled()
|
|
payment_pubkey = other_config.payment_basepoint.pubkey
|
|
return make_commitment(
|
|
ctn=ctn,
|
|
local_funding_pubkey=this_config.multisig_key.pubkey,
|
|
remote_funding_pubkey=other_config.multisig_key.pubkey,
|
|
remote_payment_pubkey=payment_pubkey,
|
|
funder_payment_basepoint=self.config[LOCAL if self.constraints.is_initiator else REMOTE].payment_basepoint.pubkey,
|
|
fundee_payment_basepoint=self.config[LOCAL if not self.constraints.is_initiator else REMOTE].payment_basepoint.pubkey,
|
|
revocation_pubkey=other_revocation_pubkey,
|
|
delayed_pubkey=derive_pubkey(this_config.delayed_basepoint.pubkey, this_point),
|
|
to_self_delay=other_config.to_self_delay,
|
|
funding_txid=self.funding_outpoint.txid,
|
|
funding_pos=self.funding_outpoint.output_index,
|
|
funding_sat=self.constraints.capacity,
|
|
local_amount=local_msat,
|
|
remote_amount=remote_msat,
|
|
dust_limit_sat=this_config.dust_limit_sat,
|
|
fees_per_participant=onchain_fees,
|
|
htlcs=htlcs,
|
|
)
|
|
|
|
def make_closing_tx(self, local_script: bytes, remote_script: bytes,
|
|
fee_sat: int, *, drop_remote = False) -> Tuple[bytes, PartialTransaction]:
|
|
""" cooperative close """
|
|
_, outputs = make_commitment_outputs(
|
|
fees_per_participant={
|
|
LOCAL: fee_sat * 1000 if self.constraints.is_initiator else 0,
|
|
REMOTE: fee_sat * 1000 if not self.constraints.is_initiator else 0,
|
|
},
|
|
local_amount_msat=self.balance(LOCAL),
|
|
remote_amount_msat=self.balance(REMOTE) if not drop_remote else 0,
|
|
local_script=local_script.hex(),
|
|
remote_script=remote_script.hex(),
|
|
htlcs=[],
|
|
dust_limit_sat=self.config[LOCAL].dust_limit_sat)
|
|
|
|
closing_tx = make_closing_tx(self.config[LOCAL].multisig_key.pubkey,
|
|
self.config[REMOTE].multisig_key.pubkey,
|
|
funding_txid=self.funding_outpoint.txid,
|
|
funding_pos=self.funding_outpoint.output_index,
|
|
funding_sat=self.constraints.capacity,
|
|
outputs=outputs)
|
|
|
|
der_sig = bfh(closing_tx.sign_txin(0, self.config[LOCAL].multisig_key.privkey))
|
|
sig = ecc.sig_string_from_der_sig(der_sig[:-1])
|
|
return sig, closing_tx
|
|
|
|
def signature_fits(self, tx: PartialTransaction) -> bool:
|
|
remote_sig = self.config[LOCAL].current_commitment_signature
|
|
preimage_hex = tx.serialize_preimage(0)
|
|
msg_hash = sha256d(bfh(preimage_hex))
|
|
assert remote_sig
|
|
res = ecc.verify_signature(self.config[REMOTE].multisig_key.pubkey, remote_sig, msg_hash)
|
|
return res
|
|
|
|
def force_close_tx(self) -> PartialTransaction:
|
|
tx = self.get_latest_commitment(LOCAL)
|
|
assert self.signature_fits(tx)
|
|
tx.sign({self.config[LOCAL].multisig_key.pubkey.hex(): (self.config[LOCAL].multisig_key.privkey, True)})
|
|
remote_sig = self.config[LOCAL].current_commitment_signature
|
|
remote_sig = ecc.der_sig_from_sig_string(remote_sig) + Sighash.to_sigbytes(Sighash.ALL)
|
|
tx.add_signature_to_txin(txin_idx=0,
|
|
signing_pubkey=self.config[REMOTE].multisig_key.pubkey.hex(),
|
|
sig=remote_sig.hex())
|
|
assert tx.is_complete()
|
|
return tx
|
|
|
|
def get_close_options(self) -> Sequence[ChanCloseOption]:
|
|
# This method is used both in the GUI, and in lnpeer.schedule_force_closing
|
|
# in the latter case, the result does not depend on peer_state
|
|
ret = []
|
|
if not self.is_closed() and self.peer_state == PeerState.GOOD:
|
|
# If there are unsettled HTLCs, althought is possible to cooperatively close,
|
|
# we choose not to expose that option in the GUI, because it is very likely
|
|
# that HTLCs will take a long time to settle (submarine swap, or stuck payment),
|
|
# and the close dialog would be taking a very long time to finish
|
|
if not self.has_unsettled_htlcs():
|
|
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)
|
|
|
|
def has_pending_changes(self, subject: HTLCOwner) -> bool:
|
|
next_htlcs = self.hm.get_htlcs_in_next_ctx(subject)
|
|
latest_htlcs = self.hm.get_htlcs_in_latest_ctx(subject)
|
|
return not (next_htlcs == latest_htlcs and self.get_next_feerate(subject) == self.get_latest_feerate(subject))
|
|
|
|
def should_be_closed_due_to_expiring_htlcs(self, local_height) -> bool:
|
|
htlcs_we_could_reclaim = {} # type: Dict[Tuple[Direction, int], UpdateAddHtlc]
|
|
# If there is a received HTLC for which we already released the preimage
|
|
# but the remote did not revoke yet, and the CLTV of this HTLC is dangerously close
|
|
# to the present, then unilaterally close channel
|
|
recv_htlc_deadline = lnutil.NBLOCK_DEADLINE_BEFORE_EXPIRY_FOR_RECEIVED_HTLCS
|
|
for sub, dir, ctn in ((LOCAL, RECEIVED, self.get_latest_ctn(LOCAL)),
|
|
(REMOTE, SENT, self.get_oldest_unrevoked_ctn(REMOTE)),
|
|
(REMOTE, SENT, self.get_latest_ctn(REMOTE)),):
|
|
for htlc_id, htlc in self.hm.htlcs_by_direction(subject=sub, direction=dir, ctn=ctn).items():
|
|
if not self.hm.was_htlc_preimage_released(htlc_id=htlc_id, htlc_proposer=REMOTE):
|
|
continue
|
|
if htlc.cltv_expiry - recv_htlc_deadline > local_height:
|
|
continue
|
|
htlcs_we_could_reclaim[(RECEIVED, htlc_id)] = htlc
|
|
# If there is an offered HTLC which has already expired (+ some grace period after), we
|
|
# will unilaterally close the channel and time out the HTLC
|
|
offered_htlc_deadline = lnutil.NBLOCK_DEADLINE_AFTER_EXPIRY_FOR_OFFERED_HTLCS
|
|
for sub, dir, ctn in ((LOCAL, SENT, self.get_latest_ctn(LOCAL)),
|
|
(REMOTE, RECEIVED, self.get_oldest_unrevoked_ctn(REMOTE)),
|
|
(REMOTE, RECEIVED, self.get_latest_ctn(REMOTE)),):
|
|
for htlc_id, htlc in self.hm.htlcs_by_direction(subject=sub, direction=dir, ctn=ctn).items():
|
|
if htlc.cltv_expiry + offered_htlc_deadline > local_height:
|
|
continue
|
|
htlcs_we_could_reclaim[(SENT, htlc_id)] = htlc
|
|
|
|
total_value_sat = sum([htlc.amount_msat // 1000 for htlc in htlcs_we_could_reclaim.values()])
|
|
num_htlcs = len(htlcs_we_could_reclaim)
|
|
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 is_funding_tx_mined(self, funding_height):
|
|
funding_txid = self.funding_outpoint.txid
|
|
funding_idx = self.funding_outpoint.output_index
|
|
conf = funding_height.conf
|
|
if conf < self.funding_txn_minimum_depth():
|
|
self.logger.info(f"funding tx is still not at sufficient depth. actual depth: {conf}")
|
|
return False
|
|
assert conf > 0
|
|
# check funding_tx amount and script
|
|
funding_tx = self.lnworker.lnwatcher.adb.get_transaction(funding_txid)
|
|
if not funding_tx:
|
|
self.logger.info(f"no funding_tx {funding_txid}")
|
|
return False
|
|
outp = funding_tx.outputs()[funding_idx]
|
|
redeem_script = funding_output_script(self.config[REMOTE], self.config[LOCAL])
|
|
funding_address = redeem_script_to_address('p2wsh', redeem_script)
|
|
funding_sat = self.constraints.capacity
|
|
if not (outp.address == funding_address and outp.value == funding_sat):
|
|
self.logger.info('funding outpoint mismatch')
|
|
return False
|
|
return True
|