-
-
Notifications
You must be signed in to change notification settings - Fork 98
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Determinstic secrets / ecash restore #131
Conversation
Codecov ReportPatch coverage:
Additional details and impacted files@@ Coverage Diff @@
## main #131 +/- ##
==========================================
+ Coverage 56.18% 56.62% +0.44%
==========================================
Files 43 43
Lines 3713 3961 +248
==========================================
+ Hits 2086 2243 +157
- Misses 1627 1718 +91
☔ View full report in Codecov by Sentry. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @callebtc, very cool new feature, I really like it.
I had a high-level look through the code and it looks good to me, just some minor comments. I probably don't yet understand all implications of this change though. 🙂
One additional question, from your description:
Should I derive secret from r or generate them independently?
Looks like you decided to generate them separately. Was there any specific reason for this or just a random choice?
Also, this will probably become a NUT, right?
Hey there, yes at least for now There are intricacies that arise if you derive one from the other. In order to keep those out, I've decided on this approach. I will write a NUT for this for sure after it has proven to work well! |
Great work, I tried to break it and found it difficult to do so. Will try put some eyes on the code, though with the codebase being quite mature as a whole it is difficult to fully create a mental model without substantial time reviewing/dabbling. Main test that found inconsistent behavior was around the initiation of restoring wallets while there were pending ecash sends at play. Sometimes able to receive previous sends even after wallet is restored (simulating alternative device). This leads to confusion and inability to accurately get pending information. It also can result in it being difficult to initiate new sends potentially pinning the funds (temporarily?). Also performed test for 1000 cycles of invoicing (1sat per cycle) from test mint. Got to around 700ish and then started to receive errors from the mint about the invoice secrets already being used. Test with around 100 iterations of invoices and some sends restored and balance reflected correctly. Similar test with 700+ iterations showed balance of funds as expected. Removed DB and restored by balance showed 0 on restore. This item could be related to the test mint. Does it have some time limit/restrictions on the tokens minted? (it did take some time to cycle through the 700+ iterations). Some restorations showed 0 balance but then other times after some time restoration showed funds. Again this could be related to the test mint if there are some odd interactions with mint time limits or if the funds get swept after some time. Happy to do some more testing/repeat actions as needed. |
Hello @Mynima thank you for that awesome detailed response. Could you share the exact steps and set of commands that lead to some of the inconsistencies you have described? I'm not sure if there is an easy fix for restoring when pending tokens are in the db. The tokens don't have a special state from the perspective of the mint so a restore should make them available in the new wallet as if they are normal non-pending tokens again. If that was the case for you, everything should be fine. As I said, I would love to be able to reproduce the other behavior you described. If you can help me with those, I should be able to write tests that reveal these inconsistencies. Cheers! |
No worries at all, it was definitely doing something funny where it appeared that funds (after restore) were pinned (perhaps temporarily) from the perspective of performing a 'SEND ecash' action. Here is the sequence of events to re-test:
You may want to repeat this test a few times as sometimes it looked like it could be OK and the funds were available in the restored wallet. The situation this simulates is if someone has tried to send funds and then restored or dual restored with a second device. By offering recovery we inherently open the door to people thinking about having the same seed on multiple devices, as it is how we're conditioned to think about bitcoin. However, the state of the ecash is governed by the private DBs and not a shared ledger, which means that restoring in multiple places does give you access to the initial pool of funds, however each devices/restore will have a separate database on which the state of the wallet will be tracked. This means we need to provide clear guidance that users should consider restoration as a means of moving devices (or retrieval of lost funds), not means of having multiple installations. It would be more advisable that for multiple devices users maintain separate wallets and send/receive between these wallets as needed. For the other tests I wrote a simple loops in the shell.
|
Gotcha, I think here lies the crux: When you do a To your first point: Now, when you restore To your second point: When you try to send the "pending" 100 sats from Hope it makes sense. Very interesting case you've looked at. It will cause weird problems when you use the same seed across two wallets and I can't think of a way to prevent that. |
Perfect summary 🙏 |
Thank you to all reviewers! LFG! |
This PR introduces the ability to restore the user's ecash balance from a single secret with the help of the mint.
Teaser: https://twitter.com/callebtc/status/1620186555993456641
User testing
To test this feature, please install cashu from the GitHub repository. Then add the testnut mint that already runs this branch. This is the process:
Your wallet should be ok now. You can now mint tokens, send them to yourself, delete your wallet and restore it:
Deterministic secret generation
The wallet generates a private key and has a counter (called
counter
below) that starts at0
. There are two (previously random) inputs for token generation:secret
andblinding_factor
to generateBlindedMessages
.Previously, we used random numbers for these. Here, we generate them deterministically from the
private_key
and a counter that increments for every token we create.Deriving secrets
We produce a standardized BIP32-compatible derivation path for generating
secret
and blinding factorr
for each token. The path encodes:129372'
(UTF-8 for 🥜)0'
keyset_id_int'
counter'
<-- this one goes up0
or1
Thus:
The wallet increments
counter
after every derivation. When the wallet restores its funds, it restartscounter
at0
and increments it as long as necessary to restore its balance.Quirks
When a wallet sends a
POST /mint
request, it attachesBlindMessages
to it that will necessarily increment thecounter
. However, if the invoice has not been paid yet, they won't be converted intoProofs
. Therefore, say, if the wallet would attach 10BlindMessages
and tried to request the mint 5 times without success, thecounter
would be incremented by 50 without having any correspondingProofs
for the appropriate values. The number of missedcounter
values a wallet might have missed is not easy to predict either. Therefore, a wallet MUST not incrementcounter
for failedPOST /mint
requests but only for successful ones.Backup restore
After a database wipe, we can use the deterministic derivation to re-generate the same
BlindedMessages
that we had generated before. Generally, the user won't remember the state of their lastcounter
before the database wipe, so we generate many of these, say100
per request.We can send these 100
BlindedMessages
to the mint and ask it politely to re-issue theBlindedSignatures
for them. The mint was so kind to keep these in their database. The mint matches the providedBlindedMessages
against its database and returns allBlindedSignatures
it issued for these matches.The wallet uses the endpoint
POST /restore
and sends an ordinaryPostMintRequest
(which includes theBlindedMessages
(this is the same object we use forPOST /mint
).The wallet then receives the object
PostRestoreResponse
:The fields
outputs
and promisesonly contain positive matches. If we had 10 tokens during the wallet's life time, we will receive 10 matches (and not 100 objects as we sent to the mint). The wallet now needs to take these
outputsand filter the the correct
secretand
blinding_factorfrom the
100` it generated in the beginning of the restore process.Things to decide:
secret
fromr
or generate them independently? -> independentlyTodo:
POST /mint
requests do not increment the counterProof
in the db