Browse Source

lnworker: nicer logs/error msgs for payment failure

try to decode onion error and log it in human-readable form
patch-4
SomberNight 4 years ago
parent
commit
d8352f1a0a
No known key found for this signature in database GPG Key ID: B33B5F232C6271E9
  1. 14
      electrum/lnmsg.py
  2. 13
      electrum/lnonion.py
  3. 4
      electrum/lnutil.py
  4. 21
      electrum/lnworker.py
  5. 17
      electrum/tests/test_lnmsg.py

14
electrum/lnmsg.py

@ -7,7 +7,11 @@ from collections import OrderedDict
from .lnutil import OnionFailureCodeMetaFlag from .lnutil import OnionFailureCodeMetaFlag
class MalformedMsg(Exception): pass class FailedToParseMsg(Exception): pass
class MalformedMsg(FailedToParseMsg): pass
class UnknownMsgType(FailedToParseMsg): pass
class UnknownMsgFieldType(MalformedMsg): pass class UnknownMsgFieldType(MalformedMsg): pass
class UnexpectedEndOfStream(MalformedMsg): pass class UnexpectedEndOfStream(MalformedMsg): pass
class FieldEncodingNotMinimal(MalformedMsg): pass class FieldEncodingNotMinimal(MalformedMsg): pass
@ -465,13 +469,17 @@ class LNSerializer:
Decode Lightning message by reading the first Decode Lightning message by reading the first
two bytes to determine message type. two bytes to determine message type.
Returns message type string and parsed message contents dict Returns message type string and parsed message contents dict,
or raises FailedToParseMsg.
""" """
#print(f"decode_msg >>> {data.hex()}") #print(f"decode_msg >>> {data.hex()}")
assert len(data) >= 2 assert len(data) >= 2
msg_type_bytes = data[:2] msg_type_bytes = data[:2]
msg_type_int = int.from_bytes(msg_type_bytes, byteorder="big", signed=False) msg_type_int = int.from_bytes(msg_type_bytes, byteorder="big", signed=False)
scheme = self.msg_scheme_from_type[msg_type_bytes] try:
scheme = self.msg_scheme_from_type[msg_type_bytes]
except KeyError:
raise UnknownMsgType(f"msg_type={msg_type_int}") # TODO even/odd type?
assert scheme[0][2] == msg_type_int assert scheme[0][2] == msg_type_int
msg_type_name = scheme[0][1] msg_type_name = scheme[0][1]
parsed = {} parsed = {}

13
electrum/lnonion.py

@ -25,7 +25,7 @@
import io import io
import hashlib import hashlib
from typing import Sequence, List, Tuple, NamedTuple, TYPE_CHECKING from typing import Sequence, List, Tuple, NamedTuple, TYPE_CHECKING, Dict, Any, Optional
from enum import IntEnum, IntFlag from enum import IntEnum, IntFlag
from . import ecc from . import ecc
@ -33,7 +33,7 @@ from .crypto import sha256, hmac_oneshot, chacha20_encrypt
from .util import bh2u, profiler, xor_bytes, bfh from .util import bh2u, profiler, xor_bytes, bfh
from .lnutil import (get_ecdh, PaymentFailure, NUM_MAX_HOPS_IN_PAYMENT_PATH, from .lnutil import (get_ecdh, PaymentFailure, NUM_MAX_HOPS_IN_PAYMENT_PATH,
NUM_MAX_EDGES_IN_PAYMENT_PATH, ShortChannelID, OnionFailureCodeMetaFlag) NUM_MAX_EDGES_IN_PAYMENT_PATH, ShortChannelID, OnionFailureCodeMetaFlag)
from .lnmsg import OnionWireSerializer, read_bigsize_int, write_bigsize_int from .lnmsg import OnionWireSerializer, read_bigsize_int, write_bigsize_int, UnknownMsgType
if TYPE_CHECKING: if TYPE_CHECKING:
from .lnrouter import LNPaymentRoute from .lnrouter import LNPaymentRoute
@ -431,7 +431,7 @@ class OnionRoutingFailure(Exception):
try: try:
failure_code = OnionFailureCode(failure_code) failure_code = OnionFailureCode(failure_code)
except ValueError: except ValueError:
pass # uknown failure code pass # unknown failure code
failure_data = failure_msg[2:] failure_data = failure_msg[2:]
return OnionRoutingFailure(failure_code, failure_data) return OnionRoutingFailure(failure_code, failure_data)
@ -440,6 +440,13 @@ 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 decode_data(self) -> Optional[Dict[str, Any]]:
try:
message_type, payload = OnionWireSerializer.decode_msg(self.to_bytes())
except UnknownMsgType:
payload = None
return payload
def construct_onion_error( def construct_onion_error(
reason: OnionRoutingFailure, reason: OnionRoutingFailure,

4
electrum/lnutil.py

@ -30,7 +30,7 @@ from .transaction import BCDataStream
if TYPE_CHECKING: if TYPE_CHECKING:
from .lnchannel import Channel, AbstractChannel from .lnchannel import Channel, AbstractChannel
from .lnrouter import LNPaymentRoute from .lnrouter import LNPaymentRoute
from .lnonion import OnionRoutingFailureMessage from .lnonion import OnionRoutingFailure
# defined in BOLT-03: # defined in BOLT-03:
@ -263,7 +263,7 @@ class HtlcLog(NamedTuple):
route: Optional['LNPaymentRoute'] = None route: Optional['LNPaymentRoute'] = None
preimage: Optional[bytes] = None preimage: Optional[bytes] = None
error_bytes: Optional[bytes] = None error_bytes: Optional[bytes] = None
failure_msg: Optional['OnionRoutingFailureMessage'] = None failure_msg: Optional['OnionRoutingFailure'] = None
sender_idx: Optional[int] = None sender_idx: Optional[int] = None
def formatted_tuple(self): def formatted_tuple(self):

21
electrum/lnworker.py

@ -65,7 +65,7 @@ from .lnutil import (Outpoint, LNPeerAddr,
from .lnutil import ln_dummy_address, ln_compare_features, IncompatibleLightningFeatures from .lnutil import ln_dummy_address, ln_compare_features, IncompatibleLightningFeatures
from .lnrouter import TrampolineEdge from .lnrouter import TrampolineEdge
from .transaction import PartialTxOutput, PartialTransaction, PartialTxInput from .transaction import PartialTxOutput, PartialTransaction, PartialTxInput
from .lnonion import OnionFailureCode, process_onion_packet, OnionPacket, OnionRoutingFailure from .lnonion import OnionFailureCode, 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,
@ -1115,7 +1115,7 @@ class LNWallet(LNWorker):
full_path=full_path) full_path=full_path)
success = True success = True
except PaymentFailure as e: except PaymentFailure as e:
self.logger.exception('') self.logger.info(f'payment failure: {e!r}')
success = False success = False
reason = str(e) reason = str(e)
if success: if success:
@ -1207,7 +1207,8 @@ class LNWallet(LNWorker):
sender_idx = htlc_log.sender_idx sender_idx = htlc_log.sender_idx
failure_msg = htlc_log.failure_msg failure_msg = htlc_log.failure_msg
code, data = failure_msg.code, failure_msg.data code, data = failure_msg.code, failure_msg.data
self.logger.info(f"UPDATE_FAIL_HTLC {repr(code)} {data}") self.logger.info(f"UPDATE_FAIL_HTLC. code={repr(code)}. "
f"decoded_data={failure_msg.decode_data()}. data={data.hex()}")
self.logger.info(f"error reported by {bh2u(route[sender_idx].node_id)}") self.logger.info(f"error reported by {bh2u(route[sender_idx].node_id)}")
if code == OnionFailureCode.MPP_TIMEOUT: if code == OnionFailureCode.MPP_TIMEOUT:
raise PaymentFailure(failure_msg.code_name()) raise PaymentFailure(failure_msg.code_name())
@ -1222,7 +1223,8 @@ class LNWallet(LNWorker):
else: else:
raise PaymentFailure(failure_msg.code_name()) raise PaymentFailure(failure_msg.code_name())
else: else:
self.handle_error_code_from_failed_htlc(route, sender_idx, failure_msg, code, data) self.handle_error_code_from_failed_htlc(
route=route, sender_idx=sender_idx, failure_msg=failure_msg)
async def pay_to_route( async def pay_to_route(
self, *, self, *,
@ -1263,8 +1265,13 @@ class LNWallet(LNWorker):
self.sent_buckets[payment_secret] = amount_sent, amount_failed self.sent_buckets[payment_secret] = amount_sent, amount_failed
util.trigger_callback('htlc_added', chan, htlc, SENT) util.trigger_callback('htlc_added', chan, htlc, SENT)
def handle_error_code_from_failed_htlc(
def handle_error_code_from_failed_htlc(self, route, sender_idx, failure_msg, code, data): self,
*,
route: LNPaymentRoute,
sender_idx: int,
failure_msg: OnionRoutingFailure) -> None:
code, data = failure_msg.code, failure_msg.data
# handle some specific error codes # handle some specific error codes
failure_codes = { failure_codes = {
OnionFailureCode.TEMPORARY_CHANNEL_FAILURE: 0, OnionFailureCode.TEMPORARY_CHANNEL_FAILURE: 0,
@ -1299,7 +1306,7 @@ class LNWallet(LNWorker):
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:
raise PaymentFailure('payment destination reported error') raise PaymentFailure(f'payment destination reported error: {failure_msg.code_name()}') from None
# TODO: for MPP we need to save the amount for which # TODO: for MPP we need to save the amount for which
# we saw temporary channel failure # we saw temporary channel failure
self.logger.info(f'blacklisting channel {short_chan_id}') self.logger.info(f'blacklisting channel {short_chan_id}')

17
electrum/tests/test_lnmsg.py

@ -3,7 +3,9 @@ import io
from electrum.lnmsg import (read_bigsize_int, write_bigsize_int, FieldEncodingNotMinimal, from electrum.lnmsg import (read_bigsize_int, write_bigsize_int, FieldEncodingNotMinimal,
UnexpectedEndOfStream, LNSerializer, UnknownMandatoryTLVRecordType, UnexpectedEndOfStream, LNSerializer, UnknownMandatoryTLVRecordType,
MalformedMsg, MsgTrailingGarbage, MsgInvalidFieldOrder, encode_msg, MalformedMsg, MsgTrailingGarbage, MsgInvalidFieldOrder, encode_msg,
decode_msg, UnexpectedFieldSizeForEncoder) decode_msg, UnexpectedFieldSizeForEncoder, OnionWireSerializer,
UnknownMsgType)
from electrum.lnonion import OnionRoutingFailure
from electrum.util import bfh from electrum.util import bfh
from electrum.lnutil import ShortChannelID, LnFeatures from electrum.lnutil import ShortChannelID, LnFeatures
from electrum import constants from electrum import constants
@ -383,3 +385,16 @@ class TestLNMsg(TestCaseForTestnet):
{'chains': b'CI\x7f\xd7\xf8&\x95q\x08\xf4\xa3\x0f\xd9\xce\xc3\xae\xbay\x97 \x84\xe9\x0e\xad\x01\xea3\t\x00\x00\x00\x00'} {'chains': b'CI\x7f\xd7\xf8&\x95q\x08\xf4\xa3\x0f\xd9\xce\xc3\xae\xbay\x97 \x84\xe9\x0e\xad\x01\xea3\t\x00\x00\x00\x00'}
}}), }}),
decode_msg(bfh("001000022200000302aaa2012043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000"))) decode_msg(bfh("001000022200000302aaa2012043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000")))
def test_decode_onion_error(self):
orf = OnionRoutingFailure.from_bytes(bfh("400f0000000017d2d8b0001d9458"))
self.assertEqual(('incorrect_or_unknown_payment_details', {'htlc_msat': 399694000, 'height': 1938520}),
OnionWireSerializer.decode_msg(orf.to_bytes()))
self.assertEqual({'htlc_msat': 399694000, 'height': 1938520},
orf.decode_data())
orf2 = OnionRoutingFailure(26399, bytes.fromhex("0000000017d2d8b0001d9458"))
with self.assertRaises(UnknownMsgType):
OnionWireSerializer.decode_msg(orf2.to_bytes())
self.assertEqual(None, orf2.decode_data())

Loading…
Cancel
Save