Browse Source

lnworker/lnpeer: add some type hints, force some kwargs

patch-4
SomberNight 4 years ago
parent
commit
691ebaf4f8
No known key found for this signature in database GPG Key ID: B33B5F232C6271E9
  1. 7
      electrum/lnonion.py
  2. 55
      electrum/lnpeer.py
  3. 5
      electrum/lnrater.py
  4. 150
      electrum/lnworker.py
  5. 8
      electrum/tests/test_lnpeer.py

7
electrum/lnonion.py

@ -437,9 +437,12 @@ class OnionRoutingFailure(Exception):
return str(self.code.name) return str(self.code.name)
return f"Unknown error ({self.code!r})" return f"Unknown error ({self.code!r})"
def construct_onion_error(reason: OnionRoutingFailure,
def construct_onion_error(
reason: OnionRoutingFailure,
onion_packet: OnionPacket, onion_packet: OnionPacket,
our_onion_private_key: bytes) -> bytes: our_onion_private_key: bytes,
) -> bytes:
# create payload # create payload
failure_msg = reason.to_bytes() failure_msg = reason.to_bytes()
failure_len = len(failure_msg) failure_len = len(failure_msg)

55
electrum/lnpeer.py

@ -1373,9 +1373,12 @@ class Peer(Logger):
chan.receive_htlc(htlc, onion_packet) chan.receive_htlc(htlc, onion_packet)
util.trigger_callback('htlc_added', chan, htlc, RECEIVED) util.trigger_callback('htlc_added', chan, htlc, RECEIVED)
def maybe_forward_htlc(self, chan: Channel, htlc: UpdateAddHtlc, *, def maybe_forward_htlc(
onion_packet: OnionPacket, processed_onion: ProcessedOnionPacket self,
) -> Tuple[Optional[bytes], Optional[int], Optional[OnionRoutingFailure]]: *,
htlc: UpdateAddHtlc,
processed_onion: ProcessedOnionPacket,
) -> Tuple[bytes, int]:
# Forward HTLC # Forward HTLC
# FIXME: there are critical safety checks MISSING here # FIXME: there are critical safety checks MISSING here
forwarding_enabled = self.network.config.get('lightning_forward_payments', False) forwarding_enabled = self.network.config.get('lightning_forward_payments', False)
@ -1662,7 +1665,7 @@ class Peer(Logger):
self.shutdown_received[chan_id] = asyncio.Future() self.shutdown_received[chan_id] = asyncio.Future()
await self.send_shutdown(chan) await self.send_shutdown(chan)
payload = await self.shutdown_received[chan_id] payload = await self.shutdown_received[chan_id]
txid = await self._shutdown(chan, payload, True) txid = await self._shutdown(chan, payload, is_local=True)
self.logger.info(f'({chan.get_id_for_log()}) Channel closed {txid}') self.logger.info(f'({chan.get_id_for_log()}) Channel closed {txid}')
return txid return txid
@ -1686,10 +1689,10 @@ class Peer(Logger):
else: else:
chan = self.channels[chan_id] chan = self.channels[chan_id]
await self.send_shutdown(chan) await self.send_shutdown(chan)
txid = await self._shutdown(chan, payload, False) txid = await self._shutdown(chan, payload, is_local=False)
self.logger.info(f'({chan.get_id_for_log()}) Channel closed by remote peer {txid}') self.logger.info(f'({chan.get_id_for_log()}) Channel closed by remote peer {txid}')
def can_send_shutdown(self, chan): def can_send_shutdown(self, chan: Channel):
if chan.get_state() >= ChannelState.OPENING: if chan.get_state() >= ChannelState.OPENING:
return True return True
if chan.constraints.is_initiator and chan.channel_id in self.funding_created_sent: if chan.constraints.is_initiator and chan.channel_id in self.funding_created_sent:
@ -1718,7 +1721,7 @@ class Peer(Logger):
chan.set_can_send_ctx_updates(True) chan.set_can_send_ctx_updates(True)
@log_exceptions @log_exceptions
async def _shutdown(self, chan: Channel, payload, is_local): async def _shutdown(self, chan: Channel, payload, *, is_local: bool):
# wait until no HTLCs remain in either commitment transaction # wait until no HTLCs remain in either commitment transaction
while len(chan.hm.htlcs(LOCAL)) + len(chan.hm.htlcs(REMOTE)) > 0: while len(chan.hm.htlcs(LOCAL)) + len(chan.hm.htlcs(REMOTE)) > 0:
self.logger.info(f'(chan: {chan.short_channel_id}) waiting for htlcs to settle...') self.logger.info(f'(chan: {chan.short_channel_id}) waiting for htlcs to settle...')
@ -1826,7 +1829,12 @@ class Peer(Logger):
error_reason = e error_reason = e
else: else:
try: try:
preimage, fw_info, error_bytes = self.process_unfulfilled_htlc(chan, htlc_id, htlc, forwarding_info, onion_packet_bytes, onion_packet) preimage, fw_info, error_bytes = self.process_unfulfilled_htlc(
chan=chan,
htlc=htlc,
forwarding_info=forwarding_info,
onion_packet_bytes=onion_packet_bytes,
onion_packet=onion_packet)
except OnionRoutingFailure as e: except OnionRoutingFailure as e:
error_bytes = construct_onion_error(e, onion_packet, our_onion_private_key=self.privkey) error_bytes = construct_onion_error(e, onion_packet, our_onion_private_key=self.privkey)
if fw_info: if fw_info:
@ -1850,13 +1858,24 @@ class Peer(Logger):
for htlc_id in done: for htlc_id in done:
unfulfilled.pop(htlc_id) unfulfilled.pop(htlc_id)
def process_unfulfilled_htlc(self, chan, htlc_id, htlc, forwarding_info, onion_packet_bytes, onion_packet): def process_unfulfilled_htlc(
self,
*,
chan: Channel,
htlc: UpdateAddHtlc,
forwarding_info: Tuple[str, int],
onion_packet_bytes: bytes,
onion_packet: OnionPacket,
) -> Tuple[Optional[bytes], Union[bool, None, Tuple[str, int]], Optional[bytes]]:
""" """
returns either preimage or fw_info or error_bytes or (None, None, None) returns either preimage or fw_info or error_bytes or (None, None, None)
raise an OnionRoutingFailure if we need to fail the htlc raise an OnionRoutingFailure if we need to fail the htlc
""" """
payment_hash = htlc.payment_hash payment_hash = htlc.payment_hash
processed_onion = self.process_onion_packet(onion_packet, payment_hash, onion_packet_bytes) processed_onion = self.process_onion_packet(
onion_packet,
payment_hash=payment_hash,
onion_packet_bytes=onion_packet_bytes)
if processed_onion.are_we_final: if processed_onion.are_we_final:
preimage = self.maybe_fulfill_htlc( preimage = self.maybe_fulfill_htlc(
chan=chan, chan=chan,
@ -1867,8 +1886,8 @@ class Peer(Logger):
if not forwarding_info: if not forwarding_info:
trampoline_onion = self.process_onion_packet( trampoline_onion = self.process_onion_packet(
processed_onion.trampoline_onion_packet, processed_onion.trampoline_onion_packet,
htlc.payment_hash, payment_hash=htlc.payment_hash,
onion_packet_bytes, onion_packet_bytes=onion_packet_bytes,
is_trampoline=True) is_trampoline=True)
if trampoline_onion.are_we_final: if trampoline_onion.are_we_final:
preimage = self.maybe_fulfill_htlc( preimage = self.maybe_fulfill_htlc(
@ -1892,11 +1911,8 @@ class Peer(Logger):
elif not forwarding_info: elif not forwarding_info:
next_chan_id, next_htlc_id = self.maybe_forward_htlc( next_chan_id, next_htlc_id = self.maybe_forward_htlc(
chan=chan,
htlc=htlc, htlc=htlc,
onion_packet=onion_packet,
processed_onion=processed_onion) processed_onion=processed_onion)
if next_chan_id:
fw_info = (next_chan_id.hex(), next_htlc_id) fw_info = (next_chan_id.hex(), next_htlc_id)
return None, fw_info, None return None, fw_info, None
else: else:
@ -1913,7 +1929,14 @@ class Peer(Logger):
return preimage, None, None return preimage, None, None
return None, None, None return None, None, None
def process_onion_packet(self, onion_packet, payment_hash, onion_packet_bytes, is_trampoline=False): def process_onion_packet(
self,
onion_packet: OnionPacket,
*,
payment_hash: bytes,
onion_packet_bytes: bytes,
is_trampoline: bool = False,
) -> ProcessedOnionPacket:
failure_data = sha256(onion_packet_bytes) failure_data = sha256(onion_packet_bytes)
try: try:
processed_onion = process_onion_packet( processed_onion = process_onion_packet(

5
electrum/lnrater.py

@ -268,7 +268,10 @@ class LNRater(Logger):
return pk, self._node_stats[pk] return pk, self._node_stats[pk]
def suggest_peer(self): def suggest_peer(self) -> Optional[bytes]:
"""Suggests a LN node to open a channel with.
Returns a node ID (pubkey).
"""
self.maybe_analyze_graph() self.maybe_analyze_graph()
if self._node_ratings: if self._node_ratings:
return self.suggest_node_channel_open()[0] return self.suggest_node_channel_open()[0]

150
electrum/lnworker.py

@ -7,7 +7,8 @@ import os
from decimal import Decimal from decimal import Decimal
import random import random
import time import time
from typing import Optional, Sequence, Tuple, List, Set, Dict, TYPE_CHECKING, NamedTuple, Union, Mapping, Any from typing import (Optional, Sequence, Tuple, List, Set, Dict, TYPE_CHECKING,
NamedTuple, Union, Mapping, Any, Iterable)
import threading import threading
import socket import socket
import aiohttp import aiohttp
@ -266,10 +267,10 @@ class LNWorker(Logger, NetworkRetryManager[LNPeerAddr]):
with self.lock: with self.lock:
return self._peers.copy() return self._peers.copy()
def channels_for_peer(self, node_id): def channels_for_peer(self, node_id: bytes) -> Dict[bytes, Channel]:
return {} return {}
def get_node_alias(self, node_id): def get_node_alias(self, node_id: bytes) -> str:
if self.channel_db: if self.channel_db:
node_info = self.channel_db.get_node_info_for_node_id(node_id) node_info = self.channel_db.get_node_info_for_node_id(node_id)
node_alias = (node_info.alias if node_info else '') or node_id.hex() node_alias = (node_info.alias if node_info else '') or node_id.hex()
@ -380,7 +381,7 @@ class LNWorker(Logger, NetworkRetryManager[LNPeerAddr]):
self._add_peer(host, int(port), bfh(pubkey)), self._add_peer(host, int(port), bfh(pubkey)),
self.network.asyncio_loop) self.network.asyncio_loop)
def is_good_peer(self, peer): def is_good_peer(self, peer: LNPeerAddr) -> bool:
# the purpose of this method is to filter peers that advertise the desired feature bits # the purpose of this method is to filter peers that advertise the desired feature bits
# it is disabled for now, because feature bits published in node announcements seem to be unreliable # it is disabled for now, because feature bits published in node announcements seem to be unreliable
return True return True
@ -566,7 +567,7 @@ class LNGossip(LNWorker):
self.channel_db.prune_orphaned_channels() self.channel_db.prune_orphaned_channels()
await asyncio.sleep(120) await asyncio.sleep(120)
async def add_new_ids(self, ids): async def add_new_ids(self, ids: Iterable[bytes]):
known = self.channel_db.get_channel_ids() known = self.channel_db.get_channel_ids()
new = set(ids) - set(known) new = set(ids) - set(known)
self.unknown_ids.update(new) self.unknown_ids.update(new)
@ -574,7 +575,7 @@ class LNGossip(LNWorker):
util.trigger_callback('gossip_peers', self.num_peers()) util.trigger_callback('gossip_peers', self.num_peers())
util.trigger_callback('ln_gossip_sync_progress') util.trigger_callback('ln_gossip_sync_progress')
def get_ids_to_query(self): def get_ids_to_query(self) -> Sequence[bytes]:
N = 500 N = 500
l = list(self.unknown_ids) l = list(self.unknown_ids)
self.unknown_ids = set(l[N:]) self.unknown_ids = set(l[N:])
@ -910,7 +911,7 @@ class LNWallet(LNWorker):
if chan.funding_outpoint.to_str() == txo: if chan.funding_outpoint.to_str() == txo:
return chan return chan
async def on_channel_update(self, chan): async def on_channel_update(self, chan: Channel):
if chan.get_state() == ChannelState.OPEN and chan.should_be_closed_due_to_expiring_htlcs(self.network.get_local_height()): if chan.get_state() == ChannelState.OPEN and chan.should_be_closed_due_to_expiring_htlcs(self.network.get_local_height()):
self.logger.info(f"force-closing due to expiring htlcs") self.logger.info(f"force-closing due to expiring htlcs")
@ -938,10 +939,14 @@ class LNWallet(LNWorker):
@log_exceptions @log_exceptions
async def _open_channel_coroutine( async def _open_channel_coroutine(
self, *, connect_str: str, self,
*,
connect_str: str,
funding_tx: PartialTransaction, funding_tx: PartialTransaction,
funding_sat: int, push_sat: int, funding_sat: int,
password: Optional[str]) -> Tuple[Channel, PartialTransaction]: push_sat: int,
password: Optional[str],
) -> Tuple[Channel, PartialTransaction]:
peer = await self.add_peer(connect_str) peer = await self.add_peer(connect_str)
coro = peer.channel_establishment_flow( coro = peer.channel_establishment_flow(
funding_tx=funding_tx, funding_tx=funding_tx,
@ -1006,7 +1011,7 @@ class LNWallet(LNWorker):
if chan.short_channel_id == short_channel_id: if chan.short_channel_id == short_channel_id:
return chan return chan
def create_routes_from_invoice(self, amount_msat, decoded_invoice, *, full_path=None): def create_routes_from_invoice(self, amount_msat: int, decoded_invoice: LnAddr, *, full_path=None):
return self.create_routes_for_payment( return self.create_routes_for_payment(
amount_msat=amount_msat, amount_msat=amount_msat,
invoice_pubkey=decoded_invoice.pubkey.serialize(), invoice_pubkey=decoded_invoice.pubkey.serialize(),
@ -1051,9 +1056,16 @@ class LNWallet(LNWorker):
util.trigger_callback('invoice_status', self.wallet, key) util.trigger_callback('invoice_status', self.wallet, key)
try: try:
await self.pay_to_node( await self.pay_to_node(
invoice_pubkey, payment_hash, payment_secret, amount_to_pay, node_pubkey=invoice_pubkey,
min_cltv_expiry, r_tags, t_tags, invoice_features, payment_hash=payment_hash,
attempts=attempts, full_path=full_path) payment_secret=payment_secret,
amount_to_pay=amount_to_pay,
min_cltv_expiry=min_cltv_expiry,
r_tags=r_tags,
t_tags=t_tags,
invoice_features=invoice_features,
attempts=attempts,
full_path=full_path)
success = True success = True
except PaymentFailure as e: except PaymentFailure as e:
self.logger.exception('') self.logger.exception('')
@ -1068,12 +1080,23 @@ class LNWallet(LNWorker):
log = self.logs[key] log = self.logs[key]
return success, log return success, log
async def pay_to_node( async def pay_to_node(
self, node_pubkey, payment_hash, payment_secret, amount_to_pay, self,
min_cltv_expiry, r_tags, t_tags, invoice_features, *, *,
attempts: int = 1, full_path: LNPaymentPath=None, node_pubkey: bytes,
trampoline_onion=None, trampoline_fee=None, trampoline_cltv_delta=None): payment_hash: bytes,
payment_secret: Optional[bytes],
amount_to_pay: int, # in msat
min_cltv_expiry: int,
r_tags,
t_tags,
invoice_features: int,
attempts: int = 1,
full_path: LNPaymentPath = None,
trampoline_onion=None,
trampoline_fee=None,
trampoline_cltv_delta=None,
) -> None:
if trampoline_onion: if trampoline_onion:
# todo: compare to the fee of the actual route we found # todo: compare to the fee of the actual route we found
@ -1095,7 +1118,14 @@ class LNWallet(LNWorker):
min_cltv_expiry, r_tags, t_tags, invoice_features, full_path=full_path)) min_cltv_expiry, r_tags, t_tags, invoice_features, full_path=full_path))
# 2. send htlcs # 2. send htlcs
for route, amount_msat in routes: for route, amount_msat in routes:
await self.pay_to_route(route, amount_msat, amount_to_pay, payment_hash, payment_secret, min_cltv_expiry, trampoline_onion) await self.pay_to_route(
route,
amount_msat=amount_msat,
total_msat=amount_to_pay,
payment_hash=payment_hash,
payment_secret=payment_secret,
min_cltv_expiry=min_cltv_expiry,
trampoline_onion=trampoline_onion)
amount_inflight += amount_msat amount_inflight += amount_msat
util.trigger_callback('invoice_status', self.wallet, payment_hash.hex()) util.trigger_callback('invoice_status', self.wallet, payment_hash.hex())
# 3. await a queue # 3. await a queue
@ -1111,9 +1141,17 @@ class LNWallet(LNWorker):
# if we get a channel update, we might retry the same route and amount # if we get a channel update, we might retry the same route and amount
self.handle_error_code_from_failed_htlc(htlc_log) self.handle_error_code_from_failed_htlc(htlc_log)
async def pay_to_route(self, route: LNPaymentRoute, amount_msat: int, async def pay_to_route(
total_msat: int, payment_hash: bytes, payment_secret: bytes, self,
min_cltv_expiry: int, trampoline_onion: bytes=None): route: LNPaymentRoute,
*,
amount_msat: int,
total_msat: int,
payment_hash: bytes,
payment_secret: Optional[bytes],
min_cltv_expiry: int,
trampoline_onion: bytes = None,
) -> None:
# send a single htlc # send a single htlc
short_channel_id = route[0].short_channel_id short_channel_id = route[0].short_channel_id
chan = self.get_channel_by_short_id(short_channel_id) chan = self.get_channel_by_short_id(short_channel_id)
@ -1267,7 +1305,7 @@ class LNWallet(LNWorker):
result.append(bitstring.BitArray(pubkey) + bitstring.BitArray(channel) + bitstring.pack('intbe:32', feebase) + bitstring.pack('intbe:32', feerate) + bitstring.pack('intbe:16', cltv)) result.append(bitstring.BitArray(pubkey) + bitstring.BitArray(channel) + bitstring.pack('intbe:32', feebase) + bitstring.pack('intbe:32', feerate) + bitstring.pack('intbe:16', cltv))
return result.tobytes() return result.tobytes()
def is_trampoline_peer(self, node_id): def is_trampoline_peer(self, node_id: bytes) -> bool:
# until trampoline is advertised in lnfeatures, check against hardcoded list # until trampoline is advertised in lnfeatures, check against hardcoded list
if is_hardcoded_trampoline(node_id): if is_hardcoded_trampoline(node_id):
return True return True
@ -1276,8 +1314,11 @@ class LNWallet(LNWorker):
return True return True
return False return False
def suggest_peer(self): def suggest_peer(self) -> Optional[bytes]:
return self.lnrater.suggest_peer() if self.channel_db else random.choice(list(hardcoded_trampoline_nodes().values())).pubkey if self.channel_db:
return self.lnrater.suggest_peer()
else:
return random.choice(list(hardcoded_trampoline_nodes().values())).pubkey
def create_trampoline_route( def create_trampoline_route(
self, amount_msat:int, self, amount_msat:int,
@ -1400,8 +1441,10 @@ class LNWallet(LNWorker):
invoice_pubkey, invoice_pubkey,
min_cltv_expiry, min_cltv_expiry,
r_tags, t_tags, r_tags, t_tags,
invoice_features, invoice_features: int,
*, full_path: LNPaymentPath = None) -> Sequence[Tuple[LNPaymentRoute, int]]: *,
full_path: LNPaymentPath = None,
) -> Sequence[Tuple[LNPaymentRoute, int]]:
"""Creates multiple routes for splitting a payment over the available """Creates multiple routes for splitting a payment over the available
private channels. private channels.
@ -1411,13 +1454,14 @@ class LNWallet(LNWorker):
# try to send over a single channel # try to send over a single channel
try: try:
routes = [self.create_route_for_payment( routes = [self.create_route_for_payment(
amount_msat, amount_msat=amount_msat,
invoice_pubkey, invoice_pubkey=invoice_pubkey,
min_cltv_expiry, min_cltv_expiry=min_cltv_expiry,
r_tags, t_tags, r_tags=r_tags,
invoice_features, t_tags=t_tags,
None, invoice_features=invoice_features,
full_path=full_path outgoing_channel=None,
full_path=full_path,
)] )]
except NoPathFound: except NoPathFound:
if not invoice_features.supports(LnFeatures.BASIC_MPP_OPT): if not invoice_features.supports(LnFeatures.BASIC_MPP_OPT):
@ -1439,12 +1483,13 @@ class LNWallet(LNWorker):
# its capacity. This could be dealt with by temporarily # its capacity. This could be dealt with by temporarily
# iteratively blacklisting channels for this mpp attempt. # iteratively blacklisting channels for this mpp attempt.
route, amt = self.create_route_for_payment( route, amt = self.create_route_for_payment(
part_amount_msat, amount_msat=part_amount_msat,
invoice_pubkey, invoice_pubkey=invoice_pubkey,
min_cltv_expiry, min_cltv_expiry=min_cltv_expiry,
r_tags, t_tags, r_tags=r_tags,
invoice_features, t_tags=t_tags,
channel, invoice_features=invoice_features,
outgoing_channel=channel,
full_path=None) full_path=None)
routes.append((route, amt)) routes.append((route, amt))
self.logger.info(f"found acceptable split configuration: {list(s[0].values())} rating: {s[1]}") self.logger.info(f"found acceptable split configuration: {list(s[0].values())} rating: {s[1]}")
@ -1457,13 +1502,16 @@ class LNWallet(LNWorker):
def create_route_for_payment( def create_route_for_payment(
self, self,
*,
amount_msat: int, amount_msat: int,
invoice_pubkey, invoice_pubkey: bytes,
min_cltv_expiry, min_cltv_expiry: int,
r_tags, t_tags, r_tags,
invoice_features, t_tags,
invoice_features: int,
outgoing_channel: Channel = None, outgoing_channel: Channel = None,
*, full_path: Optional[LNPaymentPath]) -> Tuple[LNPaymentRoute, int]: full_path: Optional[LNPaymentPath],
) -> Tuple[LNPaymentRoute, int]:
channels = [outgoing_channel] if outgoing_channel else list(self.channels.values()) channels = [outgoing_channel] if outgoing_channel else list(self.channels.values())
if not self.channel_db: if not self.channel_db:
@ -1554,7 +1602,13 @@ class LNWallet(LNWorker):
raise Exception(_("add invoice timed out")) raise Exception(_("add invoice timed out"))
@log_exceptions @log_exceptions
async def create_invoice(self, *, amount_msat: Optional[int], message, expiry: int): async def create_invoice(
self,
*,
amount_msat: Optional[int],
message,
expiry: int,
) -> Tuple[LnAddr, str]:
timestamp = int(time.time()) timestamp = int(time.time())
routing_hints = await self._calc_routing_hints_for_invoice(amount_msat) routing_hints = await self._calc_routing_hints_for_invoice(amount_msat)
if not routing_hints: if not routing_hints:
@ -1628,7 +1682,7 @@ class LNWallet(LNWorker):
self.payments[key] = info.amount_msat, info.direction, info.status self.payments[key] = info.amount_msat, info.direction, info.status
self.wallet.save_db() self.wallet.save_db()
def htlc_received(self, short_channel_id, htlc, expected_msat): def htlc_received(self, short_channel_id, htlc: UpdateAddHtlc, expected_msat: int):
status = self.get_payment_status(htlc.payment_hash) status = self.get_payment_status(htlc.payment_hash)
if status == PR_PAID: if status == PR_PAID:
return True, None return True, None

8
electrum/tests/test_lnpeer.py

@ -775,7 +775,13 @@ class TestPeer(ElectrumTestCase):
min_cltv_expiry = lnaddr.get_min_final_cltv_expiry() min_cltv_expiry = lnaddr.get_min_final_cltv_expiry()
payment_hash = lnaddr.paymenthash payment_hash = lnaddr.paymenthash
payment_secret = lnaddr.payment_secret payment_secret = lnaddr.payment_secret
pay = w1.pay_to_route(route, amount_msat, amount_msat, payment_hash, payment_secret, min_cltv_expiry) pay = w1.pay_to_route(
route,
amount_msat=amount_msat,
total_msat=amount_msat,
payment_hash=payment_hash,
payment_secret=payment_secret,
min_cltv_expiry=min_cltv_expiry)
await asyncio.gather(pay, p1._message_loop(), p2._message_loop(), p1.htlc_switch(), p2.htlc_switch()) await asyncio.gather(pay, p1._message_loop(), p2._message_loop(), p1.htlc_switch(), p2.htlc_switch())
with self.assertRaises(PaymentFailure): with self.assertRaises(PaymentFailure):
run(f()) run(f())

Loading…
Cancel
Save