1
0

lnworker: enforce creation of PaymentInfo for b11

Enforce that the information used to create a bolt11 invoice using
`get_bolt11_invoice()` is similar to the related instance of PaymentInfo
by requiring a PaymentInfo as argument for `get_bolt11_invoice()`.
This way the invoice cannot differ from the created PaymentInfo.
This allows to use the information in PaymentInfo for validation of
incoming htlcs more reliably.

To cover all required information for the creation of a b11 invoice the
PaymentInfo class has to be extended with a expiry and
min_final_cltv_expiry. This requires a db upgrade.
This commit is contained in:
f321x
2025-09-26 16:11:20 +02:00
parent d62b627a0b
commit 286fc4b86e
9 changed files with 134 additions and 80 deletions

View File

@@ -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)

View File

@@ -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),

View File

@@ -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

View File

@@ -65,11 +65,11 @@ 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,
@@ -119,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()
@@ -864,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
@@ -1567,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())
@@ -2238,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
@@ -2265,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,
@@ -2285,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):
@@ -2299,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)
@@ -2380,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)
@@ -2411,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:
@@ -3031,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],
)

View File

@@ -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
)

View File

@@ -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

View File

@@ -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 []

View File

@@ -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

View File

@@ -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
@@ -563,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:
@@ -584,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,