Browse Source

split code in htlc_switch:

- raise OnionRoutingFailure whenever we want to fail a htlc
 - catch that exception in htlc_switch
 - this will avoid code duplication in the case of trampoline
patch-4
ThomasV 4 years ago
parent
commit
87a080d30e
  1. 13
      electrum/lnchannel.py
  2. 13
      electrum/lnonion.py
  3. 178
      electrum/lnpeer.py
  4. 6
      electrum/lnworker.py

13
electrum/lnchannel.py

@ -41,7 +41,7 @@ from .bitcoin import redeem_script_to_address
from .crypto import sha256, sha256d from .crypto import sha256, sha256d
from .transaction import Transaction, PartialTransaction, TxInput from .transaction import Transaction, PartialTransaction, TxInput
from .logging import Logger from .logging import Logger
from .lnonion import decode_onion_error, OnionFailureCode, OnionRoutingFailureMessage from .lnonion import decode_onion_error, OnionFailureCode, OnionRoutingFailure
from . import lnutil from . import lnutil
from .lnutil import (Outpoint, LocalConfig, RemoteConfig, Keypair, OnlyPubkeyKeypair, ChannelConstraints, from .lnutil import (Outpoint, LocalConfig, RemoteConfig, Keypair, OnlyPubkeyKeypair, ChannelConstraints,
get_per_commitment_secret_from_seed, secret_to_pubkey, derive_privkey, make_closing_tx, get_per_commitment_secret_from_seed, secret_to_pubkey, derive_privkey, make_closing_tx,
@ -528,7 +528,7 @@ class Channel(AbstractChannel):
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 = {} # type: Dict[int, (bytes, OnionRoutingFailureMessage)] self._receive_fail_reasons = {} # type: Dict[int, (bytes, OnionRoutingFailure)]
self._ignore_max_htlc_value = False # used in tests self._ignore_max_htlc_value = False # used in tests
def is_initiator(self): def is_initiator(self):
@ -1008,8 +1008,7 @@ class Channel(AbstractChannel):
self, self,
htlc_id: int, htlc_id: int,
error_bytes: Optional[bytes], error_bytes: Optional[bytes],
failure_message: Optional['OnionRoutingFailureMessage'], failure_message: Optional['OnionRoutingFailure']):
):
error_hex = error_bytes.hex() if error_bytes else None error_hex = error_bytes.hex() if error_bytes else None
failure_hex = failure_message.to_bytes().hex() if failure_message else None failure_hex = failure_message.to_bytes().hex() if failure_message else None
self.hm.log['fail_htlc_reasons'][htlc_id] = (error_hex, failure_hex) self.hm.log['fail_htlc_reasons'][htlc_id] = (error_hex, failure_hex)
@ -1017,7 +1016,7 @@ class Channel(AbstractChannel):
def pop_fail_htlc_reason(self, htlc_id): def pop_fail_htlc_reason(self, htlc_id):
error_hex, failure_hex = self.hm.log['fail_htlc_reasons'].pop(htlc_id, (None, None)) error_hex, failure_hex = self.hm.log['fail_htlc_reasons'].pop(htlc_id, (None, None))
error_bytes = bytes.fromhex(error_hex) if error_hex else None error_bytes = bytes.fromhex(error_hex) if error_hex else None
failure_message = OnionRoutingFailureMessage.from_bytes(bytes.fromhex(failure_hex)) if failure_hex else None failure_message = OnionRoutingFailure.from_bytes(bytes.fromhex(failure_hex)) if failure_hex else None
return error_bytes, failure_message return error_bytes, failure_message
def extract_preimage_from_htlc_txin(self, txin: TxInput) -> None: def extract_preimage_from_htlc_txin(self, txin: TxInput) -> None:
@ -1238,7 +1237,7 @@ class Channel(AbstractChannel):
return htlc.payment_hash return htlc.payment_hash
def decode_onion_error(self, reason: bytes, route: Sequence['RouteEdge'], def decode_onion_error(self, reason: bytes, route: Sequence['RouteEdge'],
htlc_id: int) -> Tuple[OnionRoutingFailureMessage, int]: htlc_id: int) -> Tuple[OnionRoutingFailure, int]:
failure_msg, sender_idx = decode_onion_error( failure_msg, sender_idx = decode_onion_error(
reason, reason,
[x.node_id for x in route], [x.node_id for x in route],
@ -1267,7 +1266,7 @@ class Channel(AbstractChannel):
def receive_fail_htlc(self, htlc_id: int, *, def receive_fail_htlc(self, htlc_id: int, *,
error_bytes: Optional[bytes], error_bytes: Optional[bytes],
reason: Optional[OnionRoutingFailureMessage] = None) -> None: reason: Optional[OnionRoutingFailure] = None) -> None:
"""Fail a pending offered HTLC. """Fail a pending offered HTLC.
Action must be initiated by REMOTE. Action must be initiated by REMOTE.
""" """

13
electrum/lnonion.py

@ -381,7 +381,7 @@ def process_onion_packet(onion_packet: OnionPacket, associated_data: bytes,
class FailedToDecodeOnionError(Exception): pass class FailedToDecodeOnionError(Exception): pass
class OnionRoutingFailureMessage: class OnionRoutingFailure(Exception):
def __init__(self, code: int, data: bytes): def __init__(self, code: int, data: bytes):
self.code = code self.code = code
@ -403,15 +403,14 @@ class OnionRoutingFailureMessage:
except ValueError: except ValueError:
pass # uknown failure code pass # uknown failure code
failure_data = failure_msg[2:] failure_data = failure_msg[2:]
return OnionRoutingFailureMessage(failure_code, failure_data) return OnionRoutingFailure(failure_code, failure_data)
def code_name(self) -> str: def code_name(self) -> str:
if isinstance(self.code, OnionFailureCode): if isinstance(self.code, OnionFailureCode):
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: OnionRoutingFailureMessage,
onion_packet: OnionPacket, onion_packet: OnionPacket,
our_onion_private_key: bytes) -> bytes: our_onion_private_key: bytes) -> bytes:
# create payload # create payload
@ -453,19 +452,19 @@ def _decode_onion_error(error_packet: bytes, payment_path_pubkeys: Sequence[byte
def decode_onion_error(error_packet: bytes, payment_path_pubkeys: Sequence[bytes], def decode_onion_error(error_packet: bytes, payment_path_pubkeys: Sequence[bytes],
session_key: bytes) -> (OnionRoutingFailureMessage, int): session_key: bytes) -> (OnionRoutingFailure, int):
"""Returns the failure message, and the index of the sender of the error.""" """Returns the failure message, and the index of the sender of the error."""
decrypted_error, sender_index = _decode_onion_error(error_packet, payment_path_pubkeys, session_key) decrypted_error, sender_index = _decode_onion_error(error_packet, payment_path_pubkeys, session_key)
failure_msg = get_failure_msg_from_onion_error(decrypted_error) failure_msg = get_failure_msg_from_onion_error(decrypted_error)
return failure_msg, sender_index return failure_msg, sender_index
def get_failure_msg_from_onion_error(decrypted_error_packet: bytes) -> OnionRoutingFailureMessage: def get_failure_msg_from_onion_error(decrypted_error_packet: bytes) -> OnionRoutingFailure:
# get failure_msg bytes from error packet # get failure_msg bytes from error packet
failure_len = int.from_bytes(decrypted_error_packet[32:34], byteorder='big') failure_len = int.from_bytes(decrypted_error_packet[32:34], byteorder='big')
failure_msg = decrypted_error_packet[34:34+failure_len] failure_msg = decrypted_error_packet[34:34+failure_len]
# create failure message object # create failure message object
return OnionRoutingFailureMessage.from_bytes(failure_msg) return OnionRoutingFailure.from_bytes(failure_msg)

178
electrum/lnpeer.py

@ -25,7 +25,7 @@ from . import transaction
from .transaction import PartialTxOutput, match_script_against_template from .transaction import PartialTxOutput, match_script_against_template
from .logging import Logger from .logging import Logger
from .lnonion import (new_onion_packet, OnionFailureCode, calc_hops_data_for_payment, from .lnonion import (new_onion_packet, OnionFailureCode, calc_hops_data_for_payment,
process_onion_packet, OnionPacket, construct_onion_error, OnionRoutingFailureMessage, process_onion_packet, OnionPacket, construct_onion_error, OnionRoutingFailure,
ProcessedOnionPacket, UnsupportedOnionPacketVersion, InvalidOnionMac, InvalidOnionPubkey, ProcessedOnionPacket, UnsupportedOnionPacketVersion, InvalidOnionMac, InvalidOnionPubkey,
OnionFailureCodeMetaFlag) OnionFailureCodeMetaFlag)
from .lnchannel import Channel, RevokeAndAck, RemoteCtnTooFarInFuture, ChannelState, PeerState from .lnchannel import Channel, RevokeAndAck, RemoteCtnTooFarInFuture, ChannelState, PeerState
@ -1267,7 +1267,7 @@ 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 = OnionRoutingFailureMessage(code=failure_code, data=payload["sha256_of_onion"]) reason = OnionRoutingFailure(code=failure_code, data=payload["sha256_of_onion"])
chan.receive_fail_htlc(htlc_id, error_bytes=None, reason=reason) chan.receive_fail_htlc(htlc_id, error_bytes=None, reason=reason)
self.maybe_send_commitment(chan) self.maybe_send_commitment(chan)
@ -1295,56 +1295,56 @@ class Peer(Logger):
def maybe_forward_htlc(self, chan: Channel, htlc: UpdateAddHtlc, *, def maybe_forward_htlc(self, chan: Channel, htlc: UpdateAddHtlc, *,
onion_packet: OnionPacket, processed_onion: ProcessedOnionPacket onion_packet: OnionPacket, processed_onion: ProcessedOnionPacket
) -> Tuple[Optional[bytes], Optional[int], Optional[OnionRoutingFailureMessage]]: ) -> Tuple[Optional[bytes], Optional[int], Optional[OnionRoutingFailure]]:
# 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)
if not forwarding_enabled: if not forwarding_enabled:
self.logger.info(f"forwarding is disabled. failing htlc.") self.logger.info(f"forwarding is disabled. failing htlc.")
return None, None, OnionRoutingFailureMessage(code=OnionFailureCode.PERMANENT_CHANNEL_FAILURE, data=b'') raise OnionRoutingFailure(code=OnionFailureCode.PERMANENT_CHANNEL_FAILURE, data=b'')
chain = self.network.blockchain() chain = self.network.blockchain()
if chain.is_tip_stale(): if chain.is_tip_stale():
return None, None, OnionRoutingFailureMessage(code=OnionFailureCode.TEMPORARY_NODE_FAILURE, data=b'') raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_NODE_FAILURE, data=b'')
try: try:
next_chan_scid = processed_onion.hop_data.payload["short_channel_id"]["short_channel_id"] next_chan_scid = processed_onion.hop_data.payload["short_channel_id"]["short_channel_id"]
except: except:
return None, None, OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00') raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00')
next_chan = self.lnworker.get_channel_by_short_id(next_chan_scid) next_chan = self.lnworker.get_channel_by_short_id(next_chan_scid)
local_height = chain.height() local_height = chain.height()
if next_chan is None: if next_chan is None:
self.logger.info(f"cannot forward htlc. cannot find next_chan {next_chan_scid}") self.logger.info(f"cannot forward htlc. cannot find next_chan {next_chan_scid}")
return None, None, OnionRoutingFailureMessage(code=OnionFailureCode.UNKNOWN_NEXT_PEER, data=b'') raise OnionRoutingFailure(code=OnionFailureCode.UNKNOWN_NEXT_PEER, data=b'')
outgoing_chan_upd = next_chan.get_outgoing_gossip_channel_update()[2:] outgoing_chan_upd = next_chan.get_outgoing_gossip_channel_update()[2:]
outgoing_chan_upd_len = len(outgoing_chan_upd).to_bytes(2, byteorder="big") outgoing_chan_upd_len = len(outgoing_chan_upd).to_bytes(2, byteorder="big")
if not next_chan.can_send_update_add_htlc(): if not next_chan.can_send_update_add_htlc():
self.logger.info(f"cannot forward htlc. next_chan {next_chan_scid} cannot send ctx updates. " self.logger.info(f"cannot forward htlc. next_chan {next_chan_scid} cannot send ctx updates. "
f"chan state {next_chan.get_state()!r}, peer state: {next_chan.peer_state!r}") f"chan state {next_chan.get_state()!r}, peer state: {next_chan.peer_state!r}")
data = outgoing_chan_upd_len + outgoing_chan_upd data = outgoing_chan_upd_len + outgoing_chan_upd
return None, None, OnionRoutingFailureMessage(code=OnionFailureCode.TEMPORARY_CHANNEL_FAILURE, data=data) raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_CHANNEL_FAILURE, data=data)
try: try:
next_cltv_expiry = processed_onion.hop_data.payload["outgoing_cltv_value"]["outgoing_cltv_value"] next_cltv_expiry = processed_onion.hop_data.payload["outgoing_cltv_value"]["outgoing_cltv_value"]
except: except:
return None, None, OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00') raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00')
if htlc.cltv_expiry - next_cltv_expiry < NBLOCK_OUR_CLTV_EXPIRY_DELTA: if htlc.cltv_expiry - next_cltv_expiry < NBLOCK_OUR_CLTV_EXPIRY_DELTA:
data = htlc.cltv_expiry.to_bytes(4, byteorder="big") + outgoing_chan_upd_len + outgoing_chan_upd data = htlc.cltv_expiry.to_bytes(4, byteorder="big") + outgoing_chan_upd_len + outgoing_chan_upd
return None, None, OnionRoutingFailureMessage(code=OnionFailureCode.INCORRECT_CLTV_EXPIRY, data=data) raise OnionRoutingFailure(code=OnionFailureCode.INCORRECT_CLTV_EXPIRY, data=data)
if htlc.cltv_expiry - lnutil.MIN_FINAL_CLTV_EXPIRY_ACCEPTED <= local_height \ if htlc.cltv_expiry - lnutil.MIN_FINAL_CLTV_EXPIRY_ACCEPTED <= local_height \
or next_cltv_expiry <= local_height: or next_cltv_expiry <= local_height:
data = outgoing_chan_upd_len + outgoing_chan_upd data = outgoing_chan_upd_len + outgoing_chan_upd
return None, None, OnionRoutingFailureMessage(code=OnionFailureCode.EXPIRY_TOO_SOON, data=data) raise OnionRoutingFailure(code=OnionFailureCode.EXPIRY_TOO_SOON, data=data)
if max(htlc.cltv_expiry, next_cltv_expiry) > local_height + lnutil.NBLOCK_CLTV_EXPIRY_TOO_FAR_INTO_FUTURE: if max(htlc.cltv_expiry, next_cltv_expiry) > local_height + lnutil.NBLOCK_CLTV_EXPIRY_TOO_FAR_INTO_FUTURE:
return None, None, OnionRoutingFailureMessage(code=OnionFailureCode.EXPIRY_TOO_FAR, data=b'') raise OnionRoutingFailure(code=OnionFailureCode.EXPIRY_TOO_FAR, data=b'')
try: try:
next_amount_msat_htlc = processed_onion.hop_data.payload["amt_to_forward"]["amt_to_forward"] next_amount_msat_htlc = processed_onion.hop_data.payload["amt_to_forward"]["amt_to_forward"]
except: except:
return None, None, OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00') raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00')
forwarding_fees = fee_for_edge_msat( forwarding_fees = fee_for_edge_msat(
forwarded_amount_msat=next_amount_msat_htlc, forwarded_amount_msat=next_amount_msat_htlc,
fee_base_msat=lnutil.OUR_FEE_BASE_MSAT, fee_base_msat=lnutil.OUR_FEE_BASE_MSAT,
fee_proportional_millionths=lnutil.OUR_FEE_PROPORTIONAL_MILLIONTHS) fee_proportional_millionths=lnutil.OUR_FEE_PROPORTIONAL_MILLIONTHS)
if htlc.amount_msat - next_amount_msat_htlc < forwarding_fees: if htlc.amount_msat - next_amount_msat_htlc < forwarding_fees:
data = next_amount_msat_htlc.to_bytes(8, byteorder="big") + outgoing_chan_upd_len + outgoing_chan_upd data = next_amount_msat_htlc.to_bytes(8, byteorder="big") + outgoing_chan_upd_len + outgoing_chan_upd
return None, None, OnionRoutingFailureMessage(code=OnionFailureCode.FEE_INSUFFICIENT, data=data) raise OnionRoutingFailure(code=OnionFailureCode.FEE_INSUFFICIENT, data=data)
self.logger.info(f'forwarding htlc to {next_chan.node_id}') self.logger.info(f'forwarding htlc to {next_chan.node_id}')
next_htlc = UpdateAddHtlc( next_htlc = UpdateAddHtlc(
amount_msat=next_amount_msat_htlc, amount_msat=next_amount_msat_htlc,
@ -1366,19 +1366,18 @@ class Peer(Logger):
except BaseException as e: except BaseException as e:
self.logger.info(f"failed to forward htlc: error sending message. {e}") self.logger.info(f"failed to forward htlc: error sending message. {e}")
data = outgoing_chan_upd_len + outgoing_chan_upd data = outgoing_chan_upd_len + outgoing_chan_upd
return None, None, OnionRoutingFailureMessage(code=OnionFailureCode.TEMPORARY_CHANNEL_FAILURE, data=data) raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_CHANNEL_FAILURE, data=data)
return next_chan_scid, next_htlc.htlc_id, None return next_chan_scid, next_htlc.htlc_id
def maybe_fulfill_htlc( def maybe_fulfill_htlc(
self, *, self, *,
chan: Channel, chan: Channel,
htlc: UpdateAddHtlc, htlc: UpdateAddHtlc,
processed_onion: ProcessedOnionPacket) -> Tuple[Optional[bytes], Optional[OnionRoutingFailureMessage]]: processed_onion: ProcessedOnionPacket) -> Tuple[Optional[bytes], Optional[OnionRoutingFailure]]:
info = self.lnworker.get_payment_info(htlc.payment_hash) info = self.lnworker.get_payment_info(htlc.payment_hash)
if info is None: if info is None:
reason = OnionRoutingFailureMessage(code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, data=b'') raise OnionRoutingFailure(code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, data=b'')
return None, reason
preimage = self.lnworker.get_preimage(htlc.payment_hash) preimage = self.lnworker.get_preimage(htlc.payment_hash)
try: try:
payment_secret_from_onion = processed_onion.hop_data.payload["payment_data"]["payment_secret"] payment_secret_from_onion = processed_onion.hop_data.payload["payment_data"]["payment_secret"]
@ -1386,59 +1385,49 @@ class Peer(Logger):
pass # skip pass # skip
else: else:
if payment_secret_from_onion != derive_payment_secret_from_payment_preimage(preimage): if payment_secret_from_onion != derive_payment_secret_from_payment_preimage(preimage):
reason = OnionRoutingFailureMessage(code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, data=b'') raise OnionRoutingFailure(code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, data=b'')
return None, reason
expected_received_msat = info.amount_msat expected_received_msat = info.amount_msat
# Check that our blockchain tip is sufficiently recent so that we have an approx idea of the height. # Check that our blockchain tip is sufficiently recent so that we have an approx idea of the height.
# We should not release the preimage for an HTLC that its sender could already time out as # We should not release the preimage for an HTLC that its sender could already time out as
# then they might try to force-close and it becomes a race. # then they might try to force-close and it becomes a race.
chain = self.network.blockchain() chain = self.network.blockchain()
if chain.is_tip_stale(): if chain.is_tip_stale():
reason = OnionRoutingFailureMessage(code=OnionFailureCode.TEMPORARY_NODE_FAILURE, data=b'') raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_NODE_FAILURE, data=b'')
return None, reason
local_height = chain.height() local_height = chain.height()
if local_height + MIN_FINAL_CLTV_EXPIRY_ACCEPTED > htlc.cltv_expiry: if local_height + MIN_FINAL_CLTV_EXPIRY_ACCEPTED > htlc.cltv_expiry:
reason = OnionRoutingFailureMessage(code=OnionFailureCode.FINAL_EXPIRY_TOO_SOON, data=b'') raise OnionRoutingFailure(code=OnionFailureCode.FINAL_EXPIRY_TOO_SOON, data=b'')
return None, reason
try: try:
cltv_from_onion = processed_onion.hop_data.payload["outgoing_cltv_value"]["outgoing_cltv_value"] cltv_from_onion = processed_onion.hop_data.payload["outgoing_cltv_value"]["outgoing_cltv_value"]
except: except:
reason = OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00') raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00')
return None, reason
if cltv_from_onion != htlc.cltv_expiry: if cltv_from_onion != htlc.cltv_expiry:
reason = OnionRoutingFailureMessage( raise OnionRoutingFailure(
code=OnionFailureCode.FINAL_INCORRECT_CLTV_EXPIRY, code=OnionFailureCode.FINAL_INCORRECT_CLTV_EXPIRY,
data=htlc.cltv_expiry.to_bytes(4, byteorder="big")) data=htlc.cltv_expiry.to_bytes(4, byteorder="big"))
return None, reason
try: try:
amt_to_forward = processed_onion.hop_data.payload["amt_to_forward"]["amt_to_forward"] amt_to_forward = processed_onion.hop_data.payload["amt_to_forward"]["amt_to_forward"]
except: except:
reason = OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00') raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00')
return None, reason
try: try:
total_msat = processed_onion.hop_data.payload["payment_data"]["total_msat"] total_msat = processed_onion.hop_data.payload["payment_data"]["total_msat"]
except: except:
total_msat = amt_to_forward # fall back to "amt_to_forward" total_msat = amt_to_forward # fall back to "amt_to_forward"
if amt_to_forward != htlc.amount_msat: if amt_to_forward != htlc.amount_msat:
reason = OnionRoutingFailureMessage( raise OnionRoutingFailure(
code=OnionFailureCode.FINAL_INCORRECT_HTLC_AMOUNT, code=OnionFailureCode.FINAL_INCORRECT_HTLC_AMOUNT,
data=total_msat.to_bytes(8, byteorder="big")) data=total_msat.to_bytes(8, byteorder="big"))
return None, reason
if expected_received_msat is None: if expected_received_msat is None:
return preimage, None return preimage
if not (expected_received_msat <= total_msat <= 2 * expected_received_msat): if not (expected_received_msat <= total_msat <= 2 * expected_received_msat):
reason = OnionRoutingFailureMessage(code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, data=b'') raise OnionRoutingFailure(code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, data=b'')
return None, reason
accepted, expired = self.lnworker.htlc_received(chan.short_channel_id, htlc, expected_received_msat) accepted, expired = self.lnworker.htlc_received(chan.short_channel_id, htlc, expected_received_msat)
if accepted: if accepted:
return preimage, None return preimage
elif expired: elif expired:
reason = OnionRoutingFailureMessage(code=OnionFailureCode.MPP_TIMEOUT, data=b'') raise OnionRoutingFailure(code=OnionFailureCode.MPP_TIMEOUT, data=b'')
return None, reason
else: else:
# waiting for more htlcs return None
return None, None
def fulfill_htlc(self, chan: Channel, htlc_id: int, preimage: bytes): def fulfill_htlc(self, chan: Channel, htlc_id: int, preimage: bytes):
self.logger.info(f"_fulfill_htlc. chan {chan.short_channel_id}. htlc_id {htlc_id}") self.logger.info(f"_fulfill_htlc. chan {chan.short_channel_id}. htlc_id {htlc_id}")
@ -1461,7 +1450,7 @@ class Peer(Logger):
len=len(error_bytes), len=len(error_bytes),
reason=error_bytes) reason=error_bytes)
def fail_malformed_htlc(self, *, chan: Channel, htlc_id: int, reason: OnionRoutingFailureMessage): def fail_malformed_htlc(self, *, chan: Channel, htlc_id: int, reason: OnionRoutingFailure):
self.logger.info(f"fail_malformed_htlc. chan {chan.short_channel_id}. htlc_id {htlc_id}.") self.logger.info(f"fail_malformed_htlc. chan {chan.short_channel_id}. htlc_id {htlc_id}.")
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)
@ -1679,70 +1668,93 @@ class Peer(Logger):
for htlc_id, (local_ctn, remote_ctn, onion_packet_hex, forwarding_info) in unfulfilled.items(): for htlc_id, (local_ctn, remote_ctn, onion_packet_hex, forwarding_info) in unfulfilled.items():
if not chan.hm.is_add_htlc_irrevocably_committed_yet(htlc_proposer=REMOTE, htlc_id=htlc_id): if not chan.hm.is_add_htlc_irrevocably_committed_yet(htlc_proposer=REMOTE, htlc_id=htlc_id):
continue continue
#chan.logger.info(f'found unfulfilled htlc: {htlc_id}')
htlc = chan.hm.get_htlc_by_id(REMOTE, htlc_id) htlc = chan.hm.get_htlc_by_id(REMOTE, htlc_id)
payment_hash = htlc.payment_hash error_reason = None # type: Optional[OnionRoutingFailure]
error_reason = None # type: Optional[OnionRoutingFailureMessage]
error_bytes = None # type: Optional[bytes] error_bytes = None # type: Optional[bytes]
preimage = None preimage = None
fw_info = None
onion_packet_bytes = bytes.fromhex(onion_packet_hex) onion_packet_bytes = bytes.fromhex(onion_packet_hex)
onion_packet = None onion_packet = None
try: try:
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) except OnionRoutingFailure as e:
except UnsupportedOnionPacketVersion: error_reason = e
error_reason = OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_VERSION, data=sha256(onion_packet_bytes))
except InvalidOnionPubkey:
error_reason = OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_KEY, data=sha256(onion_packet_bytes))
except InvalidOnionMac:
error_reason = OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_HMAC, data=sha256(onion_packet_bytes))
except Exception as e:
self.logger.info(f"error processing onion packet: {e!r}")
error_reason = OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_VERSION, data=sha256(onion_packet_bytes))
else: else:
if self.network.config.get('test_fail_malformed_htlc'): try:
error_reason = OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_VERSION, data=sha256(onion_packet_bytes)) preimage, fw_info, error_bytes = self.process_unfulfilled_htlc(chan, htlc_id, htlc, forwarding_info, onion_packet_bytes, onion_packet)
if self.network.config.get('test_fail_htlcs_with_temp_node_failure'): except OnionRoutingFailure as e:
error_reason = OnionRoutingFailureMessage(code=OnionFailureCode.TEMPORARY_NODE_FAILURE, data=b'') error_bytes = construct_onion_error(e, onion_packet, our_onion_private_key=self.privkey)
if not error_reason: if fw_info:
unfulfilled[htlc_id] = local_ctn, remote_ctn, onion_packet_hex, fw_info
elif preimage or error_reason or error_bytes:
if preimage:
await self.lnworker.enable_htlc_settle.wait()
self.fulfill_htlc(chan, htlc.htlc_id, preimage)
elif error_bytes:
self.fail_htlc(
chan=chan,
htlc_id=htlc.htlc_id,
error_bytes=error_bytes)
else:
self.fail_malformed_htlc(
chan=chan,
htlc_id=htlc.htlc_id,
reason=error_reason)
done.add(htlc_id)
# cleanup
for htlc_id in done:
unfulfilled.pop(htlc_id)
def process_unfulfilled_htlc(self, chan, htlc_id, htlc, forwarding_info, onion_packet_bytes, onion_packet):
"""
returns either preimage or fw_info or error_bytes or (None, None, None)
raise an OnionRoutingFailure if we need to fail the htlc
"""
payment_hash = htlc.payment_hash
processed_onion = self.process_onion_packet(onion_packet, payment_hash, onion_packet_bytes)
if processed_onion.are_we_final: if processed_onion.are_we_final:
preimage, error_reason = self.maybe_fulfill_htlc( preimage = self.maybe_fulfill_htlc(
chan=chan, chan=chan,
htlc=htlc, htlc=htlc,
processed_onion=processed_onion) processed_onion=processed_onion)
elif not forwarding_info: elif not forwarding_info:
next_chan_id, next_htlc_id, error_reason = self.maybe_forward_htlc( next_chan_id, next_htlc_id = 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 next_chan_id: if next_chan_id:
fw_info = (next_chan_id.hex(), next_htlc_id) fw_info = (next_chan_id.hex(), next_htlc_id)
unfulfilled[htlc_id] = local_ctn, remote_ctn, onion_packet_hex, fw_info return None, fw_info, None
else: else:
preimage = self.lnworker.get_preimage(payment_hash) preimage = self.lnworker.get_preimage(payment_hash)
next_chan_id_hex, htlc_id = forwarding_info next_chan_id_hex, htlc_id = forwarding_info
next_chan = self.lnworker.get_channel_by_short_id(bytes.fromhex(next_chan_id_hex)) next_chan = self.lnworker.get_channel_by_short_id(bytes.fromhex(next_chan_id_hex))
if next_chan: if next_chan:
error_bytes, error_reason = next_chan.pop_fail_htlc_reason(htlc_id) error_bytes, error_reason = next_chan.pop_fail_htlc_reason(htlc_id)
if preimage:
await self.lnworker.enable_htlc_settle.wait()
self.fulfill_htlc(chan, htlc.htlc_id, preimage)
done.add(htlc_id)
if error_reason or error_bytes:
if onion_packet and error_reason:
error_bytes = construct_onion_error(error_reason, onion_packet, our_onion_private_key=self.privkey)
if error_bytes: if error_bytes:
self.fail_htlc( return None, None, error_bytes
chan=chan, if error_reason:
htlc_id=htlc.htlc_id, raise error_reason
error_bytes=error_bytes) if preimage:
else: return preimage, None, None
self.fail_malformed_htlc( return None, None, None
chan=chan,
htlc_id=htlc.htlc_id, def process_onion_packet(self, onion_packet, payment_hash, onion_packet_bytes):
reason=error_reason) failure_data = sha256(onion_packet_bytes)
done.add(htlc_id) try:
# cleanup processed_onion = process_onion_packet(onion_packet, associated_data=payment_hash, our_onion_private_key=self.privkey)
for htlc_id in done: except UnsupportedOnionPacketVersion:
unfulfilled.pop(htlc_id) raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_VERSION, data=failure_data)
except InvalidOnionPubkey:
raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_KEY, data=failure_data)
except InvalidOnionMac:
raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_HMAC, data=failure_data)
except Exception as e:
self.logger.info(f"error processing onion packet: {e!r}")
raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_VERSION, data=failure_data)
if self.network.config.get('test_fail_malformed_htlc'):
raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_VERSION, data=failure_data)
if self.network.config.get('test_fail_htlcs_with_temp_node_failure'):
raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_NODE_FAILURE, data=b'')
return processed_onion

6
electrum/lnworker.py

@ -59,7 +59,7 @@ from .lnutil import (Outpoint, LNPeerAddr,
HtlcLog, derive_payment_secret_from_payment_preimage) HtlcLog, derive_payment_secret_from_payment_preimage)
from .lnutil import ln_dummy_address, ln_compare_features, IncompatibleLightningFeatures from .lnutil import ln_dummy_address, ln_compare_features, IncompatibleLightningFeatures
from .transaction import PartialTxOutput, PartialTransaction, PartialTxInput from .transaction import PartialTxOutput, PartialTransaction, PartialTxInput
from .lnonion import OnionFailureCode, process_onion_packet, OnionPacket, OnionRoutingFailureMessage from .lnonion import OnionFailureCode, process_onion_packet, OnionPacket, OnionRoutingFailure
from .lnmsg import decode_msg from .lnmsg import decode_msg
from .i18n import _ from .i18n import _
from .lnrouter import (RouteEdge, LNPaymentRoute, LNPaymentPath, is_route_sane_to_use, from .lnrouter import (RouteEdge, LNPaymentRoute, LNPaymentPath, is_route_sane_to_use,
@ -1379,7 +1379,7 @@ class LNWallet(LNWorker):
htlc_id: int, htlc_id: int,
amount_msat:int, amount_msat:int,
error_bytes: Optional[bytes], error_bytes: Optional[bytes],
failure_message: Optional['OnionRoutingFailureMessage']): failure_message: Optional['OnionRoutingFailure']):
route = self.htlc_routes.get((payment_hash, chan.short_channel_id, htlc_id)) route = self.htlc_routes.get((payment_hash, chan.short_channel_id, htlc_id))
if not route: if not route:
@ -1392,7 +1392,7 @@ class LNWallet(LNWorker):
failure_message, sender_idx = chan.decode_onion_error(error_bytes, route, htlc_id) failure_message, sender_idx = chan.decode_onion_error(error_bytes, route, htlc_id)
except Exception as e: except Exception as e:
sender_idx = None sender_idx = None
failure_message = OnionRoutingFailureMessage(-1, str(e)) failure_message = OnionRoutingFailure(-1, str(e))
else: else:
# probably got "update_fail_malformed_htlc". well... who to penalise now? # probably got "update_fail_malformed_htlc". well... who to penalise now?
assert failure_message is not None assert failure_message is not None

Loading…
Cancel
Save