Browse Source

lnpeer: implement basic handling of "update_fail_malformed_htlc"

hard-fail-on-bad-server-string
SomberNight 5 years ago
parent
commit
b524460fdf
No known key found for this signature in database GPG Key ID: B33B5F232C6271E9
  1. 8
      electrum/lnchannel.py
  2. 44
      electrum/lnhtlc.py
  3. 7
      electrum/lnonion.py
  4. 124
      electrum/lnpeer.py
  5. 5
      electrum/lnworker.py

8
electrum/lnchannel.py

@ -41,7 +41,7 @@ from .bitcoin import redeem_script_to_address
from .crypto import sha256, sha256d
from .transaction import Transaction, PartialTransaction
from .logging import Logger
from .lnonion import decode_onion_error
from .lnonion import decode_onion_error, OnionFailureCode, OnionRoutingFailureMessage
from . import lnutil
from .lnutil import (Outpoint, LocalConfig, RemoteConfig, Keypair, OnlyPubkeyKeypair, ChannelConstraints,
get_per_commitment_secret_from_seed, secret_to_pubkey, derive_privkey, make_closing_tx,
@ -59,6 +59,7 @@ from .lnmsg import encode_msg, decode_msg
if TYPE_CHECKING:
from .lnworker import LNWallet
from .json_db import StoredDict
from .lnrouter import RouteEdge
# lightning channel states
@ -769,7 +770,8 @@ class Channel(Logger):
htlc = log['adds'][htlc_id]
return htlc.payment_hash
def decode_onion_error(self, reason, route, htlc_id):
def decode_onion_error(self, reason: bytes, route: Sequence['RouteEdge'],
htlc_id: int) -> Tuple[OnionRoutingFailureMessage, int]:
failure_msg, sender_idx = decode_onion_error(
reason,
[x.node_id for x in route],
@ -791,7 +793,7 @@ class Channel(Logger):
with self.db_lock:
self.hm.send_fail(htlc_id)
def receive_fail_htlc(self, htlc_id, reason):
def receive_fail_htlc(self, htlc_id: int, reason: bytes):
self.logger.info("receive_fail_htlc")
with self.db_lock:
self.hm.recv_fail(htlc_id)

44
electrum/lnhtlc.py

@ -88,16 +88,28 @@ class HTLCManager:
self._maybe_active_htlc_ids[REMOTE].add(htlc_id)
def send_settle(self, htlc_id: int) -> None:
self.log[REMOTE]['settles'][htlc_id] = {LOCAL: None, REMOTE: self.ctn_latest(REMOTE) + 1}
next_ctn = self.ctn_latest(REMOTE) + 1
if not self.is_htlc_active_at_ctn(ctx_owner=REMOTE, ctn=next_ctn, htlc_proposer=REMOTE, htlc_id=htlc_id):
raise Exception(f"(local) cannot remove htlc that is not there...")
self.log[REMOTE]['settles'][htlc_id] = {LOCAL: None, REMOTE: next_ctn}
def recv_settle(self, htlc_id: int) -> None:
self.log[LOCAL]['settles'][htlc_id] = {LOCAL: self.ctn_latest(LOCAL) + 1, REMOTE: None}
next_ctn = self.ctn_latest(LOCAL) + 1
if not self.is_htlc_active_at_ctn(ctx_owner=LOCAL, ctn=next_ctn, htlc_proposer=LOCAL, htlc_id=htlc_id):
raise Exception(f"(remote) cannot remove htlc that is not there...")
self.log[LOCAL]['settles'][htlc_id] = {LOCAL: next_ctn, REMOTE: None}
def send_fail(self, htlc_id: int) -> None:
self.log[REMOTE]['fails'][htlc_id] = {LOCAL: None, REMOTE: self.ctn_latest(REMOTE) + 1}
next_ctn = self.ctn_latest(REMOTE) + 1
if not self.is_htlc_active_at_ctn(ctx_owner=REMOTE, ctn=next_ctn, htlc_proposer=REMOTE, htlc_id=htlc_id):
raise Exception(f"(local) cannot remove htlc that is not there...")
self.log[REMOTE]['fails'][htlc_id] = {LOCAL: None, REMOTE: next_ctn}
def recv_fail(self, htlc_id: int) -> None:
self.log[LOCAL]['fails'][htlc_id] = {LOCAL: self.ctn_latest(LOCAL) + 1, REMOTE: None}
next_ctn = self.ctn_latest(LOCAL) + 1
if not self.is_htlc_active_at_ctn(ctx_owner=LOCAL, ctn=next_ctn, htlc_proposer=LOCAL, htlc_id=htlc_id):
raise Exception(f"(remote) cannot remove htlc that is not there...")
self.log[LOCAL]['fails'][htlc_id] = {LOCAL: next_ctn, REMOTE: None}
def send_update_fee(self, feerate: int) -> None:
fee_update = FeeUpdate(rate=feerate,
@ -249,6 +261,20 @@ class HTLCManager:
##### Queries re HTLCs:
def is_htlc_active_at_ctn(self, *, ctx_owner: HTLCOwner, ctn: int,
htlc_proposer: HTLCOwner, htlc_id: int) -> bool:
if htlc_id >= self.get_next_htlc_id(htlc_proposer):
return False
settles = self.log[htlc_proposer]['settles']
fails = self.log[htlc_proposer]['fails']
ctns = self.log[htlc_proposer]['locked_in'][htlc_id]
if ctns[ctx_owner] is not None and ctns[ctx_owner] <= ctn:
not_settled = htlc_id not in settles or settles[htlc_id][ctx_owner] is None or settles[htlc_id][ctx_owner] > ctn
not_failed = htlc_id not in fails or fails[htlc_id][ctx_owner] is None or fails[htlc_id][ctx_owner] > ctn
if not_settled and not_failed:
return True
return False
def htlcs_by_direction(self, subject: HTLCOwner, direction: Direction,
ctn: int = None) -> Dict[int, UpdateAddHtlc]:
"""Return the dict of received or sent (depending on direction) HTLCs
@ -264,19 +290,13 @@ class HTLCManager:
# subject's ctx
# party is the proposer of the HTLCs
party = subject if direction == SENT else subject.inverted()
settles = self.log[party]['settles']
fails = self.log[party]['fails']
if ctn >= self.ctn_oldest_unrevoked(subject):
considered_htlc_ids = self._maybe_active_htlc_ids[party]
else: # ctn is too old; need to consider full log (slow...)
considered_htlc_ids = self.log[party]['locked_in']
for htlc_id in considered_htlc_ids:
ctns = self.log[party]['locked_in'][htlc_id]
if ctns[subject] is not None and ctns[subject] <= ctn:
not_settled = htlc_id not in settles or settles[htlc_id][subject] is None or settles[htlc_id][subject] > ctn
not_failed = htlc_id not in fails or fails[htlc_id][subject] is None or fails[htlc_id][subject] > ctn
if not_settled and not_failed:
d[htlc_id] = self.log[party]['adds'][htlc_id]
if self.is_htlc_active_at_ctn(ctx_owner=subject, ctn=ctn, htlc_proposer=party, htlc_id=htlc_id):
d[htlc_id] = self.log[party]['adds'][htlc_id]
return d
def htlcs(self, subject: HTLCOwner, ctn: int = None) -> Sequence[Tuple[Direction, UpdateAddHtlc]]:

7
electrum/lnonion.py

@ -45,6 +45,7 @@ PER_HOP_HMAC_SIZE = 32
class UnsupportedOnionPacketVersion(Exception): pass
class InvalidOnionMac(Exception): pass
class InvalidOnionPubkey(Exception): pass
class OnionPerHop:
@ -109,6 +110,8 @@ class OnionPacket:
self.public_key = public_key
self.hops_data = hops_data # also called RoutingInfo in bolt-04
self.hmac = hmac
if not ecc.ECPubkey.is_pubkey_bytes(public_key):
raise InvalidOnionPubkey()
def to_bytes(self) -> bytes:
ret = bytes([self.version])
@ -243,6 +246,8 @@ class ProcessedOnionPacket(NamedTuple):
# TODO replay protection
def process_onion_packet(onion_packet: OnionPacket, associated_data: bytes,
our_onion_private_key: bytes) -> ProcessedOnionPacket:
if not ecc.ECPubkey.is_pubkey_bytes(onion_packet.public_key):
raise InvalidOnionPubkey()
shared_secret = get_ecdh(our_onion_private_key, onion_packet.public_key)
# check message integrity
@ -322,7 +327,7 @@ def construct_onion_error(reason: OnionRoutingFailureMessage,
def _decode_onion_error(error_packet: bytes, payment_path_pubkeys: Sequence[bytes],
session_key: bytes) -> (bytes, int):
session_key: bytes) -> Tuple[bytes, int]:
"""Returns the decoded error bytes, and the index of the sender of the error."""
num_hops = len(payment_path_pubkeys)
hop_shared_secrets = get_shared_secrets_along_route(payment_path_pubkeys, session_key)

124
electrum/lnpeer.py

@ -30,7 +30,8 @@ from .transaction import Transaction, TxOutput, PartialTxOutput, match_script_ag
from .logging import Logger
from .lnonion import (new_onion_packet, decode_onion_error, OnionFailureCode, calc_hops_data_for_payment,
process_onion_packet, OnionPacket, construct_onion_error, OnionRoutingFailureMessage,
ProcessedOnionPacket)
ProcessedOnionPacket, UnsupportedOnionPacketVersion, InvalidOnionMac, InvalidOnionPubkey,
OnionFailureCodeMetaFlag)
from .lnchannel import Channel, RevokeAndAck, htlcsum, RemoteCtnTooFarInFuture, channel_states, peer_states
from . import lnutil
from .lnutil import (Outpoint, LocalConfig, RECEIVED, UpdateAddHtlc,
@ -229,7 +230,7 @@ class Peer(Logger):
chan.set_remote_update(payload['raw'])
self.logger.info("saved remote_update")
def on_announcement_signatures(self, chan, payload):
def on_announcement_signatures(self, chan: Channel, payload):
if chan.config[LOCAL].was_announced:
h, local_node_sig, local_bitcoin_sig = self.send_announcement_signatures(chan)
else:
@ -900,7 +901,7 @@ class Peer(Logger):
if chan.config[LOCAL].funding_locked_received and chan.short_channel_id:
self.mark_open(chan)
def on_funding_locked(self, chan, payload):
def on_funding_locked(self, chan: Channel, payload):
self.logger.info(f"on_funding_locked. channel: {bh2u(chan.channel_id)}")
if not chan.config[LOCAL].funding_locked_received:
their_next_point = payload["next_per_commitment_point"]
@ -926,7 +927,7 @@ class Peer(Logger):
asyncio.run_coroutine_threadsafe(coro, self.network.asyncio_loop)
@log_exceptions
async def handle_announcements(self, chan):
async def handle_announcements(self, chan: Channel):
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"]
@ -1002,11 +1003,11 @@ class Peer(Logger):
)
return msg_hash, node_signature, bitcoin_signature
def on_update_fail_htlc(self, chan, payload):
def on_update_fail_htlc(self, chan: Channel, payload):
htlc_id = int.from_bytes(payload["id"], "big")
reason = payload["reason"]
self.logger.info(f"on_update_fail_htlc. chan {chan.short_channel_id}. htlc_id {htlc_id}")
chan.receive_fail_htlc(htlc_id, reason)
chan.receive_fail_htlc(htlc_id, reason) # TODO handle exc and maybe fail channel (e.g. bad htlc_id)
self.maybe_send_commitment(chan)
def maybe_send_commitment(self, chan: Channel):
@ -1057,7 +1058,7 @@ class Peer(Logger):
next_per_commitment_point=rev.next_per_commitment_point)
self.maybe_send_commitment(chan)
def on_commitment_signed(self, chan, payload):
def on_commitment_signed(self, chan: Channel, payload):
if chan.peer_state == peer_states.BAD:
return
self.logger.info(f'on_commitment_signed. chan {chan.short_channel_id}. ctn: {chan.get_next_ctn(LOCAL)}.')
@ -1075,19 +1076,29 @@ class Peer(Logger):
chan.receive_new_commitment(payload["signature"], htlc_sigs)
self.send_revoke_and_ack(chan)
def on_update_fulfill_htlc(self, chan, payload):
def on_update_fulfill_htlc(self, chan: Channel, payload):
preimage = payload["payment_preimage"]
payment_hash = sha256(preimage)
htlc_id = int.from_bytes(payload["id"], "big")
self.logger.info(f"on_update_fulfill_htlc. chan {chan.short_channel_id}. htlc_id {htlc_id}")
chan.receive_htlc_settle(preimage, htlc_id)
chan.receive_htlc_settle(preimage, htlc_id) # TODO handle exc and maybe fail channel (e.g. bad htlc_id)
self.lnworker.save_preimage(payment_hash, preimage)
self.maybe_send_commitment(chan)
def on_update_fail_malformed_htlc(self, chan, payload):
self.logger.info(f"on_update_fail_malformed_htlc. error {payload['data'].decode('ascii')}")
def on_update_fail_malformed_htlc(self, chan: Channel, payload):
htlc_id = payload["id"]
failure_code = payload["failure_code"]
self.logger.info(f"on_update_fail_malformed_htlc. chan {chan.get_id_for_log()}. "
f"htlc_id {htlc_id}. failure_code={failure_code}")
if failure_code & OnionFailureCodeMetaFlag.BADONION == 0:
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}")
reason = b'' # TODO somehow propagate "failure_code" ?
chan.receive_fail_htlc(htlc_id, reason) # TODO handle exc and maybe fail channel (e.g. bad htlc_id)
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, payload):
def on_update_add_htlc(self, chan: Channel, payload):
payment_hash = payload["payment_hash"]
htlc_id = int.from_bytes(payload["id"], 'big')
self.logger.info(f"on_update_add_htlc. chan {chan.short_channel_id}. htlc_id {htlc_id}")
@ -1211,19 +1222,27 @@ class Peer(Logger):
id=htlc_id,
payment_preimage=preimage)
def fail_htlc(self, chan: Channel, htlc_id: int, onion_packet: OnionPacket,
def fail_htlc(self, chan: Channel, htlc_id: int, onion_packet: Optional[OnionPacket],
reason: OnionRoutingFailureMessage):
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}"
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)
def on_revoke_and_ack(self, chan, payload):
if onion_packet:
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)
else:
assert len(reason.data) == 32, f"unexpected reason when sending 'update_fail_malformed_htlc': {reason!r}"
self.send_message("update_fail_malformed_htlc",
channel_id=chan.channel_id,
id=htlc_id,
sha256_of_onion=reason.data,
failure_code=reason.code)
def on_revoke_and_ack(self, chan: Channel, payload):
if chan.peer_state == peer_states.BAD:
return
self.logger.info(f'on_revoke_and_ack. chan {chan.short_channel_id}. ctn: {chan.get_oldest_unrevoked_ctn(REMOTE)}')
@ -1232,7 +1251,7 @@ class Peer(Logger):
self.lnworker.save_channel(chan)
self.maybe_send_commitment(chan)
def on_update_fee(self, chan, payload):
def on_update_fee(self, chan: Channel, payload):
feerate = int.from_bytes(payload["feerate_per_kw"], "big")
chan.update_fee(feerate, False)
@ -1282,7 +1301,7 @@ class Peer(Logger):
return txid
@log_exceptions
async def on_shutdown(self, chan, payload):
async def on_shutdown(self, chan: Channel, payload):
their_scriptpubkey = payload['scriptpubkey']
# BOLT-02 restrict the scriptpubkey to some templates:
if not (match_script_against_template(their_scriptpubkey, transaction.SCRIPTPUBKEY_TEMPLATE_WITNESS_V0)
@ -1404,33 +1423,44 @@ class Peer(Logger):
if chan.get_oldest_unrevoked_ctn(REMOTE) <= remote_ctn:
continue
chan.logger.info(f'found unfulfilled htlc: {htlc_id}')
onion_packet = OnionPacket.from_bytes(bytes.fromhex(onion_packet_hex))
htlc = chan.hm.log[REMOTE]['adds'][htlc_id]
payment_hash = htlc.payment_hash
processed_onion = process_onion_packet(onion_packet, associated_data=payment_hash, our_onion_private_key=self.privkey)
preimage, error = None, None
if processed_onion.are_we_final:
preimage, error = self.maybe_fulfill_htlc(
chan=chan,
htlc=htlc,
onion_packet=onion_packet,
processed_onion=processed_onion)
elif not forwarded:
error = self.maybe_forward_htlc(
chan=chan,
htlc=htlc,
onion_packet=onion_packet,
processed_onion=processed_onion)
if not error:
unfulfilled[htlc_id] = local_ctn, remote_ctn, onion_packet_hex, True
error = None # type: Optional[OnionRoutingFailureMessage]
preimage = None
onion_packet_bytes = bytes.fromhex(onion_packet_hex)
onion_packet = None
try:
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 UnsupportedOnionPacketVersion:
error = OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_VERSION, data=sha256(onion_packet_bytes))
except InvalidOnionPubkey:
error = OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_KEY, data=sha256(onion_packet_bytes))
except InvalidOnionMac:
error = OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_HMAC, data=sha256(onion_packet_bytes))
else:
f = self.lnworker.pending_payments[payment_hash]
if f.done():
success, preimage, error = f.result()
if preimage:
await self.lnworker.enable_htlc_settle.wait()
self.fulfill_htlc(chan, htlc.htlc_id, preimage)
done.add(htlc_id)
if processed_onion.are_we_final:
preimage, error = self.maybe_fulfill_htlc(
chan=chan,
htlc=htlc,
onion_packet=onion_packet,
processed_onion=processed_onion)
elif not forwarded:
error = self.maybe_forward_htlc(
chan=chan,
htlc=htlc,
onion_packet=onion_packet,
processed_onion=processed_onion)
if not error:
unfulfilled[htlc_id] = local_ctn, remote_ctn, onion_packet_hex, True
else:
f = self.lnworker.pending_payments[payment_hash]
if f.done():
success, preimage, error = f.result()
if preimage:
await self.lnworker.enable_htlc_settle.wait()
self.fulfill_htlc(chan, htlc.htlc_id, preimage)
done.add(htlc_id)
if error:
self.fail_htlc(chan, htlc.htlc_id, onion_packet, error)
done.add(htlc_id)

5
electrum/lnworker.py

@ -958,6 +958,9 @@ class LNWallet(LNWorker):
if success:
failure_log = None
else:
# TODO this blacklisting is fragile, consider (who to ban/penalize?):
# - we might not be able to decode "reason" (coming from update_fail_htlc).
# - handle update_fail_malformed_htlc case, where there is (kinda) no "reason"
failure_msg, sender_idx = chan.decode_onion_error(reason, route, htlc.htlc_id)
blacklist = self.handle_error_code_from_failed_htlc(failure_msg, sender_idx, route, peer)
if blacklist:
@ -1216,7 +1219,7 @@ class LNWallet(LNWorker):
info = info._replace(status=status)
self.save_payment_info(info)
def payment_failed(self, chan, payment_hash: bytes, reason):
def payment_failed(self, chan, payment_hash: bytes, reason: bytes):
self.set_payment_status(payment_hash, PR_UNPAID)
key = payment_hash.hex()
f = self.pending_payments.get(payment_hash)

Loading…
Cancel
Save