Skip to content
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

Accounts discovery #2086

Merged
merged 9 commits into from
Jun 10, 2023
Merged

Accounts discovery #2086

merged 9 commits into from
Jun 10, 2023

Conversation

benma
Copy link
Contributor

@benma benma commented Apr 27, 2023

I open a draft PR without unit tests, to discuss the concept. Unit tests of this nature (lots of async) is tedious to write, so I will do that once the code is somewhat stable.

This implements accounts discovery for BTC/LTC.

This also allows users to create more than 5 accounts. At the moment, the first 5 can always be added like before, and afterwards an account can only be added if the previous one was used. We could limit adding the first 5 accounts in the same way - if so, in another PR.

The first three commits (fix race condition, simplify RenameAccount, mock btc/eth) are kind of independent of the main concept and can be merged if review is ok regardless of the the rest.

The commits are designed to be small enough to be easy to review.

@benma benma requested a review from Beerosagos April 27, 2023 02:55
@benma benma force-pushed the accountsync2 branch 2 times, most recently from 93b5664 to 1ae91cc Compare April 27, 2023 03:11
@Beerosagos
Copy link
Collaborator

Beerosagos commented May 2, 2023

First 3 commits looks good to me. I'm still looking into the code for the others, but with the first tests I noticed an inconsistency in the account summary view (see pic).

What I did:

  1. checkout master
  2. remove appfolder and start the app in dev mode
  3. go to account summary
  4. stop the app (edit: only the backend)
  5. checkout the PR
  6. start again the app -> the transition between the existing persisted accounts and the discovery doesn't look correct

Am I doing smth wrong?

EDIT: Tested again, properly restarting both the backend and the frontend. Everything seems to work, so I'd say that this is not a real problem.

immagine

@Beerosagos
Copy link
Collaborator

Code and tests LGTM, really nice!
The only thing I noticed: If the last available account is empty and it receives a transaction, the discovery of the next address doesn't seem to start until the app Is restarted/the account is disabled and re-enabled. Would it make sense to call maybeAddHiddenUnusedAccounts when an address status changes (e.g. https:/digitalbitbox/bitbox-wallet-app/blob/master/backend/coins/btc/account.go#L583)?

Copy link
Collaborator

@Beerosagos Beerosagos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tACK, with possible nit in the previous comment

@Beerosagos
Copy link
Collaborator

EDIT: Tested again, properly restarting both the backend and the frontend. Everything seems to work, so I'd say that this is not a real problem.

Tested again this morning on Android, got the same weird behavior. It was not meant to be a test for this PR, just needed to compile account discovery on testnet for Android, so here is what I did:

I don't see how these changes should break this tho. Closing the app and opening again fixed the issue.

image

Copy link
Collaborator

@Beerosagos Beerosagos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I think I found the problem:
backend.Accounts() sorts the account list while locking the Read lock (see https:/benma/bitbox-wallet-app/blob/accountsync2/backend/backend.go#LL429C16-L429C17), which doesn't seem correct. Moving the sortAccounts call inside backend.addAccount seems to solve the issue.
Locking the write lock inside backend.Accounts seems to cause a deadlock, and doesn't look good anyway, imho.

A couple notes:

  • with the suggested modification I could not reproduce the issue again, but I made a lot of tests removing the accounts.json file and restarting the servewallet. I noticed that sometimes the bb02 seems to take longer in deriving the keys (I can tell from the bb02 screen saver staying still instead of moving around), sometimes up to a minute or so. I thought of another race condition, but it apparently solves by itself. It also happened once that the bb02 apparently aborted some request and needed to be unplugged and replugged (no need to restart the servewallet) to get the app working again. No idea why.
  • I noticed that the number of scanned addresses is not always shown by the frontend, I didn't investigate it and neither checked if it is related to this PR or if that happens also on master.

benma added 4 commits June 9, 2023 23:29
An account could be Initialized() after it was already Closed() if
they run at the same time. In ETH, the isClosed check was missing
altogether, but even in btc, there was a race condition. We need to
use the same lock for both.

The `WaitSynchronized()` in the Close function of the ETH account also
seems unneccesary and if it blocks the Close indefinitely, the DB
cannot be closed and the an account with the same DB cannot be
initialized (e.g. in `ReinitializeAccounts()`). Not sure why it was
added, the commit that introduced it had no explanation.

I ran into these issues while working on unit tests for accounts sync.
The `account.Config().Config` is a pointer to the persisted account
stored in `backend.config.accountsConfig.Accounts`.

By making the the slice entry also a pointer, we can make sure that
the `account.Config().Config` always points to the persisted
config. Then, we can simply modify the persisted config to also modify
the loaded config.

Before that, the same was true but not deterministically: the stored
account config and the loaded config would point to the same memory
location, but only as long as the persisted account was not moved,
which can happen when the slice is extended (so the slice and all
contents is moved to a new location while growing the slice). If this
happens, then the loaded config would become a shallow copy, which is
error prone: modifying a primitive field would modify a copy, while
modifying a pointer field (e.g. the signing configuration) would
modify both the loaded and the persisted config.

The alternative to this commit would have been to instead make a deep
copy of the persisted config when loading it. It seemed to me like just
sharing the same memory here is simpler overall.
By being able to mock the accounts, it is much easier to write unit
tests that rely on specific acocunt behavior. For example, in accounts
discovery we check if `.Transactions()` returns an empty or non empty
list to decide when to stop scanning accounts. Without mocking the
account, we'd have to connect to mocked Electrum/Etherscan servers,
which is much harder to do and maintain.
We want to track if an account has been used (i.e. has a transaction
history). This is info is required to decide if the next account can
be created, according to BIP39.
@benma benma marked this pull request as ready for review June 9, 2023 21:35
In order to scan accounts until we find an empty account, we always
need to scan one account ahead of what is visible to the user.

For this, we introduce a flag "hiddenBecauseUnused", which is false by
default and only true if an account is added to be scanned in the
background. It is not shown to the user at all, until either:

- it is scanned and has a transaction history
- the user adds a new account in the settings - this activtes the
  hidden account if it was automatically added before.

Such hidden accounts are persisted for the same reason the regular
accounts are persisted: creating the entry requires the xpubs, which
requires a keystore (BitBox02), which is slow. By persisting it we do
not need to query the keystore for multiple xpubs everytime for the
hidden account, but just once.

The part where we add the hidden account automatically follows in the
next commit.
We implement BIP-44 accounts scanning, meaning: accounts per coin are
scanned until we find an unused account.

See the docstring of `maybeAddHiddenUnusedAccounts()` for details,
especially how and why we deviate from BIP-44.
`Accounts()` only held the read lock but modified the accounts, which
could lead to a race condition.
@benma
Copy link
Contributor Author

benma commented Jun 10, 2023

Thanks @Beerosagos !!

I fixed the race condition and added unit tests. While adding the unit tests I discovered that adding a new account in the settings does not set the name correctly if a previously added hidden account is activated - I fixed that too.

@benma benma merged commit 5bfb0ef into BitBoxSwiss:master Jun 10, 2023
@benma benma deleted the accountsync2 branch June 10, 2023 02:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants