Skip to content

feat(crypto): migrate BDHKE to BLS12-381 (v3 keysets)#999

Draft
a1denvalu3 wants to merge 30 commits into
mainfrom
feature/bls12-381-v3-keyset
Draft

feat(crypto): migrate BDHKE to BLS12-381 (v3 keysets)#999
a1denvalu3 wants to merge 30 commits into
mainfrom
feature/bls12-381-v3-keyset

Conversation

@a1denvalu3
Copy link
Copy Markdown
Collaborator

Summary

This pull request introduces BLS12-381 cryptography into the Cashu protocol, enabling smaller proofs and paving the way for multi-signature schemes and batch verification.

Core Changes

  • Introduces v3 keysets using the BLS12-381 curve.
  • Adds multiplicative blinding logic (Y * r) to replace legacy additive blinding (Y + r*G).
  • Replaces DLEQ proof requirements with BLS pairing verification (e(C, G2) == e(Y, K2)).
  • Modifies wallet redemption and proof construction steps to unblind signatures cleanly and omit unneeded DLEQ verification logic.
  • Maintains complete backwards compatibility with v1/v2 (secp256k1) keysets.
  • Implements comprehensive batch pairing verification for unblinded BLS signatures.
  • Updates keyset ID generation format to utilize the 02 prefix for BLS keysets.

Testing

  • Updates the wallet and mint CLI tests to dynamically accommodate both secp256k1 and BLS12-381 logic.
  • Adds tests/test_crypto_bls.py test suite specifically for deterministic hash-to-curve testing, verification of individual BLS protocol steps, and batched BLS pairing checks.

a1denvalu3 added 8 commits May 5, 2026 11:12
… verification

- Fixed keyset v3 derivation logic and ID generation
- Replaced additive blinding logic with BLS multiplicative blinding
- Handled backwards compatibility for DLEQ skipping and verifying
- Implemented batch BLS pairing verification for unblinded signatures
- Removed redundant dummy DLEQ generation in BLS operations
- Fixed failing unit tests and resolved type-checking union issues
@codecov
Copy link
Copy Markdown

codecov Bot commented May 5, 2026

❌ 46 Tests Failed:

Tests completed Failed Passed Skipped
654 46 608 73
View the top 3 failed test(s) by shortest run time
tests.test_crypto::test_dleq_alice_direct_verify_dleq
Stack Traces | 0.001s run time
def test_dleq_alice_direct_verify_dleq():
        # ----- test again with B_ and C_ as per step1 and step2
        a = PrivateKey(
            bytes.fromhex(
                "0000000000000000000000000000000000000000000000000000000000000001"
            )
        )
        A = a.public_key
        assert A
        B_, _ = step1_alice(
            "test_message",
            blinding_factor=PrivateKey(
                bytes.fromhex(
                    "0000000000000000000000000000000000000000000000000000000000000001"
                ),
    
            ),
        )
>       C_, e, s = step2_bob(B_, a)

tests/test_crypto.py:262: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

B_ = <coincurve.keys.PublicKey object at 0x7efe7f2eae00>
a = <cashu.core.crypto.secp.SecpPrivateKey object at 0x7efe7f2e8730>

    def step2_bob(B_: SecpPublicKey, a: SecpPrivateKey) -> Tuple[SecpPublicKey, SecpPrivateKey, SecpPrivateKey]:
>       C_: SecpPublicKey = B_ * a  # type: ignore
E       TypeError: unsupported operand type(s) for *: 'PublicKey' and 'SecpPrivateKey'

.../core/crypto/b_dhke.py:104: TypeError
tests.test_crypto::test_dleq_carol_verify_from_bob
Stack Traces | 0.001s run time
def test_dleq_carol_verify_from_bob():
        a = PrivateKey(
            bytes.fromhex(
                "0000000000000000000000000000000000000000000000000000000000000001"
            ),
    
        )
        A = a.public_key
        assert A
        assert (
            A.format().hex()
            == "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
        )
        secret_msg = "test_message"
        r = PrivateKey(
            bytes.fromhex(
                "0000000000000000000000000000000000000000000000000000000000000001"
            )
        )
        B_, _ = step1_alice(secret_msg, r)
>       C_, e, s = step2_bob(B_, a)

tests/test_crypto.py:286: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

B_ = <coincurve.keys.PublicKey object at 0x7efe7f2de950>
a = <cashu.core.crypto.secp.SecpPrivateKey object at 0x7efe7f2deb00>

    def step2_bob(B_: SecpPublicKey, a: SecpPrivateKey) -> Tuple[SecpPublicKey, SecpPrivateKey, SecpPrivateKey]:
>       C_: SecpPublicKey = B_ * a  # type: ignore
E       TypeError: unsupported operand type(s) for *: 'PublicKey' and 'SecpPrivateKey'

.../core/crypto/b_dhke.py:104: TypeError
tests.test_crypto::test_dleq_step2_bob_dleq
Stack Traces | 0.001s run time
def test_dleq_step2_bob_dleq():
        B_, _ = step1_alice(
            "test_message",
            blinding_factor=PrivateKey(
                bytes.fromhex(
                    "0000000000000000000000000000000000000000000000000000000000000001"
                ),
    
            ),
        )
        a = PrivateKey(
            bytes.fromhex(
                "0000000000000000000000000000000000000000000000000000000000000001"
            ),
    
        )
        p_bytes = bytes.fromhex(
            "0000000000000000000000000000000000000000000000000000000000000001"
        )  # 32 bytes
>       e, s = step2_bob_dleq(B_, a, p_bytes)

tests/test_crypto.py:165: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

B_ = <coincurve.keys.PublicKey object at 0x7efe7f2dfd30>
a = <cashu.core.crypto.secp.SecpPrivateKey object at 0x7efe7f2dfa90>
p_bytes = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01'

    def step2_bob_dleq(
        B_: SecpPublicKey,
        a: SecpPrivateKey,
        p_bytes: bytes = b"",
    ) -> Tuple[Union[SecpPrivateKey, BlsPrivateKey], Union[SecpPrivateKey, BlsPrivateKey]]:
    
        if p_bytes:
            # deterministic p for testing
            p = SecpPrivateKey(p_bytes)
        else:
            p = SecpPrivateKey()
    
    
        R1 = SecpPublicKey(p.public_key.format())  # R1 = pG
        assert R1
>       R2: PublicKey = cast(PublicKey, B_ * p)  # type: ignore
E       TypeError: unsupported operand type(s) for *: 'PublicKey' and 'SecpPrivateKey'

.../core/crypto/b_dhke.py:157: TypeError
tests.test_crypto::test_dleq_step2_bob_dleq_deprecated
Stack Traces | 0.001s run time
def test_dleq_step2_bob_dleq_deprecated():
        B_, _ = step1_alice_deprecated(
            "test_message",
            blinding_factor=PrivateKey(
                bytes.fromhex(
                    "0000000000000000000000000000000000000000000000000000000000000001"
                ),
    
            ),
        )
        a = PrivateKey(
            bytes.fromhex(
                "0000000000000000000000000000000000000000000000000000000000000001"
            ),
    
        )
        p_bytes = bytes.fromhex(
            "0000000000000000000000000000000000000000000000000000000000000001"
        )  # 32 bytes
>       e, s = step2_bob_dleq(B_, a, p_bytes)

tests/test_crypto.py:451: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

B_ = <coincurve.keys.PublicKey object at 0x7efe7f2ea410>
a = <cashu.core.crypto.secp.SecpPrivateKey object at 0x7efe7f2e8070>
p_bytes = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01'

    def step2_bob_dleq(
        B_: SecpPublicKey,
        a: SecpPrivateKey,
        p_bytes: bytes = b"",
    ) -> Tuple[Union[SecpPrivateKey, BlsPrivateKey], Union[SecpPrivateKey, BlsPrivateKey]]:
    
        if p_bytes:
            # deterministic p for testing
            p = SecpPrivateKey(p_bytes)
        else:
            p = SecpPrivateKey()
    
    
        R1 = SecpPublicKey(p.public_key.format())  # R1 = pG
        assert R1
>       R2: PublicKey = cast(PublicKey, B_ * p)  # type: ignore
E       TypeError: unsupported operand type(s) for *: 'PublicKey' and 'SecpPrivateKey'

.../core/crypto/b_dhke.py:157: TypeError
tests.test_crypto::test_step2
Stack Traces | 0.001s run time
def test_step2():
        B_, _ = step1_alice(
            "test_message",
            blinding_factor=PrivateKey(
                bytes.fromhex(
                    "0000000000000000000000000000000000000000000000000000000000000001"
                ),
    
            ),
        )
        a = PrivateKey(
            bytes.fromhex(
                "0000000000000000000000000000000000000000000000000000000000000001"
            ),
    
        )
>       C_, e, s = step2_bob(B_, a)

tests/test_crypto.py:82: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

B_ = <coincurve.keys.PublicKey object at 0x7efe7f2dded0>
a = <cashu.core.crypto.secp.SecpPrivateKey object at 0x7efe7f2de020>

    def step2_bob(B_: SecpPublicKey, a: SecpPrivateKey) -> Tuple[SecpPublicKey, SecpPrivateKey, SecpPrivateKey]:
>       C_: SecpPublicKey = B_ * a  # type: ignore
E       TypeError: unsupported operand type(s) for *: 'PublicKey' and 'SecpPrivateKey'

.../core/crypto/b_dhke.py:104: TypeError
tests.test_crypto::test_step2_deprecated
Stack Traces | 0.001s run time
def test_step2_deprecated():
        B_, _ = step1_alice_deprecated(
            "test_message",
            blinding_factor=PrivateKey(
                bytes.fromhex(
                    "0000000000000000000000000000000000000000000000000000000000000001"
                ),
    
            ),
        )
        a = PrivateKey(
            bytes.fromhex(
                "0000000000000000000000000000000000000000000000000000000000000001"
            ),
    
        )
>       C_, e, s = step2_bob(B_, a)

tests/test_crypto.py:396: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

B_ = <coincurve.keys.PublicKey object at 0x7efe7f16f520>
a = <cashu.core.crypto.secp.SecpPrivateKey object at 0x7efe7f16c160>

    def step2_bob(B_: SecpPublicKey, a: SecpPrivateKey) -> Tuple[SecpPublicKey, SecpPrivateKey, SecpPrivateKey]:
>       C_: SecpPublicKey = B_ * a  # type: ignore
E       TypeError: unsupported operand type(s) for *: 'PublicKey' and 'SecpPrivateKey'

.../core/crypto/b_dhke.py:104: TypeError
tests.wallet.test_npc::test_mint_quotes
Stack Traces | 0.009s run time
npc = <cashu.wallet.npc.NpubCash object at 0x7f2cf6574c70>
mock_wallet = <MagicMock spec='Wallet' id='139831088212784'>

    @pytest.mark.asyncio
    async def test_mint_quotes(npc, mock_wallet):
        with patch("cashu.wallet.npc.httpx.AsyncClient") as mock_client, \
             patch("cashu.wallet.npc.get_bolt11_mint_quote", new_callable=AsyncMock) as mock_get_quote:
    
            mock_instance = mock_client.return_value.__aenter__.return_value
    
            # Mock quotes response
            mock_resp = MagicMock()
            mock_resp.status_code = 200
            mock_resp.json.return_value = {
                "error": False,
                "data": {
                    "quotes": [
                        {"quoteId": "q1", "amount": 100, "state": "PAID", "mintUrl": "https://mint.example.com"},
                        {"quoteId": "q2", "amount": 200, "state": "PAID", "mintUrl": "https://other.mint.com"} # Different mint
                    ]
                }
            }
            mock_instance.get.return_value = mock_resp
    
            # Mock local DB quote (not found)
            mock_get_quote.return_value = None
    
            # Mock wallet.get_mint_quote (return quote with state != issued)
            mock_mint_quote = MagicMock()
            mock_mint_quote.state = "PAID"  # Not MintQuoteState.issued
            mock_wallet.get_mint_quote = AsyncMock(return_value=mock_mint_quote)
    
            # Mock wallet mint
>           mock_wallet.mint = AsyncMock(return_value=[Proof(id="1", amount=100, C="C", secret="s")])

tests/wallet/test_npc.py:122: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
cashu/core/base.py:158: in __init__
    if self.id and is_bls_keyset(self.id):
.../core/crypto/keys.py:181: in is_bls_keyset
    version = get_keyset_id_version(keyset_id)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

keyset_id = '1'

    def get_keyset_id_version(keyset_id: str) -> str:
        """
        Extract the version from a keyset ID.
    
        Returns:
            - "00" for version 0 (hex keyset IDs with 00 prefix)
            - "01" for version 1 (v2 keyset IDs with 01 prefix)
            - "base64" for legacy base64 keyset IDs (pre-0.15.0)
        """
        if len(keyset_id) < 2:
>           raise ValueError("Invalid keyset ID: too short")
E           ValueError: Invalid keyset ID: too short

.../core/crypto/keys.py:170: ValueError
tests.wallet.test_wallet_v1_api::test_get_keys_parses_response
Stack Traces | 0.025s run time
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x7f2cf7c61d50>
api = <cashu.wallet.v1_api.LedgerAPI object at 0x7f2cf7c63a30>

    @pytest.mark.asyncio
    async def test_get_keys_parses_response(monkeypatch, api: LedgerAPI):
        pubkey_hex = PrivateKey().public_key.format().hex()
        response = _response(
            200,
            {
                "keysets": [
                    {
                        "id": "keyset-1",
                        "unit": "sat",
                        "active": True,
                        "input_fee_ppk": 0,
                        "keys": {"1": pubkey_hex},
                    }
                ]
            },
        )
    
        async def fake_request(self, method, path, **kwargs):
            assert method == "GET"
            assert path == "keys"
            return response
    
        monkeypatch.setattr(api, "_request", MethodType(fake_request, api))
>       keysets = await api._get_keys()

tests/wallet/test_wallet_v1_api.py:212: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
cashu/wallet/v1_api.py:93: in wrapper
    return await func(self, *args, **kwargs)
cashu/wallet/v1_api.py:259: in _get_keys
    pub_keys[int(amt)] = SecpPublicKey(bytes.fromhex(val))
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <cashu.core.crypto.secp.SecpPublicKey object at 0x7f2cf7da51e0>
data = b"\xa5\xd8D4I\xdb\xd4R\xf1\xe0 \xe6\xc1}\x1e\xa6\xe7\xfc\xf6\x87\xe4\xbb\xb4\xfd@D\x1a\x9d\xab\xdd\xdd\xf4p\x9f\x99D\x...ce\xb7-\t\xa5\x048\x9d\x0ed\xeax\xf3\xa4BPIXh\xden\xbdI`\xb2\xf9\\\xee*\xb8\x15|\xa2\x85>\xa9H'\x18k\xc7\x82+i\x17\xfa"
context = GLOBAL_CONTEXT

    def __init__(self, data, context: Context = GLOBAL_CONTEXT):
        """
        :param data: The formatted public key. This class supports parsing
                     compressed (33 bytes, header byte `0x02` or `0x03`),
                     uncompressed (65 bytes, header byte `0x04`), or
                     hybrid (65 bytes, header byte `0x06` or `0x07`) format public keys.
        :type data: bytes
        :param context:
        :raises ValueError: If the public key could not be parsed or was invalid.
        """
        if not isinstance(data, bytes):
            self.public_key = data
        else:
            public_key = ffi.new('secp256k1_pubkey *')
    
            parsed = lib.secp256k1_ec_pubkey_parse(context.ctx, public_key, data, len(data))
    
            if not parsed:
>               raise ValueError('The public key could not be parsed or is invalid.')
E               ValueError: The public key could not be parsed or is invalid.

../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.10/lib/python3.10.../site-packages/coincurve/keys.py:314: ValueError
tests.wallet.test_wallet_v1_api::test_get_keyset_uses_urlsafe_keyset_id
Stack Traces | 0.026s run time
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x7f2cf58c8bb0>
api = <cashu.wallet.v1_api.LedgerAPI object at 0x7f2cf58c8e50>

    @pytest.mark.asyncio
    async def test_get_keyset_uses_urlsafe_keyset_id(monkeypatch, api: LedgerAPI):
        pubkey_hex = PrivateKey().public_key.format().hex()
        response = _response(
            200,
            {
                "keysets": [
                    {
                        "id": "server-id",
                        "unit": "sat",
                        "active": True,
                        "input_fee_ppk": 0,
                        "keys": {"1": pubkey_hex},
                    }
                ]
            },
        )
    
        async def fake_request(self, method, path, **kwargs):
            assert path == "keys/a-_b"
            return response
    
        monkeypatch.setattr(api, "_request", MethodType(fake_request, api))
>       keyset = await api._get_keyset("a+/b")

tests/wallet/test_wallet_v1_api.py:241: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
cashu/wallet/v1_api.py:93: in wrapper
    return await func(self, *args, **kwargs)
cashu/wallet/v1_api.py:301: in _get_keyset
    keyset_keys[int(amt)] = SecpPublicKey(bytes.fromhex(val))
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <cashu.core.crypto.secp.SecpPublicKey object at 0x7f2cf58c9c00>
data = b'\xb8@P\xa2\xda=\x99\x7f]"\xc9\xcb\naiX\xc0A\x9d\xddg\x9a\x17\x1b\xa7\\\x89\x8cP\x05\xb2\xc9N1\x8eZ\x03+0\x9ez\'\x1f\...5fiJ\xd3\xaaO}@\x82\xe4:~\xe9LN\xe7\x8eR\xf5\xb2=xU\xf1\xd8\xbc\xe3c\xea\xda7+`-\xde\x89cZ\xc1\xd7X\xcf\xab\xc9\xe9\nJ'
context = GLOBAL_CONTEXT

    def __init__(self, data, context: Context = GLOBAL_CONTEXT):
        """
        :param data: The formatted public key. This class supports parsing
                     compressed (33 bytes, header byte `0x02` or `0x03`),
                     uncompressed (65 bytes, header byte `0x04`), or
                     hybrid (65 bytes, header byte `0x06` or `0x07`) format public keys.
        :type data: bytes
        :param context:
        :raises ValueError: If the public key could not be parsed or was invalid.
        """
        if not isinstance(data, bytes):
            self.public_key = data
        else:
            public_key = ffi.new('secp256k1_pubkey *')
    
            parsed = lib.secp256k1_ec_pubkey_parse(context.ctx, public_key, data, len(data))
    
            if not parsed:
>               raise ValueError('The public key could not be parsed or is invalid.')
E               ValueError: The public key could not be parsed or is invalid.

../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.10/lib/python3.10.../site-packages/coincurve/keys.py:314: ValueError
tests.wallet.test_wallet::testactivate_keyset_specific_keyset
Stack Traces | 0.111s run time
wallet1 = <cashu.wallet.wallet.Wallet object at 0x7efe7defd0c0>

    @pytest.mark.asyncio
    async def testactivate_keyset_specific_keyset(wallet1: Wallet):
        await wallet1.activate_keyset()
>       assert list(wallet1.keysets.keys()) == [
            "0200351069db7a17336468dda24c22ce79a0fc1ebaf81b75adff42ecb7db118288"
        ]
E       AssertionError: assert ['01d8a63077d...ed06c039f6bc'] == ['0200351069d...ecb7db118288']
E         
E         At index 0 diff: '01d8a63077d0a51f9855f066409782ffcb322dc8a2265291865221ed06c039f6bc' != '0200351069db7a17336468dda24c22ce79a0fc1ebaf81b75adff42ecb7db118288'
E         
E         Full diff:
E           [
E         -     '0200351069db7a17336468dda24c22ce79a0fc1ebaf81b75adff42ecb7db118288',
E         +     '01d8a63077d0a51f9855f066409782ffcb322dc8a2265291865221ed06c039f6bc',
E           ]

tests/wallet/test_wallet.py:552: AssertionError
tests.wallet.test_wallet::test_get_keys
Stack Traces | 0.156s run time
wallet1 = <cashu.wallet.wallet.Wallet object at 0x7efe7dff6620>

    @pytest.mark.asyncio
    async def test_get_keys(wallet1: Wallet):
        assert wallet1.keysets[wallet1.keyset_id].public_keys
        assert len(wallet1.keysets[wallet1.keyset_id].public_keys) == settings.max_order
        keysets = await wallet1._get_keys()
        keyset = keysets[0]
        assert keyset.id is not None
        # assert keyset.id_deprecated == "eGnEWtdJ0PIM"
>       assert (
            keyset.id
            == "0200351069db7a17336468dda24c22ce79a0fc1ebaf81b75adff42ecb7db118288"
        )
E       AssertionError: assert '01d8a63077d0...1ed06c039f6bc' == '0200351069db...2ecb7db118288'
E         
E         - 0200351069db7a17336468dda24c22ce79a0fc1ebaf81b75adff42ecb7db118288
E         + 01d8a63077d0a51f9855f066409782ffcb322dc8a2265291865221ed06c039f6bc

tests/wallet/test_wallet.py:104: AssertionError
tests.wallet.test_wallet_crud_unit::test_keyset_and_derivation_counter_flow
Stack Traces | 0.19s run time
wallet_db = <cashu.core.db.Database object at 0x7f2cf667a1a0>

    @pytest.mark.asyncio
    async def test_keyset_and_derivation_counter_flow(wallet_db: Database):
        keyset = _keyset("keyset-derivation")
        await store_keyset(keyset, db=wallet_db)
    
>       fetched = await get_keysets(id="keyset-derivation", db=wallet_db)

tests/wallet/test_wallet_crud_unit.py:176: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
cashu/wallet/crud.py:235: in get_keysets
    return [WalletKeyset.from_row(r) for r in rows]  # type: ignore
cashu/wallet/crud.py:235: in <listcomp>
    return [WalletKeyset.from_row(r) for r in rows]  # type: ignore
cashu/core/base.py:795: in from_row
    deserialize(str(row["public_keys"]))
cashu/core/base.py:788: in deserialize
    pub_keys[int(amount)] = SecpPublicKey(bytes.fromhex(hex_key)) # type: ignore
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <cashu.core.crypto.secp.SecpPublicKey object at 0x7f2cf5adac50>
data = b'\x81\xf2<(q\xafH\n\x0f\x03O\xd2\x02L\xd5\xbb\xe6\xa0\x8eE\x9c\xd6\x91H\x1e\xccZ\\\xbb\xed\xa2d\xc68\xefy\xbd\x83U\x9...06f\x1aS\xdb\x14\xc1\xe7h$!\xd8\xcelq\x8bj\x95\x1e%z\x80\x1dY1t\xb7\x86\x85\xe9\x83\xdb\xc3\xa9i\xc7\xd1l1\xab\xfd\xa5'
context = GLOBAL_CONTEXT

    def __init__(self, data, context: Context = GLOBAL_CONTEXT):
        """
        :param data: The formatted public key. This class supports parsing
                     compressed (33 bytes, header byte `0x02` or `0x03`),
                     uncompressed (65 bytes, header byte `0x04`), or
                     hybrid (65 bytes, header byte `0x06` or `0x07`) format public keys.
        :type data: bytes
        :param context:
        :raises ValueError: If the public key could not be parsed or was invalid.
        """
        if not isinstance(data, bytes):
            self.public_key = data
        else:
            public_key = ffi.new('secp256k1_pubkey *')
    
            parsed = lib.secp256k1_ec_pubkey_parse(context.ctx, public_key, data, len(data))
    
            if not parsed:
>               raise ValueError('The public key could not be parsed or is invalid.')
E               ValueError: The public key could not be parsed or is invalid.

../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.10/lib/python3.10.../site-packages/coincurve/keys.py:314: ValueError
tests.wallet.test_wallet_restore::test_bump_secret_derivation
Stack Traces | 0.459s run time
wallet3 = <cashu.wallet.wallet.Wallet object at 0x7f2cf7d07610>

    @pytest.mark.asyncio
    async def test_bump_secret_derivation(wallet3: Wallet):
        await wallet3._init_private_key(
            "half depart obvious quality work element tank gorilla view sugar picture"
            " humble"
        )
        secrets1, rs1, derivation_paths1 = await wallet3.generate_n_secrets(5)
        secrets2, rs2, derivation_paths2 = await wallet3.generate_secrets_from_to(0, 4)
>       assert wallet3.keyset_id == "01d8a63077d0a51f9855f066409782ffcb322dc8a2265291865221ed06c039f6bc"
E       AssertionError: assert '0200351069db...2ecb7db118288' == '01d8a63077d0...1ed06c039f6bc'
E         
E         - 01d8a63077d0a51f9855f066409782ffcb322dc8a2265291865221ed06c039f6bc
E         + 0200351069db7a17336468dda24c22ce79a0fc1ebaf81b75adff42ecb7db118288

tests/wallet/test_wallet_restore.py:95: AssertionError
tests.mint.test_mint_verification::test_together_sig_all_fails_wrong_signature
Stack Traces | 0.488s run time
ledger = <cashu.mint.ledger.Ledger object at 0x7f47b17b8280>

    def test_together_sig_all_fails_wrong_signature(ledger: Ledger):
        kid = ledger.keyset.id
        signer = PrivateKey()
        pub = signer.public_key.format().hex()
        secret_str = _p2pk_sig_all_secret(pub)
        p = Proof(
            id=kid,
            amount=16,
            secret=secret_str,
            C="02" + secrets.token_hex(32),
            witness=None,
        )
        fee = ledger.get_fees_for_proofs([p])
        outs = [_blinded_output(ledger, amount=p.amount - fee, label="sigall-bad-sig")]
        p.witness = P2PKWitness(signatures=["00" * 64]).model_dump_json()
        with pytest.raises(TransactionError, match="signature threshold not met"):
>           ledger._verify_inputs_and_outputs_together([p], outs)

tests/mint/test_mint_verification.py:990: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
cashu/mint/verification.py:184: in _verify_inputs_and_outputs_together
    self._verify_input_output_spending_conditions(proofs, outputs)
cashu/mint/conditions.py:421: in _verify_input_output_spending_conditions
    return self._verify_sigall_spending_conditions(proofs, outputs, message_to_sign)
cashu/mint/conditions.py:388: in _verify_sigall_spending_conditions
    pubkey=SecpPublicKey(bytes.fromhex(p)),
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <cashu.core.crypto.secp.SecpPublicKey object at 0x7f47b162b070>
data = b'\xa4\x12\xb4\x82(\xbf\xfcQ\xae@\xab\x99\xfc\x8b~JT;\x9b\x95\x08\xde|\xef\x10\x91\xe0"{\xdf\x0f0_\xfb\xf2\x13y\xc7*\x...5s\x88\xf6\xcc\xd26?\x96z0\xcf)\xae}\xaa\xff\xfd^P\xb5\xe7\xc3\x80\xa1\xd2G\xb9\x1a\x9b\xeb\xa1\x1cl4\xbc96T\x0erR\x14'
context = GLOBAL_CONTEXT

    def __init__(self, data, context: Context = GLOBAL_CONTEXT):
        """
        :param data: The formatted public key. This class supports parsing
                     compressed (33 bytes, header byte `0x02` or `0x03`),
                     uncompressed (65 bytes, header byte `0x04`), or
                     hybrid (65 bytes, header byte `0x06` or `0x07`) format public keys.
        :type data: bytes
        :param context:
        :raises ValueError: If the public key could not be parsed or was invalid.
        """
        if not isinstance(data, bytes):
            self.public_key = data
        else:
            public_key = ffi.new('secp256k1_pubkey *')
    
            parsed = lib.secp256k1_ec_pubkey_parse(context.ctx, public_key, data, len(data))
    
            if not parsed:
>               raise ValueError('The public key could not be parsed or is invalid.')
E               ValueError: The public key could not be parsed or is invalid.

../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.10/lib/python3.10.../site-packages/coincurve/keys.py:314: ValueError
tests.mint.test_mint_db_operations::test_update_blinded_message_signature_before_store_blinded_message_errors
Stack Traces | 0.49s run time
self = <cashu.core.crypto.bls.PublicKey object at 0x7f47c6f11660>
compressed = b'\x02\xf8\xcbb{^\xe3\xdc?)\xebc\x0c\x92\xd8\x89.\x81/\x03\x84".P\xef\xdci\x84\xb1\xa3\x04\x10\xb0'
point = None, group = 'G1'

    def __init__(self, compressed: bytes = b"", point=None, group="G1"):
        self.group = group
        try:
            if point is not None:
                self.point = point
            elif compressed:
                if self.group == "G1":
>                   self.point = pyblst.BlstP1Element().uncompress(compressed)
E                   ValueError: blst error Blst(BLST_BAD_ENCODING)

.../core/crypto/bls.py:46: ValueError

During handling of the above exception, another exception occurred:

ledger = <cashu.mint.ledger.Ledger object at 0x7f47c6e11720>

    @pytest.mark.asyncio
    async def test_update_blinded_message_signature_before_store_blinded_message_errors(
        ledger: Ledger,
    ):
        from cashu.core.crypto.b_dhke import step1_alice, step2_bob
    
        amount = 8
        # Generate a blinded message that we will NOT store
        B_pub, _ = step1_alice("test_sign_before_store_blinded_message")
        b_hex = B_pub.format().hex()
    
        # Create a valid signature tuple for that blinded message
        priv = ledger.keyset.private_keys[amount]
>       C_point, e, s = step2_bob(PublicKey(bytes.fromhex(b_hex)), priv)

tests/mint/test_mint_db_operations.py:570: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <cashu.core.crypto.bls.PublicKey object at 0x7f47c6f11660>
compressed = b'\x02\xf8\xcbb{^\xe3\xdc?)\xebc\x0c\x92\xd8\x89.\x81/\x03\x84".P\xef\xdci\x84\xb1\xa3\x04\x10\xb0'
point = None, group = 'G1'

    def __init__(self, compressed: bytes = b"", point=None, group="G1"):
        self.group = group
        try:
            if point is not None:
                self.point = point
            elif compressed:
                if self.group == "G1":
                    self.point = pyblst.BlstP1Element().uncompress(compressed)
                else:
                    self.point = pyblst.BlstP2Element().uncompress(compressed)
            else:
                raise ValueError("Must provide point or compressed bytes")
        except Exception as e:
            print(f"Exception parsing BLS public key (group={self.group}, compressed={compressed.hex() if compressed else ''}):", repr(e))
>           raise ValueError("The public key could not be parsed or is invalid.")
E           ValueError: The public key could not be parsed or is invalid.

.../core/crypto/bls.py:53: ValueError
tests.mint.test_mint_verification::test_together_sig_all_succeeds_when_signed
Stack Traces | 0.5s run time
ledger = <cashu.mint.ledger.Ledger object at 0x7f47b1679720>

    def test_together_sig_all_succeeds_when_signed(ledger: Ledger):
        kid = ledger.keyset.id
        signer = PrivateKey()
        pub = signer.public_key.format().hex()
        secret_str = _p2pk_sig_all_secret(pub)
        p = Proof(
            id=kid,
            amount=16,
            secret=secret_str,
            C="02" + secrets.token_hex(32),
            witness=None,
        )
        fee = ledger.get_fees_for_proofs([p])
        outs = [_blinded_output(ledger, amount=p.amount - fee, label="sigall-ok")]
        msg = "".join([p.secret] + [o.B_ for o in outs])
>       sig = schnorr_sign(msg.encode("utf-8"), signer).hex()

tests/mint/test_mint_verification.py:1008: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

message = b'["P2PK", {"data": "b3eaa947e6d00df8e593eca1b5490ae196b55548627b6d896260fc0df70e4a0f5315b2abdede206a5e408ff01a0f2ea81...000000000000000", "tags": [["sigflag", "SIG_ALL"]]}]025bd637c50f1b754be5cce4ca5fbf70e8b46153b4cb7f3892c55ab3bafca342b2'
private_key = <cashu.core.crypto.bls.PrivateKey object at 0x7f47b17b9390>

    def schnorr_sign(message: bytes, private_key: SecpPrivateKey) -> bytes:
>       signature = private_key.sign_schnorr(
            hashlib.sha256(message).digest(),
            None,  # type: ignore
        )
E       AttributeError: 'PrivateKey' object has no attribute 'sign_schnorr'

cashu/core/p2pk.py:56: AttributeError
tests.mint.test_mint_db_operations::test_store_and_sign_blinded_message
Stack Traces | 0.532s run time
self = <cashu.core.crypto.bls.PublicKey object at 0x7f47c708a020>
compressed = b"\x03\x80'\xf4)\x07W\x89\x04\xef\x96c;\xe3[\x17\xe4\x1f}\xe8\xd8\xfb\xcbQ\x85\x87l\x84\xd7r\xf4\xbcL"
point = None, group = 'G1'

    def __init__(self, compressed: bytes = b"", point=None, group="G1"):
        self.group = group
        try:
            if point is not None:
                self.point = point
            elif compressed:
                if self.group == "G1":
>                   self.point = pyblst.BlstP1Element().uncompress(compressed)
E                   ValueError: blst error Blst(BLST_BAD_ENCODING)

.../core/crypto/bls.py:46: ValueError

During handling of the above exception, another exception occurred:

ledger = <cashu.mint.ledger.Ledger object at 0x7f47c708b010>

    @pytest.mark.asyncio
    async def test_store_and_sign_blinded_message(ledger: Ledger):
        # Localized imports to avoid polluting module scope
        from cashu.core.crypto.b_dhke import step1_alice, step2_bob
    
        # Arrange: prepare a blinded message tied to current active keyset
        amount = 8
        keyset_id = ledger.keyset.id
        B_pubkey, _ = step1_alice("test_store_and_sign_blinded_message")
        B_hex = B_pubkey.format().hex()
    
        # Act: store the blinded message (unsinged promise row)
        await ledger.crud.store_blinded_message(
            db=ledger.db,
            amount=amount,
            b_=B_hex,
            id=keyset_id,
        )
    
        # Act: compute a valid blind signature for the stored row and persist it
        private_key_amount = ledger.keyset.private_keys[amount]
>       B_point = PublicKey(bytes.fromhex(B_hex))

tests/mint/test_mint_db_operations.py:381: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <cashu.core.crypto.bls.PublicKey object at 0x7f47c708a020>
compressed = b"\x03\x80'\xf4)\x07W\x89\x04\xef\x96c;\xe3[\x17\xe4\x1f}\xe8\xd8\xfb\xcbQ\x85\x87l\x84\xd7r\xf4\xbcL"
point = None, group = 'G1'

    def __init__(self, compressed: bytes = b"", point=None, group="G1"):
        self.group = group
        try:
            if point is not None:
                self.point = point
            elif compressed:
                if self.group == "G1":
                    self.point = pyblst.BlstP1Element().uncompress(compressed)
                else:
                    self.point = pyblst.BlstP2Element().uncompress(compressed)
            else:
                raise ValueError("Must provide point or compressed bytes")
        except Exception as e:
            print(f"Exception parsing BLS public key (group={self.group}, compressed={compressed.hex() if compressed else ''}):", repr(e))
>           raise ValueError("The public key could not be parsed or is invalid.")
E           ValueError: The public key could not be parsed or is invalid.

.../core/crypto/bls.py:53: ValueError
tests.mint.test_mint_db_operations::test_get_blinded_messages_by_melt_id_filters_signed
Stack Traces | 0.686s run time
self = <cashu.core.crypto.bls.PublicKey object at 0x7f47c6d99390>
compressed = b"\x03\xba\xdce\x0b\xf6\x89\xc3\xcc\xbc\x95J\x17\xb6\xee \x16\xf2U\xb5\x04'\x1a\xb0\xfa~\x0f\\\x9bmf\x8d\xcc"
point = None, group = 'G1'

    def __init__(self, compressed: bytes = b"", point=None, group="G1"):
        self.group = group
        try:
            if point is not None:
                self.point = point
            elif compressed:
                if self.group == "G1":
>                   self.point = pyblst.BlstP1Element().uncompress(compressed)
E                   ValueError: blst error Blst(BLST_BAD_ENCODING)

.../core/crypto/bls.py:46: ValueError

During handling of the above exception, another exception occurred:

wallet = <cashu.wallet.wallet.Wallet object at 0x7f47c6e03850>
ledger = <cashu.mint.ledger.Ledger object at 0x7f47c6d2d210>

    @pytest.mark.asyncio
    async def test_get_blinded_messages_by_melt_id_filters_signed(
        wallet: Wallet, ledger: Ledger
    ):
        from cashu.core.crypto.b_dhke import step1_alice, step2_bob
    
        amount = 2
        keyset_id = ledger.keyset.id
        # Create a real melt quote to satisfy FK on promises.melt_quote
        mint_quote = await wallet.request_mint(64)
        melt_quote = await ledger.melt_quote(
            PostMeltQuoteRequest(request=mint_quote.request, unit="sat")
        )
        melt_id = melt_quote.quote
    
        B1, _ = step1_alice("filter_by_melt_id_1")
        B2, _ = step1_alice("filter_by_melt_id_2")
        b1_hex = B1.format().hex()
        b2_hex = B2.format().hex()
    
        # Persist two unsigned messages
        await ledger.crud.store_blinded_message(
            db=ledger.db, amount=amount, b_=b1_hex, id=keyset_id, melt_id=melt_id
        )
        await ledger.crud.store_blinded_message(
            db=ledger.db, amount=amount, b_=b2_hex, id=keyset_id, melt_id=melt_id
        )
    
        # Sign one of them (it should no longer be returned by get_blinded_messages_melt_id which filters c_ IS NULL)
        priv = ledger.keyset.private_keys[amount]
>       C_point, e, s = step2_bob(PublicKey(bytes.fromhex(b1_hex)), priv)

tests/mint/test_mint_db_operations.py:510: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <cashu.core.crypto.bls.PublicKey object at 0x7f47c6d99390>
compressed = b"\x03\xba\xdce\x0b\xf6\x89\xc3\xcc\xbc\x95J\x17\xb6\xee \x16\xf2U\xb5\x04'\x1a\xb0\xfa~\x0f\\\x9bmf\x8d\xcc"
point = None, group = 'G1'

    def __init__(self, compressed: bytes = b"", point=None, group="G1"):
        self.group = group
        try:
            if point is not None:
                self.point = point
            elif compressed:
                if self.group == "G1":
                    self.point = pyblst.BlstP1Element().uncompress(compressed)
                else:
                    self.point = pyblst.BlstP2Element().uncompress(compressed)
            else:
                raise ValueError("Must provide point or compressed bytes")
        except Exception as e:
            print(f"Exception parsing BLS public key (group={self.group}, compressed={compressed.hex() if compressed else ''}):", repr(e))
>           raise ValueError("The public key could not be parsed or is invalid.")
E           ValueError: The public key could not be parsed or is invalid.

.../core/crypto/bls.py:53: ValueError
tests.mint.test_mint_db_operations::test_get_melt_quote_includes_change_signatures
Stack Traces | 0.69s run time
self = <cashu.core.crypto.bls.PublicKey object at 0x7f47c6f073a0>
compressed = b'\x03\x88\xc5A$\xb7B\xc8DBC\xe1\x17\xf5\x06J\xc9T9T\xa6u\xc1}\x08\xd6*\xfe:%@\x1c\xe6'
point = None, group = 'G1'

    def __init__(self, compressed: bytes = b"", point=None, group="G1"):
        self.group = group
        try:
            if point is not None:
                self.point = point
            elif compressed:
                if self.group == "G1":
>                   self.point = pyblst.BlstP1Element().uncompress(compressed)
E                   ValueError: blst error Blst(BLST_BAD_ENCODING)

.../core/crypto/bls.py:46: ValueError

During handling of the above exception, another exception occurred:

wallet = <cashu.wallet.wallet.Wallet object at 0x7f47c6fee410>
ledger = <cashu.mint.ledger.Ledger object at 0x7f47c70fa050>

    @pytest.mark.asyncio
    async def test_get_melt_quote_includes_change_signatures(
        wallet: Wallet, ledger: Ledger
    ):
        from cashu.core.crypto.b_dhke import step1_alice, step2_bob
    
        amount = 8
        keyset_id = ledger.keyset.id
    
        # Create melt quote and attach outputs/promises under its melt_id
        mint_quote = await wallet.request_mint(64)
        melt_quote = await ledger.melt_quote(
            PostMeltQuoteRequest(request=mint_quote.request, unit="sat")
        )
    
        melt_id = melt_quote.quote
    
        # Create two blinded messages, sign one -> becomes change
        B1, _ = step1_alice("melt_quote_change_1")
        B2, _ = step1_alice("melt_quote_change_2")
        b1_hex = B1.format().hex()
        b2_hex = B2.format().hex()
    
        await ledger.crud.store_blinded_message(
            db=ledger.db, amount=amount, b_=b1_hex, id=keyset_id, melt_id=melt_id
        )
        await ledger.crud.store_blinded_message(
            db=ledger.db, amount=amount, b_=b2_hex, id=keyset_id, melt_id=melt_id
        )
    
        # Sign one -> should appear in change loaded by get_melt_quote
        priv = ledger.keyset.private_keys[amount]
>       C_point, e, s = step2_bob(PublicKey(bytes.fromhex(b1_hex)), priv)

tests/mint/test_mint_db_operations.py:684: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <cashu.core.crypto.bls.PublicKey object at 0x7f47c6f073a0>
compressed = b'\x03\x88\xc5A$\xb7B\xc8DBC\xe1\x17\xf5\x06J\xc9T9T\xa6u\xc1}\x08\xd6*\xfe:%@\x1c\xe6'
point = None, group = 'G1'

    def __init__(self, compressed: bytes = b"", point=None, group="G1"):
        self.group = group
        try:
            if point is not None:
                self.point = point
            elif compressed:
                if self.group == "G1":
                    self.point = pyblst.BlstP1Element().uncompress(compressed)
                else:
                    self.point = pyblst.BlstP2Element().uncompress(compressed)
            else:
                raise ValueError("Must provide point or compressed bytes")
        except Exception as e:
            print(f"Exception parsing BLS public key (group={self.group}, compressed={compressed.hex() if compressed else ''}):", repr(e))
>           raise ValueError("The public key could not be parsed or is invalid.")
E           ValueError: The public key could not be parsed or is invalid.

.../core/crypto/bls.py:53: ValueError
tests.mint.test_mint_db_operations::test_get_blind_signatures_by_melt_id_returns_signed
Stack Traces | 0.701s run time
self = <cashu.core.crypto.bls.PublicKey object at 0x7f47c7255ea0>
compressed = b'\x03o\xfbV\x94\xb4\x96@_\xf7K\xa4\x85\xde\x01\x14H\x00R\xaa\x03I\xce\xa0\xdd%|`?\xc3\\\x95\xa1'
point = None, group = 'G1'

    def __init__(self, compressed: bytes = b"", point=None, group="G1"):
        self.group = group
        try:
            if point is not None:
                self.point = point
            elif compressed:
                if self.group == "G1":
>                   self.point = pyblst.BlstP1Element().uncompress(compressed)
E                   ValueError: blst error Blst(BLST_BAD_ENCODING)

.../core/crypto/bls.py:46: ValueError

During handling of the above exception, another exception occurred:

wallet = <cashu.wallet.wallet.Wallet object at 0x7f47c77dca60>
ledger = <cashu.mint.ledger.Ledger object at 0x7f47c7248b80>

    @pytest.mark.asyncio
    async def test_get_blind_signatures_by_melt_id_returns_signed(
        wallet: Wallet, ledger: Ledger
    ):
        from cashu.core.crypto.b_dhke import step1_alice, step2_bob
    
        amount = 4
        keyset_id = ledger.keyset.id
        # Create a real melt quote to satisfy FK on promises.melt_quote
        mint_quote = await wallet.request_mint(64)
        melt_quote = await ledger.melt_quote(
            PostMeltQuoteRequest(request=mint_quote.request, unit="sat")
        )
        melt_id = melt_quote.quote
    
        # Prepare two blinded messages under the same melt_id
        B1, _ = step1_alice("signed_promises_by_melt_id_1")
        B2, _ = step1_alice("signed_promises_by_melt_id_2")
        b1_hex = B1.format().hex()
        b2_hex = B2.format().hex()
    
        await ledger.crud.store_blinded_message(
            db=ledger.db, amount=amount, b_=b1_hex, id=keyset_id, melt_id=melt_id
        )
        await ledger.crud.store_blinded_message(
            db=ledger.db, amount=amount, b_=b2_hex, id=keyset_id, melt_id=melt_id
        )
    
        # Sign only one of them -> should be returned by get_blind_signatures_melt_id
        priv = ledger.keyset.private_keys[amount]
>       C_point, e, s = step2_bob(PublicKey(bytes.fromhex(b1_hex)), priv)

tests/mint/test_mint_db_operations.py:631: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <cashu.core.crypto.bls.PublicKey object at 0x7f47c7255ea0>
compressed = b'\x03o\xfbV\x94\xb4\x96@_\xf7K\xa4\x85\xde\x01\x14H\x00R\xaa\x03I\xce\xa0\xdd%|`?\xc3\\\x95\xa1'
point = None, group = 'G1'

    def __init__(self, compressed: bytes = b"", point=None, group="G1"):
        self.group = group
        try:
            if point is not None:
                self.point = point
            elif compressed:
                if self.group == "G1":
                    self.point = pyblst.BlstP1Element().uncompress(compressed)
                else:
                    self.point = pyblst.BlstP2Element().uncompress(compressed)
            else:
                raise ValueError("Must provide point or compressed bytes")
        except Exception as e:
            print(f"Exception parsing BLS public key (group={self.group}, compressed={compressed.hex() if compressed else ''}):", repr(e))
>           raise ValueError("The public key could not be parsed or is invalid.")
E           ValueError: The public key could not be parsed or is invalid.

.../core/crypto/bls.py:53: ValueError
tests.wallet.test_wallet_requests::test_swap_outputs_are_sorted
Stack Traces | 1.42s run time
self = <cashu.core.crypto.bls.PublicKey object at 0x7f2cf7ca0850>
compressed = b"\x02Q\x0b \x80\x1a/ \xde\x82\x96q:<\x178\xb4\x9c&#\xed?\xc9#.\xfa7\xce\xaa&'e\x0e"
point = None, group = 'G1'

    def __init__(self, compressed: bytes = b"", point=None, group="G1"):
        self.group = group
        try:
            if point is not None:
                self.point = point
            elif compressed:
                if self.group == "G1":
>                   self.point = pyblst.BlstP1Element().uncompress(compressed)
E                   ValueError: blst error Blst(BLST_BAD_ENCODING)

.../core/crypto/bls.py:46: ValueError

During handling of the above exception, another exception occurred:

wallet1 = <cashu.wallet.wallet.Wallet object at 0x7f2cf7c491b0>

    @pytest.mark.asyncio
    async def test_swap_outputs_are_sorted(wallet1: Wallet):
        await wallet1.load_mint()
        mint_quote = await wallet1.request_mint(16)
        await pay_if_regtest(mint_quote.request)
        await wallet1.mint(16, quote_id=mint_quote.quote, split=[16])
        assert wallet1.balance == 16
    
        test_url = f"{wallet1.url}/v1/swap"
        key = hash_to_curve("test".encode("utf-8"))
        mock_blind_signature = BlindedSignature(
            id=wallet1.keyset_id,
            amount=8,
            C_=key.format().hex(),
        )
        mock_response_data = {"signatures": [mock_blind_signature.model_dump()]}
        with respx.mock() as mock:
            route = mock.post(test_url).mock(
                return_value=Response(200, json=mock_response_data)
            )
>           await wallet1.select_to_send(wallet1.proofs, 5)

tests/wallet/test_wallet_requests.py:47: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
cashu/wallet/wallet.py:1273: in select_to_send
    _, send_proofs = await self.swap_to_send(
cashu/wallet/wallet.py:1334: in swap_to_send
    keep_proofs, send_proofs = await self.split(
cashu/wallet/wallet.py:742: in split
    new_proofs = await self._construct_proofs(
cashu/wallet/wallet.py:1034: in _construct_proofs
    C_ = cast(PublicKey, BlsPublicKey(bytes.fromhex(promise.C_)))
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <cashu.core.crypto.bls.PublicKey object at 0x7f2cf7ca0850>
compressed = b"\x02Q\x0b \x80\x1a/ \xde\x82\x96q:<\x178\xb4\x9c&#\xed?\xc9#.\xfa7\xce\xaa&'e\x0e"
point = None, group = 'G1'

    def __init__(self, compressed: bytes = b"", point=None, group="G1"):
        self.group = group
        try:
            if point is not None:
                self.point = point
            elif compressed:
                if self.group == "G1":
                    self.point = pyblst.BlstP1Element().uncompress(compressed)
                else:
                    self.point = pyblst.BlstP2Element().uncompress(compressed)
            else:
                raise ValueError("Must provide point or compressed bytes")
        except Exception as e:
            print(f"Exception parsing BLS public key (group={self.group}, compressed={compressed.hex() if compressed else ''}):", repr(e))
>           raise ValueError("The public key could not be parsed or is invalid.")
E           ValueError: The public key could not be parsed or is invalid.

.../core/crypto/bls.py:53: ValueError
tests.wallet.test_wallet_htlc::test_htlc_redeem_with_3_of_3_signatures_but_only_2_provided
Stack Traces | 1.61s run time
wallet1 = <cashu.wallet.wallet.Wallet object at 0x7f2cf5995cf0>
wallet2 = <cashu.wallet.wallet.Wallet object at 0x7f2cf5adb0a0>

    @pytest.mark.asyncio
    async def test_htlc_redeem_with_3_of_3_signatures_but_only_2_provided(
        wallet1: Wallet, wallet2: Wallet
    ):
        mint_quote = await wallet1.request_mint(64)
        await pay_if_regtest(mint_quote.request)
        await wallet1.mint(64, quote_id=mint_quote.quote)
        preimage = "0000000000000000000000000000000000000000000000000000000000000000"
        pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
        pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
        # preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
        secret = await wallet1.create_htlc_lock(
            preimage=preimage,
            hashlock_pubkeys=[pubkey_wallet1, pubkey_wallet2],
            hashlock_n_sigs=3,
        )
        _, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret)
    
        signatures1 = wallet1.signatures_proofs_sig_inputs(send_proofs)
>       signatures2 = wallet2.signatures_proofs_sig_inputs(send_proofs)

tests/wallet/test_wallet_htlc.py:277: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
cashu/wallet/p2pk.py:97: in signatures_proofs_sig_inputs
    signatures = [
cashu/wallet/p2pk.py:98: in <listcomp>
    schnorr_sign(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

message = b'["HTLC", {"data": "66687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925", "nonce": "3a473384761acedb7cb8...160f3a0bc0675df4c61554ff4eefb55e56c52750c63d5267f1d7f42d2f2d07ca167d1435fb53396217c4731f56435ce3"], ["n_sigs", "3"]]}]'
private_key = <cashu.core.crypto.bls.PrivateKey object at 0x7f2cf65289a0>

    def schnorr_sign(message: bytes, private_key: SecpPrivateKey) -> bytes:
>       signature = private_key.sign_schnorr(
            hashlib.sha256(message).digest(),
            None,  # type: ignore
        )
E       AttributeError: 'PrivateKey' object has no attribute 'sign_schnorr'

cashu/core/p2pk.py:56: AttributeError
tests.wallet.test_wallet_htlc::test_htlc_redeem_with_2_of_1_signatures
Stack Traces | 1.61s run time
wallet1 = <cashu.wallet.wallet.Wallet object at 0x7f2cf5a49b10>
wallet2 = <cashu.wallet.wallet.Wallet object at 0x7f2cf5a4acb0>

    @pytest.mark.asyncio
    async def test_htlc_redeem_with_2_of_1_signatures(wallet1: Wallet, wallet2: Wallet):
        mint_quote = await wallet1.request_mint(64)
        await pay_if_regtest(mint_quote.request)
        await wallet1.mint(64, quote_id=mint_quote.quote)
        preimage = "0000000000000000000000000000000000000000000000000000000000000000"
        pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
        pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
        # preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
        secret = await wallet1.create_htlc_lock(
            preimage=preimage,
            hashlock_pubkeys=[pubkey_wallet1, pubkey_wallet2],
            hashlock_n_sigs=1,
        )
        _, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret)
    
        signatures1 = wallet1.signatures_proofs_sig_inputs(send_proofs)
>       signatures2 = wallet2.signatures_proofs_sig_inputs(send_proofs)

tests/wallet/test_wallet_htlc.py:198: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
cashu/wallet/p2pk.py:97: in signatures_proofs_sig_inputs
    signatures = [
cashu/wallet/p2pk.py:98: in <listcomp>
    schnorr_sign(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

message = b'["HTLC", {"data": "66687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925", "nonce": "840b7b91fc1f0a81b4b8...1047fffdb49cff18b412cd7003a993da918098528d1092a5e1e01d903a184e75f8ac7a5a9c5579bc8038030ccdcb60b3"], ["n_sigs", "1"]]}]'
private_key = <cashu.core.crypto.bls.PrivateKey object at 0x7f2cf9eca4a0>

    def schnorr_sign(message: bytes, private_key: SecpPrivateKey) -> bytes:
>       signature = private_key.sign_schnorr(
            hashlib.sha256(message).digest(),
            None,  # type: ignore
        )
E       AttributeError: 'PrivateKey' object has no attribute 'sign_schnorr'

cashu/core/p2pk.py:56: AttributeError
tests.wallet.test_wallet_htlc::test_htlc_redeem_with_2_of_2_signatures
Stack Traces | 1.62s run time
wallet1 = <cashu.wallet.wallet.Wallet object at 0x7f2cf58bd750>
wallet2 = <cashu.wallet.wallet.Wallet object at 0x7f2cf5813580>

    @pytest.mark.asyncio
    async def test_htlc_redeem_with_2_of_2_signatures(wallet1: Wallet, wallet2: Wallet):
        mint_quote = await wallet1.request_mint(64)
        await pay_if_regtest(mint_quote.request)
        await wallet1.mint(64, quote_id=mint_quote.quote)
        preimage = "0000000000000000000000000000000000000000000000000000000000000000"
        pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
        pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
        # preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
        secret = await wallet1.create_htlc_lock(
            preimage=preimage,
            hashlock_pubkeys=[pubkey_wallet1, pubkey_wallet2],
            hashlock_n_sigs=2,
        )
        _, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret)
    
        signatures1 = wallet1.signatures_proofs_sig_inputs(send_proofs)
>       signatures2 = wallet2.signatures_proofs_sig_inputs(send_proofs)

tests/wallet/test_wallet_htlc.py:222: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
cashu/wallet/p2pk.py:97: in signatures_proofs_sig_inputs
    signatures = [
cashu/wallet/p2pk.py:98: in <listcomp>
    schnorr_sign(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

message = b'["HTLC", {"data": "66687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925", "nonce": "2280538004530306aad2...1124a1c6c9ff5c26cf825368d622185e9794043ad86e67e72bc89142f5328e17b357b1fa29ebde51b12d7e9dc4c1161b"], ["n_sigs", "2"]]}]'
private_key = <cashu.core.crypto.bls.PrivateKey object at 0x7f2cf58bea10>

    def schnorr_sign(message: bytes, private_key: SecpPrivateKey) -> bytes:
>       signature = private_key.sign_schnorr(
            hashlib.sha256(message).digest(),
            None,  # type: ignore
        )
E       AttributeError: 'PrivateKey' object has no attribute 'sign_schnorr'

cashu/core/p2pk.py:56: AttributeError
tests.wallet.test_wallet_p2pk::test_p2pk_multisig_case_bypass
Stack Traces | 1.62s run time
wallet1 = <cashu.wallet.wallet.Wallet object at 0x7f2cf58ca8f0>
wallet2 = <cashu.wallet.wallet.Wallet object at 0x7f2cf58c9390>

    @pytest.mark.asyncio
    async def test_p2pk_multisig_case_bypass(wallet1: Wallet, wallet2: Wallet):
        """
        Tests an exploit where the same pubkey and signature are duplicated using
        upper and lower case to bypass deduplication checks in the mint.
        """
        mint_quote = await wallet1.request_mint(64)
        await pay_if_regtest(mint_quote.request)
        await wallet1.mint(64, quote_id=mint_quote.quote)
    
        # We use wallet2's pubkey, but provide it twice: once lower, once upper.
        pubkey_wallet2_lower = await wallet2.create_p2pk_pubkey()
        pubkey_wallet2_upper = pubkey_wallet2_lower.upper()
    
        # p2pk test: require 2 signatures, but provide the same pubkey (just cased differently)
        secret_lock = await wallet1.create_p2pk_lock(
            pubkey_wallet2_lower, tags=Tags([["pubkeys", pubkey_wallet2_upper]]), n_sigs=2
        )
    
        # wallet1 locks the proofs to the bypass 2-of-2 secret
        _, send_proofs = await wallet1.swap_to_send(
            wallet1.proofs, 8, secret_lock=secret_lock
        )
    
        # wallet2 creates one valid signature
>       send_proofs = wallet2.sign_p2pk_sig_inputs(send_proofs)

tests/wallet/test_wallet_p2pk.py:382: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
cashu/wallet/p2pk.py:311: in sign_p2pk_sig_inputs
    p2pk_signatures = self.signatures_proofs_sig_inputs(our_pubkey_proofs)
cashu/wallet/p2pk.py:97: in signatures_proofs_sig_inputs
    signatures = [
cashu/wallet/p2pk.py:98: in <listcomp>
    schnorr_sign(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

message = b'["P2PK", {"data": "ae1ded8f29c9e4da172f82c76a4c04898474134843cacca9c847b2d3683a0eee20bd49a40a0ae09fe3f46aa06f46d5bc1...17E1D86CEFDD679E78FA69CA3CEB3B688412A33F44432526A7E9D587E32624C2622D7"], ["sigflag", "SIG_INPUTS"], ["n_sigs", "2"]]}]'
private_key = <cashu.core.crypto.bls.PrivateKey object at 0x7f2cf67d9bd0>

    def schnorr_sign(message: bytes, private_key: SecpPrivateKey) -> bytes:
>       signature = private_key.sign_schnorr(
            hashlib.sha256(message).digest(),
            None,  # type: ignore
        )
E       AttributeError: 'PrivateKey' object has no attribute 'sign_schnorr'

cashu/core/p2pk.py:56: AttributeError
tests.wallet.test_wallet_p2pk::test_p2pk_multisig_two_signatures_same_pubkey
Stack Traces | 1.62s run time
wallet1 = <cashu.wallet.wallet.Wallet object at 0x7f2cf5a4b100>
wallet2 = <cashu.wallet.wallet.Wallet object at 0x7f2cf5a492a0>

    @pytest.mark.asyncio
    async def test_p2pk_multisig_two_signatures_same_pubkey(
        wallet1: Wallet, wallet2: Wallet
    ):
        # we generate two different signatures from the same private key
        mint_quote = await wallet2.request_mint(64)
        await pay_if_regtest(mint_quote.request)
        await wallet2.mint(64, quote_id=mint_quote.quote)
        pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
        pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
        assert pubkey_wallet1 != pubkey_wallet2
        # p2pk test
        secret_lock = await wallet1.create_p2pk_lock(
            pubkey_wallet2, tags=Tags([["pubkeys", pubkey_wallet1]]), n_sigs=2
        )
    
        _, send_proofs = await wallet2.swap_to_send(
            wallet2.proofs, 1, secret_lock=secret_lock
        )
        assert len(send_proofs) == 1
        proof = send_proofs[0]
        # create coincurve private key so we can sign the message
        coincurve_privatekey2 = CoincurvePrivateKey(
            bytes.fromhex(wallet2.private_key.to_hex())
        )
        # check if private keys are the same
        assert coincurve_privatekey2.to_hex() == wallet2.private_key.to_hex()
    
        msg = hashlib.sha256(proof.secret.encode("utf-8")).digest()
        coincurve_signature = coincurve_privatekey2.sign_schnorr(msg)
    
        # add signatures of wallet2 – this is a duplicate signature
>       send_proofs = wallet2.sign_p2pk_sig_inputs(send_proofs)

tests/wallet/test_wallet_p2pk.py:429: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
cashu/wallet/p2pk.py:311: in sign_p2pk_sig_inputs
    p2pk_signatures = self.signatures_proofs_sig_inputs(our_pubkey_proofs)
cashu/wallet/p2pk.py:97: in signatures_proofs_sig_inputs
    signatures = [
cashu/wallet/p2pk.py:98: in <listcomp>
    schnorr_sign(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

message = b'["P2PK", {"data": "997f2153b227e5e35c2de9e68d4c4ffeb077daa10a73207bb66772f09249255ddf5a0ad175fd332d11e2d093384e9be30..., "03d7c9be08db9465125e5c89fd2d19af5ebb186a1051030bd3040bc547280a97bc"], ["sigflag", "SIG_INPUTS"], ["n_sigs", "2"]]}]'
private_key = <cashu.core.crypto.bls.PrivateKey object at 0x7f2cf5ad94b0>

    def schnorr_sign(message: bytes, private_key: SecpPrivateKey) -> bytes:
>       signature = private_key.sign_schnorr(
            hashlib.sha256(message).digest(),
            None,  # type: ignore
        )
E       AttributeError: 'PrivateKey' object has no attribute 'sign_schnorr'

cashu/core/p2pk.py:56: AttributeError
tests.wallet.test_wallet_htlc::test_htlc_hashlock_path_valid_after_locktime
Stack Traces | 1.62s run time
wallet1 = <cashu.wallet.wallet.Wallet object at 0x7f2cf8d584c0>
wallet2 = <cashu.wallet.wallet.Wallet object at 0x7f2cf65163b0>

    @pytest.mark.asyncio
    async def test_htlc_hashlock_path_valid_after_locktime(
        wallet1: Wallet, wallet2: Wallet
    ):
        """Ensure the preimage path continues to work even after the locktime expires."""
        mint_quote = await wallet1.request_mint(64)
        await pay_if_regtest(mint_quote.request)
        await wallet1.mint(64, quote_id=mint_quote.quote)
    
        preimage = "0000000000000000000000000000000000000000000000000000000000000000"
        pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
    
        # locktime_seconds negative => locktime already in the past
        secret = await wallet1.create_htlc_lock(
            preimage=preimage,
            hashlock_pubkeys=[pubkey_wallet2],
            locktime_seconds=-5,
        )
    
        _, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret)
    
>       signatures = wallet2.signatures_proofs_sig_inputs(send_proofs)

tests/wallet/test_wallet_htlc.py:373: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
cashu/wallet/p2pk.py:97: in signatures_proofs_sig_inputs
    signatures = [
cashu/wallet/p2pk.py:98: in <listcomp>
    schnorr_sign(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

message = b'["HTLC", {"data": "66687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925", "nonce": "3066f6a2e7fa2b4f6ee1...cca6ef3f83b697fa60fc9342fe5ab85b8928b373da32855cdd3ed03fc92f4984120af74f7dfe41074e04a8510142edc56d18b77ad36e5db35"]]}]'
private_key = <cashu.core.crypto.bls.PrivateKey object at 0x7f2cfc423670>

    def schnorr_sign(message: bytes, private_key: SecpPrivateKey) -> bytes:
>       signature = private_key.sign_schnorr(
            hashlib.sha256(message).digest(),
            None,  # type: ignore
        )
E       AttributeError: 'PrivateKey' object has no attribute 'sign_schnorr'

cashu/core/p2pk.py:56: AttributeError
tests.wallet.test_wallet_htlc::test_htlc_redeem_with_2_of_3_signatures_with_2_valid_and_1_invalid_provided
Stack Traces | 1.63s run time
wallet1 = <cashu.wallet.wallet.Wallet object at 0x7f2cf5a56b90>
wallet2 = <cashu.wallet.wallet.Wallet object at 0x7f2cf58c9bd0>

    @pytest.mark.asyncio
    async def test_htlc_redeem_with_2_of_3_signatures_with_2_valid_and_1_invalid_provided(
        wallet1: Wallet, wallet2: Wallet
    ):
        mint_quote = await wallet1.request_mint(64)
        await pay_if_regtest(mint_quote.request)
        await wallet1.mint(64, quote_id=mint_quote.quote)
        preimage = "0000000000000000000000000000000000000000000000000000000000000000"
        pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
        pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
        privatekey_wallet3 = PrivateKey(secrets.token_bytes(32))
        assert privatekey_wallet3.public_key
        pubkey_wallet3 = privatekey_wallet3.public_key.format().hex()
    
        # preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
        secret = await wallet1.create_htlc_lock(
            preimage=preimage,
            hashlock_pubkeys=[pubkey_wallet1, pubkey_wallet2, pubkey_wallet3],
            hashlock_n_sigs=2,
        )
        _, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret)
    
        signatures1 = wallet1.signatures_proofs_sig_inputs(send_proofs)
>       signatures2 = wallet2.signatures_proofs_sig_inputs(send_proofs)

tests/wallet/test_wallet_htlc.py:310: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
cashu/wallet/p2pk.py:97: in signatures_proofs_sig_inputs
    signatures = [
cashu/wallet/p2pk.py:98: in <listcomp>
    schnorr_sign(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

message = b'["HTLC", {"data": "66687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925", "nonce": "83e99dc64bbc4830f105...00ec7be712666c947bf076b6d7e6820331e426cce86e2f426745333eeb89fe3d958fdf91273e29e10e33a05a73ed48aa"], ["n_sigs", "2"]]}]'
private_key = <cashu.core.crypto.bls.PrivateKey object at 0x7f2cf5897bb0>

    def schnorr_sign(message: bytes, private_key: SecpPrivateKey) -> bytes:
>       signature = private_key.sign_schnorr(
            hashlib.sha256(message).digest(),
            None,  # type: ignore
        )
E       AttributeError: 'PrivateKey' object has no attribute 'sign_schnorr'

cashu/core/p2pk.py:56: AttributeError
tests.wallet.test_wallet_p2pk::test_p2pk
Stack Traces | 1.63s run time
wallet1 = <cashu.wallet.wallet.Wallet object at 0x7f2cf9f1da20>
wallet2 = <cashu.wallet.wallet.Wallet object at 0x7f2cf57ba410>

    @pytest.mark.asyncio
    async def test_p2pk(wallet1: Wallet, wallet2: Wallet):
        mint_quote = await wallet1.request_mint(64)
        await pay_if_regtest(mint_quote.request)
        await wallet1.mint(64, quote_id=mint_quote.quote)
        pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
        # p2pk test
        secret_lock = await wallet1.create_p2pk_lock(pubkey_wallet2)  # sender side
        _, send_proofs = await wallet1.swap_to_send(
            wallet1.proofs, 8, secret_lock=secret_lock
        )
>       await wallet2.redeem(send_proofs)

tests/wallet/test_wallet_p2pk.py:84: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
cashu/wallet/wallet.py:656: in redeem
    return await self.split(proofs=proofs, amount=0)
cashu/wallet/wallet.py:722: in split
    proofs = self.sign_proofs_inplace_swap(proofs, outputs)
cashu/wallet/p2pk.py:184: in sign_proofs_inplace_swap
    proofs = self.add_witnesses_sig_inputs(proofs)
cashu/wallet/p2pk.py:330: in add_witnesses_sig_inputs
    signed_proofs = self.sign_p2pk_sig_inputs(proofs)
cashu/wallet/p2pk.py:311: in sign_p2pk_sig_inputs
    p2pk_signatures = self.signatures_proofs_sig_inputs(our_pubkey_proofs)
cashu/wallet/p2pk.py:97: in signatures_proofs_sig_inputs
    signatures = [
cashu/wallet/p2pk.py:98: in <listcomp>
    schnorr_sign(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

message = b'["P2PK", {"data": "8f7462bbf81ea6233f9eaf981181a983afbd30cddf978d35c6d885c7823fde1b043b798c3c42fb4fad1da972f03cf38f1...fec13db76c5d29636cdb9281d558e074f", "nonce": "3a1f339bcbb811574d12fc3334acdaa2", "tags": [["sigflag", "SIG_INPUTS"]]}]'
private_key = <cashu.core.crypto.bls.PrivateKey object at 0x7f2cf57b81f0>

    def schnorr_sign(message: bytes, private_key: SecpPrivateKey) -> bytes:
>       signature = private_key.sign_schnorr(
            hashlib.sha256(message).digest(),
            None,  # type: ignore
        )
E       AttributeError: 'PrivateKey' object has no attribute 'sign_schnorr'

cashu/core/p2pk.py:56: AttributeError
tests.wallet.test_wallet_p2pk::test_p2pk_multisig_duplicate_signature
Stack Traces | 1.63s run time
wallet1 = <cashu.wallet.wallet.Wallet object at 0x7f2cfa6a9ed0>
wallet2 = <cashu.wallet.wallet.Wallet object at 0x7f2cf67e6a70>

    @pytest.mark.asyncio
    async def test_p2pk_multisig_duplicate_signature(wallet1: Wallet, wallet2: Wallet):
        mint_quote = await wallet1.request_mint(64)
        await pay_if_regtest(mint_quote.request)
        await wallet1.mint(64, quote_id=mint_quote.quote)
        pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
        pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
        assert pubkey_wallet1 != pubkey_wallet2
        # p2pk test
        secret_lock = await wallet1.create_p2pk_lock(
            pubkey_wallet2, tags=Tags([["pubkeys", pubkey_wallet1]]), n_sigs=2
        )
    
        _, send_proofs = await wallet1.swap_to_send(
            wallet1.proofs, 8, secret_lock=secret_lock
        )
        # add signatures of wallet2 – this is a duplicate signature
>       send_proofs = wallet2.sign_p2pk_sig_inputs(send_proofs)

tests/wallet/test_wallet_p2pk.py:349: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
cashu/wallet/p2pk.py:311: in sign_p2pk_sig_inputs
    p2pk_signatures = self.signatures_proofs_sig_inputs(our_pubkey_proofs)
cashu/wallet/p2pk.py:97: in signatures_proofs_sig_inputs
    signatures = [
cashu/wallet/p2pk.py:98: in <listcomp>
    schnorr_sign(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

message = b'["P2PK", {"data": "959e19a52a790a993c334225a506a9f5133728b76be8f49ff1ef97ac2a15ebd173af83c6cd44ed87501c312a8fdf08e90..., "03d7c9be08db9465125e5c89fd2d19af5ebb186a1051030bd3040bc547280a97bc"], ["sigflag", "SIG_INPUTS"], ["n_sigs", "2"]]}]'
private_key = <cashu.core.crypto.bls.PrivateKey object at 0x7f2cf5994280>

    def schnorr_sign(message: bytes, private_key: SecpPrivateKey) -> bytes:
>       signature = private_key.sign_schnorr(
            hashlib.sha256(message).digest(),
            None,  # type: ignore
        )
E       AttributeError: 'PrivateKey' object has no attribute 'sign_schnorr'

cashu/core/p2pk.py:56: AttributeError
tests.wallet.test_wallet_p2pk::test_p2pk_multisig_quorum_not_met_1_of_2
Stack Traces | 1.64s run time
f = <coroutine object Wallet.redeem at 0x7f2cf87169d0>
msg = 'Mint Error: not enough pubkeys (2) or signatures (1) present for n_sigs (2)'

    async def assert_err(f, msg):
        """Compute f() and expect an error message 'msg'."""
        try:
>           await f

tests/wallet/test_wallet_p2pk.py:30: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
cashu/wallet/wallet.py:656: in redeem
    return await self.split(proofs=proofs, amount=0)
cashu/wallet/wallet.py:722: in split
    proofs = self.sign_proofs_inplace_swap(proofs, outputs)
cashu/wallet/p2pk.py:184: in sign_proofs_inplace_swap
    proofs = self.add_witnesses_sig_inputs(proofs)
cashu/wallet/p2pk.py:330: in add_witnesses_sig_inputs
    signed_proofs = self.sign_p2pk_sig_inputs(proofs)
cashu/wallet/p2pk.py:311: in sign_p2pk_sig_inputs
    p2pk_signatures = self.signatures_proofs_sig_inputs(our_pubkey_proofs)
cashu/wallet/p2pk.py:97: in signatures_proofs_sig_inputs
    signatures = [
cashu/wallet/p2pk.py:98: in <listcomp>
    schnorr_sign(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

message = b'["P2PK", {"data": "aef005016da5a34c66598de849348fb589ace85bd1a72458fe5dbe0d74110d4afa91a2fbe90231538ab716d9796a892b0..., "03d7c9be08db9465125e5c89fd2d19af5ebb186a1051030bd3040bc547280a97bc"], ["sigflag", "SIG_INPUTS"], ["n_sigs", "2"]]}]'
private_key = <cashu.core.crypto.bls.PrivateKey object at 0x7f2cf8d3ed10>

    def schnorr_sign(message: bytes, private_key: SecpPrivateKey) -> bytes:
>       signature = private_key.sign_schnorr(
            hashlib.sha256(message).digest(),
            None,  # type: ignore
        )
E       AttributeError: 'PrivateKey' object has no attribute 'sign_schnorr'

cashu/core/p2pk.py:56: AttributeError

During handling of the above exception, another exception occurred:

wallet1 = <cashu.wallet.wallet.Wallet object at 0x7f2cfab236d0>
wallet2 = <cashu.wallet.wallet.Wallet object at 0x7f2cf58c8b80>

    @pytest.mark.asyncio
    async def test_p2pk_multisig_quorum_not_met_1_of_2(wallet1: Wallet, wallet2: Wallet):
        mint_quote = await wallet1.request_mint(64)
        await pay_if_regtest(mint_quote.request)
        await wallet1.mint(64, quote_id=mint_quote.quote)
        pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
        pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
        assert pubkey_wallet1 != pubkey_wallet2
        # p2pk test
        secret_lock = await wallet1.create_p2pk_lock(
            pubkey_wallet2, tags=Tags([["pubkeys", pubkey_wallet1]]), n_sigs=2
        )
        _, send_proofs = await wallet1.swap_to_send(
            wallet1.proofs, 8, secret_lock=secret_lock
        )
>       await assert_err(
            wallet2.redeem(send_proofs),
            "Mint Error: not enough pubkeys (2) or signatures (1) present for n_sigs (2)",
        )

tests/wallet/test_wallet_p2pk.py:466: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

f = <coroutine object Wallet.redeem at 0x7f2cf87169d0>
msg = 'Mint Error: not enough pubkeys (2) or signatures (1) present for n_sigs (2)'

    async def assert_err(f, msg):
        """Compute f() and expect an error message 'msg'."""
        try:
            await f
        except Exception as exc:
            if msg not in str(exc.args[0]):
>               raise Exception(f"Expected error: {msg}, got: {exc.args[0]}")
E               Exception: Expected error: Mint Error: not enough pubkeys (2) or signatures (1) present for n_sigs (2), got: 'PrivateKey' object has no attribute 'sign_schnorr'

tests/wallet/test_wallet_p2pk.py:33: Exception
tests.wallet.test_wallet_p2pk::test_p2pk_multisig_2_of_2
Stack Traces | 1.64s run time
wallet1 = <cashu.wallet.wallet.Wallet object at 0x7f2cfa6a97e0>
wallet2 = <cashu.wallet.wallet.Wallet object at 0x7f2cf6528670>

    @pytest.mark.asyncio
    async def test_p2pk_multisig_2_of_2(wallet1: Wallet, wallet2: Wallet):
        mint_quote = await wallet1.request_mint(64)
        await pay_if_regtest(mint_quote.request)
        await wallet1.mint(64, quote_id=mint_quote.quote)
        pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
        pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
        assert pubkey_wallet1 != pubkey_wallet2
        # p2pk test
        secret_lock = await wallet1.create_p2pk_lock(
            pubkey_wallet2, tags=Tags([["pubkeys", pubkey_wallet1]]), n_sigs=2
        )
    
        _, send_proofs = await wallet1.swap_to_send(
            wallet1.proofs, 8, secret_lock=secret_lock
        )
        # add signatures of wallet1
        send_proofs = wallet1.sign_p2pk_sig_inputs(send_proofs)
        # here we add the signatures of wallet2
>       await wallet2.redeem(send_proofs)

tests/wallet/test_wallet_p2pk.py:329: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
cashu/wallet/wallet.py:656: in redeem
    return await self.split(proofs=proofs, amount=0)
cashu/wallet/wallet.py:722: in split
    proofs = self.sign_proofs_inplace_swap(proofs, outputs)
cashu/wallet/p2pk.py:184: in sign_proofs_inplace_swap
    proofs = self.add_witnesses_sig_inputs(proofs)
cashu/wallet/p2pk.py:330: in add_witnesses_sig_inputs
    signed_proofs = self.sign_p2pk_sig_inputs(proofs)
cashu/wallet/p2pk.py:311: in sign_p2pk_sig_inputs
    p2pk_signatures = self.signatures_proofs_sig_inputs(our_pubkey_proofs)
cashu/wallet/p2pk.py:97: in signatures_proofs_sig_inputs
    signatures = [
cashu/wallet/p2pk.py:98: in <listcomp>
    schnorr_sign(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

message = b'["P2PK", {"data": "84c2dece659b94395b354816cef7e9c0e154bf16358ff3bef5a08c42df10709e946957197cb8ae628f2d6c83834374391..., "03d7c9be08db9465125e5c89fd2d19af5ebb186a1051030bd3040bc547280a97bc"], ["sigflag", "SIG_INPUTS"], ["n_sigs", "2"]]}]'
private_key = <cashu.core.crypto.bls.PrivateKey object at 0x7f2cfa6aa260>

    def schnorr_sign(message: bytes, private_key: SecpPrivateKey) -> bytes:
>       signature = private_key.sign_schnorr(
            hashlib.sha256(message).digest(),
            None,  # type: ignore
        )
E       AttributeError: 'PrivateKey' object has no attribute 'sign_schnorr'

cashu/core/p2pk.py:56: AttributeError
tests.wallet.test_wallet_htlc::test_htlc_redeem_with_2_of_2_signatures_with_duplicate_pubkeys
Stack Traces | 1.65s run time
wallet1 = <cashu.wallet.wallet.Wallet object at 0x7f2cf6528ac0>
wallet2 = <cashu.wallet.wallet.Wallet object at 0x7f2cf5997580>

    @pytest.mark.asyncio
    async def test_htlc_redeem_with_2_of_2_signatures_with_duplicate_pubkeys(
        wallet1: Wallet, wallet2: Wallet
    ):
        mint_quote = await wallet1.request_mint(64)
        await pay_if_regtest(mint_quote.request)
        await wallet1.mint(64, quote_id=mint_quote.quote)
        preimage = "0000000000000000000000000000000000000000000000000000000000000000"
        pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
        pubkey_wallet2 = pubkey_wallet1
        # preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
        secret = await wallet1.create_htlc_lock(
            preimage=preimage,
            hashlock_pubkeys=[pubkey_wallet1, pubkey_wallet2],
            hashlock_n_sigs=2,
        )
        _, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret)
    
        signatures1 = wallet1.signatures_proofs_sig_inputs(send_proofs)
>       signatures2 = wallet2.signatures_proofs_sig_inputs(send_proofs)

tests/wallet/test_wallet_htlc.py:248: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
cashu/wallet/p2pk.py:97: in signatures_proofs_sig_inputs
    signatures = [
cashu/wallet/p2pk.py:98: in <listcomp>
    schnorr_sign(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

message = b'["HTLC", {"data": "66687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925", "nonce": "d114fd01f2e78ac2abcc...1051030bd3040bc547280a97bc", "03d7c9be08db9465125e5c89fd2d19af5ebb186a1051030bd3040bc547280a97bc"], ["n_sigs", "2"]]}]'
private_key = <cashu.core.crypto.bls.PrivateKey object at 0x7f2cf5995b10>

    def schnorr_sign(message: bytes, private_key: SecpPrivateKey) -> bytes:
>       signature = private_key.sign_schnorr(
            hashlib.sha256(message).digest(),
            None,  # type: ignore
        )
E       AttributeError: 'PrivateKey' object has no attribute 'sign_schnorr'

cashu/core/p2pk.py:56: AttributeError
tests.wallet.test_wallet_p2pk::test_p2pk_multisig_quorum_not_met_2_of_3
Stack Traces | 1.65s run time
f = <coroutine object Wallet.redeem at 0x7f2cf45fa7a0>
msg = 'Mint Error: not enough pubkeys (3) or signatures (2) present for n_sigs (3)'

    async def assert_err(f, msg):
        """Compute f() and expect an error message 'msg'."""
        try:
>           await f

tests/wallet/test_wallet_p2pk.py:30: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
cashu/wallet/wallet.py:656: in redeem
    return await self.split(proofs=proofs, amount=0)
cashu/wallet/wallet.py:722: in split
    proofs = self.sign_proofs_inplace_swap(proofs, outputs)
cashu/wallet/p2pk.py:184: in sign_proofs_inplace_swap
    proofs = self.add_witnesses_sig_inputs(proofs)
cashu/wallet/p2pk.py:330: in add_witnesses_sig_inputs
    signed_proofs = self.sign_p2pk_sig_inputs(proofs)
cashu/wallet/p2pk.py:311: in sign_p2pk_sig_inputs
    p2pk_signatures = self.signatures_proofs_sig_inputs(our_pubkey_proofs)
cashu/wallet/p2pk.py:97: in signatures_proofs_sig_inputs
    signatures = [
cashu/wallet/p2pk.py:98: in <listcomp>
    schnorr_sign(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

message = b'["P2PK", {"data": "b5e842b3ad14da87402f2fb13f78e16cb8a39826d9194fa1688658dbc9dd0e57cd0c471e9e6203afb237b2112dcfb3a40...7c8e0a89cef1a990cbe7aed44b01a6a1f4fbd13577b7bc52312738e997fd486e04734"], ["sigflag", "SIG_INPUTS"], ["n_sigs", "3"]]}]'
private_key = <cashu.core.crypto.bls.PrivateKey object at 0x7f2cf8807280>

    def schnorr_sign(message: bytes, private_key: SecpPrivateKey) -> bytes:
>       signature = private_key.sign_schnorr(
            hashlib.sha256(message).digest(),
            None,  # type: ignore
        )
E       AttributeError: 'PrivateKey' object has no attribute 'sign_schnorr'

cashu/core/p2pk.py:56: AttributeError

During handling of the above exception, another exception occurred:

wallet1 = <cashu.wallet.wallet.Wallet object at 0x7f2cf875f250>
wallet2 = <cashu.wallet.wallet.Wallet object at 0x7f2cf65779a0>

    @pytest.mark.asyncio
    async def test_p2pk_multisig_quorum_not_met_2_of_3(wallet1: Wallet, wallet2: Wallet):
        mint_quote = await wallet1.request_mint(64)
        await pay_if_regtest(mint_quote.request)
        await wallet1.mint(64, quote_id=mint_quote.quote)
        pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
        pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
        garbage_priv = PrivateKey(secrets.token_bytes(32))
        garbage_pubkey = garbage_priv.public_key
        assert garbage_pubkey
        assert pubkey_wallet1 != pubkey_wallet2
        # p2pk test
        secret_lock = await wallet1.create_p2pk_lock(
            pubkey_wallet2,
            tags=Tags([["pubkeys", pubkey_wallet1, garbage_pubkey.format().hex()]]),
            n_sigs=3,
        )
        # create locked proofs
        _, send_proofs = await wallet1.swap_to_send(
            wallet1.proofs, 8, secret_lock=secret_lock
        )
        # add signatures of wallet1
        send_proofs = wallet1.sign_p2pk_sig_inputs(send_proofs)
        # here we add the signatures of wallet2
>       await assert_err(
            wallet2.redeem(send_proofs),
            "Mint Error: not enough pubkeys (3) or signatures (2) present for n_sigs (3)",
        )

tests/wallet/test_wallet_p2pk.py:496: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

f = <coroutine object Wallet.redeem at 0x7f2cf45fa7a0>
msg = 'Mint Error: not enough pubkeys (3) or signatures (2) present for n_sigs (3)'

    async def assert_err(f, msg):
        """Compute f() and expect an error message 'msg'."""
        try:
            await f
        except Exception as exc:
            if msg not in str(exc.args[0]):
>               raise Exception(f"Expected error: {msg}, got: {exc.args[0]}")
E               Exception: Expected error: Mint Error: not enough pubkeys (3) or signatures (2) present for n_sigs (3), got: 'PrivateKey' object has no attribute 'sign_schnorr'

tests/wallet/test_wallet_p2pk.py:33: Exception
tests.wallet.test_wallet_p2pk::test_p2pk_multisig_with_wrong_first_private_key
Stack Traces | 1.66s run time
f = <coroutine object Wallet.redeem at 0x7f2cf45e6ce0>
msg = 'Mint Error: not enough pubkeys (2) or signatures (1) present for n_sigs (2).'

    async def assert_err(f, msg):
        """Compute f() and expect an error message 'msg'."""
        try:
>           await f

tests/wallet/test_wallet_p2pk.py:30: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
cashu/wallet/wallet.py:656: in redeem
    return await self.split(proofs=proofs, amount=0)
cashu/wallet/wallet.py:722: in split
    proofs = self.sign_proofs_inplace_swap(proofs, outputs)
cashu/wallet/p2pk.py:184: in sign_proofs_inplace_swap
    proofs = self.add_witnesses_sig_inputs(proofs)
cashu/wallet/p2pk.py:330: in add_witnesses_sig_inputs
    signed_proofs = self.sign_p2pk_sig_inputs(proofs)
cashu/wallet/p2pk.py:311: in sign_p2pk_sig_inputs
    p2pk_signatures = self.signatures_proofs_sig_inputs(our_pubkey_proofs)
cashu/wallet/p2pk.py:97: in signatures_proofs_sig_inputs
    signatures = [
cashu/wallet/p2pk.py:98: in <listcomp>
    schnorr_sign(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

message = b'["P2PK", {"data": "82e1d45759575ad0759a91a20d9b4cc9ad6488931dfc9f3611cada8be507d62f5f2aa30357a66f81b459e01d50f7d8790...d869d481842f9d60a71745ee8b6597246f0e5e4aba7451cd4d6b1b314745477b70dfc"], ["sigflag", "SIG_INPUTS"], ["n_sigs", "2"]]}]'
private_key = <cashu.core.crypto.bls.PrivateKey object at 0x7f2cf8ccdd50>

    def schnorr_sign(message: bytes, private_key: SecpPrivateKey) -> bytes:
>       signature = private_key.sign_schnorr(
            hashlib.sha256(message).digest(),
            None,  # type: ignore
        )
E       AttributeError: 'PrivateKey' object has no attribute 'sign_schnorr'

cashu/core/p2pk.py:56: AttributeError

During handling of the above exception, another exception occurred:

wallet1 = <cashu.wallet.wallet.Wallet object at 0x7f2cf59fcee0>
wallet2 = <cashu.wallet.wallet.Wallet object at 0x7f2cf5996fe0>

    @pytest.mark.asyncio
    async def test_p2pk_multisig_with_wrong_first_private_key(
        wallet1: Wallet, wallet2: Wallet
    ):
        mint_quote = await wallet1.request_mint(64)
        await pay_if_regtest(mint_quote.request)
        await wallet1.mint(64, quote_id=mint_quote.quote)
        await wallet1.create_p2pk_pubkey()
        pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
        wrong_pubklic_key = PrivateKey().public_key
        assert wrong_pubklic_key
        wrong_public_key_hex = wrong_pubklic_key.format().hex()
    
        assert wrong_public_key_hex != pubkey_wallet2
    
        # p2pk test
        secret_lock = await wallet1.create_p2pk_lock(
            pubkey_wallet2, tags=Tags([["pubkeys", wrong_public_key_hex]]), n_sigs=2
        )
        _, send_proofs = await wallet1.swap_to_send(
            wallet1.proofs, 8, secret_lock=secret_lock
        )
>       await assert_err(
            wallet2.redeem(send_proofs),
            "Mint Error: not enough pubkeys (2) or signatures (1) present for n_sigs (2).",
        )

tests/wallet/test_wallet_p2pk.py:540: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

f = <coroutine object Wallet.redeem at 0x7f2cf45e6ce0>
msg = 'Mint Error: not enough pubkeys (2) or signatures (1) present for n_sigs (2).'

    async def assert_err(f, msg):
        """Compute f() and expect an error message 'msg'."""
        try:
            await f
        except Exception as exc:
            if msg not in str(exc.args[0]):
>               raise Exception(f"Expected error: {msg}, got: {exc.args[0]}")
E               Exception: Expected error: Mint Error: not enough pubkeys (2) or signatures (1) present for n_sigs (2)., got: 'PrivateKey' object has no attribute 'sign_schnorr'

tests/wallet/test_wallet_p2pk.py:33: Exception
tests.wallet.test_wallet_p2pk::test_p2pk_multisig_with_duplicate_publickey
Stack Traces | 1.66s run time
f = <coroutine object Wallet.redeem at 0x7f2cf8716ea0>
msg = 'Mint Error: pubkeys must be unique.'

    async def assert_err(f, msg):
        """Compute f() and expect an error message 'msg'."""
        try:
>           await f

tests/wallet/test_wallet_p2pk.py:30: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
cashu/wallet/wallet.py:656: in redeem
    return await self.split(proofs=proofs, amount=0)
cashu/wallet/wallet.py:722: in split
    proofs = self.sign_proofs_inplace_swap(proofs, outputs)
cashu/wallet/p2pk.py:184: in sign_proofs_inplace_swap
    proofs = self.add_witnesses_sig_inputs(proofs)
cashu/wallet/p2pk.py:330: in add_witnesses_sig_inputs
    signed_proofs = self.sign_p2pk_sig_inputs(proofs)
cashu/wallet/p2pk.py:311: in sign_p2pk_sig_inputs
    p2pk_signatures = self.signatures_proofs_sig_inputs(our_pubkey_proofs)
cashu/wallet/p2pk.py:97: in signatures_proofs_sig_inputs
    signatures = [
cashu/wallet/p2pk.py:98: in <listcomp>
    schnorr_sign(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

message = b'["P2PK", {"data": "b81cdaa1b5c586ec3689e5819ed8b368c240cf3af323dde843e2b8723d2066b59c3c8dd0d685f710399dbc0cbfc762c41...b68f410544a29b1443e754d99bbaeee2f316e24e07d625beef1675a9b0342cfe33cd5"], ["sigflag", "SIG_INPUTS"], ["n_sigs", "2"]]}]'
private_key = <cashu.core.crypto.bls.PrivateKey object at 0x7f2cf6575c00>

    def schnorr_sign(message: bytes, private_key: SecpPrivateKey) -> bytes:
>       signature = private_key.sign_schnorr(
            hashlib.sha256(message).digest(),
            None,  # type: ignore
        )
E       AttributeError: 'PrivateKey' object has no attribute 'sign_schnorr'

cashu/core/p2pk.py:56: AttributeError

During handling of the above exception, another exception occurred:

wallet1 = <cashu.wallet.wallet.Wallet object at 0x7f2cf6574c40>
wallet2 = <cashu.wallet.wallet.Wallet object at 0x7f2cf5b43b80>

    @pytest.mark.asyncio
    async def test_p2pk_multisig_with_duplicate_publickey(wallet1: Wallet, wallet2: Wallet):
        mint_quote = await wallet1.request_mint(64)
        await pay_if_regtest(mint_quote.request)
        await wallet1.mint(64, quote_id=mint_quote.quote)
        pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
        # p2pk test
        secret_lock = await wallet1.create_p2pk_lock(
            pubkey_wallet2, tags=Tags([["pubkeys", pubkey_wallet2]]), n_sigs=2
        )
        _, send_proofs = await wallet1.swap_to_send(
            wallet1.proofs, 8, secret_lock=secret_lock
        )
>       await assert_err(wallet2.redeem(send_proofs), "Mint Error: pubkeys must be unique.")

tests/wallet/test_wallet_p2pk.py:515: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

f = <coroutine object Wallet.redeem at 0x7f2cf8716ea0>
msg = 'Mint Error: pubkeys must be unique.'

    async def assert_err(f, msg):
        """Compute f() and expect an error message 'msg'."""
        try:
            await f
        except Exception as exc:
            if msg not in str(exc.args[0]):
>               raise Exception(f"Expected error: {msg}, got: {exc.args[0]}")
E               Exception: Expected error: Mint Error: pubkeys must be unique., got: 'PrivateKey' object has no attribute 'sign_schnorr'

tests/wallet/test_wallet_p2pk.py:33: Exception
tests.wallet.test_wallet_htlc::test_htlc_redeem_hashlock_wrong_signature_timelock_wrong_signature
Stack Traces | 1.66s run time
f = <coroutine object Wallet.redeem at 0x7f2cf875b920>
msg = 'Mint Error: signature threshold not met. 0 < 1.'

    async def assert_err(f, msg):
        """Compute f() and expect an error message 'msg'."""
        try:
>           await f

tests/wallet/test_wallet_htlc.py:27: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
cashu/wallet/wallet.py:656: in redeem
    return await self.split(proofs=proofs, amount=0)
cashu/wallet/wallet.py:731: in split
    sorted_promises = await super().split(proofs, list(sorted_outputs))
cashu/wallet/v1_api.py:93: in wrapper
    return await func(self, *args, **kwargs)
cashu/wallet/v1_api.py:106: in wrapper
    return await func(self, *args, **kwargs)
cashu/wallet/v1_api.py:592: in split
    self.raise_on_unsupported_version(resp, "POST /v1/swap")
cashu/wallet/v1_api.py:161: in raise_on_unsupported_version
    self.raise_on_error_request(resp)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

resp = <Response [400 Bad Request]>

    @staticmethod
    def raise_on_error_request(
        resp: Response,
    ) -> None:
        """Raises an exception if the response from the mint contains an error.
    
        Args:
            resp_dict (Response): Response dict (previously JSON) from mint
    
        Raises:
            Exception: if the response contains an error
        """
        try:
            resp_dict = resp.json()
        except json.JSONDecodeError:
            resp.raise_for_status()
            return
        if "detail" in resp_dict:
            logger.trace(f"Error from mint: {resp_dict}")
            error_message = f"Mint Error: {resp_dict['detail']}"
            if "code" in resp_dict:
                error_message += f" (Code: {resp_dict['code']})"
>           raise Exception(error_message)
E           Exception: Mint Error: The public key could not be parsed or is invalid. (Code: 0)

cashu/wallet/v1_api.py:147: Exception

During handling of the above exception, another exception occurred:

wallet1 = <cashu.wallet.wallet.Wallet object at 0x7f2cf5a63070>
wallet2 = <cashu.wallet.wallet.Wallet object at 0x7f2cf5996380>

    @pytest.mark.asyncio
    async def test_htlc_redeem_hashlock_wrong_signature_timelock_wrong_signature(
        wallet1: Wallet, wallet2: Wallet
    ):
        mint_quote = await wallet1.request_mint(64)
        await pay_if_regtest(mint_quote.request)
        await wallet1.mint(64, quote_id=mint_quote.quote)
        preimage = "0000000000000000000000000000000000000000000000000000000000000000"
        pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
        pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
        # preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
        secret = await wallet1.create_htlc_lock(
            preimage=preimage,
            hashlock_pubkeys=[pubkey_wallet2],
            locktime_seconds=2,
            locktime_pubkeys=[pubkey_wallet1, pubkey_wallet2],
            locktime_n_sigs=2,
        )
        _, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret)
    
        signatures = wallet1.signatures_proofs_sig_inputs(send_proofs)
        for p, s in zip(send_proofs, signatures):
            p.witness = HTLCWitness(
                preimage=preimage, signatures=[f"{s[:-5]}11111"]
            ).model_dump_json()  # wrong signature
    
        # should error because we used wallet2 signatures for the hash lock
>       await assert_err(
            wallet1.redeem(send_proofs),
            "Mint Error: signature threshold not met. 0 < 1.",
        )

tests/wallet/test_wallet_htlc.py:408: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

f = <coroutine object Wallet.redeem at 0x7f2cf875b920>
msg = 'Mint Error: signature threshold not met. 0 < 1.'

    async def assert_err(f, msg):
        """Compute f() and expect an error message 'msg'."""
        try:
            await f
        except Exception as exc:
            if msg not in str(exc.args[0]):
>               raise Exception(f"Expected error: {msg}, got: {exc.args[0]}")
E               Exception: Expected error: Mint Error: signature threshold not met. 0 < 1., got: Mint Error: The public key could not be parsed or is invalid. (Code: 0)

tests/wallet/test_wallet_htlc.py:30: Exception
tests.wallet.test_wallet_p2pk::test_p2pk_sig_all
Stack Traces | 1.67s run time
wallet1 = <cashu.wallet.wallet.Wallet object at 0x7f2cf5a6fd90>
wallet2 = <cashu.wallet.wallet.Wallet object at 0x7f2cf5a4b2e0>

    @pytest.mark.asyncio
    async def test_p2pk_sig_all(wallet1: Wallet, wallet2: Wallet):
        mint_quote = await wallet1.request_mint(64)
        await pay_if_regtest(mint_quote.request)
        await wallet1.mint(64, quote_id=mint_quote.quote)
        pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
        # p2pk test
        secret_lock = await wallet1.create_p2pk_lock(
            pubkey_wallet2, sig_all=True
        )  # sender side
        _, send_proofs = await wallet1.swap_to_send(
            wallet1.proofs, 8, secret_lock=secret_lock
        )
>       await wallet2.redeem(send_proofs)

tests/wallet/test_wallet_p2pk.py:110: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
cashu/wallet/wallet.py:656: in redeem
    return await self.split(proofs=proofs, amount=0)
cashu/wallet/wallet.py:731: in split
    sorted_promises = await super().split(proofs, list(sorted_outputs))
cashu/wallet/v1_api.py:93: in wrapper
    return await func(self, *args, **kwargs)
cashu/wallet/v1_api.py:106: in wrapper
    return await func(self, *args, **kwargs)
cashu/wallet/v1_api.py:592: in split
    self.raise_on_unsupported_version(resp, "POST /v1/swap")
cashu/wallet/v1_api.py:161: in raise_on_unsupported_version
    self.raise_on_error_request(resp)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

resp = <Response [400 Bad Request]>

    @staticmethod
    def raise_on_error_request(
        resp: Response,
    ) -> None:
        """Raises an exception if the response from the mint contains an error.
    
        Args:
            resp_dict (Response): Response dict (previously JSON) from mint
    
        Raises:
            Exception: if the response contains an error
        """
        try:
            resp_dict = resp.json()
        except json.JSONDecodeError:
            resp.raise_for_status()
            return
        if "detail" in resp_dict:
            logger.trace(f"Error from mint: {resp_dict}")
            error_message = f"Mint Error: {resp_dict['detail']}"
            if "code" in resp_dict:
                error_message += f" (Code: {resp_dict['code']})"
>           raise Exception(error_message)
E           Exception: Mint Error: no witness in proof. (Code: 11000)

cashu/wallet/v1_api.py:147: Exception
tests.wallet.test_wallet_htlc::test_htlc_redeem_hashlock_wrong_signature_timelock_correct_signature
Stack Traces | 1.67s run time
f = <coroutine object Wallet.redeem at 0x7f2cfbedf060>
msg = 'Mint Error: signature threshold not met'

    async def assert_err(f, msg):
        """Compute f() and expect an error message 'msg'."""
        try:
>           await f

tests/wallet/test_wallet_htlc.py:27: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
cashu/wallet/wallet.py:656: in redeem
    return await self.split(proofs=proofs, amount=0)
cashu/wallet/wallet.py:731: in split
    sorted_promises = await super().split(proofs, list(sorted_outputs))
cashu/wallet/v1_api.py:93: in wrapper
    return await func(self, *args, **kwargs)
cashu/wallet/v1_api.py:106: in wrapper
    return await func(self, *args, **kwargs)
cashu/wallet/v1_api.py:592: in split
    self.raise_on_unsupported_version(resp, "POST /v1/swap")
cashu/wallet/v1_api.py:161: in raise_on_unsupported_version
    self.raise_on_error_request(resp)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

resp = <Response [400 Bad Request]>

    @staticmethod
    def raise_on_error_request(
        resp: Response,
    ) -> None:
        """Raises an exception if the response from the mint contains an error.
    
        Args:
            resp_dict (Response): Response dict (previously JSON) from mint
    
        Raises:
            Exception: if the response contains an error
        """
        try:
            resp_dict = resp.json()
        except json.JSONDecodeError:
            resp.raise_for_status()
            return
        if "detail" in resp_dict:
            logger.trace(f"Error from mint: {resp_dict}")
            error_message = f"Mint Error: {resp_dict['detail']}"
            if "code" in resp_dict:
                error_message += f" (Code: {resp_dict['code']})"
>           raise Exception(error_message)
E           Exception: Mint Error: The public key could not be parsed or is invalid. (Code: 0)

cashu/wallet/v1_api.py:147: Exception

During handling of the above exception, another exception occurred:

wallet1 = <cashu.wallet.wallet.Wallet object at 0x7f2cf444f4c0>
wallet2 = <cashu.wallet.wallet.Wallet object at 0x7f2cf6537a00>

    @pytest.mark.asyncio
    async def test_htlc_redeem_hashlock_wrong_signature_timelock_correct_signature(
        wallet1: Wallet, wallet2: Wallet
    ):
        mint_quote = await wallet1.request_mint(64)
        await pay_if_regtest(mint_quote.request)
        await wallet1.mint(64, quote_id=mint_quote.quote)
        preimage = "0000000000000000000000000000000000000000000000000000000000000000"
        pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
        pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
        # preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
        secret = await wallet1.create_htlc_lock(
            preimage=preimage,
            hashlock_pubkeys=[pubkey_wallet2],
            locktime_seconds=2,
            locktime_pubkeys=[pubkey_wallet1],
        )
        _, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret)
    
        signatures = wallet1.signatures_proofs_sig_inputs(send_proofs)
        for p, s in zip(send_proofs, signatures):
            p.witness = HTLCWitness(preimage=preimage, signatures=[s]).model_dump_json()
    
        # should error because we used wallet2 signatures for the hash lock
>       await assert_err(
            wallet1.redeem(send_proofs),
            "Mint Error: signature threshold not met",
        )

tests/wallet/test_wallet_htlc.py:342: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

f = <coroutine object Wallet.redeem at 0x7f2cfbedf060>
msg = 'Mint Error: signature threshold not met'

    async def assert_err(f, msg):
        """Compute f() and expect an error message 'msg'."""
        try:
            await f
        except Exception as exc:
            if msg not in str(exc.args[0]):
>               raise Exception(f"Expected error: {msg}, got: {exc.args[0]}")
E               Exception: Expected error: Mint Error: signature threshold not met, got: Mint Error: The public key could not be parsed or is invalid. (Code: 0)

tests/wallet/test_wallet_htlc.py:30: Exception
tests.wallet.test_wallet_p2pk::test_p2pk_locktime_with_second_refund_pubkey
Stack Traces | 1.67s run time
f = <coroutine object Wallet.redeem at 0x7f2cf5a1af10>
msg = 'Mint Error: signature threshold not met. 0 < 1.'

    async def assert_err(f, msg):
        """Compute f() and expect an error message 'msg'."""
        try:
>           await f

tests/wallet/test_wallet_p2pk.py:30: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
cashu/wallet/wallet.py:656: in redeem
    return await self.split(proofs=proofs, amount=0)
cashu/wallet/wallet.py:731: in split
    sorted_promises = await super().split(proofs, list(sorted_outputs))
cashu/wallet/v1_api.py:93: in wrapper
    return await func(self, *args, **kwargs)
cashu/wallet/v1_api.py:106: in wrapper
    return await func(self, *args, **kwargs)
cashu/wallet/v1_api.py:592: in split
    self.raise_on_unsupported_version(resp, "POST /v1/swap")
cashu/wallet/v1_api.py:161: in raise_on_unsupported_version
    self.raise_on_error_request(resp)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

resp = <Response [400 Bad Request]>

    @staticmethod
    def raise_on_error_request(
        resp: Response,
    ) -> None:
        """Raises an exception if the response from the mint contains an error.
    
        Args:
            resp_dict (Response): Response dict (previously JSON) from mint
    
        Raises:
            Exception: if the response contains an error
        """
        try:
            resp_dict = resp.json()
        except json.JSONDecodeError:
            resp.raise_for_status()
            return
        if "detail" in resp_dict:
            logger.trace(f"Error from mint: {resp_dict}")
            error_message = f"Mint Error: {resp_dict['detail']}"
            if "code" in resp_dict:
                error_message += f" (Code: {resp_dict['code']})"
>           raise Exception(error_message)
E           Exception: Mint Error: The public key could not be parsed or is invalid. (Code: 0)

cashu/wallet/v1_api.py:147: Exception

During handling of the above exception, another exception occurred:

wallet1 = <cashu.wallet.wallet.Wallet object at 0x7f2cfab236d0>
wallet2 = <cashu.wallet.wallet.Wallet object at 0x7f2cf58976d0>

    @pytest.mark.asyncio
    async def test_p2pk_locktime_with_second_refund_pubkey(
        wallet1: Wallet, wallet2: Wallet
    ):
        mint_quote = await wallet1.request_mint(64)
        await pay_if_regtest(mint_quote.request)
        await wallet1.mint(64, quote_id=mint_quote.quote)
        pubkey_wallet1 = await wallet1.create_p2pk_pubkey()  # receiver side
        pubkey_wallet2 = await wallet2.create_p2pk_pubkey()  # receiver side
        # sender side
        garbage_priv = PrivateKey(secrets.token_bytes(32))
        garbage_pubkey = garbage_priv.public_key
        assert garbage_pubkey
        secret_lock = await wallet1.create_p2pk_lock(
            garbage_pubkey.format().hex(),  # create lock to unspendable pubkey
            locktime_seconds=2,  # locktime
            tags=Tags(
                [["refund", pubkey_wallet2, pubkey_wallet1]]
            ),  # multiple refund pubkeys
        )  # sender side
        _, send_proofs = await wallet1.swap_to_send(
            wallet1.proofs, 8, secret_lock=secret_lock
        )
        send_proofs_copy = copy.deepcopy(send_proofs)
        # receiver side: can't redeem since we used a garbage pubkey
        # and locktime has not passed
        # WALLET WILL ADD A SIGNATURE BECAUSE IT SEES ITS REFUND PUBKEY (it adds a signature even though the locktime hasn't passed)
>       await assert_err(
            wallet1.redeem(send_proofs),
            "Mint Error: signature threshold not met. 0 < 1.",
        )

tests/wallet/test_wallet_p2pk.py:252: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

f = <coroutine object Wallet.redeem at 0x7f2cf5a1af10>
msg = 'Mint Error: signature threshold not met. 0 < 1.'

    async def assert_err(f, msg):
        """Compute f() and expect an error message 'msg'."""
        try:
            await f
        except Exception as exc:
            if msg not in str(exc.args[0]):
>               raise Exception(f"Expected error: {msg}, got: {exc.args[0]}")
E               Exception: Expected error: Mint Error: signature threshold not met. 0 < 1., got: Mint Error: The public key could not be parsed or is invalid. (Code: 0)

tests/wallet/test_wallet_p2pk.py:33: Exception
tests.wallet.test_wallet_htlc::test_htlc_redeem_timelock_2_of_2_signatures
Stack Traces | 1.67s run time
f = <coroutine object Wallet.redeem at 0x7f2cf45e6f80>
msg = 'Mint Error: signature threshold not met. 0 < 1.'

    async def assert_err(f, msg):
        """Compute f() and expect an error message 'msg'."""
        try:
>           await f

tests/wallet/test_wallet_htlc.py:27: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
cashu/wallet/wallet.py:656: in redeem
    return await self.split(proofs=proofs, amount=0)
cashu/wallet/wallet.py:731: in split
    sorted_promises = await super().split(proofs, list(sorted_outputs))
cashu/wallet/v1_api.py:93: in wrapper
    return await func(self, *args, **kwargs)
cashu/wallet/v1_api.py:106: in wrapper
    return await func(self, *args, **kwargs)
cashu/wallet/v1_api.py:592: in split
    self.raise_on_unsupported_version(resp, "POST /v1/swap")
cashu/wallet/v1_api.py:161: in raise_on_unsupported_version
    self.raise_on_error_request(resp)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

resp = <Response [400 Bad Request]>

    @staticmethod
    def raise_on_error_request(
        resp: Response,
    ) -> None:
        """Raises an exception if the response from the mint contains an error.
    
        Args:
            resp_dict (Response): Response dict (previously JSON) from mint
    
        Raises:
            Exception: if the response contains an error
        """
        try:
            resp_dict = resp.json()
        except json.JSONDecodeError:
            resp.raise_for_status()
            return
        if "detail" in resp_dict:
            logger.trace(f"Error from mint: {resp_dict}")
            error_message = f"Mint Error: {resp_dict['detail']}"
            if "code" in resp_dict:
                error_message += f" (Code: {resp_dict['code']})"
>           raise Exception(error_message)
E           Exception: Mint Error: The public key could not be parsed or is invalid. (Code: 0)

cashu/wallet/v1_api.py:147: Exception

During handling of the above exception, another exception occurred:

wallet1 = <cashu.wallet.wallet.Wallet object at 0x7f2cf5a624a0>
wallet2 = <cashu.wallet.wallet.Wallet object at 0x7f2cf67e6f20>

    @pytest.mark.asyncio
    async def test_htlc_redeem_timelock_2_of_2_signatures(wallet1: Wallet, wallet2: Wallet):
        """Testing the 2-of-2 timelock (refund) signature case."""
        mint_quote = await wallet1.request_mint(64)
        await pay_if_regtest(mint_quote.request)
        await wallet1.mint(64, quote_id=mint_quote.quote)
        preimage = "0000000000000000000000000000000000000000000000000000000000000000"
        pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
        pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
        # preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
        secret = await wallet1.create_htlc_lock(
            preimage=preimage,
            hashlock_pubkeys=[pubkey_wallet2],
            locktime_seconds=2,
            locktime_pubkeys=[pubkey_wallet1, pubkey_wallet2],
            locktime_n_sigs=2,
        )
        _, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret)
        send_proofs_copy = send_proofs.copy()
    
        signatures = wallet1.signatures_proofs_sig_inputs(send_proofs)
        for p, s in zip(send_proofs, signatures):
            p.witness = HTLCWitness(preimage=preimage, signatures=[s]).model_dump_json()
    
        # should error because we used wallet2 signatures for the hash lock
>       await assert_err(
            wallet1.redeem(send_proofs),
            "Mint Error: signature threshold not met. 0 < 1.",
        )

tests/wallet/test_wallet_htlc.py:446: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

f = <coroutine object Wallet.redeem at 0x7f2cf45e6f80>
msg = 'Mint Error: signature threshold not met. 0 < 1.'

    async def assert_err(f, msg):
        """Compute f() and expect an error message 'msg'."""
        try:
            await f
        except Exception as exc:
            if msg not in str(exc.args[0]):
>               raise Exception(f"Expected error: {msg}, got: {exc.args[0]}")
E               Exception: Expected error: Mint Error: signature threshold not met. 0 < 1., got: Mint Error: The public key could not be parsed or is invalid. (Code: 0)

tests/wallet/test_wallet_htlc.py:30: Exception
tests.wallet.test_wallet_p2pk::test_p2pk_locktime_with_2_of_2_refund_pubkeys
Stack Traces | 1.68s run time
f = <coroutine object Wallet.redeem at 0x7f2cf87deb90>
msg = 'Mint Error: signature threshold not met. 0 < 1.'

    async def assert_err(f, msg):
        """Compute f() and expect an error message 'msg'."""
        try:
>           await f

tests/wallet/test_wallet_p2pk.py:30: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
cashu/wallet/wallet.py:656: in redeem
    return await self.split(proofs=proofs, amount=0)
cashu/wallet/wallet.py:731: in split
    sorted_promises = await super().split(proofs, list(sorted_outputs))
cashu/wallet/v1_api.py:93: in wrapper
    return await func(self, *args, **kwargs)
cashu/wallet/v1_api.py:106: in wrapper
    return await func(self, *args, **kwargs)
cashu/wallet/v1_api.py:592: in split
    self.raise_on_unsupported_version(resp, "POST /v1/swap")
cashu/wallet/v1_api.py:161: in raise_on_unsupported_version
    self.raise_on_error_request(resp)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

resp = <Response [400 Bad Request]>

    @staticmethod
    def raise_on_error_request(
        resp: Response,
    ) -> None:
        """Raises an exception if the response from the mint contains an error.
    
        Args:
            resp_dict (Response): Response dict (previously JSON) from mint
    
        Raises:
            Exception: if the response contains an error
        """
        try:
            resp_dict = resp.json()
        except json.JSONDecodeError:
            resp.raise_for_status()
            return
        if "detail" in resp_dict:
            logger.trace(f"Error from mint: {resp_dict}")
            error_message = f"Mint Error: {resp_dict['detail']}"
            if "code" in resp_dict:
                error_message += f" (Code: {resp_dict['code']})"
>           raise Exception(error_message)
E           Exception: Mint Error: The public key could not be parsed or is invalid. (Code: 0)

cashu/wallet/v1_api.py:147: Exception

During handling of the above exception, another exception occurred:

wallet1 = <cashu.wallet.wallet.Wallet object at 0x7f2cf5aceb90>
wallet2 = <cashu.wallet.wallet.Wallet object at 0x7f2cf863f9d0>

    @pytest.mark.asyncio
    async def test_p2pk_locktime_with_2_of_2_refund_pubkeys(
        wallet1: Wallet, wallet2: Wallet
    ):
        """Testing the case where we expect a 2-of-2 signature from the refund pubkeys"""
        mint_quote = await wallet1.request_mint(64)
        await pay_if_regtest(mint_quote.request)
        await wallet1.mint(64, quote_id=mint_quote.quote)
        pubkey_wallet1 = await wallet1.create_p2pk_pubkey()  # receiver side
        pubkey_wallet2 = await wallet2.create_p2pk_pubkey()  # receiver side
        # sender side
        garbage_priv = PrivateKey(secrets.token_bytes(32))
        garbage_pubkey = garbage_priv.public_key
        assert garbage_pubkey
        secret_lock = await wallet1.create_p2pk_lock(
            garbage_pubkey.format().hex(),  # create lock to unspendable pubkey
            locktime_seconds=2,  # locktime
            tags=Tags(
                [["refund", pubkey_wallet2, pubkey_wallet1], ["n_sigs_refund", "2"]],
            ),  # multiple refund pubkeys
        )  # sender side
        _, send_proofs = await wallet1.swap_to_send(
            wallet1.proofs, 8, secret_lock=secret_lock
        )
        # we need to copy the send_proofs because the redeem function
        # modifies the send_proofs in place by adding the signatures
        send_proofs_copy = copy.deepcopy(send_proofs)
        send_proofs_copy2 = copy.deepcopy(send_proofs)
        # receiver side: can't redeem since we used a garbage pubkey
        # and locktime has not passed
>       await assert_err(
            wallet1.redeem(send_proofs),
            "Mint Error: signature threshold not met. 0 < 1.",
        )

tests/wallet/test_wallet_p2pk.py:291: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

f = <coroutine object Wallet.redeem at 0x7f2cf87deb90>
msg = 'Mint Error: signature threshold not met. 0 < 1.'

    async def assert_err(f, msg):
        """Compute f() and expect an error message 'msg'."""
        try:
            await f
        except Exception as exc:
            if msg not in str(exc.args[0]):
>               raise Exception(f"Expected error: {msg}, got: {exc.args[0]}")
E               Exception: Expected error: Mint Error: signature threshold not met. 0 < 1., got: Mint Error: The public key could not be parsed or is invalid. (Code: 0)

tests/wallet/test_wallet_p2pk.py:33: Exception
tests.wallet.test_wallet_htlc::test_htlc_sigall_behavior
Stack Traces | 1.75s run time
f = <coroutine object Wallet.redeem at 0x7f2cf45e6110>
msg = 'Mint Error: no HTLC preimage provided'

    async def assert_err(f, msg):
        """Compute f() and expect an error message 'msg'."""
        try:
>           await f

tests/wallet/test_wallet_htlc.py:27: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
cashu/wallet/wallet.py:656: in redeem
    return await self.split(proofs=proofs, amount=0)
cashu/wallet/wallet.py:731: in split
    sorted_promises = await super().split(proofs, list(sorted_outputs))
cashu/wallet/v1_api.py:93: in wrapper
    return await func(self, *args, **kwargs)
cashu/wallet/v1_api.py:106: in wrapper
    return await func(self, *args, **kwargs)
cashu/wallet/v1_api.py:592: in split
    self.raise_on_unsupported_version(resp, "POST /v1/swap")
cashu/wallet/v1_api.py:161: in raise_on_unsupported_version
    self.raise_on_error_request(resp)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

resp = <Response [400 Bad Request]>

    @staticmethod
    def raise_on_error_request(
        resp: Response,
    ) -> None:
        """Raises an exception if the response from the mint contains an error.
    
        Args:
            resp_dict (Response): Response dict (previously JSON) from mint
    
        Raises:
            Exception: if the response contains an error
        """
        try:
            resp_dict = resp.json()
        except json.JSONDecodeError:
            resp.raise_for_status()
            return
        if "detail" in resp_dict:
            logger.trace(f"Error from mint: {resp_dict}")
            error_message = f"Mint Error: {resp_dict['detail']}"
            if "code" in resp_dict:
                error_message += f" (Code: {resp_dict['code']})"
>           raise Exception(error_message)
E           Exception: Mint Error: Witness is missing for htlc preimage (Code: 0)

cashu/wallet/v1_api.py:147: Exception

During handling of the above exception, another exception occurred:

wallet1 = <cashu.wallet.wallet.Wallet object at 0x7f2cf5a60820>
wallet2 = <cashu.wallet.wallet.Wallet object at 0x7f2cf88b2890>

    @pytest.mark.asyncio
    async def test_htlc_sigall_behavior(wallet1: Wallet, wallet2: Wallet):
        """Test HTLC with SIG_ALL flag, requiring signatures on both inputs and outputs."""
        # Mint tokens for testing
        mint_quote = await wallet1.request_mint(64)
        await pay_if_regtest(mint_quote.request)
        await wallet1.mint(64, quote_id=mint_quote.quote)
    
        # Setup HTLC parameters
        preimage = "0000000000000000000000000000000000000000000000000000000000000000"
        # preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
        pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
    
        # Create HTLC lock with SIG_ALL flag
        secret = await wallet1.create_htlc_lock(
            preimage=preimage,
            hashlock_pubkeys=[pubkey_wallet2],
            hashlock_n_sigs=1,
        )
    
        # Modify the secret to use SIG_ALL
        secret.tags["sigflag"] = SigFlags.SIG_ALL.value
    
        # Send tokens with this HTLC lock
        _, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret)
    
        # verify sigflag is SIG_ALL
        assert HTLCSecret.from_secret(secret).kind == SecretKind.HTLC.value
        assert HTLCSecret.from_secret(secret).sigflag == SigFlags.SIG_ALL
    
        # first redeem fails because no preimage
>       await assert_err(
            wallet2.redeem(send_proofs), "Mint Error: no HTLC preimage provided"
        )

tests/wallet/test_wallet_htlc.py:498: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

f = <coroutine object Wallet.redeem at 0x7f2cf45e6110>
msg = 'Mint Error: no HTLC preimage provided'

    async def assert_err(f, msg):
        """Compute f() and expect an error message 'msg'."""
        try:
            await f
        except Exception as exc:
            if msg not in str(exc.args[0]):
>               raise Exception(f"Expected error: {msg}, got: {exc.args[0]}")
E               Exception: Expected error: Mint Error: no HTLC preimage provided, got: Mint Error: Witness is missing for htlc preimage (Code: 0)

tests/wallet/test_wallet_htlc.py:30: Exception
tests.wallet.test_wallet_p2pk::test_p2pk_locktime_with_3_of_3_refund_pubkeys
Stack Traces | 2.11s run time
f = <coroutine object Wallet.redeem at 0x7f2cf57a30d0>
msg = 'Mint Error: signature threshold not met. 0 < 1.'

    async def assert_err(f, msg):
        """Compute f() and expect an error message 'msg'."""
        try:
>           await f

tests/wallet/test_wallet_p2pk.py:30: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
cashu/wallet/wallet.py:656: in redeem
    return await self.split(proofs=proofs, amount=0)
cashu/wallet/wallet.py:731: in split
    sorted_promises = await super().split(proofs, list(sorted_outputs))
cashu/wallet/v1_api.py:93: in wrapper
    return await func(self, *args, **kwargs)
cashu/wallet/v1_api.py:106: in wrapper
    return await func(self, *args, **kwargs)
cashu/wallet/v1_api.py:592: in split
    self.raise_on_unsupported_version(resp, "POST /v1/swap")
cashu/wallet/v1_api.py:161: in raise_on_unsupported_version
    self.raise_on_error_request(resp)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

resp = <Response [400 Bad Request]>

    @staticmethod
    def raise_on_error_request(
        resp: Response,
    ) -> None:
        """Raises an exception if the response from the mint contains an error.
    
        Args:
            resp_dict (Response): Response dict (previously JSON) from mint
    
        Raises:
            Exception: if the response contains an error
        """
        try:
            resp_dict = resp.json()
        except json.JSONDecodeError:
            resp.raise_for_status()
            return
        if "detail" in resp_dict:
            logger.trace(f"Error from mint: {resp_dict}")
            error_message = f"Mint Error: {resp_dict['detail']}"
            if "code" in resp_dict:
                error_message += f" (Code: {resp_dict['code']})"
>           raise Exception(error_message)
E           Exception: Mint Error: The public key could not be parsed or is invalid. (Code: 0)

cashu/wallet/v1_api.py:147: Exception

During handling of the above exception, another exception occurred:

wallet1 = <cashu.wallet.wallet.Wallet object at 0x7f2cf5a625f0>
wallet2 = <cashu.wallet.wallet.Wallet object at 0x7f2cf875e1d0>

    @pytest.mark.asyncio
    async def test_p2pk_locktime_with_3_of_3_refund_pubkeys(
        wallet1: Wallet, wallet2: Wallet
    ):
        """Testing the case where we expect a 3-of-3 signature from the refund pubkeys"""
        # Create a third wallet for this test
        wallet3 = await Wallet2.with_db(
            SERVER_ENDPOINT, "test_data/wallet_p2pk_3", "wallet3"
        )
        await migrate_databases(wallet3.db, migrations)
        wallet3.private_key = PrivateKey(secrets.token_bytes(32))
        await wallet3.load_mint()
    
        # Get tokens and create public keys for all wallets
        mint_quote = await wallet1.request_mint(64)
        await pay_if_regtest(mint_quote.request)
        await wallet1.mint(64, quote_id=mint_quote.quote)
        pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
        pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
        pubkey_wallet3 = await wallet3.create_p2pk_pubkey()
    
        # Create an unspendable lock with refund conditions requiring 3 signatures
        garbage_pubkey = PrivateKey().public_key
        assert garbage_pubkey
        secret_lock = await wallet1.create_p2pk_lock(
            garbage_pubkey.format().hex(),  # create lock to unspendable pubkey
            locktime_seconds=2,  # locktime
            tags=Tags(
                [
                    ["refund", pubkey_wallet1, pubkey_wallet2, pubkey_wallet3],
                    ["n_sigs_refund", "3"],
                ],
            ),  # multiple refund pubkeys with required 3 signatures
        )
    
        # Send tokens with this lock
        _, send_proofs = await wallet1.swap_to_send(
            wallet1.proofs, 8, secret_lock=secret_lock
        )
    
        # Create copies for different test scenarios
        send_proofs_copy1 = copy.deepcopy(send_proofs)
        send_proofs_copy2 = copy.deepcopy(send_proofs)
    
        # Verify tokens can't be redeemed before locktime
>       await assert_err(
            wallet1.redeem(send_proofs),
            "Mint Error: signature threshold not met. 0 < 1.",
        )

tests/wallet/test_wallet_p2pk.py:719: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

f = <coroutine object Wallet.redeem at 0x7f2cf57a30d0>
msg = 'Mint Error: signature threshold not met. 0 < 1.'

    async def assert_err(f, msg):
        """Compute f() and expect an error message 'msg'."""
        try:
            await f
        except Exception as exc:
            if msg not in str(exc.args[0]):
>               raise Exception(f"Expected error: {msg}, got: {exc.args[0]}")
E               Exception: Expected error: Mint Error: signature threshold not met. 0 < 1., got: Mint Error: The public key could not be parsed or is invalid. (Code: 0)

tests/wallet/test_wallet_p2pk.py:33: Exception
tests.wallet.test_wallet_htlc::test_htlc_n_sigs_refund_locktime
Stack Traces | 2.14s run time
wallet1 = <cashu.wallet.wallet.Wallet object at 0x7f2cf58111b0>
wallet2 = <cashu.wallet.wallet.Wallet object at 0x7f2cf58110f0>

    @pytest.mark.asyncio
    async def test_htlc_n_sigs_refund_locktime(wallet1: Wallet, wallet2: Wallet):
        """Test HTLC with n_sigs_refund parameter requiring multiple signatures for refund after locktime."""
        # Create a third wallet for the third signature
        wallet3 = await Wallet.with_db(
            SERVER_ENDPOINT, "test_data/wallet_htlc_3", "wallet3"
        )
        await migrate_databases(wallet3.db, migrations)
        wallet3.private_key = PrivateKey(secrets.token_bytes(32))
        await wallet3.load_mint()
    
        # Mint tokens for testing
        mint_quote = await wallet1.request_mint(64)
        await pay_if_regtest(mint_quote.request)
        await wallet1.mint(64, quote_id=mint_quote.quote)
    
        # Setup parameters
        preimage = "0000000000000000000000000000000000000000000000000000000000000000"
        pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
        pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
        pubkey_wallet3 = await wallet3.create_p2pk_pubkey()
    
        # Wrong preimage - making it so we can only spend via locktime
        wrong_preimage = "1111111111111111111111111111111111111111111111111111111111111111"
    
        # Create HTLC with:
        # 1. Timelock in the past
        # 2. Three refund pubkeys with 2-of-3 signature requirement
        secret = await wallet1.create_htlc_lock(
            preimage=wrong_preimage,  # this ensures we can't redeem via preimage
            hashlock_pubkeys=[pubkey_wallet2],
            locktime_seconds=-200000,
            locktime_pubkeys=[pubkey_wallet1, pubkey_wallet2, pubkey_wallet3],
            locktime_n_sigs=2,  # require 2 of 3 signatures for refund
        )
    
        # # Send tokens with this lock
        _, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret)
        send_proofs_copy = copy.deepcopy(send_proofs)
    
        # First, try to redeem with a preimage + signatures, which will trigger the preimage path and fail
        signatures1 = wallet1.signatures_proofs_sig_inputs(send_proofs_copy)
        for p, sig in zip(send_proofs_copy, signatures1):
            p.witness = HTLCWitness(preimage=preimage, signatures=[sig]).model_dump_json()
    
        # Should fail because we need 2 signatures
        await assert_err(
            wallet1.redeem(send_proofs_copy),
            "HTLC preimage does not match.",
        )
    
        # NOW: try to redeem via locktime path but with only 1 signature
        # Try redeeming with only 1 signature after locktime
        signatures1 = wallet1.signatures_proofs_sig_inputs(send_proofs_copy)
        for p, sig in zip(send_proofs_copy, signatures1):
            p.witness = HTLCWitness(signatures=[sig]).model_dump_json()
    
        # Should fail because we need 2 signatures
        await assert_err(
            wallet1.redeem(send_proofs_copy),
            "Mint Error: not enough pubkeys (3) or signatures (1) present for n_sigs (2)",
        )
    
        # Make a fresh copy and add 2 signatures
        send_proofs_copy2 = copy.deepcopy(send_proofs)
        signatures1 = wallet1.signatures_proofs_sig_inputs(send_proofs_copy2)
>       signatures2 = wallet2.signatures_proofs_sig_inputs(send_proofs_copy2)

tests/wallet/test_wallet_htlc.py:576: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
cashu/wallet/p2pk.py:97: in signatures_proofs_sig_inputs
    signatures = [
cashu/wallet/p2pk.py:98: in <listcomp>
    schnorr_sign(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

message = b'["HTLC", {"data": "02d449a31fbb267c8f352e9968a79e3e5fc95c1bbeaa502fd6454ebde5a4bedc", "nonce": "2706bcc03be05de5e5c0...4923ef8abd3f7859a190d1de42ec3a058a4108967111120cebb7a7e661a99b399a603d8aa6b0ac2faf54c56ff93261b31b9322221e6e8e5f6"]]}]'
private_key = <cashu.core.crypto.bls.PrivateKey object at 0x7f2cf8a8c610>

    def schnorr_sign(message: bytes, private_key: SecpPrivateKey) -> bytes:
>       signature = private_key.sign_schnorr(
            hashlib.sha256(message).digest(),
            None,  # type: ignore
        )
E       AttributeError: 'PrivateKey' object has no attribute 'sign_schnorr'

cashu/core/p2pk.py:56: AttributeError
tests.wallet.test_wallet_p2pk_methods::test_filter_proofs_locked_to_our_pubkey
Stack Traces | 2.15s run time
wallet1 = <cashu.wallet.wallet.Wallet object at 0x7f2cf444c550>
wallet2 = <cashu.wallet.wallet.Wallet object at 0x7f2cf86defb0>

    @pytest.mark.asyncio
    async def test_filter_proofs_locked_to_our_pubkey(wallet1: Wallet, wallet2: Wallet):
        """Test filtering proofs locked to our public key."""
        # Mint tokens to wallet1
        mint_quote = await wallet1.request_mint(640)
        await pay_if_regtest(mint_quote.request)
        await wallet1.mint(640, quote_id=mint_quote.quote)
    
        # Get pubkeys for both wallets
        pubkey1 = await wallet1.create_p2pk_pubkey()
        pubkey2 = await wallet2.create_p2pk_pubkey()
    
        # Create proofs locked to wallet1's pubkey
        secret_lock1 = await wallet1.create_p2pk_lock(pubkey1)
        _, proofs1 = await wallet1.swap_to_send(
            wallet1.proofs, 16, secret_lock=secret_lock1
        )
    
        # Create proofs locked to wallet2's pubkey
        secret_lock2 = await wallet1.create_p2pk_lock(pubkey2)
        _, proofs2 = await wallet1.swap_to_send(
            wallet1.proofs, 16, secret_lock=secret_lock2
        )
    
        # Create proofs with multiple pubkeys
        secret_lock3 = await wallet1.create_p2pk_lock(
            pubkey1, tags=Tags([["pubkeys", pubkey2]])
        )
        _, proofs3 = await wallet1.swap_to_send(
            wallet1.proofs, 16, secret_lock=secret_lock3
        )
    
        # Sign the proofs to avoid witness errors
        signed_proofs1 = wallet1.sign_p2pk_sig_inputs(proofs1)
>       signed_proofs2 = wallet2.sign_p2pk_sig_inputs(proofs2)

tests/wallet/test_wallet_p2pk_methods.py:307: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
cashu/wallet/p2pk.py:311: in sign_p2pk_sig_inputs
    p2pk_signatures = self.signatures_proofs_sig_inputs(our_pubkey_proofs)
cashu/wallet/p2pk.py:97: in signatures_proofs_sig_inputs
    signatures = [
cashu/wallet/p2pk.py:98: in <listcomp>
    schnorr_sign(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

message = b'["P2PK", {"data": "9665418c4f3c32a660e164155b55b0df78301930f430556927e6fec6b3e0d38786c06b5ea2e6b7303370bc9b53be610c1...2b2e820004ebd4b3c148438c6d04dd420", "nonce": "79c3790cdd600c8dd8824a49f781136d", "tags": [["sigflag", "SIG_INPUTS"]]}]'
private_key = <cashu.core.crypto.bls.PrivateKey object at 0x7f2cf6528b50>

    def schnorr_sign(message: bytes, private_key: SecpPrivateKey) -> bytes:
>       signature = private_key.sign_schnorr(
            hashlib.sha256(message).digest(),
            None,  # type: ignore
        )
E       AttributeError: 'PrivateKey' object has no attribute 'sign_schnorr'

cashu/core/p2pk.py:56: AttributeError
tests.wallet.test_wallet_restore::test_restore_wallet_after_mint
Stack Traces | 2.19s run time
wallet3 = <cashu.wallet.wallet.Wallet object at 0x7f2cf7ca1db0>

    @pytest.mark.asyncio
    async def test_restore_wallet_after_mint(wallet3: Wallet):
        await reset_wallet_db(wallet3)
        mint_quote = await wallet3.request_mint(64)
        await pay_if_regtest(mint_quote.request)
        await wallet3.mint(64, quote_id=mint_quote.quote)
        assert wallet3.balance == 64
        await reset_wallet_db(wallet3)
        await wallet3.load_proofs()
        wallet3.proofs = []
        assert wallet3.balance == 0
        await wallet3.restore_promises_from_to(wallet3.keyset_id, 0, 20)
        assert wallet3.balance == 64
    
        # expect that DLEQ proofs are restored
>       assert all([p.dleq for p in wallet3.proofs])
E       assert False
E        +  where False = all([None, None, None, None, None, None, ...])

tests/wallet/test_wallet_restore.py:173: AssertionError
tests.wallet.test_wallet_p2pk::test_p2pk_locktime_with_refund_pubkey
Stack Traces | 3.67s run time
wallet1 = <cashu.wallet.wallet.Wallet object at 0x7f2cf6678cd0>
wallet2 = <cashu.wallet.wallet.Wallet object at 0x7f2cf5658550>

    @pytest.mark.asyncio
    async def test_p2pk_locktime_with_refund_pubkey(wallet1: Wallet, wallet2: Wallet):
        mint_quote = await wallet1.request_mint(64)
        await pay_if_regtest(mint_quote.request)
        await wallet1.mint(64, quote_id=mint_quote.quote)
        pubkey_wallet2 = await wallet2.create_p2pk_pubkey()  # receiver side
        # sender side
        garbage_priv = PrivateKey(secrets.token_bytes(32))
        garbage_pubkey = garbage_priv.public_key
        assert garbage_pubkey
        secret_lock = await wallet1.create_p2pk_lock(
            garbage_pubkey.format().hex(),  # create lock to unspendable pubkey
            locktime_seconds=2,  # locktime
            tags=Tags([["refund", pubkey_wallet2]]),  # refund pubkey
        )  # sender side
        _, send_proofs = await wallet1.swap_to_send(
            wallet1.proofs, 8, secret_lock=secret_lock
        )
        send_proofs_copy = copy.deepcopy(send_proofs)
        # receiver side: can't redeem since we used a garbage pubkey
        # and locktime has not passed
        await assert_err(
            wallet2.redeem(send_proofs),
            "",
        )
        await asyncio.sleep(2)
        # we can now redeem because of the refund locktime
>       await wallet2.redeem(send_proofs_copy)

tests/wallet/test_wallet_p2pk.py:187: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
cashu/wallet/wallet.py:656: in redeem
    return await self.split(proofs=proofs, amount=0)
cashu/wallet/wallet.py:722: in split
    proofs = self.sign_proofs_inplace_swap(proofs, outputs)
cashu/wallet/p2pk.py:184: in sign_proofs_inplace_swap
    proofs = self.add_witnesses_sig_inputs(proofs)
cashu/wallet/p2pk.py:330: in add_witnesses_sig_inputs
    signed_proofs = self.sign_p2pk_sig_inputs(proofs)
cashu/wallet/p2pk.py:311: in sign_p2pk_sig_inputs
    p2pk_signatures = self.signatures_proofs_sig_inputs(our_pubkey_proofs)
cashu/wallet/p2pk.py:97: in signatures_proofs_sig_inputs
    signatures = [
cashu/wallet/p2pk.py:98: in <listcomp>
    schnorr_sign(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

message = b'["P2PK", {"data": "a00494ec9581271ca5afc811df6848af5adba398883da4d6db8c0df6131783f4934008bcb8380610e94e43788b94e8ef0...59351ee6ff1013a0578a923311fa7a44b9131ea9a1104a72a23e4c4750"], ["locktime", "1778681280"], ["sigflag", "SIG_INPUTS"]]}]'
private_key = <cashu.core.crypto.bls.PrivateKey object at 0x7f2cf5810130>

    def schnorr_sign(message: bytes, private_key: SecpPrivateKey) -> bytes:
>       signature = private_key.sign_schnorr(
            hashlib.sha256(message).digest(),
            None,  # type: ignore
        )
E       AttributeError: 'PrivateKey' object has no attribute 'sign_schnorr'

cashu/core/p2pk.py:56: AttributeError

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

@ye0man ye0man added this to the 0.21.0 milestone May 8, 2026
@a1denvalu3 a1denvalu3 force-pushed the feature/bls12-381-v3-keyset branch 2 times, most recently from a8f8486 to 0c63c63 Compare May 10, 2026 17:16
a1denvalu3 and others added 2 commits May 13, 2026 15:58
fix v3 Proof.Y hash-to-curve and BlindedSignature dleq=None
robwoodgate added a commit to robwoodgate/nutshell that referenced this pull request May 13, 2026
Four coupled fixes that surface together when a fresh Nutshell 0.21.0
auth mint runs against any OIDC provider (e.g. Keycloak 25+) and a v3
(BLS) keyset is generated on first start. Each fix is small; bundled
because they share one architectural root: AuthLedger inherits the
mint CRUD (LedgerCrudSqlite) and the global `mint_input_fee_ppk`
setting, while the auth migrations chain / response models / user-id
contract never kept up.

1. Force `input_fee_ppk=0` on auth keyset generation.

   Auth proofs are NUT-22 amount-1 bearer tokens — never swapped or
   melted. `AuthLedger.verify_blind_auth` already explicitly skips
   fee calculation ("We do not calculate fees for auth keysets").
   But `Ledger.activate_keyset` reads `settings.mint_input_fee_ppk`
   unconditionally, so any mint with a non-zero global fee bakes
   that value into the auth keyset id — semantically wrong, and
   breaks wallet-side id re-derivation (auth router publishes
   `input_fee_ppk=null`, wallet derives without the suffix → id
   mismatch → keyset rejected as inauthentic). Matches CDK's
   behaviour (crates/cdk/src/mint/builder.rs forces fee=0 for the
   Auth unit).

   Implementation: `LedgerKeysets` exposes a per-instance
   `keyset_input_fee_ppk: Optional[int] = None` defaulting to
   `settings.mint_input_fee_ppk`; `AuthLedger` overrides to `0`.
   No behaviour change for non-auth ledgers.

2. m003: add `final_expiry` column to auth `keysets` table.

   LedgerCrudSqlite.store_keyset INSERTs `final_expiry` (added on
   the mint side in m031 for keysets v2). Auth migrations stopped
   at m002, so v3 keyset generation crashes with
   `no column named final_expiry`. Mirrors mint m031.

3. m004: align auth `promises` table with the mint-side schema.

   The mint side evolved `promises` to add mint_quote / swap_id
   (m023) and melt_quote / signed_at + drop the `c_ NOT NULL`
   constraint (m032-ish). LedgerCrudSqlite.store_promise INSERTs
   the full column set, so auth-side blind minting (first
   exercised by v3 BAT issuance at 0.21+) trips first
   `no column named mint_quote` then
   `NOT NULL constraint failed: promises.c_`. Auth never populates
   any of these new columns, but the schema must accept the
   INSERT. SQLite path rebuilds the table (matching mint m032
   shape); Postgres path uses ALTER chain.

4. Tolerate missing `sub` claim in clear-auth tokens.

   `_get_user` hard-coded `decoded_token["sub"]`, which raises
   KeyError when the IdP omits `sub` from access tokens. Keycloak
   25+ does this by default for public clients (the
   `oidc-subject-mapper` declared in the cashu-realm.json gets
   silently dropped on import). CDK's `verify_cat` doesn't read
   `sub` at all and works against the same realm. Fall back to
   `preferred_username` then `azp` so single-user-per-realm
   rate-limit tracking still works on those setups without
   changing happy-path semantics for IdPs that do ship `sub`.
   Cross-IdP, not Keycloak-specific.

Verified end-to-end against a freshly-built local container and
cashu-ts (v3 BAT path) — wallet OIDC password grant → 3 BATs minted →
auth keyset id verifies (`02 + sha256("1:<G2-pubkey>|unit:auth")` →
`027cbc55...`) → BLS pairing accepts the BATs → mint/swap/receive all
green.

Out of scope: the underlying smell is `AuthLedger` using
`LedgerCrudSqlite` instead of the (existing-but-unused)
`AuthLedgerCrudSqlite`, whose leaner `store_keyset` / `store_promise`
already match the auth m001 schema and would obviate (1)–(3).
Switching requires adding several missing methods to `AuthLedgerCrud`
(`store_blinded_message`, `update_keyset`, `bump_keyset_*`, balance
logs) — too wide for this PR. Worth a follow-up issue.

Refs cashubtc#999.
robwoodgate added a commit to robwoodgate/nutshell that referenced this pull request May 13, 2026
Five coupled fixes that surface together when a fresh Nutshell 0.21.0
auth mint runs against any OIDC provider (e.g. Keycloak 25+) and a v3
(BLS) keyset is generated on first start. Each fix is small; bundled
because they share one architectural root: AuthLedger inherits the
mint CRUD (LedgerCrudSqlite) and the global `mint_input_fee_ppk`
setting, while the auth migrations chain / response models / user-id
contract / auth-side CRUD never kept up.

1. Force `input_fee_ppk=0` on auth keyset generation.

   Auth proofs are NUT-22 amount-1 bearer tokens — never swapped or
   melted. `AuthLedger.verify_blind_auth` already explicitly skips
   fee calculation ("We do not calculate fees for auth keysets").
   But `Ledger.activate_keyset` reads `settings.mint_input_fee_ppk`
   unconditionally, so any mint with a non-zero global fee bakes
   that value into the auth keyset id — semantically wrong, and
   breaks wallet-side id re-derivation (auth router publishes
   `input_fee_ppk=null`, wallet derives without the suffix → id
   mismatch → keyset rejected as inauthentic). Matches CDK's
   behaviour (crates/cdk/src/mint/builder.rs forces fee=0 for the
   Auth unit).

   Implementation: `LedgerKeysets` exposes a per-instance
   `keyset_input_fee_ppk: Optional[int] = None` defaulting to
   `settings.mint_input_fee_ppk`; `AuthLedger` overrides to `0`.
   No behaviour change for non-auth ledgers.

2. m003: add `final_expiry` column to auth `keysets` table.

   LedgerCrudSqlite.store_keyset INSERTs `final_expiry` (added on
   the mint side in m031 for keysets v2). Auth migrations stopped
   at m002, so v3 keyset generation crashes with
   `no column named final_expiry`. Mirrors mint m031.

3. m004: align auth `promises` table with the mint-side schema.

   The mint side evolved `promises` to add mint_quote / swap_id
   (m023) and melt_quote / signed_at + drop the `c_ NOT NULL`
   constraint (m032-ish). LedgerCrudSqlite.store_promise INSERTs
   the full column set, so auth-side blind minting (first
   exercised by v3 BAT issuance at 0.21+) trips first
   `no column named mint_quote` then
   `NOT NULL constraint failed: promises.c_`. Auth never populates
   any of these new columns, but the schema must accept the
   INSERT. SQLite path rebuilds the table (matching mint m032
   shape); Postgres path uses ALTER chain.

4. Tolerate missing `sub` claim in clear-auth tokens.

   `_get_user` hard-coded `decoded_token["sub"]`, which raises
   KeyError when the IdP omits `sub` from access tokens. Keycloak
   25+ does this by default for public clients (the
   `oidc-subject-mapper` declared in the cashu-realm.json gets
   silently dropped on import). CDK's `verify_cat` doesn't read
   `sub` at all and works against the same realm. Fall back to
   `preferred_username` then `azp` so single-user-per-realm
   rate-limit tracking still works on those setups without
   changing happy-path semantics for IdPs that do ship `sub`.
   Cross-IdP, not Keycloak-specific.

5. AuthLedgerCrudSqlite.get_keyset: use MintKeyset.from_row.

   `MintKeyset(**row)` passes `amounts` as the raw stringified-JSON
   stored in SQLite (e.g. `"[1]"`) directly into the constructor.
   `MintKeyset.from_row` does `json.loads(row["amounts"])` first.
   Iteration over `self.amounts` would then walk characters instead
   of elements — producing junk key material. Latent in the current
   architecture because AuthLedger uses LedgerCrudSqlite (whose
   get_keyset is correct) and self.auth_crud is only invoked for
   user CRUD, but a real trap for the eventual switch-to-
   AuthLedgerCrudSqlite cleanup. Spotted by the security-scan bot
   on this PR.

Verified end-to-end against a freshly-built local container and
cashu-ts (v3 BAT path) — wallet OIDC password grant → 3 BATs minted →
auth keyset id verifies (`02 + sha256("1:<G2-pubkey>|unit:auth")` →
`027cbc55...`) → BLS pairing accepts the BATs → mint/swap/receive all
green.

Out of scope: the underlying smell is `AuthLedger` using
`LedgerCrudSqlite` instead of the (existing-but-unused)
`AuthLedgerCrudSqlite`, whose leaner `store_keyset` / `store_promise`
already match the auth m001 schema and would obviate (1)–(3).
Switching requires adding several missing methods to `AuthLedgerCrud`
(`store_blinded_message`, `update_keyset`, `bump_keyset_*`, balance
logs) — too wide for this PR. Worth a follow-up issue.

Refs cashubtc#999.
robwoodgate added a commit to robwoodgate/nutshell that referenced this pull request May 13, 2026
Five coupled fixes that surface together when a fresh Nutshell 0.21.0
auth mint runs against any OIDC provider (e.g. Keycloak 25+) and a v3
(BLS) keyset is generated on first start. Each fix is small; bundled
because they share one architectural root: AuthLedger inherits the
mint CRUD (LedgerCrudSqlite) and the global `mint_input_fee_ppk`
setting, while the auth migrations chain / response models / user-id
contract / auth-side CRUD never kept up.

1. Force `input_fee_ppk=0` on auth keyset generation.

   Auth proofs are NUT-22 amount-1 bearer tokens — never swapped or
   melted. `AuthLedger.verify_blind_auth` already explicitly skips
   fee calculation ("We do not calculate fees for auth keysets").
   But `Ledger.activate_keyset` reads `settings.mint_input_fee_ppk`
   unconditionally, so any mint with a non-zero global fee bakes
   that value into the auth keyset id — semantically wrong, and
   breaks wallet-side id re-derivation (auth router publishes
   `input_fee_ppk=null`, wallet derives without the suffix → id
   mismatch → keyset rejected as inauthentic). Matches CDK's
   behaviour (crates/cdk/src/mint/builder.rs forces fee=0 for the
   Auth unit).

   Implementation: `LedgerKeysets` exposes a per-instance
   `keyset_input_fee_ppk: Optional[int] = None` defaulting to
   `settings.mint_input_fee_ppk`; `AuthLedger` overrides to `0`.
   No behaviour change for non-auth ledgers.

2. m003: add `final_expiry` column to auth `keysets` table.

   LedgerCrudSqlite.store_keyset INSERTs `final_expiry` (added on
   the mint side in m031 for keysets v2). Auth migrations stopped
   at m002, so v3 keyset generation crashes with
   `no column named final_expiry`. Mirrors mint m031.

3. m004: align auth `promises` table with the mint-side schema.

   The mint side evolved `promises` to add mint_quote / swap_id
   (m023) and melt_quote / signed_at + drop the `c_ NOT NULL`
   constraint (m032-ish). LedgerCrudSqlite.store_promise INSERTs
   the full column set, so auth-side blind minting (first
   exercised by v3 BAT issuance at 0.21+) trips first
   `no column named mint_quote` then
   `NOT NULL constraint failed: promises.c_`. Auth never populates
   any of these new columns, but the schema must accept the
   INSERT. SQLite path rebuilds the table (matching mint m032
   shape); Postgres path uses ALTER chain.

4. Tolerate missing `sub` claim in clear-auth tokens.

   `_get_user` hard-coded `decoded_token["sub"]`, which raises
   KeyError when the IdP omits `sub` from access tokens. Keycloak
   25+ does this by default for public clients (the
   `oidc-subject-mapper` declared in the cashu-realm.json gets
   silently dropped on import). CDK's `verify_cat` doesn't read
   `sub` at all and works against the same realm. Fall back to
   `preferred_username` then `azp` so single-user-per-realm
   rate-limit tracking still works on those setups without
   changing happy-path semantics for IdPs that do ship `sub`.
   Cross-IdP, not Keycloak-specific.

5. AuthLedgerCrudSqlite.get_keyset: use MintKeyset.from_row.

   `MintKeyset(**row)` passes `amounts` as the raw stringified-JSON
   stored in SQLite (e.g. `"[1]"`) directly into the constructor.
   `MintKeyset.from_row` does `json.loads(row["amounts"])` first.
   Iteration over `self.amounts` would then walk characters instead
   of elements — producing junk key material. Latent in the current
   architecture because AuthLedger uses LedgerCrudSqlite (whose
   get_keyset is correct) and self.auth_crud is only invoked for
   user CRUD, but a real trap for the eventual switch-to-
   AuthLedgerCrudSqlite cleanup.

Verified end-to-end against a freshly-built local container and
cashu-ts (v3 BAT path) — wallet OIDC password grant → 3 BATs minted →
auth keyset id verifies (`02 + sha256("1:<G2-pubkey>|unit:auth")` →
`027cbc55...`) → BLS pairing accepts the BATs → mint/swap/receive all
green.

Out of scope: the underlying smell is `AuthLedger` using
`LedgerCrudSqlite` instead of the (existing-but-unused)
`AuthLedgerCrudSqlite`, whose leaner `store_keyset` / `store_promise`
already match the auth m001 schema and would obviate (1)–(3).
Switching requires adding several missing methods to `AuthLedgerCrud`
(`store_blinded_message`, `update_keyset`, `bump_keyset_*`, balance
logs) — too wide for this PR. Worth a follow-up issue.

Refs cashubtc#999.
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.

3 participants