Browse Source

renames: use consistent naming of cltv delta vs cltv abs

to avoid confusing relative vs absolute cltvs
(see b0401a6386)
master
SomberNight 2 years ago
parent
commit
22a8348303
No known key found for this signature in database
GPG Key ID: B33B5F232C6271E9
  1. 10
      electrum/channel_db.py
  2. 2
      electrum/gui/qt/channel_details.py
  3. 4
      electrum/gui/qt/util.py
  4. 4
      electrum/lnaddr.py
  5. 14
      electrum/lnchannel.py
  6. 20
      electrum/lnonion.py
  7. 106
      electrum/lnpeer.py
  8. 19
      electrum/lnrouter.py
  9. 26
      electrum/lnsweep.py
  10. 78
      electrum/lnutil.py
  11. 4
      electrum/lnwatcher.py
  12. 52
      electrum/lnworker.py
  13. 8
      electrum/tests/test_bolt11.py
  14. 44
      electrum/tests/test_lnchannel.py
  15. 32
      electrum/tests/test_lnpeer.py
  16. 21
      electrum/tests/test_lnutil.py
  17. 36
      electrum/trampoline.py

10
electrum/channel_db.py

@ -103,7 +103,7 @@ class ChannelInfo(NamedTuple):
class Policy(NamedTuple): class Policy(NamedTuple):
key: bytes key: bytes
cltv_expiry_delta: int cltv_delta: int
htlc_minimum_msat: int htlc_minimum_msat: int
htlc_maximum_msat: Optional[int] htlc_maximum_msat: Optional[int]
fee_base_msat: int fee_base_msat: int
@ -116,7 +116,7 @@ class Policy(NamedTuple):
def from_msg(payload: dict) -> 'Policy': def from_msg(payload: dict) -> 'Policy':
return Policy( return Policy(
key = payload['short_channel_id'] + payload['start_node'], key = payload['short_channel_id'] + payload['start_node'],
cltv_expiry_delta = payload['cltv_expiry_delta'], cltv_delta = payload['cltv_expiry_delta'],
htlc_minimum_msat = payload['htlc_minimum_msat'], htlc_minimum_msat = payload['htlc_minimum_msat'],
htlc_maximum_msat = payload.get('htlc_maximum_msat', None), htlc_maximum_msat = payload.get('htlc_maximum_msat', None),
fee_base_msat = payload['fee_base_msat'], fee_base_msat = payload['fee_base_msat'],
@ -136,7 +136,7 @@ class Policy(NamedTuple):
def from_route_edge(route_edge: 'RouteEdge') -> 'Policy': def from_route_edge(route_edge: 'RouteEdge') -> 'Policy':
return Policy( return Policy(
key=route_edge.short_channel_id + route_edge.start_node, key=route_edge.short_channel_id + route_edge.start_node,
cltv_expiry_delta=route_edge.cltv_expiry_delta, cltv_delta=route_edge.cltv_delta,
htlc_minimum_msat=0, htlc_minimum_msat=0,
htlc_maximum_msat=None, htlc_maximum_msat=None,
fee_base_msat=route_edge.fee_base_msat, fee_base_msat=route_edge.fee_base_msat,
@ -441,10 +441,10 @@ class ChannelDB(SqlDB):
def policy_changed(self, old_policy: Policy, new_policy: Policy, verbose: bool) -> bool: def policy_changed(self, old_policy: Policy, new_policy: Policy, verbose: bool) -> bool:
changed = False changed = False
if old_policy.cltv_expiry_delta != new_policy.cltv_expiry_delta: if old_policy.cltv_delta != new_policy.cltv_delta:
changed |= True changed |= True
if verbose: if verbose:
self.logger.info(f'cltv_expiry_delta: {old_policy.cltv_expiry_delta} -> {new_policy.cltv_expiry_delta}') self.logger.info(f'cltv_expiry_delta: {old_policy.cltv_delta} -> {new_policy.cltv_delta}')
if old_policy.htlc_minimum_msat != new_policy.htlc_minimum_msat: if old_policy.htlc_minimum_msat != new_policy.htlc_minimum_msat:
changed |= True changed |= True
if verbose: if verbose:

2
electrum/gui/qt/channel_details.py

@ -85,7 +85,7 @@ class ChannelDetailsDialog(QtWidgets.QDialog, MessageBoxMixin, QtEventListener):
def make_htlc_item(self, i: UpdateAddHtlc, direction: Direction) -> HTLCItem: def make_htlc_item(self, i: UpdateAddHtlc, direction: Direction) -> HTLCItem:
it = HTLCItem(_('Sent HTLC with ID {}' if Direction.SENT == direction else 'Received HTLC with ID {}').format(i.htlc_id)) it = HTLCItem(_('Sent HTLC with ID {}' if Direction.SENT == direction else 'Received HTLC with ID {}').format(i.htlc_id))
it.appendRow([HTLCItem(_('Amount')),HTLCItem(self.format_msat(i.amount_msat))]) it.appendRow([HTLCItem(_('Amount')),HTLCItem(self.format_msat(i.amount_msat))])
it.appendRow([HTLCItem(_('CLTV expiry')),HTLCItem(str(i.cltv_expiry))]) it.appendRow([HTLCItem(_('CLTV expiry')), HTLCItem(str(i.cltv_abs))])
it.appendRow([HTLCItem(_('Payment hash')),HTLCItem(i.payment_hash.hex())]) it.appendRow([HTLCItem(_('Payment hash')),HTLCItem(i.payment_hash.hex())])
return it return it

4
electrum/gui/qt/util.py

@ -31,7 +31,7 @@ from PyQt5.QtWidgets import (QPushButton, QLabel, QMessageBox, QHBoxLayout,
from electrum.i18n import _, languages from electrum.i18n import _, languages
from electrum.util import FileImportFailed, FileExportFailed, make_aiohttp_session, resource_path from electrum.util import FileImportFailed, FileExportFailed, make_aiohttp_session, resource_path
from electrum.util import EventListener, event_listener from electrum.util import EventListener, event_listener, get_logger
from electrum.invoices import PR_UNPAID, PR_PAID, PR_EXPIRED, PR_INFLIGHT, PR_UNKNOWN, PR_FAILED, PR_ROUTING, PR_UNCONFIRMED, PR_BROADCASTING, PR_BROADCAST from electrum.invoices import PR_UNPAID, PR_PAID, PR_EXPIRED, PR_INFLIGHT, PR_UNKNOWN, PR_FAILED, PR_ROUTING, PR_UNCONFIRMED, PR_BROADCASTING, PR_BROADCAST
from electrum.logging import Logger from electrum.logging import Logger
from electrum.qrreader import MissingQrDetectionLib from electrum.qrreader import MissingQrDetectionLib
@ -52,6 +52,8 @@ else:
MONOSPACE_FONT = 'monospace' MONOSPACE_FONT = 'monospace'
_logger = get_logger(__name__)
dialogs = [] dialogs = []
pr_icons = { pr_icons = {

4
electrum/lnaddr.py

@ -336,7 +336,7 @@ class LnAddr(object):
", ".join([k + '=' + str(v) for k, v in self.tags]) ", ".join([k + '=' + str(v) for k, v in self.tags])
) )
def get_min_final_cltv_expiry(self) -> int: def get_min_final_cltv_delta(self) -> int:
cltv = self.get_tag('c') cltv = self.get_tag('c')
if cltv is None: if cltv is None:
return 18 return 18
@ -375,7 +375,7 @@ class LnAddr(object):
'description': self.get_description(), 'description': self.get_description(),
'exp': self.get_expiry(), 'exp': self.get_expiry(),
'time': self.date, 'time': self.date,
'min_final_cltv_expiry': self.get_min_final_cltv_expiry(), 'min_final_cltv_delta': self.get_min_final_cltv_delta(),
'features': self.get_features().get_names(), 'features': self.get_features().get_names(),
'tags': self.tags, 'tags': self.tags,
'unknown_tags': self.unknown_tags, 'unknown_tags': self.unknown_tags,

14
electrum/lnchannel.py

@ -631,7 +631,7 @@ class Channel(AbstractChannel):
# TODO enforce this ^ # TODO enforce this ^
# our forwarding parameters for forwarding HTLCs through this channel # our forwarding parameters for forwarding HTLCs through this channel
forwarding_cltv_expiry_delta = 144 forwarding_cltv_delta = 144
forwarding_fee_base_msat = 1000 forwarding_fee_base_msat = 1000
forwarding_fee_proportional_millionths = 1 forwarding_fee_proportional_millionths = 1
@ -784,7 +784,7 @@ class Channel(AbstractChannel):
short_channel_id=scid, short_channel_id=scid,
channel_flags=channel_flags, channel_flags=channel_flags,
message_flags=b'\x01', message_flags=b'\x01',
cltv_expiry_delta=self.forwarding_cltv_expiry_delta, cltv_expiry_delta=self.forwarding_cltv_delta,
htlc_minimum_msat=self.config[REMOTE].htlc_minimum_msat, htlc_minimum_msat=self.config[REMOTE].htlc_minimum_msat,
htlc_maximum_msat=htlc_maximum_msat, htlc_maximum_msat=htlc_maximum_msat,
fee_base_msat=self.forwarding_fee_base_msat, fee_base_msat=self.forwarding_fee_base_msat,
@ -1549,7 +1549,7 @@ class Channel(AbstractChannel):
remote_htlc_pubkey=other_htlc_pubkey, remote_htlc_pubkey=other_htlc_pubkey,
local_htlc_pubkey=this_htlc_pubkey, local_htlc_pubkey=this_htlc_pubkey,
payment_hash=htlc.payment_hash, payment_hash=htlc.payment_hash,
cltv_expiry=htlc.cltv_expiry), htlc)) cltv_abs=htlc.cltv_abs), htlc))
# note: maybe flip initiator here for fee purposes, we want LOCAL and REMOTE # note: maybe flip initiator here for fee purposes, we want LOCAL and REMOTE
# in the resulting dict to correspond to the to_local and to_remote *outputs* of the ctx # in the resulting dict to correspond to the to_local and to_remote *outputs* of the ctx
onchain_fees = calc_fees_for_commitment_tx( onchain_fees = calc_fees_for_commitment_tx(
@ -1656,24 +1656,24 @@ class Channel(AbstractChannel):
# If there is a received HTLC for which we already released the preimage # If there is a received HTLC for which we already released the preimage
# but the remote did not revoke yet, and the CLTV of this HTLC is dangerously close # but the remote did not revoke yet, and the CLTV of this HTLC is dangerously close
# to the present, then unilaterally close channel # to the present, then unilaterally close channel
recv_htlc_deadline = lnutil.NBLOCK_DEADLINE_BEFORE_EXPIRY_FOR_RECEIVED_HTLCS recv_htlc_deadline_delta = lnutil.NBLOCK_DEADLINE_DELTA_BEFORE_EXPIRY_FOR_RECEIVED_HTLCS
for sub, dir, ctn in ((LOCAL, RECEIVED, self.get_latest_ctn(LOCAL)), for sub, dir, ctn in ((LOCAL, RECEIVED, self.get_latest_ctn(LOCAL)),
(REMOTE, SENT, self.get_oldest_unrevoked_ctn(REMOTE)), (REMOTE, SENT, self.get_oldest_unrevoked_ctn(REMOTE)),
(REMOTE, SENT, self.get_latest_ctn(REMOTE)),): (REMOTE, SENT, self.get_latest_ctn(REMOTE)),):
for htlc_id, htlc in self.hm.htlcs_by_direction(subject=sub, direction=dir, ctn=ctn).items(): for htlc_id, htlc in self.hm.htlcs_by_direction(subject=sub, direction=dir, ctn=ctn).items():
if not self.hm.was_htlc_preimage_released(htlc_id=htlc_id, htlc_proposer=REMOTE): if not self.hm.was_htlc_preimage_released(htlc_id=htlc_id, htlc_proposer=REMOTE):
continue continue
if htlc.cltv_expiry - recv_htlc_deadline > local_height: if htlc.cltv_abs - recv_htlc_deadline_delta > local_height:
continue continue
htlcs_we_could_reclaim[(RECEIVED, htlc_id)] = htlc htlcs_we_could_reclaim[(RECEIVED, htlc_id)] = htlc
# If there is an offered HTLC which has already expired (+ some grace period after), we # If there is an offered HTLC which has already expired (+ some grace period after), we
# will unilaterally close the channel and time out the HTLC # will unilaterally close the channel and time out the HTLC
offered_htlc_deadline = lnutil.NBLOCK_DEADLINE_AFTER_EXPIRY_FOR_OFFERED_HTLCS offered_htlc_deadline_delta = lnutil.NBLOCK_DEADLINE_DELTA_AFTER_EXPIRY_FOR_OFFERED_HTLCS
for sub, dir, ctn in ((LOCAL, SENT, self.get_latest_ctn(LOCAL)), for sub, dir, ctn in ((LOCAL, SENT, self.get_latest_ctn(LOCAL)),
(REMOTE, RECEIVED, self.get_oldest_unrevoked_ctn(REMOTE)), (REMOTE, RECEIVED, self.get_oldest_unrevoked_ctn(REMOTE)),
(REMOTE, RECEIVED, self.get_latest_ctn(REMOTE)),): (REMOTE, RECEIVED, self.get_latest_ctn(REMOTE)),):
for htlc_id, htlc in self.hm.htlcs_by_direction(subject=sub, direction=dir, ctn=ctn).items(): for htlc_id, htlc in self.hm.htlcs_by_direction(subject=sub, direction=dir, ctn=ctn).items():
if htlc.cltv_expiry + offered_htlc_deadline > local_height: if htlc.cltv_abs + offered_htlc_deadline_delta > local_height:
continue continue
htlcs_we_could_reclaim[(SENT, htlc_id)] = htlc htlcs_we_could_reclaim[(SENT, htlc_id)] = htlc

20
electrum/lnonion.py

@ -212,23 +212,23 @@ def new_onion_packet(
def calc_hops_data_for_payment( def calc_hops_data_for_payment(
route: 'LNPaymentRoute', route: 'LNPaymentRoute',
amount_msat: int, amount_msat: int, # that final recipient receives
final_cltv: int, *, *,
final_cltv_abs: int,
total_msat: int, total_msat: int,
payment_secret: bytes, payment_secret: bytes,
) -> Tuple[List[OnionHopsDataSingle], int, int]: ) -> Tuple[List[OnionHopsDataSingle], int, int]:
"""Returns the hops_data to be used for constructing an onion packet, """Returns the hops_data to be used for constructing an onion packet,
and the amount_msat and cltv to be used on our immediate channel. and the amount_msat and cltv_abs to be used on our immediate channel.
""" """
if len(route) > NUM_MAX_EDGES_IN_PAYMENT_PATH: if len(route) > NUM_MAX_EDGES_IN_PAYMENT_PATH:
raise PaymentFailure(f"too long route ({len(route)} edges)") raise PaymentFailure(f"too long route ({len(route)} edges)")
# payload that will be seen by the last hop: # payload that will be seen by the last hop:
amt = amount_msat amt = amount_msat
cltv = final_cltv cltv_abs = final_cltv_abs
hop_payload = { hop_payload = {
"amt_to_forward": {"amt_to_forward": amt}, "amt_to_forward": {"amt_to_forward": amt},
"outgoing_cltv_value": {"outgoing_cltv_value": cltv}, "outgoing_cltv_value": {"outgoing_cltv_value": cltv_abs},
} }
# for multipart payments we need to tell the receiver about the total and # for multipart payments we need to tell the receiver about the total and
# partial amounts # partial amounts
@ -244,19 +244,19 @@ def calc_hops_data_for_payment(
is_trampoline = route_edge.is_trampoline() is_trampoline = route_edge.is_trampoline()
if is_trampoline: if is_trampoline:
amt += route_edge.fee_for_edge(amt) amt += route_edge.fee_for_edge(amt)
cltv += route_edge.cltv_expiry_delta cltv_abs += route_edge.cltv_delta
hop_payload = { hop_payload = {
"amt_to_forward": {"amt_to_forward": amt}, "amt_to_forward": {"amt_to_forward": amt},
"outgoing_cltv_value": {"outgoing_cltv_value": cltv}, "outgoing_cltv_value": {"outgoing_cltv_value": cltv_abs},
"short_channel_id": {"short_channel_id": route_edge.short_channel_id}, "short_channel_id": {"short_channel_id": route_edge.short_channel_id},
} }
hops_data.append( hops_data.append(
OnionHopsDataSingle(payload=hop_payload)) OnionHopsDataSingle(payload=hop_payload))
if not is_trampoline: if not is_trampoline:
amt += route_edge.fee_for_edge(amt) amt += route_edge.fee_for_edge(amt)
cltv += route_edge.cltv_expiry_delta cltv_abs += route_edge.cltv_delta
hops_data.reverse() hops_data.reverse()
return hops_data, amt, cltv return hops_data, amt, cltv_abs
def _generate_filler(key_type: bytes, hops_data: Sequence[OnionHopsDataSingle], def _generate_filler(key_type: bytes, hops_data: Sequence[OnionHopsDataSingle],

106
electrum/lnpeer.py

@ -38,7 +38,7 @@ from .lnutil import (Outpoint, LocalConfig, RECEIVED, UpdateAddHtlc, ChannelConf
funding_output_script, get_per_commitment_secret_from_seed, funding_output_script, get_per_commitment_secret_from_seed,
secret_to_pubkey, PaymentFailure, LnFeatures, secret_to_pubkey, PaymentFailure, LnFeatures,
LOCAL, REMOTE, HTLCOwner, LOCAL, REMOTE, HTLCOwner,
ln_compare_features, privkey_to_pubkey, MIN_FINAL_CLTV_EXPIRY_ACCEPTED, ln_compare_features, privkey_to_pubkey, MIN_FINAL_CLTV_DELTA_ACCEPTED,
LightningPeerConnectionClosed, HandshakeFailed, LightningPeerConnectionClosed, HandshakeFailed,
RemoteMisbehaving, ShortChannelID, RemoteMisbehaving, ShortChannelID,
IncompatibleLightningFeatures, derive_payment_secret_from_payment_preimage, IncompatibleLightningFeatures, derive_payment_secret_from_payment_preimage,
@ -1476,25 +1476,25 @@ class Peer(Logger):
amount_msat: int, amount_msat: int,
total_msat: int, total_msat: int,
payment_hash: bytes, payment_hash: bytes,
min_final_cltv_expiry: int, min_final_cltv_delta: int,
payment_secret: bytes, payment_secret: bytes,
trampoline_onion: Optional[OnionPacket] = None, trampoline_onion: Optional[OnionPacket] = None,
): ):
# add features learned during "init" for direct neighbour: # add features learned during "init" for direct neighbour:
route[0].node_features |= self.features route[0].node_features |= self.features
local_height = self.network.get_local_height() local_height = self.network.get_local_height()
final_cltv = local_height + min_final_cltv_expiry final_cltv_abs = local_height + min_final_cltv_delta
hops_data, amount_msat, cltv = calc_hops_data_for_payment( hops_data, amount_msat, cltv_abs = calc_hops_data_for_payment(
route, route,
amount_msat, amount_msat,
final_cltv, final_cltv_abs=final_cltv_abs,
total_msat=total_msat, total_msat=total_msat,
payment_secret=payment_secret) payment_secret=payment_secret)
num_hops = len(hops_data) num_hops = len(hops_data)
self.logger.info(f"lnpeer.pay len(route)={len(route)}") self.logger.info(f"lnpeer.pay len(route)={len(route)}")
for i in range(len(route)): for i in range(len(route)):
self.logger.info(f" {i}: edge={route[i].short_channel_id} hop_data={hops_data[i]!r}") self.logger.info(f" {i}: edge={route[i].short_channel_id} hop_data={hops_data[i]!r}")
assert final_cltv <= cltv, (final_cltv, cltv) assert final_cltv_abs <= cltv_abs, (final_cltv_abs, cltv_abs)
session_key = os.urandom(32) # session_key session_key = os.urandom(32) # session_key
# if we are forwarding a trampoline payment, add trampoline onion # if we are forwarding a trampoline payment, add trampoline onion
if trampoline_onion: if trampoline_onion:
@ -1517,12 +1517,21 @@ class Peer(Logger):
onion = new_onion_packet(payment_path_pubkeys, session_key, hops_data, associated_data=payment_hash) # must use another sessionkey onion = new_onion_packet(payment_path_pubkeys, session_key, hops_data, associated_data=payment_hash) # must use another sessionkey
self.logger.info(f"starting payment. len(route)={len(hops_data)}.") self.logger.info(f"starting payment. len(route)={len(hops_data)}.")
# create htlc # create htlc
if cltv > local_height + lnutil.NBLOCK_CLTV_EXPIRY_TOO_FAR_INTO_FUTURE: if cltv_abs > local_height + lnutil.NBLOCK_CLTV_DELTA_TOO_FAR_INTO_FUTURE:
raise PaymentFailure(f"htlc expiry too far into future. (in {cltv-local_height} blocks)") raise PaymentFailure(f"htlc expiry too far into future. (in {cltv_abs-local_height} blocks)")
return onion, amount_msat, cltv, session_key return onion, amount_msat, cltv_abs, session_key
def send_htlc(self, chan, payment_hash, amount_msat, cltv, onion, session_key=None) -> UpdateAddHtlc: def send_htlc(
htlc = UpdateAddHtlc(amount_msat=amount_msat, payment_hash=payment_hash, cltv_expiry=cltv, timestamp=int(time.time())) self,
*,
chan: Channel,
payment_hash: bytes,
amount_msat: int,
cltv_abs: int,
onion: OnionPacket,
session_key: Optional[bytes] = None,
) -> UpdateAddHtlc:
htlc = UpdateAddHtlc(amount_msat=amount_msat, payment_hash=payment_hash, cltv_abs=cltv_abs, timestamp=int(time.time()))
htlc = chan.add_htlc(htlc) htlc = chan.add_htlc(htlc)
if session_key: if session_key:
chan.set_onion_key(htlc.htlc_id, session_key) # should it be the outer onion secret? chan.set_onion_key(htlc.htlc_id, session_key) # should it be the outer onion secret?
@ -1531,7 +1540,7 @@ class Peer(Logger):
"update_add_htlc", "update_add_htlc",
channel_id=chan.channel_id, channel_id=chan.channel_id,
id=htlc.htlc_id, id=htlc.htlc_id,
cltv_expiry=htlc.cltv_expiry, cltv_expiry=htlc.cltv_abs,
amount_msat=htlc.amount_msat, amount_msat=htlc.amount_msat,
payment_hash=htlc.payment_hash, payment_hash=htlc.payment_hash,
onion_routing_packet=onion.to_bytes()) onion_routing_packet=onion.to_bytes())
@ -1544,7 +1553,7 @@ class Peer(Logger):
amount_msat: int, amount_msat: int,
total_msat: int, total_msat: int,
payment_hash: bytes, payment_hash: bytes,
min_final_cltv_expiry: int, min_final_cltv_delta: int,
payment_secret: bytes, payment_secret: bytes,
trampoline_onion: Optional[OnionPacket] = None, trampoline_onion: Optional[OnionPacket] = None,
) -> UpdateAddHtlc: ) -> UpdateAddHtlc:
@ -1553,16 +1562,23 @@ class Peer(Logger):
assert len(route) > 0 assert len(route) > 0
if not chan.can_send_update_add_htlc(): if not chan.can_send_update_add_htlc():
raise PaymentFailure("Channel cannot send update_add_htlc") raise PaymentFailure("Channel cannot send update_add_htlc")
onion, amount_msat, cltv, session_key = self.create_onion_for_route( onion, amount_msat, cltv_abs, session_key = self.create_onion_for_route(
route=route, route=route,
amount_msat=amount_msat, amount_msat=amount_msat,
total_msat=total_msat, total_msat=total_msat,
payment_hash=payment_hash, payment_hash=payment_hash,
min_final_cltv_expiry=min_final_cltv_expiry, min_final_cltv_delta=min_final_cltv_delta,
payment_secret=payment_secret, payment_secret=payment_secret,
trampoline_onion=trampoline_onion trampoline_onion=trampoline_onion
) )
htlc = self.send_htlc(chan, payment_hash, amount_msat, cltv, onion, session_key=session_key) htlc = self.send_htlc(
chan=chan,
payment_hash=payment_hash,
amount_msat=amount_msat,
cltv_abs=cltv_abs,
onion=onion,
session_key=session_key,
)
return htlc return htlc
def send_revoke_and_ack(self, chan: Channel): def send_revoke_and_ack(self, chan: Channel):
@ -1619,21 +1635,21 @@ class Peer(Logger):
def on_update_add_htlc(self, chan: Channel, payload): def on_update_add_htlc(self, chan: Channel, payload):
payment_hash = payload["payment_hash"] payment_hash = payload["payment_hash"]
htlc_id = payload["id"] htlc_id = payload["id"]
cltv_expiry = payload["cltv_expiry"] cltv_abs = payload["cltv_expiry"]
amount_msat_htlc = payload["amount_msat"] amount_msat_htlc = payload["amount_msat"]
onion_packet = payload["onion_routing_packet"] onion_packet = payload["onion_routing_packet"]
htlc = UpdateAddHtlc( htlc = UpdateAddHtlc(
amount_msat=amount_msat_htlc, amount_msat=amount_msat_htlc,
payment_hash=payment_hash, payment_hash=payment_hash,
cltv_expiry=cltv_expiry, cltv_abs=cltv_abs,
timestamp=int(time.time()), timestamp=int(time.time()),
htlc_id=htlc_id) htlc_id=htlc_id)
self.logger.info(f"on_update_add_htlc. chan {chan.short_channel_id}. htlc={str(htlc)}") self.logger.info(f"on_update_add_htlc. chan {chan.short_channel_id}. htlc={str(htlc)}")
if chan.get_state() != ChannelState.OPEN: if chan.get_state() != ChannelState.OPEN:
raise RemoteMisbehaving(f"received update_add_htlc while chan.get_state() != OPEN. state was {chan.get_state()!r}") raise RemoteMisbehaving(f"received update_add_htlc while chan.get_state() != OPEN. state was {chan.get_state()!r}")
if cltv_expiry > bitcoin.NLOCKTIME_BLOCKHEIGHT_MAX: if cltv_abs > bitcoin.NLOCKTIME_BLOCKHEIGHT_MAX:
self.schedule_force_closing(chan.channel_id) self.schedule_force_closing(chan.channel_id)
raise RemoteMisbehaving(f"received update_add_htlc with cltv_expiry > BLOCKHEIGHT_MAX. value was {cltv_expiry}") raise RemoteMisbehaving(f"received update_add_htlc with {cltv_abs=} > BLOCKHEIGHT_MAX")
# add htlc # add htlc
chan.receive_htlc(htlc, onion_packet) chan.receive_htlc(htlc, onion_packet)
util.trigger_callback('htlc_added', chan, htlc, RECEIVED) util.trigger_callback('htlc_added', chan, htlc, RECEIVED)
@ -1674,7 +1690,7 @@ class Peer(Logger):
except Exception: except Exception:
raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00') raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00')
try: try:
next_cltv_expiry = processed_onion.hop_data.payload["outgoing_cltv_value"]["outgoing_cltv_value"] next_cltv_abs = processed_onion.hop_data.payload["outgoing_cltv_value"]["outgoing_cltv_value"]
except Exception: except Exception:
raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00') raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00')
next_chan = self.lnworker.get_channel_by_short_id(next_chan_scid) next_chan = self.lnworker.get_channel_by_short_id(next_chan_scid)
@ -1693,16 +1709,16 @@ class Peer(Logger):
if not next_chan.can_pay(next_amount_msat_htlc): if not next_chan.can_pay(next_amount_msat_htlc):
log_fail_reason(f"transient error (likely due to insufficient funds): not next_chan.can_pay(amt)") log_fail_reason(f"transient error (likely due to insufficient funds): not next_chan.can_pay(amt)")
raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_CHANNEL_FAILURE, data=outgoing_chan_upd_message) raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_CHANNEL_FAILURE, data=outgoing_chan_upd_message)
if htlc.cltv_expiry - next_cltv_expiry < next_chan.forwarding_cltv_expiry_delta: if htlc.cltv_abs - next_cltv_abs < next_chan.forwarding_cltv_delta:
log_fail_reason( log_fail_reason(
f"INCORRECT_CLTV_EXPIRY. " f"INCORRECT_CLTV_EXPIRY. "
f"{htlc.cltv_expiry=} - {next_cltv_expiry=} < {next_chan.forwarding_cltv_expiry_delta=}") f"{htlc.cltv_abs=} - {next_cltv_abs=} < {next_chan.forwarding_cltv_delta=}")
data = htlc.cltv_expiry.to_bytes(4, byteorder="big") + outgoing_chan_upd_message data = htlc.cltv_abs.to_bytes(4, byteorder="big") + outgoing_chan_upd_message
raise OnionRoutingFailure(code=OnionFailureCode.INCORRECT_CLTV_EXPIRY, data=data) raise OnionRoutingFailure(code=OnionFailureCode.INCORRECT_CLTV_EXPIRY, data=data)
if htlc.cltv_expiry - lnutil.MIN_FINAL_CLTV_EXPIRY_ACCEPTED <= local_height \ if htlc.cltv_abs - lnutil.MIN_FINAL_CLTV_DELTA_ACCEPTED <= local_height \
or next_cltv_expiry <= local_height: or next_cltv_abs <= local_height:
raise OnionRoutingFailure(code=OnionFailureCode.EXPIRY_TOO_SOON, data=outgoing_chan_upd_message) raise OnionRoutingFailure(code=OnionFailureCode.EXPIRY_TOO_SOON, data=outgoing_chan_upd_message)
if max(htlc.cltv_expiry, next_cltv_expiry) > local_height + lnutil.NBLOCK_CLTV_EXPIRY_TOO_FAR_INTO_FUTURE: if max(htlc.cltv_abs, next_cltv_abs) > local_height + lnutil.NBLOCK_CLTV_DELTA_TOO_FAR_INTO_FUTURE:
raise OnionRoutingFailure(code=OnionFailureCode.EXPIRY_TOO_FAR, data=b'') raise OnionRoutingFailure(code=OnionFailureCode.EXPIRY_TOO_FAR, data=b'')
forwarding_fees = fee_for_edge_msat( forwarding_fees = fee_for_edge_msat(
forwarded_amount_msat=next_amount_msat_htlc, forwarded_amount_msat=next_amount_msat_htlc,
@ -1722,7 +1738,13 @@ class Peer(Logger):
log_fail_reason(f"next_peer offline ({next_chan.node_id.hex()})") log_fail_reason(f"next_peer offline ({next_chan.node_id.hex()})")
raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_CHANNEL_FAILURE, data=outgoing_chan_upd_message) raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_CHANNEL_FAILURE, data=outgoing_chan_upd_message)
try: try:
next_htlc = next_peer.send_htlc(next_chan, htlc.payment_hash, next_amount_msat_htlc, next_cltv_expiry, processed_onion.next_packet) next_htlc = next_peer.send_htlc(
chan=next_chan,
payment_hash=htlc.payment_hash,
amount_msat=next_amount_msat_htlc,
cltv_abs=next_cltv_abs,
onion=processed_onion.next_packet,
)
except BaseException as e: except BaseException as e:
log_fail_reason(f"error sending message to next_peer={next_chan.node_id.hex()}") log_fail_reason(f"error sending message to next_peer={next_chan.node_id.hex()}")
raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_CHANNEL_FAILURE, data=outgoing_chan_upd_message) raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_CHANNEL_FAILURE, data=outgoing_chan_upd_message)
@ -1732,7 +1754,7 @@ class Peer(Logger):
async def maybe_forward_trampoline( async def maybe_forward_trampoline(
self, *, self, *,
payment_hash: bytes, payment_hash: bytes,
cltv_expiry: int, inc_cltv_abs: int,
outer_onion: ProcessedOnionPacket, outer_onion: ProcessedOnionPacket,
trampoline_onion: ProcessedOnionPacket): trampoline_onion: ProcessedOnionPacket):
@ -1751,7 +1773,7 @@ class Peer(Logger):
try: try:
outgoing_node_id = payload["outgoing_node_id"]["outgoing_node_id"] outgoing_node_id = payload["outgoing_node_id"]["outgoing_node_id"]
amt_to_forward = payload["amt_to_forward"]["amt_to_forward"] amt_to_forward = payload["amt_to_forward"]["amt_to_forward"]
cltv_from_onion = payload["outgoing_cltv_value"]["outgoing_cltv_value"] out_cltv_abs = payload["outgoing_cltv_value"]["outgoing_cltv_value"]
if "invoice_features" in payload: if "invoice_features" in payload:
self.logger.info('forward_trampoline: legacy') self.logger.info('forward_trampoline: legacy')
next_trampoline_onion = None next_trampoline_onion = None
@ -1777,11 +1799,11 @@ class Peer(Logger):
# these are the fee/cltv paid by the sender # these are the fee/cltv paid by the sender
# pay_to_node will raise if they are not sufficient # pay_to_node will raise if they are not sufficient
trampoline_cltv_delta = cltv_expiry - cltv_from_onion # cltv budget trampoline_cltv_delta = inc_cltv_abs - out_cltv_abs # cltv budget
total_msat = outer_onion.hop_data.payload["payment_data"]["total_msat"] total_msat = outer_onion.hop_data.payload["payment_data"]["total_msat"]
trampoline_fee = total_msat - amt_to_forward trampoline_fee = total_msat - amt_to_forward
self.logger.info(f'trampoline forwarding. fee_budget={trampoline_fee}') self.logger.info(f'trampoline forwarding. fee_budget={trampoline_fee}')
self.logger.info(f'trampoline forwarding. cltv_budget={trampoline_cltv_delta}. (inc={cltv_expiry}. out={cltv_from_onion})') self.logger.info(f'trampoline forwarding. cltv_budget={trampoline_cltv_delta}. (inc={inc_cltv_abs}. out={out_cltv_abs})')
# To convert abs vs rel cltvs, we need to guess blockheight used by original sender as "current blockheight". # To convert abs vs rel cltvs, we need to guess blockheight used by original sender as "current blockheight".
# Blocks might have been mined since. # Blocks might have been mined since.
# - if we skew towards the past, we decrease our own cltv_budget accordingly (which is ok) # - if we skew towards the past, we decrease our own cltv_budget accordingly (which is ok)
@ -1789,7 +1811,7 @@ class Peer(Logger):
# which can result in them failing the payment. # which can result in them failing the payment.
# So we skew towards the past and guess that there has been 1 new block mined since the payment began: # So we skew towards the past and guess that there has been 1 new block mined since the payment began:
local_height_of_onion_creator = self.network.get_local_height() - 1 local_height_of_onion_creator = self.network.get_local_height() - 1
cltv_budget_for_rest_of_route = cltv_from_onion - local_height_of_onion_creator cltv_budget_for_rest_of_route = out_cltv_abs - local_height_of_onion_creator
try: try:
await self.lnworker.pay_to_node( await self.lnworker.pay_to_node(
@ -1797,10 +1819,10 @@ class Peer(Logger):
payment_hash=payment_hash, payment_hash=payment_hash,
payment_secret=payment_secret, payment_secret=payment_secret,
amount_to_pay=amt_to_forward, amount_to_pay=amt_to_forward,
# FIXME this API (min_cltv_expiry) is confusing. The value will be added to local_height # FIXME this API (min_final_cltv_delta) is confusing. The value will be added to local_height
# to form the abs cltv used on the last edge on the path to the *next trampoline* node. # to form the abs cltv used on the last edge on the path to the *next trampoline* node.
# We should rewrite pay_to_node to operate on a cltv-budget (and fee-budget). # We should rewrite pay_to_node to operate on a cltv-budget (and fee-budget).
min_cltv_expiry=cltv_budget_for_rest_of_route, min_final_cltv_delta=cltv_budget_for_rest_of_route,
r_tags=r_tags, r_tags=r_tags,
invoice_features=invoice_features, invoice_features=invoice_features,
fwd_trampoline_onion=next_trampoline_onion, fwd_trampoline_onion=next_trampoline_onion,
@ -1863,20 +1885,20 @@ class Peer(Logger):
exc_incorrect_or_unknown_pd = OnionRoutingFailure( exc_incorrect_or_unknown_pd = OnionRoutingFailure(
code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS,
data=amt_to_forward.to_bytes(8, byteorder="big") + local_height.to_bytes(4, byteorder="big")) data=amt_to_forward.to_bytes(8, byteorder="big") + local_height.to_bytes(4, byteorder="big"))
if local_height + MIN_FINAL_CLTV_EXPIRY_ACCEPTED > htlc.cltv_expiry: if local_height + MIN_FINAL_CLTV_DELTA_ACCEPTED > htlc.cltv_abs:
log_fail_reason(f"htlc.cltv_expiry is unreasonably close") log_fail_reason(f"htlc.cltv_abs is unreasonably close")
raise exc_incorrect_or_unknown_pd raise exc_incorrect_or_unknown_pd
try: try:
cltv_from_onion = processed_onion.hop_data.payload["outgoing_cltv_value"]["outgoing_cltv_value"] cltv_abs_from_onion = processed_onion.hop_data.payload["outgoing_cltv_value"]["outgoing_cltv_value"]
except Exception: except Exception:
log_fail_reason(f"'outgoing_cltv_value' missing from onion") log_fail_reason(f"'outgoing_cltv_value' missing from onion")
raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00') raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00')
if cltv_from_onion > htlc.cltv_expiry: if cltv_abs_from_onion > htlc.cltv_abs:
log_fail_reason(f"cltv_from_onion != htlc.cltv_expiry") log_fail_reason(f"cltv_abs_from_onion != htlc.cltv_abs")
raise OnionRoutingFailure( raise OnionRoutingFailure(
code=OnionFailureCode.FINAL_INCORRECT_CLTV_EXPIRY, code=OnionFailureCode.FINAL_INCORRECT_CLTV_EXPIRY,
data=htlc.cltv_expiry.to_bytes(4, byteorder="big")) data=htlc.cltv_abs.to_bytes(4, byteorder="big"))
try: try:
total_msat = processed_onion.hop_data.payload["payment_data"]["total_msat"] total_msat = processed_onion.hop_data.payload["payment_data"]["total_msat"]
except Exception: except Exception:
@ -1943,7 +1965,7 @@ class Peer(Logger):
else: else:
callback = lambda: self.maybe_forward_trampoline( callback = lambda: self.maybe_forward_trampoline(
payment_hash=payment_hash, payment_hash=payment_hash,
cltv_expiry=htlc.cltv_expiry, # TODO: use max or enforce same value across mpp parts inc_cltv_abs=htlc.cltv_abs, # TODO: use max or enforce same value across mpp parts
outer_onion=processed_onion, outer_onion=processed_onion,
trampoline_onion=trampoline_onion) trampoline_onion=trampoline_onion)
return None, callback return None, callback

19
electrum/lnrouter.py

@ -35,7 +35,7 @@ from math import inf
from .util import profiler, with_lock from .util import profiler, with_lock
from .logging import Logger from .logging import Logger
from .lnutil import (NUM_MAX_EDGES_IN_PAYMENT_PATH, ShortChannelID, LnFeatures, from .lnutil import (NUM_MAX_EDGES_IN_PAYMENT_PATH, ShortChannelID, LnFeatures,
NBLOCK_CLTV_EXPIRY_TOO_FAR_INTO_FUTURE) NBLOCK_CLTV_DELTA_TOO_FAR_INTO_FUTURE)
from .channel_db import ChannelDB, Policy, NodeInfo from .channel_db import ChannelDB, Policy, NodeInfo
if TYPE_CHECKING: if TYPE_CHECKING:
@ -75,7 +75,7 @@ class PathEdge:
class RouteEdge(PathEdge): class RouteEdge(PathEdge):
fee_base_msat = attr.ib(type=int, kw_only=True) fee_base_msat = attr.ib(type=int, kw_only=True)
fee_proportional_millionths = attr.ib(type=int, kw_only=True) fee_proportional_millionths = attr.ib(type=int, kw_only=True)
cltv_expiry_delta = attr.ib(type=int, kw_only=True) cltv_delta = attr.ib(type=int, kw_only=True)
node_features = attr.ib(type=int, kw_only=True, repr=lambda val: str(int(val))) # note: for end node! node_features = attr.ib(type=int, kw_only=True, repr=lambda val: str(int(val))) # note: for end node!
def fee_for_edge(self, amount_msat: int) -> int: def fee_for_edge(self, amount_msat: int) -> int:
@ -102,13 +102,13 @@ class RouteEdge(PathEdge):
short_channel_id=ShortChannelID.normalize(short_channel_id), short_channel_id=ShortChannelID.normalize(short_channel_id),
fee_base_msat=channel_policy.fee_base_msat, fee_base_msat=channel_policy.fee_base_msat,
fee_proportional_millionths=channel_policy.fee_proportional_millionths, fee_proportional_millionths=channel_policy.fee_proportional_millionths,
cltv_expiry_delta=channel_policy.cltv_expiry_delta, cltv_delta=channel_policy.cltv_delta,
node_features=node_info.features if node_info else 0) node_features=node_info.features if node_info else 0)
def is_sane_to_use(self, amount_msat: int) -> bool: def is_sane_to_use(self, amount_msat: int) -> bool:
# TODO revise ad-hoc heuristics # TODO revise ad-hoc heuristics
# cltv cannot be more than 2 weeks # cltv cannot be more than 2 weeks
if self.cltv_expiry_delta > 14 * 144: if self.cltv_delta > 14 * 144:
return False return False
total_fee = self.fee_for_edge(amount_msat) total_fee = self.fee_for_edge(amount_msat)
if not is_fee_sane(total_fee, payment_amount_msat=amount_msat): if not is_fee_sane(total_fee, payment_amount_msat=amount_msat):
@ -135,23 +135,24 @@ class TrampolineEdge(RouteEdge):
LNPaymentPath = Sequence[PathEdge] LNPaymentPath = Sequence[PathEdge]
LNPaymentRoute = Sequence[RouteEdge] LNPaymentRoute = Sequence[RouteEdge]
LNPaymentTRoute = Sequence[TrampolineEdge]
def is_route_sane_to_use(route: LNPaymentRoute, invoice_amount_msat: int, min_final_cltv_expiry: int) -> bool: def is_route_sane_to_use(route: LNPaymentRoute, invoice_amount_msat: int, min_final_cltv_delta: int) -> bool:
"""Run some sanity checks on the whole route, before attempting to use it. """Run some sanity checks on the whole route, before attempting to use it.
called when we are paying; so e.g. lower cltv is better called when we are paying; so e.g. lower cltv is better
""" """
if len(route) > NUM_MAX_EDGES_IN_PAYMENT_PATH: if len(route) > NUM_MAX_EDGES_IN_PAYMENT_PATH:
return False return False
amt = invoice_amount_msat amt = invoice_amount_msat
cltv = min_final_cltv_expiry cltv_delta = min_final_cltv_delta
for route_edge in reversed(route[1:]): for route_edge in reversed(route[1:]):
if not route_edge.is_sane_to_use(amt): return False if not route_edge.is_sane_to_use(amt): return False
amt += route_edge.fee_for_edge(amt) amt += route_edge.fee_for_edge(amt)
cltv += route_edge.cltv_expiry_delta cltv_delta += route_edge.cltv_delta
total_fee = amt - invoice_amount_msat total_fee = amt - invoice_amount_msat
# TODO revise ad-hoc heuristics # TODO revise ad-hoc heuristics
if cltv > NBLOCK_CLTV_EXPIRY_TOO_FAR_INTO_FUTURE: if cltv_delta > NBLOCK_CLTV_DELTA_TOO_FAR_INTO_FUTURE:
return False return False
# FIXME in case of MPP, the fee checks are done independently for each part, # FIXME in case of MPP, the fee checks are done independently for each part,
# which is ok for the proportional checks but not for the absolute ones. # which is ok for the proportional checks but not for the absolute ones.
@ -527,7 +528,7 @@ class LNPathFinder(Logger):
if ignore_costs: if ignore_costs:
return DEFAULT_PENALTY_BASE_MSAT, 0 return DEFAULT_PENALTY_BASE_MSAT, 0
fee_msat = route_edge.fee_for_edge(payment_amt_msat) fee_msat = route_edge.fee_for_edge(payment_amt_msat)
cltv_cost = route_edge.cltv_expiry_delta * payment_amt_msat * 15 / 1_000_000_000 cltv_cost = route_edge.cltv_delta * payment_amt_msat * 15 / 1_000_000_000
# the liquidty penalty takes care we favor edges that should be able to forward # the liquidty penalty takes care we favor edges that should be able to forward
# the payment and penalize edges that cannot # the payment and penalize edges that cannot
liquidity_penalty = self.liquidity_hints.penalty(start_node, end_node, short_channel_id, payment_amt_msat) liquidity_penalty = self.liquidity_hints.penalty(start_node, end_node, short_channel_id, payment_amt_msat)

26
electrum/lnsweep.py

@ -33,7 +33,7 @@ _logger = get_logger(__name__)
class SweepInfo(NamedTuple): class SweepInfo(NamedTuple):
name: str name: str
csv_delay: int csv_delay: int
cltv_expiry: int cltv_abs: int
gen_tx: Callable[[], Optional[Transaction]] gen_tx: Callable[[], Optional[Transaction]]
@ -177,7 +177,7 @@ def create_sweeptx_for_their_revoked_htlc(
return SweepInfo( return SweepInfo(
name='redeem_htlc2', name='redeem_htlc2',
csv_delay=0, csv_delay=0,
cltv_expiry=0, cltv_abs=0,
gen_tx=gen_tx) gen_tx=gen_tx)
@ -240,7 +240,7 @@ def create_sweeptxs_for_our_ctx(
txs[prevout] = SweepInfo( txs[prevout] = SweepInfo(
name='our_ctx_to_local', name='our_ctx_to_local',
csv_delay=to_self_delay, csv_delay=to_self_delay,
cltv_expiry=0, cltv_abs=0,
gen_tx=sweep_tx) gen_tx=sweep_tx)
we_breached = ctn < chan.get_oldest_unrevoked_ctn(LOCAL) we_breached = ctn < chan.get_oldest_unrevoked_ctn(LOCAL)
if we_breached: if we_breached:
@ -277,12 +277,12 @@ def create_sweeptxs_for_our_ctx(
txs[htlc_tx.inputs()[0].prevout.to_str()] = SweepInfo( txs[htlc_tx.inputs()[0].prevout.to_str()] = SweepInfo(
name='first-stage-htlc', name='first-stage-htlc',
csv_delay=0, csv_delay=0,
cltv_expiry=htlc_tx.locktime, cltv_abs=htlc_tx.locktime,
gen_tx=lambda: htlc_tx) gen_tx=lambda: htlc_tx)
txs[htlc_tx.txid() + ':0'] = SweepInfo( txs[htlc_tx.txid() + ':0'] = SweepInfo(
name='second-stage-htlc', name='second-stage-htlc',
csv_delay=to_self_delay, csv_delay=to_self_delay,
cltv_expiry=0, cltv_abs=0,
gen_tx=sweep_tx) gen_tx=sweep_tx)
# offered HTLCs, in our ctx --> "timeout" # offered HTLCs, in our ctx --> "timeout"
@ -381,7 +381,7 @@ def create_sweeptxs_for_their_ctx(
txs[tx.inputs()[0].prevout.to_str()] = SweepInfo( txs[tx.inputs()[0].prevout.to_str()] = SweepInfo(
name='to_local_for_revoked_ctx', name='to_local_for_revoked_ctx',
csv_delay=0, csv_delay=0,
cltv_expiry=0, cltv_abs=0,
gen_tx=gen_tx) gen_tx=gen_tx)
# prep # prep
our_htlc_privkey = derive_privkey(secret=int.from_bytes(our_conf.htlc_basepoint.privkey, 'big'), per_commitment_point=their_pcp) our_htlc_privkey = derive_privkey(secret=int.from_bytes(our_conf.htlc_basepoint.privkey, 'big'), per_commitment_point=their_pcp)
@ -400,9 +400,9 @@ def create_sweeptxs_for_their_ctx(
remote_htlc_pubkey=our_htlc_privkey.get_public_key_bytes(compressed=True), remote_htlc_pubkey=our_htlc_privkey.get_public_key_bytes(compressed=True),
local_htlc_pubkey=their_htlc_pubkey, local_htlc_pubkey=their_htlc_pubkey,
payment_hash=htlc.payment_hash, payment_hash=htlc.payment_hash,
cltv_expiry=htlc.cltv_expiry) cltv_abs=htlc.cltv_abs)
cltv_expiry = htlc.cltv_expiry if is_received_htlc and not is_revocation else 0 cltv_abs = htlc.cltv_abs if is_received_htlc and not is_revocation else 0
prevout = ctx.txid() + ':%d'%ctx_output_idx prevout = ctx.txid() + ':%d'%ctx_output_idx
sweep_tx = lambda: create_sweeptx_their_ctx_htlc( sweep_tx = lambda: create_sweeptx_their_ctx_htlc(
ctx=ctx, ctx=ctx,
@ -412,12 +412,12 @@ def create_sweeptxs_for_their_ctx(
output_idx=ctx_output_idx, output_idx=ctx_output_idx,
privkey=our_revocation_privkey if is_revocation else our_htlc_privkey.get_secret_bytes(), privkey=our_revocation_privkey if is_revocation else our_htlc_privkey.get_secret_bytes(),
is_revocation=is_revocation, is_revocation=is_revocation,
cltv_expiry=cltv_expiry, cltv_abs=cltv_abs,
config=chan.lnworker.config) config=chan.lnworker.config)
txs[prevout] = SweepInfo( txs[prevout] = SweepInfo(
name=f'their_ctx_htlc_{ctx_output_idx}', name=f'their_ctx_htlc_{ctx_output_idx}',
csv_delay=0, csv_delay=0,
cltv_expiry=cltv_expiry, cltv_abs=cltv_abs,
gen_tx=sweep_tx) gen_tx=sweep_tx)
# received HTLCs, in their ctx --> "timeout" # received HTLCs, in their ctx --> "timeout"
# offered HTLCs, in their ctx --> "success" # offered HTLCs, in their ctx --> "success"
@ -480,8 +480,8 @@ def create_sweeptx_their_ctx_htlc(
ctx: Transaction, witness_script: bytes, sweep_address: str, ctx: Transaction, witness_script: bytes, sweep_address: str,
preimage: Optional[bytes], output_idx: int, preimage: Optional[bytes], output_idx: int,
privkey: bytes, is_revocation: bool, privkey: bytes, is_revocation: bool,
cltv_expiry: int, config: SimpleConfig) -> Optional[PartialTransaction]: cltv_abs: int, config: SimpleConfig) -> Optional[PartialTransaction]:
assert type(cltv_expiry) is int assert type(cltv_abs) is int
preimage = preimage or b'' # preimage is required iff (not is_revocation and htlc is offered) preimage = preimage or b'' # preimage is required iff (not is_revocation and htlc is offered)
val = ctx.outputs()[output_idx].value val = ctx.outputs()[output_idx].value
prevout = TxOutpoint(txid=bfh(ctx.txid()), out_idx=output_idx) prevout = TxOutpoint(txid=bfh(ctx.txid()), out_idx=output_idx)
@ -495,7 +495,7 @@ def create_sweeptx_their_ctx_htlc(
outvalue = val - fee outvalue = val - fee
if outvalue <= dust_threshold(): return None if outvalue <= dust_threshold(): return None
sweep_outputs = [PartialTxOutput.from_address_and_value(sweep_address, outvalue)] sweep_outputs = [PartialTxOutput.from_address_and_value(sweep_address, outvalue)]
tx = PartialTransaction.from_io(sweep_inputs, sweep_outputs, version=2, locktime=cltv_expiry) tx = PartialTransaction.from_io(sweep_inputs, sweep_outputs, version=2, locktime=cltv_abs)
sig = bfh(tx.sign_txin(0, privkey)) sig = bfh(tx.sign_txin(0, privkey))
if not is_revocation: if not is_revocation:
witness = construct_witness([sig, preimage, witness_script]) witness = construct_witness([sig, preimage, witness_script])

78
electrum/lnutil.py

@ -446,20 +446,20 @@ MIN_FUNDING_SAT = 200_000
# the minimum cltv_expiry accepted for newly received HTLCs # the minimum cltv_expiry accepted for newly received HTLCs
# note: when changing, consider Blockchain.is_tip_stale() # note: when changing, consider Blockchain.is_tip_stale()
MIN_FINAL_CLTV_EXPIRY_ACCEPTED = 144 MIN_FINAL_CLTV_DELTA_ACCEPTED = 144
# set it a tiny bit higher for invoices as blocks could get mined # set it a tiny bit higher for invoices as blocks could get mined
# during forward path of payment # during forward path of payment
MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE = MIN_FINAL_CLTV_EXPIRY_ACCEPTED + 3 MIN_FINAL_CLTV_DELTA_FOR_INVOICE = MIN_FINAL_CLTV_DELTA_ACCEPTED + 3
# the deadline for offered HTLCs: # the deadline for offered HTLCs:
# the deadline after which the channel has to be failed and timed out on-chain # the deadline after which the channel has to be failed and timed out on-chain
NBLOCK_DEADLINE_AFTER_EXPIRY_FOR_OFFERED_HTLCS = 1 NBLOCK_DEADLINE_DELTA_AFTER_EXPIRY_FOR_OFFERED_HTLCS = 1
# the deadline for received HTLCs this node has fulfilled: # the deadline for received HTLCs this node has fulfilled:
# the deadline after which the channel has to be failed and the HTLC fulfilled on-chain before its cltv_expiry # the deadline after which the channel has to be failed and the HTLC fulfilled on-chain before its cltv_expiry
NBLOCK_DEADLINE_BEFORE_EXPIRY_FOR_RECEIVED_HTLCS = 72 NBLOCK_DEADLINE_DELTA_BEFORE_EXPIRY_FOR_RECEIVED_HTLCS = 72
NBLOCK_CLTV_EXPIRY_TOO_FAR_INTO_FUTURE = 28 * 144 NBLOCK_CLTV_DELTA_TOO_FAR_INTO_FUTURE = 28 * 144
MAXIMUM_REMOTE_TO_SELF_DELAY_ACCEPTED = 2016 MAXIMUM_REMOTE_TO_SELF_DELAY_ACCEPTED = 2016
@ -621,14 +621,19 @@ def make_htlc_tx_inputs(htlc_output_txid: str, htlc_output_index: int,
c_inputs = [txin] c_inputs = [txin]
return c_inputs return c_inputs
def make_htlc_tx(*, cltv_expiry: int, inputs: List[PartialTxInput], output: PartialTxOutput) -> PartialTransaction: def make_htlc_tx(*, cltv_abs: int, inputs: List[PartialTxInput], output: PartialTxOutput) -> PartialTransaction:
assert type(cltv_expiry) is int assert type(cltv_abs) is int
c_outputs = [output] c_outputs = [output]
tx = PartialTransaction.from_io(inputs, c_outputs, locktime=cltv_expiry, version=2) tx = PartialTransaction.from_io(inputs, c_outputs, locktime=cltv_abs, version=2)
return tx return tx
def make_offered_htlc(revocation_pubkey: bytes, remote_htlcpubkey: bytes, def make_offered_htlc(
local_htlcpubkey: bytes, payment_hash: bytes) -> bytes: *,
revocation_pubkey: bytes,
remote_htlcpubkey: bytes,
local_htlcpubkey: bytes,
payment_hash: bytes,
) -> bytes:
assert type(revocation_pubkey) is bytes assert type(revocation_pubkey) is bytes
assert type(remote_htlcpubkey) is bytes assert type(remote_htlcpubkey) is bytes
assert type(local_htlcpubkey) is bytes assert type(local_htlcpubkey) is bytes
@ -663,11 +668,17 @@ def make_offered_htlc(revocation_pubkey: bytes, remote_htlcpubkey: bytes,
])) ]))
return script return script
def make_received_htlc(revocation_pubkey: bytes, remote_htlcpubkey: bytes, def make_received_htlc(
local_htlcpubkey: bytes, payment_hash: bytes, cltv_expiry: int) -> bytes: *,
revocation_pubkey: bytes,
remote_htlcpubkey: bytes,
local_htlcpubkey: bytes,
payment_hash: bytes,
cltv_abs: int,
) -> bytes:
for i in [revocation_pubkey, remote_htlcpubkey, local_htlcpubkey, payment_hash]: for i in [revocation_pubkey, remote_htlcpubkey, local_htlcpubkey, payment_hash]:
assert type(i) is bytes assert type(i) is bytes
assert type(cltv_expiry) is int assert type(cltv_abs) is int
script = bfh(construct_script([ script = bfh(construct_script([
opcodes.OP_DUP, opcodes.OP_DUP,
@ -693,7 +704,7 @@ def make_received_htlc(revocation_pubkey: bytes, remote_htlcpubkey: bytes,
opcodes.OP_CHECKMULTISIG, opcodes.OP_CHECKMULTISIG,
opcodes.OP_ELSE, opcodes.OP_ELSE,
opcodes.OP_DROP, opcodes.OP_DROP,
cltv_expiry, cltv_abs,
opcodes.OP_CHECKLOCKTIMEVERIFY, opcodes.OP_CHECKLOCKTIMEVERIFY,
opcodes.OP_DROP, opcodes.OP_DROP,
opcodes.OP_CHECKSIG, opcodes.OP_CHECKSIG,
@ -764,14 +775,21 @@ WITNESS_TEMPLATE_RECEIVED_HTLC = [
] ]
def make_htlc_output_witness_script(is_received_htlc: bool, remote_revocation_pubkey: bytes, remote_htlc_pubkey: bytes, def make_htlc_output_witness_script(
local_htlc_pubkey: bytes, payment_hash: bytes, cltv_expiry: Optional[int]) -> bytes: *,
is_received_htlc: bool,
remote_revocation_pubkey: bytes,
remote_htlc_pubkey: bytes,
local_htlc_pubkey: bytes,
payment_hash: bytes,
cltv_abs: Optional[int],
) -> bytes:
if is_received_htlc: if is_received_htlc:
return make_received_htlc(revocation_pubkey=remote_revocation_pubkey, return make_received_htlc(revocation_pubkey=remote_revocation_pubkey,
remote_htlcpubkey=remote_htlc_pubkey, remote_htlcpubkey=remote_htlc_pubkey,
local_htlcpubkey=local_htlc_pubkey, local_htlcpubkey=local_htlc_pubkey,
payment_hash=payment_hash, payment_hash=payment_hash,
cltv_expiry=cltv_expiry) cltv_abs=cltv_abs)
else: else:
return make_offered_htlc(revocation_pubkey=remote_revocation_pubkey, return make_offered_htlc(revocation_pubkey=remote_revocation_pubkey,
remote_htlcpubkey=remote_htlc_pubkey, remote_htlcpubkey=remote_htlc_pubkey,
@ -789,7 +807,7 @@ def get_ordered_channel_configs(chan: 'AbstractChannel', for_us: bool) -> Tuple[
def possible_output_idxs_of_htlc_in_ctx(*, chan: 'Channel', pcp: bytes, subject: 'HTLCOwner', def possible_output_idxs_of_htlc_in_ctx(*, chan: 'Channel', pcp: bytes, subject: 'HTLCOwner',
htlc_direction: 'Direction', ctx: Transaction, htlc_direction: 'Direction', ctx: Transaction,
htlc: 'UpdateAddHtlc') -> Set[int]: htlc: 'UpdateAddHtlc') -> Set[int]:
amount_msat, cltv_expiry, payment_hash = htlc.amount_msat, htlc.cltv_expiry, htlc.payment_hash amount_msat, cltv_abs, payment_hash = htlc.amount_msat, htlc.cltv_abs, htlc.payment_hash
for_us = subject == LOCAL for_us = subject == LOCAL
conf, other_conf = get_ordered_channel_configs(chan=chan, for_us=for_us) conf, other_conf = get_ordered_channel_configs(chan=chan, for_us=for_us)
@ -801,7 +819,7 @@ def possible_output_idxs_of_htlc_in_ctx(*, chan: 'Channel', pcp: bytes, subject:
remote_htlc_pubkey=other_htlc_pubkey, remote_htlc_pubkey=other_htlc_pubkey,
local_htlc_pubkey=htlc_pubkey, local_htlc_pubkey=htlc_pubkey,
payment_hash=payment_hash, payment_hash=payment_hash,
cltv_expiry=cltv_expiry) cltv_abs=cltv_abs)
htlc_address = redeem_script_to_address('p2wsh', preimage_script.hex()) htlc_address = redeem_script_to_address('p2wsh', preimage_script.hex())
candidates = ctx.get_output_idxs_from_address(htlc_address) candidates = ctx.get_output_idxs_from_address(htlc_address)
return {output_idx for output_idx in candidates return {output_idx for output_idx in candidates
@ -814,9 +832,9 @@ def map_htlcs_to_ctx_output_idxs(*, chan: 'Channel', ctx: Transaction, pcp: byte
htlc_to_ctx_output_idx_map = {} # type: Dict[Tuple[Direction, UpdateAddHtlc], int] htlc_to_ctx_output_idx_map = {} # type: Dict[Tuple[Direction, UpdateAddHtlc], int]
unclaimed_ctx_output_idxs = set(range(len(ctx.outputs()))) unclaimed_ctx_output_idxs = set(range(len(ctx.outputs())))
offered_htlcs = chan.included_htlcs(subject, SENT, ctn=ctn) offered_htlcs = chan.included_htlcs(subject, SENT, ctn=ctn)
offered_htlcs.sort(key=lambda htlc: htlc.cltv_expiry) offered_htlcs.sort(key=lambda htlc: htlc.cltv_abs)
received_htlcs = chan.included_htlcs(subject, RECEIVED, ctn=ctn) received_htlcs = chan.included_htlcs(subject, RECEIVED, ctn=ctn)
received_htlcs.sort(key=lambda htlc: htlc.cltv_expiry) received_htlcs.sort(key=lambda htlc: htlc.cltv_abs)
for direction, htlcs in zip([SENT, RECEIVED], [offered_htlcs, received_htlcs]): for direction, htlcs in zip([SENT, RECEIVED], [offered_htlcs, received_htlcs]):
for htlc in htlcs: for htlc in htlcs:
cands = sorted(possible_output_idxs_of_htlc_in_ctx(chan=chan, cands = sorted(possible_output_idxs_of_htlc_in_ctx(chan=chan,
@ -841,7 +859,7 @@ def map_htlcs_to_ctx_output_idxs(*, chan: 'Channel', ctx: Transaction, pcp: byte
def make_htlc_tx_with_open_channel(*, chan: 'Channel', pcp: bytes, subject: 'HTLCOwner', ctn: int, def make_htlc_tx_with_open_channel(*, chan: 'Channel', pcp: bytes, subject: 'HTLCOwner', ctn: int,
htlc_direction: 'Direction', commit: Transaction, ctx_output_idx: int, htlc_direction: 'Direction', commit: Transaction, ctx_output_idx: int,
htlc: 'UpdateAddHtlc', name: str = None) -> Tuple[bytes, PartialTransaction]: htlc: 'UpdateAddHtlc', name: str = None) -> Tuple[bytes, PartialTransaction]:
amount_msat, cltv_expiry, payment_hash = htlc.amount_msat, htlc.cltv_expiry, htlc.payment_hash amount_msat, cltv_abs, payment_hash = htlc.amount_msat, htlc.cltv_abs, htlc.payment_hash
for_us = subject == LOCAL for_us = subject == LOCAL
conf, other_conf = get_ordered_channel_configs(chan=chan, for_us=for_us) conf, other_conf = get_ordered_channel_configs(chan=chan, for_us=for_us)
@ -864,14 +882,14 @@ def make_htlc_tx_with_open_channel(*, chan: 'Channel', pcp: bytes, subject: 'HTL
remote_htlc_pubkey=other_htlc_pubkey, remote_htlc_pubkey=other_htlc_pubkey,
local_htlc_pubkey=htlc_pubkey, local_htlc_pubkey=htlc_pubkey,
payment_hash=payment_hash, payment_hash=payment_hash,
cltv_expiry=cltv_expiry) cltv_abs=cltv_abs)
htlc_tx_inputs = make_htlc_tx_inputs( htlc_tx_inputs = make_htlc_tx_inputs(
commit.txid(), ctx_output_idx, commit.txid(), ctx_output_idx,
amount_msat=amount_msat, amount_msat=amount_msat,
witness_script=preimage_script.hex()) witness_script=preimage_script.hex())
if is_htlc_success: if is_htlc_success:
cltv_expiry = 0 cltv_abs = 0
htlc_tx = make_htlc_tx(cltv_expiry=cltv_expiry, inputs=htlc_tx_inputs, output=htlc_tx_output) htlc_tx = make_htlc_tx(cltv_abs=cltv_abs, inputs=htlc_tx_inputs, output=htlc_tx_output)
return witness_script_of_htlc_tx_output, htlc_tx return witness_script_of_htlc_tx_output, htlc_tx
def make_funding_input(local_funding_pubkey: bytes, remote_funding_pubkey: bytes, def make_funding_input(local_funding_pubkey: bytes, remote_funding_pubkey: bytes,
@ -1009,7 +1027,7 @@ def make_commitment(
# the outputs are ordered in increasing cltv_expiry order." # the outputs are ordered in increasing cltv_expiry order."
# so we sort by cltv_expiry now; and the later BIP69-sort is assumed to be *stable* # so we sort by cltv_expiry now; and the later BIP69-sort is assumed to be *stable*
htlcs = list(htlcs) htlcs = list(htlcs)
htlcs.sort(key=lambda x: x.htlc.cltv_expiry) htlcs.sort(key=lambda x: x.htlc.cltv_abs)
htlc_outputs, c_outputs_filtered = make_commitment_outputs( htlc_outputs, c_outputs_filtered = make_commitment_outputs(
fees_per_participant=fees_per_participant, fees_per_participant=fees_per_participant,
@ -1595,21 +1613,21 @@ NUM_MAX_EDGES_IN_PAYMENT_PATH = NUM_MAX_HOPS_IN_PAYMENT_PATH
class UpdateAddHtlc: class UpdateAddHtlc:
amount_msat = attr.ib(type=int, kw_only=True) amount_msat = attr.ib(type=int, kw_only=True)
payment_hash = attr.ib(type=bytes, kw_only=True, converter=hex_to_bytes, repr=lambda val: val.hex()) payment_hash = attr.ib(type=bytes, kw_only=True, converter=hex_to_bytes, repr=lambda val: val.hex())
cltv_expiry = attr.ib(type=int, kw_only=True) cltv_abs = attr.ib(type=int, kw_only=True)
timestamp = attr.ib(type=int, kw_only=True) timestamp = attr.ib(type=int, kw_only=True)
htlc_id = attr.ib(type=int, kw_only=True, default=None) htlc_id = attr.ib(type=int, kw_only=True, default=None)
@stored_in('adds', tuple) @stored_in('adds', tuple)
def from_tuple(amount_msat, payment_hash, cltv_expiry, htlc_id, timestamp) -> 'UpdateAddHtlc': def from_tuple(amount_msat, payment_hash, cltv_abs, htlc_id, timestamp) -> 'UpdateAddHtlc':
return UpdateAddHtlc( return UpdateAddHtlc(
amount_msat=amount_msat, amount_msat=amount_msat,
payment_hash=payment_hash, payment_hash=payment_hash,
cltv_expiry=cltv_expiry, cltv_abs=cltv_abs,
htlc_id=htlc_id, htlc_id=htlc_id,
timestamp=timestamp) timestamp=timestamp)
def to_json(self): def to_json(self):
return (self.amount_msat, self.payment_hash, self.cltv_expiry, self.htlc_id, self.timestamp) return (self.amount_msat, self.payment_hash, self.cltv_abs, self.htlc_id, self.timestamp)
class OnionFailureCodeMetaFlag(IntFlag): class OnionFailureCodeMetaFlag(IntFlag):

4
electrum/lnwatcher.py

@ -510,8 +510,8 @@ class LNWalletWatcher(LNWatcher):
prev_txid, prev_index = prevout.split(':') prev_txid, prev_index = prevout.split(':')
can_broadcast = True can_broadcast = True
local_height = self.network.get_local_height() local_height = self.network.get_local_height()
if sweep_info.cltv_expiry: if sweep_info.cltv_abs:
wanted_height = sweep_info.cltv_expiry wanted_height = sweep_info.cltv_abs
if wanted_height - local_height > 0: if wanted_height - local_height > 0:
can_broadcast = False can_broadcast = False
# self.logger.debug(f"pending redeem for {prevout}. waiting for {name}: CLTV ({local_height=}, {wanted_height=})") # self.logger.debug(f"pending redeem for {prevout}. waiting for {name}: CLTV ({local_height=}, {wanted_height=})")

52
electrum/lnworker.py

@ -61,7 +61,7 @@ from .lnutil import (Outpoint, LNPeerAddr,
get_compressed_pubkey_from_bech32, extract_nodeid, get_compressed_pubkey_from_bech32, extract_nodeid,
PaymentFailure, split_host_port, ConnStringFormatError, PaymentFailure, split_host_port, ConnStringFormatError,
generate_keypair, LnKeyFamily, LOCAL, REMOTE, generate_keypair, LnKeyFamily, LOCAL, REMOTE,
MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE, MIN_FINAL_CLTV_DELTA_FOR_INVOICE,
NUM_MAX_EDGES_IN_PAYMENT_PATH, SENT, RECEIVED, HTLCOwner, NUM_MAX_EDGES_IN_PAYMENT_PATH, SENT, RECEIVED, HTLCOwner,
UpdateAddHtlc, Direction, LnFeatures, ShortChannelID, UpdateAddHtlc, Direction, LnFeatures, ShortChannelID,
HtlcLog, derive_payment_secret_from_payment_preimage, HtlcLog, derive_payment_secret_from_payment_preimage,
@ -662,7 +662,7 @@ class PaySession(Logger):
initial_trampoline_fee_level: int, initial_trampoline_fee_level: int,
invoice_features: int, invoice_features: int,
r_tags, r_tags,
min_cltv_expiry: int, min_final_cltv_delta: int, # delta for last edge (typically from invoice)
amount_to_pay: int, # total payment amount final receiver will get amount_to_pay: int, # total payment amount final receiver will get
invoice_pubkey: bytes, invoice_pubkey: bytes,
uses_trampoline: bool, # whether sender uses trampoline or gossip uses_trampoline: bool, # whether sender uses trampoline or gossip
@ -677,7 +677,7 @@ class PaySession(Logger):
self.invoice_features = LnFeatures(invoice_features) self.invoice_features = LnFeatures(invoice_features)
self.r_tags = r_tags self.r_tags = r_tags
self.min_cltv_expiry = min_cltv_expiry self.min_final_cltv_delta = min_final_cltv_delta
self.amount_to_pay = amount_to_pay self.amount_to_pay = amount_to_pay
self.invoice_pubkey = invoice_pubkey self.invoice_pubkey = invoice_pubkey
@ -1387,7 +1387,7 @@ class LNWallet(LNWorker):
) -> Tuple[bool, List[HtlcLog]]: ) -> Tuple[bool, List[HtlcLog]]:
lnaddr = self._check_invoice(invoice, amount_msat=amount_msat) lnaddr = self._check_invoice(invoice, amount_msat=amount_msat)
min_cltv_expiry = lnaddr.get_min_final_cltv_expiry() min_final_cltv_delta = lnaddr.get_min_final_cltv_delta()
payment_hash = lnaddr.paymenthash payment_hash = lnaddr.paymenthash
key = payment_hash.hex() key = payment_hash.hex()
payment_secret = lnaddr.payment_secret payment_secret = lnaddr.payment_secret
@ -1417,7 +1417,7 @@ class LNWallet(LNWorker):
payment_hash=payment_hash, payment_hash=payment_hash,
payment_secret=payment_secret, payment_secret=payment_secret,
amount_to_pay=amount_to_pay, amount_to_pay=amount_to_pay,
min_cltv_expiry=min_cltv_expiry, min_final_cltv_delta=min_final_cltv_delta,
r_tags=r_tags, r_tags=r_tags,
invoice_features=invoice_features, invoice_features=invoice_features,
attempts=attempts, attempts=attempts,
@ -1447,7 +1447,7 @@ class LNWallet(LNWorker):
payment_hash: bytes, payment_hash: bytes,
payment_secret: bytes, payment_secret: bytes,
amount_to_pay: int, # in msat amount_to_pay: int, # in msat
min_cltv_expiry: int, min_final_cltv_delta: int,
r_tags, r_tags,
invoice_features: int, invoice_features: int,
attempts: int = None, attempts: int = None,
@ -1473,7 +1473,7 @@ class LNWallet(LNWorker):
initial_trampoline_fee_level=self.config.INITIAL_TRAMPOLINE_FEE_LEVEL, initial_trampoline_fee_level=self.config.INITIAL_TRAMPOLINE_FEE_LEVEL,
invoice_features=invoice_features, invoice_features=invoice_features,
r_tags=r_tags, r_tags=r_tags,
min_cltv_expiry=min_cltv_expiry, min_final_cltv_delta=min_final_cltv_delta,
amount_to_pay=amount_to_pay, amount_to_pay=amount_to_pay,
invoice_pubkey=node_pubkey, invoice_pubkey=node_pubkey,
uses_trampoline=self.uses_trampoline(), uses_trampoline=self.uses_trampoline(),
@ -1502,7 +1502,7 @@ class LNWallet(LNWorker):
await self.pay_to_route( await self.pay_to_route(
paysession=paysession, paysession=paysession,
sent_htlc_info=sent_htlc_info, sent_htlc_info=sent_htlc_info,
min_cltv_expiry=cltv_delta, min_final_cltv_delta=cltv_delta,
trampoline_onion=trampoline_onion, trampoline_onion=trampoline_onion,
) )
# invoice_status is triggered in self.set_invoice_status when it actually changes. # invoice_status is triggered in self.set_invoice_status when it actually changes.
@ -1559,7 +1559,7 @@ class LNWallet(LNWorker):
self, *, self, *,
paysession: PaySession, paysession: PaySession,
sent_htlc_info: SentHtlcInfo, sent_htlc_info: SentHtlcInfo,
min_cltv_expiry: int, min_final_cltv_delta: int,
trampoline_onion: Optional[OnionPacket] = None, trampoline_onion: Optional[OnionPacket] = None,
) -> None: ) -> None:
"""Sends a single HTLC.""" """Sends a single HTLC."""
@ -1578,7 +1578,7 @@ class LNWallet(LNWorker):
amount_msat=shi.amount_msat, amount_msat=shi.amount_msat,
total_msat=shi.bucket_msat, total_msat=shi.bucket_msat,
payment_hash=paysession.payment_hash, payment_hash=paysession.payment_hash,
min_final_cltv_expiry=min_cltv_expiry, min_final_cltv_delta=min_final_cltv_delta,
payment_secret=shi.payment_secret_bucket, payment_secret=shi.payment_secret_bucket,
trampoline_onion=trampoline_onion) trampoline_onion=trampoline_onion)
@ -1735,10 +1735,10 @@ class LNWallet(LNWorker):
if addr.amount is None: if addr.amount is None:
raise InvoiceError(_("Missing amount")) raise InvoiceError(_("Missing amount"))
# check cltv # check cltv
if addr.get_min_final_cltv_expiry() > lnutil.NBLOCK_CLTV_EXPIRY_TOO_FAR_INTO_FUTURE: if addr.get_min_final_cltv_delta() > lnutil.NBLOCK_CLTV_DELTA_TOO_FAR_INTO_FUTURE:
raise InvoiceError("{}\n{}".format( raise InvoiceError("{}\n{}".format(
_("Invoice wants us to risk locking funds for unreasonably long."), _("Invoice wants us to risk locking funds for unreasonably long."),
f"min_final_cltv_expiry: {addr.get_min_final_cltv_expiry()}")) f"min_final_cltv_delta: {addr.get_min_final_cltv_delta()}"))
# check features # check features
addr.validate_and_compare_features(self.features) addr.validate_and_compare_features(self.features)
return addr return addr
@ -1858,13 +1858,13 @@ class LNWallet(LNWorker):
trampoline_onion = None trampoline_onion = None
per_trampoline_secret = paysession.payment_secret per_trampoline_secret = paysession.payment_secret
per_trampoline_amount_with_fees = amount_msat per_trampoline_amount_with_fees = amount_msat
per_trampoline_cltv_delta = paysession.min_cltv_expiry per_trampoline_cltv_delta = paysession.min_final_cltv_delta
per_trampoline_fees = 0 per_trampoline_fees = 0
else: else:
trampoline_route, trampoline_onion, per_trampoline_amount_with_fees, per_trampoline_cltv_delta = create_trampoline_route_and_onion( trampoline_route, trampoline_onion, per_trampoline_amount_with_fees, per_trampoline_cltv_delta = create_trampoline_route_and_onion(
amount_msat=per_trampoline_amount, amount_msat=per_trampoline_amount,
total_msat=paysession.amount_to_pay, total_msat=paysession.amount_to_pay,
min_cltv_expiry=paysession.min_cltv_expiry, min_final_cltv_delta=paysession.min_final_cltv_delta,
my_pubkey=self.node_keypair.pubkey, my_pubkey=self.node_keypair.pubkey,
invoice_pubkey=paysession.invoice_pubkey, invoice_pubkey=paysession.invoice_pubkey,
invoice_features=paysession.invoice_features, invoice_features=paysession.invoice_features,
@ -1896,7 +1896,7 @@ class LNWallet(LNWorker):
short_channel_id=chan.short_channel_id, short_channel_id=chan.short_channel_id,
fee_base_msat=0, fee_base_msat=0,
fee_proportional_millionths=0, fee_proportional_millionths=0,
cltv_expiry_delta=0, cltv_delta=0,
node_features=trampoline_features) node_features=trampoline_features)
] ]
self.logger.info(f'adding route {part_amount_msat} {delta_fee} {margin}') self.logger.info(f'adding route {part_amount_msat} {delta_fee} {margin}')
@ -1925,7 +1925,7 @@ class LNWallet(LNWorker):
self.create_route_for_payment, self.create_route_for_payment,
amount_msat=part_amount_msat, amount_msat=part_amount_msat,
invoice_pubkey=paysession.invoice_pubkey, invoice_pubkey=paysession.invoice_pubkey,
min_cltv_expiry=paysession.min_cltv_expiry, min_final_cltv_delta=paysession.min_final_cltv_delta,
r_tags=paysession.r_tags, r_tags=paysession.r_tags,
invoice_features=paysession.invoice_features, invoice_features=paysession.invoice_features,
my_sending_channels=[channel] if is_multichan_mpp else my_active_channels, my_sending_channels=[channel] if is_multichan_mpp else my_active_channels,
@ -1942,7 +1942,7 @@ class LNWallet(LNWorker):
trampoline_fee_level=None, trampoline_fee_level=None,
trampoline_route=None, trampoline_route=None,
) )
routes.append((shi, paysession.min_cltv_expiry, fwd_trampoline_onion)) routes.append((shi, paysession.min_final_cltv_delta, fwd_trampoline_onion))
except NoPathFound: except NoPathFound:
continue continue
for route in routes: for route in routes:
@ -1955,7 +1955,7 @@ class LNWallet(LNWorker):
self, *, self, *,
amount_msat: int, amount_msat: int,
invoice_pubkey: bytes, invoice_pubkey: bytes,
min_cltv_expiry: int, min_final_cltv_delta: int,
r_tags, r_tags,
invoice_features: int, invoice_features: int,
my_sending_channels: List[Channel], my_sending_channels: List[Channel],
@ -1978,7 +1978,7 @@ class LNWallet(LNWorker):
self.logger.info(f'create_route: skipping alias {ShortChannelID(private_path[0][1])}') self.logger.info(f'create_route: skipping alias {ShortChannelID(private_path[0][1])}')
continue continue
for end_node, edge_rest in zip(private_path_nodes, private_path_rest): for end_node, edge_rest in zip(private_path_nodes, private_path_rest):
short_channel_id, fee_base_msat, fee_proportional_millionths, cltv_expiry_delta = edge_rest short_channel_id, fee_base_msat, fee_proportional_millionths, cltv_delta = edge_rest
short_channel_id = ShortChannelID(short_channel_id) short_channel_id = ShortChannelID(short_channel_id)
# if we have a routing policy for this edge in the db, that takes precedence, # if we have a routing policy for this edge in the db, that takes precedence,
# as it is likely from a previous failure # as it is likely from a previous failure
@ -1989,7 +1989,7 @@ class LNWallet(LNWorker):
if channel_policy: if channel_policy:
fee_base_msat = channel_policy.fee_base_msat fee_base_msat = channel_policy.fee_base_msat
fee_proportional_millionths = channel_policy.fee_proportional_millionths fee_proportional_millionths = channel_policy.fee_proportional_millionths
cltv_expiry_delta = channel_policy.cltv_expiry_delta cltv_delta = channel_policy.cltv_delta
node_info = self.channel_db.get_node_info_for_node_id(node_id=end_node) node_info = self.channel_db.get_node_info_for_node_id(node_id=end_node)
route_edge = RouteEdge( route_edge = RouteEdge(
start_node=start_node, start_node=start_node,
@ -1997,7 +1997,7 @@ class LNWallet(LNWorker):
short_channel_id=short_channel_id, short_channel_id=short_channel_id,
fee_base_msat=fee_base_msat, fee_base_msat=fee_base_msat,
fee_proportional_millionths=fee_proportional_millionths, fee_proportional_millionths=fee_proportional_millionths,
cltv_expiry_delta=cltv_expiry_delta, cltv_delta=cltv_delta,
node_features=node_info.features if node_info else 0) node_features=node_info.features if node_info else 0)
private_route_edges[route_edge.short_channel_id] = route_edge private_route_edges[route_edge.short_channel_id] = route_edge
start_node = end_node start_node = end_node
@ -2015,7 +2015,7 @@ class LNWallet(LNWorker):
if not route: if not route:
raise NoPathFound() raise NoPathFound()
# test sanity # test sanity
if not is_route_sane_to_use(route, amount_msat, min_cltv_expiry): if not is_route_sane_to_use(route, amount_msat, min_final_cltv_delta):
self.logger.info(f"rejecting insane route {route}") self.logger.info(f"rejecting insane route {route}")
raise NoPathFound() raise NoPathFound()
assert len(route) > 0 assert len(route) > 0
@ -2059,7 +2059,7 @@ class LNWallet(LNWorker):
amount=amount_btc, amount=amount_btc,
tags=[ tags=[
('d', message), ('d', message),
('c', MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE), ('c', MIN_FINAL_CLTV_DELTA_FOR_INVOICE),
('x', expiry), ('x', expiry),
('9', invoice_features), ('9', invoice_features),
('f', fallback_address), ('f', fallback_address),
@ -2400,14 +2400,14 @@ class LNWallet(LNWorker):
# (they will get the channel update from the onion error) # (they will get the channel update from the onion error)
# at least, that's the theory. https://github.com/lightningnetwork/lnd/issues/2066 # at least, that's the theory. https://github.com/lightningnetwork/lnd/issues/2066
fee_base_msat = fee_proportional_millionths = 0 fee_base_msat = fee_proportional_millionths = 0
cltv_expiry_delta = 1 # lnd won't even try with zero cltv_delta = 1 # lnd won't even try with zero
missing_info = True missing_info = True
if channel_info: if channel_info:
policy = get_mychannel_policy(channel_info.short_channel_id, chan.node_id, scid_to_my_channels) policy = get_mychannel_policy(channel_info.short_channel_id, chan.node_id, scid_to_my_channels)
if policy: if policy:
fee_base_msat = policy.fee_base_msat fee_base_msat = policy.fee_base_msat
fee_proportional_millionths = policy.fee_proportional_millionths fee_proportional_millionths = policy.fee_proportional_millionths
cltv_expiry_delta = policy.cltv_expiry_delta cltv_delta = policy.cltv_delta
missing_info = False missing_info = False
if missing_info: if missing_info:
self.logger.info( self.logger.info(
@ -2418,7 +2418,7 @@ class LNWallet(LNWorker):
alias_or_scid, alias_or_scid,
fee_base_msat, fee_base_msat,
fee_proportional_millionths, fee_proportional_millionths,
cltv_expiry_delta)])) cltv_delta)]))
return routing_hints return routing_hints
def delete_payment_info(self, payment_hash_hex: str): def delete_payment_info(self, payment_hash_hex: str):

8
electrum/tests/test_bolt11.py

@ -142,19 +142,19 @@ class TestBolt11(ElectrumTestCase):
def test_min_final_cltv_expiry_decoding(self): def test_min_final_cltv_expiry_decoding(self):
lnaddr = lndecode("lnsb500u1pdsgyf3pp5nmrqejdsdgs4n9ukgxcp2kcq265yhrxd4k5dyue58rxtp5y83s3qsp5qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqsdqqcqzys9qypqsqp2h6a5xeytuc3fad2ed4gxvhd593lwjdna3dxsyeem0qkzjx6guk44jend0xq4zzvp6f3fy07wnmxezazzsxgmvqee8shxjuqu2eu0qpnvc95x", lnaddr = lndecode("lnsb500u1pdsgyf3pp5nmrqejdsdgs4n9ukgxcp2kcq265yhrxd4k5dyue58rxtp5y83s3qsp5qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqsdqqcqzys9qypqsqp2h6a5xeytuc3fad2ed4gxvhd593lwjdna3dxsyeem0qkzjx6guk44jend0xq4zzvp6f3fy07wnmxezazzsxgmvqee8shxjuqu2eu0qpnvc95x",
net=constants.BitcoinSimnet) net=constants.BitcoinSimnet)
self.assertEqual(144, lnaddr.get_min_final_cltv_expiry()) self.assertEqual(144, lnaddr.get_min_final_cltv_delta())
lnaddr = lndecode("lntb15u1p0m6lzupp5zqjthgvaad9mewmdjuehwddyze9d8zyxcc43zhaddeegt37sndgsdq4xysyymr0vd4kzcmrd9hx7cqp7xqrrss9qy9qsqsp5vlhcs24hwm747w8f3uau2tlrdkvjaglffnsstwyamj84cxuhrn2s8tut3jqumepu42azyyjpgqa4w9w03204zp9h4clk499y2umstl6s29hqyj8vv4as6zt5567ux7l3f66m8pjhk65zjaq2esezk7ll2kcpljewkg", lnaddr = lndecode("lntb15u1p0m6lzupp5zqjthgvaad9mewmdjuehwddyze9d8zyxcc43zhaddeegt37sndgsdq4xysyymr0vd4kzcmrd9hx7cqp7xqrrss9qy9qsqsp5vlhcs24hwm747w8f3uau2tlrdkvjaglffnsstwyamj84cxuhrn2s8tut3jqumepu42azyyjpgqa4w9w03204zp9h4clk499y2umstl6s29hqyj8vv4as6zt5567ux7l3f66m8pjhk65zjaq2esezk7ll2kcpljewkg",
net=constants.BitcoinTestnet) net=constants.BitcoinTestnet)
self.assertEqual(30, lnaddr.get_min_final_cltv_expiry()) self.assertEqual(30, lnaddr.get_min_final_cltv_delta())
def test_min_final_cltv_expiry_roundtrip(self): def test_min_final_cltv_expiry_roundtrip(self):
for cltv in (1, 15, 16, 31, 32, 33, 150, 511, 512, 513, 1023, 1024, 1025): for cltv in (1, 15, 16, 31, 32, 33, 150, 511, 512, 513, 1023, 1024, 1025):
lnaddr = LnAddr( lnaddr = LnAddr(
paymenthash=RHASH, payment_secret=b"\x01"*32, amount=Decimal('0.001'), tags=[('d', '1 cup coffee'), ('x', 60), ('c', cltv), ('9', 33282)]) paymenthash=RHASH, payment_secret=b"\x01"*32, amount=Decimal('0.001'), tags=[('d', '1 cup coffee'), ('x', 60), ('c', cltv), ('9', 33282)])
self.assertEqual(cltv, lnaddr.get_min_final_cltv_expiry()) self.assertEqual(cltv, lnaddr.get_min_final_cltv_delta())
invoice = lnencode(lnaddr, PRIVKEY) invoice = lnencode(lnaddr, PRIVKEY)
self.assertEqual(cltv, lndecode(invoice).get_min_final_cltv_expiry()) self.assertEqual(cltv, lndecode(invoice).get_min_final_cltv_delta())
def test_features(self): def test_features(self):
lnaddr = lndecode("lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsdq5vdhkven9v5sxyetpdees9qypqsztrz5v3jfnxskfv7g8chmyzyrfhf2vupcavuq5rce96kyt6g0zh337h206awccwp335zarqrud4wccgdn39vur44d8um4hmgv06aj0sgpdrv73z") lnaddr = lndecode("lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsdq5vdhkven9v5sxyetpdees9qypqsztrz5v3jfnxskfv7g8chmyzyrfhf2vupcavuq5rce96kyt6g0zh337h206awccwp335zarqrud4wccgdn39vur44d8um4hmgv06aj0sgpdrv73z")

44
electrum/tests/test_lnchannel.py

@ -33,7 +33,7 @@ from electrum import lnpeer
from electrum import lnchannel from electrum import lnchannel
from electrum import lnutil from electrum import lnutil
from electrum import bip32 as bip32_utils from electrum import bip32 as bip32_utils
from electrum.lnutil import SENT, LOCAL, REMOTE, RECEIVED from electrum.lnutil import SENT, LOCAL, REMOTE, RECEIVED, UpdateAddHtlc
from electrum.logging import console_stderr_handler from electrum.logging import console_stderr_handler
from electrum.lnchannel import ChannelState from electrum.lnchannel import ChannelState
from electrum.json_db import StoredDict from electrum.json_db import StoredDict
@ -240,7 +240,7 @@ class TestChannel(ElectrumTestCase):
self.htlc_dict = { self.htlc_dict = {
'payment_hash' : paymentHash, 'payment_hash' : paymentHash,
'amount_msat' : one_bitcoin_in_msat, 'amount_msat' : one_bitcoin_in_msat,
'cltv_expiry' : 5, 'cltv_abs' : 5,
'timestamp' : 0, 'timestamp' : 0,
} }
@ -648,15 +648,15 @@ class TestAvailableToSpend(ElectrumTestCase):
paymentPreimage = b"\x01" * 32 paymentPreimage = b"\x01" * 32
paymentHash = bitcoin.sha256(paymentPreimage) paymentHash = bitcoin.sha256(paymentPreimage)
htlc_dict = { htlc = UpdateAddHtlc(
'payment_hash' : paymentHash, payment_hash=paymentHash,
'amount_msat' : one_bitcoin_in_msat * 41 // 10, amount_msat=one_bitcoin_in_msat * 41 // 10,
'cltv_expiry' : 5, cltv_abs=5,
'timestamp' : 0, timestamp=0,
} )
alice_idx = alice_channel.add_htlc(htlc_dict).htlc_id alice_idx = alice_channel.add_htlc(htlc).htlc_id
bob_idx = bob_channel.receive_htlc(htlc_dict).htlc_id bob_idx = bob_channel.receive_htlc(htlc).htlc_id
self.assertEqual(89984088000, alice_channel.available_to_spend(LOCAL)) self.assertEqual(89984088000, alice_channel.available_to_spend(LOCAL))
self.assertEqual(500000000000, bob_channel.available_to_spend(LOCAL)) self.assertEqual(500000000000, bob_channel.available_to_spend(LOCAL))
@ -672,20 +672,20 @@ class TestAvailableToSpend(ElectrumTestCase):
# available. # available.
# We try adding an HTLC of value 1 BTC, which should fail because the # We try adding an HTLC of value 1 BTC, which should fail because the
# balance is unavailable. # balance is unavailable.
htlc_dict = { htlc = UpdateAddHtlc(
'payment_hash' : paymentHash, payment_hash=paymentHash,
'amount_msat' : one_bitcoin_in_msat, amount_msat=one_bitcoin_in_msat,
'cltv_expiry' : 5, cltv_abs=5,
'timestamp' : 0, timestamp=0,
} )
with self.assertRaises(lnutil.PaymentFailure): with self.assertRaises(lnutil.PaymentFailure):
alice_channel.add_htlc(htlc_dict) alice_channel.add_htlc(htlc)
# Now do a state transition, which will ACK the FailHTLC, making Alice # Now do a state transition, which will ACK the FailHTLC, making Alice
# able to add the new HTLC. # able to add the new HTLC.
force_state_transition(alice_channel, bob_channel) force_state_transition(alice_channel, bob_channel)
self.assertEqual(499986152000, alice_channel.available_to_spend(LOCAL)) self.assertEqual(499986152000, alice_channel.available_to_spend(LOCAL))
self.assertEqual(500000000000, bob_channel.available_to_spend(LOCAL)) self.assertEqual(500000000000, bob_channel.available_to_spend(LOCAL))
alice_channel.add_htlc(htlc_dict) alice_channel.add_htlc(htlc)
class TestChanReserve(ElectrumTestCase): class TestChanReserve(ElectrumTestCase):
@ -722,7 +722,7 @@ class TestChanReserve(ElectrumTestCase):
htlc_dict = { htlc_dict = {
'payment_hash' : paymentHash, 'payment_hash' : paymentHash,
'amount_msat' : int(.5 * one_bitcoin_in_msat), 'amount_msat' : int(.5 * one_bitcoin_in_msat),
'cltv_expiry' : 5, 'cltv_abs' : 5,
'timestamp' : 0, 'timestamp' : 0,
} }
self.alice_channel.add_htlc(htlc_dict) self.alice_channel.add_htlc(htlc_dict)
@ -761,7 +761,7 @@ class TestChanReserve(ElectrumTestCase):
htlc_dict = { htlc_dict = {
'payment_hash' : paymentHash, 'payment_hash' : paymentHash,
'amount_msat' : int(3.5 * one_bitcoin_in_msat), 'amount_msat' : int(3.5 * one_bitcoin_in_msat),
'cltv_expiry' : 5, 'cltv_abs' : 5,
} }
self.alice_channel.add_htlc(htlc_dict) self.alice_channel.add_htlc(htlc_dict)
self.bob_channel.receive_htlc(htlc_dict) self.bob_channel.receive_htlc(htlc_dict)
@ -785,7 +785,7 @@ class TestChanReserve(ElectrumTestCase):
htlc_dict = { htlc_dict = {
'payment_hash' : paymentHash, 'payment_hash' : paymentHash,
'amount_msat' : int(2 * one_bitcoin_in_msat), 'amount_msat' : int(2 * one_bitcoin_in_msat),
'cltv_expiry' : 5, 'cltv_abs' : 5,
'timestamp' : 0, 'timestamp' : 0,
} }
alice_idx = self.alice_channel.add_htlc(htlc_dict).htlc_id alice_idx = self.alice_channel.add_htlc(htlc_dict).htlc_id
@ -830,7 +830,7 @@ class TestDust(ElectrumTestCase):
htlc = { htlc = {
'payment_hash' : paymentHash, 'payment_hash' : paymentHash,
'amount_msat' : 1000 * htlcAmt, 'amount_msat' : 1000 * htlcAmt,
'cltv_expiry' : 5, # also in create_test_channels 'cltv_abs' : 5, # also in create_test_channels
'timestamp' : 0, 'timestamp' : 0,
} }

32
electrum/tests/test_lnpeer.py

@ -239,7 +239,7 @@ class MockLNWallet(Logger, EventListener, NetworkRetryManager[LNPeerAddr]):
initial_trampoline_fee_level=0, initial_trampoline_fee_level=0,
invoice_features=decoded_invoice.get_features(), invoice_features=decoded_invoice.get_features(),
r_tags=decoded_invoice.get_routing_info('r'), r_tags=decoded_invoice.get_routing_info('r'),
min_cltv_expiry=decoded_invoice.get_min_final_cltv_expiry(), min_final_cltv_delta=decoded_invoice.get_min_final_cltv_delta(),
amount_to_pay=amount_msat, amount_to_pay=amount_msat,
invoice_pubkey=decoded_invoice.pubkey.serialize(), invoice_pubkey=decoded_invoice.pubkey.serialize(),
uses_trampoline=False, uses_trampoline=False,
@ -455,7 +455,7 @@ class TestPeer(ElectrumTestCase):
payment_preimage: bytes = None, payment_preimage: bytes = None,
payment_hash: bytes = None, payment_hash: bytes = None,
invoice_features: LnFeatures = None, invoice_features: LnFeatures = None,
min_final_cltv_expiry_delta: int = None, min_final_cltv_delta: int = None,
) -> Tuple[LnAddr, str]: ) -> Tuple[LnAddr, str]:
amount_btc = amount_msat/Decimal(COIN*1000) amount_btc = amount_msat/Decimal(COIN*1000)
if payment_preimage is None and not payment_hash: if payment_preimage is None and not payment_hash:
@ -477,13 +477,13 @@ class TestPeer(ElectrumTestCase):
payment_secret = w2.get_payment_secret(payment_hash) payment_secret = w2.get_payment_secret(payment_hash)
else: else:
payment_secret = None payment_secret = None
if min_final_cltv_expiry_delta is None: if min_final_cltv_delta is None:
min_final_cltv_expiry_delta = lnutil.MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE min_final_cltv_delta = lnutil.MIN_FINAL_CLTV_DELTA_FOR_INVOICE
lnaddr1 = LnAddr( lnaddr1 = LnAddr(
paymenthash=payment_hash, paymenthash=payment_hash,
amount=amount_btc, amount=amount_btc,
tags=[ tags=[
('c', min_final_cltv_expiry_delta), ('c', min_final_cltv_delta),
('d', 'coffee'), ('d', 'coffee'),
('9', invoice_features), ('9', invoice_features),
] + routing_hints, ] + routing_hints,
@ -574,13 +574,13 @@ class TestPeerDirect(TestPeer):
@staticmethod @staticmethod
def _send_fake_htlc(peer: Peer, chan: Channel) -> UpdateAddHtlc: def _send_fake_htlc(peer: Peer, chan: Channel) -> UpdateAddHtlc:
htlc = UpdateAddHtlc(amount_msat=10000, payment_hash=os.urandom(32), cltv_expiry=999, timestamp=1) htlc = UpdateAddHtlc(amount_msat=10000, payment_hash=os.urandom(32), cltv_abs=999, timestamp=1)
htlc = chan.add_htlc(htlc) htlc = chan.add_htlc(htlc)
peer.send_message( peer.send_message(
"update_add_htlc", "update_add_htlc",
channel_id=chan.channel_id, channel_id=chan.channel_id,
id=htlc.htlc_id, id=htlc.htlc_id,
cltv_expiry=htlc.cltv_expiry, cltv_expiry=htlc.cltv_abs,
amount_msat=htlc.amount_msat, amount_msat=htlc.amount_msat,
payment_hash=htlc.payment_hash, payment_hash=htlc.payment_hash,
onion_routing_packet=1366 * b"0", onion_routing_packet=1366 * b"0",
@ -798,7 +798,7 @@ class TestPeerDirect(TestPeer):
with self.assertRaises(lnutil.IncompatibleOrInsaneFeatures): with self.assertRaises(lnutil.IncompatibleOrInsaneFeatures):
result, log = await w1.pay_invoice(pay_req) result, log = await w1.pay_invoice(pay_req)
# too large CLTV # too large CLTV
lnaddr, pay_req = self.prepare_invoice(w2, min_final_cltv_expiry_delta=10**6) lnaddr, pay_req = self.prepare_invoice(w2, min_final_cltv_delta=10**6)
with self.assertRaises(InvoiceError): with self.assertRaises(InvoiceError):
result, log = await w1.pay_invoice(pay_req) result, log = await w1.pay_invoice(pay_req)
raise SuccessfulTest() raise SuccessfulTest()
@ -848,7 +848,7 @@ class TestPeerDirect(TestPeer):
await w1.pay_to_route( await w1.pay_to_route(
sent_htlc_info=shi1, sent_htlc_info=shi1,
paysession=paysession1, paysession=paysession1,
min_cltv_expiry=lnaddr2.get_min_final_cltv_expiry(), min_final_cltv_delta=lnaddr2.get_min_final_cltv_delta(),
) )
p1.maybe_send_commitment = _maybe_send_commitment1 p1.maybe_send_commitment = _maybe_send_commitment1
# bob sends htlc BUT NOT COMMITMENT_SIGNED # bob sends htlc BUT NOT COMMITMENT_SIGNED
@ -868,7 +868,7 @@ class TestPeerDirect(TestPeer):
await w2.pay_to_route( await w2.pay_to_route(
sent_htlc_info=shi2, sent_htlc_info=shi2,
paysession=paysession2, paysession=paysession2,
min_cltv_expiry=lnaddr1.get_min_final_cltv_expiry(), min_final_cltv_delta=lnaddr1.get_min_final_cltv_delta(),
) )
p2.maybe_send_commitment = _maybe_send_commitment2 p2.maybe_send_commitment = _maybe_send_commitment2
# sleep a bit so that they both receive msgs sent so far # sleep a bit so that they both receive msgs sent so far
@ -941,7 +941,7 @@ class TestPeerDirect(TestPeer):
amount_msat=1000, amount_msat=1000,
total_msat=lnaddr1.get_amount_msat(), total_msat=lnaddr1.get_amount_msat(),
payment_hash=lnaddr1.paymenthash, payment_hash=lnaddr1.paymenthash,
min_final_cltv_expiry=lnaddr1.get_min_final_cltv_expiry(), min_final_cltv_delta=lnaddr1.get_min_final_cltv_delta(),
payment_secret=lnaddr1.payment_secret, payment_secret=lnaddr1.payment_secret,
) )
p1.pay( p1.pay(
@ -950,7 +950,7 @@ class TestPeerDirect(TestPeer):
amount_msat=lnaddr1.get_amount_msat() - 1000, amount_msat=lnaddr1.get_amount_msat() - 1000,
total_msat=lnaddr1.get_amount_msat(), total_msat=lnaddr1.get_amount_msat(),
payment_hash=lnaddr2.paymenthash, payment_hash=lnaddr2.paymenthash,
min_final_cltv_expiry=lnaddr1.get_min_final_cltv_expiry(), min_final_cltv_delta=lnaddr1.get_min_final_cltv_delta(),
payment_secret=lnaddr1.payment_secret, payment_secret=lnaddr1.payment_secret,
) )
@ -1017,7 +1017,7 @@ class TestPeerDirect(TestPeer):
amount_msat=1000, amount_msat=1000,
total_msat=2000, total_msat=2000,
payment_hash=lnaddr1.paymenthash, payment_hash=lnaddr1.paymenthash,
min_final_cltv_expiry=lnaddr1.get_min_final_cltv_expiry(), min_final_cltv_delta=lnaddr1.get_min_final_cltv_delta(),
payment_secret=lnaddr1.payment_secret, payment_secret=lnaddr1.payment_secret,
) )
p1.pay( p1.pay(
@ -1026,7 +1026,7 @@ class TestPeerDirect(TestPeer):
amount_msat=1000, amount_msat=1000,
total_msat=lnaddr1.get_amount_msat(), total_msat=lnaddr1.get_amount_msat(),
payment_hash=lnaddr1.paymenthash, payment_hash=lnaddr1.paymenthash,
min_final_cltv_expiry=lnaddr1.get_min_final_cltv_expiry(), min_final_cltv_delta=lnaddr1.get_min_final_cltv_delta(),
payment_secret=lnaddr1.payment_secret, payment_secret=lnaddr1.payment_secret,
) )
@ -1121,7 +1121,7 @@ class TestPeerDirect(TestPeer):
amount_msat=lnaddr.get_amount_msat(), amount_msat=lnaddr.get_amount_msat(),
total_msat=lnaddr.get_amount_msat(), total_msat=lnaddr.get_amount_msat(),
payment_hash=lnaddr.paymenthash, payment_hash=lnaddr.paymenthash,
min_final_cltv_expiry=lnaddr.get_min_final_cltv_expiry(), min_final_cltv_delta=lnaddr.get_min_final_cltv_delta(),
payment_secret=lnaddr.payment_secret) payment_secret=lnaddr.payment_secret)
# alice closes # alice closes
await p1.close_channel(alice_channel.channel_id) await p1.close_channel(alice_channel.channel_id)
@ -1264,7 +1264,7 @@ class TestPeerDirect(TestPeer):
pay = w1.pay_to_route( pay = w1.pay_to_route(
sent_htlc_info=shi, sent_htlc_info=shi,
paysession=paysession, paysession=paysession,
min_cltv_expiry=lnaddr.get_min_final_cltv_expiry(), min_final_cltv_delta=lnaddr.get_min_final_cltv_delta(),
) )
await asyncio.gather(pay, p1._message_loop(), p2._message_loop(), p1.htlc_switch(), p2.htlc_switch()) await asyncio.gather(pay, p1._message_loop(), p2._message_loop(), p1.htlc_switch(), p2.htlc_switch())
with self.assertRaises(PaymentFailure): with self.assertRaises(PaymentFailure):

21
electrum/tests/test_lnutil.py

@ -482,26 +482,31 @@ class TestLNUtil(ElectrumTestCase):
htlc_cltv_timeout = {} htlc_cltv_timeout = {}
htlc_payment_preimage = {} htlc_payment_preimage = {}
htlc = {} htlc = {}
htlc_pubkeys = {
"revocation_pubkey": local_revocation_pubkey,
"remote_htlcpubkey": remote_htlcpubkey,
"local_htlcpubkey": local_htlcpubkey,
}
htlc_cltv_timeout[2] = 502 htlc_cltv_timeout[2] = 502
htlc_payment_preimage[2] = b"\x02" * 32 htlc_payment_preimage[2] = b"\x02" * 32
htlc[2] = make_offered_htlc(local_revocation_pubkey, remote_htlcpubkey, local_htlcpubkey, bitcoin.sha256(htlc_payment_preimage[2])) htlc[2] = make_offered_htlc(**htlc_pubkeys, payment_hash=bitcoin.sha256(htlc_payment_preimage[2]))
htlc_cltv_timeout[3] = 503 htlc_cltv_timeout[3] = 503
htlc_payment_preimage[3] = b"\x03" * 32 htlc_payment_preimage[3] = b"\x03" * 32
htlc[3] = make_offered_htlc(local_revocation_pubkey, remote_htlcpubkey, local_htlcpubkey, bitcoin.sha256(htlc_payment_preimage[3])) htlc[3] = make_offered_htlc(**htlc_pubkeys, payment_hash=bitcoin.sha256(htlc_payment_preimage[3]))
htlc_cltv_timeout[0] = 500 htlc_cltv_timeout[0] = 500
htlc_payment_preimage[0] = b"\x00" * 32 htlc_payment_preimage[0] = b"\x00" * 32
htlc[0] = make_received_htlc(local_revocation_pubkey, remote_htlcpubkey, local_htlcpubkey, bitcoin.sha256(htlc_payment_preimage[0]), htlc_cltv_timeout[0]) htlc[0] = make_received_htlc(**htlc_pubkeys, payment_hash=bitcoin.sha256(htlc_payment_preimage[0]), cltv_abs=htlc_cltv_timeout[0])
htlc_cltv_timeout[1] = 501 htlc_cltv_timeout[1] = 501
htlc_payment_preimage[1] = b"\x01" * 32 htlc_payment_preimage[1] = b"\x01" * 32
htlc[1] = make_received_htlc(local_revocation_pubkey, remote_htlcpubkey, local_htlcpubkey, bitcoin.sha256(htlc_payment_preimage[1]), htlc_cltv_timeout[1]) htlc[1] = make_received_htlc(**htlc_pubkeys, payment_hash=bitcoin.sha256(htlc_payment_preimage[1]), cltv_abs=htlc_cltv_timeout[1])
htlc_cltv_timeout[4] = 504 htlc_cltv_timeout[4] = 504
htlc_payment_preimage[4] = b"\x04" * 32 htlc_payment_preimage[4] = b"\x04" * 32
htlc[4] = make_received_htlc(local_revocation_pubkey, remote_htlcpubkey, local_htlcpubkey, bitcoin.sha256(htlc_payment_preimage[4]), htlc_cltv_timeout[4]) htlc[4] = make_received_htlc(**htlc_pubkeys, payment_hash=bitcoin.sha256(htlc_payment_preimage[4]), cltv_abs=htlc_cltv_timeout[4])
remote_signature = "304402204fd4928835db1ccdfc40f5c78ce9bd65249b16348df81f0c44328dcdefc97d630220194d3869c38bc732dd87d13d2958015e2fc16829e74cd4377f84d215c0b70606" remote_signature = "304402204fd4928835db1ccdfc40f5c78ce9bd65249b16348df81f0c44328dcdefc97d630220194d3869c38bc732dd87d13d2958015e2fc16829e74cd4377f84d215c0b70606"
output_commit_tx = "02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8007e80300000000000022002052bfef0479d7b293c27e0f1eb294bea154c63a3294ef092c19af51409bce0e2ad007000000000000220020403d394747cae42e98ff01734ad5c08f82ba123d3d9a620abda88989651e2ab5d007000000000000220020748eba944fedc8827f6b06bc44678f93c0f9e6078b35c6331ed31e75f8ce0c2db80b000000000000220020c20b5d1f8584fd90443e7b7b720136174fa4b9333c261d04dbbd012635c0f419a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de843110e0a06a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e04004730440220275b0c325a5e9355650dc30c0eccfbc7efb23987c24b556b9dfdd40effca18d202206caceb2c067836c51f296740c7ae807ffcbfbf1dd3a0d56b6de9a5b247985f060147304402204fd4928835db1ccdfc40f5c78ce9bd65249b16348df81f0c44328dcdefc97d630220194d3869c38bc732dd87d13d2958015e2fc16829e74cd4377f84d215c0b7060601475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220" output_commit_tx = "02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8007e80300000000000022002052bfef0479d7b293c27e0f1eb294bea154c63a3294ef092c19af51409bce0e2ad007000000000000220020403d394747cae42e98ff01734ad5c08f82ba123d3d9a620abda88989651e2ab5d007000000000000220020748eba944fedc8827f6b06bc44678f93c0f9e6078b35c6331ed31e75f8ce0c2db80b000000000000220020c20b5d1f8584fd90443e7b7b720136174fa4b9333c261d04dbbd012635c0f419a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de843110e0a06a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e04004730440220275b0c325a5e9355650dc30c0eccfbc7efb23987c24b556b9dfdd40effca18d202206caceb2c067836c51f296740c7ae807ffcbfbf1dd3a0d56b6de9a5b247985f060147304402204fd4928835db1ccdfc40f5c78ce9bd65249b16348df81f0c44328dcdefc97d630220194d3869c38bc732dd87d13d2958015e2fc16829e74cd4377f84d215c0b7060601475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220"
@ -512,7 +517,7 @@ class TestLNUtil(ElectrumTestCase):
(1, 2000 * 1000), (1, 2000 * 1000),
(3, 3000 * 1000), (3, 3000 * 1000),
(4, 4000 * 1000)]: (4, 4000 * 1000)]:
htlc_obj[num] = UpdateAddHtlc(amount_msat=msat, payment_hash=bitcoin.sha256(htlc_payment_preimage[num]), cltv_expiry=0, htlc_id=None, timestamp=0) htlc_obj[num] = UpdateAddHtlc(amount_msat=msat, payment_hash=bitcoin.sha256(htlc_payment_preimage[num]), cltv_abs=0, htlc_id=None, timestamp=0)
htlcs = [ScriptHtlc(htlc[x], htlc_obj[x]) for x in range(5)] htlcs = [ScriptHtlc(htlc[x], htlc_obj[x]) for x in range(5)]
our_commit_tx = make_commitment( our_commit_tx = make_commitment(
@ -567,7 +572,7 @@ class TestLNUtil(ElectrumTestCase):
local_feerate_per_kw, local_feerate_per_kw,
our_commit_tx)) our_commit_tx))
def htlc_tx(self, htlc, htlc_output_index, amount_msat, htlc_payment_preimage, remote_htlc_sig, success, cltv_timeout, local_feerate_per_kw, our_commit_tx): def htlc_tx(self, htlc, htlc_output_index, amount_msat, htlc_payment_preimage, remote_htlc_sig, success, cltv_abs, local_feerate_per_kw, our_commit_tx):
_script, our_htlc_tx_output = make_htlc_tx_output( _script, our_htlc_tx_output = make_htlc_tx_output(
amount_msat=amount_msat, amount_msat=amount_msat,
local_feerate=local_feerate_per_kw, local_feerate=local_feerate_per_kw,
@ -581,7 +586,7 @@ class TestLNUtil(ElectrumTestCase):
amount_msat=amount_msat, amount_msat=amount_msat,
witness_script=htlc.hex()) witness_script=htlc.hex())
our_htlc_tx = make_htlc_tx( our_htlc_tx = make_htlc_tx(
cltv_expiry=cltv_timeout, cltv_abs=cltv_abs,
inputs=our_htlc_tx_inputs, inputs=our_htlc_tx_inputs,
output=our_htlc_tx_output) output=our_htlc_tx_output)

36
electrum/trampoline.py

@ -6,7 +6,7 @@ from typing import Mapping, DefaultDict, Tuple, Optional, Dict, List, Iterable,
from .lnutil import LnFeatures from .lnutil import LnFeatures
from .lnonion import calc_hops_data_for_payment, new_onion_packet from .lnonion import calc_hops_data_for_payment, new_onion_packet
from .lnrouter import RouteEdge, TrampolineEdge, LNPaymentRoute, is_route_sane_to_use from .lnrouter import RouteEdge, TrampolineEdge, LNPaymentRoute, is_route_sane_to_use, LNPaymentTRoute
from .lnutil import NoPathFound, LNPeerAddr from .lnutil import NoPathFound, LNPeerAddr
from . import constants from . import constants
from .logging import get_logger from .logging import get_logger
@ -183,7 +183,7 @@ def _extend_trampoline_route(
end_node=end_node, end_node=end_node,
fee_base_msat=policy['fee_base_msat'] if pay_fees else 0, fee_base_msat=policy['fee_base_msat'] if pay_fees else 0,
fee_proportional_millionths=policy['fee_proportional_millionths'] if pay_fees else 0, fee_proportional_millionths=policy['fee_proportional_millionths'] if pay_fees else 0,
cltv_expiry_delta=policy['cltv_expiry_delta'] if pay_fees else 0, cltv_delta=policy['cltv_expiry_delta'] if pay_fees else 0,
node_features=trampoline_features)) node_features=trampoline_features))
@ -208,7 +208,7 @@ def _choose_second_trampoline(
def create_trampoline_route( def create_trampoline_route(
*, *,
amount_msat: int, amount_msat: int,
min_cltv_expiry: int, min_final_cltv_delta: int,
invoice_pubkey: bytes, invoice_pubkey: bytes,
invoice_features: int, invoice_features: int,
my_pubkey: bytes, my_pubkey: bytes,
@ -217,7 +217,7 @@ def create_trampoline_route(
trampoline_fee_level: int, trampoline_fee_level: int,
use_two_trampolines: bool, use_two_trampolines: bool,
failed_routes: Iterable[Sequence[str]], failed_routes: Iterable[Sequence[str]],
) -> LNPaymentRoute: ) -> LNPaymentTRoute:
# we decide whether to convert to a legacy payment # we decide whether to convert to a legacy payment
is_legacy, invoice_trampolines = is_legacy_relay(invoice_features, r_tags) is_legacy, invoice_trampolines = is_legacy_relay(invoice_features, r_tags)
@ -262,25 +262,25 @@ def create_trampoline_route(
# check that we can pay amount and fees # check that we can pay amount and fees
for edge in route[::-1]: for edge in route[::-1]:
amount_msat += edge.fee_for_edge(amount_msat) amount_msat += edge.fee_for_edge(amount_msat)
if not is_route_sane_to_use(route, amount_msat, min_cltv_expiry): if not is_route_sane_to_use(route, amount_msat, min_final_cltv_delta):
raise NoPathFound("We cannot afford to pay the fees.") raise NoPathFound("We cannot afford to pay the fees.")
return route return route
def create_trampoline_onion( def create_trampoline_onion(
*, *,
route, route: LNPaymentTRoute,
amount_msat, amount_msat: int,
final_cltv, final_cltv_abs: int,
total_msat: int, total_msat: int,
payment_hash: bytes, payment_hash: bytes,
payment_secret: bytes, payment_secret: bytes,
): ):
# all edges are trampoline # all edges are trampoline
hops_data, amount_msat, cltv = calc_hops_data_for_payment( hops_data, amount_msat, cltv_abs = calc_hops_data_for_payment(
route, route,
amount_msat, amount_msat,
final_cltv, final_cltv_abs=final_cltv_abs,
total_msat=total_msat, total_msat=total_msat,
payment_secret=payment_secret) payment_secret=payment_secret)
# detect trampoline hops. # detect trampoline hops.
@ -313,14 +313,14 @@ def create_trampoline_onion(
trampoline_onion = new_onion_packet(payment_path_pubkeys, trampoline_session_key, hops_data, associated_data=payment_hash, trampoline=True) trampoline_onion = new_onion_packet(payment_path_pubkeys, trampoline_session_key, hops_data, associated_data=payment_hash, trampoline=True)
trampoline_onion._debug_hops_data = hops_data trampoline_onion._debug_hops_data = hops_data
trampoline_onion._debug_route = route trampoline_onion._debug_route = route
return trampoline_onion, amount_msat, cltv return trampoline_onion, amount_msat, cltv_abs
def create_trampoline_route_and_onion( def create_trampoline_route_and_onion(
*, *,
amount_msat, amount_msat,
total_msat, total_msat,
min_cltv_expiry, min_final_cltv_delta: int,
invoice_pubkey, invoice_pubkey,
invoice_features, invoice_features,
my_pubkey: bytes, my_pubkey: bytes,
@ -335,7 +335,7 @@ def create_trampoline_route_and_onion(
# create route for the trampoline_onion # create route for the trampoline_onion
trampoline_route = create_trampoline_route( trampoline_route = create_trampoline_route(
amount_msat=amount_msat, amount_msat=amount_msat,
min_cltv_expiry=min_cltv_expiry, min_final_cltv_delta=min_final_cltv_delta,
my_pubkey=my_pubkey, my_pubkey=my_pubkey,
invoice_pubkey=invoice_pubkey, invoice_pubkey=invoice_pubkey,
invoice_features=invoice_features, invoice_features=invoice_features,
@ -345,16 +345,16 @@ def create_trampoline_route_and_onion(
use_two_trampolines=use_two_trampolines, use_two_trampolines=use_two_trampolines,
failed_routes=failed_routes) failed_routes=failed_routes)
# compute onion and fees # compute onion and fees
final_cltv = local_height + min_cltv_expiry final_cltv_abs = local_height + min_final_cltv_delta
trampoline_onion, amount_with_fees, bucket_cltv = create_trampoline_onion( trampoline_onion, amount_with_fees, bucket_cltv_abs = create_trampoline_onion(
route=trampoline_route, route=trampoline_route,
amount_msat=amount_msat, amount_msat=amount_msat,
final_cltv=final_cltv, final_cltv_abs=final_cltv_abs,
total_msat=total_msat, total_msat=total_msat,
payment_hash=payment_hash, payment_hash=payment_hash,
payment_secret=payment_secret) payment_secret=payment_secret)
bucket_cltv_delta = bucket_cltv - local_height bucket_cltv_delta = bucket_cltv_abs - local_height
bucket_cltv_delta += trampoline_route[0].cltv_expiry_delta bucket_cltv_delta += trampoline_route[0].cltv_delta
# trampoline fee for this very trampoline # trampoline fee for this very trampoline
trampoline_fee = trampoline_route[0].fee_for_edge(amount_with_fees) trampoline_fee = trampoline_route[0].fee_for_edge(amount_with_fees)
amount_with_fees += trampoline_fee amount_with_fees += trampoline_fee

Loading…
Cancel
Save