Loosen NUT-29 batch quote checks - #422
Conversation
|
I got two questions:
|
There was a problem hiding this comment.
ACK the concept. This will only be implemented on mints who will also be aware of the UUIDv7 quote id requirement, so my concerns about creating a batch oracle are avoided (UUIDv7 has 74 bits of entropy per millisecond timestamp).
We should also consider making UUIDv7 a MUST, at least until all quotes are locked.
I still think we should return unknowns, as this tells a wallet unequivocably the quote should be dropped (just omitting unknowns could indicate a mint bug, truncated reponse or other ambiguities).
My suggestion is to include a positional "unknown", as this also makes response processing simpler: quoteids in = quote responses out in same order
Co-authored-by: Rob Woodgate <robwoodgate@users.noreply.github.com>
Co-authored-by: Rob Woodgate <robwoodgate@users.noreply.github.com>
Co-authored-by: Rob Woodgate <robwoodgate@users.noreply.github.com>
The merge dropped the closing bracket on the check response example, leaving invalid JSON. Also lowercases the entry index, backticks the field references, and normalises the unknown entry block to 2-space indent.
robwoodgate
left a comment
There was a problem hiding this comment.
Apologies I munged the suggestions last time, so made two commits to fix them up after you merged them. Below are the remaining suggestions to make it consistent.
| ] | ||
| ``` | ||
|
|
||
| If the mint cannot handle any of the requested quote IDs, it returns an empty JSON array. |
There was a problem hiding this comment.
| If the mint cannot handle any of the requested quote IDs, it returns an empty JSON array. | |
| If the mint cannot handle any of the requested quote IDs, every entry in the response is an `unknown` entry. The response is never shorter than the request. |
There was a problem hiding this comment.
In CTS, I have a failIf(quotes.length != responses.length), so if the view is to make an exception for fully failed lookup, I will need to loosen that.
Co-authored-by: Rob Woodgate <robwoodgate@users.noreply.github.com>
Co-authored-by: Rob Woodgate <robwoodgate@users.noreply.github.com>
Co-authored-by: Rob Woodgate <robwoodgate@users.noreply.github.com>
| ### Unknown or Malformed Quote IDs | ||
|
|
||
| This is a query endpoint that uses all-or-nothing error handling, matching the behavior of the batch mint endpoint: | ||
| This is a query endpoint and does not use the all-or-nothing error handling of the batch mint endpoint. If a quote ID is unknown or cannot be parsed, the mint **MUST** return an `unknown` entry for it as below and continue processing the remaining IDs. |
There was a problem hiding this comment.
I wonder if we should name the return type, as we do for other response shapes.
| This is a query endpoint and does not use the all-or-nothing error handling of the batch mint endpoint. If a quote ID is unknown or cannot be parsed, the mint **MUST** return an `unknown` entry for it as below and continue processing the remaining IDs. | |
| This is a query endpoint and does not use the all-or-nothing error handling of the batch mint endpoint. If a quote ID is unknown or cannot be parsed, the mint **MUST** return an `UnknownQuote` for it as below and continue processing the remaining IDs. |
a1denvalu3
left a comment
There was a problem hiding this comment.
Splitting my earlier review into threads on the relevant lines. The two I'd block on are the empty-array contradiction in the test vector and the undefined equality check for partially-minted quotes.
| ] | ||
| ``` | ||
|
|
||
| If the mint cannot handle any of the requested quote IDs, it returns an empty JSON array. |
There was a problem hiding this comment.
This contradicts 29.md: the spec now says a successful response always has exactly one entry per requested ID, but this line still says an empty array is returned. An empty response to a non-empty request breaks the positional contract. Shouldn't this read that every entry is an unknown entry (response never shorter than the request)?
EDIT: just needs to be updated ig
| 5. **Currency unit consistency**: All quotes MUST use the same currency unit | ||
| 6. **Quote state**: All quotes MUST be in PAID state (or have a mintable amount for payment methods that allow multiple mint operations like bolt12) | ||
| 6. **Mintable amount**: All quotes MUST have a positive currently mintable amount (`amount_paid - amount_issued`) | ||
| 7. **Amount balance**: The sum of amounts contained in the `outputs` MUST equal the sum of `quote_amounts` (bolt11) or MUST NOT exceed it (bolt12) |
There was a problem hiding this comment.
With rule 6 loosened to "positive mintable amount", a batch can now include an already partially-minted bolt11 quote — but this equality check doesn't say what the target is in that case: the quote's original amount, or its currently mintable amount (amount_paid - amount_issued)? If it's the original amount, a mint can be pushed past NUT-04's cap. Worth defining, including what quote_amounts means for a partially-issued quote.
| #### Example | ||
| Known quotes are returned as mint quote objects as defined by the payment method's NUT specification. A quote ID that the mint does not know or cannot parse is returned as an [unknown entry](#unknown-or-malformed-quote-ids). | ||
|
|
||
| Mints **MUST** return the quote object for every quote ID they hold a record of, including quotes whose `expiry` has passed. An unknown entry indicates only that the mint holds no record of the ID. |
There was a problem hiding this comment.
What about an ID the mint does hold, but under a different method (a bolt12 ID sent to /quote/bolt11/check), or a melt quote? Returning the object leaks cross-method existence; returning unknown contradicts "holds no record of the ID". Suggest specifying the endpoint is scoped to {method} and anything outside that scope MUST be returned as unknown.
| The mint returns a JSON array with exactly one entry per requested quote ID, in request order. Entry `i` corresponds to `quotes[i]`, and its `quote` field **MUST** equal the requested ID. | ||
|
|
||
| #### Example | ||
| Known quotes are returned as mint quote objects as defined by the payment method's NUT specification. A quote ID that the mint does not know or cannot parse is returned as an [unknown entry](#unknown-or-malformed-quote-ids). |
There was a problem hiding this comment.
Worth being explicit about what "mint quote objects as defined by the payment method's NUT" includes here. Returning the full object (incl. request) makes this endpoint an efficient existence oracle: per-entry unknowns leak ~N bits per query vs ~1 bit with all-or-nothing, and NUT-04 only makes UUIDv7 a SHOULD. Either require strong quote IDs for mints advertising NUT-29, or return only the accounting subset (quote, amount_paid, amount_issued, updated_at) — the test vector already shows that minimal shape.
| { | ||
| "quote": <str>, | ||
| "unknown": true | ||
| } |
There was a problem hiding this comment.
Suggest stating that unknown entries contain exactly quote and unknown: true, so wallets have a clean discriminator and an entry can't be both a full quote object and unknown.
| 4. **Payment method consistency**: All quotes MUST have the same payment method, matching `{method}` in the URL path | ||
| 5. **Currency unit consistency**: All quotes MUST use the same currency unit | ||
| 6. **Quote state**: All quotes MUST be in PAID state (or have a mintable amount for payment methods that allow multiple mint operations like bolt12) | ||
| 6. **Mintable amount**: All quotes MUST have a positive currently mintable amount (`amount_paid - amount_issued`) |
There was a problem hiding this comment.
Dropping the PAID-state rule also drops any mention of expiry from mint validation: nothing in this list says whether an expired (but paid) quote can still be batch-minted. Some mints will reject, some won't — should be stated explicitly, especially since the check section now requires returning expired quotes.
| @@ -28,13 +28,19 @@ The wallet includes the following body in its request: | |||
|
|
|||
| where `quotes` is an array of _unique_ mint quote IDs. | |||
There was a problem hiding this comment.
For the check endpoint specifically: is an empty quotes array allowed (→ empty response)? And are duplicate IDs rejected like in batch mint (11016), or answered positionally? Both are undefined for check.
Implementations
What changed
statereferences with NUT-04'samount_paid,amount_issued, andupdated_ataccounting modelamount_paid - amount_issuedWhy
Batch quote checking is a read-only query. A wallet should still receive usable quote states when some requested identifiers are malformed or unknown, without weakening the atomic validation of the batch mint endpoint.
Impact
Mints can process batch-check requests on a best-effort basis. Wallets must treat the response as an order-preserving subset of the requested quotes.
Batch-check responses and batch-mint validation now use NUT-04's canonical mint-quote accounting fields instead of the deprecated state model.
Batch mint execution remains atomic and unchanged.
Validation
npx prettier@3.9.1 --check 29.md tests/29-tests.mdgit diff --check origin/main...HEAD