aboutsummaryrefslogtreecommitdiff
path: root/src/wallet/wallet2.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/wallet/wallet2.cpp')
-rw-r--r--src/wallet/wallet2.cpp1026
1 files changed, 945 insertions, 81 deletions
diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp
index 951e8cb6a..f5d4583c3 100644
--- a/src/wallet/wallet2.cpp
+++ b/src/wallet/wallet2.cpp
@@ -156,6 +156,8 @@ static const std::string MULTISIG_SIGNATURE_MAGIC = "SigMultisigPkV1";
static const std::string ASCII_OUTPUT_MAGIC = "MoneroAsciiDataV1";
+static const std::string BACKGROUND_WALLET_SUFFIX = ".background";
+
boost::mutex tools::wallet2::default_daemon_address_lock;
std::string tools::wallet2::default_daemon_address = "";
@@ -1008,14 +1010,14 @@ uint64_t num_priv_multisig_keys_post_setup(uint64_t threshold, uint64_t total)
* @param keys_data_key the chacha key that encrypts wallet keys files
* @return crypto::chacha_key the chacha key that encrypts the wallet cache files
*/
-crypto::chacha_key derive_cache_key(const crypto::chacha_key& keys_data_key)
+crypto::chacha_key derive_cache_key(const crypto::chacha_key& keys_data_key, const unsigned char domain_separator)
{
static_assert(HASH_SIZE == sizeof(crypto::chacha_key), "Mismatched sizes of hash and chacha key");
crypto::chacha_key cache_key;
epee::mlocked<tools::scrubbed_arr<char, HASH_SIZE+1>> cache_key_data;
memcpy(cache_key_data.data(), &keys_data_key, HASH_SIZE);
- cache_key_data[HASH_SIZE] = config::HASH_KEY_WALLET_CACHE;
+ cache_key_data[HASH_SIZE] = domain_separator;
cn_fast_hash(cache_key_data.data(), HASH_SIZE+1, (crypto::hash&) cache_key);
return cache_key;
@@ -1103,7 +1105,7 @@ wallet_keys_unlocker::wallet_keys_unlocker(wallet2 &w, const boost::optional<too
boost::lock_guard<boost::mutex> lock(lockers_lock);
if (lockers++ > 0)
locked = false;
- if (!locked || w.is_unattended() || w.ask_password() != tools::wallet2::AskPasswordToDecrypt || w.watch_only())
+ if (!locked || w.is_unattended() || w.ask_password() != tools::wallet2::AskPasswordToDecrypt || w.watch_only() || w.is_background_syncing())
{
locked = false;
return;
@@ -1219,6 +1221,11 @@ wallet2::wallet2(network_type nettype, uint64_t kdf_rounds, bool unattended, std
m_ignore_outputs_above(MONEY_SUPPLY),
m_ignore_outputs_below(0),
m_track_uses(false),
+ m_is_background_wallet(false),
+ m_background_sync_type(BackgroundSyncOff),
+ m_background_syncing(false),
+ m_processing_background_cache(false),
+ m_custom_background_key(boost::none),
m_show_wallet_name_when_locked(false),
m_inactivity_lock_timeout(DEFAULT_INACTIVITY_LOCK_TIMEOUT),
m_setup_background_mining(BackgroundMiningMaybe),
@@ -1854,6 +1861,9 @@ bool has_nonrequested_tx_at_height_or_above_requested(uint64_t height, const std
//----------------------------------------------------------------------------------------------------
void wallet2::scan_tx(const std::unordered_set<crypto::hash> &txids)
{
+ THROW_WALLET_EXCEPTION_IF(m_background_syncing || m_is_background_wallet, error::wallet_internal_error,
+ "cannot scan tx from background wallet");
+
// Get the transactions from daemon in batches sorted lowest height to highest
tx_entry_data txs_to_scan = get_tx_entries(txids);
if (txs_to_scan.tx_entries.empty())
@@ -2161,11 +2171,11 @@ void wallet2::scan_output(const cryptonote::transaction &tx, bool miner_tx, cons
THROW_WALLET_EXCEPTION_IF(i >= tx.vout.size(), error::wallet_internal_error, "Invalid vout index");
// if keys are encrypted, ask for password
- if (m_ask_password == AskPasswordToDecrypt && !m_unattended && !m_watch_only && !m_multisig_rescan_k)
+ if (m_ask_password == AskPasswordToDecrypt && !m_unattended && !m_watch_only && !m_multisig_rescan_k && !m_background_syncing)
{
static critical_section password_lock;
CRITICAL_REGION_LOCAL(password_lock);
- if (!m_encrypt_keys_after_refresh)
+ if (!m_encrypt_keys_after_refresh && !m_processing_background_cache)
{
boost::optional<epee::wipeable_string> pwd = m_callback->on_get_password(pool ? "output found in pool" : "output received");
THROW_WALLET_EXCEPTION_IF(!pwd, error::password_needed, tr("Password is needed to compute key image for incoming monero"));
@@ -2177,7 +2187,7 @@ void wallet2::scan_output(const cryptonote::transaction &tx, bool miner_tx, cons
crypto::public_key output_public_key;
THROW_WALLET_EXCEPTION_IF(!get_output_public_key(tx.vout[i], output_public_key), error::wallet_internal_error, "Failed to get output public key");
- if (m_multisig)
+ if (m_multisig || m_background_syncing/*no spend key*/)
{
tx_scan_info.in_ephemeral.pub = output_public_key;
tx_scan_info.in_ephemeral.sec = crypto::null_skey;
@@ -2434,6 +2444,22 @@ void wallet2::process_new_transaction(const crypto::hash &txid, const cryptonote
THROW_WALLET_EXCEPTION_IF(tx.vout.size() != o_indices.size(), error::wallet_internal_error,
"transactions outputs size=" + std::to_string(tx.vout.size()) +
" not match with daemon response size=" + std::to_string(o_indices.size()));
+
+ // we're going to re-process this receive when background sync is disabled
+ if (m_background_syncing && m_background_sync_data.txs.find(txid) == m_background_sync_data.txs.end())
+ {
+ size_t bgs_idx = m_background_sync_data.txs.size();
+ background_synced_tx_t bgs_tx = {
+ .index_in_background_sync_data = bgs_idx,
+ .tx = tx,
+ .output_indices = o_indices,
+ .height = height,
+ .block_timestamp = ts,
+ .double_spend_seen = double_spend_seen
+ };
+ LOG_PRINT_L2("Adding received tx " << txid << " to background sync data (idx=" << bgs_idx << ")");
+ m_background_sync_data.txs.insert({txid, std::move(bgs_tx)});
+ }
}
for(size_t o: outs)
@@ -2459,7 +2485,7 @@ void wallet2::process_new_transaction(const crypto::hash &txid, const cryptonote
td.m_tx = (const cryptonote::transaction_prefix&)tx;
td.m_txid = txid;
td.m_key_image = tx_scan_info[o].ki;
- td.m_key_image_known = !m_watch_only && !m_multisig;
+ td.m_key_image_known = !m_watch_only && !m_multisig && !m_background_syncing;
if (!td.m_key_image_known)
{
// we might have cold signed, and have a mapping to key images
@@ -2649,10 +2675,25 @@ void wallet2::process_new_transaction(const crypto::hash &txid, const cryptonote
set_spent(it->second, height);
if (!ignore_callbacks && 0 != m_callback)
m_callback->on_money_spent(height, txid, tx, amount, tx, td.m_subaddr_index);
+
+ if (m_background_syncing && m_background_sync_data.txs.find(txid) == m_background_sync_data.txs.end())
+ {
+ size_t bgs_idx = m_background_sync_data.txs.size();
+ background_synced_tx_t bgs_tx = {
+ .index_in_background_sync_data = bgs_idx,
+ .tx = tx,
+ .output_indices = o_indices,
+ .height = height,
+ .block_timestamp = ts,
+ .double_spend_seen = double_spend_seen
+ };
+ LOG_PRINT_L2("Adding spent tx " << txid << " to background sync data (idx=" << bgs_idx << ")");
+ m_background_sync_data.txs.insert({txid, std::move(bgs_tx)});
+ }
}
}
- if (!pool && m_track_uses)
+ if (!pool && (m_track_uses || (m_background_syncing && it == m_key_images.end())))
{
PERF_TIMER(track_uses);
const uint64_t amount = in_to_key.amount;
@@ -2666,7 +2707,27 @@ void wallet2::process_new_transaction(const crypto::hash &txid, const cryptonote
{
size_t idx = i->second;
THROW_WALLET_EXCEPTION_IF(idx >= m_transfers.size(), error::wallet_internal_error, "Output tracker cache index out of range");
- m_transfers[idx].m_uses.push_back(std::make_pair(height, txid));
+
+ if (m_track_uses)
+ m_transfers[idx].m_uses.push_back(std::make_pair(height, txid));
+
+ // We'll re-process all txs which *might* be spends when we disable
+ // background sync and retrieve the spend key. We don't know if an
+ // output is a spend in this tx if we don't know its key image.
+ if (m_background_syncing && !m_transfers[idx].m_key_image_known && m_background_sync_data.txs.find(txid) == m_background_sync_data.txs.end())
+ {
+ size_t bgs_idx = m_background_sync_data.txs.size();
+ background_synced_tx_t bgs_tx = {
+ .index_in_background_sync_data = bgs_idx,
+ .tx = tx,
+ .output_indices = o_indices,
+ .height = height,
+ .block_timestamp = ts,
+ .double_spend_seen = double_spend_seen
+ };
+ LOG_PRINT_L2("Adding plausible spent tx " << txid << " to background sync data (idx=" << bgs_idx << ")");
+ m_background_sync_data.txs.insert({txid, std::move(bgs_tx)});
+ }
}
}
}
@@ -2676,7 +2737,24 @@ void wallet2::process_new_transaction(const crypto::hash &txid, const cryptonote
continue;
for (uint64_t offset: offsets)
if (offset == td.m_global_output_index)
- td.m_uses.push_back(std::make_pair(height, txid));
+ {
+ if (m_track_uses)
+ td.m_uses.push_back(std::make_pair(height, txid));
+ if (m_background_syncing && !td.m_key_image_known && m_background_sync_data.txs.find(txid) == m_background_sync_data.txs.end())
+ {
+ size_t bgs_idx = m_background_sync_data.txs.size();
+ background_synced_tx_t bgs_tx = {
+ .index_in_background_sync_data = bgs_idx,
+ .tx = tx,
+ .output_indices = o_indices,
+ .height = height,
+ .block_timestamp = ts,
+ .double_spend_seen = double_spend_seen
+ };
+ LOG_PRINT_L2("Adding plausible spent tx " << txid << " to background sync data (idx=" << bgs_idx << ")");
+ m_background_sync_data.txs.insert({txid, std::move(bgs_tx)});
+ }
+ }
}
}
}
@@ -3049,8 +3127,8 @@ void wallet2::pull_blocks(bool first, bool try_incremental, uint64_t start_heigh
req.start_height = start_height;
req.no_miner_tx = m_refresh_type == RefreshNoCoinbase;
- req.requested_info = first ? COMMAND_RPC_GET_BLOCKS_FAST::BLOCKS_AND_POOL : COMMAND_RPC_GET_BLOCKS_FAST::BLOCKS_ONLY;
- if (try_incremental)
+ req.requested_info = (first && !m_background_syncing) ? COMMAND_RPC_GET_BLOCKS_FAST::BLOCKS_AND_POOL : COMMAND_RPC_GET_BLOCKS_FAST::BLOCKS_ONLY;
+ if (try_incremental && !m_background_syncing)
req.pool_info_since = m_pool_info_query_time;
{
@@ -3073,7 +3151,7 @@ void wallet2::pull_blocks(bool first, bool try_incremental, uint64_t start_heigh
<< ", height " << blocks_start_height + blocks.size() << ", node height " << res.current_height
<< ", pool info " << static_cast<unsigned int>(res.pool_info_extent));
- if (first)
+ if (first && !m_background_syncing)
{
if (res.pool_info_extent != COMMAND_RPC_GET_BLOCKS_FAST::NONE)
{
@@ -3587,6 +3665,9 @@ void wallet2::process_unconfirmed_transfer(bool incremental, const crypto::hash
// incremental update anymore, because with that we might miss some txs altogether.
void wallet2::update_pool_state(std::vector<std::tuple<cryptonote::transaction, crypto::hash, bool>> &process_txs, bool refreshed, bool try_incremental)
{
+ process_txs.clear();
+ if (m_background_syncing)
+ return;
bool updated = false;
if (m_pool_info_query_time != 0 && try_incremental)
{
@@ -4121,6 +4202,8 @@ void wallet2::refresh(bool trusted_daemon, uint64_t start_height, uint64_t & blo
}
m_first_refresh_done = true;
+ if (m_background_syncing || m_is_background_wallet)
+ m_background_sync_data.first_refresh_done = true;
LOG_PRINT_L1("Refresh done, blocks received: " << blocks_fetched << ", balance (all accounts): " << print_money(balance_all(false)) << ", unlocked: " << print_money(unlocked_balance_all(false)));
}
@@ -4203,6 +4286,14 @@ wallet2::detached_blockchain_data wallet2::detach_blockchain(uint64_t height, st
td.m_uses.pop_back();
}
+ for (auto it = m_background_sync_data.txs.begin(); it != m_background_sync_data.txs.end(); )
+ {
+ if(height <= it->second.height)
+ it = m_background_sync_data.txs.erase(it);
+ else
+ ++it;
+ }
+
if (output_tracker_cache)
output_tracker_cache->clear();
@@ -4277,8 +4368,12 @@ void wallet2::handle_reorg(uint64_t height, std::map<std::pair<uint64_t, uint64_
// C
THROW_WALLET_EXCEPTION_IF(height < m_blockchain.offset() && m_blockchain.size() > m_blockchain.offset(),
error::wallet_internal_error, "Daemon claims reorg below last checkpoint");
+
detached_blockchain_data dbd = detach_blockchain(height, output_tracker_cache);
+ if (m_background_syncing && height < m_background_sync_data.start_height)
+ m_background_sync_data.start_height = height;
+
if (m_callback)
m_callback->on_reorg(height, dbd.detached_blockchain.size(), dbd.detached_tx_hashes.size());
}
@@ -4288,6 +4383,7 @@ bool wallet2::deinit()
if(m_is_initialized) {
m_is_initialized = false;
unlock_keys_file();
+ unlock_background_keys_file();
m_account.deinit();
}
return true;
@@ -4314,6 +4410,7 @@ bool wallet2::clear()
m_device_last_key_image_sync = 0;
m_pool_info_query_time = 0;
m_skip_to_height = 0;
+ m_background_sync_data = background_sync_data_t{};
return true;
}
//----------------------------------------------------------------------------------------------------
@@ -4332,13 +4429,30 @@ void wallet2::clear_soft(bool keep_key_images)
m_scanned_pool_txs[1].clear();
m_pool_info_query_time = 0;
m_skip_to_height = 0;
+ m_background_sync_data = background_sync_data_t{};
cryptonote::block b;
generate_genesis(b);
m_blockchain.push_back(get_block_hash(b));
m_last_block_reward = cryptonote::get_outs_money_amount(b.miner_tx);
}
-
+//----------------------------------------------------------------------------------------------------
+void wallet2::clear_user_data()
+{
+ for (auto i = m_confirmed_txs.begin(); i != m_confirmed_txs.end(); ++i)
+ i->second.m_dests.clear();
+ for (auto i = m_unconfirmed_txs.begin(); i != m_unconfirmed_txs.end(); ++i)
+ i->second.m_dests.clear();
+ for (auto i = m_transfers.begin(); i != m_transfers.end(); ++i)
+ i->m_frozen = false;
+ m_tx_keys.clear();
+ m_tx_notes.clear();
+ m_address_book.clear();
+ m_subaddress_labels.clear();
+ m_attributes.clear();
+ m_account_tags = std::pair<std::map<std::string, std::string>, std::vector<std::string>>();
+}
+//----------------------------------------------------------------------------------------------------
/*!
* \brief Stores wallet information to wallet file.
* \param keys_file_name Name of wallet file
@@ -4350,16 +4464,35 @@ bool wallet2::store_keys(const std::string& keys_file_name, const epee::wipeable
{
boost::optional<wallet2::keys_file_data> keys_file_data = get_keys_file_data(password, watch_only);
CHECK_AND_ASSERT_MES(keys_file_data != boost::none, false, "failed to generate wallet keys data");
-
+ return store_keys_file_data(keys_file_name, keys_file_data.get());
+}
+//----------------------------------------------------------------------------------------------------
+bool wallet2::store_keys(const std::string& keys_file_name, const crypto::chacha_key& key, bool watch_only, bool background_keys_file)
+{
+ boost::optional<wallet2::keys_file_data> keys_file_data = get_keys_file_data(key, watch_only, background_keys_file);
+ CHECK_AND_ASSERT_MES(keys_file_data != boost::none, false, "failed to generate wallet keys data");
+ return store_keys_file_data(keys_file_name, keys_file_data.get(), background_keys_file);
+}
+//----------------------------------------------------------------------------------------------------
+bool wallet2::store_keys_file_data(const std::string& keys_file_name, wallet2::keys_file_data &keys_file_data, bool background_keys_file)
+{
std::string tmp_file_name = keys_file_name + ".new";
std::string buf;
- bool r = ::serialization::dump_binary(keys_file_data.get(), buf);
+ bool r = ::serialization::dump_binary(keys_file_data, buf);
r = r && save_to_file(tmp_file_name, buf);
CHECK_AND_ASSERT_MES(r, false, "failed to generate wallet keys file " << tmp_file_name);
- unlock_keys_file();
+ if (!background_keys_file)
+ unlock_keys_file();
+ else
+ unlock_background_keys_file();
+
std::error_code e = tools::replace_file(tmp_file_name, keys_file_name);
- lock_keys_file();
+
+ if (!background_keys_file)
+ lock_keys_file();
+ else
+ lock_background_keys_file(keys_file_name);
if (e) {
boost::filesystem::remove(tmp_file_name);
@@ -4372,25 +4505,26 @@ bool wallet2::store_keys(const std::string& keys_file_name, const epee::wipeable
//----------------------------------------------------------------------------------------------------
boost::optional<wallet2::keys_file_data> wallet2::get_keys_file_data(const epee::wipeable_string& password, bool watch_only)
{
+ crypto::chacha_key key;
+ crypto::generate_chacha_key(password.data(), password.size(), key, m_kdf_rounds);
+ verify_password_with_cached_key(key);
+ return get_keys_file_data(key, watch_only);
+}
+//----------------------------------------------------------------------------------------------------
+boost::optional<wallet2::keys_file_data> wallet2::get_keys_file_data(const crypto::chacha_key& key, bool watch_only, bool background_keys_file)
+{
epee::byte_slice account_data;
std::string multisig_signers;
std::string multisig_derivations;
cryptonote::account_base account = m_account;
- crypto::chacha_key key;
- crypto::generate_chacha_key(password.data(), password.size(), key, m_kdf_rounds);
-
- // We use m_cache_key as a deterministic test to see if given key corresponds to original password
- const crypto::chacha_key cache_key = derive_cache_key(key);
- THROW_WALLET_EXCEPTION_IF(cache_key != m_cache_key, error::invalid_password);
-
if (m_ask_password == AskPasswordToDecrypt && !m_unattended && !m_watch_only)
{
account.encrypt_viewkey(key);
account.decrypt_keys(key);
}
- if (watch_only)
+ if (watch_only || background_keys_file)
account.forget_spend_key();
account.encrypt_keys(key);
@@ -4525,6 +4659,9 @@ boost::optional<wallet2::keys_file_data> wallet2::get_keys_file_data(const epee:
value2.SetInt(m_track_uses ? 1 : 0);
json.AddMember("track_uses", value2, json.GetAllocator());
+ value2.SetInt(m_background_sync_type);
+ json.AddMember("background_sync_type", value2, json.GetAllocator());
+
value2.SetInt(m_show_wallet_name_when_locked ? 1 : 0);
json.AddMember("show_wallet_name_when_locked", value2, json.GetAllocator());
@@ -4585,6 +4722,12 @@ boost::optional<wallet2::keys_file_data> wallet2::get_keys_file_data(const epee:
value2.SetInt(m_enable_multisig ? 1 : 0);
json.AddMember("enable_multisig", value2, json.GetAllocator());
+ if (m_background_sync_type == BackgroundSyncCustomPassword && !background_keys_file && m_custom_background_key)
+ {
+ value.SetString(reinterpret_cast<const char*>(m_custom_background_key.get().data()), m_custom_background_key.get().size());
+ json.AddMember("custom_background_key", value, json.GetAllocator());
+ }
+
// Serialize the JSON object
rapidjson::StringBuffer buffer;
rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
@@ -4611,13 +4754,81 @@ void wallet2::setup_keys(const epee::wipeable_string &password)
m_account.decrypt_viewkey(key);
}
- m_cache_key = derive_cache_key(key);
+ m_cache_key = derive_cache_key(key, config::HASH_KEY_WALLET_CACHE);
get_ringdb_key();
}
//----------------------------------------------------------------------------------------------------
+void validate_background_cache_password_usage(const tools::wallet2::BackgroundSyncType background_sync_type, const boost::optional<epee::wipeable_string> &background_cache_password, const bool multisig, const bool watch_only, const bool key_on_device)
+{
+ THROW_WALLET_EXCEPTION_IF(multisig || watch_only || key_on_device, error::wallet_internal_error, multisig
+ ? "Background sync not implemented for multisig wallets" : watch_only
+ ? "Background sync not implemented for view only wallets"
+ : "Background sync not implemented for HW wallets");
+
+ switch (background_sync_type)
+ {
+ case tools::wallet2::BackgroundSyncOff:
+ {
+ THROW_WALLET_EXCEPTION(error::wallet_internal_error, "background sync is not enabled");
+ break;
+ }
+ case tools::wallet2::BackgroundSyncReusePassword:
+ {
+ THROW_WALLET_EXCEPTION_IF(background_cache_password, error::wallet_internal_error,
+ "unexpected custom background cache password");
+ break;
+ }
+ case tools::wallet2::BackgroundSyncCustomPassword:
+ {
+ THROW_WALLET_EXCEPTION_IF(!background_cache_password, error::wallet_internal_error,
+ "expected custom background cache password");
+ break;
+ }
+ default: THROW_WALLET_EXCEPTION(error::wallet_internal_error, "unknown background sync type");
+ }
+}
+//----------------------------------------------------------------------------------------------------
+void get_custom_background_key(const epee::wipeable_string &password, crypto::chacha_key &custom_background_key, const uint64_t kdf_rounds)
+{
+ crypto::chacha_key key;
+ crypto::generate_chacha_key(password.data(), password.size(), key, kdf_rounds);
+ custom_background_key = derive_cache_key(key, config::HASH_KEY_BACKGROUND_KEYS_FILE);
+}
+//----------------------------------------------------------------------------------------------------
+const crypto::chacha_key wallet2::get_cache_key()
+{
+ if (m_background_sync_type == BackgroundSyncCustomPassword && m_background_syncing)
+ {
+ THROW_WALLET_EXCEPTION_IF(!m_custom_background_key, error::wallet_internal_error, "Custom background key not set");
+ // Domain separate keys used to encrypt background keys file and cache
+ return derive_cache_key(m_custom_background_key.get(), config::HASH_KEY_BACKGROUND_CACHE);
+ }
+ else
+ {
+ return m_cache_key;
+ }
+}
+//----------------------------------------------------------------------------------------------------
+void wallet2::verify_password_with_cached_key(const epee::wipeable_string &password)
+{
+ crypto::chacha_key key;
+ crypto::generate_chacha_key(password.data(), password.size(), key, m_kdf_rounds);
+ verify_password_with_cached_key(key);
+}
+//----------------------------------------------------------------------------------------------------
+void wallet2::verify_password_with_cached_key(const crypto::chacha_key &key)
+{
+ // We use m_cache_key as a deterministic test to see if given key corresponds to original password
+ const crypto::chacha_key cache_key = derive_cache_key(key, config::HASH_KEY_WALLET_CACHE);
+ THROW_WALLET_EXCEPTION_IF(cache_key != m_cache_key, error::invalid_password);
+}
+//----------------------------------------------------------------------------------------------------
void wallet2::change_password(const std::string &filename, const epee::wipeable_string &original_password, const epee::wipeable_string &new_password)
{
+ THROW_WALLET_EXCEPTION_IF(m_background_syncing || m_is_background_wallet, error::wallet_internal_error,
+ "cannot change password from background wallet");
+
if (m_ask_password == AskPasswordToDecrypt && !m_unattended && !m_watch_only)
decrypt_keys(original_password);
setup_keys(new_password);
@@ -4676,8 +4887,24 @@ bool wallet2::load_keys_buf(const std::string& keys_buf, const epee::wipeable_st
std::string account_data;
account_data.resize(keys_file_data.account_data.size());
crypto::chacha20(keys_file_data.account_data.data(), keys_file_data.account_data.size(), key, keys_file_data.iv, &account_data[0]);
- if (json.Parse(account_data.c_str()).HasParseError() || !json.IsObject())
+ const bool try_v0_format = json.Parse(account_data.c_str()).HasParseError() || !json.IsObject();
+ if (try_v0_format)
crypto::chacha8(keys_file_data.account_data.data(), keys_file_data.account_data.size(), key, keys_file_data.iv, &account_data[0]);
+
+ // Check if it's a background keys file if both of the above formats fail
+ {
+ m_is_background_wallet = false;
+ m_background_syncing = false;
+ cryptonote::account_base account_data_check;
+ if (try_v0_format && !epee::serialization::load_t_from_binary(account_data_check, account_data))
+ {
+ get_custom_background_key(password, key, m_kdf_rounds);
+ crypto::chacha20(keys_file_data.account_data.data(), keys_file_data.account_data.size(), key, keys_file_data.iv, &account_data[0]);
+ m_is_background_wallet = !json.Parse(account_data.c_str()).HasParseError() && json.IsObject();
+ m_background_syncing = m_is_background_wallet; // start a background wallet background syncing
+ }
+ }
+
// The contents should be JSON if the wallet follows the new format.
if (json.Parse(account_data.c_str()).HasParseError())
{
@@ -4714,6 +4941,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;
m_show_wallet_name_when_locked = false;
m_inactivity_lock_timeout = DEFAULT_INACTIVITY_LOCK_TIMEOUT;
m_setup_background_mining = BackgroundMiningMaybe;
@@ -4728,6 +4956,7 @@ bool wallet2::load_keys_buf(const std::string& keys_buf, const epee::wipeable_st
encrypted_secret_keys = false;
m_enable_multisig = false;
m_allow_mismatched_daemon_version = false;
+ m_custom_background_key = boost::none;
}
else if(json.IsObject())
{
@@ -4955,6 +5184,39 @@ bool wallet2::load_keys_buf(const std::string& keys_buf, const epee::wipeable_st
GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, enable_multisig, int, Int, false, false);
m_enable_multisig = field_enable_multisig;
+
+ GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, background_sync_type, BackgroundSyncType, Int, false, BackgroundSyncOff);
+ m_background_sync_type = field_background_sync_type;
+
+ // Load encryption key used to encrypt background cache
+ crypto::chacha_key custom_background_key;
+ m_custom_background_key = boost::none;
+ if (m_background_sync_type == BackgroundSyncCustomPassword && !m_is_background_wallet)
+ {
+ if (!json.HasMember("custom_background_key"))
+ {
+ LOG_ERROR("Field custom_background_key not found in JSON");
+ return false;
+ }
+ else if (!json["custom_background_key"].IsString())
+ {
+ LOG_ERROR("Field custom_background_key found in JSON, but not String");
+ return false;
+ }
+ else if (json["custom_background_key"].GetStringLength() != sizeof(crypto::chacha_key))
+ {
+ LOG_ERROR("Field custom_background_key found in JSON, but not correct length");
+ return false;
+ }
+ const char *field_custom_background_key = json["custom_background_key"].GetString();
+ memcpy(custom_background_key.data(), field_custom_background_key, sizeof(crypto::chacha_key));
+ m_custom_background_key = boost::optional<crypto::chacha_key>(custom_background_key);
+ LOG_PRINT_L1("Loaded custom background key derived from custom password");
+ }
+ else if (json.HasMember("custom_background_key"))
+ {
+ LOG_ERROR("Unexpected field custom_background_key found in JSON");
+ }
}
else
{
@@ -5018,12 +5280,17 @@ bool wallet2::load_keys_buf(const std::string& keys_buf, const epee::wipeable_st
const cryptonote::account_keys& keys = m_account.get_keys();
hw::device &hwdev = m_account.get_device();
r = r && hwdev.verify_keys(keys.m_view_secret_key, keys.m_account_address.m_view_public_key);
- if (!m_watch_only && !m_multisig && hwdev.device_protocol() != hw::device::PROTOCOL_COLD)
+ if (!m_watch_only && !m_multisig && hwdev.device_protocol() != hw::device::PROTOCOL_COLD && !m_is_background_wallet)
r = r && hwdev.verify_keys(keys.m_spend_secret_key, keys.m_account_address.m_spend_public_key);
THROW_WALLET_EXCEPTION_IF(!r, error::wallet_files_doesnt_correspond, m_keys_file, m_wallet_file);
if (r)
- setup_keys(password);
+ {
+ if (!m_is_background_wallet)
+ setup_keys(password);
+ else
+ m_custom_background_key = boost::optional<crypto::chacha_key>(key);
+ }
return true;
}
@@ -5038,11 +5305,12 @@ bool wallet2::load_keys_buf(const std::string& keys_buf, const epee::wipeable_st
* can be used prior to rewriting wallet keys file, to ensure user has entered the correct password
*
*/
-bool wallet2::verify_password(const epee::wipeable_string& password)
+bool wallet2::verify_password(const epee::wipeable_string& password, crypto::secret_key &spend_key_out)
{
// this temporary unlocking is necessary for Windows (otherwise the file couldn't be loaded).
unlock_keys_file();
- bool r = verify_password(m_keys_file, password, m_account.get_device().device_protocol() == hw::device::PROTOCOL_COLD || m_watch_only || m_multisig, m_account.get_device(), m_kdf_rounds);
+ const bool no_spend_key = m_account.get_device().device_protocol() == hw::device::PROTOCOL_COLD || m_watch_only || m_multisig || m_is_background_wallet;
+ bool r = verify_password(m_keys_file, password, no_spend_key, m_account.get_device(), m_kdf_rounds, spend_key_out);
lock_keys_file();
return r;
}
@@ -5060,7 +5328,7 @@ bool wallet2::verify_password(const epee::wipeable_string& password)
* can be used prior to rewriting wallet keys file, to ensure user has entered the correct password
*
*/
-bool wallet2::verify_password(const std::string& keys_file_name, const epee::wipeable_string& password, bool no_spend_key, hw::device &hwdev, uint64_t kdf_rounds)
+bool wallet2::verify_password(const std::string& keys_file_name, const epee::wipeable_string& password, bool no_spend_key, hw::device &hwdev, uint64_t kdf_rounds, crypto::secret_key &spend_key_out)
{
rapidjson::Document json;
wallet2::keys_file_data keys_file_data;
@@ -5077,9 +5345,22 @@ bool wallet2::verify_password(const std::string& keys_file_name, const epee::wip
std::string account_data;
account_data.resize(keys_file_data.account_data.size());
crypto::chacha20(keys_file_data.account_data.data(), keys_file_data.account_data.size(), key, keys_file_data.iv, &account_data[0]);
- if (json.Parse(account_data.c_str()).HasParseError() || !json.IsObject())
+ const bool try_v0_format = json.Parse(account_data.c_str()).HasParseError() || !json.IsObject();
+ if (try_v0_format)
crypto::chacha8(keys_file_data.account_data.data(), keys_file_data.account_data.size(), key, keys_file_data.iv, &account_data[0]);
+ // Check if it's a background keys file if both of the above formats fail
+ {
+ cryptonote::account_base account_data_check;
+ if (try_v0_format && !epee::serialization::load_t_from_binary(account_data_check, account_data))
+ {
+ get_custom_background_key(password, key, kdf_rounds);
+ crypto::chacha20(keys_file_data.account_data.data(), keys_file_data.account_data.size(), key, keys_file_data.iv, &account_data[0]);
+ const bool is_background_wallet = json.Parse(account_data.c_str()).HasParseError() && json.IsObject();
+ no_spend_key = no_spend_key || is_background_wallet;
+ }
+ }
+
// The contents should be JSON if the wallet follows the new format.
if (json.Parse(account_data.c_str()).HasParseError())
{
@@ -5104,6 +5385,7 @@ bool wallet2::verify_password(const std::string& keys_file_name, const epee::wip
r = r && hwdev.verify_keys(keys.m_view_secret_key, keys.m_account_address.m_view_public_key);
if(!no_spend_key)
r = r && hwdev.verify_keys(keys.m_spend_secret_key, keys.m_account_address.m_spend_public_key);
+ spend_key_out = (!no_spend_key && r) ? keys.m_spend_secret_key : crypto::null_skey;
return r;
}
@@ -5115,9 +5397,7 @@ void wallet2::encrypt_keys(const crypto::chacha_key &key)
void wallet2::decrypt_keys(const crypto::chacha_key &key)
{
- // We use m_cache_key as a deterministic test to see if given key corresponds to original password
- const crypto::chacha_key cache_key = derive_cache_key(key);
- THROW_WALLET_EXCEPTION_IF(cache_key != m_cache_key, error::invalid_password);
+ verify_password_with_cached_key(key);
m_account.encrypt_viewkey(key);
m_account.decrypt_keys(key);
@@ -5868,11 +6148,30 @@ void wallet2::rewrite(const std::string& wallet_name, const epee::wipeable_strin
{
if (wallet_name.empty())
return;
+ THROW_WALLET_EXCEPTION_IF(m_background_syncing || m_is_background_wallet, error::wallet_internal_error,
+ "cannot change wallet settings from background wallet");
prepare_file_names(wallet_name);
boost::system::error_code ignored_ec;
THROW_WALLET_EXCEPTION_IF(!boost::filesystem::exists(m_keys_file, ignored_ec), error::file_not_found, m_keys_file);
bool r = store_keys(m_keys_file, password, m_watch_only);
THROW_WALLET_EXCEPTION_IF(!r, error::file_save_error, m_keys_file);
+
+ // Update the background keys file when we rewrite the main wallet keys file
+ if (m_background_sync_type == BackgroundSyncCustomPassword && m_custom_background_key)
+ {
+ const std::string background_keys_filename = make_background_keys_file_name(wallet_name);
+ if (!lock_background_keys_file(background_keys_filename))
+ {
+ LOG_ERROR("Background keys file " << background_keys_filename << " is opened by another wallet program and cannot be rewritten");
+ return; // not fatal, background keys file will just have different wallet settings
+ }
+ store_background_keys(m_custom_background_key.get());
+ store_background_cache(m_custom_background_key.get(), true/*do_reset_background_sync_data*/);
+ }
+ else if (m_background_sync_type == BackgroundSyncReusePassword)
+ {
+ reset_background_sync_data(m_background_sync_data);
+ }
}
/*!
* \brief Writes to a file named based on the normal wallet (doesn't generate key, assumes it's already there)
@@ -5906,6 +6205,16 @@ bool wallet2::wallet_valid_path_format(const std::string& file_path)
return !file_path.empty();
}
//----------------------------------------------------------------------------------------------------
+std::string wallet2::make_background_wallet_file_name(const std::string &wallet_file)
+{
+ return wallet_file + BACKGROUND_WALLET_SUFFIX;
+}
+//----------------------------------------------------------------------------------------------------
+std::string wallet2::make_background_keys_file_name(const std::string &wallet_file)
+{
+ return make_background_wallet_file_name(wallet_file) + ".keys";
+}
+//----------------------------------------------------------------------------------------------------
bool wallet2::parse_long_payment_id(const std::string& payment_id_str, crypto::hash& payment_id)
{
cryptonote::blobdata payment_id_data;
@@ -6131,10 +6440,78 @@ void wallet2::load(const std::string& wallet_, const epee::wipeable_string& pass
THROW_WALLET_EXCEPTION_IF(true, error::file_read_error, "failed to load keys from buffer");
}
- wallet_keys_unlocker unlocker(*this, m_ask_password == AskPasswordToDecrypt && !m_unattended && !m_watch_only, password);
+ wallet_keys_unlocker unlocker(*this, m_ask_password == AskPasswordToDecrypt && !m_unattended && !m_watch_only && !m_is_background_wallet, password);
//keys loaded ok!
//try to load wallet cache. but even if we failed, it is not big problem
+ load_wallet_cache(use_fs, cache_buf);
+
+ // Wallets used to wipe, but not erase, old unused multisig key info, which lead to huge memory leaks.
+ // Here we erase these multisig keys if they're zero'd out to free up space.
+ for (auto &td : m_transfers)
+ {
+ auto mk_it = td.m_multisig_k.begin();
+ while (mk_it != td.m_multisig_k.end())
+ {
+ if (*mk_it == rct::zero())
+ mk_it = td.m_multisig_k.erase(mk_it);
+ else
+ ++mk_it;
+ }
+ }
+
+ cryptonote::block genesis;
+ generate_genesis(genesis);
+ crypto::hash genesis_hash = get_block_hash(genesis);
+
+ if (m_blockchain.empty())
+ {
+ m_blockchain.push_back(genesis_hash);
+ m_last_block_reward = cryptonote::get_outs_money_amount(genesis.miner_tx);
+ }
+ else
+ {
+ check_genesis(genesis_hash);
+ }
+
+ trim_hashchain();
+
+ if (get_num_subaddress_accounts() == 0)
+ add_subaddress_account(tr("Primary account"));
+
+ try
+ {
+ find_and_save_rings(false);
+ }
+ catch (const std::exception &e)
+ {
+ MERROR("Failed to save rings, will try again next time");
+ }
+
+ try
+ {
+ if (use_fs)
+ m_message_store.read_from_file(get_multisig_wallet_state(), m_mms_file, m_load_deprecated_formats);
+ }
+ catch (const std::exception &e)
+ {
+ MERROR("Failed to initialize MMS, it will be unusable");
+ }
+
+ try
+ {
+ if (use_fs)
+ process_background_cache_on_open();
+ }
+ catch (const std::exception &e)
+ {
+ MERROR("Failed to process background cache on open: " << e.what());
+ }
+}
+//----------------------------------------------------------------------------------------------------
+void wallet2::load_wallet_cache(const bool use_fs, const std::string& cache_buf)
+{
+ boost::system::error_code e;
bool cache_missing = use_fs ? (!boost::filesystem::exists(m_wallet_file, e) || e) : cache_buf.empty();
if (cache_missing)
{
@@ -6148,7 +6525,7 @@ void wallet2::load(const std::string& wallet_, const epee::wipeable_string& pass
bool r = true;
if (use_fs)
{
- load_from_file(m_wallet_file, cache_file_buf, std::numeric_limits<size_t>::max());
+ r = load_from_file(m_wallet_file, cache_file_buf, std::numeric_limits<size_t>::max());
THROW_WALLET_EXCEPTION_IF(!r, error::file_read_error, m_wallet_file);
}
@@ -6161,7 +6538,7 @@ void wallet2::load(const std::string& wallet_, const epee::wipeable_string& pass
THROW_WALLET_EXCEPTION_IF(!r, error::wallet_internal_error, "internal error: failed to deserialize \"" + m_wallet_file + '\"');
std::string cache_data;
cache_data.resize(cache_file_data.cache_data.size());
- crypto::chacha20(cache_file_data.cache_data.data(), cache_file_data.cache_data.size(), m_cache_key, cache_file_data.iv, &cache_data[0]);
+ crypto::chacha20(cache_file_data.cache_data.data(), cache_file_data.cache_data.size(), get_cache_key(), cache_file_data.iv, &cache_data[0]);
try {
bool loaded = false;
@@ -6251,57 +6628,76 @@ void wallet2::load(const std::string& wallet_, const epee::wipeable_string& pass
m_account_public_address.m_view_public_key != m_account.get_keys().m_account_address.m_view_public_key,
error::wallet_files_doesnt_correspond, m_keys_file, m_wallet_file);
}
+}
+//----------------------------------------------------------------------------------------------------
+void wallet2::process_background_cache_on_open()
+{
+ if (m_wallet_file.empty())
+ return;
+ if (m_background_syncing || m_is_background_wallet)
+ return;
+ if (m_background_sync_type == BackgroundSyncOff)
+ return;
- // Wallets used to wipe, but not erase, old unused multisig key info, which lead to huge memory leaks.
- // Here we erase these multisig keys if they're zero'd out to free up space.
- for (auto &td : m_transfers)
+ if (m_background_sync_type == BackgroundSyncReusePassword)
{
- auto mk_it = td.m_multisig_k.begin();
- while (mk_it != td.m_multisig_k.end())
+ const background_sync_data_t background_sync_data = m_background_sync_data;
+ const hashchain blockchain = m_blockchain;
+ process_background_cache(background_sync_data, blockchain, m_last_block_reward);
+
+ // Reset the background cache after processing
+ reset_background_sync_data(m_background_sync_data);
+ }
+ else if (m_background_sync_type == BackgroundSyncCustomPassword)
+ {
+ // If the background wallet files don't exist, recreate them
+ const std::string background_keys_file = make_background_keys_file_name(m_wallet_file);
+ const std::string background_wallet_file = make_background_wallet_file_name(m_wallet_file);
+ const bool background_keys_file_exists = boost::filesystem::exists(background_keys_file);
+ const bool background_wallet_exists = boost::filesystem::exists(background_wallet_file);
+
+ THROW_WALLET_EXCEPTION_IF(!lock_background_keys_file(background_keys_file), error::background_wallet_already_open, background_wallet_file);
+ THROW_WALLET_EXCEPTION_IF(!m_custom_background_key, error::wallet_internal_error, "Custom background key not set");
+
+ if (!background_keys_file_exists)
{
- if (*mk_it == rct::zero())
- mk_it = td.m_multisig_k.erase(mk_it);
- else
- ++mk_it;
+ MDEBUG("Background keys file not found, restoring");
+ store_background_keys(m_custom_background_key.get());
}
- }
- cryptonote::block genesis;
- generate_genesis(genesis);
- crypto::hash genesis_hash = get_block_hash(genesis);
+ if (!background_wallet_exists)
+ {
+ MDEBUG("Background cache not found, restoring");
+ store_background_cache(m_custom_background_key.get(), true/*do_reset_background_sync_data*/);
+ return;
+ }
- if (m_blockchain.empty())
- {
- m_blockchain.push_back(genesis_hash);
- m_last_block_reward = cryptonote::get_outs_money_amount(genesis.miner_tx);
- }
- else
- {
- check_genesis(genesis_hash);
- }
+ MDEBUG("Loading background cache");
- trim_hashchain();
+ // Set up a minimal background wallet2 instance
+ std::unique_ptr<wallet2> background_w2(new wallet2(m_nettype));
+ background_w2->m_is_background_wallet = true;
+ background_w2->m_background_syncing = true;
+ background_w2->m_background_sync_type = m_background_sync_type;
+ background_w2->m_custom_background_key = m_custom_background_key;
- if (get_num_subaddress_accounts() == 0)
- add_subaddress_account(tr("Primary account"));
+ cryptonote::account_base account = m_account;
+ account.forget_spend_key();
+ background_w2->m_account = account;
- try
- {
- find_and_save_rings(false);
- }
- catch (const std::exception &e)
- {
- MERROR("Failed to save rings, will try again next time");
- }
-
- try
- {
- if (use_fs)
- m_message_store.read_from_file(get_multisig_wallet_state(), m_mms_file, m_load_deprecated_formats);
+ // Load background cache from file
+ background_w2->clear();
+ background_w2->prepare_file_names(background_wallet_file);
+ background_w2->load_wallet_cache(true/*use_fs*/);
+
+ process_background_cache(background_w2->m_background_sync_data, background_w2->m_blockchain, background_w2->m_last_block_reward);
+
+ // Reset the background cache after processing
+ store_background_cache(m_custom_background_key.get(), true/*do_reset_background_sync_data*/);
}
- catch (const std::exception &e)
+ else
{
- MERROR("Failed to initialize MMS, it will be unusable");
+ THROW_WALLET_EXCEPTION(error::wallet_internal_error, "unknown background sync type");
}
}
//----------------------------------------------------------------------------------------------------
@@ -6383,6 +6779,8 @@ void wallet2::store_to(const std::string &path, const epee::wipeable_string &pas
same_file = canonical_old_path == canonical_new_path;
}
+ THROW_WALLET_EXCEPTION_IF(m_is_background_wallet && !same_file, error::wallet_internal_error,
+ "Cannot save background wallet files to a different location");
if (!same_file)
{
@@ -6399,6 +6797,21 @@ void wallet2::store_to(const std::string &path, const epee::wipeable_string &pas
}
}
}
+ else if (m_background_sync_type == BackgroundSyncCustomPassword && m_background_syncing && !m_is_background_wallet)
+ {
+ // We're background syncing, so store the wallet cache as a background cache
+ // keeping the background sync data
+ 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*/);
+ }
+ catch (const std::exception &e)
+ {
+ MERROR("Failed to store background cache while background syncing: " << e.what());
+ }
+ return;
+ }
// get wallet cache data
boost::optional<wallet2::cache_file_data> cache_file_data = get_cache_file_data();
@@ -6492,6 +6905,22 @@ void wallet2::store_to(const std::string &path, const epee::wipeable_string &pas
// store should only exist if the MMS is really active
m_message_store.write_to_file(get_multisig_wallet_state(), m_mms_file);
}
+
+ if (m_background_sync_type == BackgroundSyncCustomPassword && !m_background_syncing && !m_is_background_wallet)
+ {
+ // Update the background wallet cache when we store the main wallet cache
+ // Note: if background syncing when this is called, it means the background
+ // wallet is open and was already stored above
+ 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(), true/*do_reset_background_sync_data*/);
+ }
+ catch (const std::exception &e)
+ {
+ MERROR("Failed to update background cache: " << e.what());
+ }
+ }
}
//----------------------------------------------------------------------------------------------------
boost::optional<wallet2::cache_file_data> wallet2::get_cache_file_data()
@@ -6509,7 +6938,7 @@ boost::optional<wallet2::cache_file_data> wallet2::get_cache_file_data()
std::string cipher;
cipher.resize(cache_file_data.get().cache_data.size());
cache_file_data.get().iv = crypto::rand<crypto::chacha_iv>();
- crypto::chacha20(cache_file_data.get().cache_data.data(), cache_file_data.get().cache_data.size(), m_cache_key, cache_file_data.get().iv, &cipher[0]);
+ crypto::chacha20(cache_file_data.get().cache_data.data(), cache_file_data.get().cache_data.size(), get_cache_key(), cache_file_data.get().iv, &cipher[0]);
cache_file_data.get().cache_data = cipher;
return cache_file_data;
}
@@ -8560,6 +8989,34 @@ bool wallet2::is_keys_file_locked() const
return m_keys_file_locker->locked();
}
+bool wallet2::lock_background_keys_file(const std::string &background_keys_file)
+{
+ if (background_keys_file.empty() || !boost::filesystem::exists(background_keys_file))
+ return true;
+ if (m_background_keys_file_locker && m_background_keys_file_locker->locked())
+ return true;
+ m_background_keys_file_locker.reset(new tools::file_locker(background_keys_file));
+ return m_background_keys_file_locker->locked();
+}
+
+bool wallet2::unlock_background_keys_file()
+{
+ if (!m_background_keys_file_locker)
+ {
+ MDEBUG("background keys file locker is not set");
+ return false;
+ }
+ m_background_keys_file_locker.reset();
+ return true;
+}
+
+bool wallet2::is_background_keys_file_locked() const
+{
+ if (!m_background_keys_file_locker)
+ return false;
+ return m_background_keys_file_locker->locked();
+}
+
bool wallet2::tx_add_fake_output(std::vector<std::vector<tools::wallet2::get_outs_entry>> &outs, uint64_t global_index, const crypto::public_key& output_public_key, const rct::key& mask, uint64_t real_index, bool unlocked, std::unordered_set<crypto::public_key> &valid_public_keys_cache) const
{
if (!unlocked) // don't add locked outs
@@ -13241,6 +13698,413 @@ bool wallet2::import_key_images(signed_tx_set & signed_tx, size_t offset, bool o
return import_key_images(signed_tx.key_images, offset, only_selected_transfers ? boost::make_optional(selected_transfers) : boost::none);
}
+/*
+ In background sync mode, we use just the view key when the wallet is scanning
+ to identify all txs where:
+
+ 1. We received an output.
+ 2. We spent an output.
+ 3. We *may* have spent a received output but we didn't know for sure because
+ the spend key was not loaded while background sync was enabled.
+
+ When the user is ready to use the spend key again, we call this function to
+ process all those background synced transactions with the spend key loaded,
+ so that we can properly generate key images for the transactions which we
+ we were not able to do so for while background sync was enabled. This allows
+ us to determine *all* receives and spends the user completed while the wallet
+ had background sync enabled. Once this function completes, we can continue
+ scanning from where the background sync left off.
+
+ Txs of type 3 (txs which we *may* have spent received output(s)) are txs where
+ 1+ rings contain an output that the user received and the wallet does not know
+ the associated key image for that output. We don't know if the user spent in
+ this type of tx or not. This function will generate key images for all outputs
+ we don't know key images for, and then check if those outputs were spent in
+ the txs of type 3.
+
+ By storing this type of "plausible spend tx" when scanning in background sync
+ mode, we avoid the need to query the daemon with key images when background
+ sync mode is disabled to see if those key images were spent. This would
+ reveal key images to 3rd party nodes for users who don't run their own.
+ Although this is not a perfect solution to avoid revealing key images to a 3rd
+ party node (since tx submission trivially reveals key images to a node), it's
+ probably better than revealing *unused* key images to a 3rd party node, which
+ would enable the 3rd party to deduce that a tx is spending an output at least
+ X old when the key image is included in the chain.
+*/
+void wallet2::process_background_cache(const background_sync_data_t &background_sync_data, const hashchain &background_synced_chain, uint64_t last_block_reward)
+{
+ // We expect the spend key to be in a decrypted state while
+ // m_processing_background_cache is true
+ m_processing_background_cache = true;
+ auto done_processing = epee::misc_utils::create_scope_leave_handler([&, this]() {
+ m_processing_background_cache = false;
+ });
+
+ if (m_background_syncing || m_multisig || m_watch_only || key_on_device())
+ return;
+
+ if (!background_sync_data.first_refresh_done)
+ {
+ MDEBUG("Skipping processing background cache, background cache has not synced yet");
+ return;
+ }
+
+ // Skip processing if wallet cache is synced higher than background cache
+ const uint64_t current_height = m_blockchain.size();
+ const uint64_t background_height = background_synced_chain.size();
+ MDEBUG("Background cache height " << background_height << " , wallet height " << current_height);
+ if (current_height > background_height)
+ {
+ MWARNING("Skipping processing background cache, synced height is higher than background cache");
+ return;
+ }
+
+ if (m_refresh_from_block_height < background_sync_data.wallet_refresh_from_block_height ||
+ m_subaddress_lookahead_major > background_sync_data.subaddress_lookahead_major ||
+ m_subaddress_lookahead_minor > background_sync_data.subaddress_lookahead_minor ||
+ m_refresh_type < background_sync_data.wallet_refresh_type)
+ {
+ MWARNING("Skipping processing background cache, background wallet sync settings did not match main wallet's");
+ MDEBUG("Wallet settings: " <<
+ ", m_refresh_from_block_height: " << m_refresh_from_block_height << " vs " << background_sync_data.wallet_refresh_from_block_height <<
+ ", m_subaddress_lookahead_major: " << m_subaddress_lookahead_major << " vs " << background_sync_data.subaddress_lookahead_major <<
+ ", m_subaddress_lookahead_minor: " << m_subaddress_lookahead_minor << " vs " << background_sync_data.subaddress_lookahead_minor <<
+ ", m_refresh_type: " << m_refresh_type << " vs " << background_sync_data.wallet_refresh_type);
+ return;
+ }
+
+ // Sort background synced txs in the order they appeared in the cache so that
+ // we process them in the order they appeared in the chain. Thus if tx2 spends
+ // from tx1, we will know because tx1 is processed before tx2.
+ std::vector<std::pair<crypto::hash, background_synced_tx_t>> sorted_bgs_cache(background_sync_data.txs.begin(), background_sync_data.txs.end());
+ std::sort(sorted_bgs_cache.begin(), sorted_bgs_cache.end(),
+ [](const std::pair<crypto::hash, background_synced_tx_t>& l, const std::pair<crypto::hash, background_synced_tx_t>& r)
+ {
+ uint64_t left_index = l.second.index_in_background_sync_data;
+ uint64_t right_index = r.second.index_in_background_sync_data;
+ THROW_WALLET_EXCEPTION_IF(
+ (left_index < right_index && l.second.height > r.second.height) ||
+ (left_index > right_index && l.second.height < r.second.height),
+ error::wallet_internal_error, "Unexpected background sync data order");
+ return left_index < right_index;
+ });
+
+ // All txs in the background cache should have height >= sync start height,
+ // but not fatal if not
+ if (!sorted_bgs_cache.empty() && sorted_bgs_cache[0].second.height < background_sync_data.start_height)
+ MWARNING("First tx in background cache has height (" << sorted_bgs_cache[0].second.height << ") lower than sync start height (" << background_sync_data.start_height << ")");
+
+ // We want to process all background synced txs in order to make sure
+ // the wallet state updates correctly. First we remove all txs from the wallet
+ // from before the background sync start height, then re-process them in
+ // chronological order. The background cache should contain a superset of
+ // *all* the wallet's txs from after the background sync start height.
+ MDEBUG("Processing " << background_sync_data.txs.size() << " background synced txs starting from height " << background_sync_data.start_height);
+ detached_blockchain_data dbd = detach_blockchain(background_sync_data.start_height);
+
+ for (const auto &bgs_tx : sorted_bgs_cache)
+ {
+ MDEBUG("Processing background synced tx " << bgs_tx.first);
+
+ process_new_transaction(bgs_tx.first, bgs_tx.second.tx, bgs_tx.second.output_indices, bgs_tx.second.height, 0, bgs_tx.second.block_timestamp,
+ cryptonote::is_coinbase(bgs_tx.second.tx), false/*pool*/, bgs_tx.second.double_spend_seen, {}, {}, true/*ignore_callbacks*/);
+
+ // Re-set destination addresses if they were previously set
+ if (m_confirmed_txs.find(bgs_tx.first) != m_confirmed_txs.end() &&
+ dbd.detached_confirmed_txs_dests.find(bgs_tx.first) != dbd.detached_confirmed_txs_dests.end())
+ {
+ m_confirmed_txs[bgs_tx.first].m_dests = std::move(dbd.detached_confirmed_txs_dests[bgs_tx.first]);
+ }
+ }
+
+ m_blockchain = background_synced_chain;
+ m_last_block_reward = last_block_reward;
+
+ MDEBUG("Finished processing background sync data");
+}
+//----------------------------------------------------------------------------------------------------
+void wallet2::reset_background_sync_data(background_sync_data_t &background_sync_data)
+{
+ background_sync_data.first_refresh_done = false;
+ background_sync_data.start_height = get_blockchain_current_height();
+ background_sync_data.txs.clear();
+
+ background_sync_data.wallet_refresh_from_block_height = m_refresh_from_block_height;
+ background_sync_data.subaddress_lookahead_major = m_subaddress_lookahead_major;
+ background_sync_data.subaddress_lookahead_minor = m_subaddress_lookahead_minor;
+ background_sync_data.wallet_refresh_type = m_refresh_type;
+}
+//----------------------------------------------------------------------------------------------------
+void wallet2::store_background_cache(const crypto::chacha_key &custom_background_key, const bool do_reset_background_sync_data)
+{
+ MDEBUG("Storing background cache (do_reset_background_sync_data=" << do_reset_background_sync_data << ")");
+
+ THROW_WALLET_EXCEPTION_IF(m_background_sync_type != BackgroundSyncCustomPassword, error::wallet_internal_error,
+ "Can only write a background cache when using a custom background password");
+ THROW_WALLET_EXCEPTION_IF(m_wallet_file.empty(), error::wallet_internal_error,
+ "No wallet file known, can't store background cache");
+
+ std::unique_ptr<wallet2> background_w2(new wallet2(m_nettype));
+ background_w2->prepare_file_names(make_background_wallet_file_name(m_wallet_file));
+
+ // Make sure background wallet is opened by this wallet
+ THROW_WALLET_EXCEPTION_IF(!lock_background_keys_file(background_w2->m_keys_file),
+ error::background_wallet_already_open, background_w2->m_wallet_file);
+
+ // Load a background wallet2 instance using this wallet2 instance
+ std::string this_wallet2;
+ bool r = ::serialization::dump_binary(*this, this_wallet2);
+ THROW_WALLET_EXCEPTION_IF(!r, error::wallet_internal_error, "Failed to serialize wallet cache");
+
+ background_w2->clear();
+ r = ::serialization::parse_binary(this_wallet2, *background_w2);
+ THROW_WALLET_EXCEPTION_IF(!r, error::wallet_internal_error, "Failed to deserialize wallet cache");
+
+ // Clear sensitive data from background cache not needed to sync
+ background_w2->clear_user_data();
+
+ background_w2->m_is_background_wallet = true;
+ if (do_reset_background_sync_data)
+ reset_background_sync_data(background_w2->m_background_sync_data);
+ else
+ background_w2->m_background_sync_data = m_background_sync_data;
+ background_w2->m_background_syncing = true;
+
+ background_w2->m_custom_background_key = boost::optional<crypto::chacha_key>(custom_background_key);
+ background_w2->m_background_sync_type = m_background_sync_type;
+ background_w2->store();
+
+ MDEBUG("Background cache stored (" << background_w2->m_transfers.size() << " transfers, " << background_w2->m_background_sync_data.txs.size() << " background synced txs)");
+}
+//----------------------------------------------------------------------------------------------------
+void wallet2::store_background_keys(const crypto::chacha_key &custom_background_key)
+{
+ MDEBUG("Storing background keys");
+
+ THROW_WALLET_EXCEPTION_IF(m_wallet_file.empty(), error::wallet_internal_error,
+ "No wallet file known, can't store background keys");
+
+ const std::string background_keys_file = make_background_keys_file_name(m_wallet_file);
+ bool r = store_keys(background_keys_file, custom_background_key, false/*watch_only*/, true/*background_keys_file*/);
+ THROW_WALLET_EXCEPTION_IF(!r, error::file_save_error, background_keys_file);
+ THROW_WALLET_EXCEPTION_IF(!is_background_keys_file_locked(), error::wallet_internal_error, background_keys_file + "\" should be locked");
+
+ // GUI uses the address file to differentiate non-mainnet wallets in the UI
+ const std::string background_address_file = make_background_wallet_file_name(m_wallet_file) + ".address.txt";
+ if (m_nettype != MAINNET && !boost::filesystem::exists(background_address_file))
+ {
+ r = save_to_file(background_address_file, m_account.get_public_address_str(m_nettype), true);
+ if (!r) MERROR("String with address text not saved");
+ }
+
+ MDEBUG("Background keys stored");
+}
+//----------------------------------------------------------------------------------------------------
+void wallet2::write_background_sync_wallet(const epee::wipeable_string &wallet_password, const epee::wipeable_string &background_cache_password)
+{
+ MDEBUG("Storing background sync wallet");
+ THROW_WALLET_EXCEPTION_IF(m_background_sync_type != BackgroundSyncCustomPassword, error::wallet_internal_error,
+ "Can only write a background sync wallet when using a custom background password");
+ THROW_WALLET_EXCEPTION_IF(m_background_syncing || m_is_background_wallet, error::wallet_internal_error,
+ "Can't write background sync wallet from an existing background cache");
+ THROW_WALLET_EXCEPTION_IF(wallet_password == background_cache_password,
+ error::background_custom_password_same_as_wallet_password);
+
+ // Set the background encryption key
+ crypto::chacha_key custom_background_key;
+ get_custom_background_key(background_cache_password, custom_background_key, m_kdf_rounds);
+
+ // Keep the background encryption key in memory so the main wallet can update
+ // the background cache when it stores the main wallet cache
+ m_custom_background_key = boost::optional<crypto::chacha_key>(custom_background_key);
+
+ if (m_wallet_file.empty() || m_keys_file.empty())
+ return;
+
+ // Save background keys file, then background cache, then update main wallet settings
+ store_background_keys(custom_background_key);
+ store_background_cache(custom_background_key, true/*do_reset_background_sync_data*/);
+ bool r = store_keys(m_keys_file, wallet_password, false/*watch_only*/);
+ THROW_WALLET_EXCEPTION_IF(!r, error::file_save_error, m_keys_file);
+
+ MDEBUG("Background sync wallet saved successfully");
+}
+//----------------------------------------------------------------------------------------------------
+void wallet2::setup_background_sync(BackgroundSyncType background_sync_type, const epee::wipeable_string &wallet_password, const boost::optional<epee::wipeable_string> &background_cache_password)
+{
+ MDEBUG("Setting background sync to type " << background_sync_type);
+ THROW_WALLET_EXCEPTION_IF(m_background_syncing || m_is_background_wallet, error::wallet_internal_error,
+ "Can't set background sync type from an existing background cache");
+ verify_password_with_cached_key(wallet_password);
+
+ if (background_sync_type != BackgroundSyncOff)
+ validate_background_cache_password_usage(background_sync_type, background_cache_password, m_multisig, m_watch_only, key_on_device());
+
+ THROW_WALLET_EXCEPTION_IF(background_sync_type == BackgroundSyncCustomPassword && wallet_password == background_cache_password,
+ error::background_custom_password_same_as_wallet_password);
+
+ if (m_background_sync_type == background_sync_type && background_sync_type != BackgroundSyncCustomPassword)
+ return; // No need to make any changes
+
+ if (!m_wallet_file.empty())
+ {
+ // Delete existing background files if they already exist
+ const std::string old_background_wallet_file = make_background_wallet_file_name(m_wallet_file);
+ const std::string old_background_keys_file = make_background_keys_file_name(m_wallet_file);
+ const std::string old_background_address_file = old_background_wallet_file + ".address.txt";
+
+ // Make sure no other program is using the background wallet
+ THROW_WALLET_EXCEPTION_IF(!lock_background_keys_file(old_background_keys_file),
+ error::background_wallet_already_open, old_background_wallet_file);
+
+ if (boost::filesystem::exists(old_background_wallet_file))
+ if (!boost::filesystem::remove(old_background_wallet_file))
+ LOG_ERROR("Error deleting background wallet file: " << old_background_wallet_file);
+
+ if (boost::filesystem::exists(old_background_keys_file))
+ if (!boost::filesystem::remove(old_background_keys_file))
+ LOG_ERROR("Error deleting background keys file: " << old_background_keys_file);
+
+ if (boost::filesystem::exists(old_background_address_file))
+ if (!boost::filesystem::remove(old_background_address_file))
+ LOG_ERROR("Error deleting background address file: " << old_background_address_file);
+ }
+
+ m_background_sync_type = background_sync_type;
+ m_custom_background_key = boost::none;
+
+ // Write the new files
+ switch (background_sync_type)
+ {
+ case BackgroundSyncOff:
+ case BackgroundSyncReusePassword: rewrite(m_wallet_file, wallet_password); break;
+ case BackgroundSyncCustomPassword: write_background_sync_wallet(wallet_password, background_cache_password.get()); break;
+ default: THROW_WALLET_EXCEPTION(error::wallet_internal_error, "unknown background sync type");
+ }
+
+ MDEBUG("Done setting background sync type");
+}
+//----------------------------------------------------------------------------------------------------
+/*
+ When background syncing, the wallet scans using just the view key, without
+ keeping the spend key in decrypted state. When a user returns to the wallet
+ and decrypts the spend key, the wallet processes the background synced txs,
+ then the wallet picks up scanning normally right where the background sync
+ left off.
+*/
+void wallet2::start_background_sync()
+{
+ THROW_WALLET_EXCEPTION_IF(m_background_sync_type == BackgroundSyncOff, error::wallet_internal_error,
+ "must setup background sync first before using background sync");
+ THROW_WALLET_EXCEPTION_IF(m_is_background_wallet, error::wallet_internal_error,
+ "Can't start background syncing from a background wallet (it is always background syncing)");
+
+ MDEBUG("Starting background sync");
+
+ if (m_background_syncing)
+ {
+ MDEBUG("Already background syncing");
+ return;
+ }
+
+ if (m_background_sync_type == BackgroundSyncCustomPassword && !m_wallet_file.empty())
+ {
+ // Save the current state of the wallet cache. Only necessary when using a
+ // custom background password which uses distinct background wallet to sync.
+ // When reusing wallet password to sync we reuse the main wallet cache.
+ store();
+
+ // Wipe user data from the background wallet cache not needed to sync.
+ // Only wipe user data from background cache if wallet cache is stored
+ // on disk; otherwise we could lose the data.
+ clear_user_data();
+
+ // Wipe m_cache_key since it can be used to decrypt main wallet cache
+ m_cache_key.scrub();
+ }
+
+ reset_background_sync_data(m_background_sync_data);
+ m_background_syncing = true;
+
+ // Wipe the spend key from memory
+ m_account.forget_spend_key();
+
+ MDEBUG("Background sync started at height " << m_background_sync_data.start_height);
+}
+//----------------------------------------------------------------------------------------------------
+void wallet2::stop_background_sync(const epee::wipeable_string &wallet_password, const crypto::secret_key &spend_secret_key)
+{
+ MDEBUG("Stopping background sync");
+
+ // Verify provided password and spend secret key. If no spend secret key is
+ // provided, recover it from the wallet keys file
+ crypto::secret_key recovered_spend_key = crypto::null_skey;
+ if (!m_wallet_file.empty())
+ {
+ THROW_WALLET_EXCEPTION_IF(!verify_password(wallet_password, recovered_spend_key), error::invalid_password);
+ }
+ else
+ {
+ verify_password_with_cached_key(wallet_password);
+ }
+
+ if (spend_secret_key != crypto::null_skey)
+ {
+ THROW_WALLET_EXCEPTION_IF(!m_wallet_file.empty() && spend_secret_key != recovered_spend_key,
+ error::invalid_spend_key);
+ MDEBUG("Setting spend secret key with the provided key");
+ recovered_spend_key = spend_secret_key;
+ }
+
+ // Verify private spend key derives to wallet's public spend key
+ const auto verify_spend_key = [this](crypto::secret_key &recovered_spend_key) -> bool
+ {
+ crypto::public_key spend_public_key;
+ return recovered_spend_key != crypto::null_skey &&
+ crypto::secret_key_to_public_key(recovered_spend_key, spend_public_key) &&
+ m_account.get_keys().m_account_address.m_spend_public_key == spend_public_key;
+ };
+ THROW_WALLET_EXCEPTION_IF(!verify_spend_key(recovered_spend_key), error::invalid_spend_key);
+
+ THROW_WALLET_EXCEPTION_IF(m_background_sync_type == BackgroundSyncOff, error::wallet_internal_error,
+ "must setup background sync first before using background sync");
+ THROW_WALLET_EXCEPTION_IF(m_is_background_wallet, error::wallet_internal_error,
+ "Can't stop background syncing from a background wallet");
+
+ if (!m_background_syncing)
+ return;
+
+ // Copy background cache, we're about to overwrite it
+ const background_sync_data_t background_sync_data = m_background_sync_data;
+ const hashchain background_synced_chain = m_blockchain;
+ const uint64_t last_block_reward = m_last_block_reward;
+
+ if (m_background_sync_type == BackgroundSyncCustomPassword && !m_wallet_file.empty())
+ {
+ // Reload the wallet from disk
+ load(m_wallet_file, wallet_password);
+ THROW_WALLET_EXCEPTION_IF(!verify_spend_key(recovered_spend_key), error::invalid_spend_key);
+ }
+ m_background_syncing = false;
+
+ // Set the plaintext spend key
+ m_account.set_spend_key(recovered_spend_key);
+
+ // Encrypt the spend key when done if needed
+ epee::misc_utils::auto_scope_leave_caller keys_reencryptor;
+ if (m_ask_password == AskPasswordToDecrypt && !m_unattended && !m_watch_only)
+ keys_reencryptor = epee::misc_utils::create_scope_leave_handler([&, this]{encrypt_keys(wallet_password);});
+
+ // Now we can use the decrypted spend key to process background cache
+ process_background_cache(background_sync_data, background_synced_chain, last_block_reward);
+
+ // Reset the background cache after processing
+ reset_background_sync_data(m_background_sync_data);
+
+ MDEBUG("Background sync stopped");
+}
+//----------------------------------------------------------------------------------------------------
wallet2::payment_container wallet2::export_payments() const
{
payment_container payments;