From 77ba35649993d229f5f01c2ab2cf66278884cd30 Mon Sep 17 00:00:00 2001 From: lollerfirst <43107113+lollerfirst@users.noreply.github.com> Date: Tue, 30 Jul 2024 16:30:01 +0200 Subject: [PATCH] CLN multinut test fix (#602) --- cashu/lightning/clnrest.py | 5 ++++- tests/helpers.py | 14 ++++++++++++++ tests/test_wallet_regtest_mpp.py | 33 ++++++++++++++++++++------------ 3 files changed, 39 insertions(+), 13 deletions(-) diff --git a/cashu/lightning/clnrest.py b/cashu/lightning/clnrest.py index 8d4a4c1..129c4d7 100644 --- a/cashu/lightning/clnrest.py +++ b/cashu/lightning/clnrest.py @@ -29,7 +29,7 @@ from .base import ( class CLNRestWallet(LightningBackend): supported_units = set([Unit.sat, Unit.msat]) unit = Unit.sat - supports_mpp = False # settings.mint_clnrest_enable_mpp + supports_mpp = settings.mint_clnrest_enable_mpp supports_incoming_payment_stream: bool = True def __init__(self, unit: Unit = Unit.sat, **kwargs): @@ -195,11 +195,14 @@ class CLNRestWallet(LightningBackend): } # Handle Multi-Mint payout where we must only pay part of the invoice amount + logger.trace(f"{quote_amount_msat = }, {invoice.amount_msat = }") if quote_amount_msat != invoice.amount_msat: + logger.trace("Detected Multi-Nut payment") if self.supports_mpp: post_data["partial_msat"] = quote_amount_msat else: error_message = "mint does not support MPP" + logger.error(error_message) return PaymentResponse( ok=False, checking_id=None, diff --git a/tests/helpers.py b/tests/helpers.py index 185fa1b..5f4efe4 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -96,6 +96,16 @@ docker_lightning_unconnected_cli = [ "--rpcserver=lnd-2", ] +def docker_clightning_cli(index): + return [ + "docker", + "exec", + f"cashu-clightning-{index}-1", + "lightning-cli", + "--network", + "regtest", + "--keywords", + ] def run_cmd(cmd: list) -> str: timeout = 20 @@ -161,6 +171,10 @@ def pay_real_invoice(invoice: str) -> str: cmd.extend(["payinvoice", "--force", invoice]) return run_cmd(cmd) +def partial_pay_real_invoice(invoice: str, amount: int, node: int) -> str: + cmd = docker_clightning_cli(node) + cmd.extend(["pay", f"bolt11={invoice}", f"partial_msat={amount*1000}"]) + return run_cmd(cmd) def mine_blocks(blocks: int = 1) -> str: cmd = docker_bitcoin_cli.copy() diff --git a/tests/test_wallet_regtest_mpp.py b/tests/test_wallet_regtest_mpp.py index 4ece5eb..a6040d6 100644 --- a/tests/test_wallet_regtest_mpp.py +++ b/tests/test_wallet_regtest_mpp.py @@ -1,16 +1,19 @@ import asyncio +import threading from typing import List import pytest import pytest_asyncio from cashu.core.base import Method, Proof +from cashu.lightning.clnrest import CLNRestWallet from cashu.mint.ledger import Ledger from cashu.wallet.wallet import Wallet from tests.conftest import SERVER_ENDPOINT from tests.helpers import ( get_real_invoice, is_fake, + partial_pay_real_invoice, pay_if_regtest, ) @@ -42,32 +45,33 @@ async def test_regtest_pay_mpp(wallet: Wallet, ledger: Ledger): proofs1 = await wallet.mint(128, id=topup_invoice.id) assert wallet.balance == 128 - topup_invoice = await wallet.request_mint(128) - await pay_if_regtest(topup_invoice.bolt11) - proofs2 = await wallet.mint(128, id=topup_invoice.id) - assert wallet.balance == 256 - # this is the invoice we want to pay in two parts invoice_dict = get_real_invoice(64) invoice_payment_request = invoice_dict["payment_request"] - async def pay_mpp(amount: int, proofs: List[Proof], delay: float = 0.0): - await asyncio.sleep(delay) + async def _mint_pay_mpp(invoice: str, amount: int, proofs: List[Proof]): # wallet pays 32 sat of the invoice - quote = await wallet.melt_quote(invoice_payment_request, amount=amount) + quote = await wallet.melt_quote(invoice, amount=amount) assert quote.amount == amount await wallet.melt( proofs, - invoice_payment_request, + invoice, fee_reserve_sat=quote.fee_reserve, quote_id=quote.quote, ) + def mint_pay_mpp(invoice: str, amount: int, proofs: List[Proof]): + asyncio.run(_mint_pay_mpp(invoice, amount, proofs)) # call pay_mpp twice in parallel to pay the full invoice - # we delay the second payment so that the wallet doesn't derive the same blindedmessages twice due to a race condition - await asyncio.gather(pay_mpp(32, proofs1), pay_mpp(32, proofs2, delay=0.5)) + t1 = threading.Thread(target=mint_pay_mpp, args=(invoice_payment_request, 32, proofs1)) + t2 = threading.Thread(target=partial_pay_real_invoice, args=(invoice_payment_request, 32, 1)) - assert wallet.balance <= 256 - 64 + t1.start() + t2.start() + t1.join() + t2.join() + + assert wallet.balance <= 256 - 32 @pytest.mark.asyncio @@ -76,6 +80,11 @@ async def test_regtest_pay_mpp_incomplete_payment(wallet: Wallet, ledger: Ledger # make sure that mpp is supported by the bolt11-sat backend if not ledger.backends[Method["bolt11"]][wallet.unit].supports_mpp: pytest.skip("backend does not support mpp") + + # This test cannot be done with CLN because we only have one mint + # and CLN hates multiple partial payment requests + if isinstance(ledger.backends[Method["bolt11"]][wallet.unit], CLNRestWallet): + pytest.skip("CLN cannot perform this test") # make sure wallet knows the backend supports mpp assert wallet.mint_info.supports_mpp("bolt11", wallet.unit)