From b8271ab88154730149d54a31b2b165f210ac5b47 Mon Sep 17 00:00:00 2001 From: j-berman Date: Fri, 26 Aug 2022 16:13:19 -0700 Subject: wallet2: check wallet compatibility with daemon's hard fork version --- src/wallet/api/wallet.cpp | 10 ++- src/wallet/node_rpc_proxy.cpp | 32 ++++++++- src/wallet/node_rpc_proxy.h | 4 +- src/wallet/wallet2.cpp | 153 ++++++++++++++++++++++++++++++++++++++---- src/wallet/wallet2.h | 7 +- src/wallet/wallet_errors.h | 10 +++ 6 files changed, 199 insertions(+), 17 deletions(-) (limited to 'src/wallet') diff --git a/src/wallet/api/wallet.cpp b/src/wallet/api/wallet.cpp index 807dba33b..470206bc5 100644 --- a/src/wallet/api/wallet.cpp +++ b/src/wallet/api/wallet.cpp @@ -2173,9 +2173,15 @@ bool WalletImpl::connectToDaemon() Wallet::ConnectionStatus WalletImpl::connected() const { uint32_t version = 0; - m_is_connected = m_wallet->check_connection(&version, NULL, DEFAULT_CONNECTION_TIMEOUT_MILLIS); + bool wallet_is_outdated, daemon_is_outdated = false; + m_is_connected = m_wallet->check_connection(&version, NULL, DEFAULT_CONNECTION_TIMEOUT_MILLIS, &wallet_is_outdated, &daemon_is_outdated); if (!m_is_connected) - return Wallet::ConnectionStatus_Disconnected; + { + if (!m_wallet->light_wallet() && (wallet_is_outdated || daemon_is_outdated)) + return Wallet::ConnectionStatus_WrongVersion; + else + return Wallet::ConnectionStatus_Disconnected; + } // Version check is not implemented in light wallets nodes/wallets if (!m_wallet->light_wallet() && (version >> 16) != CORE_RPC_VERSION_MAJOR) return Wallet::ConnectionStatus_WrongVersion; diff --git a/src/wallet/node_rpc_proxy.cpp b/src/wallet/node_rpc_proxy.cpp index 7810abdd2..0a9ea8f7b 100644 --- a/src/wallet/node_rpc_proxy.cpp +++ b/src/wallet/node_rpc_proxy.cpp @@ -82,18 +82,21 @@ void NodeRPCProxy::invalidate() m_rpc_payment_seed_hash = crypto::null_hash; m_rpc_payment_next_seed_hash = crypto::null_hash; m_height_time = 0; + m_target_height_time = 0; m_rpc_payment_diff = 0; m_rpc_payment_credits_per_hash_found = 0; m_rpc_payment_height = 0; m_rpc_payment_cookie = 0; + m_daemon_hard_forks.clear(); } -boost::optional NodeRPCProxy::get_rpc_version(uint32_t &rpc_version) +boost::optional NodeRPCProxy::get_rpc_version(uint32_t &rpc_version, std::vector> &daemon_hard_forks, uint64_t &height, uint64_t &target_height) { if (m_offline) return boost::optional("offline"); if (m_rpc_version == 0) { + const time_t now = time(NULL); cryptonote::COMMAND_RPC_GET_VERSION::request req_t = AUTO_VAL_INIT(req_t); cryptonote::COMMAND_RPC_GET_VERSION::response resp_t = AUTO_VAL_INIT(resp_t); { @@ -101,9 +104,28 @@ boost::optional NodeRPCProxy::get_rpc_version(uint32_t &rpc_version bool r = net_utils::invoke_http_json_rpc("/json_rpc", "get_version", req_t, resp_t, m_http_client, rpc_timeout); RETURN_ON_RPC_RESPONSE_ERROR(r, epee::json_rpc::error{}, resp_t, "get_version"); } + m_rpc_version = resp_t.version; + m_daemon_hard_forks.clear(); + for (const auto &hf : resp_t.hard_forks) + m_daemon_hard_forks.push_back(std::make_pair(hf.hf_version, hf.height)); + if (resp_t.current_height > 0 || resp_t.target_height > 0) + { + m_height = resp_t.current_height; + m_target_height = resp_t.target_height; + m_height_time = now; + m_target_height_time = now; + } } + rpc_version = m_rpc_version; + daemon_hard_forks = m_daemon_hard_forks; + boost::optional result = get_height(height); + if (result) + return result; + result = get_target_height(target_height); + if (result) + return result; return boost::optional(); } @@ -138,6 +160,7 @@ boost::optional NodeRPCProxy::get_info() m_adjusted_time = resp_t.adjusted_time; m_get_info_time = now; m_height_time = now; + m_target_height_time = now; } return boost::optional(); } @@ -160,6 +183,13 @@ boost::optional NodeRPCProxy::get_height(uint64_t &height) boost::optional NodeRPCProxy::get_target_height(uint64_t &height) { + const time_t now = time(NULL); + if (now < m_target_height_time + 30) // re-cache every 30 seconds + { + height = m_target_height; + return boost::optional(); + } + auto res = get_info(); if (res) return res; diff --git a/src/wallet/node_rpc_proxy.h b/src/wallet/node_rpc_proxy.h index 07675cdb0..e320565ac 100644 --- a/src/wallet/node_rpc_proxy.h +++ b/src/wallet/node_rpc_proxy.h @@ -48,7 +48,7 @@ public: void invalidate(); void set_offline(bool offline) { m_offline = offline; } - boost::optional get_rpc_version(uint32_t &version); + boost::optional get_rpc_version(uint32_t &rpc_version, std::vector> &daemon_hard_forks, uint64_t &height, uint64_t &target_height); boost::optional get_height(uint64_t &height); void set_height(uint64_t h); boost::optional get_target_height(uint64_t &height); @@ -103,6 +103,8 @@ private: crypto::hash m_rpc_payment_next_seed_hash; uint32_t m_rpc_payment_cookie; time_t m_height_time; + time_t m_target_height_time; + std::vector> m_daemon_hard_forks; }; } diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp index b6d95b6b7..2328350d2 100644 --- a/src/wallet/wallet2.cpp +++ b/src/wallet/wallet2.cpp @@ -47,6 +47,7 @@ using namespace epee; #include "cryptonote_config.h" +#include "hardforks/hardforks.h" #include "cryptonote_core/tx_sanity_check.h" #include "wallet_rpc_helpers.h" #include "wallet2.h" @@ -275,6 +276,7 @@ struct options { const command_line::arg_descriptor no_dns = {"no-dns", tools::wallet2::tr("Do not use DNS"), false}; const command_line::arg_descriptor offline = {"offline", tools::wallet2::tr("Do not connect to a daemon, nor use DNS"), false}; const command_line::arg_descriptor extra_entropy = {"extra-entropy", tools::wallet2::tr("File containing extra entropy to initialize the PRNG (any data, aim for 256 bits of entropy to be useful, which typically means more than 256 bits of data)")}; + const command_line::arg_descriptor allow_mismatched_daemon_version = {"allow-mismatched-daemon-version", tools::wallet2::tr("Allow communicating with a daemon that uses a different version"), false}; }; void do_prepare_file_names(const std::string& file_path, std::string& keys_file, std::string& wallet_file, std::string &mms_file) @@ -484,6 +486,9 @@ std::unique_ptr make_basic(const boost::program_options::variabl add_extra_entropy_thread_safe(data.data(), data.size()); } + if (command_line::has_arg(vm, opts.allow_mismatched_daemon_version)) + wallet->allow_mismatched_daemon_version(true); + try { if (!command_line::is_arg_defaulted(vm, opts.tx_notify)) @@ -1219,7 +1224,8 @@ wallet2::wallet2(network_type nettype, uint64_t kdf_rounds, bool unattended, std m_load_deprecated_formats(false), m_credits_target(0), m_enable_multisig(false), - m_has_ever_refreshed_from_node(false) + m_has_ever_refreshed_from_node(false), + m_allow_mismatched_daemon_version(false) { set_rpc_client_secret_key(rct::rct2sk(rct::skGen())); } @@ -1279,6 +1285,7 @@ void wallet2::init_options(boost::program_options::options_description& desc_par command_line::add_arg(desc_params, opts.no_dns); command_line::add_arg(desc_params, opts.offline); command_line::add_arg(desc_params, opts.extra_entropy); + command_line::add_arg(desc_params, opts.allow_mismatched_daemon_version); } std::pair, tools::password_container> wallet2::make_from_json(const boost::program_options::variables_map& vm, bool unattended, const std::string& json_file, const std::function(const char *, bool)> &password_prompter) @@ -2915,6 +2922,26 @@ void wallet2::refresh(bool trusted_daemon, uint64_t start_height, uint64_t & blo refresh(trusted_daemon, start_height, blocks_fetched, received_money); } //---------------------------------------------------------------------------------------------------- +void check_block_hard_fork_version(cryptonote::network_type nettype, uint8_t hf_version, uint64_t height, bool &wallet_is_outdated, bool &daemon_is_outdated) +{ + const size_t wallet_num_hard_forks = nettype == TESTNET ? num_testnet_hard_forks + : nettype == STAGENET ? num_stagenet_hard_forks : num_mainnet_hard_forks; + const hardfork_t *wallet_hard_forks = nettype == TESTNET ? testnet_hard_forks + : nettype == STAGENET ? stagenet_hard_forks : mainnet_hard_forks; + + wallet_is_outdated = static_cast(hf_version) > wallet_num_hard_forks; + if (wallet_is_outdated) + return; + + // check block's height falls within wallet's expected range for block's given version + uint64_t start_height = hf_version == 1 ? 0 : wallet_hard_forks[hf_version - 1].height; + uint64_t end_height = static_cast(hf_version) + 1 > wallet_num_hard_forks + ? std::numeric_limits::max() + : wallet_hard_forks[hf_version].height; + + daemon_is_outdated = height < start_height || height >= end_height; +} +//---------------------------------------------------------------------------------------------------- void wallet2::pull_and_parse_next_blocks(uint64_t start_height, uint64_t &blocks_start_height, std::list &short_chain_history, const std::vector &prev_blocks, const std::vector &prev_parsed_blocks, std::vector &blocks, std::vector &parsed_blocks, bool &last, bool &error, std::exception_ptr &exception) { error = false; @@ -2956,6 +2983,23 @@ void wallet2::pull_and_parse_next_blocks(uint64_t start_height, uint64_t &blocks error = true; break; } + + if (!m_allow_mismatched_daemon_version) + { + // make sure block's hard fork version is expected at the block's height + uint8_t hf_version = parsed_blocks[i].block.major_version; + uint64_t height = blocks_start_height + i; + bool wallet_is_outdated = false; + bool daemon_is_outdated = false; + check_block_hard_fork_version(m_nettype, hf_version, height, wallet_is_outdated, daemon_is_outdated); + THROW_WALLET_EXCEPTION_IF(wallet_is_outdated || daemon_is_outdated, error::incorrect_fork_version, + "Unexpected hard fork version v" + std::to_string(hf_version) + " at height " + std::to_string(height) + ". " + + (wallet_is_outdated + ? "Make sure your wallet is up to date" + : "Make sure the node you are connected to is running the latest version") + ); + } + parsed_blocks[i].o_indices = std::move(o_indices[i]); } @@ -3578,6 +3622,11 @@ void wallet2::refresh(bool trusted_daemon, uint64_t start_height, uint64_t & blo THROW_WALLET_EXCEPTION_IF(!waiter.wait(), error::wallet_internal_error, "Exception in thread pool"); throw; } + catch (const error::incorrect_fork_version&) + { + THROW_WALLET_EXCEPTION_IF(!waiter.wait(), error::wallet_internal_error, "Exception in thread pool"); + throw; + } catch (const std::exception&) { blocks_fetched += added_blocks; @@ -4188,6 +4237,7 @@ bool wallet2::load_keys_buf(const std::string& keys_buf, const epee::wipeable_st m_auto_mine_for_rpc_payment_threshold = -1.0f; m_credits_target = 0; m_enable_multisig = false; + m_allow_mismatched_daemon_version = false; } else if(json.IsObject()) { @@ -5331,7 +5381,7 @@ bool wallet2::prepare_file_names(const std::string& file_path) return true; } //---------------------------------------------------------------------------------------------------- -bool wallet2::check_connection(uint32_t *version, bool *ssl, uint32_t timeout) +bool wallet2::check_connection(uint32_t *version, bool *ssl, uint32_t timeout, bool *wallet_is_outdated, bool *daemon_is_outdated) { THROW_WALLET_EXCEPTION_IF(!m_is_initialized, error::wallet_not_initialized); @@ -5368,20 +5418,99 @@ bool wallet2::check_connection(uint32_t *version, bool *ssl, uint32_t timeout) } } - if (!m_rpc_version) + if (!m_rpc_version && !check_version(version, wallet_is_outdated, daemon_is_outdated)) + return false; + if (version) + *version = m_rpc_version; + + return true; +} +//---------------------------------------------------------------------------------------------------- +bool wallet2::check_version(uint32_t *version, bool *wallet_is_outdated, bool *daemon_is_outdated) +{ + uint32_t rpc_version; + std::vector> daemon_hard_forks; + uint64_t height; + uint64_t target_height; + if (m_node_rpc_proxy.get_rpc_version(rpc_version, daemon_hard_forks, height, target_height)) { - cryptonote::COMMAND_RPC_GET_VERSION::request req_t = AUTO_VAL_INIT(req_t); - cryptonote::COMMAND_RPC_GET_VERSION::response resp_t = AUTO_VAL_INIT(resp_t); - bool r = invoke_http_json_rpc("/json_rpc", "get_version", req_t, resp_t); - if(!r || resp_t.status != CORE_RPC_STATUS_OK) { - if(version) - *version = 0; + if(version) + *version = 0; + return false; + } + + // check wallet compatibility with daemon's hard fork version + if (!m_allow_mismatched_daemon_version) + if (!check_hard_fork_version(m_nettype, daemon_hard_forks, height, target_height, wallet_is_outdated, daemon_is_outdated)) return false; + + m_rpc_version = rpc_version; + return true; +} +//---------------------------------------------------------------------------------------------------- +bool wallet2::check_hard_fork_version(cryptonote::network_type nettype, const std::vector> &daemon_hard_forks, const uint64_t height, const uint64_t target_height, bool *wallet_is_outdated, bool *daemon_is_outdated) +{ + const size_t wallet_num_hard_forks = nettype == TESTNET ? num_testnet_hard_forks + : nettype == STAGENET ? num_stagenet_hard_forks : num_mainnet_hard_forks; + const hardfork_t *wallet_hard_forks = nettype == TESTNET ? testnet_hard_forks + : nettype == STAGENET ? stagenet_hard_forks : mainnet_hard_forks; + + // First check if wallet or daemon is outdated (whether either are unaware of + // a hard fork). Then check if fork has passed rendering versions incompatible + if (daemon_hard_forks.size() > 0) + { + bool daemon_outdated = daemon_hard_forks.size() < wallet_num_hard_forks; + bool wallet_outdated = daemon_hard_forks.size() > wallet_num_hard_forks; + + if (daemon_is_outdated) + *daemon_is_outdated = daemon_outdated; + if (wallet_is_outdated) + *wallet_is_outdated = wallet_outdated; + + if (daemon_outdated) + { + uint64_t daemon_missed_fork_height = wallet_hard_forks[daemon_hard_forks.size()].height; + + // If the daemon missed the fork, then technically it is no longer part of + // the Monero network. Don't connect. + bool daemon_missed_fork = height >= daemon_missed_fork_height || target_height >= daemon_missed_fork_height; + if (daemon_missed_fork) + return false; + } + else if (wallet_outdated) + { + uint64_t wallet_missed_fork_height = daemon_hard_forks[wallet_num_hard_forks].second; + + // If the wallet missed the fork, then technically it is no longer able + // to communicate with the Monero network. Don't connect. + bool wallet_missed_fork = height >= wallet_missed_fork_height || target_height >= wallet_missed_fork_height; + if (wallet_missed_fork) + return false; } - m_rpc_version = resp_t.version; } - if (version) - *version = m_rpc_version; + else + { + // Non-updated daemons won't return daemon_hard_forks in response to + // get_version. Fall back to extra call to get_hard_fork_info by version. + uint64_t daemon_fork_height; + get_hard_fork_info(wallet_num_hard_forks-1/* wallet expects "double fork" pattern */, daemon_fork_height); + bool daemon_outdated = daemon_fork_height == std::numeric_limits::max(); + + if (daemon_is_outdated) + *daemon_is_outdated = daemon_outdated; + + if (daemon_outdated) + { + uint64_t daemon_missed_fork_height = wallet_hard_forks[wallet_num_hard_forks-2].height; + bool daemon_missed_fork = height >= daemon_missed_fork_height || target_height >= daemon_missed_fork_height; + if (daemon_missed_fork) + return false; + } + + // Don't need to check if wallet is outdated here because the daemons updated + // for a future hard fork will serve daemon_hard_forks above. The check for + // an outdated wallet is done above using daemon_hard_forks. + } return true; } diff --git a/src/wallet/wallet2.h b/src/wallet/wallet2.h index 1f84458a6..83c22d5f8 100644 --- a/src/wallet/wallet2.h +++ b/src/wallet/wallet2.h @@ -1091,7 +1091,9 @@ private: bool sign_multisig_tx_to_file(multisig_tx_set &exported_txs, const std::string &filename, std::vector &txids); std::vector create_unmixable_sweep_transactions(); void discard_unmixable_outputs(); - bool check_connection(uint32_t *version = NULL, bool *ssl = NULL, uint32_t timeout = 200000); + bool check_connection(uint32_t *version = NULL, bool *ssl = NULL, uint32_t timeout = 200000, bool *wallet_is_outdated = NULL, bool *daemon_is_outdated = NULL); + bool check_version(uint32_t *version, bool *wallet_is_outdated, bool *daemon_is_outdated); + bool check_hard_fork_version(cryptonote::network_type nettype, const std::vector> &daemon_hard_forks, const uint64_t height, const uint64_t target_height, bool *wallet_is_outdated, bool *daemon_is_outdated); void get_transfers(wallet2::transfer_container& incoming_transfers) const; void get_payments(const crypto::hash& payment_id, std::list& payments, uint64_t min_height = 0, const boost::optional& subaddr_account = boost::none, const std::set& subaddr_indices = {}) const; void get_payments(std::list>& payments, uint64_t min_height, uint64_t max_height = (uint64_t)-1, const boost::optional& subaddr_account = boost::none, const std::set& subaddr_indices = {}) const; @@ -1358,6 +1360,8 @@ private: void credits_target(uint64_t threshold) { m_credits_target = threshold; } bool is_multisig_enabled() const { return m_enable_multisig; } void enable_multisig(bool enable) { m_enable_multisig = enable; } + bool is_mismatched_daemon_version_allowed() const { return m_allow_mismatched_daemon_version; } + void allow_mismatched_daemon_version(bool allow_mismatch) { m_allow_mismatched_daemon_version = allow_mismatch; } bool get_tx_key_cached(const crypto::hash &txid, crypto::secret_key &tx_key, std::vector &additional_tx_keys) const; void set_tx_key(const crypto::hash &txid, const crypto::secret_key &tx_key, const std::vector &additional_tx_keys, const boost::optional &single_destination_subaddress = boost::none); @@ -1875,6 +1879,7 @@ private: rpc_payment_state_t m_rpc_payment_state; uint64_t m_credits_target; bool m_enable_multisig; + bool m_allow_mismatched_daemon_version; // Aux transaction data from device serializable_unordered_map m_tx_device; diff --git a/src/wallet/wallet_errors.h b/src/wallet/wallet_errors.h index df594aa21..0b8512163 100644 --- a/src/wallet/wallet_errors.h +++ b/src/wallet/wallet_errors.h @@ -439,6 +439,16 @@ namespace tools std::string to_string() const { return refresh_error::to_string(); } }; //---------------------------------------------------------------------------------------------------- + struct incorrect_fork_version : public refresh_error + { + explicit incorrect_fork_version(std::string&& loc, const std::string& message) + : refresh_error(std::move(loc), message) + { + } + + std::string to_string() const { return refresh_error::to_string(); } + }; + //---------------------------------------------------------------------------------------------------- struct signature_check_failed : public wallet_logic_error { explicit signature_check_failed(std::string&& loc, const std::string& message) -- cgit v1.2.3