{"id":"8ecff59933d64c446401de10e692d9c7db1f1bd11e686bae15262916f00d4f5d","pubkey":"d1678e7ef965374bbea308a1215609a78376dc158277a7d657680f9d5efd5c38","created_at":1783436689,"kind":30817,"tags":[["d","nut-20"],["title","NUT-20: Signature on Mint Quote"],["summary","Binding a mint quote to a public key, so only the holder of the matching secret key can redeem it."],["s","optional"],["t","cashu"],["t","ecash"],["t","nut"],["alt","A specification: NUT-20: Signature on Mint Quote"],["client","openspecs-import"],["published_at","1735157996"],["proxy","https://github.com/cashubtc/nuts/blob/a845dfc998abae501fc3419592d53dc995d34b12/20.md","web"],["x","0539022a60a04298c9b90b9cbc25b45e757854a24aefedc44ff8dfadbedc38e6"]],"content":"# NUT-20: Signature on Mint Quote\n\n`optional`\n\n`depends on: NUT-04`\n\n---\n\nThis NUT defines signature-based authentication for mint quote redemption. When requesting a mint quote, clients provide a public key. The mint will then require a valid signature from the corresponding secret key to process the mint operation.\n\n> [!CAUTION]\n>\n> [NUT-04][04] mint quotes without a public key can be minted by anyone who knows the mint quote id without providing a signature.\n\n## Mint quote\n\nTo request a mint quote, the wallet of `Alice` makes a `POST /v1/mint/quote/{method}` request where `method` is the payment method requested. We present an example with the `method` being `bolt11` here.\n\n```http\nPOST https://mint.host:3338/v1/mint/quote/bolt11\n```\n\nThe wallet of `Alice` includes the following `PostMintQuoteBolt11Request` data in its request:\n\n```json\n{\n  \"amount\": <int>,\n  \"unit\": <str_enum[\"sat\"]>,\n  \"description\": <str|null>, // Optional\n  \"pubkey\": <str|null> // Optional <-- New\n}\n```\n\nwith the requested `amount`,`unit`, and `description` according to [NUT-04][04].\n\n`pubkey` is the compressed secp256k1 public key (33 bytes, hex-encoded) that will be required for signature verification during the minting operation. The mint will only mint ecash after receiving a valid signature from the corresponding private key in the subsequent `PostMintRequest`.\n\n> [!IMPORTANT]\n>\n> **Privacy:** To prevent the mint from being able to link multiple mint quotes, wallets **SHOULD** generate a unique public key for each mint quote request.\n\n### Deterministic quote locking key derivation\n\nWallets can deterministically generate private keys for NUT-20 quote locking from their wallet seed.\n\nThe following BIP32 derivation path is used to derive the private key:\n\n```\nm/129373'/20'/0'/0'/{counter}\n```\n\nWhere:\n\n- `129373'` is the Cashu wallet derivation namespace.\n- `20'` is the wallet derivation index for NUT-20 quote locking keys.\n- `{counter}` is an incrementing, non-hardened BIP32 child index.\n\nThe wallet **SHOULD** use a new counter value for each mint quote request. The NUT-20 quote locking counter is independent from NUT-13 keyset counters and other wallet derivation counters.\n\nThe wallet uses the compressed secp256k1 public key derived from the private key as the quote `pubkey`. This allows a wallet to restore the private key needed to sign for unpaid or paid NUT-20 locked mint quotes during a recovery process.\n\nThe mint `Bob` then responds with a `PostMintQuoteBolt11Response`:\n\n```json\n{\n  \"quote\": <str>,\n  \"request\": <str>,\n  \"unit\":  <str_enum[UNIT]>,\n  \"amount_paid\": <int>,\n  \"amount_issued\": <int>,\n  \"updated_at\": <int>,\n  \"state\": <str_enum[STATE]>, // Deprecated, optional\n  \"expiry\": <int>,\n  \"pubkey\": <str|null> // Optional <-- New\n}\n```\n\nThe response is the same as the `bolt11` mint quote response in [NUT-23][23] except for `pubkey` which has been provided by the wallet in the previous request.\n\n## Example\n\nRequest of `Alice` with curl:\n\n```bash\ncurl -X POST http://localhost:3338/v1/mint/quote/bolt11 -d '{\"amount\": 10, \"unit\": \"sat\", \"pubkey\": \"03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac\"}' -H \"Content-Type: application/json\"\n```\n\nResponse of `Bob`:\n\n```json\n{\n  \"quote\": \"9d745270-1405-46de-b5c5-e2762b4f5e00\",\n  \"request\": \"lnbc100n1pj4apw9...\",\n  \"amount_paid\": 0,\n  \"amount_issued\": 0,\n  \"updated_at\": 1701704657,\n  \"state\": \"UNPAID\",\n  \"expiry\": 1701704757,\n  \"pubkey\": \"03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac\"\n}\n```\n\n## Signing the mint request\n\n### Message aggregation\n\nTo authenticate a mint request, the signer commits to the quote ID and all `BlindedMessages` (the outputs, see [NUT-00][00]) of the `PostMintBolt11Request`, in the order they appear in the request. The message is built over raw bytes:\n\n```\nmsg_to_sign = b\"Cashu_MintQuoteSig_v1\"\n              || len32(quote) || quote\n              || for each output i (in request order):\n                   len32(amount_i) || amount_i\n                   || len32(B_i)   || B_i\n```\n\nWhere:\n\n- `b\"Cashu_MintQuoteSig_v1\"` is the domain-separation tag as raw ASCII bytes, not length-prefixed.\n- `||` denotes byte concatenation and `len32(x)` is the 32-bit (4-byte) big-endian length of the byte array `x` in bytes.\n- `quote` is the UTF-8 quote id from the `PostMintQuoteBolt11Response`.\n- `amount_i` is the output amount as canonical minimal big-endian bytes (e.g. `0` → empty byte array, `1` → `0x01`, `256` → `0x0100`); thus `len32(amount_i)` is its length in bytes as a 32-bit integer (e.g. `0` for amount `0`, `1` for amount `1`, `2` for amount `256`).\n- `B_i` is the raw byte representation of the blinded message (e.g. 33-byte compressed secp256k1 or 48-byte BLS12-381 point), decoded from the request's hex string.\n\n### Signature scheme\n\nTo mint a quote where a public key was provided, the wallet includes a signature on `msg_to_sign` in the `PostMintBolt11Request`. We use a [BIP340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki) Schnorr signature on the SHA-256 hash of the message to sign as defined above.\n\n## Minting tokens\n\nAfter requesting a mint quote and paying the request, the wallet proceeds with minting new tokens by calling the `POST /v1/mint/{method}` endpoint where `method` is the payment method requested (here `bolt11`).\n\n```http\nPOST https://mint.host:3338/v1/mint/bolt11\n```\n\nThe wallet `Alice` includes the following `PostMintBolt11Request` data in its request\n\n```json\n{\n  \"quote\": <str>,\n  \"outputs\": <Array[BlindedMessage]>,\n  \"signature\": <str|null> <-- New\n}\n```\n\nwith the `quote` being the quote ID from the previous step and `outputs` being `BlindedMessages` as in [NUT-04][04].\n\n`signature` is the signature on the `msg_to_sign` as defined above.\n\nThe mint responds with a `PostMintBolt11Response` as in [NUT-04][04] if all validations are successful.\n\n## Example\n\nRequest of `Alice` with curl:\n\n```bash\ncurl -X POST https://mint.host:3338/v1/mint/bolt11 -H \"Content-Type: application/json\" -d \\\n'{\n  \"quote\": \"9d745270-1405-46de-b5c5-e2762b4f5e00\",\n  \"outputs\": [\n    {\n      \"amount\": 8,\n      \"id\": \"009a1f293253e41e\",\n      \"B_\": \"035015e6d7ade60ba8426cefaf1832bbd27257636e44a76b922d78e79b47cb689d\"\n    },\n    {\n      \"amount\": 2,\n      \"id\": \"009a1f293253e41e\",\n      \"B_\": \"0288d7649652d0a83fc9c966c969fb217f15904431e61a44b14999fabc1b5d9ac6\"\n    }\n  ],\n  \"signature\": \"d9be080b33179387e504bb6991ea41ae0dd715e28b01ce9f63d57198a095bccc776874914288e6989e97ac9d255ac667c205fa8d90a211184b417b4ffdd24092\"\n\n}'\n```\n\nResponse of `Bob`:\n\n```json\n{\n  \"signatures\": [\n    {\n      \"id\": \"009a1f293253e41e\",\n      \"amount\": 2,\n      \"C_\": \"0224f1c4c564230ad3d96c5033efdc425582397a5a7691d600202732edc6d4b1ec\"\n    },\n    {\n      \"id\": \"009a1f293253e41e\",\n      \"amount\": 8,\n      \"C_\": \"0277d1de806ed177007e5b94a8139343b6382e472c752a74e99949d511f7194f6c\"\n    }\n  ]\n}\n```\n\n## Errors\n\nIf the wallet user `Alice` does not include a signature on the `PostMintBolt11Request` but did include a `pubkey` in the `PostMintBolt11QuoteRequest` then `Bob` **MUST** respond with an error. `Alice` **CAN** repeat the request with a valid signature.\n\nSee [Error Codes][errors]:\n\n- `20008`: Mint quote with `pubkey` but no valid `signature` provided for mint request.\n- `20009`: Mint quote requires `pubkey` but none given or invalid `pubkey`.\n\n## Settings\n\nThe settings for this NUT indicate the support for requiring a signature before minting. They are part of the info response of the mint ([NUT-06][06]) which in this case reads\n\n```json\n{\n  \"20\": {\n    \"supported\": <bool>,\n  }\n}\n```\n\n[00]: nostr:naddr1qvzqqqrcvypzp5t83el0jefhfwl2xz9py9tqnfurwmwptqnh5lt9w6q0n4006hpcqqrxuat595crqqpd6d9\n[04]: nostr:naddr1qvzqqqrcvypzp5t83el0jefhfwl2xz9py9tqnfurwmwptqnh5lt9w6q0n4006hpcqqrxuat595crg74fknc\n[23]: nostr:naddr1qvzqqqrcvypzp5t83el0jefhfwl2xz9py9tqnfurwmwptqnh5lt9w6q0n4006hpcqqrxuat595erx7v8gg2\n[06]: nostr:naddr1qvzqqqrcvypzp5t83el0jefhfwl2xz9py9tqnfurwmwptqnh5lt9w6q0n4006hpcqqrxuat595crv3ltsuz\n[errors]: https://github.com/cashubtc/nuts/blob/a845dfc998abae501fc3419592d53dc995d34b12/error_codes.md\n","sig":"5853ee3407abdb32f15f2a584a73722c28e67fd1b91bdca74f0a400bc8cd0354d69ec8b017065f94dd10502eb7ce8e7dcc9ca321f788c907b42fd1312da184b3"}