NUT-XX: Get quotes by pubkeys #341

Open
callebtc wants to merge 12 commits from get-quotes-by-pubkeys into main
callebtc commented 2026-02-16 12:38:36 +00:00 (Migrated from github.com)

supersedes #329
Get NUT-20 quotes by pubkeys. Requires signatures to prove possession of the corresponding private keys.

supersedes #329 Get NUT-20 quotes by `pubkeys`. Requires signatures to prove possession of the corresponding private keys.
robwoodgate (Migrated from github.com) requested changes 2026-02-16 13:48:43 +00:00
robwoodgate (Migrated from github.com) left a comment

The signature scheme is a bit worrying, but otherwise just some nit/questions for clarity.

The signature scheme is a bit worrying, but otherwise just some nit/questions for clarity.
@ -0,0 +1,60 @@
# NUT-29: Mint Quote Lookup by Public Key
robwoodgate (Migrated from github.com) commented 2026-02-16 13:35:57 +00:00

28 now taken... 29?

  "29": {
28 now taken... 29? ```suggestion "29": { ```
robwoodgate (Migrated from github.com) commented 2026-02-16 13:42:18 +00:00

28 now taken... 29?

# NUT-29: Mint Quote Lookup by Public Key
28 now taken... 29? ```suggestion # NUT-29: Mint Quote Lookup by Public Key ```
@ -0,0 +21,4 @@
```json
{
"pubkeys": <Array[str]>,
"pubkey_signatures": <Array[str]>
robwoodgate (Migrated from github.com) commented 2026-02-16 13:47:34 +00:00

Would it be useful to group quotes by pubkey?

Would it be useful to group quotes by pubkey?
@ -0,0 +28,4 @@
- `pubkeys` is an array of hex-encoded compressed secp256k1 NUT-20 public keys (33 bytes each)
- `pubkey_signatures` is an array of hex-encoded Schnorr signatures on `pubkeys` in the same order (64 bytes each)
The wallet **MUST** provide a valid signature in `pubkey_signatures` for each public key in `pubkeys` with the corresponding private key in the same order as the `pubkeys` array. The message to sign is the byte representation of the public key.
robwoodgate (Migrated from github.com) commented 2026-02-16 13:33:38 +00:00

I think signature on the plain public key is pretty risky, as it allows signature replay in any "proof of key ownership" context, or even for future uses on other endpoints and other mints.

It would be better defined as a signature on a domain separated message like we do in P2BK / keyset v2

eg: SHA-256( b"Cashu_NUTXX_QUOTE_v1" || MINT_URL || method || pubkey )

or

eg: SHA-256( b"Cashu_NUTXX_QUOTE_v1" || NONCE || pubkey )

where NONCE is a random shared nonce, supplied in a nonce param:

{
  "pubkeys": <Array[str]>,
  "pubkey_signatures": <Array[str]>,
  "nonce": str
}

)

I think signature on the plain public key is pretty risky, as it allows signature replay in any "proof of key ownership" context, or even for future uses on other endpoints and other mints. It would be better defined as a signature on a domain separated message like we do in P2BK / keyset v2 ``` eg: SHA-256( b"Cashu_NUTXX_QUOTE_v1" || MINT_URL || method || pubkey ) ``` or ``` eg: SHA-256( b"Cashu_NUTXX_QUOTE_v1" || NONCE || pubkey ) where NONCE is a random shared nonce, supplied in a nonce param: { "pubkeys": <Array[str]>, "pubkey_signatures": <Array[str]>, "nonce": str } ``` )
@ -0,0 +39,4 @@
"quotes": <Array[MintQuoteResponse]>
}
```
robwoodgate (Migrated from github.com) commented 2026-02-16 13:37:30 +00:00

We should probably specify what happens if any signature is invalid. Does entire op fail, or does mint return only the valid ones? What happens if more/fewer sigs are provided than pubkeys?

We should probably specify what happens if any signature is invalid. Does entire op fail, or does mint return only the valid ones? What happens if more/fewer sigs are provided than pubkeys?
thesimplekid (Migrated from github.com) reviewed 2026-02-16 13:51:40 +00:00
@ -0,0 +21,4 @@
```json
{
"pubkeys": <Array[str]>,
"pubkey_signatures": <Array[str]>
thesimplekid (Migrated from github.com) commented 2026-02-16 13:51:39 +00:00

in the mint response? I think best to not define the ordering and let the wallet figure it out.

in the mint response? I think best to not define the ordering and let the wallet figure it out.
robwoodgate (Migrated from github.com) reviewed 2026-02-16 13:56:43 +00:00
@ -0,0 +21,4 @@
```json
{
"pubkeys": <Array[str]>,
"pubkey_signatures": <Array[str]>
robwoodgate (Migrated from github.com) commented 2026-02-16 13:56:43 +00:00

Yeah, I don't mind, a flat array is easy to sort by a wallet. Simpler response. And pubkey is in the quote.

Yeah, I don't mind, a flat array is easy to sort by a wallet. Simpler response. And pubkey is in the quote.
thesimplekid (Migrated from github.com) reviewed 2026-02-16 17:41:11 +00:00
@ -0,0 +39,4 @@
"quotes": <Array[MintQuoteResponse]>
}
```
thesimplekid (Migrated from github.com) commented 2026-02-16 17:41:11 +00:00

I think we should match the plan for batched minting where if any are invalid the whole op fails.

I think we should match the plan for batched minting where if any are invalid the whole op fails.
robwoodgate (Migrated from github.com) reviewed 2026-02-17 15:23:53 +00:00
@ -0,0 +39,4 @@
"quotes": <Array[MintQuoteResponse]>
}
```
robwoodgate (Migrated from github.com) commented 2026-02-17 15:23:53 +00:00

Agree. It lowers the abuse surface too, because a mint can reject at first invalid signature (or on mismatch key/sig count), rather than continue processing.

Agree. It lowers the abuse surface too, because a mint can reject at first invalid signature (or on mismatch key/sig count), rather than continue processing.
robwoodgate (Migrated from github.com) reviewed 2026-02-17 15:28:41 +00:00
@ -0,0 +28,4 @@
- `pubkeys` is an array of hex-encoded compressed secp256k1 NUT-20 public keys (33 bytes each)
- `pubkey_signatures` is an array of hex-encoded Schnorr signatures on `pubkeys` in the same order (64 bytes each)
The wallet **MUST** provide a valid signature in `pubkey_signatures` for each public key in `pubkeys` with the corresponding private key in the same order as the `pubkeys` array. The message to sign is the byte representation of the public key.
robwoodgate (Migrated from github.com) commented 2026-02-17 15:28:41 +00:00
The wallet **MUST** provide a valid signature in `pubkey_signatures` for each public key in `pubkeys` with the corresponding private key in the same order as the `pubkeys` array. The message to sign is the byte representation of the public key.

### Signature Validation Failure

If there is a mismatch between signature and pubkey counts, or **any signature is invalid**, the mint MUST reject the request and return an error.
```suggestion The wallet **MUST** provide a valid signature in `pubkey_signatures` for each public key in `pubkeys` with the corresponding private key in the same order as the `pubkeys` array. The message to sign is the byte representation of the public key. ### Signature Validation Failure If there is a mismatch between signature and pubkey counts, or **any signature is invalid**, the mint MUST reject the request and return an error. ```
robwoodgate (Migrated from github.com) reviewed 2026-02-17 15:31:42 +00:00
@ -0,0 +39,4 @@
"quotes": <Array[MintQuoteResponse]>
}
```
robwoodgate (Migrated from github.com) commented 2026-02-17 15:31:42 +00:00

I added a suggestion to the request section above, but it could be moved to response section if preferred.

I added a suggestion to the request section above, but it could be moved to response section if preferred.
callebtc (Migrated from github.com) reviewed 2026-02-26 15:21:26 +00:00
@ -0,0 +28,4 @@
- `pubkeys` is an array of hex-encoded compressed secp256k1 NUT-20 public keys (33 bytes each)
- `pubkey_signatures` is an array of hex-encoded Schnorr signatures on `pubkeys` in the same order (64 bytes each)
The wallet **MUST** provide a valid signature in `pubkey_signatures` for each public key in `pubkeys` with the corresponding private key in the same order as the `pubkeys` array. The message to sign is the byte representation of the public key.
callebtc (Migrated from github.com) commented 2026-02-26 15:21:26 +00:00

good point!

good point!
a1denvalu3 (Migrated from github.com) approved these changes 2026-03-19 13:51:05 +00:00
d4rp4t (Migrated from github.com) approved these changes 2026-03-19 19:07:34 +00:00
TheMhv (Migrated from github.com) reviewed 2026-03-31 13:25:08 +00:00
@ -0,0 +1,60 @@
# NUT-29: Mint Quote Lookup by Public Key
TheMhv (Migrated from github.com) commented 2026-03-31 13:25:08 +00:00

This needs a new NUT number, NUT-29 is already defined in NUT-29: Batched Minting

This needs a new NUT number, NUT-29 is already defined in [NUT-29: Batched Minting](https://github.com/cashubtc/nuts/blob/main/29.md)
TheMhv (Migrated from github.com) reviewed 2026-03-31 13:27:48 +00:00
@ -0,0 +48,4 @@
```json
{
"29": {
TheMhv (Migrated from github.com) commented 2026-03-31 13:27:49 +00:00

This needs a new NUT number, NUT-29 is already defined in NUT-29: Batched Minting

Same thing here

> This needs a new NUT number, NUT-29 is already defined in [NUT-29: Batched Minting](https://github.com/cashubtc/nuts/blob/main/29.md) Same thing here
thesimplekid (Migrated from github.com) reviewed 2026-03-31 13:39:51 +00:00
@ -0,0 +1,60 @@
# NUT-29: Mint Quote Lookup by Public Key
thesimplekid (Migrated from github.com) commented 2026-03-31 13:39:51 +00:00

Generally we us XX and just define before merge

Generally we us XX and just define before merge
a1denvalu3 (Migrated from github.com) reviewed 2026-04-10 17:16:59 +00:00
@ -0,0 +28,4 @@
- `pubkeys` is an array of hex-encoded compressed secp256k1 NUT-20 public keys (33 bytes each)
- `pubkey_signatures` is an array of hex-encoded Schnorr signatures on `pubkeys` in the same order (64 bytes each)
The wallet **MUST** provide a valid signature in `pubkey_signatures` for each public key in `pubkeys` with the corresponding private key in the same order as the `pubkeys` array. The message to sign is the byte representation of the public key.
a1denvalu3 (Migrated from github.com) commented 2026-04-10 17:16:59 +00:00

A static schnorr signature allows replay attacks: if I observe a signature i can use it to query the status of a quote again and again.

A static schnorr signature allows replay attacks: if I observe a signature i can use it to query the status of a quote again and again.
TheMhv (Migrated from github.com) requested changes 2026-04-11 12:48:32 +00:00
TheMhv (Migrated from github.com) left a comment

Following the suggestion from @robwoodgate :

My suggestion is create a GET request for the same path to mint generate a nonce that wallet can be use for sign a hash message, this will prevent replay attacks.

Following the [suggestion](https://github.com/cashubtc/nuts/pull/341#discussion_r2812465774) from @robwoodgate : My suggestion is create a GET request for the same path to mint generate a nonce that wallet can be use for sign a hash message, this will prevent replay attacks.
@ -0,0 +10,4 @@
## Request
To query quotes assigned to a public key, the wallet makes a `POST /v1/mint/quote/{method}/pubkey` request.
TheMhv (Migrated from github.com) commented 2026-04-11 12:33:45 +00:00
To query quotes assigned to a public key, the wallet needs to
1. Get a nonce from `GET /v1/mint/quote/{method}/pubkey` request. 
2. Makes a `POST /v1/mint/quote/{method}/pubkey` request.
```suggestion To query quotes assigned to a public key, the wallet needs to 1. Get a nonce from `GET /v1/mint/quote/{method}/pubkey` request. 2. Makes a `POST /v1/mint/quote/{method}/pubkey` request. ```
@ -0,0 +29,4 @@
- `pubkey_signatures` is an array of hex-encoded Schnorr signatures on `pubkeys` in the same order (64 bytes each)
The wallet **MUST** provide a valid signature in `pubkey_signatures` for each public key in `pubkeys` with the corresponding private key in the same order as the `pubkeys` array. The message to sign is the byte representation of the public key.
TheMhv (Migrated from github.com) commented 2026-04-11 12:37:53 +00:00
```json
{
  "pubkeys": <Array[str]>,
  "pubkey_signatures": <Array[str]>,
  "nonce": <hex_str>
}
```

- `pubkeys` is an array of hex-encoded compressed secp256k1 NUT-20 public keys (33 bytes each)
- `pubkey_signatures` is an array of hex-encoded Schnorr signatures on `pubkeys` in the same order (64 bytes each)
- `nonce` is an hex-encoded nonce fetched from previous request.

The wallet **MUST** provide a valid signature in `pubkey_signatures` for each public key in `pubkeys` with the corresponding private key in the same order as the `pubkeys` array. The message to sign is the byte representation of the following hash:

```
SHA-256( DOMAIN_SEPARATOR || nonce || pubkey)
```

Where:
- `DOMAIN_SEPARATOR` constant byte string `b"Cashu_MINTQUOTES_v1"`
- `nonce` is the previous nonce fetched by `GET` request
- `pubkey` is the byte representation of the public key
- `||` denotes concatenation
````suggestion ```json { "pubkeys": <Array[str]>, "pubkey_signatures": <Array[str]>, "nonce": <hex_str> } ``` - `pubkeys` is an array of hex-encoded compressed secp256k1 NUT-20 public keys (33 bytes each) - `pubkey_signatures` is an array of hex-encoded Schnorr signatures on `pubkeys` in the same order (64 bytes each) - `nonce` is an hex-encoded nonce fetched from previous request. The wallet **MUST** provide a valid signature in `pubkey_signatures` for each public key in `pubkeys` with the corresponding private key in the same order as the `pubkeys` array. The message to sign is the byte representation of the following hash: ``` SHA-256( DOMAIN_SEPARATOR || nonce || pubkey) ``` Where: - `DOMAIN_SEPARATOR` constant byte string `b"Cashu_MINTQUOTES_v1"` - `nonce` is the previous nonce fetched by `GET` request - `pubkey` is the byte representation of the public key - `||` denotes concatenation ````
@ -0,0 +32,4 @@
## Response
The mint responds with a `PostMintQuotesByPubkeyResponse`:
TheMhv (Migrated from github.com) commented 2026-04-11 12:45:34 +00:00
On GET request, the mint responds with a `GetMintQuotesByPubkeyResponse`:
```json
{
  "nonce": <hex_str>
}
```

On POST request, the mint responds with a `PostMintQuotesByPubkeyResponse`:
````suggestion On GET request, the mint responds with a `GetMintQuotesByPubkeyResponse`: ```json { "nonce": <hex_str> } ``` On POST request, the mint responds with a `PostMintQuotesByPubkeyResponse`: ````
a1denvalu3 commented 2026-04-17 10:12:24 +00:00 (Migrated from github.com)

I've opened a PR with the updated signature logic and test vectors here: #363

I've opened a PR with the updated signature logic and test vectors here: #363
robwoodgate (Migrated from github.com) reviewed 2026-04-17 16:10:34 +00:00
@ -0,0 +10,4 @@
## Request
To query quotes assigned to a public key, the wallet makes a `POST /v1/mint/quote/{method}/pubkey` request.
robwoodgate (Migrated from github.com) commented 2026-04-17 16:10:34 +00:00

A nonce doesn't need to be mint-supplied. It can just be a random string.

A nonce doesn't need to be mint-supplied. It can just be a random string.
robwoodgate commented 2026-04-17 16:22:12 +00:00 (Migrated from github.com)

I've opened a PR with the updated signature logic and test vectors here: #363

I support @a1denvalu3 's approach in #363 - using a timestamp also allows a mint to prevent reuse of the signature after xx minutes too.

It would be even better with a domain prefix, in case a similar scheme is used later on elsewhere:

SHA-256( b"Cashu_NUTXX_QUOTE_v1" || pubkey || timestamp || mint_pubkey )

But with a timestamp max age specified, the reuse window would be minimal in any case.

> I've opened a PR with the updated signature logic and test vectors here: #363 I support @a1denvalu3 's approach in #363 - using a timestamp also allows a mint to prevent reuse of the signature after xx minutes too. It would be even better with a domain prefix, in case a similar scheme is used later on elsewhere: ``` SHA-256( b"Cashu_NUTXX_QUOTE_v1" || pubkey || timestamp || mint_pubkey ) ``` But with a timestamp max age specified, the reuse window would be minimal in any case.
c03rad0r commented 2026-06-03 16:42:09 +00:00 (Migrated from github.com)

Thanks for opening this — it's great to see this direction being taken seriously. I'm building an ESP32-based captive portal (TollGate) that provides WiFi access in exchange for ecash payments. My use case: a mining proxy on the ESP32 connects to a hashpool translator via SV1 stratum. The translator mints ehash tokens for the miner's locking pubkey and currently pushes them downstream via a custom mining.token notification.

This NUT ("Get quotes by pubkeys") would let me eliminate the translator middleman — the ESP32 could directly query the mint for quotes attributed to its pubkey, receive tokens without the push notification hack. This is especially important for resource-constrained devices where running a full translator is impractical.

The domain-separated signature scheme in #363 (timestamp + mint pubkey) looks solid. I'm really looking forward to using this functionality — happy to test with my ESP32 implementation once the spec stabilizes.

Thanks for opening this — it's great to see this direction being taken seriously. I'm building an ESP32-based captive portal (TollGate) that provides WiFi access in exchange for ecash payments. My use case: a mining proxy on the ESP32 connects to a hashpool translator via SV1 stratum. The translator mints ehash tokens for the miner's locking pubkey and currently pushes them downstream via a custom `mining.token` notification. This NUT ("Get quotes by pubkeys") would let me eliminate the translator middleman — the ESP32 could directly query the mint for quotes attributed to its pubkey, receive tokens without the push notification hack. This is especially important for resource-constrained devices where running a full translator is impractical. The domain-separated signature scheme in #363 (timestamp + mint pubkey) looks solid. I'm really looking forward to using this functionality — happy to test with my ESP32 implementation once the spec stabilizes.
This pull request has changes conflicting with the target branch.
  • README.md
View command line instructions

Manual merge helper

Use this merge commit message when completing the merge manually.

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin get-quotes-by-pubkeys:get-quotes-by-pubkeys
git switch get-quotes-by-pubkeys

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch main
git merge --no-ff get-quotes-by-pubkeys
git switch get-quotes-by-pubkeys
git rebase main
git switch main
git merge --ff-only get-quotes-by-pubkeys
git switch get-quotes-by-pubkeys
git rebase main
git switch main
git merge --no-ff get-quotes-by-pubkeys
git switch main
git merge --squash get-quotes-by-pubkeys
git switch main
git merge --ff-only get-quotes-by-pubkeys
git switch main
git merge get-quotes-by-pubkeys
git push origin main
Sign in to join this conversation.
No description provided.