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. 108
      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. 46
      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):
key: bytes
cltv_expiry_delta: int
cltv_delta: int
htlc_minimum_msat: int
htlc_maximum_msat: Optional[int]
fee_base_msat: int
@ -116,7 +116,7 @@ class Policy(NamedTuple):
def from_msg(payload: dict) -> 'Policy':
return Policy(
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_maximum_msat = payload.get('htlc_maximum_msat', None),
fee_base_msat = payload['fee_base_msat'],
@ -136,7 +136,7 @@ class Policy(NamedTuple):
def from_route_edge(route_edge: 'RouteEdge') -> 'Policy':
return Policy(
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_maximum_msat=None,
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:
changed = False
if old_policy.cltv_expiry_delta != new_policy.cltv_expiry_delta:
if old_policy.cltv_delta != new_policy.cltv_delta:
changed |= True
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:
changed |= True
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:
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(_('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())])
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.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.logging import Logger
from electrum.qrreader import MissingQrDetectionLib
@ -52,6 +52,8 @@ else:
MONOSPACE_FONT = 'monospace'
_logger = get_logger(__name__)
dialogs = []
pr_icons = {

4
electrum/lnaddr.py

@ -336,7 +336,7 @@ class LnAddr(object):
", ".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')
if cltv is None:
return 18
@ -375,7 +375,7 @@ class LnAddr(object):
'description': self.get_description(),
'exp': self.get_expiry(),
'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(),
'tags': self.tags,
'unknown_tags': self.unknown_tags,

14
electrum/lnchannel.py

@ -631,7 +631,7 @@ class Channel(AbstractChannel):
# TODO enforce this ^
# 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_proportional_millionths = 1
@ -784,7 +784,7 @@ class Channel(AbstractChannel):
short_channel_id=scid,
channel_flags=channel_flags,
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_maximum_msat=htlc_maximum_msat,
fee_base_msat=self.forwarding_fee_base_msat,
@ -1549,7 +1549,7 @@ class Channel(AbstractChannel):
remote_htlc_pubkey=other_htlc_pubkey,
local_htlc_pubkey=this_htlc_pubkey,
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
# in the resulting dict to correspond to the to_local and to_remote *outputs* of the ctx
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
# but the remote did not revoke yet, and the CLTV of this HTLC is dangerously close
# 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)),
(REMOTE, SENT, self.get_oldest_unrevoked_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():
if not self.hm.was_htlc_preimage_released(htlc_id=htlc_id, htlc_proposer=REMOTE):
continue
if htlc.cltv_expiry - recv_htlc_deadline > local_height:
if htlc.cltv_abs - recv_htlc_deadline_delta > local_height:
continue
htlcs_we_could_reclaim[(RECEIVED, htlc_id)] = htlc
# 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
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)),
(REMOTE, RECEIVED, self.get_oldest_unrevoked_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():
if htlc.cltv_expiry + offered_htlc_deadline > local_height:
if htlc.cltv_abs + offered_htlc_deadline_delta > local_height:
continue
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(
route: 'LNPaymentRoute',
amount_msat: int,
final_cltv: int, *,
amount_msat: int, # that final recipient receives
*,
final_cltv_abs: int,
total_msat: int,
payment_secret: bytes,
) -> Tuple[List[OnionHopsDataSingle], int, int]:
"""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:
raise PaymentFailure(f"too long route ({len(route)} edges)")
# payload that will be seen by the last hop:
amt = amount_msat
cltv = final_cltv
cltv_abs = final_cltv_abs
hop_payload = {
"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
# partial amounts
@ -244,19 +244,19 @@ def calc_hops_data_for_payment(
is_trampoline = route_edge.is_trampoline()
if is_trampoline:
amt += route_edge.fee_for_edge(amt)
cltv += route_edge.cltv_expiry_delta
cltv_abs += route_edge.cltv_delta
hop_payload = {
"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},
}
hops_data.append(
OnionHopsDataSingle(payload=hop_payload))
if not is_trampoline:
amt += route_edge.fee_for_edge(amt)
cltv += route_edge.cltv_expiry_delta
cltv_abs += route_edge.cltv_delta
hops_data.reverse()
return hops_data, amt, cltv
return hops_data, amt, cltv_abs
def _generate_filler(key_type: bytes, hops_data: Sequence[OnionHopsDataSingle],

108
electrum/lnpeer.py

@ -38,7 +38,7 @@ from .lnutil import (Outpoint, LocalConfig, RECEIVED, UpdateAddHtlc, ChannelConf
funding_output_script, get_per_commitment_secret_from_seed,
secret_to_pubkey, PaymentFailure, LnFeatures,
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,
RemoteMisbehaving, ShortChannelID,
IncompatibleLightningFeatures, derive_payment_secret_from_payment_preimage,
@ -1476,25 +1476,25 @@ class Peer(Logger):
amount_msat: int,
total_msat: int,
payment_hash: bytes,
min_final_cltv_expiry: int,
min_final_cltv_delta: int,
payment_secret: bytes,
trampoline_onion: Optional[OnionPacket] = None,
):
# add features learned during "init" for direct neighbour:
route[0].node_features |= self.features
local_height = self.network.get_local_height()
final_cltv = local_height + min_final_cltv_expiry
hops_data, amount_msat, cltv = calc_hops_data_for_payment(
final_cltv_abs = local_height + min_final_cltv_delta
hops_data, amount_msat, cltv_abs = calc_hops_data_for_payment(
route,
amount_msat,
final_cltv,
final_cltv_abs=final_cltv_abs,
total_msat=total_msat,
payment_secret=payment_secret)
num_hops = len(hops_data)
self.logger.info(f"lnpeer.pay len(route)={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}")
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
# if we are forwarding a trampoline payment, add 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
self.logger.info(f"starting payment. len(route)={len(hops_data)}.")
# create htlc
if cltv > local_height + lnutil.NBLOCK_CLTV_EXPIRY_TOO_FAR_INTO_FUTURE:
raise PaymentFailure(f"htlc expiry too far into future. (in {cltv-local_height} blocks)")
return onion, amount_msat, cltv, session_key
def send_htlc(self, chan, payment_hash, amount_msat, cltv, onion, session_key=None) -> UpdateAddHtlc:
htlc = UpdateAddHtlc(amount_msat=amount_msat, payment_hash=payment_hash, cltv_expiry=cltv, timestamp=int(time.time()))
if cltv_abs > local_height + lnutil.NBLOCK_CLTV_DELTA_TOO_FAR_INTO_FUTURE:
raise PaymentFailure(f"htlc expiry too far into future. (in {cltv_abs-local_height} blocks)")
return onion, amount_msat, cltv_abs, session_key
def send_htlc(
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)
if session_key:
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",
channel_id=chan.channel_id,
id=htlc.htlc_id,
cltv_expiry=htlc.cltv_expiry,
cltv_expiry=htlc.cltv_abs,
amount_msat=htlc.amount_msat,
payment_hash=htlc.payment_hash,
onion_routing_packet=onion.to_bytes())
@ -1544,7 +1553,7 @@ class Peer(Logger):
amount_msat: int,
total_msat: int,
payment_hash: bytes,
min_final_cltv_expiry: int,
min_final_cltv_delta: int,
payment_secret: bytes,
trampoline_onion: Optional[OnionPacket] = None,
) -> UpdateAddHtlc:
@ -1553,16 +1562,23 @@ class Peer(Logger):
assert len(route) > 0
if not chan.can_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,
amount_msat=amount_msat,
total_msat=total_msat,
payment_hash=payment_hash,
min_final_cltv_expiry=min_final_cltv_expiry,
min_final_cltv_delta=min_final_cltv_delta,
payment_secret=payment_secret,
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
def send_revoke_and_ack(self, chan: Channel):
@ -1619,21 +1635,21 @@ class Peer(Logger):
def on_update_add_htlc(self, chan: Channel, payload):
payment_hash = payload["payment_hash"]
htlc_id = payload["id"]
cltv_expiry = payload["cltv_expiry"]
cltv_abs = payload["cltv_expiry"]
amount_msat_htlc = payload["amount_msat"]
onion_packet = payload["onion_routing_packet"]
htlc = UpdateAddHtlc(
amount_msat=amount_msat_htlc,
payment_hash=payment_hash,
cltv_expiry=cltv_expiry,
cltv_abs=cltv_abs,
timestamp=int(time.time()),
htlc_id=htlc_id)
self.logger.info(f"on_update_add_htlc. chan {chan.short_channel_id}. htlc={str(htlc)}")
if chan.get_state() != ChannelState.OPEN:
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)
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
chan.receive_htlc(htlc, onion_packet)
util.trigger_callback('htlc_added', chan, htlc, RECEIVED)
@ -1674,7 +1690,7 @@ class Peer(Logger):
except Exception:
raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00')
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:
raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00')
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):
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)
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(
f"INCORRECT_CLTV_EXPIRY. "
f"{htlc.cltv_expiry=} - {next_cltv_expiry=} < {next_chan.forwarding_cltv_expiry_delta=}")
data = htlc.cltv_expiry.to_bytes(4, byteorder="big") + outgoing_chan_upd_message
f"{htlc.cltv_abs=} - {next_cltv_abs=} < {next_chan.forwarding_cltv_delta=}")
data = htlc.cltv_abs.to_bytes(4, byteorder="big") + outgoing_chan_upd_message
raise OnionRoutingFailure(code=OnionFailureCode.INCORRECT_CLTV_EXPIRY, data=data)
if htlc.cltv_expiry - lnutil.MIN_FINAL_CLTV_EXPIRY_ACCEPTED <= local_height \
or next_cltv_expiry <= local_height:
if htlc.cltv_abs - lnutil.MIN_FINAL_CLTV_DELTA_ACCEPTED <= local_height \
or next_cltv_abs <= local_height:
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'')
forwarding_fees = fee_for_edge_msat(
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()})")
raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_CHANNEL_FAILURE, data=outgoing_chan_upd_message)
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:
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)
@ -1732,7 +1754,7 @@ class Peer(Logger):
async def maybe_forward_trampoline(
self, *,
payment_hash: bytes,
cltv_expiry: int,
inc_cltv_abs: int,
outer_onion: ProcessedOnionPacket,
trampoline_onion: ProcessedOnionPacket):
@ -1751,7 +1773,7 @@ class Peer(Logger):
try:
outgoing_node_id = payload["outgoing_node_id"]["outgoing_node_id"]
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:
self.logger.info('forward_trampoline: legacy')
next_trampoline_onion = None
@ -1777,11 +1799,11 @@ class Peer(Logger):
# these are the fee/cltv paid by the sender
# 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"]
trampoline_fee = total_msat - amt_to_forward
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".
# Blocks might have been mined since.
# - 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.
# 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
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:
await self.lnworker.pay_to_node(
@ -1797,10 +1819,10 @@ class Peer(Logger):
payment_hash=payment_hash,
payment_secret=payment_secret,
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.
# 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,
invoice_features=invoice_features,
fwd_trampoline_onion=next_trampoline_onion,
@ -1863,20 +1885,20 @@ class Peer(Logger):
exc_incorrect_or_unknown_pd = OnionRoutingFailure(
code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS,
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:
log_fail_reason(f"htlc.cltv_expiry is unreasonably close")
if local_height + MIN_FINAL_CLTV_DELTA_ACCEPTED > htlc.cltv_abs:
log_fail_reason(f"htlc.cltv_abs is unreasonably close")
raise exc_incorrect_or_unknown_pd
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:
log_fail_reason(f"'outgoing_cltv_value' missing from onion")
raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00')
if cltv_from_onion > htlc.cltv_expiry:
log_fail_reason(f"cltv_from_onion != htlc.cltv_expiry")
if cltv_abs_from_onion > htlc.cltv_abs:
log_fail_reason(f"cltv_abs_from_onion != htlc.cltv_abs")
raise OnionRoutingFailure(
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:
total_msat = processed_onion.hop_data.payload["payment_data"]["total_msat"]
except Exception:
@ -1943,7 +1965,7 @@ class Peer(Logger):
else:
callback = lambda: self.maybe_forward_trampoline(
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,
trampoline_onion=trampoline_onion)
return None, callback

19
electrum/lnrouter.py

@ -35,7 +35,7 @@ from math import inf
from .util import profiler, with_lock
from .logging import Logger
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
if TYPE_CHECKING:
@ -75,7 +75,7 @@ class PathEdge:
class RouteEdge(PathEdge):
fee_base_msat = 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!
def fee_for_edge(self, amount_msat: int) -> int:
@ -102,13 +102,13 @@ class RouteEdge(PathEdge):
short_channel_id=ShortChannelID.normalize(short_channel_id),
fee_base_msat=channel_policy.fee_base_msat,
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)
def is_sane_to_use(self, amount_msat: int) -> bool:
# TODO revise ad-hoc heuristics
# cltv cannot be more than 2 weeks
if self.cltv_expiry_delta > 14 * 144:
if self.cltv_delta > 14 * 144:
return False
total_fee = self.fee_for_edge(amount_msat)
if not is_fee_sane(total_fee, payment_amount_msat=amount_msat):
@ -135,23 +135,24 @@ class TrampolineEdge(RouteEdge):
LNPaymentPath = Sequence[PathEdge]
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.
called when we are paying; so e.g. lower cltv is better
"""
if len(route) > NUM_MAX_EDGES_IN_PAYMENT_PATH:
return False
amt = invoice_amount_msat
cltv = min_final_cltv_expiry
cltv_delta = min_final_cltv_delta
for route_edge in reversed(route[1:]):
if not route_edge.is_sane_to_use(amt): return False
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
# 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
# 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.
@ -527,7 +528,7 @@ class LNPathFinder(Logger):
if ignore_costs:
return DEFAULT_PENALTY_BASE_MSAT, 0
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 payment and penalize edges that cannot
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):
name: str
csv_delay: int
cltv_expiry: int
cltv_abs: int
gen_tx: Callable[[], Optional[Transaction]]
@ -177,7 +177,7 @@ def create_sweeptx_for_their_revoked_htlc(
return SweepInfo(
name='redeem_htlc2',
csv_delay=0,
cltv_expiry=0,
cltv_abs=0,
gen_tx=gen_tx)
@ -240,7 +240,7 @@ def create_sweeptxs_for_our_ctx(
txs[prevout] = SweepInfo(
name='our_ctx_to_local',
csv_delay=to_self_delay,
cltv_expiry=0,
cltv_abs=0,
gen_tx=sweep_tx)
we_breached = ctn < chan.get_oldest_unrevoked_ctn(LOCAL)
if we_breached:
@ -277,12 +277,12 @@ def create_sweeptxs_for_our_ctx(
txs[htlc_tx.inputs()[0].prevout.to_str()] = SweepInfo(
name='first-stage-htlc',
csv_delay=0,
cltv_expiry=htlc_tx.locktime,
cltv_abs=htlc_tx.locktime,
gen_tx=lambda: htlc_tx)
txs[htlc_tx.txid() + ':0'] = SweepInfo(
name='second-stage-htlc',
csv_delay=to_self_delay,
cltv_expiry=0,
cltv_abs=0,
gen_tx=sweep_tx)
# offered HTLCs, in our ctx --> "timeout"
@ -381,7 +381,7 @@ def create_sweeptxs_for_their_ctx(
txs[tx.inputs()[0].prevout.to_str()] = SweepInfo(
name='to_local_for_revoked_ctx',
csv_delay=0,
cltv_expiry=0,
cltv_abs=0,
gen_tx=gen_tx)
# prep
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),
local_htlc_pubkey=their_htlc_pubkey,
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
sweep_tx = lambda: create_sweeptx_their_ctx_htlc(
ctx=ctx,
@ -412,12 +412,12 @@ def create_sweeptxs_for_their_ctx(
output_idx=ctx_output_idx,
privkey=our_revocation_privkey if is_revocation else our_htlc_privkey.get_secret_bytes(),
is_revocation=is_revocation,
cltv_expiry=cltv_expiry,
cltv_abs=cltv_abs,
config=chan.lnworker.config)
txs[prevout] = SweepInfo(
name=f'their_ctx_htlc_{ctx_output_idx}',
csv_delay=0,
cltv_expiry=cltv_expiry,
cltv_abs=cltv_abs,
gen_tx=sweep_tx)
# received HTLCs, in their ctx --> "timeout"
# offered HTLCs, in their ctx --> "success"
@ -480,8 +480,8 @@ def create_sweeptx_their_ctx_htlc(
ctx: Transaction, witness_script: bytes, sweep_address: str,
preimage: Optional[bytes], output_idx: int,
privkey: bytes, is_revocation: bool,
cltv_expiry: int, config: SimpleConfig) -> Optional[PartialTransaction]:
assert type(cltv_expiry) is int
cltv_abs: int, config: SimpleConfig) -> Optional[PartialTransaction]:
assert type(cltv_abs) is int
preimage = preimage or b'' # preimage is required iff (not is_revocation and htlc is offered)
val = ctx.outputs()[output_idx].value
prevout = TxOutpoint(txid=bfh(ctx.txid()), out_idx=output_idx)
@ -495,7 +495,7 @@ def create_sweeptx_their_ctx_htlc(
outvalue = val - fee
if outvalue <= dust_threshold(): return None
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))
if not is_revocation:
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
# 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
# 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 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 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
@ -621,14 +621,19 @@ def make_htlc_tx_inputs(htlc_output_txid: str, htlc_output_index: int,
c_inputs = [txin]
return c_inputs
def make_htlc_tx(*, cltv_expiry: int, inputs: List[PartialTxInput], output: PartialTxOutput) -> PartialTransaction:
assert type(cltv_expiry) is int
def make_htlc_tx(*, cltv_abs: int, inputs: List[PartialTxInput], output: PartialTxOutput) -> PartialTransaction:
assert type(cltv_abs) is int
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
def make_offered_htlc(revocation_pubkey: bytes, remote_htlcpubkey: bytes,
local_htlcpubkey: bytes, payment_hash: bytes) -> bytes:
def make_offered_htlc(
*,
revocation_pubkey: bytes,
remote_htlcpubkey: bytes,
local_htlcpubkey: bytes,
payment_hash: bytes,
) -> bytes:
assert type(revocation_pubkey) is bytes
assert type(remote_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
def make_received_htlc(revocation_pubkey: bytes, remote_htlcpubkey: bytes,
local_htlcpubkey: bytes, payment_hash: bytes, cltv_expiry: int) -> bytes:
def make_received_htlc(
*,
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]:
assert type(i) is bytes
assert type(cltv_expiry) is int
assert type(cltv_abs) is int
script = bfh(construct_script([
opcodes.OP_DUP,
@ -693,7 +704,7 @@ def make_received_htlc(revocation_pubkey: bytes, remote_htlcpubkey: bytes,
opcodes.OP_CHECKMULTISIG,
opcodes.OP_ELSE,
opcodes.OP_DROP,
cltv_expiry,
cltv_abs,
opcodes.OP_CHECKLOCKTIMEVERIFY,
opcodes.OP_DROP,
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,
local_htlc_pubkey: bytes, payment_hash: bytes, cltv_expiry: Optional[int]) -> bytes:
def make_htlc_output_witness_script(
*,
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:
return make_received_htlc(revocation_pubkey=remote_revocation_pubkey,
remote_htlcpubkey=remote_htlc_pubkey,
local_htlcpubkey=local_htlc_pubkey,
payment_hash=payment_hash,
cltv_expiry=cltv_expiry)
cltv_abs=cltv_abs)
else:
return make_offered_htlc(revocation_pubkey=remote_revocation_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',
htlc_direction: 'Direction', ctx: Transaction,
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
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,
local_htlc_pubkey=htlc_pubkey,
payment_hash=payment_hash,
cltv_expiry=cltv_expiry)
cltv_abs=cltv_abs)
htlc_address = redeem_script_to_address('p2wsh', preimage_script.hex())
candidates = ctx.get_output_idxs_from_address(htlc_address)
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]
unclaimed_ctx_output_idxs = set(range(len(ctx.outputs())))
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.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 htlc in htlcs:
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,
htlc_direction: 'Direction', commit: Transaction, ctx_output_idx: int,
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
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,
local_htlc_pubkey=htlc_pubkey,
payment_hash=payment_hash,
cltv_expiry=cltv_expiry)
cltv_abs=cltv_abs)
htlc_tx_inputs = make_htlc_tx_inputs(
commit.txid(), ctx_output_idx,
amount_msat=amount_msat,
witness_script=preimage_script.hex())
if is_htlc_success:
cltv_expiry = 0
htlc_tx = make_htlc_tx(cltv_expiry=cltv_expiry, inputs=htlc_tx_inputs, output=htlc_tx_output)
cltv_abs = 0
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
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."
# so we sort by cltv_expiry now; and the later BIP69-sort is assumed to be *stable*
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(
fees_per_participant=fees_per_participant,
@ -1595,21 +1613,21 @@ NUM_MAX_EDGES_IN_PAYMENT_PATH = NUM_MAX_HOPS_IN_PAYMENT_PATH
class UpdateAddHtlc:
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())
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)
htlc_id = attr.ib(type=int, kw_only=True, default=None)
@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(
amount_msat=amount_msat,
payment_hash=payment_hash,
cltv_expiry=cltv_expiry,
cltv_abs=cltv_abs,
htlc_id=htlc_id,
timestamp=timestamp)
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):

4
electrum/lnwatcher.py

@ -510,8 +510,8 @@ class LNWalletWatcher(LNWatcher):
prev_txid, prev_index = prevout.split(':')
can_broadcast = True
local_height = self.network.get_local_height()
if sweep_info.cltv_expiry:
wanted_height = sweep_info.cltv_expiry
if sweep_info.cltv_abs:
wanted_height = sweep_info.cltv_abs
if wanted_height - local_height > 0:
can_broadcast = False
# 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,
PaymentFailure, split_host_port, ConnStringFormatError,
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,
UpdateAddHtlc, Direction, LnFeatures, ShortChannelID,
HtlcLog, derive_payment_secret_from_payment_preimage,
@ -662,7 +662,7 @@ class PaySession(Logger):
initial_trampoline_fee_level: int,
invoice_features: int,
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
invoice_pubkey: bytes,
uses_trampoline: bool, # whether sender uses trampoline or gossip
@ -677,7 +677,7 @@ class PaySession(Logger):
self.invoice_features = LnFeatures(invoice_features)
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.invoice_pubkey = invoice_pubkey
@ -1387,7 +1387,7 @@ class LNWallet(LNWorker):
) -> Tuple[bool, List[HtlcLog]]:
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
key = payment_hash.hex()
payment_secret = lnaddr.payment_secret
@ -1417,7 +1417,7 @@ class LNWallet(LNWorker):
payment_hash=payment_hash,
payment_secret=payment_secret,
amount_to_pay=amount_to_pay,
min_cltv_expiry=min_cltv_expiry,
min_final_cltv_delta=min_final_cltv_delta,
r_tags=r_tags,
invoice_features=invoice_features,
attempts=attempts,
@ -1447,7 +1447,7 @@ class LNWallet(LNWorker):
payment_hash: bytes,
payment_secret: bytes,
amount_to_pay: int, # in msat
min_cltv_expiry: int,
min_final_cltv_delta: int,
r_tags,
invoice_features: int,
attempts: int = None,
@ -1473,7 +1473,7 @@ class LNWallet(LNWorker):
initial_trampoline_fee_level=self.config.INITIAL_TRAMPOLINE_FEE_LEVEL,
invoice_features=invoice_features,
r_tags=r_tags,
min_cltv_expiry=min_cltv_expiry,
min_final_cltv_delta=min_final_cltv_delta,
amount_to_pay=amount_to_pay,
invoice_pubkey=node_pubkey,
uses_trampoline=self.uses_trampoline(),
@ -1502,7 +1502,7 @@ class LNWallet(LNWorker):
await self.pay_to_route(
paysession=paysession,
sent_htlc_info=sent_htlc_info,
min_cltv_expiry=cltv_delta,
min_final_cltv_delta=cltv_delta,
trampoline_onion=trampoline_onion,
)
# invoice_status is triggered in self.set_invoice_status when it actually changes.
@ -1559,7 +1559,7 @@ class LNWallet(LNWorker):
self, *,
paysession: PaySession,
sent_htlc_info: SentHtlcInfo,
min_cltv_expiry: int,
min_final_cltv_delta: int,
trampoline_onion: Optional[OnionPacket] = None,
) -> None:
"""Sends a single HTLC."""
@ -1578,7 +1578,7 @@ class LNWallet(LNWorker):
amount_msat=shi.amount_msat,
total_msat=shi.bucket_msat,
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,
trampoline_onion=trampoline_onion)
@ -1735,10 +1735,10 @@ class LNWallet(LNWorker):
if addr.amount is None:
raise InvoiceError(_("Missing amount"))
# 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(
_("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
addr.validate_and_compare_features(self.features)
return addr
@ -1858,13 +1858,13 @@ class LNWallet(LNWorker):
trampoline_onion = None
per_trampoline_secret = paysession.payment_secret
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
else:
trampoline_route, trampoline_onion, per_trampoline_amount_with_fees, per_trampoline_cltv_delta = create_trampoline_route_and_onion(
amount_msat=per_trampoline_amount,
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,
invoice_pubkey=paysession.invoice_pubkey,
invoice_features=paysession.invoice_features,
@ -1896,7 +1896,7 @@ class LNWallet(LNWorker):
short_channel_id=chan.short_channel_id,
fee_base_msat=0,
fee_proportional_millionths=0,
cltv_expiry_delta=0,
cltv_delta=0,
node_features=trampoline_features)
]
self.logger.info(f'adding route {part_amount_msat} {delta_fee} {margin}')
@ -1925,7 +1925,7 @@ class LNWallet(LNWorker):
self.create_route_for_payment,
amount_msat=part_amount_msat,
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,
invoice_features=paysession.invoice_features,
my_sending_channels=[channel] if is_multichan_mpp else my_active_channels,
@ -1942,7 +1942,7 @@ class LNWallet(LNWorker):
trampoline_fee_level=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:
continue
for route in routes:
@ -1955,7 +1955,7 @@ class LNWallet(LNWorker):
self, *,
amount_msat: int,
invoice_pubkey: bytes,
min_cltv_expiry: int,
min_final_cltv_delta: int,
r_tags,
invoice_features: int,
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])}')
continue
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)
# if we have a routing policy for this edge in the db, that takes precedence,
# as it is likely from a previous failure
@ -1989,7 +1989,7 @@ class LNWallet(LNWorker):
if channel_policy:
fee_base_msat = channel_policy.fee_base_msat
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)
route_edge = RouteEdge(
start_node=start_node,
@ -1997,7 +1997,7 @@ class LNWallet(LNWorker):
short_channel_id=short_channel_id,
fee_base_msat=fee_base_msat,
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)
private_route_edges[route_edge.short_channel_id] = route_edge
start_node = end_node
@ -2015,7 +2015,7 @@ class LNWallet(LNWorker):
if not route:
raise NoPathFound()
# 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}")
raise NoPathFound()
assert len(route) > 0
@ -2059,7 +2059,7 @@ class LNWallet(LNWorker):
amount=amount_btc,
tags=[
('d', message),
('c', MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE),
('c', MIN_FINAL_CLTV_DELTA_FOR_INVOICE),
('x', expiry),
('9', invoice_features),
('f', fallback_address),
@ -2400,14 +2400,14 @@ class LNWallet(LNWorker):
# (they will get the channel update from the onion error)
# at least, that's the theory. https://github.com/lightningnetwork/lnd/issues/2066
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
if channel_info:
policy = get_mychannel_policy(channel_info.short_channel_id, chan.node_id, scid_to_my_channels)
if policy:
fee_base_msat = policy.fee_base_msat
fee_proportional_millionths = policy.fee_proportional_millionths
cltv_expiry_delta = policy.cltv_expiry_delta
cltv_delta = policy.cltv_delta
missing_info = False
if missing_info:
self.logger.info(
@ -2418,7 +2418,7 @@ class LNWallet(LNWorker):
alias_or_scid,
fee_base_msat,
fee_proportional_millionths,
cltv_expiry_delta)]))
cltv_delta)]))
return routing_hints
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):
lnaddr = lndecode("lnsb500u1pdsgyf3pp5nmrqejdsdgs4n9ukgxcp2kcq265yhrxd4k5dyue58rxtp5y83s3qsp5qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqsdqqcqzys9qypqsqp2h6a5xeytuc3fad2ed4gxvhd593lwjdna3dxsyeem0qkzjx6guk44jend0xq4zzvp6f3fy07wnmxezazzsxgmvqee8shxjuqu2eu0qpnvc95x",
net=constants.BitcoinSimnet)
self.assertEqual(144, lnaddr.get_min_final_cltv_expiry())
self.assertEqual(144, lnaddr.get_min_final_cltv_delta())
lnaddr = lndecode("lntb15u1p0m6lzupp5zqjthgvaad9mewmdjuehwddyze9d8zyxcc43zhaddeegt37sndgsdq4xysyymr0vd4kzcmrd9hx7cqp7xqrrss9qy9qsqsp5vlhcs24hwm747w8f3uau2tlrdkvjaglffnsstwyamj84cxuhrn2s8tut3jqumepu42azyyjpgqa4w9w03204zp9h4clk499y2umstl6s29hqyj8vv4as6zt5567ux7l3f66m8pjhk65zjaq2esezk7ll2kcpljewkg",
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):
for cltv in (1, 15, 16, 31, 32, 33, 150, 511, 512, 513, 1023, 1024, 1025):
lnaddr = LnAddr(
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)
self.assertEqual(cltv, lndecode(invoice).get_min_final_cltv_expiry())
self.assertEqual(cltv, lndecode(invoice).get_min_final_cltv_delta())
def test_features(self):
lnaddr = lndecode("lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsdq5vdhkven9v5sxyetpdees9qypqsztrz5v3jfnxskfv7g8chmyzyrfhf2vupcavuq5rce96kyt6g0zh337h206awccwp335zarqrud4wccgdn39vur44d8um4hmgv06aj0sgpdrv73z")

46
electrum/tests/test_lnchannel.py

@ -33,7 +33,7 @@ from electrum import lnpeer
from electrum import lnchannel
from electrum import lnutil
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.lnchannel import ChannelState
from electrum.json_db import StoredDict
@ -240,7 +240,7 @@ class TestChannel(ElectrumTestCase):
self.htlc_dict = {
'payment_hash' : paymentHash,
'amount_msat' : one_bitcoin_in_msat,
'cltv_expiry' : 5,
'cltv_abs' : 5,
'timestamp' : 0,
}
@ -648,15 +648,15 @@ class TestAvailableToSpend(ElectrumTestCase):
paymentPreimage = b"\x01" * 32
paymentHash = bitcoin.sha256(paymentPreimage)
htlc_dict = {
'payment_hash' : paymentHash,
'amount_msat' : one_bitcoin_in_msat * 41 // 10,
'cltv_expiry' : 5,
'timestamp' : 0,
}
alice_idx = alice_channel.add_htlc(htlc_dict).htlc_id
bob_idx = bob_channel.receive_htlc(htlc_dict).htlc_id
htlc = UpdateAddHtlc(
payment_hash=paymentHash,
amount_msat=one_bitcoin_in_msat * 41 // 10,
cltv_abs=5,
timestamp=0,
)
alice_idx = alice_channel.add_htlc(htlc).htlc_id
bob_idx = bob_channel.receive_htlc(htlc).htlc_id
self.assertEqual(89984088000, alice_channel.available_to_spend(LOCAL))
self.assertEqual(500000000000, bob_channel.available_to_spend(LOCAL))
@ -672,20 +672,20 @@ class TestAvailableToSpend(ElectrumTestCase):
# available.
# We try adding an HTLC of value 1 BTC, which should fail because the
# balance is unavailable.
htlc_dict = {
'payment_hash' : paymentHash,
'amount_msat' : one_bitcoin_in_msat,
'cltv_expiry' : 5,
'timestamp' : 0,
}
htlc = UpdateAddHtlc(
payment_hash=paymentHash,
amount_msat=one_bitcoin_in_msat,
cltv_abs=5,
timestamp=0,
)
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
# able to add the new HTLC.
force_state_transition(alice_channel, bob_channel)
self.assertEqual(499986152000, alice_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):
@ -722,7 +722,7 @@ class TestChanReserve(ElectrumTestCase):
htlc_dict = {
'payment_hash' : paymentHash,
'amount_msat' : int(.5 * one_bitcoin_in_msat),
'cltv_expiry' : 5,
'cltv_abs' : 5,
'timestamp' : 0,
}
self.alice_channel.add_htlc(htlc_dict)
@ -761,7 +761,7 @@ class TestChanReserve(ElectrumTestCase):
htlc_dict = {
'payment_hash' : paymentHash,
'amount_msat' : int(3.5 * one_bitcoin_in_msat),
'cltv_expiry' : 5,
'cltv_abs' : 5,
}
self.alice_channel.add_htlc(htlc_dict)
self.bob_channel.receive_htlc(htlc_dict)
@ -785,7 +785,7 @@ class TestChanReserve(ElectrumTestCase):
htlc_dict = {
'payment_hash' : paymentHash,
'amount_msat' : int(2 * one_bitcoin_in_msat),
'cltv_expiry' : 5,
'cltv_abs' : 5,
'timestamp' : 0,
}
alice_idx = self.alice_channel.add_htlc(htlc_dict).htlc_id
@ -830,7 +830,7 @@ class TestDust(ElectrumTestCase):
htlc = {
'payment_hash' : paymentHash,
'amount_msat' : 1000 * htlcAmt,
'cltv_expiry' : 5, # also in create_test_channels
'cltv_abs' : 5, # also in create_test_channels
'timestamp' : 0,
}

32
electrum/tests/test_lnpeer.py

@ -239,7 +239,7 @@ class MockLNWallet(Logger, EventListener, NetworkRetryManager[LNPeerAddr]):
initial_trampoline_fee_level=0,
invoice_features=decoded_invoice.get_features(),
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,
invoice_pubkey=decoded_invoice.pubkey.serialize(),
uses_trampoline=False,
@ -455,7 +455,7 @@ class TestPeer(ElectrumTestCase):
payment_preimage: bytes = None,
payment_hash: bytes = None,
invoice_features: LnFeatures = None,
min_final_cltv_expiry_delta: int = None,
min_final_cltv_delta: int = None,
) -> Tuple[LnAddr, str]:
amount_btc = amount_msat/Decimal(COIN*1000)
if payment_preimage is None and not payment_hash:
@ -477,13 +477,13 @@ class TestPeer(ElectrumTestCase):
payment_secret = w2.get_payment_secret(payment_hash)
else:
payment_secret = None
if min_final_cltv_expiry_delta is None:
min_final_cltv_expiry_delta = lnutil.MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE
if min_final_cltv_delta is None:
min_final_cltv_delta = lnutil.MIN_FINAL_CLTV_DELTA_FOR_INVOICE
lnaddr1 = LnAddr(
paymenthash=payment_hash,
amount=amount_btc,
tags=[
('c', min_final_cltv_expiry_delta),
('c', min_final_cltv_delta),
('d', 'coffee'),
('9', invoice_features),
] + routing_hints,
@ -574,13 +574,13 @@ class TestPeerDirect(TestPeer):
@staticmethod
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)
peer.send_message(
"update_add_htlc",
channel_id=chan.channel_id,
id=htlc.htlc_id,
cltv_expiry=htlc.cltv_expiry,
cltv_expiry=htlc.cltv_abs,
amount_msat=htlc.amount_msat,
payment_hash=htlc.payment_hash,
onion_routing_packet=1366 * b"0",
@ -798,7 +798,7 @@ class TestPeerDirect(TestPeer):
with self.assertRaises(lnutil.IncompatibleOrInsaneFeatures):
result, log = await w1.pay_invoice(pay_req)
# 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):
result, log = await w1.pay_invoice(pay_req)
raise SuccessfulTest()
@ -848,7 +848,7 @@ class TestPeerDirect(TestPeer):
await w1.pay_to_route(
sent_htlc_info=shi1,
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
# bob sends htlc BUT NOT COMMITMENT_SIGNED
@ -868,7 +868,7 @@ class TestPeerDirect(TestPeer):
await w2.pay_to_route(
sent_htlc_info=shi2,
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
# sleep a bit so that they both receive msgs sent so far
@ -941,7 +941,7 @@ class TestPeerDirect(TestPeer):
amount_msat=1000,
total_msat=lnaddr1.get_amount_msat(),
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,
)
p1.pay(
@ -950,7 +950,7 @@ class TestPeerDirect(TestPeer):
amount_msat=lnaddr1.get_amount_msat() - 1000,
total_msat=lnaddr1.get_amount_msat(),
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,
)
@ -1017,7 +1017,7 @@ class TestPeerDirect(TestPeer):
amount_msat=1000,
total_msat=2000,
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,
)
p1.pay(
@ -1026,7 +1026,7 @@ class TestPeerDirect(TestPeer):
amount_msat=1000,
total_msat=lnaddr1.get_amount_msat(),
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,
)
@ -1121,7 +1121,7 @@ class TestPeerDirect(TestPeer):
amount_msat=lnaddr.get_amount_msat(),
total_msat=lnaddr.get_amount_msat(),
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)
# alice closes
await p1.close_channel(alice_channel.channel_id)
@ -1264,7 +1264,7 @@ class TestPeerDirect(TestPeer):
pay = w1.pay_to_route(
sent_htlc_info=shi,
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())
with self.assertRaises(PaymentFailure):

21
electrum/tests/test_lnutil.py

@ -482,26 +482,31 @@ class TestLNUtil(ElectrumTestCase):
htlc_cltv_timeout = {}
htlc_payment_preimage = {}
htlc = {}
htlc_pubkeys = {
"revocation_pubkey": local_revocation_pubkey,
"remote_htlcpubkey": remote_htlcpubkey,
"local_htlcpubkey": local_htlcpubkey,
}
htlc_cltv_timeout[2] = 502
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_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_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_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_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"
output_commit_tx = "02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8007e80300000000000022002052bfef0479d7b293c27e0f1eb294bea154c63a3294ef092c19af51409bce0e2ad007000000000000220020403d394747cae42e98ff01734ad5c08f82ba123d3d9a620abda88989651e2ab5d007000000000000220020748eba944fedc8827f6b06bc44678f93c0f9e6078b35c6331ed31e75f8ce0c2db80b000000000000220020c20b5d1f8584fd90443e7b7b720136174fa4b9333c261d04dbbd012635c0f419a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de843110e0a06a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e04004730440220275b0c325a5e9355650dc30c0eccfbc7efb23987c24b556b9dfdd40effca18d202206caceb2c067836c51f296740c7ae807ffcbfbf1dd3a0d56b6de9a5b247985f060147304402204fd4928835db1ccdfc40f5c78ce9bd65249b16348df81f0c44328dcdefc97d630220194d3869c38bc732dd87d13d2958015e2fc16829e74cd4377f84d215c0b7060601475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220"
@ -512,7 +517,7 @@ class TestLNUtil(ElectrumTestCase):
(1, 2000 * 1000),
(3, 3000 * 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)]
our_commit_tx = make_commitment(
@ -567,7 +572,7 @@ class TestLNUtil(ElectrumTestCase):
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_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(
amount_msat=amount_msat,
local_feerate=local_feerate_per_kw,
@ -581,7 +586,7 @@ class TestLNUtil(ElectrumTestCase):
amount_msat=amount_msat,
witness_script=htlc.hex())
our_htlc_tx = make_htlc_tx(
cltv_expiry=cltv_timeout,
cltv_abs=cltv_abs,
inputs=our_htlc_tx_inputs,
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 .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 . import constants
from .logging import get_logger
@ -183,7 +183,7 @@ def _extend_trampoline_route(
end_node=end_node,
fee_base_msat=policy['fee_base_msat'] 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))
@ -208,7 +208,7 @@ def _choose_second_trampoline(
def create_trampoline_route(
*,
amount_msat: int,
min_cltv_expiry: int,
min_final_cltv_delta: int,
invoice_pubkey: bytes,
invoice_features: int,
my_pubkey: bytes,
@ -217,7 +217,7 @@ def create_trampoline_route(
trampoline_fee_level: int,
use_two_trampolines: bool,
failed_routes: Iterable[Sequence[str]],
) -> LNPaymentRoute:
) -> LNPaymentTRoute:
# we decide whether to convert to a legacy payment
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
for edge in route[::-1]:
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.")
return route
def create_trampoline_onion(
*,
route,
amount_msat,
final_cltv,
route: LNPaymentTRoute,
amount_msat: int,
final_cltv_abs: int,
total_msat: int,
payment_hash: bytes,
payment_secret: bytes,
):
# 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,
amount_msat,
final_cltv,
final_cltv_abs=final_cltv_abs,
total_msat=total_msat,
payment_secret=payment_secret)
# 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._debug_hops_data = hops_data
trampoline_onion._debug_route = route
return trampoline_onion, amount_msat, cltv
return trampoline_onion, amount_msat, cltv_abs
def create_trampoline_route_and_onion(
*,
amount_msat,
total_msat,
min_cltv_expiry,
min_final_cltv_delta: int,
invoice_pubkey,
invoice_features,
my_pubkey: bytes,
@ -335,7 +335,7 @@ def create_trampoline_route_and_onion(
# create route for the trampoline_onion
trampoline_route = create_trampoline_route(
amount_msat=amount_msat,
min_cltv_expiry=min_cltv_expiry,
min_final_cltv_delta=min_final_cltv_delta,
my_pubkey=my_pubkey,
invoice_pubkey=invoice_pubkey,
invoice_features=invoice_features,
@ -345,16 +345,16 @@ def create_trampoline_route_and_onion(
use_two_trampolines=use_two_trampolines,
failed_routes=failed_routes)
# compute onion and fees
final_cltv = local_height + min_cltv_expiry
trampoline_onion, amount_with_fees, bucket_cltv = create_trampoline_onion(
final_cltv_abs = local_height + min_final_cltv_delta
trampoline_onion, amount_with_fees, bucket_cltv_abs = create_trampoline_onion(
route=trampoline_route,
amount_msat=amount_msat,
final_cltv=final_cltv,
final_cltv_abs=final_cltv_abs,
total_msat=total_msat,
payment_hash=payment_hash,
payment_secret=payment_secret)
bucket_cltv_delta = bucket_cltv - local_height
bucket_cltv_delta += trampoline_route[0].cltv_expiry_delta
bucket_cltv_delta = bucket_cltv_abs - local_height
bucket_cltv_delta += trampoline_route[0].cltv_delta
# trampoline fee for this very trampoline
trampoline_fee = trampoline_route[0].fee_for_edge(amount_with_fees)
amount_with_fees += trampoline_fee

Loading…
Cancel
Save