From 8516bf74d4d88b062c1baaf622893dc2e9086865 Mon Sep 17 00:00:00 2001 From: jeffro256 Date: Wed, 21 Jun 2023 21:55:14 +0200 Subject: [PATCH] wallet2_basic: robust compat lib for loading/storing legacy wallets [SERAPHIS] This library has no dependency on wallet2.h and gives us a way forward to move away from `wallet2` in the (not-so-distant) future, while still supporting conversions of old wallet files. This lib is also useful if you have an application where you want to extract information directly from the wallet file with or without having to setup accounts and devices. This is now possible because I have split the wallet keys loading into two steps: `load_from_memory` and `setup_account_and_devices`. When one is loading a wallet keys file, the user of the API can choose whether or not to contact external devices during this process with use of the flag `allow_external_devices_setup`. Reorganized for `seraphis_lib`. --- CMakeLists.txt | 1 + .../polymorphic_portable_binary_iarchive.h | 56 ++ .../polymorphic_portable_binary_oarchive.h | 56 ++ src/wallet/CMakeLists.txt | 1 + src/wallet/wallet2.h | 3 + src/wallet/wallet2_basic/CMakeLists.txt | 74 ++ .../wallet2_boost_serialization.cpp | 39 + .../wallet2_boost_serialization.h | 559 ++++++++++++ src/wallet/wallet2_basic/wallet2_constants.h | 42 + src/wallet/wallet2_basic/wallet2_storage.cpp | 859 ++++++++++++++++++ src/wallet/wallet2_basic/wallet2_storage.h | 330 +++++++ src/wallet/wallet2_basic/wallet2_types.h | 373 ++++++++ src/wallet/wallet_errors.h | 6 +- .../functional_tests/functional_tests_rpc.py | 3 + tests/functional_tests/wallet.py | 18 + tests/unit_tests/CMakeLists.txt | 3 + tests/unit_tests/unit_tests_utils.h | 9 + tests/unit_tests/wallet_storage.cpp | 577 ++++++++++++ 18 files changed, 3006 insertions(+), 3 deletions(-) create mode 100644 src/serialization/polymorphic_portable_binary_iarchive.h create mode 100644 src/serialization/polymorphic_portable_binary_oarchive.h create mode 100644 src/wallet/wallet2_basic/CMakeLists.txt create mode 100644 src/wallet/wallet2_basic/wallet2_boost_serialization.cpp create mode 100644 src/wallet/wallet2_basic/wallet2_boost_serialization.h create mode 100644 src/wallet/wallet2_basic/wallet2_constants.h create mode 100644 src/wallet/wallet2_basic/wallet2_storage.cpp create mode 100644 src/wallet/wallet2_basic/wallet2_storage.h create mode 100644 src/wallet/wallet2_basic/wallet2_types.h create mode 100644 tests/unit_tests/wallet_storage.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index ed321a972d..4c73c29b06 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1097,6 +1097,7 @@ endif() include_directories(SYSTEM ${Boost_INCLUDE_DIRS}) if(MINGW) set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Wa,-mbig-obj") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wa,-mbig-obj") set(EXTRA_LIBRARIES mswsock;ws2_32;iphlpapi;crypt32;bcrypt) if(DEPENDS) set(ICU_LIBRARIES icuio icui18n icuuc icudata icutu iconv) diff --git a/src/serialization/polymorphic_portable_binary_iarchive.h b/src/serialization/polymorphic_portable_binary_iarchive.h new file mode 100644 index 0000000000..12439f6104 --- /dev/null +++ b/src/serialization/polymorphic_portable_binary_iarchive.h @@ -0,0 +1,56 @@ +// Copyright (c) 2023, The Monero Project +// +// 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. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT HOLDER 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. +// + +#pragma once + +#include +#include +#include + +namespace boost +{ +namespace archive +{ +class polymorphic_portable_binary_iarchive : + public detail::polymorphic_iarchive_route +{ +public: + polymorphic_portable_binary_iarchive(std::istream & is, unsigned int flags = 0) : + detail::polymorphic_iarchive_route(is, flags) + {} + ~polymorphic_portable_binary_iarchive() {} +}; + +} // namespace archive +} // namespace boost + +// required by export +BOOST_SERIALIZATION_REGISTER_ARCHIVE( + boost::archive::polymorphic_portable_binary_iarchive +) diff --git a/src/serialization/polymorphic_portable_binary_oarchive.h b/src/serialization/polymorphic_portable_binary_oarchive.h new file mode 100644 index 0000000000..aa6b03738f --- /dev/null +++ b/src/serialization/polymorphic_portable_binary_oarchive.h @@ -0,0 +1,56 @@ +// Copyright (c) 2023, The Monero Project +// +// 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. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT HOLDER 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. +// + +#pragma once + +#include +#include +#include + +namespace boost +{ +namespace archive +{ +class polymorphic_portable_binary_oarchive : + public detail::polymorphic_oarchive_route +{ +public: + polymorphic_portable_binary_oarchive(std::ostream & os, unsigned int flags = 0) : + detail::polymorphic_oarchive_route(os, flags) + {} + ~polymorphic_portable_binary_oarchive() {} +}; + +} // namespace archive +} // namespace boost + +// required by export +BOOST_SERIALIZATION_REGISTER_ARCHIVE( + boost::archive::polymorphic_portable_binary_oarchive +) diff --git a/src/wallet/CMakeLists.txt b/src/wallet/CMakeLists.txt index 48c7f1c5b2..69f4f4d830 100644 --- a/src/wallet/CMakeLists.txt +++ b/src/wallet/CMakeLists.txt @@ -104,3 +104,4 @@ if(NOT IOS) endif() add_subdirectory(api) +add_subdirectory(wallet2_basic) diff --git a/src/wallet/wallet2.h b/src/wallet/wallet2.h index 944fcb86ad..836a593479 100644 --- a/src/wallet/wallet2.h +++ b/src/wallet/wallet2.h @@ -93,6 +93,8 @@ namespace tools class wallet2; class Notify; + extern void check_wallet_9svHk1_cache_contents(const wallet2&); + class gamma_picker { public: @@ -225,6 +227,7 @@ namespace tools { friend class ::Serialization_portability_wallet_Test; friend class ::wallet_accessor_test; + friend void check_wallet_9svHk1_cache_contents(const wallet2&); friend class wallet_keys_unlocker; friend class wallet_device_callback; public: diff --git a/src/wallet/wallet2_basic/CMakeLists.txt b/src/wallet/wallet2_basic/CMakeLists.txt new file mode 100644 index 0000000000..eae5c44559 --- /dev/null +++ b/src/wallet/wallet2_basic/CMakeLists.txt @@ -0,0 +1,74 @@ +# Copyright (c) 2023, The Monero Project +# +# 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. +# +# 3. Neither the name of the copyright holder nor the names of its contributors may be +# used to endorse or promote products derived from this software without specific +# prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT HOLDER 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. + +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) + +set(wallet2_basic_sources + wallet2_boost_serialization.cpp + wallet2_storage.cpp +) + +set(wallet2_basic_headers + wallet2_constants.h + wallet2_storage.h + wallet2_types.h +) + +set(wallet2_basic_private_headers +) + +monero_private_headers(wallet2_basic + ${wallet2_basic_private_headers}) +monero_add_library(wallet2_basic + ${wallet2_basic_sources} + ${wallet2_basic_headers} + ${wallet2_basic_private_headers}) +target_link_libraries(wallet2_basic + PUBLIC + common + cryptonote_core + device_trezor + ${Boost_LOCALE_LIBRARY} + ${ICU_LIBRARIES} + ${Boost_SERIALIZATION_LIBRARY} + ${Boost_FILESYSTEM_LIBRARY} + ${Boost_SYSTEM_LIBRARY} + ${OPENSSL_LIBRARIES} + PRIVATE + ${EXTRA_LIBRARIES}) + +set_property(TARGET wallet2_basic PROPERTY EXCLUDE_FROM_ALL TRUE) + +if(IOS) + set(lib_folder lib-${ARCH}) +else() + set(lib_folder lib) +endif() + +install(FILES ${wallet2_basic_headers} + DESTINATION include/wallet/wallet2_basic) diff --git a/src/wallet/wallet2_basic/wallet2_boost_serialization.cpp b/src/wallet/wallet2_basic/wallet2_boost_serialization.cpp new file mode 100644 index 0000000000..008f84776b --- /dev/null +++ b/src/wallet/wallet2_basic/wallet2_boost_serialization.cpp @@ -0,0 +1,39 @@ +// Copyright (c) 2023, The Monero Project +// +// 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. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT HOLDER 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 "wallet2_boost_serialization.h" + +BOOST_CLASS_EXPORT_IMPLEMENT(::wallet2_basic::hashchain) +BOOST_CLASS_EXPORT_IMPLEMENT(::wallet2_basic::transfer_details) +BOOST_CLASS_EXPORT_IMPLEMENT(::wallet2_basic::multisig_info) +BOOST_CLASS_EXPORT_IMPLEMENT(::wallet2_basic::unconfirmed_transfer_details) +BOOST_CLASS_EXPORT_IMPLEMENT(::wallet2_basic::confirmed_transfer_details) +BOOST_CLASS_EXPORT_IMPLEMENT(::wallet2_basic::payment_details) +BOOST_CLASS_EXPORT_IMPLEMENT(::wallet2_basic::pool_payment_details) +BOOST_CLASS_EXPORT_IMPLEMENT(::wallet2_basic::address_book_row) +BOOST_CLASS_EXPORT_IMPLEMENT(::wallet2_basic::cache) diff --git a/src/wallet/wallet2_basic/wallet2_boost_serialization.h b/src/wallet/wallet2_basic/wallet2_boost_serialization.h new file mode 100644 index 0000000000..f0e77d7055 --- /dev/null +++ b/src/wallet/wallet2_basic/wallet2_boost_serialization.h @@ -0,0 +1,559 @@ +// Copyright (c) 2023, The Monero Project +// +// 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. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT HOLDER 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. + +#pragma once + +#include +#include +#include +#include + +#include "cryptonote_basic/cryptonote_boost_serialization.h" +#include "cryptonote_basic/account_boost_serialization.h" +#include "serialization/polymorphic_portable_binary_iarchive.h" +#include "serialization/polymorphic_portable_binary_oarchive.h" +#include "wallet2_storage.h" +#include "wallet2_types.h" + +namespace wallet2_basic +{ +struct HashchainAccessor +{ + hashchain& hc; + HashchainAccessor(hashchain& hc): hc(hc) {} + inline std::size_t& m_offset() { return hc.m_offset; } + inline crypto::hash& m_genesis() { return hc.m_genesis; } + inline std::deque& m_blockchain() { return hc.m_blockchain; } +}; +} // namespace wallet2_basic + +namespace boost +{ +namespace serialization +{ +template +void serialize(Archive &a, wallet2_basic::multisig_info::LR &x, const version_type ver) +{ + a & x.m_L; + a & x.m_R; +} + +template +void serialize(Archive &a, wallet2_basic::multisig_info &x, const version_type ver) +{ + a & x.m_signer; + a & x.m_LR; + a & x.m_partial_key_images; +} + +template +std::enable_if_t +initialize_transfer_details(Archive &a, wallet2_basic::transfer_details &x, const version_type ver) +{} + +template +std::enable_if_t +initialize_transfer_details(Archive &a, wallet2_basic::transfer_details &x, const version_type ver) +{ + if (ver < 1) + { + x.m_mask = rct::identity(); + x.m_amount = x.m_tx.vout[x.m_internal_output_index].amount; + } + if (ver < 2) + { + x.m_spent_height = 0; + } + if (ver < 4) + { + x.m_rct = x.m_tx.vout[x.m_internal_output_index].amount == 0; + } + if (ver < 6) + { + x.m_key_image_known = true; + } + if (ver < 7) + { + x.m_pk_index = 0; + } + if (ver < 8) + { + x.m_subaddr_index = {}; + } + if (ver < 9) + { + x.m_key_image_partial = false; + x.m_multisig_k.clear(); + x.m_multisig_info.clear(); + } + if (ver < 10) + { + x.m_key_image_request = false; + } + if (ver < 12) + { + x.m_frozen = false; + } +} + +template +void serialize(Archive &a, wallet2_basic::hashchain &x, const version_type ver) +{ + wallet2_basic::HashchainAccessor xaccess(x); + a & xaccess.m_offset(); + a & xaccess.m_genesis(); + a & xaccess.m_blockchain(); +} + +template +void serialize(Archive &a, wallet2_basic::transfer_details &x, const version_type ver) +{ + a & x.m_block_height; + a & x.m_global_output_index; + a & x.m_internal_output_index; + if (ver < 3) + { + cryptonote::transaction tx; + a & tx; + x.m_tx = (const cryptonote::transaction_prefix&)tx; + x.m_txid = cryptonote::get_transaction_hash(tx); + } + else + { + a & x.m_tx; + } + a & x.m_spent; + a & x.m_key_image; + if (ver < 1) + { + // ensure mask and amount are set + initialize_transfer_details(a, x, ver); + return; + } + a & x.m_mask; + a & x.m_amount; + if (ver < 2) + { + initialize_transfer_details(a, x, ver); + return; + } + a & x.m_spent_height; + if (ver < 3) + { + initialize_transfer_details(a, x, ver); + return; + } + a & x.m_txid; + if (ver < 4) + { + initialize_transfer_details(a, x, ver); + return; + } + a & x.m_rct; + if (ver < 5) + { + initialize_transfer_details(a, x, ver); + return; + } + if (ver < 6) + { + // v5 did not properly initialize + uint8_t u = 0; + a & u; + x.m_key_image_known = true; + return; + } + a & x.m_key_image_known; + if (ver < 7) + { + initialize_transfer_details(a, x, ver); + return; + } + a & x.m_pk_index; + if (ver < 8) + { + initialize_transfer_details(a, x, ver); + return; + } + a & x.m_subaddr_index; + if (ver < 9) + { + initialize_transfer_details(a, x, ver); + return; + } + a & x.m_multisig_info; + a & x.m_multisig_k; + a & x.m_key_image_partial; + if (ver < 10) + { + initialize_transfer_details(a, x, ver); + return; + } + a & x.m_key_image_request; + if (ver < 11) + { + initialize_transfer_details(a, x, ver); + return; + } + a & x.m_uses; + if (ver < 12) + { + initialize_transfer_details(a, x, ver); + return; + } + a & x.m_frozen; +} + +template +void serialize(Archive &a, wallet2_basic::unconfirmed_transfer_details &x, const version_type ver) +{ + a & x.m_change; + a & x.m_sent_time; + if (ver < 5) + { + cryptonote::transaction tx; + a & tx; + x.m_tx = (const cryptonote::transaction_prefix&)tx; + } + else + { + a & x.m_tx; + } + if (ver < 1) + return; + a & x.m_dests; + a & x.m_payment_id; + if (ver < 2) + return; + a & x.m_state; + if (ver < 3) + return; + a & x.m_timestamp; + if (ver < 4) + return; + a & x.m_amount_in; + a & x.m_amount_out; + if (ver < 6) + { + // v<6 may not have change accumulated in m_amount_out, which is a pain, + // as it's readily understood to be sum of outputs. + // We convert it to include change from v6 + if (!typename Archive::is_saving() && x.m_change != (uint64_t)1) + x.m_amount_out += x.m_change; + } + if (ver < 7) + { + x.m_subaddr_account = 0; + return; + } + a & x.m_subaddr_account; + a & x.m_subaddr_indices; + if (ver < 8) + return; + a & x.m_rings; +} + +template +void serialize(Archive &a, wallet2_basic::confirmed_transfer_details &x, const version_type ver) +{ + a & x.m_amount_in; + a & x.m_amount_out; + a & x.m_change; + a & x.m_block_height; + if (ver < 1) + return; + a & x.m_dests; + a & x.m_payment_id; + if (ver < 2) + return; + a & x.m_timestamp; + if (ver < 3) + { + // v<3 may not have change accumulated in m_amount_out, which is a pain, + // as it's readily understood to be sum of outputs. Whether it got added + // or not depends on whether it came from a unconfirmed_transfer_details + // (not included) or not (included). We can't reliably tell here, so we + // check whether either yields a "negative" fee, or use the other if so. + // We convert it to include change from v3 + if (!typename Archive::is_saving() && x.m_change != (uint64_t)1) + { + if (x.m_amount_in > (x.m_amount_out + x.m_change)) + x.m_amount_out += x.m_change; + } + } + if (ver < 4) + { + if (!typename Archive::is_saving()) + x.m_unlock_time = 0; + return; + } + a & x.m_unlock_time; + if (ver < 5) + { + x.m_subaddr_account = 0; + return; + } + a & x.m_subaddr_account; + a & x.m_subaddr_indices; + if (ver < 6) + return; + a & x.m_rings; +} + +template +void serialize(Archive& a, wallet2_basic::payment_details& x, const version_type ver) +{ + a & x.m_tx_hash; + a & x.m_amount; + a & x.m_block_height; + a & x.m_unlock_time; + if (ver < 1) + return; + a & x.m_timestamp; + if (ver < 2) + { + x.m_coinbase = false; + x.m_subaddr_index = {}; + return; + } + a & x.m_subaddr_index; + if (ver < 3) + { + x.m_coinbase = false; + x.m_fee = 0; + return; + } + a & x.m_fee; + if (ver < 4) + { + x.m_coinbase = false; + return; + } + a & x.m_coinbase; + if (ver < 5) + return; + a & x.m_amounts; +} + +template +void serialize(Archive& a, wallet2_basic::pool_payment_details& x, const version_type ver) +{ + a & x.m_pd; + a & x.m_double_spend_seen; +} + +template +void serialize(Archive& a, wallet2_basic::address_book_row& x, const version_type ver) +{ + a & x.m_address; + if (ver < 18) + { + crypto::hash payment_id; + a & payment_id; + x.m_has_payment_id = !(payment_id == crypto::null_hash); + if (x.m_has_payment_id) + { + bool is_long = false; + for (int i = 8; i < 32; ++i) + is_long |= payment_id.data[i]; + if (is_long) + { + MWARNING("Long payment ID ignored on address book load"); + x.m_payment_id = crypto::null_hash8; + x.m_has_payment_id = false; + } + else + memcpy(x.m_payment_id.data, payment_id.data, 8); + } + } + a & x.m_description; + if (ver < 17) + { + x.m_is_subaddress = false; + return; + } + a & x.m_is_subaddress; + if (ver < 18) + return; + a & x.m_has_payment_id; + if (x.m_has_payment_id) + a & x.m_payment_id; +} + +template +void serialize(Archive& a, wallet2_basic::cache& x, const version_type ver) +{ + using namespace wallet2_basic; + + uint64_t dummy_refresh_height = 0; // moved to keys file + if(ver < 5) + return; + if (ver < 19) + { + std::vector blockchain; + a & blockchain; + x.m_blockchain.clear(); + for (const auto &b: blockchain) + { + x.m_blockchain.push_back(b); + } + } + else + { + a & x.m_blockchain; + } + a & x.m_transfers; + a & x.m_account_public_address; + a & x.m_key_images.parent(); + if(ver < 6) + return; + a & x.m_unconfirmed_txs.parent(); + if(ver < 7) + return; + a & x.m_payments.parent(); + if(ver < 8) + return; + a & x.m_tx_keys.parent(); + if(ver < 9) + return; + a & x.m_confirmed_txs.parent(); + if(ver < 11) + return; + a & dummy_refresh_height; + if(ver < 12) + return; + a & x.m_tx_notes.parent(); + if(ver < 13) + return; + if (ver < 17) + { + // we're loading an old version, where m_unconfirmed_payments was a std::map + std::unordered_map m; + a & m; + x.m_unconfirmed_payments.clear(); + for (const auto& i : m) + x.m_unconfirmed_payments.insert({i.first, pool_payment_details{i.second, false}}); + } + if(ver < 14) + return; + if(ver < 15) + { + // we're loading an older wallet without a pubkey map, rebuild it + x.m_pub_keys.clear(); + for (size_t i = 0; i < x.m_transfers.size(); ++i) + { + const transfer_details &td = x.m_transfers[i]; + x.m_pub_keys.emplace(td.get_public_key(), i); + } + return; + } + a & x.m_pub_keys.parent(); + if(ver < 16) + return; + a & x.m_address_book; + if(ver < 17) + return; + if (ver < 22) + { + // we're loading an old version, where m_unconfirmed_payments payload was payment_details + std::unordered_multimap m; + a & m; + x.m_unconfirmed_payments.clear(); + for (const auto &i: m) + x.m_unconfirmed_payments.insert({i.first, pool_payment_details{i.second, false}}); + } + if(ver < 18) + return; + a & x.m_scanned_pool_txs[0]; + a & x.m_scanned_pool_txs[1]; + if (ver < 20) + return; + a & x.m_subaddresses.parent(); + std::unordered_map dummy_subaddresses_inv; + a & dummy_subaddresses_inv; + a & x.m_subaddress_labels; + a & x.m_additional_tx_keys.parent(); + if(ver < 21) + return; + a & x.m_attributes.parent(); + if(ver < 22) + return; + a & x.m_unconfirmed_payments.parent(); + if(ver < 23) + return; + a & (std::pair, std::vector>&) x.m_account_tags; + if(ver < 24) + return; + a & x.m_ring_history_saved; + if(ver < 25) + return; + a & x.m_last_block_reward; + if(ver < 26) + return; + a & x.m_tx_device.parent(); + if(ver < 27) + return; + a & x.m_device_last_key_image_sync; + if(ver < 28) + return; + a & x.m_cold_key_images.parent(); + if(ver < 29) + return; + crypto::secret_key dummy_rpc_client_secret_key; // Compatibility for old RPC payment system + a & dummy_rpc_client_secret_key; + if(ver < 30) + { + x.m_has_ever_refreshed_from_node = false; + return; + } + a & x.m_has_ever_refreshed_from_node; +} +} // namespace serialization +} // namespace boost + +BOOST_CLASS_VERSION(wallet2_basic::hashchain, 0) +BOOST_CLASS_VERSION(wallet2_basic::transfer_details, 12) +BOOST_CLASS_VERSION(wallet2_basic::multisig_info::LR, 0) +BOOST_CLASS_VERSION(wallet2_basic::multisig_info, 1) +BOOST_CLASS_VERSION(wallet2_basic::unconfirmed_transfer_details, 8) +BOOST_CLASS_VERSION(wallet2_basic::confirmed_transfer_details, 6) +BOOST_CLASS_VERSION(wallet2_basic::payment_details, 5) +BOOST_CLASS_VERSION(wallet2_basic::pool_payment_details, 1) +BOOST_CLASS_VERSION(wallet2_basic::address_book_row, 18) +BOOST_CLASS_VERSION(wallet2_basic::cache, 30) + +BOOST_CLASS_EXPORT_KEY(::wallet2_basic::hashchain) +BOOST_CLASS_EXPORT_KEY(::wallet2_basic::transfer_details) +BOOST_CLASS_EXPORT_KEY(::wallet2_basic::multisig_info::LR) +BOOST_CLASS_EXPORT_KEY(::wallet2_basic::multisig_info) +BOOST_CLASS_EXPORT_KEY(::wallet2_basic::unconfirmed_transfer_details) +BOOST_CLASS_EXPORT_KEY(::wallet2_basic::confirmed_transfer_details) +BOOST_CLASS_EXPORT_KEY(::wallet2_basic::payment_details) +BOOST_CLASS_EXPORT_KEY(::wallet2_basic::pool_payment_details) +BOOST_CLASS_EXPORT_KEY(::wallet2_basic::address_book_row) +BOOST_CLASS_EXPORT_KEY(::wallet2_basic::cache) diff --git a/src/wallet/wallet2_basic/wallet2_constants.h b/src/wallet/wallet2_basic/wallet2_constants.h new file mode 100644 index 0000000000..82623bca80 --- /dev/null +++ b/src/wallet/wallet2_basic/wallet2_constants.h @@ -0,0 +1,42 @@ +// Copyright (c) 2023, The Monero Project +// +// 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. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT HOLDER 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. + +#pragma once + +#include +#include + +namespace wallet2_basic +{ + // a minute and a half + constexpr uint32_t DEFAULT_INACTIVITY_LOCK_TIMEOUT = 90; + + constexpr size_t SUBADDRESS_LOOKAHEAD_MAJOR = 50; + constexpr size_t SUBADDRESS_LOOKAHEAD_MINOR = 200; + +} // namespace wallet2_basic diff --git a/src/wallet/wallet2_basic/wallet2_storage.cpp b/src/wallet/wallet2_basic/wallet2_storage.cpp new file mode 100644 index 0000000000..2d03ab0420 --- /dev/null +++ b/src/wallet/wallet2_basic/wallet2_storage.cpp @@ -0,0 +1,859 @@ +// Copyright (c) 2023, The Monero Project +// +// 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. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT HOLDER 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 +#include +#include +#include +#include + +#include "cryptonote_basic/account.h" +#include "device/device_cold.hpp" +#include "device_trezor/device_trezor.hpp" +#include "file_io_utils.h" +#include "serialization/binary_utils.h" +#include "storages/portable_storage_template_helper.h" +#include "wallet2_boost_serialization.h" +#include "wallet2_storage.h" + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "wallet.wallet2_basic.storage" + +using namespace boost::archive; +using namespace tools; +using rapidjson::Document; + +#define TRY_NOFAIL(stmt) try { stmt; } catch (...) {} + +namespace +{ +struct cache_file_data +{ + crypto::chacha_iv iv; + std::string cache_data; + + BEGIN_SERIALIZE_OBJECT() + FIELD(iv) + FIELD(cache_data) + END_SERIALIZE() +}; + +struct keys_file_data +{ + crypto::chacha_iv iv; + std::string account_data; + + BEGIN_SERIALIZE_OBJECT() + FIELD(iv) + FIELD(account_data) + END_SERIALIZE() +}; + +static hw::i_device_callback noop_device_cb; + +// https://github.com/monero-project/monero/blob/67d190ce7c33602b6a3b804f633ee1ddb7fbb4a1/src/wallet/wallet2.cpp#L156 +static constexpr const char WALLET2_ASCII_OUTPUT_MAGIC[] = "MoneroAsciiDataV1"; + +template +wallet2_basic::cache boost_deserialize_cache(const std::string& cache_data) +{ + wallet2_basic::cache c; + std::istringstream iss(cache_data); + Archive ar(iss); + ar >> c; + return c; +} + +void save_pem_ascii_file(const std::string& path, const std::string& data) +{ + std::unique_ptr fp(fopen(path.c_str(), "w+"), &fclose); + CHECK_AND_ASSERT_THROW_MES(fp, + "Failed to open wallet file for writing: " << path << ": " << strerror(errno)); + + const unsigned char* const data_uc = reinterpret_cast(data.data()); + CHECK_AND_ASSERT_THROW_MES(PEM_write(fp.get(), WALLET2_ASCII_OUTPUT_MAGIC, "", data_uc, data.size()), + "Failed to PEM write to file: " << path); +} + +std::string load_pem_ascii_string(const std::string& pem_contents) +{ + std::unique_ptr bb(BIO_new_mem_buf(pem_contents.data(), pem_contents.size()), &BIO_free); + + char* name = NULL; + char* header = NULL; + unsigned char* data = NULL; + long data_len = 0; + const bool read_success = PEM_read_bio(bb.get(), &name, &header, &data, &data_len); + + std::string result_data; + bool alloc_success = false; + try + { + result_data = std::string((const char*) data, data_len); + alloc_success = true; + } + catch (...) {} + + OPENSSL_free((void *) name); + OPENSSL_free((void *) header); + OPENSSL_free((void *) data); + + CHECK_AND_ASSERT_THROW_MES(read_success, "Could not read string contents as PEM data"); + CHECK_AND_ASSERT_THROW_MES(alloc_success, "Could not allocate new result string from PEM read"); + + return result_data; +} + +/*************************************************************************************************** +********************************JSON ADAPTER HELPER FUNCTIONS*************************************** +***************************************************************************************************/ + +template void assign_when_mutable(T& dst, const U& src) { dst = src; } +template void assign_when_mutable(const T& dst, const U& src) {} + +template const T& as_const_ref(T& t) { return t; } + +template std::enable_if_t::value || std::is_enum::value> +adapt_json_field(T& out, const Document& json, const char* name, bool mand) +{ + const rapidjson::Value::ConstMemberIterator memb_it = json.FindMember(name); + if (memb_it != json.MemberEnd()) + { + if (memb_it->value.IsInt()) + out = static_cast(memb_it->value.GetInt()); + else if (memb_it->value.IsUint()) + out = static_cast(memb_it->value.GetUint()); + else if (memb_it->value.IsUint64()) + out = static_cast(memb_it->value.GetUint64()); + else + ASSERT_MES_AND_THROW("Field " << name << " found in JSON, but not an int-like number"); + } + else if (mand) + ASSERT_MES_AND_THROW("Field " << name << " not found in JSON"); +} + +void adapt_json_field(std::string& out, const Document& json, const char* name, bool mand) +{ + const rapidjson::Value::ConstMemberIterator memb_it = json.FindMember(name); + if (memb_it != json.MemberEnd()) + { + if (memb_it->value.IsString()) + out = std::string(memb_it->value.GetString(), memb_it->value.GetStringLength()); + else + ASSERT_MES_AND_THROW("Field " << name << " found in JSON, but not " << "String"); + } + else if (mand) + ASSERT_MES_AND_THROW("Field " << name << " not found in JSON"); +} + +// Load arbitrary types from JSON string fields represented in binary_archive format +template std::enable_if_t::value && !std::is_enum::value> +adapt_json_field(T& out, const Document& json, const char* name, bool mand) +{ + std::string binary_repr; + adapt_json_field(binary_repr, json, name, mand); + const bool r = serialization::parse_binary(binary_repr, out); + CHECK_AND_ASSERT_THROW_MES(r, "Could not parse object from binary archive in JSON field"); +} + +template std::enable_if_t::value || std::is_enum::value> +adapt_json_field(const T& in, Document& json, const char* name, bool) +{ + rapidjson::Value k(name, json.GetAllocator()); + rapidjson::Value v; + if (in < T{}) // Is negative? + v.SetInt(static_cast(in)); + else // Is positive + v.SetUint64(static_cast(in)); + json.AddMember(k, v, json.GetAllocator()); +} + +void adapt_json_field(const std::string& in, Document& json, const char* name, bool) +{ + rapidjson::Value k(name, json.GetAllocator()); + rapidjson::Value v(in.data(), in.size(), json.GetAllocator()); + json.AddMember(k, v, json.GetAllocator()); +} + +// Store arbitrary types to JSON string fields represented in binary_archive format +template std::enable_if_t::value && !std::is_enum::value> +adapt_json_field(const T& in, Document& json, const char* name, bool) +{ + std::string binary_repr; + const bool r = serialization::dump_binary(const_cast(in), binary_repr); + CHECK_AND_ASSERT_THROW_MES(r, "Could not represent object in binary archive"); + adapt_json_field(as_const_ref(binary_repr), json, name, true); +} + +template +void adapt_json_field(const T&, const Document&, const char*, bool) +{} + +} // anonymous namespace + +namespace wallet2_basic +{ +template +void adapt_keysdata_tofrom_json_object +( + detail::reference_mutate_enabled kd, + detail::reference_mutate_enabled obj, + const crypto::chacha_key& keys_key, + bool downgrade_to_watch_only +); + +/*************************************************************************************************** +*************************************CACHE STORAGE************************************************** +***************************************************************************************************/ +crypto::chacha_key cache::pwd_to_cache_key(const char* pwd, size_t len, uint64_t kdf_rounds) +{ + static_assert(crypto::HASH_SIZE == sizeof(crypto::chacha_key), "Mismatched sizes of hash and chacha key"); + + crypto::chacha_key key; + crypto::generate_chacha_key(pwd, len, key, kdf_rounds); + + epee::mlocked> cache_key_data; + memcpy(cache_key_data.data(), &key, crypto::HASH_SIZE); + cache_key_data[crypto::HASH_SIZE] = config::HASH_KEY_WALLET_CACHE; + crypto::cn_fast_hash(cache_key_data.data(), crypto::HASH_SIZE+1, reinterpret_cast(key)); + + return key; +} + +crypto::chacha_key cache::account_to_old_cache_key(const cryptonote::account_base& account, uint64_t kdf_rounds) +{ + crypto::chacha_key key; + hw::device &hwdev = account.get_device(); + const bool r = hwdev.generate_chacha_key(account.get_keys(), key, kdf_rounds); + THROW_WALLET_EXCEPTION_IF(!r, error::wallet_internal_error, "device failed to generate chacha key"); + return key; +} + +cache cache::load_from_memory +( + const std::string& cache_file_buf, + const epee::wipeable_string& password, + const cryptonote::account_base& wallet_account, + uint64_t kdf_rounds +) +{ + // Try to deserialize cache file buf into `cache_file_data` type. If success, + // then we are dealing with encrypted cache + cache_file_data cfd; + const bool encrypted_cache = ::serialization::parse_binary(cache_file_buf, cfd); + + if (encrypted_cache) + { + LOG_PRINT_L1("Taking encrypted wallet cache load path..."); + + // Decrypt cache contents into buffer + crypto::chacha_key cache_key = pwd_to_cache_key(password.data(), password.size(), kdf_rounds); + std::string cache_data; + cache_data.resize(cfd.cache_data.size()); + crypto::chacha20(cfd.cache_data.data(), cfd.cache_data.size(), cache_key, cfd.iv, &cache_data[0]); + + LOG_PRINT_L1("Trying to read from recent binary archive"); + try + { + cache c; + binary_archive ar{epee::strspan(cache_data)}; + if (::serialization::serialize(ar, c)) + if (::serialization::check_stream_state(ar)) + return c; + } + catch (...) {} + + LOG_PRINT_L1("Trying to read from binary archive with varint incompatibility"); + try + { + cache c; + binary_archive ar{epee::strspan(cache_data)}; + ar.enable_varint_bug_backward_compatibility(); + if (::serialization::serialize(ar, c)) + if (::serialization::check_stream_state(ar)) + return c; + } + catch (...) {} + + LOG_PRINT_L1("Trying to read from boost portable binary archive"); + TRY_NOFAIL(return boost_deserialize_cache(cache_data)); + + LOG_PRINT_L1("Switching to decryption key derived from account keys..."); + cache_key = account_to_old_cache_key(wallet_account, kdf_rounds); + crypto::chacha20(cfd.cache_data.data(), cfd.cache_data.size(), cache_key, cfd.iv, &cache_data[0]); + + LOG_PRINT_L1("Trying to read from boost portable binary archive encrypted with account keys"); + TRY_NOFAIL(return boost_deserialize_cache(cache_data)); + + LOG_PRINT_L1("Switching to old chacha8 encryption..."); + crypto::chacha8(cfd.cache_data.data(), cfd.cache_data.size(), cache_key, cfd.iv, &cache_data[0]); + + LOG_PRINT_L1("Trying to read from boost portable binary archive encrypted with account keys & chacha8"); + TRY_NOFAIL(return boost_deserialize_cache(cache_data)); + + LOG_PRINT_L1("Trying to read from boost UNportable binary archive encrypted with account keys & chacha8"); + TRY_NOFAIL(return boost_deserialize_cache(cache_data)); + } + else // not encrypted cache + { + LOG_PRINT_L1("Taking unencrypted wallet cache load path..."); + + LOG_PRINT_L1("Trying to read from boost portable binary archive unencrypted"); + TRY_NOFAIL(return boost_deserialize_cache(cache_file_buf)); + + LOG_PRINT_L1("Trying to read from boost UNportable binary archive unencrypted"); + TRY_NOFAIL(return boost_deserialize_cache(cache_file_buf)); + } + + THROW_WALLET_EXCEPTION(error::wallet_internal_error, "failed to load wallet cache"); +} + +std::string cache::store_to_memory(const epee::wipeable_string& password, uint64_t kdf_rounds) const +{ + return store_to_memory(pwd_to_cache_key(password.data(), password.size(), kdf_rounds)); +} + +std::string cache::store_to_memory(const crypto::chacha_key& encryption_key) const +{ + // Serialize cache + std::stringstream oss; + binary_archive ar1(oss); + THROW_WALLET_EXCEPTION_IF(!::serialization::serialize(ar1, const_cast(*this)), + error::wallet_internal_error, "Failed to serialize cache"); + + // Prepare outer cache_file_data data structure + std::string cache_pt = oss.str(); + cache_file_data cfd; + cfd.iv = crypto::rand(); + cfd.cache_data.resize(cache_pt.size()); + + // Encrypt cache + crypto::chacha20(cache_pt.data(), cache_pt.size(), encryption_key, cfd.iv, &cfd.cache_data[0]); + + // Serialize cache_file_data structure + oss.str(""); + binary_archive ar2(oss); + THROW_WALLET_EXCEPTION_IF(!::serialization::serialize(ar2, cfd), + error::wallet_internal_error, "Failed to serialize outer cache file data"); + + return oss.str(); +} + +/*************************************************************************************************** +*********************************WALLET KEYS STORAGE************************************************ +***************************************************************************************************/ + +crypto::chacha_key keys_data::pwd_to_keys_data_key(const char* pwd, size_t len, uint64_t kdf_rounds) +{ + crypto::chacha_key key; + crypto::generate_chacha_key(pwd, len, key, kdf_rounds); + return key; +} + +keys_data keys_data::load_from_memory +( + const std::string& keys_file_buf, + const epee::wipeable_string& password, + cryptonote::network_type nettype, + uint64_t kdf_rounds +) +{ + const crypto::chacha_key encryption_key = pwd_to_keys_data_key(password.data(), password.size(), kdf_rounds); + return load_from_memory(keys_file_buf, encryption_key, nettype); +} + +keys_data keys_data::load_from_memory +( + const std::string& keys_file_buf, + const crypto::chacha_key& encryption_key, + cryptonote::network_type nettype +) +{ + // Deserialize encrypted data and IV into `keys_file_data` structure + keys_file_data kfd; + bool r = ::serialization::parse_binary(keys_file_buf, kfd); + THROW_WALLET_EXCEPTION_IF(!r, error::wallet_internal_error, "internal error: failed to deserialize keys buffer"); + + // Derive chacha decryption key from password and decrypt key buffer + std::string decrypted_keys_data; + decrypted_keys_data.resize(kfd.account_data.size()); + crypto::chacha20(kfd.account_data.data(), kfd.account_data.size(), encryption_key, kfd.iv, &decrypted_keys_data[0]); + + rapidjson::Document json; + if (json.Parse(decrypted_keys_data.c_str()).HasParseError() || !json.IsObject()) + crypto::chacha8(kfd.account_data.data(), kfd.account_data.size(), encryption_key, kfd.iv, &decrypted_keys_data[0]); + + keys_data kd; + kd.m_nettype = nettype; + + if (json.Parse(decrypted_keys_data.c_str()).HasParseError()) + { + CHECK_AND_ASSERT_THROW_MES(nettype != cryptonote::UNDEFINED, + "No network type was provided and we can't deduce nettype from old wallet keys files"); + kd.is_old_file_format = true; + r = epee::serialization::load_t_from_binary(kd.m_account, decrypted_keys_data); + THROW_WALLET_EXCEPTION_IF(!r, error::invalid_password); + } + else if (json.IsObject()) // The contents should be JSON if the wallet follows the new format. + { + adapt_keysdata_tofrom_json_object(kd, json, encryption_key, false); + } + else + { + THROW_WALLET_EXCEPTION(error::wallet_internal_error, + "malformed wallet keys JSON: Document root is not an object"); + } + + return kd; +} + +std::string keys_data::store_to_memory +( + const epee::wipeable_string& password, + bool downgrade_to_watch_only, + uint64_t kdf_rounds +) const +{ + const crypto::chacha_key encryption_key = pwd_to_keys_data_key(password.data(), password.size(), kdf_rounds); + return store_to_memory(encryption_key, downgrade_to_watch_only); +} + +std::string keys_data::store_to_memory +( + const crypto::chacha_key& encryption_key, + bool downgrade_to_watch_only +) const +{ + // Create JSON object containing all the information we need about our keys data + rapidjson::Document json; + json.SetObject(); + adapt_keysdata_tofrom_json_object(*this, json, encryption_key, downgrade_to_watch_only); + + // Serialize the JSON object + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + json.Accept(writer); + + // Encrypt the JSON buffer into a keys_file_data structure + keys_file_data kfd; + kfd.account_data.resize(buffer.GetSize()); + kfd.iv = crypto::rand(); + crypto::chacha20(buffer.GetString(), buffer.GetSize(), encryption_key, kfd.iv, &kfd.account_data[0]); + + // Serialize the keys_file_data structure as a binary archive + std::string final_buf; + const bool r = ::serialization::dump_binary(kfd, final_buf); + CHECK_AND_ASSERT_THROW_MES(r, "Failed to serialize keys_file_data into binary archive"); + + return final_buf; +} + +void keys_data::setup_account_keys_and_devices +( + const epee::wipeable_string& password, + hw::i_device_callback* device_cb, + uint64_t kdf_rounds +) +{ + const crypto::chacha_key encryption_key = pwd_to_keys_data_key(password.data(), password.size(), kdf_rounds); + setup_account_keys_and_devices(encryption_key, device_cb); +} + +void keys_data::setup_account_keys_and_devices +( + const crypto::chacha_key& encryption_key, + hw::i_device_callback* device_cb +) +{ + if (m_key_device_type == hw::device::device_type::LEDGER || m_key_device_type == hw::device::device_type::TREZOR) + { + LOG_PRINT_L0("Account on device. Initing device..."); + hw::device &hwdev = reconnect_device(device_cb); + + cryptonote::account_public_address device_account_public_address; + bool fetch_device_address = true; + + ::hw::device_cold* dev_cold = nullptr; + if (m_key_device_type == hw::device::device_type::TREZOR && (dev_cold = dynamic_cast<::hw::device_cold*>(&hwdev)) != nullptr) + { + THROW_WALLET_EXCEPTION_IF( + !dev_cold->get_public_address_with_no_passphrase(device_account_public_address), + error::wallet_internal_error, "Cannot get a device address"); + if (device_account_public_address == m_account.get_keys().m_account_address) + { + LOG_PRINT_L0("Wallet opened with an empty passphrase"); + fetch_device_address = false; + dev_cold->set_use_empty_passphrase(true); + } + else + { + fetch_device_address = true; + LOG_PRINT_L0("Wallet opening with an empty passphrase failed. Retry again: " << fetch_device_address); + dev_cold->reset_session(); + } + } + + if (fetch_device_address) + { + THROW_WALLET_EXCEPTION_IF(!hwdev.get_public_address(device_account_public_address), + error::wallet_internal_error, "Cannot get a device address"); + } + + THROW_WALLET_EXCEPTION_IF(device_account_public_address != m_account.get_keys().m_account_address, + error::wallet_internal_error, + "Device wallet does not match wallet address. If the device uses the passphrase feature, " + "please check whether the passphrase was entered correctly (it may have been misspelled - " + "different passphrases generate different wallets, passphrase is case-sensitive). " + "Device address: " + cryptonote::get_account_address_as_str(m_nettype, false, device_account_public_address) + + ", wallet address: " + m_account.get_public_address_str(m_nettype)); + LOG_PRINT_L0("Device inited..."); + } + else if (requires_external_device()) + { + THROW_WALLET_EXCEPTION(error::wallet_internal_error, "hardware device not supported"); + } + + hw::device& hwdev = m_account.get_keys().get_device(); + const bool view_only = m_watch_only || m_multisig || hwdev.device_protocol() == hw::device::PROTOCOL_COLD; + const bool keys_verified = verify_account_keys(view_only); + CHECK_AND_ASSERT_THROW_MES(keys_verified, "Device does not appear to correspond to this wallet file"); +} + +bool keys_data::verify_account_keys +( + bool view_only, + hw::device* alt_device +) const +{ + return wallet2_basic::verify_account_keys(m_account.get_keys(), view_only, alt_device); +} + +hw::device& keys_data::reconnect_device(hw::i_device_callback* device_cb) +{ +#ifdef WITH_DEVICE_TREZOR + hw::trezor::register_all(); +#endif + hw::device& hwdev = hw::get_device(m_device_name); + + THROW_WALLET_EXCEPTION_IF(!hwdev.set_name(m_device_name), error::wallet_internal_error, + "Could not set device name " + m_device_name); + hwdev.set_network_type(m_nettype); + hwdev.set_derivation_path(m_device_derivation_path); + hwdev.set_callback(device_cb ? device_cb : &noop_device_cb); + THROW_WALLET_EXCEPTION_IF(!hwdev.init(), error::wallet_internal_error, + "Could not initialize the device " + m_device_name); + THROW_WALLET_EXCEPTION_IF(!hwdev.connect(), error::wallet_internal_error, + "Could not connect to the device " + m_device_name); + m_account.set_device(hwdev); + + return hwdev; +} + +#define ADAPT_JSON_FIELD_N(name, jtype, mandatory, var) \ + do { \ + detail::reference_mutate_enabled, !SAVING> var_ref = var; \ + adapt_json_field(var_ref, obj, #name, mandatory); \ + } while (0); \ + +#define ADAPT_JSON_FIELD(name, jtype, mandatory) \ + ADAPT_JSON_FIELD_N(name, jtype, mandatory, kd.m_##name) \ + +template +void adapt_keysdata_tofrom_json_object +( + detail::reference_mutate_enabled kd, + detail::reference_mutate_enabled obj, + const crypto::chacha_key& keys_key, + bool downgrade_to_watch_only +) +{ + // Important prereq: we assume we already know obj is an object and not an array, number, etc + + // We always encrypt the account when storing now, but very old wallets didn't + bool account_keys_are_encrypted = SAVING; + ADAPT_JSON_FIELD_N(encrypted_secret_keys, Int, false, account_keys_are_encrypted); + assign_when_mutable(kd.m_keys_were_encrypted_on_load, account_keys_are_encrypted); + + if (SAVING) // Saving account to JSON + { + cryptonote::account_base encrypted_account = kd.m_account; + if (downgrade_to_watch_only) + encrypted_account.forget_spend_key(); + encrypted_account.encrypt_keys(keys_key); + const epee::byte_slice account_data_slice = epee::serialization::store_t_to_binary(encrypted_account); + const std::string account_data(reinterpret_cast(account_data_slice.data()), account_data_slice.size()); + ADAPT_JSON_FIELD_N(key_data, String, true, account_data); + } + else // Loading account from JSON + { + std::string account_data; + ADAPT_JSON_FIELD_N(key_data, String, true, account_data); + cryptonote::account_base decrypted_account; + CHECK_AND_ASSERT_THROW_MES( + epee::serialization::load_t_from_binary(decrypted_account, account_data), + "Could not parse account keys from EPEE binary"); + if (account_keys_are_encrypted) + decrypted_account.decrypt_keys(keys_key); + assign_when_mutable(kd.m_account, decrypted_account); + } + + ADAPT_JSON_FIELD(nettype, Uint, kd.m_nettype == cryptonote::UNDEFINED); + CHECK_AND_ASSERT_THROW_MES( + kd.m_nettype == cryptonote::MAINNET || + kd.m_nettype == cryptonote::TESTNET || + kd.m_nettype == cryptonote::STAGENET || + kd.m_nettype == cryptonote::FAKECHAIN, + "unrecognized network type for keys_data"); + + ADAPT_JSON_FIELD(multisig, Int, false); + ADAPT_JSON_FIELD(multisig_threshold, Uint, kd.m_multisig); + ADAPT_JSON_FIELD(multisig_rounds_passed, Uint, false); + ADAPT_JSON_FIELD(enable_multisig, Int, false); + ADAPT_JSON_FIELD(multisig_signers, binary_archive, kd.m_multisig); + ADAPT_JSON_FIELD(multisig_derivations, binary_archive, false); + + ADAPT_JSON_FIELD(watch_only, Int, false); + ADAPT_JSON_FIELD(confirm_non_default_ring_size, Int, false); + ADAPT_JSON_FIELD(ask_password, Int, false); // @TODO: Check AskPasswordType + ADAPT_JSON_FIELD(refresh_type, Int, false); // @TODO: Check RefreshType + ADAPT_JSON_FIELD(skip_to_height, Uint64, false); + ADAPT_JSON_FIELD(max_reorg_depth, Uint64, false); + ADAPT_JSON_FIELD(min_output_count, Uint, false); + ADAPT_JSON_FIELD(min_output_value, Uint64, false); + ADAPT_JSON_FIELD(merge_destinations, Int, false); + ADAPT_JSON_FIELD(confirm_backlog, Int, false); + ADAPT_JSON_FIELD(confirm_backlog_threshold, Uint, false); + ADAPT_JSON_FIELD(confirm_export_overwrite, Int, false); + ADAPT_JSON_FIELD(auto_low_priority, Int, false); + ADAPT_JSON_FIELD(confirm_export_overwrite, Int, false); + ADAPT_JSON_FIELD(segregate_pre_fork_outputs, Int, false); + ADAPT_JSON_FIELD(key_reuse_mitigation2, Int, false); + ADAPT_JSON_FIELD(segregation_height, Uint, false); + ADAPT_JSON_FIELD(ignore_fractional_outputs, Int, false); + ADAPT_JSON_FIELD(ignore_outputs_above, Uint64, false); + ADAPT_JSON_FIELD(ignore_outputs_below, Uint64, false); + ADAPT_JSON_FIELD(track_uses, Int, false); + ADAPT_JSON_FIELD(show_wallet_name_when_locked, Int, false); + ADAPT_JSON_FIELD(inactivity_lock_timeout, Uint, false); + ADAPT_JSON_FIELD(setup_background_mining, Int, false); + ADAPT_JSON_FIELD(subaddress_lookahead_major, Uint, false); + ADAPT_JSON_FIELD(subaddress_lookahead_minor, Uint, false); + ADAPT_JSON_FIELD(always_confirm_transfers, Int, false); + ADAPT_JSON_FIELD(print_ring_members, Int, false); + ADAPT_JSON_FIELD(store_tx_info, Int, false); + ADAPT_JSON_FIELD(default_mixin, Uint, false); + ADAPT_JSON_FIELD(export_format, Int, false); // @TODO Check ExportFormat + ADAPT_JSON_FIELD(load_deprecated_formats, Int, false); + ADAPT_JSON_FIELD(default_priority, Uint, false); + ADAPT_JSON_FIELD(auto_refresh, Int, false); + ADAPT_JSON_FIELD(device_derivation_path, String, false); + + ADAPT_JSON_FIELD_N(store_tx_keys, Int, false, kd.m_store_tx_info); // backward compat + ADAPT_JSON_FIELD_N(default_fee_multiplier, Uint, false, kd.m_default_priority); // backward compat + ADAPT_JSON_FIELD_N(refresh_height, Uint64, false, kd.m_refresh_from_block_height); + ADAPT_JSON_FIELD_N(key_on_device, Int, false, kd.m_key_device_type); + ADAPT_JSON_FIELD_N(seed_language, String, false, kd.seed_language); + + assign_when_mutable(kd.m_device_name, (kd.m_key_device_type == hw::device::device_type::LEDGER) ? "Ledger" : "default"); + ADAPT_JSON_FIELD(device_name, String, false); + + ADAPT_JSON_FIELD(original_keys_available, Int, false); + if (kd.m_original_keys_available) + { + std::string original_address, original_view_secret_key; + if (SAVING) + { + original_address = get_account_address_as_str(kd.m_nettype, false, kd.m_original_address); + ADAPT_JSON_FIELD_N(original_address, String, true, original_address); + original_view_secret_key = epee::string_tools::pod_to_hex(kd.m_original_view_secret_key); + ADAPT_JSON_FIELD_N(original_view_secret_key, String, true, original_view_secret_key); + } + else // loading original address + { + ADAPT_JSON_FIELD_N(original_address, String, true, original_address); + cryptonote::address_parse_info info; + CHECK_AND_ASSERT_THROW_MES(get_account_address_from_str(info, kd.m_nettype, original_address), + "Failed to parse original_address from JSON"); + assign_when_mutable(kd.m_original_address, info.address); + + ADAPT_JSON_FIELD_N(original_view_secret_key, String, true, original_view_secret_key); + crypto::secret_key original_view_secret_key_pod; + CHECK_AND_ASSERT_THROW_MES( + epee::string_tools::hex_to_pod(original_view_secret_key, original_view_secret_key_pod), + "Failed to parse original_view_secret_key from JSON"); + assign_when_mutable(kd.m_original_view_secret_key, original_view_secret_key_pod); + } + } +} + +/*************************************************************************************************** +********************************** MISC ACCOUNT UTILS ********************************************* +***************************************************************************************************/ + +bool verify_account_keys +( + const cryptonote::account_keys& keys, + bool view_only, + hw::device* hwdev +) +{ + if (nullptr == hwdev) + { + hwdev = std::addressof(keys.get_device()); + CHECK_AND_ASSERT_THROW_MES(hwdev, "Account device is NULL and no alternate was provided"); + } + + if (!hwdev->verify_keys(keys.m_view_secret_key, keys.m_account_address.m_view_public_key)) + return false; + + if (!view_only) + if (!hwdev->verify_keys(keys.m_spend_secret_key, keys.m_account_address.m_spend_public_key)) + return false; + + return true; +} + +/*************************************************************************************************** +********************* WALLET KEYS/CACHE COMBINATION LOADING/STORING ******************************** +***************************************************************************************************/ + +void load_keys_and_cache_from_memory +( + const std::string& cache_file_buf, + const std::string& keys_file_buf, + const epee::wipeable_string& password, + cache& c, + keys_data& k, + cryptonote::network_type nettype, + bool allow_external_devices_setup, + hw::i_device_callback* device_cb, + uint64_t kdf_rounds +) +{ + k = keys_data::load_from_memory(keys_file_buf, password, nettype, kdf_rounds); + if (!k.requires_external_device() || allow_external_devices_setup) + { + k.setup_account_keys_and_devices(password, device_cb, kdf_rounds); + } + c = cache::load_from_memory(cache_file_buf, password, k.m_account, kdf_rounds); +} + +void load_keys_and_cache_from_file +( + const std::string& cache_path, + const epee::wipeable_string& password, + cache& c, + keys_data& k, + cryptonote::network_type nettype, + std::string keys_path, + bool allow_external_devices_setup, + hw::i_device_callback* device_cb, + uint64_t kdf_rounds +) +{ + if (keys_path.empty()) + { + keys_path = cache_path + ".keys"; + } + + std::string keys_file_contents; + CHECK_AND_ASSERT_THROW_MES(epee::file_io_utils::load_file_to_string(keys_path, keys_file_contents), + "Could not load keys wallet file: " << keys_path); + + try + { + k = keys_data::load_from_memory(keys_file_contents, password, nettype, kdf_rounds); + } + catch (...) + { + keys_file_contents = load_pem_ascii_string(keys_file_contents); + k = keys_data::load_from_memory(keys_file_contents, password, nettype, kdf_rounds); + } + + if (!k.requires_external_device() || allow_external_devices_setup) + { + k.setup_account_keys_and_devices(password, device_cb, kdf_rounds); + } + + std::string cache_file_buf; + const bool loaded_cache = epee::file_io_utils::load_file_to_string(cache_path, cache_file_buf); + + if (loaded_cache) + { + c = cache::load_from_memory(cache_file_buf, password, k.m_account, kdf_rounds); + } + else + { + MWARNING("Could not load cache from filesystem, returning default cache"); + c = cache(); + } +} + +void store_keys_and_cache_to_memory +( + const cache& c, + const keys_data& k, + const epee::wipeable_string& password, + std::string& cache_buf, + std::string& keys_buf, + uint64_t kdf_rounds +) +{ + cache_buf = c.store_to_memory(password, kdf_rounds); + keys_buf = k.store_to_memory(password, false, kdf_rounds); +} + +void store_keys_and_cache_to_file +( + const cache& c, + const keys_data& k, + const epee::wipeable_string& password, + const std::string& cache_path, + uint64_t kdf_rounds, + ExportFormat keys_file_format +) +{ + const std::string keys_path = cache_path + ".keys"; + + std::string file_buf = c.store_to_memory(password, kdf_rounds); + CHECK_AND_ASSERT_THROW_MES(epee::file_io_utils::save_string_to_file(cache_path, file_buf), + "could not save cache data to path '" << cache_path << "'"); + + file_buf = k.store_to_memory(password, false, kdf_rounds); + + if (keys_file_format == Binary) + { + CHECK_AND_ASSERT_THROW_MES(epee::file_io_utils::save_string_to_file(keys_path, file_buf), + "could not save keys data to path '" << keys_path << "'"); + } + else // keys_file_format == Ascii + { + save_pem_ascii_file(keys_path, file_buf); + } +} +} // namespace wallet2_basic diff --git a/src/wallet/wallet2_basic/wallet2_storage.h b/src/wallet/wallet2_basic/wallet2_storage.h new file mode 100644 index 0000000000..e190ef3281 --- /dev/null +++ b/src/wallet/wallet2_basic/wallet2_storage.h @@ -0,0 +1,330 @@ +// Copyright (c) 2023, The Monero Project +// +// 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. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT HOLDER 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. + +/** + * @file utilities for loading and storing legacy wallet2 wallet data files, as well as handling device interaction + * + * Basic example of loading a wallet2 software wallet file: + * ```C++ + * const std::string wallet_filename = "testfile123"; + * const epee::wipeable_string wallet_password = "supersecret42"; + * + * wallet2_basic::cache example_cache; + * wallet2_basic::keys_data example_keys; + * + * wallet2_basic::load_keys_and_cache_from_file(wallet_filename, + wallet_password, + example_cache, + example_keys); + * + * std::cout << "my private view key is: " << + * epee::string_tools::pod_to_hex(example_keys.m_account.get_keys().m_view_secret_key) << std::endl; + * + * std::cout << "my transaction notes:" << std::endl; + * for (const auto& kv : example_cache.m_tx_notes) + * std::cout << " " << epee::string_tools::pod_to_hex(kv.first) << " - " << kv.second << std::endl; + * ``` +*/ + +#pragma once + +#include + +#include "wallet2_constants.h" +#include "wallet2_types.h" + +namespace wallet2_basic +{ +namespace detail +{ +template +using reference_mutate_enabled = std::conditional_t; +} +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +struct cache +{ + hashchain m_blockchain; + transfer_container m_transfers; + cryptonote::account_public_address m_account_public_address; + serializable_unordered_map m_key_images; + serializable_unordered_map m_unconfirmed_txs; + payment_container m_payments; + serializable_unordered_map m_tx_keys; + serializable_unordered_map m_confirmed_txs; + serializable_unordered_map m_tx_notes; + serializable_unordered_multimap m_unconfirmed_payments; + serializable_unordered_map m_pub_keys; + std::vector m_address_book; + std::unordered_set m_scanned_pool_txs[2]; + serializable_unordered_map m_subaddresses; + std::vector> m_subaddress_labels; + serializable_unordered_map> m_additional_tx_keys; + serializable_unordered_map m_attributes; + std::pair, std::vector> m_account_tags; + bool m_ring_history_saved; + uint64_t m_last_block_reward; + // Aux transaction data from device + serializable_unordered_map m_tx_device; + uint64_t m_device_last_key_image_sync; + serializable_unordered_map m_cold_key_images; + bool m_has_ever_refreshed_from_node; + + // There's special key derivations for specifically wallet cache files for some reason + static crypto::chacha_key pwd_to_cache_key(const char* pwd, size_t len, uint64_t kdf_rounds = 1); + static crypto::chacha_key account_to_old_cache_key(const cryptonote::account_base& account, uint64_t kdf_rounds = 1); + + static cache load_from_memory + ( + const std::string& cache_file_buf, + const epee::wipeable_string& password, + const cryptonote::account_base& wallet_account, + uint64_t kdf_rounds = 1 + ); + + std::string store_to_memory(const epee::wipeable_string& password, uint64_t kdf_rounds = 1) const; + std::string store_to_memory(const crypto::chacha_key& encryption_key) const; + + BEGIN_SERIALIZE_OBJECT() + MAGIC_FIELD("monero wallet cache") + VERSION_FIELD(1) + FIELD(m_blockchain) + FIELD(m_transfers) + FIELD(m_account_public_address) + FIELD(m_key_images) + FIELD(m_unconfirmed_txs) + FIELD(m_payments) + FIELD(m_tx_keys) + FIELD(m_confirmed_txs) + FIELD(m_tx_notes) + FIELD(m_unconfirmed_payments) + FIELD(m_pub_keys) + FIELD(m_address_book) + FIELD(m_scanned_pool_txs[0]) + FIELD(m_scanned_pool_txs[1]) + FIELD(m_subaddresses) + FIELD(m_subaddress_labels) + FIELD(m_additional_tx_keys) + FIELD(m_attributes) + FIELD(m_account_tags) + FIELD(m_ring_history_saved) + FIELD(m_last_block_reward) + FIELD(m_tx_device) + FIELD(m_device_last_key_image_sync) + FIELD(m_cold_key_images) + crypto::secret_key dummy_rpc_client_secret_key; // Compatibility for old RPC payment system + FIELD_N("m_rpc_client_secret_key", dummy_rpc_client_secret_key) + if (version < 1) + { + m_has_ever_refreshed_from_node = false; + return true; + } + FIELD(m_has_ever_refreshed_from_node) + END_SERIALIZE() +}; +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +struct keys_data +{ + cryptonote::account_base m_account; + + bool is_old_file_format = false; + bool m_watch_only = false; /*!< no spend key */ + bool m_multisig = false; /*!< if > 1 spend secret key will not match spend public key */ + std::string seed_language = ""; /*!< Language of the mnemonics (seed). */ + cryptonote::network_type m_nettype = cryptonote::UNDEFINED; + uint32_t m_multisig_threshold = 0; + std::vector m_multisig_signers; + //in case of general M/N multisig wallet we should perform N - M + 1 key exchange rounds and remember how many rounds are passed. + uint32_t m_multisig_rounds_passed = 0; + std::vector m_multisig_derivations; + bool m_always_confirm_transfers = true; + bool m_print_ring_members = false; + bool m_store_tx_info = true; /*!< request txkey to be returned in RPC, and store in the wallet cache file */ + uint32_t m_default_mixin = 0; + uint32_t m_default_priority = 0; + bool m_auto_refresh = true; + RefreshType m_refresh_type = RefreshDefault; + uint64_t m_refresh_from_block_height = 0; + uint64_t m_skip_to_height = 0; + // m_skip_to_height is useful when we don't want to modify the wallet's restore height. + // m_refresh_from_block_height is also a wallet's restore height which should remain constant unless explicitly modified by the user. + bool m_confirm_non_default_ring_size = true; + AskPasswordType m_ask_password = AskPasswordToDecrypt; + uint64_t m_max_reorg_depth = ORPHANED_BLOCKS_MAX_COUNT; + uint32_t m_min_output_count = 0; + uint64_t m_min_output_value = 0; + bool m_merge_destinations = false; + bool m_confirm_backlog = true; + uint32_t m_confirm_backlog_threshold = 0; + bool m_confirm_export_overwrite = true; + bool m_auto_low_priority = true; + bool m_segregate_pre_fork_outputs = true; + bool m_key_reuse_mitigation2 = true; + uint64_t m_segregation_height = 0; + bool m_ignore_fractional_outputs = true; + uint64_t m_ignore_outputs_above = MONEY_SUPPLY; + uint64_t m_ignore_outputs_below = 0; + bool m_track_uses = false; + bool m_show_wallet_name_when_locked = false; + uint32_t m_inactivity_lock_timeout = DEFAULT_INACTIVITY_LOCK_TIMEOUT; + BackgroundMiningSetupType m_setup_background_mining = BackgroundMiningMaybe; + size_t m_subaddress_lookahead_major = SUBADDRESS_LOOKAHEAD_MAJOR; + size_t m_subaddress_lookahead_minor = SUBADDRESS_LOOKAHEAD_MINOR; + bool m_original_keys_available = false; + cryptonote::account_public_address m_original_address; + crypto::secret_key m_original_view_secret_key; + ExportFormat m_export_format = ExportFormat::Binary; + bool m_load_deprecated_formats = false; + std::string m_device_name = ""; + std::string m_device_derivation_path = ""; + hw::device::device_type m_key_device_type = hw::device::device_type::SOFTWARE; + bool m_enable_multisig = false; + bool m_allow_mismatched_daemon_version = false; + + static crypto::chacha_key pwd_to_keys_data_key(const char* pwd, size_t len, uint64_t kdf_rounds = 1); + + static keys_data load_from_memory + ( + const std::string& keys_file_buf, + const epee::wipeable_string& password, + cryptonote::network_type nettype = cryptonote::UNDEFINED, + uint64_t kdf_rounds = 1 + ); + static keys_data load_from_memory + ( + const std::string& keys_file_buf, + const crypto::chacha_key& encryption_key, + cryptonote::network_type nettype = cryptonote::UNDEFINED + ); + + std::string store_to_memory + ( + const epee::wipeable_string& password, + bool downgrade_to_watch_only = false, + uint64_t kdf_rounds = 1 + ) const; + std::string store_to_memory + ( + const crypto::chacha_key& encryption_key, + bool downgrade_to_watch_only = false + ) const; + + bool requires_external_device() const + { return m_key_device_type != hw::device::device_type::SOFTWARE; } + + bool keys_were_encrypted_on_load() const { return m_keys_were_encrypted_on_load; } + + void setup_account_keys_and_devices + ( + const epee::wipeable_string& password, + hw::i_device_callback* device_cb = nullptr, + uint64_t kdf_rounds = 1 + ); + + void setup_account_keys_and_devices + ( + const crypto::chacha_key& encryption_key, + hw::i_device_callback* device_cb = nullptr + ); + + bool verify_account_keys + ( + bool view_only = false, + hw::device* alt_device = nullptr + ) const; + + hw::device& reconnect_device(hw::i_device_callback* device_cb = nullptr); + +private: + bool m_keys_were_encrypted_on_load = false; + + template + friend void adapt_keysdata_tofrom_json_object + ( + detail::reference_mutate_enabled kd, + detail::reference_mutate_enabled obj, + const crypto::chacha_key& keys_key, + bool downgrade_to_watch_only + ); +}; // struct keys_data +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +bool verify_account_keys +( + const cryptonote::account_keys& keys, + bool view_only = false, + hw::device* alt_device = nullptr +); + +void load_keys_and_cache_from_memory +( + const std::string& cache_file_buf, + const std::string& keys_file_buf, + const epee::wipeable_string& password, + cache& c, + keys_data& k, + cryptonote::network_type nettype = cryptonote::UNDEFINED, + bool allow_external_devices_setup = true, + hw::i_device_callback* device_cb = nullptr, + uint64_t kdf_rounds = 1 +); + +void load_keys_and_cache_from_file +( + const std::string& cache_path, + const epee::wipeable_string& password, + cache& c, + keys_data& k, + cryptonote::network_type nettype = cryptonote::UNDEFINED, + std::string keys_path = "", + bool allow_external_devices_setup = true, + hw::i_device_callback* device_cb = nullptr, + uint64_t kdf_rounds = 1 +); + +void store_keys_and_cache_to_memory +( + const cache& c, + const keys_data& k, + const epee::wipeable_string& password, + std::string& cache_buf, + std::string& keys_buf, + uint64_t kdf_rounds = 1 +); + +void store_keys_and_cache_to_file +( + const cache& c, + const keys_data& k, + const epee::wipeable_string& password, + const std::string& cache_path, + uint64_t kdf_rounds = 1, + ExportFormat keys_file_format = Binary +); +} // namespace wallet2_basic diff --git a/src/wallet/wallet2_basic/wallet2_types.h b/src/wallet/wallet2_basic/wallet2_types.h new file mode 100644 index 0000000000..e0205dbae4 --- /dev/null +++ b/src/wallet/wallet2_basic/wallet2_types.h @@ -0,0 +1,373 @@ +// Copyright (c) 2023, The Monero Project +// +// 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. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT HOLDER 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. + +#pragma once + +#include "cryptonote_basic/cryptonote_basic.h" +#include "wallet/wallet_errors.h" + +namespace wallet2_basic +{ +struct HashchainAccessor; + +/** + * @brief: caches a contiguous list of block hashes and a genesis block +*/ +class hashchain +{ +public: + hashchain(): m_genesis(crypto::null_hash), m_offset(0) {} + + /** + * @brief: get the "height" of the blockchain, not the number of hashes stored + */ + size_t size() const { return m_blockchain.size() + m_offset; } + /** + * @brief: get the height that the hash list begins at + */ + size_t offset() const { return m_offset; } + /** + * @brief: get the genesis bloch hash + */ + const crypto::hash &genesis() const { return m_genesis; } + /** + * @brief: add a block hash to the top of the chain + */ + void push_back(const crypto::hash &hash) { if (m_offset == 0 && m_blockchain.empty()) m_genesis = hash; m_blockchain.push_back(hash); } + /** + * @brief: query if there is a hash available for a given height + */ + bool is_in_bounds(size_t idx) const { return idx >= m_offset && idx < size(); } + /** + * @brief: get a const reference to the block hash at a given height + */ + const crypto::hash &operator[](size_t idx) const { return m_blockchain[idx - m_offset]; } + /** + * @brief: get a mutable reference to the block hash at a given height + */ + crypto::hash &operator[](size_t idx) { return m_blockchain[idx - m_offset]; } + /** + * @brief: crop stored hashes after a certain height, where the height of the top block == `height`-1 + */ + void crop(size_t height) { m_blockchain.resize(std::max(std::min(height, size()), m_offset) - m_offset); } + /** + * @brief: delete all stored hashes and set the offset to 0 + */ + void clear() { m_offset = 0; m_blockchain.clear(); } + /** + * @brief: query if the blockchain is "empty": there are no stored hashes and the offset is 0 + */ + bool empty() const { return m_blockchain.empty() && m_offset == 0; } + /** + * @brief: crop stored hashes before a certain height and shift the offset accordingly, but always leave at least 1 hash + */ + void trim(size_t height) { while (height > m_offset && m_blockchain.size() > 1) { m_blockchain.pop_front(); ++m_offset; } m_blockchain.shrink_to_fit(); } + /** + * @brief: push a block hash onto the chain and move all block hashes back by one block + */ + void refill(const crypto::hash &hash) { m_blockchain.push_back(hash); --m_offset; } + + BEGIN_SERIALIZE_OBJECT() + VERSION_FIELD(0) + VARINT_FIELD(m_offset) + FIELD(m_genesis) + FIELD(m_blockchain) + END_SERIALIZE() + +private: + size_t m_offset; + crypto::hash m_genesis; + std::deque m_blockchain; + + friend struct HashchainAccessor; +}; + +struct multisig_info +{ + struct LR + { + rct::key m_L; + rct::key m_R; + + BEGIN_SERIALIZE_OBJECT() + FIELD(m_L) + FIELD(m_R) + END_SERIALIZE() + }; + + crypto::public_key m_signer; + std::vector m_LR; + std::vector m_partial_key_images; // one per key the participant has + + BEGIN_SERIALIZE_OBJECT() + FIELD(m_signer) + FIELD(m_LR) + FIELD(m_partial_key_images) + END_SERIALIZE() +}; + +struct transfer_details +{ + uint64_t m_block_height; + cryptonote::transaction_prefix m_tx; + crypto::hash m_txid; + uint64_t m_internal_output_index; + uint64_t m_global_output_index; + bool m_spent; + bool m_frozen; + uint64_t m_spent_height; + crypto::key_image m_key_image; //TODO: key_image stored twice :( + rct::key m_mask; + uint64_t m_amount; + bool m_rct; + bool m_key_image_known; + bool m_key_image_request; // view wallets: we want to request it; cold wallets: it was requested + uint64_t m_pk_index; + cryptonote::subaddress_index m_subaddr_index; + bool m_key_image_partial; + std::vector m_multisig_k; + std::vector m_multisig_info; // one per other participant + std::vector> m_uses; + + bool is_rct() const { return m_rct; } + uint64_t amount() const { return m_amount; } + const crypto::public_key get_public_key() const { + crypto::public_key output_public_key; + THROW_WALLET_EXCEPTION_IF(m_tx.vout.size() <= m_internal_output_index, + tools::error::wallet_internal_error, "Too few outputs, outputs may be corrupted"); + THROW_WALLET_EXCEPTION_IF(!get_output_public_key(m_tx.vout[m_internal_output_index], output_public_key), + tools::error::wallet_internal_error, "Unable to get output public key from output"); + return output_public_key; + }; + + BEGIN_SERIALIZE_OBJECT() + FIELD(m_block_height) + FIELD(m_tx) + FIELD(m_txid) + FIELD(m_internal_output_index) + FIELD(m_global_output_index) + FIELD(m_spent) + FIELD(m_frozen) + FIELD(m_spent_height) + FIELD(m_key_image) + FIELD(m_mask) + FIELD(m_amount) + FIELD(m_rct) + FIELD(m_key_image_known) + FIELD(m_key_image_request) + FIELD(m_pk_index) + FIELD(m_subaddr_index) + FIELD(m_key_image_partial) + FIELD(m_multisig_k) + FIELD(m_multisig_info) + FIELD(m_uses) + END_SERIALIZE() +}; + +typedef std::vector transfer_container; + +struct unconfirmed_transfer_details +{ + cryptonote::transaction_prefix m_tx; + uint64_t m_amount_in; + uint64_t m_amount_out; + uint64_t m_change; + time_t m_sent_time; + std::vector m_dests; + crypto::hash m_payment_id; + enum { pending, pending_in_pool, failed } m_state; + uint64_t m_timestamp; + uint32_t m_subaddr_account; // subaddress account of your wallet to be used in this transfer + std::set m_subaddr_indices; // set of address indices used as inputs in this transfer + std::vector>> m_rings; // relative + + BEGIN_SERIALIZE_OBJECT() + VERSION_FIELD(1) + FIELD(m_tx) + VARINT_FIELD(m_amount_in) + VARINT_FIELD(m_amount_out) + VARINT_FIELD(m_change) + VARINT_FIELD(m_sent_time) + FIELD(m_dests) + FIELD(m_payment_id) + if (version >= 1) + VARINT_FIELD(m_state) + VARINT_FIELD(m_timestamp) + VARINT_FIELD(m_subaddr_account) + FIELD(m_subaddr_indices) + FIELD(m_rings) + END_SERIALIZE() +}; + +struct confirmed_transfer_details +{ + cryptonote::transaction_prefix m_tx; + uint64_t m_amount_in; + uint64_t m_amount_out; + uint64_t m_change; + uint64_t m_block_height; + std::vector m_dests; + crypto::hash m_payment_id; + uint64_t m_timestamp; + uint64_t m_unlock_time; + uint32_t m_subaddr_account; // subaddress account of your wallet to be used in this transfer + std::set m_subaddr_indices; // set of address indices used as inputs in this transfer + std::vector>> m_rings; // relative + + confirmed_transfer_details() + : m_amount_in(0) + , m_amount_out(0) + , m_change((uint64_t)-1) + , m_block_height(0) + , m_payment_id(crypto::null_hash) + , m_timestamp(0) + , m_unlock_time(0) + , m_subaddr_account((uint32_t)-1) + {} + + confirmed_transfer_details(const unconfirmed_transfer_details &utd, uint64_t height) + : m_tx(utd.m_tx) + , m_amount_in(utd.m_amount_in) + , m_amount_out(utd.m_amount_out) + , m_change(utd.m_change) + , m_block_height(height) + , m_dests(utd.m_dests) + , m_payment_id(utd.m_payment_id) + , m_timestamp(utd.m_timestamp) + , m_unlock_time(utd.m_tx.unlock_time) + , m_subaddr_account(utd.m_subaddr_account) + , m_subaddr_indices(utd.m_subaddr_indices) + , m_rings(utd.m_rings) + {} + + BEGIN_SERIALIZE_OBJECT() + VERSION_FIELD(1) + if (version >= 1) + FIELD(m_tx) + VARINT_FIELD(m_amount_in) + VARINT_FIELD(m_amount_out) + VARINT_FIELD(m_change) + VARINT_FIELD(m_block_height) + FIELD(m_dests) + FIELD(m_payment_id) + VARINT_FIELD(m_timestamp) + VARINT_FIELD(m_unlock_time) + VARINT_FIELD(m_subaddr_account) + FIELD(m_subaddr_indices) + FIELD(m_rings) + END_SERIALIZE() +}; + +typedef std::vector amounts_container; +struct payment_details +{ + crypto::hash m_tx_hash; + uint64_t m_amount; + amounts_container m_amounts; + uint64_t m_fee; + uint64_t m_block_height; + uint64_t m_unlock_time; + uint64_t m_timestamp; + bool m_coinbase; + cryptonote::subaddress_index m_subaddr_index; + + BEGIN_SERIALIZE_OBJECT() + VERSION_FIELD(0) + FIELD(m_tx_hash) + VARINT_FIELD(m_amount) + FIELD(m_amounts) + VARINT_FIELD(m_fee) + VARINT_FIELD(m_block_height) + VARINT_FIELD(m_unlock_time) + VARINT_FIELD(m_timestamp) + FIELD(m_coinbase) + FIELD(m_subaddr_index) + END_SERIALIZE() +}; + +typedef serializable_unordered_multimap payment_container; + +struct pool_payment_details +{ + payment_details m_pd; + bool m_double_spend_seen; + + BEGIN_SERIALIZE_OBJECT() + VERSION_FIELD(0) + FIELD(m_pd) + FIELD(m_double_spend_seen) + END_SERIALIZE() +}; + +// GUI Address book +struct address_book_row +{ + cryptonote::account_public_address m_address; + crypto::hash8 m_payment_id; + std::string m_description; + bool m_is_subaddress; + bool m_has_payment_id; + + BEGIN_SERIALIZE_OBJECT() + VERSION_FIELD(0) + FIELD(m_address) + FIELD(m_payment_id) + FIELD(m_description) + FIELD(m_is_subaddress) + FIELD(m_has_payment_id) + END_SERIALIZE() +}; + +enum RefreshType +{ + RefreshFull, + RefreshOptimizeCoinbase, + RefreshNoCoinbase, + RefreshDefault = RefreshOptimizeCoinbase, +}; + +enum AskPasswordType +{ + AskPasswordNever = 0, + AskPasswordOnAction = 1, + AskPasswordToDecrypt = 2, +}; + +enum BackgroundMiningSetupType +{ + BackgroundMiningMaybe = 0, + BackgroundMiningYes = 1, + BackgroundMiningNo = 2, +}; + +enum ExportFormat +{ + Binary = 0, + Ascii, +}; +} // namespace wallet2_basic diff --git a/src/wallet/wallet_errors.h b/src/wallet/wallet_errors.h index a2b2484343..77a28b68c6 100644 --- a/src/wallet/wallet_errors.h +++ b/src/wallet/wallet_errors.h @@ -920,7 +920,7 @@ namespace tools #if !defined(_MSC_VER) template - void throw_wallet_ex(std::string&& loc, const TArgs&... args) + [[noreturn]] void throw_wallet_ex(std::string&& loc, const TArgs&... args) { TException e(std::move(loc), args...); LOG_PRINT_L0(e.to_string()); @@ -933,7 +933,7 @@ namespace tools #include template - void throw_wallet_ex(std::string&& loc) + [[noreturn]] void throw_wallet_ex(std::string&& loc) { TException e(std::move(loc)); LOG_PRINT_L0(e.to_string()); @@ -942,7 +942,7 @@ namespace tools #define GEN_throw_wallet_ex(z, n, data) \ template \ - void throw_wallet_ex(std::string&& loc, BOOST_PP_ENUM_BINARY_PARAMS(n, const TArg, &arg)) \ + [[noreturn]] void throw_wallet_ex(std::string&& loc, BOOST_PP_ENUM_BINARY_PARAMS(n, const TArg, &arg)) \ { \ TException e(std::move(loc), BOOST_PP_ENUM_PARAMS(n, arg)); \ LOG_PRINT_L0(e.to_string()); \ diff --git a/tests/functional_tests/functional_tests_rpc.py b/tests/functional_tests/functional_tests_rpc.py index 9975cdfa23..1be5fea3c2 100755 --- a/tests/functional_tests/functional_tests_rpc.py +++ b/tests/functional_tests/functional_tests_rpc.py @@ -86,6 +86,9 @@ print('Starting servers...') try: + # Setup directories + subprocess.Popen(['rm', '-rf', WALLET_DIRECTORY]) + PYTHONPATH = os.environ['PYTHONPATH'] if 'PYTHONPATH' in os.environ else '' if len(PYTHONPATH) > 0: PYTHONPATH += ':' diff --git a/tests/functional_tests/wallet.py b/tests/functional_tests/wallet.py index 3bb4459d6b..c0f4676f1b 100755 --- a/tests/functional_tests/wallet.py +++ b/tests/functional_tests/wallet.py @@ -301,6 +301,24 @@ def open_close(self): except: ok = True assert ok + #################################################################################################################### + # This create->close->open pattern reveals if you have code which performs loading correctly but storing incorrectly + + wallet.create_wallet('createcloseopen', password='NIST SP 800-90A -- Dual_EC_DRBG') + cco_addr = wallet.get_address().address + assert cco_addr != '44Kbx4sJ7JDRDV5aAhLJzQCjDz2ViLRduE3ijDZu3osWKBjMGkV1XPk4pfDUMqt1Aiezvephdqm6YD19GKFD9ZcXVUTp6BW' # what are the chances haha + + wallet.close_wallet() + ok = False + try: wallet.get_address() + except: ok = True + assert ok + + wallet.open_wallet('createcloseopen', password='NIST SP 800-90A -- Dual_EC_DRBG') + res = wallet.get_address() + assert res.address == cco_addr + #################################################################################################################### + wallet.restore_deterministic_wallet(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.get_address() assert res.address == '42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm' diff --git a/tests/unit_tests/CMakeLists.txt b/tests/unit_tests/CMakeLists.txt index ddcf64f7f5..32b8acfa9d 100644 --- a/tests/unit_tests/CMakeLists.txt +++ b/tests/unit_tests/CMakeLists.txt @@ -113,6 +113,7 @@ set(unit_tests_sources output_selection.cpp vercmp.cpp ringdb.cpp + wallet_storage.cpp wipeable_string.cpp is_hdd.cpp aligned.cpp @@ -134,6 +135,7 @@ target_link_libraries(unit_tests cryptonote_core daemon_messages daemon_rpc_server + device_trezor blockchain_db lmdb_lib mx25519_static @@ -146,6 +148,7 @@ target_link_libraries(unit_tests seraphis_main seraphis_mocks wallet + wallet2_basic p2p version ${Boost_CHRONO_LIBRARY} diff --git a/tests/unit_tests/unit_tests_utils.h b/tests/unit_tests/unit_tests_utils.h index e3c6c2521f..2a452d21f8 100644 --- a/tests/unit_tests/unit_tests_utils.h +++ b/tests/unit_tests/unit_tests_utils.h @@ -72,3 +72,12 @@ namespace unit_test ASSERT_TRUE(found != map.end()); \ ASSERT_EQ(val, found->second); \ } while (false) + +#define EXPECT_EQ_MAP(val, map, key) \ + do { \ + auto found = map.find(key); \ + EXPECT_TRUE(found != map.end()); \ + if (found == map.end()) break; \ + EXPECT_EQ(val, found->second); \ + } while (false) \ + diff --git a/tests/unit_tests/wallet_storage.cpp b/tests/unit_tests/wallet_storage.cpp new file mode 100644 index 0000000000..27ceeaaa79 --- /dev/null +++ b/tests/unit_tests/wallet_storage.cpp @@ -0,0 +1,577 @@ +// Copyright (c) 2023, The Monero Project +// +// 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. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT HOLDER 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 "gtest/gtest.h" + +#include "unit_tests_utils.h" +#include "wallet/wallet2_basic/wallet2_storage.h" +#include "wallet/wallet2.h" + +using namespace boost::filesystem; +using namespace epee::file_io_utils; + +static void check_wallet_9svHk1_key_contents(const tools::wallet2& w2, const tools::wallet2::ExportFormat export_format = tools::wallet2::Binary) +{ + // if wallet fails this first test, make sure that the wallet keys are decrypted + EXPECT_EQ("a16cc88f85ee9403bc642def92334ed203032ce91b060d353e6a532f47ff6200", epee::string_tools::pod_to_hex(w2.get_account().get_keys().m_spend_secret_key)); + EXPECT_EQ("339673bb1187e2f73ba7841ab6841c5553f96e9f13f8fe6612e69318db4e9d0a", epee::string_tools::pod_to_hex(w2.get_account().get_keys().m_view_secret_key)); + EXPECT_EQ(1483262038, w2.get_account().get_createtime()); + EXPECT_EQ(false, w2.is_deprecated()); // getter for member field is_old_file_format + EXPECT_EQ(false, w2.watch_only()); + + EXPECT_EQ(false, w2.multisig()); + EXPECT_EQ(false, w2.is_multisig_enabled()); + // @TODO: missing fields m_multisig_signers, m_multisig_rounds_passed, m_multisig_threshold, m_multisig_derivations + + EXPECT_EQ("English", w2.get_seed_language()); + EXPECT_EQ(cryptonote::TESTNET, w2.nettype()); + EXPECT_EQ(true, w2.always_confirm_transfers()); + EXPECT_EQ(false, w2.print_ring_members()); + EXPECT_EQ(true, w2.store_tx_info()); + EXPECT_EQ(0, w2.default_mixin()); + EXPECT_EQ(0, w2.get_default_priority()); + EXPECT_EQ(true, w2.auto_refresh()); + EXPECT_EQ(wallet2_basic::RefreshDefault, w2.get_refresh_type()); + EXPECT_EQ(818413, w2.get_refresh_from_block_height()); + // @TODO: missing m_skip_to_height + EXPECT_EQ(true, w2.confirm_non_default_ring_size()); + EXPECT_EQ(wallet2_basic::AskPasswordToDecrypt, w2.ask_password()); + EXPECT_EQ(ORPHANED_BLOCKS_MAX_COUNT, w2.max_reorg_depth()); + EXPECT_EQ(0, w2.get_min_output_count()); + EXPECT_EQ(0, w2.get_min_output_value()); + EXPECT_EQ(false, w2.merge_destinations()); + EXPECT_EQ(true, w2.confirm_backlog()); + EXPECT_EQ(0, w2.get_confirm_backlog_threshold()); + EXPECT_EQ(true, w2.confirm_export_overwrite()); + EXPECT_EQ(true, w2.auto_low_priority()); + EXPECT_EQ(true, w2.segregate_pre_fork_outputs()); + EXPECT_EQ(true, w2.key_reuse_mitigation2()); + EXPECT_EQ(0, w2.segregation_height()); + EXPECT_EQ(true, w2.ignore_fractional_outputs()); + EXPECT_EQ(MONEY_SUPPLY, w2.ignore_outputs_above()); + EXPECT_EQ(0, w2.ignore_outputs_below()); + EXPECT_EQ(false, w2.track_uses()); + EXPECT_EQ(false, w2.show_wallet_name_when_locked()); + EXPECT_EQ(wallet2_basic::DEFAULT_INACTIVITY_LOCK_TIMEOUT, w2.inactivity_lock_timeout()); + EXPECT_EQ(wallet2_basic::BackgroundMiningMaybe, w2.setup_background_mining()); + const std::pair exp_lookahead = {wallet2_basic::SUBADDRESS_LOOKAHEAD_MAJOR, wallet2_basic::SUBADDRESS_LOOKAHEAD_MINOR}; + EXPECT_EQ(exp_lookahead, w2.get_subaddress_lookahead()); + // @TODO: missing m_original_keys_available, m_original_address + EXPECT_EQ(export_format, w2.export_format()); + EXPECT_EQ(false, w2.load_deprecated_formats()); + EXPECT_EQ("default", w2.device_name()); + EXPECT_EQ("", w2.device_derivation_path()); + EXPECT_EQ(hw::device::device_type::SOFTWARE, w2.get_device_type()); + EXPECT_EQ(false, w2.is_mismatched_daemon_version_allowed()); +} + +static void check_wallet_9svHk1_key_contents(const wallet2_basic::keys_data& w2b, const wallet2_basic::ExportFormat export_format = wallet2_basic::Binary) +{ + // if wallet fails this first test, make sure that the wallet keys are decrypted + EXPECT_EQ("a16cc88f85ee9403bc642def92334ed203032ce91b060d353e6a532f47ff6200", epee::string_tools::pod_to_hex(w2b.m_account.get_keys().m_spend_secret_key)); + EXPECT_EQ("339673bb1187e2f73ba7841ab6841c5553f96e9f13f8fe6612e69318db4e9d0a", epee::string_tools::pod_to_hex(w2b.m_account.get_keys().m_view_secret_key)); + EXPECT_EQ(1483262038, w2b.m_account.get_createtime()); + EXPECT_EQ(false, w2b.is_old_file_format); // getter for member field is_old_file_format + EXPECT_EQ(false, w2b.m_watch_only); + + EXPECT_EQ(false, w2b.m_multisig); + EXPECT_EQ(false, w2b.m_enable_multisig); + // @TODO: missing fields m_multisig_signers, m_multisig_rounds_passed, m_multisig_threshold, m_multisig_derivations + + EXPECT_EQ("English", w2b.seed_language); + EXPECT_EQ(cryptonote::TESTNET, w2b.m_nettype); + EXPECT_EQ(true, w2b.m_always_confirm_transfers); + EXPECT_EQ(false, w2b.m_print_ring_members); + EXPECT_EQ(true, w2b.m_store_tx_info); + EXPECT_EQ(0, w2b.m_default_mixin); + EXPECT_EQ(0, w2b.m_default_priority); + EXPECT_EQ(true, w2b.m_auto_refresh); + EXPECT_EQ(wallet2_basic::RefreshDefault, w2b.m_refresh_type); + EXPECT_EQ(818413, w2b.m_refresh_from_block_height); + // @TODO: missing m_skip_to_height + EXPECT_EQ(true, w2b.m_confirm_non_default_ring_size); + EXPECT_EQ(wallet2_basic::AskPasswordToDecrypt, w2b.m_ask_password); + EXPECT_EQ(ORPHANED_BLOCKS_MAX_COUNT, w2b.m_max_reorg_depth); + EXPECT_EQ(0, w2b.m_min_output_count); + EXPECT_EQ(0, w2b.m_min_output_value); + EXPECT_EQ(false, w2b.m_merge_destinations); + EXPECT_EQ(true, w2b.m_confirm_backlog); + EXPECT_EQ(0, w2b.m_confirm_backlog_threshold); + EXPECT_EQ(true, w2b.m_confirm_export_overwrite); + EXPECT_EQ(true, w2b.m_auto_low_priority); + EXPECT_EQ(true, w2b.m_segregate_pre_fork_outputs); + EXPECT_EQ(true, w2b.m_key_reuse_mitigation2); + EXPECT_EQ(0, w2b.m_segregation_height); + EXPECT_EQ(true, w2b.m_ignore_fractional_outputs); + EXPECT_EQ(MONEY_SUPPLY, w2b.m_ignore_outputs_above); + EXPECT_EQ(0, w2b.m_ignore_outputs_below); + EXPECT_EQ(false, w2b.m_track_uses); + EXPECT_EQ(false, w2b.m_show_wallet_name_when_locked); + EXPECT_EQ(wallet2_basic::DEFAULT_INACTIVITY_LOCK_TIMEOUT, w2b.m_inactivity_lock_timeout); + EXPECT_EQ(wallet2_basic::BackgroundMiningMaybe, w2b.m_setup_background_mining); + EXPECT_EQ(wallet2_basic::SUBADDRESS_LOOKAHEAD_MAJOR, w2b.m_subaddress_lookahead_major); + EXPECT_EQ(wallet2_basic::SUBADDRESS_LOOKAHEAD_MINOR, w2b.m_subaddress_lookahead_minor); + // @TODO: missing m_original_keys_available, m_original_address + EXPECT_EQ(export_format, w2b.m_export_format); + EXPECT_EQ(false, w2b.m_load_deprecated_formats); + EXPECT_EQ("default", w2b.m_device_name); + EXPECT_EQ("", w2b.m_device_derivation_path); + EXPECT_EQ(hw::device::device_type::SOFTWARE, w2b.m_key_device_type); + EXPECT_EQ(false, w2b.m_allow_mismatched_daemon_version); +} + +namespace tools +{ +/*static*/ void check_wallet_9svHk1_cache_contents(const tools::wallet2& w2) +{ + /* + fields of tools::wallet2 to be checked: + std::vector m_blockchain + std::vector m_transfers // TODO + cryptonote::account_public_address m_account_public_address + std::unordered_map m_key_images + std::unordered_map m_unconfirmed_txs + std::unordered_multimap m_payments + std::unordered_map m_tx_keys + std::unordered_map m_confirmed_txs + std::unordered_map m_tx_notes + std::unordered_map m_unconfirmed_payments + std::unordered_map m_pub_keys + std::vector m_address_book + */ + // blockchain + ASSERT_TRUE(w2.m_blockchain.size() == 1); + EXPECT_TRUE(epee::string_tools::pod_to_hex(w2.m_blockchain[0]) == "48ca7cd3c8de5b6a4d53d2861fbdaedca141553559f9be9520068053cda8430b"); + // transfers (TODO) + EXPECT_TRUE(w2.m_transfers.size() == 3); + // account public address + EXPECT_TRUE(epee::string_tools::pod_to_hex(w2.m_account_public_address.m_view_public_key) == "e47d4b6df6ab7339539148c2a03ad3e2f3434e5ab2046848e1f21369a3937cad"); + EXPECT_TRUE(epee::string_tools::pod_to_hex(w2.m_account_public_address.m_spend_public_key) == "13daa2af00ad26a372d317195de0bdd716f7a05d33bc4d7aff1664b6ee93c060"); + // key images + ASSERT_TRUE(w2.m_key_images.size() == 3); + { + crypto::key_image ki[3]; + epee::string_tools::hex_to_pod("c5680d3735b90871ca5e3d90cd82d6483eed1151b9ab75c2c8c3a7d89e00a5a8", ki[0]); + epee::string_tools::hex_to_pod("d54cbd435a8d636ad9b01b8d4f3eb13bd0cf1ce98eddf53ab1617f9b763e66c0", ki[1]); + epee::string_tools::hex_to_pod("6c3cd6af97c4070a7aef9b1344e7463e29c7cd245076fdb65da447a34da3ca76", ki[2]); + EXPECT_EQ_MAP(0, w2.m_key_images, ki[0]); + EXPECT_EQ_MAP(1, w2.m_key_images, ki[1]); + EXPECT_EQ_MAP(2, w2.m_key_images, ki[2]); + } + // unconfirmed txs + EXPECT_TRUE(w2.m_unconfirmed_txs.size() == 0); + // payments + ASSERT_TRUE(w2.m_payments.size() == 2); + { + auto pd0 = w2.m_payments.begin(); + auto pd1 = pd0; + ++pd1; + EXPECT_TRUE(epee::string_tools::pod_to_hex(pd0->first) == "0000000000000000000000000000000000000000000000000000000000000000"); + EXPECT_TRUE(epee::string_tools::pod_to_hex(pd1->first) == "0000000000000000000000000000000000000000000000000000000000000000"); + if (epee::string_tools::pod_to_hex(pd0->second.m_tx_hash) == "ec34c9bb12b99af33d49691384eee5bed9171498ff04e59516505f35d1fc5efc") + swap(pd0, pd1); + EXPECT_TRUE(epee::string_tools::pod_to_hex(pd0->second.m_tx_hash) == "15024343b38e77a1a9860dfed29921fa17e833fec837191a6b04fa7cb9605b8e"); + EXPECT_TRUE(epee::string_tools::pod_to_hex(pd1->second.m_tx_hash) == "ec34c9bb12b99af33d49691384eee5bed9171498ff04e59516505f35d1fc5efc"); + EXPECT_TRUE(pd0->second.m_amount == 13400845012231); + EXPECT_TRUE(pd1->second.m_amount == 1200000000000); + EXPECT_TRUE(pd0->second.m_block_height == 818424); + EXPECT_TRUE(pd1->second.m_block_height == 818522); + EXPECT_TRUE(pd0->second.m_unlock_time == 818484); + EXPECT_TRUE(pd1->second.m_unlock_time == 0); + EXPECT_TRUE(pd0->second.m_timestamp == 1483263366); + EXPECT_TRUE(pd1->second.m_timestamp == 1483272963); + } + // tx keys + ASSERT_TRUE(w2.m_tx_keys.size() == 2); + { + const std::vector> txid_txkey = + { + {"b9aac8c020ab33859e0c0b6331f46a8780d349e7ac17b067116e2d87bf48daad", "bf3614c6de1d06c09add5d92a5265d8c76af706f7bc6ac830d6b0d109aa87701"}, + {"6e7013684d35820f66c6679197ded9329bfe0e495effa47e7b25258799858dba", "e556884246df5a787def6732c6ea38f1e092fa13e5ea98f732b99c07a6332003"}, + }; + for (size_t i = 0; i < txid_txkey.size(); ++i) + { + crypto::hash txid; + crypto::secret_key txkey; + epee::string_tools::hex_to_pod(txid_txkey[i].first, txid); + epee::string_tools::hex_to_pod(txid_txkey[i].second, txkey); + EXPECT_EQ_MAP(txkey, w2.m_tx_keys, txid); + } + } + // confirmed txs + EXPECT_TRUE(w2.m_confirmed_txs.size() == 1); + // tx notes + ASSERT_TRUE(w2.m_tx_notes.size() == 2); + { + crypto::hash h[2]; + epee::string_tools::hex_to_pod("15024343b38e77a1a9860dfed29921fa17e833fec837191a6b04fa7cb9605b8e", h[0]); + epee::string_tools::hex_to_pod("6e7013684d35820f66c6679197ded9329bfe0e495effa47e7b25258799858dba", h[1]); + EXPECT_EQ_MAP("sample note", w2.m_tx_notes, h[0]); + EXPECT_EQ_MAP("sample note 2", w2.m_tx_notes, h[1]); + } + // unconfirmed payments + EXPECT_TRUE(w2.m_unconfirmed_payments.size() == 0); + // pub keys + ASSERT_TRUE(w2.m_pub_keys.size() == 3); + { + crypto::public_key pubkey[3]; + epee::string_tools::hex_to_pod("33f75f264574cb3a9ea5b24220a5312e183d36dc321c9091dfbb720922a4f7b0", pubkey[0]); + epee::string_tools::hex_to_pod("5066ff2ce9861b1d131cf16eeaa01264933a49f28242b97b153e922ec7b4b3cb", pubkey[1]); + epee::string_tools::hex_to_pod("0d8467e16e73d16510452b78823e082e05ee3a63788d40de577cf31eb555f0c8", pubkey[2]); + EXPECT_EQ_MAP(0, w2.m_pub_keys, pubkey[0]); + EXPECT_EQ_MAP(1, w2.m_pub_keys, pubkey[1]); + EXPECT_EQ_MAP(2, w2.m_pub_keys, pubkey[2]); + } + // address book + ASSERT_TRUE(w2.m_address_book.size() == 1); + { + auto address_book_row = w2.m_address_book.begin(); + EXPECT_TRUE(epee::string_tools::pod_to_hex(address_book_row->m_address.m_spend_public_key) == "9bc53a6ff7b0831c9470f71b6b972dbe5ad1e8606f72682868b1dda64e119fb3"); + EXPECT_TRUE(epee::string_tools::pod_to_hex(address_book_row->m_address.m_view_public_key) == "49fece1ef97dc0c0f7a5e2106e75e96edd910f7e86b56e1e308cd0cf734df191"); + EXPECT_TRUE(address_book_row->m_description == "testnet wallet 9y52S6"); + } +} +} // namespace tools + +static void check_wallet_9svHk1_cache_contents(const wallet2_basic::cache& c) +{ + /* + This test suite is adapated from unit test Serialization.portability_wallet + Cache fields to be checked: + std::vector m_blockchain + std::vector m_transfers + cryptonote::account_public_address m_account_public_address + std::unordered_map m_key_images + std::unordered_map m_unconfirmed_txs + std::unordered_multimap m_payments + std::unordered_map m_tx_keys + std::unordered_map m_confirmed_txs + std::unordered_map m_tx_notes + std::unordered_map m_unconfirmed_payments + std::unordered_map m_pub_keys + std::vector m_address_book + */ + + // blockchain + EXPECT_TRUE(c.m_blockchain.size() == 1); + EXPECT_TRUE(epee::string_tools::pod_to_hex(c.m_blockchain[0]) == "48ca7cd3c8de5b6a4d53d2861fbdaedca141553559f9be9520068053cda8430b"); + // transfers (TODO) + EXPECT_TRUE(c.m_transfers.size() == 3); + // account public address + EXPECT_TRUE(epee::string_tools::pod_to_hex(c.m_account_public_address.m_view_public_key) == "e47d4b6df6ab7339539148c2a03ad3e2f3434e5ab2046848e1f21369a3937cad"); + EXPECT_TRUE(epee::string_tools::pod_to_hex(c.m_account_public_address.m_spend_public_key) == "13daa2af00ad26a372d317195de0bdd716f7a05d33bc4d7aff1664b6ee93c060"); + // key images + ASSERT_TRUE(c.m_key_images.size() == 3); + { + crypto::key_image ki[3]; + epee::string_tools::hex_to_pod("c5680d3735b90871ca5e3d90cd82d6483eed1151b9ab75c2c8c3a7d89e00a5a8", ki[0]); + epee::string_tools::hex_to_pod("d54cbd435a8d636ad9b01b8d4f3eb13bd0cf1ce98eddf53ab1617f9b763e66c0", ki[1]); + epee::string_tools::hex_to_pod("6c3cd6af97c4070a7aef9b1344e7463e29c7cd245076fdb65da447a34da3ca76", ki[2]); + EXPECT_EQ_MAP(0, c.m_key_images, ki[0]); + EXPECT_EQ_MAP(1, c.m_key_images, ki[1]); + EXPECT_EQ_MAP(2, c.m_key_images, ki[2]); + } + // unconfirmed txs + EXPECT_TRUE(c.m_unconfirmed_txs.size() == 0); + // payments + ASSERT_TRUE(c.m_payments.size() == 2); + { + auto pd0 = c.m_payments.begin(); + auto pd1 = pd0; + ++pd1; + EXPECT_TRUE(epee::string_tools::pod_to_hex(pd0->first) == "0000000000000000000000000000000000000000000000000000000000000000"); + EXPECT_TRUE(epee::string_tools::pod_to_hex(pd1->first) == "0000000000000000000000000000000000000000000000000000000000000000"); + if (epee::string_tools::pod_to_hex(pd0->second.m_tx_hash) == "ec34c9bb12b99af33d49691384eee5bed9171498ff04e59516505f35d1fc5efc") + swap(pd0, pd1); + EXPECT_TRUE(epee::string_tools::pod_to_hex(pd0->second.m_tx_hash) == "15024343b38e77a1a9860dfed29921fa17e833fec837191a6b04fa7cb9605b8e"); + EXPECT_TRUE(epee::string_tools::pod_to_hex(pd1->second.m_tx_hash) == "ec34c9bb12b99af33d49691384eee5bed9171498ff04e59516505f35d1fc5efc"); + EXPECT_TRUE(pd0->second.m_amount == 13400845012231); + EXPECT_TRUE(pd1->second.m_amount == 1200000000000); + EXPECT_TRUE(pd0->second.m_block_height == 818424); + EXPECT_TRUE(pd1->second.m_block_height == 818522); + EXPECT_TRUE(pd0->second.m_unlock_time == 818484); + EXPECT_TRUE(pd1->second.m_unlock_time == 0); + EXPECT_TRUE(pd0->second.m_timestamp == 1483263366); + EXPECT_TRUE(pd1->second.m_timestamp == 1483272963); + } + // tx keys + ASSERT_TRUE(c.m_tx_keys.size() == 2); + { + const std::vector> txid_txkey = + { + {"b9aac8c020ab33859e0c0b6331f46a8780d349e7ac17b067116e2d87bf48daad", "bf3614c6de1d06c09add5d92a5265d8c76af706f7bc6ac830d6b0d109aa87701"}, + {"6e7013684d35820f66c6679197ded9329bfe0e495effa47e7b25258799858dba", "e556884246df5a787def6732c6ea38f1e092fa13e5ea98f732b99c07a6332003"}, + }; + for (size_t i = 0; i < txid_txkey.size(); ++i) + { + crypto::hash txid; + crypto::secret_key txkey; + epee::string_tools::hex_to_pod(txid_txkey[i].first, txid); + epee::string_tools::hex_to_pod(txid_txkey[i].second, txkey); + EXPECT_EQ_MAP(txkey, c.m_tx_keys, txid); + } + } + // confirmed txs + EXPECT_TRUE(c.m_confirmed_txs.size() == 1); + // tx notes + ASSERT_TRUE(c.m_tx_notes.size() == 2); + { + crypto::hash h[2]; + epee::string_tools::hex_to_pod("15024343b38e77a1a9860dfed29921fa17e833fec837191a6b04fa7cb9605b8e", h[0]); + epee::string_tools::hex_to_pod("6e7013684d35820f66c6679197ded9329bfe0e495effa47e7b25258799858dba", h[1]); + EXPECT_EQ_MAP("sample note", c.m_tx_notes, h[0]); + EXPECT_EQ_MAP("sample note 2", c.m_tx_notes, h[1]); + } + // unconfirmed payments + EXPECT_TRUE(c.m_unconfirmed_payments.size() == 0); + // pub keys + ASSERT_TRUE(c.m_pub_keys.size() == 3); + { + crypto::public_key pubkey[3]; + epee::string_tools::hex_to_pod("33f75f264574cb3a9ea5b24220a5312e183d36dc321c9091dfbb720922a4f7b0", pubkey[0]); + epee::string_tools::hex_to_pod("5066ff2ce9861b1d131cf16eeaa01264933a49f28242b97b153e922ec7b4b3cb", pubkey[1]); + epee::string_tools::hex_to_pod("0d8467e16e73d16510452b78823e082e05ee3a63788d40de577cf31eb555f0c8", pubkey[2]); + EXPECT_EQ_MAP(0, c.m_pub_keys, pubkey[0]); + EXPECT_EQ_MAP(1, c.m_pub_keys, pubkey[1]); + EXPECT_EQ_MAP(2, c.m_pub_keys, pubkey[2]); + } + // address book + ASSERT_TRUE(c.m_address_book.size() == 1); + { + auto address_book_row = c.m_address_book.begin(); + EXPECT_TRUE(epee::string_tools::pod_to_hex(address_book_row->m_address.m_spend_public_key) == "9bc53a6ff7b0831c9470f71b6b972dbe5ad1e8606f72682868b1dda64e119fb3"); + EXPECT_TRUE(epee::string_tools::pod_to_hex(address_book_row->m_address.m_view_public_key) == "49fece1ef97dc0c0f7a5e2106e75e96edd910f7e86b56e1e308cd0cf734df191"); + EXPECT_TRUE(address_book_row->m_description == "testnet wallet 9y52S6"); + } +} + +TEST(wallet_storage, legacy_load_sanity) +{ + const boost::filesystem::path original_wallet_file = unit_test::data_dir / "wallet_9svHk1"; + const epee::wipeable_string password = "test"; + + tools::wallet2 w2(cryptonote::TESTNET, 1, true); + w2.load(original_wallet_file.string(), password); + + check_wallet_9svHk1_cache_contents(w2); + check_wallet_9svHk1_key_contents(w2); +} + +TEST(wallet_storage, read_old_wallet) +{ + const boost::filesystem::path wallet_file = unit_test::data_dir / "wallet_9svHk1"; + const epee::wipeable_string password = "test"; + + wallet2_basic::cache c; + wallet2_basic::keys_data k; + wallet2_basic::load_keys_and_cache_from_file(wallet_file.string(), password, c, k); + + check_wallet_9svHk1_cache_contents(c); + check_wallet_9svHk1_key_contents(k); +} + +TEST(wallet_storage, backwards_compatible_store_file) +{ + const boost::filesystem::path original_wallet_file = unit_test::data_dir / "wallet_9svHk1"; + const epee::wipeable_string password = "test"; + + const boost::filesystem::path target_wallet_file = unit_test::data_dir / "wallet_9svHk1_backwards_compatible_store_file"; + + wallet2_basic::cache c; + wallet2_basic::keys_data k; + + // load then save to target_wallet_file + wallet2_basic::load_keys_and_cache_from_file + ( + original_wallet_file.string(), + password, + c, + k + ); + wallet2_basic::store_keys_and_cache_to_file + ( + c, + k, + password, + target_wallet_file.string() + ); + + tools::wallet2 w2(cryptonote::TESTNET, 1, true); + w2.load(target_wallet_file.string(), password); // load the new file created by wallet2_basic + + check_wallet_9svHk1_cache_contents(w2); + check_wallet_9svHk1_key_contents(w2); +} + +TEST(wallet_storage, back_compat_ascii_format) +{ + const boost::filesystem::path original_wallet_file = unit_test::data_dir / "wallet_9svHk1"; + const boost::filesystem::path intermediate_wallet_file = unit_test::data_dir / "wallet_9svHk1_back_compat_ascii_load"; + const boost::filesystem::path final_wallet_file = unit_test::data_dir / "wallet_9svHk1_back_compat_ascii_load_w2b"; + const epee::wipeable_string password = "test"; + + copy_file(original_wallet_file, intermediate_wallet_file, copy_option::overwrite_if_exists); + copy_file(original_wallet_file.string() + ".keys", intermediate_wallet_file.string() + ".keys", copy_option::overwrite_if_exists); + + { + tools::wallet2 w(cryptonote::TESTNET, 1, true); + w.load(intermediate_wallet_file.string(), password); + w.set_export_format(tools::wallet2::Ascii); + w.store(); + w.rewrite(intermediate_wallet_file.string(), password); + } + + { + wallet2_basic::cache c; + wallet2_basic::keys_data k; + wallet2_basic::load_keys_and_cache_from_file + ( + intermediate_wallet_file.string(), + password, + c, + k + ); + + check_wallet_9svHk1_cache_contents(c); + check_wallet_9svHk1_key_contents(k, wallet2_basic::Ascii); + + wallet2_basic::store_keys_and_cache_to_file + ( + c, + k, + password, + final_wallet_file.string(), + 1, + wallet2_basic::Ascii + ); + } + + { + tools::wallet2 w(cryptonote::TESTNET, 1, true); + w.set_export_format(tools::wallet2::Ascii); + w.load(final_wallet_file.string(), password); + + check_wallet_9svHk1_cache_contents(w); + check_wallet_9svHk1_key_contents(w, tools::wallet2::Ascii); + } +} + +TEST(wallet_storage, back_compat_kdf_rounds) +{ + static constexpr uint64_t const KDF_ROUNDS_TEST_MIN = 2; + static constexpr uint64_t const KDF_ROUNDS_TEST_MAX = 8; + static constexpr uint64_t const KDF_ROUNDS_TEST_STEP = 3; + + const boost::filesystem::path original_wallet_file = unit_test::data_dir / "wallet_9svHk1"; + const epee::wipeable_string password = "test"; + + for (uint64_t kdf_rounds = KDF_ROUNDS_TEST_MIN; kdf_rounds <= KDF_ROUNDS_TEST_MAX; kdf_rounds += KDF_ROUNDS_TEST_STEP) + { + const boost::filesystem::path target_wallet_file = unit_test::data_dir / ("wallet_9svHk1_back_compat_kdf_rounds_" + std::to_string(kdf_rounds)); + + wallet2_basic::cache c; + wallet2_basic::keys_data k; + + // load then save to target_wallet_file + wallet2_basic::load_keys_and_cache_from_file + ( + original_wallet_file.string(), + password, + c, + k + ); + wallet2_basic::store_keys_and_cache_to_file + ( + c, + k, + password, + target_wallet_file.string(), + kdf_rounds /// <----- non-standard KDF rounds + ); + + tools::wallet2 w2(cryptonote::TESTNET, kdf_rounds, true); /// <----- non-standard KDF rounds + w2.load(target_wallet_file.string(), password); // load the new file created by wallet2_basic + + check_wallet_9svHk1_cache_contents(w2); + check_wallet_9svHk1_key_contents(w2); + } +} + +TEST(wallet_storage, load_multiple_kdf_rounds) +{ + const boost::filesystem::path wallet_file = unit_test::data_dir / "wallet_load_non_standard_kdf_rounds"; + const uint32_t kdf_rounds = 2 + crypto::rand_idx(10); // kdf_rounds in [2, 11] + const epee::wipeable_string password("88 FR 72701"); + const crypto::hash random_txid = crypto::rand(); + const std::string txid_note = "note for txid ;)"; + + cryptonote::account_base acc1, acc2; + + if (exists(wallet_file)) + remove(wallet_file); + if (exists(wallet_file.string() + ".keys")) + remove(wallet_file.string() + ".keys"); + + { + tools::wallet2 w(cryptonote::STAGENET, kdf_rounds, true); + w.generate(wallet_file.string(), password); + acc1 = w.get_account(); + w.set_tx_note(random_txid, txid_note); + w.store(); + } + + { + wallet2_basic::cache c; + wallet2_basic::keys_data k; + + wallet2_basic::load_keys_and_cache_from_file + ( + wallet_file.string(), + password, + c, + k, + cryptonote::UNDEFINED, + "", + false, + nullptr, + kdf_rounds + ); + + acc2 = k.m_account; + + ASSERT_TRUE(c.m_tx_notes.find(random_txid) != c.m_tx_notes.cend()); + EXPECT_EQ(txid_note, c.m_tx_notes[random_txid]); + } + + ASSERT_NE(crypto::secret_key{}, acc1.get_keys().m_spend_secret_key); + ASSERT_NE(crypto::secret_key{}, acc2.get_keys().m_spend_secret_key); + + EXPECT_EQ(acc1.get_keys().m_view_secret_key, acc2.get_keys().m_view_secret_key); + EXPECT_EQ(acc1.get_keys().m_spend_secret_key, acc2.get_keys().m_spend_secret_key); + EXPECT_EQ(acc1.get_createtime(), acc2.get_createtime()); +}