You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

915 lines
42 KiB

# Copyright (C) 2018 The Electrum developers
# Copyright (C) 2015-2018 The Lightning Network 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.
# API (method signatures and docstrings) partially copied from lnd
# 42de4400bff5105352d0552155f73589166d162b
import os
from collections import namedtuple, defaultdict
import binascii
import json
from enum import IntEnum
from typing import Optional, Dict, List, Tuple, NamedTuple, Set, Callable, Iterable, Sequence, TYPE_CHECKING
import time
import threading
from . import ecc
from . import constants
from .util import bfh, bh2u
from .bitcoin import redeem_script_to_address
from .crypto import sha256, sha256d
from .transaction import Transaction, PartialTransaction
from .logging import Logger
from .lnonion import decode_onion_error
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_onchain_fees, RemoteMisbehaving, make_htlc_output_witness_script,
ShortChannelID, map_htlcs_to_ctx_output_idxs)
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
if TYPE_CHECKING:
from .lnworker import LNWallet
from .json_db import StoredDict
# 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 channel_states(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
FORCE_CLOSING = 4 # force-close tx has been broadcast
CLOSING = 5 # shutdown has been sent.
CLOSED = 6 # funding txo has been spent
REDEEMED = 7 # we can stop watching
class peer_states(IntEnum):
DISCONNECTED = 0
REESTABLISHING = 1
GOOD = 2
BAD = 3
cs = channel_states
state_transitions = [
(cs.PREOPENING, cs.OPENING),
(cs.OPENING, cs.FUNDED),
(cs.FUNDED, cs.OPEN),
(cs.OPENING, cs.CLOSING),
(cs.FUNDED, cs.CLOSING),
(cs.OPEN, cs.CLOSING),
(cs.OPENING, cs.FORCE_CLOSING),
(cs.FUNDED, cs.FORCE_CLOSING),
(cs.OPEN, cs.FORCE_CLOSING),
(cs.CLOSING, cs.FORCE_CLOSING),
(cs.OPENING, cs.CLOSED),
(cs.FUNDED, cs.CLOSED),
(cs.OPEN, cs.CLOSED),
(cs.CLOSING, cs.CLOSING), # if we reestablish
(cs.CLOSING, cs.CLOSED),
(cs.FORCE_CLOSING, cs.CLOSED),
(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
RevokeAndAck = namedtuple("RevokeAndAck", ["per_commitment_secret", "next_per_commitment_point"])
class RemoteCtnTooFarInFuture(Exception): pass
def htlcsum(htlcs):
return sum([x.amount_msat for x in htlcs])
class Channel(Logger):
# 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 ^
def diagnostic_name(self):
if self.name:
return str(self.name)
try:
return f"lnchannel_{bh2u(self.channel_id[-4:])}"
except:
return super().diagnostic_name()
def __init__(self, state: 'StoredDict', *, sweep_address=None, name=None, lnworker=None, initial_feerate=None):
self.name = name
Logger.__init__(self)
self.lnworker = lnworker # type: Optional[LNWallet]
self.sweep_address = sweep_address
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.channel_id = bfh(state["channel_id"])
self.constraints = state["constraints"]
self.funding_outpoint = state["funding_outpoint"]
self.node_id = bfh(state["node_id"])
self.short_channel_id = ShortChannelID.normalize(state["short_channel_id"])
self.onion_keys = state['onion_keys']
self.data_loss_protect_remote_pcp = state['data_loss_protect_remote_pcp']
self.hm = HTLCManager(log=state['log'], initial_feerate=initial_feerate)
self._state = channel_states[state['state']]
self.peer_state = peer_states.DISCONNECTED
self.sweep_info = {} # type: Dict[str, Dict[str, SweepInfo]]
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
if scid:
return str(scid)
return self.channel_id.hex()
def set_onion_key(self, key, value):
self.onion_keys[key] = value
def get_onion_key(self, key):
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, raw: bytes) -> None:
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 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=lnutil.NBLOCK_OUR_CLTV_EXPIRY_DELTA.to_bytes(2, byteorder="big"),
htlc_minimum_msat=self.config[REMOTE].htlc_minimum_msat.to_bytes(8, byteorder="big"),
htlc_maximum_msat=htlc_maximum_msat.to_bytes(8, byteorder="big"),
fee_base_msat=lnutil.OUR_FEE_BASE_MSAT.to_bytes(4, byteorder="big"),
fee_proportional_millionths=lnutil.OUR_FEE_PROPORTIONAL_MILLIONTHS.to_bytes(4, byteorder="big"),
chain_hash=constants.net.rev_genesis_bytes(),
timestamp=now.to_bytes(4, byteorder="big"),
)
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):
return self.storage.get('static_remotekey_enabled')
def set_short_channel_id(self, short_id):
self.short_channel_id = short_id
self.storage["short_channel_id"] = short_id
def get_feerate(self, subject, ctn):
return self.hm.get_feerate(subject, ctn)
def get_oldest_unrevoked_feerate(self, subject):
return self.hm.get_feerate_in_oldest_unrevoked_ctx(subject)
def get_latest_feerate(self, subject):
return self.hm.get_feerate_in_latest_ctx(subject)
def get_next_feerate(self, subject):
return self.hm.get_feerate_in_next_ctx(subject)
def get_payments(self):
out = []
for subject in LOCAL, REMOTE:
log = self.hm.log[subject]
for htlc_id, htlc in log.get('adds', {}).items():
if htlc_id in log.get('fails',{}):
status = 'failed'
elif htlc_id in log.get('settles',{}):
status = 'settled'
else:
status = 'inflight'
direction = SENT if subject is LOCAL else RECEIVED
rhash = bh2u(htlc.payment_hash)
out.append((rhash, self.channel_id, htlc, direction, status))
return out
def get_settled_payments(self):
out = {}
for subject in LOCAL, REMOTE:
log = self.hm.log[subject]
for htlc_id, htlc in log.get('adds', {}).items():
if htlc_id in log.get('settles',{}):
direction = SENT if subject is LOCAL else RECEIVED
rhash = bh2u(htlc.payment_hash)
out[rhash] = (self.channel_id, htlc, direction)
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 = peer_states.GOOD
def set_state(self, state):
""" set on-chain state """
old_state = self._state
if (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.save_channel(self)
self.lnworker.network.trigger_callback('channel', self)
def get_state(self):
return self._state
def is_open(self):
return self.get_state() == channel_states.OPEN
def is_closing(self):
return self.get_state() in [channel_states.CLOSING, channel_states.FORCE_CLOSING]
def is_closed(self):
# 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() or self.is_closing()):
return False
if self.peer_state != peer_states.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 not self.is_closing()
def save_funding_height(self, txid, height, timestamp):
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, height, timestamp):
self.storage['closing_height'] = txid, height, timestamp
def get_closing_height(self):
return self.storage.get('closing_height')
def is_redeemed(self):
return self.get_state() == channel_states.REDEEMED
def _check_can_pay(self, amount_msat: int) -> None:
# TODO check if this method uses correct ctns (should use "latest" + 1)
if self.is_closed():
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:
raise PaymentFailure('Too many HTLCs already in channel')
current_htlc_sum = (htlcsum(self.hm.htlcs_by_direction(LOCAL, SENT).values())
+ htlcsum(self.hm.htlcs_by_direction(LOCAL, RECEIVED).values()))
if current_htlc_sum + amount_msat > self.config[REMOTE].max_htlc_value_in_flight_msat:
raise PaymentFailure(f'HTLC value sum (sum of pending htlcs: {current_htlc_sum/1000} sat plus new htlc: {amount_msat/1000} sat) would exceed max allowed: {self.config[REMOTE].max_htlc_value_in_flight_msat/1000} sat')
if amount_msat < self.config[REMOTE].htlc_minimum_msat:
raise PaymentFailure(f'HTLC value too small: {amount_msat} msat')
def can_pay(self, amount_msat):
try:
self._check_can_pay(amount_msat)
except PaymentFailure:
return False
return True
def should_try_to_reestablish_peer(self) -> bool:
return channel_states.PREOPENING < self._state < channel_states.CLOSED and self.peer_state == peer_states.DISCONNECTED
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:
"""
AddHTLC adds an HTLC to the state machine's local update log. This method
should be called when preparing to send an outgoing HTLC.
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)
self._check_can_pay(htlc.amount_msat)
if htlc.htlc_id is None:
htlc = htlc._replace(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) -> UpdateAddHtlc:
"""
ReceiveHTLC adds an HTLC to the state machine's remote update log. This
method should be called in response to receiving a new HTLC from the remote
party.
This docstring is from LND.
"""
if isinstance(htlc, dict): # legacy conversion # FIXME remove
htlc = UpdateAddHtlc(**htlc)
assert isinstance(htlc, UpdateAddHtlc)
if htlc.htlc_id is None: # used in unit tests
htlc = htlc._replace(htlc_id=self.hm.get_next_htlc_id(REMOTE))
if 0 <= self.available_to_spend(REMOTE) < htlc.amount_msat:
raise RemoteMisbehaving('Remote dipped below channel reserve.' +\
f' Available at remote: {self.available_to_spend(REMOTE)},' +\
f' HTLC amount: {htlc.amount_msat}')
with self.db_lock:
self.hm.recv_htlc(htlc)
self.logger.info("receive_htlc")
return htlc
def sign_next_commitment(self):
"""
SignNextCommitment signs a new commitment which includes any previous
unsettled HTLCs, any new HTLCs, and any modifications to prior HTLCs
committed in previous commitment updates.
The first return parameter is the signature for the commitment transaction
itself, while the second parameter is are all HTLC signatures concatenated.
any). The HTLC signatures are sorted according to the BIP 69 order of the
HTLC's on the commitment transaction.
This docstring was adapted from LND.
"""
next_remote_ctn = self.get_next_ctn(REMOTE)
self.logger.info(f"sign_next_commitment {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])
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,
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, htlc_sigs):
"""
ReceiveNewCommitment process a signature for a new commitment state sent by
the remote party. This method should be called in response to the
remote party initiating a new change, or when the remote party sends a
signature fully accepting a new state we've initiated. If we are able to
successfully validate the signature, then the generated commitment is added
to our local commitment chain. Once we send a revocation for our prior
state, then this newly added commitment becomes our current accepted channel
state.
This docstring is from LND.
"""
# TODO in many failure cases below, we should "fail" the channel (force-close)
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 Exception(f'failed verifying signature of our updated commitment transaction: {bh2u(sig)} preimage is {preimage_hex}')
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 Exception(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(htlc=htlc,
htlc_sig=htlc_sig,
htlc_direction=direction,
pcp=pcp,
ctx=pending_local_commitment,
ctx_output_idx=ctx_output_idx)
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(self, *, htlc: UpdateAddHtlc, htlc_sig: bytes, htlc_direction: Direction,
pcp: bytes, ctx: Transaction, ctx_output_idx: int) -> None:
_script, htlc_tx = make_htlc_tx_with_open_channel(chan=self,
pcp=pcp,
subject=LOCAL,
htlc_direction=htlc_direction,
commit=ctx,
ctx_output_idx=ctx_output_idx,
htlc=htlc)
pre_hash = sha256d(bfh(htlc_tx.serialize_preimage(0)))
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 Exception(f'failed verifying HTLC signatures: {htlc} {htlc_direction}')
def get_remote_htlc_sig_for_htlc(self, *, htlc_relative_idx: int) -> bytes:
data = self.config[LOCAL].current_htlc_signatures
htlc_sigs = [data[i:i + 64] for i in range(0, len(data), 64)]
htlc_sig = htlc_sigs[htlc_relative_idx]
remote_htlc_sig = ecc.der_sig_from_sig_string(htlc_sig) + b'\x01'
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()
received = self.hm.received_in_ctn(new_ctn)
sent = self.hm.sent_in_ctn(new_ctn)
if self.lnworker:
for htlc in received:
self.lnworker.payment_completed(self, RECEIVED, htlc)
for htlc in sent:
self.lnworker.payment_completed(self, SENT, htlc)
received_this_batch = htlcsum(received)
sent_this_batch = htlcsum(sent)
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), (received_this_batch, sent_this_batch)
def receive_revocation(self, revocation: RevokeAndAck):
self.logger.info("receive_revocation")
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
def balance(self, whose, *, ctx_owner=HTLCOwner.LOCAL, ctn=None):
"""
This balance in mSAT is not including reserve and fees.
So a node cannot actually use its whole balance.
But this number is simple, since it is derived simply
from the initial balance, and the value of settled HTLCs.
Note that it does not decrease once an HTLC is added,
failed or fulfilled, since the balance change is only
committed to later when the respective commitment
transaction has been revoked.
"""
assert type(whose) is HTLCOwner
initial = self.config[whose].initial_msat
for direction, htlc in self.hm.all_settled_htlcs_ever(ctx_owner, ctn):
# note: could "simplify" to (whose * ctx_owner == direction * SENT)
if whose == ctx_owner:
if direction == SENT:
initial -= htlc.amount_msat
else:
initial += htlc.amount_msat
else:
if direction == SENT:
initial += htlc.amount_msat
else:
initial -= htlc.amount_msat
return initial
def balance_minus_outgoing_htlcs(self, whose: HTLCOwner, *, ctx_owner: HTLCOwner = HTLCOwner.LOCAL):
"""
This balance in mSAT, which includes the value of
pending outgoing HTLCs, is used in the UI.
"""
assert type(whose) is HTLCOwner
ctn = self.get_next_ctn(ctx_owner)
return self.balance(whose, ctx_owner=ctx_owner, ctn=ctn)\
- htlcsum(self.hm.htlcs_by_direction(ctx_owner, SENT, ctn).values())
def available_to_spend(self, subject):
"""
This balance in mSAT, while technically correct, can
not be used in the UI cause it fluctuates (commit fee)
"""
# FIXME whose balance? whose ctx?
# FIXME confusing/mixing ctns (should probably use latest_ctn + 1; not oldest_unrevoked + 1)
assert type(subject) is HTLCOwner
return self.balance_minus_outgoing_htlcs(subject, ctx_owner=subject)\
- self.config[-subject].reserve_sat * 1000\
- calc_onchain_fees(
# TODO should we include a potential new htlc, when we are called from receive_htlc?
len(self.included_htlcs(subject, SENT) + self.included_htlcs(subject, RECEIVED)),
self.get_latest_feerate(subject),
self.constraints.is_initiator,
)[subject]
def included_htlcs(self, subject, direction, ctn=None):
"""
return filter of non-dust htlcs for subjects commitment transaction, initiated by given party
"""
assert type(subject) is HTLCOwner
assert type(direction) is Direction
if ctn is None:
ctn = self.get_oldest_unrevoked_ctn(subject)
feerate = self.get_feerate(subject, ctn)
conf = self.config[subject]
if (subject, direction) in [(REMOTE, RECEIVED), (LOCAL, SENT)]:
weight = HTLC_SUCCESS_WEIGHT
else:
weight = HTLC_TIMEOUT_WEIGHT
htlcs = self.hm.htlcs_by_direction(subject, direction, ctn=ctn).values()
htlc_value_after_fees = lambda htlc: htlc.amount_msat // 1000 - (weight * feerate // 1000)
return list(filter(lambda htlc: htlc_value_after_fees(htlc) >= conf.dust_limit_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, ctn):
secret, point = self.get_secret_and_point(subject, ctn)
ctx = self.make_commitment(subject, point, ctn)
return secret, ctx
def get_commitment(self, subject, ctn) -> PartialTransaction:
secret, ctx = self.get_secret_and_commitment(subject, ctn)
return ctx
def get_next_commitment(self, subject: HTLCOwner) -> PartialTransaction:
ctn = self.get_next_ctn(subject)
return self.get_commitment(subject, ctn)
def get_latest_commitment(self, subject: HTLCOwner) -> PartialTransaction:
ctn = self.get_latest_ctn(subject)
return self.get_commitment(subject, ctn)
def get_oldest_unrevoked_commitment(self, subject: HTLCOwner) -> PartialTransaction:
ctn = self.get_oldest_unrevoked_ctn(subject)
return self.get_commitment(subject, 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)
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):
"""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, htlc_id):
"""
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)
assert htlc_id not in log['settles']
self.hm.send_settle(htlc_id)
def get_payment_hash(self, htlc_id):
log = self.hm.log[LOCAL]
htlc = log['adds'][htlc_id]
return htlc.payment_hash
def decode_onion_error(self, reason, route, htlc_id):
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, htlc_id):
self.logger.info("receive_htlc_settle")
log = self.hm.log[LOCAL]
htlc = log['adds'][htlc_id]
assert htlc.payment_hash == sha256(preimage)
assert htlc_id not in log['settles']
with self.db_lock:
self.hm.recv_settle(htlc_id)
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)
def receive_fail_htlc(self, htlc_id):
self.logger.info("receive_fail_htlc")
with self.db_lock:
self.hm.recv_fail(htlc_id)
def pending_local_fee(self):
return self.constraints.capacity - sum(x.value for x in self.get_next_commitment(LOCAL).outputs())
def get_latest_fee(self, subject):
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):
# feerate uses sat/kw
if self.constraints.is_initiator != from_us:
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)
def make_commitment(self, subject, this_point, ctn) -> PartialTransaction:
assert type(subject) is HTLCOwner
feerate = self.get_feerate(subject, ctn)
other = REMOTE if LOCAL == subject else LOCAL
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, SENT if subject == LOCAL else RECEIVED, ctn).values()
sent_htlcs = self.hm.htlcs_by_direction(subject, RECEIVED if subject == LOCAL else SENT, ctn).values()
if subject != LOCAL:
remote_msat -= htlcsum(received_htlcs)
local_msat -= htlcsum(sent_htlcs)
else:
remote_msat -= htlcsum(sent_htlcs)
local_msat -= htlcsum(received_htlcs)
assert remote_msat >= 0
assert local_msat >= 0
# same htlcs as before, but now without dust.
received_htlcs = self.included_htlcs(subject, SENT if subject == LOCAL else RECEIVED, ctn)
sent_htlcs = self.included_htlcs(subject, RECEIVED if subject == LOCAL else 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((subject != LOCAL, subject == LOCAL), (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))
onchain_fees = calc_onchain_fees(
len(htlcs),
feerate,
self.constraints.is_initiator == (subject == LOCAL),
)
if self.is_static_remotekey_enabled():
payment_pubkey = other_config.payment_basepoint.pubkey
else:
payment_pubkey = derive_pubkey(other_config.payment_basepoint.pubkey, this_point)
return make_commitment(
ctn,
this_config.multisig_key.pubkey,
other_config.multisig_key.pubkey,
payment_pubkey,
self.config[LOCAL if self.constraints.is_initiator else REMOTE].payment_basepoint.pubkey,
self.config[LOCAL if not self.constraints.is_initiator else REMOTE].payment_basepoint.pubkey,
other_revocation_pubkey,
derive_pubkey(this_config.delayed_basepoint.pubkey, this_point),
other_config.to_self_delay,
self.funding_outpoint.txid,
self.funding_outpoint.output_index,
self.constraints.capacity,
local_msat,
remote_msat,
this_config.dust_limit_sat,
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=bh2u(local_script),
remote_script=bh2u(remote_script),
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):
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):
tx = self.get_latest_commitment(LOCAL)
assert self.signature_fits(tx)
tx.sign({bh2u(self.config[LOCAL].multisig_key.pubkey): (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) + b"\x01"
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 sweep_ctx(self, ctx: Transaction) -> Dict[str, SweepInfo]:
txid = ctx.txid()
if self.sweep_info.get(txid) is None:
our_sweep_info = create_sweeptxs_for_our_ctx(chan=self, ctx=ctx, sweep_address=self.sweep_address)
their_sweep_info = create_sweeptxs_for_their_ctx(chan=self, ctx=ctx, sweep_address=self.sweep_address)
if our_sweep_info is not None:
self.sweep_info[txid] = our_sweep_info
self.logger.info(f'we force closed.')
elif their_sweep_info is not None:
self.sweep_info[txid] = their_sweep_info
self.logger.info(f'they force closed.')
else:
self.sweep_info[txid] = {}
return self.sweep_info[txid]
def sweep_htlc(self, ctx:Transaction, htlc_tx: Transaction):
# 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):
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))