commit d511ecdc0073ba2af55a0c608ccd2b38f448ad8d
parent ded11b4d9ef637eb58471151c79bc499e59860e3
Author: SomberNight <somber.night@protonmail.com>
Date: Thu, 18 Oct 2018 22:56:40 +0200
start failing htlcs
Diffstat:
5 files changed, 96 insertions(+), 29 deletions(-)
diff --git a/electrum/lnbase.py b/electrum/lnbase.py
@@ -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
@@ -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
@@ -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):
@@ -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
@@ -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)
diff --git a/electrum/lnchan.py b/electrum/lnchan.py
@@ -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)
diff --git a/electrum/lnonion.py b/electrum/lnonion.py
@@ -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
@@ -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
diff --git a/electrum/lnutil.py b/electrum/lnutil.py
@@ -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:
diff --git a/electrum/lnworker.py b/electrum/lnworker.py
@@ -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
@@ -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)"""