tstart failing htlcs - electrum - Electrum Bitcoin wallet HTML git clone https://git.parazyd.org/electrum DIR Log DIR Files DIR Refs DIR Submodules --- DIR commit d511ecdc0073ba2af55a0c608ccd2b38f448ad8d DIR parent ded11b4d9ef637eb58471151c79bc499e59860e3 HTML Author: SomberNight <somber.night@protonmail.com> Date: Thu, 18 Oct 2018 22:56:40 +0200 start failing htlcs Diffstat: M electrum/lnbase.py | 86 +++++++++++++++++++++++++------ M electrum/lnchan.py | 10 +++++++--- M electrum/lnonion.py | 8 ++++---- M electrum/lnutil.py | 8 +++++++- M electrum/lnworker.py | 13 ++++++++----- 5 files changed, 96 insertions(+), 29 deletions(-) --- DIR diff --git a/electrum/lnbase.py b/electrum/lnbase.py t@@ -10,7 +10,7 @@ import asyncio import os import time from functools import partial -from typing import List, Tuple +from typing import List, Tuple, Dict import traceback import sys t@@ -23,15 +23,15 @@ from .ecc import sig_string_from_r_and_s, get_r_and_s_from_sig_string from . import constants from .util import PrintError, bh2u, print_error, bfh, log_exceptions, list_enabled_bits, ignore_exceptions from .transaction import Transaction, TxOutput -from .lnonion import new_onion_packet, decode_onion_error, OnionFailureCode, calc_hops_data_for_payment -from .lnaddr import lndecode +from .lnonion import (new_onion_packet, decode_onion_error, OnionFailureCode, calc_hops_data_for_payment, + process_onion_packet, OnionPacket, construct_onion_error, OnionRoutingFailureMessage) from .lnchan import Channel, RevokeAndAck, htlcsum from .lnutil import (Outpoint, LocalConfig, ChannelConfig, RemoteConfig, OnlyPubkeyKeypair, ChannelConstraints, RevocationStore, funding_output_script, get_per_commitment_secret_from_seed, secret_to_pubkey, LNPeerAddr, PaymentFailure, LnLocalFeatures, LOCAL, REMOTE, HTLCOwner, generate_keypair, LnKeyFamily, - get_ln_flag_pair_of_bit, privkey_to_pubkey) + get_ln_flag_pair_of_bit, privkey_to_pubkey, UnknownPaymentHash, MIN_FINAL_CLTV_EXPIRY_ACCEPTED) from .lnutil import LightningPeerConnectionClosed, HandshakeFailed from .lnrouter import NotFoundChanAnnouncementForUpdate, RouteEdge from .lntransport import LNTransport t@@ -231,7 +231,7 @@ class Peer(PrintError): self.initialized.set_result(True) @property - def channels(self): + def channels(self) -> Dict[bytes, Channel]: return self.lnworker.channels_for_peer(self.peer_addr.pubkey) def diagnostic_name(self): t@@ -877,8 +877,7 @@ class Peer(PrintError): failure_msg, sender_idx = decode_onion_error(error_reason, [x.node_id for x in route], chan.onion_keys[htlc_id]) - code = OnionFailureCode(failure_msg.code) - data = failure_msg.data + code, data = failure_msg.code, failure_msg.data self.print_error("UPDATE_FAIL_HTLC", repr(code), data) self.print_error(f"error reported by {bh2u(route[sender_idx].node_id)}") # handle some specific error codes t@@ -1000,30 +999,85 @@ class Peer(PrintError): self.print_error('on_update_add_htlc') # check if this in our list of requests payment_hash = payload["payment_hash"] - preimage, invoice = self.lnworker.get_invoice(payment_hash) - expected_received_msat = int(invoice.amount * bitcoin.COIN * 1000) channel_id = payload['channel_id'] htlc_id = int.from_bytes(payload["id"], 'big') cltv_expiry = int.from_bytes(payload["cltv_expiry"], 'big') - amount_msat = int.from_bytes(payload["amount_msat"], 'big') + amount_msat_htlc = int.from_bytes(payload["amount_msat"], 'big') + onion_packet = OnionPacket.from_bytes(payload["onion_routing_packet"]) + processed_onion = process_onion_packet(onion_packet, associated_data=payment_hash, our_onion_private_key=self.privkey) chan = self.channels[channel_id] - assert htlc_id == chan.config[REMOTE].next_htlc_id, (htlc_id, chan.config[REMOTE].next_htlc_id) assert chan.get_state() == "OPEN" - # TODO verify sanity of their cltv expiry - assert amount_msat == expected_received_msat - htlc = {'amount_msat': amount_msat, 'payment_hash':payment_hash, 'cltv_expiry':cltv_expiry} + assert htlc_id == chan.config[REMOTE].next_htlc_id, (htlc_id, chan.config[REMOTE].next_htlc_id) # TODO fail channel instead + if cltv_expiry >= 500_000_000: + pass # TODO fail the channel + # add htlc + htlc = {'amount_msat': amount_msat_htlc, 'payment_hash':payment_hash, 'cltv_expiry':cltv_expiry} chan.receive_htlc(htlc) assert (await self.receive_commitment(chan)) <= 1 self.revoke(chan) self.send_commitment(chan) await self.receive_revoke(chan) + # maybe fail htlc + if not processed_onion.are_we_final: + # no forwarding for now + reason = OnionRoutingFailureMessage(code=OnionFailureCode.PERMANENT_CHANNEL_FAILURE, data=b'') + await self.fail_htlc(chan, htlc_id, onion_packet, reason) + return + try: + preimage, invoice = self.lnworker.get_invoice(payment_hash) + except UnknownPaymentHash: + reason = OnionRoutingFailureMessage(code=OnionFailureCode.UNKNOWN_PAYMENT_HASH, data=b'') + await self.fail_htlc(chan, htlc_id, onion_packet, reason) + return + expected_received_msat = int(invoice.amount * bitcoin.COIN * 1000) if invoice.amount is not None else None + if expected_received_msat is not None and \ + (amount_msat_htlc < expected_received_msat or amount_msat_htlc > 2 * expected_received_msat): + reason = OnionRoutingFailureMessage(code=OnionFailureCode.INCORRECT_PAYMENT_AMOUNT, data=b'') + await self.fail_htlc(chan, htlc_id, onion_packet, reason) + return + local_height = self.network.get_local_height() + if local_height + MIN_FINAL_CLTV_EXPIRY_ACCEPTED > cltv_expiry: + reason = OnionRoutingFailureMessage(code=OnionFailureCode.FINAL_EXPIRY_TOO_SOON, data=b'') + await self.fail_htlc(chan, htlc_id, onion_packet, reason) + return + cltv_from_onion = int.from_bytes(processed_onion.hop_data.per_hop.outgoing_cltv_value, byteorder="big") + if cltv_from_onion != cltv_expiry: + reason = OnionRoutingFailureMessage(code=OnionFailureCode.FINAL_INCORRECT_CLTV_EXPIRY, + data=cltv_expiry.to_bytes(4, byteorder="big")) + await self.fail_htlc(chan, htlc_id, onion_packet, reason) + return + amount_from_onion = int.from_bytes(processed_onion.hop_data.per_hop.amt_to_forward, byteorder="big") + if amount_from_onion > amount_msat_htlc: + reason = OnionRoutingFailureMessage(code=OnionFailureCode.FINAL_INCORRECT_HTLC_AMOUNT, + data=amount_msat_htlc.to_bytes(8, byteorder="big")) + await self.fail_htlc(chan, htlc_id, onion_packet, reason) + return + # settle htlc + await self.settle_htlc(chan, htlc_id, preimage) + + async def settle_htlc(self, chan: Channel, htlc_id: int, preimage: bytes): chan.settle_htlc(preimage, htlc_id) - await self.update_channel(chan, "update_fulfill_htlc", channel_id=channel_id, id=htlc_id, payment_preimage=preimage) + await self.update_channel(chan, "update_fulfill_htlc", + channel_id=chan.channel_id, + id=htlc_id, + payment_preimage=preimage) self.lnworker.save_channel(chan) self.network.trigger_callback('ln_message', self.lnworker, 'Payment received') + async def fail_htlc(self, chan: Channel, htlc_id: int, onion_packet: OnionPacket, + reason: OnionRoutingFailureMessage): + self.print_error(f"failing received htlc {(bh2u(chan.channel_id), htlc_id)}. reason: {reason}") + chan.fail_htlc(htlc_id) + error_packet = construct_onion_error(reason, onion_packet, our_onion_private_key=self.privkey) + await self.update_channel(chan, "update_fail_htlc", + channel_id=chan.channel_id, + id=htlc_id, + len=len(error_packet), + reason=error_packet) + self.lnworker.save_channel(chan) + def on_revoke_and_ack(self, payload): - print("got revoke_and_ack") + self.print_error("got revoke_and_ack") channel_id = payload["channel_id"] self.revoke_and_ack[channel_id].put_nowait(payload) DIR diff --git a/electrum/lnchan.py b/electrum/lnchan.py t@@ -533,13 +533,17 @@ class Channel(PrintError): self.log[LOCAL]['settles'].append(htlc_id) # not saving preimage because it's already saved in LNWorker.invoices - def receive_htlc_settle(self, preimage, htlc_index): + def receive_htlc_settle(self, preimage, htlc_id): self.print_error("receive_htlc_settle") - htlc = self.log[LOCAL]['adds'][htlc_index] + htlc = self.log[LOCAL]['adds'][htlc_id] assert htlc.payment_hash == sha256(preimage) - self.log[REMOTE]['settles'].append(htlc_index) + self.log[REMOTE]['settles'].append(htlc_id) # we don't save the preimage because we don't need to forward it anyway + def fail_htlc(self, htlc_id): + self.print_error("fail_htlc") + self.log[REMOTE]['adds'].pop(htlc_id) + def receive_fail_htlc(self, htlc_id): self.print_error("receive_fail_htlc") self.log[LOCAL]['adds'].pop(htlc_id) DIR diff --git a/electrum/lnonion.py b/electrum/lnonion.py t@@ -24,9 +24,7 @@ # SOFTWARE. import hashlib -import hmac -from collections import namedtuple -from typing import Sequence, List, Tuple +from typing import Sequence, List, Tuple, NamedTuple from enum import IntEnum, IntFlag from cryptography.hazmat.primitives.ciphers import Cipher, algorithms t@@ -231,7 +229,9 @@ def generate_cipher_stream(stream_key: bytes, num_bytes: int) -> bytes: return encryptor.update(bytes(num_bytes)) -ProcessedOnionPacket = namedtuple("ProcessedOnionPacket", ["are_we_final", "hop_data", "next_packet"]) +ProcessedOnionPacket = NamedTuple("ProcessedOnionPacket", [("are_we_final", bool), + ("hop_data", OnionHopsDataSingle), + ("next_packet", OnionPacket)]) # TODO replay protection DIR diff --git a/electrum/lnutil.py b/electrum/lnutil.py t@@ -62,7 +62,13 @@ class LightningPeerConnectionClosed(LightningError): pass class UnableToDeriveSecret(LightningError): pass class HandshakeFailed(LightningError): pass class PaymentFailure(LightningError): pass -class ConnStringFormatError(LightningError): pass +class ConnStringFormatError(LightningError): pass +class UnknownPaymentHash(LightningError): pass + + +# TODO make configurable? +MIN_FINAL_CLTV_EXPIRY_ACCEPTED = 144 +MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE = MIN_FINAL_CLTV_EXPIRY_ACCEPTED + 1 class RevocationStore: DIR diff --git a/electrum/lnworker.py b/electrum/lnworker.py t@@ -24,8 +24,8 @@ from .lnchan import Channel from .lnutil import (Outpoint, calc_short_channel_id, LNPeerAddr, get_compressed_pubkey_from_bech32, extract_nodeid, PaymentFailure, split_host_port, ConnStringFormatError, - generate_keypair, LnKeyFamily) -from .lnutil import LOCAL, REMOTE + generate_keypair, LnKeyFamily, LOCAL, REMOTE, + UnknownPaymentHash, MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE) from .lnaddr import lndecode from .i18n import _ from .lnrouter import RouteEdge t@@ -318,20 +318,23 @@ class LNWorker(PrintError): if not routing_hints: self.print_error("Warning. No routing hints added to invoice. " "Other clients will likely not be able to send to us.") - pay_req = lnencode(LnAddr(RHASH, amount_btc, tags=[('d', message)]+routing_hints), + pay_req = lnencode(LnAddr(RHASH, amount_btc, + tags=[('d', message), + ('c', MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE)] + + routing_hints), self.node_keypair.privkey) self.invoices[bh2u(payment_preimage)] = pay_req self.wallet.storage.put('lightning_invoices', self.invoices) self.wallet.storage.write() return pay_req - def get_invoice(self, payment_hash): + def get_invoice(self, payment_hash: bytes) -> Tuple[bytes, LnAddr]: for k in self.invoices.keys(): preimage = bfh(k) if sha256(preimage) == payment_hash: return preimage, lndecode(self.invoices[k], expected_hrp=constants.net.SEGWIT_HRP) else: - raise Exception('unknown payment hash') + raise UnknownPaymentHash() def _calc_routing_hints_for_invoice(self, amount_sat): """calculate routing hints (BOLT-11 'r' field)"""