partial merge 2: f321x's "lightning: refactor htlc switch"
split-off from https://github.com/spesmilo/electrum/pull/10230 Merge commit '286fc4b86e4d23cb9af15b9061b3d709e7592bcb'
This commit is contained in:
+12
-12
@@ -68,7 +68,7 @@ from .wallet import (
|
||||
)
|
||||
from .address_synchronizer import TX_HEIGHT_LOCAL
|
||||
from .mnemonic import Mnemonic
|
||||
from .lnutil import (channel_id_from_funding_tx, LnFeatures, SENT, MIN_FINAL_CLTV_DELTA_FOR_INVOICE,
|
||||
from .lnutil import (channel_id_from_funding_tx, LnFeatures, SENT, MIN_FINAL_CLTV_DELTA_ACCEPTED,
|
||||
PaymentFeeBudget, NBLOCK_CLTV_DELTA_TOO_FAR_INTO_FUTURE)
|
||||
from .plugin import run_hook, DeviceMgr, Plugins
|
||||
from .version import ELECTRUM_VERSION
|
||||
@@ -1382,7 +1382,7 @@ class Commands(Logger):
|
||||
amount: Optional[Decimal] = None,
|
||||
memo: str = "",
|
||||
expiry: int = 3600,
|
||||
min_final_cltv_expiry_delta: int = MIN_FINAL_CLTV_DELTA_FOR_INVOICE * 2,
|
||||
min_final_cltv_expiry_delta: int = MIN_FINAL_CLTV_DELTA_ACCEPTED * 2,
|
||||
wallet: Abstract_Wallet = None
|
||||
) -> dict:
|
||||
"""
|
||||
@@ -1399,23 +1399,23 @@ class Commands(Logger):
|
||||
assert payment_hash not in wallet.lnworker.payment_info, "Payment hash already used!"
|
||||
assert payment_hash not in wallet.lnworker.dont_settle_htlcs, "Payment hash already used!"
|
||||
assert wallet.lnworker.get_preimage(bfh(payment_hash)) is None, "Already got a preimage for this payment hash!"
|
||||
assert MIN_FINAL_CLTV_DELTA_FOR_INVOICE < min_final_cltv_expiry_delta < 576, "Use a sane min_final_cltv_expiry_delta value"
|
||||
assert MIN_FINAL_CLTV_DELTA_ACCEPTED < min_final_cltv_expiry_delta < 576, "Use a sane min_final_cltv_expiry_delta value"
|
||||
amount = amount if amount and satoshis(amount) > 0 else None # make amount either >0 or None
|
||||
inbound_capacity = wallet.lnworker.num_sats_can_receive()
|
||||
assert inbound_capacity > satoshis(amount or 0), \
|
||||
f"Not enough inbound capacity [{inbound_capacity} sat] to receive this payment"
|
||||
|
||||
lnaddr, invoice = wallet.lnworker.get_bolt11_invoice(
|
||||
payment_hash=bfh(payment_hash),
|
||||
amount_msat=satoshis(amount) * 1000 if amount else None,
|
||||
message=memo,
|
||||
expiry=expiry,
|
||||
min_final_cltv_expiry_delta=min_final_cltv_expiry_delta,
|
||||
fallback_address=None
|
||||
)
|
||||
wallet.lnworker.add_payment_info_for_hold_invoice(
|
||||
bfh(payment_hash),
|
||||
satoshis(amount) if amount else None,
|
||||
lightning_amount_sat=satoshis(amount) if amount else None,
|
||||
min_final_cltv_delta=min_final_cltv_expiry_delta,
|
||||
exp_delay=expiry,
|
||||
)
|
||||
info = wallet.lnworker.get_payment_info(bfh(payment_hash))
|
||||
lnaddr, invoice = wallet.lnworker.get_bolt11_invoice(
|
||||
payment_info=info,
|
||||
message=memo,
|
||||
fallback_address=None
|
||||
)
|
||||
wallet.lnworker.dont_settle_htlcs[payment_hash] = None
|
||||
wallet.set_label(payment_hash, memo)
|
||||
|
||||
@@ -2758,7 +2758,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
|
||||
self.closing_warning_callbacks.append(warning_callback)
|
||||
|
||||
def _check_ongoing_force_closures(self) -> Optional[str]:
|
||||
from electrum.lnutil import MIN_FINAL_CLTV_DELTA_FOR_INVOICE
|
||||
from electrum.lnutil import MIN_FINAL_CLTV_DELTA_ACCEPTED
|
||||
if not self.wallet.has_lightning():
|
||||
return None
|
||||
if not self.network:
|
||||
@@ -2767,7 +2767,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
|
||||
if not force_closes:
|
||||
return
|
||||
# fixme: this is inaccurate, we need local_height - cltv_of_htlc
|
||||
cltv_delta = MIN_FINAL_CLTV_DELTA_FOR_INVOICE
|
||||
cltv_delta = MIN_FINAL_CLTV_DELTA_ACCEPTED
|
||||
msg = '\n\n'.join([
|
||||
_("Pending channel force-close"),
|
||||
messages.MSG_FORCE_CLOSE_WARNING.format(cltv_delta),
|
||||
|
||||
+3
-321
@@ -1926,57 +1926,6 @@ class Peer(Logger, EventListener):
|
||||
self.send_message("commitment_signed", channel_id=chan.channel_id, signature=sig_64, num_htlcs=len(htlc_sigs), htlc_signature=b"".join(htlc_sigs))
|
||||
return True
|
||||
|
||||
def create_onion_for_route(
|
||||
self, *,
|
||||
route: 'LNPaymentRoute',
|
||||
amount_msat: int,
|
||||
total_msat: int,
|
||||
payment_hash: bytes,
|
||||
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_abs = local_height + min_final_cltv_delta
|
||||
hops_data, amount_msat, cltv_abs = calc_hops_data_for_payment(
|
||||
route,
|
||||
amount_msat,
|
||||
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_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:
|
||||
self.logger.info(f'adding trampoline onion to final payload')
|
||||
trampoline_payload = hops_data[-1].payload
|
||||
trampoline_payload["trampoline_onion_packet"] = {
|
||||
"version": trampoline_onion.version,
|
||||
"public_key": trampoline_onion.public_key,
|
||||
"hops_data": trampoline_onion.hops_data,
|
||||
"hmac": trampoline_onion.hmac
|
||||
}
|
||||
if t_hops_data := trampoline_onion._debug_hops_data: # None if trampoline-forwarding
|
||||
t_route = trampoline_onion._debug_route
|
||||
assert t_route is not None
|
||||
self.logger.info(f"lnpeer.pay len(t_route)={len(t_route)}")
|
||||
for i in range(len(t_route)):
|
||||
self.logger.info(f" {i}: t_node={t_route[i].end_node.hex()} hop_data={t_hops_data[i]!r}")
|
||||
# create onion packet
|
||||
payment_path_pubkeys = [x.node_id for x in route]
|
||||
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_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,
|
||||
*,
|
||||
@@ -2019,7 +1968,7 @@ class Peer(Logger, EventListener):
|
||||
assert len(route) > 0
|
||||
if not chan.can_send_update_add_htlc():
|
||||
raise PaymentFailure("Channel cannot send update_add_htlc")
|
||||
onion, amount_msat, cltv_abs, session_key = self.create_onion_for_route(
|
||||
onion, amount_msat, cltv_abs, session_key = self.lnworker.create_onion_for_route(
|
||||
route=route,
|
||||
amount_msat=amount_msat,
|
||||
total_msat=total_msat,
|
||||
@@ -2131,273 +2080,6 @@ class Peer(Logger, EventListener):
|
||||
chan.receive_htlc(htlc, onion_packet)
|
||||
util.trigger_callback('htlc_added', chan, htlc, RECEIVED)
|
||||
|
||||
|
||||
async def maybe_forward_htlc(
|
||||
self, *,
|
||||
incoming_chan: Channel,
|
||||
htlc: UpdateAddHtlc,
|
||||
processed_onion: ProcessedOnionPacket,
|
||||
) -> str:
|
||||
|
||||
# Forward HTLC
|
||||
# FIXME: there are critical safety checks MISSING here
|
||||
# - for example; atm we forward first and then persist "forwarding_info",
|
||||
# so if we segfault in-between and restart, we might forward an HTLC twice...
|
||||
# (same for trampoline forwarding)
|
||||
# - we could check for the exposure to dust HTLCs, see:
|
||||
# https://github.com/ACINQ/eclair/pull/1985
|
||||
|
||||
def log_fail_reason(reason: str):
|
||||
self.logger.debug(
|
||||
f"maybe_forward_htlc. will FAIL HTLC: inc_chan={incoming_chan.get_id_for_log()}. "
|
||||
f"{reason}. inc_htlc={str(htlc)}. onion_payload={processed_onion.hop_data.payload}")
|
||||
|
||||
forwarding_enabled = self.network.config.EXPERIMENTAL_LN_FORWARD_PAYMENTS
|
||||
if not forwarding_enabled:
|
||||
log_fail_reason("forwarding is disabled")
|
||||
raise OnionRoutingFailure(code=OnionFailureCode.PERMANENT_CHANNEL_FAILURE, data=b'')
|
||||
chain = self.network.blockchain()
|
||||
if chain.is_tip_stale():
|
||||
raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_NODE_FAILURE, data=b'')
|
||||
try:
|
||||
_next_chan_scid = processed_onion.hop_data.payload["short_channel_id"]["short_channel_id"] # type: bytes
|
||||
next_chan_scid = ShortChannelID(_next_chan_scid)
|
||||
except Exception:
|
||||
raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00')
|
||||
try:
|
||||
next_amount_msat_htlc = processed_onion.hop_data.payload["amt_to_forward"]["amt_to_forward"]
|
||||
except Exception:
|
||||
raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00')
|
||||
try:
|
||||
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)
|
||||
|
||||
if self.lnworker.features.supports(LnFeatures.OPTION_ZEROCONF_OPT):
|
||||
next_peer = self.lnworker.get_peer_by_static_jit_scid_alias(next_chan_scid)
|
||||
else:
|
||||
next_peer = None
|
||||
|
||||
if not next_chan and next_peer and next_peer.accepts_zeroconf():
|
||||
# check if an already existing channel can be used.
|
||||
# todo: split the payment
|
||||
for next_chan in next_peer.channels.values():
|
||||
if next_chan.can_pay(next_amount_msat_htlc):
|
||||
break
|
||||
else:
|
||||
return await self.lnworker.open_channel_just_in_time(
|
||||
next_peer=next_peer,
|
||||
next_amount_msat_htlc=next_amount_msat_htlc,
|
||||
next_cltv_abs=next_cltv_abs,
|
||||
payment_hash=htlc.payment_hash,
|
||||
next_onion=processed_onion.next_packet)
|
||||
|
||||
local_height = chain.height()
|
||||
if next_chan is None:
|
||||
log_fail_reason(f"cannot find next_chan {next_chan_scid}")
|
||||
raise OnionRoutingFailure(code=OnionFailureCode.UNKNOWN_NEXT_PEER, data=b'')
|
||||
outgoing_chan_upd = next_chan.get_outgoing_gossip_channel_update(scid=next_chan_scid)[2:]
|
||||
outgoing_chan_upd_len = len(outgoing_chan_upd).to_bytes(2, byteorder="big")
|
||||
outgoing_chan_upd_message = outgoing_chan_upd_len + outgoing_chan_upd
|
||||
if not next_chan.can_send_update_add_htlc():
|
||||
log_fail_reason(
|
||||
f"next_chan {next_chan.get_id_for_log()} cannot send ctx updates. "
|
||||
f"chan state {next_chan.get_state()!r}, peer state: {next_chan.peer_state!r}")
|
||||
raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_CHANNEL_FAILURE, data=outgoing_chan_upd_message)
|
||||
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_abs - next_cltv_abs < next_chan.forwarding_cltv_delta:
|
||||
log_fail_reason(
|
||||
f"INCORRECT_CLTV_EXPIRY. "
|
||||
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_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_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,
|
||||
fee_base_msat=next_chan.forwarding_fee_base_msat,
|
||||
fee_proportional_millionths=next_chan.forwarding_fee_proportional_millionths)
|
||||
if htlc.amount_msat - next_amount_msat_htlc < forwarding_fees:
|
||||
data = next_amount_msat_htlc.to_bytes(8, byteorder="big") + outgoing_chan_upd_message
|
||||
raise OnionRoutingFailure(code=OnionFailureCode.FEE_INSUFFICIENT, data=data)
|
||||
if self._maybe_refuse_to_forward_htlc_that_corresponds_to_payreq_we_created(htlc.payment_hash):
|
||||
log_fail_reason(f"RHASH corresponds to payreq we created")
|
||||
raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_NODE_FAILURE, data=b'')
|
||||
self.logger.info(
|
||||
f"maybe_forward_htlc. will forward HTLC: inc_chan={incoming_chan.short_channel_id}. inc_htlc={str(htlc)}. "
|
||||
f"next_chan={next_chan.get_id_for_log()}.")
|
||||
|
||||
next_peer = self.lnworker.peers.get(next_chan.node_id)
|
||||
if next_peer is None:
|
||||
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(
|
||||
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)
|
||||
|
||||
htlc_key = serialize_htlc_key(next_chan.get_scid_or_local_alias(), next_htlc.htlc_id)
|
||||
return htlc_key
|
||||
|
||||
@log_exceptions
|
||||
async def maybe_forward_trampoline(
|
||||
self, *,
|
||||
payment_hash: bytes,
|
||||
inc_cltv_abs: int,
|
||||
outer_onion: ProcessedOnionPacket,
|
||||
trampoline_onion: ProcessedOnionPacket,
|
||||
fw_payment_key: str,
|
||||
) -> None:
|
||||
|
||||
forwarding_enabled = self.network.config.EXPERIMENTAL_LN_FORWARD_PAYMENTS
|
||||
forwarding_trampoline_enabled = self.network.config.EXPERIMENTAL_LN_FORWARD_TRAMPOLINE_PAYMENTS
|
||||
if not (forwarding_enabled and forwarding_trampoline_enabled):
|
||||
self.logger.info(f"trampoline forwarding is disabled. failing htlc.")
|
||||
raise OnionRoutingFailure(code=OnionFailureCode.PERMANENT_CHANNEL_FAILURE, data=b'')
|
||||
payload = trampoline_onion.hop_data.payload
|
||||
payment_data = payload.get('payment_data')
|
||||
try:
|
||||
payment_secret = payment_data['payment_secret'] if payment_data else os.urandom(32)
|
||||
outgoing_node_id = payload["outgoing_node_id"]["outgoing_node_id"]
|
||||
amt_to_forward = payload["amt_to_forward"]["amt_to_forward"]
|
||||
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
|
||||
invoice_features = payload["invoice_features"]["invoice_features"]
|
||||
invoice_routing_info = payload["invoice_routing_info"]["invoice_routing_info"]
|
||||
r_tags = decode_routing_info(invoice_routing_info)
|
||||
self.logger.info(f'r_tags {r_tags}')
|
||||
# TODO legacy mpp payment, use total_msat from trampoline onion
|
||||
else:
|
||||
self.logger.info('forward_trampoline: end-to-end')
|
||||
invoice_features = LnFeatures.BASIC_MPP_OPT
|
||||
next_trampoline_onion = trampoline_onion.next_packet
|
||||
r_tags = []
|
||||
except Exception as e:
|
||||
self.logger.exception('')
|
||||
raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00')
|
||||
|
||||
if self._maybe_refuse_to_forward_htlc_that_corresponds_to_payreq_we_created(payment_hash):
|
||||
self.logger.debug(
|
||||
f"maybe_forward_trampoline. will FAIL HTLC(s). "
|
||||
f"RHASH corresponds to payreq we created. {payment_hash.hex()=}")
|
||||
raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_NODE_FAILURE, data=b'')
|
||||
|
||||
# these are the fee/cltv paid by the sender
|
||||
# pay_to_node will raise if they are not sufficient
|
||||
total_msat = outer_onion.hop_data.payload["payment_data"]["total_msat"]
|
||||
budget = PaymentFeeBudget(
|
||||
fee_msat=total_msat - amt_to_forward,
|
||||
cltv=inc_cltv_abs - out_cltv_abs,
|
||||
)
|
||||
self.logger.info(f'trampoline forwarding. budget={budget}')
|
||||
self.logger.info(f'trampoline forwarding. {inc_cltv_abs=}, {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)
|
||||
# - if we skew towards the future, we decrease the cltv_budget for the subsequent nodes in the path,
|
||||
# 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 = out_cltv_abs - local_height_of_onion_creator
|
||||
|
||||
if budget.fee_msat < 1000:
|
||||
raise OnionRoutingFailure(code=OnionFailureCode.TRAMPOLINE_FEE_INSUFFICIENT, data=b'')
|
||||
if budget.cltv < 576:
|
||||
raise OnionRoutingFailure(code=OnionFailureCode.TRAMPOLINE_EXPIRY_TOO_SOON, data=b'')
|
||||
|
||||
# do we have a connection to the node?
|
||||
next_peer = self.lnworker.peers.get(outgoing_node_id)
|
||||
if next_peer and next_peer.accepts_zeroconf():
|
||||
self.logger.info(f'JIT: found next_peer')
|
||||
for next_chan in next_peer.channels.values():
|
||||
if next_chan.can_pay(amt_to_forward):
|
||||
# todo: detect if we can do mpp
|
||||
self.logger.info(f'jit: next_chan can pay')
|
||||
break
|
||||
else:
|
||||
scid_alias = self.lnworker._scid_alias_of_node(next_peer.pubkey)
|
||||
route = [RouteEdge(
|
||||
start_node=next_peer.pubkey,
|
||||
end_node=outgoing_node_id,
|
||||
short_channel_id=scid_alias,
|
||||
fee_base_msat=0,
|
||||
fee_proportional_millionths=0,
|
||||
cltv_delta=144,
|
||||
node_features=0
|
||||
)]
|
||||
next_onion, amount_msat, cltv_abs, session_key = self.create_onion_for_route(
|
||||
route=route,
|
||||
amount_msat=amt_to_forward,
|
||||
total_msat=amt_to_forward,
|
||||
payment_hash=payment_hash,
|
||||
min_final_cltv_delta=cltv_budget_for_rest_of_route,
|
||||
payment_secret=payment_secret,
|
||||
trampoline_onion=next_trampoline_onion,
|
||||
)
|
||||
await self.lnworker.open_channel_just_in_time(
|
||||
next_peer=next_peer,
|
||||
next_amount_msat_htlc=amt_to_forward,
|
||||
next_cltv_abs=cltv_abs,
|
||||
payment_hash=payment_hash,
|
||||
next_onion=next_onion)
|
||||
return
|
||||
|
||||
try:
|
||||
await self.lnworker.pay_to_node(
|
||||
node_pubkey=outgoing_node_id,
|
||||
payment_hash=payment_hash,
|
||||
payment_secret=payment_secret,
|
||||
amount_to_pay=amt_to_forward,
|
||||
min_final_cltv_delta=cltv_budget_for_rest_of_route,
|
||||
r_tags=r_tags,
|
||||
invoice_features=invoice_features,
|
||||
fwd_trampoline_onion=next_trampoline_onion,
|
||||
budget=budget,
|
||||
attempts=100,
|
||||
fw_payment_key=fw_payment_key,
|
||||
)
|
||||
except OnionRoutingFailure as e:
|
||||
raise
|
||||
except FeeBudgetExceeded:
|
||||
raise OnionRoutingFailure(code=OnionFailureCode.TRAMPOLINE_FEE_INSUFFICIENT, data=b'')
|
||||
except PaymentFailure as e:
|
||||
self.logger.debug(
|
||||
f"maybe_forward_trampoline. PaymentFailure for {payment_hash.hex()=}, {payment_secret.hex()=}: {e!r}")
|
||||
raise OnionRoutingFailure(code=OnionFailureCode.UNKNOWN_NEXT_PEER, data=b'')
|
||||
|
||||
def _maybe_refuse_to_forward_htlc_that_corresponds_to_payreq_we_created(self, payment_hash: bytes) -> bool:
|
||||
"""Returns True if the HTLC should be failed.
|
||||
We must not forward HTLCs with a matching payment_hash to a payment request we created.
|
||||
Example attack:
|
||||
- Bob creates payment request with HASH1, for 1 BTC; and gives the payreq to Alice
|
||||
- Alice sends htlc A->B->C, for 1 sat, with HASH1
|
||||
- Bob must not release the preimage of HASH1
|
||||
"""
|
||||
payment_info = self.lnworker.get_payment_info(payment_hash)
|
||||
is_our_payreq = payment_info and payment_info.direction == RECEIVED
|
||||
# note: If we don't have the preimage for a payment request, then it must be a hold invoice.
|
||||
# Hold invoices are created by other parties (e.g. a counterparty initiating a submarine swap),
|
||||
# and it is the other party choosing the payment_hash. If we failed HTLCs with payment_hashes colliding
|
||||
# with hold invoices, then a party that can make us save a hold invoice for an arbitrary hash could
|
||||
# also make us fail arbitrary HTLCs.
|
||||
return bool(is_our_payreq and self.lnworker.get_preimage(payment_hash))
|
||||
|
||||
def check_accepted_htlc(
|
||||
self, *,
|
||||
chan: Channel,
|
||||
@@ -2502,7 +2184,7 @@ class Peer(Logger, EventListener):
|
||||
return None, None
|
||||
# use the htlc key if we are forwarding
|
||||
payment_key = serialize_htlc_key(chan.get_scid_or_local_alias(), htlc.htlc_id)
|
||||
callback = lambda: self.maybe_forward_htlc(
|
||||
callback = lambda: self.lnworker.maybe_forward_htlc(
|
||||
incoming_chan=chan,
|
||||
htlc=htlc,
|
||||
processed_onion=processed_onion)
|
||||
@@ -2572,7 +2254,7 @@ class Peer(Logger, EventListener):
|
||||
already_forwarded=already_forwarded,
|
||||
)
|
||||
else:
|
||||
callback = lambda: self.maybe_forward_trampoline(
|
||||
callback = lambda: self.lnworker.maybe_forward_trampoline(
|
||||
payment_hash=payment_hash,
|
||||
inc_cltv_abs=htlc.cltv_abs, # TODO: use max or enforce same value across mpp parts
|
||||
outer_onion=processed_onion,
|
||||
|
||||
+4
-3
@@ -505,9 +505,10 @@ 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_DELTA_ACCEPTED = 144
|
||||
# set it a tiny bit higher for invoices as blocks could get mined
|
||||
# during forward path of payment
|
||||
MIN_FINAL_CLTV_DELTA_FOR_INVOICE = MIN_FINAL_CLTV_DELTA_ACCEPTED + 3
|
||||
|
||||
# buffer added to min_final_cltv_delta of created bolt11 invoices to make verifying the cltv delta
|
||||
# of incoming payment htlcs reliable even if some blocks have been mined during forwarding
|
||||
MIN_FINAL_CLTV_DELTA_BUFFER_INVOICE = 3
|
||||
|
||||
# the deadline for offered HTLCs:
|
||||
# the deadline after which the channel has to be failed and timed out on-chain
|
||||
|
||||
+384
-31
@@ -32,7 +32,7 @@ from .i18n import _
|
||||
from .json_db import stored_in
|
||||
from .channel_db import UpdateStatus, ChannelDBNotLoaded, get_mychannel_info, get_mychannel_policy
|
||||
|
||||
from . import constants, util
|
||||
from . import constants, util, lnutil
|
||||
from .util import (
|
||||
profiler, OldTaskGroup, ESocksProxy, NetworkRetryManager, JsonRPCClient, NotEnoughFunds, EventListener,
|
||||
event_listener, bfh, InvoiceError, resolve_dns_srv, is_ip_address, log_exceptions, ignore_exceptions,
|
||||
@@ -65,23 +65,27 @@ from .lnchannel import Channel, AbstractChannel, ChannelState, PeerState, HTLCWi
|
||||
from .lnrater import LNRater
|
||||
from .lnutil import (
|
||||
get_compressed_pubkey_from_bech32, serialize_htlc_key, deserialize_htlc_key, PaymentFailure, generate_keypair,
|
||||
LnKeyFamily, LOCAL, REMOTE, MIN_FINAL_CLTV_DELTA_FOR_INVOICE, SENT, RECEIVED, HTLCOwner, UpdateAddHtlc, LnFeatures,
|
||||
LnKeyFamily, LOCAL, REMOTE, MIN_FINAL_CLTV_DELTA_ACCEPTED, SENT, RECEIVED, HTLCOwner, UpdateAddHtlc, LnFeatures,
|
||||
ShortChannelID, HtlcLog, NoPathFound, InvalidGossipMsg, FeeBudgetExceeded, ImportedChannelBackupStorage,
|
||||
OnchainChannelBackupStorage, ln_compare_features, IncompatibleLightningFeatures, PaymentFeeBudget,
|
||||
NBLOCK_CLTV_DELTA_TOO_FAR_INTO_FUTURE, GossipForwardingMessage, MIN_FUNDING_SAT,
|
||||
RecvMPPResolution, ReceivedMPPStatus,
|
||||
MIN_FINAL_CLTV_DELTA_BUFFER_INVOICE, ReceivedMPPStatus, RecvMPPResolution,
|
||||
)
|
||||
from .lnonion import (
|
||||
decode_onion_error, OnionFailureCode, OnionRoutingFailure, OnionPacket,
|
||||
ProcessedOnionPacket, calc_hops_data_for_payment, new_onion_packet,
|
||||
)
|
||||
from .lnonion import decode_onion_error, OnionFailureCode, OnionRoutingFailure, OnionPacket
|
||||
from .lnmsg import decode_msg
|
||||
from .lnrouter import (
|
||||
RouteEdge, LNPaymentRoute, LNPaymentPath, is_route_within_budget, NoChannelPolicy, LNPathInconsistent
|
||||
RouteEdge, LNPaymentRoute, LNPaymentPath, is_route_within_budget, NoChannelPolicy,
|
||||
LNPathInconsistent, fee_for_edge_msat,
|
||||
)
|
||||
from .lnwatcher import LNWatcher
|
||||
from .submarine_swaps import SwapManager
|
||||
from .mpp_split import suggest_splits, SplitConfigRating
|
||||
from .trampoline import (
|
||||
create_trampoline_route_and_onion, is_legacy_relay, trampolines_by_id, hardcoded_trampoline_nodes,
|
||||
is_hardcoded_trampoline
|
||||
is_hardcoded_trampoline, decode_routing_info
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -115,12 +119,22 @@ class PaymentInfo:
|
||||
amount_msat: Optional[int]
|
||||
direction: int
|
||||
status: int
|
||||
min_final_cltv_delta: int
|
||||
expiry_delay: int
|
||||
creation_ts: int = dataclasses.field(default_factory=lambda: int(time.time()))
|
||||
|
||||
@property
|
||||
def expiration_ts(self):
|
||||
return self.creation_ts + self.expiry_delay
|
||||
|
||||
def validate(self):
|
||||
assert isinstance(self.payment_hash, bytes) and len(self.payment_hash) == 32
|
||||
assert self.amount_msat is None or isinstance(self.amount_msat, int)
|
||||
assert isinstance(self.direction, int)
|
||||
assert isinstance(self.status, int)
|
||||
assert isinstance(self.min_final_cltv_delta, int)
|
||||
assert isinstance(self.expiry_delay, int) and self.expiry_delay > 0
|
||||
assert isinstance(self.creation_ts, int)
|
||||
|
||||
def __post_init__(self):
|
||||
self.validate()
|
||||
@@ -860,7 +874,8 @@ class LNWallet(LNWorker):
|
||||
LNWorker.__init__(self, self.node_keypair, features, config=self.config)
|
||||
self.lnwatcher = LNWatcher(self)
|
||||
self.lnrater: LNRater = None
|
||||
self.payment_info = self.db.get_dict('lightning_payments') # RHASH -> amount, direction, is_paid
|
||||
# lightning_payments: RHASH -> amount_msat, direction, status, min_final_cltv_delta, expiry_delay, creation_ts
|
||||
self.payment_info = self.db.get_dict('lightning_payments') # type: dict[str, Tuple[Optional[int], int, int, int, int, int]]
|
||||
self._preimages = self.db.get_dict('lightning_preimages') # RHASH -> preimage
|
||||
self._bolt11_cache = {}
|
||||
# note: this sweep_address is only used as fallback; as it might result in address-reuse
|
||||
@@ -1563,6 +1578,8 @@ class LNWallet(LNWorker):
|
||||
amount_msat=amount_to_pay,
|
||||
direction=SENT,
|
||||
status=PR_UNPAID,
|
||||
min_final_cltv_delta=min_final_cltv_delta,
|
||||
expiry_delay=LN_EXPIRY_NEVER,
|
||||
)
|
||||
self.save_payment_info(info)
|
||||
self.wallet.set_label(key, lnaddr.get_description())
|
||||
@@ -2234,17 +2251,13 @@ class LNWallet(LNWorker):
|
||||
|
||||
def get_bolt11_invoice(
|
||||
self, *,
|
||||
payment_hash: bytes,
|
||||
amount_msat: Optional[int],
|
||||
payment_info: PaymentInfo,
|
||||
message: str,
|
||||
expiry: int, # expiration of invoice (in seconds, relative)
|
||||
fallback_address: Optional[str],
|
||||
channels: Optional[Sequence[Channel]] = None,
|
||||
min_final_cltv_expiry_delta: Optional[int] = None,
|
||||
) -> Tuple[LnAddr, str]:
|
||||
assert isinstance(payment_hash, bytes), f"expected bytes, but got {type(payment_hash)}"
|
||||
|
||||
pair = self._bolt11_cache.get(payment_hash)
|
||||
amount_msat = payment_info.amount_msat
|
||||
pair = self._bolt11_cache.get(payment_info.payment_hash)
|
||||
if pair:
|
||||
lnaddr, invoice = pair
|
||||
assert lnaddr.get_amount_msat() == amount_msat
|
||||
@@ -2261,19 +2274,16 @@ class LNWallet(LNWorker):
|
||||
if needs_jit:
|
||||
# jit only works with single htlcs, mpp will cause LSP to open channels for each htlc
|
||||
invoice_features &= ~ LnFeatures.BASIC_MPP_OPT & ~ LnFeatures.BASIC_MPP_REQ
|
||||
payment_secret = self.get_payment_secret(payment_hash)
|
||||
payment_secret = self.get_payment_secret(payment_info.payment_hash)
|
||||
amount_btc = amount_msat/Decimal(COIN*1000) if amount_msat else None
|
||||
if expiry == 0:
|
||||
expiry = LN_EXPIRY_NEVER
|
||||
if min_final_cltv_expiry_delta is None:
|
||||
min_final_cltv_expiry_delta = MIN_FINAL_CLTV_DELTA_FOR_INVOICE
|
||||
min_final_cltv_delta = payment_info.min_final_cltv_delta + MIN_FINAL_CLTV_DELTA_BUFFER_INVOICE
|
||||
lnaddr = LnAddr(
|
||||
paymenthash=payment_hash,
|
||||
paymenthash=payment_info.payment_hash,
|
||||
amount=amount_btc,
|
||||
tags=[
|
||||
('d', message),
|
||||
('c', min_final_cltv_expiry_delta),
|
||||
('x', expiry),
|
||||
('c', min_final_cltv_delta),
|
||||
('x', payment_info.expiry_delay),
|
||||
('9', invoice_features),
|
||||
('f', fallback_address),
|
||||
] + routing_hints,
|
||||
@@ -2281,7 +2291,7 @@ class LNWallet(LNWorker):
|
||||
payment_secret=payment_secret)
|
||||
invoice = lnencode(lnaddr, self.node_keypair.privkey)
|
||||
pair = lnaddr, invoice
|
||||
self._bolt11_cache[payment_hash] = pair
|
||||
self._bolt11_cache[payment_info.payment_hash] = pair
|
||||
return pair
|
||||
|
||||
def get_payment_secret(self, payment_hash):
|
||||
@@ -2295,14 +2305,23 @@ class LNWallet(LNWorker):
|
||||
payment_secret = self.get_payment_secret(payment_hash)
|
||||
return payment_hash + payment_secret
|
||||
|
||||
def create_payment_info(self, *, amount_msat: Optional[int], write_to_disk=True) -> bytes:
|
||||
def create_payment_info(
|
||||
self, *,
|
||||
amount_msat: Optional[int],
|
||||
min_final_cltv_delta: Optional[int] = None,
|
||||
exp_delay: int = LN_EXPIRY_NEVER,
|
||||
write_to_disk=True
|
||||
) -> bytes:
|
||||
payment_preimage = os.urandom(32)
|
||||
payment_hash = sha256(payment_preimage)
|
||||
min_final_cltv_delta = min_final_cltv_delta or MIN_FINAL_CLTV_DELTA_ACCEPTED
|
||||
info = PaymentInfo(
|
||||
payment_hash=payment_hash,
|
||||
amount_msat=amount_msat,
|
||||
direction=RECEIVED,
|
||||
status=PR_UNPAID,
|
||||
min_final_cltv_delta=min_final_cltv_delta,
|
||||
expiry_delay=exp_delay
|
||||
)
|
||||
self.save_preimage(payment_hash, payment_preimage, write_to_disk=False)
|
||||
self.save_payment_info(info, write_to_disk=False)
|
||||
@@ -2376,22 +2395,34 @@ class LNWallet(LNWorker):
|
||||
key = payment_hash.hex()
|
||||
with self.lock:
|
||||
if key in self.payment_info:
|
||||
amount_msat, direction, status = self.payment_info[key]
|
||||
stored_tuple = self.payment_info[key]
|
||||
amount_msat, direction, status, min_final_cltv_delta, expiry_delay, creation_ts = stored_tuple
|
||||
return PaymentInfo(
|
||||
payment_hash=payment_hash,
|
||||
amount_msat=amount_msat,
|
||||
direction=direction,
|
||||
status=status,
|
||||
min_final_cltv_delta=min_final_cltv_delta,
|
||||
expiry_delay=expiry_delay,
|
||||
creation_ts=creation_ts,
|
||||
)
|
||||
return None
|
||||
|
||||
def add_payment_info_for_hold_invoice(self, payment_hash: bytes, lightning_amount_sat: Optional[int]):
|
||||
def add_payment_info_for_hold_invoice(
|
||||
self,
|
||||
payment_hash: bytes, *,
|
||||
lightning_amount_sat: Optional[int],
|
||||
min_final_cltv_delta: int,
|
||||
exp_delay: int,
|
||||
):
|
||||
amount = lightning_amount_sat * 1000 if lightning_amount_sat else None
|
||||
info = PaymentInfo(
|
||||
payment_hash=payment_hash,
|
||||
amount_msat=amount,
|
||||
direction=RECEIVED,
|
||||
status=PR_UNPAID,
|
||||
min_final_cltv_delta=min_final_cltv_delta,
|
||||
expiry_delay=exp_delay,
|
||||
)
|
||||
self.save_payment_info(info, write_to_disk=False)
|
||||
|
||||
@@ -2407,9 +2438,12 @@ class LNWallet(LNWorker):
|
||||
if old_info := self.get_payment_info(payment_hash=info.payment_hash):
|
||||
if info == old_info:
|
||||
return # already saved
|
||||
if info.direction == SENT:
|
||||
# allow saving of newer PaymentInfo if it is a sending attempt
|
||||
old_info = dataclasses.replace(old_info, creation_ts=info.creation_ts)
|
||||
if info != dataclasses.replace(old_info, status=info.status):
|
||||
# differs more than in status. let's fail
|
||||
raise Exception("payment_hash already in use")
|
||||
raise Exception(f"payment_hash already in use: {info=} != {old_info=}")
|
||||
key = info.payment_hash.hex()
|
||||
self.payment_info[key] = dataclasses.astuple(info)[1:] # drop the payment hash at index 0
|
||||
if write_to_disk:
|
||||
@@ -3027,12 +3061,14 @@ class LNWallet(LNWorker):
|
||||
raise Exception('Rebalance requires two different channels')
|
||||
if self.uses_trampoline() and chan1.node_id == chan2.node_id:
|
||||
raise Exception('Rebalance requires channels from different trampolines')
|
||||
payment_hash = self.create_payment_info(amount_msat=amount_msat)
|
||||
lnaddr, invoice = self.get_bolt11_invoice(
|
||||
payment_hash=payment_hash,
|
||||
payment_hash = self.create_payment_info(
|
||||
amount_msat=amount_msat,
|
||||
exp_delay=3600,
|
||||
)
|
||||
info = self.get_payment_info(payment_hash)
|
||||
lnaddr, invoice = self.get_bolt11_invoice(
|
||||
payment_info=info,
|
||||
message='rebalance',
|
||||
expiry=3600,
|
||||
fallback_address=None,
|
||||
channels=[chan2],
|
||||
)
|
||||
@@ -3349,6 +3385,323 @@ class LNWallet(LNWorker):
|
||||
util.trigger_callback('channels_updated', self.wallet)
|
||||
self.lnwatcher.add_channel(cb)
|
||||
|
||||
async def maybe_forward_htlc(
|
||||
self, *,
|
||||
incoming_chan: Channel,
|
||||
htlc: UpdateAddHtlc,
|
||||
processed_onion: ProcessedOnionPacket,
|
||||
) -> str:
|
||||
|
||||
# Forward HTLC
|
||||
# FIXME: there are critical safety checks MISSING here
|
||||
# - for example; atm we forward first and then persist "forwarding_info",
|
||||
# so if we segfault in-between and restart, we might forward an HTLC twice...
|
||||
# (same for trampoline forwarding)
|
||||
# - we could check for the exposure to dust HTLCs, see:
|
||||
# https://github.com/ACINQ/eclair/pull/1985
|
||||
|
||||
def log_fail_reason(reason: str):
|
||||
self.logger.debug(
|
||||
f"_maybe_forward_htlc. will FAIL HTLC: inc_chan={incoming_chan.get_id_for_log()}. "
|
||||
f"{reason}. inc_htlc={str(htlc)}. onion_payload={processed_onion.hop_data.payload}")
|
||||
|
||||
forwarding_enabled = self.network.config.EXPERIMENTAL_LN_FORWARD_PAYMENTS
|
||||
if not forwarding_enabled:
|
||||
log_fail_reason("forwarding is disabled")
|
||||
raise OnionRoutingFailure(code=OnionFailureCode.PERMANENT_CHANNEL_FAILURE, data=b'')
|
||||
chain = self.network.blockchain()
|
||||
if chain.is_tip_stale():
|
||||
raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_NODE_FAILURE, data=b'')
|
||||
try:
|
||||
_next_chan_scid = processed_onion.hop_data.payload["short_channel_id"]["short_channel_id"] # type: bytes
|
||||
next_chan_scid = ShortChannelID(_next_chan_scid)
|
||||
except Exception:
|
||||
raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00')
|
||||
try:
|
||||
next_amount_msat_htlc = processed_onion.hop_data.payload["amt_to_forward"]["amt_to_forward"]
|
||||
except Exception:
|
||||
raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00')
|
||||
try:
|
||||
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.get_channel_by_short_id(next_chan_scid)
|
||||
|
||||
if self.features.supports(LnFeatures.OPTION_ZEROCONF_OPT):
|
||||
next_peer = self.get_peer_by_static_jit_scid_alias(next_chan_scid)
|
||||
else:
|
||||
next_peer = None
|
||||
|
||||
if not next_chan and next_peer and next_peer.accepts_zeroconf():
|
||||
# check if an already existing channel can be used.
|
||||
# todo: split the payment
|
||||
for next_chan in next_peer.channels.values():
|
||||
if next_chan.can_pay(next_amount_msat_htlc):
|
||||
break
|
||||
else:
|
||||
return await self.open_channel_just_in_time(
|
||||
next_peer=next_peer,
|
||||
next_amount_msat_htlc=next_amount_msat_htlc,
|
||||
next_cltv_abs=next_cltv_abs,
|
||||
payment_hash=htlc.payment_hash,
|
||||
next_onion=processed_onion.next_packet)
|
||||
|
||||
local_height = chain.height()
|
||||
if next_chan is None:
|
||||
log_fail_reason(f"cannot find next_chan {next_chan_scid}")
|
||||
raise OnionRoutingFailure(code=OnionFailureCode.UNKNOWN_NEXT_PEER, data=b'')
|
||||
outgoing_chan_upd = next_chan.get_outgoing_gossip_channel_update(scid=next_chan_scid)[2:]
|
||||
outgoing_chan_upd_len = len(outgoing_chan_upd).to_bytes(2, byteorder="big")
|
||||
outgoing_chan_upd_message = outgoing_chan_upd_len + outgoing_chan_upd
|
||||
if not next_chan.can_send_update_add_htlc():
|
||||
log_fail_reason(
|
||||
f"next_chan {next_chan.get_id_for_log()} cannot send ctx updates. "
|
||||
f"chan state {next_chan.get_state()!r}, peer state: {next_chan.peer_state!r}")
|
||||
raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_CHANNEL_FAILURE, data=outgoing_chan_upd_message)
|
||||
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_abs - next_cltv_abs < next_chan.forwarding_cltv_delta:
|
||||
log_fail_reason(
|
||||
f"INCORRECT_CLTV_EXPIRY. "
|
||||
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_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_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,
|
||||
fee_base_msat=next_chan.forwarding_fee_base_msat,
|
||||
fee_proportional_millionths=next_chan.forwarding_fee_proportional_millionths)
|
||||
if htlc.amount_msat - next_amount_msat_htlc < forwarding_fees:
|
||||
data = next_amount_msat_htlc.to_bytes(8, byteorder="big") + outgoing_chan_upd_message
|
||||
raise OnionRoutingFailure(code=OnionFailureCode.FEE_INSUFFICIENT, data=data)
|
||||
if self._maybe_refuse_to_forward_htlc_that_corresponds_to_payreq_we_created(htlc.payment_hash):
|
||||
log_fail_reason(f"RHASH corresponds to payreq we created")
|
||||
raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_NODE_FAILURE, data=b'')
|
||||
self.logger.info(
|
||||
f"maybe_forward_htlc. will forward HTLC: inc_chan={incoming_chan.short_channel_id}. inc_htlc={str(htlc)}. "
|
||||
f"next_chan={next_chan.get_id_for_log()}.")
|
||||
|
||||
next_peer = self.peers.get(next_chan.node_id)
|
||||
if next_peer is None:
|
||||
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(
|
||||
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)
|
||||
|
||||
htlc_key = serialize_htlc_key(next_chan.get_scid_or_local_alias(), next_htlc.htlc_id)
|
||||
return htlc_key
|
||||
|
||||
@log_exceptions
|
||||
async def maybe_forward_trampoline(
|
||||
self, *,
|
||||
payment_hash: bytes,
|
||||
inc_cltv_abs: int,
|
||||
outer_onion: ProcessedOnionPacket,
|
||||
trampoline_onion: ProcessedOnionPacket,
|
||||
fw_payment_key: str,
|
||||
) -> None:
|
||||
|
||||
forwarding_enabled = self.network.config.EXPERIMENTAL_LN_FORWARD_PAYMENTS
|
||||
forwarding_trampoline_enabled = self.network.config.EXPERIMENTAL_LN_FORWARD_TRAMPOLINE_PAYMENTS
|
||||
if not (forwarding_enabled and forwarding_trampoline_enabled):
|
||||
self.logger.info(f"trampoline forwarding is disabled. failing htlc.")
|
||||
raise OnionRoutingFailure(code=OnionFailureCode.PERMANENT_CHANNEL_FAILURE, data=b'')
|
||||
payload = trampoline_onion.hop_data.payload
|
||||
payment_data = payload.get('payment_data')
|
||||
try:
|
||||
payment_secret = payment_data['payment_secret'] if payment_data else os.urandom(32)
|
||||
outgoing_node_id = payload["outgoing_node_id"]["outgoing_node_id"]
|
||||
amt_to_forward = payload["amt_to_forward"]["amt_to_forward"]
|
||||
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
|
||||
invoice_features = payload["invoice_features"]["invoice_features"]
|
||||
invoice_routing_info = payload["invoice_routing_info"]["invoice_routing_info"]
|
||||
r_tags = decode_routing_info(invoice_routing_info)
|
||||
self.logger.info(f'r_tags {r_tags}')
|
||||
# TODO legacy mpp payment, use total_msat from trampoline onion
|
||||
else:
|
||||
self.logger.info('forward_trampoline: end-to-end')
|
||||
invoice_features = LnFeatures.BASIC_MPP_OPT
|
||||
next_trampoline_onion = trampoline_onion.next_packet
|
||||
r_tags = []
|
||||
except Exception as e:
|
||||
self.logger.exception('')
|
||||
raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00')
|
||||
|
||||
if self._maybe_refuse_to_forward_htlc_that_corresponds_to_payreq_we_created(payment_hash):
|
||||
self.logger.debug(
|
||||
f"maybe_forward_trampoline. will FAIL HTLC(s). "
|
||||
f"RHASH corresponds to payreq we created. {payment_hash.hex()=}")
|
||||
raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_NODE_FAILURE, data=b'')
|
||||
|
||||
# these are the fee/cltv paid by the sender
|
||||
# pay_to_node will raise if they are not sufficient
|
||||
total_msat = outer_onion.hop_data.payload["payment_data"]["total_msat"]
|
||||
budget = PaymentFeeBudget(
|
||||
fee_msat=total_msat - amt_to_forward,
|
||||
cltv=inc_cltv_abs - out_cltv_abs,
|
||||
)
|
||||
self.logger.info(f'trampoline forwarding. budget={budget}')
|
||||
self.logger.info(f'trampoline forwarding. {inc_cltv_abs=}, {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)
|
||||
# - if we skew towards the future, we decrease the cltv_budget for the subsequent nodes in the path,
|
||||
# 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 = out_cltv_abs - local_height_of_onion_creator
|
||||
|
||||
if budget.fee_msat < 1000:
|
||||
raise OnionRoutingFailure(code=OnionFailureCode.TRAMPOLINE_FEE_INSUFFICIENT, data=b'')
|
||||
if budget.cltv < 576:
|
||||
raise OnionRoutingFailure(code=OnionFailureCode.TRAMPOLINE_EXPIRY_TOO_SOON, data=b'')
|
||||
|
||||
# do we have a connection to the node?
|
||||
next_peer = self.peers.get(outgoing_node_id)
|
||||
if next_peer and next_peer.accepts_zeroconf():
|
||||
self.logger.info(f'JIT: found next_peer')
|
||||
for next_chan in next_peer.channels.values():
|
||||
if next_chan.can_pay(amt_to_forward):
|
||||
# todo: detect if we can do mpp
|
||||
self.logger.info(f'jit: next_chan can pay')
|
||||
break
|
||||
else:
|
||||
scid_alias = self._scid_alias_of_node(next_peer.pubkey)
|
||||
route = [RouteEdge(
|
||||
start_node=next_peer.pubkey,
|
||||
end_node=outgoing_node_id,
|
||||
short_channel_id=scid_alias,
|
||||
fee_base_msat=0,
|
||||
fee_proportional_millionths=0,
|
||||
cltv_delta=144,
|
||||
node_features=0
|
||||
)]
|
||||
next_onion, amount_msat, cltv_abs, session_key = self.create_onion_for_route(
|
||||
route=route,
|
||||
amount_msat=amt_to_forward,
|
||||
total_msat=amt_to_forward,
|
||||
payment_hash=payment_hash,
|
||||
min_final_cltv_delta=cltv_budget_for_rest_of_route,
|
||||
payment_secret=payment_secret,
|
||||
trampoline_onion=next_trampoline_onion,
|
||||
)
|
||||
await self.open_channel_just_in_time(
|
||||
next_peer=next_peer,
|
||||
next_amount_msat_htlc=amt_to_forward,
|
||||
next_cltv_abs=cltv_abs,
|
||||
payment_hash=payment_hash,
|
||||
next_onion=next_onion)
|
||||
return
|
||||
|
||||
try:
|
||||
await self.pay_to_node(
|
||||
node_pubkey=outgoing_node_id,
|
||||
payment_hash=payment_hash,
|
||||
payment_secret=payment_secret,
|
||||
amount_to_pay=amt_to_forward,
|
||||
min_final_cltv_delta=cltv_budget_for_rest_of_route,
|
||||
r_tags=r_tags,
|
||||
invoice_features=invoice_features,
|
||||
fwd_trampoline_onion=next_trampoline_onion,
|
||||
budget=budget,
|
||||
attempts=100,
|
||||
fw_payment_key=fw_payment_key,
|
||||
)
|
||||
except OnionRoutingFailure as e:
|
||||
raise
|
||||
except FeeBudgetExceeded:
|
||||
raise OnionRoutingFailure(code=OnionFailureCode.TRAMPOLINE_FEE_INSUFFICIENT, data=b'')
|
||||
except PaymentFailure as e:
|
||||
self.logger.debug(
|
||||
f"maybe_forward_trampoline. PaymentFailure for {payment_hash.hex()=}, {payment_secret.hex()=}: {e!r}")
|
||||
raise OnionRoutingFailure(code=OnionFailureCode.UNKNOWN_NEXT_PEER, data=b'')
|
||||
|
||||
def _maybe_refuse_to_forward_htlc_that_corresponds_to_payreq_we_created(self, payment_hash: bytes) -> bool:
|
||||
"""Returns True if the HTLC should be failed.
|
||||
We must not forward HTLCs with a matching payment_hash to a payment request we created.
|
||||
Example attack:
|
||||
- Bob creates payment request with HASH1, for 1 BTC; and gives the payreq to Alice
|
||||
- Alice sends htlc A->B->C, for 1 sat, with HASH1
|
||||
- Bob must not release the preimage of HASH1
|
||||
"""
|
||||
payment_info = self.get_payment_info(payment_hash)
|
||||
is_our_payreq = payment_info and payment_info.direction == RECEIVED
|
||||
# note: If we don't have the preimage for a payment request, then it must be a hold invoice.
|
||||
# Hold invoices are created by other parties (e.g. a counterparty initiating a submarine swap),
|
||||
# and it is the other party choosing the payment_hash. If we failed HTLCs with payment_hashes colliding
|
||||
# with hold invoices, then a party that can make us save a hold invoice for an arbitrary hash could
|
||||
# also make us fail arbitrary HTLCs.
|
||||
return bool(is_our_payreq and self.get_preimage(payment_hash))
|
||||
|
||||
def create_onion_for_route(
|
||||
self, *,
|
||||
route: 'LNPaymentRoute',
|
||||
amount_msat: int,
|
||||
total_msat: int,
|
||||
payment_hash: bytes,
|
||||
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_abs = local_height + min_final_cltv_delta
|
||||
hops_data, amount_msat, cltv_abs = calc_hops_data_for_payment(
|
||||
route,
|
||||
amount_msat,
|
||||
final_cltv_abs=final_cltv_abs,
|
||||
total_msat=total_msat,
|
||||
payment_secret=payment_secret)
|
||||
num_hops = len(hops_data)
|
||||
self.logger.info(f"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_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:
|
||||
self.logger.info(f'adding trampoline onion to final payload')
|
||||
trampoline_payload = hops_data[-1].payload
|
||||
trampoline_payload["trampoline_onion_packet"] = {
|
||||
"version": trampoline_onion.version,
|
||||
"public_key": trampoline_onion.public_key,
|
||||
"hops_data": trampoline_onion.hops_data,
|
||||
"hmac": trampoline_onion.hmac
|
||||
}
|
||||
if t_hops_data := trampoline_onion._debug_hops_data: # None if trampoline-forwarding
|
||||
t_route = trampoline_onion._debug_route
|
||||
assert t_route is not None
|
||||
self.logger.info(f"lnpeer.pay len(t_route)={len(t_route)}")
|
||||
for i in range(len(t_route)):
|
||||
self.logger.info(f" {i}: t_node={t_route[i].end_node.hex()} hop_data={t_hops_data[i]!r}")
|
||||
# create onion packet
|
||||
payment_path_pubkeys = [x.node_id for x in route]
|
||||
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_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 save_forwarding_failure(
|
||||
self,
|
||||
payment_key: str,
|
||||
|
||||
@@ -480,12 +480,11 @@ class NWCServer(Logger, EventListener):
|
||||
address=None
|
||||
)
|
||||
req: Request = self.wallet.get_request(key)
|
||||
info = self.wallet.lnworker.get_payment_info(req.payment_hash)
|
||||
try:
|
||||
lnaddr, b11 = self.wallet.lnworker.get_bolt11_invoice(
|
||||
payment_hash=req.payment_hash,
|
||||
amount_msat=amount_msat,
|
||||
payment_info=info,
|
||||
message=description,
|
||||
expiry=expiry,
|
||||
fallback_address=None
|
||||
)
|
||||
except Exception:
|
||||
@@ -538,11 +537,10 @@ class NWCServer(Logger, EventListener):
|
||||
b11 = invoice.lightning_invoice
|
||||
elif self.wallet.get_request(invoice.rhash):
|
||||
direction = "incoming"
|
||||
info = self.wallet.lnworker.get_payment_info(invoice.payment_hash)
|
||||
_, b11 = self.wallet.lnworker.get_bolt11_invoice(
|
||||
payment_hash=bytes.fromhex(invoice.rhash),
|
||||
amount_msat=invoice.amount_msat,
|
||||
payment_info=info,
|
||||
message=invoice.message,
|
||||
expiry=invoice.exp,
|
||||
fallback_address=None
|
||||
)
|
||||
|
||||
@@ -749,11 +747,10 @@ class NWCServer(Logger, EventListener):
|
||||
request: Optional[Request] = self.wallet.get_request(key)
|
||||
if not request or not request.is_lightning() or not status == PR_PAID:
|
||||
return
|
||||
info = self.wallet.lnworker.get_payment_info(request.payment_hash)
|
||||
_, b11 = self.wallet.lnworker.get_bolt11_invoice(
|
||||
payment_hash=request.payment_hash,
|
||||
amount_msat=request.get_amount_msat(),
|
||||
payment_info=info,
|
||||
message=request.message,
|
||||
expiry=request.exp,
|
||||
fallback_address=None
|
||||
)
|
||||
|
||||
|
||||
+18
-13
@@ -36,7 +36,8 @@ from .util import (
|
||||
run_sync_function_on_asyncio_thread, trigger_callback, NoDynamicFeeEstimates, UserFacingException,
|
||||
)
|
||||
from . import lnutil
|
||||
from .lnutil import hex_to_bytes, REDEEM_AFTER_DOUBLE_SPENT_DELAY, Keypair
|
||||
from .lnutil import (hex_to_bytes, REDEEM_AFTER_DOUBLE_SPENT_DELAY, Keypair,
|
||||
MIN_FINAL_CLTV_DELTA_ACCEPTED)
|
||||
from .lnaddr import lndecode
|
||||
from .json_db import StoredObject, stored_in
|
||||
from . import constants
|
||||
@@ -66,7 +67,6 @@ MAX_LOCKTIME_DELTA = 100
|
||||
MIN_FINAL_CLTV_DELTA_FOR_CLIENT = 3 * 144 # note: put in invoice, but is not enforced by receiver in lnpeer.py
|
||||
assert MIN_LOCKTIME_DELTA <= LOCKTIME_DELTA_REFUND <= MAX_LOCKTIME_DELTA
|
||||
assert MAX_LOCKTIME_DELTA < lnutil.MIN_FINAL_CLTV_DELTA_ACCEPTED
|
||||
assert MAX_LOCKTIME_DELTA < lnutil.MIN_FINAL_CLTV_DELTA_FOR_INVOICE
|
||||
assert MAX_LOCKTIME_DELTA < MIN_FINAL_CLTV_DELTA_FOR_CLIENT
|
||||
|
||||
|
||||
@@ -645,33 +645,38 @@ class SwapManager(Logger):
|
||||
else:
|
||||
invoice_amount_sat = lightning_amount_sat
|
||||
|
||||
# add payment info to lnworker
|
||||
self.lnworker.add_payment_info_for_hold_invoice(
|
||||
payment_hash,
|
||||
lightning_amount_sat=invoice_amount_sat,
|
||||
min_final_cltv_delta=min_final_cltv_expiry_delta or MIN_FINAL_CLTV_DELTA_ACCEPTED,
|
||||
exp_delay=300,
|
||||
)
|
||||
info = self.lnworker.get_payment_info(payment_hash)
|
||||
lnaddr1, invoice = self.lnworker.get_bolt11_invoice(
|
||||
payment_hash=payment_hash,
|
||||
amount_msat=invoice_amount_sat * 1000,
|
||||
payment_info=info,
|
||||
message='Submarine swap',
|
||||
expiry=300,
|
||||
fallback_address=None,
|
||||
channels=channels,
|
||||
min_final_cltv_expiry_delta=min_final_cltv_expiry_delta,
|
||||
)
|
||||
margin_to_get_refund_tx_mined = MIN_LOCKTIME_DELTA
|
||||
if not (locktime + margin_to_get_refund_tx_mined < self.network.get_local_height() + lnaddr1.get_min_final_cltv_delta()):
|
||||
raise Exception(
|
||||
f"onchain locktime ({locktime}+{margin_to_get_refund_tx_mined}) "
|
||||
f"too close to LN-htlc-expiry ({self.network.get_local_height()+lnaddr1.get_min_final_cltv_delta()})")
|
||||
# add payment info to lnworker
|
||||
self.lnworker.add_payment_info_for_hold_invoice(payment_hash, invoice_amount_sat)
|
||||
|
||||
if prepay:
|
||||
prepay_hash = self.lnworker.create_payment_info(amount_msat=prepay_amount_sat*1000)
|
||||
prepay_hash = self.lnworker.create_payment_info(
|
||||
amount_msat=prepay_amount_sat*1000,
|
||||
min_final_cltv_delta=min_final_cltv_expiry_delta or MIN_FINAL_CLTV_DELTA_ACCEPTED,
|
||||
exp_delay=300,
|
||||
)
|
||||
info = self.lnworker.get_payment_info(prepay_hash)
|
||||
lnaddr2, prepay_invoice = self.lnworker.get_bolt11_invoice(
|
||||
payment_hash=prepay_hash,
|
||||
amount_msat=prepay_amount_sat * 1000,
|
||||
payment_info=info,
|
||||
message='Submarine swap prepayment',
|
||||
expiry=300,
|
||||
fallback_address=None,
|
||||
channels=channels,
|
||||
min_final_cltv_expiry_delta=min_final_cltv_expiry_delta,
|
||||
)
|
||||
self.lnworker.bundle_payments([payment_hash, prepay_hash])
|
||||
self._prepayments[prepay_hash] = payment_hash
|
||||
|
||||
+8
-4
@@ -3011,11 +3011,11 @@ class Abstract_Wallet(ABC, Logger, EventListener):
|
||||
return ''
|
||||
amount_msat = req.get_amount_msat() or None
|
||||
assert (amount_msat is None or amount_msat > 0), amount_msat
|
||||
info = self.lnworker.get_payment_info(payment_hash)
|
||||
assert info.amount_msat == amount_msat, f"{info.amount_msat=} != {amount_msat=}"
|
||||
lnaddr, invoice = self.lnworker.get_bolt11_invoice(
|
||||
payment_hash=payment_hash,
|
||||
amount_msat=amount_msat,
|
||||
payment_info=info,
|
||||
message=req.message,
|
||||
expiry=req.exp,
|
||||
fallback_address=None)
|
||||
return invoice
|
||||
|
||||
@@ -3031,7 +3031,11 @@ class Abstract_Wallet(ABC, Logger, EventListener):
|
||||
timestamp = int(Request._get_cur_time())
|
||||
if address is None:
|
||||
assert self.has_lightning()
|
||||
payment_hash = self.lnworker.create_payment_info(amount_msat=amount_msat, write_to_disk=False)
|
||||
payment_hash = self.lnworker.create_payment_info(
|
||||
amount_msat=amount_msat,
|
||||
exp_delay=exp_delay,
|
||||
write_to_disk=False,
|
||||
)
|
||||
else:
|
||||
payment_hash = None
|
||||
outputs = [PartialTxOutput.from_address_and_value(address, amount_sat)] if address else []
|
||||
|
||||
+14
-1
@@ -73,7 +73,7 @@ class WalletUnfinished(WalletFileException):
|
||||
# seed_version is now used for the version of the wallet file
|
||||
OLD_SEED_VERSION = 4 # electrum versions < 2.0
|
||||
NEW_SEED_VERSION = 11 # electrum versions >= 2.0
|
||||
FINAL_SEED_VERSION = 60 # electrum >= 2.7 will set this to prevent
|
||||
FINAL_SEED_VERSION = 61 # electrum >= 2.7 will set this to prevent
|
||||
# old versions from overwriting new format
|
||||
|
||||
|
||||
@@ -236,6 +236,7 @@ class WalletDBUpgrader(Logger):
|
||||
self._convert_version_58()
|
||||
self._convert_version_59()
|
||||
self._convert_version_60()
|
||||
self._convert_version_61()
|
||||
self.put('seed_version', FINAL_SEED_VERSION) # just to be sure
|
||||
|
||||
def _convert_wallet_type(self):
|
||||
@@ -1157,6 +1158,18 @@ class WalletDBUpgrader(Logger):
|
||||
cb['multisig_funding_privkey'] = None
|
||||
self.data['seed_version'] = 60
|
||||
|
||||
def _convert_version_61(self):
|
||||
if not self._is_upgrade_method_needed(60, 60):
|
||||
return
|
||||
# adding additional fields to PaymentInfo
|
||||
lightning_payments = self.data.get('lightning_payments', {})
|
||||
expiry_never = 100 * 365 * 24 * 60 * 60
|
||||
migration_time = int(time.time())
|
||||
for rhash, (amount_msat, direction, is_paid) in list(lightning_payments.items()):
|
||||
new = (amount_msat, direction, is_paid, 147, expiry_never, migration_time)
|
||||
lightning_payments[rhash] = new
|
||||
self.data['seed_version'] = 61
|
||||
|
||||
def _convert_imported(self):
|
||||
if not self._is_upgrade_method_needed(0, 13):
|
||||
return
|
||||
|
||||
+15
-9
@@ -41,7 +41,7 @@ from electrum.lnworker import PaymentInfo, RECEIVED
|
||||
from electrum.lnonion import OnionFailureCode, OnionRoutingFailure
|
||||
from electrum.lnutil import UpdateAddHtlc
|
||||
from electrum.lnutil import LOCAL, REMOTE
|
||||
from electrum.invoices import PR_PAID, PR_UNPAID, Invoice
|
||||
from electrum.invoices import PR_PAID, PR_UNPAID, Invoice, LN_EXPIRY_NEVER
|
||||
from electrum.interface import GracefulDisconnect
|
||||
from electrum.simple_config import SimpleConfig
|
||||
from electrum.fee_policy import FeeTimeEstimates, FEE_ETA_TARGETS
|
||||
@@ -347,6 +347,10 @@ class MockLNWallet(Logger, EventListener, NetworkRetryManager[LNPeerAddr]):
|
||||
current_target_feerate_per_kw = LNWallet.current_target_feerate_per_kw
|
||||
current_low_feerate_per_kw_srk_channel = LNWallet.current_low_feerate_per_kw_srk_channel
|
||||
maybe_cleanup_mpp = LNWallet.maybe_cleanup_mpp
|
||||
create_onion_for_route = LNWallet.create_onion_for_route
|
||||
maybe_forward_htlc = LNWallet.maybe_forward_htlc
|
||||
maybe_forward_trampoline = LNWallet.maybe_forward_trampoline
|
||||
_maybe_refuse_to_forward_htlc_that_corresponds_to_payreq_we_created = LNWallet._maybe_refuse_to_forward_htlc_that_corresponds_to_payreq_we_created
|
||||
|
||||
|
||||
class MockTransport:
|
||||
@@ -559,15 +563,8 @@ class TestPeer(ElectrumTestCase):
|
||||
payment_preimage = os.urandom(32)
|
||||
if payment_hash is None:
|
||||
payment_hash = sha256(payment_preimage)
|
||||
info = PaymentInfo(
|
||||
payment_hash=payment_hash,
|
||||
amount_msat=amount_msat,
|
||||
direction=RECEIVED,
|
||||
status=PR_UNPAID,
|
||||
)
|
||||
if payment_preimage:
|
||||
w2.save_preimage(payment_hash, payment_preimage)
|
||||
w2.save_payment_info(info)
|
||||
if include_routing_hints:
|
||||
routing_hints = w2.calc_routing_hints_for_invoice(amount_msat)
|
||||
else:
|
||||
@@ -580,7 +577,16 @@ class TestPeer(ElectrumTestCase):
|
||||
else:
|
||||
payment_secret = None
|
||||
if min_final_cltv_delta is None:
|
||||
min_final_cltv_delta = lnutil.MIN_FINAL_CLTV_DELTA_FOR_INVOICE
|
||||
min_final_cltv_delta = lnutil.MIN_FINAL_CLTV_DELTA_ACCEPTED
|
||||
info = PaymentInfo(
|
||||
payment_hash=payment_hash,
|
||||
amount_msat=amount_msat,
|
||||
direction=RECEIVED,
|
||||
status=PR_UNPAID,
|
||||
min_final_cltv_delta=min_final_cltv_delta,
|
||||
expiry_delay=LN_EXPIRY_NEVER,
|
||||
)
|
||||
w2.save_payment_info(info)
|
||||
lnaddr1 = LnAddr(
|
||||
paymenthash=payment_hash,
|
||||
amount=amount_btc,
|
||||
|
||||
Reference in New Issue
Block a user