Skip to content

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

Open
a1denvalu3 wants to merge 27 commits into
mainfrom
feature/bls12-381-v3-keyset
Open

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

Conversation

@a1denvalu3

@a1denvalu3 a1denvalu3 commented May 5, 2026

Copy link
Copy Markdown
Collaborator

Caution

This PR bumps Nutshell's version to 0.21

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.

@codecov

codecov Bot commented May 5, 2026

Copy link
Copy Markdown

❌ 13 Tests Failed:

Tests completed Failed Passed Skipped
938 13 925 93
View the top 3 failed test(s) by shortest run time
tests.mint.test_mint_verification::test_verify_proof_bdhke_rejects_legacy_hash_to_curve_alias
Stack Traces | 0.688s run time
ledger = <cashu.mint.ledger.Ledger object at 0x7f8ac8761f70>

    def test_verify_proof_bdhke_rejects_legacy_hash_to_curve_alias(ledger: Ledger):
        current_secret = "alias-241138080"
        legacy_alias = bytes.fromhex(
            "".join(
                [
                    "6562185f066132377f76642849e397bc001026704b1006521775301235c68f7d",
                    "01000000",
                ]
            )
        ).decode("utf-8")
        amount = 8
        private_key = ledger.keyset.private_keys[amount]
        C = (hash_to_curve(current_secret.encode()) * private_key).format().hex()  # type: ignore
    
        current_proof = Proof(
            id=ledger.keyset.id,
            amount=amount,
            secret=current_secret,
            C=C,
        )
        alias_proof = Proof(
            id=ledger.keyset.id,
            amount=amount,
            secret=legacy_alias,
            C=C,
        )
    
>       assert ledger._verify_proof_bdhke(current_proof) is True
E       AssertionError: assert False is True
E        +  where False = _verify_proof_bdhke(Proof(id='022de6c59498cf5804d5ad4a28ad84f5ab69b3a4f00284e012988afd8514ea69c8', amount=8, secret='alias-241138080', Y='..._e=None, reserved=False, send_id='', time_created='', time_reserved='', derivation_path='', mint_id=None, melt_id=None))
E        +    where _verify_proof_bdhke = <cashu.mint.ledger.Ledger object at 0x7f8ac8761f70>._verify_proof_bdhke

tests/mint/test_mint_verification.py:439: AssertionError
tests.mint.test_mint_verification::test_verify_outputs_rejects_duplicate_blinds
Stack Traces | 0.693s run time
ledger = <cashu.mint.ledger.Ledger object at 0x7f8ac8593ec0>

    @pytest.mark.asyncio
    async def test_verify_outputs_rejects_duplicate_blinds(ledger: Ledger):
        o = _blinded_output(ledger, label="dupb")
        with pytest.raises(TransactionDuplicateOutputsError):
>           await ledger._verify_outputs([o, o.model_copy()])

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

self = <cashu.mint.ledger.Ledger object at 0x7f8ac8593ec0>
outputs = [BlindedMessage(amount=8, id='022de6c59498cf5804d5ad4a28ad84f5ab69b3a4f00284e012988afd8514ea69c8', B_='0349271e25089cf...ab69b3a4f00284e012988afd8514ea69c8', B_='0349271e25089cf647a6a1f6042c468c2cad790f72a3e4e1b257319d5c05c067c7', C_=None)]
skip_amount_check = False, expected_unit = None, conn = None

    async def _verify_outputs(
        self,
        outputs: List[BlindedMessage],
        skip_amount_check=False,
        expected_unit: Optional[Unit] = None,
        conn: Optional[Connection] = None,
    ):
        """Verify that the outputs are valid."""
        logger.trace(f"Verifying {len(outputs)} outputs.")
        if not outputs:
            raise TransactionError("no outputs provided.")
        # Verify all outputs have the same keyset id
        if not all([o.id == outputs[0].id for o in outputs]):
            raise TransactionError("outputs have different keyset ids.")
        # Verify that the keyset id is known and active
        if outputs[0].id not in self.keysets:
            raise TransactionError("keyset id unknown.")
        if not self.keysets[outputs[0].id].active:
            raise TransactionError("keyset id inactive.")
        if expected_unit and self.keysets[outputs[0].id].unit != expected_unit:
            raise TransactionError(
                f"output unit {self.keysets[outputs[0].id].unit.name} does not match quote unit {expected_unit.name}"
            )
        # Verify that all blinded messages are valid curve points
        if not all([self._verify_blinded_message(o) for o in outputs]):
>           raise TransactionError("invalid blinded message.")
E           cashu.core.errors.TransactionError: invalid blinded message.

cashu/mint/verification.py:191: TransactionError
tests.mint.test_mint_verification::test_verify_outputs_stored_before_pending_when_no_signed_in_list
Stack Traces | 0.714s run time
ledger = <cashu.mint.ledger.Ledger object at 0x7f8ac8162a50>

    @pytest.mark.asyncio
    async def test_verify_outputs_stored_before_pending_when_no_signed_in_list(
        ledger: Ledger,
    ):
        o = _blinded_output(ledger, label="pending-branch")
    
        async def fake_check(outputs, conn=None):
            return [
                o.model_copy(update={"C_": None}),
                o.model_copy(update={"C_": None}),
            ]
    
        with patch.object(ledger, "_check_outputs_pending_or_issued_before", fake_check):
            with pytest.raises(OutputsArePendingError):
>               await ledger._verify_outputs([o])

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

self = <cashu.mint.ledger.Ledger object at 0x7f8ac8162a50>
outputs = [BlindedMessage(amount=8, id='022de6c59498cf5804d5ad4a28ad84f5ab69b3a4f00284e012988afd8514ea69c8', B_='0262e4e4ca7c078d73c8f685f2ba3d6a30ad6520aa537055a0b3398cd89a013e49', C_=None)]
skip_amount_check = False, expected_unit = None, conn = None

    async def _verify_outputs(
        self,
        outputs: List[BlindedMessage],
        skip_amount_check=False,
        expected_unit: Optional[Unit] = None,
        conn: Optional[Connection] = None,
    ):
        """Verify that the outputs are valid."""
        logger.trace(f"Verifying {len(outputs)} outputs.")
        if not outputs:
            raise TransactionError("no outputs provided.")
        # Verify all outputs have the same keyset id
        if not all([o.id == outputs[0].id for o in outputs]):
            raise TransactionError("outputs have different keyset ids.")
        # Verify that the keyset id is known and active
        if outputs[0].id not in self.keysets:
            raise TransactionError("keyset id unknown.")
        if not self.keysets[outputs[0].id].active:
            raise TransactionError("keyset id inactive.")
        if expected_unit and self.keysets[outputs[0].id].unit != expected_unit:
            raise TransactionError(
                f"output unit {self.keysets[outputs[0].id].unit.name} does not match quote unit {expected_unit.name}"
            )
        # Verify that all blinded messages are valid curve points
        if not all([self._verify_blinded_message(o) for o in outputs]):
>           raise TransactionError("invalid blinded message.")
E           cashu.core.errors.TransactionError: invalid blinded message.

cashu/mint/verification.py:191: TransactionError
tests.mint.test_mint_verification::test_verify_outputs_rejects_already_signed
Stack Traces | 0.715s run time
ledger = <cashu.mint.ledger.Ledger object at 0x7f8ac820aa80>

    @pytest.mark.asyncio
    async def test_verify_outputs_rejects_already_signed(ledger: Ledger):
        o = _blinded_output(ledger, label="sgn")
        signed = [o.model_copy(update={"C_": "03" + "ab" * 32})]
    
        async def fake_check(outputs, conn=None):
            return signed
    
        with patch.object(ledger, "_check_outputs_pending_or_issued_before", fake_check):
            with pytest.raises(OutputsAlreadySignedError):
>               await ledger._verify_outputs([o])

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

self = <cashu.mint.ledger.Ledger object at 0x7f8ac820aa80>
outputs = [BlindedMessage(amount=8, id='022de6c59498cf5804d5ad4a28ad84f5ab69b3a4f00284e012988afd8514ea69c8', B_='02eac699afe301e930e60ddfcc998e612df9cf297c88d75cb0b37c9b9554563c3a', C_=None)]
skip_amount_check = False, expected_unit = None, conn = None

    async def _verify_outputs(
        self,
        outputs: List[BlindedMessage],
        skip_amount_check=False,
        expected_unit: Optional[Unit] = None,
        conn: Optional[Connection] = None,
    ):
        """Verify that the outputs are valid."""
        logger.trace(f"Verifying {len(outputs)} outputs.")
        if not outputs:
            raise TransactionError("no outputs provided.")
        # Verify all outputs have the same keyset id
        if not all([o.id == outputs[0].id for o in outputs]):
            raise TransactionError("outputs have different keyset ids.")
        # Verify that the keyset id is known and active
        if outputs[0].id not in self.keysets:
            raise TransactionError("keyset id unknown.")
        if not self.keysets[outputs[0].id].active:
            raise TransactionError("keyset id inactive.")
        if expected_unit and self.keysets[outputs[0].id].unit != expected_unit:
            raise TransactionError(
                f"output unit {self.keysets[outputs[0].id].unit.name} does not match quote unit {expected_unit.name}"
            )
        # Verify that all blinded messages are valid curve points
        if not all([self._verify_blinded_message(o) for o in outputs]):
>           raise TransactionError("invalid blinded message.")
E           cashu.core.errors.TransactionError: invalid blinded message.

cashu/mint/verification.py:191: TransactionError
tests.mint.test_mint_verification::test_verify_outputs_multiple_fresh_outputs
Stack Traces | 0.716s run time
ledger = <cashu.mint.ledger.Ledger object at 0x7f8ac8aa1010>

    @pytest.mark.asyncio
    async def test_verify_outputs_multiple_fresh_outputs(ledger: Ledger):
>       await ledger._verify_outputs(
            [
                _blinded_output(ledger, label="m1"),
                _blinded_output(ledger, label="m2"),
            ]
        )

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

self = <cashu.mint.ledger.Ledger object at 0x7f8ac8aa1010>
outputs = [BlindedMessage(amount=8, id='022de6c59498cf5804d5ad4a28ad84f5ab69b3a4f00284e012988afd8514ea69c8', B_='033eaf4b46c802d...ab69b3a4f00284e012988afd8514ea69c8', B_='038d135ae81881d5e65ecde408e6a257693aecf928bdbe97ad775f8fdd00065765', C_=None)]
skip_amount_check = False, expected_unit = None, conn = None

    async def _verify_outputs(
        self,
        outputs: List[BlindedMessage],
        skip_amount_check=False,
        expected_unit: Optional[Unit] = None,
        conn: Optional[Connection] = None,
    ):
        """Verify that the outputs are valid."""
        logger.trace(f"Verifying {len(outputs)} outputs.")
        if not outputs:
            raise TransactionError("no outputs provided.")
        # Verify all outputs have the same keyset id
        if not all([o.id == outputs[0].id for o in outputs]):
            raise TransactionError("outputs have different keyset ids.")
        # Verify that the keyset id is known and active
        if outputs[0].id not in self.keysets:
            raise TransactionError("keyset id unknown.")
        if not self.keysets[outputs[0].id].active:
            raise TransactionError("keyset id inactive.")
        if expected_unit and self.keysets[outputs[0].id].unit != expected_unit:
            raise TransactionError(
                f"output unit {self.keysets[outputs[0].id].unit.name} does not match quote unit {expected_unit.name}"
            )
        # Verify that all blinded messages are valid curve points
        if not all([self._verify_blinded_message(o) for o in outputs]):
>           raise TransactionError("invalid blinded message.")
E           cashu.core.errors.TransactionError: invalid blinded message.

cashu/mint/verification.py:191: TransactionError
tests.mint.test_mint_verification::test_verify_outputs_stored_before_signed_branch_any_c_
Stack Traces | 0.718s run time
ledger = <cashu.mint.ledger.Ledger object at 0x7f8ac9223d40>

    @pytest.mark.asyncio
    async def test_verify_outputs_stored_before_signed_branch_any_c_(ledger: Ledger):
        """`OutputsAlreadySignedError` when any returned row has truthy C_."""
        o = _blinded_output(ledger, label="signed-branch")
    
        async def fake_check(outputs, conn=None):
            return [
                o.model_copy(update={"C_": "03" + "cd" * 32}),
                o.model_copy(update={"C_": None}),
            ]
    
        with patch.object(ledger, "_check_outputs_pending_or_issued_before", fake_check):
            with pytest.raises(OutputsAlreadySignedError):
>               await ledger._verify_outputs([o])

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

self = <cashu.mint.ledger.Ledger object at 0x7f8ac9223d40>
outputs = [BlindedMessage(amount=8, id='022de6c59498cf5804d5ad4a28ad84f5ab69b3a4f00284e012988afd8514ea69c8', B_='031c0bd28c0ff8a358c00b633be119854c8b27bffe78a6503e4c0624662b016d5c', C_=None)]
skip_amount_check = False, expected_unit = None, conn = None

    async def _verify_outputs(
        self,
        outputs: List[BlindedMessage],
        skip_amount_check=False,
        expected_unit: Optional[Unit] = None,
        conn: Optional[Connection] = None,
    ):
        """Verify that the outputs are valid."""
        logger.trace(f"Verifying {len(outputs)} outputs.")
        if not outputs:
            raise TransactionError("no outputs provided.")
        # Verify all outputs have the same keyset id
        if not all([o.id == outputs[0].id for o in outputs]):
            raise TransactionError("outputs have different keyset ids.")
        # Verify that the keyset id is known and active
        if outputs[0].id not in self.keysets:
            raise TransactionError("keyset id unknown.")
        if not self.keysets[outputs[0].id].active:
            raise TransactionError("keyset id inactive.")
        if expected_unit and self.keysets[outputs[0].id].unit != expected_unit:
            raise TransactionError(
                f"output unit {self.keysets[outputs[0].id].unit.name} does not match quote unit {expected_unit.name}"
            )
        # Verify that all blinded messages are valid curve points
        if not all([self._verify_blinded_message(o) for o in outputs]):
>           raise TransactionError("invalid blinded message.")
E           cashu.core.errors.TransactionError: invalid blinded message.

cashu/mint/verification.py:191: TransactionError
tests.mint.test_mint_verification::test_verify_inputs_and_outputs_with_outputs_calls_together
Stack Traces | 0.719s run time
ledger = <cashu.mint.ledger.Ledger object at 0x7f8ac91a5b80>

    @pytest.mark.asyncio
    async def test_verify_inputs_and_outputs_with_outputs_calls_together(ledger: Ledger):
        outs = [_blinded_output(ledger, label="pipe")]
        with (
            patch.object(ledger, "_verify_inputs", AsyncMock(return_value=None)),
            patch.object(ledger, "_verify_inputs_and_outputs_together") as together,
        ):
>           await ledger.verify_inputs_and_outputs(
                proofs=[MagicMock(spec=Proof)], outputs=outs
            )

tests/mint/test_mint_verification.py:662: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
cashu/mint/verification.py:74: in verify_inputs_and_outputs
    await self._verify_outputs(outputs, conn=conn)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <cashu.mint.ledger.Ledger object at 0x7f8ac91a5b80>
outputs = [BlindedMessage(amount=8, id='022de6c59498cf5804d5ad4a28ad84f5ab69b3a4f00284e012988afd8514ea69c8', B_='031607db0b46adce03b3cf948a3b1e828d1e8282c17faff0cb64ba0e95b4cec8af', C_=None)]
skip_amount_check = False, expected_unit = None, conn = None

    async def _verify_outputs(
        self,
        outputs: List[BlindedMessage],
        skip_amount_check=False,
        expected_unit: Optional[Unit] = None,
        conn: Optional[Connection] = None,
    ):
        """Verify that the outputs are valid."""
        logger.trace(f"Verifying {len(outputs)} outputs.")
        if not outputs:
            raise TransactionError("no outputs provided.")
        # Verify all outputs have the same keyset id
        if not all([o.id == outputs[0].id for o in outputs]):
            raise TransactionError("outputs have different keyset ids.")
        # Verify that the keyset id is known and active
        if outputs[0].id not in self.keysets:
            raise TransactionError("keyset id unknown.")
        if not self.keysets[outputs[0].id].active:
            raise TransactionError("keyset id inactive.")
        if expected_unit and self.keysets[outputs[0].id].unit != expected_unit:
            raise TransactionError(
                f"output unit {self.keysets[outputs[0].id].unit.name} does not match quote unit {expected_unit.name}"
            )
        # Verify that all blinded messages are valid curve points
        if not all([self._verify_blinded_message(o) for o in outputs]):
>           raise TransactionError("invalid blinded message.")
E           cashu.core.errors.TransactionError: invalid blinded message.

cashu/mint/verification.py:191: TransactionError
tests.mint.test_mint_verification::test_verify_outputs_second_output_invalid_amount
Stack Traces | 0.729s run time
ledger = <cashu.mint.ledger.Ledger object at 0x7f8ac81fb560>

    @pytest.mark.asyncio
    async def test_verify_outputs_second_output_invalid_amount(ledger: Ledger):
        o1 = _blinded_output(ledger, amount=8, label="ok1")
        o2 = _blinded_output(ledger, amount=0, label="bad2")
        with pytest.raises(NotAllowedError, match="invalid amount"):
>           await ledger._verify_outputs([o1, o2], skip_amount_check=False)

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

self = <cashu.mint.ledger.Ledger object at 0x7f8ac81fb560>
outputs = [BlindedMessage(amount=8, id='022de6c59498cf5804d5ad4a28ad84f5ab69b3a4f00284e012988afd8514ea69c8', B_='03e9beba6ef4959...ab69b3a4f00284e012988afd8514ea69c8', B_='0323aee45ed6f90938acd81f9b11a51d329f7a03980be4213684f373bcd8c6c72f', C_=None)]
skip_amount_check = False, expected_unit = None, conn = None

    async def _verify_outputs(
        self,
        outputs: List[BlindedMessage],
        skip_amount_check=False,
        expected_unit: Optional[Unit] = None,
        conn: Optional[Connection] = None,
    ):
        """Verify that the outputs are valid."""
        logger.trace(f"Verifying {len(outputs)} outputs.")
        if not outputs:
            raise TransactionError("no outputs provided.")
        # Verify all outputs have the same keyset id
        if not all([o.id == outputs[0].id for o in outputs]):
            raise TransactionError("outputs have different keyset ids.")
        # Verify that the keyset id is known and active
        if outputs[0].id not in self.keysets:
            raise TransactionError("keyset id unknown.")
        if not self.keysets[outputs[0].id].active:
            raise TransactionError("keyset id inactive.")
        if expected_unit and self.keysets[outputs[0].id].unit != expected_unit:
            raise TransactionError(
                f"output unit {self.keysets[outputs[0].id].unit.name} does not match quote unit {expected_unit.name}"
            )
        # Verify that all blinded messages are valid curve points
        if not all([self._verify_blinded_message(o) for o in outputs]):
>           raise TransactionError("invalid blinded message.")
E           cashu.core.errors.TransactionError: invalid blinded message.

cashu/mint/verification.py:191: TransactionError
tests.mint.test_mint_verification::test_verify_outputs_skip_amount_check_allows_zero
Stack Traces | 0.73s run time
ledger = <cashu.mint.ledger.Ledger object at 0x7f8ac8163590>

    @pytest.mark.asyncio
    async def test_verify_outputs_skip_amount_check_allows_zero(ledger: Ledger):
        o = _blinded_output(ledger, amount=0, label="nut8")
>       await ledger._verify_outputs([o], skip_amount_check=True)

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

self = <cashu.mint.ledger.Ledger object at 0x7f8ac8163590>
outputs = [BlindedMessage(amount=0, id='022de6c59498cf5804d5ad4a28ad84f5ab69b3a4f00284e012988afd8514ea69c8', B_='034946ab59feb689e84f1a4845ee4351776820194f7ee1200316eca6ae7d756bf6', C_=None)]
skip_amount_check = True, expected_unit = None, conn = None

    async def _verify_outputs(
        self,
        outputs: List[BlindedMessage],
        skip_amount_check=False,
        expected_unit: Optional[Unit] = None,
        conn: Optional[Connection] = None,
    ):
        """Verify that the outputs are valid."""
        logger.trace(f"Verifying {len(outputs)} outputs.")
        if not outputs:
            raise TransactionError("no outputs provided.")
        # Verify all outputs have the same keyset id
        if not all([o.id == outputs[0].id for o in outputs]):
            raise TransactionError("outputs have different keyset ids.")
        # Verify that the keyset id is known and active
        if outputs[0].id not in self.keysets:
            raise TransactionError("keyset id unknown.")
        if not self.keysets[outputs[0].id].active:
            raise TransactionError("keyset id inactive.")
        if expected_unit and self.keysets[outputs[0].id].unit != expected_unit:
            raise TransactionError(
                f"output unit {self.keysets[outputs[0].id].unit.name} does not match quote unit {expected_unit.name}"
            )
        # Verify that all blinded messages are valid curve points
        if not all([self._verify_blinded_message(o) for o in outputs]):
>           raise TransactionError("invalid blinded message.")
E           cashu.core.errors.TransactionError: invalid blinded message.

cashu/mint/verification.py:191: TransactionError
tests.mint.test_mint_verification::test_verify_outputs_invalid_amount_zero_without_skip
Stack Traces | 0.762s run time
ledger = <cashu.mint.ledger.Ledger object at 0x7f8ac8dd9040>

    @pytest.mark.asyncio
    async def test_verify_outputs_invalid_amount_zero_without_skip(ledger: Ledger):
        o = _blinded_output(ledger, amount=0, label="zero-amt")
        with pytest.raises(NotAllowedError, match="invalid amount"):
>           await ledger._verify_outputs([o], skip_amount_check=False)

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

self = <cashu.mint.ledger.Ledger object at 0x7f8ac8dd9040>
outputs = [BlindedMessage(amount=0, id='022de6c59498cf5804d5ad4a28ad84f5ab69b3a4f00284e012988afd8514ea69c8', B_='03c5d9cae19e0088dbdd89749e8d855381558eca131e72bc5cc6ad3d6509c82605', C_=None)]
skip_amount_check = False, expected_unit = None, conn = None

    async def _verify_outputs(
        self,
        outputs: List[BlindedMessage],
        skip_amount_check=False,
        expected_unit: Optional[Unit] = None,
        conn: Optional[Connection] = None,
    ):
        """Verify that the outputs are valid."""
        logger.trace(f"Verifying {len(outputs)} outputs.")
        if not outputs:
            raise TransactionError("no outputs provided.")
        # Verify all outputs have the same keyset id
        if not all([o.id == outputs[0].id for o in outputs]):
            raise TransactionError("outputs have different keyset ids.")
        # Verify that the keyset id is known and active
        if outputs[0].id not in self.keysets:
            raise TransactionError("keyset id unknown.")
        if not self.keysets[outputs[0].id].active:
            raise TransactionError("keyset id inactive.")
        if expected_unit and self.keysets[outputs[0].id].unit != expected_unit:
            raise TransactionError(
                f"output unit {self.keysets[outputs[0].id].unit.name} does not match quote unit {expected_unit.name}"
            )
        # Verify that all blinded messages are valid curve points
        if not all([self._verify_blinded_message(o) for o in outputs]):
>           raise TransactionError("invalid blinded message.")
E           cashu.core.errors.TransactionError: invalid blinded message.

cashu/mint/verification.py:191: TransactionError
tests.mint.test_mint_verification::test_verify_inputs_and_outputs_happy_path_outputs_only_phase
Stack Traces | 0.866s run time
ledger = <cashu.mint.ledger.Ledger object at 0x7f8aca9542f0>

    @pytest.mark.asyncio
    async def test_verify_inputs_and_outputs_happy_path_outputs_only_phase(ledger: Ledger):
        """Inputs mocked; real output verification for never-seen blinds."""
        outs = [_blinded_output(ledger, label="happy")]
        with (
            patch.object(ledger, "_verify_inputs", AsyncMock(return_value=None)),
            patch.object(ledger, "_verify_inputs_and_outputs_together", return_value=None),
        ):
>           await ledger.verify_inputs_and_outputs(
                proofs=[MagicMock(spec=Proof)], outputs=outs
            )

tests/mint/test_mint_verification.py:689: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
cashu/mint/verification.py:74: in verify_inputs_and_outputs
    await self._verify_outputs(outputs, conn=conn)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <cashu.mint.ledger.Ledger object at 0x7f8aca9542f0>
outputs = [BlindedMessage(amount=8, id='022de6c59498cf5804d5ad4a28ad84f5ab69b3a4f00284e012988afd8514ea69c8', B_='0220d90fd8a73f2febeec1814f43f2e2cec2bf0808a0085e7aaa156df9d4c87f8c', C_=None)]
skip_amount_check = False, expected_unit = None, conn = None

    async def _verify_outputs(
        self,
        outputs: List[BlindedMessage],
        skip_amount_check=False,
        expected_unit: Optional[Unit] = None,
        conn: Optional[Connection] = None,
    ):
        """Verify that the outputs are valid."""
        logger.trace(f"Verifying {len(outputs)} outputs.")
        if not outputs:
            raise TransactionError("no outputs provided.")
        # Verify all outputs have the same keyset id
        if not all([o.id == outputs[0].id for o in outputs]):
            raise TransactionError("outputs have different keyset ids.")
        # Verify that the keyset id is known and active
        if outputs[0].id not in self.keysets:
            raise TransactionError("keyset id unknown.")
        if not self.keysets[outputs[0].id].active:
            raise TransactionError("keyset id inactive.")
        if expected_unit and self.keysets[outputs[0].id].unit != expected_unit:
            raise TransactionError(
                f"output unit {self.keysets[outputs[0].id].unit.name} does not match quote unit {expected_unit.name}"
            )
        # Verify that all blinded messages are valid curve points
        if not all([self._verify_blinded_message(o) for o in outputs]):
>           raise TransactionError("invalid blinded message.")
E           cashu.core.errors.TransactionError: invalid blinded message.

cashu/mint/verification.py:191: TransactionError
tests.mint.test_mint_verification::test_verify_outputs_rejects_pending_stored_outputs
Stack Traces | 0.868s run time
ledger = <cashu.mint.ledger.Ledger object at 0x7f8ac8254b00>

    @pytest.mark.asyncio
    async def test_verify_outputs_rejects_pending_stored_outputs(ledger: Ledger):
        o = _blinded_output(ledger, label="pend")
        pending = [o.model_copy(update={"C_": None})]
    
        async def fake_check(outputs, conn=None):
            return pending
    
        with patch.object(ledger, "_check_outputs_pending_or_issued_before", fake_check):
            with pytest.raises(OutputsArePendingError):
>               await ledger._verify_outputs([o])

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

self = <cashu.mint.ledger.Ledger object at 0x7f8ac8254b00>
outputs = [BlindedMessage(amount=8, id='022de6c59498cf5804d5ad4a28ad84f5ab69b3a4f00284e012988afd8514ea69c8', B_='03e49754fdaab0e40a1f17be77d450eadae8551f71c59ce98ece4eb9a15fe1cddb', C_=None)]
skip_amount_check = False, expected_unit = None, conn = None

    async def _verify_outputs(
        self,
        outputs: List[BlindedMessage],
        skip_amount_check=False,
        expected_unit: Optional[Unit] = None,
        conn: Optional[Connection] = None,
    ):
        """Verify that the outputs are valid."""
        logger.trace(f"Verifying {len(outputs)} outputs.")
        if not outputs:
            raise TransactionError("no outputs provided.")
        # Verify all outputs have the same keyset id
        if not all([o.id == outputs[0].id for o in outputs]):
            raise TransactionError("outputs have different keyset ids.")
        # Verify that the keyset id is known and active
        if outputs[0].id not in self.keysets:
            raise TransactionError("keyset id unknown.")
        if not self.keysets[outputs[0].id].active:
            raise TransactionError("keyset id inactive.")
        if expected_unit and self.keysets[outputs[0].id].unit != expected_unit:
            raise TransactionError(
                f"output unit {self.keysets[outputs[0].id].unit.name} does not match quote unit {expected_unit.name}"
            )
        # Verify that all blinded messages are valid curve points
        if not all([self._verify_blinded_message(o) for o in outputs]):
>           raise TransactionError("invalid blinded message.")
E           cashu.core.errors.TransactionError: invalid blinded message.

cashu/mint/verification.py:191: TransactionError
tests.mint.test_mint_verification::test_verify_outputs_accepts_fresh_outputs
Stack Traces | 0.871s run time
ledger = <cashu.mint.ledger.Ledger object at 0x7f8ac8dbacf0>

    @pytest.mark.asyncio
    async def test_verify_outputs_accepts_fresh_outputs(ledger: Ledger):
        o = _blinded_output(ledger, label="fresh")
>       await ledger._verify_outputs([o])

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

self = <cashu.mint.ledger.Ledger object at 0x7f8ac8dbacf0>
outputs = [BlindedMessage(amount=8, id='022de6c59498cf5804d5ad4a28ad84f5ab69b3a4f00284e012988afd8514ea69c8', B_='022de73088368baf41f9a569673d9a1b3eeb4ca70caf81b84a0992f373935d8f6f', C_=None)]
skip_amount_check = False, expected_unit = None, conn = None

    async def _verify_outputs(
        self,
        outputs: List[BlindedMessage],
        skip_amount_check=False,
        expected_unit: Optional[Unit] = None,
        conn: Optional[Connection] = None,
    ):
        """Verify that the outputs are valid."""
        logger.trace(f"Verifying {len(outputs)} outputs.")
        if not outputs:
            raise TransactionError("no outputs provided.")
        # Verify all outputs have the same keyset id
        if not all([o.id == outputs[0].id for o in outputs]):
            raise TransactionError("outputs have different keyset ids.")
        # Verify that the keyset id is known and active
        if outputs[0].id not in self.keysets:
            raise TransactionError("keyset id unknown.")
        if not self.keysets[outputs[0].id].active:
            raise TransactionError("keyset id inactive.")
        if expected_unit and self.keysets[outputs[0].id].unit != expected_unit:
            raise TransactionError(
                f"output unit {self.keysets[outputs[0].id].unit.name} does not match quote unit {expected_unit.name}"
            )
        # Verify that all blinded messages are valid curve points
        if not all([self._verify_blinded_message(o) for o in outputs]):
>           raise TransactionError("invalid blinded message.")
E           cashu.core.errors.TransactionError: invalid blinded message.

cashu/mint/verification.py:191: TransactionError

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
a1denvalu3 force-pushed the feature/bls12-381-v3-keyset branch 2 times, most recently from 86aa13e to c894cf3 Compare May 20, 2026 16:19
@a1denvalu3
a1denvalu3 marked this pull request as ready for review May 20, 2026 20:35
@a1denvalu3
a1denvalu3 force-pushed the feature/bls12-381-v3-keyset branch 2 times, most recently from 9bf2514 to 6a6a964 Compare June 8, 2026 09:46
Comment thread cashu/core/crypto/keys.py Outdated
Comment thread cashu/core/crypto/keys.py Outdated
@a1denvalu3
a1denvalu3 force-pushed the feature/bls12-381-v3-keyset branch from 4230932 to a3acbc1 Compare July 3, 2026 16:32
- Use single miller loop accumulation by negating the signature point
- Verify against the identity element (BlstFP12Element)
- Applies to both single and batch pairing verification functions
- Add NUT-00 round-trip test vectors to  for v3 (BLS12-381)
- Add NUT-02 keyset ID test vectors to  for v3 keysets
- Add NUT-13 secret and blinding factor derivation test vector to
- Add TRACE level logging in core BLS operations (bls_dhke.py, keys.py, secrets.py) for tracking blinding factor reduction, derivation, and verification states
- Hoist in-line imports to module level
Moved _G2_HEX string definition and uncompression step to global scope in bls.py to avoid repeated initialization and uncompression in pairing and batch pairing verification functions. Imported the cached G2 point directly into bls_dhke.py.
Added is_infinity method to PublicKey class and updated step2_bob to formally verify the blinded message is not the point at infinity instead of checking the serialized hex string against a hardcoded constant.
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