electrum

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

commit 3dce65dc7390f5ec30365b14b93eca55d1a30564
parent 8274a963e6e5c9ce11af198f6ca23d21b1c5ba41
Author: ThomasV <thomasv@electrum.org>
Date:   Sat,  9 Feb 2019 10:29:33 +0100

Rename lnchan, lnchannel_verifier, lnbase
Auto-completions are a pain if files share a long prefix

Diffstat:
Melectrum/commands.py | 2+-
Melectrum/gui/qt/channel_details.py | 2+-
Melectrum/gui/qt/channels_list.py | 2+-
Delectrum/lnbase.py | 1105-------------------------------------------------------------------------------
Delectrum/lnchan.py | 810-------------------------------------------------------------------------------
Aelectrum/lnchannel.py | 810+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Delectrum/lnchannelverifier.py | 206-------------------------------------------------------------------------------
Aelectrum/lnpeer.py | 1105+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Melectrum/lnrouter.py | 4++--
Melectrum/lnsweep.py | 4++--
Melectrum/lnutil.py | 4++--
Aelectrum/lnverifier.py | 205+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Melectrum/lnworker.py | 4++--
Delectrum/tests/test_lnbase.py | 252-------------------------------------------------------------------------------
Delectrum/tests/test_lnchan.py | 826-------------------------------------------------------------------------------
Aelectrum/tests/test_lnchannel.py | 826+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/tests/test_lnpeer.py | 252+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
17 files changed, 3209 insertions(+), 3210 deletions(-)

diff --git a/electrum/commands.py b/electrum/commands.py @@ -49,7 +49,7 @@ from .address_synchronizer import TX_HEIGHT_LOCAL from .import lightning from .mnemonic import Mnemonic from .lnutil import SENT, RECEIVED -from .lnbase import channel_id_from_funding_tx +from .lnpeer import channel_id_from_funding_tx if TYPE_CHECKING: from .network import Network diff --git a/electrum/gui/qt/channel_details.py b/electrum/gui/qt/channel_details.py @@ -7,7 +7,7 @@ import PyQt5.QtCore as QtCore from electrum.i18n import _ from electrum.util import bh2u, format_time from electrum.lnutil import format_short_channel_id, LOCAL, REMOTE, UpdateAddHtlc, Direction -from electrum.lnchan import htlcsum +from electrum.lnchannel import htlcsum from electrum.lnaddr import LnAddr, lndecode from electrum.bitcoin import COIN diff --git a/electrum/gui/qt/channels_list.py b/electrum/gui/qt/channels_list.py @@ -6,7 +6,7 @@ from PyQt5.QtWidgets import * from electrum.util import inv_dict, bh2u, bfh from electrum.i18n import _ -from electrum.lnchan import Channel +from electrum.lnchannel import Channel from electrum.lnutil import LOCAL, REMOTE, ConnStringFormatError from .util import MyTreeView, WindowModalDialog, Buttons, OkButton, CancelButton, EnterButton, WWLabel diff --git a/electrum/lnbase.py b/electrum/lnbase.py @@ -1,1105 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2018 The Electrum developers -# Distributed under the MIT software license, see the accompanying -# file LICENCE or http://www.opensource.org/licenses/mit-license.php - -from collections import OrderedDict, defaultdict -import json -import asyncio -import os -import time -from functools import partial -from typing import List, Tuple, Dict, TYPE_CHECKING, Optional, Callable -import traceback -import sys - -import aiorpcx - -from .simple_config import get_config -from .crypto import sha256, sha256d -from . import bitcoin -from . import ecc -from .ecc import sig_string_from_r_and_s, get_r_and_s_from_sig_string, der_sig_from_sig_string -from . import constants -from .util import PrintError, bh2u, print_error, bfh, log_exceptions, list_enabled_bits, ignore_exceptions -from .transaction import Transaction, TxOutput -from .lnonion import (new_onion_packet, decode_onion_error, OnionFailureCode, calc_hops_data_for_payment, - process_onion_packet, OnionPacket, construct_onion_error, OnionRoutingFailureMessage) -from .lnchan import Channel, RevokeAndAck, htlcsum -from .lnutil import (Outpoint, LocalConfig, RECEIVED, UpdateAddHtlc, - RemoteConfig, OnlyPubkeyKeypair, ChannelConstraints, RevocationStore, - funding_output_script, get_per_commitment_secret_from_seed, - secret_to_pubkey, PaymentFailure, LnLocalFeatures, - LOCAL, REMOTE, HTLCOwner, generate_keypair, LnKeyFamily, - get_ln_flag_pair_of_bit, privkey_to_pubkey, UnknownPaymentHash, MIN_FINAL_CLTV_EXPIRY_ACCEPTED, - LightningPeerConnectionClosed, HandshakeFailed, NotFoundChanAnnouncementForUpdate, - MINIMUM_MAX_HTLC_VALUE_IN_FLIGHT_ACCEPTED, MAXIMUM_HTLC_MINIMUM_MSAT_ACCEPTED, - MAXIMUM_REMOTE_TO_SELF_DELAY_ACCEPTED) -from .lntransport import LNTransport, LNTransportBase -from .lnmsg import encode_msg, decode_msg - -if TYPE_CHECKING: - from .lnworker import LNWorker - from .lnrouter import RouteEdge - - -def channel_id_from_funding_tx(funding_txid: str, funding_index: int) -> Tuple[bytes, bytes]: - funding_txid_bytes = bytes.fromhex(funding_txid)[::-1] - i = int.from_bytes(funding_txid_bytes, 'big') ^ funding_index - return i.to_bytes(32, 'big'), funding_txid_bytes - -class Peer(PrintError): - - def __init__(self, lnworker: 'LNWorker', pubkey:bytes, transport: LNTransportBase, - request_initial_sync=False): - self.initialized = asyncio.Event() - self.transport = transport - self.pubkey = pubkey - self.lnworker = lnworker - self.privkey = lnworker.node_keypair.privkey - self.node_ids = [self.pubkey, privkey_to_pubkey(self.privkey)] - self.network = lnworker.network - self.lnwatcher = lnworker.network.lnwatcher - self.channel_db = lnworker.network.channel_db - self.ping_time = 0 - self.shutdown_received = defaultdict(asyncio.Future) - self.channel_accepted = defaultdict(asyncio.Queue) - self.channel_reestablished = defaultdict(asyncio.Future) - self.funding_signed = defaultdict(asyncio.Queue) - self.funding_created = defaultdict(asyncio.Queue) - self.revoke_and_ack = defaultdict(asyncio.Queue) - self.commitment_signed = defaultdict(asyncio.Queue) - self.announcement_signatures = defaultdict(asyncio.Queue) - self.closing_signed = defaultdict(asyncio.Queue) - self.payment_preimages = defaultdict(asyncio.Queue) - self.localfeatures = LnLocalFeatures(0) - if request_initial_sync: - self.localfeatures |= LnLocalFeatures.INITIAL_ROUTING_SYNC - self.localfeatures |= LnLocalFeatures.OPTION_DATA_LOSS_PROTECT_REQ - self.attempted_route = {} - self.orphan_channel_updates = OrderedDict() - - def send_message(self, message_name: str, **kwargs): - assert type(message_name) is str - self.print_error("Sending '%s'"%message_name.upper()) - self.transport.send_bytes(encode_msg(message_name, **kwargs)) - - async def initialize(self): - if isinstance(self.transport, LNTransport): - await self.transport.handshake() - self.channel_db.add_recent_peer(self.transport.peer_addr) - self.send_message("init", gflen=0, lflen=1, localfeatures=self.localfeatures) - - @property - def channels(self) -> Dict[bytes, Channel]: - return self.lnworker.channels_for_peer(self.pubkey) - - def diagnostic_name(self): - return self.transport.name() - - def ping_if_required(self): - if time.time() - self.ping_time > 120: - self.send_message('ping', num_pong_bytes=4, byteslen=4) - self.ping_time = time.time() - - def process_message(self, message): - message_type, payload = decode_msg(message) - try: - f = getattr(self, 'on_' + message_type) - except AttributeError: - self.print_error("Received '%s'" % message_type.upper(), payload) - return - # raw message is needed to check signature - if message_type=='node_announcement': - payload['raw'] = message - execution_result = f(payload) - if asyncio.iscoroutinefunction(f): - asyncio.ensure_future(execution_result) - - def on_error(self, payload): - # todo: self.channel_reestablished is not a queue - self.print_error("error", payload["data"].decode("ascii")) - chan_id = payload.get("channel_id") - for d in [ self.channel_accepted, self.funding_signed, - self.funding_created, self.revoke_and_ack, self.commitment_signed, - self.announcement_signatures, self.closing_signed ]: - if chan_id in d: - d[chan_id].put_nowait({'error':payload['data']}) - - def on_ping(self, payload): - l = int.from_bytes(payload['num_pong_bytes'], 'big') - self.send_message('pong', byteslen=l) - - def on_pong(self, payload): - pass - - def on_accept_channel(self, payload): - temp_chan_id = payload["temporary_channel_id"] - if temp_chan_id not in self.channel_accepted: raise Exception("Got unknown accept_channel") - self.channel_accepted[temp_chan_id].put_nowait(payload) - - def on_funding_signed(self, payload): - channel_id = payload['channel_id'] - if channel_id not in self.funding_signed: raise Exception("Got unknown funding_signed") - self.funding_signed[channel_id].put_nowait(payload) - - def on_funding_created(self, payload): - channel_id = payload['temporary_channel_id'] - if channel_id not in self.funding_created: raise Exception("Got unknown funding_created") - self.funding_created[channel_id].put_nowait(payload) - - def on_node_announcement(self, payload): - self.channel_db.on_node_announcement(payload) - self.network.trigger_callback('ln_status') - - def on_init(self, payload): - if self.initialized.is_set(): - self.print_error("ALREADY INITIALIZED BUT RECEIVED INIT") - return - # if they required some even flag we don't have, they will close themselves - # but if we require an even flag they don't have, we close - our_flags = set(list_enabled_bits(self.localfeatures)) - their_flags = set(list_enabled_bits(int.from_bytes(payload['localfeatures'], byteorder="big"))) - for flag in our_flags: - if flag not in their_flags and get_ln_flag_pair_of_bit(flag) not in their_flags: - # they don't have this feature we wanted :( - if flag % 2 == 0: # even flags are compulsory - raise LightningPeerConnectionClosed("remote does not have even flag {}" - .format(str(LnLocalFeatures(1 << flag)))) - self.localfeatures ^= 1 << flag # disable flag - first_timestamp = self.lnworker.get_first_timestamp() - self.send_message('gossip_timestamp_filter', chain_hash=constants.net.rev_genesis_bytes(), first_timestamp=first_timestamp, timestamp_range=b"\xff"*4) - self.initialized.set() - - def on_channel_update(self, payload): - try: - self.channel_db.on_channel_update(payload) - except NotFoundChanAnnouncementForUpdate: - # If it's for a direct channel with this peer, save it for later, as it might be - # for our own channel (and we might not yet know the short channel id for that) - short_channel_id = payload['short_channel_id'] - self.print_error("not found channel announce for channel update in db", bh2u(short_channel_id)) - self.orphan_channel_updates[short_channel_id] = payload - while len(self.orphan_channel_updates) > 10: - self.orphan_channel_updates.popitem(last=False) - - def on_channel_announcement(self, payload): - self.channel_db.on_channel_announcement(payload) - - def on_announcement_signatures(self, payload): - channel_id = payload['channel_id'] - chan = self.channels[payload['channel_id']] - if chan.config[LOCAL].was_announced: - h, local_node_sig, local_bitcoin_sig = self.send_announcement_signatures(chan) - else: - self.announcement_signatures[channel_id].put_nowait(payload) - - def handle_disconnect(func): - async def wrapper_func(self, *args, **kwargs): - try: - return await func(self, *args, **kwargs) - except LightningPeerConnectionClosed as e: - self.print_error("disconnecting gracefully. {}".format(e)) - finally: - self.close_and_cleanup() - self.lnworker.peers.pop(self.pubkey) - return wrapper_func - - @ignore_exceptions # do not kill main_taskgroup - @log_exceptions - @handle_disconnect - async def main_loop(self): - """ - This is used in LNWorker and is necessary so that we don't kill the main - task group. It is not merged with _main_loop, so that we can test if the - correct exceptions are getting thrown using _main_loop. - """ - await self._main_loop() - - async def _main_loop(self): - """This is separate from main_loop for the tests.""" - try: - await asyncio.wait_for(self.initialize(), 10) - except (OSError, asyncio.TimeoutError, HandshakeFailed) as e: - self.print_error('initialize failed, disconnecting: {}'.format(repr(e))) - return - # loop - async for msg in self.transport.read_messages(): - self.process_message(msg) - self.ping_if_required() - - def close_and_cleanup(self): - try: - if self.transport: - self.transport.close() - except: - pass - for chan in self.channels.values(): - if chan.get_state() != 'FORCE_CLOSING': - chan.set_state('DISCONNECTED') - self.network.trigger_callback('channel', chan) - - def make_local_config(self, funding_sat: int, push_msat: int, initiator: HTLCOwner) -> LocalConfig: - # key derivation - channel_counter = self.lnworker.get_and_inc_counter_for_channel_keys() - keypair_generator = lambda family: generate_keypair(self.lnworker.ln_keystore, family, channel_counter) - if initiator == LOCAL: - initial_msat = funding_sat * 1000 - push_msat - else: - initial_msat = push_msat - local_config=LocalConfig( - payment_basepoint=keypair_generator(LnKeyFamily.PAYMENT_BASE), - multisig_key=keypair_generator(LnKeyFamily.MULTISIG), - htlc_basepoint=keypair_generator(LnKeyFamily.HTLC_BASE), - delayed_basepoint=keypair_generator(LnKeyFamily.DELAY_BASE), - revocation_basepoint=keypair_generator(LnKeyFamily.REVOCATION_BASE), - to_self_delay=9, - dust_limit_sat=546, - max_htlc_value_in_flight_msat=funding_sat * 1000, - max_accepted_htlcs=5, - initial_msat=initial_msat, - ctn=-1, - next_htlc_id=0, - reserve_sat=546, - per_commitment_secret_seed=keypair_generator(LnKeyFamily.REVOCATION_ROOT).privkey, - funding_locked_received=False, - was_announced=False, - current_commitment_signature=None, - current_htlc_signatures=[], - got_sig_for_next=False, - ) - return local_config - - @log_exceptions - async def channel_establishment_flow(self, password: Optional[str], funding_sat: int, - push_msat: int, temp_channel_id: bytes) -> Channel: - wallet = self.lnworker.wallet - # dry run creating funding tx to see if we even have enough funds - funding_tx_test = wallet.mktx([TxOutput(bitcoin.TYPE_ADDRESS, wallet.dummy_address(), funding_sat)], - password, self.lnworker.config, nonlocal_only=True) - await asyncio.wait_for(self.initialized.wait(), 1) - feerate = self.lnworker.current_feerate_per_kw() - local_config = self.make_local_config(funding_sat, push_msat, LOCAL) - # for the first commitment transaction - per_commitment_secret_first = get_per_commitment_secret_from_seed(local_config.per_commitment_secret_seed, - RevocationStore.START_INDEX) - per_commitment_point_first = secret_to_pubkey(int.from_bytes(per_commitment_secret_first, 'big')) - self.send_message( - "open_channel", - temporary_channel_id=temp_channel_id, - chain_hash=constants.net.rev_genesis_bytes(), - funding_satoshis=funding_sat, - push_msat=push_msat, - dust_limit_satoshis=local_config.dust_limit_sat, - feerate_per_kw=feerate, - max_accepted_htlcs=local_config.max_accepted_htlcs, - funding_pubkey=local_config.multisig_key.pubkey, - revocation_basepoint=local_config.revocation_basepoint.pubkey, - htlc_basepoint=local_config.htlc_basepoint.pubkey, - payment_basepoint=local_config.payment_basepoint.pubkey, - delayed_payment_basepoint=local_config.delayed_basepoint.pubkey, - first_per_commitment_point=per_commitment_point_first, - to_self_delay=local_config.to_self_delay, - max_htlc_value_in_flight_msat=local_config.max_htlc_value_in_flight_msat, - channel_flags=0x00, # not willing to announce channel - channel_reserve_satoshis=local_config.reserve_sat, - htlc_minimum_msat=1, - ) - payload = await asyncio.wait_for(self.channel_accepted[temp_channel_id].get(), 1) - if payload.get('error'): - raise Exception('Remote Lightning peer reported error: ' + repr(payload.get('error'))) - remote_per_commitment_point = payload['first_per_commitment_point'] - funding_txn_minimum_depth = int.from_bytes(payload['minimum_depth'], 'big') - assert funding_txn_minimum_depth > 0, funding_txn_minimum_depth - remote_dust_limit_sat = int.from_bytes(payload['dust_limit_satoshis'], byteorder='big') - remote_reserve_sat = self.validate_remote_reserve(payload["channel_reserve_satoshis"], remote_dust_limit_sat, funding_sat) - if remote_dust_limit_sat > remote_reserve_sat: - raise Exception(f"Remote Lightning peer reports dust_limit_sat > reserve_sat which is a BOLT-02 protocol violation.") - htlc_min = int.from_bytes(payload['htlc_minimum_msat'], 'big') - if htlc_min > MAXIMUM_HTLC_MINIMUM_MSAT_ACCEPTED: - raise Exception(f"Remote Lightning peer reports htlc_minimum_msat={htlc_min} mSAT," + - f" which is above Electrums required maximum limit of that parameter ({MAXIMUM_HTLC_MINIMUM_MSAT_ACCEPTED} mSAT).") - remote_max = int.from_bytes(payload['max_htlc_value_in_flight_msat'], 'big') - if remote_max < MINIMUM_MAX_HTLC_VALUE_IN_FLIGHT_ACCEPTED: - raise Exception(f"Remote Lightning peer reports max_htlc_value_in_flight_msat at only {remote_max} mSAT" + - f" which is below Electrums required minimum ({MINIMUM_MAX_HTLC_VALUE_IN_FLIGHT_ACCEPTED} mSAT).") - max_accepted_htlcs = int.from_bytes(payload["max_accepted_htlcs"], 'big') - if max_accepted_htlcs > 483: - raise Exception("Remote Lightning peer reports max_accepted_htlcs > 483, which is a BOLT-02 protocol violation.") - remote_to_self_delay = int.from_bytes(payload['to_self_delay'], byteorder='big') - if remote_to_self_delay > MAXIMUM_REMOTE_TO_SELF_DELAY_ACCEPTED: - raise Exception(f"Remote Lightning peer reports to_self_delay={remote_to_self_delay}," + - f" which is above Electrums required maximum ({MAXIMUM_REMOTE_TO_SELF_DELAY_ACCEPTED})") - their_revocation_store = RevocationStore() - remote_config = RemoteConfig( - payment_basepoint=OnlyPubkeyKeypair(payload['payment_basepoint']), - multisig_key=OnlyPubkeyKeypair(payload["funding_pubkey"]), - htlc_basepoint=OnlyPubkeyKeypair(payload['htlc_basepoint']), - delayed_basepoint=OnlyPubkeyKeypair(payload['delayed_payment_basepoint']), - revocation_basepoint=OnlyPubkeyKeypair(payload['revocation_basepoint']), - to_self_delay=remote_to_self_delay, - dust_limit_sat=remote_dust_limit_sat, - max_htlc_value_in_flight_msat=remote_max, - max_accepted_htlcs=max_accepted_htlcs, - initial_msat=push_msat, - ctn = -1, - next_htlc_id = 0, - reserve_sat = remote_reserve_sat, - htlc_minimum_msat = htlc_min, - - next_per_commitment_point=remote_per_commitment_point, - current_per_commitment_point=None, - revocation_store=their_revocation_store, - ) - # create funding tx - redeem_script = funding_output_script(local_config, remote_config) - funding_address = bitcoin.redeem_script_to_address('p2wsh', redeem_script) - funding_output = TxOutput(bitcoin.TYPE_ADDRESS, funding_address, funding_sat) - funding_tx = wallet.mktx([funding_output], password, self.lnworker.config, nonlocal_only=True) - funding_txid = funding_tx.txid() - funding_index = funding_tx.outputs().index(funding_output) - # remote commitment transaction - channel_id, funding_txid_bytes = channel_id_from_funding_tx(funding_txid, funding_index) - chan_dict = { - "node_id": self.pubkey, - "channel_id": channel_id, - "short_channel_id": None, - "funding_outpoint": Outpoint(funding_txid, funding_index), - "remote_config": remote_config, - "local_config": local_config, - "constraints": ChannelConstraints(capacity=funding_sat, is_initiator=True, funding_txn_minimum_depth=funding_txn_minimum_depth, feerate=feerate), - "remote_commitment_to_be_revoked": None, - } - chan = Channel(chan_dict, - sweep_address=self.lnworker.sweep_address, - payment_completed=self.lnworker.payment_completed) - chan.lnwatcher = self.lnwatcher - chan.get_preimage_and_invoice = self.lnworker.get_invoice # FIXME hack. - sig_64, _ = chan.sign_next_commitment() - self.send_message("funding_created", - temporary_channel_id=temp_channel_id, - funding_txid=funding_txid_bytes, - funding_output_index=funding_index, - signature=sig_64) - payload = await asyncio.wait_for(self.funding_signed[channel_id].get(), 1) - self.print_error('received funding_signed') - remote_sig = payload['signature'] - chan.receive_new_commitment(remote_sig, []) - # broadcast funding tx - await asyncio.wait_for(self.network.broadcast_transaction(funding_tx), 1) - chan.remote_commitment_to_be_revoked = chan.pending_commitment(REMOTE) - chan.config[REMOTE] = chan.config[REMOTE]._replace(ctn=0, current_per_commitment_point=remote_per_commitment_point, next_per_commitment_point=None) - chan.config[LOCAL] = chan.config[LOCAL]._replace(ctn=0, current_commitment_signature=remote_sig, got_sig_for_next=False) - chan.set_state('OPENING') - chan.set_remote_commitment() - chan.set_local_commitment(chan.current_commitment(LOCAL)) - return chan - - async def on_open_channel(self, payload): - # payload['channel_flags'] - if payload['chain_hash'] != constants.net.rev_genesis_bytes(): - raise Exception('wrong chain_hash') - funding_sat = int.from_bytes(payload['funding_satoshis'], 'big') - push_msat = int.from_bytes(payload['push_msat'], 'big') - feerate = int.from_bytes(payload['feerate_per_kw'], 'big') - - temp_chan_id = payload['temporary_channel_id'] - local_config = self.make_local_config(funding_sat, push_msat, REMOTE) - # for the first commitment transaction - per_commitment_secret_first = get_per_commitment_secret_from_seed(local_config.per_commitment_secret_seed, - RevocationStore.START_INDEX) - per_commitment_point_first = secret_to_pubkey(int.from_bytes(per_commitment_secret_first, 'big')) - min_depth = 3 - self.send_message('accept_channel', - temporary_channel_id=temp_chan_id, - dust_limit_satoshis=local_config.dust_limit_sat, - max_htlc_value_in_flight_msat=local_config.max_htlc_value_in_flight_msat, - channel_reserve_satoshis=local_config.reserve_sat, - htlc_minimum_msat=1000, - minimum_depth=min_depth, - to_self_delay=local_config.to_self_delay, - max_accepted_htlcs=local_config.max_accepted_htlcs, - funding_pubkey=local_config.multisig_key.pubkey, - revocation_basepoint=local_config.revocation_basepoint.pubkey, - payment_basepoint=local_config.payment_basepoint.pubkey, - delayed_payment_basepoint=local_config.delayed_basepoint.pubkey, - htlc_basepoint=local_config.htlc_basepoint.pubkey, - first_per_commitment_point=per_commitment_point_first, - ) - funding_created = await self.funding_created[temp_chan_id].get() - funding_idx = int.from_bytes(funding_created['funding_output_index'], 'big') - funding_txid = bh2u(funding_created['funding_txid'][::-1]) - channel_id, funding_txid_bytes = channel_id_from_funding_tx(funding_txid, funding_idx) - their_revocation_store = RevocationStore() - remote_balance_sat = funding_sat * 1000 - push_msat - remote_dust_limit_sat = int.from_bytes(payload['dust_limit_satoshis'], byteorder='big') # TODO validate - remote_reserve_sat = self.validate_remote_reserve(payload['channel_reserve_satoshis'], remote_dust_limit_sat, funding_sat) - chan_dict = { - "node_id": self.pubkey, - "channel_id": channel_id, - "short_channel_id": None, - "funding_outpoint": Outpoint(funding_txid, funding_idx), - "remote_config": RemoteConfig( - payment_basepoint=OnlyPubkeyKeypair(payload['payment_basepoint']), - multisig_key=OnlyPubkeyKeypair(payload['funding_pubkey']), - htlc_basepoint=OnlyPubkeyKeypair(payload['htlc_basepoint']), - delayed_basepoint=OnlyPubkeyKeypair(payload['delayed_payment_basepoint']), - revocation_basepoint=OnlyPubkeyKeypair(payload['revocation_basepoint']), - to_self_delay=int.from_bytes(payload['to_self_delay'], 'big'), - dust_limit_sat=remote_dust_limit_sat, - max_htlc_value_in_flight_msat=int.from_bytes(payload['max_htlc_value_in_flight_msat'], 'big'), # TODO validate - max_accepted_htlcs=int.from_bytes(payload['max_accepted_htlcs'], 'big'), # TODO validate - initial_msat=remote_balance_sat, - ctn = -1, - next_htlc_id = 0, - reserve_sat = remote_reserve_sat, - htlc_minimum_msat=int.from_bytes(payload['htlc_minimum_msat'], 'big'), # TODO validate - - next_per_commitment_point=payload['first_per_commitment_point'], - current_per_commitment_point=None, - revocation_store=their_revocation_store, - ), - "local_config": local_config, - "constraints": ChannelConstraints(capacity=funding_sat, is_initiator=False, funding_txn_minimum_depth=min_depth, feerate=feerate), - "remote_commitment_to_be_revoked": None, - } - chan = Channel(chan_dict, - sweep_address=self.lnworker.sweep_address, - payment_completed=self.lnworker.payment_completed) - chan.lnwatcher = self.lnwatcher - chan.get_preimage_and_invoice = self.lnworker.get_invoice # FIXME hack. - remote_sig = funding_created['signature'] - chan.receive_new_commitment(remote_sig, []) - sig_64, _ = chan.sign_next_commitment() - self.send_message('funding_signed', - channel_id=channel_id, - signature=sig_64, - ) - chan.set_state('OPENING') - chan.remote_commitment_to_be_revoked = chan.pending_commitment(REMOTE) - chan.config[REMOTE] = chan.config[REMOTE]._replace(ctn=0, current_per_commitment_point=payload['first_per_commitment_point'], next_per_commitment_point=None) - chan.config[LOCAL] = chan.config[LOCAL]._replace(ctn=0, current_commitment_signature=remote_sig) - self.lnworker.save_channel(chan) - self.lnwatcher.watch_channel(chan.get_funding_address(), chan.funding_outpoint.to_str()) - self.lnworker.on_channels_updated() - while True: - try: - funding_tx = Transaction(await self.network.get_transaction(funding_txid)) - except aiorpcx.jsonrpc.RPCError as e: - print("sleeping", str(e)) - await asyncio.sleep(1) - else: - break - outp = funding_tx.outputs()[funding_idx] - redeem_script = funding_output_script(chan.config[REMOTE], chan.config[LOCAL]) - funding_address = bitcoin.redeem_script_to_address('p2wsh', redeem_script) - if outp != TxOutput(bitcoin.TYPE_ADDRESS, funding_address, funding_sat): - chan.set_state('DISCONNECTED') - raise Exception('funding outpoint mismatch') - - def validate_remote_reserve(self, payload_field: bytes, dust_limit: int, funding_sat: int) -> int: - remote_reserve_sat = int.from_bytes(payload_field, 'big') - if remote_reserve_sat < dust_limit: - raise Exception('protocol violation: reserve < dust_limit') - if remote_reserve_sat > funding_sat/100: - raise Exception(f'reserve too high: {remote_reserve_sat}, funding_sat: {funding_sat}') - return remote_reserve_sat - - def on_channel_reestablish(self, payload): - chan_id = payload["channel_id"] - self.print_error("Received channel_reestablish", bh2u(chan_id)) - chan = self.channels.get(chan_id) - if not chan: - self.print_error("Warning: received unknown channel_reestablish", bh2u(chan_id)) - return - self.channel_reestablished[chan_id].set_result(payload) - - @log_exceptions - async def reestablish_channel(self, chan: Channel): - await self.initialized.wait() - chan_id = chan.channel_id - if chan.get_state() != 'DISCONNECTED': - self.print_error('reestablish_channel was called but channel {} already in state {}' - .format(chan_id, chan.get_state())) - return - chan.set_state('REESTABLISHING') - self.network.trigger_callback('channel', chan) - self.send_message("channel_reestablish", - channel_id=chan_id, - next_local_commitment_number=chan.config[LOCAL].ctn+1, - next_remote_revocation_number=chan.config[REMOTE].ctn - ) - channel_reestablish_msg = await self.channel_reestablished[chan_id] - chan.set_state('OPENING') - - def try_to_get_remote_to_force_close_with_their_latest(): - self.print_error("trying to get remote to force close", bh2u(chan_id)) - self.send_message("channel_reestablish", - channel_id=chan_id, - next_local_commitment_number=0, - next_remote_revocation_number=0 - ) - # compare remote ctns - remote_ctn = int.from_bytes(channel_reestablish_msg["next_local_commitment_number"], 'big') - if remote_ctn != chan.config[REMOTE].ctn + 1: - self.print_error("expected remote ctn {}, got {}".format(chan.config[REMOTE].ctn + 1, remote_ctn)) - # TODO iff their ctn is lower than ours, we should force close instead - try_to_get_remote_to_force_close_with_their_latest() - return - # compare local ctns - local_ctn = int.from_bytes(channel_reestablish_msg["next_remote_revocation_number"], 'big') - if local_ctn != chan.config[LOCAL].ctn: - if remote_ctn == chan.config[LOCAL].ctn + 1: - # A node: - # if next_remote_revocation_number is equal to the - # commitment number of the last revoke_and_ack - # the receiving node sent, AND the receiving node - # hasn't already received a closing_signed: - # MUST re-send the revoke_and_ack. - chan.config[LOCAL]=chan.config[LOCAL]._replace( - ctn=remote_ctn, - ) - self.send_revoke_and_ack(chan) - else: - self.print_error("expected local ctn {}, got {}".format(chan.config[LOCAL].ctn, local_ctn)) - # TODO iff their ctn is lower than ours, we should force close instead - try_to_get_remote_to_force_close_with_their_latest() - return - # compare per commitment points (needs data_protect option) - their_pcp = channel_reestablish_msg.get("my_current_per_commitment_point", None) - if their_pcp is not None: - our_pcp = chan.config[REMOTE].current_per_commitment_point - if our_pcp is None: - our_pcp = chan.config[REMOTE].next_per_commitment_point - if our_pcp != their_pcp: - self.print_error("Remote PCP mismatch: {} {}".format(bh2u(our_pcp), bh2u(their_pcp))) - # FIXME ...what now? - try_to_get_remote_to_force_close_with_their_latest() - return - if remote_ctn == chan.config[LOCAL].ctn+1 == 1 and chan.short_channel_id: - self.send_funding_locked(chan) - # checks done - if chan.config[LOCAL].funding_locked_received and chan.short_channel_id: - self.mark_open(chan) - self.network.trigger_callback('channel', chan) - - def send_funding_locked(self, chan: Channel): - channel_id = chan.channel_id - per_commitment_secret_index = RevocationStore.START_INDEX - 1 - per_commitment_point_second = secret_to_pubkey(int.from_bytes( - get_per_commitment_secret_from_seed(chan.config[LOCAL].per_commitment_secret_seed, per_commitment_secret_index), 'big')) - # note: if funding_locked was not yet received, we might send it multiple times - self.send_message("funding_locked", channel_id=channel_id, next_per_commitment_point=per_commitment_point_second) - if chan.config[LOCAL].funding_locked_received and chan.short_channel_id: - self.mark_open(chan) - - def on_funding_locked(self, payload): - channel_id = payload['channel_id'] - chan = self.channels.get(channel_id) - if not chan: - print(self.channels) - raise Exception("Got unknown funding_locked", channel_id) - if not chan.config[LOCAL].funding_locked_received: - our_next_point = chan.config[REMOTE].next_per_commitment_point - their_next_point = payload["next_per_commitment_point"] - new_remote_state = chan.config[REMOTE]._replace(next_per_commitment_point=their_next_point) - new_local_state = chan.config[LOCAL]._replace(funding_locked_received = True) - chan.config[REMOTE]=new_remote_state - chan.config[LOCAL]=new_local_state - self.lnworker.save_channel(chan) - if chan.short_channel_id: - self.mark_open(chan) - - def on_network_update(self, chan: Channel, funding_tx_depth: int): - """ - Only called when the channel is OPEN. - - Runs on the Network thread. - """ - if not chan.config[LOCAL].was_announced and funding_tx_depth >= 6: - # don't announce our channels - # FIXME should this be a field in chan.local_state maybe? - return - chan.config[LOCAL]=chan.config[LOCAL]._replace(was_announced=True) - coro = self.handle_announcements(chan) - self.lnworker.save_channel(chan) - asyncio.run_coroutine_threadsafe(coro, self.network.asyncio_loop) - - @log_exceptions - async def handle_announcements(self, chan): - h, local_node_sig, local_bitcoin_sig = self.send_announcement_signatures(chan) - announcement_signatures_msg = await self.announcement_signatures[chan.channel_id].get() - remote_node_sig = announcement_signatures_msg["node_signature"] - remote_bitcoin_sig = announcement_signatures_msg["bitcoin_signature"] - if not ecc.verify_signature(chan.config[REMOTE].multisig_key.pubkey, remote_bitcoin_sig, h): - raise Exception("bitcoin_sig invalid in announcement_signatures") - if not ecc.verify_signature(self.pubkey, remote_node_sig, h): - raise Exception("node_sig invalid in announcement_signatures") - - node_sigs = [remote_node_sig, local_node_sig] - bitcoin_sigs = [remote_bitcoin_sig, local_bitcoin_sig] - bitcoin_keys = [chan.config[REMOTE].multisig_key.pubkey, chan.config[LOCAL].multisig_key.pubkey] - - if self.node_ids[0] > self.node_ids[1]: - node_sigs.reverse() - bitcoin_sigs.reverse() - node_ids = list(reversed(self.node_ids)) - bitcoin_keys.reverse() - else: - node_ids = self.node_ids - - self.send_message("channel_announcement", - node_signatures_1=node_sigs[0], - node_signatures_2=node_sigs[1], - bitcoin_signature_1=bitcoin_sigs[0], - bitcoin_signature_2=bitcoin_sigs[1], - len=0, - #features not set (defaults to zeros) - chain_hash=constants.net.rev_genesis_bytes(), - short_channel_id=chan.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] - ) - - print("SENT CHANNEL ANNOUNCEMENT") - - def mark_open(self, chan: Channel): - assert chan.short_channel_id is not None - if chan.get_state() == "OPEN": - return - # NOTE: even closed channels will be temporarily marked "OPEN" - assert chan.config[LOCAL].funding_locked_received - chan.set_state("OPEN") - self.network.trigger_callback('channel', chan) - # add channel to database - bitcoin_keys = [chan.config[LOCAL].multisig_key.pubkey, chan.config[REMOTE].multisig_key.pubkey] - sorted_node_ids = list(sorted(self.node_ids)) - if sorted_node_ids != self.node_ids: - node_ids = sorted_node_ids - bitcoin_keys.reverse() - else: - node_ids = self.node_ids - # note: we inject a channel announcement, and a channel update (for outgoing direction) - # This is atm needed for - # - finding routes - # - the ChanAnn is needed so that we can anchor to it a future ChanUpd - # that the remote sends, even if the channel was not announced - # (from BOLT-07: "MAY create a channel_update to communicate the channel - # parameters to the final node, even though the channel has not yet been announced") - self.channel_db.on_channel_announcement({"short_channel_id": chan.short_channel_id, "node_id_1": node_ids[0], "node_id_2": node_ids[1], - 'chain_hash': constants.net.rev_genesis_bytes(), 'len': b'\x00\x00', 'features': b'', - 'bitcoin_key_1': bitcoin_keys[0], 'bitcoin_key_2': bitcoin_keys[1]}, - trusted=True) - # only inject outgoing direction: - if node_ids[0] == privkey_to_pubkey(self.privkey): - channel_flags = b'\x00' - else: - channel_flags = b'\x01' - now = int(time.time()).to_bytes(4, byteorder="big") - self.channel_db.on_channel_update({"short_channel_id": chan.short_channel_id, 'channel_flags': channel_flags, 'cltv_expiry_delta': b'\x90', - 'htlc_minimum_msat': b'\x03\xe8', 'fee_base_msat': b'\x03\xe8', 'fee_proportional_millionths': b'\x01', - 'chain_hash': constants.net.rev_genesis_bytes(), 'timestamp': now}, - trusted=True) - # peer may have sent us a channel update for the incoming direction previously - # note: if we were offline when the 3rd conf happened, lnd will never send us this channel_update - # see https://github.com/lightningnetwork/lnd/issues/1347 - #self.send_message("query_short_channel_ids", chain_hash=constants.net.rev_genesis_bytes(), - # len=9, encoded_short_ids=b'\x00'+chan.short_channel_id) - pending_channel_update = self.orphan_channel_updates.get(chan.short_channel_id) - if pending_channel_update: - self.channel_db.on_channel_update(pending_channel_update) - - self.print_error("CHANNEL OPENING COMPLETED") - - def send_announcement_signatures(self, chan: Channel): - - bitcoin_keys = [chan.config[REMOTE].multisig_key.pubkey, - chan.config[LOCAL].multisig_key.pubkey] - - sorted_node_ids = list(sorted(self.node_ids)) - if sorted_node_ids != self.node_ids: - node_ids = sorted_node_ids - bitcoin_keys.reverse() - else: - node_ids = self.node_ids - - chan_ann = encode_msg("channel_announcement", - len=0, - #features not set (defaults to zeros) - chain_hash=constants.net.rev_genesis_bytes(), - short_channel_id=chan.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] - ) - to_hash = chan_ann[256+2:] - h = sha256d(to_hash) - bitcoin_signature = ecc.ECPrivkey(chan.config[LOCAL].multisig_key.privkey).sign(h, sig_string_from_r_and_s, get_r_and_s_from_sig_string) - node_signature = ecc.ECPrivkey(self.privkey).sign(h, sig_string_from_r_and_s, get_r_and_s_from_sig_string) - self.send_message("announcement_signatures", - channel_id=chan.channel_id, - short_channel_id=chan.short_channel_id, - node_signature=node_signature, - bitcoin_signature=bitcoin_signature - ) - - return h, node_signature, bitcoin_signature - - @log_exceptions - async def on_update_fail_htlc(self, payload): - channel_id = payload["channel_id"] - htlc_id = int.from_bytes(payload["id"], "big") - key = (channel_id, htlc_id) - try: - route = self.attempted_route[key] - except KeyError: - # the remote might try to fail an htlc after we restarted... - # attempted_route is not persisted, so we will get here then - self.print_error("UPDATE_FAIL_HTLC. cannot decode! attempted route is MISSING. {}".format(key)) - else: - try: - await self._handle_error_code_from_failed_htlc(payload["reason"], route, channel_id, htlc_id) - except Exception: - # exceptions are suppressed as failing to handle an error code - # should not block us from removing the htlc - traceback.print_exc(file=sys.stderr) - # process update_fail_htlc on channel - chan = self.channels[channel_id] - chan.receive_fail_htlc(htlc_id) - await self.receive_and_revoke(chan) - self.network.trigger_callback('ln_message', self.lnworker, 'Payment failed', htlc_id) - - async def _handle_error_code_from_failed_htlc(self, error_reason, route: List['RouteEdge'], channel_id, htlc_id): - chan = self.channels[channel_id] - failure_msg, sender_idx = decode_onion_error(error_reason, - [x.node_id for x in route], - chan.onion_keys[htlc_id]) - code, data = failure_msg.code, failure_msg.data - self.print_error("UPDATE_FAIL_HTLC", repr(code), data) - self.print_error(f"error reported by {bh2u(route[sender_idx].node_id)}") - # handle some specific error codes - failure_codes = { - OnionFailureCode.TEMPORARY_CHANNEL_FAILURE: 2, - OnionFailureCode.AMOUNT_BELOW_MINIMUM: 10, - OnionFailureCode.FEE_INSUFFICIENT: 10, - OnionFailureCode.INCORRECT_CLTV_EXPIRY: 6, - OnionFailureCode.EXPIRY_TOO_SOON: 2, - OnionFailureCode.CHANNEL_DISABLED: 4, - } - offset = failure_codes.get(code) - if offset: - channel_update = (258).to_bytes(length=2, byteorder="big") + data[offset:] - message_type, payload = decode_msg(channel_update) - try: - self.print_error("trying to apply channel update on our db", payload) - self.channel_db.on_channel_update(payload) - self.print_error("successfully applied channel update on our db") - except NotFoundChanAnnouncementForUpdate: - # maybe it is a private channel (and data in invoice was outdated) - self.print_error("maybe channel update is for private channel?") - start_node_id = route[sender_idx].node_id - self.channel_db.add_channel_update_for_private_channel(payload, start_node_id) - else: - # blacklist channel after reporter node - # TODO this should depend on the error (even more granularity) - # also, we need finer blacklisting (directed edges; nodes) - try: - short_chan_id = route[sender_idx + 1].short_channel_id - except IndexError: - self.print_error("payment destination reported error") - else: - self.network.path_finder.blacklist.add(short_chan_id) - - def send_commitment(self, chan: Channel): - sig_64, htlc_sigs = chan.sign_next_commitment() - self.send_message("commitment_signed", channel_id=chan.channel_id, signature=sig_64, num_htlcs=len(htlc_sigs), htlc_signature=b"".join(htlc_sigs)) - return len(htlc_sigs) - - async def send_and_revoke(self, chan: Channel): - """ generic channel update flow """ - self.send_commitment(chan) - await self.receive_revoke_and_ack(chan) - await self.receive_commitment(chan) - self.send_revoke_and_ack(chan) - - async def receive_and_revoke(self, chan: Channel): - await self.receive_commitment(chan) - self.send_revoke_and_ack(chan) - self.send_commitment(chan) - await self.receive_revoke_and_ack(chan) - - async def pay(self, route: List['RouteEdge'], chan: Channel, amount_msat: int, - payment_hash: bytes, min_final_cltv_expiry: int): - assert chan.get_state() == "OPEN", chan.get_state() - assert amount_msat > 0, "amount_msat is not greater zero" - # create onion packet - final_cltv = self.network.get_local_height() + min_final_cltv_expiry - hops_data, amount_msat, cltv = calc_hops_data_for_payment(route, amount_msat, final_cltv) - assert final_cltv <= cltv, (final_cltv, cltv) - secret_key = os.urandom(32) - onion = new_onion_packet([x.node_id for x in route], secret_key, hops_data, associated_data=payment_hash) - # create htlc - htlc = {'amount_msat':amount_msat, 'payment_hash':payment_hash, 'cltv_expiry':cltv} - htlc_id = chan.add_htlc(htlc) - chan.onion_keys[htlc_id] = secret_key - self.attempted_route[(chan.channel_id, htlc_id)] = route - self.print_error(f"starting payment. route: {route}") - self.send_message("update_add_htlc", - channel_id=chan.channel_id, - id=htlc_id, - cltv_expiry=cltv, - amount_msat=amount_msat, - payment_hash=payment_hash, - onion_routing_packet=onion.to_bytes()) - await self.send_and_revoke(chan) - return UpdateAddHtlc(**htlc, htlc_id=htlc_id) - - async def receive_revoke_and_ack(self, chan: Channel): - revoke_and_ack_msg = await self.revoke_and_ack[chan.channel_id].get() - chan.receive_revocation(RevokeAndAck(revoke_and_ack_msg["per_commitment_secret"], revoke_and_ack_msg["next_per_commitment_point"])) - self.lnworker.save_channel(chan) - - def send_revoke_and_ack(self, chan: Channel): - rev, _ = chan.revoke_current_commitment() - self.lnworker.save_channel(chan) - self.send_message("revoke_and_ack", - channel_id=chan.channel_id, - per_commitment_secret=rev.per_commitment_secret, - next_per_commitment_point=rev.next_per_commitment_point) - - async def receive_commitment(self, chan: Channel, commitment_signed_msg=None): - if commitment_signed_msg is None: - commitment_signed_msg = await self.commitment_signed[chan.channel_id].get() - data = commitment_signed_msg["htlc_signature"] - htlc_sigs = [data[i:i+64] for i in range(0, len(data), 64)] - chan.receive_new_commitment(commitment_signed_msg["signature"], htlc_sigs) - return len(htlc_sigs) - - def on_commitment_signed(self, payload): - self.print_error("commitment_signed", payload) - channel_id = payload['channel_id'] - self.commitment_signed[channel_id].put_nowait(payload) - - @log_exceptions - async def on_update_fulfill_htlc(self, update_fulfill_htlc_msg): - self.print_error("update_fulfill") - chan = self.channels[update_fulfill_htlc_msg["channel_id"]] - preimage = update_fulfill_htlc_msg["payment_preimage"] - htlc_id = int.from_bytes(update_fulfill_htlc_msg["id"], "big") - chan.receive_htlc_settle(preimage, htlc_id) - await self.receive_and_revoke(chan) - self.network.trigger_callback('ln_message', self.lnworker, 'Payment sent', htlc_id) - # used in lightning-integration - self.payment_preimages[sha256(preimage)].put_nowait(preimage) - - def on_update_fail_malformed_htlc(self, payload): - self.print_error("error", payload["data"].decode("ascii")) - - @log_exceptions - async def on_update_add_htlc(self, payload): - # no onion routing for the moment: we assume we are the end node - self.print_error('on_update_add_htlc') - # check if this in our list of requests - payment_hash = payload["payment_hash"] - channel_id = payload['channel_id'] - htlc_id = int.from_bytes(payload["id"], 'big') - cltv_expiry = int.from_bytes(payload["cltv_expiry"], 'big') - amount_msat_htlc = int.from_bytes(payload["amount_msat"], 'big') - onion_packet = OnionPacket.from_bytes(payload["onion_routing_packet"]) - processed_onion = process_onion_packet(onion_packet, associated_data=payment_hash, our_onion_private_key=self.privkey) - chan = self.channels[channel_id] - assert chan.get_state() == "OPEN" - assert htlc_id == chan.config[REMOTE].next_htlc_id, (htlc_id, chan.config[REMOTE].next_htlc_id) # TODO fail channel instead - if cltv_expiry >= 500_000_000: - pass # TODO fail the channel - # add htlc - htlc = {'amount_msat': amount_msat_htlc, 'payment_hash':payment_hash, 'cltv_expiry':cltv_expiry} - htlc_id = chan.receive_htlc(htlc) - await self.receive_and_revoke(chan) - # Forward HTLC - # FIXME: this is not robust to us going offline before payment is fulfilled - if not processed_onion.are_we_final: - dph = processed_onion.hop_data.per_hop - next_chan = self.lnworker.get_channel_by_short_id(dph.short_channel_id) - next_peer = self.lnworker.peers[next_chan.node_id] - if next_chan is None or next_chan.get_state() != 'OPEN': - self.print_error("cannot forward htlc", next_chan.get_state() if next_chan else None) - reason = OnionRoutingFailureMessage(code=OnionFailureCode.PERMANENT_CHANNEL_FAILURE, data=b'') - await self.fail_htlc(chan, htlc_id, onion_packet, reason) - return - self.print_error('forwarding htlc to', next_chan.node_id) - next_cltv_expiry = int.from_bytes(dph.outgoing_cltv_value, 'big') - next_amount_msat_htlc = int.from_bytes(dph.amt_to_forward, 'big') - next_htlc = {'amount_msat':next_amount_msat_htlc, 'payment_hash':payment_hash, 'cltv_expiry':next_cltv_expiry} - next_htlc_id = next_chan.add_htlc(next_htlc) - next_peer.send_message( - "update_add_htlc", - channel_id=next_chan.channel_id, - id=next_htlc_id, - cltv_expiry=dph.outgoing_cltv_value, - amount_msat=dph.amt_to_forward, - payment_hash=payment_hash, - onion_routing_packet=processed_onion.next_packet.to_bytes() - ) - await next_peer.send_and_revoke(next_chan) - # wait until we get paid - preimage = await next_peer.payment_preimages[payment_hash].get() - # fulfill the original htlc - await self.fulfill_htlc(chan, htlc_id, preimage) - self.print_error("htlc forwarded successfully") - return - try: - preimage, invoice = self.lnworker.get_invoice(payment_hash) - except UnknownPaymentHash: - reason = OnionRoutingFailureMessage(code=OnionFailureCode.UNKNOWN_PAYMENT_HASH, data=b'') - await self.fail_htlc(chan, htlc_id, onion_packet, reason) - return - expected_received_msat = int(invoice.amount * bitcoin.COIN * 1000) if invoice.amount is not None else None - if expected_received_msat is not None and \ - (amount_msat_htlc < expected_received_msat or amount_msat_htlc > 2 * expected_received_msat): - reason = OnionRoutingFailureMessage(code=OnionFailureCode.INCORRECT_PAYMENT_AMOUNT, data=b'') - await self.fail_htlc(chan, htlc_id, onion_packet, reason) - return - local_height = self.network.get_local_height() - if local_height + MIN_FINAL_CLTV_EXPIRY_ACCEPTED > cltv_expiry: - reason = OnionRoutingFailureMessage(code=OnionFailureCode.FINAL_EXPIRY_TOO_SOON, data=b'') - await self.fail_htlc(chan, htlc_id, onion_packet, reason) - return - cltv_from_onion = int.from_bytes(processed_onion.hop_data.per_hop.outgoing_cltv_value, byteorder="big") - if cltv_from_onion != cltv_expiry: - reason = OnionRoutingFailureMessage(code=OnionFailureCode.FINAL_INCORRECT_CLTV_EXPIRY, - data=cltv_expiry.to_bytes(4, byteorder="big")) - await self.fail_htlc(chan, htlc_id, onion_packet, reason) - return - amount_from_onion = int.from_bytes(processed_onion.hop_data.per_hop.amt_to_forward, byteorder="big") - if amount_from_onion > amount_msat_htlc: - reason = OnionRoutingFailureMessage(code=OnionFailureCode.FINAL_INCORRECT_HTLC_AMOUNT, - data=amount_msat_htlc.to_bytes(8, byteorder="big")) - await self.fail_htlc(chan, htlc_id, onion_packet, reason) - return - self.network.trigger_callback('htlc_added', UpdateAddHtlc(**htlc, htlc_id=htlc_id), invoice, RECEIVED) - # settle htlc - if not self.network.config.debug_lightning_do_not_settle: - # settle htlc - await self.fulfill_htlc(chan, htlc_id, preimage) - - async def fulfill_htlc(self, chan: Channel, htlc_id: int, preimage: bytes): - chan.settle_htlc(preimage, htlc_id) - self.send_message("update_fulfill_htlc", - channel_id=chan.channel_id, - id=htlc_id, - payment_preimage=preimage) - await self.send_and_revoke(chan) - self.network.trigger_callback('ln_message', self.lnworker, 'Payment received', htlc_id) - - async def fail_htlc(self, chan: Channel, htlc_id: int, onion_packet: OnionPacket, - reason: OnionRoutingFailureMessage): - self.print_error(f"failing received htlc {(bh2u(chan.channel_id), htlc_id)}. reason: {reason}") - chan.fail_htlc(htlc_id) - error_packet = construct_onion_error(reason, onion_packet, our_onion_private_key=self.privkey) - self.send_message("update_fail_htlc", - channel_id=chan.channel_id, - id=htlc_id, - len=len(error_packet), - reason=error_packet) - await self.send_and_revoke(chan) - - def on_revoke_and_ack(self, payload): - self.print_error("got revoke_and_ack") - channel_id = payload["channel_id"] - self.revoke_and_ack[channel_id].put_nowait(payload) - - def on_update_fee(self, payload): - channel_id = payload["channel_id"] - feerate =int.from_bytes(payload["feerate_per_kw"], "big") - self.channels[channel_id].update_fee(feerate, False) - - async def bitcoin_fee_update(self, chan: Channel): - """ - called when our fee estimates change - """ - if not chan.constraints.is_initiator: - # TODO force close if initiator does not update_fee enough - return - feerate_per_kw = self.lnworker.current_feerate_per_kw() - chan_fee = chan.pending_feerate(REMOTE) - self.print_error("current pending feerate", chan_fee) - self.print_error("new feerate", feerate_per_kw) - if feerate_per_kw < chan_fee / 2: - self.print_error("FEES HAVE FALLEN") - elif feerate_per_kw > chan_fee * 2: - self.print_error("FEES HAVE RISEN") - else: - return - chan.update_fee(feerate_per_kw, True) - self.send_message("update_fee", - channel_id=chan.channel_id, - feerate_per_kw=feerate_per_kw) - await self.send_and_revoke(chan) - - def on_closing_signed(self, payload): - chan_id = payload["channel_id"] - if chan_id not in self.closing_signed: raise Exception("Got unknown closing_signed") - self.closing_signed[chan_id].put_nowait(payload) - - @log_exceptions - async def close_channel(self, chan_id: bytes): - chan = self.channels[chan_id] - self.shutdown_received[chan_id] = asyncio.Future() - self.send_shutdown(chan) - payload = await self.shutdown_received[chan_id] - txid = await self._shutdown(chan, payload, True) - self.print_error('Channel closed', txid) - return txid - - @log_exceptions - async def on_shutdown(self, payload): - # length of scripts allowed in BOLT-02 - if int.from_bytes(payload['len'], 'big') not in (3+20+2, 2+20+1, 2+20, 2+32): - raise Exception('scriptpubkey length in received shutdown message invalid: ' + str(payload['len'])) - chan_id = payload['channel_id'] - if chan_id in self.shutdown_received: - self.shutdown_received[chan_id].set_result(payload) - else: - chan = self.channels[chan_id] - self.send_shutdown(chan) - txid = await self._shutdown(chan, payload, False) - self.print_error('Channel closed by remote peer', txid) - - def send_shutdown(self, chan: Channel): - scriptpubkey = bfh(bitcoin.address_to_script(chan.sweep_address)) - self.send_message('shutdown', channel_id=chan.channel_id, len=len(scriptpubkey), scriptpubkey=scriptpubkey) - - @log_exceptions - async def _shutdown(self, chan: Channel, payload, is_local): - # set state so that we stop accepting HTLCs - chan.set_state('CLOSING') - while len(chan.hm.htlcs_by_direction(LOCAL, RECEIVED)) > 0: - self.print_error('waiting for htlcs to settle...') - await asyncio.sleep(1) - our_fee = chan.pending_local_fee() - scriptpubkey = bfh(bitcoin.address_to_script(chan.sweep_address)) - # negociate fee - while True: - our_sig, closing_tx = chan.make_closing_tx(scriptpubkey, payload['scriptpubkey'], fee_sat=our_fee) - self.send_message('closing_signed', channel_id=chan.channel_id, fee_satoshis=our_fee, signature=our_sig) - cs_payload = await asyncio.wait_for(self.closing_signed[chan.channel_id].get(), 1) - their_fee = int.from_bytes(cs_payload['fee_satoshis'], 'big') - their_sig = cs_payload['signature'] - if our_fee == their_fee: - break - # TODO: negociate better - our_fee = their_fee - # index of our_sig - i = bool(chan.get_local_index()) - if not is_local: i = not i - # add signatures - closing_tx.add_signature_to_txin(0, int(i), bh2u(der_sig_from_sig_string(our_sig) + b'\x01')) - closing_tx.add_signature_to_txin(0, int(not i), bh2u(der_sig_from_sig_string(their_sig) + b'\x01')) - # broadcast - await self.network.broadcast_transaction(closing_tx) - return closing_tx.txid() diff --git a/electrum/lnchan.py b/electrum/lnchan.py @@ -1,810 +0,0 @@ -# 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 Enum, auto -from typing import Optional, Dict, List, Tuple, NamedTuple, Set, Callable, Iterable, Sequence - -from . import ecc -from .util import bfh, PrintError, bh2u -from .bitcoin import TYPE_SCRIPT, TYPE_ADDRESS -from .bitcoin import redeem_script_to_address -from .crypto import sha256, sha256d -from .simple_config import get_config -from .transaction import Transaction - -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) -from .lnsweep import create_sweeptxs_for_their_just_revoked_ctx -from .lnsweep import create_sweeptxs_for_our_latest_ctx, create_sweeptxs_for_their_latest_ctx -from .lnhtlc import HTLCManager - - -class ChannelJsonEncoder(json.JSONEncoder): - def default(self, o): - if isinstance(o, bytes): - return binascii.hexlify(o).decode("ascii") - if isinstance(o, RevocationStore): - return o.serialize() - if isinstance(o, set): - return list(o) - return super().default(o) - -RevokeAndAck = namedtuple("RevokeAndAck", ["per_commitment_secret", "next_per_commitment_point"]) - -class FeeUpdateProgress(Enum): - FUNDEE_SIGNED = auto() - FUNDEE_ACKED = auto() - FUNDER_SIGNED = auto() - -FUNDEE_SIGNED = FeeUpdateProgress.FUNDEE_SIGNED -FUNDEE_ACKED = FeeUpdateProgress.FUNDEE_ACKED -FUNDER_SIGNED = FeeUpdateProgress.FUNDER_SIGNED - -class FeeUpdate(defaultdict): - def __init__(self, chan, rate): - super().__init__(lambda: False) - self.rate = rate - self.chan = chan - - def pending_feerate(self, subject): - if self[FUNDEE_ACKED]: - return self.rate - if subject == REMOTE and self.chan.constraints.is_initiator: - return self.rate - if subject == LOCAL and not self.chan.constraints.is_initiator: - return self.rate - # implicit return None - -def decodeAll(d, local): - for k, v in d.items(): - if k == 'revocation_store': - yield (k, RevocationStore.from_json_obj(v)) - elif k.endswith("_basepoint") or k.endswith("_key"): - if local: - yield (k, Keypair(**dict(decodeAll(v, local)))) - else: - yield (k, OnlyPubkeyKeypair(**dict(decodeAll(v, local)))) - elif k in ["node_id", "channel_id", "short_channel_id", "pubkey", "privkey", "current_per_commitment_point", "next_per_commitment_point", "per_commitment_secret_seed", "current_commitment_signature", "current_htlc_signatures"] and v is not None: - yield (k, binascii.unhexlify(v)) - else: - yield (k, v) - -def htlcsum(htlcs): - return sum([x.amount_msat for x in htlcs]) - -# following two functions are used because json -# doesn't store int keys and byte string values -def str_bytes_dict_from_save(x): - return {int(k): bfh(v) for k,v in x.items()} - -def str_bytes_dict_to_save(x): - return {str(k): bh2u(v) for k, v in x.items()} - -class Channel(PrintError): - def diagnostic_name(self): - if self.name: - return str(self.name) - try: - return f"lnchan_{bh2u(self.channel_id[-4:])}" - except: - return super().diagnostic_name() - - def __init__(self, state, sweep_address = None, name = None, payment_completed : Optional[Callable[[Direction, UpdateAddHtlc, bytes], None]] = None): - self.preimages = {} - if not payment_completed: - payment_completed = lambda this, x, y, z: None - self.sweep_address = sweep_address - self.payment_completed = payment_completed - assert 'local_state' not in state - self.config = {} - self.config[LOCAL] = state["local_config"] - if type(self.config[LOCAL]) is not LocalConfig: - conf = dict(decodeAll(self.config[LOCAL], True)) - self.config[LOCAL] = LocalConfig(**conf) - assert type(self.config[LOCAL].htlc_basepoint.privkey) is bytes - - self.config[REMOTE] = state["remote_config"] - if type(self.config[REMOTE]) is not RemoteConfig: - conf = dict(decodeAll(self.config[REMOTE], False)) - self.config[REMOTE] = RemoteConfig(**conf) - assert type(self.config[REMOTE].htlc_basepoint.pubkey) is bytes - - self.channel_id = bfh(state["channel_id"]) if type(state["channel_id"]) not in (bytes, type(None)) else state["channel_id"] - self.constraints = ChannelConstraints(**state["constraints"]) if type(state["constraints"]) is not ChannelConstraints else state["constraints"] - self.funding_outpoint = Outpoint(**dict(decodeAll(state["funding_outpoint"], False))) if type(state["funding_outpoint"]) is not Outpoint else state["funding_outpoint"] - self.node_id = bfh(state["node_id"]) if type(state["node_id"]) not in (bytes, type(None)) else state["node_id"] - self.short_channel_id = bfh(state["short_channel_id"]) if type(state["short_channel_id"]) not in (bytes, type(None)) else state["short_channel_id"] - self.short_channel_id_predicted = self.short_channel_id - self.onion_keys = str_bytes_dict_from_save(state.get('onion_keys', {})) - - # FIXME this is a tx serialised in the custom electrum partial tx format. - # we should not persist txns in this format. we should persist htlcs, and be able to derive - # any past commitment transaction and use that instead; until then... - self.remote_commitment_to_be_revoked = Transaction(state["remote_commitment_to_be_revoked"]) - self.remote_commitment_to_be_revoked.deserialize(True) - - self.hm = HTLCManager(state.get('log')) - - self.name = name - - self.pending_fee = None - - self._is_funding_txo_spent = None # "don't know" - self._state = None - if state.get('force_closed', False): - self.set_state('FORCE_CLOSING') - else: - self.set_state('DISCONNECTED') - - self.lnwatcher = None - - self.local_commitment = None - self.remote_commitment = None - - def get_payments(self): - out = {} - for subject in LOCAL, REMOTE: - log = self.hm.log[subject] - for htlc_id, htlc in log.get('adds', {}).items(): - rhash = bh2u(htlc.payment_hash) - status = 'settled' if htlc_id in log.get('settles',{}) else 'inflight' - direction = SENT if subject is LOCAL else RECEIVED - out[rhash] = (self.channel_id, htlc, direction, status) - return out - - def set_local_commitment(self, ctx): - ctn = extract_ctn_from_tx_and_chan(ctx, self) - assert self.signature_fits(ctx), (self.log[LOCAL]) - self.local_commitment = ctx - if self.sweep_address is not None: - self.local_sweeptxs = create_sweeptxs_for_our_latest_ctx(self, self.local_commitment, self.sweep_address) - initial = os.path.join(get_config().electrum_path(), 'initial_commitment_tx') - tx = self.force_close_tx().serialize_to_network() - if not os.path.exists(initial): - with open(initial, 'w') as f: - f.write(tx) - - def set_remote_commitment(self): - self.remote_commitment = self.current_commitment(REMOTE) - if self.sweep_address is not None: - self.remote_sweeptxs = create_sweeptxs_for_their_latest_ctx(self, self.remote_commitment, self.sweep_address) - - def set_state(self, state: str): - if self._state == 'FORCE_CLOSING': - assert state == 'FORCE_CLOSING', 'new state was not FORCE_CLOSING: ' + state - self._state = state - - def get_state(self): - return self._state - - def is_closed(self): - return self.get_state() in ['CLOSED', 'FORCE_CLOSING'] - - def _check_can_pay(self, amount_msat: int) -> None: - if self.get_state() != 'OPEN': - raise PaymentFailure('Channel not open') - 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)) + htlcsum(self.hm.htlcs_by_direction(LOCAL, RECEIVED)) - 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 set_funding_txo_spentness(self, is_spent: bool): - assert isinstance(is_spent, bool) - self._is_funding_txo_spent = is_spent - - def should_try_to_reestablish_peer(self) -> bool: - return self._is_funding_txo_spent is False and self._state == '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): - """ - 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 type(htlc) is dict - self._check_can_pay(htlc['amount_msat']) - htlc = UpdateAddHtlc(**htlc, htlc_id=self.config[LOCAL].next_htlc_id) - self.hm.send_htlc(htlc) - self.print_error("add_htlc") - self.config[LOCAL]=self.config[LOCAL]._replace(next_htlc_id=htlc.htlc_id + 1) - return htlc.htlc_id - - def receive_htlc(self, htlc): - """ - 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. - """ - assert type(htlc) is dict - htlc = UpdateAddHtlc(**htlc, htlc_id = self.config[REMOTE].next_htlc_id) - if 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}') - self.hm.recv_htlc(htlc) - self.print_error("receive_htlc") - self.config[REMOTE]=self.config[REMOTE]._replace(next_htlc_id=htlc.htlc_id + 1) - return htlc.htlc_id - - 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. - """ - self.print_error("sign_next_commitment") - - self.hm.send_ctx() - - pending_remote_commitment = self.pending_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') - - for_us = False - - htlcsigs = [] - # they sent => we receive - for we_receive, htlcs in zip([True, False], [self.included_htlcs(REMOTE, SENT, ctn=self.config[REMOTE].ctn+1), self.included_htlcs(REMOTE, RECEIVED, ctn=self.config[REMOTE].ctn+1)]): - for htlc in htlcs: - _script, htlc_tx = make_htlc_tx_with_open_channel(chan=self, - pcp=self.config[REMOTE].next_per_commitment_point, - for_us=for_us, - we_receive=we_receive, - commit=pending_remote_commitment, - htlc=htlc) - sig = bfh(htlc_tx.sign_txin(0, their_remote_htlc_privkey)) - htlc_sig = ecc.sig_string_from_der_sig(sig[:-1]) - htlc_output_idx = htlc_tx.inputs()[0]['prevout_n'] - htlcsigs.append((htlc_output_idx, htlc_sig)) - - htlcsigs.sort() - htlcsigs = [x[1] for x in htlcsigs] - - # TODO should add remote_commitment here and handle - # both valid ctx'es in lnwatcher at the same time... - - 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. - """ - self.print_error("receive_new_commitment") - - self.hm.recv_ctx() - - assert len(htlc_sigs) == 0 or type(htlc_sigs[0]) is bytes - - pending_local_commitment = self.pending_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('failed verifying signature of our updated commitment transaction: ' + bh2u(sig) + ' preimage is ' + preimage_hex) - - htlc_sigs_string = b''.join(htlc_sigs) - - htlc_sigs = htlc_sigs[:] # copy cause we will delete now - ctn = self.config[LOCAL].ctn+1 - for htlcs, we_receive in [(self.included_htlcs(LOCAL, SENT, ctn=ctn), False), (self.included_htlcs(LOCAL, RECEIVED, ctn=ctn), True)]: - for htlc in htlcs: - idx = self.verify_htlc(htlc, htlc_sigs, we_receive, pending_local_commitment) - del htlc_sigs[idx] - if len(htlc_sigs) != 0: # all sigs should have been popped above - raise Exception('failed verifying HTLC signatures: invalid amount of correct signatures') - - self.config[LOCAL]=self.config[LOCAL]._replace( - current_commitment_signature=sig, - current_htlc_signatures=htlc_sigs_string, - got_sig_for_next=True) - - if self.pending_fee is not None: - if not self.constraints.is_initiator: - self.pending_fee[FUNDEE_SIGNED] = True - if self.constraints.is_initiator and self.pending_fee[FUNDEE_ACKED]: - self.pending_fee[FUNDER_SIGNED] = True - - self.set_local_commitment(pending_local_commitment) - - def verify_htlc(self, htlc: UpdateAddHtlc, htlc_sigs: Sequence[bytes], we_receive: bool, ctx) -> int: - ctn = extract_ctn_from_tx_and_chan(ctx, self) - 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')) - - _script, htlc_tx = make_htlc_tx_with_open_channel(chan=self, - pcp=point, - for_us=True, - we_receive=we_receive, - commit=ctx, - htlc=htlc) - pre_hash = sha256d(bfh(htlc_tx.serialize_preimage(0))) - remote_htlc_pubkey = derive_pubkey(self.config[REMOTE].htlc_basepoint.pubkey, point) - for idx, sig in enumerate(htlc_sigs): - if ecc.verify_signature(remote_htlc_pubkey, sig, pre_hash): - return idx - else: - raise Exception(f'failed verifying HTLC signatures: {htlc}, sigs: {len(htlc_sigs)}, we_receive: {we_receive}') - - def get_remote_htlc_sig_for_htlc(self, htlc: UpdateAddHtlc, we_receive: bool, ctx) -> bytes: - data = self.config[LOCAL].current_htlc_signatures - htlc_sigs = [data[i:i + 64] for i in range(0, len(data), 64)] - idx = self.verify_htlc(htlc, htlc_sigs, we_receive=we_receive, ctx=ctx) - remote_htlc_sig = ecc.der_sig_from_sig_string(htlc_sigs[idx]) + b'\x01' - return remote_htlc_sig - - def revoke_current_commitment(self): - self.print_error("revoke_current_commitment") - - last_secret, this_point, next_point, _ = self.points() - - new_feerate = self.constraints.feerate - - if self.pending_fee is not None: - if not self.constraints.is_initiator and self.pending_fee[FUNDEE_SIGNED]: - new_feerate = self.pending_fee.rate - self.pending_fee = None - print("FEERATE CHANGE COMPLETE (non-initiator)") - if self.constraints.is_initiator and self.pending_fee[FUNDER_SIGNED]: - new_feerate = self.pending_fee.rate - self.pending_fee = None - print("FEERATE CHANGE COMPLETE (initiator)") - - assert self.config[LOCAL].got_sig_for_next - self.constraints=self.constraints._replace( - feerate=new_feerate - ) - self.set_local_commitment(self.pending_commitment(LOCAL)) - ctx = self.pending_commitment(LOCAL) - self.hm.send_rev() - self.config[LOCAL]=self.config[LOCAL]._replace( - ctn=self.config[LOCAL].ctn + 1, - got_sig_for_next=False, - ) - assert self.signature_fits(ctx) - - return RevokeAndAck(last_secret, next_point), "current htlcs" - - def points(self): - last_small_num = self.config[LOCAL].ctn - this_small_num = last_small_num + 1 - next_small_num = last_small_num + 2 - last_secret = get_per_commitment_secret_from_seed(self.config[LOCAL].per_commitment_secret_seed, RevocationStore.START_INDEX - last_small_num) - this_secret = get_per_commitment_secret_from_seed(self.config[LOCAL].per_commitment_secret_seed, RevocationStore.START_INDEX - this_small_num) - this_point = secret_to_pubkey(int.from_bytes(this_secret, 'big')) - next_secret = get_per_commitment_secret_from_seed(self.config[LOCAL].per_commitment_secret_seed, RevocationStore.START_INDEX - next_small_num) - next_point = secret_to_pubkey(int.from_bytes(next_secret, 'big')) - last_point = secret_to_pubkey(int.from_bytes(last_secret, 'big')) - return last_secret, this_point, next_point, last_point - - def process_new_revocation_secret(self, per_commitment_secret: bytes): - if not self.lnwatcher: - return - outpoint = self.funding_outpoint.to_str() - ctx = self.remote_commitment_to_be_revoked # FIXME can't we just reconstruct it? - sweeptxs = create_sweeptxs_for_their_just_revoked_ctx(self, ctx, per_commitment_secret, self.sweep_address) - for prev_txid, tx in sweeptxs.items(): - if tx is not None: - self.lnwatcher.add_sweep_tx(outpoint, prev_txid, tx.as_dict()) - - def receive_revocation(self, revocation) -> Tuple[int, int]: - self.print_error("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') - - # FIXME not sure this is correct... but it seems to work - # if there are update_add_htlc msgs between commitment_signed and rev_ack, - # this might break - prev_remote_commitment = self.pending_commitment(REMOTE) - - self.config[REMOTE].revocation_store.add_next_entry(revocation.per_commitment_secret) - self.process_new_revocation_secret(revocation.per_commitment_secret) - - ##### start applying fee/htlc changes - - if self.pending_fee is not None: - if not self.constraints.is_initiator: - self.pending_fee[FUNDEE_SIGNED] = True - if self.constraints.is_initiator and self.pending_fee[FUNDEE_ACKED]: - self.pending_fee[FUNDER_SIGNED] = True - - received = self.hm.received_in_ctn(self.config[REMOTE].ctn + 1) - sent = self.hm.sent_in_ctn(self.config[REMOTE].ctn + 1) - for htlc in received: - self.payment_completed(self, RECEIVED, htlc, None) - for htlc in sent: - preimage = self.preimages.pop(htlc.htlc_id) - self.payment_completed(self, SENT, htlc, preimage) - received_this_batch = htlcsum(received) - sent_this_batch = htlcsum(sent) - - next_point = self.config[REMOTE].next_per_commitment_point - - self.hm.recv_rev() - - self.config[REMOTE]=self.config[REMOTE]._replace( - ctn=self.config[REMOTE].ctn + 1, - current_per_commitment_point=next_point, - next_per_commitment_point=revocation.next_per_commitment_point, - ) - - if self.pending_fee is not None: - if self.constraints.is_initiator: - self.pending_fee[FUNDEE_ACKED] = True - - self.set_remote_commitment() - self.remote_commitment_to_be_revoked = prev_remote_commitment - - return received_this_batch, sent_this_batch - - def balance(self, subject, ctn=None): - """ - This balance in mSAT is not including reserve and fees. - So a node cannot actually use it's 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 - commited to later when the respective commitment - transaction as been revoked. - """ - assert type(subject) is HTLCOwner - initial = self.config[subject].initial_msat - - for direction, htlc in self.hm.settled_htlcs(subject, ctn): - if direction == SENT: - initial -= htlc.amount_msat - else: - initial += htlc.amount_msat - - return initial - - def balance_minus_outgoing_htlcs(self, subject): - """ - This balance in mSAT, which includes the value of - pending outgoing HTLCs, is used in the UI. - """ - assert type(subject) is HTLCOwner - ctn = self.hm.log[subject]['ctn'] + 1 - return self.balance(subject, ctn)\ - - htlcsum(self.hm.htlcs_by_direction(subject, SENT, ctn)) - - 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) - """ - assert type(subject) is HTLCOwner - return self.balance_minus_outgoing_htlcs(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.pending_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.config[subject].ctn - feerate = self.pending_feerate(subject) - 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) - fee_for_htlc = lambda htlc: htlc.amount_msat // 1000 - (weight * feerate // 1000) - return list(filter(lambda htlc: fee_for_htlc(htlc) >= conf.dust_limit_sat, htlcs)) - - def pending_feerate(self, subject): - assert type(subject) is HTLCOwner - candidate = self.constraints.feerate - if self.pending_fee is not None: - x = self.pending_fee.pending_feerate(subject) - if x is not None: - candidate = x - return candidate - - def pending_commitment(self, subject): - assert type(subject) is HTLCOwner - this_point = self.config[REMOTE].next_per_commitment_point if subject == REMOTE else self.points()[1] - ctn = self.config[subject].ctn + 1 - feerate = self.pending_feerate(subject) - return self.make_commitment(subject, this_point, ctn, feerate, True) - - def current_commitment(self, subject): - assert type(subject) is HTLCOwner - this_point = self.config[REMOTE].current_per_commitment_point if subject == REMOTE else self.points()[3] - ctn = self.config[subject].ctn - feerate = self.constraints.feerate - return self.make_commitment(subject, this_point, ctn, feerate, False) - - def total_msat(self, direction): - assert type(direction) is Direction - sub = LOCAL if direction == SENT else REMOTE - return htlcsum(self.hm.settled_htlcs_by(sub, self.config[sub].ctn)) - - def settle_htlc(self, preimage, htlc_id): - """ - SettleHTLC attempts to settle an existing outstanding received HTLC. - """ - self.print_error("settle_htlc") - 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) - # not saving preimage because it's already saved in LNWorker.invoices - - def receive_htlc_settle(self, preimage, htlc_id): - self.print_error("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'] - self.hm.recv_settle(htlc_id) - self.preimages[htlc_id] = preimage - # we don't save the preimage because we don't need to forward it anyway - - def fail_htlc(self, htlc_id): - self.print_error("fail_htlc") - self.hm.send_fail(htlc_id) - - def receive_fail_htlc(self, htlc_id): - self.print_error("receive_fail_htlc") - self.hm.recv_fail(htlc_id) - - @property - def current_height(self): - return {LOCAL: self.config[LOCAL].ctn, REMOTE: self.config[REMOTE].ctn} - - def pending_local_fee(self): - return self.constraints.capacity - sum(x[2] for x in self.pending_commitment(LOCAL).outputs()) - - def update_fee(self, feerate, initiator): - if self.constraints.is_initiator != initiator: - raise Exception("Cannot update_fee: wrong initiator", initiator) - if self.pending_fee is not None: - raise Exception("a fee update is already in progress") - self.pending_fee = FeeUpdate(self, rate=feerate) - - def to_save(self): - to_save = { - "local_config": self.config[LOCAL], - "remote_config": self.config[REMOTE], - "channel_id": self.channel_id, - "short_channel_id": self.short_channel_id, - "constraints": self.constraints, - "funding_outpoint": self.funding_outpoint, - "node_id": self.node_id, - "remote_commitment_to_be_revoked": str(self.remote_commitment_to_be_revoked), - "log": self.hm.to_save(), - "onion_keys": str_bytes_dict_to_save(self.onion_keys), - "force_closed": self.get_state() == 'FORCE_CLOSING', - } - return to_save - - def serialize(self): - namedtuples_to_dict = lambda v: {i: j._asdict() if isinstance(j, tuple) else j for i, j in v._asdict().items()} - serialized_channel = {} - to_save_ref = self.to_save() - for k, v in to_save_ref.items(): - if isinstance(v, tuple): - serialized_channel[k] = namedtuples_to_dict(v) - else: - serialized_channel[k] = v - dumped = ChannelJsonEncoder().encode(serialized_channel) - roundtripped = json.loads(dumped) - reconstructed = Channel(roundtripped) - to_save_new = reconstructed.to_save() - if to_save_new != to_save_ref: - from pprint import PrettyPrinter - pp = PrettyPrinter(indent=168) - try: - from deepdiff import DeepDiff - except ImportError: - raise Exception("Channels did not roundtrip serialization without changes:\n" + pp.pformat(to_save_ref) + "\n" + pp.pformat(to_save_new)) - else: - raise Exception("Channels did not roundtrip serialization without changes:\n" + pp.pformat(DeepDiff(to_save_ref, to_save_new))) - return roundtripped - - def __str__(self): - return str(self.serialize()) - - def make_commitment(self, subject, this_point, ctn, feerate, pending) -> Transaction: - #if subject == REMOTE and not pending: - # ctn -= 1 - assert type(subject) is HTLCOwner - other = REMOTE if LOCAL == subject else LOCAL - remote_msat, local_msat = self.balance(other, ctn), self.balance(subject, ctn) - received_htlcs = self.hm.htlcs_by_direction(subject, SENT if subject == LOCAL else RECEIVED, ctn) - sent_htlcs = self.hm.htlcs_by_direction(subject, RECEIVED if subject == LOCAL else SENT, ctn) - 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), - ) - 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, - self.constraints.capacity, - local_msat, - remote_msat, - this_config.dust_limit_sat, - onchain_fees, - htlcs=htlcs) - - def get_local_index(self): - return int(self.config[LOCAL].multisig_key.pubkey < self.config[REMOTE].multisig_key.pubkey) - - def make_closing_tx(self, local_script: bytes, remote_script: bytes, - fee_sat: int) -> Tuple[bytes, int, str]: - """ cooperative close """ - _, outputs = make_commitment_outputs({ - LOCAL: fee_sat * 1000 if self.constraints.is_initiator else 0, - REMOTE: fee_sat * 1000 if not self.constraints.is_initiator else 0, - }, - self.balance(LOCAL), - self.balance(REMOTE), - (TYPE_SCRIPT, bh2u(local_script)), - (TYPE_SCRIPT, bh2u(remote_script)), - [], 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): - remote_sig = self.config[LOCAL].current_commitment_signature - preimage_hex = tx.serialize_preimage(0) - pre_hash = sha256d(bfh(preimage_hex)) - assert remote_sig - res = ecc.verify_signature(self.config[REMOTE].multisig_key.pubkey, remote_sig, pre_hash) - return res - - def force_close_tx(self): - tx = self.local_commitment - assert self.signature_fits(tx) - tx = Transaction(str(tx)) - tx.deserialize(True) - 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" - sigs = tx._inputs[0]["signatures"] - none_idx = sigs.index(None) - tx.add_signature_to_txin(0, none_idx, bh2u(remote_sig)) - assert tx.is_complete() - return tx - - def included_htlcs_in_their_latest_ctxs(self, htlc_initiator) -> Dict[int, List[UpdateAddHtlc]]: - """ A map from commitment number to list of HTLCs in - their latest two commitment transactions. - The oldest might have been revoked. """ - assert type(htlc_initiator) is HTLCOwner - direction = RECEIVED if htlc_initiator == LOCAL else SENT - old_ctn = self.config[REMOTE].ctn - old_htlcs = self.included_htlcs(REMOTE, direction, ctn=old_ctn) - - new_ctn = self.config[REMOTE].ctn+1 - new_htlcs = self.included_htlcs(REMOTE, direction, ctn=new_ctn) - - return {old_ctn: old_htlcs, - new_ctn: new_htlcs, } diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py @@ -0,0 +1,810 @@ +# 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 Enum, auto +from typing import Optional, Dict, List, Tuple, NamedTuple, Set, Callable, Iterable, Sequence + +from . import ecc +from .util import bfh, PrintError, bh2u +from .bitcoin import TYPE_SCRIPT, TYPE_ADDRESS +from .bitcoin import redeem_script_to_address +from .crypto import sha256, sha256d +from .simple_config import get_config +from .transaction import Transaction + +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) +from .lnsweep import create_sweeptxs_for_their_just_revoked_ctx +from .lnsweep import create_sweeptxs_for_our_latest_ctx, create_sweeptxs_for_their_latest_ctx +from .lnhtlc import HTLCManager + + +class ChannelJsonEncoder(json.JSONEncoder): + def default(self, o): + if isinstance(o, bytes): + return binascii.hexlify(o).decode("ascii") + if isinstance(o, RevocationStore): + return o.serialize() + if isinstance(o, set): + return list(o) + return super().default(o) + +RevokeAndAck = namedtuple("RevokeAndAck", ["per_commitment_secret", "next_per_commitment_point"]) + +class FeeUpdateProgress(Enum): + FUNDEE_SIGNED = auto() + FUNDEE_ACKED = auto() + FUNDER_SIGNED = auto() + +FUNDEE_SIGNED = FeeUpdateProgress.FUNDEE_SIGNED +FUNDEE_ACKED = FeeUpdateProgress.FUNDEE_ACKED +FUNDER_SIGNED = FeeUpdateProgress.FUNDER_SIGNED + +class FeeUpdate(defaultdict): + def __init__(self, chan, rate): + super().__init__(lambda: False) + self.rate = rate + self.chan = chan + + def pending_feerate(self, subject): + if self[FUNDEE_ACKED]: + return self.rate + if subject == REMOTE and self.chan.constraints.is_initiator: + return self.rate + if subject == LOCAL and not self.chan.constraints.is_initiator: + return self.rate + # implicit return None + +def decodeAll(d, local): + for k, v in d.items(): + if k == 'revocation_store': + yield (k, RevocationStore.from_json_obj(v)) + elif k.endswith("_basepoint") or k.endswith("_key"): + if local: + yield (k, Keypair(**dict(decodeAll(v, local)))) + else: + yield (k, OnlyPubkeyKeypair(**dict(decodeAll(v, local)))) + elif k in ["node_id", "channel_id", "short_channel_id", "pubkey", "privkey", "current_per_commitment_point", "next_per_commitment_point", "per_commitment_secret_seed", "current_commitment_signature", "current_htlc_signatures"] and v is not None: + yield (k, binascii.unhexlify(v)) + else: + yield (k, v) + +def htlcsum(htlcs): + return sum([x.amount_msat for x in htlcs]) + +# following two functions are used because json +# doesn't store int keys and byte string values +def str_bytes_dict_from_save(x): + return {int(k): bfh(v) for k,v in x.items()} + +def str_bytes_dict_to_save(x): + return {str(k): bh2u(v) for k, v in x.items()} + +class Channel(PrintError): + 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, sweep_address = None, name = None, payment_completed : Optional[Callable[[Direction, UpdateAddHtlc, bytes], None]] = None): + self.preimages = {} + if not payment_completed: + payment_completed = lambda this, x, y, z: None + self.sweep_address = sweep_address + self.payment_completed = payment_completed + assert 'local_state' not in state + self.config = {} + self.config[LOCAL] = state["local_config"] + if type(self.config[LOCAL]) is not LocalConfig: + conf = dict(decodeAll(self.config[LOCAL], True)) + self.config[LOCAL] = LocalConfig(**conf) + assert type(self.config[LOCAL].htlc_basepoint.privkey) is bytes + + self.config[REMOTE] = state["remote_config"] + if type(self.config[REMOTE]) is not RemoteConfig: + conf = dict(decodeAll(self.config[REMOTE], False)) + self.config[REMOTE] = RemoteConfig(**conf) + assert type(self.config[REMOTE].htlc_basepoint.pubkey) is bytes + + self.channel_id = bfh(state["channel_id"]) if type(state["channel_id"]) not in (bytes, type(None)) else state["channel_id"] + self.constraints = ChannelConstraints(**state["constraints"]) if type(state["constraints"]) is not ChannelConstraints else state["constraints"] + self.funding_outpoint = Outpoint(**dict(decodeAll(state["funding_outpoint"], False))) if type(state["funding_outpoint"]) is not Outpoint else state["funding_outpoint"] + self.node_id = bfh(state["node_id"]) if type(state["node_id"]) not in (bytes, type(None)) else state["node_id"] + self.short_channel_id = bfh(state["short_channel_id"]) if type(state["short_channel_id"]) not in (bytes, type(None)) else state["short_channel_id"] + self.short_channel_id_predicted = self.short_channel_id + self.onion_keys = str_bytes_dict_from_save(state.get('onion_keys', {})) + + # FIXME this is a tx serialised in the custom electrum partial tx format. + # we should not persist txns in this format. we should persist htlcs, and be able to derive + # any past commitment transaction and use that instead; until then... + self.remote_commitment_to_be_revoked = Transaction(state["remote_commitment_to_be_revoked"]) + self.remote_commitment_to_be_revoked.deserialize(True) + + self.hm = HTLCManager(state.get('log')) + + self.name = name + + self.pending_fee = None + + self._is_funding_txo_spent = None # "don't know" + self._state = None + if state.get('force_closed', False): + self.set_state('FORCE_CLOSING') + else: + self.set_state('DISCONNECTED') + + self.lnwatcher = None + + self.local_commitment = None + self.remote_commitment = None + + def get_payments(self): + out = {} + for subject in LOCAL, REMOTE: + log = self.hm.log[subject] + for htlc_id, htlc in log.get('adds', {}).items(): + rhash = bh2u(htlc.payment_hash) + status = 'settled' if htlc_id in log.get('settles',{}) else 'inflight' + direction = SENT if subject is LOCAL else RECEIVED + out[rhash] = (self.channel_id, htlc, direction, status) + return out + + def set_local_commitment(self, ctx): + ctn = extract_ctn_from_tx_and_chan(ctx, self) + assert self.signature_fits(ctx), (self.log[LOCAL]) + self.local_commitment = ctx + if self.sweep_address is not None: + self.local_sweeptxs = create_sweeptxs_for_our_latest_ctx(self, self.local_commitment, self.sweep_address) + initial = os.path.join(get_config().electrum_path(), 'initial_commitment_tx') + tx = self.force_close_tx().serialize_to_network() + if not os.path.exists(initial): + with open(initial, 'w') as f: + f.write(tx) + + def set_remote_commitment(self): + self.remote_commitment = self.current_commitment(REMOTE) + if self.sweep_address is not None: + self.remote_sweeptxs = create_sweeptxs_for_their_latest_ctx(self, self.remote_commitment, self.sweep_address) + + def set_state(self, state: str): + if self._state == 'FORCE_CLOSING': + assert state == 'FORCE_CLOSING', 'new state was not FORCE_CLOSING: ' + state + self._state = state + + def get_state(self): + return self._state + + def is_closed(self): + return self.get_state() in ['CLOSED', 'FORCE_CLOSING'] + + def _check_can_pay(self, amount_msat: int) -> None: + if self.get_state() != 'OPEN': + raise PaymentFailure('Channel not open') + 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)) + htlcsum(self.hm.htlcs_by_direction(LOCAL, RECEIVED)) + 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 set_funding_txo_spentness(self, is_spent: bool): + assert isinstance(is_spent, bool) + self._is_funding_txo_spent = is_spent + + def should_try_to_reestablish_peer(self) -> bool: + return self._is_funding_txo_spent is False and self._state == '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): + """ + 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 type(htlc) is dict + self._check_can_pay(htlc['amount_msat']) + htlc = UpdateAddHtlc(**htlc, htlc_id=self.config[LOCAL].next_htlc_id) + self.hm.send_htlc(htlc) + self.print_error("add_htlc") + self.config[LOCAL]=self.config[LOCAL]._replace(next_htlc_id=htlc.htlc_id + 1) + return htlc.htlc_id + + def receive_htlc(self, htlc): + """ + 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. + """ + assert type(htlc) is dict + htlc = UpdateAddHtlc(**htlc, htlc_id = self.config[REMOTE].next_htlc_id) + if 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}') + self.hm.recv_htlc(htlc) + self.print_error("receive_htlc") + self.config[REMOTE]=self.config[REMOTE]._replace(next_htlc_id=htlc.htlc_id + 1) + return htlc.htlc_id + + 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. + """ + self.print_error("sign_next_commitment") + + self.hm.send_ctx() + + pending_remote_commitment = self.pending_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') + + for_us = False + + htlcsigs = [] + # they sent => we receive + for we_receive, htlcs in zip([True, False], [self.included_htlcs(REMOTE, SENT, ctn=self.config[REMOTE].ctn+1), self.included_htlcs(REMOTE, RECEIVED, ctn=self.config[REMOTE].ctn+1)]): + for htlc in htlcs: + _script, htlc_tx = make_htlc_tx_with_open_channel(chan=self, + pcp=self.config[REMOTE].next_per_commitment_point, + for_us=for_us, + we_receive=we_receive, + commit=pending_remote_commitment, + htlc=htlc) + sig = bfh(htlc_tx.sign_txin(0, their_remote_htlc_privkey)) + htlc_sig = ecc.sig_string_from_der_sig(sig[:-1]) + htlc_output_idx = htlc_tx.inputs()[0]['prevout_n'] + htlcsigs.append((htlc_output_idx, htlc_sig)) + + htlcsigs.sort() + htlcsigs = [x[1] for x in htlcsigs] + + # TODO should add remote_commitment here and handle + # both valid ctx'es in lnwatcher at the same time... + + 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. + """ + self.print_error("receive_new_commitment") + + self.hm.recv_ctx() + + assert len(htlc_sigs) == 0 or type(htlc_sigs[0]) is bytes + + pending_local_commitment = self.pending_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('failed verifying signature of our updated commitment transaction: ' + bh2u(sig) + ' preimage is ' + preimage_hex) + + htlc_sigs_string = b''.join(htlc_sigs) + + htlc_sigs = htlc_sigs[:] # copy cause we will delete now + ctn = self.config[LOCAL].ctn+1 + for htlcs, we_receive in [(self.included_htlcs(LOCAL, SENT, ctn=ctn), False), (self.included_htlcs(LOCAL, RECEIVED, ctn=ctn), True)]: + for htlc in htlcs: + idx = self.verify_htlc(htlc, htlc_sigs, we_receive, pending_local_commitment) + del htlc_sigs[idx] + if len(htlc_sigs) != 0: # all sigs should have been popped above + raise Exception('failed verifying HTLC signatures: invalid amount of correct signatures') + + self.config[LOCAL]=self.config[LOCAL]._replace( + current_commitment_signature=sig, + current_htlc_signatures=htlc_sigs_string, + got_sig_for_next=True) + + if self.pending_fee is not None: + if not self.constraints.is_initiator: + self.pending_fee[FUNDEE_SIGNED] = True + if self.constraints.is_initiator and self.pending_fee[FUNDEE_ACKED]: + self.pending_fee[FUNDER_SIGNED] = True + + self.set_local_commitment(pending_local_commitment) + + def verify_htlc(self, htlc: UpdateAddHtlc, htlc_sigs: Sequence[bytes], we_receive: bool, ctx) -> int: + ctn = extract_ctn_from_tx_and_chan(ctx, self) + 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')) + + _script, htlc_tx = make_htlc_tx_with_open_channel(chan=self, + pcp=point, + for_us=True, + we_receive=we_receive, + commit=ctx, + htlc=htlc) + pre_hash = sha256d(bfh(htlc_tx.serialize_preimage(0))) + remote_htlc_pubkey = derive_pubkey(self.config[REMOTE].htlc_basepoint.pubkey, point) + for idx, sig in enumerate(htlc_sigs): + if ecc.verify_signature(remote_htlc_pubkey, sig, pre_hash): + return idx + else: + raise Exception(f'failed verifying HTLC signatures: {htlc}, sigs: {len(htlc_sigs)}, we_receive: {we_receive}') + + def get_remote_htlc_sig_for_htlc(self, htlc: UpdateAddHtlc, we_receive: bool, ctx) -> bytes: + data = self.config[LOCAL].current_htlc_signatures + htlc_sigs = [data[i:i + 64] for i in range(0, len(data), 64)] + idx = self.verify_htlc(htlc, htlc_sigs, we_receive=we_receive, ctx=ctx) + remote_htlc_sig = ecc.der_sig_from_sig_string(htlc_sigs[idx]) + b'\x01' + return remote_htlc_sig + + def revoke_current_commitment(self): + self.print_error("revoke_current_commitment") + + last_secret, this_point, next_point, _ = self.points() + + new_feerate = self.constraints.feerate + + if self.pending_fee is not None: + if not self.constraints.is_initiator and self.pending_fee[FUNDEE_SIGNED]: + new_feerate = self.pending_fee.rate + self.pending_fee = None + print("FEERATE CHANGE COMPLETE (non-initiator)") + if self.constraints.is_initiator and self.pending_fee[FUNDER_SIGNED]: + new_feerate = self.pending_fee.rate + self.pending_fee = None + print("FEERATE CHANGE COMPLETE (initiator)") + + assert self.config[LOCAL].got_sig_for_next + self.constraints=self.constraints._replace( + feerate=new_feerate + ) + self.set_local_commitment(self.pending_commitment(LOCAL)) + ctx = self.pending_commitment(LOCAL) + self.hm.send_rev() + self.config[LOCAL]=self.config[LOCAL]._replace( + ctn=self.config[LOCAL].ctn + 1, + got_sig_for_next=False, + ) + assert self.signature_fits(ctx) + + return RevokeAndAck(last_secret, next_point), "current htlcs" + + def points(self): + last_small_num = self.config[LOCAL].ctn + this_small_num = last_small_num + 1 + next_small_num = last_small_num + 2 + last_secret = get_per_commitment_secret_from_seed(self.config[LOCAL].per_commitment_secret_seed, RevocationStore.START_INDEX - last_small_num) + this_secret = get_per_commitment_secret_from_seed(self.config[LOCAL].per_commitment_secret_seed, RevocationStore.START_INDEX - this_small_num) + this_point = secret_to_pubkey(int.from_bytes(this_secret, 'big')) + next_secret = get_per_commitment_secret_from_seed(self.config[LOCAL].per_commitment_secret_seed, RevocationStore.START_INDEX - next_small_num) + next_point = secret_to_pubkey(int.from_bytes(next_secret, 'big')) + last_point = secret_to_pubkey(int.from_bytes(last_secret, 'big')) + return last_secret, this_point, next_point, last_point + + def process_new_revocation_secret(self, per_commitment_secret: bytes): + if not self.lnwatcher: + return + outpoint = self.funding_outpoint.to_str() + ctx = self.remote_commitment_to_be_revoked # FIXME can't we just reconstruct it? + sweeptxs = create_sweeptxs_for_their_just_revoked_ctx(self, ctx, per_commitment_secret, self.sweep_address) + for prev_txid, tx in sweeptxs.items(): + if tx is not None: + self.lnwatcher.add_sweep_tx(outpoint, prev_txid, tx.as_dict()) + + def receive_revocation(self, revocation) -> Tuple[int, int]: + self.print_error("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') + + # FIXME not sure this is correct... but it seems to work + # if there are update_add_htlc msgs between commitment_signed and rev_ack, + # this might break + prev_remote_commitment = self.pending_commitment(REMOTE) + + self.config[REMOTE].revocation_store.add_next_entry(revocation.per_commitment_secret) + self.process_new_revocation_secret(revocation.per_commitment_secret) + + ##### start applying fee/htlc changes + + if self.pending_fee is not None: + if not self.constraints.is_initiator: + self.pending_fee[FUNDEE_SIGNED] = True + if self.constraints.is_initiator and self.pending_fee[FUNDEE_ACKED]: + self.pending_fee[FUNDER_SIGNED] = True + + received = self.hm.received_in_ctn(self.config[REMOTE].ctn + 1) + sent = self.hm.sent_in_ctn(self.config[REMOTE].ctn + 1) + for htlc in received: + self.payment_completed(self, RECEIVED, htlc, None) + for htlc in sent: + preimage = self.preimages.pop(htlc.htlc_id) + self.payment_completed(self, SENT, htlc, preimage) + received_this_batch = htlcsum(received) + sent_this_batch = htlcsum(sent) + + next_point = self.config[REMOTE].next_per_commitment_point + + self.hm.recv_rev() + + self.config[REMOTE]=self.config[REMOTE]._replace( + ctn=self.config[REMOTE].ctn + 1, + current_per_commitment_point=next_point, + next_per_commitment_point=revocation.next_per_commitment_point, + ) + + if self.pending_fee is not None: + if self.constraints.is_initiator: + self.pending_fee[FUNDEE_ACKED] = True + + self.set_remote_commitment() + self.remote_commitment_to_be_revoked = prev_remote_commitment + + return received_this_batch, sent_this_batch + + def balance(self, subject, ctn=None): + """ + This balance in mSAT is not including reserve and fees. + So a node cannot actually use it's 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 + commited to later when the respective commitment + transaction as been revoked. + """ + assert type(subject) is HTLCOwner + initial = self.config[subject].initial_msat + + for direction, htlc in self.hm.settled_htlcs(subject, ctn): + if direction == SENT: + initial -= htlc.amount_msat + else: + initial += htlc.amount_msat + + return initial + + def balance_minus_outgoing_htlcs(self, subject): + """ + This balance in mSAT, which includes the value of + pending outgoing HTLCs, is used in the UI. + """ + assert type(subject) is HTLCOwner + ctn = self.hm.log[subject]['ctn'] + 1 + return self.balance(subject, ctn)\ + - htlcsum(self.hm.htlcs_by_direction(subject, SENT, ctn)) + + 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) + """ + assert type(subject) is HTLCOwner + return self.balance_minus_outgoing_htlcs(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.pending_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.config[subject].ctn + feerate = self.pending_feerate(subject) + 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) + fee_for_htlc = lambda htlc: htlc.amount_msat // 1000 - (weight * feerate // 1000) + return list(filter(lambda htlc: fee_for_htlc(htlc) >= conf.dust_limit_sat, htlcs)) + + def pending_feerate(self, subject): + assert type(subject) is HTLCOwner + candidate = self.constraints.feerate + if self.pending_fee is not None: + x = self.pending_fee.pending_feerate(subject) + if x is not None: + candidate = x + return candidate + + def pending_commitment(self, subject): + assert type(subject) is HTLCOwner + this_point = self.config[REMOTE].next_per_commitment_point if subject == REMOTE else self.points()[1] + ctn = self.config[subject].ctn + 1 + feerate = self.pending_feerate(subject) + return self.make_commitment(subject, this_point, ctn, feerate, True) + + def current_commitment(self, subject): + assert type(subject) is HTLCOwner + this_point = self.config[REMOTE].current_per_commitment_point if subject == REMOTE else self.points()[3] + ctn = self.config[subject].ctn + feerate = self.constraints.feerate + return self.make_commitment(subject, this_point, ctn, feerate, False) + + def total_msat(self, direction): + assert type(direction) is Direction + sub = LOCAL if direction == SENT else REMOTE + return htlcsum(self.hm.settled_htlcs_by(sub, self.config[sub].ctn)) + + def settle_htlc(self, preimage, htlc_id): + """ + SettleHTLC attempts to settle an existing outstanding received HTLC. + """ + self.print_error("settle_htlc") + 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) + # not saving preimage because it's already saved in LNWorker.invoices + + def receive_htlc_settle(self, preimage, htlc_id): + self.print_error("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'] + self.hm.recv_settle(htlc_id) + self.preimages[htlc_id] = preimage + # we don't save the preimage because we don't need to forward it anyway + + def fail_htlc(self, htlc_id): + self.print_error("fail_htlc") + self.hm.send_fail(htlc_id) + + def receive_fail_htlc(self, htlc_id): + self.print_error("receive_fail_htlc") + self.hm.recv_fail(htlc_id) + + @property + def current_height(self): + return {LOCAL: self.config[LOCAL].ctn, REMOTE: self.config[REMOTE].ctn} + + def pending_local_fee(self): + return self.constraints.capacity - sum(x[2] for x in self.pending_commitment(LOCAL).outputs()) + + def update_fee(self, feerate, initiator): + if self.constraints.is_initiator != initiator: + raise Exception("Cannot update_fee: wrong initiator", initiator) + if self.pending_fee is not None: + raise Exception("a fee update is already in progress") + self.pending_fee = FeeUpdate(self, rate=feerate) + + def to_save(self): + to_save = { + "local_config": self.config[LOCAL], + "remote_config": self.config[REMOTE], + "channel_id": self.channel_id, + "short_channel_id": self.short_channel_id, + "constraints": self.constraints, + "funding_outpoint": self.funding_outpoint, + "node_id": self.node_id, + "remote_commitment_to_be_revoked": str(self.remote_commitment_to_be_revoked), + "log": self.hm.to_save(), + "onion_keys": str_bytes_dict_to_save(self.onion_keys), + "force_closed": self.get_state() == 'FORCE_CLOSING', + } + return to_save + + def serialize(self): + namedtuples_to_dict = lambda v: {i: j._asdict() if isinstance(j, tuple) else j for i, j in v._asdict().items()} + serialized_channel = {} + to_save_ref = self.to_save() + for k, v in to_save_ref.items(): + if isinstance(v, tuple): + serialized_channel[k] = namedtuples_to_dict(v) + else: + serialized_channel[k] = v + dumped = ChannelJsonEncoder().encode(serialized_channel) + roundtripped = json.loads(dumped) + reconstructed = Channel(roundtripped) + to_save_new = reconstructed.to_save() + if to_save_new != to_save_ref: + from pprint import PrettyPrinter + pp = PrettyPrinter(indent=168) + try: + from deepdiff import DeepDiff + except ImportError: + raise Exception("Channels did not roundtrip serialization without changes:\n" + pp.pformat(to_save_ref) + "\n" + pp.pformat(to_save_new)) + else: + raise Exception("Channels did not roundtrip serialization without changes:\n" + pp.pformat(DeepDiff(to_save_ref, to_save_new))) + return roundtripped + + def __str__(self): + return str(self.serialize()) + + def make_commitment(self, subject, this_point, ctn, feerate, pending) -> Transaction: + #if subject == REMOTE and not pending: + # ctn -= 1 + assert type(subject) is HTLCOwner + other = REMOTE if LOCAL == subject else LOCAL + remote_msat, local_msat = self.balance(other, ctn), self.balance(subject, ctn) + received_htlcs = self.hm.htlcs_by_direction(subject, SENT if subject == LOCAL else RECEIVED, ctn) + sent_htlcs = self.hm.htlcs_by_direction(subject, RECEIVED if subject == LOCAL else SENT, ctn) + 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), + ) + 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, + self.constraints.capacity, + local_msat, + remote_msat, + this_config.dust_limit_sat, + onchain_fees, + htlcs=htlcs) + + def get_local_index(self): + return int(self.config[LOCAL].multisig_key.pubkey < self.config[REMOTE].multisig_key.pubkey) + + def make_closing_tx(self, local_script: bytes, remote_script: bytes, + fee_sat: int) -> Tuple[bytes, int, str]: + """ cooperative close """ + _, outputs = make_commitment_outputs({ + LOCAL: fee_sat * 1000 if self.constraints.is_initiator else 0, + REMOTE: fee_sat * 1000 if not self.constraints.is_initiator else 0, + }, + self.balance(LOCAL), + self.balance(REMOTE), + (TYPE_SCRIPT, bh2u(local_script)), + (TYPE_SCRIPT, bh2u(remote_script)), + [], 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): + remote_sig = self.config[LOCAL].current_commitment_signature + preimage_hex = tx.serialize_preimage(0) + pre_hash = sha256d(bfh(preimage_hex)) + assert remote_sig + res = ecc.verify_signature(self.config[REMOTE].multisig_key.pubkey, remote_sig, pre_hash) + return res + + def force_close_tx(self): + tx = self.local_commitment + assert self.signature_fits(tx) + tx = Transaction(str(tx)) + tx.deserialize(True) + 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" + sigs = tx._inputs[0]["signatures"] + none_idx = sigs.index(None) + tx.add_signature_to_txin(0, none_idx, bh2u(remote_sig)) + assert tx.is_complete() + return tx + + def included_htlcs_in_their_latest_ctxs(self, htlc_initiator) -> Dict[int, List[UpdateAddHtlc]]: + """ A map from commitment number to list of HTLCs in + their latest two commitment transactions. + The oldest might have been revoked. """ + assert type(htlc_initiator) is HTLCOwner + direction = RECEIVED if htlc_initiator == LOCAL else SENT + old_ctn = self.config[REMOTE].ctn + old_htlcs = self.included_htlcs(REMOTE, direction, ctn=old_ctn) + + new_ctn = self.config[REMOTE].ctn+1 + new_htlcs = self.included_htlcs(REMOTE, direction, ctn=new_ctn) + + return {old_ctn: old_htlcs, + new_ctn: new_htlcs, } diff --git a/electrum/lnchannelverifier.py b/electrum/lnchannelverifier.py @@ -1,206 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Electrum - lightweight Bitcoin client -# 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 asyncio -import threading -from typing import TYPE_CHECKING - -import aiorpcx - -from . import lnbase -from . import bitcoin -from . import ecc -from . import constants -from .util import bh2u, bfh, NetworkJobOnDefaultServer -from .lnutil import invert_short_channel_id, funding_output_script_from_keys -from .verifier import verify_tx_is_in_block, MerkleVerificationFailure -from .transaction import Transaction -from .interface import GracefulDisconnect -from .crypto import sha256d -from .lnmsg import encode_msg - -if TYPE_CHECKING: - from .network import Network - from .lnrouter import ChannelDB - - -class LNChannelVerifier(NetworkJobOnDefaultServer): - """ Verify channel announcements for the Channel DB """ - - # FIXME the initial routing sync is bandwidth-heavy, and the electrum server - # will start throttling us, making it even slower. one option would be to - # spread it over multiple servers. - - def __init__(self, network: 'Network', channel_db: 'ChannelDB'): - NetworkJobOnDefaultServer.__init__(self, network) - self.channel_db = channel_db - self.lock = threading.Lock() - self.unverified_channel_info = {} # short_channel_id -> channel_info - # channel announcements that seem to be invalid: - self.blacklist = set() # short_channel_id - - def _reset(self): - super()._reset() - self.started_verifying_channel = set() # short_channel_id - - # TODO make async; and rm self.lock completely - def add_new_channel_info(self, channel_info): - short_channel_id = channel_info.channel_id - if short_channel_id in self.unverified_channel_info: - return - if short_channel_id in self.blacklist: - return - if not verify_sigs_for_channel_announcement(channel_info.msg_payload): - return - with self.lock: - self.unverified_channel_info[short_channel_id] = channel_info - - def get_pending_channel_info(self, short_channel_id): - return self.unverified_channel_info.get(short_channel_id, None) - - async def _start_tasks(self): - async with self.group as group: - await group.spawn(self.main) - - async def main(self): - while True: - await self._verify_some_channels() - await asyncio.sleep(0.1) - - async def _verify_some_channels(self): - blockchain = self.network.blockchain() - local_height = blockchain.height() - - with self.lock: - unverified_channel_info = list(self.unverified_channel_info) - - for short_channel_id in unverified_channel_info: - if short_channel_id in self.started_verifying_channel: - continue - block_height, tx_pos, output_idx = invert_short_channel_id(short_channel_id) - # only resolve short_channel_id if headers are available. - if block_height <= 0 or block_height > local_height: - continue - header = blockchain.read_header(block_height) - if header is None: - if block_height < constants.net.max_checkpoint(): - await self.group.spawn(self.network.request_chunk(block_height, None, can_return_early=True)) - continue - self.started_verifying_channel.add(short_channel_id) - await self.group.spawn(self.verify_channel(block_height, tx_pos, short_channel_id)) - #self.print_error('requested short_channel_id', bh2u(short_channel_id)) - - async def verify_channel(self, block_height: int, tx_pos: int, short_channel_id: bytes): - # we are verifying channel announcements as they are from untrusted ln peers. - # we use electrum servers to do this. however we don't trust electrum servers either... - try: - result = await self.network.get_txid_from_txpos(block_height, tx_pos, True) - except aiorpcx.jsonrpc.RPCError: - # the electrum server is complaining about the tx_pos for given block. - # it is not clear what to do now, but let's believe the server. - self._blacklist_short_channel_id(short_channel_id) - return - tx_hash = result['tx_hash'] - merkle_branch = result['merkle'] - # we need to wait if header sync/reorg is still ongoing, hence lock: - async with self.network.bhi_lock: - header = self.network.blockchain().read_header(block_height) - try: - verify_tx_is_in_block(tx_hash, merkle_branch, tx_pos, header, block_height) - except MerkleVerificationFailure as e: - # the electrum server sent an incorrect proof. blame is on server, not the ln peer - raise GracefulDisconnect(e) from e - try: - raw_tx = await self.network.get_transaction(tx_hash) - except aiorpcx.jsonrpc.RPCError as e: - # the electrum server can't find the tx; but it was the - # one who told us about the txid!! blame is on server - raise GracefulDisconnect(e) from e - tx = Transaction(raw_tx) - try: - tx.deserialize() - except Exception: - # either bug in client, or electrum server is evil. - # if we connect to a diff server at some point, let's try again. - self.print_msg("cannot deserialize transaction, skipping", tx_hash) - return - if tx_hash != tx.txid(): - # either bug in client, or electrum server is evil. - # if we connect to a diff server at some point, let's try again. - self.print_error(f"received tx does not match expected txid ({tx_hash} != {tx.txid()})") - return - # check funding output - channel_info = self.unverified_channel_info[short_channel_id] - chan_ann = channel_info.msg_payload - redeem_script = funding_output_script_from_keys(chan_ann['bitcoin_key_1'], chan_ann['bitcoin_key_2']) - expected_address = bitcoin.redeem_script_to_address('p2wsh', redeem_script) - output_idx = invert_short_channel_id(short_channel_id)[2] - try: - actual_output = tx.outputs()[output_idx] - except IndexError: - self._blacklist_short_channel_id(short_channel_id) - return - if expected_address != actual_output.address: - # FIXME what now? best would be to ban the originating ln peer. - self.print_error(f"funding output script mismatch for {bh2u(short_channel_id)}") - self._remove_channel_from_unverified_db(short_channel_id) - return - # put channel into channel DB - channel_info.set_capacity(actual_output.value) - self.channel_db.add_verified_channel_info(short_channel_id, channel_info) - self._remove_channel_from_unverified_db(short_channel_id) - - def _remove_channel_from_unverified_db(self, short_channel_id: bytes): - with self.lock: - self.unverified_channel_info.pop(short_channel_id, None) - try: self.started_verifying_channel.remove(short_channel_id) - except KeyError: pass - - def _blacklist_short_channel_id(self, short_channel_id: bytes) -> None: - self.blacklist.add(short_channel_id) - with self.lock: - self.unverified_channel_info.pop(short_channel_id, None) - - -def verify_sigs_for_channel_announcement(chan_ann: dict) -> bool: - msg_bytes = encode_msg('channel_announcement', **chan_ann) - pre_hash = msg_bytes[2+256:] - h = sha256d(pre_hash) - pubkeys = [chan_ann['node_id_1'], chan_ann['node_id_2'], chan_ann['bitcoin_key_1'], chan_ann['bitcoin_key_2']] - sigs = [chan_ann['node_signature_1'], chan_ann['node_signature_2'], chan_ann['bitcoin_signature_1'], chan_ann['bitcoin_signature_2']] - for pubkey, sig in zip(pubkeys, sigs): - if not ecc.verify_signature(pubkey, sig, h): - return False - return True - - -def verify_sig_for_channel_update(chan_upd: dict, node_id: bytes) -> bool: - msg_bytes = encode_msg('channel_update', **chan_upd) - pre_hash = msg_bytes[2+64:] - h = sha256d(pre_hash) - sig = chan_upd['signature'] - if not ecc.verify_signature(node_id, sig, h): - return False - return True diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py @@ -0,0 +1,1105 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 The Electrum developers +# Distributed under the MIT software license, see the accompanying +# file LICENCE or http://www.opensource.org/licenses/mit-license.php + +from collections import OrderedDict, defaultdict +import json +import asyncio +import os +import time +from functools import partial +from typing import List, Tuple, Dict, TYPE_CHECKING, Optional, Callable +import traceback +import sys + +import aiorpcx + +from .simple_config import get_config +from .crypto import sha256, sha256d +from . import bitcoin +from . import ecc +from .ecc import sig_string_from_r_and_s, get_r_and_s_from_sig_string, der_sig_from_sig_string +from . import constants +from .util import PrintError, bh2u, print_error, bfh, log_exceptions, list_enabled_bits, ignore_exceptions +from .transaction import Transaction, TxOutput +from .lnonion import (new_onion_packet, decode_onion_error, OnionFailureCode, calc_hops_data_for_payment, + process_onion_packet, OnionPacket, construct_onion_error, OnionRoutingFailureMessage) +from .lnchannel import Channel, RevokeAndAck, htlcsum +from .lnutil import (Outpoint, LocalConfig, RECEIVED, UpdateAddHtlc, + RemoteConfig, OnlyPubkeyKeypair, ChannelConstraints, RevocationStore, + funding_output_script, get_per_commitment_secret_from_seed, + secret_to_pubkey, PaymentFailure, LnLocalFeatures, + LOCAL, REMOTE, HTLCOwner, generate_keypair, LnKeyFamily, + get_ln_flag_pair_of_bit, privkey_to_pubkey, UnknownPaymentHash, MIN_FINAL_CLTV_EXPIRY_ACCEPTED, + LightningPeerConnectionClosed, HandshakeFailed, NotFoundChanAnnouncementForUpdate, + MINIMUM_MAX_HTLC_VALUE_IN_FLIGHT_ACCEPTED, MAXIMUM_HTLC_MINIMUM_MSAT_ACCEPTED, + MAXIMUM_REMOTE_TO_SELF_DELAY_ACCEPTED) +from .lntransport import LNTransport, LNTransportBase +from .lnmsg import encode_msg, decode_msg + +if TYPE_CHECKING: + from .lnworker import LNWorker + from .lnrouter import RouteEdge + + +def channel_id_from_funding_tx(funding_txid: str, funding_index: int) -> Tuple[bytes, bytes]: + funding_txid_bytes = bytes.fromhex(funding_txid)[::-1] + i = int.from_bytes(funding_txid_bytes, 'big') ^ funding_index + return i.to_bytes(32, 'big'), funding_txid_bytes + +class Peer(PrintError): + + def __init__(self, lnworker: 'LNWorker', pubkey:bytes, transport: LNTransportBase, + request_initial_sync=False): + self.initialized = asyncio.Event() + self.transport = transport + self.pubkey = pubkey + self.lnworker = lnworker + self.privkey = lnworker.node_keypair.privkey + self.node_ids = [self.pubkey, privkey_to_pubkey(self.privkey)] + self.network = lnworker.network + self.lnwatcher = lnworker.network.lnwatcher + self.channel_db = lnworker.network.channel_db + self.ping_time = 0 + self.shutdown_received = defaultdict(asyncio.Future) + self.channel_accepted = defaultdict(asyncio.Queue) + self.channel_reestablished = defaultdict(asyncio.Future) + self.funding_signed = defaultdict(asyncio.Queue) + self.funding_created = defaultdict(asyncio.Queue) + self.revoke_and_ack = defaultdict(asyncio.Queue) + self.commitment_signed = defaultdict(asyncio.Queue) + self.announcement_signatures = defaultdict(asyncio.Queue) + self.closing_signed = defaultdict(asyncio.Queue) + self.payment_preimages = defaultdict(asyncio.Queue) + self.localfeatures = LnLocalFeatures(0) + if request_initial_sync: + self.localfeatures |= LnLocalFeatures.INITIAL_ROUTING_SYNC + self.localfeatures |= LnLocalFeatures.OPTION_DATA_LOSS_PROTECT_REQ + self.attempted_route = {} + self.orphan_channel_updates = OrderedDict() + + def send_message(self, message_name: str, **kwargs): + assert type(message_name) is str + self.print_error("Sending '%s'"%message_name.upper()) + self.transport.send_bytes(encode_msg(message_name, **kwargs)) + + async def initialize(self): + if isinstance(self.transport, LNTransport): + await self.transport.handshake() + self.channel_db.add_recent_peer(self.transport.peer_addr) + self.send_message("init", gflen=0, lflen=1, localfeatures=self.localfeatures) + + @property + def channels(self) -> Dict[bytes, Channel]: + return self.lnworker.channels_for_peer(self.pubkey) + + def diagnostic_name(self): + return self.transport.name() + + def ping_if_required(self): + if time.time() - self.ping_time > 120: + self.send_message('ping', num_pong_bytes=4, byteslen=4) + self.ping_time = time.time() + + def process_message(self, message): + message_type, payload = decode_msg(message) + try: + f = getattr(self, 'on_' + message_type) + except AttributeError: + self.print_error("Received '%s'" % message_type.upper(), payload) + return + # raw message is needed to check signature + if message_type=='node_announcement': + payload['raw'] = message + execution_result = f(payload) + if asyncio.iscoroutinefunction(f): + asyncio.ensure_future(execution_result) + + def on_error(self, payload): + # todo: self.channel_reestablished is not a queue + self.print_error("error", payload["data"].decode("ascii")) + chan_id = payload.get("channel_id") + for d in [ self.channel_accepted, self.funding_signed, + self.funding_created, self.revoke_and_ack, self.commitment_signed, + self.announcement_signatures, self.closing_signed ]: + if chan_id in d: + d[chan_id].put_nowait({'error':payload['data']}) + + def on_ping(self, payload): + l = int.from_bytes(payload['num_pong_bytes'], 'big') + self.send_message('pong', byteslen=l) + + def on_pong(self, payload): + pass + + def on_accept_channel(self, payload): + temp_chan_id = payload["temporary_channel_id"] + if temp_chan_id not in self.channel_accepted: raise Exception("Got unknown accept_channel") + self.channel_accepted[temp_chan_id].put_nowait(payload) + + def on_funding_signed(self, payload): + channel_id = payload['channel_id'] + if channel_id not in self.funding_signed: raise Exception("Got unknown funding_signed") + self.funding_signed[channel_id].put_nowait(payload) + + def on_funding_created(self, payload): + channel_id = payload['temporary_channel_id'] + if channel_id not in self.funding_created: raise Exception("Got unknown funding_created") + self.funding_created[channel_id].put_nowait(payload) + + def on_node_announcement(self, payload): + self.channel_db.on_node_announcement(payload) + self.network.trigger_callback('ln_status') + + def on_init(self, payload): + if self.initialized.is_set(): + self.print_error("ALREADY INITIALIZED BUT RECEIVED INIT") + return + # if they required some even flag we don't have, they will close themselves + # but if we require an even flag they don't have, we close + our_flags = set(list_enabled_bits(self.localfeatures)) + their_flags = set(list_enabled_bits(int.from_bytes(payload['localfeatures'], byteorder="big"))) + for flag in our_flags: + if flag not in their_flags and get_ln_flag_pair_of_bit(flag) not in their_flags: + # they don't have this feature we wanted :( + if flag % 2 == 0: # even flags are compulsory + raise LightningPeerConnectionClosed("remote does not have even flag {}" + .format(str(LnLocalFeatures(1 << flag)))) + self.localfeatures ^= 1 << flag # disable flag + first_timestamp = self.lnworker.get_first_timestamp() + self.send_message('gossip_timestamp_filter', chain_hash=constants.net.rev_genesis_bytes(), first_timestamp=first_timestamp, timestamp_range=b"\xff"*4) + self.initialized.set() + + def on_channel_update(self, payload): + try: + self.channel_db.on_channel_update(payload) + except NotFoundChanAnnouncementForUpdate: + # If it's for a direct channel with this peer, save it for later, as it might be + # for our own channel (and we might not yet know the short channel id for that) + short_channel_id = payload['short_channel_id'] + self.print_error("not found channel announce for channel update in db", bh2u(short_channel_id)) + self.orphan_channel_updates[short_channel_id] = payload + while len(self.orphan_channel_updates) > 10: + self.orphan_channel_updates.popitem(last=False) + + def on_channel_announcement(self, payload): + self.channel_db.on_channel_announcement(payload) + + def on_announcement_signatures(self, payload): + channel_id = payload['channel_id'] + chan = self.channels[payload['channel_id']] + if chan.config[LOCAL].was_announced: + h, local_node_sig, local_bitcoin_sig = self.send_announcement_signatures(chan) + else: + self.announcement_signatures[channel_id].put_nowait(payload) + + def handle_disconnect(func): + async def wrapper_func(self, *args, **kwargs): + try: + return await func(self, *args, **kwargs) + except LightningPeerConnectionClosed as e: + self.print_error("disconnecting gracefully. {}".format(e)) + finally: + self.close_and_cleanup() + self.lnworker.peers.pop(self.pubkey) + return wrapper_func + + @ignore_exceptions # do not kill main_taskgroup + @log_exceptions + @handle_disconnect + async def main_loop(self): + """ + This is used in LNWorker and is necessary so that we don't kill the main + task group. It is not merged with _main_loop, so that we can test if the + correct exceptions are getting thrown using _main_loop. + """ + await self._main_loop() + + async def _main_loop(self): + """This is separate from main_loop for the tests.""" + try: + await asyncio.wait_for(self.initialize(), 10) + except (OSError, asyncio.TimeoutError, HandshakeFailed) as e: + self.print_error('initialize failed, disconnecting: {}'.format(repr(e))) + return + # loop + async for msg in self.transport.read_messages(): + self.process_message(msg) + self.ping_if_required() + + def close_and_cleanup(self): + try: + if self.transport: + self.transport.close() + except: + pass + for chan in self.channels.values(): + if chan.get_state() != 'FORCE_CLOSING': + chan.set_state('DISCONNECTED') + self.network.trigger_callback('channel', chan) + + def make_local_config(self, funding_sat: int, push_msat: int, initiator: HTLCOwner) -> LocalConfig: + # key derivation + channel_counter = self.lnworker.get_and_inc_counter_for_channel_keys() + keypair_generator = lambda family: generate_keypair(self.lnworker.ln_keystore, family, channel_counter) + if initiator == LOCAL: + initial_msat = funding_sat * 1000 - push_msat + else: + initial_msat = push_msat + local_config=LocalConfig( + payment_basepoint=keypair_generator(LnKeyFamily.PAYMENT_BASE), + multisig_key=keypair_generator(LnKeyFamily.MULTISIG), + htlc_basepoint=keypair_generator(LnKeyFamily.HTLC_BASE), + delayed_basepoint=keypair_generator(LnKeyFamily.DELAY_BASE), + revocation_basepoint=keypair_generator(LnKeyFamily.REVOCATION_BASE), + to_self_delay=9, + dust_limit_sat=546, + max_htlc_value_in_flight_msat=funding_sat * 1000, + max_accepted_htlcs=5, + initial_msat=initial_msat, + ctn=-1, + next_htlc_id=0, + reserve_sat=546, + per_commitment_secret_seed=keypair_generator(LnKeyFamily.REVOCATION_ROOT).privkey, + funding_locked_received=False, + was_announced=False, + current_commitment_signature=None, + current_htlc_signatures=[], + got_sig_for_next=False, + ) + return local_config + + @log_exceptions + async def channel_establishment_flow(self, password: Optional[str], funding_sat: int, + push_msat: int, temp_channel_id: bytes) -> Channel: + wallet = self.lnworker.wallet + # dry run creating funding tx to see if we even have enough funds + funding_tx_test = wallet.mktx([TxOutput(bitcoin.TYPE_ADDRESS, wallet.dummy_address(), funding_sat)], + password, self.lnworker.config, nonlocal_only=True) + await asyncio.wait_for(self.initialized.wait(), 1) + feerate = self.lnworker.current_feerate_per_kw() + local_config = self.make_local_config(funding_sat, push_msat, LOCAL) + # for the first commitment transaction + per_commitment_secret_first = get_per_commitment_secret_from_seed(local_config.per_commitment_secret_seed, + RevocationStore.START_INDEX) + per_commitment_point_first = secret_to_pubkey(int.from_bytes(per_commitment_secret_first, 'big')) + self.send_message( + "open_channel", + temporary_channel_id=temp_channel_id, + chain_hash=constants.net.rev_genesis_bytes(), + funding_satoshis=funding_sat, + push_msat=push_msat, + dust_limit_satoshis=local_config.dust_limit_sat, + feerate_per_kw=feerate, + max_accepted_htlcs=local_config.max_accepted_htlcs, + funding_pubkey=local_config.multisig_key.pubkey, + revocation_basepoint=local_config.revocation_basepoint.pubkey, + htlc_basepoint=local_config.htlc_basepoint.pubkey, + payment_basepoint=local_config.payment_basepoint.pubkey, + delayed_payment_basepoint=local_config.delayed_basepoint.pubkey, + first_per_commitment_point=per_commitment_point_first, + to_self_delay=local_config.to_self_delay, + max_htlc_value_in_flight_msat=local_config.max_htlc_value_in_flight_msat, + channel_flags=0x00, # not willing to announce channel + channel_reserve_satoshis=local_config.reserve_sat, + htlc_minimum_msat=1, + ) + payload = await asyncio.wait_for(self.channel_accepted[temp_channel_id].get(), 1) + if payload.get('error'): + raise Exception('Remote Lightning peer reported error: ' + repr(payload.get('error'))) + remote_per_commitment_point = payload['first_per_commitment_point'] + funding_txn_minimum_depth = int.from_bytes(payload['minimum_depth'], 'big') + assert funding_txn_minimum_depth > 0, funding_txn_minimum_depth + remote_dust_limit_sat = int.from_bytes(payload['dust_limit_satoshis'], byteorder='big') + remote_reserve_sat = self.validate_remote_reserve(payload["channel_reserve_satoshis"], remote_dust_limit_sat, funding_sat) + if remote_dust_limit_sat > remote_reserve_sat: + raise Exception(f"Remote Lightning peer reports dust_limit_sat > reserve_sat which is a BOLT-02 protocol violation.") + htlc_min = int.from_bytes(payload['htlc_minimum_msat'], 'big') + if htlc_min > MAXIMUM_HTLC_MINIMUM_MSAT_ACCEPTED: + raise Exception(f"Remote Lightning peer reports htlc_minimum_msat={htlc_min} mSAT," + + f" which is above Electrums required maximum limit of that parameter ({MAXIMUM_HTLC_MINIMUM_MSAT_ACCEPTED} mSAT).") + remote_max = int.from_bytes(payload['max_htlc_value_in_flight_msat'], 'big') + if remote_max < MINIMUM_MAX_HTLC_VALUE_IN_FLIGHT_ACCEPTED: + raise Exception(f"Remote Lightning peer reports max_htlc_value_in_flight_msat at only {remote_max} mSAT" + + f" which is below Electrums required minimum ({MINIMUM_MAX_HTLC_VALUE_IN_FLIGHT_ACCEPTED} mSAT).") + max_accepted_htlcs = int.from_bytes(payload["max_accepted_htlcs"], 'big') + if max_accepted_htlcs > 483: + raise Exception("Remote Lightning peer reports max_accepted_htlcs > 483, which is a BOLT-02 protocol violation.") + remote_to_self_delay = int.from_bytes(payload['to_self_delay'], byteorder='big') + if remote_to_self_delay > MAXIMUM_REMOTE_TO_SELF_DELAY_ACCEPTED: + raise Exception(f"Remote Lightning peer reports to_self_delay={remote_to_self_delay}," + + f" which is above Electrums required maximum ({MAXIMUM_REMOTE_TO_SELF_DELAY_ACCEPTED})") + their_revocation_store = RevocationStore() + remote_config = RemoteConfig( + payment_basepoint=OnlyPubkeyKeypair(payload['payment_basepoint']), + multisig_key=OnlyPubkeyKeypair(payload["funding_pubkey"]), + htlc_basepoint=OnlyPubkeyKeypair(payload['htlc_basepoint']), + delayed_basepoint=OnlyPubkeyKeypair(payload['delayed_payment_basepoint']), + revocation_basepoint=OnlyPubkeyKeypair(payload['revocation_basepoint']), + to_self_delay=remote_to_self_delay, + dust_limit_sat=remote_dust_limit_sat, + max_htlc_value_in_flight_msat=remote_max, + max_accepted_htlcs=max_accepted_htlcs, + initial_msat=push_msat, + ctn = -1, + next_htlc_id = 0, + reserve_sat = remote_reserve_sat, + htlc_minimum_msat = htlc_min, + + next_per_commitment_point=remote_per_commitment_point, + current_per_commitment_point=None, + revocation_store=their_revocation_store, + ) + # create funding tx + redeem_script = funding_output_script(local_config, remote_config) + funding_address = bitcoin.redeem_script_to_address('p2wsh', redeem_script) + funding_output = TxOutput(bitcoin.TYPE_ADDRESS, funding_address, funding_sat) + funding_tx = wallet.mktx([funding_output], password, self.lnworker.config, nonlocal_only=True) + funding_txid = funding_tx.txid() + funding_index = funding_tx.outputs().index(funding_output) + # remote commitment transaction + channel_id, funding_txid_bytes = channel_id_from_funding_tx(funding_txid, funding_index) + chan_dict = { + "node_id": self.pubkey, + "channel_id": channel_id, + "short_channel_id": None, + "funding_outpoint": Outpoint(funding_txid, funding_index), + "remote_config": remote_config, + "local_config": local_config, + "constraints": ChannelConstraints(capacity=funding_sat, is_initiator=True, funding_txn_minimum_depth=funding_txn_minimum_depth, feerate=feerate), + "remote_commitment_to_be_revoked": None, + } + chan = Channel(chan_dict, + sweep_address=self.lnworker.sweep_address, + payment_completed=self.lnworker.payment_completed) + chan.lnwatcher = self.lnwatcher + chan.get_preimage_and_invoice = self.lnworker.get_invoice # FIXME hack. + sig_64, _ = chan.sign_next_commitment() + self.send_message("funding_created", + temporary_channel_id=temp_channel_id, + funding_txid=funding_txid_bytes, + funding_output_index=funding_index, + signature=sig_64) + payload = await asyncio.wait_for(self.funding_signed[channel_id].get(), 1) + self.print_error('received funding_signed') + remote_sig = payload['signature'] + chan.receive_new_commitment(remote_sig, []) + # broadcast funding tx + await asyncio.wait_for(self.network.broadcast_transaction(funding_tx), 1) + chan.remote_commitment_to_be_revoked = chan.pending_commitment(REMOTE) + chan.config[REMOTE] = chan.config[REMOTE]._replace(ctn=0, current_per_commitment_point=remote_per_commitment_point, next_per_commitment_point=None) + chan.config[LOCAL] = chan.config[LOCAL]._replace(ctn=0, current_commitment_signature=remote_sig, got_sig_for_next=False) + chan.set_state('OPENING') + chan.set_remote_commitment() + chan.set_local_commitment(chan.current_commitment(LOCAL)) + return chan + + async def on_open_channel(self, payload): + # payload['channel_flags'] + if payload['chain_hash'] != constants.net.rev_genesis_bytes(): + raise Exception('wrong chain_hash') + funding_sat = int.from_bytes(payload['funding_satoshis'], 'big') + push_msat = int.from_bytes(payload['push_msat'], 'big') + feerate = int.from_bytes(payload['feerate_per_kw'], 'big') + + temp_chan_id = payload['temporary_channel_id'] + local_config = self.make_local_config(funding_sat, push_msat, REMOTE) + # for the first commitment transaction + per_commitment_secret_first = get_per_commitment_secret_from_seed(local_config.per_commitment_secret_seed, + RevocationStore.START_INDEX) + per_commitment_point_first = secret_to_pubkey(int.from_bytes(per_commitment_secret_first, 'big')) + min_depth = 3 + self.send_message('accept_channel', + temporary_channel_id=temp_chan_id, + dust_limit_satoshis=local_config.dust_limit_sat, + max_htlc_value_in_flight_msat=local_config.max_htlc_value_in_flight_msat, + channel_reserve_satoshis=local_config.reserve_sat, + htlc_minimum_msat=1000, + minimum_depth=min_depth, + to_self_delay=local_config.to_self_delay, + max_accepted_htlcs=local_config.max_accepted_htlcs, + funding_pubkey=local_config.multisig_key.pubkey, + revocation_basepoint=local_config.revocation_basepoint.pubkey, + payment_basepoint=local_config.payment_basepoint.pubkey, + delayed_payment_basepoint=local_config.delayed_basepoint.pubkey, + htlc_basepoint=local_config.htlc_basepoint.pubkey, + first_per_commitment_point=per_commitment_point_first, + ) + funding_created = await self.funding_created[temp_chan_id].get() + funding_idx = int.from_bytes(funding_created['funding_output_index'], 'big') + funding_txid = bh2u(funding_created['funding_txid'][::-1]) + channel_id, funding_txid_bytes = channel_id_from_funding_tx(funding_txid, funding_idx) + their_revocation_store = RevocationStore() + remote_balance_sat = funding_sat * 1000 - push_msat + remote_dust_limit_sat = int.from_bytes(payload['dust_limit_satoshis'], byteorder='big') # TODO validate + remote_reserve_sat = self.validate_remote_reserve(payload['channel_reserve_satoshis'], remote_dust_limit_sat, funding_sat) + chan_dict = { + "node_id": self.pubkey, + "channel_id": channel_id, + "short_channel_id": None, + "funding_outpoint": Outpoint(funding_txid, funding_idx), + "remote_config": RemoteConfig( + payment_basepoint=OnlyPubkeyKeypair(payload['payment_basepoint']), + multisig_key=OnlyPubkeyKeypair(payload['funding_pubkey']), + htlc_basepoint=OnlyPubkeyKeypair(payload['htlc_basepoint']), + delayed_basepoint=OnlyPubkeyKeypair(payload['delayed_payment_basepoint']), + revocation_basepoint=OnlyPubkeyKeypair(payload['revocation_basepoint']), + to_self_delay=int.from_bytes(payload['to_self_delay'], 'big'), + dust_limit_sat=remote_dust_limit_sat, + max_htlc_value_in_flight_msat=int.from_bytes(payload['max_htlc_value_in_flight_msat'], 'big'), # TODO validate + max_accepted_htlcs=int.from_bytes(payload['max_accepted_htlcs'], 'big'), # TODO validate + initial_msat=remote_balance_sat, + ctn = -1, + next_htlc_id = 0, + reserve_sat = remote_reserve_sat, + htlc_minimum_msat=int.from_bytes(payload['htlc_minimum_msat'], 'big'), # TODO validate + + next_per_commitment_point=payload['first_per_commitment_point'], + current_per_commitment_point=None, + revocation_store=their_revocation_store, + ), + "local_config": local_config, + "constraints": ChannelConstraints(capacity=funding_sat, is_initiator=False, funding_txn_minimum_depth=min_depth, feerate=feerate), + "remote_commitment_to_be_revoked": None, + } + chan = Channel(chan_dict, + sweep_address=self.lnworker.sweep_address, + payment_completed=self.lnworker.payment_completed) + chan.lnwatcher = self.lnwatcher + chan.get_preimage_and_invoice = self.lnworker.get_invoice # FIXME hack. + remote_sig = funding_created['signature'] + chan.receive_new_commitment(remote_sig, []) + sig_64, _ = chan.sign_next_commitment() + self.send_message('funding_signed', + channel_id=channel_id, + signature=sig_64, + ) + chan.set_state('OPENING') + chan.remote_commitment_to_be_revoked = chan.pending_commitment(REMOTE) + chan.config[REMOTE] = chan.config[REMOTE]._replace(ctn=0, current_per_commitment_point=payload['first_per_commitment_point'], next_per_commitment_point=None) + chan.config[LOCAL] = chan.config[LOCAL]._replace(ctn=0, current_commitment_signature=remote_sig) + self.lnworker.save_channel(chan) + self.lnwatcher.watch_channel(chan.get_funding_address(), chan.funding_outpoint.to_str()) + self.lnworker.on_channels_updated() + while True: + try: + funding_tx = Transaction(await self.network.get_transaction(funding_txid)) + except aiorpcx.jsonrpc.RPCError as e: + print("sleeping", str(e)) + await asyncio.sleep(1) + else: + break + outp = funding_tx.outputs()[funding_idx] + redeem_script = funding_output_script(chan.config[REMOTE], chan.config[LOCAL]) + funding_address = bitcoin.redeem_script_to_address('p2wsh', redeem_script) + if outp != TxOutput(bitcoin.TYPE_ADDRESS, funding_address, funding_sat): + chan.set_state('DISCONNECTED') + raise Exception('funding outpoint mismatch') + + def validate_remote_reserve(self, payload_field: bytes, dust_limit: int, funding_sat: int) -> int: + remote_reserve_sat = int.from_bytes(payload_field, 'big') + if remote_reserve_sat < dust_limit: + raise Exception('protocol violation: reserve < dust_limit') + if remote_reserve_sat > funding_sat/100: + raise Exception(f'reserve too high: {remote_reserve_sat}, funding_sat: {funding_sat}') + return remote_reserve_sat + + def on_channel_reestablish(self, payload): + chan_id = payload["channel_id"] + self.print_error("Received channel_reestablish", bh2u(chan_id)) + chan = self.channels.get(chan_id) + if not chan: + self.print_error("Warning: received unknown channel_reestablish", bh2u(chan_id)) + return + self.channel_reestablished[chan_id].set_result(payload) + + @log_exceptions + async def reestablish_channel(self, chan: Channel): + await self.initialized.wait() + chan_id = chan.channel_id + if chan.get_state() != 'DISCONNECTED': + self.print_error('reestablish_channel was called but channel {} already in state {}' + .format(chan_id, chan.get_state())) + return + chan.set_state('REESTABLISHING') + self.network.trigger_callback('channel', chan) + self.send_message("channel_reestablish", + channel_id=chan_id, + next_local_commitment_number=chan.config[LOCAL].ctn+1, + next_remote_revocation_number=chan.config[REMOTE].ctn + ) + channel_reestablish_msg = await self.channel_reestablished[chan_id] + chan.set_state('OPENING') + + def try_to_get_remote_to_force_close_with_their_latest(): + self.print_error("trying to get remote to force close", bh2u(chan_id)) + self.send_message("channel_reestablish", + channel_id=chan_id, + next_local_commitment_number=0, + next_remote_revocation_number=0 + ) + # compare remote ctns + remote_ctn = int.from_bytes(channel_reestablish_msg["next_local_commitment_number"], 'big') + if remote_ctn != chan.config[REMOTE].ctn + 1: + self.print_error("expected remote ctn {}, got {}".format(chan.config[REMOTE].ctn + 1, remote_ctn)) + # TODO iff their ctn is lower than ours, we should force close instead + try_to_get_remote_to_force_close_with_their_latest() + return + # compare local ctns + local_ctn = int.from_bytes(channel_reestablish_msg["next_remote_revocation_number"], 'big') + if local_ctn != chan.config[LOCAL].ctn: + if remote_ctn == chan.config[LOCAL].ctn + 1: + # A node: + # if next_remote_revocation_number is equal to the + # commitment number of the last revoke_and_ack + # the receiving node sent, AND the receiving node + # hasn't already received a closing_signed: + # MUST re-send the revoke_and_ack. + chan.config[LOCAL]=chan.config[LOCAL]._replace( + ctn=remote_ctn, + ) + self.send_revoke_and_ack(chan) + else: + self.print_error("expected local ctn {}, got {}".format(chan.config[LOCAL].ctn, local_ctn)) + # TODO iff their ctn is lower than ours, we should force close instead + try_to_get_remote_to_force_close_with_their_latest() + return + # compare per commitment points (needs data_protect option) + their_pcp = channel_reestablish_msg.get("my_current_per_commitment_point", None) + if their_pcp is not None: + our_pcp = chan.config[REMOTE].current_per_commitment_point + if our_pcp is None: + our_pcp = chan.config[REMOTE].next_per_commitment_point + if our_pcp != their_pcp: + self.print_error("Remote PCP mismatch: {} {}".format(bh2u(our_pcp), bh2u(their_pcp))) + # FIXME ...what now? + try_to_get_remote_to_force_close_with_their_latest() + return + if remote_ctn == chan.config[LOCAL].ctn+1 == 1 and chan.short_channel_id: + self.send_funding_locked(chan) + # checks done + if chan.config[LOCAL].funding_locked_received and chan.short_channel_id: + self.mark_open(chan) + self.network.trigger_callback('channel', chan) + + def send_funding_locked(self, chan: Channel): + channel_id = chan.channel_id + per_commitment_secret_index = RevocationStore.START_INDEX - 1 + per_commitment_point_second = secret_to_pubkey(int.from_bytes( + get_per_commitment_secret_from_seed(chan.config[LOCAL].per_commitment_secret_seed, per_commitment_secret_index), 'big')) + # note: if funding_locked was not yet received, we might send it multiple times + self.send_message("funding_locked", channel_id=channel_id, next_per_commitment_point=per_commitment_point_second) + if chan.config[LOCAL].funding_locked_received and chan.short_channel_id: + self.mark_open(chan) + + def on_funding_locked(self, payload): + channel_id = payload['channel_id'] + chan = self.channels.get(channel_id) + if not chan: + print(self.channels) + raise Exception("Got unknown funding_locked", channel_id) + if not chan.config[LOCAL].funding_locked_received: + our_next_point = chan.config[REMOTE].next_per_commitment_point + their_next_point = payload["next_per_commitment_point"] + new_remote_state = chan.config[REMOTE]._replace(next_per_commitment_point=their_next_point) + new_local_state = chan.config[LOCAL]._replace(funding_locked_received = True) + chan.config[REMOTE]=new_remote_state + chan.config[LOCAL]=new_local_state + self.lnworker.save_channel(chan) + if chan.short_channel_id: + self.mark_open(chan) + + def on_network_update(self, chan: Channel, funding_tx_depth: int): + """ + Only called when the channel is OPEN. + + Runs on the Network thread. + """ + if not chan.config[LOCAL].was_announced and funding_tx_depth >= 6: + # don't announce our channels + # FIXME should this be a field in chan.local_state maybe? + return + chan.config[LOCAL]=chan.config[LOCAL]._replace(was_announced=True) + coro = self.handle_announcements(chan) + self.lnworker.save_channel(chan) + asyncio.run_coroutine_threadsafe(coro, self.network.asyncio_loop) + + @log_exceptions + async def handle_announcements(self, chan): + h, local_node_sig, local_bitcoin_sig = self.send_announcement_signatures(chan) + announcement_signatures_msg = await self.announcement_signatures[chan.channel_id].get() + remote_node_sig = announcement_signatures_msg["node_signature"] + remote_bitcoin_sig = announcement_signatures_msg["bitcoin_signature"] + if not ecc.verify_signature(chan.config[REMOTE].multisig_key.pubkey, remote_bitcoin_sig, h): + raise Exception("bitcoin_sig invalid in announcement_signatures") + if not ecc.verify_signature(self.pubkey, remote_node_sig, h): + raise Exception("node_sig invalid in announcement_signatures") + + node_sigs = [remote_node_sig, local_node_sig] + bitcoin_sigs = [remote_bitcoin_sig, local_bitcoin_sig] + bitcoin_keys = [chan.config[REMOTE].multisig_key.pubkey, chan.config[LOCAL].multisig_key.pubkey] + + if self.node_ids[0] > self.node_ids[1]: + node_sigs.reverse() + bitcoin_sigs.reverse() + node_ids = list(reversed(self.node_ids)) + bitcoin_keys.reverse() + else: + node_ids = self.node_ids + + self.send_message("channel_announcement", + node_signatures_1=node_sigs[0], + node_signatures_2=node_sigs[1], + bitcoin_signature_1=bitcoin_sigs[0], + bitcoin_signature_2=bitcoin_sigs[1], + len=0, + #features not set (defaults to zeros) + chain_hash=constants.net.rev_genesis_bytes(), + short_channel_id=chan.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] + ) + + print("SENT CHANNEL ANNOUNCEMENT") + + def mark_open(self, chan: Channel): + assert chan.short_channel_id is not None + if chan.get_state() == "OPEN": + return + # NOTE: even closed channels will be temporarily marked "OPEN" + assert chan.config[LOCAL].funding_locked_received + chan.set_state("OPEN") + self.network.trigger_callback('channel', chan) + # add channel to database + bitcoin_keys = [chan.config[LOCAL].multisig_key.pubkey, chan.config[REMOTE].multisig_key.pubkey] + sorted_node_ids = list(sorted(self.node_ids)) + if sorted_node_ids != self.node_ids: + node_ids = sorted_node_ids + bitcoin_keys.reverse() + else: + node_ids = self.node_ids + # note: we inject a channel announcement, and a channel update (for outgoing direction) + # This is atm needed for + # - finding routes + # - the ChanAnn is needed so that we can anchor to it a future ChanUpd + # that the remote sends, even if the channel was not announced + # (from BOLT-07: "MAY create a channel_update to communicate the channel + # parameters to the final node, even though the channel has not yet been announced") + self.channel_db.on_channel_announcement({"short_channel_id": chan.short_channel_id, "node_id_1": node_ids[0], "node_id_2": node_ids[1], + 'chain_hash': constants.net.rev_genesis_bytes(), 'len': b'\x00\x00', 'features': b'', + 'bitcoin_key_1': bitcoin_keys[0], 'bitcoin_key_2': bitcoin_keys[1]}, + trusted=True) + # only inject outgoing direction: + if node_ids[0] == privkey_to_pubkey(self.privkey): + channel_flags = b'\x00' + else: + channel_flags = b'\x01' + now = int(time.time()).to_bytes(4, byteorder="big") + self.channel_db.on_channel_update({"short_channel_id": chan.short_channel_id, 'channel_flags': channel_flags, 'cltv_expiry_delta': b'\x90', + 'htlc_minimum_msat': b'\x03\xe8', 'fee_base_msat': b'\x03\xe8', 'fee_proportional_millionths': b'\x01', + 'chain_hash': constants.net.rev_genesis_bytes(), 'timestamp': now}, + trusted=True) + # peer may have sent us a channel update for the incoming direction previously + # note: if we were offline when the 3rd conf happened, lnd will never send us this channel_update + # see https://github.com/lightningnetwork/lnd/issues/1347 + #self.send_message("query_short_channel_ids", chain_hash=constants.net.rev_genesis_bytes(), + # len=9, encoded_short_ids=b'\x00'+chan.short_channel_id) + pending_channel_update = self.orphan_channel_updates.get(chan.short_channel_id) + if pending_channel_update: + self.channel_db.on_channel_update(pending_channel_update) + + self.print_error("CHANNEL OPENING COMPLETED") + + def send_announcement_signatures(self, chan: Channel): + + bitcoin_keys = [chan.config[REMOTE].multisig_key.pubkey, + chan.config[LOCAL].multisig_key.pubkey] + + sorted_node_ids = list(sorted(self.node_ids)) + if sorted_node_ids != self.node_ids: + node_ids = sorted_node_ids + bitcoin_keys.reverse() + else: + node_ids = self.node_ids + + chan_ann = encode_msg("channel_announcement", + len=0, + #features not set (defaults to zeros) + chain_hash=constants.net.rev_genesis_bytes(), + short_channel_id=chan.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] + ) + to_hash = chan_ann[256+2:] + h = sha256d(to_hash) + bitcoin_signature = ecc.ECPrivkey(chan.config[LOCAL].multisig_key.privkey).sign(h, sig_string_from_r_and_s, get_r_and_s_from_sig_string) + node_signature = ecc.ECPrivkey(self.privkey).sign(h, sig_string_from_r_and_s, get_r_and_s_from_sig_string) + self.send_message("announcement_signatures", + channel_id=chan.channel_id, + short_channel_id=chan.short_channel_id, + node_signature=node_signature, + bitcoin_signature=bitcoin_signature + ) + + return h, node_signature, bitcoin_signature + + @log_exceptions + async def on_update_fail_htlc(self, payload): + channel_id = payload["channel_id"] + htlc_id = int.from_bytes(payload["id"], "big") + key = (channel_id, htlc_id) + try: + route = self.attempted_route[key] + except KeyError: + # the remote might try to fail an htlc after we restarted... + # attempted_route is not persisted, so we will get here then + self.print_error("UPDATE_FAIL_HTLC. cannot decode! attempted route is MISSING. {}".format(key)) + else: + try: + await self._handle_error_code_from_failed_htlc(payload["reason"], route, channel_id, htlc_id) + except Exception: + # exceptions are suppressed as failing to handle an error code + # should not block us from removing the htlc + traceback.print_exc(file=sys.stderr) + # process update_fail_htlc on channel + chan = self.channels[channel_id] + chan.receive_fail_htlc(htlc_id) + await self.receive_and_revoke(chan) + self.network.trigger_callback('ln_message', self.lnworker, 'Payment failed', htlc_id) + + async def _handle_error_code_from_failed_htlc(self, error_reason, route: List['RouteEdge'], channel_id, htlc_id): + chan = self.channels[channel_id] + failure_msg, sender_idx = decode_onion_error(error_reason, + [x.node_id for x in route], + chan.onion_keys[htlc_id]) + code, data = failure_msg.code, failure_msg.data + self.print_error("UPDATE_FAIL_HTLC", repr(code), data) + self.print_error(f"error reported by {bh2u(route[sender_idx].node_id)}") + # handle some specific error codes + failure_codes = { + OnionFailureCode.TEMPORARY_CHANNEL_FAILURE: 2, + OnionFailureCode.AMOUNT_BELOW_MINIMUM: 10, + OnionFailureCode.FEE_INSUFFICIENT: 10, + OnionFailureCode.INCORRECT_CLTV_EXPIRY: 6, + OnionFailureCode.EXPIRY_TOO_SOON: 2, + OnionFailureCode.CHANNEL_DISABLED: 4, + } + offset = failure_codes.get(code) + if offset: + channel_update = (258).to_bytes(length=2, byteorder="big") + data[offset:] + message_type, payload = decode_msg(channel_update) + try: + self.print_error("trying to apply channel update on our db", payload) + self.channel_db.on_channel_update(payload) + self.print_error("successfully applied channel update on our db") + except NotFoundChanAnnouncementForUpdate: + # maybe it is a private channel (and data in invoice was outdated) + self.print_error("maybe channel update is for private channel?") + start_node_id = route[sender_idx].node_id + self.channel_db.add_channel_update_for_private_channel(payload, start_node_id) + else: + # blacklist channel after reporter node + # TODO this should depend on the error (even more granularity) + # also, we need finer blacklisting (directed edges; nodes) + try: + short_chan_id = route[sender_idx + 1].short_channel_id + except IndexError: + self.print_error("payment destination reported error") + else: + self.network.path_finder.blacklist.add(short_chan_id) + + def send_commitment(self, chan: Channel): + sig_64, htlc_sigs = chan.sign_next_commitment() + self.send_message("commitment_signed", channel_id=chan.channel_id, signature=sig_64, num_htlcs=len(htlc_sigs), htlc_signature=b"".join(htlc_sigs)) + return len(htlc_sigs) + + async def send_and_revoke(self, chan: Channel): + """ generic channel update flow """ + self.send_commitment(chan) + await self.receive_revoke_and_ack(chan) + await self.receive_commitment(chan) + self.send_revoke_and_ack(chan) + + async def receive_and_revoke(self, chan: Channel): + await self.receive_commitment(chan) + self.send_revoke_and_ack(chan) + self.send_commitment(chan) + await self.receive_revoke_and_ack(chan) + + async def pay(self, route: List['RouteEdge'], chan: Channel, amount_msat: int, + payment_hash: bytes, min_final_cltv_expiry: int): + assert chan.get_state() == "OPEN", chan.get_state() + assert amount_msat > 0, "amount_msat is not greater zero" + # create onion packet + final_cltv = self.network.get_local_height() + min_final_cltv_expiry + hops_data, amount_msat, cltv = calc_hops_data_for_payment(route, amount_msat, final_cltv) + assert final_cltv <= cltv, (final_cltv, cltv) + secret_key = os.urandom(32) + onion = new_onion_packet([x.node_id for x in route], secret_key, hops_data, associated_data=payment_hash) + # create htlc + htlc = {'amount_msat':amount_msat, 'payment_hash':payment_hash, 'cltv_expiry':cltv} + htlc_id = chan.add_htlc(htlc) + chan.onion_keys[htlc_id] = secret_key + self.attempted_route[(chan.channel_id, htlc_id)] = route + self.print_error(f"starting payment. route: {route}") + self.send_message("update_add_htlc", + channel_id=chan.channel_id, + id=htlc_id, + cltv_expiry=cltv, + amount_msat=amount_msat, + payment_hash=payment_hash, + onion_routing_packet=onion.to_bytes()) + await self.send_and_revoke(chan) + return UpdateAddHtlc(**htlc, htlc_id=htlc_id) + + async def receive_revoke_and_ack(self, chan: Channel): + revoke_and_ack_msg = await self.revoke_and_ack[chan.channel_id].get() + chan.receive_revocation(RevokeAndAck(revoke_and_ack_msg["per_commitment_secret"], revoke_and_ack_msg["next_per_commitment_point"])) + self.lnworker.save_channel(chan) + + def send_revoke_and_ack(self, chan: Channel): + rev, _ = chan.revoke_current_commitment() + self.lnworker.save_channel(chan) + self.send_message("revoke_and_ack", + channel_id=chan.channel_id, + per_commitment_secret=rev.per_commitment_secret, + next_per_commitment_point=rev.next_per_commitment_point) + + async def receive_commitment(self, chan: Channel, commitment_signed_msg=None): + if commitment_signed_msg is None: + commitment_signed_msg = await self.commitment_signed[chan.channel_id].get() + data = commitment_signed_msg["htlc_signature"] + htlc_sigs = [data[i:i+64] for i in range(0, len(data), 64)] + chan.receive_new_commitment(commitment_signed_msg["signature"], htlc_sigs) + return len(htlc_sigs) + + def on_commitment_signed(self, payload): + self.print_error("commitment_signed", payload) + channel_id = payload['channel_id'] + self.commitment_signed[channel_id].put_nowait(payload) + + @log_exceptions + async def on_update_fulfill_htlc(self, update_fulfill_htlc_msg): + self.print_error("update_fulfill") + chan = self.channels[update_fulfill_htlc_msg["channel_id"]] + preimage = update_fulfill_htlc_msg["payment_preimage"] + htlc_id = int.from_bytes(update_fulfill_htlc_msg["id"], "big") + chan.receive_htlc_settle(preimage, htlc_id) + await self.receive_and_revoke(chan) + self.network.trigger_callback('ln_message', self.lnworker, 'Payment sent', htlc_id) + # used in lightning-integration + self.payment_preimages[sha256(preimage)].put_nowait(preimage) + + def on_update_fail_malformed_htlc(self, payload): + self.print_error("error", payload["data"].decode("ascii")) + + @log_exceptions + async def on_update_add_htlc(self, payload): + # no onion routing for the moment: we assume we are the end node + self.print_error('on_update_add_htlc') + # check if this in our list of requests + payment_hash = payload["payment_hash"] + channel_id = payload['channel_id'] + htlc_id = int.from_bytes(payload["id"], 'big') + cltv_expiry = int.from_bytes(payload["cltv_expiry"], 'big') + amount_msat_htlc = int.from_bytes(payload["amount_msat"], 'big') + onion_packet = OnionPacket.from_bytes(payload["onion_routing_packet"]) + processed_onion = process_onion_packet(onion_packet, associated_data=payment_hash, our_onion_private_key=self.privkey) + chan = self.channels[channel_id] + assert chan.get_state() == "OPEN" + assert htlc_id == chan.config[REMOTE].next_htlc_id, (htlc_id, chan.config[REMOTE].next_htlc_id) # TODO fail channel instead + if cltv_expiry >= 500_000_000: + pass # TODO fail the channel + # add htlc + htlc = {'amount_msat': amount_msat_htlc, 'payment_hash':payment_hash, 'cltv_expiry':cltv_expiry} + htlc_id = chan.receive_htlc(htlc) + await self.receive_and_revoke(chan) + # Forward HTLC + # FIXME: this is not robust to us going offline before payment is fulfilled + if not processed_onion.are_we_final: + dph = processed_onion.hop_data.per_hop + next_chan = self.lnworker.get_channel_by_short_id(dph.short_channel_id) + next_peer = self.lnworker.peers[next_chan.node_id] + if next_chan is None or next_chan.get_state() != 'OPEN': + self.print_error("cannot forward htlc", next_chan.get_state() if next_chan else None) + reason = OnionRoutingFailureMessage(code=OnionFailureCode.PERMANENT_CHANNEL_FAILURE, data=b'') + await self.fail_htlc(chan, htlc_id, onion_packet, reason) + return + self.print_error('forwarding htlc to', next_chan.node_id) + next_cltv_expiry = int.from_bytes(dph.outgoing_cltv_value, 'big') + next_amount_msat_htlc = int.from_bytes(dph.amt_to_forward, 'big') + next_htlc = {'amount_msat':next_amount_msat_htlc, 'payment_hash':payment_hash, 'cltv_expiry':next_cltv_expiry} + next_htlc_id = next_chan.add_htlc(next_htlc) + next_peer.send_message( + "update_add_htlc", + channel_id=next_chan.channel_id, + id=next_htlc_id, + cltv_expiry=dph.outgoing_cltv_value, + amount_msat=dph.amt_to_forward, + payment_hash=payment_hash, + onion_routing_packet=processed_onion.next_packet.to_bytes() + ) + await next_peer.send_and_revoke(next_chan) + # wait until we get paid + preimage = await next_peer.payment_preimages[payment_hash].get() + # fulfill the original htlc + await self.fulfill_htlc(chan, htlc_id, preimage) + self.print_error("htlc forwarded successfully") + return + try: + preimage, invoice = self.lnworker.get_invoice(payment_hash) + except UnknownPaymentHash: + reason = OnionRoutingFailureMessage(code=OnionFailureCode.UNKNOWN_PAYMENT_HASH, data=b'') + await self.fail_htlc(chan, htlc_id, onion_packet, reason) + return + expected_received_msat = int(invoice.amount * bitcoin.COIN * 1000) if invoice.amount is not None else None + if expected_received_msat is not None and \ + (amount_msat_htlc < expected_received_msat or amount_msat_htlc > 2 * expected_received_msat): + reason = OnionRoutingFailureMessage(code=OnionFailureCode.INCORRECT_PAYMENT_AMOUNT, data=b'') + await self.fail_htlc(chan, htlc_id, onion_packet, reason) + return + local_height = self.network.get_local_height() + if local_height + MIN_FINAL_CLTV_EXPIRY_ACCEPTED > cltv_expiry: + reason = OnionRoutingFailureMessage(code=OnionFailureCode.FINAL_EXPIRY_TOO_SOON, data=b'') + await self.fail_htlc(chan, htlc_id, onion_packet, reason) + return + cltv_from_onion = int.from_bytes(processed_onion.hop_data.per_hop.outgoing_cltv_value, byteorder="big") + if cltv_from_onion != cltv_expiry: + reason = OnionRoutingFailureMessage(code=OnionFailureCode.FINAL_INCORRECT_CLTV_EXPIRY, + data=cltv_expiry.to_bytes(4, byteorder="big")) + await self.fail_htlc(chan, htlc_id, onion_packet, reason) + return + amount_from_onion = int.from_bytes(processed_onion.hop_data.per_hop.amt_to_forward, byteorder="big") + if amount_from_onion > amount_msat_htlc: + reason = OnionRoutingFailureMessage(code=OnionFailureCode.FINAL_INCORRECT_HTLC_AMOUNT, + data=amount_msat_htlc.to_bytes(8, byteorder="big")) + await self.fail_htlc(chan, htlc_id, onion_packet, reason) + return + self.network.trigger_callback('htlc_added', UpdateAddHtlc(**htlc, htlc_id=htlc_id), invoice, RECEIVED) + # settle htlc + if not self.network.config.debug_lightning_do_not_settle: + # settle htlc + await self.fulfill_htlc(chan, htlc_id, preimage) + + async def fulfill_htlc(self, chan: Channel, htlc_id: int, preimage: bytes): + chan.settle_htlc(preimage, htlc_id) + self.send_message("update_fulfill_htlc", + channel_id=chan.channel_id, + id=htlc_id, + payment_preimage=preimage) + await self.send_and_revoke(chan) + self.network.trigger_callback('ln_message', self.lnworker, 'Payment received', htlc_id) + + async def fail_htlc(self, chan: Channel, htlc_id: int, onion_packet: OnionPacket, + reason: OnionRoutingFailureMessage): + self.print_error(f"failing received htlc {(bh2u(chan.channel_id), htlc_id)}. reason: {reason}") + chan.fail_htlc(htlc_id) + error_packet = construct_onion_error(reason, onion_packet, our_onion_private_key=self.privkey) + self.send_message("update_fail_htlc", + channel_id=chan.channel_id, + id=htlc_id, + len=len(error_packet), + reason=error_packet) + await self.send_and_revoke(chan) + + def on_revoke_and_ack(self, payload): + self.print_error("got revoke_and_ack") + channel_id = payload["channel_id"] + self.revoke_and_ack[channel_id].put_nowait(payload) + + def on_update_fee(self, payload): + channel_id = payload["channel_id"] + feerate =int.from_bytes(payload["feerate_per_kw"], "big") + self.channels[channel_id].update_fee(feerate, False) + + async def bitcoin_fee_update(self, chan: Channel): + """ + called when our fee estimates change + """ + if not chan.constraints.is_initiator: + # TODO force close if initiator does not update_fee enough + return + feerate_per_kw = self.lnworker.current_feerate_per_kw() + chan_fee = chan.pending_feerate(REMOTE) + self.print_error("current pending feerate", chan_fee) + self.print_error("new feerate", feerate_per_kw) + if feerate_per_kw < chan_fee / 2: + self.print_error("FEES HAVE FALLEN") + elif feerate_per_kw > chan_fee * 2: + self.print_error("FEES HAVE RISEN") + else: + return + chan.update_fee(feerate_per_kw, True) + self.send_message("update_fee", + channel_id=chan.channel_id, + feerate_per_kw=feerate_per_kw) + await self.send_and_revoke(chan) + + def on_closing_signed(self, payload): + chan_id = payload["channel_id"] + if chan_id not in self.closing_signed: raise Exception("Got unknown closing_signed") + self.closing_signed[chan_id].put_nowait(payload) + + @log_exceptions + async def close_channel(self, chan_id: bytes): + chan = self.channels[chan_id] + self.shutdown_received[chan_id] = asyncio.Future() + self.send_shutdown(chan) + payload = await self.shutdown_received[chan_id] + txid = await self._shutdown(chan, payload, True) + self.print_error('Channel closed', txid) + return txid + + @log_exceptions + async def on_shutdown(self, payload): + # length of scripts allowed in BOLT-02 + if int.from_bytes(payload['len'], 'big') not in (3+20+2, 2+20+1, 2+20, 2+32): + raise Exception('scriptpubkey length in received shutdown message invalid: ' + str(payload['len'])) + chan_id = payload['channel_id'] + if chan_id in self.shutdown_received: + self.shutdown_received[chan_id].set_result(payload) + else: + chan = self.channels[chan_id] + self.send_shutdown(chan) + txid = await self._shutdown(chan, payload, False) + self.print_error('Channel closed by remote peer', txid) + + def send_shutdown(self, chan: Channel): + scriptpubkey = bfh(bitcoin.address_to_script(chan.sweep_address)) + self.send_message('shutdown', channel_id=chan.channel_id, len=len(scriptpubkey), scriptpubkey=scriptpubkey) + + @log_exceptions + async def _shutdown(self, chan: Channel, payload, is_local): + # set state so that we stop accepting HTLCs + chan.set_state('CLOSING') + while len(chan.hm.htlcs_by_direction(LOCAL, RECEIVED)) > 0: + self.print_error('waiting for htlcs to settle...') + await asyncio.sleep(1) + our_fee = chan.pending_local_fee() + scriptpubkey = bfh(bitcoin.address_to_script(chan.sweep_address)) + # negociate fee + while True: + our_sig, closing_tx = chan.make_closing_tx(scriptpubkey, payload['scriptpubkey'], fee_sat=our_fee) + self.send_message('closing_signed', channel_id=chan.channel_id, fee_satoshis=our_fee, signature=our_sig) + cs_payload = await asyncio.wait_for(self.closing_signed[chan.channel_id].get(), 1) + their_fee = int.from_bytes(cs_payload['fee_satoshis'], 'big') + their_sig = cs_payload['signature'] + if our_fee == their_fee: + break + # TODO: negociate better + our_fee = their_fee + # index of our_sig + i = bool(chan.get_local_index()) + if not is_local: i = not i + # add signatures + closing_tx.add_signature_to_txin(0, int(i), bh2u(der_sig_from_sig_string(our_sig) + b'\x01')) + closing_tx.add_signature_to_txin(0, int(not i), bh2u(der_sig_from_sig_string(their_sig) + b'\x01')) + # broadcast + await self.network.broadcast_transaction(closing_tx) + return closing_tx.txid() diff --git a/electrum/lnrouter.py b/electrum/lnrouter.py @@ -36,14 +36,14 @@ import asyncio from . import constants from .util import PrintError, bh2u, profiler, get_headers_dir, bfh, is_ip_address, list_enabled_bits from .storage import JsonDB -from .lnchannelverifier import LNChannelVerifier, verify_sig_for_channel_update +from .lnverifier import LNChannelVerifier, verify_sig_for_channel_update from .crypto import sha256d from . import ecc from .lnutil import (LN_GLOBAL_FEATURES_KNOWN_SET, LNPeerAddr, NUM_MAX_EDGES_IN_PAYMENT_PATH, NotFoundChanAnnouncementForUpdate) if TYPE_CHECKING: - from .lnchan import Channel + from .lnchannel import Channel from .network import Network diff --git a/electrum/lnsweep.py b/electrum/lnsweep.py @@ -17,7 +17,7 @@ from .transaction import Transaction, TxOutput, construct_witness from .simple_config import SimpleConfig, FEERATE_FALLBACK_STATIC_FEE if TYPE_CHECKING: - from .lnchan import Channel + from .lnchannel import Channel def maybe_create_sweeptx_for_their_ctx_to_remote(ctx: Transaction, sweep_address: str, @@ -203,7 +203,7 @@ def create_sweeptxs_for_their_latest_ctx(chan: 'Channel', ctx: Transaction, Regardless of it is a breach or not, construct sweep tx for 'to_remote'. If it is a breach, also construct sweep tx for 'to_local'. Sweep txns for HTLCs are only constructed if it is NOT a breach, as - lnchan does not store old HTLCs. + lnchannel does not store old HTLCs. """ this_conf, other_conf = get_ordered_channel_configs(chan=chan, for_us=False) ctn = extract_ctn_from_tx_and_chan(ctx, chan) diff --git a/electrum/lnutil.py b/electrum/lnutil.py @@ -21,7 +21,7 @@ from .lnaddr import lndecode from .keystore import BIP32_KeyStore if TYPE_CHECKING: - from .lnchan import Channel + from .lnchannel import Channel HTLC_TIMEOUT_WEIGHT = 663 @@ -114,7 +114,7 @@ MAXIMUM_HTLC_MINIMUM_MSAT_ACCEPTED = 1000 MAXIMUM_REMOTE_TO_SELF_DELAY_ACCEPTED = 2016 class RevocationStore: - """ Taken from LND, see license in lnchan.py. """ + """ Taken from LND, see license in lnchannel.py. """ START_INDEX = 2 ** 48 - 1 diff --git a/electrum/lnverifier.py b/electrum/lnverifier.py @@ -0,0 +1,205 @@ +# -*- coding: utf-8 -*- +# +# Electrum - lightweight Bitcoin client +# 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 asyncio +import threading +from typing import TYPE_CHECKING + +import aiorpcx + +from . import bitcoin +from . import ecc +from . import constants +from .util import bh2u, bfh, NetworkJobOnDefaultServer +from .lnutil import invert_short_channel_id, funding_output_script_from_keys +from .verifier import verify_tx_is_in_block, MerkleVerificationFailure +from .transaction import Transaction +from .interface import GracefulDisconnect +from .crypto import sha256d +from .lnmsg import encode_msg + +if TYPE_CHECKING: + from .network import Network + from .lnrouter import ChannelDB + + +class LNChannelVerifier(NetworkJobOnDefaultServer): + """ Verify channel announcements for the Channel DB """ + + # FIXME the initial routing sync is bandwidth-heavy, and the electrum server + # will start throttling us, making it even slower. one option would be to + # spread it over multiple servers. + + def __init__(self, network: 'Network', channel_db: 'ChannelDB'): + NetworkJobOnDefaultServer.__init__(self, network) + self.channel_db = channel_db + self.lock = threading.Lock() + self.unverified_channel_info = {} # short_channel_id -> channel_info + # channel announcements that seem to be invalid: + self.blacklist = set() # short_channel_id + + def _reset(self): + super()._reset() + self.started_verifying_channel = set() # short_channel_id + + # TODO make async; and rm self.lock completely + def add_new_channel_info(self, channel_info): + short_channel_id = channel_info.channel_id + if short_channel_id in self.unverified_channel_info: + return + if short_channel_id in self.blacklist: + return + if not verify_sigs_for_channel_announcement(channel_info.msg_payload): + return + with self.lock: + self.unverified_channel_info[short_channel_id] = channel_info + + def get_pending_channel_info(self, short_channel_id): + return self.unverified_channel_info.get(short_channel_id, None) + + async def _start_tasks(self): + async with self.group as group: + await group.spawn(self.main) + + async def main(self): + while True: + await self._verify_some_channels() + await asyncio.sleep(0.1) + + async def _verify_some_channels(self): + blockchain = self.network.blockchain() + local_height = blockchain.height() + + with self.lock: + unverified_channel_info = list(self.unverified_channel_info) + + for short_channel_id in unverified_channel_info: + if short_channel_id in self.started_verifying_channel: + continue + block_height, tx_pos, output_idx = invert_short_channel_id(short_channel_id) + # only resolve short_channel_id if headers are available. + if block_height <= 0 or block_height > local_height: + continue + header = blockchain.read_header(block_height) + if header is None: + if block_height < constants.net.max_checkpoint(): + await self.group.spawn(self.network.request_chunk(block_height, None, can_return_early=True)) + continue + self.started_verifying_channel.add(short_channel_id) + await self.group.spawn(self.verify_channel(block_height, tx_pos, short_channel_id)) + #self.print_error('requested short_channel_id', bh2u(short_channel_id)) + + async def verify_channel(self, block_height: int, tx_pos: int, short_channel_id: bytes): + # we are verifying channel announcements as they are from untrusted ln peers. + # we use electrum servers to do this. however we don't trust electrum servers either... + try: + result = await self.network.get_txid_from_txpos(block_height, tx_pos, True) + except aiorpcx.jsonrpc.RPCError: + # the electrum server is complaining about the tx_pos for given block. + # it is not clear what to do now, but let's believe the server. + self._blacklist_short_channel_id(short_channel_id) + return + tx_hash = result['tx_hash'] + merkle_branch = result['merkle'] + # we need to wait if header sync/reorg is still ongoing, hence lock: + async with self.network.bhi_lock: + header = self.network.blockchain().read_header(block_height) + try: + verify_tx_is_in_block(tx_hash, merkle_branch, tx_pos, header, block_height) + except MerkleVerificationFailure as e: + # the electrum server sent an incorrect proof. blame is on server, not the ln peer + raise GracefulDisconnect(e) from e + try: + raw_tx = await self.network.get_transaction(tx_hash) + except aiorpcx.jsonrpc.RPCError as e: + # the electrum server can't find the tx; but it was the + # one who told us about the txid!! blame is on server + raise GracefulDisconnect(e) from e + tx = Transaction(raw_tx) + try: + tx.deserialize() + except Exception: + # either bug in client, or electrum server is evil. + # if we connect to a diff server at some point, let's try again. + self.print_msg("cannot deserialize transaction, skipping", tx_hash) + return + if tx_hash != tx.txid(): + # either bug in client, or electrum server is evil. + # if we connect to a diff server at some point, let's try again. + self.print_error(f"received tx does not match expected txid ({tx_hash} != {tx.txid()})") + return + # check funding output + channel_info = self.unverified_channel_info[short_channel_id] + chan_ann = channel_info.msg_payload + redeem_script = funding_output_script_from_keys(chan_ann['bitcoin_key_1'], chan_ann['bitcoin_key_2']) + expected_address = bitcoin.redeem_script_to_address('p2wsh', redeem_script) + output_idx = invert_short_channel_id(short_channel_id)[2] + try: + actual_output = tx.outputs()[output_idx] + except IndexError: + self._blacklist_short_channel_id(short_channel_id) + return + if expected_address != actual_output.address: + # FIXME what now? best would be to ban the originating ln peer. + self.print_error(f"funding output script mismatch for {bh2u(short_channel_id)}") + self._remove_channel_from_unverified_db(short_channel_id) + return + # put channel into channel DB + channel_info.set_capacity(actual_output.value) + self.channel_db.add_verified_channel_info(short_channel_id, channel_info) + self._remove_channel_from_unverified_db(short_channel_id) + + def _remove_channel_from_unverified_db(self, short_channel_id: bytes): + with self.lock: + self.unverified_channel_info.pop(short_channel_id, None) + try: self.started_verifying_channel.remove(short_channel_id) + except KeyError: pass + + def _blacklist_short_channel_id(self, short_channel_id: bytes) -> None: + self.blacklist.add(short_channel_id) + with self.lock: + self.unverified_channel_info.pop(short_channel_id, None) + + +def verify_sigs_for_channel_announcement(chan_ann: dict) -> bool: + msg_bytes = encode_msg('channel_announcement', **chan_ann) + pre_hash = msg_bytes[2+256:] + h = sha256d(pre_hash) + pubkeys = [chan_ann['node_id_1'], chan_ann['node_id_2'], chan_ann['bitcoin_key_1'], chan_ann['bitcoin_key_2']] + sigs = [chan_ann['node_signature_1'], chan_ann['node_signature_2'], chan_ann['bitcoin_signature_1'], chan_ann['bitcoin_signature_2']] + for pubkey, sig in zip(pubkeys, sigs): + if not ecc.verify_signature(pubkey, sig, h): + return False + return True + + +def verify_sig_for_channel_update(chan_upd: dict, node_id: bytes) -> bool: + msg_bytes = encode_msg('channel_update', **chan_upd) + pre_hash = msg_bytes[2+64:] + h = sha256d(pre_hash) + sig = chan_upd['signature'] + if not ecc.verify_signature(node_id, sig, h): + return False + return True diff --git a/electrum/lnworker.py b/electrum/lnworker.py @@ -28,10 +28,10 @@ from .bip32 import bip32_root from .util import bh2u, bfh, PrintError, InvoiceError, resolve_dns_srv, is_ip_address, log_exceptions from .util import timestamp_to_datetime from .lntransport import LNTransport, LNResponderTransport -from .lnbase import Peer +from .lnpeer import Peer from .lnaddr import lnencode, LnAddr, lndecode from .ecc import der_sig_from_sig_string -from .lnchan import Channel, ChannelJsonEncoder +from .lnchannel import Channel, ChannelJsonEncoder from .lnutil import (Outpoint, calc_short_channel_id, LNPeerAddr, get_compressed_pubkey_from_bech32, extract_nodeid, PaymentFailure, split_host_port, ConnStringFormatError, diff --git a/electrum/tests/test_lnbase.py b/electrum/tests/test_lnbase.py @@ -1,252 +0,0 @@ -import unittest -import asyncio -import tempfile -from decimal import Decimal -import os -from contextlib import contextmanager -from collections import defaultdict - -from electrum.network import Network -from electrum.ecc import ECPrivkey -from electrum import simple_config, lnutil -from electrum.lnaddr import lnencode, LnAddr, lndecode -from electrum.bitcoin import COIN, sha256 -from electrum.util import bh2u - -from electrum.lnbase import Peer -from electrum.lnutil import LNPeerAddr, Keypair, privkey_to_pubkey -from electrum.lnutil import LightningPeerConnectionClosed, RemoteMisbehaving -from electrum.lnutil import PaymentFailure -from electrum.lnrouter import ChannelDB, LNPathFinder -from electrum.lnworker import LNWorker -from electrum.lnmsg import encode_msg, decode_msg - -from .test_lnchan import create_test_channels - -def keypair(): - priv = ECPrivkey.generate_random_key().get_secret_bytes() - k1 = Keypair( - pubkey=privkey_to_pubkey(priv), - privkey=priv) - return k1 - -@contextmanager -def noop_lock(): - yield - -class MockNetwork: - def __init__(self, tx_queue): - self.callbacks = defaultdict(list) - self.lnwatcher = None - user_config = {} - user_dir = tempfile.mkdtemp(prefix="electrum-lnbase-test-") - self.config = simple_config.SimpleConfig(user_config, read_user_dir_function=lambda: user_dir) - self.asyncio_loop = asyncio.get_event_loop() - self.channel_db = ChannelDB(self) - self.interface = None - self.path_finder = LNPathFinder(self.channel_db) - self.tx_queue = tx_queue - - @property - def callback_lock(self): - return noop_lock() - - register_callback = Network.register_callback - unregister_callback = Network.unregister_callback - trigger_callback = Network.trigger_callback - - def get_local_height(self): - return 0 - - async def broadcast_transaction(self, tx): - if self.tx_queue: - await self.tx_queue.put(tx) - -class MockStorage: - def put(self, key, value): - pass - - def get(self, key, default=None): - pass - - def write(self): - pass - -class MockWallet: - storage = MockStorage() - -class MockLNWorker: - def __init__(self, remote_keypair, local_keypair, chan, tx_queue): - self.chan = chan - self.remote_keypair = remote_keypair - self.node_keypair = local_keypair - self.network = MockNetwork(tx_queue) - self.channels = {self.chan.channel_id: self.chan} - self.invoices = {} - self.inflight = {} - self.wallet = MockWallet() - - @property - def lock(self): - return noop_lock() - - @property - def peers(self): - return {self.remote_keypair.pubkey: self.peer} - - def channels_for_peer(self, pubkey): - return self.channels - - def get_channel_by_short_id(self, short_channel_id): - with self.lock: - for chan in self.channels.values(): - if chan.short_channel_id == short_channel_id: - return chan - - def save_channel(self, chan): - print("Ignoring channel save") - - def on_channels_updated(self): - pass - - def save_invoice(*args): - pass - - get_invoice = LNWorker.get_invoice - _create_route_from_invoice = LNWorker._create_route_from_invoice - _check_invoice = staticmethod(LNWorker._check_invoice) - _pay_to_route = LNWorker._pay_to_route - force_close_channel = LNWorker.force_close_channel - get_first_timestamp = lambda self: 0 - -class MockTransport: - def __init__(self): - self.queue = asyncio.Queue() - - def name(self): - return "" - - async def read_messages(self): - while True: - yield await self.queue.get() - -class NoFeaturesTransport(MockTransport): - """ - This answers the init message with a init that doesn't signal any features. - Used for testing that we require DATA_LOSS_PROTECT. - """ - def send_bytes(self, data): - decoded = decode_msg(data) - print(decoded) - if decoded[0] == 'init': - self.queue.put_nowait(encode_msg('init', lflen=1, gflen=1, localfeatures=b"\x00", globalfeatures=b"\x00")) - -class PutIntoOthersQueueTransport(MockTransport): - def __init__(self): - super().__init__() - self.other_mock_transport = None - - def send_bytes(self, data): - self.other_mock_transport.queue.put_nowait(data) - -def transport_pair(): - t1 = PutIntoOthersQueueTransport() - t2 = PutIntoOthersQueueTransport() - t1.other_mock_transport = t2 - t2.other_mock_transport = t1 - return t1, t2 - -class TestPeer(unittest.TestCase): - def setUp(self): - self.alice_channel, self.bob_channel = create_test_channels() - - def test_require_data_loss_protect(self): - mock_lnworker = MockLNWorker(keypair(), keypair(), self.alice_channel, tx_queue=None) - mock_transport = NoFeaturesTransport() - p1 = Peer(mock_lnworker, b"\x00" * 33, mock_transport, request_initial_sync=False) - mock_lnworker.peer = p1 - with self.assertRaises(LightningPeerConnectionClosed): - run(asyncio.wait_for(p1._main_loop(), 1)) - - def prepare_peers(self): - k1, k2 = keypair(), keypair() - t1, t2 = transport_pair() - q1, q2 = asyncio.Queue(), asyncio.Queue() - w1 = MockLNWorker(k1, k2, self.alice_channel, tx_queue=q1) - w2 = MockLNWorker(k2, k1, self.bob_channel, tx_queue=q2) - p1 = Peer(w1, k1.pubkey, t1, request_initial_sync=False) - p2 = Peer(w2, k2.pubkey, t2, request_initial_sync=False) - w1.peer = p1 - w2.peer = p2 - # mark_open won't work if state is already OPEN. - # so set it to OPENING - self.alice_channel.set_state("OPENING") - self.bob_channel.set_state("OPENING") - # this populates the channel graph: - p1.mark_open(self.alice_channel) - p2.mark_open(self.bob_channel) - return p1, p2, w1, w2, q1, q2 - - @staticmethod - def prepare_invoice(w2 # receiver - ): - amount_btc = 100000/Decimal(COIN) - payment_preimage = os.urandom(32) - RHASH = sha256(payment_preimage) - addr = LnAddr( - RHASH, - amount_btc, - tags=[('c', lnutil.MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE), - ('d', 'coffee') - ]) - pay_req = lnencode(addr, w2.node_keypair.privkey) - w2.invoices[bh2u(RHASH)] = (bh2u(payment_preimage), pay_req, True, None) - return pay_req - - @staticmethod - def prepare_ln_message_future(w2 # receiver - ): - fut = asyncio.Future() - def evt_set(event, _lnworker, msg, _htlc_id): - fut.set_result(msg) - w2.network.register_callback(evt_set, ['ln_message']) - return fut - - def test_payment(self): - p1, p2, w1, w2, _q1, _q2 = self.prepare_peers() - pay_req = self.prepare_invoice(w2) - fut = self.prepare_ln_message_future(w2) - - async def pay(): - addr, peer, coro = LNWorker._pay(w1, pay_req) - await coro - print("HTLC ADDED") - self.assertEqual(await fut, 'Payment received') - gath.cancel() - gath = asyncio.gather(pay(), p1._main_loop(), p2._main_loop()) - with self.assertRaises(asyncio.CancelledError): - run(gath) - - def test_channel_usage_after_closing(self): - p1, p2, w1, w2, q1, q2 = self.prepare_peers() - pay_req = self.prepare_invoice(w2) - - addr = w1._check_invoice(pay_req) - route = w1._create_route_from_invoice(decoded_invoice=addr) - - run(w1.force_close_channel(self.alice_channel.channel_id)) - # check if a tx (commitment transaction) was broadcasted: - assert q1.qsize() == 1 - - with self.assertRaises(PaymentFailure) as e: - w1._create_route_from_invoice(decoded_invoice=addr) - self.assertEqual(str(e.exception), 'No path found') - - peer = w1.peers[route[0].node_id] - # AssertionError is ok since we shouldn't use old routes, and the - # route finding should fail when channel is closed - with self.assertRaises(AssertionError): - run(asyncio.gather(w1._pay_to_route(route, addr, pay_req), p1._main_loop(), p2._main_loop())) - -def run(coro): - asyncio.get_event_loop().run_until_complete(coro) diff --git a/electrum/tests/test_lnchan.py b/electrum/tests/test_lnchan.py @@ -1,826 +0,0 @@ -# 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. - -import unittest -import os -import binascii -from pprint import pformat - -from electrum import bitcoin -from electrum import lnbase -from electrum import lnchan -from electrum import lnutil -from electrum import bip32 as bip32_utils -from electrum.lnutil import SENT, LOCAL, REMOTE, RECEIVED -from electrum.ecc import sig_string_from_der_sig -from electrum.util import set_verbosity - -one_bitcoin_in_msat = bitcoin.COIN * 1000 - -def create_channel_state(funding_txid, funding_index, funding_sat, local_feerate, is_initiator, local_amount, remote_amount, privkeys, other_pubkeys, seed, cur, nex, other_node_id, l_dust, r_dust, l_csv, r_csv): - assert local_amount > 0 - assert remote_amount > 0 - channel_id, _ = lnbase.channel_id_from_funding_tx(funding_txid, funding_index) - their_revocation_store = lnbase.RevocationStore() - - return { - "channel_id":channel_id, - "short_channel_id":channel_id[:8], - "funding_outpoint":lnbase.Outpoint(funding_txid, funding_index), - "remote_config":lnbase.RemoteConfig( - payment_basepoint=other_pubkeys[0], - multisig_key=other_pubkeys[1], - htlc_basepoint=other_pubkeys[2], - delayed_basepoint=other_pubkeys[3], - revocation_basepoint=other_pubkeys[4], - to_self_delay=r_csv, - dust_limit_sat=r_dust, - max_htlc_value_in_flight_msat=one_bitcoin_in_msat * 5, - max_accepted_htlcs=5, - initial_msat=remote_amount, - ctn = -1, - next_htlc_id = 0, - reserve_sat=0, - htlc_minimum_msat=1, - - next_per_commitment_point=nex, - current_per_commitment_point=cur, - revocation_store=their_revocation_store, - ), - "local_config":lnbase.LocalConfig( - payment_basepoint=privkeys[0], - multisig_key=privkeys[1], - htlc_basepoint=privkeys[2], - delayed_basepoint=privkeys[3], - revocation_basepoint=privkeys[4], - to_self_delay=l_csv, - dust_limit_sat=l_dust, - max_htlc_value_in_flight_msat=one_bitcoin_in_msat * 5, - max_accepted_htlcs=5, - initial_msat=local_amount, - ctn = 0, - next_htlc_id = 0, - reserve_sat=0, - - per_commitment_secret_seed=seed, - funding_locked_received=True, - was_announced=False, - current_commitment_signature=None, - current_htlc_signatures=None, - got_sig_for_next=False, - ), - "constraints":lnbase.ChannelConstraints( - capacity=funding_sat, - is_initiator=is_initiator, - funding_txn_minimum_depth=3, - feerate=local_feerate, - ), - "node_id":other_node_id, - "remote_commitment_to_be_revoked": None, - 'onion_keys': {}, - } - -def bip32(sequence): - xprv, xpub = bip32_utils.bip32_root(b"9dk", 'standard') - xprv, xpub = bip32_utils.bip32_private_derivation(xprv, "m/", sequence) - xtype, depth, fingerprint, child_number, c, k = bip32_utils.deserialize_xprv(xprv) - assert len(k) == 32 - assert type(k) is bytes - return k - -def create_test_channels(feerate=6000, local=None, remote=None): - funding_txid = binascii.hexlify(b"\x01"*32).decode("ascii") - funding_index = 0 - funding_sat = ((local + remote) // 1000) if local is not None and remote is not None else (bitcoin.COIN * 10) - local_amount = local if local is not None else (funding_sat * 1000 // 2) - remote_amount = remote if remote is not None else (funding_sat * 1000 // 2) - alice_raw = [ bip32("m/" + str(i)) for i in range(5) ] - bob_raw = [ bip32("m/" + str(i)) for i in range(5,11) ] - alice_privkeys = [lnutil.Keypair(lnutil.privkey_to_pubkey(x), x) for x in alice_raw] - bob_privkeys = [lnutil.Keypair(lnutil.privkey_to_pubkey(x), x) for x in bob_raw] - alice_pubkeys = [lnutil.OnlyPubkeyKeypair(x.pubkey) for x in alice_privkeys] - bob_pubkeys = [lnutil.OnlyPubkeyKeypair(x.pubkey) for x in bob_privkeys] - - alice_seed = b"\x01" * 32 - bob_seed = b"\x02" * 32 - - alice_first = lnutil.secret_to_pubkey(int.from_bytes(lnutil.get_per_commitment_secret_from_seed(alice_seed, lnutil.RevocationStore.START_INDEX), "big")) - bob_first = lnutil.secret_to_pubkey(int.from_bytes(lnutil.get_per_commitment_secret_from_seed(bob_seed, lnutil.RevocationStore.START_INDEX), "big")) - - alice, bob = \ - lnchan.Channel( - create_channel_state(funding_txid, funding_index, funding_sat, feerate, True, local_amount, remote_amount, alice_privkeys, bob_pubkeys, alice_seed, None, bob_first, b"\x02"*33, l_dust=200, r_dust=1300, l_csv=5, r_csv=4), name="alice"), \ - lnchan.Channel( - create_channel_state(funding_txid, funding_index, funding_sat, feerate, False, remote_amount, local_amount, bob_privkeys, alice_pubkeys, bob_seed, None, alice_first, b"\x01"*33, l_dust=1300, r_dust=200, l_csv=4, r_csv=5), name="bob") - - alice.set_state('OPEN') - bob.set_state('OPEN') - - a_out = alice.current_commitment(LOCAL).outputs() - b_out = bob.pending_commitment(REMOTE).outputs() - assert a_out == b_out, "\n" + pformat((a_out, b_out)) - - sig_from_bob, a_htlc_sigs = bob.sign_next_commitment() - sig_from_alice, b_htlc_sigs = alice.sign_next_commitment() - - assert len(a_htlc_sigs) == 0 - assert len(b_htlc_sigs) == 0 - - alice.config[LOCAL] = alice.config[LOCAL]._replace(current_commitment_signature=sig_from_bob) - bob.config[LOCAL] = bob.config[LOCAL]._replace(current_commitment_signature=sig_from_alice) - - alice.set_local_commitment(alice.current_commitment(LOCAL)) - bob.set_local_commitment(bob.current_commitment(LOCAL)) - - alice_second = lnutil.secret_to_pubkey(int.from_bytes(lnutil.get_per_commitment_secret_from_seed(alice_seed, lnutil.RevocationStore.START_INDEX - 1), "big")) - bob_second = lnutil.secret_to_pubkey(int.from_bytes(lnutil.get_per_commitment_secret_from_seed(bob_seed, lnutil.RevocationStore.START_INDEX - 1), "big")) - - alice.config[REMOTE] = alice.config[REMOTE]._replace(next_per_commitment_point=bob_second, current_per_commitment_point=bob_first) - bob.config[REMOTE] = bob.config[REMOTE]._replace(next_per_commitment_point=alice_second, current_per_commitment_point=alice_first) - - alice.set_remote_commitment() - bob.set_remote_commitment() - - alice.remote_commitment_to_be_revoked = alice.remote_commitment - bob.remote_commitment_to_be_revoked = bob.remote_commitment - - alice.config[REMOTE] = alice.config[REMOTE]._replace(ctn=0) - bob.config[REMOTE] = bob.config[REMOTE]._replace(ctn=0) - - return alice, bob - -class TestFee(unittest.TestCase): - """ - test - https://github.com/lightningnetwork/lightning-rfc/blob/e0c436bd7a3ed6a028e1cb472908224658a14eca/03-transactions.md#requirements-2 - """ - def test_fee(self): - alice_channel, bob_channel = create_test_channels(253, 10000000000, 5000000000) - self.assertIn(9999817, [x[2] for x in alice_channel.local_commitment.outputs()]) - -class TestChannel(unittest.TestCase): - maxDiff = 999 - - def assertOutputExistsByValue(self, tx, amt_sat): - for typ, scr, val in tx.outputs(): - if val == amt_sat: - break - else: - self.assertFalse() - - @staticmethod - def setUpClass(): - set_verbosity(True) - - def setUp(self): - # Create a test channel which will be used for the duration of this - # unittest. The channel will be funded evenly with Alice having 5 BTC, - # and Bob having 5 BTC. - self.alice_channel, self.bob_channel = create_test_channels() - - self.paymentPreimage = b"\x01" * 32 - paymentHash = bitcoin.sha256(self.paymentPreimage) - self.htlc_dict = { - 'payment_hash' : paymentHash, - 'amount_msat' : one_bitcoin_in_msat, - 'cltv_expiry' : 5, - } - - # First Alice adds the outgoing HTLC to her local channel's state - # update log. Then Alice sends this wire message over to Bob who adds - # this htlc to his remote state update log. - self.aliceHtlcIndex = self.alice_channel.add_htlc(self.htlc_dict) - self.assertNotEqual(self.alice_channel.hm.htlcs_by_direction(REMOTE, RECEIVED, 1), set()) - - before = self.bob_channel.balance_minus_outgoing_htlcs(REMOTE) - beforeLocal = self.bob_channel.balance_minus_outgoing_htlcs(LOCAL) - - self.bobHtlcIndex = self.bob_channel.receive_htlc(self.htlc_dict) - - self.assertEqual(1, self.bob_channel.hm.log[LOCAL]['ctn'] + 1) - self.assertNotEqual(self.bob_channel.hm.htlcs_by_direction(LOCAL, RECEIVED, 1), set()) - after = self.bob_channel.balance_minus_outgoing_htlcs(REMOTE) - afterLocal = self.bob_channel.balance_minus_outgoing_htlcs(LOCAL) - - self.assertEqual(before - after, self.htlc_dict['amount_msat']) - self.assertEqual(beforeLocal, afterLocal) - - self.bob_pending_remote_balance = after - - self.htlc = self.bob_channel.hm.log[REMOTE]['adds'][0] - - def test_concurrent_reversed_payment(self): - self.htlc_dict['payment_hash'] = bitcoin.sha256(32 * b'\x02') - self.htlc_dict['amount_msat'] += 1000 - bob_idx = self.bob_channel.add_htlc(self.htlc_dict) - alice_idx = self.alice_channel.receive_htlc(self.htlc_dict) - self.alice_channel.receive_new_commitment(*self.bob_channel.sign_next_commitment()) - self.assertEqual(len(self.alice_channel.pending_commitment(REMOTE).outputs()), 4) - - def test_SimpleAddSettleWorkflow(self): - alice_channel, bob_channel = self.alice_channel, self.bob_channel - htlc = self.htlc - - alice_out = alice_channel.current_commitment(LOCAL).outputs() - short_idx, = [idx for idx, x in enumerate(alice_out) if len(x.address) == 42] - long_idx, = [idx for idx, x in enumerate(alice_out) if len(x.address) == 62] - self.assertLess(alice_out[long_idx].value, 5 * 10**8, alice_out) - self.assertEqual(alice_out[short_idx].value, 5 * 10**8, alice_out) - - alice_out = alice_channel.current_commitment(REMOTE).outputs() - short_idx, = [idx for idx, x in enumerate(alice_out) if len(x.address) == 42] - long_idx, = [idx for idx, x in enumerate(alice_out) if len(x.address) == 62] - self.assertLess(alice_out[short_idx].value, 5 * 10**8) - self.assertEqual(alice_out[long_idx].value, 5 * 10**8) - - def com(): - return alice_channel.local_commitment - - self.assertTrue(alice_channel.signature_fits(com())) - - self.assertNotEqual(alice_channel.included_htlcs(REMOTE, RECEIVED, 1), []) - self.assertEqual({0: [], 1: [htlc]}, alice_channel.included_htlcs_in_their_latest_ctxs(LOCAL)) - self.assertNotEqual(bob_channel.included_htlcs(REMOTE, SENT, 1), []) - self.assertEqual({0: [], 1: [htlc]}, bob_channel.included_htlcs_in_their_latest_ctxs(REMOTE)) - self.assertEqual({0: [], 1: []}, alice_channel.included_htlcs_in_their_latest_ctxs(REMOTE)) - self.assertEqual({0: [], 1: []}, bob_channel.included_htlcs_in_their_latest_ctxs(LOCAL)) - - from electrum.lnutil import extract_ctn_from_tx_and_chan - tx0 = str(alice_channel.force_close_tx()) - self.assertEqual(alice_channel.config[LOCAL].ctn, 0) - self.assertEqual(extract_ctn_from_tx_and_chan(alice_channel.force_close_tx(), alice_channel), 0) - self.assertTrue(alice_channel.signature_fits(alice_channel.current_commitment(LOCAL))) - - # Next alice commits this change by sending a signature message. Since - # we expect the messages to be ordered, Bob will receive the HTLC we - # just sent before he receives this signature, so the signature will - # cover the HTLC. - aliceSig, aliceHtlcSigs = alice_channel.sign_next_commitment() - self.assertEqual(len(aliceHtlcSigs), 1, "alice should generate one htlc signature") - - self.assertTrue(alice_channel.signature_fits(com())) - self.assertEqual(str(alice_channel.current_commitment(LOCAL)), str(com())) - - self.assertEqual(next(iter(alice_channel.hm.pending_htlcs(REMOTE)))[0], RECEIVED) - self.assertEqual(alice_channel.hm.pending_htlcs(REMOTE), bob_channel.hm.pending_htlcs(LOCAL)) - self.assertEqual(alice_channel.pending_commitment(REMOTE).outputs(), bob_channel.pending_commitment(LOCAL).outputs()) - - # Bob receives this signature message, and checks that this covers the - # state he has in his remote log. This includes the HTLC just sent - # from Alice. - self.assertTrue(bob_channel.signature_fits(bob_channel.current_commitment(LOCAL))) - bob_channel.receive_new_commitment(aliceSig, aliceHtlcSigs) - self.assertTrue(bob_channel.signature_fits(bob_channel.pending_commitment(LOCAL))) - - self.assertEqual(bob_channel.config[REMOTE].ctn, 0) - self.assertEqual(bob_channel.included_htlcs(REMOTE, SENT, 1), [htlc]) - - self.assertEqual({0: [], 1: [htlc]}, alice_channel.included_htlcs_in_their_latest_ctxs(LOCAL)) - self.assertEqual({0: [], 1: [htlc]}, bob_channel.included_htlcs_in_their_latest_ctxs(REMOTE)) - self.assertEqual({0: [], 1: []}, alice_channel.included_htlcs_in_their_latest_ctxs(REMOTE)) - self.assertEqual({0: [], 1: []}, bob_channel.included_htlcs_in_their_latest_ctxs(LOCAL)) - - # Bob revokes his prior commitment given to him by Alice, since he now - # has a valid signature for a newer commitment. - bobRevocation, _ = bob_channel.revoke_current_commitment() - bob_channel.serialize() - self.assertTrue(bob_channel.signature_fits(bob_channel.current_commitment(LOCAL))) - - # Bob finally sends a signature for Alice's commitment transaction. - # This signature will cover the HTLC, since Bob will first send the - # revocation just created. The revocation also acks every received - # HTLC up to the point where Alice sent her signature. - bobSig, bobHtlcSigs = bob_channel.sign_next_commitment() - self.assertTrue(bob_channel.signature_fits(bob_channel.current_commitment(LOCAL))) - - self.assertEqual(len(bobHtlcSigs), 1) - - self.assertTrue(alice_channel.signature_fits(com())) - self.assertEqual(str(alice_channel.current_commitment(LOCAL)), str(com())) - - self.assertEqual(len(alice_channel.pending_commitment(LOCAL).outputs()), 3) - - # Alice then processes this revocation, sending her own revocation for - # her prior commitment transaction. Alice shouldn't have any HTLCs to - # forward since she's sending an outgoing HTLC. - alice_channel.receive_revocation(bobRevocation) - alice_channel.serialize() - self.assertEqual(alice_channel.remote_commitment.outputs(), alice_channel.current_commitment(REMOTE).outputs()) - - self.assertTrue(alice_channel.signature_fits(com())) - self.assertTrue(alice_channel.signature_fits(alice_channel.current_commitment(LOCAL))) - alice_channel.serialize() - self.assertEqual(str(alice_channel.current_commitment(LOCAL)), str(com())) - - self.assertEqual(len(alice_channel.current_commitment(LOCAL).outputs()), 2) - self.assertEqual(len(alice_channel.current_commitment(REMOTE).outputs()), 3) - self.assertEqual(len(com().outputs()), 2) - self.assertEqual(len(alice_channel.force_close_tx().outputs()), 2) - - self.assertEqual(alice_channel.hm.log.keys(), set([LOCAL, REMOTE])) - self.assertEqual(len(alice_channel.hm.log[LOCAL]['adds']), 1) - alice_channel.serialize() - - self.assertEqual(alice_channel.pending_commitment(LOCAL).outputs(), - bob_channel.pending_commitment(REMOTE).outputs()) - - # Alice then processes bob's signature, and since she just received - # the revocation, she expect this signature to cover everything up to - # the point where she sent her signature, including the HTLC. - alice_channel.receive_new_commitment(bobSig, bobHtlcSigs) - self.assertEqual(alice_channel.remote_commitment.outputs(), alice_channel.current_commitment(REMOTE).outputs()) - - self.assertEqual(len(alice_channel.current_commitment(REMOTE).outputs()), 3) - self.assertEqual(len(com().outputs()), 3) - self.assertEqual(len(alice_channel.force_close_tx().outputs()), 3) - - self.assertEqual(len(alice_channel.hm.log[LOCAL]['adds']), 1) - alice_channel.serialize() - - tx1 = str(alice_channel.force_close_tx()) - self.assertNotEqual(tx0, tx1) - - # Alice then generates a revocation for bob. - self.assertEqual(alice_channel.remote_commitment.outputs(), alice_channel.current_commitment(REMOTE).outputs()) - aliceRevocation, _ = alice_channel.revoke_current_commitment() - alice_channel.serialize() - #self.assertEqual(alice_channel.remote_commitment.outputs(), alice_channel.current_commitment(REMOTE).outputs()) - - tx2 = str(alice_channel.force_close_tx()) - # since alice already has the signature for the next one, it doesn't change her force close tx (it was already the newer one) - self.assertEqual(tx1, tx2) - - # Finally Bob processes Alice's revocation, at this point the new HTLC - # is fully locked in within both commitment transactions. Bob should - # also be able to forward an HTLC now that the HTLC has been locked - # into both commitment transactions. - self.assertTrue(bob_channel.signature_fits(bob_channel.current_commitment(LOCAL))) - bob_channel.receive_revocation(aliceRevocation) - bob_channel.serialize() - - # At this point, both sides should have the proper number of satoshis - # sent, and commitment height updated within their local channel - # state. - aliceSent = 0 - bobSent = 0 - - self.assertEqual(alice_channel.total_msat(SENT), aliceSent, "alice has incorrect milli-satoshis sent") - self.assertEqual(alice_channel.total_msat(RECEIVED), bobSent, "alice has incorrect milli-satoshis received") - self.assertEqual(bob_channel.total_msat(SENT), bobSent, "bob has incorrect milli-satoshis sent") - self.assertEqual(bob_channel.total_msat(RECEIVED), aliceSent, "bob has incorrect milli-satoshis received") - self.assertEqual(bob_channel.config[LOCAL].ctn, 1, "bob has incorrect commitment height") - self.assertEqual(alice_channel.config[LOCAL].ctn, 1, "alice has incorrect commitment height") - - # Both commitment transactions should have three outputs, and one of - # them should be exactly the amount of the HTLC. - alice_ctx = alice_channel.pending_commitment(LOCAL) - bob_ctx = bob_channel.pending_commitment(LOCAL) - self.assertEqual(len(alice_ctx.outputs()), 3, "alice should have three commitment outputs, instead have %s"% len(alice_ctx.outputs())) - self.assertEqual(len(bob_ctx.outputs()), 3, "bob should have three commitment outputs, instead have %s"% len(bob_ctx.outputs())) - self.assertOutputExistsByValue(alice_ctx, htlc.amount_msat // 1000) - self.assertOutputExistsByValue(bob_ctx, htlc.amount_msat // 1000) - - # Now we'll repeat a similar exchange, this time with Bob settling the - # HTLC once he learns of the preimage. - preimage = self.paymentPreimage - bob_channel.settle_htlc(preimage, self.bobHtlcIndex) - - #self.assertEqual(alice_channel.remote_commitment.outputs(), alice_channel.current_commitment(REMOTE).outputs()) - alice_channel.receive_htlc_settle(preimage, self.aliceHtlcIndex) - - tx3 = str(alice_channel.force_close_tx()) - # just settling a htlc does not change her force close tx - self.assertEqual(tx2, tx3) - - bobSig2, bobHtlcSigs2 = bob_channel.sign_next_commitment() - self.assertEqual(len(bobHtlcSigs2), 0) - - self.assertEqual(alice_channel.hm.htlcs_by_direction(REMOTE, RECEIVED), [htlc]) - self.assertEqual(alice_channel.included_htlcs(REMOTE, RECEIVED, alice_channel.config[REMOTE].ctn), [htlc]) - self.assertEqual({1: [htlc], 2: []}, alice_channel.included_htlcs_in_their_latest_ctxs(LOCAL)) - self.assertEqual({1: [htlc], 2: []}, bob_channel.included_htlcs_in_their_latest_ctxs(REMOTE)) - self.assertEqual({1: [], 2: []}, alice_channel.included_htlcs_in_their_latest_ctxs(REMOTE)) - self.assertEqual({1: [], 2: []}, bob_channel.included_htlcs_in_their_latest_ctxs(LOCAL)) - - alice_ctx_bob_version = bob_channel.pending_commitment(REMOTE).outputs() - alice_ctx_alice_version = alice_channel.pending_commitment(LOCAL).outputs() - self.assertEqual(alice_ctx_alice_version, alice_ctx_bob_version) - - alice_channel.receive_new_commitment(bobSig2, bobHtlcSigs2) - - tx4 = str(alice_channel.force_close_tx()) - self.assertNotEqual(tx3, tx4) - - self.assertEqual(alice_channel.balance(LOCAL), 500000000000) - self.assertEqual(1, alice_channel.config[LOCAL].ctn) - self.assertEqual(len(alice_channel.included_htlcs(LOCAL, RECEIVED, ctn=2)), 0) - aliceRevocation2, _ = alice_channel.revoke_current_commitment() - alice_channel.serialize() - aliceSig2, aliceHtlcSigs2 = alice_channel.sign_next_commitment() - self.assertEqual(aliceHtlcSigs2, [], "alice should generate no htlc signatures") - self.assertEqual(len(bob_channel.current_commitment(LOCAL).outputs()), 3) - self.assertEqual(len(bob_channel.pending_commitment(LOCAL).outputs()), 2) - received, sent = bob_channel.receive_revocation(aliceRevocation2) - bob_channel.serialize() - self.assertEqual(received, one_bitcoin_in_msat) - - bob_channel.receive_new_commitment(aliceSig2, aliceHtlcSigs2) - - bobRevocation2, _ = bob_channel.revoke_current_commitment() - bob_channel.serialize() - alice_channel.receive_revocation(bobRevocation2) - alice_channel.serialize() - - # At this point, Bob should have 6 BTC settled, with Alice still having - # 4 BTC. Alice's channel should show 1 BTC sent and Bob's channel - # should show 1 BTC received. They should also be at commitment height - # two, with the revocation window extended by 1 (5). - mSatTransferred = one_bitcoin_in_msat - self.assertEqual(alice_channel.total_msat(SENT), mSatTransferred, "alice satoshis sent incorrect") - self.assertEqual(alice_channel.total_msat(RECEIVED), 0, "alice satoshis received incorrect") - self.assertEqual(bob_channel.total_msat(RECEIVED), mSatTransferred, "bob satoshis received incorrect") - self.assertEqual(bob_channel.total_msat(SENT), 0, "bob satoshis sent incorrect") - self.assertEqual(bob_channel.current_height[LOCAL], 2, "bob has incorrect commitment height") - self.assertEqual(alice_channel.current_height[LOCAL], 2, "alice has incorrect commitment height") - - self.assertEqual(self.bob_pending_remote_balance, self.alice_channel.balance(LOCAL)) - - alice_channel.update_fee(100000, True) - alice_outputs = alice_channel.pending_commitment(REMOTE).outputs() - old_outputs = bob_channel.pending_commitment(LOCAL).outputs() - bob_channel.update_fee(100000, False) - new_outputs = bob_channel.pending_commitment(LOCAL).outputs() - self.assertNotEqual(old_outputs, new_outputs) - self.assertEqual(alice_outputs, new_outputs) - - tx5 = str(alice_channel.force_close_tx()) - # sending a fee update does not change her force close tx - self.assertEqual(tx4, tx5) - - force_state_transition(alice_channel, bob_channel) - - tx6 = str(alice_channel.force_close_tx()) - self.assertNotEqual(tx5, tx6) - - self.htlc_dict['amount_msat'] *= 5 - bob_index = bob_channel.add_htlc(self.htlc_dict) - alice_index = alice_channel.receive_htlc(self.htlc_dict) - - bob_channel.pending_commitment(REMOTE) - alice_channel.pending_commitment(LOCAL) - - alice_channel.pending_commitment(REMOTE) - bob_channel.pending_commitment(LOCAL) - - force_state_transition(bob_channel, alice_channel) - alice_channel.settle_htlc(self.paymentPreimage, alice_index) - bob_channel.receive_htlc_settle(self.paymentPreimage, bob_index) - force_state_transition(bob_channel, alice_channel) - self.assertEqual(alice_channel.total_msat(SENT), one_bitcoin_in_msat, "alice satoshis sent incorrect") - self.assertEqual(alice_channel.total_msat(RECEIVED), 5 * one_bitcoin_in_msat, "alice satoshis received incorrect") - self.assertEqual(bob_channel.total_msat(RECEIVED), one_bitcoin_in_msat, "bob satoshis received incorrect") - self.assertEqual(bob_channel.total_msat(SENT), 5 * one_bitcoin_in_msat, "bob satoshis sent incorrect") - - alice_channel.serialize() - - - def alice_to_bob_fee_update(self, fee=111): - aoldctx = self.alice_channel.pending_commitment(REMOTE).outputs() - self.alice_channel.update_fee(fee, True) - anewctx = self.alice_channel.pending_commitment(REMOTE).outputs() - self.assertNotEqual(aoldctx, anewctx) - boldctx = self.bob_channel.pending_commitment(LOCAL).outputs() - self.bob_channel.update_fee(fee, False) - bnewctx = self.bob_channel.pending_commitment(LOCAL).outputs() - self.assertNotEqual(boldctx, bnewctx) - self.assertEqual(anewctx, bnewctx) - return fee - - def test_UpdateFeeSenderCommits(self): - old_feerate = self.alice_channel.pending_feerate(LOCAL) - fee = self.alice_to_bob_fee_update() - - alice_channel, bob_channel = self.alice_channel, self.bob_channel - - self.assertEqual(self.alice_channel.pending_feerate(LOCAL), old_feerate) - alice_sig, alice_htlc_sigs = alice_channel.sign_next_commitment() - self.assertEqual(self.alice_channel.pending_feerate(LOCAL), old_feerate) - - bob_channel.receive_new_commitment(alice_sig, alice_htlc_sigs) - - self.assertNotEqual(fee, bob_channel.constraints.feerate) - rev, _ = bob_channel.revoke_current_commitment() - self.assertEqual(fee, bob_channel.constraints.feerate) - - bob_sig, bob_htlc_sigs = bob_channel.sign_next_commitment() - alice_channel.receive_revocation(rev) - alice_channel.receive_new_commitment(bob_sig, bob_htlc_sigs) - - self.assertNotEqual(fee, alice_channel.constraints.feerate) - rev, _ = alice_channel.revoke_current_commitment() - self.assertEqual(fee, alice_channel.constraints.feerate) - - bob_channel.receive_revocation(rev) - self.assertEqual(fee, bob_channel.constraints.feerate) - - - def test_UpdateFeeReceiverCommits(self): - fee = self.alice_to_bob_fee_update() - - alice_channel, bob_channel = self.alice_channel, self.bob_channel - - bob_sig, bob_htlc_sigs = bob_channel.sign_next_commitment() - alice_channel.receive_new_commitment(bob_sig, bob_htlc_sigs) - - alice_revocation, _ = alice_channel.revoke_current_commitment() - bob_channel.receive_revocation(alice_revocation) - alice_sig, alice_htlc_sigs = alice_channel.sign_next_commitment() - bob_channel.receive_new_commitment(alice_sig, alice_htlc_sigs) - - self.assertNotEqual(fee, bob_channel.constraints.feerate) - bob_revocation, _ = bob_channel.revoke_current_commitment() - self.assertEqual(fee, bob_channel.constraints.feerate) - - bob_sig, bob_htlc_sigs = bob_channel.sign_next_commitment() - alice_channel.receive_revocation(bob_revocation) - alice_channel.receive_new_commitment(bob_sig, bob_htlc_sigs) - - self.assertNotEqual(fee, alice_channel.constraints.feerate) - alice_revocation, _ = alice_channel.revoke_current_commitment() - self.assertEqual(fee, alice_channel.constraints.feerate) - - bob_channel.receive_revocation(alice_revocation) - self.assertEqual(fee, bob_channel.constraints.feerate) - - def test_AddHTLCNegativeBalance(self): - # the test in lnd doesn't set the fee to zero. - # probably lnd subtracts commitment fee after deciding weather - # an htlc can be added. so we set the fee to zero so that - # the test can work. - self.alice_to_bob_fee_update(0) - force_state_transition(self.alice_channel, self.bob_channel) - - self.htlc_dict['payment_hash'] = bitcoin.sha256(32 * b'\x02') - self.alice_channel.add_htlc(self.htlc_dict) - self.htlc_dict['payment_hash'] = bitcoin.sha256(32 * b'\x03') - self.alice_channel.add_htlc(self.htlc_dict) - # now there are three htlcs (one was in setUp) - - # Alice now has an available balance of 2 BTC. We'll add a new HTLC of - # value 2 BTC, which should make Alice's balance negative (since she - # has to pay a commitment fee). - new = dict(self.htlc_dict) - new['amount_msat'] *= 2.5 - new['payment_hash'] = bitcoin.sha256(32 * b'\x04') - with self.assertRaises(lnutil.PaymentFailure) as cm: - self.alice_channel.add_htlc(new) - self.assertIn('Not enough local balance', cm.exception.args[0]) - - def test_sign_commitment_is_pure(self): - force_state_transition(self.alice_channel, self.bob_channel) - self.htlc_dict['payment_hash'] = bitcoin.sha256(b'\x02' * 32) - aliceHtlcIndex = self.alice_channel.add_htlc(self.htlc_dict) - before_signing = self.alice_channel.to_save() - self.alice_channel.sign_next_commitment() - after_signing = self.alice_channel.to_save() - try: - self.assertEqual(before_signing, after_signing) - except: - try: - from deepdiff import DeepDiff - except ImportError: - raise - raise Exception(pformat(DeepDiff(before_signing, after_signing))) - -class TestAvailableToSpend(unittest.TestCase): - def test_DesyncHTLCs(self): - alice_channel, bob_channel = create_test_channels() - - paymentPreimage = b"\x01" * 32 - paymentHash = bitcoin.sha256(paymentPreimage) - htlc_dict = { - 'payment_hash' : paymentHash, - 'amount_msat' : int(4.1 * one_bitcoin_in_msat), - 'cltv_expiry' : 5, - } - - alice_idx = alice_channel.add_htlc(htlc_dict) - bob_idx = bob_channel.receive_htlc(htlc_dict) - force_state_transition(alice_channel, bob_channel) - bob_channel.fail_htlc(bob_idx) - alice_channel.receive_fail_htlc(alice_idx) - # Alice now has gotten all her original balance (5 BTC) back, however, - # adding a new HTLC at this point SHOULD fail, since if she adds the - # HTLC and signs the next state, Bob cannot assume she received the - # FailHTLC, and must assume she doesn't have the necessary balance - # available. - # We try adding an HTLC of value 1 BTC, which should fail because the - # balance is unavailable. - htlc_dict = { - 'payment_hash' : paymentHash, - 'amount_msat' : one_bitcoin_in_msat, - 'cltv_expiry' : 5, - } - with self.assertRaises(lnutil.PaymentFailure): - alice_channel.add_htlc(htlc_dict) - # Now do a state transition, which will ACK the FailHTLC, making Alice - # able to add the new HTLC. - force_state_transition(alice_channel, bob_channel) - alice_channel.add_htlc(htlc_dict) - -class TestChanReserve(unittest.TestCase): - def setUp(self): - alice_channel, bob_channel = create_test_channels() - alice_min_reserve = int(.5 * one_bitcoin_in_msat // 1000) - # We set Bob's channel reserve to a value that is larger than - # his current balance in the channel. This will ensure that - # after a channel is first opened, Bob can still receive HTLCs - # even though his balance is less than his channel reserve. - bob_min_reserve = 6 * one_bitcoin_in_msat // 1000 - # bob min reserve was decided by alice, but applies to bob - - alice_channel.config[LOCAL] =\ - alice_channel.config[LOCAL]._replace(reserve_sat=bob_min_reserve) - alice_channel.config[REMOTE] =\ - alice_channel.config[REMOTE]._replace(reserve_sat=alice_min_reserve) - - bob_channel.config[LOCAL] =\ - bob_channel.config[LOCAL]._replace(reserve_sat=alice_min_reserve) - bob_channel.config[REMOTE] =\ - bob_channel.config[REMOTE]._replace(reserve_sat=bob_min_reserve) - - self.alice_channel = alice_channel - self.bob_channel = bob_channel - - def test_part1(self): - # Add an HTLC that will increase Bob's balance. This should succeed, - # since Alice stays above her channel reserve, and Bob increases his - # balance (while still being below his channel reserve). - # - # Resulting balances: - # Alice: 4.5 - # Bob: 5.0 - paymentPreimage = b"\x01" * 32 - paymentHash = bitcoin.sha256(paymentPreimage) - htlc_dict = { - 'payment_hash' : paymentHash, - 'amount_msat' : int(.5 * one_bitcoin_in_msat), - 'cltv_expiry' : 5, - } - self.alice_channel.add_htlc(htlc_dict) - self.bob_channel.receive_htlc(htlc_dict) - # Force a state transition, making sure this HTLC is considered valid - # even though the channel reserves are not met. - force_state_transition(self.alice_channel, self.bob_channel) - - aliceSelfBalance = self.alice_channel.balance(LOCAL)\ - - lnchan.htlcsum(self.alice_channel.hm.htlcs_by_direction(LOCAL, SENT)) - bobBalance = self.bob_channel.balance(REMOTE)\ - - lnchan.htlcsum(self.alice_channel.hm.htlcs_by_direction(REMOTE, SENT)) - self.assertEqual(aliceSelfBalance, one_bitcoin_in_msat*4.5) - self.assertEqual(bobBalance, one_bitcoin_in_msat*5) - # Now let Bob try to add an HTLC. This should fail, since it will - # decrease his balance, which is already below the channel reserve. - # - # Resulting balances: - # Alice: 4.5 - # Bob: 5.0 - with self.assertRaises(lnutil.PaymentFailure): - htlc_dict['payment_hash'] = bitcoin.sha256(32 * b'\x02') - self.bob_channel.add_htlc(htlc_dict) - with self.assertRaises(lnutil.RemoteMisbehaving): - self.alice_channel.receive_htlc(htlc_dict) - - def part2(self): - paymentPreimage = b"\x01" * 32 - paymentHash = bitcoin.sha256(paymentPreimage) - # Now we'll add HTLC of 3.5 BTC to Alice's commitment, this should put - # Alice's balance at 1.5 BTC. - # - # Resulting balances: - # Alice: 1.5 - # Bob: 9.5 - htlc_dict = { - 'payment_hash' : paymentHash, - 'amount_msat' : int(3.5 * one_bitcoin_in_msat), - 'cltv_expiry' : 5, - } - self.alice_channel.add_htlc(htlc_dict) - self.bob_channel.receive_htlc(htlc_dict) - # Add a second HTLC of 1 BTC. This should fail because it will take - # Alice's balance all the way down to her channel reserve, but since - # she is the initiator the additional transaction fee makes her - # balance dip below. - htlc_dict['amount_msat'] = one_bitcoin_in_msat - with self.assertRaises(lnutil.PaymentFailure): - self.alice_channel.add_htlc(htlc_dict) - with self.assertRaises(lnutil.RemoteMisbehaving): - self.bob_channel.receive_htlc(htlc_dict) - - def part3(self): - # Add a HTLC of 2 BTC to Alice, and the settle it. - # Resulting balances: - # Alice: 3.0 - # Bob: 7.0 - htlc_dict = { - 'payment_hash' : paymentHash, - 'amount_msat' : int(2 * one_bitcoin_in_msat), - 'cltv_expiry' : 5, - } - alice_idx = self.alice_channel.add_htlc(htlc_dict) - bob_idx = self.bob_channel.receive_htlc(htlc_dict) - force_state_transition(self.alice_channel, self.bob_channel) - self.check_bals(one_bitcoin_in_msat*3\ - - self.alice_channel.pending_local_fee(), - one_bitcoin_in_msat*5) - self.bob_channel.settle_htlc(paymentPreimage, bob_idx) - self.alice_channel.receive_htlc_settle(paymentPreimage, alice_idx) - force_state_transition(self.alice_channel, self.bob_channel) - self.check_bals(one_bitcoin_in_msat*3\ - - self.alice_channel.pending_local_fee(), - one_bitcoin_in_msat*7) - # And now let Bob add an HTLC of 1 BTC. This will take Bob's balance - # all the way down to his channel reserve, but since he is not paying - # the fee this is okay. - htlc_dict['amount_msat'] = one_bitcoin_in_msat - self.bob_channel.add_htlc(htlc_dict) - self.alice_channel.receive_htlc(htlc_dict) - force_state_transition(self.alice_channel, self.bob_channel) - self.check_bals(one_bitcoin_in_msat*3\ - - self.alice_channel.pending_local_fee(), - one_bitcoin_in_msat*6) - - def check_bals(self, amt1, amt2): - self.assertEqual(self.alice_channel.available_to_spend(LOCAL), amt1) - self.assertEqual(self.bob_channel.available_to_spend(REMOTE), amt1) - self.assertEqual(self.alice_channel.available_to_spend(REMOTE), amt2) - self.assertEqual(self.bob_channel.available_to_spend(LOCAL), amt2) - -class TestDust(unittest.TestCase): - def test_DustLimit(self): - alice_channel, bob_channel = create_test_channels() - - paymentPreimage = b"\x01" * 32 - paymentHash = bitcoin.sha256(paymentPreimage) - fee_per_kw = alice_channel.constraints.feerate - self.assertEqual(fee_per_kw, 6000) - htlcAmt = 500 + lnutil.HTLC_TIMEOUT_WEIGHT * (fee_per_kw // 1000) - self.assertEqual(htlcAmt, 4478) - htlc = { - 'payment_hash' : paymentHash, - 'amount_msat' : 1000 * htlcAmt, - 'cltv_expiry' : 5, # also in create_test_channels - } - - old_values = [x.value for x in bob_channel.current_commitment(LOCAL).outputs() ] - aliceHtlcIndex = alice_channel.add_htlc(htlc) - bobHtlcIndex = bob_channel.receive_htlc(htlc) - force_state_transition(alice_channel, bob_channel) - alice_ctx = alice_channel.current_commitment(LOCAL) - bob_ctx = bob_channel.current_commitment(LOCAL) - new_values = [x.value for x in bob_ctx.outputs() ] - self.assertNotEqual(old_values, new_values) - self.assertEqual(len(alice_ctx.outputs()), 3) - self.assertEqual(len(bob_ctx.outputs()), 2) - default_fee = calc_static_fee(0) - self.assertEqual(bob_channel.pending_local_fee(), default_fee + htlcAmt) - bob_channel.settle_htlc(paymentPreimage, bobHtlcIndex) - alice_channel.receive_htlc_settle(paymentPreimage, aliceHtlcIndex) - force_state_transition(bob_channel, alice_channel) - self.assertEqual(len(alice_channel.pending_commitment(LOCAL).outputs()), 2) - self.assertEqual(alice_channel.total_msat(SENT) // 1000, htlcAmt) - -def force_state_transition(chanA, chanB): - chanB.receive_new_commitment(*chanA.sign_next_commitment()) - rev, _ = chanB.revoke_current_commitment() - bob_sig, bob_htlc_sigs = chanB.sign_next_commitment() - chanA.receive_revocation(rev) - chanA.receive_new_commitment(bob_sig, bob_htlc_sigs) - chanB.receive_revocation(chanA.revoke_current_commitment()[0]) - -# calcStaticFee calculates appropriate fees for commitment transactions. This -# function provides a simple way to allow test balance assertions to take fee -# calculations into account. -def calc_static_fee(numHTLCs): - commitWeight = 724 - htlcWeight = 172 - feePerKw = 24//4 * 1000 - return feePerKw * (commitWeight + htlcWeight*numHTLCs) // 1000 diff --git a/electrum/tests/test_lnchannel.py b/electrum/tests/test_lnchannel.py @@ -0,0 +1,826 @@ +# 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. + +import unittest +import os +import binascii +from pprint import pformat + +from electrum import bitcoin +from electrum import lnpeer +from electrum import lnchannel +from electrum import lnutil +from electrum import bip32 as bip32_utils +from electrum.lnutil import SENT, LOCAL, REMOTE, RECEIVED +from electrum.ecc import sig_string_from_der_sig +from electrum.util import set_verbosity + +one_bitcoin_in_msat = bitcoin.COIN * 1000 + +def create_channel_state(funding_txid, funding_index, funding_sat, local_feerate, is_initiator, local_amount, remote_amount, privkeys, other_pubkeys, seed, cur, nex, other_node_id, l_dust, r_dust, l_csv, r_csv): + assert local_amount > 0 + assert remote_amount > 0 + channel_id, _ = lnpeer.channel_id_from_funding_tx(funding_txid, funding_index) + their_revocation_store = lnpeer.RevocationStore() + + return { + "channel_id":channel_id, + "short_channel_id":channel_id[:8], + "funding_outpoint":lnpeer.Outpoint(funding_txid, funding_index), + "remote_config":lnpeer.RemoteConfig( + payment_basepoint=other_pubkeys[0], + multisig_key=other_pubkeys[1], + htlc_basepoint=other_pubkeys[2], + delayed_basepoint=other_pubkeys[3], + revocation_basepoint=other_pubkeys[4], + to_self_delay=r_csv, + dust_limit_sat=r_dust, + max_htlc_value_in_flight_msat=one_bitcoin_in_msat * 5, + max_accepted_htlcs=5, + initial_msat=remote_amount, + ctn = -1, + next_htlc_id = 0, + reserve_sat=0, + htlc_minimum_msat=1, + + next_per_commitment_point=nex, + current_per_commitment_point=cur, + revocation_store=their_revocation_store, + ), + "local_config":lnpeer.LocalConfig( + payment_basepoint=privkeys[0], + multisig_key=privkeys[1], + htlc_basepoint=privkeys[2], + delayed_basepoint=privkeys[3], + revocation_basepoint=privkeys[4], + to_self_delay=l_csv, + dust_limit_sat=l_dust, + max_htlc_value_in_flight_msat=one_bitcoin_in_msat * 5, + max_accepted_htlcs=5, + initial_msat=local_amount, + ctn = 0, + next_htlc_id = 0, + reserve_sat=0, + + per_commitment_secret_seed=seed, + funding_locked_received=True, + was_announced=False, + current_commitment_signature=None, + current_htlc_signatures=None, + got_sig_for_next=False, + ), + "constraints":lnpeer.ChannelConstraints( + capacity=funding_sat, + is_initiator=is_initiator, + funding_txn_minimum_depth=3, + feerate=local_feerate, + ), + "node_id":other_node_id, + "remote_commitment_to_be_revoked": None, + 'onion_keys': {}, + } + +def bip32(sequence): + xprv, xpub = bip32_utils.bip32_root(b"9dk", 'standard') + xprv, xpub = bip32_utils.bip32_private_derivation(xprv, "m/", sequence) + xtype, depth, fingerprint, child_number, c, k = bip32_utils.deserialize_xprv(xprv) + assert len(k) == 32 + assert type(k) is bytes + return k + +def create_test_channels(feerate=6000, local=None, remote=None): + funding_txid = binascii.hexlify(b"\x01"*32).decode("ascii") + funding_index = 0 + funding_sat = ((local + remote) // 1000) if local is not None and remote is not None else (bitcoin.COIN * 10) + local_amount = local if local is not None else (funding_sat * 1000 // 2) + remote_amount = remote if remote is not None else (funding_sat * 1000 // 2) + alice_raw = [ bip32("m/" + str(i)) for i in range(5) ] + bob_raw = [ bip32("m/" + str(i)) for i in range(5,11) ] + alice_privkeys = [lnutil.Keypair(lnutil.privkey_to_pubkey(x), x) for x in alice_raw] + bob_privkeys = [lnutil.Keypair(lnutil.privkey_to_pubkey(x), x) for x in bob_raw] + alice_pubkeys = [lnutil.OnlyPubkeyKeypair(x.pubkey) for x in alice_privkeys] + bob_pubkeys = [lnutil.OnlyPubkeyKeypair(x.pubkey) for x in bob_privkeys] + + alice_seed = b"\x01" * 32 + bob_seed = b"\x02" * 32 + + alice_first = lnutil.secret_to_pubkey(int.from_bytes(lnutil.get_per_commitment_secret_from_seed(alice_seed, lnutil.RevocationStore.START_INDEX), "big")) + bob_first = lnutil.secret_to_pubkey(int.from_bytes(lnutil.get_per_commitment_secret_from_seed(bob_seed, lnutil.RevocationStore.START_INDEX), "big")) + + alice, bob = \ + lnchannel.Channel( + create_channel_state(funding_txid, funding_index, funding_sat, feerate, True, local_amount, remote_amount, alice_privkeys, bob_pubkeys, alice_seed, None, bob_first, b"\x02"*33, l_dust=200, r_dust=1300, l_csv=5, r_csv=4), name="alice"), \ + lnchannel.Channel( + create_channel_state(funding_txid, funding_index, funding_sat, feerate, False, remote_amount, local_amount, bob_privkeys, alice_pubkeys, bob_seed, None, alice_first, b"\x01"*33, l_dust=1300, r_dust=200, l_csv=4, r_csv=5), name="bob") + + alice.set_state('OPEN') + bob.set_state('OPEN') + + a_out = alice.current_commitment(LOCAL).outputs() + b_out = bob.pending_commitment(REMOTE).outputs() + assert a_out == b_out, "\n" + pformat((a_out, b_out)) + + sig_from_bob, a_htlc_sigs = bob.sign_next_commitment() + sig_from_alice, b_htlc_sigs = alice.sign_next_commitment() + + assert len(a_htlc_sigs) == 0 + assert len(b_htlc_sigs) == 0 + + alice.config[LOCAL] = alice.config[LOCAL]._replace(current_commitment_signature=sig_from_bob) + bob.config[LOCAL] = bob.config[LOCAL]._replace(current_commitment_signature=sig_from_alice) + + alice.set_local_commitment(alice.current_commitment(LOCAL)) + bob.set_local_commitment(bob.current_commitment(LOCAL)) + + alice_second = lnutil.secret_to_pubkey(int.from_bytes(lnutil.get_per_commitment_secret_from_seed(alice_seed, lnutil.RevocationStore.START_INDEX - 1), "big")) + bob_second = lnutil.secret_to_pubkey(int.from_bytes(lnutil.get_per_commitment_secret_from_seed(bob_seed, lnutil.RevocationStore.START_INDEX - 1), "big")) + + alice.config[REMOTE] = alice.config[REMOTE]._replace(next_per_commitment_point=bob_second, current_per_commitment_point=bob_first) + bob.config[REMOTE] = bob.config[REMOTE]._replace(next_per_commitment_point=alice_second, current_per_commitment_point=alice_first) + + alice.set_remote_commitment() + bob.set_remote_commitment() + + alice.remote_commitment_to_be_revoked = alice.remote_commitment + bob.remote_commitment_to_be_revoked = bob.remote_commitment + + alice.config[REMOTE] = alice.config[REMOTE]._replace(ctn=0) + bob.config[REMOTE] = bob.config[REMOTE]._replace(ctn=0) + + return alice, bob + +class TestFee(unittest.TestCase): + """ + test + https://github.com/lightningnetwork/lightning-rfc/blob/e0c436bd7a3ed6a028e1cb472908224658a14eca/03-transactions.md#requirements-2 + """ + def test_fee(self): + alice_channel, bob_channel = create_test_channels(253, 10000000000, 5000000000) + self.assertIn(9999817, [x[2] for x in alice_channel.local_commitment.outputs()]) + +class TestChannel(unittest.TestCase): + maxDiff = 999 + + def assertOutputExistsByValue(self, tx, amt_sat): + for typ, scr, val in tx.outputs(): + if val == amt_sat: + break + else: + self.assertFalse() + + @staticmethod + def setUpClass(): + set_verbosity(True) + + def setUp(self): + # Create a test channel which will be used for the duration of this + # unittest. The channel will be funded evenly with Alice having 5 BTC, + # and Bob having 5 BTC. + self.alice_channel, self.bob_channel = create_test_channels() + + self.paymentPreimage = b"\x01" * 32 + paymentHash = bitcoin.sha256(self.paymentPreimage) + self.htlc_dict = { + 'payment_hash' : paymentHash, + 'amount_msat' : one_bitcoin_in_msat, + 'cltv_expiry' : 5, + } + + # First Alice adds the outgoing HTLC to her local channel's state + # update log. Then Alice sends this wire message over to Bob who adds + # this htlc to his remote state update log. + self.aliceHtlcIndex = self.alice_channel.add_htlc(self.htlc_dict) + self.assertNotEqual(self.alice_channel.hm.htlcs_by_direction(REMOTE, RECEIVED, 1), set()) + + before = self.bob_channel.balance_minus_outgoing_htlcs(REMOTE) + beforeLocal = self.bob_channel.balance_minus_outgoing_htlcs(LOCAL) + + self.bobHtlcIndex = self.bob_channel.receive_htlc(self.htlc_dict) + + self.assertEqual(1, self.bob_channel.hm.log[LOCAL]['ctn'] + 1) + self.assertNotEqual(self.bob_channel.hm.htlcs_by_direction(LOCAL, RECEIVED, 1), set()) + after = self.bob_channel.balance_minus_outgoing_htlcs(REMOTE) + afterLocal = self.bob_channel.balance_minus_outgoing_htlcs(LOCAL) + + self.assertEqual(before - after, self.htlc_dict['amount_msat']) + self.assertEqual(beforeLocal, afterLocal) + + self.bob_pending_remote_balance = after + + self.htlc = self.bob_channel.hm.log[REMOTE]['adds'][0] + + def test_concurrent_reversed_payment(self): + self.htlc_dict['payment_hash'] = bitcoin.sha256(32 * b'\x02') + self.htlc_dict['amount_msat'] += 1000 + bob_idx = self.bob_channel.add_htlc(self.htlc_dict) + alice_idx = self.alice_channel.receive_htlc(self.htlc_dict) + self.alice_channel.receive_new_commitment(*self.bob_channel.sign_next_commitment()) + self.assertEqual(len(self.alice_channel.pending_commitment(REMOTE).outputs()), 4) + + def test_SimpleAddSettleWorkflow(self): + alice_channel, bob_channel = self.alice_channel, self.bob_channel + htlc = self.htlc + + alice_out = alice_channel.current_commitment(LOCAL).outputs() + short_idx, = [idx for idx, x in enumerate(alice_out) if len(x.address) == 42] + long_idx, = [idx for idx, x in enumerate(alice_out) if len(x.address) == 62] + self.assertLess(alice_out[long_idx].value, 5 * 10**8, alice_out) + self.assertEqual(alice_out[short_idx].value, 5 * 10**8, alice_out) + + alice_out = alice_channel.current_commitment(REMOTE).outputs() + short_idx, = [idx for idx, x in enumerate(alice_out) if len(x.address) == 42] + long_idx, = [idx for idx, x in enumerate(alice_out) if len(x.address) == 62] + self.assertLess(alice_out[short_idx].value, 5 * 10**8) + self.assertEqual(alice_out[long_idx].value, 5 * 10**8) + + def com(): + return alice_channel.local_commitment + + self.assertTrue(alice_channel.signature_fits(com())) + + self.assertNotEqual(alice_channel.included_htlcs(REMOTE, RECEIVED, 1), []) + self.assertEqual({0: [], 1: [htlc]}, alice_channel.included_htlcs_in_their_latest_ctxs(LOCAL)) + self.assertNotEqual(bob_channel.included_htlcs(REMOTE, SENT, 1), []) + self.assertEqual({0: [], 1: [htlc]}, bob_channel.included_htlcs_in_their_latest_ctxs(REMOTE)) + self.assertEqual({0: [], 1: []}, alice_channel.included_htlcs_in_their_latest_ctxs(REMOTE)) + self.assertEqual({0: [], 1: []}, bob_channel.included_htlcs_in_their_latest_ctxs(LOCAL)) + + from electrum.lnutil import extract_ctn_from_tx_and_chan + tx0 = str(alice_channel.force_close_tx()) + self.assertEqual(alice_channel.config[LOCAL].ctn, 0) + self.assertEqual(extract_ctn_from_tx_and_chan(alice_channel.force_close_tx(), alice_channel), 0) + self.assertTrue(alice_channel.signature_fits(alice_channel.current_commitment(LOCAL))) + + # Next alice commits this change by sending a signature message. Since + # we expect the messages to be ordered, Bob will receive the HTLC we + # just sent before he receives this signature, so the signature will + # cover the HTLC. + aliceSig, aliceHtlcSigs = alice_channel.sign_next_commitment() + self.assertEqual(len(aliceHtlcSigs), 1, "alice should generate one htlc signature") + + self.assertTrue(alice_channel.signature_fits(com())) + self.assertEqual(str(alice_channel.current_commitment(LOCAL)), str(com())) + + self.assertEqual(next(iter(alice_channel.hm.pending_htlcs(REMOTE)))[0], RECEIVED) + self.assertEqual(alice_channel.hm.pending_htlcs(REMOTE), bob_channel.hm.pending_htlcs(LOCAL)) + self.assertEqual(alice_channel.pending_commitment(REMOTE).outputs(), bob_channel.pending_commitment(LOCAL).outputs()) + + # Bob receives this signature message, and checks that this covers the + # state he has in his remote log. This includes the HTLC just sent + # from Alice. + self.assertTrue(bob_channel.signature_fits(bob_channel.current_commitment(LOCAL))) + bob_channel.receive_new_commitment(aliceSig, aliceHtlcSigs) + self.assertTrue(bob_channel.signature_fits(bob_channel.pending_commitment(LOCAL))) + + self.assertEqual(bob_channel.config[REMOTE].ctn, 0) + self.assertEqual(bob_channel.included_htlcs(REMOTE, SENT, 1), [htlc]) + + self.assertEqual({0: [], 1: [htlc]}, alice_channel.included_htlcs_in_their_latest_ctxs(LOCAL)) + self.assertEqual({0: [], 1: [htlc]}, bob_channel.included_htlcs_in_their_latest_ctxs(REMOTE)) + self.assertEqual({0: [], 1: []}, alice_channel.included_htlcs_in_their_latest_ctxs(REMOTE)) + self.assertEqual({0: [], 1: []}, bob_channel.included_htlcs_in_their_latest_ctxs(LOCAL)) + + # Bob revokes his prior commitment given to him by Alice, since he now + # has a valid signature for a newer commitment. + bobRevocation, _ = bob_channel.revoke_current_commitment() + bob_channel.serialize() + self.assertTrue(bob_channel.signature_fits(bob_channel.current_commitment(LOCAL))) + + # Bob finally sends a signature for Alice's commitment transaction. + # This signature will cover the HTLC, since Bob will first send the + # revocation just created. The revocation also acks every received + # HTLC up to the point where Alice sent her signature. + bobSig, bobHtlcSigs = bob_channel.sign_next_commitment() + self.assertTrue(bob_channel.signature_fits(bob_channel.current_commitment(LOCAL))) + + self.assertEqual(len(bobHtlcSigs), 1) + + self.assertTrue(alice_channel.signature_fits(com())) + self.assertEqual(str(alice_channel.current_commitment(LOCAL)), str(com())) + + self.assertEqual(len(alice_channel.pending_commitment(LOCAL).outputs()), 3) + + # Alice then processes this revocation, sending her own revocation for + # her prior commitment transaction. Alice shouldn't have any HTLCs to + # forward since she's sending an outgoing HTLC. + alice_channel.receive_revocation(bobRevocation) + alice_channel.serialize() + self.assertEqual(alice_channel.remote_commitment.outputs(), alice_channel.current_commitment(REMOTE).outputs()) + + self.assertTrue(alice_channel.signature_fits(com())) + self.assertTrue(alice_channel.signature_fits(alice_channel.current_commitment(LOCAL))) + alice_channel.serialize() + self.assertEqual(str(alice_channel.current_commitment(LOCAL)), str(com())) + + self.assertEqual(len(alice_channel.current_commitment(LOCAL).outputs()), 2) + self.assertEqual(len(alice_channel.current_commitment(REMOTE).outputs()), 3) + self.assertEqual(len(com().outputs()), 2) + self.assertEqual(len(alice_channel.force_close_tx().outputs()), 2) + + self.assertEqual(alice_channel.hm.log.keys(), set([LOCAL, REMOTE])) + self.assertEqual(len(alice_channel.hm.log[LOCAL]['adds']), 1) + alice_channel.serialize() + + self.assertEqual(alice_channel.pending_commitment(LOCAL).outputs(), + bob_channel.pending_commitment(REMOTE).outputs()) + + # Alice then processes bob's signature, and since she just received + # the revocation, she expect this signature to cover everything up to + # the point where she sent her signature, including the HTLC. + alice_channel.receive_new_commitment(bobSig, bobHtlcSigs) + self.assertEqual(alice_channel.remote_commitment.outputs(), alice_channel.current_commitment(REMOTE).outputs()) + + self.assertEqual(len(alice_channel.current_commitment(REMOTE).outputs()), 3) + self.assertEqual(len(com().outputs()), 3) + self.assertEqual(len(alice_channel.force_close_tx().outputs()), 3) + + self.assertEqual(len(alice_channel.hm.log[LOCAL]['adds']), 1) + alice_channel.serialize() + + tx1 = str(alice_channel.force_close_tx()) + self.assertNotEqual(tx0, tx1) + + # Alice then generates a revocation for bob. + self.assertEqual(alice_channel.remote_commitment.outputs(), alice_channel.current_commitment(REMOTE).outputs()) + aliceRevocation, _ = alice_channel.revoke_current_commitment() + alice_channel.serialize() + #self.assertEqual(alice_channel.remote_commitment.outputs(), alice_channel.current_commitment(REMOTE).outputs()) + + tx2 = str(alice_channel.force_close_tx()) + # since alice already has the signature for the next one, it doesn't change her force close tx (it was already the newer one) + self.assertEqual(tx1, tx2) + + # Finally Bob processes Alice's revocation, at this point the new HTLC + # is fully locked in within both commitment transactions. Bob should + # also be able to forward an HTLC now that the HTLC has been locked + # into both commitment transactions. + self.assertTrue(bob_channel.signature_fits(bob_channel.current_commitment(LOCAL))) + bob_channel.receive_revocation(aliceRevocation) + bob_channel.serialize() + + # At this point, both sides should have the proper number of satoshis + # sent, and commitment height updated within their local channel + # state. + aliceSent = 0 + bobSent = 0 + + self.assertEqual(alice_channel.total_msat(SENT), aliceSent, "alice has incorrect milli-satoshis sent") + self.assertEqual(alice_channel.total_msat(RECEIVED), bobSent, "alice has incorrect milli-satoshis received") + self.assertEqual(bob_channel.total_msat(SENT), bobSent, "bob has incorrect milli-satoshis sent") + self.assertEqual(bob_channel.total_msat(RECEIVED), aliceSent, "bob has incorrect milli-satoshis received") + self.assertEqual(bob_channel.config[LOCAL].ctn, 1, "bob has incorrect commitment height") + self.assertEqual(alice_channel.config[LOCAL].ctn, 1, "alice has incorrect commitment height") + + # Both commitment transactions should have three outputs, and one of + # them should be exactly the amount of the HTLC. + alice_ctx = alice_channel.pending_commitment(LOCAL) + bob_ctx = bob_channel.pending_commitment(LOCAL) + self.assertEqual(len(alice_ctx.outputs()), 3, "alice should have three commitment outputs, instead have %s"% len(alice_ctx.outputs())) + self.assertEqual(len(bob_ctx.outputs()), 3, "bob should have three commitment outputs, instead have %s"% len(bob_ctx.outputs())) + self.assertOutputExistsByValue(alice_ctx, htlc.amount_msat // 1000) + self.assertOutputExistsByValue(bob_ctx, htlc.amount_msat // 1000) + + # Now we'll repeat a similar exchange, this time with Bob settling the + # HTLC once he learns of the preimage. + preimage = self.paymentPreimage + bob_channel.settle_htlc(preimage, self.bobHtlcIndex) + + #self.assertEqual(alice_channel.remote_commitment.outputs(), alice_channel.current_commitment(REMOTE).outputs()) + alice_channel.receive_htlc_settle(preimage, self.aliceHtlcIndex) + + tx3 = str(alice_channel.force_close_tx()) + # just settling a htlc does not change her force close tx + self.assertEqual(tx2, tx3) + + bobSig2, bobHtlcSigs2 = bob_channel.sign_next_commitment() + self.assertEqual(len(bobHtlcSigs2), 0) + + self.assertEqual(alice_channel.hm.htlcs_by_direction(REMOTE, RECEIVED), [htlc]) + self.assertEqual(alice_channel.included_htlcs(REMOTE, RECEIVED, alice_channel.config[REMOTE].ctn), [htlc]) + self.assertEqual({1: [htlc], 2: []}, alice_channel.included_htlcs_in_their_latest_ctxs(LOCAL)) + self.assertEqual({1: [htlc], 2: []}, bob_channel.included_htlcs_in_their_latest_ctxs(REMOTE)) + self.assertEqual({1: [], 2: []}, alice_channel.included_htlcs_in_their_latest_ctxs(REMOTE)) + self.assertEqual({1: [], 2: []}, bob_channel.included_htlcs_in_their_latest_ctxs(LOCAL)) + + alice_ctx_bob_version = bob_channel.pending_commitment(REMOTE).outputs() + alice_ctx_alice_version = alice_channel.pending_commitment(LOCAL).outputs() + self.assertEqual(alice_ctx_alice_version, alice_ctx_bob_version) + + alice_channel.receive_new_commitment(bobSig2, bobHtlcSigs2) + + tx4 = str(alice_channel.force_close_tx()) + self.assertNotEqual(tx3, tx4) + + self.assertEqual(alice_channel.balance(LOCAL), 500000000000) + self.assertEqual(1, alice_channel.config[LOCAL].ctn) + self.assertEqual(len(alice_channel.included_htlcs(LOCAL, RECEIVED, ctn=2)), 0) + aliceRevocation2, _ = alice_channel.revoke_current_commitment() + alice_channel.serialize() + aliceSig2, aliceHtlcSigs2 = alice_channel.sign_next_commitment() + self.assertEqual(aliceHtlcSigs2, [], "alice should generate no htlc signatures") + self.assertEqual(len(bob_channel.current_commitment(LOCAL).outputs()), 3) + self.assertEqual(len(bob_channel.pending_commitment(LOCAL).outputs()), 2) + received, sent = bob_channel.receive_revocation(aliceRevocation2) + bob_channel.serialize() + self.assertEqual(received, one_bitcoin_in_msat) + + bob_channel.receive_new_commitment(aliceSig2, aliceHtlcSigs2) + + bobRevocation2, _ = bob_channel.revoke_current_commitment() + bob_channel.serialize() + alice_channel.receive_revocation(bobRevocation2) + alice_channel.serialize() + + # At this point, Bob should have 6 BTC settled, with Alice still having + # 4 BTC. Alice's channel should show 1 BTC sent and Bob's channel + # should show 1 BTC received. They should also be at commitment height + # two, with the revocation window extended by 1 (5). + mSatTransferred = one_bitcoin_in_msat + self.assertEqual(alice_channel.total_msat(SENT), mSatTransferred, "alice satoshis sent incorrect") + self.assertEqual(alice_channel.total_msat(RECEIVED), 0, "alice satoshis received incorrect") + self.assertEqual(bob_channel.total_msat(RECEIVED), mSatTransferred, "bob satoshis received incorrect") + self.assertEqual(bob_channel.total_msat(SENT), 0, "bob satoshis sent incorrect") + self.assertEqual(bob_channel.current_height[LOCAL], 2, "bob has incorrect commitment height") + self.assertEqual(alice_channel.current_height[LOCAL], 2, "alice has incorrect commitment height") + + self.assertEqual(self.bob_pending_remote_balance, self.alice_channel.balance(LOCAL)) + + alice_channel.update_fee(100000, True) + alice_outputs = alice_channel.pending_commitment(REMOTE).outputs() + old_outputs = bob_channel.pending_commitment(LOCAL).outputs() + bob_channel.update_fee(100000, False) + new_outputs = bob_channel.pending_commitment(LOCAL).outputs() + self.assertNotEqual(old_outputs, new_outputs) + self.assertEqual(alice_outputs, new_outputs) + + tx5 = str(alice_channel.force_close_tx()) + # sending a fee update does not change her force close tx + self.assertEqual(tx4, tx5) + + force_state_transition(alice_channel, bob_channel) + + tx6 = str(alice_channel.force_close_tx()) + self.assertNotEqual(tx5, tx6) + + self.htlc_dict['amount_msat'] *= 5 + bob_index = bob_channel.add_htlc(self.htlc_dict) + alice_index = alice_channel.receive_htlc(self.htlc_dict) + + bob_channel.pending_commitment(REMOTE) + alice_channel.pending_commitment(LOCAL) + + alice_channel.pending_commitment(REMOTE) + bob_channel.pending_commitment(LOCAL) + + force_state_transition(bob_channel, alice_channel) + alice_channel.settle_htlc(self.paymentPreimage, alice_index) + bob_channel.receive_htlc_settle(self.paymentPreimage, bob_index) + force_state_transition(bob_channel, alice_channel) + self.assertEqual(alice_channel.total_msat(SENT), one_bitcoin_in_msat, "alice satoshis sent incorrect") + self.assertEqual(alice_channel.total_msat(RECEIVED), 5 * one_bitcoin_in_msat, "alice satoshis received incorrect") + self.assertEqual(bob_channel.total_msat(RECEIVED), one_bitcoin_in_msat, "bob satoshis received incorrect") + self.assertEqual(bob_channel.total_msat(SENT), 5 * one_bitcoin_in_msat, "bob satoshis sent incorrect") + + alice_channel.serialize() + + + def alice_to_bob_fee_update(self, fee=111): + aoldctx = self.alice_channel.pending_commitment(REMOTE).outputs() + self.alice_channel.update_fee(fee, True) + anewctx = self.alice_channel.pending_commitment(REMOTE).outputs() + self.assertNotEqual(aoldctx, anewctx) + boldctx = self.bob_channel.pending_commitment(LOCAL).outputs() + self.bob_channel.update_fee(fee, False) + bnewctx = self.bob_channel.pending_commitment(LOCAL).outputs() + self.assertNotEqual(boldctx, bnewctx) + self.assertEqual(anewctx, bnewctx) + return fee + + def test_UpdateFeeSenderCommits(self): + old_feerate = self.alice_channel.pending_feerate(LOCAL) + fee = self.alice_to_bob_fee_update() + + alice_channel, bob_channel = self.alice_channel, self.bob_channel + + self.assertEqual(self.alice_channel.pending_feerate(LOCAL), old_feerate) + alice_sig, alice_htlc_sigs = alice_channel.sign_next_commitment() + self.assertEqual(self.alice_channel.pending_feerate(LOCAL), old_feerate) + + bob_channel.receive_new_commitment(alice_sig, alice_htlc_sigs) + + self.assertNotEqual(fee, bob_channel.constraints.feerate) + rev, _ = bob_channel.revoke_current_commitment() + self.assertEqual(fee, bob_channel.constraints.feerate) + + bob_sig, bob_htlc_sigs = bob_channel.sign_next_commitment() + alice_channel.receive_revocation(rev) + alice_channel.receive_new_commitment(bob_sig, bob_htlc_sigs) + + self.assertNotEqual(fee, alice_channel.constraints.feerate) + rev, _ = alice_channel.revoke_current_commitment() + self.assertEqual(fee, alice_channel.constraints.feerate) + + bob_channel.receive_revocation(rev) + self.assertEqual(fee, bob_channel.constraints.feerate) + + + def test_UpdateFeeReceiverCommits(self): + fee = self.alice_to_bob_fee_update() + + alice_channel, bob_channel = self.alice_channel, self.bob_channel + + bob_sig, bob_htlc_sigs = bob_channel.sign_next_commitment() + alice_channel.receive_new_commitment(bob_sig, bob_htlc_sigs) + + alice_revocation, _ = alice_channel.revoke_current_commitment() + bob_channel.receive_revocation(alice_revocation) + alice_sig, alice_htlc_sigs = alice_channel.sign_next_commitment() + bob_channel.receive_new_commitment(alice_sig, alice_htlc_sigs) + + self.assertNotEqual(fee, bob_channel.constraints.feerate) + bob_revocation, _ = bob_channel.revoke_current_commitment() + self.assertEqual(fee, bob_channel.constraints.feerate) + + bob_sig, bob_htlc_sigs = bob_channel.sign_next_commitment() + alice_channel.receive_revocation(bob_revocation) + alice_channel.receive_new_commitment(bob_sig, bob_htlc_sigs) + + self.assertNotEqual(fee, alice_channel.constraints.feerate) + alice_revocation, _ = alice_channel.revoke_current_commitment() + self.assertEqual(fee, alice_channel.constraints.feerate) + + bob_channel.receive_revocation(alice_revocation) + self.assertEqual(fee, bob_channel.constraints.feerate) + + def test_AddHTLCNegativeBalance(self): + # the test in lnd doesn't set the fee to zero. + # probably lnd subtracts commitment fee after deciding weather + # an htlc can be added. so we set the fee to zero so that + # the test can work. + self.alice_to_bob_fee_update(0) + force_state_transition(self.alice_channel, self.bob_channel) + + self.htlc_dict['payment_hash'] = bitcoin.sha256(32 * b'\x02') + self.alice_channel.add_htlc(self.htlc_dict) + self.htlc_dict['payment_hash'] = bitcoin.sha256(32 * b'\x03') + self.alice_channel.add_htlc(self.htlc_dict) + # now there are three htlcs (one was in setUp) + + # Alice now has an available balance of 2 BTC. We'll add a new HTLC of + # value 2 BTC, which should make Alice's balance negative (since she + # has to pay a commitment fee). + new = dict(self.htlc_dict) + new['amount_msat'] *= 2.5 + new['payment_hash'] = bitcoin.sha256(32 * b'\x04') + with self.assertRaises(lnutil.PaymentFailure) as cm: + self.alice_channel.add_htlc(new) + self.assertIn('Not enough local balance', cm.exception.args[0]) + + def test_sign_commitment_is_pure(self): + force_state_transition(self.alice_channel, self.bob_channel) + self.htlc_dict['payment_hash'] = bitcoin.sha256(b'\x02' * 32) + aliceHtlcIndex = self.alice_channel.add_htlc(self.htlc_dict) + before_signing = self.alice_channel.to_save() + self.alice_channel.sign_next_commitment() + after_signing = self.alice_channel.to_save() + try: + self.assertEqual(before_signing, after_signing) + except: + try: + from deepdiff import DeepDiff + except ImportError: + raise + raise Exception(pformat(DeepDiff(before_signing, after_signing))) + +class TestAvailableToSpend(unittest.TestCase): + def test_DesyncHTLCs(self): + alice_channel, bob_channel = create_test_channels() + + paymentPreimage = b"\x01" * 32 + paymentHash = bitcoin.sha256(paymentPreimage) + htlc_dict = { + 'payment_hash' : paymentHash, + 'amount_msat' : int(4.1 * one_bitcoin_in_msat), + 'cltv_expiry' : 5, + } + + alice_idx = alice_channel.add_htlc(htlc_dict) + bob_idx = bob_channel.receive_htlc(htlc_dict) + force_state_transition(alice_channel, bob_channel) + bob_channel.fail_htlc(bob_idx) + alice_channel.receive_fail_htlc(alice_idx) + # Alice now has gotten all her original balance (5 BTC) back, however, + # adding a new HTLC at this point SHOULD fail, since if she adds the + # HTLC and signs the next state, Bob cannot assume she received the + # FailHTLC, and must assume she doesn't have the necessary balance + # available. + # We try adding an HTLC of value 1 BTC, which should fail because the + # balance is unavailable. + htlc_dict = { + 'payment_hash' : paymentHash, + 'amount_msat' : one_bitcoin_in_msat, + 'cltv_expiry' : 5, + } + with self.assertRaises(lnutil.PaymentFailure): + alice_channel.add_htlc(htlc_dict) + # Now do a state transition, which will ACK the FailHTLC, making Alice + # able to add the new HTLC. + force_state_transition(alice_channel, bob_channel) + alice_channel.add_htlc(htlc_dict) + +class TestChanReserve(unittest.TestCase): + def setUp(self): + alice_channel, bob_channel = create_test_channels() + alice_min_reserve = int(.5 * one_bitcoin_in_msat // 1000) + # We set Bob's channel reserve to a value that is larger than + # his current balance in the channel. This will ensure that + # after a channel is first opened, Bob can still receive HTLCs + # even though his balance is less than his channel reserve. + bob_min_reserve = 6 * one_bitcoin_in_msat // 1000 + # bob min reserve was decided by alice, but applies to bob + + alice_channel.config[LOCAL] =\ + alice_channel.config[LOCAL]._replace(reserve_sat=bob_min_reserve) + alice_channel.config[REMOTE] =\ + alice_channel.config[REMOTE]._replace(reserve_sat=alice_min_reserve) + + bob_channel.config[LOCAL] =\ + bob_channel.config[LOCAL]._replace(reserve_sat=alice_min_reserve) + bob_channel.config[REMOTE] =\ + bob_channel.config[REMOTE]._replace(reserve_sat=bob_min_reserve) + + self.alice_channel = alice_channel + self.bob_channel = bob_channel + + def test_part1(self): + # Add an HTLC that will increase Bob's balance. This should succeed, + # since Alice stays above her channel reserve, and Bob increases his + # balance (while still being below his channel reserve). + # + # Resulting balances: + # Alice: 4.5 + # Bob: 5.0 + paymentPreimage = b"\x01" * 32 + paymentHash = bitcoin.sha256(paymentPreimage) + htlc_dict = { + 'payment_hash' : paymentHash, + 'amount_msat' : int(.5 * one_bitcoin_in_msat), + 'cltv_expiry' : 5, + } + self.alice_channel.add_htlc(htlc_dict) + self.bob_channel.receive_htlc(htlc_dict) + # Force a state transition, making sure this HTLC is considered valid + # even though the channel reserves are not met. + force_state_transition(self.alice_channel, self.bob_channel) + + aliceSelfBalance = self.alice_channel.balance(LOCAL)\ + - lnchannel.htlcsum(self.alice_channel.hm.htlcs_by_direction(LOCAL, SENT)) + bobBalance = self.bob_channel.balance(REMOTE)\ + - lnchannel.htlcsum(self.alice_channel.hm.htlcs_by_direction(REMOTE, SENT)) + self.assertEqual(aliceSelfBalance, one_bitcoin_in_msat*4.5) + self.assertEqual(bobBalance, one_bitcoin_in_msat*5) + # Now let Bob try to add an HTLC. This should fail, since it will + # decrease his balance, which is already below the channel reserve. + # + # Resulting balances: + # Alice: 4.5 + # Bob: 5.0 + with self.assertRaises(lnutil.PaymentFailure): + htlc_dict['payment_hash'] = bitcoin.sha256(32 * b'\x02') + self.bob_channel.add_htlc(htlc_dict) + with self.assertRaises(lnutil.RemoteMisbehaving): + self.alice_channel.receive_htlc(htlc_dict) + + def part2(self): + paymentPreimage = b"\x01" * 32 + paymentHash = bitcoin.sha256(paymentPreimage) + # Now we'll add HTLC of 3.5 BTC to Alice's commitment, this should put + # Alice's balance at 1.5 BTC. + # + # Resulting balances: + # Alice: 1.5 + # Bob: 9.5 + htlc_dict = { + 'payment_hash' : paymentHash, + 'amount_msat' : int(3.5 * one_bitcoin_in_msat), + 'cltv_expiry' : 5, + } + self.alice_channel.add_htlc(htlc_dict) + self.bob_channel.receive_htlc(htlc_dict) + # Add a second HTLC of 1 BTC. This should fail because it will take + # Alice's balance all the way down to her channel reserve, but since + # she is the initiator the additional transaction fee makes her + # balance dip below. + htlc_dict['amount_msat'] = one_bitcoin_in_msat + with self.assertRaises(lnutil.PaymentFailure): + self.alice_channel.add_htlc(htlc_dict) + with self.assertRaises(lnutil.RemoteMisbehaving): + self.bob_channel.receive_htlc(htlc_dict) + + def part3(self): + # Add a HTLC of 2 BTC to Alice, and the settle it. + # Resulting balances: + # Alice: 3.0 + # Bob: 7.0 + htlc_dict = { + 'payment_hash' : paymentHash, + 'amount_msat' : int(2 * one_bitcoin_in_msat), + 'cltv_expiry' : 5, + } + alice_idx = self.alice_channel.add_htlc(htlc_dict) + bob_idx = self.bob_channel.receive_htlc(htlc_dict) + force_state_transition(self.alice_channel, self.bob_channel) + self.check_bals(one_bitcoin_in_msat*3\ + - self.alice_channel.pending_local_fee(), + one_bitcoin_in_msat*5) + self.bob_channel.settle_htlc(paymentPreimage, bob_idx) + self.alice_channel.receive_htlc_settle(paymentPreimage, alice_idx) + force_state_transition(self.alice_channel, self.bob_channel) + self.check_bals(one_bitcoin_in_msat*3\ + - self.alice_channel.pending_local_fee(), + one_bitcoin_in_msat*7) + # And now let Bob add an HTLC of 1 BTC. This will take Bob's balance + # all the way down to his channel reserve, but since he is not paying + # the fee this is okay. + htlc_dict['amount_msat'] = one_bitcoin_in_msat + self.bob_channel.add_htlc(htlc_dict) + self.alice_channel.receive_htlc(htlc_dict) + force_state_transition(self.alice_channel, self.bob_channel) + self.check_bals(one_bitcoin_in_msat*3\ + - self.alice_channel.pending_local_fee(), + one_bitcoin_in_msat*6) + + def check_bals(self, amt1, amt2): + self.assertEqual(self.alice_channel.available_to_spend(LOCAL), amt1) + self.assertEqual(self.bob_channel.available_to_spend(REMOTE), amt1) + self.assertEqual(self.alice_channel.available_to_spend(REMOTE), amt2) + self.assertEqual(self.bob_channel.available_to_spend(LOCAL), amt2) + +class TestDust(unittest.TestCase): + def test_DustLimit(self): + alice_channel, bob_channel = create_test_channels() + + paymentPreimage = b"\x01" * 32 + paymentHash = bitcoin.sha256(paymentPreimage) + fee_per_kw = alice_channel.constraints.feerate + self.assertEqual(fee_per_kw, 6000) + htlcAmt = 500 + lnutil.HTLC_TIMEOUT_WEIGHT * (fee_per_kw // 1000) + self.assertEqual(htlcAmt, 4478) + htlc = { + 'payment_hash' : paymentHash, + 'amount_msat' : 1000 * htlcAmt, + 'cltv_expiry' : 5, # also in create_test_channels + } + + old_values = [x.value for x in bob_channel.current_commitment(LOCAL).outputs() ] + aliceHtlcIndex = alice_channel.add_htlc(htlc) + bobHtlcIndex = bob_channel.receive_htlc(htlc) + force_state_transition(alice_channel, bob_channel) + alice_ctx = alice_channel.current_commitment(LOCAL) + bob_ctx = bob_channel.current_commitment(LOCAL) + new_values = [x.value for x in bob_ctx.outputs() ] + self.assertNotEqual(old_values, new_values) + self.assertEqual(len(alice_ctx.outputs()), 3) + self.assertEqual(len(bob_ctx.outputs()), 2) + default_fee = calc_static_fee(0) + self.assertEqual(bob_channel.pending_local_fee(), default_fee + htlcAmt) + bob_channel.settle_htlc(paymentPreimage, bobHtlcIndex) + alice_channel.receive_htlc_settle(paymentPreimage, aliceHtlcIndex) + force_state_transition(bob_channel, alice_channel) + self.assertEqual(len(alice_channel.pending_commitment(LOCAL).outputs()), 2) + self.assertEqual(alice_channel.total_msat(SENT) // 1000, htlcAmt) + +def force_state_transition(chanA, chanB): + chanB.receive_new_commitment(*chanA.sign_next_commitment()) + rev, _ = chanB.revoke_current_commitment() + bob_sig, bob_htlc_sigs = chanB.sign_next_commitment() + chanA.receive_revocation(rev) + chanA.receive_new_commitment(bob_sig, bob_htlc_sigs) + chanB.receive_revocation(chanA.revoke_current_commitment()[0]) + +# calcStaticFee calculates appropriate fees for commitment transactions. This +# function provides a simple way to allow test balance assertions to take fee +# calculations into account. +def calc_static_fee(numHTLCs): + commitWeight = 724 + htlcWeight = 172 + feePerKw = 24//4 * 1000 + return feePerKw * (commitWeight + htlcWeight*numHTLCs) // 1000 diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py @@ -0,0 +1,252 @@ +import unittest +import asyncio +import tempfile +from decimal import Decimal +import os +from contextlib import contextmanager +from collections import defaultdict + +from electrum.network import Network +from electrum.ecc import ECPrivkey +from electrum import simple_config, lnutil +from electrum.lnaddr import lnencode, LnAddr, lndecode +from electrum.bitcoin import COIN, sha256 +from electrum.util import bh2u + +from electrum.lnpeer import Peer +from electrum.lnutil import LNPeerAddr, Keypair, privkey_to_pubkey +from electrum.lnutil import LightningPeerConnectionClosed, RemoteMisbehaving +from electrum.lnutil import PaymentFailure +from electrum.lnrouter import ChannelDB, LNPathFinder +from electrum.lnworker import LNWorker +from electrum.lnmsg import encode_msg, decode_msg + +from .test_lnchannel import create_test_channels + +def keypair(): + priv = ECPrivkey.generate_random_key().get_secret_bytes() + k1 = Keypair( + pubkey=privkey_to_pubkey(priv), + privkey=priv) + return k1 + +@contextmanager +def noop_lock(): + yield + +class MockNetwork: + def __init__(self, tx_queue): + self.callbacks = defaultdict(list) + self.lnwatcher = None + user_config = {} + user_dir = tempfile.mkdtemp(prefix="electrum-lnpeer-test-") + self.config = simple_config.SimpleConfig(user_config, read_user_dir_function=lambda: user_dir) + self.asyncio_loop = asyncio.get_event_loop() + self.channel_db = ChannelDB(self) + self.interface = None + self.path_finder = LNPathFinder(self.channel_db) + self.tx_queue = tx_queue + + @property + def callback_lock(self): + return noop_lock() + + register_callback = Network.register_callback + unregister_callback = Network.unregister_callback + trigger_callback = Network.trigger_callback + + def get_local_height(self): + return 0 + + async def broadcast_transaction(self, tx): + if self.tx_queue: + await self.tx_queue.put(tx) + +class MockStorage: + def put(self, key, value): + pass + + def get(self, key, default=None): + pass + + def write(self): + pass + +class MockWallet: + storage = MockStorage() + +class MockLNWorker: + def __init__(self, remote_keypair, local_keypair, chan, tx_queue): + self.chan = chan + self.remote_keypair = remote_keypair + self.node_keypair = local_keypair + self.network = MockNetwork(tx_queue) + self.channels = {self.chan.channel_id: self.chan} + self.invoices = {} + self.inflight = {} + self.wallet = MockWallet() + + @property + def lock(self): + return noop_lock() + + @property + def peers(self): + return {self.remote_keypair.pubkey: self.peer} + + def channels_for_peer(self, pubkey): + return self.channels + + def get_channel_by_short_id(self, short_channel_id): + with self.lock: + for chan in self.channels.values(): + if chan.short_channel_id == short_channel_id: + return chan + + def save_channel(self, chan): + print("Ignoring channel save") + + def on_channels_updated(self): + pass + + def save_invoice(*args): + pass + + get_invoice = LNWorker.get_invoice + _create_route_from_invoice = LNWorker._create_route_from_invoice + _check_invoice = staticmethod(LNWorker._check_invoice) + _pay_to_route = LNWorker._pay_to_route + force_close_channel = LNWorker.force_close_channel + get_first_timestamp = lambda self: 0 + +class MockTransport: + def __init__(self): + self.queue = asyncio.Queue() + + def name(self): + return "" + + async def read_messages(self): + while True: + yield await self.queue.get() + +class NoFeaturesTransport(MockTransport): + """ + This answers the init message with a init that doesn't signal any features. + Used for testing that we require DATA_LOSS_PROTECT. + """ + def send_bytes(self, data): + decoded = decode_msg(data) + print(decoded) + if decoded[0] == 'init': + self.queue.put_nowait(encode_msg('init', lflen=1, gflen=1, localfeatures=b"\x00", globalfeatures=b"\x00")) + +class PutIntoOthersQueueTransport(MockTransport): + def __init__(self): + super().__init__() + self.other_mock_transport = None + + def send_bytes(self, data): + self.other_mock_transport.queue.put_nowait(data) + +def transport_pair(): + t1 = PutIntoOthersQueueTransport() + t2 = PutIntoOthersQueueTransport() + t1.other_mock_transport = t2 + t2.other_mock_transport = t1 + return t1, t2 + +class TestPeer(unittest.TestCase): + def setUp(self): + self.alice_channel, self.bob_channel = create_test_channels() + + def test_require_data_loss_protect(self): + mock_lnworker = MockLNWorker(keypair(), keypair(), self.alice_channel, tx_queue=None) + mock_transport = NoFeaturesTransport() + p1 = Peer(mock_lnworker, b"\x00" * 33, mock_transport, request_initial_sync=False) + mock_lnworker.peer = p1 + with self.assertRaises(LightningPeerConnectionClosed): + run(asyncio.wait_for(p1._main_loop(), 1)) + + def prepare_peers(self): + k1, k2 = keypair(), keypair() + t1, t2 = transport_pair() + q1, q2 = asyncio.Queue(), asyncio.Queue() + w1 = MockLNWorker(k1, k2, self.alice_channel, tx_queue=q1) + w2 = MockLNWorker(k2, k1, self.bob_channel, tx_queue=q2) + p1 = Peer(w1, k1.pubkey, t1, request_initial_sync=False) + p2 = Peer(w2, k2.pubkey, t2, request_initial_sync=False) + w1.peer = p1 + w2.peer = p2 + # mark_open won't work if state is already OPEN. + # so set it to OPENING + self.alice_channel.set_state("OPENING") + self.bob_channel.set_state("OPENING") + # this populates the channel graph: + p1.mark_open(self.alice_channel) + p2.mark_open(self.bob_channel) + return p1, p2, w1, w2, q1, q2 + + @staticmethod + def prepare_invoice(w2 # receiver + ): + amount_btc = 100000/Decimal(COIN) + payment_preimage = os.urandom(32) + RHASH = sha256(payment_preimage) + addr = LnAddr( + RHASH, + amount_btc, + tags=[('c', lnutil.MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE), + ('d', 'coffee') + ]) + pay_req = lnencode(addr, w2.node_keypair.privkey) + w2.invoices[bh2u(RHASH)] = (bh2u(payment_preimage), pay_req, True, None) + return pay_req + + @staticmethod + def prepare_ln_message_future(w2 # receiver + ): + fut = asyncio.Future() + def evt_set(event, _lnworker, msg, _htlc_id): + fut.set_result(msg) + w2.network.register_callback(evt_set, ['ln_message']) + return fut + + def test_payment(self): + p1, p2, w1, w2, _q1, _q2 = self.prepare_peers() + pay_req = self.prepare_invoice(w2) + fut = self.prepare_ln_message_future(w2) + + async def pay(): + addr, peer, coro = LNWorker._pay(w1, pay_req) + await coro + print("HTLC ADDED") + self.assertEqual(await fut, 'Payment received') + gath.cancel() + gath = asyncio.gather(pay(), p1._main_loop(), p2._main_loop()) + with self.assertRaises(asyncio.CancelledError): + run(gath) + + def test_channel_usage_after_closing(self): + p1, p2, w1, w2, q1, q2 = self.prepare_peers() + pay_req = self.prepare_invoice(w2) + + addr = w1._check_invoice(pay_req) + route = w1._create_route_from_invoice(decoded_invoice=addr) + + run(w1.force_close_channel(self.alice_channel.channel_id)) + # check if a tx (commitment transaction) was broadcasted: + assert q1.qsize() == 1 + + with self.assertRaises(PaymentFailure) as e: + w1._create_route_from_invoice(decoded_invoice=addr) + self.assertEqual(str(e.exception), 'No path found') + + peer = w1.peers[route[0].node_id] + # AssertionError is ok since we shouldn't use old routes, and the + # route finding should fail when channel is closed + with self.assertRaises(AssertionError): + run(asyncio.gather(w1._pay_to_route(route, addr, pay_req), p1._main_loop(), p2._main_loop())) + +def run(coro): + asyncio.get_event_loop().run_until_complete(coro)