Skip to content

NUT-20: Signature on Mint Quote

optional

depends on: NUT-04


This 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.

[!CAUTION]

NUT-04 mint quotes without a public key can be minted by anyone who knows the mint quote id without providing a signature.

Mint quote

To 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.

POST https://mint.host:3338/v1/mint/quote/bolt11

The wallet of Alice includes the following PostMintQuoteBolt11Request data in its request:

{
  "amount": <int>,
  "unit": <str_enum["sat"]>,
  "description": <str|null>, // Optional
  "pubkey": <str|null> // Optional <-- New
}

with the requested amount,unit, and description according to NUT-04.

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.

[!IMPORTANT]

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.

Deterministic quote locking key derivation

Wallets can deterministically generate private keys for NUT-20 quote locking from their wallet seed.

The following BIP32 derivation path is used to derive the private key:

m/129373'/20'/0'/0'/{counter}

Where:

  • 129373' is the Cashu wallet derivation namespace.
  • 20' is the wallet derivation index for NUT-20 quote locking keys.
  • {counter} is an incrementing, non-hardened BIP32 child index.

The 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.

The 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.

The mint Bob then responds with a PostMintQuoteBolt11Response:

{
  "quote": <str>,
  "request": <str>,
  "unit":  <str_enum[UNIT]>,
  "amount_paid": <int>,
  "amount_issued": <int>,
  "updated_at": <int>,
  "state": <str_enum[STATE]>, // Deprecated, optional
  "expiry": <int>,
  "pubkey": <str|null> // Optional <-- New
}

The response is the same as the bolt11 mint quote response in NUT-23 except for pubkey which has been provided by the wallet in the previous request.

Example

Request of Alice with curl:

curl -X POST http://localhost:3338/v1/mint/quote/bolt11 -d '{"amount": 10, "unit": "sat", "pubkey": "03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac"}' -H "Content-Type: application/json"

Response of Bob:

{
  "quote": "9d745270-1405-46de-b5c5-e2762b4f5e00",
  "request": "lnbc100n1pj4apw9...",
  "amount_paid": 0,
  "amount_issued": 0,
  "updated_at": 1701704657,
  "state": "UNPAID",
  "expiry": 1701704757,
  "pubkey": "03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac"
}

Signing the mint request

Message aggregation

To authenticate a mint request, the signer commits to the quote ID and all BlindedMessages (the outputs, see NUT-00) of the PostMintBolt11Request, in the order they appear in the request. The message is built over raw bytes:

msg_to_sign = b"Cashu_MintQuoteSig_v1"
              || len32(quote) || quote
              || for each output i (in request order):
                   len32(amount_i) || amount_i
                   || len32(B_i)   || B_i

Where:

  • b"Cashu_MintQuoteSig_v1" is the domain-separation tag as raw ASCII bytes, not length-prefixed.
  • || denotes byte concatenation and len32(x) is the 32-bit (4-byte) big-endian length of the byte array x in bytes.
  • quote is the UTF-8 quote id from the PostMintQuoteBolt11Response.
  • amount_i is the output amount as canonical minimal big-endian bytes (e.g. 0 → empty byte array, 10x01, 2560x0100); 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).
  • 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.

Signature scheme

To 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 Schnorr signature on the SHA-256 hash of the message to sign as defined above.

Minting tokens

After 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).

POST https://mint.host:3338/v1/mint/bolt11

The wallet Alice includes the following PostMintBolt11Request data in its request

{
  "quote": <str>,
  "outputs": <Array[BlindedMessage]>,
  "signature": <str|null> <-- New
}

with the quote being the quote ID from the previous step and outputs being BlindedMessages as in NUT-04.

signature is the signature on the msg_to_sign as defined above.

The mint responds with a PostMintBolt11Response as in NUT-04 if all validations are successful.

Example

Request of Alice with curl:

curl -X POST https://mint.host:3338/v1/mint/bolt11 -H "Content-Type: application/json" -d \
'{
  "quote": "9d745270-1405-46de-b5c5-e2762b4f5e00",
  "outputs": [
    {
      "amount": 8,
      "id": "009a1f293253e41e",
      "B_": "035015e6d7ade60ba8426cefaf1832bbd27257636e44a76b922d78e79b47cb689d"
    },
    {
      "amount": 2,
      "id": "009a1f293253e41e",
      "B_": "0288d7649652d0a83fc9c966c969fb217f15904431e61a44b14999fabc1b5d9ac6"
    }
  ],
  "signature": "d9be080b33179387e504bb6991ea41ae0dd715e28b01ce9f63d57198a095bccc776874914288e6989e97ac9d255ac667c205fa8d90a211184b417b4ffdd24092"

}'

Response of Bob:

{
  "signatures": [
    {
      "id": "009a1f293253e41e",
      "amount": 2,
      "C_": "0224f1c4c564230ad3d96c5033efdc425582397a5a7691d600202732edc6d4b1ec"
    },
    {
      "id": "009a1f293253e41e",
      "amount": 8,
      "C_": "0277d1de806ed177007e5b94a8139343b6382e472c752a74e99949d511f7194f6c"
    }
  ]
}

Errors

If 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.

See Error Codes:

  • 20008: Mint quote with pubkey but no valid signature provided for mint request.
  • 20009: Mint quote requires pubkey but none given or invalid pubkey.

Settings

The 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) which in this case reads

{
  "20": {
    "supported": <bool>,
  }
}