Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion 04.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ The mint `Bob` responds with a quote that includes some common fields for all me
}
```

Where `quote` is the quote ID string in UUID v7 format, `request` is the payment request for the quote, and `unit` corresponds to the value provided in the request.
Where `quote` is the quote ID, `request` is the payment request for the quote, and `unit` corresponds to the value provided in the request. The `quote` id **MUST** be a [UUIDv7](https://www.rfc-editor.org/rfc/rfc9562).

> [!CAUTION]
>
Expand Down
24 changes: 14 additions & 10 deletions 20.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ The mint `Bob` then responds with a `PostMintQuoteBolt11Response`:
{
"quote": <str>,
"request": <str>,
"unit": <str_enum[UNIT]>,
"state": <str_enum[STATE]>,
"expiry": <int>,
"pubkey": <str|null> // Optional <-- New
Expand Down Expand Up @@ -77,19 +78,22 @@ Response of `Bob`:

### Message aggregation

To provide a signature for a mint request, the owner of the signing public keys must concatenate the quote ID `quote` in `PostMintQuoteBolt11Response` and the `B_` fields of all `BlindedMessages` in the `PostMintBolt11Request` (i.e., the outputs, see [NUT-00][00]) to a single message string in the order they appear in the `PostMintRequest`. This concatenated string is then hashed and signed (see [Signature scheme](#signature-scheme)).

> [!NOTE]
>
> Concatenating the quote ID and the outputs into a single message prevents maliciously replacing the outputs.

If a request has `n` outputs, the message to sign becomes:
To 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:

```
msg_to_sign = quote || B_0 || ... || B_(n-1)
msg_to_sign = "Cashu_MintQuoteSig_v1" // domain-separation tag (ASCII, not length-prefixed)
|| len32(quote) || quote // quote = UTF-8 quote id
|| for each output i (in request order):
len32(amount_i) || amount_i // amount = canonical minimal big-endian
Comment thread
a1denvalu3 marked this conversation as resolved.
|| len32(B_i) || B_i // B_ = raw compressed point bytes
```

Where `||` denotes concatenation, `quote` is the UTF-8 quote id in `PostMintQuoteBolt11Response`, and each `B_n` is a UTF-8 encoded hex string of the outputs in the `PostMintBolt11Request`.
Where:

- `||` 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, `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`).
- `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

Expand All @@ -115,7 +119,7 @@ The wallet `Alice` includes the following `PostMintBolt11Request` data in its re

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

`signature` is the signature on the `msg_to_sign` which is the concatenated quote id and the outputs as defined above.
`signature` is the signature on the `msg_to_sign` as defined above.

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

Expand Down
20 changes: 13 additions & 7 deletions 29.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ Once all quoted payments are confirmed, the wallet mints the proofs by calling:
POST https://mint.host:3338/v1/mint/{method}/batch
```

The batch endpoint is method-specific: every quote in the batch **MUST** be for the same payment `method` as the `{method}` in the URL, and a batch that mixes payment methods **MUST** be rejected by the mint.

The wallet includes the following body in its request:

```json
Expand Down Expand Up @@ -213,19 +215,23 @@ Per [NUT-20][20], quotes can require authentication via signatures. When using b

#### Signature Message

Following the [NUT-20 message aggregation][20-msg-agg] pattern, the signature for `quotes[i]` is computed as:
Each locked quote is signed independently exactly as in [NUT-20][20]: `signatures[i]` is the NUT-20 signature over `quotes[i]` and the request's `outputs`. The batch `outputs` are a single consolidated set (not partitioned per quote), so each signature is computed over the full `outputs` array.

Following the [NUT-20 message aggregation][20-msg-agg] pattern, the signature message for `quotes[i]` is computed as:

```
msg_to_sign = quote_id[i] || B_0 || B_1 || ... || B_(n-1)
msg_to_sign = "Cashu_MintQuoteSig_v1" // domain-separation tag (ASCII, not length-prefixed)
|| len32(quotes[i]) || quotes[i] // quotes[i] = UTF-8 quote id at index i
|| for each output j (in request order):
len32(amount_j) || amount_j // amount_j = canonical minimal big-endian
|| len32(B_j) || B_j // B_j = raw compressed point bytes
```

Where:

- `quote_id[i]` is the UTF-8 encoded quote ID at index `i`
- `B_0 ... B_(n-1)` are **all blinded messages** from the `outputs` array (regardless of amount splitting)
- `||` denotes concatenation

The signature is a BIP340 Schnorr signature on the SHA-256 hash of `msg_to_sign`.
- `quotes[i]` is the UTF-8 encoded quote ID from the request's `quotes` array at index `i`
- `outputs` are **all blinded messages** from the request's `outputs` array (regardless of which quote they correspond to)
- `||` denotes byte concatenation and `len32(x)` is the 32-bit (4-byte) big-endian length of the following data `x`.

### Signature Validation Failure

Expand Down
70 changes: 10 additions & 60 deletions tests/20-test.md
Original file line number Diff line number Diff line change
@@ -1,79 +1,29 @@
# NUT-20 Test Vectors

The following is a `PostMintBolt11Request` with a valid signature. Where the `pubkey` in the `PostMintQuoteBolt11Request` is `03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac`.
The following is a `PostMintBolt11Request` with a valid signature, where the `pubkey` in the `PostMintQuoteBolt11Response` is `0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798` (secret key `0x01`).

```json
{
"quote": "9d745270-1405-46de-b5c5-e2762b4f5e00",
"quote": "0192d3c0-7e8a-7c3d-8e9f-1a2b3c4d5e6f",
"outputs": [
{
"amount": 1,
"id": "00456a94ab4e1c46",
"B_": "0342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834"
"id": "009a1f293253e41e",
"B_": "036d6caac248af96f6afa7f904f550253a0f3ef3f5aa2fe6838a95b216691468e2"
},
{
"amount": 1,
"id": "00456a94ab4e1c46",
"B_": "032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4"
},
{
"amount": 1,
"id": "00456a94ab4e1c46",
"B_": "033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c311"
},
{
"amount": 1,
"id": "00456a94ab4e1c46",
"B_": "02be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b53"
},
{
"amount": 1,
"id": "00456a94ab4e1c46",
"B_": "02209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79"
"id": "009a1f293253e41e",
"B_": "021f8a566c205633d029094747d2e18f44e05993dda7a5f88f496078205f656e59"
}
],
"signature": "d4b386f21f7aa7172f0994ee6e4dd966539484247ea71c99b81b8e09b1bb2acbc0026a43c221fd773471dc30d6a32b04692e6837ddaccf0830a63128308e4ee0"
"signature": "4881093a332ff7c79f3e598ce5b249d64978b47165a0b19c18adf0ced0246228e61e702f0abaf1bf27b92be4336bdbabacfbe4c914076386b3c66fdcd0b3480e"
}
```

The following is the expected message to sign on the above `PostMintBolt11Request`.
The corresponding `msg_to_sign` (hex) and its SHA-256 hash are:

```
[57, 100, 55, 52, 53, 50, 55, 48, 45, 49, 52, 48, 53, 45, 52, 54, 100, 101, 45, 98, 53, 99, 53, 45, 101, 50, 55, 54, 50, 98, 52, 102, 53, 101, 48, 48, 48, 51, 52, 50, 101, 53, 98, 99, 99, 55, 55, 102, 53, 98, 50, 97, 51, 99, 50, 97, 102, 98, 52, 48, 98, 98, 53, 57, 49, 97, 49, 101, 50, 55, 100, 97, 56, 51, 99, 100, 100, 99, 57, 54, 56, 97, 98, 100, 99, 48, 101, 99, 52, 57, 48, 52, 50, 48, 49, 97, 50, 48, 49, 56, 51, 52, 48, 51, 50, 102, 100, 51, 99, 52, 100, 99, 52, 57, 97, 50, 56, 52, 52, 97, 56, 57, 57, 57, 56, 100, 53, 101, 57, 100, 53, 98, 48, 102, 48, 98, 48, 48, 100, 100, 101, 57, 51, 49, 48, 48, 54, 51, 97, 99, 98, 56, 97, 57, 50, 101, 50, 102, 100, 97, 102, 97, 52, 49, 50, 54, 100, 52, 48, 51, 51, 98, 54, 102, 100, 101, 53, 48, 98, 54, 97, 48, 100, 102, 101, 54, 49, 97, 100, 49, 52, 56, 102, 102, 102, 49, 54, 55, 97, 100, 57, 99, 102, 56, 51, 48, 56, 100, 101, 100, 53, 102, 54, 102, 54, 98, 50, 102, 101, 48, 48, 48, 97, 48, 51, 54, 99, 52, 54, 52, 99, 51, 49, 49, 48, 50, 98, 101, 53, 97, 53, 53, 102, 48, 51, 101, 53, 99, 48, 97, 97, 101, 97, 55, 55, 53, 57, 53, 100, 53, 55, 52, 98, 99, 101, 57, 50, 99, 54, 100, 53, 55, 97, 50, 97, 48, 102, 98, 50, 98, 53, 57, 53, 53, 99, 48, 98, 56, 55, 101, 52, 53, 50, 48, 101, 48, 54, 98, 53, 51, 48, 50, 50, 48, 57, 102, 99, 50, 56, 55, 51, 102, 50, 56, 53, 50, 49, 99, 98, 100, 100, 101, 55, 102, 55, 98, 51, 98, 98, 49, 53, 50, 49, 48, 48, 50, 52, 54, 51, 102, 53, 57, 55, 57, 54, 56, 54, 102, 100, 49, 53, 54, 102, 50, 51, 102, 101, 54, 97, 56, 97, 97, 50, 98, 55, 57]
```

The following is a `PostMintBolt11Request` with an invalid signature. Where the `pubkey` in the `PostMintQuoteBolt11Request` is `03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac`.

```json
{
"quote": "9d745270-1405-46de-b5c5-e2762b4f5e00",
"outputs": [
{
"amount": 1,
"id": "00456a94ab4e1c46",
"B_": "0342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834"
},
{
"amount": 1,
"id": "00456a94ab4e1c46",
"B_": "032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4"
},
{
"amount": 1,
"id": "00456a94ab4e1c46",
"B_": "033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c311"
},
{
"amount": 1,
"id": "00456a94ab4e1c46",
"B_": "02be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b53"
},
{
"amount": 1,
"id": "00456a94ab4e1c46",
"B_": "02209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79"
}
],
"signature": "cb2b8e7ea69362dfe2a07093f2bbc319226db33db2ef686c940b5ec976bcbfc78df0cd35b3e998adf437b09ee2c950bd66dfe9eb64abd706e43ebc7c669c36c3"
}
msg_to_sign = 43617368755f4d696e7451756f74655369675f76310000002430313932643363302d376538612d376333642d386539662d316132623363346435653666000000010100000021036d6caac248af96f6afa7f904f550253a0f3ef3f5aa2fe6838a95b216691468e2000000010100000021021f8a566c205633d029094747d2e18f44e05993dda7a5f88f496078205f656e59
sha256(msg_to_sign) = c164fd384879f74ab6ea2e7cf13d90ed42e6df9d5de607eeb5c9cc7d36fb1c21
```
12 changes: 6 additions & 6 deletions tests/29-tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,16 +134,16 @@ Expected behavior:
The following is a valid NUT-20 batch mint request where the signature correctly covers all outputs in order. The quote has pubkey `0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798` (sk = 1).

```shell
quote: "019e6d5a-2347-7000-80fe-07ae8fa79774"
quote: "locked-quote"
pubkey: 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
msg_to_sign_bytes: utf8("019e6d5a-2347-7000-80fe-07ae8fa79774") || B0 || B1
msg_hash: 5ac550d5416e81c613b58e3f1fb095390fb828b55e8991fd9de231ca8e31e859
signature[0]: 9408920d0b94cee5eb6df20f14d2a655e7ce2ce309dc1f1aeb69b219efe76716933b2206eba3a54f9a953c92edaa922ab3e6912e02383dda42a193409567a0dc
msg_to_sign_bytes: 43617368755f4d696e7451756f74655369675f76310000000c6c6f636b65642d71756f7465000000010100000021036d6caac248af96f6afa7f904f550253a0f3ef3f5aa2fe6838a95b216691468e2000000010100000021021f8a566c205633d029094747d2e18f44e05993dda7a5f88f496078205f656e59
msg_hash: 03dc68d6617bba502d8648efd0965bf393841082cf04fd03e5de4bcb5777cdfc
signature[0]: a913e48177027d87e0e38c6f2021763c46997ff4866a4b63ebca800b0776b28519eab37377cf9bc1869e489d7b25747b7a998eaa1c33c2cac7fa168449d8267a
```

```json
{
"quotes": ["019e6d5a-2347-7000-80fe-07ae8fa79774"],
"quotes": ["locked-quote"],
"outputs": [
{
"amount": 1,
Expand All @@ -157,7 +157,7 @@ signature[0]: 9408920d0b94cee5eb6df20f14d2a655e7ce2ce309dc1f1aeb69b219efe7671693
}
],
"signatures": [
"9408920d0b94cee5eb6df20f14d2a655e7ce2ce309dc1f1aeb69b219efe76716933b2206eba3a54f9a953c92edaa922ab3e6912e02383dda42a193409567a0dc"
"a913e48177027d87e0e38c6f2021763c46997ff4866a4b63ebca800b0776b28519eab37377cf9bc1869e489d7b25747b7a998eaa1c33c2cac7fa168449d8267a"
]
}
```
Loading