lnworker: add invoice features to PaymentInfo class
Adds the invoice features to the `PaymentInfo` class so we can check if the sender respects our requested features (e.g. if they tried to send mpp if we requested no mpp).
This commit is contained in:
@@ -134,6 +134,7 @@ class PaymentInfo:
|
|||||||
min_final_cltv_delta: int
|
min_final_cltv_delta: int
|
||||||
expiry_delay: int
|
expiry_delay: int
|
||||||
creation_ts: int = dataclasses.field(default_factory=lambda: int(time.time()))
|
creation_ts: int = dataclasses.field(default_factory=lambda: int(time.time()))
|
||||||
|
invoice_features: LnFeatures
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def expiration_ts(self):
|
def expiration_ts(self):
|
||||||
@@ -147,6 +148,7 @@ class PaymentInfo:
|
|||||||
assert isinstance(self.min_final_cltv_delta, int)
|
assert isinstance(self.min_final_cltv_delta, int)
|
||||||
assert isinstance(self.expiry_delay, int) and self.expiry_delay > 0
|
assert isinstance(self.expiry_delay, int) and self.expiry_delay > 0
|
||||||
assert isinstance(self.creation_ts, int)
|
assert isinstance(self.creation_ts, int)
|
||||||
|
assert isinstance(self.invoice_features, LnFeatures)
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
self.validate()
|
self.validate()
|
||||||
@@ -903,8 +905,8 @@ class LNWallet(LNWorker):
|
|||||||
LNWorker.__init__(self, self.node_keypair, features, config=self.config)
|
LNWorker.__init__(self, self.node_keypair, features, config=self.config)
|
||||||
self.lnwatcher = LNWatcher(self)
|
self.lnwatcher = LNWatcher(self)
|
||||||
self.lnrater: LNRater = None
|
self.lnrater: LNRater = None
|
||||||
# lightning_payments: "RHASH:direction" -> amount_msat, status, min_final_cltv_delta, expiry_delay, creation_ts
|
# "RHASH:direction" -> amount_msat, status, min_final_cltv_delta, expiry_delay, creation_ts, invoice_features
|
||||||
self.payment_info = self.db.get_dict('lightning_payments') # type: dict[str, Tuple[Optional[int], int, int, int, int]]
|
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._preimages = self.db.get_dict('lightning_preimages') # RHASH -> preimage
|
||||||
self._bolt11_cache = {}
|
self._bolt11_cache = {}
|
||||||
# note: this sweep_address is only used as fallback; as it might result in address-reuse
|
# note: this sweep_address is only used as fallback; as it might result in address-reuse
|
||||||
@@ -1628,6 +1630,7 @@ class LNWallet(LNWorker):
|
|||||||
status=PR_UNPAID,
|
status=PR_UNPAID,
|
||||||
min_final_cltv_delta=min_final_cltv_delta,
|
min_final_cltv_delta=min_final_cltv_delta,
|
||||||
expiry_delay=LN_EXPIRY_NEVER,
|
expiry_delay=LN_EXPIRY_NEVER,
|
||||||
|
invoice_features=invoice_features,
|
||||||
)
|
)
|
||||||
self.save_payment_info(info)
|
self.save_payment_info(info)
|
||||||
self.wallet.set_label(key, lnaddr.get_description())
|
self.wallet.set_label(key, lnaddr.get_description())
|
||||||
@@ -2332,6 +2335,16 @@ class LNWallet(LNWorker):
|
|||||||
route[-1].node_features |= invoice_features
|
route[-1].node_features |= invoice_features
|
||||||
return route
|
return route
|
||||||
|
|
||||||
|
def _get_invoice_features(self, amount_msat: Optional[int]) -> LnFeatures:
|
||||||
|
invoice_features = self.features.for_invoice()
|
||||||
|
if not self.uses_trampoline():
|
||||||
|
invoice_features &= ~ LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT_ELECTRUM
|
||||||
|
needs_jit: bool = self.receive_requires_jit_channel(amount_msat)
|
||||||
|
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
|
||||||
|
return invoice_features
|
||||||
|
|
||||||
def clear_invoices_cache(self):
|
def clear_invoices_cache(self):
|
||||||
self._bolt11_cache.clear()
|
self._bolt11_cache.clear()
|
||||||
|
|
||||||
@@ -2351,15 +2364,8 @@ class LNWallet(LNWorker):
|
|||||||
|
|
||||||
assert amount_msat is None or amount_msat > 0
|
assert amount_msat is None or amount_msat > 0
|
||||||
timestamp = int(time.time())
|
timestamp = int(time.time())
|
||||||
needs_jit: bool = self.receive_requires_jit_channel(amount_msat)
|
routing_hints = self.calc_routing_hints_for_invoice(amount_msat, channels=channels)
|
||||||
routing_hints = self.calc_routing_hints_for_invoice(amount_msat, channels=channels, needs_jit=needs_jit)
|
self.logger.info(f"creating bolt11 invoice with routing_hints: {routing_hints}, sat: {(amount_msat or 0) // 1000}")
|
||||||
self.logger.info(f"creating bolt11 invoice with routing_hints: {routing_hints}, jit: {needs_jit}, sat: {(amount_msat or 0) // 1000}")
|
|
||||||
invoice_features = self.features.for_invoice()
|
|
||||||
if not self.uses_trampoline():
|
|
||||||
invoice_features &= ~ LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT_ELECTRUM
|
|
||||||
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_info.payment_hash)
|
payment_secret = self.get_payment_secret(payment_info.payment_hash)
|
||||||
amount_btc = amount_msat/Decimal(COIN*1000) if amount_msat else None
|
amount_btc = amount_msat/Decimal(COIN*1000) if amount_msat else None
|
||||||
min_final_cltv_delta = payment_info.min_final_cltv_delta + MIN_FINAL_CLTV_DELTA_BUFFER_INVOICE
|
min_final_cltv_delta = payment_info.min_final_cltv_delta + MIN_FINAL_CLTV_DELTA_BUFFER_INVOICE
|
||||||
@@ -2370,7 +2376,7 @@ class LNWallet(LNWorker):
|
|||||||
('d', message),
|
('d', message),
|
||||||
('c', min_final_cltv_delta),
|
('c', min_final_cltv_delta),
|
||||||
('x', payment_info.expiry_delay),
|
('x', payment_info.expiry_delay),
|
||||||
('9', invoice_features),
|
('9', payment_info.invoice_features),
|
||||||
('f', fallback_address),
|
('f', fallback_address),
|
||||||
] + routing_hints,
|
] + routing_hints,
|
||||||
date=timestamp,
|
date=timestamp,
|
||||||
@@ -2401,13 +2407,15 @@ class LNWallet(LNWorker):
|
|||||||
payment_preimage = os.urandom(32)
|
payment_preimage = os.urandom(32)
|
||||||
payment_hash = sha256(payment_preimage)
|
payment_hash = sha256(payment_preimage)
|
||||||
min_final_cltv_delta = min_final_cltv_delta or MIN_FINAL_CLTV_DELTA_ACCEPTED
|
min_final_cltv_delta = min_final_cltv_delta or MIN_FINAL_CLTV_DELTA_ACCEPTED
|
||||||
|
invoice_features = self._get_invoice_features(amount_msat)
|
||||||
info = PaymentInfo(
|
info = PaymentInfo(
|
||||||
payment_hash=payment_hash,
|
payment_hash=payment_hash,
|
||||||
amount_msat=amount_msat,
|
amount_msat=amount_msat,
|
||||||
direction=RECEIVED,
|
direction=RECEIVED,
|
||||||
status=PR_UNPAID,
|
status=PR_UNPAID,
|
||||||
min_final_cltv_delta=min_final_cltv_delta,
|
min_final_cltv_delta=min_final_cltv_delta,
|
||||||
expiry_delay=exp_delay
|
expiry_delay=exp_delay,
|
||||||
|
invoice_features=invoice_features,
|
||||||
)
|
)
|
||||||
self.save_preimage(payment_hash, payment_preimage, write_to_disk=False)
|
self.save_preimage(payment_hash, payment_preimage, write_to_disk=False)
|
||||||
self.save_payment_info(info, write_to_disk=False)
|
self.save_payment_info(info, write_to_disk=False)
|
||||||
@@ -2514,7 +2522,7 @@ class LNWallet(LNWorker):
|
|||||||
with self.lock:
|
with self.lock:
|
||||||
if key in self.payment_info:
|
if key in self.payment_info:
|
||||||
stored_tuple = self.payment_info[key]
|
stored_tuple = self.payment_info[key]
|
||||||
amount_msat, status, min_final_cltv_delta, expiry_delay, creation_ts = stored_tuple
|
amount_msat, status, min_final_cltv_delta, expiry_delay, creation_ts, invoice_features = stored_tuple
|
||||||
return PaymentInfo(
|
return PaymentInfo(
|
||||||
payment_hash=payment_hash,
|
payment_hash=payment_hash,
|
||||||
amount_msat=amount_msat,
|
amount_msat=amount_msat,
|
||||||
@@ -2523,6 +2531,7 @@ class LNWallet(LNWorker):
|
|||||||
min_final_cltv_delta=min_final_cltv_delta,
|
min_final_cltv_delta=min_final_cltv_delta,
|
||||||
expiry_delay=expiry_delay,
|
expiry_delay=expiry_delay,
|
||||||
creation_ts=creation_ts,
|
creation_ts=creation_ts,
|
||||||
|
invoice_features=LnFeatures(invoice_features),
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -2533,14 +2542,15 @@ class LNWallet(LNWorker):
|
|||||||
min_final_cltv_delta: int,
|
min_final_cltv_delta: int,
|
||||||
exp_delay: int,
|
exp_delay: int,
|
||||||
):
|
):
|
||||||
amount = lightning_amount_sat * 1000 if lightning_amount_sat else None
|
amount_msat = lightning_amount_sat * 1000 if lightning_amount_sat else None
|
||||||
info = PaymentInfo(
|
info = PaymentInfo(
|
||||||
payment_hash=payment_hash,
|
payment_hash=payment_hash,
|
||||||
amount_msat=amount,
|
amount_msat=amount_msat,
|
||||||
direction=RECEIVED,
|
direction=RECEIVED,
|
||||||
status=PR_UNPAID,
|
status=PR_UNPAID,
|
||||||
min_final_cltv_delta=min_final_cltv_delta,
|
min_final_cltv_delta=min_final_cltv_delta,
|
||||||
expiry_delay=exp_delay,
|
expiry_delay=exp_delay,
|
||||||
|
invoice_features=self._get_invoice_features(amount_msat),
|
||||||
)
|
)
|
||||||
self.save_payment_info(info, write_to_disk=False)
|
self.save_payment_info(info, write_to_disk=False)
|
||||||
|
|
||||||
@@ -2574,7 +2584,7 @@ class LNWallet(LNWorker):
|
|||||||
if info != dataclasses.replace(old_info, status=info.status):
|
if info != dataclasses.replace(old_info, status=info.status):
|
||||||
# differs more than in status. let's fail
|
# differs more than in status. let's fail
|
||||||
raise Exception(f"payment_hash already in use: {info=} != {old_info=}")
|
raise Exception(f"payment_hash already in use: {info=} != {old_info=}")
|
||||||
v = info.amount_msat, info.status, info.min_final_cltv_delta, info.expiry_delay, info.creation_ts
|
v = info.amount_msat, info.status, info.min_final_cltv_delta, info.expiry_delay, info.creation_ts, int(info.invoice_features)
|
||||||
self.payment_info[info.db_key] = v
|
self.payment_info[info.db_key] = v
|
||||||
if write_to_disk:
|
if write_to_disk:
|
||||||
self.wallet.save_db()
|
self.wallet.save_db()
|
||||||
@@ -2907,10 +2917,11 @@ class LNWallet(LNWorker):
|
|||||||
else:
|
else:
|
||||||
self.logger.info(f'htlc_failed: waiting for other htlcs to fail (phash={payment_hash.hex()})')
|
self.logger.info(f'htlc_failed: waiting for other htlcs to fail (phash={payment_hash.hex()})')
|
||||||
|
|
||||||
def calc_routing_hints_for_invoice(self, amount_msat: Optional[int], channels=None, needs_jit=False):
|
def calc_routing_hints_for_invoice(self, amount_msat: Optional[int], channels=None):
|
||||||
"""calculate routing hints (BOLT-11 'r' field)"""
|
"""calculate routing hints (BOLT-11 'r' field)"""
|
||||||
routing_hints = []
|
routing_hints = []
|
||||||
if needs_jit:
|
if self.receive_requires_jit_channel(amount_msat):
|
||||||
|
self.logger.debug(f"will request just-in-time channel")
|
||||||
node_id, rest = extract_nodeid(self.config.ZEROCONF_TRUSTED_NODE)
|
node_id, rest = extract_nodeid(self.config.ZEROCONF_TRUSTED_NODE)
|
||||||
alias_or_scid = self.get_static_jit_scid_alias()
|
alias_or_scid = self.get_static_jit_scid_alias()
|
||||||
routing_hints.append(('r', [(node_id, alias_or_scid, 0, 0, 144)]))
|
routing_hints.append(('r', [(node_id, alias_or_scid, 0, 0, 144)]))
|
||||||
@@ -3093,7 +3104,7 @@ class LNWallet(LNWorker):
|
|||||||
# check if zeroconf is accepted and client has trusted zeroconf node configured
|
# check if zeroconf is accepted and client has trusted zeroconf node configured
|
||||||
return False
|
return False
|
||||||
try:
|
try:
|
||||||
node_id = extract_nodeid(self.wallet.config.ZEROCONF_TRUSTED_NODE)[0]
|
node_id = extract_nodeid(self.config.ZEROCONF_TRUSTED_NODE)[0]
|
||||||
except ConnStringFormatError:
|
except ConnStringFormatError:
|
||||||
# invalid connection string
|
# invalid connection string
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ class WalletUnfinished(WalletFileException):
|
|||||||
# seed_version is now used for the version of the wallet file
|
# seed_version is now used for the version of the wallet file
|
||||||
OLD_SEED_VERSION = 4 # electrum versions < 2.0
|
OLD_SEED_VERSION = 4 # electrum versions < 2.0
|
||||||
NEW_SEED_VERSION = 11 # electrum versions >= 2.0
|
NEW_SEED_VERSION = 11 # electrum versions >= 2.0
|
||||||
FINAL_SEED_VERSION = 65 # electrum >= 2.7 will set this to prevent
|
FINAL_SEED_VERSION = 66 # electrum >= 2.7 will set this to prevent
|
||||||
# old versions from overwriting new format
|
# old versions from overwriting new format
|
||||||
|
|
||||||
|
|
||||||
@@ -237,6 +237,7 @@ class WalletDBUpgrader(Logger):
|
|||||||
self._convert_version_63()
|
self._convert_version_63()
|
||||||
self._convert_version_64()
|
self._convert_version_64()
|
||||||
self._convert_version_65()
|
self._convert_version_65()
|
||||||
|
self._convert_version_66()
|
||||||
self.put('seed_version', FINAL_SEED_VERSION) # just to be sure
|
self.put('seed_version', FINAL_SEED_VERSION) # just to be sure
|
||||||
|
|
||||||
def _convert_wallet_type(self):
|
def _convert_wallet_type(self):
|
||||||
@@ -1315,6 +1316,22 @@ class WalletDBUpgrader(Logger):
|
|||||||
self.data['received_mpp_htlcs'] = new_mpp_sets
|
self.data['received_mpp_htlcs'] = new_mpp_sets
|
||||||
self.data['seed_version'] = 65
|
self.data['seed_version'] = 65
|
||||||
|
|
||||||
|
def _convert_version_66(self):
|
||||||
|
"""Add invoice features to PaymentInfo"""
|
||||||
|
if not self._is_upgrade_method_needed(65, 65):
|
||||||
|
return
|
||||||
|
|
||||||
|
new_payment_infos = {}
|
||||||
|
old_payment_infos = self.data.get('lightning_payments', {})
|
||||||
|
for key, old_v in old_payment_infos.items():
|
||||||
|
amount_msat, status, min_final_cltv_expiry, expiry, creation_ts = old_v
|
||||||
|
invoice_features = 147712 # <VAR_ONION_REQ|PAYMENT_SECRET_REQ|BASIC_MPP_OPT: 0x24100>
|
||||||
|
new_v = (amount_msat, status, min_final_cltv_expiry, expiry, creation_ts, invoice_features)
|
||||||
|
new_payment_infos[key] = new_v
|
||||||
|
|
||||||
|
self.data['lightning_payments'] = new_payment_infos
|
||||||
|
self.data['seed_version'] = 66
|
||||||
|
|
||||||
def _convert_imported(self):
|
def _convert_imported(self):
|
||||||
if not self._is_upgrade_method_needed(0, 13):
|
if not self._is_upgrade_method_needed(0, 13):
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -359,6 +359,9 @@ class MockLNWallet(Logger, EventListener, NetworkRetryManager[LNPeerAddr]):
|
|||||||
is_payment_bundle_complete = LNWallet.is_payment_bundle_complete
|
is_payment_bundle_complete = LNWallet.is_payment_bundle_complete
|
||||||
delete_payment_bundle = LNWallet.delete_payment_bundle
|
delete_payment_bundle = LNWallet.delete_payment_bundle
|
||||||
_process_htlc_log = LNWallet._process_htlc_log
|
_process_htlc_log = LNWallet._process_htlc_log
|
||||||
|
_get_invoice_features = LNWallet._get_invoice_features
|
||||||
|
receive_requires_jit_channel = LNWallet.receive_requires_jit_channel
|
||||||
|
can_get_zeroconf_channel = LNWallet.can_get_zeroconf_channel
|
||||||
|
|
||||||
|
|
||||||
class MockTransport:
|
class MockTransport:
|
||||||
@@ -594,6 +597,7 @@ class TestPeer(ElectrumTestCase):
|
|||||||
status=PR_UNPAID,
|
status=PR_UNPAID,
|
||||||
min_final_cltv_delta=min_final_cltv_delta,
|
min_final_cltv_delta=min_final_cltv_delta,
|
||||||
expiry_delay=expiry or LN_EXPIRY_NEVER,
|
expiry_delay=expiry or LN_EXPIRY_NEVER,
|
||||||
|
invoice_features=invoice_features,
|
||||||
)
|
)
|
||||||
w2.save_payment_info(info)
|
w2.save_payment_info(info)
|
||||||
lnaddr1 = LnAddr(
|
lnaddr1 = LnAddr(
|
||||||
|
|||||||
Reference in New Issue
Block a user