Skip to content

Commit

Permalink
NUT-09 restore + NUT-13 deterministic secrets (#87)
Browse files Browse the repository at this point in the history
* Create 09.md

* mint stuff

* Update 09.md

* coin type clarify

Co-authored-by: Angus Pearson <angus@toaster.cc>

* Update 09.md

* wip

* Edit

* minor edits

* split to 09 and 13

* fix typos

* Update 09.md

Co-authored-by: Angus Pearson <angus@toaster.cc>

* Update 13.md

Co-authored-by: Angus Pearson <angus@toaster.cc>

* Update 09.md

Co-authored-by: Angus Pearson <angus@toaster.cc>

* typo

* clarify that mints must store blind signatures

* move test

---------

Co-authored-by: gandlafbtc <123852829+gandlafbtc@users.noreply.github.com>
Co-authored-by: Angus Pearson <angus@toaster.cc>
  • Loading branch information
3 people committed Mar 22, 2024
1 parent 5876503 commit 2e6fd72
Show file tree
Hide file tree
Showing 4 changed files with 230 additions and 12 deletions.
45 changes: 45 additions & 0 deletions 09.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
NUT-09: Restore signatures
==========================

`optional` `used in: NUT-13`

---

In this document, we describe how wallets can recover blind signatures, and with that their corresponding `Proofs`, by requesting from the mint to reissue the blind signatures. This can be used for a backup recovery of a lost wallet (see [NUT-09][09]) or for recovering the response of an interrupted swap request (see [NUT-03][03]).

Mints must store the `BlindedMessage` and the corresponding `BlindSignature` in their database every time they issue a `BlindSignature`. Wallets provide the `BlindedMessage` for which they request the `BlindSignature`. Mints only respond with a `BlindSignature`, if they have previously signed the `BlindedMessage`. Each returned `BlindSignature` also contains the `amount` and the keyset `id` (see [NUT-00][00]) which is all the necessary information for a wallet to recover a `Proof`.

**Request** of `Alice`:

```http
POST https://mint.host:3338/v1/restore
```

With the data being of the form `PostRestoreRequest`:

```json
{
"outputs": <Array[BlindedMessages]>
}
```

**Response** of `Bob`:

The mint `Bob` then responds with a `PostRestoreResponse`.

```json
{
"outputs": <Array[BlindedMessages]>,
"signatures": <Array[BlindSignature]>
}
```

The returned arrays `outputs` and `signatures` are of the same length and for every entry `outputs[i]`, there is a corresponding entry `signatures[i]`.

[00]: 00.md
[02]: 02.md
[03]: 03.md
[07]: 07.md
[09]: 09.md
[11]: 11.md
[tests]: tests/09-tests.md
112 changes: 112 additions & 0 deletions 13.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
NUT-13: Deterministic Secrets
==========================

`optional` `depends on: NUT-09`

---

In this document, we describe the process that allows wallets to recover their ecash balance with the help of the mint using a familiar 12 word seed phrase (mnemonic). This allows us to restore the wallet's previous state in case of a device loss or other loss of access to the wallet. The basic idea is that wallets that generate the ecash deterministically can regenerate the same tokens during a recovery process. For this, they ask the mint to reissue previously generated signatures using [NUT-09][09].

## Deterministic secret derivation

An ecash token, or a `Proof`, consists of a `secret` generated by the wallet, and a signature `C` generated by the wallet and the mint in collaboration. Here, we describe how wallets can deterministically generate the `secrets` and blinding factors `r` necessary to generate the signatures `C`.

The wallet generates a `private_key` derived from a 12-word [BIP39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) `mnemonic` seed phrase that the user stores in a secure place. The wallet uses the `private_key`, to derive deterministic values for the `secret` and the blinding factors `r` for every new ecash token that it generates.

In order to do this, the wallet keeps track of a `counter_k` for each `keyset_k` it uses. The index `k` indicates that the wallet needs to keep track of a separate counter for each keyset `k` it uses. Typically, the wallet will need to keep track of multiple keysets for every mint it interacts with. `counter_k` is used to generate a [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) derivation path which can then be used to derive `secret` and `r`.

The following BIP32 derivation path is used. The derivation path depends on the [keyset ID][02] of `keyset_k`, and the `counter_k` of that keyset.

- Purpose' = `129372'` (UTF-8 for 🥜)
- Coin type' = Always `0'`
- Keyset id' = Keyset ID represented as an integer (`keyset_k_int`)
- Coin counter' = `counter'` (this value is incremented)
- `secret` or `r` = `0` or `1`

`m / 129372' / 0' / keyset_k_int' / counter' / secret||r`

This results in the following derivation paths:

```
secret_derivation_path = `m/129372'/0'/{keyset_k_int}'/{counter_k}'/0`
r_derivation_path = `m/129372'/0'/{keyset_id_k_int}'/{counter_k}'/1`
```

Here, `{keyset_k_int}` and `{counter_k}` are the only variables that can change. `keyset_id_k_int` is an integer representation (see below) of the keyset ID the token is generated with. This means that the derivation path is unique for each keyset. Note that the coin type is always `0'`, independent of the unit of the ecash.

**Note:** For examples, see the [test vectors][tests].

#### Counter

The wallet starts with `counter_k := 0` upon encountering a new keyset and increments it by `1` every time it has successfully minted new ecash with this keyset. The wallet stores the latest `counter_k` in its database for all keysets it uses. Note that we have a `counter` (and therefore a derivation path) for each keyset `k`. We omit the keyset index `k` in the following of this document.

#### Keyset ID

The integer representation `keyset_id_int` of a keyset is calculated from its [hexadecimal ID][02] which has a length of 8 bytes or 16 hex characters. First, we convert the hex string to a big-endian sequence of bytes. This value is then modulo reduced by `2^31 - 1` to arrive at an integer that is a unique identifier `keyset_id_int`.

Example in Python:
```python
keyset_id_int = int.from_bytes(bytes.fromhex(keyset_id_hex), "big") % (2**31 - 1)
```

Example in JavaScript:
```javascript
keysetIdInt = BigInt(`0x${keysetIdHex}`) % BigInt(2 ** 31 - 1);
```

## Restore from seed phrase

Using deterministic secret derivation, a user's wallet can regenerate the same `BlindedMessages` in case of loss of a previous wallet state. To also restore the corresponding `BlindSignatures` to fully recover the ecash, the wallet can either requests the mint to re-issue past `BlindSignatures` on the regenerated `BlindedMessages` (see [NUT-09][09]) or by downloading the entire database of the mint (TBD).

The wallet takes the following steps during recovery:

1) Generate `secret` and `r` from `counter` and `keyset`
2) Generate `BlindedMessage` from `secret`
3) Obtain `BlindSignature` for `secret` from the mint
4) Unblind `BlindSignature` to `C` using `r`
5) Restore `Proof = (secret, C)`
6) Check if `Proof` is already spent

#### Generate `BlindedMessages`

To generate the `BlindedMessages`, the wallet starts with a `counter := 0` and , for each increment of the `counter`, generates a `secret` using the BIP32 private key derived from `secret_derivation_path` and converts it to a hex string.

```python
secret = bip32.get_privkey_from_path(secret_derivation_path).hex()
```
The wallet similarly generates a blinding factor `r` from the `r_derivation_path`:

```python
r = self.bip32.get_privkey_from_path(r_derivation_path)
```

**Note:** For examples, see the [test vectors][tests].

Using the `secret` string and the private key `r`, the wallet generates a `BlindedMessage`. The wallet then increases the `counter` by `1` and repeats the same process for a given batch size. It is recommended to use a batch size of 100.

The user's wallet can now request the corresponding `BlindSignatures` for theses `BlindedMessages` from the mint using the [NUT-09][09] restore endpoint or by downloading the entire mint's database.

#### Generate `Proofs`

Using the restored `BlindSignatures` and the `r` generated in the previous step, the wallet can [unblind][00] the signature to `C`. The triple `(secret, C, amount)` is a restored `Proof`.

#### Check `Proofs` states

If the wallet used the restore endpoint [NUT-09][09] for regenerating the `Proofs`, it additionally needs to check for the `Proofs` spent state using [NUT-07][07]. The wallet deletes all `Proofs` which are already spent and keeps the unspent ones in its database.

### Restoring batches

Generally, the user won't remember the last state of `counter` when starting the recovery process. Therefore, wallets need to know how far they need to increment the `counter` during the restore process to be confident to have reached the most recent state.

In short, following approach is recommended:
- Restore `Proofs` in batches of 100 and increment `counter`
- Repeat until three consecutive batches are returned empty
- Reset `counter` to the value at the last successful restore + 1

Wallets restore `Proofs` in batches of 100. The wallet starts with a `counter=0` and increments it for every `Proof` it generated during one batch. When the wallet begins restoring the first `Proofs`, it is likely that the first few batches will only contain spent `Proofs`. Eventually, the wallet will reach a `counter` that will result in unspent `Proofs` which it stores in its database. The wallet then continues to restore until *three successive batches are returned empty by the mint*. This is to be confident that the restore process did not miss any `Proofs` that might have been generated with larger gaps in the `counter` by the previous wallet that we are restoring.

[00]: 00.md
[02]: 02.md
[07]: 07.md
[09]: 09.md
[tests]: tests/09-tests.md
26 changes: 14 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,24 @@ Wallets and mints `MUST` implement all mandatory specs and `CAN` implement optio
### Mandatory
| # | Description | Wallets | Mints |
|--- | --- | --- | --- |
| [00][00] | Cryptography and Models | [Nutshell][py], [Feni][feni], [Moksha][cashume], [Nutstash][ns], [cashu-ts][ts], [cashu-crab][cashu-crab] | [Nutshell][py], [Feni][feni], [LNbits], [Moksha][moksha], [cashu-rs-mint][cashu-rs-mint]
| [01][01] | Mint public keys | [Nutshell][py], [Feni][feni], [Moksha][cashume], [Nutstash][ns], [cashu-ts][ts], [cashu-crab][cashu-crab] | [Nutshell][py], [Feni][feni], [LNbits], [Moksha][moksha], [cashu-rs-mint][cashu-rs-mint]
| [02][02] | Keysets and keyset IDs | [Nutshell][py], [Feni][feni], [Moksha][cashume], [Nutstash][ns], [cashu-ts][ts], [cashu-crab][cashu-crab] | [Nutshell][py], [Feni][feni], [LNbits], [Moksha][moksha], [cashu-rs-mint][cashu-rs-mint]
| [03][03] | Swapping tokens | [Nutshell][py], [Feni][feni], [Moksha][cashume], [Nutstash][ns], [cashu-ts][ts], [cashu-crab][cashu-crab] | [Nutshell][py], [Feni][feni], [LNbits], [Moksha][moksha], [cashu-rs-mint][cashu-rs-mint]
| [04][04] | Minting tokens | [Nutshell][py], [Feni][feni], [Moksha][cashume], [Nutstash][ns], [cashu-ts][ts], [cashu-crab][cashu-crab] | [Nutshell][py], [Feni][feni], [LNbits], [Moksha][moksha], [cashu-rs-mint][cashu-rs-mint]
| [05][05] | Melting tokens | [Nutshell][py], [Feni][feni], [Moksha][cashume], [Nutstash][ns], [cashu-ts][ts], [cashu-crab][cashu-crab] | [Nutshell][py], [Feni][feni], [LNbits], [Moksha][moksha], [cashu-rs-mint][cashu-rs-mint]
| [06][06] | Mint info | [Nutshell][py], [eNuts][enuts] | [Nutshell][py], [cashu-rs-mint][cashu-rs-mint]
| [00][00] | Cryptography and Models | [Nutshell][py], [Feni][feni], [Moksha][cashume], [cashu-ts][ts], [cashu-crab][cashu-crab] | [Nutshell][py], [Feni][feni], [Moksha][moksha], [cashu-rs-mint][cashu-rs-mint]
| [01][01] | Mint public keys | [Nutshell][py], [Feni][feni], [Moksha][cashume], [cashu-ts][ts], [cashu-crab][cashu-crab] | [Nutshell][py], [Feni][feni], [Moksha][moksha], [cashu-rs-mint][cashu-rs-mint]
| [02][02] | Keysets and keyset IDs | [Nutshell][py], [Feni][feni], [Moksha][cashume], [cashu-ts][ts], [cashu-crab][cashu-crab] | [Nutshell][py], [Feni][feni], [Moksha][moksha], [cashu-rs-mint][cashu-rs-mint]
| [03][03] | Swapping tokens | [Nutshell][py], [Feni][feni], [Moksha][cashume], [cashu-ts][ts], [cashu-crab][cashu-crab] | [Nutshell][py], [Feni][feni], [Moksha][moksha], [cashu-rs-mint][cashu-rs-mint]
| [04][04] | Minting tokens | [Nutshell][py], [Feni][feni], [Moksha][cashume], [cashu-ts][ts], [cashu-crab][cashu-crab] | [Nutshell][py], [Feni][feni], [Moksha][moksha], [cashu-rs-mint][cashu-rs-mint]
| [05][05] | Melting tokens | [Nutshell][py], [Feni][feni], [Moksha][cashume], [cashu-ts][ts], [cashu-crab][cashu-crab] | [Nutshell][py], [Feni][feni], [Moksha][moksha], [cashu-rs-mint][cashu-rs-mint]
| [06][06] | Mint info | [Nutshell][py], [cashu-ts][ts] | [Nutshell][py], [cashu-rs-mint][cashu-rs-mint]

### Optional
| # | Description | Wallets | Mints
|--- | --- | --- | --- |
| [07][07] | Token state check | [Nutshell][py], [Feni][feni], [Moksha][cashume], [Nutstash][ns], [cashu-ts][ts], [cashu-crab][cashu-crab] | [Nutshell][py], [Feni][feni], [LNbits], [Moksha][moksha], [cashu-rs-mint][cashu-rs-mint]
| [08][08] | Overpaid Lightning fees | [Nutshell][py], [Feni][feni], [Moksha][cashume], [Nutstash][ns], [cashu-ts][ts], [cashu-crab][cashu-crab] | [Nutshell][py], [LNbits], [Moksha][moksha], [cashu-rs-mint][cashu-rs-mint]
| [09][09] | Deterministic backup and restore | - | -
| [10][10] | Spending conditions | [Nutshell][py] | [Nutshell][py]
| [11][11] | Pay-To-Pubkey (P2PK) | [Nutshell][py] | [Nutshell][py]
| [07][07] | Token state check | [Nutshell][py], [Feni][feni], [Moksha][cashume], [cashu-ts][ts], [cashu-crab][cashu-crab] | [Nutshell][py], [Feni][feni], [Moksha][moksha], [cashu-rs-mint][cashu-rs-mint]
| [08][08] | Overpaid Lightning fees | [Nutshell][py], [Feni][feni], [Moksha][cashume], [cashu-ts][ts], [cashu-crab][cashu-crab] | [Nutshell][py], [Moksha][moksha], [cashu-rs-mint][cashu-rs-mint]
| [09][09] | Signature restore | [Nutshell][py], [cashu-rs-mint][cashu-rs-mint], [cashu-ts][ts] | [Nutshell][py], [cashu-rs-mint][cashu-rs-mint]
| [10][10] | Spending conditions | [Nutshell][py], [cashu-crab][cashu-crab] | [Nutshell][py], [cashu-rs-mint][cashu-rs-mint], [LNbits]
| [11][11] | Pay-To-Pubkey (P2PK) | [Nutshell][py], [cashu-crab][cashu-crab] | [Nutshell][py], [cashu-rs-mint][cashu-rs-mint]
| [12][12] | DLEQ proofs | [Nutshell][py] | [Nutshell][py]
| [13][13] | Deterministic secrets | [Nutshell][py], [Moksha][cashume], [cashu-ts][ts] | -

[py]: https://github.com/cashubtc/cashu
[feni]: https://github.com/cashubtc/cashu-feni
Expand All @@ -50,3 +51,4 @@ Wallets and mints `MUST` implement all mandatory specs and `CAN` implement optio
[10]: 10.md
[11]: 11.md
[12]: 12.md
[13]: 13.md
59 changes: 59 additions & 0 deletions tests/13-tests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# NUT-13 Test vectors

## Keyset ID integer representation

The integer representation of a keyset with an ID `009a1f293253e41e` and its corresponding derivation path for a counter of value `{counter}` are

```json
{
"keyset_id": "009a1f293253e41e",
"keyest_id_int": 864559728,
"derivation_path": "m/129372'/0'/864559728'/{counter}'"
}
```

## Secret derivatoin

We derive values starting from the following BIP39 mnemonic.

```json
{
"mnemonic": "half depart obvious quality work element tank gorilla view sugar picture humble"
}
```

The secrets derived for the first five counters from `counter=0` to `counter=4` are

```json
{
"secret_0": "485875df74771877439ac06339e284c3acfcd9be7abf3bc20b516faeadfe77ae",
"secret_1": "8f2b39e8e594a4056eb1e6dbb4b0c38ef13b1b2c751f64f810ec04ee35b77270",
"secret_2": "bc628c79accd2364fd31511216a0fab62afd4a18ff77a20deded7b858c9860c8",
"secret_3": "59284fd1650ea9fa17db2b3acf59ecd0f2d52ec3261dd4152785813ff27a33bf",
"secret_4": "576c23393a8b31cc8da6688d9c9a96394ec74b40fdaf1f693a6bb84284334ea0"
}
```

The corresponding blinding factors `r` are

```json
{
"r_0": "ad00d431add9c673e843d4c2bf9a778a5f402b985b8da2d5550bf39cda41d679",
"r_1": "967d5232515e10b81ff226ecf5a9e2e2aff92d66ebc3edf0987eb56357fd6248",
"r_2": "b20f47bb6ae083659f3aa986bfa0435c55c6d93f687d51a01f26862d9b9a4899",
"r_3": "fb5fca398eb0b1deb955a2988b5ac77d32956155f1c002a373535211a2dfdc29",
"r_4": "5f09bfbfe27c439a597719321e061e2e40aad4a36768bb2bcc3de547c9644bf9"
}
```

The corresponding derivation paths are

```json
{
"derivation_path_0": "m/129372'/0'/864559728'/0'",
"derivation_path_1": "m/129372'/0'/864559728'/1'",
"derivation_path_2": "m/129372'/0'/864559728'/2'",
"derivation_path_3": "m/129372'/0'/864559728'/3'",
"derivation_path_4": "m/129372'/0'/864559728'/4'"
}
```

0 comments on commit 2e6fd72

Please sign in to comment.