URI: 
       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)"""