summaryrefslogtreecommitdiff
path: root/patches/monero
diff options
context:
space:
mode:
authorCzarek Nakamoto <cyjan@mrcyjanek.net>2024-03-22 14:21:57 +0100
committerCzarek Nakamoto <cyjan@mrcyjanek.net>2024-03-22 14:21:57 +0100
commit380f3f41e62cb72ed24e4f46df78f36f8e28c6d6 (patch)
treee536ad49963dcf85d176bed157059f7d8397c224 /patches/monero
parent8775db7e945552e4f23c429dde4ddf91d974a5a2 (diff)
wow + build
Diffstat (limited to 'patches/monero')
-rw-r--r--patches/monero/0000-polyseed.patch1302
-rw-r--r--patches/monero/0001-background-sync.patch4172
-rw-r--r--patches/monero/0002-airgap.patch184
-rw-r--r--patches/monero/0003-coin-control.patch916
-rw-r--r--patches/monero/0004-fix-build.patch134
5 files changed, 6708 insertions, 0 deletions
diff --git a/patches/monero/0000-polyseed.patch b/patches/monero/0000-polyseed.patch
new file mode 100644
index 0000000..7513399
--- /dev/null
+++ b/patches/monero/0000-polyseed.patch
@@ -0,0 +1,1302 @@
+From edd49593044dc861b5fb4b9bae56a7636199aeaf Mon Sep 17 00:00:00 2001
+From: Czarek Nakamoto <cyjan@mrcyjanek.net>
+Date: Tue, 12 Mar 2024 09:42:37 +0100
+Subject: [PATCH] PATCH: polyseed
+
+---
+ .github/workflows/build.yml | 4 +-
+ .gitmodules | 6 +
+ CMakeLists.txt | 4 +-
+ contrib/epee/include/wipeable_string.h | 7 +
+ contrib/epee/src/wipeable_string.cpp | 10 ++
+ external/CMakeLists.txt | 2 +
+ external/polyseed | 1 +
+ external/utf8proc | 1 +
+ src/CMakeLists.txt | 1 +
+ src/cryptonote_basic/CMakeLists.txt | 1 +
+ src/cryptonote_basic/account.cpp | 23 +++-
+ src/cryptonote_basic/account.h | 6 +
+ src/cryptonote_config.h | 2 +
+ src/polyseed/CMakeLists.txt | 25 ++++
+ src/polyseed/pbkdf2.c | 85 ++++++++++++
+ src/polyseed/pbkdf2.h | 46 +++++++
+ src/polyseed/polyseed.cpp | 182 +++++++++++++++++++++++++
+ src/polyseed/polyseed.hpp | 167 +++++++++++++++++++++++
+ src/wallet/api/wallet.cpp | 71 ++++++++++
+ src/wallet/api/wallet.h | 10 ++
+ src/wallet/api/wallet2_api.h | 25 ++++
+ src/wallet/api/wallet_manager.cpp | 9 ++
+ src/wallet/api/wallet_manager.h | 10 ++
+ src/wallet/wallet2.cpp | 102 ++++++++++++--
+ src/wallet/wallet2.h | 30 +++-
+ 25 files changed, 809 insertions(+), 21 deletions(-)
+ create mode 160000 external/polyseed
+ create mode 160000 external/utf8proc
+ create mode 100644 src/polyseed/CMakeLists.txt
+ create mode 100644 src/polyseed/pbkdf2.c
+ create mode 100644 src/polyseed/pbkdf2.h
+ create mode 100644 src/polyseed/polyseed.cpp
+ create mode 100644 src/polyseed/polyseed.hpp
+
+diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
+index 4c1e381c0..70bea03b3 100644
+--- a/.github/workflows/build.yml
++++ b/.github/workflows/build.yml
+@@ -124,8 +124,8 @@ jobs:
+ - name: build
+ run: |
+ ${{env.CCACHE_SETTINGS}}
+- cmake .
+- make wallet_api -j3
++ cmake -S . -B build
++ cmake --build build wallet_api -j3
+
+ test-ubuntu:
+ needs: build-ubuntu
+diff --git a/.gitmodules b/.gitmodules
+index 721cce3b4..73a23fb35 100644
+--- a/.gitmodules
++++ b/.gitmodules
+@@ -10,6 +10,12 @@
+ [submodule "external/randomx"]
+ path = external/randomx
+ url = https://github.com/tevador/RandomX
++[submodule "external/utf8proc"]
++ path = external/utf8proc
++ url = https://github.com/JuliaStrings/utf8proc.git
++[submodule "external/polyseed"]
++ path = external/polyseed
++ url = https://github.com/tevador/polyseed.git
+ [submodule "external/supercop"]
+ path = external/supercop
+ url = https://github.com/monero-project/supercop
+diff --git a/CMakeLists.txt b/CMakeLists.txt
+index 8fb03ba1f..63b8c5079 100644
+--- a/CMakeLists.txt
++++ b/CMakeLists.txt
+@@ -369,6 +369,8 @@ if(NOT MANUAL_SUBMODULES)
+ check_submodule(external/trezor-common)
+ check_submodule(external/randomx)
+ check_submodule(external/supercop)
++ check_submodule(external/polyseed)
++ check_submodule(external/utf8proc)
+ endif()
+ endif()
+
+@@ -458,7 +460,7 @@ endif()
+ # elseif(CMAKE_SYSTEM_NAME MATCHES ".*BSDI.*")
+ # set(BSDI TRUE)
+
+-include_directories(external/rapidjson/include external/easylogging++ src contrib/epee/include external external/supercop/include)
++include_directories(external/rapidjson/include external/easylogging++ src contrib/epee/include external external/supercop/include external/polyseed/include external/utf8proc)
+
+ if(APPLE)
+ cmake_policy(SET CMP0042 NEW)
+diff --git a/contrib/epee/include/wipeable_string.h b/contrib/epee/include/wipeable_string.h
+index 65977cd97..594e15de4 100644
+--- a/contrib/epee/include/wipeable_string.h
++++ b/contrib/epee/include/wipeable_string.h
+@@ -34,6 +34,7 @@
+ #include <string>
+ #include "memwipe.h"
+ #include "fnv1.h"
++#include "serialization/keyvalue_serialization.h"
+
+ namespace epee
+ {
+@@ -75,6 +76,12 @@ namespace epee
+ bool operator!=(const wipeable_string &other) const noexcept { return buffer != other.buffer; }
+ wipeable_string &operator=(wipeable_string &&other);
+ wipeable_string &operator=(const wipeable_string &other);
++ char& operator[](size_t idx);
++ const char& operator[](size_t idx) const;
++
++ BEGIN_KV_SERIALIZE_MAP()
++ KV_SERIALIZE_CONTAINER_POD_AS_BLOB(buffer)
++ END_KV_SERIALIZE_MAP()
+
+ private:
+ void grow(size_t sz, size_t reserved = 0);
+diff --git a/contrib/epee/src/wipeable_string.cpp b/contrib/epee/src/wipeable_string.cpp
+index b016f2f48..f2f365b1b 100644
+--- a/contrib/epee/src/wipeable_string.cpp
++++ b/contrib/epee/src/wipeable_string.cpp
+@@ -261,4 +261,14 @@ wipeable_string &wipeable_string::operator=(const wipeable_string &other)
+ return *this;
+ }
+
++char& wipeable_string::operator[](size_t idx) {
++ CHECK_AND_ASSERT_THROW_MES(idx < buffer.size(), "Index out of bounds");
++ return buffer[idx];
++}
++
++const char& wipeable_string::operator[](size_t idx) const {
++ CHECK_AND_ASSERT_THROW_MES(idx < buffer.size(), "Index out of bounds");
++ return buffer[idx];
++}
++
+ }
+diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt
+index 5b7f69a56..1b9761d70 100644
+--- a/external/CMakeLists.txt
++++ b/external/CMakeLists.txt
+@@ -70,3 +70,5 @@ add_subdirectory(db_drivers)
+ add_subdirectory(easylogging++)
+ add_subdirectory(qrcodegen)
+ add_subdirectory(randomx EXCLUDE_FROM_ALL)
++add_subdirectory(polyseed EXCLUDE_FROM_ALL)
++add_subdirectory(utf8proc EXCLUDE_FROM_ALL)
+\ No newline at end of file
+diff --git a/external/polyseed b/external/polyseed
+new file mode 160000
+index 000000000..b7c35bb3c
+--- /dev/null
++++ b/external/polyseed
+@@ -0,0 +1 @@
++Subproject commit b7c35bb3c6b91e481ecb04fc235eaff69c507fa1
+diff --git a/external/utf8proc b/external/utf8proc
+new file mode 160000
+index 000000000..1cb28a66c
+--- /dev/null
++++ b/external/utf8proc
+@@ -0,0 +1 @@
++Subproject commit 1cb28a66ca79a0845e99433fd1056257456cef8b
+diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
+index 3335d3c21..06b708cf0 100644
+--- a/src/CMakeLists.txt
++++ b/src/CMakeLists.txt
+@@ -95,6 +95,7 @@ add_subdirectory(net)
+ add_subdirectory(hardforks)
+ add_subdirectory(blockchain_db)
+ add_subdirectory(mnemonics)
++add_subdirectory(polyseed)
+ add_subdirectory(rpc)
+ if(NOT IOS)
+ add_subdirectory(serialization)
+diff --git a/src/cryptonote_basic/CMakeLists.txt b/src/cryptonote_basic/CMakeLists.txt
+index 1414be1b2..414936a05 100644
+--- a/src/cryptonote_basic/CMakeLists.txt
++++ b/src/cryptonote_basic/CMakeLists.txt
+@@ -71,6 +71,7 @@ target_link_libraries(cryptonote_basic
+ checkpoints
+ cryptonote_format_utils_basic
+ device
++ polyseed_wrapper
+ ${Boost_DATE_TIME_LIBRARY}
+ ${Boost_PROGRAM_OPTIONS_LIBRARY}
+ ${Boost_SERIALIZATION_LIBRARY}
+diff --git a/src/cryptonote_basic/account.cpp b/src/cryptonote_basic/account.cpp
+index 2ac455fda..4931c3740 100644
+--- a/src/cryptonote_basic/account.cpp
++++ b/src/cryptonote_basic/account.cpp
+@@ -87,12 +87,16 @@ DISABLE_VS_WARNINGS(4244 4345)
+ void account_keys::xor_with_key_stream(const crypto::chacha_key &key)
+ {
+ // encrypt a large enough byte stream with chacha20
+- epee::wipeable_string key_stream = get_key_stream(key, m_encryption_iv, sizeof(crypto::secret_key) * (2 + m_multisig_keys.size()));
++ epee::wipeable_string key_stream = get_key_stream(key, m_encryption_iv, sizeof(crypto::secret_key) * (3 + m_multisig_keys.size()) + m_passphrase.size());
+ const char *ptr = key_stream.data();
+ for (size_t i = 0; i < sizeof(crypto::secret_key); ++i)
+ m_spend_secret_key.data[i] ^= *ptr++;
+ for (size_t i = 0; i < sizeof(crypto::secret_key); ++i)
+ m_view_secret_key.data[i] ^= *ptr++;
++ for (size_t i = 0; i < sizeof(crypto::secret_key); ++i)
++ m_polyseed.data[i] ^= *ptr++;
++ for (size_t i = 0; i < m_passphrase.size(); ++i)
++ m_passphrase.data()[i] ^= *ptr++;
+ for (crypto::secret_key &k: m_multisig_keys)
+ {
+ for (size_t i = 0; i < sizeof(crypto::secret_key); ++i)
+@@ -150,6 +154,8 @@ DISABLE_VS_WARNINGS(4244 4345)
+ {
+ m_keys.m_spend_secret_key = crypto::secret_key();
+ m_keys.m_multisig_keys.clear();
++ m_keys.m_polyseed = crypto::secret_key();
++ m_keys.m_passphrase.wipe();
+ }
+ //-----------------------------------------------------------------
+ crypto::secret_key account_base::generate(const crypto::secret_key& recovery_key, bool recover, bool two_random)
+@@ -244,6 +250,21 @@ DISABLE_VS_WARNINGS(4244 4345)
+ create_from_keys(address, fake, viewkey);
+ }
+ //-----------------------------------------------------------------
++ void account_base::create_from_polyseed(const polyseed::data& seed, const epee::wipeable_string &passphrase)
++ {
++ crypto::secret_key secret_key;
++ seed.keygen(&secret_key, sizeof(secret_key));
++
++ if (!passphrase.empty()) {
++ secret_key = cryptonote::decrypt_key(secret_key, passphrase);
++ }
++
++ generate(secret_key, true, false);
++
++ seed.save(m_keys.m_polyseed.data);
++ m_keys.m_passphrase = passphrase;
++ }
++ //-----------------------------------------------------------------
+ bool account_base::make_multisig(const crypto::secret_key &view_secret_key, const crypto::secret_key &spend_secret_key, const crypto::public_key &spend_public_key, const std::vector<crypto::secret_key> &multisig_keys)
+ {
+ m_keys.m_account_address.m_spend_public_key = spend_public_key;
+diff --git a/src/cryptonote_basic/account.h b/src/cryptonote_basic/account.h
+index 2ee9545d4..0099ebfe7 100644
+--- a/src/cryptonote_basic/account.h
++++ b/src/cryptonote_basic/account.h
+@@ -33,6 +33,7 @@
+ #include "cryptonote_basic.h"
+ #include "crypto/crypto.h"
+ #include "serialization/keyvalue_serialization.h"
++#include "polyseed/polyseed.hpp"
+
+ namespace cryptonote
+ {
+@@ -45,6 +46,8 @@ namespace cryptonote
+ std::vector<crypto::secret_key> m_multisig_keys;
+ hw::device *m_device = &hw::get_device("default");
+ crypto::chacha_iv m_encryption_iv;
++ crypto::secret_key m_polyseed;
++ epee::wipeable_string m_passphrase; // Only used with polyseed
+
+ BEGIN_KV_SERIALIZE_MAP()
+ KV_SERIALIZE(m_account_address)
+@@ -53,6 +56,8 @@ namespace cryptonote
+ KV_SERIALIZE_CONTAINER_POD_AS_BLOB(m_multisig_keys)
+ const crypto::chacha_iv default_iv{{0, 0, 0, 0, 0, 0, 0, 0}};
+ KV_SERIALIZE_VAL_POD_AS_BLOB_OPT(m_encryption_iv, default_iv)
++ KV_SERIALIZE_VAL_POD_AS_BLOB_FORCE(m_polyseed)
++ KV_SERIALIZE(m_passphrase)
+ END_KV_SERIALIZE_MAP()
+
+ void encrypt(const crypto::chacha_key &key);
+@@ -79,6 +84,7 @@ namespace cryptonote
+ void create_from_device(hw::device &hwdev);
+ void create_from_keys(const cryptonote::account_public_address& address, const crypto::secret_key& spendkey, const crypto::secret_key& viewkey);
+ void create_from_viewkey(const cryptonote::account_public_address& address, const crypto::secret_key& viewkey);
++ void create_from_polyseed(const polyseed::data &polyseed, const epee::wipeable_string &passphrase);
+ bool make_multisig(const crypto::secret_key &view_secret_key, const crypto::secret_key &spend_secret_key, const crypto::public_key &spend_public_key, const std::vector<crypto::secret_key> &multisig_keys);
+ const account_keys& get_keys() const;
+ std::string get_public_address_str(network_type nettype) const;
+diff --git a/src/cryptonote_config.h b/src/cryptonote_config.h
+index 61146a114..8e1a07110 100644
+--- a/src/cryptonote_config.h
++++ b/src/cryptonote_config.h
+@@ -207,6 +207,8 @@
+
+ #define DNS_BLOCKLIST_LIFETIME (86400 * 8)
+
++#define POLYSEED_COIN POLYSEED_MONERO
++
+ //The limit is enough for the mandatory transaction content with 16 outputs (547 bytes),
+ //a custom tag (1 byte) and up to 32 bytes of custom data for each recipient.
+ // (1+32) + (1+1+16*32) + (1+16*32) = 1060
+diff --git a/src/polyseed/CMakeLists.txt b/src/polyseed/CMakeLists.txt
+new file mode 100644
+index 000000000..cca4eb746
+--- /dev/null
++++ b/src/polyseed/CMakeLists.txt
+@@ -0,0 +1,25 @@
++set(polyseed_sources
++ pbkdf2.c
++ polyseed.cpp
++)
++
++monero_find_all_headers(polyseed_private_headers "${CMAKE_CURRENT_SOURCE_DIR}")
++
++monero_private_headers(polyseed_wrapper
++ ${polyseed_private_headers}
++)
++
++monero_add_library(polyseed_wrapper
++ ${polyseed_sources}
++ ${polyseed_headers}
++ ${polyseed_private_headers}
++)
++
++target_link_libraries(polyseed_wrapper
++PUBLIC
++ polyseed
++ utf8proc
++ ${SODIUM_LIBRARY}
++ PRIVATE
++ ${EXTRA_LIBRARIES}
++)
+diff --git a/src/polyseed/pbkdf2.c b/src/polyseed/pbkdf2.c
+new file mode 100644
+index 000000000..1c45f4708
+--- /dev/null
++++ b/src/polyseed/pbkdf2.c
+@@ -0,0 +1,85 @@
++// Copyright (c) 2023, The Monero Project
++// Copyright (c) 2021, tevador <tevador@gmail.com>
++// Copyright (c) 2005,2007,2009 Colin Percival
++//
++// All rights reserved.
++//
++// Redistribution and use in source and binary forms, with or without
++// modification, are permitted provided that the following conditions
++// are met:
++// 1. Redistributions of source code must retain the above copyright
++// notice, this list of conditions and the following disclaimer.
++// 2. Redistributions in binary form must reproduce the above copyright
++// notice, this list of conditions and the following disclaimer in the
++// documentation and/or other materials provided with the distribution.
++//
++// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
++// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
++// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
++// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
++// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
++// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
++// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
++// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
++// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
++// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
++// SUCH DAMAGE.
++
++#include <string.h>
++
++#include <sodium/crypto_auth_hmacsha256.h>
++#include <sodium/utils.h>
++
++static inline void
++store32_be(uint8_t dst[4], uint32_t w)
++{
++ dst[3] = (uint8_t) w; w >>= 8;
++ dst[2] = (uint8_t) w; w >>= 8;
++ dst[1] = (uint8_t) w; w >>= 8;
++ dst[0] = (uint8_t) w;
++}
++
++void
++crypto_pbkdf2_sha256(const uint8_t* passwd, size_t passwdlen,
++ const uint8_t* salt, size_t saltlen, uint64_t c,
++ uint8_t* buf, size_t dkLen)
++{
++ crypto_auth_hmacsha256_state Phctx, PShctx, hctx;
++ size_t i;
++ uint8_t ivec[4];
++ uint8_t U[32];
++ uint8_t T[32];
++ uint64_t j;
++ int k;
++ size_t clen;
++
++ crypto_auth_hmacsha256_init(&Phctx, passwd, passwdlen);
++ PShctx = Phctx;
++ crypto_auth_hmacsha256_update(&PShctx, salt, saltlen);
++
++ for (i = 0; i * 32 < dkLen; i++) {
++ store32_be(ivec, (uint32_t)(i + 1));
++ hctx = PShctx;
++ crypto_auth_hmacsha256_update(&hctx, ivec, 4);
++ crypto_auth_hmacsha256_final(&hctx, U);
++
++ memcpy(T, U, 32);
++ for (j = 2; j <= c; j++) {
++ hctx = Phctx;
++ crypto_auth_hmacsha256_update(&hctx, U, 32);
++ crypto_auth_hmacsha256_final(&hctx, U);
++
++ for (k = 0; k < 32; k++) {
++ T[k] ^= U[k];
++ }
++ }
++
++ clen = dkLen - i * 32;
++ if (clen > 32) {
++ clen = 32;
++ }
++ memcpy(&buf[i * 32], T, clen);
++ }
++ sodium_memzero((void*)&Phctx, sizeof Phctx);
++ sodium_memzero((void*)&PShctx, sizeof PShctx);
++}
+\ No newline at end of file
+diff --git a/src/polyseed/pbkdf2.h b/src/polyseed/pbkdf2.h
+new file mode 100644
+index 000000000..f6253b9d7
+--- /dev/null
++++ b/src/polyseed/pbkdf2.h
+@@ -0,0 +1,46 @@
++// Copyright (c) 2023, The Monero Project
++// Copyright (c) 2021, tevador <tevador@gmail.com>
++//
++// All rights reserved.
++//
++// Redistribution and use in source and binary forms, with or without
++// modification, are permitted provided that the following conditions
++// are met:
++// 1. Redistributions of source code must retain the above copyright
++// notice, this list of conditions and the following disclaimer.
++// 2. Redistributions in binary form must reproduce the above copyright
++// notice, this list of conditions and the following disclaimer in the
++// documentation and/or other materials provided with the distribution.
++//
++// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
++// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
++// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
++// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
++// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
++// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
++// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
++// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
++// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
++// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
++// SUCH DAMAGE.
++
++#ifndef PBKDF2_H
++#define PBKDF2_H
++
++#include <stddef.h>
++#include <stdint.h>
++
++#ifdef __cplusplus
++extern "C" {
++#endif
++
++void
++crypto_pbkdf2_sha256(const uint8_t* passwd, size_t passwdlen,
++ const uint8_t* salt, size_t saltlen, uint64_t c,
++ uint8_t* buf, size_t dkLen);
++
++#ifdef __cplusplus
++}
++#endif
++
++#endif
+\ No newline at end of file
+diff --git a/src/polyseed/polyseed.cpp b/src/polyseed/polyseed.cpp
+new file mode 100644
+index 000000000..b26f37574
+--- /dev/null
++++ b/src/polyseed/polyseed.cpp
+@@ -0,0 +1,182 @@
++// Copyright (c) 2023, The Monero Project
++// Copyright (c) 2021, tevador <tevador@gmail.com>
++//
++// All rights reserved.
++//
++// Redistribution and use in source and binary forms, with or without
++// modification, are permitted provided that the following conditions
++// are met:
++// 1. Redistributions of source code must retain the above copyright
++// notice, this list of conditions and the following disclaimer.
++// 2. Redistributions in binary form must reproduce the above copyright
++// notice, this list of conditions and the following disclaimer in the
++// documentation and/or other materials provided with the distribution.
++//
++// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
++// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
++// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
++// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
++// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
++// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
++// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
++// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
++// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
++// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
++// SUCH DAMAGE.
++
++#include "polyseed.hpp"
++#include "pbkdf2.h"
++
++#include <sodium/core.h>
++#include <sodium/utils.h>
++#include <sodium/randombytes.h>
++#include <utf8proc.h>
++
++#include <cstring>
++#include <algorithm>
++#include <array>
++
++namespace polyseed {
++
++ inline size_t utf8_norm(const char* str, polyseed_str norm, utf8proc_option_t options) {
++ utf8proc_int32_t buffer[POLYSEED_STR_SIZE];
++ utf8proc_ssize_t result;
++
++ result = utf8proc_decompose(reinterpret_cast<const uint8_t*>(str), 0, buffer, POLYSEED_STR_SIZE, options);
++ if (result < 0) {
++ return POLYSEED_STR_SIZE;
++ }
++ if (result > POLYSEED_STR_SIZE - 1) {
++ return result;
++ }
++
++ result = utf8proc_reencode(buffer, result, options);
++
++ strcpy(norm, reinterpret_cast<const char*>(buffer));
++ sodium_memzero(buffer, POLYSEED_STR_SIZE);
++ return result;
++ }
++
++ static size_t utf8_nfc(const char* str, polyseed_str norm) {
++ // Note: UTF8PROC_LUMP is used here to replace the ideographic space with a regular space for Japanese phrases
++ // to allow wallets to split on ' '.
++ return utf8_norm(str, norm, (utf8proc_option_t)(UTF8PROC_NULLTERM | UTF8PROC_STABLE | UTF8PROC_COMPOSE | UTF8PROC_STRIPNA | UTF8PROC_LUMP));
++ }
++
++ static size_t utf8_nfkd(const char* str, polyseed_str norm) {
++ return utf8_norm(str, norm, (utf8proc_option_t)(UTF8PROC_NULLTERM | UTF8PROC_STABLE | UTF8PROC_DECOMPOSE | UTF8PROC_COMPAT | UTF8PROC_STRIPNA));
++ }
++
++ struct dependency {
++ dependency();
++ std::vector<language> languages;
++ };
++
++ static dependency deps;
++
++ dependency::dependency() {
++ if (sodium_init() == -1) {
++ throw std::runtime_error("sodium_init failed");
++ }
++
++ polyseed_dependency pd;
++ pd.randbytes = &randombytes_buf;
++ pd.pbkdf2_sha256 = &crypto_pbkdf2_sha256;
++ pd.memzero = &sodium_memzero;
++ pd.u8_nfc = &utf8_nfc;
++ pd.u8_nfkd = &utf8_nfkd;
++ pd.time = nullptr;
++ pd.alloc = nullptr;
++ pd.free = nullptr;
++
++ polyseed_inject(&pd);
++
++ for (int i = 0; i < polyseed_get_num_langs(); ++i) {
++ languages.push_back(language(polyseed_get_lang(i)));
++ }
++ }
++
++ static language invalid_lang;
++
++ const std::vector<language>& get_langs() {
++ return deps.languages;
++ }
++
++ const language& get_lang_by_name(const std::string& name) {
++ for (auto& lang : deps.languages) {
++ if (name == lang.name_en()) {
++ return lang;
++ }
++ if (name == lang.name()) {
++ return lang;
++ }
++ }
++ return invalid_lang;
++ }
++
++ inline void data::check_init() const {
++ if (valid()) {
++ throw std::runtime_error("already initialized");
++ }
++ }
++
++ static std::array<const char*, 8> error_desc = {
++ "Success",
++ "Wrong number of words in the phrase",
++ "Unknown language or unsupported words",
++ "Checksum mismatch",
++ "Unsupported seed features",
++ "Invalid seed format",
++ "Memory allocation failure",
++ "Unicode normalization failed"
++ };
++
++ static error get_error(polyseed_status status) {
++ if (status > 0 && status < sizeof(error_desc) / sizeof(const char*)) {
++ return error(error_desc[(int)status], status);
++ }
++ return error("Unknown error", status);
++ }
++
++ void data::create(feature_type features) {
++ check_init();
++ auto status = polyseed_create(features, &m_data);
++ if (status != POLYSEED_OK) {
++ throw get_error(status);
++ }
++ }
++
++ void data::split(const language& lang, polyseed_phrase& words) {
++ check_init();
++ if (!lang.valid()) {
++ throw std::runtime_error("invalid language");
++ }
++ }
++
++ void data::load(polyseed_storage storage) {
++ check_init();
++ auto status = polyseed_load(storage, &m_data);
++ if (status != POLYSEED_OK) {
++ throw get_error(status);
++ }
++ }
++
++ void data::load(const crypto::secret_key &key) {
++ polyseed_storage d;
++ memcpy(&d, &key.data, 32);
++ auto status = polyseed_load(d, &m_data);
++ if (status != POLYSEED_OK) {
++ throw get_error(status);
++ }
++ }
++
++ language data::decode(const char* phrase) {
++ check_init();
++ const polyseed_lang* lang;
++ auto status = polyseed_decode(phrase, m_coin, &lang, &m_data);
++ if (status != POLYSEED_OK) {
++ throw get_error(status);
++ }
++ return language(lang);
++ }
++}
+diff --git a/src/polyseed/polyseed.hpp b/src/polyseed/polyseed.hpp
+new file mode 100644
+index 000000000..2c8c777a7
+--- /dev/null
++++ b/src/polyseed/polyseed.hpp
+@@ -0,0 +1,167 @@
++// Copyright (c) 2023, The Monero Project
++// Copyright (c) 2021, tevador <tevador@gmail.com>
++//
++// All rights reserved.
++//
++// Redistribution and use in source and binary forms, with or without
++// modification, are permitted provided that the following conditions
++// are met:
++// 1. Redistributions of source code must retain the above copyright
++// notice, this list of conditions and the following disclaimer.
++// 2. Redistributions in binary form must reproduce the above copyright
++// notice, this list of conditions and the following disclaimer in the
++// documentation and/or other materials provided with the distribution.
++//
++// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
++// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
++// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
++// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
++// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
++// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
++// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
++// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
++// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
++// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
++// SUCH DAMAGE.
++
++#ifndef POLYSEED_HPP
++#define POLYSEED_HPP
++
++#include <polyseed/include/polyseed.h>
++#include <polyseed/src/lang.h>
++#include <vector>
++#include <stdexcept>
++#include <string>
++#include "crypto/crypto.h"
++
++namespace polyseed {
++
++ class data;
++
++ class language {
++ public:
++ language() : m_lang(nullptr) {}
++ language(const language&) = default;
++ language(const polyseed_lang* lang) : m_lang(lang) {}
++ const char* name() const {
++ return polyseed_get_lang_name(m_lang);
++ }
++ const char* name_en() const {
++ return polyseed_get_lang_name_en(m_lang);
++ }
++ const char* separator() const {
++ return m_lang->separator;
++ }
++ bool valid() const {
++ return m_lang != nullptr;
++ }
++
++ const polyseed_lang* m_lang;
++ private:
++
++ friend class data;
++ };
++
++ const std::vector<language>& get_langs();
++ const language& get_lang_by_name(const std::string& name);
++
++ class error : public std::runtime_error {
++ public:
++ error(const char* msg, polyseed_status status)
++ : std::runtime_error(msg), m_status(status)
++ {
++ }
++ polyseed_status status() const {
++ return m_status;
++ }
++ private:
++ polyseed_status m_status;
++ };
++
++ using feature_type = unsigned int;
++
++ inline int enable_features(feature_type features) {
++ return polyseed_enable_features(features);
++ }
++
++ class data {
++ public:
++ data(const data&) = delete;
++ data(polyseed_coin coin) : m_data(nullptr), m_coin(coin) {}
++ ~data() {
++ polyseed_free(m_data);
++ }
++
++ void create(feature_type features);
++
++ void load(polyseed_storage storage);
++
++ void load(const crypto::secret_key &key);
++
++ language decode(const char* phrase);
++
++ template<class str_type>
++ void encode(const language& lang, str_type& str) const {
++ check_valid();
++ if (!lang.valid()) {
++ throw std::runtime_error("invalid language");
++ }
++ str.resize(POLYSEED_STR_SIZE);
++ auto size = polyseed_encode(m_data, lang.m_lang, m_coin, &str[0]);
++ str.resize(size);
++ }
++
++ void split(const language& lang, polyseed_phrase& words);
++
++ void save(polyseed_storage storage) const {
++ check_valid();
++ polyseed_store(m_data, storage);
++ }
++
++ void save(void *storage) const {
++ check_valid();
++ polyseed_store(m_data, (uint8_t*)storage);
++ }
++
++ void crypt(const char* password) {
++ check_valid();
++ polyseed_crypt(m_data, password);
++ }
++
++ void keygen(void* ptr, size_t key_size) const {
++ check_valid();
++ polyseed_keygen(m_data, m_coin, key_size, (uint8_t*)ptr);
++ }
++
++ bool valid() const {
++ return m_data != nullptr;
++ }
++
++ bool encrypted() const {
++ check_valid();
++ return polyseed_is_encrypted(m_data);
++ }
++
++ uint64_t birthday() const {
++ check_valid();
++ return polyseed_get_birthday(m_data);
++ }
++
++ bool has_feature(feature_type feature) const {
++ check_valid();
++ return polyseed_get_feature(m_data, feature) != 0;
++ }
++ private:
++ void check_valid() const {
++ if (m_data == nullptr) {
++ throw std::runtime_error("invalid object");
++ }
++ }
++ void check_init() const;
++
++ polyseed_data* m_data;
++ polyseed_coin m_coin;
++ };
++}
++
++#endif //POLYSEED_HPP
+\ No newline at end of file
+diff --git a/src/wallet/api/wallet.cpp b/src/wallet/api/wallet.cpp
+index 8d7364cba..472f05016 100644
+--- a/src/wallet/api/wallet.cpp
++++ b/src/wallet/api/wallet.cpp
+@@ -690,6 +690,28 @@ bool WalletImpl::recoverFromDevice(const std::string &path, const std::string &p
+ return true;
+ }
+
++bool WalletImpl::createFromPolyseed(const std::string &path, const std::string &password, const std::string &seed,
++ const std::string &passphrase, bool newWallet, uint64_t restoreHeight)
++{
++ clearStatus();
++ m_recoveringFromSeed = !newWallet;
++ m_recoveringFromDevice = false;
++
++ polyseed::data polyseed(POLYSEED_COIN);
++
++ try {
++ auto lang = polyseed.decode(seed.data());
++ m_wallet->set_seed_language(lang.name());
++ m_wallet->generate(path, password, polyseed, passphrase, !newWallet);
++ }
++ catch (const std::exception &e) {
++ setStatusError(e.what());
++ return false;
++ }
++
++ return true;
++}
++
+ Wallet::Device WalletImpl::getDeviceType() const
+ {
+ return static_cast<Wallet::Device>(m_wallet->get_device_type());
+@@ -798,6 +820,55 @@ std::string WalletImpl::seed(const std::string& seed_offset) const
+ return std::string(seed.data(), seed.size()); // TODO
+ }
+
++bool WalletImpl::getPolyseed(std::string &seed_words, std::string &passphrase) const
++{
++ epee::wipeable_string seed_words_epee(seed_words.c_str(), seed_words.size());
++ epee::wipeable_string passphrase_epee(passphrase.c_str(), passphrase.size());
++ clearStatus();
++
++ if (!m_wallet) {
++ return false;
++ }
++
++ bool result = m_wallet->get_polyseed(seed_words_epee, passphrase_epee);
++
++ seed_words.assign(seed_words_epee.data(), seed_words_epee.size());
++ passphrase.assign(passphrase_epee.data(), passphrase_epee.size());
++
++ return result;
++}
++
++std::vector<std::pair<std::string, std::string>> Wallet::getPolyseedLanguages()
++{
++ std::vector<std::pair<std::string, std::string>> languages;
++
++ auto langs = polyseed::get_langs();
++ for (const auto &lang : langs) {
++ languages.emplace_back(std::pair<std::string, std::string>(lang.name_en(), lang.name()));
++ }
++
++ return languages;
++}
++
++bool Wallet::createPolyseed(std::string &seed_words, std::string &err, const std::string &language)
++{
++ epee::wipeable_string seed_words_epee(seed_words.c_str(), seed_words.size());
++
++ try {
++ polyseed::data polyseed(POLYSEED_COIN);
++ polyseed.create(0);
++ polyseed.encode(polyseed::get_lang_by_name(language), seed_words_epee);
++
++ seed_words.assign(seed_words_epee.data(), seed_words_epee.size());
++ }
++ catch (const std::exception &e) {
++ err = e.what();
++ return false;
++ }
++
++ return true;
++}
++
+ std::string WalletImpl::getSeedLanguage() const
+ {
+ return m_wallet->get_seed_language();
+diff --git a/src/wallet/api/wallet.h b/src/wallet/api/wallet.h
+index ec2d7e9b3..787215ab3 100644
+--- a/src/wallet/api/wallet.h
++++ b/src/wallet/api/wallet.h
+@@ -79,9 +79,19 @@ public:
+ bool recoverFromDevice(const std::string &path,
+ const std::string &password,
+ const std::string &device_name);
++
++ bool createFromPolyseed(const std::string &path,
++ const std::string &password,
++ const std::string &seed,
++ const std::string &passphrase = "",
++ bool newWallet = true,
++ uint64_t restoreHeight = 0);
++
+ Device getDeviceType() const override;
+ bool close(bool store = true);
+ std::string seed(const std::string& seed_offset = "") const override;
++ bool getPolyseed(std::string &seed_words, std::string &passphrase) const override;
++
+ std::string getSeedLanguage() const override;
+ void setSeedLanguage(const std::string &arg) override;
+ // void setListener(Listener *) {}
+diff --git a/src/wallet/api/wallet2_api.h b/src/wallet/api/wallet2_api.h
+index 71991df0d..9ea753083 100644
+--- a/src/wallet/api/wallet2_api.h
++++ b/src/wallet/api/wallet2_api.h
+@@ -700,6 +700,10 @@ struct Wallet
+ static void warning(const std::string &category, const std::string &str);
+ static void error(const std::string &category, const std::string &str);
+
++ virtual bool getPolyseed(std::string &seed, std::string &passphrase) const = 0;
++ static bool createPolyseed(std::string &seed_words, std::string &err, const std::string &language = "English");
++ static std::vector<std::pair<std::string, std::string>> getPolyseedLanguages();
++
+ /**
+ * @brief StartRefresh - Start/resume refresh thread (refresh every 10 seconds)
+ */
+@@ -1256,6 +1260,27 @@ struct WalletManager
+ uint64_t kdf_rounds = 1,
+ WalletListener * listener = nullptr) = 0;
+
++ /*!
++ * \brief creates a wallet from a polyseed mnemonic phrase
++ * \param path Name of the wallet file to be created
++ * \param password Password of wallet file
++ * \param nettype Network type
++ * \param mnemonic Polyseed mnemonic
++ * \param passphrase Optional seed offset passphrase
++ * \param newWallet Whether it is a new wallet
++ * \param restoreHeight Override the embedded restore height
++ * \param kdf_rounds Number of rounds for key derivation function
++ * @return
++ */
++ virtual Wallet * createWalletFromPolyseed(const std::string &path,
++ const std::string &password,
++ NetworkType nettype,
++ const std::string &mnemonic,
++ const std::string &passphrase = "",
++ bool newWallet = true,
++ uint64_t restore_height = 0,
++ uint64_t kdf_rounds = 1) = 0;
++
+ /*!
+ * \brief Closes wallet. In case operation succeeded, wallet object deleted. in case operation failed, wallet object not deleted
+ * \param wallet previously opened / created wallet instance
+diff --git a/src/wallet/api/wallet_manager.cpp b/src/wallet/api/wallet_manager.cpp
+index e81b8f83a..c79fe25d6 100644
+--- a/src/wallet/api/wallet_manager.cpp
++++ b/src/wallet/api/wallet_manager.cpp
+@@ -156,6 +156,15 @@ Wallet *WalletManagerImpl::createWalletFromDevice(const std::string &path,
+ return wallet;
+ }
+
++Wallet *WalletManagerImpl::createWalletFromPolyseed(const std::string &path, const std::string &password, NetworkType nettype,
++ const std::string &mnemonic, const std::string &passphrase,
++ bool newWallet, uint64_t restoreHeight, uint64_t kdf_rounds)
++{
++ WalletImpl * wallet = new WalletImpl(nettype, kdf_rounds);
++ wallet->createFromPolyseed(path, password, mnemonic, passphrase, newWallet, restoreHeight);
++ return wallet;
++}
++
+ bool WalletManagerImpl::closeWallet(Wallet *wallet, bool store)
+ {
+ WalletImpl * wallet_ = dynamic_cast<WalletImpl*>(wallet);
+diff --git a/src/wallet/api/wallet_manager.h b/src/wallet/api/wallet_manager.h
+index a223e1df9..28fcd36c9 100644
+--- a/src/wallet/api/wallet_manager.h
++++ b/src/wallet/api/wallet_manager.h
+@@ -75,6 +75,16 @@ public:
+ const std::string &subaddressLookahead = "",
+ uint64_t kdf_rounds = 1,
+ WalletListener * listener = nullptr) override;
++
++ virtual Wallet * createWalletFromPolyseed(const std::string &path,
++ const std::string &password,
++ NetworkType nettype,
++ const std::string &mnemonic,
++ const std::string &passphrase,
++ bool newWallet = true,
++ uint64_t restore_height = 0,
++ uint64_t kdf_rounds = 1) override;
++
+ virtual bool closeWallet(Wallet *wallet, bool store = true) override;
+ bool walletExists(const std::string &path) override;
+ bool verifyWalletPassword(const std::string &keys_file_name, const std::string &password, bool no_spend_key, uint64_t kdf_rounds = 1) const override;
+diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp
+index f34b10988..e4e02c782 100644
+--- a/src/wallet/wallet2.cpp
++++ b/src/wallet/wallet2.cpp
+@@ -92,6 +92,7 @@ using namespace epee;
+ #include "device/device_cold.hpp"
+ #include "device_trezor/device_trezor.hpp"
+ #include "net/socks_connect.h"
++#include "polyseed/include/polyseed.h"
+
+ extern "C"
+ {
+@@ -1260,7 +1261,8 @@ wallet2::wallet2(network_type nettype, uint64_t kdf_rounds, bool unattended, std
+ m_enable_multisig(false),
+ m_pool_info_query_time(0),
+ m_has_ever_refreshed_from_node(false),
+- m_allow_mismatched_daemon_version(false)
++ m_allow_mismatched_daemon_version(false),
++ m_polyseed(false)
+ {
+ set_rpc_client_secret_key(rct::rct2sk(rct::skGen()));
+ }
+@@ -1438,10 +1440,25 @@ bool wallet2::get_seed(epee::wipeable_string& electrum_words, const epee::wipeab
+ key = cryptonote::encrypt_key(key, passphrase);
+ if (!crypto::ElectrumWords::bytes_to_words(key, electrum_words, seed_language))
+ {
+- std::cout << "Failed to create seed from key for language: " << seed_language << std::endl;
++ std::cout << "Failed to create seed from key for language: " << seed_language << ", falling back to English." << std::endl;
++ crypto::ElectrumWords::bytes_to_words(key, electrum_words, "English");
++ }
++
++ return true;
++}
++//----------------------------------------------------------------------------------------------------
++bool wallet2::get_polyseed(epee::wipeable_string& polyseed, epee::wipeable_string& passphrase) const
++{
++ if (!m_polyseed) {
+ return false;
+ }
+
++ polyseed::data data(POLYSEED_COIN);
++ data.load(get_account().get_keys().m_polyseed);
++ data.encode(polyseed::get_lang_by_name(seed_language), polyseed);
++
++ passphrase = get_account().get_keys().m_passphrase;
++
+ return true;
+ }
+ //----------------------------------------------------------------------------------------------------
+@@ -4629,6 +4646,9 @@ 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());
+
++ value2.SetInt(m_polyseed ? 1 : 0);
++ json.AddMember("polyseed", value2, json.GetAllocator());
++
+ // Serialize the JSON object
+ rapidjson::StringBuffer buffer;
+ rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
+@@ -4776,6 +4796,7 @@ bool wallet2::load_keys_buf(const std::string& keys_buf, const epee::wipeable_st
+ m_credits_target = 0;
+ m_enable_multisig = false;
+ m_allow_mismatched_daemon_version = false;
++ m_polyseed = false;
+ }
+ else if(json.IsObject())
+ {
+@@ -5012,6 +5033,8 @@ bool wallet2::load_keys_buf(const std::string& keys_buf, const epee::wipeable_st
+ m_credits_target = field_credits_target;
+ 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, polyseed, int, Int, false, false);
++ m_polyseed = field_polyseed;
+ }
+ else
+ {
+@@ -5284,6 +5307,48 @@ void wallet2::init_type(hw::device::device_type device_type)
+ m_key_device_type = device_type;
+ }
+
++/*!
++ * \brief Generates a polyseed wallet or restores one.
++ * \param wallet_ Name of wallet file
++ * \param password Password of wallet file
++ * \param passphrase Seed offset passphrase
++ * \param recover Whether it is a restore
++ * \param seed_words If it is a restore, the polyseed
++ * \param create_address_file Whether to create an address file
++ * \return The secret key of the generated wallet
++ */
++void wallet2::generate(const std::string& wallet_, const epee::wipeable_string& password,
++ const polyseed::data &seed, const epee::wipeable_string& passphrase, bool recover, uint64_t restoreHeight, bool create_address_file)
++{
++ clear();
++ prepare_file_names(wallet_);
++
++ if (!wallet_.empty()) {
++ boost::system::error_code ignored_ec;
++ THROW_WALLET_EXCEPTION_IF(boost::filesystem::exists(m_wallet_file, ignored_ec), error::file_exists, m_wallet_file);
++ THROW_WALLET_EXCEPTION_IF(boost::filesystem::exists(m_keys_file, ignored_ec), error::file_exists, m_keys_file);
++ }
++
++ m_account.create_from_polyseed(seed, passphrase);
++
++ init_type(hw::device::device_type::SOFTWARE);
++ m_polyseed = true;
++ setup_keys(password);
++
++ if (recover) {
++ m_refresh_from_block_height = estimate_blockchain_height(restoreHeight > 0 ? restoreHeight : seed.birthday());
++ } else {
++ m_refresh_from_block_height = estimate_blockchain_height();
++ }
++
++ create_keys_file(wallet_, false, password, m_nettype != MAINNET || create_address_file);
++
++ setup_new_blockchain();
++
++ if (!wallet_.empty())
++ store();
++}
++
+ /*!
+ * \brief Generates a wallet or restores one. Assumes the multisig setup
+ * has already completed for the provided multisig info.
+@@ -5411,7 +5476,7 @@ crypto::secret_key wallet2::generate(const std::string& wallet_, const epee::wip
+ return retval;
+ }
+
+- uint64_t wallet2::estimate_blockchain_height()
++ uint64_t wallet2::estimate_blockchain_height(uint64_t time)
+ {
+ // -1 month for fluctuations in block time and machine date/time setup.
+ // avg seconds per block
+@@ -5435,7 +5500,7 @@ crypto::secret_key wallet2::generate(const std::string& wallet_, const epee::wip
+ // the daemon is currently syncing.
+ // If we use the approximate height we subtract one month as
+ // a safety margin.
+- height = get_approximate_blockchain_height();
++ height = get_approximate_blockchain_height(time);
+ uint64_t target_height = get_daemon_blockchain_target_height(err);
+ if (err.empty()) {
+ if (target_height < height)
+@@ -13135,7 +13200,7 @@ uint64_t wallet2::get_daemon_blockchain_target_height(string &err)
+ return target_height;
+ }
+
+-uint64_t wallet2::get_approximate_blockchain_height() const
++uint64_t wallet2::get_approximate_blockchain_height(uint64_t t) const
+ {
+ // time of v2 fork
+ const time_t fork_time = m_nettype == TESTNET ? 1448285909 : m_nettype == STAGENET ? 1520937818 : 1458748658;
+@@ -13144,7 +13209,7 @@ uint64_t wallet2::get_approximate_blockchain_height() const
+ // avg seconds per block
+ const int seconds_per_block = DIFFICULTY_TARGET_V2;
+ // Calculated blockchain height
+- uint64_t approx_blockchain_height = fork_block + (time(NULL) - fork_time)/seconds_per_block;
++ uint64_t approx_blockchain_height = fork_block + ((t > 0 ? t : time(NULL)) - fork_time)/seconds_per_block;
+ // testnet and stagenet got some huge rollbacks, so the estimation is way off
+ static const uint64_t approximate_rolled_back_blocks = m_nettype == TESTNET ? 342100 : 30000;
+ if ((m_nettype == TESTNET || m_nettype == STAGENET) && approx_blockchain_height > approximate_rolled_back_blocks)
+@@ -14862,6 +14927,21 @@ bool wallet2::parse_uri(const std::string &uri, std::string &address, std::strin
+ //----------------------------------------------------------------------------------------------------
+ uint64_t wallet2::get_blockchain_height_by_date(uint16_t year, uint8_t month, uint8_t day)
+ {
++ std::tm date = { 0, 0, 0, 0, 0, 0, 0, 0 };
++ date.tm_year = year - 1900;
++ date.tm_mon = month - 1;
++ date.tm_mday = day;
++ if (date.tm_mon < 0 || 11 < date.tm_mon || date.tm_mday < 1 || 31 < date.tm_mday)
++ {
++ throw std::runtime_error("month or day out of range");
++ }
++
++ uint64_t timestamp_target = std::mktime(&date);
++
++ return get_blockchain_height_by_timestamp(timestamp_target);
++}
++
++uint64_t wallet2::get_blockchain_height_by_timestamp(uint64_t timestamp_target) {
+ uint32_t version;
+ if (!check_connection(&version))
+ {
+@@ -14871,15 +14951,7 @@ uint64_t wallet2::get_blockchain_height_by_date(uint16_t year, uint8_t month, ui
+ {
+ throw std::runtime_error("this function requires RPC version 1.6 or higher");
+ }
+- std::tm date = { 0, 0, 0, 0, 0, 0, 0, 0 };
+- date.tm_year = year - 1900;
+- date.tm_mon = month - 1;
+- date.tm_mday = day;
+- if (date.tm_mon < 0 || 11 < date.tm_mon || date.tm_mday < 1 || 31 < date.tm_mday)
+- {
+- throw std::runtime_error("month or day out of range");
+- }
+- uint64_t timestamp_target = std::mktime(&date);
++
+ std::string err;
+ uint64_t height_min = 0;
+ uint64_t height_max = get_daemon_blockchain_height(err) - 1;
+diff --git a/src/wallet/wallet2.h b/src/wallet/wallet2.h
+index 3144a8fd3..b540eff6b 100644
+--- a/src/wallet/wallet2.h
++++ b/src/wallet/wallet2.h
+@@ -72,6 +72,7 @@
+ #include "message_store.h"
+ #include "wallet_light_rpc.h"
+ #include "wallet_rpc_helpers.h"
++#include "polyseed/polyseed.hpp"
+
+ #undef MONERO_DEFAULT_LOG_CATEGORY
+ #define MONERO_DEFAULT_LOG_CATEGORY "wallet.wallet2"
+@@ -854,6 +855,20 @@ private:
+ void generate(const std::string& wallet_, const epee::wipeable_string& password,
+ const epee::wipeable_string& multisig_data, bool create_address_file = false);
+
++ /*!
++ * \brief Generates a wallet from a polyseed.
++ * @param wallet_ Name of wallet file
++ * @param password Password of wallet file
++ * @param seed Polyseed data
++ * @param passphrase Optional seed offset passphrase
++ * @param recover Whether it is a restore
++ * @param restoreHeight Override the embedded restore height
++ * @param create_address_file Whether to create an address file
++ */
++ void generate(const std::string& wallet_, const epee::wipeable_string& password,
++ const polyseed::data &seed, const epee::wipeable_string& passphrase = "",
++ bool recover = false, uint64_t restoreHeight = 0, bool create_address_file = false);
++
+ /*!
+ * \brief Generates a wallet or restores one.
+ * \param wallet_ Name of wallet file
+@@ -1018,6 +1033,15 @@ private:
+ bool is_deterministic() const;
+ bool get_seed(epee::wipeable_string& electrum_words, const epee::wipeable_string &passphrase = epee::wipeable_string()) const;
+
++ /*!
++ * \brief get_polyseed Gets the polyseed (if available) and passphrase (if set) needed to recover the wallet.
++ * @param seed Polyseed mnemonic phrase
++ * @param passphrase Seed offset passphrase that was used to restore the wallet
++ * @return Returns true if the wallet has a polyseed.
++ * Note: both the mnemonic phrase and the passphrase are needed to recover the wallet
++ */
++ bool get_polyseed(epee::wipeable_string& seed, epee::wipeable_string &passphrase) const;
++
+ /*!
+ * \brief Checks if light wallet. A light wallet sends view key to a server where the blockchain is scanned.
+ */
+@@ -1466,8 +1490,8 @@ private:
+ /*!
+ * \brief Calculates the approximate blockchain height from current date/time.
+ */
+- uint64_t get_approximate_blockchain_height() const;
+- uint64_t estimate_blockchain_height();
++ uint64_t get_approximate_blockchain_height(uint64_t time = 0) const;
++ uint64_t estimate_blockchain_height(uint64_t time = 0);
+ std::vector<size_t> select_available_outputs_from_histogram(uint64_t count, bool atleast, bool unlocked, bool allow_rct);
+ std::vector<size_t> select_available_outputs(const std::function<bool(const transfer_details &td)> &f);
+ std::vector<size_t> select_available_unmixable_outputs();
+@@ -1559,6 +1583,7 @@ private:
+ bool parse_uri(const std::string &uri, std::string &address, std::string &payment_id, uint64_t &amount, std::string &tx_description, std::string &recipient_name, std::vector<std::string> &unknown_parameters, std::string &error);
+
+ uint64_t get_blockchain_height_by_date(uint16_t year, uint8_t month, uint8_t day); // 1<=month<=12, 1<=day<=31
++ uint64_t get_blockchain_height_by_timestamp(uint64_t timestamp);
+
+ bool is_synced();
+
+@@ -1874,6 +1899,7 @@ private:
+ std::string seed_language; /*!< Language of the mnemonics (seed). */
+ bool is_old_file_format; /*!< Whether the wallet file is of an old file format */
+ bool m_watch_only; /*!< no spend key */
++ bool m_polyseed;
+ bool m_multisig; /*!< if > 1 spend secret key will not match spend public key */
+ uint32_t m_multisig_threshold;
+ std::vector<crypto::public_key> m_multisig_signers;
diff --git a/patches/monero/0001-background-sync.patch b/patches/monero/0001-background-sync.patch
new file mode 100644
index 0000000..63e95cb
--- /dev/null
+++ b/patches/monero/0001-background-sync.patch
@@ -0,0 +1,4172 @@
+From c7da75fee9bea42a6a741905cf6212db5ea8c54b Mon Sep 17 00:00:00 2001
+From: Czarek Nakamoto <cyjan@mrcyjanek.net>
+Date: Tue, 12 Mar 2024 10:02:23 +0100
+Subject: [PATCH] PATCH: background-sync
+
+---
+ src/cryptonote_basic/account.cpp | 11 +
+ src/cryptonote_basic/account.h | 1 +
+ src/cryptonote_config.h | 3 +
+ src/simplewallet/simplewallet.cpp | 205 +++-
+ src/simplewallet/simplewallet.h | 1 +
+ src/wallet/api/wallet.cpp | 213 ++++-
+ src/wallet/api/wallet.h | 12 +
+ src/wallet/api/wallet2_api.h | 42 +
+ src/wallet/wallet2.cpp | 948 ++++++++++++++++++-
+ src/wallet/wallet2.h | 142 ++-
+ src/wallet/wallet_errors.h | 39 +
+ src/wallet/wallet_rpc_server.cpp | 162 ++++
+ src/wallet/wallet_rpc_server.h | 6 +
+ src/wallet/wallet_rpc_server_commands_defs.h | 64 ++
+ src/wallet/wallet_rpc_server_error_codes.h | 2 +
+ tests/functional_tests/transfer.py | 401 +++++++-
+ tests/functional_tests/util_resources.py | 25 +
+ tests/functional_tests/wallet.py | 43 +-
+ tests/unit_tests/wipeable_string.cpp | 12 +
+ utils/python-rpc/framework/wallet.py | 35 +
+ 20 files changed, 2281 insertions(+), 86 deletions(-)
+
+diff --git a/src/cryptonote_basic/account.cpp b/src/cryptonote_basic/account.cpp
+index 4931c3740..2d556f285 100644
+--- a/src/cryptonote_basic/account.cpp
++++ b/src/cryptonote_basic/account.cpp
+@@ -158,6 +158,17 @@ DISABLE_VS_WARNINGS(4244 4345)
+ m_keys.m_passphrase.wipe();
+ }
+ //-----------------------------------------------------------------
++ void account_base::set_spend_key(const crypto::secret_key& spend_secret_key)
++ {
++ // make sure derived spend public key matches saved public spend key
++ crypto::public_key spend_public_key;
++ crypto::secret_key_to_public_key(spend_secret_key, spend_public_key);
++ CHECK_AND_ASSERT_THROW_MES(m_keys.m_account_address.m_spend_public_key == spend_public_key,
++ "Unexpected derived public spend key");
++
++ m_keys.m_spend_secret_key = spend_secret_key;
++ }
++ //-----------------------------------------------------------------
+ crypto::secret_key account_base::generate(const crypto::secret_key& recovery_key, bool recover, bool two_random)
+ {
+ crypto::secret_key first = generate_keys(m_keys.m_account_address.m_spend_public_key, m_keys.m_spend_secret_key, recovery_key, recover);
+diff --git a/src/cryptonote_basic/account.h b/src/cryptonote_basic/account.h
+index 0099ebfe7..1f76febce 100644
+--- a/src/cryptonote_basic/account.h
++++ b/src/cryptonote_basic/account.h
+@@ -101,6 +101,7 @@ namespace cryptonote
+ bool store(const std::string& file_path);
+
+ void forget_spend_key();
++ void set_spend_key(const crypto::secret_key& spend_secret_key);
+ const std::vector<crypto::secret_key> &get_multisig_keys() const { return m_keys.m_multisig_keys; }
+
+ void encrypt_keys(const crypto::chacha_key &key) { m_keys.encrypt(key); }
+diff --git a/src/cryptonote_config.h b/src/cryptonote_config.h
+index 8e1a07110..cf50b2adc 100644
+--- a/src/cryptonote_config.h
++++ b/src/cryptonote_config.h
+@@ -233,6 +233,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";
+
+ // Hash domain separators
+ const char HASH_KEY_BULLETPROOF_EXPONENT[] = "bulletproof";
+@@ -243,6 +244,8 @@ namespace config
+ const unsigned char HASH_KEY_ENCRYPTED_PAYMENT_ID = 0x8d;
+ const unsigned char HASH_KEY_WALLET = 0x8c;
+ const unsigned char HASH_KEY_WALLET_CACHE = 0x8d;
++ const unsigned char HASH_KEY_BACKGROUND_CACHE = 0x8e;
++ const unsigned char HASH_KEY_BACKGROUND_KEYS_FILE = 0x8f;
+ const unsigned char HASH_KEY_RPC_PAYMENT_NONCE = 0x58;
+ const unsigned char HASH_KEY_MEMORY = 'k';
+ const unsigned char HASH_KEY_MULTISIG[] = {'M', 'u', 'l', 't' , 'i', 's', 'i', 'g', 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
+diff --git a/src/simplewallet/simplewallet.cpp b/src/simplewallet/simplewallet.cpp
+index f2eaf6b13..341b0e448 100644
+--- a/src/simplewallet/simplewallet.cpp
++++ b/src/simplewallet/simplewallet.cpp
+@@ -155,6 +155,17 @@ typedef cryptonote::simple_wallet sw;
+ } \
+ } while(0)
+
++#define CHECK_IF_BACKGROUND_SYNCING(msg) \
++ do \
++ { \
++ if (m_wallet->is_background_wallet() || m_wallet->is_background_syncing()) \
++ { \
++ std::string type = m_wallet->is_background_wallet() ? "background wallet" : "background syncing wallet"; \
++ fail_msg_writer() << boost::format(tr("%s %s")) % type % msg; \
++ return false; \
++ } \
++ } while (0)
++
+ enum TransferType {
+ Transfer,
+ TransferLocked,
+@@ -332,7 +343,7 @@ namespace
+ auto pwd_container = tools::password_container::prompt(verify, prompt);
+ if (!pwd_container)
+ {
+- tools::fail_msg_writer() << sw::tr("failed to read wallet password");
++ tools::fail_msg_writer() << sw::tr("failed to read password");
+ }
+ return pwd_container;
+ }
+@@ -342,6 +353,11 @@ namespace
+ return password_prompter(verify ? sw::tr("Enter a new password for the wallet") : sw::tr("Wallet password"), verify);
+ }
+
++ boost::optional<tools::password_container> background_sync_cache_password_prompter(bool verify)
++ {
++ return password_prompter(verify ? sw::tr("Enter a custom password for the background sync cache") : sw::tr("Background sync cache password"), verify);
++ }
++
+ inline std::string interpret_rpc_response(bool ok, const std::string& status)
+ {
+ std::string err;
+@@ -459,6 +475,41 @@ namespace
+ return "invalid";
+ }
+
++ const struct
++ {
++ const char *name;
++ tools::wallet2::BackgroundSyncType background_sync_type;
++ } background_sync_type_names[] =
++ {
++ { "off", tools::wallet2::BackgroundSyncOff },
++ { "reuse-wallet-password", tools::wallet2::BackgroundSyncReusePassword },
++ { "custom-background-password", tools::wallet2::BackgroundSyncCustomPassword },
++ };
++
++ bool parse_background_sync_type(const std::string &s, tools::wallet2::BackgroundSyncType &background_sync_type)
++ {
++ for (size_t n = 0; n < sizeof(background_sync_type_names) / sizeof(background_sync_type_names[0]); ++n)
++ {
++ if (s == background_sync_type_names[n].name)
++ {
++ background_sync_type = background_sync_type_names[n].background_sync_type;
++ return true;
++ }
++ }
++ fail_msg_writer() << cryptonote::simple_wallet::tr("failed to parse background sync type");
++ return false;
++ }
++
++ std::string get_background_sync_type_name(tools::wallet2::BackgroundSyncType type)
++ {
++ for (size_t n = 0; n < sizeof(background_sync_type_names) / sizeof(background_sync_type_names[0]); ++n)
++ {
++ if (type == background_sync_type_names[n].background_sync_type)
++ return background_sync_type_names[n].name;
++ }
++ return "invalid";
++ }
++
+ std::string get_version_string(uint32_t version)
+ {
+ return boost::lexical_cast<std::string>(version >> 16) + "." + boost::lexical_cast<std::string>(version & 0xffff);
+@@ -812,6 +863,7 @@ bool simple_wallet::spendkey(const std::vector<std::string> &args/* = std::vecto
+ fail_msg_writer() << tr("wallet is watch-only and has no spend key");
+ return true;
+ }
++ CHECK_IF_BACKGROUND_SYNCING("has no spend key");
+ // don't log
+ PAUSE_READLINE();
+ if (m_wallet->key_on_device()) {
+@@ -843,6 +895,7 @@ bool simple_wallet::print_seed(bool encrypted)
+ fail_msg_writer() << tr("wallet is watch-only and has no seed");
+ return true;
+ }
++ CHECK_IF_BACKGROUND_SYNCING("has no seed");
+
+ multisig = m_wallet->multisig(&ready);
+ if (multisig)
+@@ -920,6 +973,7 @@ bool simple_wallet::seed_set_language(const std::vector<std::string> &args/* = s
+ fail_msg_writer() << tr("wallet is watch-only and has no seed");
+ return true;
+ }
++ CHECK_IF_BACKGROUND_SYNCING("has no seed");
+
+ epee::wipeable_string password;
+ {
+@@ -1066,6 +1120,7 @@ bool simple_wallet::prepare_multisig_main(const std::vector<std::string> &args,
+ fail_msg_writer() << tr("wallet is watch-only and cannot be made multisig");
+ return false;
+ }
++ CHECK_IF_BACKGROUND_SYNCING("cannot be made multisig");
+
+ if(m_wallet->get_num_transfer_details())
+ {
+@@ -2202,6 +2257,7 @@ bool simple_wallet::save_known_rings(const std::vector<std::string> &args)
+
+ bool simple_wallet::freeze_thaw(const std::vector<std::string> &args, bool freeze)
+ {
++ CHECK_IF_BACKGROUND_SYNCING("cannot freeze/thaw");
+ if (args.empty())
+ {
+ fail_msg_writer() << boost::format(tr("usage: %s <key_image>|<pubkey>")) % (freeze ? "freeze" : "thaw");
+@@ -2241,6 +2297,7 @@ bool simple_wallet::thaw(const std::vector<std::string> &args)
+
+ bool simple_wallet::frozen(const std::vector<std::string> &args)
+ {
++ CHECK_IF_BACKGROUND_SYNCING("cannot see frozen key images");
+ if (args.empty())
+ {
+ size_t ntd = m_wallet->get_num_transfer_details();
+@@ -3012,6 +3069,56 @@ bool simple_wallet::set_track_uses(const std::vector<std::string> &args/* = std:
+ return true;
+ }
+
++bool simple_wallet::setup_background_sync(const std::vector<std::string> &args/* = std::vector<std::string>()*/)
++{
++ if (m_wallet->multisig())
++ {
++ fail_msg_writer() << tr("background sync not implemented for multisig wallet");
++ return true;
++ }
++ if (m_wallet->watch_only())
++ {
++ fail_msg_writer() << tr("background sync not implemented for watch only wallet");
++ return true;
++ }
++ if (m_wallet->key_on_device())
++ {
++ fail_msg_writer() << tr("command not supported by HW wallet");
++ return true;
++ }
++
++ const auto pwd_container = get_and_verify_password();
++ if (pwd_container)
++ {
++ tools::wallet2::BackgroundSyncType background_sync_type;
++ if (!parse_background_sync_type(args[1], background_sync_type))
++ {
++ fail_msg_writer() << tr("invalid option");
++ return true;
++ }
++
++ try
++ {
++ boost::optional<epee::wipeable_string> background_cache_password = boost::none;
++ if (background_sync_type == tools::wallet2::BackgroundSyncCustomPassword)
++ {
++ const auto background_pwd_container = background_sync_cache_password_prompter(true);
++ if (!background_pwd_container)
++ return true;
++ background_cache_password = background_pwd_container->password();
++ }
++
++ LOCK_IDLE_SCOPE();
++ m_wallet->setup_background_sync(background_sync_type, pwd_container->password(), background_cache_password);
++ }
++ catch (const std::exception &e)
++ {
++ fail_msg_writer() << tr("Error setting background sync type: ") << e.what();
++ }
++ }
++ return true;
++}
++
+ bool simple_wallet::set_show_wallet_name_when_locked(const std::vector<std::string> &args/* = std::vector<std::string>()*/)
+ {
+ const auto pwd_container = get_and_verify_password();
+@@ -3244,6 +3351,7 @@ bool simple_wallet::apropos(const std::vector<std::string> &args)
+
+ bool simple_wallet::scan_tx(const std::vector<std::string> &args)
+ {
++ CHECK_IF_BACKGROUND_SYNCING("cannot scan tx");
+ if (args.empty())
+ {
+ PRINT_USAGE(USAGE_SCAN_TX);
+@@ -3473,6 +3581,8 @@ simple_wallet::simple_wallet()
+ " Ignore outputs of amount below this threshold when spending.\n "
+ "track-uses <1|0>\n "
+ " Whether to keep track of owned outputs uses.\n "
++ "background-sync <off|reuse-wallet-password|custom-background-password>\n "
++ " Set this to enable scanning in the background with just the view key while the wallet is locked.\n "
+ "setup-background-mining <1|0>\n "
+ " Whether to enable background mining. Set this to support the network and to get a chance to receive new monero.\n "
+ "device-name <device_name[:device_spec]>\n "
+@@ -3891,6 +4001,7 @@ bool simple_wallet::set_variable(const std::vector<std::string> &args)
+ success_msg_writer() << "ignore-outputs-above = " << cryptonote::print_money(m_wallet->ignore_outputs_above());
+ success_msg_writer() << "ignore-outputs-below = " << cryptonote::print_money(m_wallet->ignore_outputs_below());
+ success_msg_writer() << "track-uses = " << m_wallet->track_uses();
++ success_msg_writer() << "background-sync = " << get_background_sync_type_name(m_wallet->background_sync_type());
+ success_msg_writer() << "setup-background-mining = " << setup_background_mining_string;
+ success_msg_writer() << "device-name = " << m_wallet->device_name();
+ success_msg_writer() << "export-format = " << (m_wallet->export_format() == tools::wallet2::ExportFormat::Ascii ? "ascii" : "binary");
+@@ -3909,6 +4020,7 @@ bool simple_wallet::set_variable(const std::vector<std::string> &args)
+ }
+ else
+ {
++ CHECK_IF_BACKGROUND_SYNCING("cannot change wallet settings");
+
+ #define CHECK_SIMPLE_VARIABLE(name, f, help) do \
+ if (args[0] == name) { \
+@@ -3962,6 +4074,7 @@ bool simple_wallet::set_variable(const std::vector<std::string> &args)
+ CHECK_SIMPLE_VARIABLE("ignore-outputs-above", set_ignore_outputs_above, tr("amount"));
+ CHECK_SIMPLE_VARIABLE("ignore-outputs-below", set_ignore_outputs_below, tr("amount"));
+ CHECK_SIMPLE_VARIABLE("track-uses", set_track_uses, tr("0 or 1"));
++ CHECK_SIMPLE_VARIABLE("background-sync", setup_background_sync, tr("off (default); 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)"));
+ CHECK_SIMPLE_VARIABLE("show-wallet-name-when-locked", set_show_wallet_name_when_locked, tr("1 or 0"));
+ CHECK_SIMPLE_VARIABLE("inactivity-lock-timeout", set_inactivity_lock_timeout, tr("unsigned integer (seconds, 0 to disable)"));
+ CHECK_SIMPLE_VARIABLE("setup-background-mining", set_setup_background_mining, tr("1/yes or 0/no"));
+@@ -4915,7 +5028,10 @@ std::string simple_wallet::get_mnemonic_language()
+ //----------------------------------------------------------------------------------------------------
+ boost::optional<tools::password_container> simple_wallet::get_and_verify_password() const
+ {
+- auto pwd_container = default_password_prompter(m_wallet_file.empty());
++ const bool verify = m_wallet_file.empty();
++ auto pwd_container = (m_wallet->is_background_wallet() && m_wallet->background_sync_type() == tools::wallet2::BackgroundSyncCustomPassword)
++ ? background_sync_cache_password_prompter(verify)
++ : default_password_prompter(verify);
+ if (!pwd_container)
+ return boost::none;
+
+@@ -5218,6 +5334,8 @@ boost::optional<epee::wipeable_string> simple_wallet::open_wallet(const boost::p
+ prefix = tr("Opened watch-only wallet");
+ else if (m_wallet->multisig(&ready, &threshold, &total))
+ prefix = (boost::format(tr("Opened %u/%u multisig wallet%s")) % threshold % total % (ready ? "" : " (not yet finalized)")).str();
++ else if (m_wallet->is_background_wallet())
++ prefix = tr("Opened background wallet");
+ else
+ prefix = tr("Opened wallet");
+ message_writer(console_color_white, true) <<
+@@ -5426,6 +5544,10 @@ void simple_wallet::stop_background_mining()
+ //----------------------------------------------------------------------------------------------------
+ void simple_wallet::check_background_mining(const epee::wipeable_string &password)
+ {
++ // Background mining can be toggled from the main wallet
++ if (m_wallet->is_background_wallet() || m_wallet->is_background_syncing())
++ return;
++
+ tools::wallet2::BackgroundMiningSetupType setup = m_wallet->setup_background_mining();
+ if (setup == tools::wallet2::BackgroundMiningNo)
+ {
+@@ -6290,6 +6412,7 @@ bool simple_wallet::show_blockchain_height(const std::vector<std::string>& args)
+ //----------------------------------------------------------------------------------------------------
+ bool simple_wallet::rescan_spent(const std::vector<std::string> &args)
+ {
++ CHECK_IF_BACKGROUND_SYNCING("cannot rescan spent");
+ if (!m_wallet->is_trusted_daemon())
+ {
+ fail_msg_writer() << tr("this command requires a trusted daemon. Enable with --trusted-daemon");
+@@ -6547,10 +6670,27 @@ void simple_wallet::check_for_inactivity_lock(bool user)
+ " || ||" << std::endl <<
+ "" << std::endl;
+ }
++
++ bool started_background_sync = false;
++ if (!m_wallet->is_background_wallet() &&
++ m_wallet->background_sync_type() != tools::wallet2::BackgroundSyncOff)
++ {
++ LOCK_IDLE_SCOPE();
++ m_wallet->start_background_sync();
++ started_background_sync = true;
++ }
++
+ while (1)
+ {
+ const char *inactivity_msg = user ? "" : tr("Locked due to inactivity.");
+- tools::msg_writer() << inactivity_msg << (inactivity_msg[0] ? " " : "") << tr("The wallet password is required to unlock the console.");
++ tools::msg_writer() << inactivity_msg << (inactivity_msg[0] ? " " : "") << (
++ (m_wallet->is_background_wallet() && m_wallet->background_sync_type() == tools::wallet2::BackgroundSyncCustomPassword)
++ ? tr("The background password is required to unlock the console.")
++ : tr("The wallet password is required to unlock the console.")
++ );
++
++ if (m_wallet->is_background_syncing())
++ tools::msg_writer() << tr("\nSyncing in the background while locked...") << std::endl;
+
+ const bool show_wallet_name = m_wallet->show_wallet_name_when_locked();
+ if (show_wallet_name)
+@@ -6563,8 +6703,16 @@ void simple_wallet::check_for_inactivity_lock(bool user)
+ }
+ try
+ {
+- if (get_and_verify_password())
++ const auto pwd_container = get_and_verify_password();
++ if (pwd_container)
++ {
++ if (started_background_sync)
++ {
++ LOCK_IDLE_SCOPE();
++ m_wallet->stop_background_sync(pwd_container->password());
++ }
+ break;
++ }
+ }
+ catch (...) { /* do nothing, just let the loop loop */ }
+ }
+@@ -6591,6 +6739,7 @@ bool simple_wallet::on_command(bool (simple_wallet::*cmd)(const std::vector<std:
+ bool simple_wallet::transfer_main(int transfer_type, const std::vector<std::string> &args_, bool called_by_mms)
+ {
+ // "transfer [index=<N1>[,<N2>,...]] [<priority>] [<ring_size>] <address> <amount> [<payment_id>]"
++ CHECK_IF_BACKGROUND_SYNCING("cannot transfer");
+ if (!try_connect_to_daemon())
+ return false;
+
+@@ -7064,6 +7213,7 @@ bool simple_wallet::transfer_main(int transfer_type, const std::vector<std::stri
+ //----------------------------------------------------------------------------------------------------
+ bool simple_wallet::transfer(const std::vector<std::string> &args_)
+ {
++ CHECK_IF_BACKGROUND_SYNCING("cannot transfer");
+ if (args_.size() < 1)
+ {
+ PRINT_USAGE(USAGE_TRANSFER);
+@@ -7075,6 +7225,7 @@ bool simple_wallet::transfer(const std::vector<std::string> &args_)
+ //----------------------------------------------------------------------------------------------------
+ bool simple_wallet::locked_transfer(const std::vector<std::string> &args_)
+ {
++ CHECK_IF_BACKGROUND_SYNCING("cannot transfer");
+ if (args_.size() < 1)
+ {
+ PRINT_USAGE(USAGE_LOCKED_TRANSFER);
+@@ -7086,6 +7237,7 @@ bool simple_wallet::locked_transfer(const std::vector<std::string> &args_)
+ //----------------------------------------------------------------------------------------------------
+ bool simple_wallet::locked_sweep_all(const std::vector<std::string> &args_)
+ {
++ CHECK_IF_BACKGROUND_SYNCING("cannot sweep");
+ if (args_.size() < 1)
+ {
+ PRINT_USAGE(USAGE_LOCKED_SWEEP_ALL);
+@@ -7098,6 +7250,7 @@ bool simple_wallet::locked_sweep_all(const std::vector<std::string> &args_)
+
+ bool simple_wallet::sweep_unmixable(const std::vector<std::string> &args_)
+ {
++ CHECK_IF_BACKGROUND_SYNCING("cannot sweep");
+ if (!try_connect_to_daemon())
+ return true;
+
+@@ -7205,6 +7358,7 @@ bool simple_wallet::sweep_unmixable(const std::vector<std::string> &args_)
+ //----------------------------------------------------------------------------------------------------
+ bool simple_wallet::sweep_main(uint32_t account, uint64_t below, bool locked, const std::vector<std::string> &args_)
+ {
++ CHECK_IF_BACKGROUND_SYNCING("cannot sweep");
+ auto print_usage = [this, account, below]()
+ {
+ if (below)
+@@ -7521,6 +7675,7 @@ bool simple_wallet::sweep_main(uint32_t account, uint64_t below, bool locked, co
+ //----------------------------------------------------------------------------------------------------
+ bool simple_wallet::sweep_single(const std::vector<std::string> &args_)
+ {
++ CHECK_IF_BACKGROUND_SYNCING("cannot sweep");
+ if (!try_connect_to_daemon())
+ return true;
+
+@@ -7759,12 +7914,14 @@ bool simple_wallet::sweep_single(const std::vector<std::string> &args_)
+ //----------------------------------------------------------------------------------------------------
+ bool simple_wallet::sweep_all(const std::vector<std::string> &args_)
+ {
++ CHECK_IF_BACKGROUND_SYNCING("cannot sweep");
+ sweep_main(m_current_subaddress_account, 0, false, args_);
+ return true;
+ }
+ //----------------------------------------------------------------------------------------------------
+ bool simple_wallet::sweep_account(const std::vector<std::string> &args_)
+ {
++ CHECK_IF_BACKGROUND_SYNCING("cannot sweep");
+ auto local_args = args_;
+ if (local_args.empty())
+ {
+@@ -7785,6 +7942,7 @@ bool simple_wallet::sweep_account(const std::vector<std::string> &args_)
+ //----------------------------------------------------------------------------------------------------
+ bool simple_wallet::sweep_below(const std::vector<std::string> &args_)
+ {
++ CHECK_IF_BACKGROUND_SYNCING("cannot sweep");
+ uint64_t below = 0;
+ if (args_.size() < 1)
+ {
+@@ -7803,6 +7961,7 @@ bool simple_wallet::sweep_below(const std::vector<std::string> &args_)
+ //----------------------------------------------------------------------------------------------------
+ bool simple_wallet::donate(const std::vector<std::string> &args_)
+ {
++ CHECK_IF_BACKGROUND_SYNCING("cannot donate");
+ std::vector<std::string> local_args = args_;
+ if(local_args.empty() || local_args.size() > 5)
+ {
+@@ -7864,6 +8023,7 @@ bool simple_wallet::donate(const std::vector<std::string> &args_)
+ //----------------------------------------------------------------------------------------------------
+ bool simple_wallet::accept_loaded_tx(const std::function<size_t()> get_num_txes, const std::function<const tools::wallet2::tx_construction_data&(size_t)> &get_tx, const std::string &extra_message)
+ {
++ CHECK_IF_BACKGROUND_SYNCING("cannot load tx");
+ // gather info to ask the user
+ uint64_t amount = 0, amount_to_dests = 0, change = 0;
+ size_t min_ring_size = ~0;
+@@ -8044,6 +8204,7 @@ bool simple_wallet::sign_transfer(const std::vector<std::string> &args_)
+ fail_msg_writer() << tr("This is a watch only wallet");
+ return true;
+ }
++ CHECK_IF_BACKGROUND_SYNCING("cannot sign transfer");
+
+ bool export_raw = false;
+ std::string unsigned_filename = "unsigned_monero_tx";
+@@ -8151,6 +8312,8 @@ std::string get_tx_key_stream(crypto::secret_key tx_key, std::vector<crypto::sec
+
+ bool simple_wallet::get_tx_key(const std::vector<std::string> &args_)
+ {
++ CHECK_IF_BACKGROUND_SYNCING("cannot get tx key");
++
+ std::vector<std::string> local_args = args_;
+
+ if (m_wallet->key_on_device() && m_wallet->get_account().get_device().get_type() != hw::device::TREZOR)
+@@ -8191,6 +8354,8 @@ bool simple_wallet::get_tx_key(const std::vector<std::string> &args_)
+ //----------------------------------------------------------------------------------------------------
+ bool simple_wallet::set_tx_key(const std::vector<std::string> &args_)
+ {
++ CHECK_IF_BACKGROUND_SYNCING("cannot set tx key");
++
+ std::vector<std::string> local_args = args_;
+
+ if(local_args.size() != 2 && local_args.size() != 3) {
+@@ -8267,6 +8432,8 @@ bool simple_wallet::set_tx_key(const std::vector<std::string> &args_)
+ //----------------------------------------------------------------------------------------------------
+ bool simple_wallet::get_tx_proof(const std::vector<std::string> &args)
+ {
++ CHECK_IF_BACKGROUND_SYNCING("cannot get tx proof");
++
+ if (args.size() != 2 && args.size() != 3)
+ {
+ PRINT_USAGE(USAGE_GET_TX_PROOF);
+@@ -8473,6 +8640,7 @@ bool simple_wallet::check_tx_proof(const std::vector<std::string> &args)
+ //----------------------------------------------------------------------------------------------------
+ bool simple_wallet::get_spend_proof(const std::vector<std::string> &args)
+ {
++ CHECK_IF_BACKGROUND_SYNCING("cannot get spend proof");
+ if (m_wallet->key_on_device())
+ {
+ fail_msg_writer() << tr("command not supported by HW wallet");
+@@ -8557,6 +8725,7 @@ bool simple_wallet::check_spend_proof(const std::vector<std::string> &args)
+ //----------------------------------------------------------------------------------------------------
+ bool simple_wallet::get_reserve_proof(const std::vector<std::string> &args)
+ {
++ CHECK_IF_BACKGROUND_SYNCING("cannot get reserve proof");
+ if (m_wallet->key_on_device())
+ {
+ fail_msg_writer() << tr("command not supported by HW wallet");
+@@ -9243,6 +9412,8 @@ bool simple_wallet::unspent_outputs(const std::vector<std::string> &args_)
+ //----------------------------------------------------------------------------------------------------
+ bool simple_wallet::rescan_blockchain(const std::vector<std::string> &args_)
+ {
++ CHECK_IF_BACKGROUND_SYNCING("cannot rescan");
++
+ uint64_t start_height = 0;
+ ResetType reset_type = ResetSoft;
+
+@@ -9540,6 +9711,7 @@ bool simple_wallet::account(const std::vector<std::string> &args/* = std::vector
+ if (command == "new")
+ {
+ // create a new account and switch to it
++ CHECK_IF_BACKGROUND_SYNCING("cannot create new account");
+ std::string label = boost::join(local_args, " ");
+ if (label.empty())
+ label = tr("(Untitled account)");
+@@ -9570,6 +9742,7 @@ bool simple_wallet::account(const std::vector<std::string> &args/* = std::vector
+ else if (command == "label" && local_args.size() >= 1)
+ {
+ // set label of the specified account
++ CHECK_IF_BACKGROUND_SYNCING("cannot modify account");
+ uint32_t index_major;
+ if (!epee::string_tools::get_xtype_from_string(index_major, local_args[0]))
+ {
+@@ -9591,6 +9764,7 @@ bool simple_wallet::account(const std::vector<std::string> &args/* = std::vector
+ }
+ else if (command == "tag" && local_args.size() >= 2)
+ {
++ CHECK_IF_BACKGROUND_SYNCING("cannot modify account");
+ const std::string tag = local_args[0];
+ std::set<uint32_t> account_indices;
+ for (size_t i = 1; i < local_args.size(); ++i)
+@@ -9615,6 +9789,7 @@ bool simple_wallet::account(const std::vector<std::string> &args/* = std::vector
+ }
+ else if (command == "untag" && local_args.size() >= 1)
+ {
++ CHECK_IF_BACKGROUND_SYNCING("cannot modify account");
+ std::set<uint32_t> account_indices;
+ for (size_t i = 0; i < local_args.size(); ++i)
+ {
+@@ -9638,6 +9813,7 @@ bool simple_wallet::account(const std::vector<std::string> &args/* = std::vector
+ }
+ else if (command == "tag_description" && local_args.size() >= 1)
+ {
++ CHECK_IF_BACKGROUND_SYNCING("cannot modify account");
+ const std::string tag = local_args[0];
+ std::string description;
+ if (local_args.size() > 1)
+@@ -9755,6 +9931,7 @@ bool simple_wallet::print_address(const std::vector<std::string> &args/* = std::
+ }
+ else if (local_args[0] == "new")
+ {
++ CHECK_IF_BACKGROUND_SYNCING("cannot add address");
+ local_args.erase(local_args.begin());
+ std::string label;
+ if (local_args.size() > 0)
+@@ -9767,6 +9944,7 @@ bool simple_wallet::print_address(const std::vector<std::string> &args/* = std::
+ }
+ else if (local_args[0] == "mnew")
+ {
++ CHECK_IF_BACKGROUND_SYNCING("cannot add addresses");
+ local_args.erase(local_args.begin());
+ if (local_args.size() != 1)
+ {
+@@ -9792,6 +9970,7 @@ bool simple_wallet::print_address(const std::vector<std::string> &args/* = std::
+ }
+ else if (local_args[0] == "one-off")
+ {
++ CHECK_IF_BACKGROUND_SYNCING("cannot add address");
+ local_args.erase(local_args.begin());
+ std::string label;
+ if (local_args.size() != 2)
+@@ -9810,6 +9989,7 @@ bool simple_wallet::print_address(const std::vector<std::string> &args/* = std::
+ }
+ else if (local_args.size() >= 2 && local_args[0] == "label")
+ {
++ CHECK_IF_BACKGROUND_SYNCING("cannot modify address");
+ if (!epee::string_tools::get_xtype_from_string(index, local_args[1]))
+ {
+ fail_msg_writer() << tr("failed to parse index: ") << local_args[1];
+@@ -9956,6 +10136,8 @@ bool simple_wallet::print_integrated_address(const std::vector<std::string> &arg
+ //----------------------------------------------------------------------------------------------------
+ bool simple_wallet::address_book(const std::vector<std::string> &args/* = std::vector<std::string>()*/)
+ {
++ CHECK_IF_BACKGROUND_SYNCING("cannot get address book");
++
+ if (args.size() == 0)
+ {
+ }
+@@ -10016,6 +10198,8 @@ bool simple_wallet::address_book(const std::vector<std::string> &args/* = std::v
+ //----------------------------------------------------------------------------------------------------
+ bool simple_wallet::set_tx_note(const std::vector<std::string> &args)
+ {
++ CHECK_IF_BACKGROUND_SYNCING("cannot set tx note");
++
+ if (args.size() == 0)
+ {
+ PRINT_USAGE(USAGE_SET_TX_NOTE);
+@@ -10044,6 +10228,8 @@ bool simple_wallet::set_tx_note(const std::vector<std::string> &args)
+ //----------------------------------------------------------------------------------------------------
+ bool simple_wallet::get_tx_note(const std::vector<std::string> &args)
+ {
++ CHECK_IF_BACKGROUND_SYNCING("cannot get tx note");
++
+ if (args.size() != 1)
+ {
+ PRINT_USAGE(USAGE_GET_TX_NOTE);
+@@ -10069,6 +10255,8 @@ bool simple_wallet::get_tx_note(const std::vector<std::string> &args)
+ //----------------------------------------------------------------------------------------------------
+ bool simple_wallet::set_description(const std::vector<std::string> &args)
+ {
++ CHECK_IF_BACKGROUND_SYNCING("cannot set description");
++
+ // 0 arguments allowed, for setting the description to empty string
+
+ std::string description = "";
+@@ -10085,6 +10273,8 @@ bool simple_wallet::set_description(const std::vector<std::string> &args)
+ //----------------------------------------------------------------------------------------------------
+ bool simple_wallet::get_description(const std::vector<std::string> &args)
+ {
++ CHECK_IF_BACKGROUND_SYNCING("cannot get description");
++
+ if (args.size() != 0)
+ {
+ PRINT_USAGE(USAGE_GET_DESCRIPTION);
+@@ -10143,6 +10333,8 @@ bool simple_wallet::wallet_info(const std::vector<std::string> &args)
+ type = tr("Watch only");
+ else if (m_wallet->multisig(&ready, &threshold, &total))
+ type = (boost::format(tr("%u/%u multisig%s")) % threshold % total % (ready ? "" : " (not yet finalized)")).str();
++ else if (m_wallet->is_background_wallet())
++ type = tr("Background wallet");
+ else
+ type = tr("Normal");
+ message_writer() << tr("Type: ") << type;
+@@ -10154,6 +10346,7 @@ bool simple_wallet::wallet_info(const std::vector<std::string> &args)
+ //----------------------------------------------------------------------------------------------------
+ bool simple_wallet::sign(const std::vector<std::string> &args)
+ {
++ CHECK_IF_BACKGROUND_SYNCING("cannot sign");
+ if (m_wallet->key_on_device())
+ {
+ fail_msg_writer() << tr("command not supported by HW wallet");
+@@ -10261,6 +10454,7 @@ bool simple_wallet::export_key_images(const std::vector<std::string> &args_)
+ fail_msg_writer() << tr("command not supported by HW wallet");
+ return true;
+ }
++ CHECK_IF_BACKGROUND_SYNCING("cannot export key images");
+ auto args = args_;
+
+ if (m_wallet->watch_only())
+@@ -10314,6 +10508,7 @@ bool simple_wallet::import_key_images(const std::vector<std::string> &args)
+ fail_msg_writer() << tr("command not supported by HW wallet");
+ return true;
+ }
++ CHECK_IF_BACKGROUND_SYNCING("cannot import key images");
+ if (!m_wallet->is_trusted_daemon())
+ {
+ fail_msg_writer() << tr("this command requires a trusted daemon. Enable with --trusted-daemon");
+@@ -10422,6 +10617,7 @@ bool simple_wallet::export_outputs(const std::vector<std::string> &args_)
+ fail_msg_writer() << tr("command not supported by HW wallet");
+ return true;
+ }
++ CHECK_IF_BACKGROUND_SYNCING("cannot export outputs");
+ auto args = args_;
+
+ bool all = false;
+@@ -10471,6 +10667,7 @@ bool simple_wallet::import_outputs(const std::vector<std::string> &args)
+ fail_msg_writer() << tr("command not supported by HW wallet");
+ return true;
+ }
++ CHECK_IF_BACKGROUND_SYNCING("cannot import outputs");
+ if (args.size() != 1)
+ {
+ PRINT_USAGE(USAGE_IMPORT_OUTPUTS);
+diff --git a/src/simplewallet/simplewallet.h b/src/simplewallet/simplewallet.h
+index 7c45d45e8..79c739cdd 100644
+--- a/src/simplewallet/simplewallet.h
++++ b/src/simplewallet/simplewallet.h
+@@ -147,6 +147,7 @@ namespace cryptonote
+ bool set_ignore_outputs_above(const std::vector<std::string> &args = std::vector<std::string>());
+ bool set_ignore_outputs_below(const std::vector<std::string> &args = std::vector<std::string>());
+ bool set_track_uses(const std::vector<std::string> &args = std::vector<std::string>());
++ bool setup_background_sync(const std::vector<std::string> &args = std::vector<std::string>());
+ bool set_show_wallet_name_when_locked(const std::vector<std::string> &args = std::vector<std::string>());
+ bool set_inactivity_lock_timeout(const std::vector<std::string> &args = std::vector<std::string>());
+ bool set_setup_background_mining(const std::vector<std::string> &args = std::vector<std::string>());
+diff --git a/src/wallet/api/wallet.cpp b/src/wallet/api/wallet.cpp
+index 472f05016..1a9c6f674 100644
+--- a/src/wallet/api/wallet.cpp
++++ b/src/wallet/api/wallet.cpp
+@@ -54,6 +54,40 @@ using namespace cryptonote;
+ #undef MONERO_DEFAULT_LOG_CATEGORY
+ #define MONERO_DEFAULT_LOG_CATEGORY "WalletAPI"
+
++#define LOCK_REFRESH() \
++ bool refresh_enabled = m_refreshEnabled; \
++ m_refreshEnabled = false; \
++ m_wallet->stop(); \
++ m_refreshCV.notify_one(); \
++ boost::mutex::scoped_lock lock(m_refreshMutex); \
++ boost::mutex::scoped_lock lock2(m_refreshMutex2); \
++ epee::misc_utils::auto_scope_leave_caller scope_exit_handler = epee::misc_utils::create_scope_leave_handler([&](){ \
++ /* m_refreshMutex's still locked here */ \
++ if (refresh_enabled) \
++ startRefresh(); \
++ })
++
++#define PRE_VALIDATE_BACKGROUND_SYNC() \
++ do \
++ { \
++ clearStatus(); \
++ if (m_wallet->key_on_device()) \
++ { \
++ setStatusError(tr("HW wallet cannot use background sync")); \
++ return false; \
++ } \
++ if (m_wallet->watch_only()) \
++ { \
++ setStatusError(tr("View only wallet cannot use background sync")); \
++ return false; \
++ } \
++ if (m_wallet->multisig()) \
++ { \
++ setStatusError(tr("Multisig wallet cannot use background sync")); \
++ return false; \
++ } \
++ } while (0)
++
+ namespace Monero {
+
+ namespace {
+@@ -814,6 +848,8 @@ bool WalletImpl::close(bool store)
+
+ std::string WalletImpl::seed(const std::string& seed_offset) const
+ {
++ if (checkBackgroundSync("cannot get seed"))
++ return std::string();
+ epee::wipeable_string seed;
+ if (m_wallet)
+ m_wallet->get_seed(seed, seed_offset);
+@@ -876,6 +912,8 @@ std::string WalletImpl::getSeedLanguage() const
+
+ void WalletImpl::setSeedLanguage(const std::string &arg)
+ {
++ if (checkBackgroundSync("cannot set seed language"))
++ return;
+ m_wallet->set_seed_language(arg);
+ }
+
+@@ -899,6 +937,8 @@ void WalletImpl::statusWithErrorString(int& status, std::string& errorString) co
+
+ bool WalletImpl::setPassword(const std::string &password)
+ {
++ if (checkBackgroundSync("cannot change password"))
++ return false;
+ clearStatus();
+ try {
+ m_wallet->change_password(m_wallet->get_wallet_file(), m_password, password);
+@@ -1059,6 +1099,8 @@ bool WalletImpl::lightWalletImportWalletRequest(std::string &payment_id, uint64_
+
+ void WalletImpl::setRefreshFromBlockHeight(uint64_t refresh_from_block_height)
+ {
++ if (checkBackgroundSync("cannot change refresh height"))
++ return;
+ m_wallet->set_refresh_from_block_height(refresh_from_block_height);
+ }
+
+@@ -1176,6 +1218,8 @@ void WalletImpl::refreshAsync()
+
+ bool WalletImpl::rescanBlockchain()
+ {
++ if (checkBackgroundSync("cannot rescan blockchain"))
++ return false;
+ clearStatus();
+ m_refreshShouldRescan = true;
+ doRefresh();
+@@ -1184,6 +1228,8 @@ bool WalletImpl::rescanBlockchain()
+
+ void WalletImpl::rescanBlockchainAsync()
+ {
++ if (checkBackgroundSync("cannot rescan blockchain"))
++ return;
+ m_refreshShouldRescan = true;
+ refreshAsync();
+ }
+@@ -1207,7 +1253,7 @@ int WalletImpl::autoRefreshInterval() const
+ UnsignedTransaction *WalletImpl::loadUnsignedTx(const std::string &unsigned_filename) {
+ clearStatus();
+ UnsignedTransactionImpl * transaction = new UnsignedTransactionImpl(*this);
+- if (!m_wallet->load_unsigned_tx(unsigned_filename, transaction->m_unsigned_tx_set)){
++ if (checkBackgroundSync("cannot load tx") || !m_wallet->load_unsigned_tx(unsigned_filename, transaction->m_unsigned_tx_set)){
+ setStatusError(tr("Failed to load unsigned transactions"));
+ transaction->m_status = UnsignedTransaction::Status::Status_Error;
+ transaction->m_errorString = errorString();
+@@ -1227,6 +1273,8 @@ UnsignedTransaction *WalletImpl::loadUnsignedTx(const std::string &unsigned_file
+
+ bool WalletImpl::submitTransaction(const string &fileName) {
+ clearStatus();
++ if (checkBackgroundSync("cannot submit tx"))
++ return false;
+ std::unique_ptr<PendingTransactionImpl> transaction(new PendingTransactionImpl(*this));
+
+ bool r = m_wallet->load_tx(fileName, transaction->m_pending_tx);
+@@ -1250,6 +1298,8 @@ bool WalletImpl::exportKeyImages(const string &filename, bool all)
+ setStatusError(tr("Wallet is view only"));
+ return false;
+ }
++ if (checkBackgroundSync("cannot export key images"))
++ return false;
+
+ try
+ {
+@@ -1270,6 +1320,8 @@ bool WalletImpl::exportKeyImages(const string &filename, bool all)
+
+ bool WalletImpl::importKeyImages(const string &filename)
+ {
++ if (checkBackgroundSync("cannot import key images"))
++ return false;
+ if (!trustedDaemon()) {
+ setStatusError(tr("Key images can only be imported with a trusted daemon"));
+ return false;
+@@ -1293,6 +1345,8 @@ bool WalletImpl::importKeyImages(const string &filename)
+
+ bool WalletImpl::exportOutputs(const string &filename, bool all)
+ {
++ if (checkBackgroundSync("cannot export outputs"))
++ return false;
+ if (m_wallet->key_on_device())
+ {
+ setStatusError(string(tr("Not supported on HW wallets.")) + filename);
+@@ -1323,6 +1377,8 @@ bool WalletImpl::exportOutputs(const string &filename, bool all)
+
+ bool WalletImpl::importOutputs(const string &filename)
+ {
++ if (checkBackgroundSync("cannot import outputs"))
++ return false;
+ if (m_wallet->key_on_device())
+ {
+ setStatusError(string(tr("Not supported on HW wallets.")) + filename);
+@@ -1355,6 +1411,8 @@ bool WalletImpl::importOutputs(const string &filename)
+
+ bool WalletImpl::scanTransactions(const std::vector<std::string> &txids)
+ {
++ if (checkBackgroundSync("cannot scan transactions"))
++ return false;
+ if (txids.empty())
+ {
+ setStatusError(string(tr("Failed to scan transactions: no transaction ids provided.")));
+@@ -1393,8 +1451,86 @@ bool WalletImpl::scanTransactions(const std::vector<std::string> &txids)
+ return true;
+ }
+
++bool WalletImpl::setupBackgroundSync(const Wallet::BackgroundSyncType background_sync_type, const std::string &wallet_password, const optional<std::string> &background_cache_password)
++{
++ try
++ {
++ PRE_VALIDATE_BACKGROUND_SYNC();
++
++ tools::wallet2::BackgroundSyncType bgs_type;
++ switch (background_sync_type)
++ {
++ case Wallet::BackgroundSync_Off: bgs_type = tools::wallet2::BackgroundSyncOff; break;
++ case Wallet::BackgroundSync_ReusePassword: bgs_type = tools::wallet2::BackgroundSyncReusePassword; break;
++ case Wallet::BackgroundSync_CustomPassword: bgs_type = tools::wallet2::BackgroundSyncCustomPassword; break;
++ default: setStatusError(tr("Unknown background sync type")); return false;
++ }
++
++ boost::optional<epee::wipeable_string> bgc_password = background_cache_password
++ ? boost::optional<epee::wipeable_string>(*background_cache_password)
++ : boost::none;
++
++ LOCK_REFRESH();
++ m_wallet->setup_background_sync(bgs_type, wallet_password, bgc_password);
++ }
++ catch (const std::exception &e)
++ {
++ LOG_ERROR("Failed to setup background sync: " << e.what());
++ setStatusError(string(tr("Failed to setup background sync: ")) + e.what());
++ return false;
++ }
++ return true;
++}
++
++Wallet::BackgroundSyncType WalletImpl::getBackgroundSyncType() const
++{
++ switch (m_wallet->background_sync_type())
++ {
++ case tools::wallet2::BackgroundSyncOff: return Wallet::BackgroundSync_Off;
++ case tools::wallet2::BackgroundSyncReusePassword: return Wallet::BackgroundSync_ReusePassword;
++ case tools::wallet2::BackgroundSyncCustomPassword: return Wallet::BackgroundSync_CustomPassword;
++ default: setStatusError(tr("Unknown background sync type")); return Wallet::BackgroundSync_Off;
++ }
++}
++
++bool WalletImpl::startBackgroundSync()
++{
++ try
++ {
++ PRE_VALIDATE_BACKGROUND_SYNC();
++ LOCK_REFRESH();
++ m_wallet->start_background_sync();
++ }
++ catch (const std::exception &e)
++ {
++ LOG_ERROR("Failed to start background sync: " << e.what());
++ setStatusError(string(tr("Failed to start background sync: ")) + e.what());
++ return false;
++ }
++ return true;
++}
++
++bool WalletImpl::stopBackgroundSync(const std::string &wallet_password)
++{
++ try
++ {
++ PRE_VALIDATE_BACKGROUND_SYNC();
++ LOCK_REFRESH();
++ m_wallet->stop_background_sync(epee::wipeable_string(wallet_password));
++ }
++ catch (const std::exception &e)
++ {
++ LOG_ERROR("Failed to stop background sync: " << e.what());
++ setStatusError(string(tr("Failed to stop background sync: ")) + e.what());
++ return false;
++ }
++ return true;
++}
++
+ void WalletImpl::addSubaddressAccount(const std::string& label)
+ {
++ if (checkBackgroundSync("cannot add account"))
++ return;
+ m_wallet->add_subaddress_account(label);
+ }
+ size_t WalletImpl::numSubaddressAccounts() const
+@@ -1407,10 +1543,14 @@ size_t WalletImpl::numSubaddresses(uint32_t accountIndex) const
+ }
+ void WalletImpl::addSubaddress(uint32_t accountIndex, const std::string& label)
+ {
++ if (checkBackgroundSync("cannot add subbaddress"))
++ return;
+ m_wallet->add_subaddress(accountIndex, label);
+ }
+ std::string WalletImpl::getSubaddressLabel(uint32_t accountIndex, uint32_t addressIndex) const
+ {
++ if (checkBackgroundSync("cannot get subbaddress label"))
++ return "";
+ try
+ {
+ return m_wallet->get_subaddress_label({accountIndex, addressIndex});
+@@ -1424,6 +1564,8 @@ std::string WalletImpl::getSubaddressLabel(uint32_t accountIndex, uint32_t addre
+ }
+ void WalletImpl::setSubaddressLabel(uint32_t accountIndex, uint32_t addressIndex, const std::string &label)
+ {
++ if (checkBackgroundSync("cannot set subbaddress label"))
++ return;
+ try
+ {
+ return m_wallet->set_subaddress_label({accountIndex, addressIndex}, label);
+@@ -1437,12 +1579,16 @@ void WalletImpl::setSubaddressLabel(uint32_t accountIndex, uint32_t addressIndex
+
+ MultisigState WalletImpl::multisig() const {
+ MultisigState state;
++ if (checkBackgroundSync("cannot use multisig"))
++ return state;
+ state.isMultisig = m_wallet->multisig(&state.isReady, &state.threshold, &state.total);
+
+ return state;
+ }
+
+ string WalletImpl::getMultisigInfo() const {
++ if (checkBackgroundSync("cannot use multisig"))
++ return string();
+ try {
+ clearStatus();
+ return m_wallet->get_multisig_first_kex_msg();
+@@ -1455,6 +1601,8 @@ string WalletImpl::getMultisigInfo() const {
+ }
+
+ string WalletImpl::makeMultisig(const vector<string>& info, const uint32_t threshold) {
++ if (checkBackgroundSync("cannot make multisig"))
++ return string();
+ try {
+ clearStatus();
+
+@@ -1595,6 +1743,9 @@ PendingTransaction *WalletImpl::createTransactionMultDest(const std::vector<stri
+ PendingTransactionImpl * transaction = new PendingTransactionImpl(*this);
+
+ do {
++ if (checkBackgroundSync("cannot create transactions"))
++ break;
++
+ std::vector<uint8_t> extra;
+ std::string extra_nonce;
+ vector<cryptonote::tx_destination_entry> dsts;
+@@ -1761,6 +1912,9 @@ PendingTransaction *WalletImpl::createSweepUnmixableTransaction()
+ PendingTransactionImpl * transaction = new PendingTransactionImpl(*this);
+
+ do {
++ if (checkBackgroundSync("cannot sweep"))
++ break;
++
+ try {
+ transaction->m_pending_tx = m_wallet->create_unmixable_sweep_transactions();
+ pendingTxPostProcess(transaction);
+@@ -1894,11 +2048,15 @@ uint32_t WalletImpl::defaultMixin() const
+
+ void WalletImpl::setDefaultMixin(uint32_t arg)
+ {
++ if (checkBackgroundSync("cannot set default mixin"))
++ return;
+ m_wallet->default_mixin(arg);
+ }
+
+ bool WalletImpl::setCacheAttribute(const std::string &key, const std::string &val)
+ {
++ if (checkBackgroundSync("cannot set cache attribute"))
++ return false;
+ m_wallet->set_attribute(key, val);
+ return true;
+ }
+@@ -1912,6 +2070,8 @@ std::string WalletImpl::getCacheAttribute(const std::string &key) const
+
+ bool WalletImpl::setUserNote(const std::string &txid, const std::string &note)
+ {
++ if (checkBackgroundSync("cannot set user note"))
++ return false;
+ cryptonote::blobdata txid_data;
+ if(!epee::string_tools::parse_hexstr_to_binbuff(txid, txid_data) || txid_data.size() != sizeof(crypto::hash))
+ return false;
+@@ -1923,6 +2083,8 @@ bool WalletImpl::setUserNote(const std::string &txid, const std::string &note)
+
+ std::string WalletImpl::getUserNote(const std::string &txid) const
+ {
++ if (checkBackgroundSync("cannot get user note"))
++ return "";
+ cryptonote::blobdata txid_data;
+ if(!epee::string_tools::parse_hexstr_to_binbuff(txid, txid_data) || txid_data.size() != sizeof(crypto::hash))
+ return "";
+@@ -1933,6 +2095,9 @@ std::string WalletImpl::getUserNote(const std::string &txid) const
+
+ std::string WalletImpl::getTxKey(const std::string &txid_str) const
+ {
++ if (checkBackgroundSync("cannot get tx key"))
++ return "";
++
+ crypto::hash txid;
+ if(!epee::string_tools::hex_to_pod(txid_str, txid))
+ {
+@@ -2017,6 +2182,9 @@ bool WalletImpl::checkTxKey(const std::string &txid_str, std::string tx_key_str,
+
+ std::string WalletImpl::getTxProof(const std::string &txid_str, const std::string &address_str, const std::string &message) const
+ {
++ if (checkBackgroundSync("cannot get tx proof"))
++ return "";
++
+ crypto::hash txid;
+ if (!epee::string_tools::hex_to_pod(txid_str, txid))
+ {
+@@ -2073,6 +2241,9 @@ bool WalletImpl::checkTxProof(const std::string &txid_str, const std::string &ad
+ }
+
+ std::string WalletImpl::getSpendProof(const std::string &txid_str, const std::string &message) const {
++ if (checkBackgroundSync("cannot get spend proof"))
++ return "";
++
+ crypto::hash txid;
+ if(!epee::string_tools::hex_to_pod(txid_str, txid))
+ {
+@@ -2115,6 +2286,9 @@ bool WalletImpl::checkSpendProof(const std::string &txid_str, const std::string
+ }
+
+ std::string WalletImpl::getReserveProof(bool all, uint32_t account_index, uint64_t amount, const std::string &message) const {
++ if (checkBackgroundSync("cannot get reserve proof"))
++ return "";
++
+ try
+ {
+ clearStatus();
+@@ -2161,6 +2335,9 @@ bool WalletImpl::checkReserveProof(const std::string &address, const std::string
+
+ std::string WalletImpl::signMessage(const std::string &message, const std::string &address)
+ {
++ if (checkBackgroundSync("cannot sign message"))
++ return "";
++
+ if (address.empty()) {
+ return m_wallet->sign(message, tools::wallet2::sign_with_spend_key);
+ }
+@@ -2288,6 +2465,16 @@ bool WalletImpl::isDeterministic() const
+ return m_wallet->is_deterministic();
+ }
+
++bool WalletImpl::isBackgroundSyncing() const
++{
++ return m_wallet->is_background_syncing();
++}
++
++bool WalletImpl::isBackgroundWallet() const
++{
++ return m_wallet->is_background_wallet();
++}
++
+ void WalletImpl::clearStatus() const
+ {
+ boost::lock_guard<boost::mutex> l(m_statusMutex);
+@@ -2356,9 +2543,7 @@ void WalletImpl::doRefresh()
+ if(rescan)
+ m_wallet->rescan_blockchain(false);
+ m_wallet->refresh(trustedDaemon());
+- if (!m_synchronized) {
+- m_synchronized = true;
+- }
++ m_synchronized = m_wallet->is_synced();
+ // assuming if we have empty history, it wasn't initialized yet
+ // for further history changes client need to update history in
+ // "on_money_received" and "on_money_sent" callbacks
+@@ -2462,6 +2647,24 @@ bool WalletImpl::doInit(const string &daemon_address, const std::string &proxy_a
+ return true;
+ }
+
++bool WalletImpl::checkBackgroundSync(const std::string &message) const
++{
++ clearStatus();
++ if (m_wallet->is_background_wallet())
++ {
++ LOG_ERROR("Background wallets " + message);
++ setStatusError(tr("Background wallets ") + message);
++ return true;
++ }
++ if (m_wallet->is_background_syncing())
++ {
++ LOG_ERROR(message + " while background syncing");
++ setStatusError(message + tr(" while background syncing. Stop background syncing first."));
++ return true;
++ }
++ return false;
++}
++
+ bool WalletImpl::parse_uri(const std::string &uri, std::string &address, std::string &payment_id, uint64_t &amount, std::string &tx_description, std::string &recipient_name, std::vector<std::string> &unknown_parameters, std::string &error)
+ {
+ return m_wallet->parse_uri(uri, address, payment_id, amount, tx_description, recipient_name, unknown_parameters, error);
+@@ -2480,6 +2683,8 @@ std::string WalletImpl::getDefaultDataDir() const
+ bool WalletImpl::rescanSpent()
+ {
+ clearStatus();
++ if (checkBackgroundSync("cannot rescan spent"))
++ return false;
+ if (!trustedDaemon()) {
+ setStatusError(tr("Rescan spent can only be used with a trusted daemon"));
+ return false;
+diff --git a/src/wallet/api/wallet.h b/src/wallet/api/wallet.h
+index 787215ab3..9acd2871f 100644
+--- a/src/wallet/api/wallet.h
++++ b/src/wallet/api/wallet.h
+@@ -181,6 +181,13 @@ public:
+ bool importOutputs(const std::string &filename) override;
+ bool scanTransactions(const std::vector<std::string> &txids) override;
+
++ bool setupBackgroundSync(const BackgroundSyncType background_sync_type, const std::string &wallet_password, const optional<std::string> &background_cache_password = optional<std::string>()) override;
++ BackgroundSyncType getBackgroundSyncType() const override;
++ bool startBackgroundSync() override;
++ bool stopBackgroundSync(const std::string &wallet_password) override;
++ bool isBackgroundSyncing() const override;
++ bool isBackgroundWallet() const override;
++
+ virtual void disposeTransaction(PendingTransaction * t) override;
+ virtual uint64_t estimateTransactionFee(const std::vector<std::pair<std::string, uint64_t>> &destinations,
+ PendingTransaction::Priority priority) const override;
+@@ -249,6 +256,7 @@ private:
+ bool isNewWallet() const;
+ void pendingTxPostProcess(PendingTransactionImpl * pending);
+ bool doInit(const std::string &daemon_address, const std::string &proxy_address, uint64_t upper_transaction_size_limit = 0, bool ssl = false);
++ bool checkBackgroundSync(const std::string &message) const;
+
+ private:
+ friend class PendingTransactionImpl;
+@@ -263,6 +271,10 @@ private:
+ mutable boost::mutex m_statusMutex;
+ mutable int m_status;
+ mutable std::string m_errorString;
++ // TODO: harden password handling in the wallet API, see relevant discussion
++ // https://github.com/monero-project/monero-gui/issues/1537
++ // https://github.com/feather-wallet/feather/issues/72#issuecomment-1405602142
++ // https://github.com/monero-project/monero/pull/8619#issuecomment-1632951461
+ std::string m_password;
+ std::unique_ptr<TransactionHistoryImpl> m_history;
+ std::unique_ptr<Wallet2CallbackImpl> m_wallet2Callback;
+diff --git a/src/wallet/api/wallet2_api.h b/src/wallet/api/wallet2_api.h
+index 9ea753083..4268b656e 100644
+--- a/src/wallet/api/wallet2_api.h
++++ b/src/wallet/api/wallet2_api.h
+@@ -445,6 +445,12 @@ struct Wallet
+ ConnectionStatus_WrongVersion
+ };
+
++ enum BackgroundSyncType {
++ BackgroundSync_Off = 0,
++ BackgroundSync_ReusePassword = 1,
++ BackgroundSync_CustomPassword = 2
++ };
++
+ virtual ~Wallet() = 0;
+ virtual std::string seed(const std::string& seed_offset = "") const = 0;
+ virtual std::string getSeedLanguage() const = 0;
+@@ -940,6 +946,42 @@ struct Wallet
+ */
+ virtual bool scanTransactions(const std::vector<std::string> &txids) = 0;
+
++ /*!
++ * \brief setupBackgroundSync - setup background sync mode with just a view key
++ * \param background_sync_type - the mode the wallet background syncs in
++ * \param wallet_password
++ * \param background_cache_password - custom password to encrypt background cache, only needed for custom password background sync type
++ * \return - true on success
++ */
++ virtual bool setupBackgroundSync(const BackgroundSyncType background_sync_type, const std::string &wallet_password, const optional<std::string> &background_cache_password) = 0;
++
++ /*!
++ * \brief getBackgroundSyncType - get mode the wallet background syncs in
++ * \return - the type, or off if type is unknown
++ */
++ virtual BackgroundSyncType getBackgroundSyncType() const = 0;
++
++ /**
++ * @brief startBackgroundSync - sync the chain in the background with just view key
++ */
++ virtual bool startBackgroundSync() = 0;
++
++ /**
++ * @brief stopBackgroundSync - bring back spend key and process background synced txs
++ * \param wallet_password
++ */
++ virtual bool stopBackgroundSync(const std::string &wallet_password) = 0;
++
++ /**
++ * @brief isBackgroundSyncing - returns true if the wallet is background syncing
++ */
++ virtual bool isBackgroundSyncing() const = 0;
++
++ /**
++ * @brief isBackgroundWallet - returns true if the wallet is a background wallet
++ */
++ virtual bool isBackgroundWallet() const = 0;
++
+ virtual TransactionHistory * history() = 0;
+ virtual AddressBook * addressBook() = 0;
+ virtual Subaddress * subaddress() = 0;
+diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp
+index e4e02c782..618c43cee 100644
+--- a/src/wallet/wallet2.cpp
++++ b/src/wallet/wallet2.cpp
+@@ -1010,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;
+@@ -1105,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;
+@@ -1222,6 +1222,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),
+@@ -1877,6 +1882,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())
+@@ -2184,11 +2192,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"));
+@@ -2200,7 +2208,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;
+@@ -2457,6 +2465,14 @@ 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();
++ LOG_PRINT_L2("Adding received tx " << txid << " to background sync data (idx=" << bgs_idx << ")");
++ m_background_sync_data.txs.insert({txid, background_synced_tx{bgs_idx, tx, o_indices, height, ts, double_spend_seen}});
++ }
+ }
+
+ for(size_t o: outs)
+@@ -2482,7 +2498,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
+@@ -2672,10 +2688,17 @@ 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();
++ LOG_PRINT_L2("Adding spent tx " << txid << " to background sync data (idx=" << bgs_idx << ")");
++ m_background_sync_data.txs.insert({txid, background_synced_tx{bgs_idx, tx, o_indices, height, ts, double_spend_seen}});
++ }
+ }
+ }
+
+- 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;
+@@ -2689,7 +2712,19 @@ 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();
++ LOG_PRINT_L2("Adding plausible spent tx " << txid << " to background sync data (idx=" << bgs_idx << ")");
++ m_background_sync_data.txs.insert({txid, background_synced_tx{bgs_idx, tx, o_indices, height, ts, double_spend_seen}});
++ }
+ }
+ }
+ }
+@@ -2699,7 +2734,16 @@ 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();
++ LOG_PRINT_L2("Adding plausible spent tx " << txid << " to background sync data (idx=" << bgs_idx << ")");
++ m_background_sync_data.txs.insert({txid, background_synced_tx{bgs_idx, tx, o_indices, height, ts, double_spend_seen}});
++ }
++ }
+ }
+ }
+ }
+@@ -3072,8 +3116,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;
+
+ {
+@@ -3100,7 +3144,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)
+ {
+@@ -3612,6 +3656,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)
+ {
+@@ -4182,6 +4229,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)));
+ }
+@@ -4267,6 +4316,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();
+
+@@ -4343,6 +4400,9 @@ void wallet2::handle_reorg(uint64_t height, std::map<std::pair<uint64_t, uint64_
+ 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());
+ }
+@@ -4352,6 +4412,7 @@ bool wallet2::deinit()
+ if(m_is_initialized) {
+ m_is_initialized = false;
+ unlock_keys_file();
++ unlock_background_keys_file();
+ m_account.deinit();
+ }
+ return true;
+@@ -4378,6 +4439,9 @@ 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.first_refresh_done = false;
++ m_background_sync_data.start_height = 0;
++ m_background_sync_data.txs.clear();
+ return true;
+ }
+ //----------------------------------------------------------------------------------------------------
+@@ -4396,13 +4460,32 @@ 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.first_refresh_done = false;
++ m_background_sync_data.start_height = 0;
++ m_background_sync_data.txs.clear();
+
+ 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<serializable_map<std::string, std::string>, std::vector<std::string>>();
++}
++//----------------------------------------------------------------------------------------------------
+ /*!
+ * \brief Stores wallet information to wallet file.
+ * \param keys_file_name Name of wallet file
+@@ -4414,16 +4497,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);
+@@ -4435,26 +4537,27 @@ 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);
+@@ -4589,6 +4692,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());
+
+@@ -4646,6 +4752,13 @@ 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());
++ }
++
++
+ value2.SetInt(m_polyseed ? 1 : 0);
+ json.AddMember("polyseed", value2, json.GetAllocator());
+
+@@ -4675,13 +4788,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);
+@@ -4740,8 +4921,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())
+ {
+@@ -4779,6 +4976,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;
+@@ -4796,6 +4994,7 @@ bool wallet2::load_keys_buf(const std::string& keys_buf, const epee::wipeable_st
+ m_credits_target = 0;
+ m_enable_multisig = false;
+ m_allow_mismatched_daemon_version = false;
++ m_custom_background_key = boost::none;
+ m_polyseed = false;
+ }
+ else if(json.IsObject())
+@@ -5034,6 +5233,40 @@ 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, polyseed, int, Int, false, false);
++
++ 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");
++ }
++
+ m_polyseed = field_polyseed;
+ }
+ else
+@@ -5098,12 +5331,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;
+ }
+@@ -5118,11 +5356,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;
+ }
+@@ -5140,7 +5379,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;
+@@ -5157,9 +5396,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())
+ {
+@@ -5184,6 +5436,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;
+ }
+
+@@ -5195,9 +5448,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);
+@@ -5915,11 +6166,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)
+@@ -5953,6 +6223,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 + config::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;
+@@ -6188,10 +6468,135 @@ 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);
++
++ if (!m_persistent_rpc_client_id)
++ set_rpc_client_secret_key(rct::rct2sk(rct::skGen()));
++
++ 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::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;
++ if (m_background_sync_type == BackgroundSyncReusePassword)
++ {
++ 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)
++ {
++ MDEBUG("Background keys file not found, restoring");
++ store_background_keys(m_custom_background_key.get());
++ }
++
++ 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;
++ }
++
++ MDEBUG("Loading background cache");
++
++ // 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;
++
++ cryptonote::account_base account = m_account;
++ account.forget_spend_key();
++ background_w2->m_account = account;
++
++ // 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*/);
++ } else {
++ THROW_WALLET_EXCEPTION(error::wallet_internal_error, "unknown background sync type");
++ }
++}
++//----------------------------------------------------------------------------------------------------
++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)
+ {
+@@ -6205,7 +6610,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);
+ }
+
+@@ -6218,7 +6623,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;
+@@ -6459,6 +6864,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();
+@@ -6552,6 +6972,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()
+@@ -6569,7 +7005,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;
+ }
+@@ -8645,6 +9081,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
+@@ -13980,6 +14444,408 @@ 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>> 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>& l, const std::pair<crypto::hash, background_synced_tx>& 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");
++
++ 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");
++
++ 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;
+diff --git a/src/wallet/wallet2.h b/src/wallet/wallet2.h
+index b540eff6b..bfe3a4f19 100644
+--- a/src/wallet/wallet2.h
++++ b/src/wallet/wallet2.h
+@@ -257,6 +257,20 @@ private:
+ BackgroundMiningNo = 2,
+ };
+
++ enum BackgroundSyncType {
++ BackgroundSyncOff = 0,
++ BackgroundSyncReusePassword = 1,
++ BackgroundSyncCustomPassword = 2,
++ };
++
++ static BackgroundSyncType background_sync_type_from_str(const std::string &background_sync_type_str)
++ {
++ if (background_sync_type_str == "off") return BackgroundSyncOff;
++ if (background_sync_type_str == "reuse-wallet-password") return BackgroundSyncReusePassword;
++ if (background_sync_type_str == "custom-background-password") return BackgroundSyncCustomPassword;
++ throw std::logic_error("Unknown background sync type");
++ };
++
+ enum ExportFormat {
+ Binary = 0,
+ Ascii,
+@@ -283,7 +297,12 @@ private:
+ //! Just parses variables.
+ static std::unique_ptr<wallet2> make_dummy(const boost::program_options::variables_map& vm, bool unattended, const std::function<boost::optional<password_container>(const char *, bool)> &password_prompter);
+
+- static bool verify_password(const std::string& keys_file_name, const epee::wipeable_string& password, bool no_spend_key, hw::device &hwdev, uint64_t kdf_rounds);
++ static bool 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 = crypto::null_skey;
++ return verify_password(keys_file_name, password, no_spend_key, hwdev, kdf_rounds, spend_key);
++ };
++ static bool 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);
+ static bool query_device(hw::device::device_type& device_type, const std::string& keys_file_name, const epee::wipeable_string& password, uint64_t kdf_rounds = 1);
+
+ wallet2(cryptonote::network_type nettype = cryptonote::MAINNET, uint64_t kdf_rounds = 1, bool unattended = false, std::unique_ptr<epee::net_utils::http::http_client_factory> http_client_factory = std::unique_ptr<epee::net_utils::http::http_client_factory>(new net::http::client_factory()));
+@@ -793,6 +812,54 @@ private:
+ END_SERIALIZE()
+ };
+
++ struct background_synced_tx
++ {
++ uint64_t index_in_background_sync_data;
++ cryptonote::transaction tx;
++ std::vector<uint64_t> output_indices;
++ uint64_t height;
++ uint64_t block_timestamp;
++ bool double_spend_seen;
++
++ BEGIN_SERIALIZE_OBJECT()
++ VERSION_FIELD(0)
++ VARINT_FIELD(index_in_background_sync_data)
++
++ // prune tx; don't need to keep signature data
++ if (!tx.serialize_base(ar))
++ return false;
++
++ FIELD(output_indices)
++ VARINT_FIELD(height)
++ VARINT_FIELD(block_timestamp)
++ FIELD(double_spend_seen)
++ END_SERIALIZE()
++ };
++
++ struct background_sync_data_t
++ {
++ bool first_refresh_done;
++ uint64_t start_height;
++ serializable_unordered_map<crypto::hash, background_synced_tx> txs;
++
++ // Relevant wallet settings
++ uint64_t wallet_refresh_from_block_height;
++ size_t subaddress_lookahead_major;
++ size_t subaddress_lookahead_minor;
++ RefreshType wallet_refresh_type;
++
++ BEGIN_SERIALIZE_OBJECT()
++ VERSION_FIELD(0)
++ FIELD(first_refresh_done)
++ FIELD(start_height)
++ FIELD(txs)
++ FIELD(wallet_refresh_from_block_height)
++ VARINT_FIELD(subaddress_lookahead_major)
++ VARINT_FIELD(subaddress_lookahead_minor)
++ VARINT_FIELD(wallet_refresh_type)
++ END_SERIALIZE()
++ };
++
+ typedef std::tuple<uint64_t, crypto::public_key, rct::key> get_outs_entry;
+
+ struct parsed_block
+@@ -989,7 +1056,8 @@ private:
+ /*!
+ * \brief verifies given password is correct for default wallet keys file
+ */
+- bool verify_password(const epee::wipeable_string& password);
++ bool verify_password(const epee::wipeable_string& password) {crypto::secret_key key = crypto::null_skey; return verify_password(password, key);};
++ bool verify_password(const epee::wipeable_string& password, crypto::secret_key &spend_key_out);
+ cryptonote::account_base& get_account(){return m_account;}
+ const cryptonote::account_base& get_account()const{return m_account;}
+
+@@ -1093,6 +1161,7 @@ private:
+ cryptonote::network_type nettype() const { return m_nettype; }
+ bool watch_only() const { return m_watch_only; }
+ bool multisig(bool *ready = NULL, uint32_t *threshold = NULL, uint32_t *total = NULL) const;
++ bool is_background_wallet() const { return m_is_background_wallet; }
+ bool has_multisig_partial_key_images() const;
+ bool has_unknown_key_images() const;
+ bool get_multisig_seed(epee::wipeable_string& seed, const epee::wipeable_string &passphrase = std::string()) const;
+@@ -1300,6 +1369,11 @@ private:
+ return;
+ }
+ a & m_has_ever_refreshed_from_node;
++ if ((m_background_sync_type == BackgroundSyncCustomPassword && m_is_background_wallet) ||
++ (m_background_sync_type == BackgroundSyncReusePassword && !m_is_background_wallet))
++ {
++ a & m_background_sync_data;
++ }
+ }
+
+ BEGIN_SERIALIZE_OBJECT()
+@@ -1336,6 +1410,11 @@ private:
+ return true;
+ }
+ FIELD(m_has_ever_refreshed_from_node)
++ if ((m_background_sync_type == BackgroundSyncCustomPassword && m_is_background_wallet) ||
++ (m_background_sync_type == BackgroundSyncReusePassword && !m_is_background_wallet))
++ {
++ FIELD(m_background_sync_data)
++ }
+ END_SERIALIZE()
+
+ /*!
+@@ -1351,6 +1430,8 @@ private:
+ * \return Whether path is valid format
+ */
+ static bool wallet_valid_path_format(const std::string& file_path);
++ static std::string make_background_wallet_file_name(const std::string &wallet_file);
++ static std::string make_background_keys_file_name(const std::string &wallet_file);
+ static bool parse_long_payment_id(const std::string& payment_id_str, crypto::hash& payment_id);
+ static bool parse_short_payment_id(const std::string& payment_id_str, crypto::hash8& payment_id);
+ static bool parse_payment_id(const std::string& payment_id_str, crypto::hash& payment_id);
+@@ -1399,6 +1480,9 @@ private:
+ void ignore_outputs_below(uint64_t value) { m_ignore_outputs_below = value; }
+ bool track_uses() const { return m_track_uses; }
+ void track_uses(bool value) { m_track_uses = value; }
++ BackgroundSyncType background_sync_type() const { return m_background_sync_type; }
++ void setup_background_sync(BackgroundSyncType background_sync_type, const epee::wipeable_string &wallet_password, const boost::optional<epee::wipeable_string> &background_cache_password);
++ bool is_background_syncing() const { return m_background_syncing; }
+ bool show_wallet_name_when_locked() const { return m_show_wallet_name_when_locked; }
+ void show_wallet_name_when_locked(bool value) { m_show_wallet_name_when_locked = value; }
+ BackgroundMiningSetupType setup_background_mining() const { return m_setup_background_mining; }
+@@ -1425,6 +1509,7 @@ private:
+ 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; };
+
+ bool get_tx_key_cached(const crypto::hash &txid, crypto::secret_key &tx_key, std::vector<crypto::secret_key> &additional_tx_keys) const;
+ void set_tx_key(const crypto::hash &txid, const crypto::secret_key &tx_key, const std::vector<crypto::secret_key> &additional_tx_keys, const boost::optional<cryptonote::account_public_address> &single_destination_subaddress = boost::none);
+@@ -1714,6 +1799,9 @@ private:
+ uint64_t get_bytes_sent() const;
+ uint64_t get_bytes_received() const;
+
++ void start_background_sync();
++ void stop_background_sync(const epee::wipeable_string &wallet_password, const crypto::secret_key &spend_secret_key = crypto::null_skey);
++
+ // MMS -------------------------------------------------------------------------------------------------
+ mms::message_store& get_message_store() { return m_message_store; };
+ const mms::message_store& get_message_store() const { return m_message_store; };
+@@ -1749,6 +1837,9 @@ private:
+ * \return Whether it was successful.
+ */
+ bool store_keys(const std::string& keys_file_name, const epee::wipeable_string& password, bool watch_only = false);
++ bool store_keys(const std::string& keys_file_name, const crypto::chacha_key& key, bool watch_only = false, bool background_keys_file = false);
++ boost::optional<wallet2::keys_file_data> get_keys_file_data(const crypto::chacha_key& key, bool watch_only = false, bool background_keys_file = false);
++ bool store_keys_file_data(const std::string& keys_file_name, wallet2::keys_file_data &keys_file_data, bool background_keys_file = false);
+ /*!
+ * \brief Load wallet keys information from wallet file.
+ * \param keys_file_name Name of wallet file
+@@ -1762,6 +1853,7 @@ private:
+ */
+ 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 load_wallet_cache(const bool use_fs, const std::string& cache_buf = "");
+ 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);
+ 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);
+@@ -1770,6 +1862,7 @@ private:
+ 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();
+ void pull_blocks(bool first, bool try_incremental, uint64_t start_height, uint64_t& blocks_start_height, const std::list<crypto::hash> &short_chain_history, std::vector<cryptonote::block_complete_entry> &blocks, std::vector<cryptonote::COMMAND_RPC_GET_BLOCKS_FAST::block_output_indices> &o_indices, uint64_t &current_height);
+ void pull_hashes(uint64_t start_height, uint64_t& blocks_start_height, const std::list<crypto::hash> &short_chain_history, std::vector<crypto::hash> &hashes);
+ void fast_refresh(uint64_t stop_height, uint64_t &blocks_start_height, std::list<crypto::hash> &short_chain_history, bool force = false);
+@@ -1821,10 +1914,23 @@ private:
+ bool get_ring(const crypto::chacha_key &key, const crypto::key_image &key_image, std::vector<uint64_t> &outs);
+ crypto::chacha_key get_ringdb_key();
+ void setup_keys(const epee::wipeable_string &password);
++ const crypto::chacha_key get_cache_key();
++ void verify_password_with_cached_key(const epee::wipeable_string &password);
++ void verify_password_with_cached_key(const crypto::chacha_key &key);
+ size_t get_transfer_details(const crypto::key_image &ki) const;
+ tx_entry_data get_tx_entries(const std::unordered_set<crypto::hash> &txids);
+ void sort_scan_tx_entries(std::vector<process_tx_entry_t> &unsorted_tx_entries);
+ void process_scan_txs(const tx_entry_data &txs_to_scan, const tx_entry_data &txs_to_reprocess, const std::unordered_set<crypto::hash> &tx_hashes_to_reprocess, detached_blockchain_data &dbd);
++ void write_background_sync_wallet(const epee::wipeable_string &wallet_password, const epee::wipeable_string &background_cache_password);
++ void process_background_cache_on_open();
++ void process_background_cache(const background_sync_data_t &background_sync_data, const hashchain &background_chain, uint64_t last_block_reward);
++ void reset_background_sync_data(background_sync_data_t &background_sync_data);
++ void store_background_cache(const crypto::chacha_key &custom_background_key, const bool do_reset_background_sync_data = true);
++ void store_background_keys(const crypto::chacha_key &custom_background_key);
++
++ bool lock_background_keys_file(const std::string &background_keys_file);
++ bool unlock_background_keys_file();
++ bool is_background_keys_file_locked() const;
+
+ void register_devices();
+ hw::device& lookup_device(const std::string & device_descriptor);
+@@ -1940,6 +2046,11 @@ private:
+ 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;
++ bool m_processing_background_cache;
++ background_sync_data_t m_background_sync_data;
+ bool m_show_wallet_name_when_locked;
+ uint32_t m_inactivity_lock_timeout;
+ BackgroundMiningSetupType m_setup_background_mining;
+@@ -1985,6 +2096,7 @@ private:
+
+ uint64_t m_last_block_reward;
+ std::unique_ptr<tools::file_locker> m_keys_file_locker;
++ std::unique_ptr<tools::file_locker> m_background_keys_file_locker;
+
+ mms::message_store m_message_store;
+ bool m_original_keys_available;
+@@ -1992,6 +2104,7 @@ private:
+ crypto::secret_key m_original_view_secret_key;
+
+ crypto::chacha_key m_cache_key;
++ boost::optional<crypto::chacha_key> m_custom_background_key = boost::none;
+ std::shared_ptr<wallet_keys_unlocker> m_encrypt_keys_after_refresh;
+
+ bool m_unattended;
+@@ -2025,6 +2138,8 @@ BOOST_CLASS_VERSION(tools::wallet2::signed_tx_set, 1)
+ BOOST_CLASS_VERSION(tools::wallet2::tx_construction_data, 4)
+ BOOST_CLASS_VERSION(tools::wallet2::pending_tx, 3)
+ BOOST_CLASS_VERSION(tools::wallet2::multisig_sig, 1)
++BOOST_CLASS_VERSION(tools::wallet2::background_synced_tx, 0)
++BOOST_CLASS_VERSION(tools::wallet2::background_sync_data_t, 0)
+
+ namespace boost
+ {
+@@ -2523,6 +2638,29 @@ namespace boost
+ return;
+ a & x.multisig_sigs;
+ }
++
++ template <class Archive>
++ inline void serialize(Archive& a, tools::wallet2::background_synced_tx &x, const boost::serialization::version_type ver)
++ {
++ a & x.index_in_background_sync_data;
++ a & x.tx;
++ a & x.output_indices;
++ a & x.height;
++ a & x.block_timestamp;
++ a & x.double_spend_seen;
++ }
++
++ template <class Archive>
++ inline void serialize(Archive& a, tools::wallet2::background_sync_data_t &x, const boost::serialization::version_type ver)
++ {
++ a & x.first_refresh_done;
++ a & x.start_height;
++ a & x.txs.parent();
++ a & x.wallet_refresh_from_block_height;
++ a & x.subaddress_lookahead_major;
++ a & x.subaddress_lookahead_minor;
++ a & x.wallet_refresh_type;
++ }
+ }
+ }
+
+diff --git a/src/wallet/wallet_errors.h b/src/wallet/wallet_errors.h
+index 1f7e1c75d..d17b721a9 100644
+--- a/src/wallet/wallet_errors.h
++++ b/src/wallet/wallet_errors.h
+@@ -63,6 +63,7 @@ namespace tools
+ // invalid_password
+ // invalid_priority
+ // invalid_multisig_seed
++ // invalid_spend_key
+ // refresh_error *
+ // acc_outs_lookup_error
+ // block_parse_error
+@@ -96,6 +97,9 @@ namespace tools
+ // wallet_files_doesnt_correspond
+ // scan_tx_error *
+ // wont_reprocess_recent_txs_via_untrusted_daemon
++ // background_sync_error *
++ // background_wallet_already_open
++ // background_custom_password_same_as_wallet_password
+ //
+ // * - class with protected ctor
+
+@@ -303,6 +307,16 @@ namespace tools
+ std::string to_string() const { return wallet_logic_error::to_string(); }
+ };
+
++ struct invalid_spend_key : public wallet_logic_error
++ {
++ explicit invalid_spend_key(std::string&& loc)
++ : wallet_logic_error(std::move(loc), "invalid spend key")
++ {
++ }
++
++ std::string to_string() const { return wallet_logic_error::to_string(); }
++ };
++
+ //----------------------------------------------------------------------------------------------------
+ struct invalid_pregenerated_random : public wallet_logic_error
+ {
+@@ -944,6 +958,31 @@ namespace tools
+ }
+ };
+ //----------------------------------------------------------------------------------------------------
++ struct background_sync_error : public wallet_logic_error
++ {
++ protected:
++ explicit background_sync_error(std::string&& loc, const std::string& message)
++ : wallet_logic_error(std::move(loc), message)
++ {
++ }
++ };
++ //----------------------------------------------------------------------------------------------------
++ struct background_wallet_already_open : public background_sync_error
++ {
++ explicit background_wallet_already_open(std::string&& loc, const std::string& background_wallet_file)
++ : background_sync_error(std::move(loc), "background wallet " + background_wallet_file + " is already opened by another wallet program")
++ {
++ }
++ };
++ //----------------------------------------------------------------------------------------------------
++ struct background_custom_password_same_as_wallet_password : public background_sync_error
++ {
++ explicit background_custom_password_same_as_wallet_password(std::string&& loc)
++ : background_sync_error(std::move(loc), "custom background password must be different than wallet password")
++ {
++ }
++ };
++ //----------------------------------------------------------------------------------------------------
+
+ #if !defined(_MSC_VER)
+
+diff --git a/src/wallet/wallet_rpc_server.cpp b/src/wallet/wallet_rpc_server.cpp
+index 4e42d64cd..822fc828b 100644
+--- a/src/wallet/wallet_rpc_server.cpp
++++ b/src/wallet/wallet_rpc_server.cpp
+@@ -73,6 +73,54 @@ using namespace epee;
+ } \
+ } while(0)
+
++#define CHECK_IF_BACKGROUND_SYNCING() \
++ do \
++ { \
++ if (!m_wallet) { return not_open(er); } \
++ if (m_wallet->is_background_wallet()) \
++ { \
++ er.code = WALLET_RPC_ERROR_CODE_IS_BACKGROUND_WALLET; \
++ er.message = "This command is disabled for background wallets."; \
++ return false; \
++ } \
++ if (m_wallet->is_background_syncing()) \
++ { \
++ er.code = WALLET_RPC_ERROR_CODE_IS_BACKGROUND_SYNCING; \
++ er.message = "This command is disabled while background syncing. Stop background syncing to use this command."; \
++ return false; \
++ } \
++ } while(0)
++
++#define PRE_VALIDATE_BACKGROUND_SYNC() \
++ do \
++ { \
++ if (!m_wallet) { return not_open(er); } \
++ if (m_restricted) \
++ { \
++ er.code = WALLET_RPC_ERROR_CODE_DENIED; \
++ er.message = "Command unavailable in restricted mode."; \
++ return false; \
++ } \
++ if (m_wallet->key_on_device()) \
++ { \
++ er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; \
++ er.message = "Command not supported by HW wallet"; \
++ return false; \
++ } \
++ if (m_wallet->multisig()) \
++ { \
++ er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; \
++ er.message = "Multisig wallet cannot enable background sync"; \
++ return false; \
++ } \
++ if (m_wallet->watch_only()) \
++ { \
++ er.code = WALLET_RPC_ERROR_CODE_WATCH_ONLY; \
++ er.message = "Watch-only wallet cannot enable background sync"; \
++ return false; \
++ } \
++ } while (0)
++
+ namespace
+ {
+ const command_line::arg_descriptor<std::string, true> arg_rpc_bind_port = {"rpc-bind-port", "Sets bind port for server"};
+@@ -291,6 +339,9 @@ namespace tools
+ {
+ if (!m_wallet)
+ return;
++ // Background mining can be toggled from the main wallet
++ if (m_wallet->is_background_wallet() || m_wallet->is_background_syncing())
++ return;
+
+ tools::wallet2::BackgroundMiningSetupType setup = m_wallet->setup_background_mining();
+ if (setup == tools::wallet2::BackgroundMiningNo)
+@@ -582,6 +633,7 @@ namespace tools
+ bool wallet_rpc_server::on_create_address(const wallet_rpc::COMMAND_RPC_CREATE_ADDRESS::request& req, wallet_rpc::COMMAND_RPC_CREATE_ADDRESS::response& res, epee::json_rpc::error& er, const connection_context *ctx)
+ {
+ if (!m_wallet) return not_open(er);
++ CHECK_IF_BACKGROUND_SYNCING();
+ try
+ {
+ if (req.count < 1 || req.count > 64) {
+@@ -619,6 +671,7 @@ namespace tools
+ bool wallet_rpc_server::on_label_address(const wallet_rpc::COMMAND_RPC_LABEL_ADDRESS::request& req, wallet_rpc::COMMAND_RPC_LABEL_ADDRESS::response& res, epee::json_rpc::error& er, const connection_context *ctx)
+ {
+ if (!m_wallet) return not_open(er);
++ CHECK_IF_BACKGROUND_SYNCING();
+ try
+ {
+ m_wallet->set_subaddress_label(req.index, req.label);
+@@ -681,6 +734,7 @@ namespace tools
+ bool wallet_rpc_server::on_create_account(const wallet_rpc::COMMAND_RPC_CREATE_ACCOUNT::request& req, wallet_rpc::COMMAND_RPC_CREATE_ACCOUNT::response& res, epee::json_rpc::error& er, const connection_context *ctx)
+ {
+ if (!m_wallet) return not_open(er);
++ CHECK_IF_BACKGROUND_SYNCING();
+ try
+ {
+ m_wallet->add_subaddress_account(req.label);
+@@ -698,6 +752,7 @@ namespace tools
+ bool wallet_rpc_server::on_label_account(const wallet_rpc::COMMAND_RPC_LABEL_ACCOUNT::request& req, wallet_rpc::COMMAND_RPC_LABEL_ACCOUNT::response& res, epee::json_rpc::error& er, const connection_context *ctx)
+ {
+ if (!m_wallet) return not_open(er);
++ CHECK_IF_BACKGROUND_SYNCING();
+ try
+ {
+ m_wallet->set_subaddress_label({req.account_index, 0}, req.label);
+@@ -713,6 +768,7 @@ namespace tools
+ bool wallet_rpc_server::on_get_account_tags(const wallet_rpc::COMMAND_RPC_GET_ACCOUNT_TAGS::request& req, wallet_rpc::COMMAND_RPC_GET_ACCOUNT_TAGS::response& res, epee::json_rpc::error& er, const connection_context *ctx)
+ {
+ if (!m_wallet) return not_open(er);
++ CHECK_IF_BACKGROUND_SYNCING();
+ const std::pair<std::map<std::string, std::string>, std::vector<std::string>> account_tags = m_wallet->get_account_tags();
+ for (const std::pair<const std::string, std::string>& p : account_tags.first)
+ {
+@@ -732,6 +788,7 @@ namespace tools
+ bool wallet_rpc_server::on_tag_accounts(const wallet_rpc::COMMAND_RPC_TAG_ACCOUNTS::request& req, wallet_rpc::COMMAND_RPC_TAG_ACCOUNTS::response& res, epee::json_rpc::error& er, const connection_context *ctx)
+ {
+ if (!m_wallet) return not_open(er);
++ CHECK_IF_BACKGROUND_SYNCING();
+ try
+ {
+ m_wallet->set_account_tag(req.accounts, req.tag);
+@@ -747,6 +804,7 @@ namespace tools
+ bool wallet_rpc_server::on_untag_accounts(const wallet_rpc::COMMAND_RPC_UNTAG_ACCOUNTS::request& req, wallet_rpc::COMMAND_RPC_UNTAG_ACCOUNTS::response& res, epee::json_rpc::error& er, const connection_context *ctx)
+ {
+ if (!m_wallet) return not_open(er);
++ CHECK_IF_BACKGROUND_SYNCING();
+ try
+ {
+ m_wallet->set_account_tag(req.accounts, "");
+@@ -762,6 +820,7 @@ namespace tools
+ bool wallet_rpc_server::on_set_account_tag_description(const wallet_rpc::COMMAND_RPC_SET_ACCOUNT_TAG_DESCRIPTION::request& req, wallet_rpc::COMMAND_RPC_SET_ACCOUNT_TAG_DESCRIPTION::response& res, epee::json_rpc::error& er, const connection_context *ctx)
+ {
+ if (!m_wallet) return not_open(er);
++ CHECK_IF_BACKGROUND_SYNCING();
+ try
+ {
+ m_wallet->set_account_tag_description(req.tag, req.description);
+@@ -792,6 +851,7 @@ namespace tools
+ bool wallet_rpc_server::on_freeze(const wallet_rpc::COMMAND_RPC_FREEZE::request& req, wallet_rpc::COMMAND_RPC_FREEZE::response& res, epee::json_rpc::error& er, const connection_context *ctx)
+ {
+ if (!m_wallet) return not_open(er);
++ CHECK_IF_BACKGROUND_SYNCING();
+ try
+ {
+ if (req.key_image.empty())
+@@ -820,6 +880,7 @@ namespace tools
+ bool wallet_rpc_server::on_thaw(const wallet_rpc::COMMAND_RPC_THAW::request& req, wallet_rpc::COMMAND_RPC_THAW::response& res, epee::json_rpc::error& er, const connection_context *ctx)
+ {
+ if (!m_wallet) return not_open(er);
++ CHECK_IF_BACKGROUND_SYNCING();
+ try
+ {
+ if (req.key_image.empty())
+@@ -848,6 +909,7 @@ namespace tools
+ bool wallet_rpc_server::on_frozen(const wallet_rpc::COMMAND_RPC_FROZEN::request& req, wallet_rpc::COMMAND_RPC_FROZEN::response& res, epee::json_rpc::error& er, const connection_context *ctx)
+ {
+ if (!m_wallet) return not_open(er);
++ CHECK_IF_BACKGROUND_SYNCING();
+ try
+ {
+ if (req.key_image.empty())
+@@ -875,6 +937,8 @@ namespace tools
+ //------------------------------------------------------------------------------------------------------------------------------
+ bool wallet_rpc_server::validate_transfer(const std::list<wallet_rpc::transfer_destination>& destinations, const std::string& payment_id, std::vector<cryptonote::tx_destination_entry>& dsts, std::vector<uint8_t>& extra, bool at_least_one_destination, epee::json_rpc::error& er)
+ {
++ CHECK_IF_BACKGROUND_SYNCING();
++
+ crypto::hash8 integrated_payment_id = crypto::null_hash8;
+ std::string extra_nonce;
+ for (auto it = destinations.begin(); it != destinations.end(); it++)
+@@ -1192,6 +1256,7 @@ namespace tools
+ }
+
+ CHECK_MULTISIG_ENABLED();
++ CHECK_IF_BACKGROUND_SYNCING();
+
+ cryptonote::blobdata blob;
+ if (!epee::string_tools::parse_hexstr_to_binbuff(req.unsigned_txset, blob))
+@@ -1273,6 +1338,7 @@ namespace tools
+ er.message = "command not supported by watch-only wallet";
+ return false;
+ }
++ CHECK_IF_BACKGROUND_SYNCING();
+ if(req.unsigned_txset.empty() && req.multisig_txset.empty())
+ {
+ er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR;
+@@ -1542,6 +1608,7 @@ namespace tools
+ }
+
+ CHECK_MULTISIG_ENABLED();
++ CHECK_IF_BACKGROUND_SYNCING();
+
+ try
+ {
+@@ -2091,6 +2158,7 @@ namespace tools
+ er.message = "The wallet is watch-only. Cannot retrieve seed.";
+ return false;
+ }
++ CHECK_IF_BACKGROUND_SYNCING();
+ if (!m_wallet->is_deterministic())
+ {
+ er.code = WALLET_RPC_ERROR_CODE_NON_DETERMINISTIC;
+@@ -2119,6 +2187,7 @@ namespace tools
+ er.message = "The wallet is watch-only. Cannot retrieve spend key.";
+ return false;
+ }
++ CHECK_IF_BACKGROUND_SYNCING();
+ epee::wipeable_string key = epee::to_hex::wipeable_string(m_wallet->get_account().get_keys().m_spend_secret_key);
+ res.key = std::string(key.data(), key.size());
+ }
+@@ -2140,6 +2209,7 @@ namespace tools
+ er.message = "Command unavailable in restricted mode.";
+ return false;
+ }
++ CHECK_IF_BACKGROUND_SYNCING();
+
+ try
+ {
+@@ -2153,6 +2223,79 @@ namespace tools
+ return true;
+ }
+ //------------------------------------------------------------------------------------------------------------------------------
++ bool wallet_rpc_server::on_setup_background_sync(const wallet_rpc::COMMAND_RPC_SETUP_BACKGROUND_SYNC::request& req, wallet_rpc::COMMAND_RPC_SETUP_BACKGROUND_SYNC::response& res, epee::json_rpc::error& er, const connection_context *ctx)
++ {
++ try
++ {
++ PRE_VALIDATE_BACKGROUND_SYNC();
++ const tools::wallet2::BackgroundSyncType background_sync_type = tools::wallet2::background_sync_type_from_str(req.background_sync_type);
++ boost::optional<epee::wipeable_string> background_cache_password = boost::none;
++ if (background_sync_type == tools::wallet2::BackgroundSyncCustomPassword)
++ background_cache_password = boost::optional<epee::wipeable_string>(req.background_cache_password);
++ m_wallet->setup_background_sync(background_sync_type, req.wallet_password, background_cache_password);
++ }
++ catch (...)
++ {
++ handle_rpc_exception(std::current_exception(), er, WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR);
++ return false;
++ }
++ return true;
++ }
++ //------------------------------------------------------------------------------------------------------------------------------
++ bool wallet_rpc_server::on_start_background_sync(const wallet_rpc::COMMAND_RPC_START_BACKGROUND_SYNC::request& req, wallet_rpc::COMMAND_RPC_START_BACKGROUND_SYNC::response& res, epee::json_rpc::error& er, const connection_context *ctx)
++ {
++ try
++ {
++ PRE_VALIDATE_BACKGROUND_SYNC();
++ m_wallet->start_background_sync();
++ }
++ catch (...)
++ {
++ handle_rpc_exception(std::current_exception(), er, WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR);
++ return false;
++ }
++ return true;
++ }
++ //------------------------------------------------------------------------------------------------------------------------------
++ bool wallet_rpc_server::on_stop_background_sync(const wallet_rpc::COMMAND_RPC_STOP_BACKGROUND_SYNC::request& req, wallet_rpc::COMMAND_RPC_STOP_BACKGROUND_SYNC::response& res, epee::json_rpc::error& er, const connection_context *ctx)
++ {
++ try
++ {
++ PRE_VALIDATE_BACKGROUND_SYNC();
++ crypto::secret_key spend_secret_key = crypto::null_skey;
++
++ // Load the spend key from seed if seed is provided
++ if (!req.seed.empty())
++ {
++ crypto::secret_key recovery_key;
++ std::string language;
++
++ if (!crypto::ElectrumWords::words_to_bytes(req.seed, recovery_key, language))
++ {
++ er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR;
++ er.message = "Electrum-style word list failed verification";
++ return false;
++ }
++
++ if (!req.seed_offset.empty())
++ recovery_key = cryptonote::decrypt_key(recovery_key, req.seed_offset);
++
++ // generate spend key
++ cryptonote::account_base account;
++ account.generate(recovery_key, true, false);
++ spend_secret_key = account.get_keys().m_spend_secret_key;
++ }
++
++ m_wallet->stop_background_sync(req.wallet_password, spend_secret_key);
++ }
++ catch (...)
++ {
++ handle_rpc_exception(std::current_exception(), er, WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR);
++ return false;
++ }
++ return true;
++ }
++ //------------------------------------------------------------------------------------------------------------------------------
+ bool wallet_rpc_server::on_sign(const wallet_rpc::COMMAND_RPC_SIGN::request& req, wallet_rpc::COMMAND_RPC_SIGN::response& res, epee::json_rpc::error& er, const connection_context *ctx)
+ {
+ if (!m_wallet) return not_open(er);
+@@ -2162,6 +2305,7 @@ namespace tools
+ er.message = "Command unavailable in restricted mode.";
+ return false;
+ }
++ CHECK_IF_BACKGROUND_SYNCING();
+
+ tools::wallet2::message_signature_type_t signature_type = tools::wallet2::sign_with_spend_key;
+ if (req.signature_type == "spend" || req.signature_type == "")
+@@ -2254,6 +2398,7 @@ namespace tools
+ er.message = "Command unavailable in restricted mode.";
+ return false;
+ }
++ CHECK_IF_BACKGROUND_SYNCING();
+
+ if (req.txids.size() != req.notes.size())
+ {
+@@ -2326,6 +2471,7 @@ namespace tools
+ er.message = "Command unavailable in restricted mode.";
+ return false;
+ }
++ CHECK_IF_BACKGROUND_SYNCING();
+
+ m_wallet->set_attribute(req.key, req.value);
+
+@@ -2353,6 +2499,7 @@ namespace tools
+ bool wallet_rpc_server::on_get_tx_key(const wallet_rpc::COMMAND_RPC_GET_TX_KEY::request& req, wallet_rpc::COMMAND_RPC_GET_TX_KEY::response& res, epee::json_rpc::error& er, const connection_context *ctx)
+ {
+ if (!m_wallet) return not_open(er);
++ CHECK_IF_BACKGROUND_SYNCING();
+
+ crypto::hash txid;
+ if (!epee::string_tools::hex_to_pod(req.txid, txid))
+@@ -2444,6 +2591,7 @@ namespace tools
+ bool wallet_rpc_server::on_get_tx_proof(const wallet_rpc::COMMAND_RPC_GET_TX_PROOF::request& req, wallet_rpc::COMMAND_RPC_GET_TX_PROOF::response& res, epee::json_rpc::error& er, const connection_context *ctx)
+ {
+ if (!m_wallet) return not_open(er);
++ CHECK_IF_BACKGROUND_SYNCING();
+
+ crypto::hash txid;
+ if (!epee::string_tools::hex_to_pod(req.txid, txid))
+@@ -2560,6 +2708,7 @@ namespace tools
+ bool wallet_rpc_server::on_get_reserve_proof(const wallet_rpc::COMMAND_RPC_GET_RESERVE_PROOF::request& req, wallet_rpc::COMMAND_RPC_GET_RESERVE_PROOF::response& res, epee::json_rpc::error& er, const connection_context *ctx)
+ {
+ if (!m_wallet) return not_open(er);
++ CHECK_IF_BACKGROUND_SYNCING();
+
+ boost::optional<std::pair<uint32_t, uint64_t>> account_minreserve;
+ if (!req.all)
+@@ -2802,6 +2951,7 @@ namespace tools
+ er.message = "command not supported by HW wallet";
+ return false;
+ }
++ CHECK_IF_BACKGROUND_SYNCING();
+
+ try
+ {
+@@ -2831,6 +2981,7 @@ namespace tools
+ er.message = "command not supported by HW wallet";
+ return false;
+ }
++ CHECK_IF_BACKGROUND_SYNCING();
+
+ cryptonote::blobdata blob;
+ if (!epee::string_tools::parse_hexstr_to_binbuff(req.outputs_data_hex, blob))
+@@ -2856,6 +3007,7 @@ namespace tools
+ bool wallet_rpc_server::on_export_key_images(const wallet_rpc::COMMAND_RPC_EXPORT_KEY_IMAGES::request& req, wallet_rpc::COMMAND_RPC_EXPORT_KEY_IMAGES::response& res, epee::json_rpc::error& er, const connection_context *ctx)
+ {
+ if (!m_wallet) return not_open(er);
++ CHECK_IF_BACKGROUND_SYNCING();
+ try
+ {
+ std::pair<uint64_t, std::vector<std::pair<crypto::key_image, crypto::signature>>> ski = m_wallet->export_key_images(req.all);
+@@ -2892,6 +3044,7 @@ namespace tools
+ er.message = "This command requires a trusted daemon.";
+ return false;
+ }
++ CHECK_IF_BACKGROUND_SYNCING();
+ try
+ {
+ std::vector<std::pair<crypto::key_image, crypto::signature>> ski;
+@@ -2960,6 +3113,7 @@ namespace tools
+ bool wallet_rpc_server::on_get_address_book(const wallet_rpc::COMMAND_RPC_GET_ADDRESS_BOOK_ENTRY::request& req, wallet_rpc::COMMAND_RPC_GET_ADDRESS_BOOK_ENTRY::response& res, epee::json_rpc::error& er, const connection_context *ctx)
+ {
+ if (!m_wallet) return not_open(er);
++ CHECK_IF_BACKGROUND_SYNCING();
+ const auto ab = m_wallet->get_address_book();
+ if (req.entries.empty())
+ {
+@@ -3005,6 +3159,7 @@ namespace tools
+ er.message = "Command unavailable in restricted mode.";
+ return false;
+ }
++ CHECK_IF_BACKGROUND_SYNCING();
+
+ cryptonote::address_parse_info info;
+ er.message = "";
+@@ -3047,6 +3202,7 @@ namespace tools
+ er.message = "Command unavailable in restricted mode.";
+ return false;
+ }
++ CHECK_IF_BACKGROUND_SYNCING();
+
+ const auto ab = m_wallet->get_address_book();
+ if (req.index >= ab.size())
+@@ -3109,6 +3265,7 @@ namespace tools
+ er.message = "Command unavailable in restricted mode.";
+ return false;
+ }
++ CHECK_IF_BACKGROUND_SYNCING();
+
+ const auto ab = m_wallet->get_address_book();
+ if (req.index >= ab.size())
+@@ -3179,6 +3336,7 @@ namespace tools
+ er.message = "Command unavailable in restricted mode.";
+ return false;
+ }
++ CHECK_IF_BACKGROUND_SYNCING();
+
+ std::unordered_set<crypto::hash> txids;
+ std::list<std::string>::const_iterator i = req.txids.begin();
+@@ -3218,6 +3376,7 @@ namespace tools
+ er.message = "Command unavailable in restricted mode.";
+ return false;
+ }
++ CHECK_IF_BACKGROUND_SYNCING();
+ try
+ {
+ m_wallet->rescan_spent();
+@@ -3482,6 +3641,7 @@ namespace tools
+ er.message = "Command unavailable in restricted mode.";
+ return false;
+ }
++ CHECK_IF_BACKGROUND_SYNCING();
+ if (m_wallet->verify_password(req.old_password))
+ {
+ try
+@@ -4009,6 +4169,7 @@ namespace tools
+ er.message = "wallet is watch-only and cannot be made multisig";
+ return false;
+ }
++ CHECK_IF_BACKGROUND_SYNCING();
+
+ res.multisig_info = m_wallet->get_multisig_first_kex_msg();
+ return true;
+@@ -4036,6 +4197,7 @@ namespace tools
+ er.message = "wallet is watch-only and cannot be made multisig";
+ return false;
+ }
++ CHECK_IF_BACKGROUND_SYNCING();
+
+ try
+ {
+diff --git a/src/wallet/wallet_rpc_server.h b/src/wallet/wallet_rpc_server.h
+index 3308d1751..c2329aafe 100644
+--- a/src/wallet/wallet_rpc_server.h
++++ b/src/wallet/wallet_rpc_server.h
+@@ -160,6 +160,9 @@ namespace tools
+ MAP_JON_RPC_WE("set_log_categories", on_set_log_categories, wallet_rpc::COMMAND_RPC_SET_LOG_CATEGORIES)
+ MAP_JON_RPC_WE("estimate_tx_size_and_weight", on_estimate_tx_size_and_weight, wallet_rpc::COMMAND_RPC_ESTIMATE_TX_SIZE_AND_WEIGHT)
+ MAP_JON_RPC_WE("get_version", on_get_version, wallet_rpc::COMMAND_RPC_GET_VERSION)
++ MAP_JON_RPC_WE("setup_background_sync", on_setup_background_sync, wallet_rpc::COMMAND_RPC_SETUP_BACKGROUND_SYNC)
++ MAP_JON_RPC_WE("start_background_sync", on_start_background_sync, wallet_rpc::COMMAND_RPC_START_BACKGROUND_SYNC)
++ MAP_JON_RPC_WE("stop_background_sync", on_stop_background_sync, wallet_rpc::COMMAND_RPC_STOP_BACKGROUND_SYNC)
+ END_JSON_RPC_MAP()
+ END_URI_MAP2()
+
+@@ -251,6 +254,9 @@ namespace tools
+ bool on_set_log_categories(const wallet_rpc::COMMAND_RPC_SET_LOG_CATEGORIES::request& req, wallet_rpc::COMMAND_RPC_SET_LOG_CATEGORIES::response& res, epee::json_rpc::error& er, const connection_context *ctx = NULL);
+ bool on_estimate_tx_size_and_weight(const wallet_rpc::COMMAND_RPC_ESTIMATE_TX_SIZE_AND_WEIGHT::request& req, wallet_rpc::COMMAND_RPC_ESTIMATE_TX_SIZE_AND_WEIGHT::response& res, epee::json_rpc::error& er, const connection_context *ctx = NULL);
+ bool on_get_version(const wallet_rpc::COMMAND_RPC_GET_VERSION::request& req, wallet_rpc::COMMAND_RPC_GET_VERSION::response& res, epee::json_rpc::error& er, const connection_context *ctx = NULL);
++ bool on_setup_background_sync(const wallet_rpc::COMMAND_RPC_SETUP_BACKGROUND_SYNC::request& req, wallet_rpc::COMMAND_RPC_SETUP_BACKGROUND_SYNC::response& res, epee::json_rpc::error& er, const connection_context *ctx = NULL);
++ bool on_start_background_sync(const wallet_rpc::COMMAND_RPC_START_BACKGROUND_SYNC::request& req, wallet_rpc::COMMAND_RPC_START_BACKGROUND_SYNC::response& res, epee::json_rpc::error& er, const connection_context *ctx = NULL);
++ bool on_stop_background_sync(const wallet_rpc::COMMAND_RPC_STOP_BACKGROUND_SYNC::request& req, wallet_rpc::COMMAND_RPC_STOP_BACKGROUND_SYNC::response& res, epee::json_rpc::error& er, const connection_context *ctx = NULL);
+
+ //json rpc v2
+ bool on_query_key(const wallet_rpc::COMMAND_RPC_QUERY_KEY::request& req, wallet_rpc::COMMAND_RPC_QUERY_KEY::response& res, epee::json_rpc::error& er, const connection_context *ctx = NULL);
+diff --git a/src/wallet/wallet_rpc_server_commands_defs.h b/src/wallet/wallet_rpc_server_commands_defs.h
+index b6098d95c..a44b56ed6 100644
+--- a/src/wallet/wallet_rpc_server_commands_defs.h
++++ b/src/wallet/wallet_rpc_server_commands_defs.h
+@@ -2696,5 +2696,69 @@ namespace wallet_rpc
+ typedef epee::misc_utils::struct_init<response_t> response;
+ };
+
++ struct COMMAND_RPC_SETUP_BACKGROUND_SYNC
++ {
++ struct request_t
++ {
++ std::string background_sync_type;
++ std::string wallet_password;
++ std::string background_cache_password;
++
++ BEGIN_KV_SERIALIZE_MAP()
++ KV_SERIALIZE(background_sync_type)
++ KV_SERIALIZE(wallet_password)
++ KV_SERIALIZE_OPT(background_cache_password, (std::string)"")
++ END_KV_SERIALIZE_MAP()
++ };
++ typedef epee::misc_utils::struct_init<request_t> request;
++
++ struct response_t
++ {
++ BEGIN_KV_SERIALIZE_MAP()
++ END_KV_SERIALIZE_MAP()
++ };
++ typedef epee::misc_utils::struct_init<response_t> response;
++ };
++
++ struct COMMAND_RPC_START_BACKGROUND_SYNC
++ {
++ struct request_t
++ {
++ BEGIN_KV_SERIALIZE_MAP()
++ END_KV_SERIALIZE_MAP()
++ };
++ typedef epee::misc_utils::struct_init<request_t> request;
++
++ struct response_t
++ {
++ BEGIN_KV_SERIALIZE_MAP()
++ END_KV_SERIALIZE_MAP()
++ };
++ typedef epee::misc_utils::struct_init<response_t> response;
++ };
++
++ struct COMMAND_RPC_STOP_BACKGROUND_SYNC
++ {
++ struct request_t
++ {
++ std::string wallet_password;
++ std::string seed;
++ std::string seed_offset;
++
++ BEGIN_KV_SERIALIZE_MAP()
++ KV_SERIALIZE(wallet_password)
++ KV_SERIALIZE_OPT(seed, (std::string)"")
++ KV_SERIALIZE_OPT(seed_offset, (std::string)"")
++ END_KV_SERIALIZE_MAP()
++ };
++ typedef epee::misc_utils::struct_init<request_t> request;
++
++ struct response_t
++ {
++ BEGIN_KV_SERIALIZE_MAP()
++ END_KV_SERIALIZE_MAP()
++ };
++ typedef epee::misc_utils::struct_init<response_t> response;
++ };
+ }
+ }
+diff --git a/src/wallet/wallet_rpc_server_error_codes.h b/src/wallet/wallet_rpc_server_error_codes.h
+index 734229380..b964036bd 100644
+--- a/src/wallet/wallet_rpc_server_error_codes.h
++++ b/src/wallet/wallet_rpc_server_error_codes.h
+@@ -79,3 +79,5 @@
+ #define WALLET_RPC_ERROR_CODE_ZERO_AMOUNT -46
+ #define WALLET_RPC_ERROR_CODE_INVALID_SIGNATURE_TYPE -47
+ #define WALLET_RPC_ERROR_CODE_DISABLED -48
++#define WALLET_RPC_ERROR_CODE_IS_BACKGROUND_WALLET -49
++#define WALLET_RPC_ERROR_CODE_IS_BACKGROUND_SYNCING -50
+diff --git a/tests/functional_tests/transfer.py b/tests/functional_tests/transfer.py
+index 4063911f4..39ee86df3 100755
+--- a/tests/functional_tests/transfer.py
++++ b/tests/functional_tests/transfer.py
+@@ -30,6 +30,7 @@
+
+ from __future__ import print_function
+ import json
++import util_resources
+ import pprint
+ from deepdiff import DeepDiff
+ pp = pprint.PrettyPrinter(indent=2)
+@@ -46,6 +47,17 @@ seeds = [
+ 'dilute gutter certain antics pamphlet macro enjoy left slid guarded bogeys upload nineteen bomb jubilee enhanced irritate turnip eggs swung jukebox loudly reduce sedan slid',
+ ]
+
++def diff_transfers(actual_transfers, expected_transfers, ignore_order = True):
++ # The payments containers aren't ordered; re-scanning can lead to diff orders
++ diff = DeepDiff(actual_transfers, expected_transfers, ignore_order = ignore_order)
++ if diff != {}:
++ pp.pprint(diff)
++ assert diff == {}
++
++def diff_incoming_transfers(actual_transfers, expected_transfers):
++ # wallet2 m_transfers container is ordered and order should be the same across rescans
++ diff_transfers(actual_transfers, expected_transfers, ignore_order = False)
++
+ class TransferTest():
+ def run_test(self):
+ self.reset()
+@@ -63,6 +75,8 @@ class TransferTest():
+ self.check_is_key_image_spent()
+ self.check_scan_tx()
+ self.check_subtract_fee_from_outputs()
++ self.check_background_sync()
++ self.check_background_sync_reorg_recovery()
+
+ def reset(self):
+ print('Resetting blockchain')
+@@ -71,6 +85,387 @@ class TransferTest():
+ daemon.pop_blocks(res.height - 1)
+ daemon.flush_txpool()
+
++ def check_background_sync(self):
++ daemon = Daemon()
++
++ print('Testing background sync')
++
++ # Background sync type options
++ REUSE_PASSWORD = 'reuse-wallet-password'
++ CUSTOM_PASSWORD = 'custom-background-password'
++
++ # Some helper functions
++ def stop_with_wrong_inputs(wallet, wallet_password, seed = ''):
++ invalid = False
++ try: wallet.stop_background_sync(wallet_password = wallet_password, seed = seed)
++ except: invalid = True
++ assert invalid
++
++ def open_with_wrong_password(wallet, filename, password):
++ invalid_password = False
++ try: wallet.open_wallet(filename, password = password)
++ except: invalid_password = True
++ assert invalid_password
++
++ def restore_wallet(wallet, seed, filename = '', password = ''):
++ wallet.close_wallet()
++ if filename != '':
++ util_resources.remove_wallet_files(filename)
++ wallet.restore_deterministic_wallet(seed = seed, filename = filename, password = password)
++ wallet.auto_refresh(enable = False)
++ assert wallet.get_transfers() == {}
++
++ def assert_correct_transfers(wallet, expected_transfers, expected_inc_transfers, expected_balance):
++ diff_transfers(wallet.get_transfers(), expected_transfers)
++ diff_incoming_transfers(wallet.incoming_transfers(transfer_type = 'all'), expected_inc_transfers)
++ assert wallet.get_balance().balance == expected_balance
++
++ # Set up sender_wallet. Prepare to sweep single output to receiver.
++ # We're testing a sweep because it makes sure background sync can
++ # properly pick up txs which do not have a change output back to sender.
++ sender_wallet = self.wallet[0]
++ try: sender_wallet.close_wallet()
++ except: pass
++ sender_wallet.restore_deterministic_wallet(seed = seeds[0])
++ sender_wallet.auto_refresh(enable = False)
++ sender_wallet.refresh()
++ res = sender_wallet.incoming_transfers(transfer_type = 'available')
++ unlocked = [x for x in res.transfers if x.unlocked and x.amount > 0]
++ assert len(unlocked) > 0
++ ki = unlocked[0].key_image
++ amount = unlocked[0].amount
++ spent_txid = unlocked[0].tx_hash
++ sender_wallet.refresh()
++ res = sender_wallet.get_transfers()
++ out_len = 0 if 'out' not in res else len(res.out)
++ sender_starting_balance = sender_wallet.get_balance().balance
++
++ # set up receiver_wallet
++ receiver_wallet = self.wallet[1]
++ try: receiver_wallet.close_wallet()
++ except: pass
++ receiver_wallet.restore_deterministic_wallet(seed = seeds[1])
++ receiver_wallet.auto_refresh(enable = False)
++ receiver_wallet.refresh()
++ res = receiver_wallet.get_transfers()
++ in_len = 0 if 'in' not in res else len(res['in'])
++ receiver_starting_balance = receiver_wallet.get_balance().balance
++
++ # transfer from sender_wallet to receiver_wallet
++ dst = '44Kbx4sJ7JDRDV5aAhLJzQCjDz2ViLRduE3ijDZu3osWKBjMGkV1XPk4pfDUMqt1Aiezvephdqm6YD19GKFD9ZcXVUTp6BW'
++ res = sender_wallet.sweep_single(dst, key_image = ki)
++ assert len(res.tx_hash) == 32*2
++ txid = res.tx_hash
++ assert res.fee > 0
++ fee = res.fee
++ assert res.amount == amount - fee
++
++ expected_sender_balance = sender_starting_balance - amount
++ expected_receiver_balance = receiver_starting_balance + (amount - fee)
++
++ print('Checking background sync on outgoing wallet')
++ sender_wallet.setup_background_sync(background_sync_type = REUSE_PASSWORD)
++ sender_wallet.start_background_sync()
++ # Mine block to an uninvolved wallet
++ daemon.generateblocks('46r4nYSevkfBUMhuykdK3gQ98XDqDTYW1hNLaXNvjpsJaSbNtdXh1sKMsdVgqkaihChAzEy29zEDPMR3NHQvGoZCLGwTerK', 1)
++ # sender should still be able to scan the transfer normally because we
++ # spent an output that had a known key image
++ sender_wallet.refresh()
++ transfers = sender_wallet.get_transfers()
++ assert 'pending' not in transfers or len(transfers.pending) == 0
++ assert 'pool' not in transfers or len (transfers.pool) == 0
++ assert len(transfers.out) == out_len + 1
++ tx = [x for x in transfers.out if x.txid == txid]
++ assert len(tx) == 1
++ tx = tx[0]
++ assert tx.amount == amount - fee
++ assert tx.fee == fee
++ assert len(tx.destinations) == 1
++ assert tx.destinations[0].amount == amount - fee
++ assert tx.destinations[0].address == dst
++ incoming_transfers = sender_wallet.incoming_transfers(transfer_type = 'all')
++ assert len([x for x in incoming_transfers.transfers if x.tx_hash == spent_txid and x.key_image == ki and x.spent]) == 1
++ assert sender_wallet.get_balance().balance == expected_sender_balance
++
++ # Restore and check background syncing outgoing wallet
++ restore_wallet(sender_wallet, seeds[0])
++ sender_wallet.setup_background_sync(background_sync_type = REUSE_PASSWORD)
++ sender_wallet.start_background_sync()
++ sender_wallet.refresh()
++ for i, out_tx in enumerate(transfers.out):
++ if 'destinations' in out_tx:
++ del transfers.out[i]['destinations'] # destinations are not expected after wallet restore
++ # sender's balance should be higher because can't detect spends while
++ # background sync enabled, only receives
++ background_bal = sender_wallet.get_balance().balance
++ assert background_bal > expected_sender_balance
++ background_transfers = sender_wallet.get_transfers()
++ assert 'out' not in background_transfers or len(background_transfers.out) == 0
++ assert 'in' in background_transfers and len(background_transfers['in']) > 0
++ background_incoming_transfers = sender_wallet.incoming_transfers(transfer_type = 'all')
++ assert len(background_incoming_transfers) == len(incoming_transfers)
++ assert len([x for x in background_incoming_transfers.transfers if x.spent or x.key_image != '']) == 0
++ assert len([x for x in background_incoming_transfers.transfers if x.tx_hash == spent_txid]) == 1
++
++ # Try to stop background sync with the wrong seed
++ stop_with_wrong_inputs(sender_wallet, wallet_password = '', seed = seeds[1])
++
++ # Stop background sync and check transfers update correctly
++ sender_wallet.stop_background_sync(wallet_password = '', seed = seeds[0])
++ assert_correct_transfers(sender_wallet, transfers, incoming_transfers, expected_sender_balance)
++
++ # Check stopping a wallet with wallet files saved to disk
++ for background_sync_type in [REUSE_PASSWORD, CUSTOM_PASSWORD]:
++ restore_wallet(sender_wallet, seeds[0], 'test1', 'test_password')
++ background_cache_password = None if background_sync_type == REUSE_PASSWORD else 'background_password'
++ sender_wallet.setup_background_sync(background_sync_type = background_sync_type, wallet_password = 'test_password', background_cache_password = background_cache_password)
++ sender_wallet.start_background_sync()
++ sender_wallet.refresh()
++ assert_correct_transfers(sender_wallet, background_transfers, background_incoming_transfers, background_bal)
++ stop_with_wrong_inputs(sender_wallet, 'wrong_password')
++ sender_wallet.stop_background_sync(wallet_password = 'test_password')
++ assert_correct_transfers(sender_wallet, transfers, incoming_transfers, expected_sender_balance)
++
++ # Close wallet while background syncing, then reopen
++ for background_sync_type in [REUSE_PASSWORD, CUSTOM_PASSWORD]:
++ restore_wallet(sender_wallet, seeds[0], 'test1', 'test_password')
++ background_cache_password = None if background_sync_type == REUSE_PASSWORD else 'background_password'
++ sender_wallet.setup_background_sync(background_sync_type = background_sync_type, wallet_password = 'test_password', background_cache_password = background_cache_password)
++ sender_wallet.start_background_sync()
++ sender_wallet.refresh()
++ assert_correct_transfers(sender_wallet, background_transfers, background_incoming_transfers, background_bal)
++ sender_wallet.close_wallet()
++ open_with_wrong_password(sender_wallet, 'test1', 'wrong_password')
++ sender_wallet.open_wallet('test1', password = 'test_password')
++ # It should reopen with spend key loaded and correctly scan all transfers
++ assert_correct_transfers(sender_wallet, transfers, incoming_transfers, expected_sender_balance)
++
++ # Close wallet while syncing normally, then reopen
++ for background_sync_type in [REUSE_PASSWORD, CUSTOM_PASSWORD]:
++ restore_wallet(sender_wallet, seeds[0], 'test1', 'test_password')
++ background_cache_password = None if background_sync_type == REUSE_PASSWORD else 'background_password'
++ sender_wallet.setup_background_sync(background_sync_type = background_sync_type, wallet_password = 'test_password', background_cache_password = background_cache_password)
++ sender_wallet.refresh()
++ assert_correct_transfers(sender_wallet, transfers, incoming_transfers, expected_sender_balance)
++ sender_wallet.close_wallet()
++ open_with_wrong_password(sender_wallet, 'test1', 'wrong_password')
++ sender_wallet.open_wallet('test1', password = 'test_password')
++ assert_correct_transfers(sender_wallet, transfers, incoming_transfers, expected_sender_balance)
++
++ # Create background cache using custom password, then use it to sync, then reopen main wallet
++ for background_cache_password in ['background_password', '']:
++ restore_wallet(sender_wallet, seeds[0], 'test1', 'test_password')
++ assert not util_resources.file_exists('test1.background')
++ assert not util_resources.file_exists('test1.background.keys')
++ sender_wallet.setup_background_sync(background_sync_type = CUSTOM_PASSWORD, wallet_password = 'test_password', background_cache_password = background_cache_password)
++ assert util_resources.file_exists('test1.background')
++ assert util_resources.file_exists('test1.background.keys')
++ sender_wallet.close_wallet()
++ open_with_wrong_password(sender_wallet, 'test1.background', 'test_password')
++ sender_wallet.open_wallet('test1.background', password = background_cache_password)
++ sender_wallet.refresh()
++ assert_correct_transfers(sender_wallet, background_transfers, background_incoming_transfers, background_bal)
++ sender_wallet.close_wallet()
++ sender_wallet.open_wallet('test1', password = 'test_password')
++ assert_correct_transfers(sender_wallet, transfers, incoming_transfers, expected_sender_balance)
++
++ # Check that main wallet keeps background cache encrypted with custom password in sync
++ restore_wallet(sender_wallet, seeds[0], 'test1', 'test_password')
++ sender_wallet.setup_background_sync(background_sync_type = background_sync_type, wallet_password = 'test_password', background_cache_password = 'background_password')
++ sender_wallet.refresh()
++ assert_correct_transfers(sender_wallet, transfers, incoming_transfers, expected_sender_balance)
++ sender_wallet.close_wallet()
++ sender_wallet.open_wallet('test1.background', password = 'background_password')
++ assert_correct_transfers(sender_wallet, transfers, incoming_transfers, expected_sender_balance)
++
++ # Try using wallet password as custom background password
++ restore_wallet(sender_wallet, seeds[0], 'test1', 'test_password')
++ assert not util_resources.file_exists('test1.background')
++ assert not util_resources.file_exists('test1.background.keys')
++ same_password = False
++ try: sender_wallet.setup_background_sync(background_sync_type = CUSTOM_PASSWORD, wallet_password = 'test_password', background_cache_password = 'test_password')
++ except: same_password = True
++ assert same_password
++ assert not util_resources.file_exists('test1.background')
++ assert not util_resources.file_exists('test1.background.keys')
++
++ # Turn off background sync
++ for background_sync_type in [REUSE_PASSWORD, CUSTOM_PASSWORD]:
++ restore_wallet(sender_wallet, seeds[0], 'test1', 'test_password')
++ background_cache_password = None if background_sync_type == REUSE_PASSWORD else 'background_password'
++ sender_wallet.setup_background_sync(background_sync_type = background_sync_type, wallet_password = 'test_password', background_cache_password = background_cache_password)
++ if background_sync_type == CUSTOM_PASSWORD:
++ assert util_resources.file_exists('test1.background')
++ assert util_resources.file_exists('test1.background.keys')
++ sender_wallet.close_wallet()
++ assert util_resources.file_exists('test1.background')
++ assert util_resources.file_exists('test1.background.keys')
++ else:
++ assert not util_resources.file_exists('test1.background')
++ assert not util_resources.file_exists('test1.background.keys')
++ sender_wallet.close_wallet()
++ assert not util_resources.file_exists('test1.background')
++ assert not util_resources.file_exists('test1.background.keys')
++ sender_wallet.open_wallet('test1', password = 'test_password')
++ sender_wallet.setup_background_sync(background_sync_type = 'off', wallet_password = 'test_password')
++ assert not util_resources.file_exists('test1.background')
++ assert not util_resources.file_exists('test1.background.keys')
++ sender_wallet.close_wallet()
++ assert not util_resources.file_exists('test1.background')
++ assert not util_resources.file_exists('test1.background.keys')
++ sender_wallet.open_wallet('test1', password = 'test_password')
++
++ # Sanity check against outgoing wallet restored at height 0
++ sender_wallet.close_wallet()
++ sender_wallet.restore_deterministic_wallet(seed = seeds[0], restore_height = 0)
++ sender_wallet.refresh()
++ assert_correct_transfers(sender_wallet, transfers, incoming_transfers, expected_sender_balance)
++
++ print('Checking background sync on incoming wallet')
++ receiver_wallet.setup_background_sync(background_sync_type = REUSE_PASSWORD)
++ receiver_wallet.start_background_sync()
++ receiver_wallet.refresh()
++ transfers = receiver_wallet.get_transfers()
++ assert 'pending' not in transfers or len(transfers.pending) == 0
++ assert 'pool' not in transfers or len (transfers.pool) == 0
++ assert len(transfers['in']) == in_len + 1
++ tx = [x for x in transfers['in'] if x.txid == txid]
++ assert len(tx) == 1
++ tx = tx[0]
++ assert tx.amount == amount - fee
++ assert tx.fee == fee
++ incoming_transfers = receiver_wallet.incoming_transfers(transfer_type = 'all')
++ assert len([x for x in incoming_transfers.transfers if x.tx_hash == txid and x.key_image == '' and not x.spent]) == 1
++ assert receiver_wallet.get_balance().balance == expected_receiver_balance
++
++ # Restore and check background syncing incoming wallet
++ restore_wallet(receiver_wallet, seeds[1])
++ receiver_wallet.setup_background_sync(background_sync_type = REUSE_PASSWORD)
++ receiver_wallet.start_background_sync()
++ receiver_wallet.refresh()
++ if 'out' in transfers:
++ for i, out_tx in enumerate(transfers.out):
++ if 'destinations' in out_tx:
++ del transfers.out[i]['destinations'] # destinations are not expected after wallet restore
++ background_bal = receiver_wallet.get_balance().balance
++ assert background_bal >= expected_receiver_balance
++ background_transfers = receiver_wallet.get_transfers()
++ assert 'out' not in background_transfers or len(background_transfers.out) == 0
++ assert 'in' in background_transfers and len(background_transfers['in']) > 0
++ background_incoming_transfers = receiver_wallet.incoming_transfers(transfer_type = 'all')
++ assert len(background_incoming_transfers) == len(incoming_transfers)
++ assert len([x for x in background_incoming_transfers.transfers if x.spent or x.key_image != '']) == 0
++ assert len([x for x in background_incoming_transfers.transfers if x.tx_hash == txid]) == 1
++
++ # Stop background sync and check transfers update correctly
++ receiver_wallet.stop_background_sync(wallet_password = '', seed = seeds[1])
++ diff_transfers(receiver_wallet.get_transfers(), transfers)
++ incoming_transfers = receiver_wallet.incoming_transfers(transfer_type = 'all')
++ assert len(background_incoming_transfers) == len(incoming_transfers)
++ assert len([x for x in incoming_transfers.transfers if x.tx_hash == txid and x.key_image != '' and not x.spent]) == 1
++ assert receiver_wallet.get_balance().balance == expected_receiver_balance
++
++ # Check a fresh incoming wallet with wallet files saved to disk and encrypted with password
++ restore_wallet(receiver_wallet, seeds[1], 'test2', 'test_password')
++ receiver_wallet.setup_background_sync(background_sync_type = REUSE_PASSWORD, wallet_password = 'test_password')
++ receiver_wallet.start_background_sync()
++ receiver_wallet.refresh()
++ assert_correct_transfers(receiver_wallet, background_transfers, background_incoming_transfers, background_bal)
++ stop_with_wrong_inputs(receiver_wallet, 'wrong_password')
++ receiver_wallet.stop_background_sync(wallet_password = 'test_password')
++ assert_correct_transfers(receiver_wallet, transfers, incoming_transfers, expected_receiver_balance)
++
++ # Close receiver's wallet while background sync is enabled then reopen
++ restore_wallet(receiver_wallet, seeds[1], 'test2', 'test_password')
++ receiver_wallet.setup_background_sync(background_sync_type = REUSE_PASSWORD, wallet_password = 'test_password')
++ receiver_wallet.start_background_sync()
++ receiver_wallet.refresh()
++ diff_transfers(receiver_wallet.get_transfers(), background_transfers)
++ diff_incoming_transfers(receiver_wallet.incoming_transfers(transfer_type = 'all'), background_incoming_transfers)
++ assert receiver_wallet.get_balance().balance == background_bal
++ receiver_wallet.close_wallet()
++ receiver_wallet.open_wallet('test2', password = 'test_password')
++ # It should reopen with spend key loaded and correctly scan all transfers
++ assert_correct_transfers(receiver_wallet, transfers, incoming_transfers, expected_receiver_balance)
++
++ # Sanity check against incoming wallet restored at height 0
++ receiver_wallet.close_wallet()
++ receiver_wallet.restore_deterministic_wallet(seed = seeds[1], restore_height = 0)
++ receiver_wallet.refresh()
++ assert_correct_transfers(receiver_wallet, transfers, incoming_transfers, expected_receiver_balance)
++
++ # Clean up
++ util_resources.remove_wallet_files('test1')
++ util_resources.remove_wallet_files('test2')
++ for i in range(2):
++ self.wallet[i].close_wallet()
++ self.wallet[i].restore_deterministic_wallet(seed = seeds[i])
++
++ def check_background_sync_reorg_recovery(self):
++ daemon = Daemon()
++
++ print('Testing background sync reorg recovery')
++
++ # Disconnect daemon from peers
++ daemon.out_peers(0)
++
++ # Background sync type options
++ REUSE_PASSWORD = 'reuse-wallet-password'
++ CUSTOM_PASSWORD = 'custom-background-password'
++
++ for background_sync_type in [REUSE_PASSWORD, CUSTOM_PASSWORD]:
++ # Set up wallet saved to disk
++ sender_wallet = self.wallet[0]
++ sender_wallet.close_wallet()
++ util_resources.remove_wallet_files('test1')
++ sender_wallet.restore_deterministic_wallet(seed = seeds[0], filename = 'test1', password = '')
++ sender_wallet.auto_refresh(enable = False)
++ sender_wallet.refresh()
++ sender_starting_balance = sender_wallet.get_balance().balance
++
++ # Send tx and mine a block
++ amount = 1000000000000
++ assert sender_starting_balance > amount
++ dst = {'address': '44Kbx4sJ7JDRDV5aAhLJzQCjDz2ViLRduE3ijDZu3osWKBjMGkV1XPk4pfDUMqt1Aiezvephdqm6YD19GKFD9ZcXVUTp6BW', 'amount': amount}
++ res = sender_wallet.transfer([dst])
++ assert len(res.tx_hash) == 32*2
++ txid = res.tx_hash
++
++ daemon.generateblocks('46r4nYSevkfBUMhuykdK3gQ98XDqDTYW1hNLaXNvjpsJaSbNtdXh1sKMsdVgqkaihChAzEy29zEDPMR3NHQvGoZCLGwTerK', 1)
++
++ # Make sure the wallet can see the tx
++ sender_wallet.refresh()
++ transfers = sender_wallet.get_transfers()
++ assert 'pool' not in transfers or len (transfers.pool) == 0
++ tx = [x for x in transfers.out if x.txid == txid]
++ assert len(tx) == 1
++ tx = tx[0]
++ assert sender_wallet.get_balance().balance < (sender_starting_balance - amount)
++
++ # Pop the block while background syncing
++ background_cache_password = None if background_sync_type == REUSE_PASSWORD else 'background_password'
++ sender_wallet.setup_background_sync(background_sync_type = background_sync_type, wallet_password = '', background_cache_password = background_cache_password)
++ sender_wallet.start_background_sync()
++ daemon.pop_blocks(1)
++ daemon.flush_txpool()
++
++ daemon.generateblocks('46r4nYSevkfBUMhuykdK3gQ98XDqDTYW1hNLaXNvjpsJaSbNtdXh1sKMsdVgqkaihChAzEy29zEDPMR3NHQvGoZCLGwTerK', 1)
++
++ # Make sure the wallet can no longer see the tx
++ sender_wallet.refresh()
++ sender_wallet.stop_background_sync(wallet_password = '', seed = seeds[0])
++ transfers = sender_wallet.get_transfers()
++ no_tx = [x for x in transfers.out if x.txid == txid]
++ assert len(no_tx) == 0
++ assert sender_wallet.get_balance().balance == sender_starting_balance
++
++ # Clean up
++ daemon.out_peers(12)
++ util_resources.remove_wallet_files('test1')
++ self.wallet[0].close_wallet()
++ self.wallet[0].restore_deterministic_wallet(seed = seeds[0])
++
++
+ def create(self):
+ print('Creating wallets')
+ self.wallet = [None] * len(seeds)
+@@ -840,12 +1235,6 @@ class TransferTest():
+
+ print('Testing scan_tx')
+
+- def diff_transfers(actual_transfers, expected_transfers):
+- diff = DeepDiff(actual_transfers, expected_transfers)
+- if diff != {}:
+- pp.pprint(diff)
+- assert diff == {}
+-
+ # set up sender_wallet
+ sender_wallet = self.wallet[0]
+ try: sender_wallet.close_wallet()
+diff --git a/tests/functional_tests/util_resources.py b/tests/functional_tests/util_resources.py
+index e030312da..3197187f4 100755
+--- a/tests/functional_tests/util_resources.py
++++ b/tests/functional_tests/util_resources.py
+@@ -37,6 +37,8 @@
+ from __future__ import print_function
+ import subprocess
+ import psutil
++import os
++import errno
+
+ def available_ram_gb():
+ ram_bytes = psutil.virtual_memory().available
+@@ -51,3 +53,26 @@ def get_time_pi_seconds(cores, app_dir='.'):
+ miliseconds = int(decoded)
+
+ return miliseconds / 1000.0
++
++def remove_file(name):
++ WALLET_DIRECTORY = os.environ['WALLET_DIRECTORY']
++ assert WALLET_DIRECTORY != ''
++ try:
++ os.unlink(WALLET_DIRECTORY + '/' + name)
++ except OSError as e:
++ if e.errno != errno.ENOENT:
++ raise
++
++def get_file_path(name):
++ WALLET_DIRECTORY = os.environ['WALLET_DIRECTORY']
++ assert WALLET_DIRECTORY != ''
++ return WALLET_DIRECTORY + '/' + name
++
++def remove_wallet_files(name):
++ for suffix in ['', '.keys', '.background', '.background.keys']:
++ remove_file(name + suffix)
++
++def file_exists(name):
++ WALLET_DIRECTORY = os.environ['WALLET_DIRECTORY']
++ assert WALLET_DIRECTORY != ''
++ return os.path.isfile(WALLET_DIRECTORY + '/' + name)
+diff --git a/tests/functional_tests/wallet.py b/tests/functional_tests/wallet.py
+index 1ad05c98f..8182cecb2 100755
+--- a/tests/functional_tests/wallet.py
++++ b/tests/functional_tests/wallet.py
+@@ -34,8 +34,7 @@
+
+ from __future__ import print_function
+ import sys
+-import os
+-import errno
++import util_resources
+
+ from framework.wallet import Wallet
+ from framework.daemon import Daemon
+@@ -54,24 +53,6 @@ class WalletTest():
+ self.change_password()
+ self.store()
+
+- def remove_file(self, name):
+- WALLET_DIRECTORY = os.environ['WALLET_DIRECTORY']
+- assert WALLET_DIRECTORY != ''
+- try:
+- os.unlink(WALLET_DIRECTORY + '/' + name)
+- except OSError as e:
+- if e.errno != errno.ENOENT:
+- raise
+-
+- def remove_wallet_files(self, name):
+- for suffix in ['', '.keys']:
+- self.remove_file(name + suffix)
+-
+- def file_exists(self, name):
+- WALLET_DIRECTORY = os.environ['WALLET_DIRECTORY']
+- assert WALLET_DIRECTORY != ''
+- return os.path.isfile(WALLET_DIRECTORY + '/' + name)
+-
+ def reset(self):
+ print('Resetting blockchain')
+ daemon = Daemon()
+@@ -333,7 +314,7 @@ class WalletTest():
+ try: wallet.close_wallet()
+ except: pass
+
+- self.remove_wallet_files('test1')
++ util_resources.remove_wallet_files('test1')
+
+ seed = 'velvet lymph giddy number token physics poetry unquoted nibs useful sabotage limits benches lifestyle eden nitrogen anvil fewest avoid batch vials washing fences goat unquoted'
+ res = wallet.restore_deterministic_wallet(seed = seed, filename = 'test1')
+@@ -359,7 +340,7 @@ class WalletTest():
+
+ wallet.close_wallet()
+
+- self.remove_wallet_files('test1')
++ util_resources.remove_wallet_files('test1')
+
+ def store(self):
+ print('Testing store')
+@@ -369,22 +350,26 @@ class WalletTest():
+ try: wallet.close_wallet()
+ except: pass
+
+- self.remove_wallet_files('test1')
++ util_resources.remove_wallet_files('test1')
+
+ seed = 'velvet lymph giddy number token physics poetry unquoted nibs useful sabotage limits benches lifestyle eden nitrogen anvil fewest avoid batch vials washing fences goat unquoted'
+ res = wallet.restore_deterministic_wallet(seed = seed, filename = 'test1')
+ assert res.address == '42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm'
+ assert res.seed == seed
+
+- self.remove_file('test1')
+- assert self.file_exists('test1.keys')
+- assert not self.file_exists('test1')
++ util_resources.remove_file('test1')
++ assert util_resources.file_exists('test1.keys')
++ assert not util_resources.file_exists('test1')
+ wallet.store()
+- assert self.file_exists('test1.keys')
+- assert self.file_exists('test1')
++ assert util_resources.file_exists('test1.keys')
++ assert util_resources.file_exists('test1')
+
+ wallet.close_wallet()
+- self.remove_wallet_files('test1')
++
++ wallet.open_wallet(filename = 'test1', password = '')
++ wallet.close_wallet()
++
++ util_resources.remove_wallet_files('test1')
+
+
+ if __name__ == '__main__':
+diff --git a/tests/unit_tests/wipeable_string.cpp b/tests/unit_tests/wipeable_string.cpp
+index ef6964f9e..25121a02e 100644
+--- a/tests/unit_tests/wipeable_string.cpp
++++ b/tests/unit_tests/wipeable_string.cpp
+@@ -211,3 +211,15 @@ TEST(wipeable_string, to_hex)
+ ASSERT_TRUE(epee::to_hex::wipeable_string(epee::span<const uint8_t>((const uint8_t*)"", 0)) == epee::wipeable_string(""));
+ ASSERT_TRUE(epee::to_hex::wipeable_string(epee::span<const uint8_t>((const uint8_t*)"abc", 3)) == epee::wipeable_string("616263"));
+ }
++
++TEST(wipeable_string, to_string)
++{
++ // Converting a wipeable_string to a string defeats the purpose of wipeable_string,
++ // but nice to know this works
++ std::string str;
++ {
++ epee::wipeable_string wipeable_str("foo");
++ str = std::string(wipeable_str.data(), wipeable_str.size());
++ }
++ ASSERT_TRUE(str == std::string("foo"));
++}
+diff --git a/utils/python-rpc/framework/wallet.py b/utils/python-rpc/framework/wallet.py
+index 1e10e1f86..ccf6ca93e 100644
+--- a/utils/python-rpc/framework/wallet.py
++++ b/utils/python-rpc/framework/wallet.py
+@@ -1138,3 +1138,38 @@ class Wallet(object):
+ 'id': '0'
+ }
+ return self.rpc.send_json_rpc_request(frozen)
++
++ def setup_background_sync(self, background_sync_type = 'off', wallet_password = '', background_cache_password = ''):
++ setup_background_sync = {
++ 'method': 'setup_background_sync',
++ 'jsonrpc': '2.0',
++ 'params' : {
++ 'background_sync_type': background_sync_type,
++ 'wallet_password': wallet_password,
++ 'background_cache_password': background_cache_password,
++ },
++ 'id': '0'
++ }
++ return self.rpc.send_json_rpc_request(setup_background_sync)
++
++ def start_background_sync(self):
++ start_background_sync = {
++ 'method': 'start_background_sync',
++ 'jsonrpc': '2.0',
++ 'params' : {},
++ 'id': '0'
++ }
++ return self.rpc.send_json_rpc_request(start_background_sync)
++
++ def stop_background_sync(self, wallet_password = '', seed = '', seed_offset = ''):
++ stop_background_sync = {
++ 'method': 'stop_background_sync',
++ 'jsonrpc': '2.0',
++ 'params' : {
++ 'wallet_password': wallet_password,
++ 'seed': seed,
++ 'seed_offset': seed_offset,
++ },
++ 'id': '0'
++ }
++ return self.rpc.send_json_rpc_request(stop_background_sync)
diff --git a/patches/monero/0002-airgap.patch b/patches/monero/0002-airgap.patch
new file mode 100644
index 0000000..c3ce71b
--- /dev/null
+++ b/patches/monero/0002-airgap.patch
@@ -0,0 +1,184 @@
+From 559780d053549f01e9153bf43bad24c80255117d Mon Sep 17 00:00:00 2001
+From: Czarek Nakamoto <cyjan@mrcyjanek.net>
+Date: Tue, 12 Mar 2024 10:09:50 +0100
+Subject: [PATCH] PATCH: airgap
+
+---
+ src/wallet/api/wallet.cpp | 23 ++++++++++++++++++++++
+ src/wallet/api/wallet.h | 2 ++
+ src/wallet/api/wallet2_api.h | 3 +++
+ src/wallet/wallet2.cpp | 38 ++++++++++++++++++++++++++++++------
+ src/wallet/wallet2.h | 1 +
+ 5 files changed, 61 insertions(+), 6 deletions(-)
+
+diff --git a/src/wallet/api/wallet.cpp b/src/wallet/api/wallet.cpp
+index 1a9c6f674..42887dced 100644
+--- a/src/wallet/api/wallet.cpp
++++ b/src/wallet/api/wallet.cpp
+@@ -1129,6 +1129,24 @@ uint64_t WalletImpl::unlockedBalance(uint32_t accountIndex) const
+ return m_wallet->unlocked_balance(accountIndex, false);
+ }
+
++uint64_t WalletImpl::viewOnlyBalance(uint32_t accountIndex, const std::vector<std::string> &key_images) const
++{
++ clearStatus();
++
++ std::vector<crypto::key_image> kis;
++ for (const auto &key_image : key_images) {
++ crypto::key_image ki;
++ if (!epee::string_tools::hex_to_pod(key_image, ki))
++ {
++ setStatusError(tr("failed to parse key image"));
++ return 0;
++ }
++ kis.push_back(ki);
++ }
++
++ return m_wallet->view_only_balance(accountIndex, kis);
++}
++
+ uint64_t WalletImpl::blockChainHeight() const
+ {
+ if(m_wallet->light_wallet()) {
+@@ -1291,6 +1309,11 @@ bool WalletImpl::submitTransaction(const string &fileName) {
+ return true;
+ }
+
++bool WalletImpl::hasUnknownKeyImages() const
++{
++ return m_wallet->has_unknown_key_images();
++}
++
+ bool WalletImpl::exportKeyImages(const string &filename, bool all)
+ {
+ if (m_wallet->watch_only())
+diff --git a/src/wallet/api/wallet.h b/src/wallet/api/wallet.h
+index 9acd2871f..05d065c5c 100644
+--- a/src/wallet/api/wallet.h
++++ b/src/wallet/api/wallet.h
+@@ -122,6 +122,7 @@ public:
+ bool setProxy(const std::string &address) override;
+ uint64_t balance(uint32_t accountIndex = 0) const override;
+ uint64_t unlockedBalance(uint32_t accountIndex = 0) const override;
++ uint64_t viewOnlyBalance(uint32_t accountIndex, const std::vector<std::string> &key_images) const override;
+ uint64_t blockChainHeight() const override;
+ uint64_t approximateBlockChainHeight() const override;
+ uint64_t estimateBlockChainHeight() const override;
+@@ -175,6 +176,7 @@ public:
+ virtual PendingTransaction * createSweepUnmixableTransaction() override;
+ bool submitTransaction(const std::string &fileName) override;
+ virtual UnsignedTransaction * loadUnsignedTx(const std::string &unsigned_filename) override;
++ bool hasUnknownKeyImages() const override;
+ bool exportKeyImages(const std::string &filename, bool all = false) override;
+ bool importKeyImages(const std::string &filename) override;
+ bool exportOutputs(const std::string &filename, bool all = false) override;
+diff --git a/src/wallet/api/wallet2_api.h b/src/wallet/api/wallet2_api.h
+index 4268b656e..4edaefefd 100644
+--- a/src/wallet/api/wallet2_api.h
++++ b/src/wallet/api/wallet2_api.h
+@@ -626,6 +626,7 @@ struct Wallet
+ result += unlockedBalance(i);
+ return result;
+ }
++ virtual uint64_t viewOnlyBalance(uint32_t accountIndex, const std::vector<std::string> &key_images = {}) const = 0;
+
+ /**
+ * @brief watchOnly - checks if wallet is watch only
+@@ -910,6 +911,8 @@ struct Wallet
+ virtual uint64_t estimateTransactionFee(const std::vector<std::pair<std::string, uint64_t>> &destinations,
+ PendingTransaction::Priority priority) const = 0;
+
++ virtual bool hasUnknownKeyImages() const = 0;
++
+ /*!
+ * \brief exportKeyImages - exports key images to file
+ * \param filename
+diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp
+index 618c43cee..3477e084f 100644
+--- a/src/wallet/wallet2.cpp
++++ b/src/wallet/wallet2.cpp
+@@ -947,6 +947,16 @@ uint32_t get_subaddress_clamped_sum(uint32_t idx, uint32_t extra)
+ return idx + extra;
+ }
+
++bool is_preferred_input(const std::vector<crypto::key_image>& preferred_input_list, const crypto::key_image& input) {
++ if (!preferred_input_list.empty()) {
++ auto it = std::find(preferred_input_list.begin(), preferred_input_list.end(), input);
++ if (it == preferred_input_list.end()) {
++ return false;
++ }
++ }
++ return true;
++}
++
+ static void setup_shim(hw::wallet_shim * shim, tools::wallet2 * wallet)
+ {
+ shim->get_tx_pub_key_from_received_outs = std::bind(&tools::wallet2::get_tx_pub_key_from_received_outs, wallet, std::placeholders::_1);
+@@ -7045,6 +7055,25 @@ uint64_t wallet2::unlocked_balance(uint32_t index_major, bool strict, uint64_t *
+ return amount;
+ }
+ //----------------------------------------------------------------------------------------------------
++uint64_t wallet2::view_only_balance(uint32_t index_major, const std::vector<crypto::key_image>& selected_inputs)
++{
++ uint64_t amount = 0;
++ for (const auto &td : m_transfers) {
++ if (is_preferred_input(selected_inputs, td.m_key_image) &&
++ !is_spent(td, false) &&
++ !td.m_frozen &&
++ !td.m_key_image_partial &&
++ td.m_key_image_known &&
++ td.is_rct() &&
++ is_transfer_unlocked(td) &&
++ td.m_subaddr_index.major == index_major)
++ {
++ amount += td.m_amount;
++ }
++ }
++ return amount;
++}
++//----------------------------------------------------------------------------------------------------
+ std::map<uint32_t, uint64_t> wallet2::balance_per_subaddress(uint32_t index_major, bool strict) const
+ {
+ std::map<uint32_t, uint64_t> amount_per_subaddr;
+@@ -7895,9 +7924,7 @@ bool wallet2::sign_tx(unsigned_tx_set &exported_txs, std::vector<wallet2::pendin
+ crypto::key_derivation derivation;
+ std::vector<crypto::key_derivation> additional_derivations;
+
+- // compute public keys from out secret keys
+- crypto::public_key tx_pub_key;
+- crypto::secret_key_to_public_key(txs[n].tx_key, tx_pub_key);
++ crypto::public_key tx_pub_key = get_tx_pub_key_from_extra(tx);
+ std::vector<crypto::public_key> additional_tx_pub_keys;
+ for (const crypto::secret_key &skey: txs[n].additional_tx_keys)
+ {
+@@ -11295,7 +11322,7 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp
+ MDEBUG("Ignoring output " << i << " of amount " << print_money(td.amount()) << " which is below fractional threshold " << print_money(fractional_threshold));
+ continue;
+ }
+- if (!is_spent(td, false) && !td.m_frozen && !td.m_key_image_partial && (use_rct ? true : !td.is_rct()) && is_transfer_unlocked(td) && td.m_subaddr_index.major == subaddr_account && subaddr_indices.count(td.m_subaddr_index.minor) == 1)
++ if (!is_spent(td, false) && !td.m_frozen && !td.m_key_image_partial && td.m_key_image_known && (use_rct ? true : !td.is_rct()) && is_transfer_unlocked(td) && td.m_subaddr_index.major == subaddr_account && subaddr_indices.count(td.m_subaddr_index.minor) == 1)
+ {
+ if (td.amount() > m_ignore_outputs_above || td.amount() < m_ignore_outputs_below)
+ {
+@@ -11345,8 +11372,7 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp
+
+ LOG_PRINT_L2("Starting with " << num_nondust_outputs << " non-dust outputs and " << num_dust_outputs << " dust outputs");
+
+- if (unused_dust_indices_per_subaddr.empty() && unused_transfers_indices_per_subaddr.empty())
+- return std::vector<wallet2::pending_tx>();
++ THROW_WALLET_EXCEPTION_IF(unused_dust_indices_per_subaddr.empty() && unused_transfers_indices_per_subaddr.empty(), error::wallet_internal_error, "No enotes available to spend")
+
+ // if empty, put dummy entry so that the front can be referenced later in the loop
+ if (unused_dust_indices_per_subaddr.empty())
+diff --git a/src/wallet/wallet2.h b/src/wallet/wallet2.h
+index bfe3a4f19..406acac2b 100644
+--- a/src/wallet/wallet2.h
++++ b/src/wallet/wallet2.h
+@@ -1172,6 +1172,7 @@ private:
+ // locked & unlocked balance of given or current subaddress account
+ uint64_t balance(uint32_t subaddr_index_major, bool strict) const;
+ uint64_t unlocked_balance(uint32_t subaddr_index_major, bool strict, uint64_t *blocks_to_unlock = NULL, uint64_t *time_to_unlock = NULL);
++ uint64_t view_only_balance(uint32_t index_major, const std::vector<crypto::key_image>& selected_inputs = {});
+ // locked & unlocked balance per subaddress of given or current subaddress account
+ std::map<uint32_t, uint64_t> balance_per_subaddress(uint32_t subaddr_index_major, bool strict) const;
+ std::map<uint32_t, std::pair<uint64_t, std::pair<uint64_t, uint64_t>>> unlocked_balance_per_subaddress(uint32_t subaddr_index_major, bool strict);
diff --git a/patches/monero/0003-coin-control.patch b/patches/monero/0003-coin-control.patch
new file mode 100644
index 0000000..2d25beb
--- /dev/null
+++ b/patches/monero/0003-coin-control.patch
@@ -0,0 +1,916 @@
+From a95f52274d4a6a467e06bff60121850adba387ad Mon Sep 17 00:00:00 2001
+From: Czarek Nakamoto <cyjan@mrcyjanek.net>
+Date: Tue, 12 Mar 2024 11:07:57 +0100
+Subject: [PATCH] PATCH: coin control
+
+---
+ src/wallet/api/CMakeLists.txt | 8 +-
+ src/wallet/api/coins.cpp | 185 ++++++++++++++++++++++++++++++++++
+ src/wallet/api/coins.h | 40 ++++++++
+ src/wallet/api/coins_info.cpp | 122 ++++++++++++++++++++++
+ src/wallet/api/coins_info.h | 71 +++++++++++++
+ src/wallet/api/wallet.cpp | 31 +++++-
+ src/wallet/api/wallet.h | 10 +-
+ src/wallet/api/wallet2_api.h | 52 +++++++++-
+ src/wallet/wallet2.cpp | 46 ++++++++-
+ src/wallet/wallet2.h | 11 +-
+ 10 files changed, 558 insertions(+), 18 deletions(-)
+ create mode 100644 src/wallet/api/coins.cpp
+ create mode 100644 src/wallet/api/coins.h
+ create mode 100644 src/wallet/api/coins_info.cpp
+ create mode 100644 src/wallet/api/coins_info.h
+
+diff --git a/src/wallet/api/CMakeLists.txt b/src/wallet/api/CMakeLists.txt
+index af7948d8a..bb740e2ac 100644
+--- a/src/wallet/api/CMakeLists.txt
++++ b/src/wallet/api/CMakeLists.txt
+@@ -40,7 +40,9 @@ set(wallet_api_sources
+ address_book.cpp
+ subaddress.cpp
+ subaddress_account.cpp
+- unsigned_transaction.cpp)
++ unsigned_transaction.cpp
++ coins.cpp
++ coins_info.cpp)
+
+ set(wallet_api_headers
+ wallet2_api.h)
+@@ -55,7 +57,9 @@ set(wallet_api_private_headers
+ address_book.h
+ subaddress.h
+ subaddress_account.h
+- unsigned_transaction.h)
++ unsigned_transaction.h
++ coins.h
++ coins_info.h)
+
+ monero_private_headers(wallet_api
+ ${wallet_api_private_headers})
+diff --git a/src/wallet/api/coins.cpp b/src/wallet/api/coins.cpp
+new file mode 100644
+index 000000000..2321c638d
+--- /dev/null
++++ b/src/wallet/api/coins.cpp
+@@ -0,0 +1,185 @@
++#include "coins.h"
++#include "coins_info.h"
++#include "wallet.h"
++#include "crypto/hash.h"
++#include "wallet/wallet2.h"
++#include "common_defines.h"
++
++#include <string>
++#include <vector>
++
++using namespace epee;
++
++namespace Monero {
++
++Coins::~Coins() = default;
++
++CoinsImpl::CoinsImpl(WalletImpl *wallet)
++ : m_wallet(wallet) {}
++
++CoinsImpl::~CoinsImpl()
++{
++ for (auto t : m_rows)
++ delete t;
++}
++
++int CoinsImpl::count() const
++{
++ boost::shared_lock<boost::shared_mutex> lock(m_rowsMutex);
++ int result = m_rows.size();
++ return result;
++}
++
++CoinsInfo *CoinsImpl::coin(int index) const
++{
++ boost::shared_lock<boost::shared_mutex> lock(m_rowsMutex);
++ // sanity check
++ if (index < 0)
++ return nullptr;
++ auto index_ = static_cast<unsigned>(index);
++ return index_ < m_rows.size() ? m_rows[index_] : nullptr;
++}
++
++std::vector<CoinsInfo *> CoinsImpl::getAll() const
++{
++ boost::shared_lock<boost::shared_mutex> lock(m_rowsMutex);
++ return m_rows;
++}
++
++
++void CoinsImpl::refresh()
++{
++ LOG_PRINT_L2("Refreshing coins");
++
++ boost::unique_lock<boost::shared_mutex> lock(m_rowsMutex);
++ boost::shared_lock<boost::shared_mutex> transfers_lock(m_wallet->m_wallet->m_transfers_mutex);
++
++ // delete old outputs;
++ for (auto t : m_rows)
++ delete t;
++ m_rows.clear();
++
++ for (size_t i = 0; i < m_wallet->m_wallet->get_num_transfer_details(); ++i)
++ {
++ const tools::wallet2::transfer_details &td = m_wallet->m_wallet->get_transfer_details(i);
++
++ auto ci = new CoinsInfoImpl();
++ ci->m_blockHeight = td.m_block_height;
++ ci->m_hash = string_tools::pod_to_hex(td.m_txid);
++ ci->m_internalOutputIndex = td.m_internal_output_index;
++ ci->m_globalOutputIndex = td.m_global_output_index;
++ ci->m_spent = td.m_spent;
++ ci->m_frozen = td.m_frozen;
++ ci->m_spentHeight = td.m_spent_height;
++ ci->m_amount = td.m_amount;
++ ci->m_rct = td.m_rct;
++ ci->m_keyImageKnown = td.m_key_image_known;
++ ci->m_pkIndex = td.m_pk_index;
++ ci->m_subaddrIndex = td.m_subaddr_index.minor;
++ ci->m_subaddrAccount = td.m_subaddr_index.major;
++ ci->m_address = m_wallet->m_wallet->get_subaddress_as_str(td.m_subaddr_index); // todo: this is expensive, cache maybe?
++ ci->m_addressLabel = m_wallet->m_wallet->get_subaddress_label(td.m_subaddr_index);
++ ci->m_keyImage = string_tools::pod_to_hex(td.m_key_image);
++ ci->m_unlockTime = td.m_tx.unlock_time;
++ ci->m_unlocked = m_wallet->m_wallet->is_transfer_unlocked(td);
++ ci->m_pubKey = string_tools::pod_to_hex(td.get_public_key());
++ ci->m_coinbase = td.m_tx.vin.size() == 1 && td.m_tx.vin[0].type() == typeid(cryptonote::txin_gen);
++ ci->m_description = m_wallet->m_wallet->get_tx_note(td.m_txid);
++
++ m_rows.push_back(ci);
++ }
++}
++
++void CoinsImpl::setFrozen(std::string public_key)
++{
++ crypto::public_key pk;
++ if (!epee::string_tools::hex_to_pod(public_key, pk))
++ {
++ LOG_ERROR("Invalid public key: " << public_key);
++ return;
++ }
++
++ try
++ {
++ m_wallet->m_wallet->freeze(pk);
++ refresh();
++ }
++ catch (const std::exception& e)
++ {
++ LOG_ERROR("setFrozen: " << e.what());
++ }
++}
++
++void CoinsImpl::setFrozen(int index)
++{
++ try
++ {
++ m_wallet->m_wallet->freeze(index);
++ refresh();
++ }
++ catch (const std::exception& e)
++ {
++ LOG_ERROR("setLabel: " << e.what());
++ }
++}
++
++void CoinsImpl::thaw(std::string public_key)
++{
++ crypto::public_key pk;
++ if (!epee::string_tools::hex_to_pod(public_key, pk))
++ {
++ LOG_ERROR("Invalid public key: " << public_key);
++ return;
++ }
++
++ try
++ {
++ m_wallet->m_wallet->thaw(pk);
++ refresh();
++ }
++ catch (const std::exception& e)
++ {
++ LOG_ERROR("thaw: " << e.what());
++ }
++}
++
++void CoinsImpl::thaw(int index)
++{
++ try
++ {
++ m_wallet->m_wallet->thaw(index);
++ refresh();
++ }
++ catch (const std::exception& e)
++ {
++ LOG_ERROR("thaw: " << e.what());
++ }
++}
++
++bool CoinsImpl::isTransferUnlocked(uint64_t unlockTime, uint64_t blockHeight) {
++ return m_wallet->m_wallet->is_transfer_unlocked(unlockTime, blockHeight);
++}
++
++void CoinsImpl::setDescription(const std::string &public_key, const std::string &description)
++{
++ crypto::public_key pk;
++ if (!epee::string_tools::hex_to_pod(public_key, pk))
++ {
++ LOG_ERROR("Invalid public key: " << public_key);
++ return;
++ }
++
++ try
++ {
++ const size_t index = m_wallet->m_wallet->get_transfer_details(pk);
++ const tools::wallet2::transfer_details &td = m_wallet->m_wallet->get_transfer_details(index);
++ m_wallet->m_wallet->set_tx_note(td.m_txid, description);
++ refresh();
++ }
++ catch (const std::exception& e)
++ {
++ LOG_ERROR("setDescription: " << e.what());
++ }
++}
++
++} // namespace
+diff --git a/src/wallet/api/coins.h b/src/wallet/api/coins.h
+new file mode 100644
+index 000000000..b7a0a8642
+--- /dev/null
++++ b/src/wallet/api/coins.h
+@@ -0,0 +1,40 @@
++#ifndef FEATHER_COINS_H
++#define FEATHER_COINS_H
++
++#include "wallet/api/wallet2_api.h"
++#include "wallet/wallet2.h"
++
++namespace Monero {
++
++class WalletImpl;
++
++class CoinsImpl : public Coins
++{
++public:
++ explicit CoinsImpl(WalletImpl * wallet);
++ ~CoinsImpl() override;
++ int count() const override;
++ CoinsInfo * coin(int index) const override;
++ std::vector<CoinsInfo*> getAll() const override;
++ void refresh() override;
++
++ void setFrozen(std::string public_key) override;
++ void setFrozen(int index) override;
++ void thaw(std::string public_key) override;
++ void thaw(int index) override;
++
++ bool isTransferUnlocked(uint64_t unlockTime, uint64_t blockHeight) override;
++
++ void setDescription(const std::string &public_key, const std::string &description) override;
++
++private:
++ WalletImpl *m_wallet;
++ std::vector<CoinsInfo*> m_rows;
++ mutable boost::shared_mutex m_rowsMutex;
++};
++
++}
++
++namespace Bitmonero = Monero;
++
++#endif //FEATHER_COINS_H
+diff --git a/src/wallet/api/coins_info.cpp b/src/wallet/api/coins_info.cpp
+new file mode 100644
+index 000000000..5f2c4e1e4
+--- /dev/null
++++ b/src/wallet/api/coins_info.cpp
+@@ -0,0 +1,122 @@
++#include "coins_info.h"
++
++using namespace std;
++
++namespace Monero {
++
++CoinsInfo::~CoinsInfo() = default;
++
++CoinsInfoImpl::CoinsInfoImpl()
++ : m_blockHeight(0)
++ , m_internalOutputIndex(0)
++ , m_globalOutputIndex(0)
++ , m_spent(false)
++ , m_frozen(false)
++ , m_spentHeight(0)
++ , m_amount(0)
++ , m_rct(false)
++ , m_keyImageKnown(false)
++ , m_pkIndex(0)
++ , m_subaddrAccount(0)
++ , m_subaddrIndex(0)
++ , m_unlockTime(0)
++ , m_unlocked(false)
++{
++
++}
++
++CoinsInfoImpl::~CoinsInfoImpl() = default;
++
++uint64_t CoinsInfoImpl::blockHeight() const
++{
++ return m_blockHeight;
++}
++
++string CoinsInfoImpl::hash() const
++{
++ return m_hash;
++}
++
++size_t CoinsInfoImpl::internalOutputIndex() const {
++ return m_internalOutputIndex;
++}
++
++uint64_t CoinsInfoImpl::globalOutputIndex() const
++{
++ return m_globalOutputIndex;
++}
++
++bool CoinsInfoImpl::spent() const
++{
++ return m_spent;
++}
++
++bool CoinsInfoImpl::frozen() const
++{
++ return m_frozen;
++}
++
++uint64_t CoinsInfoImpl::spentHeight() const
++{
++ return m_spentHeight;
++}
++
++uint64_t CoinsInfoImpl::amount() const
++{
++ return m_amount;
++}
++
++bool CoinsInfoImpl::rct() const {
++ return m_rct;
++}
++
++bool CoinsInfoImpl::keyImageKnown() const {
++ return m_keyImageKnown;
++}
++
++size_t CoinsInfoImpl::pkIndex() const {
++ return m_pkIndex;
++}
++
++uint32_t CoinsInfoImpl::subaddrIndex() const {
++ return m_subaddrIndex;
++}
++
++uint32_t CoinsInfoImpl::subaddrAccount() const {
++ return m_subaddrAccount;
++}
++
++string CoinsInfoImpl::address() const {
++ return m_address;
++}
++
++string CoinsInfoImpl::addressLabel() const {
++ return m_addressLabel;
++}
++
++string CoinsInfoImpl::keyImage() const {
++ return m_keyImage;
++}
++
++uint64_t CoinsInfoImpl::unlockTime() const {
++ return m_unlockTime;
++}
++
++bool CoinsInfoImpl::unlocked() const {
++ return m_unlocked;
++}
++
++string CoinsInfoImpl::pubKey() const {
++ return m_pubKey;
++}
++
++bool CoinsInfoImpl::coinbase() const {
++ return m_coinbase;
++}
++
++string CoinsInfoImpl::description() const {
++ return m_description;
++}
++} // namespace
++
++namespace Bitmonero = Monero;
+diff --git a/src/wallet/api/coins_info.h b/src/wallet/api/coins_info.h
+new file mode 100644
+index 000000000..c43e45abd
+--- /dev/null
++++ b/src/wallet/api/coins_info.h
+@@ -0,0 +1,71 @@
++#ifndef FEATHER_COINS_INFO_H
++#define FEATHER_COINS_INFO_H
++
++#include "wallet/api/wallet2_api.h"
++#include <string>
++#include <ctime>
++
++namespace Monero {
++
++class CoinsImpl;
++
++class CoinsInfoImpl : public CoinsInfo
++{
++public:
++ CoinsInfoImpl();
++ ~CoinsInfoImpl();
++
++ virtual uint64_t blockHeight() const override;
++ virtual std::string hash() const override;
++ virtual size_t internalOutputIndex() const override;
++ virtual uint64_t globalOutputIndex() const override;
++ virtual bool spent() const override;
++ virtual bool frozen() const override;
++ virtual uint64_t spentHeight() const override;
++ virtual uint64_t amount() const override;
++ virtual bool rct() const override;
++ virtual bool keyImageKnown() const override;
++ virtual size_t pkIndex() const override;
++ virtual uint32_t subaddrIndex() const override;
++ virtual uint32_t subaddrAccount() const override;
++ virtual std::string address() const override;
++ virtual std::string addressLabel() const override;
++ virtual std::string keyImage() const override;
++ virtual uint64_t unlockTime() const override;
++ virtual bool unlocked() const override;
++ virtual std::string pubKey() const override;
++ virtual bool coinbase() const override;
++ virtual std::string description() const override;
++
++private:
++ uint64_t m_blockHeight;
++ std::string m_hash;
++ size_t m_internalOutputIndex;
++ uint64_t m_globalOutputIndex;
++ bool m_spent;
++ bool m_frozen;
++ uint64_t m_spentHeight;
++ uint64_t m_amount;
++ bool m_rct;
++ bool m_keyImageKnown;
++ size_t m_pkIndex;
++ uint32_t m_subaddrIndex;
++ uint32_t m_subaddrAccount;
++ std::string m_address;
++ std::string m_addressLabel;
++ std::string m_keyImage;
++ uint64_t m_unlockTime;
++ bool m_unlocked;
++ std::string m_pubKey;
++ bool m_coinbase;
++ std::string m_description;
++
++ friend class CoinsImpl;
++
++};
++
++} // namespace
++
++namespace Bitmonero = Monero;
++
++#endif //FEATHER_COINS_INFO_H
+diff --git a/src/wallet/api/wallet.cpp b/src/wallet/api/wallet.cpp
+index 42887dced..2fe0d1d29 100644
+--- a/src/wallet/api/wallet.cpp
++++ b/src/wallet/api/wallet.cpp
+@@ -35,6 +35,7 @@
+ #include "transaction_history.h"
+ #include "address_book.h"
+ #include "subaddress.h"
++#include "coins.h"
+ #include "subaddress_account.h"
+ #include "common_defines.h"
+ #include "common/util.h"
+@@ -469,6 +470,7 @@ WalletImpl::WalletImpl(NetworkType nettype, uint64_t kdf_rounds)
+ m_refreshEnabled = false;
+ m_addressBook.reset(new AddressBookImpl(this));
+ m_subaddress.reset(new SubaddressImpl(this));
++ m_coins.reset(new CoinsImpl(this));
+ m_subaddressAccount.reset(new SubaddressAccountImpl(this));
+
+
+@@ -1752,7 +1754,7 @@ PendingTransaction* WalletImpl::restoreMultisigTransaction(const string& signDat
+ // - unconfirmed_transfer_details;
+ // - confirmed_transfer_details)
+
+-PendingTransaction *WalletImpl::createTransactionMultDest(const std::vector<string> &dst_addr, const string &payment_id, optional<std::vector<uint64_t>> amount, uint32_t mixin_count, PendingTransaction::Priority priority, uint32_t subaddr_account, std::set<uint32_t> subaddr_indices)
++PendingTransaction *WalletImpl::createTransactionMultDest(const std::vector<string> &dst_addr, const string &payment_id, optional<std::vector<uint64_t>> amount, uint32_t mixin_count, PendingTransaction::Priority priority, uint32_t subaddr_account, std::set<uint32_t> subaddr_indices, const std::set<std::string> &preferred_inputs)
+
+ {
+ clearStatus();
+@@ -1821,6 +1823,19 @@ PendingTransaction *WalletImpl::createTransactionMultDest(const std::vector<stri
+ }
+ }
+ }
++ std::vector<crypto::key_image> preferred_input_list;
++ if (!preferred_inputs.empty()) {
++ for (const auto &public_key : preferred_inputs) {
++ crypto::key_image keyImage;
++ bool r = epee::string_tools::hex_to_pod(public_key, keyImage);
++ if (!r) {
++ error = true;
++ setStatusError(tr("failed to parse key image"));
++ break;
++ }
++ preferred_input_list.push_back(keyImage);
++ }
++ }
+ if (error) {
+ break;
+ }
+@@ -1833,13 +1848,14 @@ PendingTransaction *WalletImpl::createTransactionMultDest(const std::vector<stri
+ fake_outs_count = m_wallet->adjust_mixin(mixin_count);
+
+ if (amount) {
++ // (std::vector<cryptonote::tx_destination_entry> dsts, const size_t fake_outs_count, const uint64_t unlock_time, uint32_t priority, const std::vector<uint8_t>& extra, uint32_t subaddr_account, std::set<uint32_t> subaddr_indices, const unique_index_container& subtract_fee_from_outputs, const std::vector<crypto::key_image>& preferred_input_list)
+ transaction->m_pending_tx = m_wallet->create_transactions_2(dsts, fake_outs_count, 0 /* unlock_time */,
+ adjusted_priority,
+- extra, subaddr_account, subaddr_indices);
++ extra, subaddr_account, subaddr_indices, preferred_input_list);
+ } else {
+ transaction->m_pending_tx = m_wallet->create_transactions_all(0, info.address, info.is_subaddress, 1, fake_outs_count, 0 /* unlock_time */,
+ adjusted_priority,
+- extra, subaddr_account, subaddr_indices);
++ extra, subaddr_account, subaddr_indices, preferred_input_list);
+ }
+ pendingTxPostProcess(transaction);
+
+@@ -1920,10 +1936,10 @@ PendingTransaction *WalletImpl::createTransactionMultDest(const std::vector<stri
+ }
+
+ PendingTransaction *WalletImpl::createTransaction(const string &dst_addr, const string &payment_id, optional<uint64_t> amount, uint32_t mixin_count,
+- PendingTransaction::Priority priority, uint32_t subaddr_account, std::set<uint32_t> subaddr_indices)
++ PendingTransaction::Priority priority, uint32_t subaddr_account, std::set<uint32_t> subaddr_indices, const std::set<std::string> &preferred_inputs)
+
+ {
+- return createTransactionMultDest(std::vector<string> {dst_addr}, payment_id, amount ? (std::vector<uint64_t> {*amount}) : (optional<std::vector<uint64_t>>()), mixin_count, priority, subaddr_account, subaddr_indices);
++ return createTransactionMultDest(std::vector<string> {dst_addr}, payment_id, amount ? (std::vector<uint64_t> {*amount}) : (optional<std::vector<uint64_t>>()), mixin_count, priority, subaddr_account, subaddr_indices, preferred_inputs);
+ }
+
+ PendingTransaction *WalletImpl::createSweepUnmixableTransaction()
+@@ -2048,6 +2064,11 @@ AddressBook *WalletImpl::addressBook()
+ return m_addressBook.get();
+ }
+
++Coins *WalletImpl::coins()
++{
++ return m_coins.get();
++}
++
+ Subaddress *WalletImpl::subaddress()
+ {
+ return m_subaddress.get();
+diff --git a/src/wallet/api/wallet.h b/src/wallet/api/wallet.h
+index 05d065c5c..4a16ca028 100644
+--- a/src/wallet/api/wallet.h
++++ b/src/wallet/api/wallet.h
+@@ -46,6 +46,7 @@ class PendingTransactionImpl;
+ class UnsignedTransactionImpl;
+ class AddressBookImpl;
+ class SubaddressImpl;
++class CoinsImpl;
+ class SubaddressAccountImpl;
+ struct Wallet2CallbackImpl;
+
+@@ -167,12 +168,14 @@ public:
+ optional<std::vector<uint64_t>> amount, uint32_t mixin_count,
+ PendingTransaction::Priority priority = PendingTransaction::Priority_Low,
+ uint32_t subaddr_account = 0,
+- std::set<uint32_t> subaddr_indices = {}) override;
++ std::set<uint32_t> subaddr_indices = {},
++ const std::set<std::string> &preferred_inputs = {}) override;
+ PendingTransaction * createTransaction(const std::string &dst_addr, const std::string &payment_id,
+ optional<uint64_t> amount, uint32_t mixin_count,
+ PendingTransaction::Priority priority = PendingTransaction::Priority_Low,
+ uint32_t subaddr_account = 0,
+- std::set<uint32_t> subaddr_indices = {}) override;
++ std::set<uint32_t> subaddr_indices = {},
++ const std::set<std::string> &preferred_inputs = {}) override;
+ virtual PendingTransaction * createSweepUnmixableTransaction() override;
+ bool submitTransaction(const std::string &fileName) override;
+ virtual UnsignedTransaction * loadUnsignedTx(const std::string &unsigned_filename) override;
+@@ -195,6 +198,7 @@ public:
+ PendingTransaction::Priority priority) const override;
+ virtual TransactionHistory * history() override;
+ virtual AddressBook * addressBook() override;
++ virtual Coins * coins() override;
+ virtual Subaddress * subaddress() override;
+ virtual SubaddressAccount * subaddressAccount() override;
+ virtual void setListener(WalletListener * l) override;
+@@ -266,6 +270,7 @@ private:
+ friend class TransactionHistoryImpl;
+ friend struct Wallet2CallbackImpl;
+ friend class AddressBookImpl;
++ friend class CoinsImpl;
+ friend class SubaddressImpl;
+ friend class SubaddressAccountImpl;
+
+@@ -282,6 +287,7 @@ private:
+ std::unique_ptr<Wallet2CallbackImpl> m_wallet2Callback;
+ std::unique_ptr<AddressBookImpl> m_addressBook;
+ std::unique_ptr<SubaddressImpl> m_subaddress;
++ std::unique_ptr<CoinsImpl> m_coins;
+ std::unique_ptr<SubaddressAccountImpl> m_subaddressAccount;
+
+ // multi-threaded refresh stuff
+diff --git a/src/wallet/api/wallet2_api.h b/src/wallet/api/wallet2_api.h
+index 4edaefefd..8a5c4135e 100644
+--- a/src/wallet/api/wallet2_api.h
++++ b/src/wallet/api/wallet2_api.h
+@@ -261,6 +261,51 @@ struct AddressBook
+ virtual int lookupPaymentID(const std::string &payment_id) const = 0;
+ };
+
++/**
++ * @brief The CoinsInfo - interface for displaying coins information
++ */
++struct CoinsInfo
++{
++ virtual ~CoinsInfo() = 0;
++
++ virtual uint64_t blockHeight() const = 0;
++ virtual std::string hash() const = 0;
++ virtual size_t internalOutputIndex() const = 0;
++ virtual uint64_t globalOutputIndex() const = 0;
++ virtual bool spent() const = 0;
++ virtual bool frozen() const = 0;
++ virtual uint64_t spentHeight() const = 0;
++ virtual uint64_t amount() const = 0;
++ virtual bool rct() const = 0;
++ virtual bool keyImageKnown() const = 0;
++ virtual size_t pkIndex() const = 0;
++ virtual uint32_t subaddrIndex() const = 0;
++ virtual uint32_t subaddrAccount() const = 0;
++ virtual std::string address() const = 0;
++ virtual std::string addressLabel() const = 0;
++ virtual std::string keyImage() const = 0;
++ virtual uint64_t unlockTime() const = 0;
++ virtual bool unlocked() const = 0;
++ virtual std::string pubKey() const = 0;
++ virtual bool coinbase() const = 0;
++ virtual std::string description() const = 0;
++};
++
++struct Coins
++{
++ virtual ~Coins() = 0;
++ virtual int count() const = 0;
++ virtual CoinsInfo * coin(int index) const = 0;
++ virtual std::vector<CoinsInfo*> getAll() const = 0;
++ virtual void refresh() = 0;
++ virtual void setFrozen(std::string public_key) = 0;
++ virtual void setFrozen(int index) = 0;
++ virtual void thaw(std::string public_key) = 0;
++ virtual void thaw(int index) = 0;
++ virtual bool isTransferUnlocked(uint64_t unlockTime, uint64_t blockHeight) = 0;
++ virtual void setDescription(const std::string &public_key, const std::string &description) = 0;
++};
++
+ struct SubaddressRow {
+ public:
+ SubaddressRow(std::size_t _rowId, const std::string &_address, const std::string &_label):
+@@ -854,7 +899,8 @@ struct Wallet
+ optional<std::vector<uint64_t>> amount, uint32_t mixin_count,
+ PendingTransaction::Priority = PendingTransaction::Priority_Low,
+ uint32_t subaddr_account = 0,
+- std::set<uint32_t> subaddr_indices = {}) = 0;
++ std::set<uint32_t> subaddr_indices = {},
++ const std::set<std::string> &preferred_inputs = {}) = 0;
+
+ /*!
+ * \brief createTransaction creates transaction. if dst_addr is an integrated address, payment_id is ignored
+@@ -873,7 +919,8 @@ struct Wallet
+ optional<uint64_t> amount, uint32_t mixin_count,
+ PendingTransaction::Priority = PendingTransaction::Priority_Low,
+ uint32_t subaddr_account = 0,
+- std::set<uint32_t> subaddr_indices = {}) = 0;
++ std::set<uint32_t> subaddr_indices = {},
++ const std::set<std::string> &preferred_inputs = {}) = 0;
+
+ /*!
+ * \brief createSweepUnmixableTransaction creates transaction with unmixable outputs.
+@@ -987,6 +1034,7 @@ struct Wallet
+
+ virtual TransactionHistory * history() = 0;
+ virtual AddressBook * addressBook() = 0;
++ virtual Coins * coins() = 0;
+ virtual Subaddress * subaddress() = 0;
+ virtual SubaddressAccount * subaddressAccount() = 0;
+ virtual void setListener(WalletListener *) = 0;
+diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp
+index 3477e084f..c3376f7e0 100644
+--- a/src/wallet/wallet2.cpp
++++ b/src/wallet/wallet2.cpp
+@@ -2081,12 +2081,21 @@ bool wallet2::frozen(const multisig_tx_set& txs) const
+
+ return false;
+ }
++void wallet2::freeze(const crypto::public_key &pk)
++{
++ freeze(get_transfer_details(pk));
++}
+ //----------------------------------------------------------------------------------------------------
+ void wallet2::freeze(const crypto::key_image &ki)
+ {
+ freeze(get_transfer_details(ki));
+ }
+ //----------------------------------------------------------------------------------------------------
++void wallet2::thaw(const crypto::public_key &pk)
++{
++ thaw(get_transfer_details(pk));
++}
++//----------------------------------------------------------------------------------------------------
+ void wallet2::thaw(const crypto::key_image &ki)
+ {
+ thaw(get_transfer_details(ki));
+@@ -2097,6 +2106,18 @@ bool wallet2::frozen(const crypto::key_image &ki) const
+ return frozen(get_transfer_details(ki));
+ }
+ //----------------------------------------------------------------------------------------------------
++size_t wallet2::get_transfer_details(const crypto::public_key &pk) const
++{
++ for (size_t idx = 0; idx < m_transfers.size(); ++idx)
++ {
++ const transfer_details &td = m_transfers[idx];
++ if (td.get_public_key() == pk) {
++ return idx;
++ }
++ }
++ CHECK_AND_ASSERT_THROW_MES(false, "Public key not found");
++}
++//----------------------------------------------------------------------------------------------------
+ size_t wallet2::get_transfer_details(const crypto::key_image &ki) const
+ {
+ for (size_t idx = 0; idx < m_transfers.size(); ++idx)
+@@ -2500,6 +2521,7 @@ void wallet2::process_new_transaction(const crypto::hash &txid, const cryptonote
+ uint64_t amount = tx.vout[o].amount ? tx.vout[o].amount : tx_scan_info[o].amount;
+ if (!pool)
+ {
++ boost::unique_lock<boost::shared_mutex> lock(m_transfers_mutex);
+ m_transfers.push_back(transfer_details{});
+ transfer_details& td = m_transfers.back();
+ td.m_block_height = height;
+@@ -2603,6 +2625,7 @@ void wallet2::process_new_transaction(const crypto::hash &txid, const cryptonote
+ uint64_t extra_amount = amount - burnt;
+ if (!pool)
+ {
++ boost::unique_lock<boost::shared_mutex> lock(m_transfers_mutex);
+ transfer_details &td = m_transfers[kit->second];
+ td.m_block_height = height;
+ td.m_internal_output_index = o;
+@@ -10495,7 +10518,7 @@ void wallet2::transfer_selected_rct(std::vector<cryptonote::tx_destination_entry
+ LOG_PRINT_L2("transfer_selected_rct done");
+ }
+
+-std::vector<size_t> wallet2::pick_preferred_rct_inputs(uint64_t needed_money, uint32_t subaddr_account, const std::set<uint32_t> &subaddr_indices)
++std::vector<size_t> wallet2::pick_preferred_rct_inputs(uint64_t needed_money, uint32_t subaddr_account, const std::set<uint32_t> &subaddr_indices, const std::vector<crypto::key_image>& preferred_input_list)
+ {
+ std::vector<size_t> picks;
+ float current_output_relatdness = 1.0f;
+@@ -10506,6 +10529,9 @@ std::vector<size_t> wallet2::pick_preferred_rct_inputs(uint64_t needed_money, ui
+ for (size_t i = 0; i < m_transfers.size(); ++i)
+ {
+ const transfer_details& td = m_transfers[i];
++ if (!is_preferred_input(preferred_input_list, td.m_key_image)) {
++ continue;
++ }
+ if (!is_spent(td, false) && !td.m_frozen && td.is_rct() && td.amount() >= needed_money && is_transfer_unlocked(td) && td.m_subaddr_index.major == subaddr_account && subaddr_indices.count(td.m_subaddr_index.minor) == 1)
+ {
+ if (td.amount() > m_ignore_outputs_above || td.amount() < m_ignore_outputs_below)
+@@ -10526,6 +10552,9 @@ std::vector<size_t> wallet2::pick_preferred_rct_inputs(uint64_t needed_money, ui
+ for (size_t i = 0; i < m_transfers.size(); ++i)
+ {
+ const transfer_details& td = m_transfers[i];
++ if (!is_preferred_input(preferred_input_list, td.m_key_image)) {
++ continue;
++ }
+ if (!is_spent(td, false) && !td.m_frozen && !td.m_key_image_partial && td.is_rct() && is_transfer_unlocked(td) && td.m_subaddr_index.major == subaddr_account && subaddr_indices.count(td.m_subaddr_index.minor) == 1)
+ {
+ if (td.amount() > m_ignore_outputs_above || td.amount() < m_ignore_outputs_below)
+@@ -10537,6 +10566,9 @@ std::vector<size_t> wallet2::pick_preferred_rct_inputs(uint64_t needed_money, ui
+ for (size_t j = i + 1; j < m_transfers.size(); ++j)
+ {
+ const transfer_details& td2 = m_transfers[j];
++ if (!is_preferred_input(preferred_input_list, td2.m_key_image)) {
++ continue;
++ }
+ if (td2.amount() > m_ignore_outputs_above || td2.amount() < m_ignore_outputs_below)
+ {
+ MDEBUG("Ignoring output " << j << " of amount " << print_money(td2.amount()) << " which is outside prescribed range [" << print_money(m_ignore_outputs_below) << ", " << print_money(m_ignore_outputs_above) << "]");
+@@ -11109,7 +11141,7 @@ bool wallet2::light_wallet_key_image_is_ours(const crypto::key_image& key_image,
+ // This system allows for sending (almost) the entire balance, since it does
+ // not generate spurious change in all txes, thus decreasing the instantaneous
+ // usable balance.
+-std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryptonote::tx_destination_entry> dsts, const size_t fake_outs_count, const uint64_t unlock_time, uint32_t priority, const std::vector<uint8_t>& extra, uint32_t subaddr_account, std::set<uint32_t> subaddr_indices, const unique_index_container& subtract_fee_from_outputs)
++std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryptonote::tx_destination_entry> dsts, const size_t fake_outs_count, const uint64_t unlock_time, uint32_t priority, const std::vector<uint8_t>& extra, uint32_t subaddr_account, std::set<uint32_t> subaddr_indices, const std::vector<crypto::key_image>& preferred_input_list, const unique_index_container& subtract_fee_from_outputs)
+ {
+ //ensure device is let in NONE mode in any case
+ hw::device &hwdev = m_account.get_device();
+@@ -11317,6 +11349,9 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp
+ for (size_t i = 0; i < m_transfers.size(); ++i)
+ {
+ const transfer_details& td = m_transfers[i];
++ if (!is_preferred_input(preferred_input_list, td.m_key_image)) {
++ continue;
++ }
+ if (m_ignore_fractional_outputs && td.amount() < fractional_threshold)
+ {
+ MDEBUG("Ignoring output " << i << " of amount " << print_money(td.amount()) << " which is below fractional threshold " << print_money(fractional_threshold));
+@@ -11401,7 +11436,7 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp
+ // will get us a known fee.
+ uint64_t estimated_fee = estimate_fee(use_per_byte_fee, use_rct, 2, fake_outs_count, 2, extra.size(), bulletproof, clsag, bulletproof_plus, use_view_tags, base_fee, fee_quantization_mask);
+ total_needed_money = needed_money + (subtract_fee_from_outputs.size() ? 0 : estimated_fee);
+- preferred_inputs = pick_preferred_rct_inputs(total_needed_money, subaddr_account, subaddr_indices);
++ preferred_inputs = pick_preferred_rct_inputs(total_needed_money, subaddr_account, subaddr_indices, preferred_input_list);
+ if (!preferred_inputs.empty())
+ {
+ string s;
+@@ -11882,7 +11917,7 @@ bool wallet2::sanity_check(const std::vector<wallet2::pending_tx> &ptx_vector, c
+ return true;
+ }
+
+-std::vector<wallet2::pending_tx> wallet2::create_transactions_all(uint64_t below, const cryptonote::account_public_address &address, bool is_subaddress, const size_t outputs, const size_t fake_outs_count, const uint64_t unlock_time, uint32_t priority, const std::vector<uint8_t>& extra, uint32_t subaddr_account, std::set<uint32_t> subaddr_indices)
++std::vector<wallet2::pending_tx> wallet2::create_transactions_all(uint64_t below, const cryptonote::account_public_address &address, bool is_subaddress, const size_t outputs, const size_t fake_outs_count, const uint64_t unlock_time, uint32_t priority, const std::vector<uint8_t>& extra, uint32_t subaddr_account, std::set<uint32_t> subaddr_indices, const std::vector<crypto::key_image>& preferred_input_list)
+ {
+ std::vector<size_t> unused_transfers_indices;
+ std::vector<size_t> unused_dust_indices;
+@@ -11911,6 +11946,9 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_all(uint64_t below
+ for (size_t i = 0; i < m_transfers.size(); ++i)
+ {
+ const transfer_details& td = m_transfers[i];
++ if (!is_preferred_input(preferred_input_list, td.m_key_image)) {
++ continue;
++ }
+ if (m_ignore_fractional_outputs && td.amount() < fractional_threshold)
+ {
+ MDEBUG("Ignoring output " << i << " of amount " << print_money(td.amount()) << " which is below threshold " << print_money(fractional_threshold));
+diff --git a/src/wallet/wallet2.h b/src/wallet/wallet2.h
+index 406acac2b..5eae908ad 100644
+--- a/src/wallet/wallet2.h
++++ b/src/wallet/wallet2.h
+@@ -1207,8 +1207,8 @@ private:
+ bool parse_unsigned_tx_from_str(const std::string &unsigned_tx_st, unsigned_tx_set &exported_txs) const;
+ bool load_tx(const std::string &signed_filename, std::vector<tools::wallet2::pending_tx> &ptx, std::function<bool(const signed_tx_set&)> accept_func = NULL);
+ bool parse_tx_from_str(const std::string &signed_tx_st, std::vector<tools::wallet2::pending_tx> &ptx, std::function<bool(const signed_tx_set &)> accept_func);
+- std::vector<wallet2::pending_tx> create_transactions_2(std::vector<cryptonote::tx_destination_entry> dsts, const size_t fake_outs_count, const uint64_t unlock_time, uint32_t priority, const std::vector<uint8_t>& extra, uint32_t subaddr_account, std::set<uint32_t> subaddr_indices, const unique_index_container& subtract_fee_from_outputs = {}); // pass subaddr_indices by value on purpose
+- std::vector<wallet2::pending_tx> create_transactions_all(uint64_t below, const cryptonote::account_public_address &address, bool is_subaddress, const size_t outputs, const size_t fake_outs_count, const uint64_t unlock_time, uint32_t priority, const std::vector<uint8_t>& extra, uint32_t subaddr_account, std::set<uint32_t> subaddr_indices);
++ std::vector<wallet2::pending_tx> create_transactions_2(std::vector<cryptonote::tx_destination_entry> dsts, const size_t fake_outs_count, const uint64_t unlock_time, uint32_t priority, const std::vector<uint8_t>& extra, uint32_t subaddr_account, std::set<uint32_t> subaddr_indices, const std::vector<crypto::key_image>& preferred_input_list = {}, const unique_index_container& subtract_fee_from_outputs = {}); // pass subaddr_indices by value on purpose
++ std::vector<wallet2::pending_tx> create_transactions_all(uint64_t below, const cryptonote::account_public_address &address, bool is_subaddress, const size_t outputs, const size_t fake_outs_count, const uint64_t unlock_time, uint32_t priority, const std::vector<uint8_t>& extra, uint32_t subaddr_account, std::set<uint32_t> subaddr_indices, const std::vector<crypto::key_image>& preferred_input_list = {});
+ std::vector<wallet2::pending_tx> create_transactions_single(const crypto::key_image &ki, const cryptonote::account_public_address &address, bool is_subaddress, const size_t outputs, const size_t fake_outs_count, const uint64_t unlock_time, uint32_t priority, const std::vector<uint8_t>& extra);
+ std::vector<wallet2::pending_tx> create_transactions_from(const cryptonote::account_public_address &address, bool is_subaddress, const size_t outputs, std::vector<size_t> unused_transfers_indices, std::vector<size_t> unused_dust_indices, const size_t fake_outs_count, const uint64_t unlock_time, uint32_t priority, const std::vector<uint8_t>& extra);
+ bool sanity_check(const std::vector<wallet2::pending_tx> &ptx_vector, const std::vector<cryptonote::tx_destination_entry>& dsts, const unique_index_container& subtract_fee_from_outputs = {}) const;
+@@ -1559,6 +1559,7 @@ private:
+ uint64_t get_num_rct_outputs();
+ size_t get_num_transfer_details() const { return m_transfers.size(); }
+ const transfer_details &get_transfer_details(size_t idx) const;
++ size_t get_transfer_details(const crypto::public_key &pk) const;
+
+ uint8_t get_current_hard_fork();
+ void get_hard_fork_info(uint8_t version, uint64_t &earliest_height);
+@@ -1788,7 +1789,9 @@ private:
+ void freeze(size_t idx);
+ void thaw(size_t idx);
+ bool frozen(size_t idx) const;
++ void freeze(const crypto::public_key &pk);
+ void freeze(const crypto::key_image &ki);
++ void thaw(const crypto::public_key &pk);
+ void thaw(const crypto::key_image &ki);
+ bool frozen(const crypto::key_image &ki) const;
+ bool frozen(const transfer_details &td) const;
+@@ -1829,6 +1832,8 @@ private:
+
+ static std::string get_default_daemon_address() { CRITICAL_REGION_LOCAL(default_daemon_address_lock); return default_daemon_address; }
+
++ boost::shared_mutex m_transfers_mutex;
++
+ private:
+ /*!
+ * \brief Stores wallet information to wallet file.
+@@ -1892,7 +1897,7 @@ private:
+ std::vector<uint64_t> get_unspent_amounts_vector(bool strict);
+ uint64_t get_dynamic_base_fee_estimate();
+ float get_output_relatedness(const transfer_details &td0, const transfer_details &td1) const;
+- std::vector<size_t> pick_preferred_rct_inputs(uint64_t needed_money, uint32_t subaddr_account, const std::set<uint32_t> &subaddr_indices);
++ std::vector<size_t> pick_preferred_rct_inputs(uint64_t needed_money, uint32_t subaddr_account, const std::set<uint32_t> &subaddr_indices, const std::vector<crypto::key_image>& preferred_input_list);
+ void set_spent(size_t idx, uint64_t height);
+ void set_unspent(size_t idx);
+ bool is_spent(const transfer_details &td, bool strict = true) const;
diff --git a/patches/monero/0004-fix-build.patch b/patches/monero/0004-fix-build.patch
new file mode 100644
index 0000000..e3d3566
--- /dev/null
+++ b/patches/monero/0004-fix-build.patch
@@ -0,0 +1,134 @@
+From 1a05393366d94819c83b5e6983cb44938ac06c67 Mon Sep 17 00:00:00 2001
+From: Czarek Nakamoto <cyjan@mrcyjanek.net>
+Date: Tue, 12 Mar 2024 17:59:13 +0100
+Subject: [PATCH] PATCH: fix build issues
+
+---
+ contrib/depends/hosts/linux.mk | 8 +++----
+ contrib/depends/packages/android_ndk.mk | 2 ++
+ contrib/depends/packages/packages.mk | 2 +-
+ contrib/depends/packages/polyseed.mk | 23 +++++++++++++++++++
+ contrib/depends/packages/sodium.mk | 2 +-
+ .../patches/polyseed/force-static-mingw.patch | 23 +++++++++++++++++++
+ 6 files changed, 54 insertions(+), 6 deletions(-)
+ create mode 100644 contrib/depends/packages/polyseed.mk
+ create mode 100644 contrib/depends/patches/polyseed/force-static-mingw.patch
+
+diff --git a/contrib/depends/hosts/linux.mk b/contrib/depends/hosts/linux.mk
+index 912fdb03c..b79799f30 100644
+--- a/contrib/depends/hosts/linux.mk
++++ b/contrib/depends/hosts/linux.mk
+@@ -11,15 +11,15 @@ linux_debug_CXXFLAGS=$(linux_debug_CFLAGS)
+ linux_debug_CPPFLAGS=-D_GLIBCXX_DEBUG -D_GLIBCXX_DEBUG_PEDANTIC
+
+ ifeq (86,$(findstring 86,$(build_arch)))
+-i686_linux_CC=gcc -m32
+-i686_linux_CXX=g++ -m32
++i686_linux_CC=i686-linux-gnu-gcc
++i686_linux_CXX=i686-linux-gnu-g++
+ i686_linux_AR=ar
+ i686_linux_RANLIB=ranlib
+ i686_linux_NM=nm
+ i686_linux_STRIP=strip
+
+-x86_64_linux_CC=gcc -m64
+-x86_64_linux_CXX=g++ -m64
++x86_64_linux_CC=x86_64-linux-gnu-gcc
++x86_64_linux_CXX=x86_64-linux-gnu-g++
+ x86_64_linux_AR=ar
+ x86_64_linux_RANLIB=ranlib
+ x86_64_linux_NM=nm
+diff --git a/contrib/depends/packages/android_ndk.mk b/contrib/depends/packages/android_ndk.mk
+index 9b8a5332f..5deff76c7 100644
+--- a/contrib/depends/packages/android_ndk.mk
++++ b/contrib/depends/packages/android_ndk.mk
+@@ -7,6 +7,8 @@ $(package)_sha256_hash=5dfbbdc2d3ba859fed90d0e978af87c71a91a5be1f6e1c40ba697503d
+ define $(package)_set_vars
+ $(package)_config_opts_arm=--arch arm
+ $(package)_config_opts_aarch64=--arch arm64
++$(package)_config_opts_x86_64=--arch x86_64
++$(package)_config_opts_i686=--arch x86
+ endef
+
+ define $(package)_extract_cmds
+diff --git a/contrib/depends/packages/packages.mk b/contrib/depends/packages/packages.mk
+index d2d1eca85..8783d4955 100644
+--- a/contrib/depends/packages/packages.mk
++++ b/contrib/depends/packages/packages.mk
+@@ -1,4 +1,4 @@
+-packages:=boost openssl zeromq libiconv expat unbound
++packages:=boost openssl zeromq libiconv expat unbound polyseed
+
+ # ccache is useless in gitian builds
+ ifneq ($(GITIAN),1)
+diff --git a/contrib/depends/packages/polyseed.mk b/contrib/depends/packages/polyseed.mk
+new file mode 100644
+index 000000000..47a532907
+--- /dev/null
++++ b/contrib/depends/packages/polyseed.mk
+@@ -0,0 +1,23 @@
++package=polyseed
++$(package)_version=2.0.0
++$(package)_download_path=https://github.com/tevador/$(package)/archive/refs/tags/
++$(package)_download_file=v$($(package)_version).tar.gz
++$(package)_file_name=$(package)-$($(package)_version).tar.gz
++$(package)_sha256_hash=f36282fcbcd68d32461b8230c89e1a40661bd46b91109681cec637433004135a
++$(package)_patches=force-static-mingw.patch
++
++define $(package)_preprocess_cmds
++ patch -p1 < $($(package)_patch_dir)/force-static-mingw.patch
++endef
++
++define $(package)_config_cmds
++ cmake -DCMAKE_INSTALL_PREFIX=$(host_prefix) -DCMAKE_C_COMPILER=$($(package)_cc) .
++endef
++
++define $(package)_build_cmds
++ $(MAKE)
++endef
++
++define $(package)_stage_cmds
++ $(MAKE) DESTDIR=$($(package)_staging_dir) install
++endef
+diff --git a/contrib/depends/packages/sodium.mk b/contrib/depends/packages/sodium.mk
+index 87b34599e..68a5b48ba 100644
+--- a/contrib/depends/packages/sodium.mk
++++ b/contrib/depends/packages/sodium.mk
+@@ -6,7 +6,7 @@ $(package)_sha256_hash=6f504490b342a4f8a4c4a02fc9b866cbef8622d5df4e5452b46be121e
+ $(package)_patches=disable-glibc-getrandom-getentropy.patch fix-whitespace.patch
+
+ define $(package)_set_vars
+-$(package)_config_opts=--enable-static --disable-shared
++$(package)_config_opts=--enable-static --disable-shared --with-pic
+ $(package)_config_opts+=--prefix=$(host_prefix)
+ endef
+
+diff --git a/contrib/depends/patches/polyseed/force-static-mingw.patch b/contrib/depends/patches/polyseed/force-static-mingw.patch
+new file mode 100644
+index 000000000..f05cb2b6a
+--- /dev/null
++++ b/contrib/depends/patches/polyseed/force-static-mingw.patch
+@@ -0,0 +1,23 @@
++--- a/include/polyseed.h
+++++ b/include/polyseed.h
++@@ -93,13 +93,13 @@ Shared/static library definitions
++ - define POLYSEED_STATIC when linking to the static library
++ */
++ #if defined(_WIN32) || defined(__CYGWIN__)
++- #ifdef POLYSEED_SHARED
++- #define POLYSEED_API __declspec(dllexport)
++- #elif !defined(POLYSEED_STATIC)
++- #define POLYSEED_API __declspec(dllimport)
++- #else
++- #define POLYSEED_API
++- #endif
+++// #ifdef POLYSEED_SHARED
+++// #define POLYSEED_API __declspec(dllexport)
+++// #elif !defined(POLYSEED_STATIC)
+++// #define POLYSEED_API __declspec(dllimport)
+++// #else
+++ #define POLYSEED_API
+++// #endif
++ #define POLYSEED_PRIVATE
++ #else
++ #ifdef POLYSEED_SHARED