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.
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 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 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 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
orr
=0
or1
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.
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.
The integer representation keyset_id_int
of a keyset is calculated from its hexadecimal ID 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:
keyset_id_int = int.from_bytes(bytes.fromhex(keyset_id_hex), "big") % (2**31 - 1)
Example in JavaScript:
keysetIdInt = BigInt(`0x${keysetIdHex}`) % BigInt(2 ** 31 - 1);
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) or by downloading the entire database of the mint (TBD).
The wallet takes the following steps during recovery:
- Generate
secret
andr
fromcounter
andkeyset
- Generate
BlindedMessage
fromsecret
- Obtain
BlindSignature
forsecret
from the mint - Unblind
BlindSignature
toC
usingr
- Restore
Proof = (secret, C)
- Check if
Proof
is already spent
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.
secret = bip32.get_privkey_from_path(secret_derivation_path).hex()
The wallet similarly generates a blinding factor r
from the r_derivation_path
:
r = self.bip32.get_privkey_from_path(r_derivation_path)
Note: For examples, see the test vectors.
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 restore endpoint or by downloading the entire mint's database.
Using the restored BlindSignatures
and the r
generated in the previous step, the wallet can unblind the signature to C
. The triple (secret, C, amount)
is a restored Proof
.
If the wallet used the restore endpoint NUT-09 for regenerating the Proofs
, it additionally needs to check for the Proofs
spent state using NUT-07. The wallet deletes all Proofs
which are already spent and keeps the unspent ones in its database.
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 incrementcounter
- 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.