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:
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)