From d823f607f2c2b2e076dfccec0dd171ee36b9b9ac Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Sun, 10 Nov 2024 17:30:39 +0000 Subject: [PATCH 01/10] feat: nut19 signature on mint request --- 19.md | 144 ++++++++++++++++++++++++++++++++++++++++++++++ error_codes.md | 2 + tests/19-tests.md | 21 +++++++ 3 files changed, 167 insertions(+) create mode 100644 19.md create mode 100644 tests/19-tests.md diff --git a/19.md b/19.md new file mode 100644 index 0000000..1da9290 --- /dev/null +++ b/19.md @@ -0,0 +1,144 @@ +# NUT-19: Signature on Mint Quote +`optional` + +This NUT defines a protocol extension that enables signature-based authentication for mint quote redemption. When requesting a mint quote, clients can provide a public key. The mint will then require a valid signature from the corresponding secret key before processing the mint. +Caution: If the mint does not support this NUT, anyone with the mint quote id will be able to mint even 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 (here `bolt11`). +```http +POST https://mint.host:3338/v1/mint/quote/bolt11 +``` + +The wallet of `Alice` includes the following `PostMintQuoteBolt11Request` data in its request: + +```json +{ + "amount": , + "unit": , + "description": , + "pubkey": <-- New +} +``` +with the requested `amount` and the `unit`. An optional `description` can be passed if the mint signals support for it in `MintMethodSetting`. `pubkey` is the public key that will be required for signature verification during the minting process. The mint will only mint ecash after receiving a valid signature from the corresponding private key in the `PostMintRequest`. +The mint `Bob` then responds with a `PostMintQuoteBolt11Response`: +```json +{ + "quote": , + "request": , + "state": , + "expiry": +} +``` + +Where `quote` is the quote ID and `request` is the payment request to fulfill. `expiry` is the Unix timestamp until which the mint quote is valid. +`state` is an enum string field with possible values `"UNPAID"`, `"PAID"`, `"ISSUED"`: +- `"UNPAID"` means that the quote's request has not been paid yet. +- `"PAID"` means that the request has been paid. +- `"ISSUED"` means that the quote has already been issued. + + +## Example + +Request of `Alice` with curl: + +```bash +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`: + +```json +{ + "quote": "9d745270-1405-46de-b5c5-e2762b4f5e00", + "request": "lnbc100n1pj4apw9...", + "state": "UNPAID", + "expiry": 1701704757 +} +``` + +#### Signature scheme +To mint a quote where a public key was provided, the minter needs to include signatures in the `PostMintBolt11Request`. We use `libsecp256k1`'s serialized 64-byte Schnorr signatures on the SHA256 hash of the message to sign. The message to sign is the field `PostMintQuoteBolt11Response.quote`. + +# 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`). +```http +POST https://mint.host:3338/v1/mint/bolt11 +``` +The wallet `Alice` includes the following `PostMintBolt11Request` data in its request +```json +{ + "quote": , + "outputs": , + "witness": <-- New +} +``` +with the `quote` being the quote ID from the previous step and `outputs` being `BlindedMessages` (see [NUT-00][00]) that the wallet requests signatures on whose sum is `amount` as requested in the quote. `witness` is the signature on the mint quote id as defined above. +The mint `Bob` then responds with a `PostMintBolt11Response`: +```json +{ + "signatures": +} +``` +where `signatures` is an array of blind signatures on the outputs. + + +## Example + +Request of `Alice` with curl: + +```bash +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" + } + ], + "witness": "d9be080b33179387e504bb6991ea41ae0dd715e28b01ce9f63d57198a095bccc776874914288e6989e97ac9d255ac667c205fa8d90a211184b417b4ffdd24092" + +}' +``` + + +Response of `Bob`: + +```json +{ + "signatures": [ + { + "id": "009a1f293253e41e", + "amount": 2, + "C_": "0224f1c4c564230ad3d96c5033efdc425582397a5a7691d600202732edc6d4b1ec" + }, + { + "id": "009a1f293253e41e", + "amount": 8, + "C_": "0277d1de806ed177007e5b94a8139343b6382e472c752a74e99949d511f7194f6c" + } + ] +} +``` + +If the invoice was not paid yet, `Bob` responds with an error. In that case, `Alice` **CAN** repeat the same request until the Lightning invoice is settled, as in NUT04. If `Alice` does not include a witness on the `PostMintBolt11Request` but did include a `pubkey` in the `PostMintBolt11QuoteRequest` the `Bob` **MUST** respond with an error, `Alice` **CAN** repeat the request with a witness in order to mint the ecash. + + +## 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][06]) which in this case reads +```json +{ + "19": { + "supported": + } +} +``` + diff --git a/error_codes.md b/error_codes.md index f340330..7472568 100644 --- a/error_codes.md +++ b/error_codes.md @@ -16,6 +16,7 @@ | 20005 | Quote is pending | [NUT-04][04], [NUT-05][05] | | 20006 | Invoice already paid | [NUT-05][05] | | 20007 | Quote is expired | [NUT-04][04], [NUT-05][05] | +| 20008 | Witness not provided for mint quote | [NUT-19][19] | [00]: 00.md [01]: 01.md @@ -30,3 +31,4 @@ [10]: 10.md [11]: 11.md [12]: 12.md +[19]: 19.md diff --git a/tests/19-tests.md b/tests/19-tests.md new file mode 100644 index 0000000..190299e --- /dev/null +++ b/tests/19-tests.md @@ -0,0 +1,21 @@ +# NUT-19 Test Vectors + +The following is a `PostMintBolt11Request` with a valid signature. Where the `pubkey` in the `PostMintQuoteBolt11Request` is `03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac`. + +```json +{ + "quote": "9d745270-1405-46de-b5c5-e2762b4f5e00", + "outputs": [], + "witness": "d9be080b33179387e504bb6991ea41ae0dd715e28b01ce9f63d57198a095bccc776874914288e6989e97ac9d255ac667c205fa8d90a211184b417b4ffdd24092" +} +``` + +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": [], + "witness": "cb2b8e7ea69362dfe2a07093f2bbc319226db33db2ef686c940b5ec976bcbfc78df0cd35b3e998adf437b09ee2c950bd66dfe9eb64abd706e43ebc7c669c36c3" +} +``` From 40c15c4975605c1c19db57debf304dade977cf75 Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Sun, 10 Nov 2024 17:33:57 +0000 Subject: [PATCH 02/10] chore: prettier :( --- 19.md | 20 +++++++++++++++----- error_codes.md | 2 +- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/19.md b/19.md index 1da9290..9ad9d7d 100644 --- a/19.md +++ b/19.md @@ -1,4 +1,5 @@ # NUT-19: Signature on Mint Quote + `optional` This NUT defines a protocol extension that enables signature-based authentication for mint quote redemption. When requesting a mint quote, clients can provide a public key. The mint will then require a valid signature from the corresponding secret key before processing the mint. @@ -7,6 +8,7 @@ Caution: If the mint does not support this NUT, anyone with the mint quote id wi # 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 (here `bolt11`). + ```http POST https://mint.host:3338/v1/mint/quote/bolt11 ``` @@ -21,8 +23,10 @@ The wallet of `Alice` includes the following `PostMintQuoteBolt11Request` data i "pubkey": <-- New } ``` + with the requested `amount` and the `unit`. An optional `description` can be passed if the mint signals support for it in `MintMethodSetting`. `pubkey` is the public key that will be required for signature verification during the minting process. The mint will only mint ecash after receiving a valid signature from the corresponding private key in the `PostMintRequest`. The mint `Bob` then responds with a `PostMintQuoteBolt11Response`: + ```json { "quote": , @@ -34,11 +38,11 @@ The mint `Bob` then responds with a `PostMintQuoteBolt11Response`: Where `quote` is the quote ID and `request` is the payment request to fulfill. `expiry` is the Unix timestamp until which the mint quote is valid. `state` is an enum string field with possible values `"UNPAID"`, `"PAID"`, `"ISSUED"`: + - `"UNPAID"` means that the quote's request has not been paid yet. - `"PAID"` means that the request has been paid. - `"ISSUED"` means that the quote has already been issued. - ## Example Request of `Alice` with curl: @@ -59,14 +63,19 @@ Response of `Bob`: ``` #### Signature scheme + To mint a quote where a public key was provided, the minter needs to include signatures in the `PostMintBolt11Request`. We use `libsecp256k1`'s serialized 64-byte Schnorr signatures on the SHA256 hash of the message to sign. The message to sign is the field `PostMintQuoteBolt11Response.quote`. # 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`). + ```http POST https://mint.host:3338/v1/mint/bolt11 ``` + The wallet `Alice` includes the following `PostMintBolt11Request` data in its request + ```json { "quote": , @@ -74,15 +83,17 @@ The wallet `Alice` includes the following `PostMintBolt11Request` data in its re "witness": <-- New } ``` + with the `quote` being the quote ID from the previous step and `outputs` being `BlindedMessages` (see [NUT-00][00]) that the wallet requests signatures on whose sum is `amount` as requested in the quote. `witness` is the signature on the mint quote id as defined above. The mint `Bob` then responds with a `PostMintBolt11Response`: + ```json { "signatures": } ``` -where `signatures` is an array of blind signatures on the outputs. +where `signatures` is an array of blind signatures on the outputs. ## Example @@ -109,7 +120,6 @@ curl -X POST https://mint.host:3338/v1/mint/bolt11 -H "Content-Type: application }' ``` - Response of `Bob`: ```json @@ -131,9 +141,10 @@ Response of `Bob`: If the invoice was not paid yet, `Bob` responds with an error. In that case, `Alice` **CAN** repeat the same request until the Lightning invoice is settled, as in NUT04. If `Alice` does not include a witness on the `PostMintBolt11Request` but did include a `pubkey` in the `PostMintBolt11QuoteRequest` the `Bob` **MUST** respond with an error, `Alice` **CAN** repeat the request with a witness in order to mint the ecash. - ## 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][06]) which in this case reads + ```json { "19": { @@ -141,4 +152,3 @@ The settings for this NUT indicate the support for requiring a signature before } } ``` - diff --git a/error_codes.md b/error_codes.md index 7472568..76b2fcd 100644 --- a/error_codes.md +++ b/error_codes.md @@ -16,7 +16,7 @@ | 20005 | Quote is pending | [NUT-04][04], [NUT-05][05] | | 20006 | Invoice already paid | [NUT-05][05] | | 20007 | Quote is expired | [NUT-04][04], [NUT-05][05] | -| 20008 | Witness not provided for mint quote | [NUT-19][19] | +| 20008 | Witness not provided for mint quote | [NUT-19][19] | [00]: 00.md [01]: 01.md From a6fc9567b9f01277dcb7e1ee9e992e8fb4c90e41 Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Thu, 14 Nov 2024 11:48:27 +0000 Subject: [PATCH 03/10] feat: sign outputs --- 19.md | 30 ++++++++++++++++++++-- error_codes.md | 3 ++- tests/19-tests.md | 64 ++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 91 insertions(+), 6 deletions(-) diff --git a/19.md b/19.md index 9ad9d7d..1196658 100644 --- a/19.md +++ b/19.md @@ -25,6 +25,9 @@ The wallet of `Alice` includes the following `PostMintQuoteBolt11Request` data i ``` with the requested `amount` and the `unit`. An optional `description` can be passed if the mint signals support for it in `MintMethodSetting`. `pubkey` is the public key that will be required for signature verification during the minting process. The mint will only mint ecash after receiving a valid signature from the corresponding private key in the `PostMintRequest`. + +> **Privacy:** To prevent linking multiple mint quotes together, wallets **SHOULD** generate a unique public key for each mint quote request + The mint `Bob` then responds with a `PostMintQuoteBolt11Response`: ```json @@ -62,9 +65,21 @@ Response of `Bob`: } ``` +#### Message aggregation + +To provide a signature for a mint request, the owner of one of the signing public keys must concatenate the `PostMintQuoteBolt11Response.quote` and the `B_` fields of all `BlindedMessages` (outputs, see [NUT-00][00]) to a single message string in the order they appear in the `PostMintRequest`. This string concatenated is then hashed and signed (see [Signature scheme](#signature-scheme)). + +If a request has `n` outputs the message to sign becomes: + +``` +msg = quote_id || B_0 || ... || B_n +``` + +Here, `||` denotes string concatenation. The `B_` of each output is **a hex string**. + #### Signature scheme -To mint a quote where a public key was provided, the minter needs to include signatures in the `PostMintBolt11Request`. We use `libsecp256k1`'s serialized 64-byte Schnorr signatures on the SHA256 hash of the message to sign. The message to sign is the field `PostMintQuoteBolt11Response.quote`. +To mint a quote where a public key was provided, the minter needs to include signatures in the `PostMintBolt11Request`. We use `libsecp256k1`'s serialized 64-byte Schnorr signatures on the SHA256 hash of the message to sign as defined above. # Minting tokens @@ -148,7 +163,18 @@ The settings for this NUT indicate the support for requiring a signature before ```json { "19": { - "supported": + "supported": , + "required": } } ``` + +In addition to signaling optional support, the mint can require a public key for mint quotes and a corresponding witness during minting. If `required` is set to `true` in the NUT-19 settings: + +1. The wallet **MUST** provide a public key when requesting a mint quote +2. The wallet **MUST** provide a valid witness signature when submitting the mint request + +> **Note:** Enabling this setting makes the mint incompatible with wallets that do not support NUT-19 + +[00]: 00.md +[06]: 06.md diff --git a/error_codes.md b/error_codes.md index 76b2fcd..41e1c40 100644 --- a/error_codes.md +++ b/error_codes.md @@ -16,7 +16,8 @@ | 20005 | Quote is pending | [NUT-04][04], [NUT-05][05] | | 20006 | Invoice already paid | [NUT-05][05] | | 20007 | Quote is expired | [NUT-04][04], [NUT-05][05] | -| 20008 | Witness not provided for mint quote | [NUT-19][19] | +| 20008 | Invalid witness | [NUT-19][19] | +| 20009 | Witness not provided for mint quote | [NUT-19][19] | [00]: 00.md [01]: 01.md diff --git a/tests/19-tests.md b/tests/19-tests.md index 190299e..f417e61 100644 --- a/tests/19-tests.md +++ b/tests/19-tests.md @@ -5,17 +5,75 @@ The following is a `PostMintBolt11Request` with a valid signature. Where the `pu ```json { "quote": "9d745270-1405-46de-b5c5-e2762b4f5e00", - "outputs": [], - "witness": "d9be080b33179387e504bb6991ea41ae0dd715e28b01ce9f63d57198a095bccc776874914288e6989e97ac9d255ac667c205fa8d90a211184b417b4ffdd24092" + "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" + } + ], + "witness": "d4b386f21f7aa7172f0994ee6e4dd966539484247ea71c99b81b8e09b1bb2acbc0026a43c221fd773471dc30d6a32b04692e6837ddaccf0830a63128308e4ee0" } ``` +The following is the expected message to sign on the above `PostMintBolt11Request`. + +``` +9d745270-1405-46de-b5c5-e2762b4f5e000342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c31102be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b5302209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79 +``` + 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": [], + "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" + } + ], "witness": "cb2b8e7ea69362dfe2a07093f2bbc319226db33db2ef686c940b5ec976bcbfc78df0cd35b3e998adf437b09ee2c950bd66dfe9eb64abd706e43ebc7c669c36c3" } ``` From 7b3cd1866a6ad94f80d93e3753cade05458fa2aa Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Thu, 14 Nov 2024 13:37:26 +0000 Subject: [PATCH 04/10] chore: CAN -> MAY --- 19.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/19.md b/19.md index 1196658..cf76f8b 100644 --- a/19.md +++ b/19.md @@ -154,7 +154,7 @@ Response of `Bob`: } ``` -If the invoice was not paid yet, `Bob` responds with an error. In that case, `Alice` **CAN** repeat the same request until the Lightning invoice is settled, as in NUT04. If `Alice` does not include a witness on the `PostMintBolt11Request` but did include a `pubkey` in the `PostMintBolt11QuoteRequest` the `Bob` **MUST** respond with an error, `Alice` **CAN** repeat the request with a witness in order to mint the ecash. +If the invoice was not paid yet, `Bob` responds with an error. In that case, `Alice` **MAY** repeat the same request until the Lightning invoice is settled, as in NUT04. If `Alice` does not include a witness on the `PostMintBolt11Request` but did include a `pubkey` in the `PostMintBolt11QuoteRequest` then `Bob` **MUST** respond with an error, `Alice` **SHOULD** repeat the request with a witness in order to mint the ecash. ## Settings From 8144ac88fdefe5ec17ec93f61175b47da9dca5cf Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Thu, 14 Nov 2024 16:06:24 +0000 Subject: [PATCH 05/10] Update 19.md Co-authored-by: lollerfirst <43107113+lollerfirst@users.noreply.github.com> --- 19.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/19.md b/19.md index cf76f8b..9101845 100644 --- a/19.md +++ b/19.md @@ -79,7 +79,7 @@ Here, `||` denotes string concatenation. The `B_` of each output is **a hex stri #### Signature scheme -To mint a quote where a public key was provided, the minter needs to include signatures in the `PostMintBolt11Request`. We use `libsecp256k1`'s serialized 64-byte Schnorr signatures on the SHA256 hash of the message to sign as defined above. +To mint a quote where a public key was provided, the minter needs to include signatures in the `PostMintBolt11Request`. We use a [BIP340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki) signature on the SHA-256 hash of the message to sign as defined above. # Minting tokens From 5eed3b74eece65ddf77223ae9705f9f7efe800c5 Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Thu, 14 Nov 2024 18:12:17 +0000 Subject: [PATCH 06/10] chore: define quote id as utf-8 --- 19.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/19.md b/19.md index 9101845..a2b82e3 100644 --- a/19.md +++ b/19.md @@ -75,7 +75,7 @@ If a request has `n` outputs the message to sign becomes: msg = quote_id || B_0 || ... || B_n ``` -Here, `||` denotes string concatenation. The `B_` of each output is **a hex string**. +Here, `||` denotes string concatenation. The `B_` of each output is **a hex string** and the `quote_id` is **a UTF-8 string.** #### Signature scheme From 2906fe8a20ed7515ba237ed0b3d14288e89272e0 Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Fri, 15 Nov 2024 12:10:07 +0000 Subject: [PATCH 07/10] feat: pubkey in check quote --- 19.md | 6 ++++-- error_codes.md | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/19.md b/19.md index a2b82e3..9fc2aca 100644 --- a/19.md +++ b/19.md @@ -35,7 +35,8 @@ The mint `Bob` then responds with a `PostMintQuoteBolt11Response`: "quote": , "request": , "state": , - "expiry": + "expiry": , + "pubkey": <-- New } ``` @@ -61,7 +62,8 @@ Response of `Bob`: "quote": "9d745270-1405-46de-b5c5-e2762b4f5e00", "request": "lnbc100n1pj4apw9...", "state": "UNPAID", - "expiry": 1701704757 + "expiry": 1701704757, + "pubkey": "03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac" } ``` diff --git a/error_codes.md b/error_codes.md index 41e1c40..d746e01 100644 --- a/error_codes.md +++ b/error_codes.md @@ -16,8 +16,8 @@ | 20005 | Quote is pending | [NUT-04][04], [NUT-05][05] | | 20006 | Invoice already paid | [NUT-05][05] | | 20007 | Quote is expired | [NUT-04][04], [NUT-05][05] | -| 20008 | Invalid witness | [NUT-19][19] | -| 20009 | Witness not provided for mint quote | [NUT-19][19] | +| 20008 | Witness on mint request not provided or invalid | [NUT-19][19] | +| 20009 | Pubkey required on mint quote | [NUT-19][19] | [00]: 00.md [01]: 01.md From f866a2511c160870c0e250df176c595001b6d481 Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Sat, 16 Nov 2024 09:14:13 +0000 Subject: [PATCH 08/10] feat: use utf-8 bytes --- 19.md | 2 +- tests/19-tests.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/19.md b/19.md index 9fc2aca..01c6db0 100644 --- a/19.md +++ b/19.md @@ -77,7 +77,7 @@ If a request has `n` outputs the message to sign becomes: msg = quote_id || B_0 || ... || B_n ``` -Here, `||` denotes string concatenation. The `B_` of each output is **a hex string** and the `quote_id` is **a UTF-8 string.** +Where || denotes concatenation, `quote_id` is a UTF-8 string, and each `B_` is a hex string, with all components encoded to UTF-8 bytes #### Signature scheme diff --git a/tests/19-tests.md b/tests/19-tests.md index f417e61..7acffc1 100644 --- a/tests/19-tests.md +++ b/tests/19-tests.md @@ -39,7 +39,7 @@ The following is a `PostMintBolt11Request` with a valid signature. Where the `pu The following is the expected message to sign on the above `PostMintBolt11Request`. ``` -9d745270-1405-46de-b5c5-e2762b4f5e000342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c31102be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b5302209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79 +[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`. From a37fddf5b44634a5db2b6e9b342af4824a46e1aa Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Thu, 3 Oct 2024 15:02:20 +0200 Subject: [PATCH 09/10] feat: bolt12 --- 20.md | 191 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 21.md | 193 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 6 ++ 3 files changed, 390 insertions(+) create mode 100644 20.md create mode 100644 21.md diff --git a/20.md b/20.md new file mode 100644 index 0000000..e3ca95e --- /dev/null +++ b/20.md @@ -0,0 +1,191 @@ +# NUT-19: Mint tokens via BOLT12 + +`optional` + +`depends on: NUT-19` + +--- + +Similar to [NUT-04][04], which covers minting via BOLT11 invoices, minting via [BOLT12](https://github.com/lightning/bolts/blob/master/12-offer-encoding.md) is also a two-step process: requesting a mint quote and minting tokens. This document describes both steps, focusing on BOLT12-specific considerations. + +# Mint quote + +To request a mint quote, the wallet of `Alice` makes a `POST /v1/mint/quote/bolt12`. + +```http +POST https://mint.host:3338/v1/mint/quote/bolt12 +``` + +The wallet of `Alice` includes the following `PostMintQuoteBolt12Request` data in its request: + +```json +{ + "amount": , + "unit": , + "description": , + "expiry": , + "single_use": , + "pubkey": +} +``` + +The `amount` field is optional and specifies the amount to mint. The `unit` field is mandatory. An optional `description` can be passed if the mint signals support for it in `MintMethodSetting`. If `single_use` is false, the offer can be paid multiple times. The optional `expiry` field specifies the unix timestamp when the offer expires it **MUST** be before the `max_expiry` in the `MintMethodSettings` if one is given. `pubkey` is the public key that will be required for signature verification during the minting process. The mint will only mint ecash after receiving a valid signature from the corresponding private key in the `PostMintRequest`. + +> **Note:** While a pubkey is optinal as per [NUT-19][19] for [NUT-04][04] it is required in this NUT and the mint **MUST NOT** issue a mint quote if one is not included. + +> **Privacy:** To prevent linking multiple mint quotes together, wallets **SHOULD** generate a unique public key for each mint quote request + +The mint `Bob` then responds with a `PostMintQuoteBolt12Response`: + +```json +{ + "quote": , + "request": , + "expiry": , + "amount_paid": , + "amount_issued": , + "pubkey": +} +``` + +Where `quote` is the quote ID and `request` is the bolt12 offer. `expiry` is the Unix timestamp until which the mint quote is valid. `amount_paid` is the amount that has been paid to the mint via the bolt12 offer. `amount_issued` is the amount of ecash that has been issued for the given mint quote. `amount_paid` - `amount_issued` represents the amount of ecash a wallet can still mint. + +Note: `quote` is a **unique and random** id generated by the mint to internally look up the payment state. `quote` **MUST** remain a secret between user and mint and **MUST NOT** be derivable from the payment request. A third party who knows the `quote` ID can front-run and steal the ecash that this operation mints. + +## Example + +Request of `Alice` with curl: + +```bash +curl -X POST http://localhost:3338/v1/mint/quote/bolt12 -d '{"amount": 10, "unit": "sat", "single_use": true}' -H "Content-Type: application/json" +``` + +Response of `Bob`: + +```json +{ + "quote": "DSGLX9kevM...", + "request": "lno1qcp...", + "expiry": 1701704757, + "amount_paid": 0, + "amount_issued": 0, + "pubkey": "03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac" +} +``` + +After payment, the wallet continues with the next section. + +## Check mint quote + +To check whether a mint quote has been paid and has ecash that can be issued, `Alice` makes a `GET /v1/mint/quote/bolt12/{quote_id}`. + +```http +GET https://mint.host:3338/v1/mint/quote/bolt12/{quote_id} +``` + +The mint `Bob` responds with a `PostMintQuoteBolt12Response`. + +Example request of `Alice` with curl: + +```bash +curl -X GET http://localhost:3338/v1/mint/quote/bolt12/DSGLX9kevM... +``` + +#### Witness + +In order to mint ecash the wallet **MUST** include a signature as defined in [NUT-19][19]. + +# Minting tokens + +After requesting a mint quote and paying the request, the wallet proceeds with minting new tokens by calling `POST /v1/mint/bolt12`. + +```http +POST https://mint.host:3338/v1/mint/bolt12 +``` + +The wallet `Alice` includes the following `PostMintBolt12Request` data in its request: + +```json +{ + "quote": , + "outputs": , + "witness": +} +``` + +The `quote` is the quote ID from the previous step and `outputs` are `BlindedMessages` (see [NUT-00][00]) that the wallet requests signatures on. The sum of the outputs must equal the amount that can be minted (`amount_paid` - `amount_issued`). `witness` is the signature on the mint quote id as defined above. + +The mint `Bob` then responds with a `PostMintBolt12Response`: + +```json +{ + "signatures": +} +``` + +where `signatures` is an array of blind signatures on the outputs. + +## Multiple issuances + +Unlike BOLT11 invoices, BOLT12 offers can be paid multiple times as long as the `single_use` flag is false. This allows the wallet to mint multiple times for one quote. The wallet can call the check bolt12 endpoint, where the mint will return the `PostMintQuoteBolt12Response` including `amount_paid` and `amount_issued`. The difference between these values represents how much the wallet can mint by calling the mint endpoint as defined above. + +## Settings + +The settings for this nut indicate the supported method-unit pairs for minting and whether minting is disabled or not. They are part of the info response of the mint ([NUT-06][06]) which in this case reads: + +```json +{ + "19": { + "methods": [ + , + ... + ], + "disabled": + } +} +``` + +`MintMethodSetting` indicates supported `method` and `unit` pairs and additional settings of the mint. `disabled` indicates whether this minting is disabled. + +`MintMethodSetting` is of the form: + +```json +{ + "method": , + "unit": , + "min_amount": , + "max_amount": , + "description": , + "max_expiry": +} +``` + +`min_amount` and `max_amount` define the allowed range for transactions using this method-unit pair. `max_expiry` specifies the latest timestamp (in Unix time) until which payments for this offer will be accepted. + +Example `MintMethodSetting`: + +```json +{ + "method": "bolt12", + "unit": "sat", + "min_amount": 0, + "max_amount": 10000, + "description": true, + "max_expiry": 1825076378 +} +``` + +[00]: 00.md +[01]: 01.md +[02]: 02.md +[03]: 03.md +[04]: 04.md +[05]: 05.md +[06]: 06.md +[07]: 07.md +[08]: 08.md +[09]: 09.md +[10]: 10.md +[11]: 11.md +[12]: 12.md +[19]: 19.md diff --git a/21.md b/21.md new file mode 100644 index 0000000..0bb68c0 --- /dev/null +++ b/21.md @@ -0,0 +1,193 @@ +# NUT-19: Melt tokens via BOLT12 + +`optional` + +Similar to [NUT-05][05], which covers melting via BOLT11 invoices, melting via [BOLT12](https://github.com/lightning/bolts/blob/master/12-offer-encoding.md) is also a two-step process: requesting a melt quote and melting tokens. This document describes both steps, focusing on BOLT12-specific considerations. + +# Melt quote + +To request a melt quote, the wallet of `Alice` makes a `POST /v1/melt/quote/bolt12`.. + +```http +POST https://mint.host:3338/v1/melt/quote/bolt12 +``` + +The wallet `Alice` includes the following `PostMeltQuoteBolt12Request` data in its request: + +```json +{ + "request": , + "unit": + "amount": +} +``` + +Here, `request` is the bolt12 Lightning invoice to be paid and `unit` is the unit the wallet would like to pay with. `amount` is the amount the wallet would like to pay in the request unit. This `amount` can be omitted if the offer has an amount. + +The mint `Bob` then responds with a `PostMeltQuoteBolt12Response`: + +```json +{ + "quote": , + "amount": , + "fee_reserve": , + "state": , + "expiry": , + "payment_preimage": +} +``` + +Where `quote` is the quote ID, `amount` the amount that needs to be provided, and `fee_reserve` the additional fee reserve that is required. The mint expects `Alice` to include `Proofs` of _at least_ `total_amount = amount + fee_reserve`. `expiry` is the Unix timestamp until which the melt quote is valid. `payment_preimage` is the payment preimage in case of a successful payment. + +`state` is an enum string field with possible values `"UNPAID"`, `"PENDING"`, `"PAID"`: + +- `"UNPAID"` means that the request has not been paid yet. +- `"PENDING"` means that the request is currently being paid. +- `"PAID"` means that the request has been paid successfully. + +## Example + +Request of `Alice` with curl: + +```bash +curl -X POST https://mint.host:3338/v1/melt/quote/bolt12 -d \ +{ + "request": "lno1zrxq8pjw7qjlm68mtp7e3y...", + "unit": "sat" +} +``` + +Response of `Bob`: + +```json +{ + "quote": "TRmjduhIsPxd...", + "amount": 10, + "fee_reserve": 2, + "state": "UNPAID", + "expiry": 1701704757 +} +``` + +## Check melt quote state + +To check whether a melt quote has been paid, `Alice` makes a `GET /v1/melt/quote/bolt12/{quote_id}`. + +```http +GET https://mint.host:3338/v1/melt/quote/bolt12/{quote_id} +``` + +Like before, the mint `Bob` responds with a `PostMeltQuoteBolt12Response`. + +Example request of `Alice` with curl: + +```bash +curl -X GET http://localhost:3338/v1/melt/quote/bolt12/TRmjduhIsPxd... +``` + +# Melting tokens + +Now that `Alice` knows what the total amount is (`amount + fee_reserve`) in her requested `unit`, she can proceed to melting tokens for which a payment will be executed by the mint. She calls the `POST /v1/melt/bolt12`. + +```http +POST https://mint.host:3338/v1/melt/bolt12 +``` + +⚠️ **Attention:** This call will block until the Lightning payment either succeeds or fails. This can take quite a long time in case the Lightning payment is slow. Make sure to **use no (or a very long) timeout when making this call**! + +The wallet of `Alice` includes the following `PostMeltBolt12Request` data in its request + +```json +{ + "quote": , + "inputs": +} +``` + +Here, `quote` is the melt quote ID to be paid and `inputs` are the proofs with a total amount of at least `amount + fee_reserve` (see previous melt quote response). + +Like before, the mint `Bob` then responds with a `PostMeltQuoteBolt12Response`. If the payment was successful, the `state` field is set to `"PAID"` and the response includes the `payment_preimage` field containing the payment secret of the bolt11 payment. + +If `state=="PAID"`, `Alice`'s wallet can delete the `inputs` from her database (or move them to a history). If `state=="UNPAID"`, `Alice` can repeat the same request again until the payment is successful. + +## Example + +Request of `Alice` with curl: + +```bash +curl -X POST https://mint.host:3338/v1/melt/bolt12 -d \ +'{ + "quote": "od4CN5smMMS3K3QVHkbGGNCTxfcAIyIXeq8IrfhP", + "inputs": [ + { + "amount": 4, + "id": "009a1f293253e41e", + "secret": "429700b812a58436be2629af8731a31a37fce54dbf8cbbe90b3f8553179d23f5", + "C": "03b01869f528337e161a6768b480fcf9f75fd248b649c382f5e352489fd84fd011", + }, + { + "amount": 8, + "id": "009a1f293253e41e", + "secret": "4f3155acef6481108fcf354f6d06e504ce8b441e617d30c88924991298cdbcad", + "C": "0278ab1c1af35487a5ea903b693e96447b2034d0fd6bac529e753097743bf73ca9", + } + ] +}' +``` + +Response `PostMeltQuoteBolt12Response` of `Bob`: + +```json +{ + "quote": "TRmjduhIsPxd...", + "amount": 10, + "fee_reserve": 2, + "state": "PAID", + "expiry": 1701704757, + "payment_preimage": "c5a1ae1f639e1f4a3872e81500fd028bece7bedc1152f740cba5c3417b748c1b" +} +``` + +## Settings + +The mint's settings for this nut indicate the supported method-unit pairs for melting. They are part of the info response of the mint ([NUT-06][06]) which in this case reads + +```json +{ + "19": { + "methods": [ + , + ... + ], + "disabled": + } +} +``` + +`MeltMethodSetting` indicates supported `method` and `unit` pairs and additional settings of the mint. `disabled` indicates whether melting is disabled. + +`MeltMethodSetting` is of the form: + +```json +{ + "method": , + "unit": , + "min_amount": , + "max_amount": +} +``` + +`min_amount` and `max_amount` indicate the minimum and maximum amount for an operation of this method-unit pair. + +Example `MeltMethodSetting`: + +```json +{ + "method": "bolt12", + "unit": "sat", + "min_amount": 100, + "max_amount": 10000 +} +``` + +[05]: 05.md diff --git a/README.md b/README.md index b63ab38..e3d1617 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,9 @@ Wallets and mints `MUST` implement all mandatory specs and `CAN` implement optio | [16][16] | Animated QR codes | [Cashu.me][cashume] | - | | [17][17] | WebSocket subscriptions | [Nutshell][py] | [Nutshell][py] | | [18][18] | Payment requests | [Cashu.me][cashume], [Boardwalk][bwc], [cdk-cli] | - | +| [19][19] | Sign mint quotes | [cdk-cli] | [cdk-mintd] | +| [20][20] | Mint Bolt12 | [cdk-cli] | [cdk-mintd] | +| [21][21] | Melt Bolt12 | [cdk-cli] | [cdk-mintd] | #### Wallets: @@ -89,3 +92,6 @@ Wallets and mints `MUST` implement all mandatory specs and `CAN` implement optio [16]: 16.md [17]: 17.md [18]: 18.md +[19]: 19.md +[20]: 20.md +[21]: 21.md From ddf6b44e55102851b5919ffb90bb3bd0ec3e5971 Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Thu, 21 Nov 2024 09:20:26 +0000 Subject: [PATCH 10/10] fix: include pubkey in example --- 20.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/20.md b/20.md index e3ca95e..c57fa4f 100644 --- a/20.md +++ b/20.md @@ -50,14 +50,12 @@ The mint `Bob` then responds with a `PostMintQuoteBolt12Response`: Where `quote` is the quote ID and `request` is the bolt12 offer. `expiry` is the Unix timestamp until which the mint quote is valid. `amount_paid` is the amount that has been paid to the mint via the bolt12 offer. `amount_issued` is the amount of ecash that has been issued for the given mint quote. `amount_paid` - `amount_issued` represents the amount of ecash a wallet can still mint. -Note: `quote` is a **unique and random** id generated by the mint to internally look up the payment state. `quote` **MUST** remain a secret between user and mint and **MUST NOT** be derivable from the payment request. A third party who knows the `quote` ID can front-run and steal the ecash that this operation mints. - ## Example Request of `Alice` with curl: ```bash -curl -X POST http://localhost:3338/v1/mint/quote/bolt12 -d '{"amount": 10, "unit": "sat", "single_use": true}' -H "Content-Type: application/json" +curl -X POST http://localhost:3338/v1/mint/quote/bolt12 -d '{"amount": 10, "unit": "sat", "single_use": true, "pubkey": "03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac"}' -H "Content-Type: application/json" ``` Response of `Bob`: