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 andlen32(x)is the 32-bit (4-byte) big-endian length of the byte arrayxin bytes.quoteis the UTF-8 quote id from thePostMintQuoteBolt11Response.amount_iis the output amount as canonical minimal big-endian bytes (e.g.0→ empty byte array,1→0x01,256→0x0100); thuslen32(amount_i)is its length in bytes as a 32-bit integer (e.g.0for amount0,1for amount1,2for amount256).B_iis 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 withpubkeybut no validsignatureprovided for mint request.20009: Mint quote requirespubkeybut none given or invalidpubkey.
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>,
}
}