Skip to content

add more batch mint tests#1015

Open
callebtc wants to merge 1 commit into
mainfrom
batch-minting-tests
Open

add more batch mint tests#1015
callebtc wants to merge 1 commit into
mainfrom
batch-minting-tests

Conversation

@callebtc

Copy link
Copy Markdown
Collaborator

No description provided.

@codecov

codecov Bot commented May 23, 2026

Copy link
Copy Markdown

❌ 4 Tests Failed:

Tests completed Failed Passed Skipped
684 4 680 88
View the top 3 failed test(s) by shortest run time
tests.mint.test_mint_batch::test_ledger_mint_batch_invalid_signature
Stack Traces | 1.38s run time
ledger = <cashu.mint.ledger.Ledger object at 0x7f2b129f5750>
wallet = <cashu.wallet.wallet.Wallet object at 0x7f2b1285c490>

    @pytest.mark.asyncio
    async def test_ledger_mint_batch_invalid_signature(ledger: Ledger, wallet: Wallet):
        """Wrong signature for locked quote should fail."""
        import os
    
        from cashu.core.crypto.secp import PrivateKey
    
        await wallet.load_mint()
        mint_quote1 = await wallet.request_mint(64)
        await pay_if_regtest(mint_quote1.request)
    
        secrets, rs, derivation_paths = await wallet.generate_secrets_from_to(10000, 10001)
>       outputs, rs = wallet._construct_outputs([64], secrets, rs)

tests/mint/test_mint_batch.py:307: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <cashu.wallet.wallet.Wallet object at 0x7f2b1285c490>, amounts = [64]
secrets = ['22760a28925dc49290f88319c73f6b39625797d86addf8810c3a68ee9087e91d', '581096e59665fa2697d6e3d19c424f20178c09af1dafcb9c0163c77aa8efa534']
rs = [<coincurve.keys.PrivateKey object at 0x7f2b12885300>, <coincurve.keys.PrivateKey object at 0x7f2b12885540>]
keyset_id = None

    def _construct_outputs(
        self,
        amounts: List[int],
        secrets: List[str],
        rs: List[PrivateKey] = [],
        keyset_id: Optional[str] = None,
    ) -> Tuple[List[BlindedMessage], List[PrivateKey]]:
        """Takes a list of amounts and secrets and returns outputs.
        Outputs are blinded messages `outputs` and blinding factors `rs`
    
        Args:
            amounts (List[int]): list of amounts
            secrets (List[str]): list of secrets
            rs (List[PrivateKey], optional): list of blinding factors. If not given, `rs` are generated in step1_alice. Defaults to [].
    
        Returns:
            List[BlindedMessage]: list of blinded messages that can be sent to the mint
            List[PrivateKey]: list of blinding factors that can be used to construct proofs after receiving blind signatures from the mint
    
        Raises:
            AssertionError: if len(amounts) != len(secrets)
        """
>       assert len(amounts) == len(
            secrets
        ), f"len(amounts)={len(amounts)} not equal to len(secrets)={len(secrets)}"
E       AssertionError: len(amounts)=1 not equal to len(secrets)=2

cashu/wallet/wallet.py:1063: AssertionError
tests.mint.test_mint_batch::test_ledger_mint_batch_mixed_locked_unlocked
Stack Traces | 2.14s run time
ledger = <cashu.mint.ledger.Ledger object at 0x7f2b1285dd20>
wallet = <cashu.wallet.wallet.Wallet object at 0x7f2b1270b2e0>

    @pytest.mark.asyncio
    async def test_ledger_mint_batch_mixed_locked_unlocked(ledger: Ledger, wallet: Wallet):
        """Batch with one locked and one unlocked quote should succeed."""
        await wallet.load_mint()
        mint_quote1 = await wallet.request_mint(64)
        mint_quote2 = await wallet.request_mint(32)
    
        await pay_if_regtest(mint_quote1.request)
        await pay_if_regtest(mint_quote2.request)
    
        secrets, rs, derivation_paths = await wallet.generate_secrets_from_to(10000, 10001)
        outputs, rs = wallet._construct_outputs([64, 32], secrets, rs)
    
        assert mint_quote1.privkey
    
        sig1 = nut20.sign_mint_quote(mint_quote1.quote, outputs, mint_quote1.privkey)
    
>       promises = await ledger.mint_batch(
            PostMintBatchRequest(
                quotes=[mint_quote1.quote, mint_quote2.quote],
                quote_amounts=[64, 32],
                outputs=outputs,
                signatures=[sig1, None],
            )
        )

tests/mint/test_mint_batch.py:347: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <cashu.mint.ledger.Ledger object at 0x7f2b1285dd20>
payload = PostMintBatchRequest(quotes=['_q69_oCZ6is-dDREirbvx34oW9qlS_WP2q2Sn-m7', 'Pd3l-oRUGnosFYQ3LjoRiFl8Wyqa_hpQ77fDHbZ2'], ...81aafbf913ea1720654554925e7fc431e74bfde3dfe0da447e3c46f76a86fa591025de40924a26c33c6263c0ee637b5a92ff7abc861da2', None])

    async def mint_batch(
        self,
        payload: PostMintBatchRequest,
    ) -> List[BlindedSignature]:
        """Batch mint tokens.
    
        Args:
            payload (PostMintBatchRequest): Request payload containing quote IDs, outputs, and signatures.
    
        Raises:
            Exception: Validation of outputs failed.
            Exception: Quote not paid.
            Exception: Quote already issued.
            Exception: Amount to mint does not match quote amount.
    
        Returns:
            List[BlindedSignature]: Signatures on the outputs.
        """
        if not payload.quotes:
            raise TransactionError("batch must not be empty")
    
        if len(set(payload.quotes)) != len(payload.quotes):
            raise BatchDuplicateQuotesError()
    
        if payload.signatures and len(payload.signatures) != len(payload.quotes):
            raise TransactionError("signatures length must match quotes length")
    
        await self._verify_outputs(payload.outputs)
        # we already know from _verify_outputs that all outputs have the same unit because they have the same keyset
        output_unit = self.keysets[payload.outputs[0].id].unit
        sum_amount_outputs = sum([b.amount for b in payload.outputs])
    
        quotes: List[MintQuote] = []
        for quote_id in payload.quotes:
            quote = await self.get_mint_quote(quote_id)
            if not quote:
                raise TransactionError(f"quote {quote_id} not found")
            quotes.append(quote)
    
        # Check payment method consistency
        methods = set([q.method for q in quotes])
        if len(methods) > 1:
            raise TransactionError("all quotes must have the same method")
    
        # Check currency unit consistency
        units = set([q.unit for q in quotes])
        if len(units) > 1:
            raise TransactionError("all quotes must have the same unit")
        if units.pop() != output_unit.name:
            raise TransactionError("quote unit does not match output unit")
    
        for quote in quotes:
            if quote.pending:
                raise TransactionError("mint quote already pending")
            if quote.issued:
                raise QuoteAlreadyIssuedError()
            if quote.state != MintQuoteState.paid:
                raise QuoteNotPaidError()
    
        # Check amount balance
        if payload.quote_amounts:
            if len(payload.quote_amounts) != len(quotes):
                raise TransactionError("quote_amounts length must match quotes length")
            for i, quote in enumerate(quotes):
                if (
                    quote.method == "bolt11"
                    and payload.quote_amounts[i] != quote.amount
                ):
                    raise TransactionError(
                        f"quote amount {payload.quote_amounts[i]} does not match quote {quote.quote} amount {quote.amount}"
                    )
                if payload.quote_amounts[i] > quote.amount:
                    raise TransactionError(
                        f"quote amount {payload.quote_amounts[i]} exceeds quote {quote.quote} amount {quote.amount}"
                    )
    
        quote_amounts = payload.quote_amounts or [q.amount for q in quotes]
        if "bolt11" in methods:
            if sum(quote_amounts) != sum_amount_outputs:
                raise TransactionError(
                    "amount to mint does not match quote amounts sum"
                )
        else:
            if sum_amount_outputs > sum(quote_amounts):
                raise TransactionError("amount to mint exceeds quote amounts sum")
    
        # Signature validation (NUT-20)
        for i, quote in enumerate(quotes):
            sig = payload.signatures[i] if payload.signatures else None
    
            if not quote.pubkey and sig:
                raise QuoteSignatureInvalidError()
    
            # The spec says msg_to_sign = quote_id[i] || B_0 || B_1 || ... || B_(n-1)
            # This logic is inside self._verify_mint_quote_witness, let's reuse it.
            if not self._verify_mint_quote_witness(quote, payload.outputs, sig):
>               raise QuoteSignatureInvalidError()
E               cashu.core.errors.QuoteSignatureInvalidError: Signature for mint request invalid

cashu/mint/ledger.py:620: QuoteSignatureInvalidError
tests.mint.test_mint_batch::test_ledger_mint_batch_atomicity_one_invalid
Stack Traces | 2.17s run time
ledger = <cashu.mint.ledger.Ledger object at 0x7f2b1233c970>
wallet = <cashu.wallet.wallet.Wallet object at 0x7f2b12399930>

    @pytest.mark.asyncio
    async def test_ledger_mint_batch_atomicity_one_invalid(ledger: Ledger, wallet: Wallet):
        """If one quote in batch is invalid, none should be minted."""
        await wallet.load_mint()
        mint_quote1 = await wallet.request_mint(64)
        mint_quote2 = await wallet.request_mint(32)
    
        await pay_if_regtest(mint_quote1.request)
        await pay_if_regtest(mint_quote2.request)
    
        q2 = await ledger.crud.get_mint_quote(quote_id=mint_quote2.quote, db=ledger.db)
        q2.expiry = 1000000
        await ledger.crud.update_mint_quote(quote=q2, db=ledger.db)
    
        secrets, rs, derivation_paths = await wallet.generate_secrets_from_to(10000, 10001)
        outputs, rs = wallet._construct_outputs([64, 32], secrets, rs)
    
        assert mint_quote1.privkey
        assert mint_quote2.privkey
    
        sig1 = nut20.sign_mint_quote(mint_quote1.quote, outputs, mint_quote1.privkey)
        sig2 = nut20.sign_mint_quote(mint_quote2.quote, outputs, mint_quote2.privkey)
    
        try:
            await ledger.mint_batch(
                PostMintBatchRequest(
                    quotes=[mint_quote1.quote, mint_quote2.quote],
                    quote_amounts=[64, 32],
                    outputs=outputs,
                    signatures=[sig1, sig2],
                )
            )
>           assert False, "Expected Exception"
E           AssertionError: Expected Exception
E           assert False

tests/mint/test_mint_batch.py:543: AssertionError

During handling of the above exception, another exception occurred:

ledger = <cashu.mint.ledger.Ledger object at 0x7f2b1233c970>
wallet = <cashu.wallet.wallet.Wallet object at 0x7f2b12399930>

    @pytest.mark.asyncio
    async def test_ledger_mint_batch_atomicity_one_invalid(ledger: Ledger, wallet: Wallet):
        """If one quote in batch is invalid, none should be minted."""
        await wallet.load_mint()
        mint_quote1 = await wallet.request_mint(64)
        mint_quote2 = await wallet.request_mint(32)
    
        await pay_if_regtest(mint_quote1.request)
        await pay_if_regtest(mint_quote2.request)
    
        q2 = await ledger.crud.get_mint_quote(quote_id=mint_quote2.quote, db=ledger.db)
        q2.expiry = 1000000
        await ledger.crud.update_mint_quote(quote=q2, db=ledger.db)
    
        secrets, rs, derivation_paths = await wallet.generate_secrets_from_to(10000, 10001)
        outputs, rs = wallet._construct_outputs([64, 32], secrets, rs)
    
        assert mint_quote1.privkey
        assert mint_quote2.privkey
    
        sig1 = nut20.sign_mint_quote(mint_quote1.quote, outputs, mint_quote1.privkey)
        sig2 = nut20.sign_mint_quote(mint_quote2.quote, outputs, mint_quote2.privkey)
    
        try:
            await ledger.mint_batch(
                PostMintBatchRequest(
                    quotes=[mint_quote1.quote, mint_quote2.quote],
                    quote_amounts=[64, 32],
                    outputs=outputs,
                    signatures=[sig1, sig2],
                )
            )
            assert False, "Expected Exception"
        except Exception as e:
>           assert "expired" in str(e)
E           AssertionError: assert 'expired' in 'Expected Exception\nassert False'
E            +  where 'Expected Exception\nassert False' = str(AssertionError('Expected Exception\nassert False'))

tests/mint/test_mint_batch.py:545: AssertionError
tests.mint.test_mint_batch::test_ledger_mint_batch_expired_quotes
Stack Traces | 2.17s run time
ledger = <cashu.mint.ledger.Ledger object at 0x7f2b126e9cc0>
wallet = <cashu.wallet.wallet.Wallet object at 0x7f2b1266b460>

    @pytest.mark.asyncio
    async def test_ledger_mint_batch_expired_quotes(ledger: Ledger, wallet: Wallet):
        """Batch mint with expired quotes should fail."""
        await wallet.load_mint()
        mint_quote1 = await wallet.request_mint(64)
        mint_quote2 = await wallet.request_mint(32)
    
        await pay_if_regtest(mint_quote1.request)
        await pay_if_regtest(mint_quote2.request)
    
        q2 = await ledger.crud.get_mint_quote(quote_id=mint_quote2.quote, db=ledger.db)
        q2.expiry = 1000000
        await ledger.crud.update_mint_quote(quote=q2, db=ledger.db)
    
        secrets, rs, derivation_paths = await wallet.generate_secrets_from_to(10000, 10001)
        outputs, rs = wallet._construct_outputs([64, 32], secrets, rs)
    
        assert mint_quote1.privkey
        assert mint_quote2.privkey
    
        sig1 = nut20.sign_mint_quote(mint_quote1.quote, outputs, mint_quote1.privkey)
        sig2 = nut20.sign_mint_quote(mint_quote2.quote, outputs, mint_quote2.privkey)
    
        try:
            await ledger.mint_batch(
                PostMintBatchRequest(
                    quotes=[mint_quote1.quote, mint_quote2.quote],
                    quote_amounts=[64, 32],
                    outputs=outputs,
                    signatures=[sig1, sig2],
                )
            )
>           assert False, "Expected Exception"
E           AssertionError: Expected Exception
E           assert False

tests/mint/test_mint_batch.py:443: AssertionError

During handling of the above exception, another exception occurred:

ledger = <cashu.mint.ledger.Ledger object at 0x7f2b126e9cc0>
wallet = <cashu.wallet.wallet.Wallet object at 0x7f2b1266b460>

    @pytest.mark.asyncio
    async def test_ledger_mint_batch_expired_quotes(ledger: Ledger, wallet: Wallet):
        """Batch mint with expired quotes should fail."""
        await wallet.load_mint()
        mint_quote1 = await wallet.request_mint(64)
        mint_quote2 = await wallet.request_mint(32)
    
        await pay_if_regtest(mint_quote1.request)
        await pay_if_regtest(mint_quote2.request)
    
        q2 = await ledger.crud.get_mint_quote(quote_id=mint_quote2.quote, db=ledger.db)
        q2.expiry = 1000000
        await ledger.crud.update_mint_quote(quote=q2, db=ledger.db)
    
        secrets, rs, derivation_paths = await wallet.generate_secrets_from_to(10000, 10001)
        outputs, rs = wallet._construct_outputs([64, 32], secrets, rs)
    
        assert mint_quote1.privkey
        assert mint_quote2.privkey
    
        sig1 = nut20.sign_mint_quote(mint_quote1.quote, outputs, mint_quote1.privkey)
        sig2 = nut20.sign_mint_quote(mint_quote2.quote, outputs, mint_quote2.privkey)
    
        try:
            await ledger.mint_batch(
                PostMintBatchRequest(
                    quotes=[mint_quote1.quote, mint_quote2.quote],
                    quote_amounts=[64, 32],
                    outputs=outputs,
                    signatures=[sig1, sig2],
                )
            )
            assert False, "Expected Exception"
        except Exception as e:
>           assert "expired" in str(e)
E           AssertionError: assert 'expired' in 'Expected Exception\nassert False'
E            +  where 'Expected Exception\nassert False' = str(AssertionError('Expected Exception\nassert False'))

tests/mint/test_mint_batch.py:445: AssertionError

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

1 participant