Skip to content

feat(mint): support BOLT11 description hashes - #1105

Open
sotach1 wants to merge 3 commits into
cashubtc:mainfrom
sotach1:feat/bolt11-description-hash
Open

feat(mint): support BOLT11 description hashes#1105
sotach1 wants to merge 3 commits into
cashubtc:mainfrom
sotach1:feat/bolt11-description-hash

Conversation

@sotach1

@sotach1 sotach1 commented Aug 4, 2026

Copy link
Copy Markdown

Summary

  • add an optional per-quote description_hash flag to the NUT-23 BOLT11 mint quote request
  • advertise backend support as options.description_hash in mint info
  • preserve ordinary plaintext invoice descriptions unless the wallet explicitly opts in
  • pass both the computed hash and original description bytes to capable Lightning backends
  • verify that the backend returned an invoice with the requested description hash

This supports protocols such as NIP-57 where the invoice must commit to exact data supplied out of band. The behavior is deliberately per quote: ordinary Cashu deposit descriptions continue to use the BOLT11 d tag, while an opted-in quote uses the h tag.

Protocol proposal: cashubtc/nuts#417

The capability is currently enabled for FakeWallet, LND gRPC/REST, LNbits, and Blink. Other backends reject opted-in requests rather than returning an invoice with the wrong commitment.

Request example

{
  "amount": 21,
  "unit": "sat",
  "description": "<exact out-of-band data>",
  "description_hash": true
}

Testing

  • Ruff: all checks passed
  • mypy: 131 source files passed
  • targeted mint description tests: 3 passed (plaintext, hashed, missing-description rejection)
  • lnuts integration suite: 21 passed, including a NIP-57 client using the per-quote flag
  • live LND plaintext check: invoice retained its exact d description and had no h tag
  • live LND hashed check: invoice had no d tag and its h tag matched the exact description
  • live NIP-57 check: so@nuts.cash returned a 42-sat invoice whose h tag matched the exact signed kind-9734 JSON

@codecov

codecov Bot commented Aug 5, 2026

Copy link
Copy Markdown

❌ 2 Tests Failed:

Tests completed Failed Passed Skipped
820 2 818 91
View the top 2 failed test(s) by shortest run time
tests.mint.test_mint::test_mint_quote_description_hash
Stack Traces | 0.61s run time
ledger = <cashu.mint.ledger.Ledger object at 0x7f743d690440>

    @pytest.mark.asyncio
    async def test_mint_quote_description_hash(ledger: Ledger):
        description = '{"kind":9734,"content":"zap"}'
    
>       quote = await ledger.mint_quote(
            PostMintQuoteRequest(
                amount=8,
                unit="sat",
                description=description,
                description_hash=True,
            )
        )

tests/mint/test_mint.py:108: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <cashu.mint.ledger.Ledger object at 0x7f743d690440>
quote_request = PostMintQuoteRequest(unit='sat', amount=8, description='{"kind":9734,"content":"zap"}', description_hash=True, pubkey=None)

    async def mint_quote(self, quote_request: PostMintQuoteRequest) -> MintQuote:
        """Creates a mint quote and stores it in the database.
    
        Args:
            quote_request (PostMintQuoteRequest): Mint quote request.
    
        Raises:
            Exception: Quote creation failed.
    
        Returns:
            MintQuote: Mint quote object.
        """
        logger.trace("called request_mint")
        if not quote_request.amount > 0:
            raise TransactionError("amount must be positive")
        if (
            settings.mint_max_mint_bolt11_sat
            and quote_request.amount > settings.mint_max_mint_bolt11_sat
        ):
            raise TransactionAmountExceedsLimitError(
                f"Maximum mint amount is {settings.mint_max_mint_bolt11_sat} sat."
            )
        if settings.mint_bolt11_disable_mint:
            raise NotAllowedError("Minting with bolt11 is disabled.")
    
        unit, method = self._verify_and_get_unit_method(
            quote_request.unit, Method.bolt11.name
        )
    
        if (
            quote_request.description
            and not quote_request.description_hash
            and not self.backends[method][unit].supports_description
        ):
            raise NotAllowedError("Backend does not support descriptions.")
        if quote_request.description_hash and not quote_request.description:
            raise TransactionError(
                "description is required when description_hash is true"
            )
        if (
            quote_request.description_hash
            and not self.backends[method][unit].supports_description_hash
        ):
>           raise NotAllowedError("Backend does not support description hashes.")
E           cashu.core.errors.NotAllowedError: Backend does not support description hashes.

cashu/mint/ledger.py:353: NotAllowedError
tests.mint.test_mint_api::test_info
Stack Traces | 0.695s run time
ledger = <cashu.mint.ledger.Ledger object at 0x7f743c18fcb0>

    @pytest.mark.asyncio
    @pytest.mark.skipif(
        settings.debug_mint_only_deprecated,
        reason="settings.debug_mint_only_deprecated is set",
    )
    async def test_info(ledger: Ledger):
        response = httpx.get(f"{BASE_URL}/v1/info")
        assert response.status_code == 200, f"{response.url} {response.status_code}"
        assert ledger.pubkey
        assert response.json()["pubkey"] == ledger.pubkey.format().hex()
        info = GetInfoResponse(**response.json())
        assert info.nuts
        assert info.nuts[MINT_NUT]["disabled"] is False
        setting = MintMethodSetting.model_validate(info.nuts[MINT_NUT]["methods"][0])
        assert setting.method == "bolt11"
        assert setting.method_name == "bolt11"
        assert setting.unit == "sat"
        assert setting.options
        assert setting.options.description is True
>       assert setting.options.description_hash is True
E       AssertionError: assert False is True
E        +  where False = MintMethodBolt11OptionSetting(description=True, description_hash=False).description_hash
E        +    where MintMethodBolt11OptionSetting(description=True, description_hash=False) = MintMethodSetting(method='bolt11', unit='sat', method_name='bolt11', min_amount=None, max_amount=None, options=MintMethodBolt11OptionSetting(description=True, description_hash=False)).options

tests/mint/test_mint_api.py:66: AssertionError

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

@KvngMikey

KvngMikey commented Aug 10, 2026

Copy link
Copy Markdown
Member

@sotach1 a couple failed tests you should look at.

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.

2 participants