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

wallet: background sync with just the view key [master] #8619

Merged
merged 1 commit into from
Jul 16, 2024

Conversation

j-berman
Copy link
Collaborator

@j-berman j-berman commented Oct 21, 2022

This PR brings a set of additions to wallet2 to enable background syncing as discussed in #8082 (initially proposed by @hyc). Note: I've made significant changes to my original PR from discussion in this PR.

How it works

This PR exposes 2 options for background syncing.

Option 1: Reuse the wallet password to encrypt the background cache
Option 2: Use a custom background cache password

Option 1

A user would choose Option 1 if they do not want to grant their system perpetual access to their plaintext view key.

  • Use case: the user is inactive for 90 seconds or explicitly locks their wallet. The wallet wipes the spend key from memory and automatically starts background syncing. When the user returns to their wallet, provides their password and resumes their session, the wallet brings the spend key back in memory and continues syncing from where the background sync left off.

NOTE: Ideally this Option 1 would be a full wallet's default behavior and the user would not need to opt-in to this behavior. This PR does not make this Option 1 the wallet's default behavior in order to minimize impact in case there is an issue with its implementation. It can be made the wallet's default behavior in the future.

Option 2

A user would choose Option 2 if they want to grant their system perpetual access to their plaintext view key.

  • Use case 1: the user's mobile OS closes the wallet app while the wallet is syncing. The mobile device can auto-restart background syncing without the user needing to manually reopen the app (note: the mobile OS would have perpetual access to the user's plaintext view key in this case).
  • Use case 2: the user wants a desktop background wallet syncing gadget that auto-starts on boot. This syncing gadget has perpetual access to the background cache password, so the gadget can restart the background sync at any point, but cannot access the user's spend key.
  • Use case 3: same as Option 1's use case.

CLI

set background-sync <off|reuse-wallet-password|custom-background-password>

  1. off (the default)
    • Upon setting, deletes any "background" files the wallet has saved.
  2. reuse-wallet-password
    • After setting, when the user is inactive, if the inactivity-lock-timeout is set and auto-refresh is set, then the wallet will automatically background sync (wipe the spend key from memory and sync with view key hot).
    • Note: this option does not create any distinct "background" files. A user can use this option to background sync an already opened normal wallet.
  3. custom-background-password
    • When setting, the user is prompted to enter a custom background sync cache password. This password must be distinct from the user's wallet password.
    • Upon setting, the wallet generates a "background" wallet from the main wallet and stores it on disk encrypted with a key derived from the custom background password. This newly created "background" wallet has no access to the spend key, nor any user-specified data saved (contacts, destinations, tx keys, tx notes, labels, tags). This "background" wallet can be opened and synced in a distinct process when the normal wallet is closed.
    • When the user is inactive, if the inactivity-lock-timeout is set and auto-refresh is set, then the wallet will automatically background sync.

Opening a "background" wallet

This feature is only available for users that selected Option 2 (custom-background-password).

If the wallet's filename is foo, then to open its corresponding background wallet, open wallet file foo.background of the same directory.

Thus, a user could write a script to auto-start background sync on startup that starts the CLI monero-wallet-cli --wallet-file <path to background wallet> --password <custom background password>.

RPC

/setup_background_sync

Enables syncing in the background with just a view key hot in memory.

Inputs

  • background_sync_type - string("off"|"reuse-wallet-password"|"custom-background-password"); "reuse-wallet-password" (reuse the wallet password to encrypt the background cache); "custom-background-password" (use a custom background password to encrypt the background cache)
  • wallet_password - string
  • background_cache_password - string; (Optional) Custom background cache password used to encrypt the background cache. This value is only necessary when the background_sync_type is "custom-background-password".

Outputs

None.

/start_background_sync

Wipes the wallet's spend key from memory and enables an already open wallet to continue syncing with just the view key hot in memory.

Inputs

None.

Outputs

None.

/stop_background_sync

A background syncing wallet reloads the spend key back in memory and processes the background cache. After successful execution, the wallet can then continue syncing normally from where the background sync left off.

Inputs

wallet_password - string
seed - string; (Optional) Mnemonic phrase of the wallet.
seed_offset - string; (Optional) Offset used to derive a new seed from the given mnemonic to recover a secret wallet from the mnemonic phrase.

Outputs

None.

Opening a "background" wallet

If the wallet's filename is my_foo_wallet, then to open its background wallet, simply call /open_wallet with filename set to my_foo_wallet.background and password set to whatever the background password is set to.

GUI

See monero-project/monero-gui#4050

Mobile wallets

This is how mobile wallet devs can utilize the Options from this PR should they choose to (inspired by discussions with @valldrac, @r4v3r23, and @sgp).

Option 1

  • A wallet dev would call setupBackgroundSync(BackgroundSync_ReusePassword, <main wallet password>) in order for the wallet to use this option.
  • If the user does not use the app for a certain period of time, the app can auto-start background syncing.
    • The wallet dev would call startBackgroundSync() on whatever trigger the dev wants, and continue syncing the wallet as they would a normal wallet.
  • When the user returns to the app and provides their PIN/password, assuming the wallet stayed open and syncing, the app can process the background cache.
    • The wallet dev would call stopBackgroundSync(wallet_password).
  • In the event the user reboots their device or the OS closes the app while background syncing, the app would not attempt to restart background sync. The user must reopen the wallet manually with their PIN/password in order to continue syncing.
    • The wallet dev would call openWallet(path="<path>/wallet_filename", password=<main wallet password>) like normal and the wallet will process the background cache automatically.

Option 2

  • A wallet dev would call setupBackgroundSync(BackgroundSync_CustomPassword, <main wallet password>, <custom background password>) in order for the wallet to use this option.
    • A wallet dev could generate a random background password under the hood and keep it stored in the mobile device's secure key store.
  • If the user does not use the app for a certain period of time, the app can auto-start background syncing (same behavior as above).
    • The wallet dev would call startBackgroundSync() on whatever trigger the dev wants, and continue syncing the wallet as they would a normal wallet.
  • When the user returns to the app and provides their PIN/password, assuming the wallet stayed open and syncing, the app can process the background cache (same behavior as above).
    • The wallet dev would call stopBackgroundSync(wallet_password).
  • In the event the user reboots their device or the OS closes the app while background syncing, the app can attempt to restart background sync automatically under the hood by reading the custom background password from the key store and reopening the background wallet.
    • The wallet dev would call openWallet(path="<path>/wallet_filename.background", password=<random custom background password saved in key store>) and sync the background wallet as they would a normal wallet.
  • Once the user reopens their wallet with their PIN/password, the wallet will process the background cache and continue syncing.
    • The wallet dev would call openWallet(path="<path>/wallet_filename", password=<main wallet password>) like normal and the wallet will process the background cache automatically.

@j-berman
Copy link
Collaborator Author

Updated: ignored callbacks (like on_money_received) when background sync is disabled and the wallet is processing txs in the background sync cache to avoid triggering many unnecessary display changes in the GUI when processing many txs.

@@ -1699,10 +1715,11 @@ namespace tools
*/
bool load_keys_buf(const std::string& keys_buf, const epee::wipeable_string& password);
bool load_keys_buf(const std::string& keys_buf, const epee::wipeable_string& password, boost::optional<crypto::chacha_key>& keys_to_encrypt);
void process_new_transaction(const crypto::hash &txid, const cryptonote::transaction& tx, const std::vector<uint64_t> &o_indices, uint64_t height, uint8_t block_version, uint64_t ts, bool miner_tx, bool pool, bool double_spend_seen, const tx_cache_data &tx_cache_data, std::map<std::pair<uint64_t, uint64_t>, size_t> *output_tracker_cache = NULL);
void process_new_transaction(const crypto::hash &txid, const cryptonote::transaction& tx, const std::vector<uint64_t> &o_indices, uint64_t height, uint8_t block_version, uint64_t ts, bool miner_tx, bool pool, bool double_spend_seen, const tx_cache_data &tx_cache_data, std::map<std::pair<uint64_t, uint64_t>, size_t> *output_tracker_cache = NULL, bool ignore_callbacks = false);

Choose a reason for hiding this comment

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

ignore_callbacks parameter provides the option to bypass the execution of callbacks if it is set to true.

bool should_skip_block(const cryptonote::block &b, uint64_t height) const;
void process_new_blockchain_entry(const cryptonote::block& b, const cryptonote::block_complete_entry& bche, const parsed_block &parsed_block, const crypto::hash& bl_id, uint64_t height, const std::vector<tx_cache_data> &tx_cache_data, size_t tx_cache_data_offset, std::map<std::pair<uint64_t, uint64_t>, size_t> *output_tracker_cache = NULL);
void detach_blockchain(uint64_t height, std::map<std::pair<uint64_t, uint64_t>, size_t> *output_tracker_cache = NULL);
detached_blockchain_data detach_blockchain(uint64_t height, std::map<std::pair<uint64_t, uint64_t>, size_t> *output_tracker_cache = NULL);

Choose a reason for hiding this comment

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

returns a detached_blockchain_data object, which contains information about the detached part of the blockchain up to the given height.

bool should_skip_block(const cryptonote::block &b, uint64_t height) const;
void process_new_blockchain_entry(const cryptonote::block& b, const cryptonote::block_complete_entry& bche, const parsed_block &parsed_block, const crypto::hash& bl_id, uint64_t height, const std::vector<tx_cache_data> &tx_cache_data, size_t tx_cache_data_offset, std::map<std::pair<uint64_t, uint64_t>, size_t> *output_tracker_cache = NULL);
void detach_blockchain(uint64_t height, std::map<std::pair<uint64_t, uint64_t>, size_t> *output_tracker_cache = NULL);
detached_blockchain_data detach_blockchain(uint64_t height, std::map<std::pair<uint64_t, uint64_t>, size_t> *output_tracker_cache = NULL);
void handle_reorg(uint64_t height, std::map<std::pair<uint64_t, uint64_t>, size_t> *output_tracker_cache = NULL);

Choose a reason for hiding this comment

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

The purpose of the handle_reorg function is to handle a blockchain reorganization event. A blockchain reorganization occurs when a different branch of the blockchain becomes longer than the current branch, causing a switch to the new branch. When this happens, some blocks that were previously part of the blockchain may become invalidated, and their transactions must be removed from the wallet's transaction pool and reprocessed against the new branch.

handle_reorg function ensures that the wallet is synchronized with the current state of the blockchain after a reorganization event.

@@ -1741,6 +1758,7 @@ namespace tools
std::vector<size_t> get_only_rct(const std::vector<size_t> &unused_dust_indices, const std::vector<size_t> &unused_transfers_indices) const;
void scan_output(const cryptonote::transaction &tx, bool miner_tx, const crypto::public_key &tx_pub_key, size_t i, tx_scan_info_t &tx_scan_info, int &num_vouts_received, std::unordered_map<cryptonote::subaddress_index, uint64_t> &tx_money_got_in_outs, std::vector<size_t> &outs, bool pool);
void trim_hashchain();
cryptonote::block_header_response get_block_header_by_height(uint64_t height);

Choose a reason for hiding this comment

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

used to retrieve the block header data of a block at a specific height. This information can be useful for various operations such as verifying the blockchain or retrieving information about a particular block.

@@ -1753,6 +1771,8 @@ namespace tools
crypto::chacha_key get_ringdb_key();
void setup_keys(const epee::wipeable_string &password);
size_t get_transfer_details(const crypto::key_image &ki) const;
tx_entry_data get_sorted_tx_entries(const std::unordered_set<crypto::hash> &txids);

Choose a reason for hiding this comment

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

This function is typically used to retrieve transaction data from the wallet's transaction pool, which contains all unconfirmed transactions known to the wallet.
is to retrieve and sort transaction data for a given set of transaction IDs.

@plowsof
Copy link
Contributor

plowsof commented May 24, 2023

this pull request currently has a 10.5~xmr reward after being reviewed/merged https://bounties.monero.social/posts/41/10-503m-implement-viewkey-based-background-refresh-to-significantly-reduce-wallet-sync-time

@ndorf
Copy link
Contributor

ndorf commented Jun 6, 2023

One implementation detail worth highlighting/discussing: assume a user needs to rescan for their tx's (or spends from a different device), the background cache wouldn't be able to pick up that the user spent outputs (need the spend key for this).

When spends have real change outputs, those txes will be cached. When using monero-wallet-cli in view-only mode, and key images are later imported, it notices that one or more match and adjusts everything accordingly.

Unfortunately, this doesn't work when there is no change, because the dummy output is to a random address, not one of our own. So, there is nothing to indicate to a view-only wallet that it should pay attention to these transactions at all. The user must run rescan_bc keep_ki after importing the key images (or in your case, unlocking the spend key) in order to find them.

@j-berman
Copy link
Collaborator Author

j-berman commented Jun 7, 2023

I don't think this feature makes much sense for view-only wallets. View-only wallets can just keep the refresh thread alive in the background without concern. I disabled the feature for view-only wallets (you can't enable background sync mode on a view-only wallet in this PR).

Also to be clear, "background sync mode" in this PR has a totally separate implementation from "view-only" mode. It's implemented like light wallets. The "background sync mode" scanner picks up all "plausible" spends (txs with ring(s) which include a prior received output as a ring member), then when the spend key loads, processes all those "plausible" txs to determine if they are spends or not. This is how it picks up 0 change spends like light wallets.

I also included a more detailed comment above process_background_sync_cache explaining how it works, and included a test for this exact 0 change scenario.


Separate comment: will resolve merge conflicts soon. I also think #8568 is going to be merged soon which will make the diff a lot easier to get through too.

@ndorf
Copy link
Contributor

ndorf commented Jun 7, 2023

The "background sync mode" scanner picks up all "plausible" spends [...]

Nice! Thanks for the clarification.

@j-berman
Copy link
Collaborator Author

Update: I need to update this PR to ensure that when background sync is disabled, txs in the background sync cache are processed in the correct order they appear in the chain. Related to this comment on the scan_tx feature.

I'm going to wait until #8568 is merged before working on this; waiting until then will simplify conflict resolution and review.

@j-berman
Copy link
Collaborator Author

This PR is ready for review again. At the time of writing this comment, the feature this PR implements has a bounty of 10.503 XMR. If this PR is merged, I'll allocate 50% of the bounty to reviewers (proportions at my discretion), and 50% to @hyc for the initial idea.

Disclaimer 1: the CLI and RPC wallets currently wipe secret spend key material from memory on a best effort basis with possible room for improvement; users shouldn't assume this is perfectly ironclad.
Disclaimer 2: the wallet API (used by the GUI and other wallets) has known sections of code where secret spend key material is not hardened to protect from memory exfiltration.

CLI + RPC

Some avenues worth further investigation and consideration:

  • @moneromooo-monero proposed an idea to use public key encryption for wallet RPC requests/responses that include sensitive key material like seeds, private keys, and passwords (example). The idea is the wallet RPC server would be careful never to place sensitive key material in swap and would be sure to wipe memory when sensitive plaintext data is no longer in scope/on graceful shutdown. Any data passed to the OS/network stack buffers would be encrypted thereby minimizing surface area for sensitive plaintext to linger in memory or in swap.
  • Use epee::wipeable_string when serializing and de-serializing sensitive key material. (source)

Wallet API

@tobtoht proposed a relatively simple idea here:

  • Wallet password caching should be removed from wallet/api. Password verification should use wallet2::verifyPassword instead of comparing against the cached value. This causes a slight delay when opening dialogs that require the password or when unlocking the wallet.

Imo it also may be worth investigating using epee::wipeable_string instead of std::string for all sensitive data in the wallet/api (guessing this could cause binding issues). Perhaps it would make sense to offer _sensitive functions on top of the existing API (avoids breaking consumers), or just say consumers should avoid wallet/api and use wallet2 directly if they want greater protection from memory exfiltration.

@j-berman
Copy link
Collaborator Author

Implemented a cleaner and more sane approach when disabling background sync mode and reloading the spend key (by reading just the spend key from the wallet keys file, rather than reload the entire wallet keys file) and made tests more robust. Not planning any further changes on my end until review.

@j-berman
Copy link
Collaborator Author

Updated to make sure the wallet doesn't throw when processing the background sync cache if std::sort compares the same two elements together (see #8956).

@rbrunner7
Copy link
Contributor

I started to look into this today with the goal of doing a full review within a useful time frame.

@rbrunner7
Copy link
Contributor

I started to review and test. I tested what happens if you have a second wallet for the same keys and you spend in the second wallet while the first one is background-syncing.

My expectation: The first wallet sees the change coming back normally. However, it cannot recognize the spends for sure, but adds the transaction as "plausible spend" to the background sync cache.

What I see happening, if I interpret the log output correctly: Contrary to expectation, the first wallet sees everything. Here part of the log from the first wallet after it processes the transaction in question:

2023-07-23 08:58:21.958 D Adding received tx <eaa2ce3b82e3c597e018d3142e133063d1b24fdbdeefcff054360d09344ffd82> to background sync cache (idx=1)
2023-07-23 08:59:13.967 D Setting UNSPENT: ki <0000000000000000000000000000000000000000000000000000000000000000>, amount 0.040938230000
2023-07-23 09:00:02.928 W Received money: 0.040938230000, with tx: <eaa2ce3b82e3c597e018d3142e133063d1b24fdbdeefcff054360d09344ffd82>
2023-07-23 09:00:36.764 W Spent money: 0.111000000000, with tx: <eaa2ce3b82e3c597e018d3142e133063d1b24fdbdeefcff054360d09344ffd82>
2023-07-23 09:08:00.481 D Setting SPENT at 2289181: ki <a68d3ceaf08d803f2410269979fe63d9cd2fc0eca90d51408a3ce04cd2caf1d6>, amount 0.111000000000
2023-07-23 09:10:19.245 W Spent money: 0.010000000000, with tx: <eaa2ce3b82e3c597e018d3142e133063d1b24fdbdeefcff054360d09344ffd82>
2023-07-23 09:10:21.047 D Setting SPENT at 2289181: ki <294d67ae1b10b6d55ee76ccb94e17a15f60a8748206f49a7357f29a3ceb84152>, amount 0.010000000000

I checked in the debugger that the spend secret key is indeed all zero in the account of the wallet object, yet still it sees the spends somehow.

What do I misunderstand or overlook?

@rbrunner7
Copy link
Contributor

In the meantime I learned more about the system, and I think I know now the answer to my own question from a few hours ago here: It sees those spends because it knows their key images.

I am running now a test where the first wallet does not have them, and as expected, it does not see the spends yet while background-syncing.

@rbrunner7
Copy link
Contributor

I noticed that scanning / watching the transaction pool makes no difference between normal and background sync, i.e. pool processing continues normally even in background sync.

Is this really useful?

Seems to me that if we suppress that while background syncing we have lower load on the connected daemon, less file writes, less processing, and we don't have to worry whether the peculiarities of the missing spend secret key could influence detecting pool transactions somehow and lead to unfortunate differences in behavior.

size_t bgs_cache_idx = m_background_sync_cache.size();
LOG_PRINT_L2("Adding plausible spent tx " << txid << " to background sync cache (idx=" << bgs_cache_idx << ")");
m_background_sync_cache.insert({txid, background_syncd_tx{bgs_cache_idx, tx, o_indices, height, ts, double_spend_seen}});
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Info that may help fellow reviewers and testers:

This case is not easy to hit because in most situations a transaction has either an enote going into the wallet, or the wallet knows already one of the enotes that serve as inputs for a transaction, both cases which are then handled elsewhere.

The easiest way that I found to trigger this particular case while testing "manually":

  • Create a new wallet and make copies of the wallet files
  • Let the first still empty wallet go into background sync
  • Send a tx to the wallet address to fund, wait until unlocked
  • Spend the funds using the copied wallet files, using sweep_single instead of transfer to avoid change
  • The first wallet in background sync will go through this code as soon as the spend tx gets mined

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

If you're testing with the RPC wallet, the following should trigger it as well:

  • Create a new wallet
  • Receive to the wallet
  • Sweep all funds out of the wallet (or use sweep_single)
  • Enable background sync mode via the new /set_background_sync RPC
  • Hard rescan via /rescan_blockchain
    • The wallet should consider the sweep tx a "plausible spent tx" when scanning

The functional tests do this.

@j-berman
Copy link
Collaborator Author

Is this really useful?

Agree it is not and with your point that the wallet shouldn't process the pool when background sync is enabled. Will update the PR.


Adding some more color to your question prior why the second wallet could still identify a spend from the first wallet: in that example you gave, the scanner picks up the change output in that tx, which the scanner doesn't need the spend key to identify. Plus, the scanner will only add a tx to the background cache one time. Thus, I wouldn't expect it to hit that "plausible spent tx" line when scanning that tx that actually has the spend.

Additionally, when the wallet is not in background sync mode and scans a received output, the wallet will use the spend key to generate a key image for that output and save the output's associated key image in the wallet cache (this is why the default CLI asks for a password when it identifies a received output when scanning). Thus, the scanner is able to later identify when that output is spent even while in background sync mode, and does not need to add any "plausible" spends of that output when scanning later.

As you've noticed, it's only when the wallet scans a received output while in background sync mode when I'd expect the wallet to then add "plausible" spends for that tx. This is because while in background sync mode, the wallet is unable to generate a received output's associated key image and thus can't identify when the output is spent. And final point that you picked up: if the output is spent in a tx where the user received change, the "background sync" scanner will add the tx to the background sync cache as a "received tx" and won't then need to add it again as a "plausible".

@rbrunner7
Copy link
Contributor

I brainstormed a bit. Maybe the following is out-of-scope for this PR, but maybe not, curious what you people say:

If I imagine the perfect background sync mechanism for Monero wallets, I see something that is able to survive computer / notebook / smartphone restarts.

Say my machine has a Monero wallet in background sync, and as it can be a long time until I use that wallet again in a regular way, maybe weeks, there is a realistic chance that a reboot / restart will happen in the meantime, with a Windows machine probably about once a month, after updates. Can we build a really robust background sync mechanism that resumes after restart, automatically without user intervention and ist just about impossible to "kill"?

After all, especially after a long period of not using a Monero wallet, background sync would be of the greatest value, and if I then find out that background sync stopped a few weeks ago because of a restart, that's not a good UX.

What would be needed to build something like that?

I see a need for a new low-level wallet open method, or a new boolean for the existing open method in wallet2, that opens the wallet directly into background sync mode. Looking at the code I would say no big deal.

But - of course - we have a much more fundamental problem: For opening the wallet again when resuming work after a restart, any background sync service / server process would need the wallet's password, and where would it get that from without undermining the security of the whole system that we build now with this PR?

My brainstorming came up with the following possible solution:

  • When the wallet app puts a wallet into background sync, it saves a copy. That copy does not contain the spend private key and therefore needs no password, or a fixed one that the wallet app and the background sync service agree on without a need for help from the user
  • The background sync service uses that copy, is able to re-open it after a restart because it knows the password, and just resumes background sync for that wallet, with wallet2 continuing to build the tx list in m_background_sync_cache.
  • If the user finally wants to re-open the wallet, the wallet app opens the copy, grabs the content of m_background_sync_cache in there (new method), hands it over to the "real" wallet (another new method) and asks wallet2 to process it.

@j-berman
Copy link
Collaborator Author

The most significant difference with your proposed approach is that the view key would remain decrypt-able on disk forever. I had a similar line of thinking here and decided against that approach because it felt wrong to do that.

If there is significant interest from others to implement the feature like that instead (an always-on background syncing gadget that does not require user input to start but can leak a wallet's entire balance on disk), then I think it would make sense to move in that direction. Your brainstormed solution I think is a viable path. Tagging @hyc, @tobtoht, @m2049r, @SamsungGalaxyPlayer, and bounty author @CryptoGrampy in case they have opinions as well.

I personally think as is, this PR enables a sweep spot of security and usability for the feature:

  • If the user is inactive and the wallet remains open, the wallet can enable background sync without user input.
  • If the user's machine reboots, a wallet gadget can start automatically and prompt the user to re-enter their password in order to enter background sync mode.
  • No user data on disk is accessible without the user's password.

I see a need for a new low-level wallet open method, or a new boolean for the existing open method in wallet2, that opens the wallet directly into background sync mode.

This is also doable. I think it would make sense to implement it if/when someone indicates they would actually use it if it's there.

@rbrunner7
Copy link
Contributor

The most significant difference with your proposed approach is that the view key would remain decrypt-able on disk forever. I had a similar line of thinking here and decided against that approach because it felt wrong to do that.

Right, didn't think of that, security-wise it's a step to the worse. Maybe asking for the password when resuming background sync would be indeed doable.

Your current solution definitely has a surprising simplicity, which is a big plus.

@rbrunner7
Copy link
Contributor

I think thanks to reviewing and testing I finally get a conceptual grasp of this thing, and I can summarize 3 possible levels of background sync functionality based on this PR:

  • Level 1: A wallet app can keep a wallet file open all the time, for days or even weeks, and background-sync it itself
  • Level 2: A wallet app hands over a wallet file to some background sync service which then thanks to some special capabilities beyond that of the wallet app manages to run and keep open the file for a long time
  • Level 3: A wallet app hands over a wallet file to some background sync service together with a way to re-open the file or a copy thereof should a restart of the service be necessary

Level 1 is possible with the PR like it is as of today, with its only small and quite simple additions to the existing Monero code. Smartphone devs could check whether this is possible for an Android or iOS app like Monerujo or Cake Wallet: Open a wallet file in the foreground, go into background with the file open, stay there running for days or even weeks, all the while with, you guessed it, keeping the file open continuously.

Level 2 would be possible, as far as I can see, with only 1 small addition to the PR as-is: The capability to open a wallet file into background sync mode. This would make it possible for the wallet app to send a filename and a password to that background sync service, that service to open the wallet file, wipe the password from memory right away, and then keep syncing for days or weeks.

Level 3 would need more additions to the code, maybe along the lines of my earlier "brainstorming" comment here, whith the additional security trade-off of having the view key stored on disk permanently.

@j-berman
Copy link
Collaborator Author

Yep I figure this PR as is is the optimal first step to enable the feature.

@SamsungGalaxyPlayer
Copy link
Collaborator

For Cake, we don't really care about requiring a user prompt, if I'm understanding this correctly. The idea for us is having the background sync easily run in the background as much as the OS allows, without needing a "hot" private spend key.

@j-berman
Copy link
Collaborator Author

Squashed and rebased

Copy link
Contributor

@jeffro256 jeffro256 left a comment

Choose a reason for hiding this comment

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

This is my first review pass, mainly pertaining to serialization

@@ -231,6 +231,7 @@ namespace config
} }; // Bender's nightmare
std::string const GENESIS_TX = "013c01ff0001ffffffffffff03029b2e4c0281c0b02e7c53291a94d1d0cbff8883f8024f5142ee494ffbbd08807121017767aafcde9be00dcfd098715ebcf7f410daebc582fda69d24a28e9d0bc890d1";
uint32_t const GENESIS_NONCE = 10000;
std::string const BACKGROUND_WALLET_SUFFIX = ".background";
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't know if this string belongs here

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Moved into wallet2.cpp

Comment on lines 1332 to 1333
if ((m_background_sync_type == BackgroundSyncCustomPassword && m_is_background_wallet) ||
(m_background_sync_type == BackgroundSyncReusePassword && !m_is_background_wallet))
Copy link
Contributor

Choose a reason for hiding this comment

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

This conditional makes deserialization dependent on runtime variables, which makes it impossible to load as-is without dependencies. What I suggest is incrementing the version, then, if the version is great enough, serializing m_background_sync_data unconditionally. It makes the code much cleaner, allows the cache to be deserialized without any known context, and makes recovering from errors easier, only at the cost of a couple bytes. Since you can check the actual value of m_is_background_wallet and m_background_sync_type against the expected values, you can actually provide meaningful feedback to the user instead of "huh, wallet didn't deserialize, that sucks".

if (ver < 31)
{
    m_background_sync_data = background_sync_data_t{};
    return;
}
a & m_background_sync_data;

... (AT THE BOTTOM OF THE FILE)

BOOST_CLASS_VERSION(tools::wallet2, 31)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done in the latest.

The reason I originally did that was to enable loading a newer normal wallet with no background cache in an older wallet version, but in hindsight I think that was unnecessary.

Comment on lines 1374 to 1388
if ((m_background_sync_type == BackgroundSyncCustomPassword && m_is_background_wallet) ||
(m_background_sync_type == BackgroundSyncReusePassword && !m_is_background_wallet))
{
FIELD(m_background_sync_data)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Same comment here, except you change VERSION_FIELD.

@@ -1376,6 +1460,7 @@ namespace tools
void enable_multisig(bool enable) { m_enable_multisig = enable; }
bool is_mismatched_daemon_version_allowed() const { return m_allow_mismatched_daemon_version; }
void allow_mismatched_daemon_version(bool allow_mismatch) { m_allow_mismatched_daemon_version = allow_mismatch; }
uint64_t kdf_rounds() const { return m_kdf_rounds; };
Copy link
Contributor

Choose a reason for hiding this comment

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

unused

VERSION_FIELD(0)
VARINT_FIELD(index_in_background_sync_data)

// prune tx; don't need to keep signature data
Copy link
Contributor

Choose a reason for hiding this comment

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

If we're not serializing the signatures, could we use a cryptonote::transaction_prefix?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

process_new_transaction processes background synced txs upon reloading the spend key and takes a cryptonote::transaction as a param; changing it to take a cryptnote::transaction_prefix instead has other undesirable effects e.g. requires changing the wallet2 callbacks as well. Which means we'd want to initialize a cryptonote::transaction from a cryptonote::transaction_prefix instead so we don't have to change process_new_transaction's function signature, which seems more complex than simply serializing the base here. I'm open to an alternative if you think there's a simpler solution here.

Comment on lines 4364 to 4366
m_background_sync_data.first_refresh_done = false;
m_background_sync_data.start_height = 0;
m_background_sync_data.txs.clear();
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
m_background_sync_data.first_refresh_done = false;
m_background_sync_data.start_height = 0;
m_background_sync_data.txs.clear();
m_background_sync_data = background_sync_data_t{};

Comment on lines 4385 to 4387
m_background_sync_data.first_refresh_done = false;
m_background_sync_data.start_height = 0;
m_background_sync_data.txs.clear();
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
m_background_sync_data.first_refresh_done = false;
m_background_sync_data.start_height = 0;
m_background_sync_data.txs.clear();
m_background_sync_data = background_sync_data_t{};

@@ -1685,6 +1777,7 @@ namespace tools
void get_short_chain_history(std::list<crypto::hash>& ids, uint64_t granularity = 1) const;
bool clear();
void clear_soft(bool keep_key_images=false);
void clear_user_data();
Copy link
Contributor

Choose a reason for hiding this comment

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

I would put a comment here explaining to future devs what constitutes "user data" in this case, so if they add fields in the future they can know to clear them here when applicable. We know that this method is meant to clear large blobs of data pertaining to wallets that isn't useful for background scanning, but future devs won't have that context.

@@ -1850,6 +1956,11 @@ namespace tools
uint64_t m_ignore_outputs_above;
uint64_t m_ignore_outputs_below;
bool m_track_uses;
bool m_is_background_wallet;
BackgroundSyncType m_background_sync_type;
bool m_background_syncing;
Copy link
Contributor

Choose a reason for hiding this comment

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

If this variable is being accessed across threads, it needs to be an std::atomic<bool>.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think the best way to get thread safe access to m_background_syncing is to let the caller handle thread safety, which I made sure to do via LOCK_REFRESH in the wallet API, using LOCK_IDLE_SCOPE in the CLI, and the RPC should be ok because the server runs in a single thread.

I think if we wanted to add more logic into wallet2 that handles thread safety surrounding m_background_syncing, then every function that reads/writes m_background_syncing would need to grab a lock, which adds a fair bit of complexity I think for little to no gain. I don't think an atomic would solve the problem here, but maybe I'm missing something. Can you expand on an example where an atomic would yield correct behavior where the current code doesn't?

@@ -4700,6 +4896,7 @@ bool wallet2::load_keys_buf(const std::string& keys_buf, const epee::wipeable_st
m_ignore_outputs_above = MONEY_SUPPLY;
m_ignore_outputs_below = 0;
m_track_uses = false;
m_background_sync_type = BackgroundSyncOff;
Copy link
Contributor

Choose a reason for hiding this comment

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

should we also set m_is_background_wallet here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It's set above already

Copy link
Contributor

@jeffro256 jeffro256 left a comment

Choose a reason for hiding this comment

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

Some more minor comments

@@ -6310,6 +6705,21 @@ void wallet2::store_to(const std::string &path, const epee::wipeable_string &pas
}
Copy link
Contributor

Choose a reason for hiding this comment

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

We should throw an error if m_is_background_wallet and !same_file since it semantically makes sense to not move just the background sync file.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good spot -- this made me think to also add slightly clearer THROW_WALLET_EXCEPTION_IF(m_wallet_file.empty() to both store_background_cache and store_background_keys. Those shouldn't be triggerable.

try
{
THROW_WALLET_EXCEPTION_IF(!m_custom_background_key, error::wallet_internal_error, "Custom background key not set");
store_background_cache(m_custom_background_key.get(), false/*do_reset_background_sync_data*/);
Copy link
Contributor

Choose a reason for hiding this comment

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

Is only overwriting the background cache the behavior we want when storing the main wallet file? I would think that we would want to store the main wallet file as-is and not overwrite the background cache so we don't lose progress when calling store_to().

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Sensitive user data from the cache + the m_cache_key have been wiped by this point (grep m_cache_key.scrub()). For all intents and purposes, it's as if the opened wallet is the background wallet at this point. As long as the main wallet processes the background cache on reopen, which it will attempt to do automatically via process_background_cache_on_open, no progress gets lost.

If you look at the test in transfer.py with the comment # Close wallet while background syncing, then reopen, the custom password test is testing this case to make sure no progress is lost.

return WALLET_DIRECTORY + '/' + name

def remove_wallet_files(name):
for suffix in ['', '.keys', '.background', '.background.keys']:
Copy link
Contributor

Choose a reason for hiding this comment

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

Should .address.txt be included here?

Comment on lines 1098 to 1099
REUSE_PASSWORD = 'reuse-wallet-password'
CUSTOM_PASSWORD = 'custom-background-password'
Copy link
Contributor

Choose a reason for hiding this comment

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

These constants (as well as off) could be really useful to add to the Wallet class

@jeffro256
Copy link
Contributor

jeffro256 commented Jan 12, 2024

After those last comments, I'm ready to approve. Awesome work @j-berman

@jeffro256
Copy link
Contributor

I think this PR needs to be rebased against master and refactored to remove references to the serializable_* container classes, which is the reason why compilation is failing. Relevant PR: #9077. I know that the new class background_sync_data_t contains a field named txs which is of type serializable_unordered_map. To make the correct change, simply change its type to std::unordered_map. So on and so forth.

@j-berman
Copy link
Collaborator Author

Squashed

@J0J0XMR
Copy link

J0J0XMR commented Apr 11, 2024

Another +1 in favor of merging.

I recently added this to Monerujo and had no issues, API is simple and everything works as it should.

Comment on lines 2889 to 2894
tools::wallet2::BackgroundSyncType background_sync_type;
if (!parse_background_sync_type(args[1], background_sync_type))
{
fail_msg_writer() << tr("invalid option");
return true;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be put before get_and_verify_password() since it would be frustrating to type your password in and THEN get a parse error.

}

const auto pwd_container = get_and_verify_password();
if (pwd_container)
Copy link
Contributor

Choose a reason for hiding this comment

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

For code clarity, this if branch could be negated and styled as a guard i.e:

if (!pwd_container)
    return true;

<UNINDENT EVERYTHING ELSE INSIDE>

Copy link
Contributor

@jeffro256 jeffro256 left a comment

Choose a reason for hiding this comment

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

LGTM

- When background syncing, the wallet wipes the spend key
from memory and processes all new transactions. The wallet saves
all receives, spends, and "plausible" spends of receives the
wallet does not know key images for.
- When background sync disabled, the wallet processes all
background synced txs and then clears the background sync cache.
- Adding "plausible" spends to the background sync cache ensures
that the wallet does not need to query the daemon to see if any
received outputs were spent while background sync was enabled.
This would harm privacy especially for users of 3rd party daemons.
- To enable the feature in the CLI wallet, the user can set
background-sync to reuse-wallet-password or
custom-background-password and the wallet automatically syncs in
the background when the wallet locks, then processes all
background synced txs when the wallet is unlocked.
- The custom-background-password option enables the user to
open a distinct background wallet that only has a view key saved
and can be opened/closed/synced separately from the main wallet.
When the main wallet opens, it processes the background wallet's
cache.
- To enable the feature in the RPC wallet, there is a new
`/setup_background_sync` endpoint.
- HW, multsig and view-only wallets cannot background sync.
@j-berman
Copy link
Collaborator Author

Squashed and rebased

Copy link
Contributor

@rbrunner7 rbrunner7 left a comment

Choose a reason for hiding this comment

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

About half a year since my last review I looked over the code again, and played around with the new commands in the CLI wallet again. Everything ok.

It's a bit difficult to keep the overview what happened in the code over that half year, with all the rebasing and changes from other PRs coming in, but at least for the most important source file wallet2.cpp I confirmed that nothing "bad" happened in the code that is relevant to this PR in the meantime that I may have overlooked in my pass over the latest version of the code.

I think the only sane way to continue with this is merging it into master now so that people start to actually use it, and find problems if there should be any still, which we can correct with new PRs.

@luigi1111 luigi1111 merged commit d7eece3 into monero-project:master Jul 16, 2024
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.