Browse Source

lnworker: fix type error re pending_payments, and impl malformed htlcs

In old code, in lnpeer.htlc_switch(), "error" in lnworker.pending_payments
had incorrect type.

TODO: we need tests for payment failures...
hard-fail-on-bad-server-string
SomberNight 5 years ago
parent
commit
2cc76fbbbd
No known key found for this signature in database GPG Key ID: B33B5F232C6271E9
  1. 17
      electrum/lnchannel.py
  2. 51
      electrum/lnpeer.py
  3. 16
      electrum/lnutil.py
  4. 66
      electrum/lnworker.py
  5. 2
      electrum/tests/test_lnchannel.py

17
electrum/lnchannel.py

@ -50,7 +50,7 @@ from .lnutil import (Outpoint, LocalConfig, RemoteConfig, Keypair, OnlyPubkeyKey
HTLC_TIMEOUT_WEIGHT, HTLC_SUCCESS_WEIGHT, extract_ctn_from_tx_and_chan, UpdateAddHtlc, HTLC_TIMEOUT_WEIGHT, HTLC_SUCCESS_WEIGHT, extract_ctn_from_tx_and_chan, UpdateAddHtlc,
funding_output_script, SENT, RECEIVED, LOCAL, REMOTE, HTLCOwner, make_commitment_outputs, funding_output_script, SENT, RECEIVED, LOCAL, REMOTE, HTLCOwner, make_commitment_outputs,
ScriptHtlc, PaymentFailure, calc_onchain_fees, RemoteMisbehaving, make_htlc_output_witness_script, ScriptHtlc, PaymentFailure, calc_onchain_fees, RemoteMisbehaving, make_htlc_output_witness_script,
ShortChannelID, map_htlcs_to_ctx_output_idxs, LNPeerAddr) ShortChannelID, map_htlcs_to_ctx_output_idxs, LNPeerAddr, BarePaymentAttemptLog)
from .lnsweep import create_sweeptxs_for_our_ctx, create_sweeptxs_for_their_ctx from .lnsweep import create_sweeptxs_for_our_ctx, create_sweeptxs_for_their_ctx
from .lnsweep import create_sweeptx_for_their_revoked_htlc, SweepInfo from .lnsweep import create_sweeptx_for_their_revoked_htlc, SweepInfo
from .lnhtlc import HTLCManager from .lnhtlc import HTLCManager
@ -158,7 +158,7 @@ class Channel(Logger):
self._chan_ann_without_sigs = None # type: Optional[bytes] self._chan_ann_without_sigs = None # type: Optional[bytes]
self.revocation_store = RevocationStore(state["revocation_store"]) self.revocation_store = RevocationStore(state["revocation_store"])
self._can_send_ctx_updates = True # type: bool self._can_send_ctx_updates = True # type: bool
self._receive_fail_reasons = {} self._receive_fail_reasons = {} # type: Dict[int, BarePaymentAttemptLog]
def get_id_for_log(self) -> str: def get_id_for_log(self) -> str:
scid = self.short_channel_id scid = self.short_channel_id
@ -622,8 +622,8 @@ class Channel(Logger):
self.lnworker.payment_sent(self, htlc.payment_hash) self.lnworker.payment_sent(self, htlc.payment_hash)
failed = self.hm.failed_in_ctn(new_ctn) failed = self.hm.failed_in_ctn(new_ctn)
for htlc in failed: for htlc in failed:
reason = self._receive_fail_reasons.get(htlc.htlc_id) payment_attempt = self._receive_fail_reasons.get(htlc.htlc_id)
self.lnworker.payment_failed(self, htlc.payment_hash, reason) self.lnworker.payment_failed(self, htlc.payment_hash, payment_attempt)
def balance(self, whose: HTLCOwner, *, ctx_owner=HTLCOwner.LOCAL, ctn: int = None) -> int: def balance(self, whose: HTLCOwner, *, ctx_owner=HTLCOwner.LOCAL, ctn: int = None) -> int:
""" """
@ -793,11 +793,16 @@ class Channel(Logger):
with self.db_lock: with self.db_lock:
self.hm.send_fail(htlc_id) self.hm.send_fail(htlc_id)
def receive_fail_htlc(self, htlc_id: int, reason: bytes): def receive_fail_htlc(self, htlc_id: int, *,
error_bytes: Optional[bytes],
reason: Optional[OnionRoutingFailureMessage] = None):
self.logger.info("receive_fail_htlc") self.logger.info("receive_fail_htlc")
with self.db_lock: with self.db_lock:
self.hm.recv_fail(htlc_id) self.hm.recv_fail(htlc_id)
self._receive_fail_reasons[htlc_id] = reason self._receive_fail_reasons[htlc_id] = BarePaymentAttemptLog(success=False,
preimage=None,
error_bytes=error_bytes,
error_reason=reason)
def pending_local_fee(self): def pending_local_fee(self):
return self.constraints.capacity - sum(x.value for x in self.get_next_commitment(LOCAL).outputs()) return self.constraints.capacity - sum(x.value for x in self.get_next_commitment(LOCAL).outputs())

51
electrum/lnpeer.py

@ -1007,7 +1007,7 @@ class Peer(Logger):
htlc_id = int.from_bytes(payload["id"], "big") htlc_id = int.from_bytes(payload["id"], "big")
reason = payload["reason"] reason = payload["reason"]
self.logger.info(f"on_update_fail_htlc. chan {chan.short_channel_id}. htlc_id {htlc_id}") self.logger.info(f"on_update_fail_htlc. chan {chan.short_channel_id}. htlc_id {htlc_id}")
chan.receive_fail_htlc(htlc_id, reason) # TODO handle exc and maybe fail channel (e.g. bad htlc_id) chan.receive_fail_htlc(htlc_id, error_bytes=reason) # TODO handle exc and maybe fail channel (e.g. bad htlc_id)
self.maybe_send_commitment(chan) self.maybe_send_commitment(chan)
def maybe_send_commitment(self, chan: Channel): def maybe_send_commitment(self, chan: Channel):
@ -1093,10 +1093,9 @@ class Peer(Logger):
if failure_code & OnionFailureCodeMetaFlag.BADONION == 0: if failure_code & OnionFailureCodeMetaFlag.BADONION == 0:
asyncio.ensure_future(self.lnworker.try_force_closing(chan.channel_id)) asyncio.ensure_future(self.lnworker.try_force_closing(chan.channel_id))
raise RemoteMisbehaving(f"received update_fail_malformed_htlc with unexpected failure code: {failure_code}") raise RemoteMisbehaving(f"received update_fail_malformed_htlc with unexpected failure code: {failure_code}")
reason = b'' # TODO somehow propagate "failure_code" ? reason = OnionRoutingFailureMessage(code=failure_code, data=payload["sha256_of_onion"])
chan.receive_fail_htlc(htlc_id, reason) # TODO handle exc and maybe fail channel (e.g. bad htlc_id) chan.receive_fail_htlc(htlc_id, error_bytes=None, reason=reason)
self.maybe_send_commitment(chan) self.maybe_send_commitment(chan)
# TODO when forwarding, we need to propagate this "update_fail_malformed_htlc" downstream
def on_update_add_htlc(self, chan: Channel, payload): def on_update_add_htlc(self, chan: Channel, payload):
payment_hash = payload["payment_hash"] payment_hash = payload["payment_hash"]
@ -1222,20 +1221,22 @@ class Peer(Logger):
id=htlc_id, id=htlc_id,
payment_preimage=preimage) payment_preimage=preimage)
def fail_htlc(self, chan: Channel, htlc_id: int, onion_packet: Optional[OnionPacket], def fail_htlc(self, *, chan: Channel, htlc_id: int, onion_packet: Optional[OnionPacket],
reason: OnionRoutingFailureMessage): reason: Optional[OnionRoutingFailureMessage], error_bytes: Optional[bytes]):
self.logger.info(f"fail_htlc. chan {chan.short_channel_id}. htlc_id {htlc_id}. reason: {reason}") self.logger.info(f"fail_htlc. chan {chan.short_channel_id}. htlc_id {htlc_id}. reason: {reason}")
assert chan.can_send_ctx_updates(), f"cannot send updates: {chan.short_channel_id}" assert chan.can_send_ctx_updates(), f"cannot send updates: {chan.short_channel_id}"
chan.fail_htlc(htlc_id) chan.fail_htlc(htlc_id)
if onion_packet: if onion_packet:
error_packet = construct_onion_error(reason, onion_packet, our_onion_private_key=self.privkey) error_bytes = construct_onion_error(reason, onion_packet, our_onion_private_key=self.privkey)
if error_bytes:
self.send_message("update_fail_htlc", self.send_message("update_fail_htlc",
channel_id=chan.channel_id, channel_id=chan.channel_id,
id=htlc_id, id=htlc_id,
len=len(error_packet), len=len(error_bytes),
reason=error_packet) reason=error_bytes)
else: else:
assert len(reason.data) == 32, f"unexpected reason when sending 'update_fail_malformed_htlc': {reason!r}" if not (reason.code & OnionFailureCodeMetaFlag.BADONION and len(reason.data) == 32):
raise Exception(f"unexpected reason when sending 'update_fail_malformed_htlc': {reason!r}")
self.send_message("update_fail_malformed_htlc", self.send_message("update_fail_malformed_htlc",
channel_id=chan.channel_id, channel_id=chan.channel_id,
id=htlc_id, id=htlc_id,
@ -1425,7 +1426,8 @@ class Peer(Logger):
chan.logger.info(f'found unfulfilled htlc: {htlc_id}') chan.logger.info(f'found unfulfilled htlc: {htlc_id}')
htlc = chan.hm.log[REMOTE]['adds'][htlc_id] htlc = chan.hm.log[REMOTE]['adds'][htlc_id]
payment_hash = htlc.payment_hash payment_hash = htlc.payment_hash
error = None # type: Optional[OnionRoutingFailureMessage] error_reason = None # type: Optional[OnionRoutingFailureMessage]
error_bytes = None # type: Optional[bytes]
preimage = None preimage = None
onion_packet_bytes = bytes.fromhex(onion_packet_hex) onion_packet_bytes = bytes.fromhex(onion_packet_hex)
onion_packet = None onion_packet = None
@ -1433,36 +1435,45 @@ class Peer(Logger):
onion_packet = OnionPacket.from_bytes(onion_packet_bytes) onion_packet = OnionPacket.from_bytes(onion_packet_bytes)
processed_onion = process_onion_packet(onion_packet, associated_data=payment_hash, our_onion_private_key=self.privkey) processed_onion = process_onion_packet(onion_packet, associated_data=payment_hash, our_onion_private_key=self.privkey)
except UnsupportedOnionPacketVersion: except UnsupportedOnionPacketVersion:
error = OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_VERSION, data=sha256(onion_packet_bytes)) error_reason = OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_VERSION, data=sha256(onion_packet_bytes))
except InvalidOnionPubkey: except InvalidOnionPubkey:
error = OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_KEY, data=sha256(onion_packet_bytes)) error_reason = OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_KEY, data=sha256(onion_packet_bytes))
except InvalidOnionMac: except InvalidOnionMac:
error = OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_HMAC, data=sha256(onion_packet_bytes)) error_reason = OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_HMAC, data=sha256(onion_packet_bytes))
else: else:
if processed_onion.are_we_final: if processed_onion.are_we_final:
preimage, error = self.maybe_fulfill_htlc( preimage, error_reason = self.maybe_fulfill_htlc(
chan=chan, chan=chan,
htlc=htlc, htlc=htlc,
onion_packet=onion_packet, onion_packet=onion_packet,
processed_onion=processed_onion) processed_onion=processed_onion)
elif not forwarded: elif not forwarded:
error = self.maybe_forward_htlc( error_reason = self.maybe_forward_htlc(
chan=chan, chan=chan,
htlc=htlc, htlc=htlc,
onion_packet=onion_packet, onion_packet=onion_packet,
processed_onion=processed_onion) processed_onion=processed_onion)
if not error: if not error_reason:
unfulfilled[htlc_id] = local_ctn, remote_ctn, onion_packet_hex, True unfulfilled[htlc_id] = local_ctn, remote_ctn, onion_packet_hex, True
else: else:
# TODO self.lnworker.pending_payments is not persisted,
# so what happens if we restart the process?...
f = self.lnworker.pending_payments[payment_hash] f = self.lnworker.pending_payments[payment_hash]
if f.done(): if f.done():
success, preimage, error = f.result() payment_attempt = f.result()
preimage = payment_attempt.preimage
error_bytes = payment_attempt.error_bytes
error_reason = payment_attempt.error_reason
if preimage: if preimage:
await self.lnworker.enable_htlc_settle.wait() await self.lnworker.enable_htlc_settle.wait()
self.fulfill_htlc(chan, htlc.htlc_id, preimage) self.fulfill_htlc(chan, htlc.htlc_id, preimage)
done.add(htlc_id) done.add(htlc_id)
if error: if error_reason or error_bytes:
self.fail_htlc(chan, htlc.htlc_id, onion_packet, error) self.fail_htlc(chan=chan,
htlc_id=htlc.htlc_id,
onion_packet=onion_packet,
reason=error_reason,
error_bytes=error_bytes)
done.add(htlc_id) done.add(htlc_id)
# cleanup # cleanup
for htlc_id in done: for htlc_id in done:

16
electrum/lnutil.py

@ -112,7 +112,7 @@ class Outpoint(StoredObject):
class PaymentAttemptFailureDetails(NamedTuple): class PaymentAttemptFailureDetails(NamedTuple):
sender_idx: int sender_idx: Optional[int]
failure_msg: 'OnionRoutingFailureMessage' failure_msg: 'OnionRoutingFailureMessage'
is_blacklisted: bool is_blacklisted: bool
@ -128,16 +128,17 @@ class PaymentAttemptLog(NamedTuple):
if not self.exception: if not self.exception:
route = self.route route = self.route
route_str = '%d'%len(route) route_str = '%d'%len(route)
short_channel_id = None
if not self.success: if not self.success:
sender_idx = self.failure_details.sender_idx sender_idx = self.failure_details.sender_idx
failure_msg = self.failure_details.failure_msg failure_msg = self.failure_details.failure_msg
short_channel_id = route[sender_idx+1].short_channel_id if sender_idx is not None:
data = failure_msg.data short_channel_id = route[sender_idx+1].short_channel_id
message = str(failure_msg.code.name) message = str(failure_msg.code.name)
else: else:
short_channel_id = route[-1].short_channel_id short_channel_id = route[-1].short_channel_id
message = _('Success') message = _('Success')
chan_str = str(short_channel_id) chan_str = str(short_channel_id) if short_channel_id else _("Unknown")
else: else:
route_str = 'None' route_str = 'None'
chan_str = 'N/A' chan_str = 'N/A'
@ -145,6 +146,13 @@ class PaymentAttemptLog(NamedTuple):
return route_str, chan_str, message return route_str, chan_str, message
class BarePaymentAttemptLog(NamedTuple):
success: bool
preimage: Optional[bytes]
error_bytes: Optional[bytes]
error_reason: Optional['OnionRoutingFailureMessage'] = None
class LightningError(Exception): pass class LightningError(Exception): pass
class LightningPeerConnectionClosed(LightningError): pass class LightningPeerConnectionClosed(LightningError): pass
class UnableToDeriveSecret(LightningError): pass class UnableToDeriveSecret(LightningError): pass

66
electrum/lnworker.py

@ -53,7 +53,8 @@ from .lnutil import (Outpoint, LNPeerAddr,
UnknownPaymentHash, MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE, UnknownPaymentHash, MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE,
NUM_MAX_EDGES_IN_PAYMENT_PATH, SENT, RECEIVED, HTLCOwner, NUM_MAX_EDGES_IN_PAYMENT_PATH, SENT, RECEIVED, HTLCOwner,
UpdateAddHtlc, Direction, LnLocalFeatures, UpdateAddHtlc, Direction, LnLocalFeatures,
ShortChannelID, PaymentAttemptLog, PaymentAttemptFailureDetails) ShortChannelID, PaymentAttemptLog, PaymentAttemptFailureDetails,
BarePaymentAttemptLog)
from .lnutil import ln_dummy_address, ln_compare_features from .lnutil import ln_dummy_address, ln_compare_features
from .transaction import PartialTxOutput, PartialTransaction, PartialTxInput from .transaction import PartialTxOutput, PartialTransaction, PartialTxInput
from .lnonion import OnionFailureCode, process_onion_packet, OnionPacket from .lnonion import OnionFailureCode, process_onion_packet, OnionPacket
@ -436,8 +437,7 @@ class LNWallet(LNWorker):
for channel_id, c in channels.items(): for channel_id, c in channels.items():
self.channels[bfh(channel_id)] = Channel(c, sweep_address=self.sweep_address, lnworker=self) self.channels[bfh(channel_id)] = Channel(c, sweep_address=self.sweep_address, lnworker=self)
# timestamps of opening and closing transactions self.pending_payments = defaultdict(asyncio.Future) # type: Dict[bytes, asyncio.Future[BarePaymentAttemptLog]]
self.pending_payments = defaultdict(asyncio.Future)
@ignore_exceptions @ignore_exceptions
@log_exceptions @log_exceptions
@ -954,31 +954,36 @@ class LNWallet(LNWorker):
await peer.initialized await peer.initialized
htlc = peer.pay(route, chan, int(lnaddr.amount * COIN * 1000), lnaddr.paymenthash, lnaddr.get_min_final_cltv_expiry()) htlc = peer.pay(route, chan, int(lnaddr.amount * COIN * 1000), lnaddr.paymenthash, lnaddr.get_min_final_cltv_expiry())
self.network.trigger_callback('htlc_added', htlc, lnaddr, SENT) self.network.trigger_callback('htlc_added', htlc, lnaddr, SENT)
success, preimage, reason = await self.await_payment(lnaddr.paymenthash) payment_attempt = await self.await_payment(lnaddr.paymenthash)
if success: if payment_attempt.success:
failure_log = None failure_log = None
else: else:
# TODO this blacklisting is fragile, consider (who to ban/penalize?): if payment_attempt.error_bytes:
# - we might not be able to decode "reason" (coming from update_fail_htlc). # TODO "decode_onion_error" might raise, catch and maybe blacklist/penalise someone?
# - handle update_fail_malformed_htlc case, where there is (kinda) no "reason" failure_msg, sender_idx = chan.decode_onion_error(payment_attempt.error_bytes, route, htlc.htlc_id)
failure_msg, sender_idx = chan.decode_onion_error(reason, route, htlc.htlc_id) is_blacklisted = self.handle_error_code_from_failed_htlc(failure_msg, sender_idx, route, peer)
blacklist = self.handle_error_code_from_failed_htlc(failure_msg, sender_idx, route, peer) if is_blacklisted:
if blacklist: # blacklist channel after reporter node
# blacklist channel after reporter node # TODO this should depend on the error (even more granularity)
# TODO this should depend on the error (even more granularity) # also, we need finer blacklisting (directed edges; nodes)
# also, we need finer blacklisting (directed edges; nodes) try:
try: short_chan_id = route[sender_idx + 1].short_channel_id
short_chan_id = route[sender_idx + 1].short_channel_id except IndexError:
except IndexError: self.logger.info("payment destination reported error")
self.logger.info("payment destination reported error") else:
else: self.network.path_finder.add_to_blacklist(short_chan_id)
self.network.path_finder.add_to_blacklist(short_chan_id) else:
# probably got "update_fail_malformed_htlc". well... who to penalise now?
assert payment_attempt.error_reason is not None
sender_idx = None
failure_msg = payment_attempt.error_reason
is_blacklisted = False
failure_log = PaymentAttemptFailureDetails(sender_idx=sender_idx, failure_log = PaymentAttemptFailureDetails(sender_idx=sender_idx,
failure_msg=failure_msg, failure_msg=failure_msg,
is_blacklisted=blacklist) is_blacklisted=is_blacklisted)
return PaymentAttemptLog(route=route, return PaymentAttemptLog(route=route,
success=success, success=payment_attempt.success,
preimage=preimage, preimage=payment_attempt.preimage,
failure_details=failure_log) failure_details=failure_log)
def handle_error_code_from_failed_htlc(self, failure_msg, sender_idx, route, peer): def handle_error_code_from_failed_htlc(self, failure_msg, sender_idx, route, peer):
@ -1205,10 +1210,10 @@ class LNWallet(LNWorker):
if status in SAVED_PR_STATUS: if status in SAVED_PR_STATUS:
self.set_payment_status(bfh(key), status) self.set_payment_status(bfh(key), status)
async def await_payment(self, payment_hash): async def await_payment(self, payment_hash: bytes) -> BarePaymentAttemptLog:
success, preimage, reason = await self.pending_payments[payment_hash] payment_attempt = await self.pending_payments[payment_hash]
self.pending_payments.pop(payment_hash) self.pending_payments.pop(payment_hash)
return success, preimage, reason return payment_attempt
def set_payment_status(self, payment_hash: bytes, status): def set_payment_status(self, payment_hash: bytes, status):
try: try:
@ -1219,12 +1224,12 @@ class LNWallet(LNWorker):
info = info._replace(status=status) info = info._replace(status=status)
self.save_payment_info(info) self.save_payment_info(info)
def payment_failed(self, chan, payment_hash: bytes, reason: bytes): def payment_failed(self, chan, payment_hash: bytes, payment_attempt: BarePaymentAttemptLog):
self.set_payment_status(payment_hash, PR_UNPAID) self.set_payment_status(payment_hash, PR_UNPAID)
key = payment_hash.hex() key = payment_hash.hex()
f = self.pending_payments.get(payment_hash) f = self.pending_payments.get(payment_hash)
if f and not f.cancelled(): if f and not f.cancelled():
f.set_result((False, None, reason)) f.set_result(payment_attempt)
else: else:
chan.logger.info('received unexpected payment_failed, probably from previous session') chan.logger.info('received unexpected payment_failed, probably from previous session')
self.network.trigger_callback('invoice_status', key) self.network.trigger_callback('invoice_status', key)
@ -1237,7 +1242,10 @@ class LNWallet(LNWorker):
key = payment_hash.hex() key = payment_hash.hex()
f = self.pending_payments.get(payment_hash) f = self.pending_payments.get(payment_hash)
if f and not f.cancelled(): if f and not f.cancelled():
f.set_result((True, preimage, None)) payment_attempt = BarePaymentAttemptLog(success=True,
preimage=preimage,
error_bytes=None)
f.set_result(payment_attempt)
else: else:
chan.logger.info('received unexpected payment_sent, probably from previous session') chan.logger.info('received unexpected payment_sent, probably from previous session')
self.network.trigger_callback('invoice_status', key) self.network.trigger_callback('invoice_status', key)

2
electrum/tests/test_lnchannel.py

@ -619,7 +619,7 @@ class TestAvailableToSpend(ElectrumTestCase):
bob_idx = bob_channel.receive_htlc(htlc_dict).htlc_id bob_idx = bob_channel.receive_htlc(htlc_dict).htlc_id
force_state_transition(alice_channel, bob_channel) force_state_transition(alice_channel, bob_channel)
bob_channel.fail_htlc(bob_idx) bob_channel.fail_htlc(bob_idx)
alice_channel.receive_fail_htlc(alice_idx, None) alice_channel.receive_fail_htlc(alice_idx, error_bytes=None)
# Alice now has gotten all her original balance (5 BTC) back, however, # 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 # 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 # HTLC and signs the next state, Bob cannot assume she received the

Loading…
Cancel
Save