// Copyright (c) 2014-2017, 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. // // Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers #include #include #include #include #include #include #include #include #include #include "include_base_utils.h" using namespace epee; #include "cryptonote_config.h" #include "wallet2.h" #include "cryptonote_basic/cryptonote_format_utils.h" #include "rpc/core_rpc_server_commands_defs.h" #include "misc_language.h" #include "cryptonote_basic/cryptonote_basic_impl.h" #include "multisig/multisig.h" #include "common/boost_serialization_helper.h" #include "common/command_line.h" #include "common/threadpool.h" #include "profile_tools.h" #include "crypto/crypto.h" #include "serialization/binary_utils.h" #include "cryptonote_basic/blobdatatype.h" #include "mnemonics/electrum-words.h" #include "common/i18n.h" #include "common/util.h" #include "common/apply_permutation.h" #include "rapidjson/document.h" #include "rapidjson/writer.h" #include "rapidjson/stringbuffer.h" #include "common/json_util.h" #include "common/memwipe.h" #include "common/base58.h" #include "ringct/rctSigs.h" extern "C" { #include "crypto/keccak.h" #include "crypto/crypto-ops.h" } using namespace std; using namespace crypto; using namespace cryptonote; #undef MONERO_DEFAULT_LOG_CATEGORY #define MONERO_DEFAULT_LOG_CATEGORY "wallet.wallet2" // used to choose when to stop adding outputs to a tx #define APPROXIMATE_INPUT_BYTES 80 // used to target a given block size (additional outputs may be added on top to build fee) #define TX_SIZE_TARGET(bytes) (bytes*2/3) // arbitrary, used to generate different hashes from the same input #define CHACHA8_KEY_TAIL 0x8c #define UNSIGNED_TX_PREFIX "Monero unsigned tx set\004" #define SIGNED_TX_PREFIX "Monero signed tx set\004" #define MULTISIG_UNSIGNED_TX_PREFIX "Monero multisig unsigned tx set\001" #define RECENT_OUTPUT_RATIO (0.5) // 50% of outputs are from the recent zone #define RECENT_OUTPUT_ZONE ((time_t)(1.8 * 86400)) // last 1.8 day makes up the recent zone (taken from monerolink.pdf, Miller et al) #define FEE_ESTIMATE_GRACE_BLOCKS 10 // estimate fee valid for that many blocks #define SECOND_OUTPUT_RELATEDNESS_THRESHOLD 0.0f #define SUBADDRESS_LOOKAHEAD_MAJOR 50 #define SUBADDRESS_LOOKAHEAD_MINOR 200 #define KEY_IMAGE_EXPORT_FILE_MAGIC "Monero key image export\002" #define MULTISIG_EXPORT_FILE_MAGIC "Monero multisig export\001" namespace { // Create on-demand to prevent static initialization order fiasco issues. struct options { const command_line::arg_descriptor daemon_address = {"daemon-address", tools::wallet2::tr("Use daemon instance at :"), ""}; const command_line::arg_descriptor daemon_host = {"daemon-host", tools::wallet2::tr("Use daemon instance at host instead of localhost"), ""}; const command_line::arg_descriptor password = {"password", tools::wallet2::tr("Wallet password (escape/quote as needed)"), "", true}; const command_line::arg_descriptor password_file = {"password-file", tools::wallet2::tr("Wallet password file"), "", true}; const command_line::arg_descriptor daemon_port = {"daemon-port", tools::wallet2::tr("Use daemon instance at port instead of 18081"), 0}; const command_line::arg_descriptor daemon_login = {"daemon-login", tools::wallet2::tr("Specify username[:password] for daemon RPC client"), "", true}; const command_line::arg_descriptor testnet = {"testnet", tools::wallet2::tr("For testnet. Daemon must also be launched with --testnet flag"), false}; const command_line::arg_descriptor restricted = {"restricted-rpc", tools::wallet2::tr("Restricts to view-only commands"), false}; }; void do_prepare_file_names(const std::string& file_path, std::string& keys_file, std::string& wallet_file) { keys_file = file_path; wallet_file = file_path; boost::system::error_code e; if(string_tools::get_extension(keys_file) == "keys") {//provided keys file name wallet_file = string_tools::cut_off_extension(wallet_file); }else {//provided wallet file name keys_file += ".keys"; } } uint64_t calculate_fee(uint64_t fee_per_kb, size_t bytes, uint64_t fee_multiplier) { uint64_t kB = (bytes + 1023) / 1024; return kB * fee_per_kb * fee_multiplier; } uint64_t calculate_fee(uint64_t fee_per_kb, const cryptonote::blobdata &blob, uint64_t fee_multiplier) { return calculate_fee(fee_per_kb, blob.size(), fee_multiplier); } std::string get_size_string(size_t sz) { return std::to_string(sz) + " bytes (" + std::to_string((sz + 1023) / 1024) + " kB)"; } std::string get_size_string(const cryptonote::blobdata &tx) { return get_size_string(tx.size()); } std::unique_ptr make_basic(const boost::program_options::variables_map& vm, const options& opts, const std::function(const char *, bool)> &password_prompter) { const bool testnet = command_line::get_arg(vm, opts.testnet); const bool restricted = command_line::get_arg(vm, opts.restricted); auto daemon_address = command_line::get_arg(vm, opts.daemon_address); auto daemon_host = command_line::get_arg(vm, opts.daemon_host); auto daemon_port = command_line::get_arg(vm, opts.daemon_port); THROW_WALLET_EXCEPTION_IF(!daemon_address.empty() && !daemon_host.empty() && 0 != daemon_port, tools::error::wallet_internal_error, tools::wallet2::tr("can't specify daemon host or port more than once")); boost::optional login{}; if (command_line::has_arg(vm, opts.daemon_login)) { auto parsed = tools::login::parse( command_line::get_arg(vm, opts.daemon_login), false, [password_prompter](bool verify) { return password_prompter("Daemon client password", verify); } ); if (!parsed) return nullptr; login.emplace(std::move(parsed->username), std::move(parsed->password).password()); } if (daemon_host.empty()) daemon_host = "localhost"; if (!daemon_port) { daemon_port = testnet ? config::testnet::RPC_DEFAULT_PORT : config::RPC_DEFAULT_PORT; } if (daemon_address.empty()) daemon_address = std::string("http://") + daemon_host + ":" + std::to_string(daemon_port); std::unique_ptr wallet(new tools::wallet2(testnet, restricted)); wallet->init(std::move(daemon_address), std::move(login)); return wallet; } boost::optional get_password(const boost::program_options::variables_map& vm, const options& opts, const std::function(const char*, bool)> &password_prompter, const bool verify) { if (command_line::has_arg(vm, opts.password) && command_line::has_arg(vm, opts.password_file)) { THROW_WALLET_EXCEPTION(tools::error::wallet_internal_error, tools::wallet2::tr("can't specify more than one of --password and --password-file")); } if (command_line::has_arg(vm, opts.password)) { return tools::password_container{command_line::get_arg(vm, opts.password)}; } if (command_line::has_arg(vm, opts.password_file)) { std::string password; bool r = epee::file_io_utils::load_file_to_string(command_line::get_arg(vm, opts.password_file), password); THROW_WALLET_EXCEPTION_IF(!r, tools::error::wallet_internal_error, tools::wallet2::tr("the password file specified could not be read")); // Remove line breaks the user might have inserted boost::trim_right_if(password, boost::is_any_of("\r\n")); return {tools::password_container{std::move(password)}}; } THROW_WALLET_EXCEPTION_IF(!password_prompter, tools::error::wallet_internal_error, tools::wallet2::tr("no password specified; use --prompt-for-password to prompt for a password")); return password_prompter(verify ? tr("Enter new wallet password") : tr("Wallet password"), verify); } std::unique_ptr generate_from_json(const std::string& json_file, const boost::program_options::variables_map& vm, const options& opts, const std::function(const char *, bool)> &password_prompter) { const bool testnet = command_line::get_arg(vm, opts.testnet); /* GET_FIELD_FROM_JSON_RETURN_ON_ERROR Is a generic macro that can return false. Gcc will coerce this into unique_ptr(nullptr), but clang correctly fails. This large wrapper is for the use of that macro */ std::unique_ptr wallet; const auto do_generate = [&]() -> bool { std::string buf; if (!epee::file_io_utils::load_file_to_string(json_file, buf)) { THROW_WALLET_EXCEPTION(tools::error::wallet_internal_error, std::string(tools::wallet2::tr("Failed to load file ")) + json_file); return false; } rapidjson::Document json; if (json.Parse(buf.c_str()).HasParseError()) { THROW_WALLET_EXCEPTION(tools::error::wallet_internal_error, tools::wallet2::tr("Failed to parse JSON")); return false; } GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, version, unsigned, Uint, true, 0); const int current_version = 1; THROW_WALLET_EXCEPTION_IF(field_version > current_version, tools::error::wallet_internal_error, ((boost::format(tools::wallet2::tr("Version %u too new, we can only grok up to %u")) % field_version % current_version)).str()); GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, filename, std::string, String, true, std::string()); GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, scan_from_height, uint64_t, Uint64, false, 0); const bool recover = field_scan_from_height_found; GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, password, std::string, String, false, std::string()); GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, viewkey, std::string, String, false, std::string()); crypto::secret_key viewkey; if (field_viewkey_found) { cryptonote::blobdata viewkey_data; if(!epee::string_tools::parse_hexstr_to_binbuff(field_viewkey, viewkey_data) || viewkey_data.size() != sizeof(crypto::secret_key)) { THROW_WALLET_EXCEPTION(tools::error::wallet_internal_error, tools::wallet2::tr("failed to parse view key secret key")); } viewkey = *reinterpret_cast(viewkey_data.data()); crypto::public_key pkey; if (!crypto::secret_key_to_public_key(viewkey, pkey)) { THROW_WALLET_EXCEPTION(tools::error::wallet_internal_error, tools::wallet2::tr("failed to verify view key secret key")); } } GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, spendkey, std::string, String, false, std::string()); crypto::secret_key spendkey; if (field_spendkey_found) { cryptonote::blobdata spendkey_data; if(!epee::string_tools::parse_hexstr_to_binbuff(field_spendkey, spendkey_data) || spendkey_data.size() != sizeof(crypto::secret_key)) { THROW_WALLET_EXCEPTION(tools::error::wallet_internal_error, tools::wallet2::tr("failed to parse spend key secret key")); } spendkey = *reinterpret_cast(spendkey_data.data()); crypto::public_key pkey; if (!crypto::secret_key_to_public_key(spendkey, pkey)) { THROW_WALLET_EXCEPTION(tools::error::wallet_internal_error, tools::wallet2::tr("failed to verify spend key secret key")); } } GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, seed, std::string, String, false, std::string()); std::string old_language; crypto::secret_key recovery_key; bool restore_deterministic_wallet = false; if (field_seed_found) { if (!crypto::ElectrumWords::words_to_bytes(field_seed, recovery_key, old_language)) { THROW_WALLET_EXCEPTION(tools::error::wallet_internal_error, tools::wallet2::tr("Electrum-style word list failed verification")); } restore_deterministic_wallet = true; GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, seed_passphrase, std::string, String, false, std::string()); if (field_seed_passphrase_found) { if (!field_seed_passphrase.empty()) recovery_key = cryptonote::decrypt_key(recovery_key, field_seed_passphrase); } } GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, address, std::string, String, false, std::string()); // compatibility checks if (!field_seed_found && !field_viewkey_found && !field_spendkey_found) { THROW_WALLET_EXCEPTION(tools::error::wallet_internal_error, tools::wallet2::tr("At least one of Electrum-style word list and private view key and private spend key must be specified")); } if (field_seed_found && (field_viewkey_found || field_spendkey_found)) { THROW_WALLET_EXCEPTION(tools::error::wallet_internal_error, tools::wallet2::tr("Both Electrum-style word list and private key(s) specified")); } // if an address was given, we check keys against it, and deduce the spend // public key if it was not given if (field_address_found) { cryptonote::address_parse_info info; if(!get_account_address_from_str(info, testnet, field_address)) { THROW_WALLET_EXCEPTION(tools::error::wallet_internal_error, tools::wallet2::tr("invalid address")); } if (field_viewkey_found) { crypto::public_key pkey; if (!crypto::secret_key_to_public_key(viewkey, pkey)) { THROW_WALLET_EXCEPTION(tools::error::wallet_internal_error, tools::wallet2::tr("failed to verify view key secret key")); } if (info.address.m_view_public_key != pkey) { THROW_WALLET_EXCEPTION(tools::error::wallet_internal_error, tools::wallet2::tr("view key does not match standard address")); } } if (field_spendkey_found) { crypto::public_key pkey; if (!crypto::secret_key_to_public_key(spendkey, pkey)) { THROW_WALLET_EXCEPTION(tools::error::wallet_internal_error, tools::wallet2::tr("failed to verify spend key secret key")); } if (info.address.m_spend_public_key != pkey) { THROW_WALLET_EXCEPTION(tools::error::wallet_internal_error, tools::wallet2::tr("spend key does not match standard address")); } } } const bool deprecated_wallet = restore_deterministic_wallet && ((old_language == crypto::ElectrumWords::old_language_name) || crypto::ElectrumWords::get_is_old_style_seed(field_seed)); THROW_WALLET_EXCEPTION_IF(deprecated_wallet, tools::error::wallet_internal_error, tools::wallet2::tr("Cannot create deprecated wallets from JSON")); wallet.reset(make_basic(vm, opts, password_prompter).release()); wallet->set_refresh_from_block_height(field_scan_from_height); try { if (!field_seed.empty()) { wallet->generate(field_filename, field_password, recovery_key, recover, false); } else if (field_viewkey.empty() && !field_spendkey.empty()) { wallet->generate(field_filename, field_password, spendkey, recover, false); } else { cryptonote::account_public_address address; if (!crypto::secret_key_to_public_key(viewkey, address.m_view_public_key)) { THROW_WALLET_EXCEPTION(tools::error::wallet_internal_error, tools::wallet2::tr("failed to verify view key secret key")); } if (field_spendkey.empty()) { // if we have an addres but no spend key, we can deduce the spend public key // from the address if (field_address_found) { cryptonote::address_parse_info info; if(!get_account_address_from_str(info, testnet, field_address)) { THROW_WALLET_EXCEPTION(tools::error::wallet_internal_error, std::string(tools::wallet2::tr("failed to parse address: ")) + field_address); } address.m_spend_public_key = info.address.m_spend_public_key; } else { THROW_WALLET_EXCEPTION(tools::error::wallet_internal_error, tools::wallet2::tr("Address must be specified in order to create watch-only wallet")); } wallet->generate(field_filename, field_password, address, viewkey); } else { if (!crypto::secret_key_to_public_key(spendkey, address.m_spend_public_key)) { THROW_WALLET_EXCEPTION(tools::error::wallet_internal_error, tools::wallet2::tr("failed to verify spend key secret key")); } wallet->generate(field_filename, field_password, address, spendkey, viewkey); } } } catch (const std::exception& e) { THROW_WALLET_EXCEPTION(tools::error::wallet_internal_error, std::string(tools::wallet2::tr("failed to generate new wallet: ")) + e.what()); } return true; }; if (do_generate()) { return wallet; } return nullptr; } static void throw_on_rpc_response_error(const boost::optional &status, const char *method) { // no error if (!status) return; // empty string -> not connection THROW_WALLET_EXCEPTION_IF(status->empty(), tools::error::no_connection_to_daemon, method); THROW_WALLET_EXCEPTION_IF(*status == CORE_RPC_STATUS_BUSY, tools::error::daemon_busy, method); THROW_WALLET_EXCEPTION_IF(*status != CORE_RPC_STATUS_OK, tools::error::wallet_generic_rpc_error, method, *status); } std::string strjoin(const std::vector &V, const char *sep) { std::stringstream ss; bool first = true; for (const auto &v: V) { if (!first) ss << sep; ss << std::to_string(v); first = false; } return ss.str(); } static void emplace_or_replace(std::unordered_multimap &container, const crypto::hash &key, const tools::wallet2::pool_payment_details &pd) { auto range = container.equal_range(key); for (auto i = range.first; i != range.second; ++i) { if (i->second.m_pd.m_tx_hash == pd.m_pd.m_tx_hash && i->second.m_pd.m_subaddr_index == pd.m_pd.m_subaddr_index) { i->second = pd; return; } } container.emplace(key, pd); } void drop_from_short_history(std::list &short_chain_history, size_t N) { std::list::iterator right; // drop early N off, skipping the genesis block if (short_chain_history.size() > N) { right = short_chain_history.end(); std::advance(right,-1); std::list::iterator left = right; std::advance(left, -N); short_chain_history.erase(left, right); } } size_t estimate_rct_tx_size(int n_inputs, int mixin, int n_outputs, size_t extra_size, bool bulletproof) { size_t size = 0; // tx prefix // first few bytes size += 1 + 6; // vin size += n_inputs * (1+6+(mixin+1)*2+32); // vout size += n_outputs * (6+32); // extra size += extra_size; // rct signatures // type size += 1; // rangeSigs if (bulletproof) size += ((2*6 + 4 + 5)*32 + 3) * n_outputs; else size += (2*64*32+32+64*32) * n_outputs; // MGs size += n_inputs * (64 * (mixin+1) + 32); // mixRing - not serialized, can be reconstructed /* size += 2 * 32 * (mixin+1) * n_inputs; */ // pseudoOuts size += 32 * n_inputs; // ecdhInfo size += 2 * 32 * n_outputs; // outPk - only commitment is saved size += 32 * n_outputs; // txnFee size += 4; LOG_PRINT_L2("estimated rct tx size for " << n_inputs << " with ring size " << (mixin+1) << " and " << n_outputs << ": " << size << " (" << ((32 * n_inputs/*+1*/) + 2 * 32 * (mixin+1) * n_inputs + 32 * n_outputs) << " saved)"); return size; } size_t estimate_tx_size(bool use_rct, int n_inputs, int mixin, int n_outputs, size_t extra_size, bool bulletproof) { if (use_rct) return estimate_rct_tx_size(n_inputs, mixin, n_outputs + 1, extra_size, bulletproof); else return n_inputs * (mixin+1) * APPROXIMATE_INPUT_BYTES + extra_size; } uint8_t get_bulletproof_fork(bool testnet) { if (testnet) return 7; else return 255; // TODO } crypto::hash8 get_short_payment_id(const tools::wallet2::pending_tx &ptx) { crypto::hash8 payment_id8 = null_hash8; std::vector tx_extra_fields; if(!parse_tx_extra(ptx.tx.extra, tx_extra_fields)) return payment_id8; cryptonote::tx_extra_nonce extra_nonce; if (find_tx_extra_field_by_type(tx_extra_fields, extra_nonce)) { if(get_encrypted_payment_id_from_tx_extra_nonce(extra_nonce.nonce, payment_id8)) { if (ptx.dests.empty()) { MWARNING("Encrypted payment id found, but no destinations public key, cannot decrypt"); return crypto::null_hash8; } decrypt_payment_id(payment_id8, ptx.dests[0].addr.m_view_public_key, ptx.tx_key); } } return payment_id8; } tools::wallet2::tx_construction_data get_construction_data_with_decrypted_short_payment_id(const tools::wallet2::pending_tx &ptx) { tools::wallet2::tx_construction_data construction_data = ptx.construction_data; crypto::hash8 payment_id = get_short_payment_id(ptx); if (payment_id != null_hash8) { // Remove encrypted remove_field_from_tx_extra(construction_data.extra, typeid(cryptonote::tx_extra_nonce)); // Add decrypted std::string extra_nonce; set_encrypted_payment_id_to_tx_extra_nonce(extra_nonce, payment_id); THROW_WALLET_EXCEPTION_IF(!add_extra_nonce_to_tx_extra(construction_data.extra, extra_nonce), tools::error::wallet_internal_error, "Failed to add decrypted payment id to tx extra"); LOG_PRINT_L1("Decrypted payment ID: " << payment_id); } return construction_data; } //----------------------------------------------------------------- } //namespace namespace tools { // for now, limit to 30 attempts. TODO: discuss a good number to limit to. const size_t MAX_SPLIT_ATTEMPTS = 30; constexpr const std::chrono::seconds wallet2::rpc_timeout; const char* wallet2::tr(const char* str) { return i18n_translate(str, "tools::wallet2"); } wallet2::wallet2(bool testnet, bool restricted): m_multisig_rescan_info(NULL), m_multisig_rescan_k(NULL), m_run(true), m_callback(0), m_testnet(testnet), m_always_confirm_transfers(true), m_print_ring_members(false), m_store_tx_info(true), m_default_mixin(0), m_default_priority(0), m_refresh_type(RefreshOptimizeCoinbase), m_auto_refresh(true), m_refresh_from_block_height(0), m_confirm_missing_payment_id(true), m_ask_password(true), m_min_output_count(0), m_min_output_value(0), m_merge_destinations(false), m_confirm_backlog(true), m_confirm_backlog_threshold(0), m_is_initialized(false), m_restricted(restricted), is_old_file_format(false), m_node_rpc_proxy(m_http_client, m_daemon_rpc_mutex), m_subaddress_lookahead_major(SUBADDRESS_LOOKAHEAD_MAJOR), m_subaddress_lookahead_minor(SUBADDRESS_LOOKAHEAD_MINOR), m_light_wallet(false), m_light_wallet_scanned_block_height(0), m_light_wallet_blockchain_height(0), m_light_wallet_connected(false), m_light_wallet_balance(0), m_light_wallet_unlocked_balance(0) { } bool wallet2::has_testnet_option(const boost::program_options::variables_map& vm) { return command_line::get_arg(vm, options().testnet); } void wallet2::init_options(boost::program_options::options_description& desc_params) { const options opts{}; command_line::add_arg(desc_params, opts.daemon_address); command_line::add_arg(desc_params, opts.daemon_host); command_line::add_arg(desc_params, opts.password); command_line::add_arg(desc_params, opts.password_file); command_line::add_arg(desc_params, opts.daemon_port); command_line::add_arg(desc_params, opts.daemon_login); command_line::add_arg(desc_params, opts.testnet); command_line::add_arg(desc_params, opts.restricted); } std::unique_ptr wallet2::make_from_json(const boost::program_options::variables_map& vm, const std::string& json_file, const std::function(const char *, bool)> &password_prompter) { const options opts{}; return generate_from_json(json_file, vm, opts, password_prompter); } std::pair, password_container> wallet2::make_from_file( const boost::program_options::variables_map& vm, const std::string& wallet_file, const std::function(const char *, bool)> &password_prompter) { const options opts{}; auto pwd = get_password(vm, opts, password_prompter, false); if (!pwd) { return {nullptr, password_container{}}; } auto wallet = make_basic(vm, opts, password_prompter); if (wallet) { wallet->load(wallet_file, pwd->password()); } return {std::move(wallet), std::move(*pwd)}; } std::pair, password_container> wallet2::make_new(const boost::program_options::variables_map& vm, const std::function(const char *, bool)> &password_prompter) { const options opts{}; auto pwd = get_password(vm, opts, password_prompter, true); if (!pwd) { return {nullptr, password_container{}}; } return {make_basic(vm, opts, password_prompter), std::move(*pwd)}; } std::unique_ptr wallet2::make_dummy(const boost::program_options::variables_map& vm, const std::function(const char *, bool)> &password_prompter) { const options opts{}; return make_basic(vm, opts, password_prompter); } //---------------------------------------------------------------------------------------------------- bool wallet2::init(std::string daemon_address, boost::optional daemon_login, uint64_t upper_transaction_size_limit, bool ssl) { m_checkpoints.init_default_checkpoints(m_testnet); if(m_http_client.is_connected()) m_http_client.disconnect(); m_is_initialized = true; m_upper_transaction_size_limit = upper_transaction_size_limit; m_daemon_address = std::move(daemon_address); m_daemon_login = std::move(daemon_login); // When switching from light wallet to full wallet, we need to reset the height we got from lw node. if(m_light_wallet) m_local_bc_height = m_blockchain.size(); return m_http_client.set_server(get_daemon_address(), get_daemon_login(), ssl); } //---------------------------------------------------------------------------------------------------- bool wallet2::is_deterministic() const { crypto::secret_key second; keccak((uint8_t *)&get_account().get_keys().m_spend_secret_key, sizeof(crypto::secret_key), (uint8_t *)&second, sizeof(crypto::secret_key)); sc_reduce32((uint8_t *)&second); bool keys_deterministic = memcmp(second.data,get_account().get_keys().m_view_secret_key.data, sizeof(crypto::secret_key)) == 0; return keys_deterministic; } //---------------------------------------------------------------------------------------------------- bool wallet2::get_seed(std::string& electrum_words, const epee::wipeable_string &passphrase) const { bool keys_deterministic = is_deterministic(); if (!keys_deterministic) { std::cout << "This is not a deterministic wallet" << std::endl; return false; } if (seed_language.empty()) { std::cout << "seed_language not set" << std::endl; return false; } crypto::secret_key key = get_account().get_keys().m_spend_secret_key; if (!passphrase.empty()) key = cryptonote::encrypt_key(key, passphrase); if (!crypto::ElectrumWords::bytes_to_words(key, electrum_words, seed_language)) { std::cout << "Failed to create seed from key for language: " << seed_language << std::endl; return false; } return true; } //---------------------------------------------------------------------------------------------------- bool wallet2::get_multisig_seed(std::string& seed, const epee::wipeable_string &passphrase, bool raw) const { bool ready; uint32_t threshold, total; if (!multisig(&ready, &threshold, &total)) { std::cout << "This is not a multisig wallet" << std::endl; return false; } if (!ready) { std::cout << "This multisig wallet is not yet finalized" << std::endl; return false; } if (!raw && seed_language.empty()) { std::cout << "seed_language not set" << std::endl; return false; } crypto::secret_key skey; crypto::public_key pkey; const account_keys &keys = get_account().get_keys(); std::string data; data.append((const char*)&threshold, sizeof(uint32_t)); data.append((const char*)&total, sizeof(uint32_t)); skey = keys.m_spend_secret_key; data.append((const char*)&skey, sizeof(skey)); pkey = keys.m_account_address.m_spend_public_key; data.append((const char*)&pkey, sizeof(pkey)); skey = keys.m_view_secret_key; data.append((const char*)&skey, sizeof(skey)); pkey = keys.m_account_address.m_view_public_key; data.append((const char*)&pkey, sizeof(pkey)); for (const auto &skey: keys.m_multisig_keys) data.append((const char*)&skey, sizeof(skey)); for (const auto &signer: m_multisig_signers) data.append((const char*)&signer, sizeof(signer)); if (!passphrase.empty()) { crypto::secret_key key; crypto::cn_slow_hash(passphrase.data(), passphrase.size(), (crypto::hash&)key); sc_reduce32((unsigned char*)key.data); data = encrypt(data, key, true); } if (raw) { seed = epee::string_tools::buff_to_hex_nodelimer(data); } else { if (!crypto::ElectrumWords::bytes_to_words(data.data(), data.size(), seed, seed_language)) { std::cout << "Failed to encode seed"; return false; } } return true; } //---------------------------------------------------------------------------------------------------- /*! * \brief Gets the seed language */ const std::string &wallet2::get_seed_language() const { return seed_language; } /*! * \brief Sets the seed language * \param language Seed language to set to */ void wallet2::set_seed_language(const std::string &language) { seed_language = language; } //---------------------------------------------------------------------------------------------------- cryptonote::account_public_address wallet2::get_subaddress(const cryptonote::subaddress_index& index) const { const cryptonote::account_keys& keys = m_account.get_keys(); if (index.is_zero()) return keys.m_account_address; crypto::public_key D = get_subaddress_spend_public_key(index); // C = a*D crypto::public_key C = rct::rct2pk(rct::scalarmultKey(rct::pk2rct(D), rct::sk2rct(keys.m_view_secret_key))); // could have defined secret_key_mult_public_key() under src/crypto // result: (C, D) cryptonote::account_public_address address; address.m_view_public_key = C; address.m_spend_public_key = D; return address; } //---------------------------------------------------------------------------------------------------- crypto::public_key wallet2::get_subaddress_spend_public_key(const cryptonote::subaddress_index& index) const { const cryptonote::account_keys& keys = m_account.get_keys(); if (index.is_zero()) return keys.m_account_address.m_spend_public_key; // m = Hs(a || index_major || index_minor) crypto::secret_key m = cryptonote::get_subaddress_secret_key(keys.m_view_secret_key, index); // M = m*G crypto::public_key M; crypto::secret_key_to_public_key(m, M); // D = B + M rct::key D_rct; rct::addKeys(D_rct, rct::pk2rct(keys.m_account_address.m_spend_public_key), rct::pk2rct(M)); // could have defined add_public_key() under src/crypto crypto::public_key D = rct::rct2pk(D_rct); return D; } //---------------------------------------------------------------------------------------------------- std::string wallet2::get_subaddress_as_str(const cryptonote::subaddress_index& index) const { cryptonote::account_public_address address = get_subaddress(index); return cryptonote::get_account_address_as_str(m_testnet, !index.is_zero(), address); } //---------------------------------------------------------------------------------------------------- std::string wallet2::get_integrated_address_as_str(const crypto::hash8& payment_id) const { return cryptonote::get_account_integrated_address_as_str(m_testnet, get_address(), payment_id); } //---------------------------------------------------------------------------------------------------- void wallet2::add_subaddress_account(const std::string& label) { uint32_t index_major = (uint32_t)get_num_subaddress_accounts(); expand_subaddresses({index_major, 0}); m_subaddress_labels[index_major][0] = label; } //---------------------------------------------------------------------------------------------------- void wallet2::add_subaddress(uint32_t index_major, const std::string& label) { THROW_WALLET_EXCEPTION_IF(index_major >= m_subaddress_labels.size(), error::account_index_outofbound); uint32_t index_minor = (uint32_t)get_num_subaddresses(index_major); expand_subaddresses({index_major, index_minor}); m_subaddress_labels[index_major][index_minor] = label; } //---------------------------------------------------------------------------------------------------- void wallet2::expand_subaddresses(const cryptonote::subaddress_index& index) { if (m_subaddress_labels.size() <= index.major) { // add new accounts cryptonote::subaddress_index index2; for (index2.major = m_subaddress_labels.size(); index2.major < index.major + m_subaddress_lookahead_major; ++index2.major) { for (index2.minor = 0; index2.minor < (index2.major == index.major ? index.minor : 0) + m_subaddress_lookahead_minor; ++index2.minor) { if (m_subaddresses_inv.count(index2) == 0) { crypto::public_key D = get_subaddress_spend_public_key(index2); m_subaddresses[D] = index2; m_subaddresses_inv[index2] = D; } } } m_subaddress_labels.resize(index.major + 1, {"Untitled account"}); m_subaddress_labels[index.major].resize(index.minor + 1); } else if (m_subaddress_labels[index.major].size() <= index.minor) { // add new subaddresses cryptonote::subaddress_index index2 = index; for (index2.minor = m_subaddress_labels[index.major].size(); index2.minor < index.minor + m_subaddress_lookahead_minor; ++index2.minor) { if (m_subaddresses_inv.count(index2) == 0) { crypto::public_key D = get_subaddress_spend_public_key(index2); m_subaddresses[D] = index2; m_subaddresses_inv[index2] = D; } } m_subaddress_labels[index.major].resize(index.minor + 1); } } //---------------------------------------------------------------------------------------------------- std::string wallet2::get_subaddress_label(const cryptonote::subaddress_index& index) const { if (index.major >= m_subaddress_labels.size() || index.minor >= m_subaddress_labels[index.major].size()) { MERROR("Subaddress label doesn't exist"); return ""; } return m_subaddress_labels[index.major][index.minor]; } //---------------------------------------------------------------------------------------------------- void wallet2::set_subaddress_label(const cryptonote::subaddress_index& index, const std::string &label) { THROW_WALLET_EXCEPTION_IF(index.major >= m_subaddress_labels.size(), error::account_index_outofbound); THROW_WALLET_EXCEPTION_IF(index.minor >= m_subaddress_labels[index.major].size(), error::address_index_outofbound); m_subaddress_labels[index.major][index.minor] = label; } //---------------------------------------------------------------------------------------------------- void wallet2::set_subaddress_lookahead(size_t major, size_t minor) { m_subaddress_lookahead_major = major; m_subaddress_lookahead_minor = minor; } //---------------------------------------------------------------------------------------------------- /*! * \brief Tells if the wallet file is deprecated. */ bool wallet2::is_deprecated() const { return is_old_file_format; } //---------------------------------------------------------------------------------------------------- void wallet2::set_spent(size_t idx, uint64_t height) { transfer_details &td = m_transfers[idx]; LOG_PRINT_L2("Setting SPENT at " << height << ": ki " << td.m_key_image << ", amount " << print_money(td.m_amount)); td.m_spent = true; td.m_spent_height = height; } //---------------------------------------------------------------------------------------------------- void wallet2::set_unspent(size_t idx) { transfer_details &td = m_transfers[idx]; LOG_PRINT_L2("Setting UNSPENT: ki " << td.m_key_image << ", amount " << print_money(td.m_amount)); td.m_spent = false; td.m_spent_height = 0; } //---------------------------------------------------------------------------------------------------- void wallet2::check_acc_out_precomp(const tx_out &o, const crypto::key_derivation &derivation, const std::vector &additional_derivations, size_t i, tx_scan_info_t &tx_scan_info) const { if (o.target.type() != typeid(txout_to_key)) { tx_scan_info.error = true; LOG_ERROR("wrong type id in transaction out"); return; } tx_scan_info.received = is_out_to_acc_precomp(m_subaddresses, boost::get(o.target).key, derivation, additional_derivations, i); if(tx_scan_info.received) { tx_scan_info.money_transfered = o.amount; // may be 0 for ringct outputs } else { tx_scan_info.money_transfered = 0; } tx_scan_info.error = false; } //---------------------------------------------------------------------------------------------------- static uint64_t decodeRct(const rct::rctSig & rv, const crypto::key_derivation &derivation, unsigned int i, rct::key & mask) { crypto::secret_key scalar1; crypto::derivation_to_scalar(derivation, i, scalar1); try { switch (rv.type) { case rct::RCTTypeSimple: case rct::RCTTypeSimpleBulletproof: return rct::decodeRctSimple(rv, rct::sk2rct(scalar1), i, mask); case rct::RCTTypeFull: case rct::RCTTypeFullBulletproof: return rct::decodeRct(rv, rct::sk2rct(scalar1), i, mask); default: LOG_ERROR("Unsupported rct type: " << rv.type); return 0; } } catch (const std::exception &e) { LOG_ERROR("Failed to decode input " << i); return 0; } } //---------------------------------------------------------------------------------------------------- void wallet2::scan_output(const cryptonote::account_keys &keys, const cryptonote::transaction &tx, const crypto::public_key &tx_pub_key, size_t i, tx_scan_info_t &tx_scan_info, int &num_vouts_received, std::unordered_map &tx_money_got_in_outs, std::vector &outs) { THROW_WALLET_EXCEPTION_IF(i >= tx.vout.size(), error::wallet_internal_error, "Invalid vout index"); if (m_multisig) { tx_scan_info.in_ephemeral.pub = boost::get(tx.vout[i].target).key; tx_scan_info.in_ephemeral.sec = crypto::null_skey; tx_scan_info.ki = rct::rct2ki(rct::zero()); } else { bool r = cryptonote::generate_key_image_helper_precomp(keys, boost::get(tx.vout[i].target).key, tx_scan_info.received->derivation, i, tx_scan_info.received->index, tx_scan_info.in_ephemeral, tx_scan_info.ki); THROW_WALLET_EXCEPTION_IF(!r, error::wallet_internal_error, "Failed to generate key image"); THROW_WALLET_EXCEPTION_IF(tx_scan_info.in_ephemeral.pub != boost::get(tx.vout[i].target).key, error::wallet_internal_error, "key_image generated ephemeral public key not matched with output_key"); } outs.push_back(i); if (tx_scan_info.money_transfered == 0) { tx_scan_info.money_transfered = tools::decodeRct(tx.rct_signatures, tx_scan_info.received->derivation, i, tx_scan_info.mask); } tx_money_got_in_outs[tx_scan_info.received->index] += tx_scan_info.money_transfered; tx_scan_info.amount = tx_scan_info.money_transfered; ++num_vouts_received; } //---------------------------------------------------------------------------------------------------- void wallet2::process_new_transaction(const crypto::hash &txid, const cryptonote::transaction& tx, const std::vector &o_indices, uint64_t height, uint64_t ts, bool miner_tx, bool pool, bool double_spend_seen) { // In this function, tx (probably) only contains the base information // (that is, the prunable stuff may or may not be included) if (!miner_tx && !pool) process_unconfirmed(txid, tx, height); std::vector outs; std::unordered_map tx_money_got_in_outs; // per receiving subaddress index crypto::public_key tx_pub_key = null_pkey; std::vector tx_extra_fields; if(!parse_tx_extra(tx.extra, tx_extra_fields)) { // Extra may only be partially parsed, it's OK if tx_extra_fields contains public key LOG_PRINT_L0("Transaction extra has unsupported format: " << txid); } // Don't try to extract tx public key if tx has no ouputs size_t pk_index = 0; std::vector tx_scan_info(tx.vout.size()); while (!tx.vout.empty()) { // if tx.vout is not empty, we loop through all tx pubkeys tx_extra_pub_key pub_key_field; if(!find_tx_extra_field_by_type(tx_extra_fields, pub_key_field, pk_index++)) { if (pk_index > 1) break; LOG_PRINT_L0("Public key wasn't found in the transaction extra. Skipping transaction " << txid); if(0 != m_callback) m_callback->on_skip_transaction(height, txid, tx); break; } int num_vouts_received = 0; tx_pub_key = pub_key_field.pub_key; tools::threadpool& tpool = tools::threadpool::getInstance(); tools::threadpool::waiter waiter; const cryptonote::account_keys& keys = m_account.get_keys(); crypto::key_derivation derivation; if (!generate_key_derivation(tx_pub_key, keys.m_view_secret_key, derivation)) { MWARNING("Failed to generate key derivation from tx pubkey, skipping"); static_assert(sizeof(derivation) == sizeof(rct::key), "Mismatched sizes of key_derivation and rct::key"); memcpy(&derivation, rct::identity().bytes, sizeof(derivation)); } // additional tx pubkeys and derivations for multi-destination transfers involving one or more subaddresses std::vector additional_tx_pub_keys = get_additional_tx_pub_keys_from_extra(tx); std::vector additional_derivations; for (size_t i = 0; i < additional_tx_pub_keys.size(); ++i) { additional_derivations.push_back({}); if (!generate_key_derivation(additional_tx_pub_keys[i], keys.m_view_secret_key, additional_derivations.back())) { MWARNING("Failed to generate key derivation from tx pubkey, skipping"); additional_derivations.pop_back(); } } if (miner_tx && m_refresh_type == RefreshNoCoinbase) { // assume coinbase isn't for us } else if (miner_tx && m_refresh_type == RefreshOptimizeCoinbase) { check_acc_out_precomp(tx.vout[0], derivation, additional_derivations, 0, tx_scan_info[0]); THROW_WALLET_EXCEPTION_IF(tx_scan_info[0].error, error::acc_outs_lookup_error, tx, tx_pub_key, m_account.get_keys()); // this assumes that the miner tx pays a single address if (tx_scan_info[0].received) { // process the other outs from that tx // the first one was already checked for (size_t i = 1; i < tx.vout.size(); ++i) { tpool.submit(&waiter, boost::bind(&wallet2::check_acc_out_precomp, this, std::cref(tx.vout[i]), std::cref(derivation), std::cref(additional_derivations), i, std::ref(tx_scan_info[i]))); } waiter.wait(); // then scan all outputs from 0 for (size_t i = 0; i < tx.vout.size(); ++i) { THROW_WALLET_EXCEPTION_IF(tx_scan_info[i].error, error::acc_outs_lookup_error, tx, tx_pub_key, m_account.get_keys()); if (tx_scan_info[i].received) { scan_output(keys, tx, tx_pub_key, i, tx_scan_info[i], num_vouts_received, tx_money_got_in_outs, outs); } } } } else if (tx.vout.size() > 1 && tools::threadpool::getInstance().get_max_concurrency() > 1) { for (size_t i = 0; i < tx.vout.size(); ++i) { tpool.submit(&waiter, boost::bind(&wallet2::check_acc_out_precomp, this, std::cref(tx.vout[i]), std::cref(derivation), std::cref(additional_derivations), i, std::ref(tx_scan_info[i]))); } waiter.wait(); for (size_t i = 0; i < tx.vout.size(); ++i) { THROW_WALLET_EXCEPTION_IF(tx_scan_info[i].error, error::acc_outs_lookup_error, tx, tx_pub_key, m_account.get_keys()); if (tx_scan_info[i].received) { scan_output(keys, tx, tx_pub_key, i, tx_scan_info[i], num_vouts_received, tx_money_got_in_outs, outs); } } } else { for (size_t i = 0; i < tx.vout.size(); ++i) { check_acc_out_precomp(tx.vout[i], derivation, additional_derivations, i, tx_scan_info[i]); THROW_WALLET_EXCEPTION_IF(tx_scan_info[i].error, error::acc_outs_lookup_error, tx, tx_pub_key, m_account.get_keys()); if (tx_scan_info[i].received) { scan_output(keys, tx, tx_pub_key, i, tx_scan_info[i], num_vouts_received, tx_money_got_in_outs, outs); } } } if(!outs.empty() && num_vouts_received > 0) { //good news - got money! take care about it //usually we have only one transfer for user in transaction if (!pool) { THROW_WALLET_EXCEPTION_IF(tx.vout.size() != o_indices.size(), error::wallet_internal_error, "transactions outputs size=" + std::to_string(tx.vout.size()) + " not match with daemon response size=" + std::to_string(o_indices.size())); } for(size_t o: outs) { THROW_WALLET_EXCEPTION_IF(tx.vout.size() <= o, error::wallet_internal_error, "wrong out in transaction: internal index=" + std::to_string(o) + ", total_outs=" + std::to_string(tx.vout.size())); auto kit = m_pub_keys.find(tx_scan_info[o].in_ephemeral.pub); THROW_WALLET_EXCEPTION_IF(kit != m_pub_keys.end() && kit->second >= m_transfers.size(), error::wallet_internal_error, std::string("Unexpected transfer index from public key: ") + "got " + (kit == m_pub_keys.end() ? "" : boost::lexical_cast(kit->second)) + ", m_transfers.size() is " + boost::lexical_cast(m_transfers.size())); if (kit == m_pub_keys.end()) { if (!pool) { m_transfers.push_back(boost::value_initialized()); transfer_details& td = m_transfers.back(); td.m_block_height = height; td.m_internal_output_index = o; td.m_global_output_index = o_indices[o]; td.m_tx = (const cryptonote::transaction_prefix&)tx; td.m_txid = txid; td.m_key_image = tx_scan_info[o].ki; td.m_key_image_known = !m_watch_only && !m_multisig; td.m_key_image_partial = m_multisig; td.m_amount = tx.vout[o].amount; td.m_pk_index = pk_index - 1; td.m_subaddr_index = tx_scan_info[o].received->index; expand_subaddresses(tx_scan_info[o].received->index); if (td.m_amount == 0) { td.m_mask = tx_scan_info[o].mask; td.m_amount = tx_scan_info[o].amount; td.m_rct = true; } else if (miner_tx && tx.version == 2) { td.m_mask = rct::identity(); td.m_rct = true; } else { td.m_mask = rct::identity(); td.m_rct = false; } set_unspent(m_transfers.size()-1); if (!m_multisig) m_key_images[td.m_key_image] = m_transfers.size()-1; m_pub_keys[tx_scan_info[o].in_ephemeral.pub] = m_transfers.size()-1; if (m_multisig) { THROW_WALLET_EXCEPTION_IF(!m_multisig_rescan_k && m_multisig_rescan_info, error::wallet_internal_error, "NULL m_multisig_rescan_k"); if (m_multisig_rescan_info && m_multisig_rescan_info->front().size() >= m_transfers.size()) update_multisig_rescan_info(*m_multisig_rescan_k, *m_multisig_rescan_info, m_transfers.size() - 1); } LOG_PRINT_L0("Received money: " << print_money(td.amount()) << ", with tx: " << txid); if (0 != m_callback) m_callback->on_money_received(height, txid, tx, td.m_amount, td.m_subaddr_index); } } else if (m_transfers[kit->second].m_spent || m_transfers[kit->second].amount() >= tx.vout[o].amount) { LOG_ERROR("Public key " << epee::string_tools::pod_to_hex(kit->first) << " from received " << print_money(tx.vout[o].amount) << " output already exists with " << (m_transfers[kit->second].m_spent ? "spent" : "unspent") << " " << print_money(m_transfers[kit->second].amount()) << ", received output ignored"); } else { LOG_ERROR("Public key " << epee::string_tools::pod_to_hex(kit->first) << " from received " << print_money(tx.vout[o].amount) << " output already exists with " << print_money(m_transfers[kit->second].amount()) << ", replacing with new output"); // The new larger output replaced a previous smaller one tx_money_got_in_outs[tx_scan_info[o].received->index] -= tx.vout[o].amount; if (!pool) { transfer_details &td = m_transfers[kit->second]; td.m_block_height = height; td.m_internal_output_index = o; td.m_global_output_index = o_indices[o]; td.m_tx = (const cryptonote::transaction_prefix&)tx; td.m_txid = txid; td.m_amount = tx.vout[o].amount; td.m_pk_index = pk_index - 1; td.m_subaddr_index = tx_scan_info[o].received->index; expand_subaddresses(tx_scan_info[o].received->index); if (td.m_amount == 0) { td.m_mask = tx_scan_info[o].mask; td.m_amount = tx_scan_info[o].amount; td.m_rct = true; } else if (miner_tx && tx.version == 2) { td.m_mask = rct::identity(); td.m_rct = true; } else { td.m_mask = rct::identity(); td.m_rct = false; } if (m_multisig) { THROW_WALLET_EXCEPTION_IF(!m_multisig_rescan_k && m_multisig_rescan_info, error::wallet_internal_error, "NULL m_multisig_rescan_k"); if (m_multisig_rescan_info && m_multisig_rescan_info->front().size() >= m_transfers.size()) update_multisig_rescan_info(*m_multisig_rescan_k, *m_multisig_rescan_info, m_transfers.size() - 1); } THROW_WALLET_EXCEPTION_IF(td.get_public_key() != tx_scan_info[o].in_ephemeral.pub, error::wallet_internal_error, "Inconsistent public keys"); THROW_WALLET_EXCEPTION_IF(td.m_spent, error::wallet_internal_error, "Inconsistent spent status"); LOG_PRINT_L0("Received money: " << print_money(td.amount()) << ", with tx: " << txid); if (0 != m_callback) m_callback->on_money_received(height, txid, tx, td.m_amount, td.m_subaddr_index); } } } } } uint64_t tx_money_spent_in_ins = 0; // The line below is equivalent to "boost::optional subaddr_account;", but avoids the GCC warning: ‘*((void*)& subaddr_account +4)’ may be used uninitialized in this function // It's a GCC bug with boost::optional, see https://gcc.gnu.org/bugzilla/show_bug.cgi?id=47679 auto subaddr_account ([]()->boost::optional {return boost::none;}()); std::set subaddr_indices; // check all outputs for spending (compare key images) for(auto& in: tx.vin) { if(in.type() != typeid(cryptonote::txin_to_key)) continue; auto it = m_key_images.find(boost::get(in).k_image); if(it != m_key_images.end()) { transfer_details& td = m_transfers[it->second]; uint64_t amount = boost::get(in).amount; if (amount > 0) { THROW_WALLET_EXCEPTION_IF(amount != td.amount(), error::wallet_internal_error, std::string("Inconsistent amount in tx input: got ") + print_money(amount) + std::string(", expected ") + print_money(td.amount())); } amount = td.amount(); tx_money_spent_in_ins += amount; if (subaddr_account && *subaddr_account != td.m_subaddr_index.major) LOG_ERROR("spent funds are from different subaddress accounts; count of incoming/outgoing payments will be incorrect"); subaddr_account = td.m_subaddr_index.major; subaddr_indices.insert(td.m_subaddr_index.minor); if (!pool) { LOG_PRINT_L0("Spent money: " << print_money(amount) << ", with tx: " << txid); set_spent(it->second, height); if (0 != m_callback) m_callback->on_money_spent(height, txid, tx, amount, tx, td.m_subaddr_index); } } } if (tx_money_spent_in_ins > 0 && !pool) { uint64_t self_received = std::accumulate(tx_money_got_in_outs.begin(), tx_money_got_in_outs.end(), 0, [&subaddr_account] (uint64_t acc, const std::pair& p) { return acc + (p.first.major == *subaddr_account ? p.second : 0); }); process_outgoing(txid, tx, height, ts, tx_money_spent_in_ins, self_received, *subaddr_account, subaddr_indices); // if sending to yourself at the same subaddress account, set the outgoing payment amount to 0 so that it's less confusing uint64_t fee = tx.version == 1 ? tx_money_spent_in_ins - get_outs_money_amount(tx) : tx.rct_signatures.txnFee; if (tx_money_spent_in_ins == self_received + fee) { auto i = m_confirmed_txs.find(txid); THROW_WALLET_EXCEPTION_IF(i == m_confirmed_txs.end(), error::wallet_internal_error, "confirmed tx wasn't found: " + string_tools::pod_to_hex(txid)); i->second.m_change = self_received; } } // remove change sent to the spending subaddress account from the list of received funds for (auto i = tx_money_got_in_outs.begin(); i != tx_money_got_in_outs.end();) { if (subaddr_account && i->first.major == *subaddr_account) i = tx_money_got_in_outs.erase(i); else ++i; } // create payment_details for each incoming transfer to a subaddress index if (tx_money_got_in_outs.size() > 0) { tx_extra_nonce extra_nonce; crypto::hash payment_id = null_hash; if (find_tx_extra_field_by_type(tx_extra_fields, extra_nonce)) { crypto::hash8 payment_id8 = null_hash8; if(get_encrypted_payment_id_from_tx_extra_nonce(extra_nonce.nonce, payment_id8)) { // We got a payment ID to go with this tx LOG_PRINT_L2("Found encrypted payment ID: " << payment_id8); if (tx_pub_key != null_pkey) { if (!decrypt_payment_id(payment_id8, tx_pub_key, m_account.get_keys().m_view_secret_key)) { LOG_PRINT_L0("Failed to decrypt payment ID: " << payment_id8); } else { LOG_PRINT_L2("Decrypted payment ID: " << payment_id8); // put the 64 bit decrypted payment id in the first 8 bytes memcpy(payment_id.data, payment_id8.data, 8); // rest is already 0, but guard against code changes above memset(payment_id.data + 8, 0, 24); } } else { LOG_PRINT_L1("No public key found in tx, unable to decrypt payment id"); } } else if (get_payment_id_from_tx_extra_nonce(extra_nonce.nonce, payment_id)) { LOG_PRINT_L2("Found unencrypted payment ID: " << payment_id); } } else if (get_payment_id_from_tx_extra_nonce(extra_nonce.nonce, payment_id)) { LOG_PRINT_L2("Found unencrypted payment ID: " << payment_id); } for (const auto& i : tx_money_got_in_outs) { payment_details payment; payment.m_tx_hash = txid; payment.m_amount = i.second; payment.m_block_height = height; payment.m_unlock_time = tx.unlock_time; payment.m_timestamp = ts; payment.m_subaddr_index = i.first; if (pool) { emplace_or_replace(m_unconfirmed_payments, payment_id, pool_payment_details{payment, double_spend_seen}); if (0 != m_callback) m_callback->on_unconfirmed_money_received(height, txid, tx, payment.m_amount, payment.m_subaddr_index); } else m_payments.emplace(payment_id, payment); LOG_PRINT_L2("Payment found in " << (pool ? "pool" : "block") << ": " << payment_id << " / " << payment.m_tx_hash << " / " << payment.m_amount); } } } //---------------------------------------------------------------------------------------------------- void wallet2::process_unconfirmed(const crypto::hash &txid, const cryptonote::transaction& tx, uint64_t height) { if (m_unconfirmed_txs.empty()) return; auto unconf_it = m_unconfirmed_txs.find(txid); if(unconf_it != m_unconfirmed_txs.end()) { if (store_tx_info()) { try { m_confirmed_txs.insert(std::make_pair(txid, confirmed_transfer_details(unconf_it->second, height))); } catch (...) { // can fail if the tx has unexpected input types LOG_PRINT_L0("Failed to add outgoing transaction to confirmed transaction map"); } } m_unconfirmed_txs.erase(unconf_it); } } //---------------------------------------------------------------------------------------------------- void wallet2::process_outgoing(const crypto::hash &txid, const cryptonote::transaction &tx, uint64_t height, uint64_t ts, uint64_t spent, uint64_t received, uint32_t subaddr_account, const std::set& subaddr_indices) { std::pair::iterator, bool> entry = m_confirmed_txs.insert(std::make_pair(txid, confirmed_transfer_details())); // fill with the info we know, some info might already be there if (entry.second) { // this case will happen if the tx is from our outputs, but was sent by another // wallet (eg, we're a cold wallet and the hot wallet sent it). For RCT transactions, // we only see 0 input amounts, so have to deduce amount out from other parameters. entry.first->second.m_amount_in = spent; if (tx.version == 1) entry.first->second.m_amount_out = get_outs_money_amount(tx); else entry.first->second.m_amount_out = spent - tx.rct_signatures.txnFee; entry.first->second.m_change = received; std::vector tx_extra_fields; if(parse_tx_extra(tx.extra, tx_extra_fields)) { tx_extra_nonce extra_nonce; if (find_tx_extra_field_by_type(tx_extra_fields, extra_nonce)) { // we do not care about failure here get_payment_id_from_tx_extra_nonce(extra_nonce.nonce, entry.first->second.m_payment_id); } } entry.first->second.m_subaddr_account = subaddr_account; entry.first->second.m_subaddr_indices = subaddr_indices; } entry.first->second.m_block_height = height; entry.first->second.m_timestamp = ts; entry.first->second.m_unlock_time = tx.unlock_time; } //---------------------------------------------------------------------------------------------------- void wallet2::process_new_blockchain_entry(const cryptonote::block& b, const cryptonote::block_complete_entry& bche, const crypto::hash& bl_id, uint64_t height, const cryptonote::COMMAND_RPC_GET_BLOCKS_FAST::block_output_indices &o_indices) { size_t txidx = 0; THROW_WALLET_EXCEPTION_IF(bche.txs.size() + 1 != o_indices.indices.size(), error::wallet_internal_error, "block transactions=" + std::to_string(bche.txs.size()) + " not match with daemon response size=" + std::to_string(o_indices.indices.size())); //handle transactions from new block //optimization: seeking only for blocks that are not older then the wallet creation time plus 1 day. 1 day is for possible user incorrect time setup if(b.timestamp + 60*60*24 > m_account.get_createtime() && height >= m_refresh_from_block_height) { TIME_MEASURE_START(miner_tx_handle_time); process_new_transaction(get_transaction_hash(b.miner_tx), b.miner_tx, o_indices.indices[txidx++].indices, height, b.timestamp, true, false, false); TIME_MEASURE_FINISH(miner_tx_handle_time); TIME_MEASURE_START(txs_handle_time); THROW_WALLET_EXCEPTION_IF(bche.txs.size() != b.tx_hashes.size(), error::wallet_internal_error, "Wrong amount of transactions for block"); size_t idx = 0; for (const auto& txblob: bche.txs) { cryptonote::transaction tx; bool r = parse_and_validate_tx_base_from_blob(txblob, tx); THROW_WALLET_EXCEPTION_IF(!r, error::tx_parse_error, txblob); process_new_transaction(b.tx_hashes[idx], tx, o_indices.indices[txidx++].indices, height, b.timestamp, false, false, false); ++idx; } TIME_MEASURE_FINISH(txs_handle_time); LOG_PRINT_L2("Processed block: " << bl_id << ", height " << height << ", " << miner_tx_handle_time + txs_handle_time << "(" << miner_tx_handle_time << "/" << txs_handle_time <<")ms"); }else { if (!(height % 100)) LOG_PRINT_L2( "Skipped block by timestamp, height: " << height << ", block time " << b.timestamp << ", account time " << m_account.get_createtime()); } m_blockchain.push_back(bl_id); ++m_local_bc_height; if (0 != m_callback) m_callback->on_new_block(height, b); } //---------------------------------------------------------------------------------------------------- void wallet2::get_short_chain_history(std::list& ids) const { size_t i = 0; size_t current_multiplier = 1; size_t sz = m_blockchain.size() - m_blockchain.offset(); if(!sz) { ids.push_back(m_blockchain.genesis()); return; } size_t current_back_offset = 1; bool base_included = false; while(current_back_offset < sz) { ids.push_back(m_blockchain[m_blockchain.offset() + sz-current_back_offset]); if(sz-current_back_offset == 0) base_included = true; if(i < 10) { ++current_back_offset; }else { current_back_offset += current_multiplier *= 2; } ++i; } if(!base_included) ids.push_back(m_blockchain[m_blockchain.offset()]); if(m_blockchain.offset()) ids.push_back(m_blockchain.genesis()); } //---------------------------------------------------------------------------------------------------- void wallet2::parse_block_round(const cryptonote::blobdata &blob, cryptonote::block &bl, crypto::hash &bl_id, bool &error) const { error = !cryptonote::parse_and_validate_block_from_blob(blob, bl); if (!error) bl_id = get_block_hash(bl); } //---------------------------------------------------------------------------------------------------- void wallet2::pull_blocks(uint64_t start_height, uint64_t &blocks_start_height, const std::list &short_chain_history, std::list &blocks, std::vector &o_indices) { cryptonote::COMMAND_RPC_GET_BLOCKS_FAST::request req = AUTO_VAL_INIT(req); cryptonote::COMMAND_RPC_GET_BLOCKS_FAST::response res = AUTO_VAL_INIT(res); req.block_ids = short_chain_history; uint32_t rpc_version; boost::optional result = m_node_rpc_proxy.get_rpc_version(rpc_version); // no error if (!!result) { // empty string -> not connection THROW_WALLET_EXCEPTION_IF(result->empty(), tools::error::no_connection_to_daemon, "getversion"); THROW_WALLET_EXCEPTION_IF(*result == CORE_RPC_STATUS_BUSY, tools::error::daemon_busy, "getversion"); if (*result != CORE_RPC_STATUS_OK) { MDEBUG("Cannot determined daemon RPC version, not asking for pruned blocks"); req.prune = false; // old daemon } } else { if (rpc_version >= MAKE_CORE_RPC_VERSION(1, 7)) { MDEBUG("Daemon is recent enough, asking for pruned blocks"); req.prune = true; } else { MDEBUG("Daemon is too old, not asking for pruned blocks"); req.prune = false; } } req.start_height = start_height; m_daemon_rpc_mutex.lock(); bool r = net_utils::invoke_http_bin("/getblocks.bin", req, res, m_http_client, rpc_timeout); m_daemon_rpc_mutex.unlock(); THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "getblocks.bin"); THROW_WALLET_EXCEPTION_IF(res.status == CORE_RPC_STATUS_BUSY, error::daemon_busy, "getblocks.bin"); THROW_WALLET_EXCEPTION_IF(res.status != CORE_RPC_STATUS_OK, error::get_blocks_error, res.status); THROW_WALLET_EXCEPTION_IF(res.blocks.size() != res.output_indices.size(), error::wallet_internal_error, "mismatched blocks (" + boost::lexical_cast(res.blocks.size()) + ") and output_indices (" + boost::lexical_cast(res.output_indices.size()) + ") sizes from daemon"); blocks_start_height = res.start_height; blocks = res.blocks; o_indices = res.output_indices; } //---------------------------------------------------------------------------------------------------- void wallet2::pull_hashes(uint64_t start_height, uint64_t &blocks_start_height, const std::list &short_chain_history, std::list &hashes) { cryptonote::COMMAND_RPC_GET_HASHES_FAST::request req = AUTO_VAL_INIT(req); cryptonote::COMMAND_RPC_GET_HASHES_FAST::response res = AUTO_VAL_INIT(res); req.block_ids = short_chain_history; req.start_height = start_height; m_daemon_rpc_mutex.lock(); bool r = net_utils::invoke_http_bin("/gethashes.bin", req, res, m_http_client, rpc_timeout); m_daemon_rpc_mutex.unlock(); THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "gethashes.bin"); THROW_WALLET_EXCEPTION_IF(res.status == CORE_RPC_STATUS_BUSY, error::daemon_busy, "gethashes.bin"); THROW_WALLET_EXCEPTION_IF(res.status != CORE_RPC_STATUS_OK, error::get_hashes_error, res.status); blocks_start_height = res.start_height; hashes = res.m_block_ids; } //---------------------------------------------------------------------------------------------------- void wallet2::process_blocks(uint64_t start_height, const std::list &blocks, const std::vector &o_indices, uint64_t& blocks_added) { size_t current_index = start_height; blocks_added = 0; size_t tx_o_indices_idx = 0; THROW_WALLET_EXCEPTION_IF(blocks.size() != o_indices.size(), error::wallet_internal_error, "size mismatch"); THROW_WALLET_EXCEPTION_IF(!m_blockchain.is_in_bounds(current_index), error::wallet_internal_error, "Index out of bounds of hashchain"); tools::threadpool& tpool = tools::threadpool::getInstance(); int threads = tpool.get_max_concurrency(); if (threads > 1) { std::vector round_block_hashes(threads); std::vector round_blocks(threads); std::deque error(threads); size_t blocks_size = blocks.size(); std::list::const_iterator blocki = blocks.begin(); for (size_t b = 0; b < blocks_size; b += threads) { size_t round_size = std::min((size_t)threads, blocks_size - b); tools::threadpool::waiter waiter; std::list::const_iterator tmpblocki = blocki; for (size_t i = 0; i < round_size; ++i) { tpool.submit(&waiter, boost::bind(&wallet2::parse_block_round, this, std::cref(tmpblocki->block), std::ref(round_blocks[i]), std::ref(round_block_hashes[i]), std::ref(error[i]))); ++tmpblocki; } waiter.wait(); tmpblocki = blocki; for (size_t i = 0; i < round_size; ++i) { THROW_WALLET_EXCEPTION_IF(error[i], error::block_parse_error, tmpblocki->block); ++tmpblocki; } for (size_t i = 0; i < round_size; ++i) { const crypto::hash &bl_id = round_block_hashes[i]; cryptonote::block &bl = round_blocks[i]; if(current_index >= m_blockchain.size()) { process_new_blockchain_entry(bl, *blocki, bl_id, current_index, o_indices[b+i]); ++blocks_added; } else if(bl_id != m_blockchain[current_index]) { //split detected here !!! THROW_WALLET_EXCEPTION_IF(current_index == start_height, error::wallet_internal_error, "wrong daemon response: split starts from the first block in response " + string_tools::pod_to_hex(bl_id) + " (height " + std::to_string(start_height) + "), local block id at this height: " + string_tools::pod_to_hex(m_blockchain[current_index])); detach_blockchain(current_index); process_new_blockchain_entry(bl, *blocki, bl_id, current_index, o_indices[b+i]); } else { LOG_PRINT_L2("Block is already in blockchain: " << string_tools::pod_to_hex(bl_id)); } ++current_index; ++blocki; } } } else { for(auto& bl_entry: blocks) { cryptonote::block bl; bool r = cryptonote::parse_and_validate_block_from_blob(bl_entry.block, bl); THROW_WALLET_EXCEPTION_IF(!r, error::block_parse_error, bl_entry.block); crypto::hash bl_id = get_block_hash(bl); if(current_index >= m_blockchain.size()) { process_new_blockchain_entry(bl, bl_entry, bl_id, current_index, o_indices[tx_o_indices_idx]); ++blocks_added; } else if(bl_id != m_blockchain[current_index]) { //split detected here !!! THROW_WALLET_EXCEPTION_IF(current_index == start_height, error::wallet_internal_error, "wrong daemon response: split starts from the first block in response " + string_tools::pod_to_hex(bl_id) + " (height " + std::to_string(start_height) + "), local block id at this height: " + string_tools::pod_to_hex(m_blockchain[current_index])); detach_blockchain(current_index); process_new_blockchain_entry(bl, bl_entry, bl_id, current_index, o_indices[tx_o_indices_idx]); } else { LOG_PRINT_L2("Block is already in blockchain: " << string_tools::pod_to_hex(bl_id)); } ++current_index; ++tx_o_indices_idx; } } } //---------------------------------------------------------------------------------------------------- void wallet2::refresh() { uint64_t blocks_fetched = 0; refresh(0, blocks_fetched); } //---------------------------------------------------------------------------------------------------- void wallet2::refresh(uint64_t start_height, uint64_t & blocks_fetched) { bool received_money = false; refresh(start_height, blocks_fetched, received_money); } //---------------------------------------------------------------------------------------------------- void wallet2::pull_next_blocks(uint64_t start_height, uint64_t &blocks_start_height, std::list &short_chain_history, const std::list &prev_blocks, std::list &blocks, std::vector &o_indices, bool &error) { error = false; try { drop_from_short_history(short_chain_history, 3); // prepend the last 3 blocks, should be enough to guard against a block or two's reorg cryptonote::block bl; std::list::const_reverse_iterator i = prev_blocks.rbegin(); for (size_t n = 0; n < std::min((size_t)3, prev_blocks.size()); ++n) { bool ok = cryptonote::parse_and_validate_block_from_blob(i->block, bl); THROW_WALLET_EXCEPTION_IF(!ok, error::block_parse_error, i->block); short_chain_history.push_front(cryptonote::get_block_hash(bl)); ++i; } // pull the new blocks pull_blocks(start_height, blocks_start_height, short_chain_history, blocks, o_indices); } catch(...) { error = true; } } void wallet2::remove_obsolete_pool_txs(const std::vector &tx_hashes) { // remove pool txes to us that aren't in the pool anymore std::unordered_multimap::iterator uit = m_unconfirmed_payments.begin(); while (uit != m_unconfirmed_payments.end()) { const crypto::hash &txid = uit->second.m_pd.m_tx_hash; bool found = false; for (const auto &it2: tx_hashes) { if (it2 == txid) { found = true; break; } } auto pit = uit++; if (!found) { MDEBUG("Removing " << txid << " from unconfirmed payments, not found in pool"); m_unconfirmed_payments.erase(pit); if (0 != m_callback) m_callback->on_pool_tx_removed(txid); } } } //---------------------------------------------------------------------------------------------------- void wallet2::update_pool_state(bool refreshed) { MDEBUG("update_pool_state start"); // get the pool state cryptonote::COMMAND_RPC_GET_TRANSACTION_POOL_HASHES::request req; cryptonote::COMMAND_RPC_GET_TRANSACTION_POOL_HASHES::response res; m_daemon_rpc_mutex.lock(); bool r = epee::net_utils::invoke_http_json("/get_transaction_pool_hashes.bin", req, res, m_http_client, rpc_timeout); m_daemon_rpc_mutex.unlock(); THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "get_transaction_pool_hashes.bin"); THROW_WALLET_EXCEPTION_IF(res.status == CORE_RPC_STATUS_BUSY, error::daemon_busy, "get_transaction_pool_hashes.bin"); THROW_WALLET_EXCEPTION_IF(res.status != CORE_RPC_STATUS_OK, error::get_tx_pool_error); MDEBUG("update_pool_state got pool"); // remove any pending tx that's not in the pool std::unordered_map::iterator it = m_unconfirmed_txs.begin(); while (it != m_unconfirmed_txs.end()) { const crypto::hash &txid = it->first; bool found = false; for (const auto &it2: res.tx_hashes) { if (it2 == txid) { found = true; break; } } auto pit = it++; if (!found) { // we want to avoid a false positive when we ask for the pool just after // a tx is removed from the pool due to being found in a new block, but // just before the block is visible by refresh. So we keep a boolean, so // that the first time we don't see the tx, we set that boolean, and only // delete it the second time it is checked (but only when refreshed, so // we're sure we've seen the blockchain state first) if (pit->second.m_state == wallet2::unconfirmed_transfer_details::pending) { LOG_PRINT_L1("Pending txid " << txid << " not in pool, marking as not in pool"); pit->second.m_state = wallet2::unconfirmed_transfer_details::pending_not_in_pool; } else if (pit->second.m_state == wallet2::unconfirmed_transfer_details::pending_not_in_pool && refreshed) { LOG_PRINT_L1("Pending txid " << txid << " not in pool, marking as failed"); pit->second.m_state = wallet2::unconfirmed_transfer_details::failed; // the inputs aren't spent anymore, since the tx failed for (size_t vini = 0; vini < pit->second.m_tx.vin.size(); ++vini) { if (pit->second.m_tx.vin[vini].type() == typeid(txin_to_key)) { txin_to_key &tx_in_to_key = boost::get(pit->second.m_tx.vin[vini]); for (size_t i = 0; i < m_transfers.size(); ++i) { const transfer_details &td = m_transfers[i]; if (td.m_key_image == tx_in_to_key.k_image) { LOG_PRINT_L1("Resetting spent status for output " << vini << ": " << td.m_key_image); set_unspent(i); break; } } } } } } } MDEBUG("update_pool_state done first loop"); // remove pool txes to us that aren't in the pool anymore // but only if we just refreshed, so that the tx can go in // the in transfers list instead (or nowhere if it just // disappeared without being mined) if (refreshed) remove_obsolete_pool_txs(res.tx_hashes); MDEBUG("update_pool_state done second loop"); // gather txids of new pool txes to us std::vector> txids; for (const auto &txid: res.tx_hashes) { bool txid_found_in_up = false; for (const auto &up: m_unconfirmed_payments) { if (up.second.m_pd.m_tx_hash == txid) { txid_found_in_up = true; break; } } if (m_scanned_pool_txs[0].find(txid) != m_scanned_pool_txs[0].end() || m_scanned_pool_txs[1].find(txid) != m_scanned_pool_txs[1].end()) { // if it's for us, we want to keep track of whether we saw a double spend, so don't bail out if (!txid_found_in_up) { LOG_PRINT_L2("Already seen " << txid << ", and not for us, skipped"); continue; } } if (!txid_found_in_up) { LOG_PRINT_L1("Found new pool tx: " << txid); bool found = false; for (const auto &i: m_unconfirmed_txs) { if (i.first == txid) { found = true; // if this is a payment to yourself at a different subaddress account, don't skip it // so that you can see the incoming pool tx with 'show_transfers' on that receiving subaddress account const unconfirmed_transfer_details& utd = i.second; for (const auto& dst : utd.m_dests) { auto subaddr_index = m_subaddresses.find(dst.addr.m_spend_public_key); if (subaddr_index != m_subaddresses.end() && subaddr_index->second.major != utd.m_subaddr_account) { found = false; break; } } break; } } if (!found) { // not one of those we sent ourselves txids.push_back({txid, false}); } else { LOG_PRINT_L1("We sent that one"); } } else { LOG_PRINT_L1("Already saw that one, it's for us"); txids.push_back({txid, true}); } } // get those txes if (!txids.empty()) { cryptonote::COMMAND_RPC_GET_TRANSACTIONS::request req; cryptonote::COMMAND_RPC_GET_TRANSACTIONS::response res; for (const auto &p: txids) req.txs_hashes.push_back(epee::string_tools::pod_to_hex(p.first)); MDEBUG("asking for " << txids.size() << " transactions"); req.decode_as_json = false; m_daemon_rpc_mutex.lock(); bool r = epee::net_utils::invoke_http_json("/gettransactions", req, res, m_http_client, rpc_timeout); m_daemon_rpc_mutex.unlock(); MDEBUG("Got " << r << " and " << res.status); if (r && res.status == CORE_RPC_STATUS_OK) { if (res.txs.size() == txids.size()) { for (const auto &tx_entry: res.txs) { if (tx_entry.in_pool) { cryptonote::transaction tx; cryptonote::blobdata bd; crypto::hash tx_hash, tx_prefix_hash; if (epee::string_tools::parse_hexstr_to_binbuff(tx_entry.as_hex, bd)) { if (cryptonote::parse_and_validate_tx_from_blob(bd, tx, tx_hash, tx_prefix_hash)) { const std::vector>::const_iterator i = std::find_if(txids.begin(), txids.end(), [tx_hash](const std::pair &e) { return e.first == tx_hash; }); if (i != txids.end()) { process_new_transaction(tx_hash, tx, std::vector(), 0, time(NULL), false, true, tx_entry.double_spend_seen); m_scanned_pool_txs[0].insert(tx_hash); if (m_scanned_pool_txs[0].size() > 5000) { std::swap(m_scanned_pool_txs[0], m_scanned_pool_txs[1]); m_scanned_pool_txs[0].clear(); } } else { MERROR("Got txid " << tx_hash << " which we did not ask for"); } } else { LOG_PRINT_L0("failed to validate transaction from daemon"); } } else { LOG_PRINT_L0("Failed to parse transaction from daemon"); } } else { LOG_PRINT_L1("Transaction from daemon was in pool, but is no more"); } } } else { LOG_PRINT_L0("Expected " << txids.size() << " tx(es), got " << res.txs.size()); } } else { LOG_PRINT_L0("Error calling gettransactions daemon RPC: r " << r << ", status " << res.status); } } MDEBUG("update_pool_state end"); } //---------------------------------------------------------------------------------------------------- void wallet2::fast_refresh(uint64_t stop_height, uint64_t &blocks_start_height, std::list &short_chain_history) { std::list hashes; const uint64_t checkpoint_height = m_checkpoints.get_max_height(); if (stop_height > checkpoint_height && m_blockchain.size()-1 < checkpoint_height) { // we will drop all these, so don't bother getting them uint64_t missing_blocks = m_checkpoints.get_max_height() - m_blockchain.size(); while (missing_blocks-- > 0) m_blockchain.push_back(crypto::null_hash); // maybe a bit suboptimal, but deque won't do huge reallocs like vector m_blockchain.push_back(m_checkpoints.get_points().at(checkpoint_height)); m_local_bc_height = m_blockchain.size(); short_chain_history.clear(); get_short_chain_history(short_chain_history); } size_t current_index = m_blockchain.size(); while(m_run.load(std::memory_order_relaxed) && current_index < stop_height) { pull_hashes(0, blocks_start_height, short_chain_history, hashes); if (hashes.size() <= 3) return; if (hashes.size() + current_index < stop_height) { drop_from_short_history(short_chain_history, 3); std::list::iterator right = hashes.end(); // prepend 3 more for (int i = 0; i<3; i++) { right--; short_chain_history.push_front(*right); } } current_index = blocks_start_height; for(auto& bl_id: hashes) { if(current_index >= m_blockchain.size()) { if (!(current_index % 1000)) LOG_PRINT_L2( "Skipped block by height: " << current_index); m_blockchain.push_back(bl_id); ++m_local_bc_height; if (0 != m_callback) { // FIXME: this isn't right, but simplewallet just logs that we got a block. cryptonote::block dummy; m_callback->on_new_block(current_index, dummy); } } else if(bl_id != m_blockchain[current_index]) { //split detected here !!! return; } ++current_index; if (current_index >= stop_height) return; } } } bool wallet2::add_address_book_row(const cryptonote::account_public_address &address, const crypto::hash &payment_id, const std::string &description, bool is_subaddress) { wallet2::address_book_row a; a.m_address = address; a.m_payment_id = payment_id; a.m_description = description; a.m_is_subaddress = is_subaddress; auto old_size = m_address_book.size(); m_address_book.push_back(a); if(m_address_book.size() == old_size+1) return true; return false; } bool wallet2::delete_address_book_row(std::size_t row_id) { if(m_address_book.size() <= row_id) return false; m_address_book.erase(m_address_book.begin()+row_id); return true; } //---------------------------------------------------------------------------------------------------- void wallet2::refresh(uint64_t start_height, uint64_t & blocks_fetched, bool& received_money) { if(m_light_wallet) { // MyMonero get_address_info needs to be called occasionally to trigger wallet sync. // This call is not really needed for other purposes and can be removed if mymonero changes their backend. cryptonote::COMMAND_RPC_GET_ADDRESS_INFO::response res; // Get basic info if(light_wallet_get_address_info(res)) { // Last stored block height uint64_t prev_height = m_light_wallet_blockchain_height; // Update lw heights m_light_wallet_scanned_block_height = res.scanned_block_height; m_light_wallet_blockchain_height = res.blockchain_height; m_local_bc_height = res.blockchain_height; // If new height - call new_block callback if(m_light_wallet_blockchain_height != prev_height) { MDEBUG("new block since last time!"); m_callback->on_lw_new_block(m_light_wallet_blockchain_height - 1); } m_light_wallet_connected = true; MDEBUG("lw scanned block height: " << m_light_wallet_scanned_block_height); MDEBUG("lw blockchain height: " << m_light_wallet_blockchain_height); MDEBUG(m_light_wallet_blockchain_height-m_light_wallet_scanned_block_height << " blocks behind"); // TODO: add wallet created block info light_wallet_get_address_txs(); } else m_light_wallet_connected = false; // Lighwallet refresh done return; } received_money = false; blocks_fetched = 0; uint64_t added_blocks = 0; size_t try_count = 0; crypto::hash last_tx_hash_id = m_transfers.size() ? m_transfers.back().m_txid : null_hash; std::list short_chain_history; tools::threadpool& tpool = tools::threadpool::getInstance(); tools::threadpool::waiter waiter; uint64_t blocks_start_height; std::list blocks; std::vector o_indices; bool refreshed = false; // pull the first set of blocks get_short_chain_history(short_chain_history); m_run.store(true, std::memory_order_relaxed); if (start_height > m_blockchain.size() || m_refresh_from_block_height > m_blockchain.size()) { if (!start_height) start_height = m_refresh_from_block_height; // we can shortcut by only pulling hashes up to the start_height fast_refresh(start_height, blocks_start_height, short_chain_history); // regenerate the history now that we've got a full set of hashes short_chain_history.clear(); get_short_chain_history(short_chain_history); start_height = 0; // and then fall through to regular refresh processing } // If stop() is called during fast refresh we don't need to continue if(!m_run.load(std::memory_order_relaxed)) return; pull_blocks(start_height, blocks_start_height, short_chain_history, blocks, o_indices); // always reset start_height to 0 to force short_chain_ history to be used on // subsequent pulls in this refresh. start_height = 0; while(m_run.load(std::memory_order_relaxed)) { try { // pull the next set of blocks while we're processing the current one uint64_t next_blocks_start_height; std::list next_blocks; std::vector next_o_indices; bool error = false; tpool.submit(&waiter, [&]{pull_next_blocks(start_height, next_blocks_start_height, short_chain_history, blocks, next_blocks, next_o_indices, error);}); process_blocks(blocks_start_height, blocks, o_indices, added_blocks); blocks_fetched += added_blocks; waiter.wait(); if(blocks_start_height == next_blocks_start_height) { m_node_rpc_proxy.set_height(m_blockchain.size()); refreshed = true; break; } // switch to the new blocks from the daemon blocks_start_height = next_blocks_start_height; blocks = next_blocks; o_indices = next_o_indices; // handle error from async fetching thread if (error) { throw std::runtime_error("proxy exception in refresh thread"); } } catch (const std::exception&) { blocks_fetched += added_blocks; waiter.wait(); if(try_count < 3) { LOG_PRINT_L1("Another try pull_blocks (try_count=" << try_count << ")..."); ++try_count; } else { LOG_ERROR("pull_blocks failed, try_count=" << try_count); throw; } } } if(last_tx_hash_id != (m_transfers.size() ? m_transfers.back().m_txid : null_hash)) received_money = true; try { // If stop() is called we don't need to check pending transactions if(m_run.load(std::memory_order_relaxed)) update_pool_state(refreshed); } catch (...) { LOG_PRINT_L1("Failed to check pending transactions"); } LOG_PRINT_L1("Refresh done, blocks received: " << blocks_fetched << ", balance (all accounts): " << print_money(balance_all()) << ", unlocked: " << print_money(unlocked_balance_all())); } //---------------------------------------------------------------------------------------------------- bool wallet2::refresh(uint64_t & blocks_fetched, bool& received_money, bool& ok) { try { refresh(0, blocks_fetched, received_money); ok = true; } catch (...) { ok = false; } return ok; } //---------------------------------------------------------------------------------------------------- void wallet2::detach_blockchain(uint64_t height) { LOG_PRINT_L0("Detaching blockchain on height " << height); // size 1 2 3 4 5 6 7 8 9 // block 0 1 2 3 4 5 6 7 8 // C THROW_WALLET_EXCEPTION_IF(height <= m_checkpoints.get_max_height() && m_blockchain.size() > m_checkpoints.get_max_height(), error::wallet_internal_error, "Daemon claims reorg below last checkpoint"); size_t transfers_detached = 0; for (size_t i = 0; i < m_transfers.size(); ++i) { wallet2::transfer_details &td = m_transfers[i]; if (td.m_spent && td.m_spent_height >= height) { LOG_PRINT_L1("Resetting spent status for output " << i << ": " << td.m_key_image); set_unspent(i); } } auto it = std::find_if(m_transfers.begin(), m_transfers.end(), [&](const transfer_details& td){return td.m_block_height >= height;}); size_t i_start = it - m_transfers.begin(); for(size_t i = i_start; i!= m_transfers.size();i++) { if (!m_transfers[i].m_key_image_known || m_transfers[i].m_key_image_partial) continue; auto it_ki = m_key_images.find(m_transfers[i].m_key_image); THROW_WALLET_EXCEPTION_IF(it_ki == m_key_images.end(), error::wallet_internal_error, "key image not found: index " + std::to_string(i) + ", ki " + epee::string_tools::pod_to_hex(m_transfers[i].m_key_image) + ", " + std::to_string(m_key_images.size()) + " key images known"); m_key_images.erase(it_ki); } for(size_t i = i_start; i!= m_transfers.size();i++) { auto it_pk = m_pub_keys.find(m_transfers[i].get_public_key()); THROW_WALLET_EXCEPTION_IF(it_pk == m_pub_keys.end(), error::wallet_internal_error, "public key not found"); m_pub_keys.erase(it_pk); } m_transfers.erase(it, m_transfers.end()); size_t blocks_detached = m_blockchain.size() - height; m_blockchain.crop(height); m_local_bc_height -= blocks_detached; for (auto it = m_payments.begin(); it != m_payments.end(); ) { if(height <= it->second.m_block_height) it = m_payments.erase(it); else ++it; } for (auto it = m_confirmed_txs.begin(); it != m_confirmed_txs.end(); ) { if(height <= it->second.m_block_height) it = m_confirmed_txs.erase(it); else ++it; } LOG_PRINT_L0("Detached blockchain on height " << height << ", transfers detached " << transfers_detached << ", blocks detached " << blocks_detached); } //---------------------------------------------------------------------------------------------------- bool wallet2::deinit() { m_is_initialized=false; return true; } //---------------------------------------------------------------------------------------------------- bool wallet2::clear() { m_blockchain.clear(); m_transfers.clear(); m_key_images.clear(); m_pub_keys.clear(); m_unconfirmed_txs.clear(); m_payments.clear(); m_tx_keys.clear(); m_additional_tx_keys.clear(); m_confirmed_txs.clear(); m_unconfirmed_payments.clear(); m_scanned_pool_txs[0].clear(); m_scanned_pool_txs[1].clear(); m_address_book.clear(); m_local_bc_height = 1; m_subaddresses.clear(); m_subaddresses_inv.clear(); m_subaddress_labels.clear(); return true; } /*! * \brief Stores wallet information to wallet file. * \param keys_file_name Name of wallet file * \param password Password of wallet file * \param watch_only true to save only view key, false to save both spend and view keys * \return Whether it was successful. */ bool wallet2::store_keys(const std::string& keys_file_name, const epee::wipeable_string& password, bool watch_only) { std::string account_data; std::string multisig_signers; cryptonote::account_base account = m_account; if (watch_only) account.forget_spend_key(); bool r = epee::serialization::store_t_to_binary(account, account_data); CHECK_AND_ASSERT_MES(r, false, "failed to serialize wallet keys"); wallet2::keys_file_data keys_file_data = boost::value_initialized(); // Create a JSON object with "key_data" and "seed_language" as keys. rapidjson::Document json; json.SetObject(); rapidjson::Value value(rapidjson::kStringType); value.SetString(account_data.c_str(), account_data.length()); json.AddMember("key_data", value, json.GetAllocator()); if (!seed_language.empty()) { value.SetString(seed_language.c_str(), seed_language.length()); json.AddMember("seed_language", value, json.GetAllocator()); } rapidjson::Value value2(rapidjson::kNumberType); value2.SetInt(watch_only ? 1 :0); // WTF ? JSON has different true and false types, and not boolean ?? json.AddMember("watch_only", value2, json.GetAllocator()); value2.SetInt(m_multisig ? 1 :0); json.AddMember("multisig", value2, json.GetAllocator()); value2.SetUint(m_multisig_threshold); json.AddMember("multisig_threshold", value2, json.GetAllocator()); if (m_multisig) { bool r = ::serialization::dump_binary(m_multisig_signers, multisig_signers); CHECK_AND_ASSERT_MES(r, false, "failed to serialize wallet multisig signers"); value.SetString(multisig_signers.c_str(), multisig_signers.length()); json.AddMember("multisig_signers", value, json.GetAllocator()); } value2.SetInt(m_always_confirm_transfers ? 1 :0); json.AddMember("always_confirm_transfers", value2, json.GetAllocator()); value2.SetInt(m_print_ring_members ? 1 :0); json.AddMember("print_ring_members", value2, json.GetAllocator()); value2.SetInt(m_store_tx_info ? 1 :0); json.AddMember("store_tx_info", value2, json.GetAllocator()); value2.SetUint(m_default_mixin); json.AddMember("default_mixin", value2, json.GetAllocator()); value2.SetUint(m_default_priority); json.AddMember("default_priority", value2, json.GetAllocator()); value2.SetInt(m_auto_refresh ? 1 :0); json.AddMember("auto_refresh", value2, json.GetAllocator()); value2.SetInt(m_refresh_type); json.AddMember("refresh_type", value2, json.GetAllocator()); value2.SetUint64(m_refresh_from_block_height); json.AddMember("refresh_height", value2, json.GetAllocator()); value2.SetInt(m_confirm_missing_payment_id ? 1 :0); json.AddMember("confirm_missing_payment_id", value2, json.GetAllocator()); value2.SetInt(m_ask_password ? 1 :0); json.AddMember("ask_password", value2, json.GetAllocator()); value2.SetUint(m_min_output_count); json.AddMember("min_output_count", value2, json.GetAllocator()); value2.SetUint64(m_min_output_value); json.AddMember("min_output_value", value2, json.GetAllocator()); value2.SetInt(cryptonote::get_default_decimal_point()); json.AddMember("default_decimal_point", value2, json.GetAllocator()); value2.SetInt(m_merge_destinations ? 1 :0); json.AddMember("merge_destinations", value2, json.GetAllocator()); value2.SetInt(m_confirm_backlog ? 1 :0); json.AddMember("confirm_backlog", value2, json.GetAllocator()); value2.SetUint(m_confirm_backlog_threshold); json.AddMember("confirm_backlog_threshold", value2, json.GetAllocator()); value2.SetInt(m_testnet ? 1 :0); json.AddMember("testnet", value2, json.GetAllocator()); // Serialize the JSON object rapidjson::StringBuffer buffer; rapidjson::Writer writer(buffer); json.Accept(writer); account_data = buffer.GetString(); // Encrypt the entire JSON object. crypto::chacha_key key; crypto::generate_chacha_key(password.data(), password.size(), key); std::string cipher; cipher.resize(account_data.size()); keys_file_data.iv = crypto::rand(); crypto::chacha20(account_data.data(), account_data.size(), key, keys_file_data.iv, &cipher[0]); keys_file_data.account_data = cipher; std::string buf; r = ::serialization::dump_binary(keys_file_data, buf); r = r && epee::file_io_utils::save_string_to_file(keys_file_name, buf); //and never touch wallet_keys_file again, only read CHECK_AND_ASSERT_MES(r, false, "failed to generate wallet keys file " << keys_file_name); return true; } //---------------------------------------------------------------------------------------------------- namespace { bool verify_keys(const crypto::secret_key& sec, const crypto::public_key& expected_pub) { crypto::public_key pub; bool r = crypto::secret_key_to_public_key(sec, pub); return r && expected_pub == pub; } } /*! * \brief Load wallet information from wallet file. * \param keys_file_name Name of wallet file * \param password Password of wallet file */ bool wallet2::load_keys(const std::string& keys_file_name, const epee::wipeable_string& password) { rapidjson::Document json; wallet2::keys_file_data keys_file_data; std::string buf; bool r = epee::file_io_utils::load_file_to_string(keys_file_name, buf); THROW_WALLET_EXCEPTION_IF(!r, error::file_read_error, keys_file_name); // Decrypt the contents r = ::serialization::parse_binary(buf, keys_file_data); THROW_WALLET_EXCEPTION_IF(!r, error::wallet_internal_error, "internal error: failed to deserialize \"" + keys_file_name + '\"'); crypto::chacha_key key; crypto::generate_chacha_key(password.data(), password.size(), key); std::string account_data; account_data.resize(keys_file_data.account_data.size()); crypto::chacha20(keys_file_data.account_data.data(), keys_file_data.account_data.size(), key, keys_file_data.iv, &account_data[0]); if (json.Parse(account_data.c_str()).HasParseError() || !json.IsObject()) crypto::chacha8(keys_file_data.account_data.data(), keys_file_data.account_data.size(), key, keys_file_data.iv, &account_data[0]); // The contents should be JSON if the wallet follows the new format. if (json.Parse(account_data.c_str()).HasParseError()) { is_old_file_format = true; m_watch_only = false; m_multisig = false; m_multisig_threshold = 0; m_multisig_signers.clear(); m_always_confirm_transfers = false; m_print_ring_members = false; m_default_mixin = 0; m_default_priority = 0; m_auto_refresh = true; m_refresh_type = RefreshType::RefreshDefault; m_confirm_missing_payment_id = true; m_ask_password = true; m_min_output_count = 0; m_min_output_value = 0; m_merge_destinations = false; m_confirm_backlog = true; m_confirm_backlog_threshold = 0; } else if(json.IsObject()) { if (!json.HasMember("key_data")) { LOG_ERROR("Field key_data not found in JSON"); return false; } if (!json["key_data"].IsString()) { LOG_ERROR("Field key_data found in JSON, but not String"); return false; } const char *field_key_data = json["key_data"].GetString(); account_data = std::string(field_key_data, field_key_data + json["key_data"].GetStringLength()); GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, seed_language, std::string, String, false, std::string()); if (field_seed_language_found) { set_seed_language(field_seed_language); } GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, watch_only, int, Int, false, false); m_watch_only = field_watch_only; GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, multisig, int, Int, false, false); m_multisig = field_multisig; GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, multisig_threshold, unsigned int, Uint, m_multisig, 0); m_multisig_threshold = field_multisig_threshold; if (m_multisig) { if (!json.HasMember("multisig_signers")) { LOG_ERROR("Field multisig_signers not found in JSON"); return false; } if (!json["multisig_signers"].IsString()) { LOG_ERROR("Field multisig_signers found in JSON, but not String"); return false; } const char *field_multisig_signers = json["multisig_signers"].GetString(); std::string multisig_signers = std::string(field_multisig_signers, field_multisig_signers + json["multisig_signers"].GetStringLength()); r = ::serialization::parse_binary(multisig_signers, m_multisig_signers); if (!r) { LOG_ERROR("Field multisig_signers found in JSON, but failed to parse"); return false; } } GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, always_confirm_transfers, int, Int, false, true); m_always_confirm_transfers = field_always_confirm_transfers; GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, print_ring_members, int, Int, false, true); m_print_ring_members = field_print_ring_members; GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, store_tx_keys, int, Int, false, true); GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, store_tx_info, int, Int, false, true); m_store_tx_info = ((field_store_tx_keys != 0) || (field_store_tx_info != 0)); GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, default_mixin, unsigned int, Uint, false, 0); m_default_mixin = field_default_mixin; GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, default_priority, unsigned int, Uint, false, 0); if (field_default_priority_found) { m_default_priority = field_default_priority; } else { GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, default_fee_multiplier, unsigned int, Uint, false, 0); if (field_default_fee_multiplier_found) m_default_priority = field_default_fee_multiplier; else m_default_priority = 0; } GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, auto_refresh, int, Int, false, true); m_auto_refresh = field_auto_refresh; GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, refresh_type, int, Int, false, RefreshType::RefreshDefault); m_refresh_type = RefreshType::RefreshDefault; if (field_refresh_type_found) { if (field_refresh_type == RefreshFull || field_refresh_type == RefreshOptimizeCoinbase || field_refresh_type == RefreshNoCoinbase) m_refresh_type = (RefreshType)field_refresh_type; else LOG_PRINT_L0("Unknown refresh-type value (" << field_refresh_type << "), using default"); } GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, refresh_height, uint64_t, Uint64, false, 0); m_refresh_from_block_height = field_refresh_height; GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, confirm_missing_payment_id, int, Int, false, true); m_confirm_missing_payment_id = field_confirm_missing_payment_id; GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, ask_password, int, Int, false, true); m_ask_password = field_ask_password; GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, default_decimal_point, int, Int, false, CRYPTONOTE_DISPLAY_DECIMAL_POINT); cryptonote::set_default_decimal_point(field_default_decimal_point); GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, min_output_count, uint32_t, Uint, false, 0); m_min_output_count = field_min_output_count; GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, min_output_value, uint64_t, Uint64, false, 0); m_min_output_value = field_min_output_value; GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, merge_destinations, int, Int, false, false); m_merge_destinations = field_merge_destinations; GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, confirm_backlog, int, Int, false, true); m_confirm_backlog = field_confirm_backlog; GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, confirm_backlog_threshold, uint32_t, Uint, false, 0); m_confirm_backlog_threshold = field_confirm_backlog_threshold; GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, testnet, int, Int, false, m_testnet); // Wallet is being opened with testnet flag, but is saved as a mainnet wallet THROW_WALLET_EXCEPTION_IF(m_testnet && !field_testnet, error::wallet_internal_error, "Mainnet wallet can not be opened as testnet wallet"); // Wallet is being opened without testnet flag but is saved as a testnet wallet. THROW_WALLET_EXCEPTION_IF(!m_testnet && field_testnet, error::wallet_internal_error, "Testnet wallet can not be opened as mainnet wallet"); } else { THROW_WALLET_EXCEPTION(error::wallet_internal_error, "invalid password"); return false; } const cryptonote::account_keys& keys = m_account.get_keys(); r = epee::serialization::load_t_from_binary(m_account, account_data); r = r && verify_keys(keys.m_view_secret_key, keys.m_account_address.m_view_public_key); if(!m_watch_only && !m_multisig) r = r && verify_keys(keys.m_spend_secret_key, keys.m_account_address.m_spend_public_key); THROW_WALLET_EXCEPTION_IF(!r, error::invalid_password); return true; } /*! * \brief verify password for default wallet keys file. * \param password Password to verify * \return true if password is correct * * for verification only * should not mutate state, unlike load_keys() * can be used prior to rewriting wallet keys file, to ensure user has entered the correct password * */ bool wallet2::verify_password(const epee::wipeable_string& password) const { return verify_password(m_keys_file, password, m_watch_only || m_multisig); } /*! * \brief verify password for specified wallet keys file. * \param keys_file_name Keys file to verify password for * \param password Password to verify * \param no_spend_key If set = only verify view keys, otherwise also spend keys * \return true if password is correct * * for verification only * should not mutate state, unlike load_keys() * can be used prior to rewriting wallet keys file, to ensure user has entered the correct password * */ bool wallet2::verify_password(const std::string& keys_file_name, const epee::wipeable_string& password, bool no_spend_key) { rapidjson::Document json; wallet2::keys_file_data keys_file_data; std::string buf; bool r = epee::file_io_utils::load_file_to_string(keys_file_name, buf); THROW_WALLET_EXCEPTION_IF(!r, error::file_read_error, keys_file_name); // Decrypt the contents r = ::serialization::parse_binary(buf, keys_file_data); THROW_WALLET_EXCEPTION_IF(!r, error::wallet_internal_error, "internal error: failed to deserialize \"" + keys_file_name + '\"'); crypto::chacha_key key; crypto::generate_chacha_key(password.data(), password.size(), key); std::string account_data; account_data.resize(keys_file_data.account_data.size()); crypto::chacha20(keys_file_data.account_data.data(), keys_file_data.account_data.size(), key, keys_file_data.iv, &account_data[0]); if (json.Parse(account_data.c_str()).HasParseError() || !json.IsObject()) crypto::chacha8(keys_file_data.account_data.data(), keys_file_data.account_data.size(), key, keys_file_data.iv, &account_data[0]); // The contents should be JSON if the wallet follows the new format. if (json.Parse(account_data.c_str()).HasParseError()) { // old format before JSON wallet key file format } else { account_data = std::string(json["key_data"].GetString(), json["key_data"].GetString() + json["key_data"].GetStringLength()); } cryptonote::account_base account_data_check; r = epee::serialization::load_t_from_binary(account_data_check, account_data); const cryptonote::account_keys& keys = account_data_check.get_keys(); r = r && verify_keys(keys.m_view_secret_key, keys.m_account_address.m_view_public_key); if(!no_spend_key) r = r && verify_keys(keys.m_spend_secret_key, keys.m_account_address.m_spend_public_key); return r; } /*! * \brief Generates a wallet or restores one. * \param wallet_ Name of wallet file * \param password Password of wallet file * \param multisig_data The multisig restore info and keys */ void wallet2::generate(const std::string& wallet_, const epee::wipeable_string& password, const std::string& multisig_data) { clear(); prepare_file_names(wallet_); if (!wallet_.empty()) { boost::system::error_code ignored_ec; THROW_WALLET_EXCEPTION_IF(boost::filesystem::exists(m_wallet_file, ignored_ec), error::file_exists, m_wallet_file); THROW_WALLET_EXCEPTION_IF(boost::filesystem::exists(m_keys_file, ignored_ec), error::file_exists, m_keys_file); } m_account.generate(rct::rct2sk(rct::zero()), true, false); THROW_WALLET_EXCEPTION_IF(multisig_data.size() < 32, error::invalid_multisig_seed); size_t offset = 0; uint32_t threshold = *(uint32_t*)(multisig_data.data() + offset); offset += sizeof(uint32_t); uint32_t total = *(uint32_t*)(multisig_data.data() + offset); offset += sizeof(uint32_t); THROW_WALLET_EXCEPTION_IF(threshold < 2, error::invalid_multisig_seed); THROW_WALLET_EXCEPTION_IF(total != threshold && total != threshold + 1, error::invalid_multisig_seed); const size_t n_multisig_keys = total == threshold ? 1 : threshold; THROW_WALLET_EXCEPTION_IF(multisig_data.size() != 8 + 32 * (4 + n_multisig_keys + total), error::invalid_multisig_seed); std::vector multisig_keys; std::vector multisig_signers; crypto::secret_key spend_secret_key = *(crypto::secret_key*)(multisig_data.data() + offset); offset += sizeof(crypto::secret_key); crypto::public_key spend_public_key = *(crypto::public_key*)(multisig_data.data() + offset); offset += sizeof(crypto::public_key); crypto::secret_key view_secret_key = *(crypto::secret_key*)(multisig_data.data() + offset); offset += sizeof(crypto::secret_key); crypto::public_key view_public_key = *(crypto::public_key*)(multisig_data.data() + offset); offset += sizeof(crypto::public_key); for (size_t n = 0; n < n_multisig_keys; ++n) { multisig_keys.push_back(*(crypto::secret_key*)(multisig_data.data() + offset)); offset += sizeof(crypto::secret_key); } for (size_t n = 0; n < total; ++n) { multisig_signers.push_back(*(crypto::public_key*)(multisig_data.data() + offset)); offset += sizeof(crypto::public_key); } crypto::public_key calculated_view_public_key; THROW_WALLET_EXCEPTION_IF(!crypto::secret_key_to_public_key(view_secret_key, calculated_view_public_key), error::invalid_multisig_seed); THROW_WALLET_EXCEPTION_IF(view_public_key != calculated_view_public_key, error::invalid_multisig_seed); crypto::public_key local_signer; THROW_WALLET_EXCEPTION_IF(!crypto::secret_key_to_public_key(spend_secret_key, local_signer), error::invalid_multisig_seed); THROW_WALLET_EXCEPTION_IF(std::find(multisig_signers.begin(), multisig_signers.end(), local_signer) == multisig_signers.end(), error::invalid_multisig_seed); rct::key skey = rct::zero(); for (const auto &msk: multisig_keys) sc_add(skey.bytes, skey.bytes, rct::sk2rct(msk).bytes); THROW_WALLET_EXCEPTION_IF(!(rct::rct2sk(skey) == spend_secret_key), error::invalid_multisig_seed); m_account.make_multisig(view_secret_key, spend_secret_key, spend_public_key, multisig_keys); m_account.finalize_multisig(spend_public_key); m_account_public_address = m_account.get_keys().m_account_address; m_watch_only = false; m_multisig = true; m_multisig_threshold = threshold; m_multisig_signers = multisig_signers; if (!wallet_.empty()) { bool r = store_keys(m_keys_file, password, false); THROW_WALLET_EXCEPTION_IF(!r, error::file_save_error, m_keys_file); r = file_io_utils::save_string_to_file(m_wallet_file + ".address.txt", m_account.get_public_address_str(m_testnet)); if(!r) MERROR("String with address text not saved"); } cryptonote::block b; generate_genesis(b); m_blockchain.push_back(get_block_hash(b)); add_subaddress_account(tr("Primary account")); if (!wallet_.empty()) store(); } /*! * \brief Generates a wallet or restores one. * \param wallet_ Name of wallet file * \param password Password of wallet file * \param recovery_param If it is a restore, the recovery key * \param recover Whether it is a restore * \param two_random Whether it is a non-deterministic wallet * \return The secret key of the generated wallet */ crypto::secret_key wallet2::generate(const std::string& wallet_, const epee::wipeable_string& password, const crypto::secret_key& recovery_param, bool recover, bool two_random) { clear(); prepare_file_names(wallet_); if (!wallet_.empty()) { boost::system::error_code ignored_ec; THROW_WALLET_EXCEPTION_IF(boost::filesystem::exists(m_wallet_file, ignored_ec), error::file_exists, m_wallet_file); THROW_WALLET_EXCEPTION_IF(boost::filesystem::exists(m_keys_file, ignored_ec), error::file_exists, m_keys_file); } crypto::secret_key retval = m_account.generate(recovery_param, recover, two_random); m_account_public_address = m_account.get_keys().m_account_address; m_watch_only = false; m_multisig = false; m_multisig_threshold = 0; m_multisig_signers.clear(); // -1 month for fluctuations in block time and machine date/time setup. // avg seconds per block const int seconds_per_block = DIFFICULTY_TARGET_V2; // ~num blocks per month const uint64_t blocks_per_month = 60*60*24*30/seconds_per_block; // try asking the daemon first if(m_refresh_from_block_height == 0 && !recover){ uint64_t height = estimate_blockchain_height(); m_refresh_from_block_height = height >= blocks_per_month ? height - blocks_per_month : 0; } if (!wallet_.empty()) { bool r = store_keys(m_keys_file, password, false); THROW_WALLET_EXCEPTION_IF(!r, error::file_save_error, m_keys_file); r = file_io_utils::save_string_to_file(m_wallet_file + ".address.txt", m_account.get_public_address_str(m_testnet)); if(!r) MERROR("String with address text not saved"); } cryptonote::block b; generate_genesis(b); m_blockchain.push_back(get_block_hash(b)); add_subaddress_account(tr("Primary account")); if (!wallet_.empty()) store(); return retval; } uint64_t wallet2::estimate_blockchain_height() { // -1 month for fluctuations in block time and machine date/time setup. // avg seconds per block const int seconds_per_block = DIFFICULTY_TARGET_V2; // ~num blocks per month const uint64_t blocks_per_month = 60*60*24*30/seconds_per_block; // try asking the daemon first std::string err; uint64_t height = 0; // we get the max of approximated height and known height // approximated height is the least of daemon target height // (the max of what the other daemons are claiming is their // height) and the theoretical height based on the local // clock. This will be wrong only if both the local clock // is bad *and* a peer daemon claims a highest height than // the real chain. // known height is the height the local daemon is currently // synced to, it will be lower than the real chain height if // the daemon is currently syncing. height = get_approximate_blockchain_height(); uint64_t target_height = get_daemon_blockchain_target_height(err); if (err.empty() && target_height < height) height = target_height; uint64_t local_height = get_daemon_blockchain_height(err); if (err.empty() && local_height > height) height = local_height; return height; } /*! * \brief Creates a watch only wallet from a public address and a view secret key. * \param wallet_ Name of wallet file * \param password Password of wallet file * \param viewkey view secret key */ void wallet2::generate(const std::string& wallet_, const epee::wipeable_string& password, const cryptonote::account_public_address &account_public_address, const crypto::secret_key& viewkey) { clear(); prepare_file_names(wallet_); if (!wallet_.empty()) { boost::system::error_code ignored_ec; THROW_WALLET_EXCEPTION_IF(boost::filesystem::exists(m_wallet_file, ignored_ec), error::file_exists, m_wallet_file); THROW_WALLET_EXCEPTION_IF(boost::filesystem::exists(m_keys_file, ignored_ec), error::file_exists, m_keys_file); } m_account.create_from_viewkey(account_public_address, viewkey); m_account_public_address = account_public_address; m_watch_only = true; m_multisig = false; m_multisig_threshold = 0; m_multisig_signers.clear(); if (!wallet_.empty()) { bool r = store_keys(m_keys_file, password, true); THROW_WALLET_EXCEPTION_IF(!r, error::file_save_error, m_keys_file); r = file_io_utils::save_string_to_file(m_wallet_file + ".address.txt", m_account.get_public_address_str(m_testnet)); if(!r) MERROR("String with address text not saved"); } cryptonote::block b; generate_genesis(b); m_blockchain.push_back(get_block_hash(b)); add_subaddress_account(tr("Primary account")); if (!wallet_.empty()) store(); } /*! * \brief Creates a wallet from a public address and a spend/view secret key pair. * \param wallet_ Name of wallet file * \param password Password of wallet file * \param spendkey spend secret key * \param viewkey view secret key */ void wallet2::generate(const std::string& wallet_, const epee::wipeable_string& password, const cryptonote::account_public_address &account_public_address, const crypto::secret_key& spendkey, const crypto::secret_key& viewkey) { clear(); prepare_file_names(wallet_); if (!wallet_.empty()) { boost::system::error_code ignored_ec; THROW_WALLET_EXCEPTION_IF(boost::filesystem::exists(m_wallet_file, ignored_ec), error::file_exists, m_wallet_file); THROW_WALLET_EXCEPTION_IF(boost::filesystem::exists(m_keys_file, ignored_ec), error::file_exists, m_keys_file); } m_account.create_from_keys(account_public_address, spendkey, viewkey); m_account_public_address = account_public_address; m_watch_only = false; m_multisig = false; m_multisig_threshold = 0; m_multisig_signers.clear(); if (!wallet_.empty()) { bool r = store_keys(m_keys_file, password, false); THROW_WALLET_EXCEPTION_IF(!r, error::file_save_error, m_keys_file); r = file_io_utils::save_string_to_file(m_wallet_file + ".address.txt", m_account.get_public_address_str(m_testnet)); if(!r) MERROR("String with address text not saved"); } cryptonote::block b; generate_genesis(b); m_blockchain.push_back(get_block_hash(b)); if (!wallet_.empty()) store(); } std::string wallet2::make_multisig(const epee::wipeable_string &password, const std::vector &view_keys, const std::vector &spend_keys, uint32_t threshold) { CHECK_AND_ASSERT_THROW_MES(!view_keys.empty(), "empty view keys"); CHECK_AND_ASSERT_THROW_MES(view_keys.size() == spend_keys.size(), "Mismatched view/spend key sizes"); CHECK_AND_ASSERT_THROW_MES(threshold > 1 && threshold <= spend_keys.size() + 1, "Invalid threshold"); CHECK_AND_ASSERT_THROW_MES(threshold == spend_keys.size() || threshold == spend_keys.size() + 1, "Unsupported threshold case"); std::string extra_multisig_info; crypto::hash hash; clear(); MINFO("Creating spend key..."); std::vector multisig_keys; rct::key spend_pkey, spend_skey; if (threshold == spend_keys.size() + 1) { cryptonote::generate_multisig_N_N(get_account().get_keys(), spend_keys, multisig_keys, spend_skey, spend_pkey); } else if (threshold == spend_keys.size()) { cryptonote::generate_multisig_N1_N(get_account().get_keys(), spend_keys, multisig_keys, spend_skey, spend_pkey); // We need an extra step, so we package all the composite public keys // we know about, and make a signed string out of them std::string data; crypto::public_key signer; CHECK_AND_ASSERT_THROW_MES(crypto::secret_key_to_public_key(rct::rct2sk(spend_skey), signer), "Failed to derive public spend key"); data += std::string((const char *)&signer, sizeof(crypto::public_key)); for (const auto &msk: multisig_keys) { rct::key pmsk = rct::scalarmultBase(rct::sk2rct(msk)); data += std::string((const char *)&pmsk, sizeof(crypto::public_key)); } data.resize(data.size() + sizeof(crypto::signature)); crypto::cn_fast_hash(data.data(), data.size() - sizeof(signature), hash); crypto::signature &signature = *(crypto::signature*)&data[data.size() - sizeof(crypto::signature)]; crypto::generate_signature(hash, signer, rct::rct2sk(spend_skey), signature); extra_multisig_info = std::string("MultisigxV1") + tools::base58::encode(data); } else { CHECK_AND_ASSERT_THROW_MES(false, "Unsupported threshold case"); } // the multisig view key is shared by all, make one all can derive MINFO("Creating view key..."); crypto::secret_key view_skey = cryptonote::generate_multisig_view_secret_key(get_account().get_keys().m_view_secret_key, view_keys); MINFO("Creating multisig address..."); CHECK_AND_ASSERT_THROW_MES(m_account.make_multisig(view_skey, rct::rct2sk(spend_skey), rct::rct2pk(spend_pkey), multisig_keys), "Failed to create multisig wallet due to bad keys"); m_account_public_address = m_account.get_keys().m_account_address; m_watch_only = false; m_multisig = true; m_multisig_threshold = threshold; if (threshold == spend_keys.size() + 1) { m_multisig_signers = spend_keys; m_multisig_signers.push_back(get_multisig_signer_public_key()); } else { m_multisig_signers = std::vector(spend_keys.size() + 1, crypto::null_pkey); } if (!m_wallet_file.empty()) { bool r = store_keys(m_keys_file, password, false); THROW_WALLET_EXCEPTION_IF(!r, error::file_save_error, m_keys_file); r = file_io_utils::save_string_to_file(m_wallet_file + ".address.txt", m_account.get_public_address_str(m_testnet)); if(!r) MERROR("String with address text not saved"); } cryptonote::block b; generate_genesis(b); m_blockchain.push_back(get_block_hash(b)); add_subaddress_account(tr("Primary account")); if (!m_wallet_file.empty()) store(); return extra_multisig_info; } std::string wallet2::make_multisig(const epee::wipeable_string &password, const std::vector &info, uint32_t threshold) { // parse all multisig info std::vector secret_keys(info.size()); std::vector public_keys(info.size()); for (size_t i = 0; i < info.size(); ++i) { THROW_WALLET_EXCEPTION_IF(!verify_multisig_info(info[i], secret_keys[i], public_keys[i]), error::wallet_internal_error, "Bad multisig info: " + info[i]); } // remove duplicates for (size_t i = 0; i < secret_keys.size(); ++i) { for (size_t j = i + 1; j < secret_keys.size(); ++j) { if (rct::sk2rct(secret_keys[i]) == rct::sk2rct(secret_keys[j])) { MDEBUG("Duplicate key found, ignoring"); secret_keys[j] = secret_keys.back(); public_keys[j] = public_keys.back(); secret_keys.pop_back(); public_keys.pop_back(); --j; } } } // people may include their own, weed it out const crypto::secret_key local_skey = cryptonote::get_multisig_blinded_secret_key(get_account().get_keys().m_view_secret_key); const crypto::public_key local_pkey = get_multisig_signer_public_key(get_account().get_keys().m_spend_secret_key); for (size_t i = 0; i < secret_keys.size(); ++i) { if (secret_keys[i] == local_skey) { MDEBUG("Local key is present, ignoring"); secret_keys[i] = secret_keys.back(); public_keys[i] = public_keys.back(); secret_keys.pop_back(); public_keys.pop_back(); --i; } else { THROW_WALLET_EXCEPTION_IF(public_keys[i] == local_pkey, error::wallet_internal_error, "Found local spend public key, but not local view secret key - something very weird"); } } return make_multisig(password, secret_keys, public_keys, threshold); } bool wallet2::finalize_multisig(const epee::wipeable_string &password, std::unordered_set pkeys, std::vector signers) { CHECK_AND_ASSERT_THROW_MES(!pkeys.empty(), "empty pkeys"); // add ours if not included crypto::public_key local_signer; CHECK_AND_ASSERT_THROW_MES(crypto::secret_key_to_public_key(get_account().get_keys().m_spend_secret_key, local_signer), "Failed to derive public spend key"); if (std::find(signers.begin(), signers.end(), local_signer) == signers.end()) { signers.push_back(local_signer); for (const auto &msk: get_account().get_multisig_keys()) { pkeys.insert(rct::rct2pk(rct::scalarmultBase(rct::sk2rct(msk)))); } } CHECK_AND_ASSERT_THROW_MES(signers.size() == m_multisig_signers.size(), "Bad signers size"); crypto::public_key spend_public_key = cryptonote::generate_multisig_N1_N_spend_public_key(std::vector(pkeys.begin(), pkeys.end())); m_account_public_address.m_spend_public_key = spend_public_key; m_account.finalize_multisig(spend_public_key); m_multisig_signers = signers; std::sort(m_multisig_signers.begin(), m_multisig_signers.end(), [](const crypto::public_key &e0, const crypto::public_key &e1){ return memcmp(&e0, &e1, sizeof(e0)); }); if (!m_wallet_file.empty()) { bool r = store_keys(m_keys_file, password, false); THROW_WALLET_EXCEPTION_IF(!r, error::file_save_error, m_keys_file); r = file_io_utils::save_string_to_file(m_wallet_file + ".address.txt", m_account.get_public_address_str(m_testnet)); if(!r) MERROR("String with address text not saved"); } m_subaddresses.clear(); m_subaddresses_inv.clear(); m_subaddress_labels.clear(); add_subaddress_account(tr("Primary account")); if (!m_wallet_file.empty()) store(); return true; } bool wallet2::finalize_multisig(const epee::wipeable_string &password, const std::vector &info) { // parse all multisig info std::unordered_set public_keys; std::vector signers(info.size(), crypto::null_pkey); for (size_t i = 0; i < info.size(); ++i) { if (!verify_extra_multisig_info(info[i], public_keys, signers[i])) { MERROR("Bad multisig info"); return false; } } return finalize_multisig(password, public_keys, signers); } std::string wallet2::get_multisig_info() const { // It's a signed package of private view key and public spend key const crypto::secret_key skey = cryptonote::get_multisig_blinded_secret_key(get_account().get_keys().m_view_secret_key); const crypto::public_key pkey = get_multisig_signer_public_key(get_account().get_keys().m_spend_secret_key); crypto::hash hash; std::string data; data += std::string((const char *)&skey, sizeof(crypto::secret_key)); data += std::string((const char *)&pkey, sizeof(crypto::public_key)); data.resize(data.size() + sizeof(crypto::signature)); crypto::cn_fast_hash(data.data(), data.size() - sizeof(signature), hash); crypto::signature &signature = *(crypto::signature*)&data[data.size() - sizeof(crypto::signature)]; crypto::generate_signature(hash, pkey, get_multisig_blinded_secret_key(get_account().get_keys().m_spend_secret_key), signature); return std::string("MultisigV1") + tools::base58::encode(data); } bool wallet2::verify_multisig_info(const std::string &data, crypto::secret_key &skey, crypto::public_key &pkey) { const size_t header_len = strlen("MultisigV1"); if (data.size() < header_len || data.substr(0, header_len) != "MultisigV1") { MERROR("Multisig info header check error"); return false; } std::string decoded; if (!tools::base58::decode(data.substr(header_len), decoded)) { MERROR("Multisig info decoding error"); return false; } if (decoded.size() != sizeof(crypto::secret_key) + sizeof(crypto::public_key) + sizeof(crypto::signature)) { MERROR("Multisig info is corrupt"); return false; } size_t offset = 0; skey = *(const crypto::secret_key*)(decoded.data() + offset); offset += sizeof(skey); pkey = *(const crypto::public_key*)(decoded.data() + offset); offset += sizeof(pkey); const crypto::signature &signature = *(const crypto::signature*)(decoded.data() + offset); crypto::hash hash; crypto::cn_fast_hash(decoded.data(), decoded.size() - sizeof(signature), hash); if (!crypto::check_signature(hash, pkey, signature)) { MERROR("Multisig info signature is invalid"); return false; } return true; } bool wallet2::verify_extra_multisig_info(const std::string &data, std::unordered_set &pkeys, crypto::public_key &signer) { const size_t header_len = strlen("MultisigxV1"); if (data.size() < header_len || data.substr(0, header_len) != "MultisigxV1") { MERROR("Multisig info header check error"); return false; } std::string decoded; if (!tools::base58::decode(data.substr(header_len), decoded)) { MERROR("Multisig info decoding error"); return false; } if (decoded.size() < sizeof(crypto::public_key) + sizeof(crypto::signature)) { MERROR("Multisig info is corrupt"); return false; } if ((decoded.size() - (sizeof(crypto::public_key) + sizeof(crypto::signature))) % sizeof(crypto::public_key)) { MERROR("Multisig info is corrupt"); return false; } const size_t n_keys = (decoded.size() - (sizeof(crypto::public_key) + sizeof(crypto::signature))) / sizeof(crypto::public_key); size_t offset = 0; signer = *(const crypto::public_key*)(decoded.data() + offset); offset += sizeof(signer); const crypto::signature &signature = *(const crypto::signature*)(decoded.data() + offset + n_keys * sizeof(crypto::public_key)); crypto::hash hash; crypto::cn_fast_hash(decoded.data(), decoded.size() - sizeof(signature), hash); if (!crypto::check_signature(hash, signer, signature)) { MERROR("Multisig info signature is invalid"); return false; } for (size_t n = 0; n < n_keys; ++n) { crypto::public_key mspk = *(const crypto::public_key*)(decoded.data() + offset); pkeys.insert(mspk); offset += sizeof(mspk); } return true; } bool wallet2::multisig(bool *ready, uint32_t *threshold, uint32_t *total) const { if (!m_multisig) return false; if (threshold) *threshold = m_multisig_threshold; if (total) *total = m_multisig_signers.size(); if (ready) *ready = !(get_account().get_keys().m_account_address.m_spend_public_key == rct::rct2pk(rct::identity())); return true; } bool wallet2::has_multisig_partial_key_images() const { if (!m_multisig) return false; for (const auto &td: m_transfers) if (td.m_key_image_partial) return true; return false; } /*! * \brief Rewrites to the wallet file for wallet upgrade (doesn't generate key, assumes it's already there) * \param wallet_name Name of wallet file (should exist) * \param password Password for wallet file */ void wallet2::rewrite(const std::string& wallet_name, const epee::wipeable_string& password) { if (wallet_name.empty()) return; prepare_file_names(wallet_name); boost::system::error_code ignored_ec; THROW_WALLET_EXCEPTION_IF(!boost::filesystem::exists(m_keys_file, ignored_ec), error::file_not_found, m_keys_file); bool r = store_keys(m_keys_file, password, m_watch_only); THROW_WALLET_EXCEPTION_IF(!r, error::file_save_error, m_keys_file); } /*! * \brief Writes to a file named based on the normal wallet (doesn't generate key, assumes it's already there) * \param wallet_name Base name of wallet file * \param password Password for wallet file */ void wallet2::write_watch_only_wallet(const std::string& wallet_name, const epee::wipeable_string& password) { prepare_file_names(wallet_name); boost::system::error_code ignored_ec; std::string filename = m_keys_file + "-watchonly"; bool watch_only_keys_file_exists = boost::filesystem::exists(filename, ignored_ec); THROW_WALLET_EXCEPTION_IF(watch_only_keys_file_exists, error::file_save_error, filename); bool r = store_keys(filename, password, true); THROW_WALLET_EXCEPTION_IF(!r, error::file_save_error, filename); } //---------------------------------------------------------------------------------------------------- void wallet2::wallet_exists(const std::string& file_path, bool& keys_file_exists, bool& wallet_file_exists) { std::string keys_file, wallet_file; do_prepare_file_names(file_path, keys_file, wallet_file); boost::system::error_code ignore; keys_file_exists = boost::filesystem::exists(keys_file, ignore); wallet_file_exists = boost::filesystem::exists(wallet_file, ignore); } //---------------------------------------------------------------------------------------------------- bool wallet2::wallet_valid_path_format(const std::string& file_path) { return !file_path.empty(); } //---------------------------------------------------------------------------------------------------- bool wallet2::parse_long_payment_id(const std::string& payment_id_str, crypto::hash& payment_id) { cryptonote::blobdata payment_id_data; if(!epee::string_tools::parse_hexstr_to_binbuff(payment_id_str, payment_id_data)) return false; if(sizeof(crypto::hash) != payment_id_data.size()) return false; payment_id = *reinterpret_cast(payment_id_data.data()); return true; } //---------------------------------------------------------------------------------------------------- bool wallet2::parse_short_payment_id(const std::string& payment_id_str, crypto::hash8& payment_id) { cryptonote::blobdata payment_id_data; if(!epee::string_tools::parse_hexstr_to_binbuff(payment_id_str, payment_id_data)) return false; if(sizeof(crypto::hash8) != payment_id_data.size()) return false; payment_id = *reinterpret_cast(payment_id_data.data()); return true; } //---------------------------------------------------------------------------------------------------- bool wallet2::parse_payment_id(const std::string& payment_id_str, crypto::hash& payment_id) { if (parse_long_payment_id(payment_id_str, payment_id)) return true; crypto::hash8 payment_id8; if (parse_short_payment_id(payment_id_str, payment_id8)) { memcpy(payment_id.data, payment_id8.data, 8); memset(payment_id.data + 8, 0, 24); return true; } return false; } //---------------------------------------------------------------------------------------------------- void wallet2::set_default_decimal_point(unsigned int decimal_point) { cryptonote::set_default_decimal_point(decimal_point); } //---------------------------------------------------------------------------------------------------- unsigned int wallet2::get_default_decimal_point() const { return cryptonote::get_default_decimal_point(); } //---------------------------------------------------------------------------------------------------- bool wallet2::prepare_file_names(const std::string& file_path) { do_prepare_file_names(file_path, m_keys_file, m_wallet_file); return true; } //---------------------------------------------------------------------------------------------------- bool wallet2::check_connection(uint32_t *version, uint32_t timeout) { THROW_WALLET_EXCEPTION_IF(!m_is_initialized, error::wallet_not_initialized); boost::lock_guard lock(m_daemon_rpc_mutex); // TODO: Add light wallet version check. if(m_light_wallet) { version = 0; return m_light_wallet_connected; } if(!m_http_client.is_connected()) { m_node_rpc_proxy.invalidate(); if (!m_http_client.connect(std::chrono::milliseconds(timeout))) return false; } if (version) { epee::json_rpc::request req_t = AUTO_VAL_INIT(req_t); epee::json_rpc::response resp_t = AUTO_VAL_INIT(resp_t); req_t.jsonrpc = "2.0"; req_t.id = epee::serialization::storage_entry(0); req_t.method = "get_version"; bool r = net_utils::invoke_http_json("/json_rpc", req_t, resp_t, m_http_client); if(!r) { *version = 0; return false; } if (resp_t.result.status != CORE_RPC_STATUS_OK) *version = 0; else *version = resp_t.result.version; } return true; } //---------------------------------------------------------------------------------------------------- bool wallet2::generate_chacha_key_from_secret_keys(crypto::chacha_key &key) const { const account_keys &keys = m_account.get_keys(); const crypto::secret_key &view_key = keys.m_view_secret_key; const crypto::secret_key &spend_key = keys.m_spend_secret_key; tools::scrubbed_arr data; memcpy(data.data(), &view_key, sizeof(view_key)); memcpy(data.data() + sizeof(view_key), &spend_key, sizeof(spend_key)); data[sizeof(data) - 1] = CHACHA8_KEY_TAIL; crypto::generate_chacha_key(data.data(), sizeof(data), key); return true; } //---------------------------------------------------------------------------------------------------- void wallet2::load(const std::string& wallet_, const epee::wipeable_string& password) { clear(); prepare_file_names(wallet_); boost::system::error_code e; bool exists = boost::filesystem::exists(m_keys_file, e); THROW_WALLET_EXCEPTION_IF(e || !exists, error::file_not_found, m_keys_file); if (!load_keys(m_keys_file, password)) { THROW_WALLET_EXCEPTION_IF(true, error::file_read_error, m_keys_file); } LOG_PRINT_L0("Loaded wallet keys file, with public address: " << m_account.get_public_address_str(m_testnet)); //keys loaded ok! //try to load wallet file. but even if we failed, it is not big problem if(!boost::filesystem::exists(m_wallet_file, e) || e) { LOG_PRINT_L0("file not found: " << m_wallet_file << ", starting with empty blockchain"); m_account_public_address = m_account.get_keys().m_account_address; } else { wallet2::cache_file_data cache_file_data; std::string buf; bool r = epee::file_io_utils::load_file_to_string(m_wallet_file, buf); THROW_WALLET_EXCEPTION_IF(!r, error::file_read_error, m_wallet_file); // try to read it as an encrypted cache try { LOG_PRINT_L1("Trying to decrypt cache data"); r = ::serialization::parse_binary(buf, cache_file_data); THROW_WALLET_EXCEPTION_IF(!r, error::wallet_internal_error, "internal error: failed to deserialize \"" + m_wallet_file + '\"'); crypto::chacha_key key; generate_chacha_key_from_secret_keys(key); std::string cache_data; cache_data.resize(cache_file_data.cache_data.size()); crypto::chacha20(cache_file_data.cache_data.data(), cache_file_data.cache_data.size(), key, cache_file_data.iv, &cache_data[0]); try { std::stringstream iss; iss << cache_data; boost::archive::portable_binary_iarchive ar(iss); ar >> *this; } catch (...) { crypto::chacha8(cache_file_data.cache_data.data(), cache_file_data.cache_data.size(), key, cache_file_data.iv, &cache_data[0]); try { std::stringstream iss; iss << cache_data; boost::archive::portable_binary_iarchive ar(iss); ar >> *this; } catch (...) { LOG_PRINT_L0("Failed to open portable binary, trying unportable"); boost::filesystem::copy_file(m_wallet_file, m_wallet_file + ".unportable", boost::filesystem::copy_option::overwrite_if_exists); std::stringstream iss; iss.str(""); iss << cache_data; boost::archive::binary_iarchive ar(iss); ar >> *this; } } } catch (...) { LOG_PRINT_L1("Failed to load encrypted cache, trying unencrypted"); try { std::stringstream iss; iss << buf; boost::archive::portable_binary_iarchive ar(iss); ar >> *this; } catch (...) { LOG_PRINT_L0("Failed to open portable binary, trying unportable"); boost::filesystem::copy_file(m_wallet_file, m_wallet_file + ".unportable", boost::filesystem::copy_option::overwrite_if_exists); std::stringstream iss; iss.str(""); iss << buf; boost::archive::binary_iarchive ar(iss); ar >> *this; } } THROW_WALLET_EXCEPTION_IF( m_account_public_address.m_spend_public_key != m_account.get_keys().m_account_address.m_spend_public_key || m_account_public_address.m_view_public_key != m_account.get_keys().m_account_address.m_view_public_key, error::wallet_files_doesnt_correspond, m_keys_file, m_wallet_file); } cryptonote::block genesis; generate_genesis(genesis); crypto::hash genesis_hash = get_block_hash(genesis); if (m_blockchain.empty()) { m_blockchain.push_back(genesis_hash); } else { check_genesis(genesis_hash); } trim_hashchain(); if (get_num_subaddress_accounts() == 0) add_subaddress_account(tr("Primary account")); m_local_bc_height = m_blockchain.size(); } //---------------------------------------------------------------------------------------------------- void wallet2::trim_hashchain() { uint64_t height = m_checkpoints.get_max_height(); if (!m_blockchain.empty() && m_blockchain.size() == m_blockchain.offset()) { MINFO("Fixing empty hashchain"); epee::json_rpc::request req = AUTO_VAL_INIT(req); epee::json_rpc::response res = AUTO_VAL_INIT(res); m_daemon_rpc_mutex.lock(); req.jsonrpc = "2.0"; req.id = epee::serialization::storage_entry(0); req.method = "getblockheaderbyheight"; req.params.height = m_blockchain.size() - 1; bool r = net_utils::invoke_http_json("/json_rpc", req, res, m_http_client, rpc_timeout); m_daemon_rpc_mutex.unlock(); if (r && res.result.status == CORE_RPC_STATUS_OK) { crypto::hash hash; epee::string_tools::hex_to_pod(res.result.block_header.hash, hash); m_blockchain.refill(hash); } else { MERROR("Failed to request block header from daemon, hash chain may be unable to sync till the wallet is loaded with a usable daemon"); } } if (height > 0 && m_blockchain.size() > height) { --height; MDEBUG("trimming to " << height << ", offset " << m_blockchain.offset()); m_blockchain.trim(height); } } //---------------------------------------------------------------------------------------------------- void wallet2::check_genesis(const crypto::hash& genesis_hash) const { std::string what("Genesis block mismatch. You probably use wallet without testnet flag with blockchain from test network or vice versa"); THROW_WALLET_EXCEPTION_IF(genesis_hash != m_blockchain.genesis(), error::wallet_internal_error, what); } //---------------------------------------------------------------------------------------------------- std::string wallet2::path() const { return m_wallet_file; } //---------------------------------------------------------------------------------------------------- void wallet2::store() { store_to("", epee::wipeable_string()); } //---------------------------------------------------------------------------------------------------- void wallet2::store_to(const std::string &path, const epee::wipeable_string &password) { trim_hashchain(); // if file is the same, we do: // 1. save wallet to the *.new file // 2. remove old wallet file // 3. rename *.new to wallet_name // handle if we want just store wallet state to current files (ex store() replacement); bool same_file = true; if (!path.empty()) { std::string canonical_path = boost::filesystem::canonical(m_wallet_file).string(); size_t pos = canonical_path.find(path); same_file = pos != std::string::npos; } if (!same_file) { // check if we want to store to directory which doesn't exists yet boost::filesystem::path parent_path = boost::filesystem::path(path).parent_path(); // if path is not exists, try to create it if (!parent_path.empty() && !boost::filesystem::exists(parent_path)) { boost::system::error_code ec; if (!boost::filesystem::create_directories(parent_path, ec)) { throw std::logic_error(ec.message()); } } } // preparing wallet data std::stringstream oss; boost::archive::portable_binary_oarchive ar(oss); ar << *this; wallet2::cache_file_data cache_file_data = boost::value_initialized(); cache_file_data.cache_data = oss.str(); crypto::chacha_key key; generate_chacha_key_from_secret_keys(key); std::string cipher; cipher.resize(cache_file_data.cache_data.size()); cache_file_data.iv = crypto::rand(); crypto::chacha20(cache_file_data.cache_data.data(), cache_file_data.cache_data.size(), key, cache_file_data.iv, &cipher[0]); cache_file_data.cache_data = cipher; const std::string new_file = same_file ? m_wallet_file + ".new" : path; const std::string old_file = m_wallet_file; const std::string old_keys_file = m_keys_file; const std::string old_address_file = m_wallet_file + ".address.txt"; // save keys to the new file // if we here, main wallet file is saved and we only need to save keys and address files if (!same_file) { prepare_file_names(path); bool r = store_keys(m_keys_file, password, false); THROW_WALLET_EXCEPTION_IF(!r, error::file_save_error, m_keys_file); // save address to the new file const std::string address_file = m_wallet_file + ".address.txt"; r = file_io_utils::save_string_to_file(address_file, m_account.get_public_address_str(m_testnet)); THROW_WALLET_EXCEPTION_IF(!r, error::file_save_error, m_wallet_file); // remove old wallet file r = boost::filesystem::remove(old_file); if (!r) { LOG_ERROR("error removing file: " << old_file); } // remove old keys file r = boost::filesystem::remove(old_keys_file); if (!r) { LOG_ERROR("error removing file: " << old_keys_file); } // remove old address file r = boost::filesystem::remove(old_address_file); if (!r) { LOG_ERROR("error removing file: " << old_address_file); } } else { // save to new file std::ofstream ostr; ostr.open(new_file, std::ios_base::binary | std::ios_base::out | std::ios_base::trunc); binary_archive oar(ostr); bool success = ::serialization::serialize(oar, cache_file_data); ostr.close(); THROW_WALLET_EXCEPTION_IF(!success || !ostr.good(), error::file_save_error, new_file); // here we have "*.new" file, we need to rename it to be without ".new" std::error_code e = tools::replace_file(new_file, m_wallet_file); THROW_WALLET_EXCEPTION_IF(e, error::file_save_error, m_wallet_file, e); } } //---------------------------------------------------------------------------------------------------- uint64_t wallet2::balance(uint32_t index_major) const { uint64_t amount = 0; if(m_light_wallet) return m_light_wallet_unlocked_balance; for (const auto& i : balance_per_subaddress(index_major)) amount += i.second; return amount; } //---------------------------------------------------------------------------------------------------- uint64_t wallet2::unlocked_balance(uint32_t index_major) const { uint64_t amount = 0; if(m_light_wallet) return m_light_wallet_balance; for (const auto& i : unlocked_balance_per_subaddress(index_major)) amount += i.second; return amount; } //---------------------------------------------------------------------------------------------------- std::map wallet2::balance_per_subaddress(uint32_t index_major) const { std::map amount_per_subaddr; for (const auto& td: m_transfers) { if (td.m_subaddr_index.major == index_major && !td.m_spent) { auto found = amount_per_subaddr.find(td.m_subaddr_index.minor); if (found == amount_per_subaddr.end()) amount_per_subaddr[td.m_subaddr_index.minor] = td.amount(); else found->second += td.amount(); } } for (const auto& utx: m_unconfirmed_txs) { if (utx.second.m_subaddr_account == index_major && utx.second.m_state != wallet2::unconfirmed_transfer_details::failed) { // all changes go to 0-th subaddress (in the current subaddress account) auto found = amount_per_subaddr.find(0); if (found == amount_per_subaddr.end()) amount_per_subaddr[0] = utx.second.m_change; else found->second += utx.second.m_change; } } return amount_per_subaddr; } //---------------------------------------------------------------------------------------------------- std::map wallet2::unlocked_balance_per_subaddress(uint32_t index_major) const { std::map amount_per_subaddr; for(const transfer_details& td: m_transfers) { if(td.m_subaddr_index.major == index_major && !td.m_spent && is_transfer_unlocked(td)) { auto found = amount_per_subaddr.find(td.m_subaddr_index.minor); if (found == amount_per_subaddr.end()) amount_per_subaddr[td.m_subaddr_index.minor] = td.amount(); else found->second += td.amount(); } } return amount_per_subaddr; } //---------------------------------------------------------------------------------------------------- uint64_t wallet2::balance_all() const { uint64_t r = 0; for (uint32_t index_major = 0; index_major < get_num_subaddress_accounts(); ++index_major) r += balance(index_major); return r; } //---------------------------------------------------------------------------------------------------- uint64_t wallet2::unlocked_balance_all() const { uint64_t r = 0; for (uint32_t index_major = 0; index_major < get_num_subaddress_accounts(); ++index_major) r += unlocked_balance(index_major); return r; } //---------------------------------------------------------------------------------------------------- void wallet2::get_transfers(wallet2::transfer_container& incoming_transfers) const { incoming_transfers = m_transfers; } //---------------------------------------------------------------------------------------------------- void wallet2::get_payments(const crypto::hash& payment_id, std::list& payments, uint64_t min_height, const boost::optional& subaddr_account, const std::set& subaddr_indices) const { auto range = m_payments.equal_range(payment_id); std::for_each(range.first, range.second, [&payments, &min_height, &subaddr_account, &subaddr_indices](const payment_container::value_type& x) { if (min_height < x.second.m_block_height && (!subaddr_account || *subaddr_account == x.second.m_subaddr_index.major) && (subaddr_indices.empty() || subaddr_indices.count(x.second.m_subaddr_index.minor) == 1)) { payments.push_back(x.second); } }); } //---------------------------------------------------------------------------------------------------- void wallet2::get_payments(std::list>& payments, uint64_t min_height, uint64_t max_height, const boost::optional& subaddr_account, const std::set& subaddr_indices) const { auto range = std::make_pair(m_payments.begin(), m_payments.end()); std::for_each(range.first, range.second, [&payments, &min_height, &max_height, &subaddr_account, &subaddr_indices](const payment_container::value_type& x) { if (min_height < x.second.m_block_height && max_height >= x.second.m_block_height && (!subaddr_account || *subaddr_account == x.second.m_subaddr_index.major) && (subaddr_indices.empty() || subaddr_indices.count(x.second.m_subaddr_index.minor) == 1)) { payments.push_back(x); } }); } //---------------------------------------------------------------------------------------------------- void wallet2::get_payments_out(std::list>& confirmed_payments, uint64_t min_height, uint64_t max_height, const boost::optional& subaddr_account, const std::set& subaddr_indices) const { for (auto i = m_confirmed_txs.begin(); i != m_confirmed_txs.end(); ++i) { if (i->second.m_block_height <= min_height || i->second.m_block_height > max_height) continue; if (subaddr_account && *subaddr_account != i->second.m_subaddr_account) continue; if (!subaddr_indices.empty() && std::count_if(i->second.m_subaddr_indices.begin(), i->second.m_subaddr_indices.end(), [&subaddr_indices](uint32_t index) { return subaddr_indices.count(index) == 1; }) == 0) continue; confirmed_payments.push_back(*i); } } //---------------------------------------------------------------------------------------------------- void wallet2::get_unconfirmed_payments_out(std::list>& unconfirmed_payments, const boost::optional& subaddr_account, const std::set& subaddr_indices) const { for (auto i = m_unconfirmed_txs.begin(); i != m_unconfirmed_txs.end(); ++i) { if (subaddr_account && *subaddr_account != i->second.m_subaddr_account) continue; if (!subaddr_indices.empty() && std::count_if(i->second.m_subaddr_indices.begin(), i->second.m_subaddr_indices.end(), [&subaddr_indices](uint32_t index) { return subaddr_indices.count(index) == 1; }) == 0) continue; unconfirmed_payments.push_back(*i); } } //---------------------------------------------------------------------------------------------------- void wallet2::get_unconfirmed_payments(std::list>& unconfirmed_payments, const boost::optional& subaddr_account, const std::set& subaddr_indices) const { for (auto i = m_unconfirmed_payments.begin(); i != m_unconfirmed_payments.end(); ++i) { if ((!subaddr_account || *subaddr_account == i->second.m_pd.m_subaddr_index.major) && (subaddr_indices.empty() || subaddr_indices.count(i->second.m_pd.m_subaddr_index.minor) == 1)) unconfirmed_payments.push_back(*i); } } //---------------------------------------------------------------------------------------------------- void wallet2::rescan_spent() { // This is RPC call that can take a long time if there are many outputs, // so we call it several times, in stripes, so we don't time out spuriously std::vector spent_status; spent_status.reserve(m_transfers.size()); const size_t chunk_size = 1000; for (size_t start_offset = 0; start_offset < m_transfers.size(); start_offset += chunk_size) { const size_t n_outputs = std::min(chunk_size, m_transfers.size() - start_offset); MDEBUG("Calling is_key_image_spent on " << start_offset << " - " << (start_offset + n_outputs - 1) << ", out of " << m_transfers.size()); COMMAND_RPC_IS_KEY_IMAGE_SPENT::request req = AUTO_VAL_INIT(req); COMMAND_RPC_IS_KEY_IMAGE_SPENT::response daemon_resp = AUTO_VAL_INIT(daemon_resp); for (size_t n = start_offset; n < start_offset + n_outputs; ++n) req.key_images.push_back(string_tools::pod_to_hex(m_transfers[n].m_key_image)); m_daemon_rpc_mutex.lock(); bool r = epee::net_utils::invoke_http_json("/is_key_image_spent", req, daemon_resp, m_http_client, rpc_timeout); m_daemon_rpc_mutex.unlock(); THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "is_key_image_spent"); THROW_WALLET_EXCEPTION_IF(daemon_resp.status == CORE_RPC_STATUS_BUSY, error::daemon_busy, "is_key_image_spent"); THROW_WALLET_EXCEPTION_IF(daemon_resp.status != CORE_RPC_STATUS_OK, error::is_key_image_spent_error, daemon_resp.status); THROW_WALLET_EXCEPTION_IF(daemon_resp.spent_status.size() != n_outputs, error::wallet_internal_error, "daemon returned wrong response for is_key_image_spent, wrong amounts count = " + std::to_string(daemon_resp.spent_status.size()) + ", expected " + std::to_string(n_outputs)); std::copy(daemon_resp.spent_status.begin(), daemon_resp.spent_status.end(), std::back_inserter(spent_status)); } // update spent status for (size_t i = 0; i < m_transfers.size(); ++i) { transfer_details& td = m_transfers[i]; // a view wallet may not know about key images if (!td.m_key_image_known || td.m_key_image_partial) continue; if (td.m_spent != (spent_status[i] != COMMAND_RPC_IS_KEY_IMAGE_SPENT::UNSPENT)) { if (td.m_spent) { LOG_PRINT_L0("Marking output " << i << "(" << td.m_key_image << ") as unspent, it was marked as spent"); set_unspent(i); td.m_spent_height = 0; } else { LOG_PRINT_L0("Marking output " << i << "(" << td.m_key_image << ") as spent, it was marked as unspent"); set_spent(i, td.m_spent_height); // unknown height, if this gets reorged, it might still be missed } } } } //---------------------------------------------------------------------------------------------------- void wallet2::rescan_blockchain(bool refresh) { clear(); cryptonote::block genesis; generate_genesis(genesis); crypto::hash genesis_hash = get_block_hash(genesis); m_blockchain.push_back(genesis_hash); add_subaddress_account(tr("Primary account")); m_local_bc_height = 1; if (refresh) this->refresh(); } //---------------------------------------------------------------------------------------------------- bool wallet2::is_transfer_unlocked(const transfer_details& td) const { return is_transfer_unlocked(td.m_tx.unlock_time, td.m_block_height); } //---------------------------------------------------------------------------------------------------- bool wallet2::is_transfer_unlocked(uint64_t unlock_time, uint64_t block_height) const { if(!is_tx_spendtime_unlocked(unlock_time, block_height)) return false; if(block_height + CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE > m_local_bc_height) return false; return true; } //---------------------------------------------------------------------------------------------------- bool wallet2::is_tx_spendtime_unlocked(uint64_t unlock_time, uint64_t block_height) const { if(unlock_time < CRYPTONOTE_MAX_BLOCK_NUMBER) { //interpret as block index if(m_local_bc_height-1 + CRYPTONOTE_LOCKED_TX_ALLOWED_DELTA_BLOCKS >= unlock_time) return true; else return false; }else { //interpret as time uint64_t current_time = static_cast(time(NULL)); // XXX: this needs to be fast, so we'd need to get the starting heights // from the daemon to be correct once voting kicks in uint64_t v2height = m_testnet ? 624634 : 1009827; uint64_t leeway = block_height < v2height ? CRYPTONOTE_LOCKED_TX_ALLOWED_DELTA_SECONDS_V1 : CRYPTONOTE_LOCKED_TX_ALLOWED_DELTA_SECONDS_V2; if(current_time + leeway >= unlock_time) return true; else return false; } return false; } //---------------------------------------------------------------------------------------------------- namespace { template T pop_index(std::vector& vec, size_t idx) { CHECK_AND_ASSERT_MES(!vec.empty(), T(), "Vector must be non-empty"); CHECK_AND_ASSERT_MES(idx < vec.size(), T(), "idx out of bounds"); T res = vec[idx]; if (idx + 1 != vec.size()) { vec[idx] = vec.back(); } vec.resize(vec.size() - 1); return res; } template T pop_random_value(std::vector& vec) { CHECK_AND_ASSERT_MES(!vec.empty(), T(), "Vector must be non-empty"); size_t idx = crypto::rand() % vec.size(); return pop_index (vec, idx); } template T pop_back(std::vector& vec) { CHECK_AND_ASSERT_MES(!vec.empty(), T(), "Vector must be non-empty"); T res = vec.back(); vec.pop_back(); return res; } template void pop_if_present(std::vector& vec, T e) { for (size_t i = 0; i < vec.size(); ++i) { if (e == vec[i]) { pop_index (vec, i); return; } } } } //---------------------------------------------------------------------------------------------------- // This returns a handwavy estimation of how much two outputs are related // If they're from the same tx, then they're fully related. From close block // heights, they're kinda related. The actual values don't matter, just // their ordering, but it could become more murky if we add scores later. float wallet2::get_output_relatedness(const transfer_details &td0, const transfer_details &td1) const { int dh; // expensive test, and same tx will fall onto the same block height below if (td0.m_txid == td1.m_txid) return 1.0f; // same block height -> possibly tx burst, or same tx (since above is disabled) dh = td0.m_block_height > td1.m_block_height ? td0.m_block_height - td1.m_block_height : td1.m_block_height - td0.m_block_height; if (dh == 0) return 0.9f; // adjacent blocks -> possibly tx burst if (dh == 1) return 0.8f; // could extract the payment id, and compare them, but this is a bit expensive too // similar block heights if (dh < 10) return 0.2f; // don't think these are particularly related return 0.0f; } //---------------------------------------------------------------------------------------------------- size_t wallet2::pop_best_value_from(const transfer_container &transfers, std::vector &unused_indices, const std::vector& selected_transfers, bool smallest) const { std::vector candidates; float best_relatedness = 1.0f; for (size_t n = 0; n < unused_indices.size(); ++n) { const transfer_details &candidate = transfers[unused_indices[n]]; float relatedness = 0.0f; for (std::vector::const_iterator i = selected_transfers.begin(); i != selected_transfers.end(); ++i) { float r = get_output_relatedness(candidate, transfers[*i]); if (r > relatedness) { relatedness = r; if (relatedness == 1.0f) break; } } if (relatedness < best_relatedness) { best_relatedness = relatedness; candidates.clear(); } if (relatedness == best_relatedness) candidates.push_back(n); } // we have all the least related outputs in candidates, so we can pick either // the smallest, or a random one, depending on request size_t idx; if (smallest) { idx = 0; for (size_t n = 0; n < candidates.size(); ++n) { const transfer_details &td = transfers[unused_indices[candidates[n]]]; if (td.amount() < transfers[unused_indices[candidates[idx]]].amount()) idx = n; } } else { idx = crypto::rand() % candidates.size(); } return pop_index (unused_indices, candidates[idx]); } //---------------------------------------------------------------------------------------------------- size_t wallet2::pop_best_value(std::vector &unused_indices, const std::vector& selected_transfers, bool smallest) const { return pop_best_value_from(m_transfers, unused_indices, selected_transfers, smallest); } //---------------------------------------------------------------------------------------------------- // Select random input sources for transaction. // returns: // direct return: amount of money found // modified reference: selected_transfers, a list of iterators/indices of input sources uint64_t wallet2::select_transfers(uint64_t needed_money, std::vector unused_transfers_indices, std::vector& selected_transfers, bool trusted_daemon) { uint64_t found_money = 0; selected_transfers.reserve(unused_transfers_indices.size()); while (found_money < needed_money && !unused_transfers_indices.empty()) { size_t idx = pop_best_value(unused_transfers_indices, selected_transfers); transfer_container::iterator it = m_transfers.begin() + idx; selected_transfers.push_back(idx); found_money += it->amount(); } return found_money; } //---------------------------------------------------------------------------------------------------- void wallet2::add_unconfirmed_tx(const cryptonote::transaction& tx, uint64_t amount_in, const std::vector &dests, const crypto::hash &payment_id, uint64_t change_amount, uint32_t subaddr_account, const std::set& subaddr_indices) { unconfirmed_transfer_details& utd = m_unconfirmed_txs[cryptonote::get_transaction_hash(tx)]; utd.m_amount_in = amount_in; utd.m_amount_out = 0; for (const auto &d: dests) utd.m_amount_out += d.amount; utd.m_amount_out += change_amount; // dests does not contain change utd.m_change = change_amount; utd.m_sent_time = time(NULL); utd.m_tx = (const cryptonote::transaction_prefix&)tx; utd.m_dests = dests; utd.m_payment_id = payment_id; utd.m_state = wallet2::unconfirmed_transfer_details::pending; utd.m_timestamp = time(NULL); utd.m_subaddr_account = subaddr_account; utd.m_subaddr_indices = subaddr_indices; } //---------------------------------------------------------------------------------------------------- void wallet2::transfer(const std::vector& dsts, const size_t fake_outs_count, const std::vector &unused_transfers_indices, uint64_t unlock_time, uint64_t fee, const std::vector& extra, cryptonote::transaction& tx, pending_tx& ptx, bool trusted_daemon) { transfer(dsts, fake_outs_count, unused_transfers_indices, unlock_time, fee, extra, detail::digit_split_strategy, tx_dust_policy(::config::DEFAULT_DUST_THRESHOLD), tx, ptx, trusted_daemon); } //---------------------------------------------------------------------------------------------------- void wallet2::transfer(const std::vector& dsts, const size_t fake_outs_count, const std::vector &unused_transfers_indices, uint64_t unlock_time, uint64_t fee, const std::vector& extra, bool trusted_daemon) { cryptonote::transaction tx; pending_tx ptx; transfer(dsts, fake_outs_count, unused_transfers_indices, unlock_time, fee, extra, tx, ptx, trusted_daemon); } namespace { // split_amounts(vector dsts, size_t num_splits) // // split amount for each dst in dsts into num_splits parts // and make num_splits new vector instances to hold these new amounts std::vector> split_amounts( std::vector dsts, size_t num_splits) { std::vector> retVal; if (num_splits <= 1) { retVal.push_back(dsts); return retVal; } // for each split required for (size_t i=0; i < num_splits; i++) { std::vector new_dsts; // for each destination for (size_t j=0; j < dsts.size(); j++) { cryptonote::tx_destination_entry de; uint64_t amount; amount = dsts[j].amount; amount = amount / num_splits; // if last split, add remainder if (i + 1 == num_splits) { amount += dsts[j].amount % num_splits; } de.addr = dsts[j].addr; de.amount = amount; new_dsts.push_back(de); } retVal.push_back(new_dsts); } return retVal; } } // anonymous namespace //---------------------------------------------------------------------------------------------------- crypto::hash wallet2::get_payment_id(const pending_tx &ptx) const { std::vector tx_extra_fields; if(!parse_tx_extra(ptx.tx.extra, tx_extra_fields)) return crypto::null_hash; tx_extra_nonce extra_nonce; crypto::hash payment_id = null_hash; if (find_tx_extra_field_by_type(tx_extra_fields, extra_nonce)) { crypto::hash8 payment_id8 = null_hash8; if(get_encrypted_payment_id_from_tx_extra_nonce(extra_nonce.nonce, payment_id8)) { if (ptx.dests.empty()) { MWARNING("Encrypted payment id found, but no destinations public key, cannot decrypt"); return crypto::null_hash; } if (decrypt_payment_id(payment_id8, ptx.dests[0].addr.m_view_public_key, ptx.tx_key)) { memcpy(payment_id.data, payment_id8.data, 8); } } else if (!get_payment_id_from_tx_extra_nonce(extra_nonce.nonce, payment_id)) { payment_id = crypto::null_hash; } } return payment_id; } //---------------------------------------------------------------------------------------------------- // take a pending tx and actually send it to the daemon void wallet2::commit_tx(pending_tx& ptx) { using namespace cryptonote; if(m_light_wallet) { cryptonote::COMMAND_RPC_SUBMIT_RAW_TX::request oreq; cryptonote::COMMAND_RPC_SUBMIT_RAW_TX::response ores; oreq.address = get_account().get_public_address_str(m_testnet); oreq.view_key = string_tools::pod_to_hex(get_account().get_keys().m_view_secret_key); oreq.tx = epee::string_tools::buff_to_hex_nodelimer(tx_to_blob(ptx.tx)); m_daemon_rpc_mutex.lock(); bool r = epee::net_utils::invoke_http_json("/submit_raw_tx", oreq, ores, m_http_client, rpc_timeout, "POST"); m_daemon_rpc_mutex.unlock(); THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "submit_raw_tx"); // MyMonero and OpenMonero use different status strings THROW_WALLET_EXCEPTION_IF(ores.status != "OK" && ores.status != "success" , error::tx_rejected, ptx.tx, ores.status, ores.error); } else { // Normal submit COMMAND_RPC_SEND_RAW_TX::request req; req.tx_as_hex = epee::string_tools::buff_to_hex_nodelimer(tx_to_blob(ptx.tx)); req.do_not_relay = false; COMMAND_RPC_SEND_RAW_TX::response daemon_send_resp; m_daemon_rpc_mutex.lock(); bool r = epee::net_utils::invoke_http_json("/sendrawtransaction", req, daemon_send_resp, m_http_client, rpc_timeout); m_daemon_rpc_mutex.unlock(); THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "sendrawtransaction"); THROW_WALLET_EXCEPTION_IF(daemon_send_resp.status == CORE_RPC_STATUS_BUSY, error::daemon_busy, "sendrawtransaction"); THROW_WALLET_EXCEPTION_IF(daemon_send_resp.status != CORE_RPC_STATUS_OK, error::tx_rejected, ptx.tx, daemon_send_resp.status, daemon_send_resp.reason); // sanity checks for (size_t idx: ptx.selected_transfers) { THROW_WALLET_EXCEPTION_IF(idx >= m_transfers.size(), error::wallet_internal_error, "Bad output index in selected transfers: " + boost::lexical_cast(idx)); } } crypto::hash txid; txid = get_transaction_hash(ptx.tx); crypto::hash payment_id = crypto::null_hash; std::vector dests; uint64_t amount_in = 0; if (store_tx_info()) { payment_id = get_payment_id(ptx); dests = ptx.dests; for(size_t idx: ptx.selected_transfers) amount_in += m_transfers[idx].amount(); } add_unconfirmed_tx(ptx.tx, amount_in, dests, payment_id, ptx.change_dts.amount, ptx.construction_data.subaddr_account, ptx.construction_data.subaddr_indices); if (store_tx_info()) { m_tx_keys.insert(std::make_pair(txid, ptx.tx_key)); m_additional_tx_keys.insert(std::make_pair(txid, ptx.additional_tx_keys)); } LOG_PRINT_L2("transaction " << txid << " generated ok and sent to daemon, key_images: [" << ptx.key_images << "]"); for(size_t idx: ptx.selected_transfers) { set_spent(idx, 0); } // tx generated, get rid of used k values for (size_t idx: ptx.selected_transfers) m_transfers[idx].m_multisig_k.clear(); //fee includes dust if dust policy specified it. LOG_PRINT_L1("Transaction successfully sent. <" << txid << ">" << ENDL << "Commission: " << print_money(ptx.fee) << " (dust sent to dust addr: " << print_money((ptx.dust_added_to_fee ? 0 : ptx.dust)) << ")" << ENDL << "Balance: " << print_money(balance(ptx.construction_data.subaddr_account)) << ENDL << "Unlocked: " << print_money(unlocked_balance(ptx.construction_data.subaddr_account)) << ENDL << "Please, wait for confirmation for your balance to be unlocked."); } void wallet2::commit_tx(std::vector& ptx_vector) { for (auto & ptx : ptx_vector) { commit_tx(ptx); } } //---------------------------------------------------------------------------------------------------- bool wallet2::save_tx(const std::vector& ptx_vector, const std::string &filename) { LOG_PRINT_L0("saving " << ptx_vector.size() << " transactions"); unsigned_tx_set txs; for (auto &tx: ptx_vector) { // Short payment id is encrypted with tx_key. // Since sign_tx() generates new tx_keys and encrypts the payment id, we need to save the decrypted payment ID // Save tx construction_data to unsigned_tx_set txs.txes.push_back(get_construction_data_with_decrypted_short_payment_id(tx)); } txs.transfers = m_transfers; // save as binary std::ostringstream oss; boost::archive::portable_binary_oarchive ar(oss); try { ar << txs; } catch (...) { return false; } LOG_PRINT_L2("Saving unsigned tx data: " << oss.str()); std::string ciphertext = encrypt_with_view_secret_key(oss.str()); return epee::file_io_utils::save_string_to_file(filename, std::string(UNSIGNED_TX_PREFIX) + ciphertext); } //---------------------------------------------------------------------------------------------------- bool wallet2::load_unsigned_tx(const std::string &unsigned_filename, unsigned_tx_set &exported_txs) { std::string s; boost::system::error_code errcode; if (!boost::filesystem::exists(unsigned_filename, errcode)) { LOG_PRINT_L0("File " << unsigned_filename << " does not exist: " << errcode); return false; } if (!epee::file_io_utils::load_file_to_string(unsigned_filename.c_str(), s)) { LOG_PRINT_L0("Failed to load from " << unsigned_filename); return false; } const size_t magiclen = strlen(UNSIGNED_TX_PREFIX) - 1; if (strncmp(s.c_str(), UNSIGNED_TX_PREFIX, magiclen)) { LOG_PRINT_L0("Bad magic from " << unsigned_filename); return false; } s = s.substr(magiclen); const char version = s[0]; s = s.substr(1); if (version == '\003') { try { std::istringstream iss(s); boost::archive::portable_binary_iarchive ar(iss); ar >> exported_txs; } catch (...) { LOG_PRINT_L0("Failed to parse data from " << unsigned_filename); return false; } } else if (version == '\004') { try { s = decrypt_with_view_secret_key(s); try { std::istringstream iss(s); boost::archive::portable_binary_iarchive ar(iss); ar >> exported_txs; } catch (...) { LOG_PRINT_L0("Failed to parse data from " << unsigned_filename); return false; } } catch (const std::exception &e) { LOG_PRINT_L0("Failed to decrypt " << unsigned_filename << ": " << e.what()); return false; } } else { LOG_PRINT_L0("Unsupported version in " << unsigned_filename); return false; } LOG_PRINT_L1("Loaded tx unsigned data from binary: " << exported_txs.txes.size() << " transactions"); return true; } //---------------------------------------------------------------------------------------------------- bool wallet2::sign_tx(const std::string &unsigned_filename, const std::string &signed_filename, std::vector &txs, std::function accept_func, bool export_raw) { unsigned_tx_set exported_txs; if(!load_unsigned_tx(unsigned_filename, exported_txs)) return false; if (accept_func && !accept_func(exported_txs)) { LOG_PRINT_L1("Transactions rejected by callback"); return false; } return sign_tx(exported_txs, signed_filename, txs, export_raw); } //---------------------------------------------------------------------------------------------------- bool wallet2::sign_tx(unsigned_tx_set &exported_txs, const std::string &signed_filename, std::vector &txs, bool export_raw) { import_outputs(exported_txs.transfers); // sign the transactions signed_tx_set signed_txes; for (size_t n = 0; n < exported_txs.txes.size(); ++n) { tools::wallet2::tx_construction_data &sd = exported_txs.txes[n]; THROW_WALLET_EXCEPTION_IF(sd.sources.empty(), error::wallet_internal_error, "Empty sources"); LOG_PRINT_L1(" " << (n+1) << ": " << sd.sources.size() << " inputs, ring size " << sd.sources[0].outputs.size()); signed_txes.ptx.push_back(pending_tx()); tools::wallet2::pending_tx &ptx = signed_txes.ptx.back(); bool bulletproof = sd.use_rct && !ptx.tx.rct_signatures.p.bulletproofs.empty(); crypto::secret_key tx_key; std::vector additional_tx_keys; rct::multisig_out msout; bool r = cryptonote::construct_tx_and_get_tx_key(m_account.get_keys(), m_subaddresses, sd.sources, sd.splitted_dsts, sd.change_dts.addr, sd.extra, ptx.tx, sd.unlock_time, tx_key, additional_tx_keys, sd.use_rct, bulletproof, m_multisig ? &msout : NULL); THROW_WALLET_EXCEPTION_IF(!r, error::tx_not_constructed, sd.sources, sd.splitted_dsts, sd.unlock_time, m_testnet); // we don't test tx size, because we don't know the current limit, due to not having a blockchain, // and it's a bit pointless to fail there anyway, since it'd be a (good) guess only. We sign anyway, // and if we really go over limit, the daemon will reject when it gets submitted. Chances are it's // OK anyway since it was generated in the first place, and rerolling should be within a few bytes. // normally, the tx keys are saved in commit_tx, when the tx is actually sent to the daemon. // we can't do that here since the tx will be sent from the compromised wallet, which we don't want // to see that info, so we save it here if (store_tx_info()) { const crypto::hash txid = get_transaction_hash(ptx.tx); m_tx_keys.insert(std::make_pair(txid, tx_key)); m_additional_tx_keys.insert(std::make_pair(txid, additional_tx_keys)); } std::string key_images; bool all_are_txin_to_key = std::all_of(ptx.tx.vin.begin(), ptx.tx.vin.end(), [&](const txin_v& s_e) -> bool { CHECKED_GET_SPECIFIC_VARIANT(s_e, const txin_to_key, in, false); key_images += boost::to_string(in.k_image) + " "; return true; }); THROW_WALLET_EXCEPTION_IF(!all_are_txin_to_key, error::unexpected_txin_type, ptx.tx); ptx.key_images = key_images; ptx.fee = 0; for (const auto &i: sd.sources) ptx.fee += i.amount; for (const auto &i: sd.splitted_dsts) ptx.fee -= i.amount; ptx.dust = 0; ptx.dust_added_to_fee = false; ptx.change_dts = sd.change_dts; ptx.selected_transfers = sd.selected_transfers; ptx.tx_key = rct::rct2sk(rct::identity()); // don't send it back to the untrusted view wallet ptx.dests = sd.dests; ptx.construction_data = sd; txs.push_back(ptx); } // add key images signed_txes.key_images.resize(m_transfers.size()); for (size_t i = 0; i < m_transfers.size(); ++i) { if (!m_transfers[i].m_key_image_known || m_transfers[i].m_key_image_partial) LOG_PRINT_L0("WARNING: key image not known in signing wallet at index " << i); signed_txes.key_images[i] = m_transfers[i].m_key_image; } // save as binary std::ostringstream oss; boost::archive::portable_binary_oarchive ar(oss); try { ar << signed_txes; } catch(...) { return false; } LOG_PRINT_L3("Saving signed tx data (with encryption): " << oss.str()); std::string ciphertext = encrypt_with_view_secret_key(oss.str()); if (!epee::file_io_utils::save_string_to_file(signed_filename, std::string(SIGNED_TX_PREFIX) + ciphertext)) { LOG_PRINT_L0("Failed to save file to " << signed_filename); return false; } // export signed raw tx without encryption if (export_raw) { for (size_t i = 0; i < signed_txes.ptx.size(); ++i) { std::string tx_as_hex = epee::string_tools::buff_to_hex_nodelimer(tx_to_blob(signed_txes.ptx[i].tx)); std::string raw_filename = signed_filename + "_raw" + (signed_txes.ptx.size() == 1 ? "" : ("_" + std::to_string(i))); if (!epee::file_io_utils::save_string_to_file(raw_filename, tx_as_hex)) { LOG_PRINT_L0("Failed to save file to " << raw_filename); return false; } } } return true; } //---------------------------------------------------------------------------------------------------- bool wallet2::load_tx(const std::string &signed_filename, std::vector &ptx, std::function accept_func) { std::string s; boost::system::error_code errcode; signed_tx_set signed_txs; if (!boost::filesystem::exists(signed_filename, errcode)) { LOG_PRINT_L0("File " << signed_filename << " does not exist: " << errcode); return false; } if (!epee::file_io_utils::load_file_to_string(signed_filename.c_str(), s)) { LOG_PRINT_L0("Failed to load from " << signed_filename); return false; } const size_t magiclen = strlen(SIGNED_TX_PREFIX) - 1; if (strncmp(s.c_str(), SIGNED_TX_PREFIX, magiclen)) { LOG_PRINT_L0("Bad magic from " << signed_filename); return false; } s = s.substr(magiclen); const char version = s[0]; s = s.substr(1); if (version == '\003') { try { std::istringstream iss(s); boost::archive::portable_binary_iarchive ar(iss); ar >> signed_txs; } catch (...) { LOG_PRINT_L0("Failed to parse data from " << signed_filename); return false; } } else if (version == '\004') { try { s = decrypt_with_view_secret_key(s); try { std::istringstream iss(s); boost::archive::portable_binary_iarchive ar(iss); ar >> signed_txs; } catch (...) { LOG_PRINT_L0("Failed to parse decrypted data from " << signed_filename); return false; } } catch (const std::exception &e) { LOG_PRINT_L0("Failed to decrypt " << signed_filename << ": " << e.what()); return false; } } else { LOG_PRINT_L0("Unsupported version in " << signed_filename); return false; } LOG_PRINT_L0("Loaded signed tx data from binary: " << signed_txs.ptx.size() << " transactions"); for (auto &ptx: signed_txs.ptx) LOG_PRINT_L0(cryptonote::obj_to_json_str(ptx.tx)); if (accept_func && !accept_func(signed_txs)) { LOG_PRINT_L1("Transactions rejected by callback"); return false; } // import key images if (signed_txs.key_images.size() > m_transfers.size()) { LOG_PRINT_L1("More key images returned that we know outputs for"); return false; } for (size_t i = 0; i < signed_txs.key_images.size(); ++i) { transfer_details &td = m_transfers[i]; if (td.m_key_image_known && !td.m_key_image_partial && td.m_key_image != signed_txs.key_images[i]) LOG_PRINT_L0("WARNING: imported key image differs from previously known key image at index " << i << ": trusting imported one"); td.m_key_image = signed_txs.key_images[i]; m_key_images[m_transfers[i].m_key_image] = i; td.m_key_image_known = true; td.m_key_image_partial = false; m_pub_keys[m_transfers[i].get_public_key()] = i; } ptx = signed_txs.ptx; return true; } //---------------------------------------------------------------------------------------------------- std::string wallet2::save_multisig_tx(multisig_tx_set txs) { LOG_PRINT_L0("saving " << txs.m_ptx.size() << " multisig transactions"); // txes generated, get rid of used k values for (size_t n = 0; n < txs.m_ptx.size(); ++n) for (size_t idx: txs.m_ptx[n].construction_data.selected_transfers) m_transfers[idx].m_multisig_k.clear(); // zero out some data we don't want to share for (auto &ptx: txs.m_ptx) { for (auto &e: ptx.construction_data.sources) e.multisig_kLRki.k = rct::zero(); } for (auto &ptx: txs.m_ptx) { // Get decrypted payment id from pending_tx ptx.construction_data = get_construction_data_with_decrypted_short_payment_id(ptx); } // save as binary std::ostringstream oss; boost::archive::portable_binary_oarchive ar(oss); try { ar << txs; } catch (...) { return std::string(); } LOG_PRINT_L2("Saving multisig unsigned tx data: " << oss.str()); std::string ciphertext = encrypt_with_view_secret_key(oss.str()); return std::string(MULTISIG_UNSIGNED_TX_PREFIX) + ciphertext; } //---------------------------------------------------------------------------------------------------- bool wallet2::save_multisig_tx(const multisig_tx_set &txs, const std::string &filename) { std::string ciphertext = save_multisig_tx(txs); if (ciphertext.empty()) return false; return epee::file_io_utils::save_string_to_file(filename, ciphertext); } //---------------------------------------------------------------------------------------------------- std::string wallet2::save_multisig_tx(const std::vector& ptx_vector) { multisig_tx_set txs; txs.m_ptx = ptx_vector; for (const auto &msk: get_account().get_multisig_keys()) { crypto::public_key pkey = get_multisig_signing_public_key(msk); for (auto &ptx: txs.m_ptx) for (auto &sig: ptx.multisig_sigs) sig.signing_keys.insert(pkey); } txs.m_signers.insert(get_multisig_signer_public_key()); return save_multisig_tx(txs); } //---------------------------------------------------------------------------------------------------- bool wallet2::save_multisig_tx(const std::vector& ptx_vector, const std::string &filename) { std::string ciphertext = save_multisig_tx(ptx_vector); if (ciphertext.empty()) return false; return epee::file_io_utils::save_string_to_file(filename, ciphertext); } //---------------------------------------------------------------------------------------------------- bool wallet2::load_multisig_tx(cryptonote::blobdata s, multisig_tx_set &exported_txs, std::function accept_func) { const size_t magiclen = strlen(MULTISIG_UNSIGNED_TX_PREFIX); if (strncmp(s.c_str(), MULTISIG_UNSIGNED_TX_PREFIX, magiclen)) { LOG_PRINT_L0("Bad magic from multisig tx data"); return false; } try { s = decrypt_with_view_secret_key(std::string(s, magiclen)); } catch (const std::exception &e) { LOG_PRINT_L0("Failed to decrypt multisig tx data: " << e.what()); return false; } try { std::istringstream iss(s); boost::archive::portable_binary_iarchive ar(iss); ar >> exported_txs; } catch (...) { LOG_PRINT_L0("Failed to parse multisig tx data"); return false; } // sanity checks for (const auto &ptx: exported_txs.m_ptx) { CHECK_AND_ASSERT_MES(ptx.selected_transfers.size() == ptx.tx.vin.size(), false, "Mismatched selected_transfers/vin sizes"); for (size_t idx: ptx.selected_transfers) CHECK_AND_ASSERT_MES(idx < m_transfers.size(), false, "Transfer index out of range"); CHECK_AND_ASSERT_MES(ptx.construction_data.selected_transfers.size() == ptx.tx.vin.size(), false, "Mismatched cd selected_transfers/vin sizes"); for (size_t idx: ptx.construction_data.selected_transfers) CHECK_AND_ASSERT_MES(idx < m_transfers.size(), false, "Transfer index out of range"); CHECK_AND_ASSERT_MES(ptx.construction_data.sources.size() == ptx.tx.vin.size(), false, "Mismatched sources/vin sizes"); } LOG_PRINT_L1("Loaded multisig tx unsigned data from binary: " << exported_txs.m_ptx.size() << " transactions"); for (auto &ptx: exported_txs.m_ptx) LOG_PRINT_L0(cryptonote::obj_to_json_str(ptx.tx)); if (accept_func && !accept_func(exported_txs)) { LOG_PRINT_L1("Transactions rejected by callback"); return false; } const bool is_signed = exported_txs.m_signers.size() >= m_multisig_threshold; if (is_signed) { for (const auto &ptx: exported_txs.m_ptx) { const crypto::hash txid = get_transaction_hash(ptx.tx); if (store_tx_info()) { m_tx_keys.insert(std::make_pair(txid, ptx.tx_key)); m_additional_tx_keys.insert(std::make_pair(txid, ptx.additional_tx_keys)); } } } return true; } //---------------------------------------------------------------------------------------------------- bool wallet2::load_multisig_tx_from_file(const std::string &filename, multisig_tx_set &exported_txs, std::function accept_func) { std::string s; boost::system::error_code errcode; if (!boost::filesystem::exists(filename, errcode)) { LOG_PRINT_L0("File " << filename << " does not exist: " << errcode); return false; } if (!epee::file_io_utils::load_file_to_string(filename.c_str(), s)) { LOG_PRINT_L0("Failed to load from " << filename); return false; } if (!load_multisig_tx(s, exported_txs, accept_func)) { LOG_PRINT_L0("Failed to parse multisig tx data from " << filename); return false; } return true; } //---------------------------------------------------------------------------------------------------- bool wallet2::sign_multisig_tx(multisig_tx_set &exported_txs, std::vector &txids) { THROW_WALLET_EXCEPTION_IF(exported_txs.m_ptx.empty(), error::wallet_internal_error, "No tx found"); const crypto::public_key local_signer = get_multisig_signer_public_key(); THROW_WALLET_EXCEPTION_IF(exported_txs.m_signers.find(local_signer) != exported_txs.m_signers.end(), error::wallet_internal_error, "Transaction already signed by this private key"); THROW_WALLET_EXCEPTION_IF(exported_txs.m_signers.size() > m_multisig_threshold, error::wallet_internal_error, "Transaction was signed by too many signers"); THROW_WALLET_EXCEPTION_IF(exported_txs.m_signers.size() == m_multisig_threshold, error::wallet_internal_error, "Transaction is already fully signed"); txids.clear(); // sign the transactions for (size_t n = 0; n < exported_txs.m_ptx.size(); ++n) { tools::wallet2::pending_tx &ptx = exported_txs.m_ptx[n]; THROW_WALLET_EXCEPTION_IF(ptx.multisig_sigs.empty(), error::wallet_internal_error, "No signatures found in multisig tx"); tools::wallet2::tx_construction_data &sd = ptx.construction_data; LOG_PRINT_L1(" " << (n+1) << ": " << sd.sources.size() << " inputs, mixin " << (sd.sources[0].outputs.size()-1) << ", signed by " << exported_txs.m_signers.size() << "/" << m_multisig_threshold); cryptonote::transaction tx; rct::multisig_out msout = ptx.multisig_sigs.front().msout; auto sources = sd.sources; const bool bulletproof = sd.use_rct && (ptx.tx.rct_signatures.type == rct::RCTTypeFullBulletproof || ptx.tx.rct_signatures.type == rct::RCTTypeSimpleBulletproof); bool r = cryptonote::construct_tx_with_tx_key(m_account.get_keys(), m_subaddresses, sources, sd.splitted_dsts, ptx.change_dts.addr, sd.extra, tx, sd.unlock_time, ptx.tx_key, ptx.additional_tx_keys, sd.use_rct, bulletproof, &msout); THROW_WALLET_EXCEPTION_IF(!r, error::tx_not_constructed, sd.sources, sd.splitted_dsts, sd.unlock_time, m_testnet); THROW_WALLET_EXCEPTION_IF(get_transaction_prefix_hash (tx) != get_transaction_prefix_hash(ptx.tx), error::wallet_internal_error, "Transaction prefix does not match data"); // Tests passed, sign std::vector indices; for (const auto &source: sources) indices.push_back(source.real_output); for (auto &sig: ptx.multisig_sigs) { if (sig.ignore != local_signer) { ptx.tx.rct_signatures = sig.sigs; rct::keyV k; for (size_t idx: sd.selected_transfers) k.push_back(get_multisig_k(idx, sig.used_L)); rct::key skey = rct::zero(); for (const auto &msk: get_account().get_multisig_keys()) { crypto::public_key pmsk = get_multisig_signing_public_key(msk); if (sig.signing_keys.find(pmsk) == sig.signing_keys.end()) { sc_add(skey.bytes, skey.bytes, rct::sk2rct(msk).bytes); sig.signing_keys.insert(pmsk); } } THROW_WALLET_EXCEPTION_IF(!rct::signMultisig(ptx.tx.rct_signatures, indices, k, sig.msout, skey), error::wallet_internal_error, "Failed signing, transaction likely malformed"); sig.sigs = ptx.tx.rct_signatures; } } const bool is_last = exported_txs.m_signers.size() + 1 >= m_multisig_threshold; if (is_last) { // when the last signature on a multisig tx is made, we select the right // signature to plug into the final tx bool found = false; for (const auto &sig: ptx.multisig_sigs) { if (sig.ignore != local_signer && exported_txs.m_signers.find(sig.ignore) == exported_txs.m_signers.end()) { THROW_WALLET_EXCEPTION_IF(found, error::wallet_internal_error, "More than one transaction is final"); ptx.tx.rct_signatures = sig.sigs; found = true; } } THROW_WALLET_EXCEPTION_IF(!found, error::wallet_internal_error, "Final signed transaction not found: this transaction was likely made without our export data, so we cannot sign it"); const crypto::hash txid = get_transaction_hash(ptx.tx); if (store_tx_info()) { m_tx_keys.insert(std::make_pair(txid, ptx.tx_key)); m_additional_tx_keys.insert(std::make_pair(txid, ptx.additional_tx_keys)); } txids.push_back(txid); } } // txes generated, get rid of used k values for (size_t n = 0; n < exported_txs.m_ptx.size(); ++n) for (size_t idx: exported_txs.m_ptx[n].construction_data.selected_transfers) m_transfers[idx].m_multisig_k.clear(); exported_txs.m_signers.insert(get_multisig_signer_public_key()); return true; } //---------------------------------------------------------------------------------------------------- bool wallet2::sign_multisig_tx_to_file(multisig_tx_set &exported_txs, const std::string &filename, std::vector &txids) { bool r = sign_multisig_tx(exported_txs, txids); if (!r) return false; return save_multisig_tx(exported_txs, filename); } //---------------------------------------------------------------------------------------------------- bool wallet2::sign_multisig_tx_from_file(const std::string &filename, std::vector &txids, std::function accept_func) { multisig_tx_set exported_txs; if(!load_multisig_tx_from_file(filename, exported_txs)) return false; if (accept_func && !accept_func(exported_txs)) { LOG_PRINT_L1("Transactions rejected by callback"); return false; } return sign_multisig_tx_to_file(exported_txs, filename, txids); } //---------------------------------------------------------------------------------------------------- uint64_t wallet2::get_fee_multiplier(uint32_t priority, int fee_algorithm) { static const uint64_t old_multipliers[3] = {1, 2, 3}; static const uint64_t new_multipliers[3] = {1, 20, 166}; static const uint64_t newer_multipliers[4] = {1, 4, 20, 166}; if (fee_algorithm == -1) fee_algorithm = get_fee_algorithm(); // 0 -> default (here, x1 till fee algorithm 2, x4 from it) if (priority == 0) priority = m_default_priority; if (priority == 0) { if (fee_algorithm >= 2) priority = 2; else priority = 1; } // 1 to 3/4 are allowed as priorities uint32_t max_priority = (fee_algorithm >= 2) ? 4 : 3; if (priority >= 1 && priority <= max_priority) { switch (fee_algorithm) { case 0: return old_multipliers[priority-1]; case 1: return new_multipliers[priority-1]; case 2: return newer_multipliers[priority-1]; default: THROW_WALLET_EXCEPTION_IF (true, error::invalid_priority); } } THROW_WALLET_EXCEPTION_IF (false, error::invalid_priority); return 1; } //---------------------------------------------------------------------------------------------------- uint64_t wallet2::get_dynamic_per_kb_fee_estimate() { uint64_t fee; boost::optional result = m_node_rpc_proxy.get_dynamic_per_kb_fee_estimate(FEE_ESTIMATE_GRACE_BLOCKS, fee); if (!result) return fee; LOG_PRINT_L1("Failed to query per kB fee, using " << print_money(FEE_PER_KB)); return FEE_PER_KB; } //---------------------------------------------------------------------------------------------------- uint64_t wallet2::get_per_kb_fee() { if(m_light_wallet) return m_light_wallet_per_kb_fee; bool use_dyn_fee = use_fork_rules(HF_VERSION_DYNAMIC_FEE, -720 * 1); if (!use_dyn_fee) return FEE_PER_KB; return get_dynamic_per_kb_fee_estimate(); } //---------------------------------------------------------------------------------------------------- int wallet2::get_fee_algorithm() { // changes at v3 and v5 if (use_fork_rules(5, 0)) return 2; if (use_fork_rules(3, -720 * 14)) return 1; return 0; } //------------------------------------------------------------------------------------------------------------------------------ uint64_t wallet2::adjust_mixin(uint64_t mixin) { if (mixin < 4 && use_fork_rules(6, 10)) { MWARNING("Requested ring size " << (mixin + 1) << " too low for hard fork 6, using 5"); mixin = 4; } else if (mixin < 2 && use_fork_rules(2, 10)) { MWARNING("Requested ring size " << (mixin + 1) << " too low for hard fork 2, using 3"); mixin = 2; } return mixin; } //---------------------------------------------------------------------------------------------------- // separated the call(s) to wallet2::transfer into their own function // // this function will make multiple calls to wallet2::transfer if multiple // transactions will be required std::vector wallet2::create_transactions(std::vector dsts, const size_t fake_outs_count, const uint64_t unlock_time, uint32_t priority, const std::vector& extra, bool trusted_daemon) { const std::vector unused_transfers_indices = select_available_outputs_from_histogram(fake_outs_count + 1, true, true, true, trusted_daemon); const uint64_t fee_per_kb = get_per_kb_fee(); const uint64_t fee_multiplier = get_fee_multiplier(priority, get_fee_algorithm()); // failsafe split attempt counter size_t attempt_count = 0; for(attempt_count = 1; ;attempt_count++) { size_t num_tx = 0.5 + pow(1.7,attempt_count-1); auto split_values = split_amounts(dsts, num_tx); // Throw if split_amounts comes back with a vector of size different than it should if (split_values.size() != num_tx) { throw std::runtime_error("Splitting transactions returned a number of potential tx not equal to what was requested"); } std::vector ptx_vector; try { // for each new destination vector (i.e. for each new tx) for (auto & dst_vector : split_values) { cryptonote::transaction tx; pending_tx ptx; // loop until fee is met without increasing tx size to next KB boundary. const size_t estimated_tx_size = estimate_tx_size(false, unused_transfers_indices.size(), fake_outs_count, dst_vector.size(), extra.size(), false); uint64_t needed_fee = calculate_fee(fee_per_kb, estimated_tx_size, fee_multiplier); do { transfer(dst_vector, fake_outs_count, unused_transfers_indices, unlock_time, needed_fee, extra, tx, ptx, trusted_daemon); auto txBlob = t_serializable_object_to_blob(ptx.tx); needed_fee = calculate_fee(fee_per_kb, txBlob, fee_multiplier); } while (ptx.fee < needed_fee); ptx_vector.push_back(ptx); // mark transfers to be used as "spent" for(size_t idx: ptx.selected_transfers) { set_spent(idx, 0); } } // if we made it this far, we've selected our transactions. committing them will mark them spent, // so this is a failsafe in case they don't go through // unmark pending tx transfers as spent for (auto & ptx : ptx_vector) { // mark transfers to be used as not spent for(size_t idx2: ptx.selected_transfers) { set_unspent(idx2); } } // if we made it this far, we're OK to actually send the transactions return ptx_vector; } // only catch this here, other exceptions need to pass through to the calling function catch (const tools::error::tx_too_big& e) { // unmark pending tx transfers as spent for (auto & ptx : ptx_vector) { // mark transfers to be used as not spent for(size_t idx2: ptx.selected_transfers) { set_unspent(idx2); } } if (attempt_count >= MAX_SPLIT_ATTEMPTS) { throw; } } catch (...) { // in case of some other exception, make sure any tx in queue are marked unspent again // unmark pending tx transfers as spent for (auto & ptx : ptx_vector) { // mark transfers to be used as not spent for(size_t idx2: ptx.selected_transfers) { set_unspent(idx2); } } throw; } } } bool wallet2::tx_add_fake_output(std::vector> &outs, uint64_t global_index, const crypto::public_key& tx_public_key, const rct::key& mask, uint64_t real_index, bool unlocked) const { if (!unlocked) // don't add locked outs return false; if (global_index == real_index) // don't re-add real one return false; auto item = std::make_tuple(global_index, tx_public_key, mask); CHECK_AND_ASSERT_MES(!outs.empty(), false, "internal error: outs is empty"); if (std::find(outs.back().begin(), outs.back().end(), item) != outs.back().end()) // don't add duplicates return false; outs.back().push_back(item); return true; } void wallet2::light_wallet_get_outs(std::vector> &outs, const std::vector &selected_transfers, size_t fake_outputs_count) { MDEBUG("LIGHTWALLET - Getting random outs"); cryptonote::COMMAND_RPC_GET_RANDOM_OUTS::request oreq; cryptonote::COMMAND_RPC_GET_RANDOM_OUTS::response ores; size_t light_wallet_requested_outputs_count = (size_t)((fake_outputs_count + 1) * 1.5 + 1); // Amounts to ask for // MyMonero api handle amounts and fees as strings for(size_t idx: selected_transfers) { const uint64_t ask_amount = m_transfers[idx].is_rct() ? 0 : m_transfers[idx].amount(); std::ostringstream amount_ss; amount_ss << ask_amount; oreq.amounts.push_back(amount_ss.str()); } oreq.count = light_wallet_requested_outputs_count; m_daemon_rpc_mutex.lock(); bool r = epee::net_utils::invoke_http_json("/get_random_outs", oreq, ores, m_http_client, rpc_timeout, "POST"); m_daemon_rpc_mutex.unlock(); THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "get_random_outs"); THROW_WALLET_EXCEPTION_IF(ores.amount_outs.empty() , error::wallet_internal_error, "No outputs recieved from light wallet node. Error: " + ores.Error); // Check if we got enough outputs for each amount for(auto& out: ores.amount_outs) { const uint64_t out_amount = boost::lexical_cast(out.amount); THROW_WALLET_EXCEPTION_IF(out.outputs.size() < light_wallet_requested_outputs_count , error::wallet_internal_error, "Not enough outputs for amount: " + boost::lexical_cast(out.amount)); MDEBUG(out.outputs.size() << " outputs for amount "+ boost::lexical_cast(out.amount) + " received from light wallet node"); } MDEBUG("selected transfers size: " << selected_transfers.size()); for(size_t idx: selected_transfers) { // Create new index outs.push_back(std::vector()); outs.back().reserve(fake_outputs_count + 1); // add real output first const transfer_details &td = m_transfers[idx]; const uint64_t amount = td.is_rct() ? 0 : td.amount(); outs.back().push_back(std::make_tuple(td.m_global_output_index, td.get_public_key(), rct::commit(td.amount(), td.m_mask))); MDEBUG("added real output " << string_tools::pod_to_hex(td.get_public_key())); // Even if the lightwallet server returns random outputs, we pick them randomly. std::vector order; order.resize(light_wallet_requested_outputs_count); for (size_t n = 0; n < order.size(); ++n) order[n] = n; std::shuffle(order.begin(), order.end(), std::default_random_engine(crypto::rand())); LOG_PRINT_L2("Looking for " << (fake_outputs_count+1) << " outputs with amounts " << print_money(td.is_rct() ? 0 : td.amount())); MDEBUG("OUTS SIZE: " << outs.back().size()); for (size_t o = 0; o < light_wallet_requested_outputs_count && outs.back().size() < fake_outputs_count + 1; ++o) { // Random pick size_t i = order[o]; // Find which random output key to use bool found_amount = false; size_t amount_key; for(amount_key = 0; amount_key < ores.amount_outs.size(); ++amount_key) { if(boost::lexical_cast(ores.amount_outs[amount_key].amount) == amount) { found_amount = true; break; } } THROW_WALLET_EXCEPTION_IF(!found_amount , error::wallet_internal_error, "Outputs for amount " + boost::lexical_cast(ores.amount_outs[amount_key].amount) + " not found" ); LOG_PRINT_L2("Index " << i << "/" << light_wallet_requested_outputs_count << ": idx " << ores.amount_outs[amount_key].outputs[i].global_index << " (real " << td.m_global_output_index << "), unlocked " << "(always in light)" << ", key " << ores.amount_outs[0].outputs[i].public_key); // Convert light wallet string data to proper data structures crypto::public_key tx_public_key; rct::key mask = AUTO_VAL_INIT(mask); // decrypted mask - not used here rct::key rct_commit = AUTO_VAL_INIT(rct_commit); THROW_WALLET_EXCEPTION_IF(string_tools::validate_hex(64, ores.amount_outs[amount_key].outputs[i].public_key), error::wallet_internal_error, "Invalid public_key"); string_tools::hex_to_pod(ores.amount_outs[amount_key].outputs[i].public_key, tx_public_key); const uint64_t global_index = ores.amount_outs[amount_key].outputs[i].global_index; if(!light_wallet_parse_rct_str(ores.amount_outs[amount_key].outputs[i].rct, tx_public_key, 0, mask, rct_commit, false)) rct_commit = rct::zeroCommit(td.amount()); if (tx_add_fake_output(outs, global_index, tx_public_key, rct_commit, td.m_global_output_index, true)) { MDEBUG("added fake output " << ores.amount_outs[amount_key].outputs[i].public_key); MDEBUG("index " << global_index); } } THROW_WALLET_EXCEPTION_IF(outs.back().size() < fake_outputs_count + 1 , error::wallet_internal_error, "Not enough fake outputs found" ); // Real output is the first. Shuffle outputs MTRACE(outs.back().size() << " outputs added. Sorting outputs by index:"); std::sort(outs.back().begin(), outs.back().end(), [](const get_outs_entry &a, const get_outs_entry &b) { return std::get<0>(a) < std::get<0>(b); }); // Print output order for(auto added_out: outs.back()) MTRACE(std::get<0>(added_out)); } } void wallet2::get_outs(std::vector> &outs, const std::vector &selected_transfers, size_t fake_outputs_count) { LOG_PRINT_L2("fake_outputs_count: " << fake_outputs_count); outs.clear(); if(m_light_wallet && fake_outputs_count > 0) { light_wallet_get_outs(outs, selected_transfers, fake_outputs_count); return; } if (fake_outputs_count > 0) { // get histogram for the amounts we need epee::json_rpc::request req_t = AUTO_VAL_INIT(req_t); epee::json_rpc::response resp_t = AUTO_VAL_INIT(resp_t); m_daemon_rpc_mutex.lock(); req_t.jsonrpc = "2.0"; req_t.id = epee::serialization::storage_entry(0); req_t.method = "get_output_histogram"; for(size_t idx: selected_transfers) req_t.params.amounts.push_back(m_transfers[idx].is_rct() ? 0 : m_transfers[idx].amount()); std::sort(req_t.params.amounts.begin(), req_t.params.amounts.end()); auto end = std::unique(req_t.params.amounts.begin(), req_t.params.amounts.end()); req_t.params.amounts.resize(std::distance(req_t.params.amounts.begin(), end)); req_t.params.unlocked = true; req_t.params.recent_cutoff = time(NULL) - RECENT_OUTPUT_ZONE; bool r = net_utils::invoke_http_json("/json_rpc", req_t, resp_t, m_http_client, rpc_timeout); m_daemon_rpc_mutex.unlock(); THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "transfer_selected"); THROW_WALLET_EXCEPTION_IF(resp_t.result.status == CORE_RPC_STATUS_BUSY, error::daemon_busy, "get_output_histogram"); THROW_WALLET_EXCEPTION_IF(resp_t.result.status != CORE_RPC_STATUS_OK, error::get_histogram_error, resp_t.result.status); // we ask for more, to have spares if some outputs are still locked size_t base_requested_outputs_count = (size_t)((fake_outputs_count + 1) * 1.5 + 1); LOG_PRINT_L2("base_requested_outputs_count: " << base_requested_outputs_count); // generate output indices to request COMMAND_RPC_GET_OUTPUTS_BIN::request req = AUTO_VAL_INIT(req); COMMAND_RPC_GET_OUTPUTS_BIN::response daemon_resp = AUTO_VAL_INIT(daemon_resp); size_t num_selected_transfers = 0; for(size_t idx: selected_transfers) { ++num_selected_transfers; const transfer_details &td = m_transfers[idx]; const uint64_t amount = td.is_rct() ? 0 : td.amount(); std::unordered_set seen_indices; // request more for rct in base recent (locked) coinbases are picked, since they're locked for longer size_t requested_outputs_count = base_requested_outputs_count + (td.is_rct() ? CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW - CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE : 0); size_t start = req.outputs.size(); // if there are just enough outputs to mix with, use all of them. // Eventually this should become impossible. uint64_t num_outs = 0, num_recent_outs = 0; for (const auto &he: resp_t.result.histogram) { if (he.amount == amount) { LOG_PRINT_L2("Found " << print_money(amount) << ": " << he.total_instances << " total, " << he.unlocked_instances << " unlocked, " << he.recent_instances << " recent"); num_outs = he.unlocked_instances; num_recent_outs = he.recent_instances; break; } } LOG_PRINT_L1("" << num_outs << " unlocked outputs of size " << print_money(amount)); THROW_WALLET_EXCEPTION_IF(num_outs == 0, error::wallet_internal_error, "histogram reports no unlocked outputs for " + boost::lexical_cast(amount) + ", not even ours"); THROW_WALLET_EXCEPTION_IF(num_recent_outs > num_outs, error::wallet_internal_error, "histogram reports more recent outs than outs for " + boost::lexical_cast(amount)); // X% of those outs are to be taken from recent outputs size_t recent_outputs_count = requested_outputs_count * RECENT_OUTPUT_RATIO; if (recent_outputs_count == 0) recent_outputs_count = 1; // ensure we have at least one, if possible if (recent_outputs_count > num_recent_outs) recent_outputs_count = num_recent_outs; if (td.m_global_output_index >= num_outs - num_recent_outs && recent_outputs_count > 0) --recent_outputs_count; // if the real out is recent, pick one less recent fake out LOG_PRINT_L1("Using " << recent_outputs_count << " recent outputs"); if (num_outs <= requested_outputs_count) { for (uint64_t i = 0; i < num_outs; i++) req.outputs.push_back({amount, i}); // duplicate to make up shortfall: this will be caught after the RPC call, // so we can also output the amounts for which we can't reach the required // mixin after checking the actual unlockedness for (uint64_t i = num_outs; i < requested_outputs_count; ++i) req.outputs.push_back({amount, num_outs - 1}); } else { // start with real one uint64_t num_found = 1; seen_indices.emplace(td.m_global_output_index); req.outputs.push_back({amount, td.m_global_output_index}); LOG_PRINT_L1("Selecting real output: " << td.m_global_output_index << " for " << print_money(amount)); // while we still need more mixins while (num_found < requested_outputs_count) { // if we've gone through every possible output, we've gotten all we can if (seen_indices.size() == num_outs) break; // get a random output index from the DB. If we've already seen it, // return to the top of the loop and try again, otherwise add it to the // list of output indices we've seen. uint64_t i; if (num_found - 1 < recent_outputs_count) // -1 to account for the real one we seeded with { // triangular distribution over [a,b) with a=0, mode c=b=up_index_limit uint64_t r = crypto::rand() % ((uint64_t)1 << 53); double frac = std::sqrt((double)r / ((uint64_t)1 << 53)); i = (uint64_t)(frac*num_recent_outs) + num_outs - num_recent_outs; // just in case rounding up to 1 occurs after calc if (i == num_outs) --i; LOG_PRINT_L2("picking " << i << " as recent"); } else { // triangular distribution over [a,b) with a=0, mode c=b=up_index_limit uint64_t r = crypto::rand() % ((uint64_t)1 << 53); double frac = std::sqrt((double)r / ((uint64_t)1 << 53)); i = (uint64_t)(frac*num_outs); // just in case rounding up to 1 occurs after calc if (i == num_outs) --i; LOG_PRINT_L2("picking " << i << " as triangular"); } if (seen_indices.count(i)) continue; seen_indices.emplace(i); req.outputs.push_back({amount, i}); ++num_found; } } // sort the subsection, to ensure the daemon doesn't know wich output is ours std::sort(req.outputs.begin() + start, req.outputs.end(), [](const get_outputs_out &a, const get_outputs_out &b) { return a.index < b.index; }); } for (auto i: req.outputs) LOG_PRINT_L1("asking for output " << i.index << " for " << print_money(i.amount)); // get the keys for those m_daemon_rpc_mutex.lock(); r = epee::net_utils::invoke_http_bin("/get_outs.bin", req, daemon_resp, m_http_client, rpc_timeout); m_daemon_rpc_mutex.unlock(); THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "get_outs.bin"); THROW_WALLET_EXCEPTION_IF(daemon_resp.status == CORE_RPC_STATUS_BUSY, error::daemon_busy, "get_outs.bin"); THROW_WALLET_EXCEPTION_IF(daemon_resp.status != CORE_RPC_STATUS_OK, error::get_random_outs_error, daemon_resp.status); THROW_WALLET_EXCEPTION_IF(daemon_resp.outs.size() != req.outputs.size(), error::wallet_internal_error, "daemon returned wrong response for get_outs.bin, wrong amounts count = " + std::to_string(daemon_resp.outs.size()) + ", expected " + std::to_string(req.outputs.size())); std::unordered_map scanty_outs; size_t base = 0; outs.reserve(num_selected_transfers); for(size_t idx: selected_transfers) { const transfer_details &td = m_transfers[idx]; size_t requested_outputs_count = base_requested_outputs_count + (td.is_rct() ? CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW - CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE : 0); outs.push_back(std::vector()); outs.back().reserve(fake_outputs_count + 1); const rct::key mask = td.is_rct() ? rct::commit(td.amount(), td.m_mask) : rct::zeroCommit(td.amount()); // make sure the real outputs we asked for are really included, along // with the correct key and mask: this guards against an active attack // where the node sends dummy data for all outputs, and we then send // the real one, which the node can then tell from the fake outputs, // as it has different data than the dummy data it had sent earlier bool real_out_found = false; for (size_t n = 0; n < requested_outputs_count; ++n) { size_t i = base + n; if (req.outputs[i].index == td.m_global_output_index) if (daemon_resp.outs[i].key == boost::get(td.m_tx.vout[td.m_internal_output_index].target).key) if (daemon_resp.outs[i].mask == mask) real_out_found = true; } THROW_WALLET_EXCEPTION_IF(!real_out_found, error::wallet_internal_error, "Daemon response did not include the requested real output"); // pick real out first (it will be sorted when done) outs.back().push_back(std::make_tuple(td.m_global_output_index, boost::get(td.m_tx.vout[td.m_internal_output_index].target).key, mask)); // then pick others in random order till we reach the required number // since we use an equiprobable pick here, we don't upset the triangular distribution std::vector order; order.resize(requested_outputs_count); for (size_t n = 0; n < order.size(); ++n) order[n] = n; std::shuffle(order.begin(), order.end(), std::default_random_engine(crypto::rand())); LOG_PRINT_L2("Looking for " << (fake_outputs_count+1) << " outputs of size " << print_money(td.is_rct() ? 0 : td.amount())); for (size_t o = 0; o < requested_outputs_count && outs.back().size() < fake_outputs_count + 1; ++o) { size_t i = base + order[o]; LOG_PRINT_L2("Index " << i << "/" << requested_outputs_count << ": idx " << req.outputs[i].index << " (real " << td.m_global_output_index << "), unlocked " << daemon_resp.outs[i].unlocked << ", key " << daemon_resp.outs[i].key); tx_add_fake_output(outs, req.outputs[i].index, daemon_resp.outs[i].key, daemon_resp.outs[i].mask, td.m_global_output_index, daemon_resp.outs[i].unlocked); } if (outs.back().size() < fake_outputs_count + 1) { scanty_outs[td.is_rct() ? 0 : td.amount()] = outs.back().size(); } else { // sort the subsection, so any spares are reset in order std::sort(outs.back().begin(), outs.back().end(), [](const get_outs_entry &a, const get_outs_entry &b) { return std::get<0>(a) < std::get<0>(b); }); } base += requested_outputs_count; } THROW_WALLET_EXCEPTION_IF(!scanty_outs.empty(), error::not_enough_outs_to_mix, scanty_outs, fake_outputs_count); } else { for (size_t idx: selected_transfers) { const transfer_details &td = m_transfers[idx]; std::vector v; const rct::key mask = td.is_rct() ? rct::commit(td.amount(), td.m_mask) : rct::zeroCommit(td.amount()); v.push_back(std::make_tuple(td.m_global_output_index, td.get_public_key(), mask)); outs.push_back(v); } } } template void wallet2::transfer_selected(const std::vector& dsts, const std::vector& selected_transfers, size_t fake_outputs_count, std::vector> &outs, uint64_t unlock_time, uint64_t fee, const std::vector& extra, T destination_split_strategy, const tx_dust_policy& dust_policy, cryptonote::transaction& tx, pending_tx &ptx) { using namespace cryptonote; // throw if attempting a transaction with no destinations THROW_WALLET_EXCEPTION_IF(dsts.empty(), error::zero_destination); THROW_WALLET_EXCEPTION_IF(m_multisig, error::wallet_internal_error, "Multisig wallets cannot spend non rct outputs"); uint64_t upper_transaction_size_limit = get_upper_transaction_size_limit(); uint64_t needed_money = fee; LOG_PRINT_L2("transfer: starting with fee " << print_money (needed_money)); // calculate total amount being sent to all destinations // throw if total amount overflows uint64_t for(auto& dt: dsts) { THROW_WALLET_EXCEPTION_IF(0 == dt.amount, error::zero_destination); needed_money += dt.amount; LOG_PRINT_L2("transfer: adding " << print_money(dt.amount) << ", for a total of " << print_money (needed_money)); THROW_WALLET_EXCEPTION_IF(needed_money < dt.amount, error::tx_sum_overflow, dsts, fee, m_testnet); } uint64_t found_money = 0; for(size_t idx: selected_transfers) { found_money += m_transfers[idx].amount(); } LOG_PRINT_L2("wanted " << print_money(needed_money) << ", found " << print_money(found_money) << ", fee " << print_money(fee)); THROW_WALLET_EXCEPTION_IF(found_money < needed_money, error::not_enough_unlocked_money, found_money, needed_money - fee, fee); uint32_t subaddr_account = m_transfers[*selected_transfers.begin()].m_subaddr_index.major; for (auto i = ++selected_transfers.begin(); i != selected_transfers.end(); ++i) THROW_WALLET_EXCEPTION_IF(subaddr_account != m_transfers[*i].m_subaddr_index.major, error::wallet_internal_error, "the tx uses funds from multiple accounts"); if (outs.empty()) get_outs(outs, selected_transfers, fake_outputs_count); // may throw //prepare inputs LOG_PRINT_L2("preparing outputs"); typedef cryptonote::tx_source_entry::output_entry tx_output_entry; size_t i = 0, out_index = 0; std::vector sources; for(size_t idx: selected_transfers) { sources.resize(sources.size()+1); cryptonote::tx_source_entry& src = sources.back(); const transfer_details& td = m_transfers[idx]; src.amount = td.amount(); src.rct = td.is_rct(); //paste keys (fake and real) for (size_t n = 0; n < fake_outputs_count + 1; ++n) { tx_output_entry oe; oe.first = std::get<0>(outs[out_index][n]); oe.second.dest = rct::pk2rct(std::get<1>(outs[out_index][n])); oe.second.mask = std::get<2>(outs[out_index][n]); src.outputs.push_back(oe); ++i; } //paste real transaction to the random index auto it_to_replace = std::find_if(src.outputs.begin(), src.outputs.end(), [&](const tx_output_entry& a) { return a.first == td.m_global_output_index; }); THROW_WALLET_EXCEPTION_IF(it_to_replace == src.outputs.end(), error::wallet_internal_error, "real output not found"); tx_output_entry real_oe; real_oe.first = td.m_global_output_index; real_oe.second.dest = rct::pk2rct(boost::get(td.m_tx.vout[td.m_internal_output_index].target).key); real_oe.second.mask = rct::commit(td.amount(), td.m_mask); *it_to_replace = real_oe; src.real_out_tx_key = get_tx_pub_key_from_extra(td.m_tx, td.m_pk_index); src.real_out_additional_tx_keys = get_additional_tx_pub_keys_from_extra(td.m_tx); src.real_output = it_to_replace - src.outputs.begin(); src.real_output_in_tx_index = td.m_internal_output_index; src.multisig_kLRki = rct::multisig_kLRki({rct::zero(), rct::zero(), rct::zero(), rct::zero()}); detail::print_source_entry(src); ++out_index; } LOG_PRINT_L2("outputs prepared"); cryptonote::tx_destination_entry change_dts = AUTO_VAL_INIT(change_dts); if (needed_money < found_money) { change_dts.addr = get_subaddress({subaddr_account, 0}); change_dts.amount = found_money - needed_money; } std::vector splitted_dsts, dust_dsts; uint64_t dust = 0; destination_split_strategy(dsts, change_dts, dust_policy.dust_threshold, splitted_dsts, dust_dsts); for(auto& d: dust_dsts) { THROW_WALLET_EXCEPTION_IF(dust_policy.dust_threshold < d.amount, error::wallet_internal_error, "invalid dust value: dust = " + std::to_string(d.amount) + ", dust_threshold = " + std::to_string(dust_policy.dust_threshold)); } for(auto& d: dust_dsts) { if (!dust_policy.add_to_fee) splitted_dsts.push_back(cryptonote::tx_destination_entry(d.amount, dust_policy.addr_for_dust, d.is_subaddress)); dust += d.amount; } crypto::secret_key tx_key; std::vector additional_tx_keys; rct::multisig_out msout; LOG_PRINT_L2("constructing tx"); bool r = cryptonote::construct_tx_and_get_tx_key(m_account.get_keys(), m_subaddresses, sources, splitted_dsts, change_dts.addr, extra, tx, unlock_time, tx_key, additional_tx_keys, false, false, m_multisig ? &msout : NULL); LOG_PRINT_L2("constructed tx, r="< bool { CHECKED_GET_SPECIFIC_VARIANT(s_e, const txin_to_key, in, false); key_images += boost::to_string(in.k_image) + " "; return true; }); THROW_WALLET_EXCEPTION_IF(!all_are_txin_to_key, error::unexpected_txin_type, tx); bool dust_sent_elsewhere = (dust_policy.addr_for_dust.m_view_public_key != change_dts.addr.m_view_public_key || dust_policy.addr_for_dust.m_spend_public_key != change_dts.addr.m_spend_public_key); if (dust_policy.add_to_fee || dust_sent_elsewhere) change_dts.amount -= dust; ptx.key_images = key_images; ptx.fee = (dust_policy.add_to_fee ? fee+dust : fee); ptx.dust = ((dust_policy.add_to_fee || dust_sent_elsewhere) ? dust : 0); ptx.dust_added_to_fee = dust_policy.add_to_fee; ptx.tx = tx; ptx.change_dts = change_dts; ptx.selected_transfers = selected_transfers; ptx.tx_key = tx_key; ptx.additional_tx_keys = additional_tx_keys; ptx.dests = dsts; ptx.construction_data.sources = sources; ptx.construction_data.change_dts = change_dts; ptx.construction_data.splitted_dsts = splitted_dsts; ptx.construction_data.selected_transfers = selected_transfers; ptx.construction_data.extra = tx.extra; ptx.construction_data.unlock_time = unlock_time; ptx.construction_data.use_rct = false; ptx.construction_data.dests = dsts; // record which subaddress indices are being used as inputs ptx.construction_data.subaddr_account = subaddr_account; ptx.construction_data.subaddr_indices.clear(); for (size_t idx: selected_transfers) ptx.construction_data.subaddr_indices.insert(m_transfers[idx].m_subaddr_index.minor); LOG_PRINT_L2("transfer_selected done"); } void wallet2::transfer_selected_rct(std::vector dsts, const std::vector& selected_transfers, size_t fake_outputs_count, std::vector> &outs, uint64_t unlock_time, uint64_t fee, const std::vector& extra, cryptonote::transaction& tx, pending_tx &ptx, bool bulletproof) { using namespace cryptonote; // throw if attempting a transaction with no destinations THROW_WALLET_EXCEPTION_IF(dsts.empty(), error::zero_destination); uint64_t upper_transaction_size_limit = get_upper_transaction_size_limit(); uint64_t needed_money = fee; LOG_PRINT_L2("transfer_selected_rct: starting with fee " << print_money (needed_money)); LOG_PRINT_L2("selected transfers: " << strjoin(selected_transfers, " ")); // calculate total amount being sent to all destinations // throw if total amount overflows uint64_t for(auto& dt: dsts) { THROW_WALLET_EXCEPTION_IF(0 == dt.amount, error::zero_destination); needed_money += dt.amount; LOG_PRINT_L2("transfer: adding " << print_money(dt.amount) << ", for a total of " << print_money (needed_money)); THROW_WALLET_EXCEPTION_IF(needed_money < dt.amount, error::tx_sum_overflow, dsts, fee, m_testnet); } // if this is a multisig wallet, create a list of multisig signers we can use std::deque multisig_signers; size_t n_multisig_txes = 0; if (m_multisig && !m_transfers.empty()) { const crypto::public_key local_signer = get_multisig_signer_public_key(); size_t n_available_signers = 1; for (const crypto::public_key &signer: m_multisig_signers) { if (signer == local_signer) continue; multisig_signers.push_front(signer); for (const auto &i: m_transfers[0].m_multisig_info) { if (i.m_signer == signer) { multisig_signers.pop_front(); multisig_signers.push_back(signer); ++n_available_signers; break; } } } multisig_signers.push_back(local_signer); MDEBUG("We can use " << n_available_signers << "/" << m_multisig_signers.size() << " other signers"); THROW_WALLET_EXCEPTION_IF(n_available_signers+1 < m_multisig_threshold, error::multisig_import_needed); n_multisig_txes = n_available_signers == m_multisig_signers.size() ? m_multisig_threshold : 1; MDEBUG("We will create " << n_multisig_txes << " txes"); } uint64_t found_money = 0; for(size_t idx: selected_transfers) { found_money += m_transfers[idx].amount(); } LOG_PRINT_L2("wanted " << print_money(needed_money) << ", found " << print_money(found_money) << ", fee " << print_money(fee)); THROW_WALLET_EXCEPTION_IF(found_money < needed_money, error::not_enough_unlocked_money, found_money, needed_money - fee, fee); uint32_t subaddr_account = m_transfers[*selected_transfers.begin()].m_subaddr_index.major; for (auto i = ++selected_transfers.begin(); i != selected_transfers.end(); ++i) THROW_WALLET_EXCEPTION_IF(subaddr_account != m_transfers[*i].m_subaddr_index.major, error::wallet_internal_error, "the tx uses funds from multiple accounts"); if (outs.empty()) get_outs(outs, selected_transfers, fake_outputs_count); // may throw //prepare inputs LOG_PRINT_L2("preparing outputs"); size_t i = 0, out_index = 0; std::vector sources; std::unordered_set used_L; for(size_t idx: selected_transfers) { sources.resize(sources.size()+1); cryptonote::tx_source_entry& src = sources.back(); const transfer_details& td = m_transfers[idx]; src.amount = td.amount(); src.rct = td.is_rct(); //paste mixin transaction THROW_WALLET_EXCEPTION_IF(outs.size() < out_index + 1 , error::wallet_internal_error, "outs.size() < out_index + 1"); THROW_WALLET_EXCEPTION_IF(outs[out_index].size() < fake_outputs_count , error::wallet_internal_error, "fake_outputs_count > random outputs found"); typedef cryptonote::tx_source_entry::output_entry tx_output_entry; for (size_t n = 0; n < fake_outputs_count + 1; ++n) { tx_output_entry oe; oe.first = std::get<0>(outs[out_index][n]); oe.second.dest = rct::pk2rct(std::get<1>(outs[out_index][n])); oe.second.mask = std::get<2>(outs[out_index][n]); src.outputs.push_back(oe); } ++i; //paste real transaction to the random index auto it_to_replace = std::find_if(src.outputs.begin(), src.outputs.end(), [&](const tx_output_entry& a) { return a.first == td.m_global_output_index; }); THROW_WALLET_EXCEPTION_IF(it_to_replace == src.outputs.end(), error::wallet_internal_error, "real output not found"); tx_output_entry real_oe; real_oe.first = td.m_global_output_index; real_oe.second.dest = rct::pk2rct(td.get_public_key()); real_oe.second.mask = rct::commit(td.amount(), td.m_mask); *it_to_replace = real_oe; src.real_out_tx_key = get_tx_pub_key_from_extra(td.m_tx, td.m_pk_index); src.real_out_additional_tx_keys = get_additional_tx_pub_keys_from_extra(td.m_tx); src.real_output = it_to_replace - src.outputs.begin(); src.real_output_in_tx_index = td.m_internal_output_index; src.mask = td.m_mask; if (m_multisig) { crypto::public_key ignore = m_multisig_threshold == m_multisig_signers.size() ? crypto::null_pkey : multisig_signers.front(); src.multisig_kLRki = get_multisig_composite_kLRki(idx, ignore, used_L, used_L); } else src.multisig_kLRki = rct::multisig_kLRki({rct::zero(), rct::zero(), rct::zero(), rct::zero()}); detail::print_source_entry(src); ++out_index; } LOG_PRINT_L2("outputs prepared"); // we still keep a copy, since we want to keep dsts free of change for user feedback purposes std::vector splitted_dsts = dsts; cryptonote::tx_destination_entry change_dts = AUTO_VAL_INIT(change_dts); change_dts.amount = found_money - needed_money; if (change_dts.amount == 0) { if (splitted_dsts.size() == 1) { // If the change is 0, send it to a random address, to avoid confusing // the sender with a 0 amount output. We send a 0 amount in order to avoid // letting the destination be able to work out which of the inputs is the // real one in our rings LOG_PRINT_L2("generating dummy address for 0 change"); cryptonote::account_base dummy; dummy.generate(); change_dts.addr = dummy.get_keys().m_account_address; LOG_PRINT_L2("generated dummy address for 0 change"); splitted_dsts.push_back(change_dts); } } else { change_dts.addr = get_subaddress({subaddr_account, 0}); splitted_dsts.push_back(change_dts); } crypto::secret_key tx_key; std::vector additional_tx_keys; rct::multisig_out msout; LOG_PRINT_L2("constructing tx"); auto sources_copy = sources; bool r = cryptonote::construct_tx_and_get_tx_key(m_account.get_keys(), m_subaddresses, sources, splitted_dsts, change_dts.addr, extra, tx, unlock_time, tx_key, additional_tx_keys, true, bulletproof, m_multisig ? &msout : NULL); LOG_PRINT_L2("constructed tx, r="< ins_order; for (size_t n = 0; n < sources.size(); ++n) { for (size_t idx = 0; idx < sources_copy.size(); ++idx) { THROW_WALLET_EXCEPTION_IF((size_t)sources_copy[idx].real_output >= sources_copy[idx].outputs.size(), error::wallet_internal_error, "Invalid real_output"); if (sources_copy[idx].outputs[sources_copy[idx].real_output].second.dest == sources[n].outputs[sources[n].real_output].second.dest) ins_order.push_back(idx); } } THROW_WALLET_EXCEPTION_IF(ins_order.size() != sources.size(), error::wallet_internal_error, "Failed to work out sources permutation"); std::vector multisig_sigs; if (m_multisig) { crypto::public_key ignore = m_multisig_threshold == m_multisig_signers.size() ? crypto::null_pkey : multisig_signers.front(); multisig_sigs.push_back({tx.rct_signatures, ignore, used_L, {}, msout}); if (m_multisig_threshold < m_multisig_signers.size()) { const crypto::hash prefix_hash = cryptonote::get_transaction_prefix_hash(tx); // create the other versions, one for every other participant (the first one's already done above) for (size_t signer_index = 1; signer_index < n_multisig_txes; ++signer_index) { std::unordered_set new_used_L; size_t src_idx = 0; THROW_WALLET_EXCEPTION_IF(selected_transfers.size() != sources.size(), error::wallet_internal_error, "mismatched selected_transfers and sources sixes"); for(size_t idx: selected_transfers) { cryptonote::tx_source_entry& src = sources[src_idx]; src.multisig_kLRki = get_multisig_composite_kLRki(idx, multisig_signers[signer_index], used_L, new_used_L); ++src_idx; } LOG_PRINT_L2("Creating supplementary multisig transaction"); cryptonote::transaction ms_tx; auto sources_copy_copy = sources_copy; bool r = cryptonote::construct_tx_with_tx_key(m_account.get_keys(), m_subaddresses, sources_copy_copy, splitted_dsts, change_dts.addr, extra, ms_tx, unlock_time,tx_key, additional_tx_keys, true, bulletproof, &msout); LOG_PRINT_L2("constructed tx, r="< bool { CHECKED_GET_SPECIFIC_VARIANT(s_e, const txin_to_key, in, false); key_images += boost::to_string(in.k_image) + " "; return true; }); THROW_WALLET_EXCEPTION_IF(!all_are_txin_to_key, error::unexpected_txin_type, tx); LOG_PRINT_L2("gathered key images"); ptx.key_images = key_images; ptx.fee = fee; ptx.dust = 0; ptx.dust_added_to_fee = false; ptx.tx = tx; ptx.change_dts = change_dts; ptx.selected_transfers = selected_transfers; tools::apply_permutation(ins_order, ptx.selected_transfers); ptx.tx_key = tx_key; ptx.additional_tx_keys = additional_tx_keys; ptx.dests = dsts; ptx.multisig_sigs = multisig_sigs; ptx.construction_data.sources = sources_copy; ptx.construction_data.change_dts = change_dts; ptx.construction_data.splitted_dsts = splitted_dsts; ptx.construction_data.selected_transfers = ptx.selected_transfers; ptx.construction_data.extra = tx.extra; ptx.construction_data.unlock_time = unlock_time; ptx.construction_data.use_rct = true; ptx.construction_data.dests = dsts; // record which subaddress indices are being used as inputs ptx.construction_data.subaddr_account = subaddr_account; ptx.construction_data.subaddr_indices.clear(); for (size_t idx: selected_transfers) ptx.construction_data.subaddr_indices.insert(m_transfers[idx].m_subaddr_index.minor); LOG_PRINT_L2("transfer_selected_rct done"); } std::vector wallet2::pick_preferred_rct_inputs(uint64_t needed_money, uint32_t subaddr_account, const std::set &subaddr_indices) const { std::vector picks; float current_output_relatdness = 1.0f; LOG_PRINT_L2("pick_preferred_rct_inputs: needed_money " << print_money(needed_money)); // try to find a rct input of enough size for (size_t i = 0; i < m_transfers.size(); ++i) { const transfer_details& td = m_transfers[i]; if (!td.m_spent && td.is_rct() && td.amount() >= needed_money && is_transfer_unlocked(td) && td.m_subaddr_index.major == subaddr_account && subaddr_indices.count(td.m_subaddr_index.minor) == 1) { LOG_PRINT_L2("We can use " << i << " alone: " << print_money(td.amount())); picks.push_back(i); return picks; } } // then try to find two outputs // this could be made better by picking one of the outputs to be a small one, since those // are less useful since often below the needed money, so if one can be used in a pair, // it gets rid of it for the future for (size_t i = 0; i < m_transfers.size(); ++i) { const transfer_details& td = m_transfers[i]; if (!td.m_spent && !td.m_key_image_partial && td.is_rct() && is_transfer_unlocked(td) && td.m_subaddr_index.major == subaddr_account && subaddr_indices.count(td.m_subaddr_index.minor) == 1) { LOG_PRINT_L2("Considering input " << i << ", " << print_money(td.amount())); for (size_t j = i + 1; j < m_transfers.size(); ++j) { const transfer_details& td2 = m_transfers[j]; if (!td2.m_spent && !td.m_key_image_partial && td2.is_rct() && td.amount() + td2.amount() >= needed_money && is_transfer_unlocked(td2) && td2.m_subaddr_index == td.m_subaddr_index) { // update our picks if those outputs are less related than any we // already found. If the same, don't update, and oldest suitable outputs // will be used in preference. float relatedness = get_output_relatedness(td, td2); LOG_PRINT_L2(" with input " << j << ", " << print_money(td2.amount()) << ", relatedness " << relatedness); if (relatedness < current_output_relatdness) { // reset the current picks with those, and return them directly // if they're unrelated. If they are related, we'll end up returning // them if we find nothing better picks.clear(); picks.push_back(i); picks.push_back(j); LOG_PRINT_L0("we could use " << i << " and " << j); if (relatedness == 0.0f) return picks; current_output_relatdness = relatedness; } } } } } return picks; } bool wallet2::should_pick_a_second_output(bool use_rct, size_t n_transfers, const std::vector &unused_transfers_indices, const std::vector &unused_dust_indices) const { if (!use_rct) return false; if (n_transfers > 1) return false; if (unused_dust_indices.empty() && unused_transfers_indices.empty()) return false; // we want at least one free rct output to avoid a corner case where // we'd choose a non rct output which doesn't have enough "siblings" // value-wise on the chain, and thus can't be mixed bool found = false; for (auto i: unused_dust_indices) { if (m_transfers[i].is_rct()) { found = true; break; } } if (!found) for (auto i: unused_transfers_indices) { if (m_transfers[i].is_rct()) { found = true; break; } } if (!found) return false; return true; } std::vector wallet2::get_only_rct(const std::vector &unused_dust_indices, const std::vector &unused_transfers_indices) const { std::vector indices; for (size_t n: unused_dust_indices) if (m_transfers[n].is_rct()) indices.push_back(n); for (size_t n: unused_transfers_indices) if (m_transfers[n].is_rct()) indices.push_back(n); return indices; } static uint32_t get_count_above(const std::vector &transfers, const std::vector &indices, uint64_t threshold) { uint32_t count = 0; for (size_t idx: indices) if (transfers[idx].amount() >= threshold) ++count; return count; } bool wallet2::light_wallet_login(bool &new_address) { MDEBUG("Light wallet login request"); m_light_wallet_connected = false; cryptonote::COMMAND_RPC_LOGIN::request request; cryptonote::COMMAND_RPC_LOGIN::response response; request.address = get_account().get_public_address_str(m_testnet); request.view_key = string_tools::pod_to_hex(get_account().get_keys().m_view_secret_key); // Always create account if it doesnt exist. request.create_account = true; m_daemon_rpc_mutex.lock(); bool connected = epee::net_utils::invoke_http_json("/login", request, response, m_http_client, rpc_timeout, "POST"); m_daemon_rpc_mutex.unlock(); // MyMonero doesn't send any status message. OpenMonero does. m_light_wallet_connected = connected && (response.status.empty() || response.status == "success"); new_address = response.new_address; MDEBUG("Status: " << response.status); MDEBUG("Reason: " << response.reason); MDEBUG("New wallet: " << response.new_address); if(m_light_wallet_connected) { // Clear old data on successfull login. // m_transfers.clear(); // m_payments.clear(); // m_unconfirmed_payments.clear(); } return m_light_wallet_connected; } bool wallet2::light_wallet_import_wallet_request(cryptonote::COMMAND_RPC_IMPORT_WALLET_REQUEST::response &response) { MDEBUG("Light wallet import wallet request"); cryptonote::COMMAND_RPC_IMPORT_WALLET_REQUEST::request oreq; oreq.address = get_account().get_public_address_str(m_testnet); oreq.view_key = string_tools::pod_to_hex(get_account().get_keys().m_view_secret_key); m_daemon_rpc_mutex.lock(); bool r = epee::net_utils::invoke_http_json("/import_wallet_request", oreq, response, m_http_client, rpc_timeout, "POST"); m_daemon_rpc_mutex.unlock(); THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "import_wallet_request"); return true; } void wallet2::light_wallet_get_unspent_outs() { MDEBUG("Getting unspent outs"); cryptonote::COMMAND_RPC_GET_UNSPENT_OUTS::request oreq; cryptonote::COMMAND_RPC_GET_UNSPENT_OUTS::response ores; oreq.amount = "0"; oreq.address = get_account().get_public_address_str(m_testnet); oreq.view_key = string_tools::pod_to_hex(get_account().get_keys().m_view_secret_key); // openMonero specific oreq.dust_threshold = boost::lexical_cast(::config::DEFAULT_DUST_THRESHOLD); // below are required by openMonero api - but are not used. oreq.mixin = 0; oreq.use_dust = true; m_daemon_rpc_mutex.lock(); bool r = epee::net_utils::invoke_http_json("/get_unspent_outs", oreq, ores, m_http_client, rpc_timeout, "POST"); m_daemon_rpc_mutex.unlock(); THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "get_unspent_outs"); THROW_WALLET_EXCEPTION_IF(ores.status == "error", error::wallet_internal_error, ores.reason); m_light_wallet_per_kb_fee = ores.per_kb_fee; std::unordered_map transfers_txs; for(const auto &t: m_transfers) transfers_txs.emplace(t.m_txid,t.m_spent); MDEBUG("FOUND " << ores.outputs.size() <<" outputs"); // return if no outputs found if(ores.outputs.empty()) return; // Clear old outputs m_transfers.clear(); for (const auto &o: ores.outputs) { bool spent = false; bool add_transfer = true; crypto::key_image unspent_key_image; crypto::public_key tx_public_key = AUTO_VAL_INIT(tx_public_key); THROW_WALLET_EXCEPTION_IF(string_tools::validate_hex(64, o.tx_pub_key), error::wallet_internal_error, "Invalid tx_pub_key field"); string_tools::hex_to_pod(o.tx_pub_key, tx_public_key); for (const std::string &ski: o.spend_key_images) { spent = false; // Check if key image is ours THROW_WALLET_EXCEPTION_IF(string_tools::validate_hex(64, ski), error::wallet_internal_error, "Invalid key image"); string_tools::hex_to_pod(ski, unspent_key_image); if(light_wallet_key_image_is_ours(unspent_key_image, tx_public_key, o.index)){ MTRACE("Output " << o.public_key << " is spent. Key image: " << ski); spent = true; break; } { MTRACE("Unspent output found. " << o.public_key); } } // Check if tx already exists in m_transfers. crypto::hash txid; crypto::public_key tx_pub_key; crypto::public_key public_key; THROW_WALLET_EXCEPTION_IF(string_tools::validate_hex(64, o.tx_hash), error::wallet_internal_error, "Invalid tx_hash field"); THROW_WALLET_EXCEPTION_IF(string_tools::validate_hex(64, o.public_key), error::wallet_internal_error, "Invalid public_key field"); THROW_WALLET_EXCEPTION_IF(string_tools::validate_hex(64, o.tx_pub_key), error::wallet_internal_error, "Invalid tx_pub_key field"); string_tools::hex_to_pod(o.tx_hash, txid); string_tools::hex_to_pod(o.public_key, public_key); string_tools::hex_to_pod(o.tx_pub_key, tx_pub_key); for(auto &t: m_transfers){ if(t.get_public_key() == public_key) { t.m_spent = spent; add_transfer = false; break; } } if(!add_transfer) continue; m_transfers.push_back(boost::value_initialized()); transfer_details& td = m_transfers.back(); td.m_block_height = o.height; td.m_global_output_index = o.global_index; td.m_txid = txid; // Add to extra add_tx_pub_key_to_extra(td.m_tx, tx_pub_key); td.m_key_image = unspent_key_image; td.m_key_image_known = !m_watch_only && !m_multisig; td.m_key_image_partial = m_multisig; td.m_amount = o.amount; td.m_pk_index = 0; td.m_internal_output_index = o.index; td.m_spent = spent; tx_out txout; txout.target = txout_to_key(public_key); txout.amount = td.m_amount; td.m_tx.vout.resize(td.m_internal_output_index + 1); td.m_tx.vout[td.m_internal_output_index] = txout; // Add unlock time and coinbase bool got from get_address_txs api call std::unordered_map::const_iterator found = m_light_wallet_address_txs.find(txid); THROW_WALLET_EXCEPTION_IF(found == m_light_wallet_address_txs.end(), error::wallet_internal_error, "Lightwallet: tx not found in m_light_wallet_address_txs"); bool miner_tx = found->second.m_coinbase; td.m_tx.unlock_time = found->second.m_unlock_time; if (!o.rct.empty()) { // Coinbase tx's if(miner_tx) { td.m_mask = rct::identity(); } else { // rct txs // decrypt rct mask, calculate commit hash and compare against blockchain commit hash rct::key rct_commit; light_wallet_parse_rct_str(o.rct, tx_pub_key, td.m_internal_output_index, td.m_mask, rct_commit, true); bool valid_commit = (rct_commit == rct::commit(td.amount(), td.m_mask)); if(!valid_commit) { MDEBUG("output index: " << o.global_index); MDEBUG("mask: " + string_tools::pod_to_hex(td.m_mask)); MDEBUG("calculated commit: " + string_tools::pod_to_hex(rct::commit(td.amount(), td.m_mask))); MDEBUG("expected commit: " + string_tools::pod_to_hex(rct_commit)); MDEBUG("amount: " << td.amount()); } THROW_WALLET_EXCEPTION_IF(!valid_commit, error::wallet_internal_error, "Lightwallet: rct commit hash mismatch!"); } td.m_rct = true; } else { td.m_mask = rct::identity(); td.m_rct = false; } if(!spent) set_unspent(m_transfers.size()-1); m_key_images[td.m_key_image] = m_transfers.size()-1; m_pub_keys[td.get_public_key()] = m_transfers.size()-1; } } bool wallet2::light_wallet_get_address_info(cryptonote::COMMAND_RPC_GET_ADDRESS_INFO::response &response) { MTRACE(__FUNCTION__); cryptonote::COMMAND_RPC_GET_ADDRESS_INFO::request request; request.address = get_account().get_public_address_str(m_testnet); request.view_key = string_tools::pod_to_hex(get_account().get_keys().m_view_secret_key); m_daemon_rpc_mutex.lock(); bool r = epee::net_utils::invoke_http_json("/get_address_info", request, response, m_http_client, rpc_timeout, "POST"); m_daemon_rpc_mutex.unlock(); THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "get_address_info"); // TODO: Validate result return true; } void wallet2::light_wallet_get_address_txs() { MDEBUG("Refreshing light wallet"); cryptonote::COMMAND_RPC_GET_ADDRESS_TXS::request ireq; cryptonote::COMMAND_RPC_GET_ADDRESS_TXS::response ires; ireq.address = get_account().get_public_address_str(m_testnet); ireq.view_key = string_tools::pod_to_hex(get_account().get_keys().m_view_secret_key); m_daemon_rpc_mutex.lock(); bool r = epee::net_utils::invoke_http_json("/get_address_txs", ireq, ires, m_http_client, rpc_timeout, "POST"); m_daemon_rpc_mutex.unlock(); THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "get_address_txs"); //OpenMonero sends status=success, Mymonero doesn't. THROW_WALLET_EXCEPTION_IF((!ires.status.empty() && ires.status != "success"), error::no_connection_to_daemon, "get_address_txs"); // Abort if no transactions if(ires.transactions.empty()) return; // Create searchable vectors std::vector payments_txs; for(const auto &p: m_payments) payments_txs.push_back(p.second.m_tx_hash); std::vector unconfirmed_payments_txs; for(const auto &up: m_unconfirmed_payments) unconfirmed_payments_txs.push_back(up.second.m_pd.m_tx_hash); // for balance calculation uint64_t wallet_total_sent = 0; uint64_t wallet_total_unlocked_sent = 0; // txs in pool std::vector pool_txs; for (const auto &t: ires.transactions) { const uint64_t total_received = t.total_received; uint64_t total_sent = t.total_sent; // Check key images - subtract fake outputs from total_sent for(const auto &so: t.spent_outputs) { crypto::public_key tx_public_key; crypto::key_image key_image; THROW_WALLET_EXCEPTION_IF(string_tools::validate_hex(64, so.tx_pub_key), error::wallet_internal_error, "Invalid tx_pub_key field"); THROW_WALLET_EXCEPTION_IF(string_tools::validate_hex(64, so.key_image), error::wallet_internal_error, "Invalid key_image field"); string_tools::hex_to_pod(so.tx_pub_key, tx_public_key); string_tools::hex_to_pod(so.key_image, key_image); if(!light_wallet_key_image_is_ours(key_image, tx_public_key, so.out_index)) { THROW_WALLET_EXCEPTION_IF(so.amount > t.total_sent, error::wallet_internal_error, "Lightwallet: total sent is negative!"); total_sent -= so.amount; } } // Do not add tx if empty. if(total_sent == 0 && total_received == 0) continue; crypto::hash payment_id = null_hash; crypto::hash tx_hash; THROW_WALLET_EXCEPTION_IF(string_tools::validate_hex(64, t.payment_id), error::wallet_internal_error, "Invalid payment_id field"); THROW_WALLET_EXCEPTION_IF(string_tools::validate_hex(64, t.hash), error::wallet_internal_error, "Invalid hash field"); string_tools::hex_to_pod(t.payment_id, payment_id); string_tools::hex_to_pod(t.hash, tx_hash); // lightwallet specific info bool incoming = (total_received > total_sent); address_tx address_tx; address_tx.m_tx_hash = tx_hash; address_tx.m_incoming = incoming; address_tx.m_amount = incoming ? total_received - total_sent : total_sent - total_received; address_tx.m_block_height = t.height; address_tx.m_unlock_time = t.unlock_time; address_tx.m_timestamp = t.timestamp; address_tx.m_coinbase = t.coinbase; address_tx.m_mempool = t.mempool; m_light_wallet_address_txs.emplace(tx_hash,address_tx); // populate data needed for history (m_payments, m_unconfirmed_payments, m_confirmed_txs) // INCOMING transfers if(total_received > total_sent) { payment_details payment; payment.m_tx_hash = tx_hash; payment.m_amount = total_received - total_sent; payment.m_block_height = t.height; payment.m_unlock_time = t.unlock_time; payment.m_timestamp = t.timestamp; if (t.mempool) { if (std::find(unconfirmed_payments_txs.begin(), unconfirmed_payments_txs.end(), tx_hash) == unconfirmed_payments_txs.end()) { pool_txs.push_back(tx_hash); // assume false as we don't get that info from the light wallet server crypto::hash payment_id; THROW_WALLET_EXCEPTION_IF(!epee::string_tools::hex_to_pod(t.payment_id, payment_id), error::wallet_internal_error, "Failed to parse payment id"); emplace_or_replace(m_unconfirmed_payments, payment_id, pool_payment_details{payment, false}); if (0 != m_callback) { m_callback->on_lw_unconfirmed_money_received(t.height, payment.m_tx_hash, payment.m_amount); } } } else { if (std::find(payments_txs.begin(), payments_txs.end(), tx_hash) == payments_txs.end()) { m_payments.emplace(tx_hash, payment); if (0 != m_callback) { m_callback->on_lw_money_received(t.height, payment.m_tx_hash, payment.m_amount); } } } // Outgoing transfers } else { uint64_t amount_sent = total_sent - total_received; cryptonote::transaction dummy_tx; // not used by light wallet // increase wallet total sent wallet_total_sent += total_sent; if (t.mempool) { // Handled by add_unconfirmed_tx in commit_tx // If sent from another wallet instance we need to add it if(m_unconfirmed_txs.find(tx_hash) == m_unconfirmed_txs.end()) { unconfirmed_transfer_details utd; utd.m_amount_in = amount_sent; utd.m_amount_out = amount_sent; utd.m_change = 0; utd.m_payment_id = payment_id; utd.m_timestamp = t.timestamp; utd.m_state = wallet2::unconfirmed_transfer_details::pending; m_unconfirmed_txs.emplace(tx_hash,utd); } } else { // Only add if new auto confirmed_tx = m_confirmed_txs.find(tx_hash); if(confirmed_tx == m_confirmed_txs.end()) { // tx is added to m_unconfirmed_txs - move to confirmed if(m_unconfirmed_txs.find(tx_hash) != m_unconfirmed_txs.end()) { process_unconfirmed(tx_hash, dummy_tx, t.height); } // Tx sent by another wallet instance else { confirmed_transfer_details ctd; ctd.m_amount_in = amount_sent; ctd.m_amount_out = amount_sent; ctd.m_change = 0; ctd.m_payment_id = payment_id; ctd.m_block_height = t.height; ctd.m_timestamp = t.timestamp; m_confirmed_txs.emplace(tx_hash,ctd); } if (0 != m_callback) { m_callback->on_lw_money_spent(t.height, tx_hash, amount_sent); } } // If not new - check the amount and update if necessary. // when sending a tx to same wallet the receiving amount has to be credited else { if(confirmed_tx->second.m_amount_in != amount_sent || confirmed_tx->second.m_amount_out != amount_sent) { MDEBUG("Adjusting amount sent/received for tx: <" + t.hash + ">. Is tx sent to own wallet? " << print_money(amount_sent) << " != " << print_money(confirmed_tx->second.m_amount_in)); confirmed_tx->second.m_amount_in = amount_sent; confirmed_tx->second.m_amount_out = amount_sent; confirmed_tx->second.m_change = 0; } } } } } // TODO: purge old unconfirmed_txs remove_obsolete_pool_txs(pool_txs); // Calculate wallet balance m_light_wallet_balance = ires.total_received-wallet_total_sent; // MyMonero doesnt send unlocked balance if(ires.total_received_unlocked > 0) m_light_wallet_unlocked_balance = ires.total_received_unlocked-wallet_total_sent; else m_light_wallet_unlocked_balance = m_light_wallet_balance; } bool wallet2::light_wallet_parse_rct_str(const std::string& rct_string, const crypto::public_key& tx_pub_key, uint64_t internal_output_index, rct::key& decrypted_mask, rct::key& rct_commit, bool decrypt) const { // rct string is empty if output is non RCT if (rct_string.empty()) return false; // rct_string is a string with length 64+64+64 ( + + ) rct::key encrypted_mask; std::string rct_commit_str = rct_string.substr(0,64); std::string encrypted_mask_str = rct_string.substr(64,64); THROW_WALLET_EXCEPTION_IF(string_tools::validate_hex(64, rct_commit_str), error::wallet_internal_error, "Invalid rct commit hash: " + rct_commit_str); THROW_WALLET_EXCEPTION_IF(string_tools::validate_hex(64, encrypted_mask_str), error::wallet_internal_error, "Invalid rct mask: " + encrypted_mask_str); string_tools::hex_to_pod(rct_commit_str, rct_commit); string_tools::hex_to_pod(encrypted_mask_str, encrypted_mask); if (decrypt) { // Decrypt the mask crypto::key_derivation derivation; bool r = generate_key_derivation(tx_pub_key, get_account().get_keys().m_view_secret_key, derivation); THROW_WALLET_EXCEPTION_IF(!r, error::wallet_internal_error, "Failed to generate key derivation"); crypto::secret_key scalar; crypto::derivation_to_scalar(derivation, internal_output_index, scalar); sc_sub(decrypted_mask.bytes,encrypted_mask.bytes,rct::hash_to_scalar(rct::sk2rct(scalar)).bytes); } return true; } bool wallet2::light_wallet_key_image_is_ours(const crypto::key_image& key_image, const crypto::public_key& tx_public_key, uint64_t out_index) { // Lookup key image from cache std::map index_keyimage_map; std::unordered_map >::const_iterator found_pub_key = m_key_image_cache.find(tx_public_key); if(found_pub_key != m_key_image_cache.end()) { // pub key found. key image for index cached? index_keyimage_map = found_pub_key->second; std::map::const_iterator index_found = index_keyimage_map.find(out_index); if(index_found != index_keyimage_map.end()) return key_image == index_found->second; } // Not in cache - calculate key image crypto::key_image calculated_key_image; cryptonote::keypair in_ephemeral; // Subaddresses aren't supported in mymonero/openmonero yet. Roll out the original scheme: // compute D = a*R // compute P = Hs(D || i)*G + B // compute x = Hs(D || i) + b (and check if P==x*G) // compute I = x*Hp(P) const account_keys& ack = get_account().get_keys(); crypto::key_derivation derivation; bool r = crypto::generate_key_derivation(tx_public_key, ack.m_view_secret_key, derivation); CHECK_AND_ASSERT_MES(r, false, "failed to generate_key_derivation(" << tx_public_key << ", " << ack.m_view_secret_key << ")"); r = crypto::derive_public_key(derivation, out_index, ack.m_account_address.m_spend_public_key, in_ephemeral.pub); CHECK_AND_ASSERT_MES(r, false, "failed to derive_public_key (" << derivation << ", " << out_index << ", " << ack.m_account_address.m_spend_public_key << ")"); crypto::derive_secret_key(derivation, out_index, ack.m_spend_secret_key, in_ephemeral.sec); crypto::public_key out_pkey_test; r = crypto::secret_key_to_public_key(in_ephemeral.sec, out_pkey_test); CHECK_AND_ASSERT_MES(r, false, "failed to secret_key_to_public_key(" << in_ephemeral.sec << ")"); CHECK_AND_ASSERT_MES(in_ephemeral.pub == out_pkey_test, false, "derived secret key doesn't match derived public key"); crypto::generate_key_image(in_ephemeral.pub, in_ephemeral.sec, calculated_key_image); index_keyimage_map.emplace(out_index, calculated_key_image); m_key_image_cache.emplace(tx_public_key, index_keyimage_map); return key_image == calculated_key_image; } // Another implementation of transaction creation that is hopefully better // While there is anything left to pay, it goes through random outputs and tries // to fill the next destination/amount. If it fully fills it, it will use the // remainder to try to fill the next one as well. // The tx size if roughly estimated as a linear function of only inputs, and a // new tx will be created when that size goes above a given fraction of the // max tx size. At that point, more outputs may be added if the fee cannot be // satisfied. // If the next output in the next tx would go to the same destination (ie, we // cut off at a tx boundary in the middle of paying a given destination), the // fee will be carved out of the current input if possible, to avoid having to // add another output just for the fee and getting change. // This system allows for sending (almost) the entire balance, since it does // not generate spurious change in all txes, thus decreasing the instantaneous // usable balance. std::vector wallet2::create_transactions_2(std::vector dsts, const size_t fake_outs_count, const uint64_t unlock_time, uint32_t priority, const std::vector& extra, uint32_t subaddr_account, std::set subaddr_indices, bool trusted_daemon) { if(m_light_wallet) { // Populate m_transfers light_wallet_get_unspent_outs(); } std::vector>> unused_transfers_indices_per_subaddr; std::vector>> unused_dust_indices_per_subaddr; uint64_t needed_money; uint64_t accumulated_fee, accumulated_outputs, accumulated_change; struct TX { std::vector selected_transfers; std::vector dsts; cryptonote::transaction tx; pending_tx ptx; size_t bytes; void add(const account_public_address &addr, bool is_subaddress, uint64_t amount, unsigned int original_output_index, bool merge_destinations) { if (merge_destinations) { std::vector::iterator i; i = std::find_if(dsts.begin(), dsts.end(), [&](const cryptonote::tx_destination_entry &d) { return !memcmp (&d.addr, &addr, sizeof(addr)); }); if (i == dsts.end()) { dsts.push_back(tx_destination_entry(0,addr,is_subaddress)); i = dsts.end() - 1; } i->amount += amount; } else { THROW_WALLET_EXCEPTION_IF(original_output_index > dsts.size(), error::wallet_internal_error, std::string("original_output_index too large: ") + std::to_string(original_output_index) + " > " + std::to_string(dsts.size())); if (original_output_index == dsts.size()) dsts.push_back(tx_destination_entry(0,addr,is_subaddress)); THROW_WALLET_EXCEPTION_IF(memcmp(&dsts[original_output_index].addr, &addr, sizeof(addr)), error::wallet_internal_error, "Mismatched destination address"); dsts[original_output_index].amount += amount; } } }; std::vector txes; bool adding_fee; // true if new outputs go towards fee, rather than destinations uint64_t needed_fee, available_for_fee = 0; uint64_t upper_transaction_size_limit = get_upper_transaction_size_limit(); const bool use_rct = use_fork_rules(4, 0); const bool bulletproof = use_fork_rules(get_bulletproof_fork(m_testnet), 0); const uint64_t fee_per_kb = get_per_kb_fee(); const uint64_t fee_multiplier = get_fee_multiplier(priority, get_fee_algorithm()); // throw if attempting a transaction with no destinations THROW_WALLET_EXCEPTION_IF(dsts.empty(), error::zero_destination); // calculate total amount being sent to all destinations // throw if total amount overflows uint64_t needed_money = 0; for(auto& dt: dsts) { THROW_WALLET_EXCEPTION_IF(0 == dt.amount, error::zero_destination); needed_money += dt.amount; LOG_PRINT_L2("transfer: adding " << print_money(dt.amount) << ", for a total of " << print_money (needed_money)); THROW_WALLET_EXCEPTION_IF(needed_money < dt.amount, error::tx_sum_overflow, dsts, 0, m_testnet); } // throw if attempting a transaction with no money THROW_WALLET_EXCEPTION_IF(needed_money == 0, error::zero_destination); std::map unlocked_balance_per_subaddr = unlocked_balance_per_subaddress(subaddr_account); std::map balance_per_subaddr = balance_per_subaddress(subaddr_account); if (subaddr_indices.empty()) // "index=[,,...]" wasn't specified -> use all the indices with non-zero unlocked balance { for (const auto& i : balance_per_subaddr) subaddr_indices.insert(i.first); } // early out if we know we can't make it anyway // we could also check for being within FEE_PER_KB, but if the fee calculation // ever changes, this might be missed, so let this go through uint64_t balance_subtotal = 0; uint64_t unlocked_balance_subtotal = 0; for (uint32_t index_minor : subaddr_indices) { balance_subtotal += balance_per_subaddr[index_minor]; unlocked_balance_subtotal += unlocked_balance_per_subaddr[index_minor]; } THROW_WALLET_EXCEPTION_IF(needed_money > balance_subtotal, error::not_enough_money, balance_subtotal, needed_money, 0); // first check overall balance is enough, then unlocked one, so we throw distinct exceptions THROW_WALLET_EXCEPTION_IF(needed_money > unlocked_balance_subtotal, error::not_enough_unlocked_money, unlocked_balance_subtotal, needed_money, 0); for (uint32_t i : subaddr_indices) LOG_PRINT_L2("Candidate subaddress index for spending: " << i); // gather all dust and non-dust outputs belonging to specified subaddresses size_t num_nondust_outputs = 0; size_t num_dust_outputs = 0; for (size_t i = 0; i < m_transfers.size(); ++i) { const transfer_details& td = m_transfers[i]; if (!td.m_spent && !td.m_key_image_partial && (use_rct ? true : !td.is_rct()) && is_transfer_unlocked(td) && td.m_subaddr_index.major == subaddr_account && subaddr_indices.count(td.m_subaddr_index.minor) == 1) { const uint32_t index_minor = td.m_subaddr_index.minor; auto find_predicate = [&index_minor](const std::pair>& x) { return x.first == index_minor; }; if ((td.is_rct()) || is_valid_decomposed_amount(td.amount())) { auto found = std::find_if(unused_transfers_indices_per_subaddr.begin(), unused_transfers_indices_per_subaddr.end(), find_predicate); if (found == unused_transfers_indices_per_subaddr.end()) { unused_transfers_indices_per_subaddr.push_back({index_minor, {i}}); } else { found->second.push_back(i); } ++num_nondust_outputs; } else { auto found = std::find_if(unused_dust_indices_per_subaddr.begin(), unused_dust_indices_per_subaddr.end(), find_predicate); if (found == unused_dust_indices_per_subaddr.end()) { unused_dust_indices_per_subaddr.push_back({index_minor, {i}}); } else { found->second.push_back(i); } ++num_dust_outputs; } } } // shuffle & sort output indices { std::random_device rd; std::mt19937 g(rd()); std::shuffle(unused_transfers_indices_per_subaddr.begin(), unused_transfers_indices_per_subaddr.end(), g); std::shuffle(unused_dust_indices_per_subaddr.begin(), unused_dust_indices_per_subaddr.end(), g); auto sort_predicate = [&unlocked_balance_per_subaddr] (const std::pair>& x, const std::pair>& y) { return unlocked_balance_per_subaddr[x.first] > unlocked_balance_per_subaddr[y.first]; }; std::sort(unused_transfers_indices_per_subaddr.begin(), unused_transfers_indices_per_subaddr.end(), sort_predicate); std::sort(unused_dust_indices_per_subaddr.begin(), unused_dust_indices_per_subaddr.end(), sort_predicate); } LOG_PRINT_L2("Starting with " << num_nondust_outputs << " non-dust outputs and " << num_dust_outputs << " dust outputs"); if (unused_dust_indices_per_subaddr.empty() && unused_transfers_indices_per_subaddr.empty()) return std::vector(); // if empty, put dummy entry so that the front can be referenced later in the loop if (unused_dust_indices_per_subaddr.empty()) unused_dust_indices_per_subaddr.push_back({}); if (unused_transfers_indices_per_subaddr.empty()) unused_transfers_indices_per_subaddr.push_back({}); // start with an empty tx txes.push_back(TX()); accumulated_fee = 0; accumulated_outputs = 0; accumulated_change = 0; adding_fee = false; needed_fee = 0; std::vector> outs; // for rct, since we don't see the amounts, we will try to make all transactions // look the same, with 1 or 2 inputs, and 2 outputs. One input is preferable, as // this prevents linking to another by provenance analysis, but two is ok if we // try to pick outputs not from the same block. We will get two outputs, one for // the destination, and one for change. LOG_PRINT_L2("checking preferred"); std::vector preferred_inputs; uint64_t rct_outs_needed = 2 * (fake_outs_count + 1); rct_outs_needed += 100; // some fudge factor since we don't know how many are locked if (use_rct) { // this is used to build a tx that's 1 or 2 inputs, and 2 outputs, which // will get us a known fee. uint64_t estimated_fee = calculate_fee(fee_per_kb, estimate_rct_tx_size(2, fake_outs_count, 2, extra.size(), bulletproof), fee_multiplier); preferred_inputs = pick_preferred_rct_inputs(needed_money + estimated_fee, subaddr_account, subaddr_indices); if (!preferred_inputs.empty()) { string s; for (auto i: preferred_inputs) s += boost::lexical_cast(i) + " (" + print_money(m_transfers[i].amount()) + ") "; LOG_PRINT_L1("Found prefered rct inputs for rct tx: " << s); // bring the list of available outputs stored by the same subaddress index to the front of the list uint32_t index_minor = m_transfers[preferred_inputs[0]].m_subaddr_index.minor; for (size_t i = 1; i < unused_transfers_indices_per_subaddr.size(); ++i) { if (unused_transfers_indices_per_subaddr[i].first == index_minor) { std::swap(unused_transfers_indices_per_subaddr[0], unused_transfers_indices_per_subaddr[i]); break; } } for (size_t i = 1; i < unused_dust_indices_per_subaddr.size(); ++i) { if (unused_dust_indices_per_subaddr[i].first == index_minor) { std::swap(unused_dust_indices_per_subaddr[0], unused_dust_indices_per_subaddr[i]); break; } } } } LOG_PRINT_L2("done checking preferred"); // while: // - we have something to send // - or we need to gather more fee // - or we have just one input in that tx, which is rct (to try and make all/most rct txes 2/2) unsigned int original_output_index = 0; std::vector* unused_transfers_indices = &unused_transfers_indices_per_subaddr[0].second; std::vector* unused_dust_indices = &unused_dust_indices_per_subaddr[0].second; while ((!dsts.empty() && dsts[0].amount > 0) || adding_fee || !preferred_inputs.empty() || should_pick_a_second_output(use_rct, txes.back().selected_transfers.size(), *unused_transfers_indices, *unused_dust_indices)) { TX &tx = txes.back(); LOG_PRINT_L2("Start of loop with " << unused_transfers_indices->size() << " " << unused_dust_indices->size()); LOG_PRINT_L2("unused_transfers_indices: " << strjoin(*unused_transfers_indices, " ")); LOG_PRINT_L2("unused_dust_indices: " << strjoin(*unused_dust_indices, " ")); LOG_PRINT_L2("dsts size " << dsts.size() << ", first " << (dsts.empty() ? "-" : cryptonote::print_money(dsts[0].amount))); LOG_PRINT_L2("adding_fee " << adding_fee << ", use_rct " << use_rct); // if we need to spend money and don't have any left, we fail if (unused_dust_indices->empty() && unused_transfers_indices->empty()) { LOG_PRINT_L2("No more outputs to choose from"); THROW_WALLET_EXCEPTION_IF(1, error::tx_not_possible, unlocked_balance(subaddr_account), needed_money, accumulated_fee + needed_fee); } // get a random unspent output and use it to pay part (or all) of the current destination (and maybe next one, etc) // This could be more clever, but maybe at the cost of making probabilistic inferences easier size_t idx; if (!preferred_inputs.empty()) { idx = pop_back(preferred_inputs); pop_if_present(*unused_transfers_indices, idx); pop_if_present(*unused_dust_indices, idx); } else if ((dsts.empty() || dsts[0].amount == 0) && !adding_fee) { // the "make rct txes 2/2" case - we pick a small value output to "clean up" the wallet too std::vector indices = get_only_rct(*unused_dust_indices, *unused_transfers_indices); idx = pop_best_value(indices, tx.selected_transfers, true); // we might not want to add it if it's a large output and we don't have many left if (m_transfers[idx].amount() >= m_min_output_value) { if (get_count_above(m_transfers, *unused_transfers_indices, m_min_output_value) < m_min_output_count) { LOG_PRINT_L2("Second output was not strictly needed, and we're running out of outputs above " << print_money(m_min_output_value) << ", not adding"); break; } } // since we're trying to add a second output which is not strictly needed, // we only add it if it's unrelated enough to the first one float relatedness = get_output_relatedness(m_transfers[idx], m_transfers[tx.selected_transfers.front()]); if (relatedness > SECOND_OUTPUT_RELATEDNESS_THRESHOLD) { LOG_PRINT_L2("Second output was not strictly needed, and relatedness " << relatedness << ", not adding"); break; } pop_if_present(*unused_transfers_indices, idx); pop_if_present(*unused_dust_indices, idx); } else idx = pop_best_value(unused_transfers_indices->empty() ? *unused_dust_indices : *unused_transfers_indices, tx.selected_transfers); const transfer_details &td = m_transfers[idx]; LOG_PRINT_L2("Picking output " << idx << ", amount " << print_money(td.amount()) << ", ki " << td.m_key_image); // add this output to the list to spend tx.selected_transfers.push_back(idx); uint64_t available_amount = td.amount(); accumulated_outputs += available_amount; // clear any fake outs we'd already gathered, since we'll need a new set outs.clear(); if (adding_fee) { LOG_PRINT_L2("We need more fee, adding it to fee"); available_for_fee += available_amount; } else { while (!dsts.empty() && dsts[0].amount <= available_amount && estimate_tx_size(use_rct, tx.selected_transfers.size(), fake_outs_count, tx.dsts.size(), extra.size(), bulletproof) < TX_SIZE_TARGET(upper_transaction_size_limit)) { // we can fully pay that destination LOG_PRINT_L2("We can fully pay " << get_account_address_as_str(m_testnet, dsts[0].is_subaddress, dsts[0].addr) << " for " << print_money(dsts[0].amount)); tx.add(dsts[0].addr, dsts[0].is_subaddress, dsts[0].amount, original_output_index, m_merge_destinations); available_amount -= dsts[0].amount; dsts[0].amount = 0; pop_index(dsts, 0); ++original_output_index; } if (available_amount > 0 && !dsts.empty() && estimate_tx_size(use_rct, tx.selected_transfers.size(), fake_outs_count, tx.dsts.size(), extra.size(), bulletproof) < TX_SIZE_TARGET(upper_transaction_size_limit)) { // we can partially fill that destination LOG_PRINT_L2("We can partially pay " << get_account_address_as_str(m_testnet, dsts[0].is_subaddress, dsts[0].addr) << " for " << print_money(available_amount) << "/" << print_money(dsts[0].amount)); tx.add(dsts[0].addr, dsts[0].is_subaddress, available_amount, original_output_index, m_merge_destinations); dsts[0].amount -= available_amount; available_amount = 0; } } // here, check if we need to sent tx and start a new one LOG_PRINT_L2("Considering whether to create a tx now, " << tx.selected_transfers.size() << " inputs, tx limit " << upper_transaction_size_limit); bool try_tx = false; // if we have preferred picks, but haven't yet used all of them, continue if (preferred_inputs.empty()) { if (adding_fee) { /* might not actually be enough if adding this output bumps size to next kB, but we need to try */ try_tx = available_for_fee >= needed_fee; } else { const size_t estimated_rct_tx_size = estimate_tx_size(use_rct, tx.selected_transfers.size(), fake_outs_count, tx.dsts.size(), extra.size(), bulletproof); try_tx = dsts.empty() || (estimated_rct_tx_size >= TX_SIZE_TARGET(upper_transaction_size_limit)); } } if (try_tx) { cryptonote::transaction test_tx; pending_tx test_ptx; const size_t estimated_tx_size = estimate_tx_size(use_rct, tx.selected_transfers.size(), fake_outs_count, tx.dsts.size(), extra.size(), bulletproof); needed_fee = calculate_fee(fee_per_kb, estimated_tx_size, fee_multiplier); LOG_PRINT_L2("Trying to create a tx now, with " << tx.dsts.size() << " outputs and " << tx.selected_transfers.size() << " inputs"); if (use_rct) transfer_selected_rct(tx.dsts, tx.selected_transfers, fake_outs_count, outs, unlock_time, needed_fee, extra, test_tx, test_ptx, bulletproof); else transfer_selected(tx.dsts, tx.selected_transfers, fake_outs_count, outs, unlock_time, needed_fee, extra, detail::digit_split_strategy, tx_dust_policy(::config::DEFAULT_DUST_THRESHOLD), test_tx, test_ptx); auto txBlob = t_serializable_object_to_blob(test_ptx.tx); needed_fee = calculate_fee(fee_per_kb, txBlob, fee_multiplier); available_for_fee = test_ptx.fee + test_ptx.change_dts.amount + (!test_ptx.dust_added_to_fee ? test_ptx.dust : 0); LOG_PRINT_L2("Made a " << get_size_string(txBlob) << " tx, with " << print_money(available_for_fee) << " available for fee (" << print_money(needed_fee) << " needed)"); if (needed_fee > available_for_fee && !dsts.empty() && dsts[0].amount > 0) { // we don't have enough for the fee, but we've only partially paid the current address, // so we can take the fee from the paid amount, since we'll have to make another tx anyway std::vector::iterator i; i = std::find_if(tx.dsts.begin(), tx.dsts.end(), [&](const cryptonote::tx_destination_entry &d) { return !memcmp (&d.addr, &dsts[0].addr, sizeof(dsts[0].addr)); }); THROW_WALLET_EXCEPTION_IF(i == tx.dsts.end(), error::wallet_internal_error, "paid address not found in outputs"); if (i->amount > needed_fee) { uint64_t new_paid_amount = i->amount /*+ test_ptx.fee*/ - needed_fee; LOG_PRINT_L2("Adjusting amount paid to " << get_account_address_as_str(m_testnet, i->is_subaddress, i->addr) << " from " << print_money(i->amount) << " to " << print_money(new_paid_amount) << " to accommodate " << print_money(needed_fee) << " fee"); dsts[0].amount += i->amount - new_paid_amount; i->amount = new_paid_amount; test_ptx.fee = needed_fee; available_for_fee = needed_fee; } } if (needed_fee > available_for_fee) { LOG_PRINT_L2("We could not make a tx, switching to fee accumulation"); adding_fee = true; } else { LOG_PRINT_L2("We made a tx, adjusting fee and saving it, we need " << print_money(needed_fee) << " and we have " << print_money(test_ptx.fee)); while (needed_fee > test_ptx.fee) { if (use_rct) transfer_selected_rct(tx.dsts, tx.selected_transfers, fake_outs_count, outs, unlock_time, needed_fee, extra, test_tx, test_ptx, bulletproof); else transfer_selected(tx.dsts, tx.selected_transfers, fake_outs_count, outs, unlock_time, needed_fee, extra, detail::digit_split_strategy, tx_dust_policy(::config::DEFAULT_DUST_THRESHOLD), test_tx, test_ptx); txBlob = t_serializable_object_to_blob(test_ptx.tx); needed_fee = calculate_fee(fee_per_kb, txBlob, fee_multiplier); LOG_PRINT_L2("Made an attempt at a final " << get_size_string(txBlob) << " tx, with " << print_money(test_ptx.fee) << " fee and " << print_money(test_ptx.change_dts.amount) << " change"); } LOG_PRINT_L2("Made a final " << get_size_string(txBlob) << " tx, with " << print_money(test_ptx.fee) << " fee and " << print_money(test_ptx.change_dts.amount) << " change"); tx.tx = test_tx; tx.ptx = test_ptx; tx.bytes = txBlob.size(); accumulated_fee += test_ptx.fee; accumulated_change += test_ptx.change_dts.amount; adding_fee = false; if (!dsts.empty()) { LOG_PRINT_L2("We have more to pay, starting another tx"); txes.push_back(TX()); original_output_index = 0; } } } // if unused_*_indices is empty while unused_*_indices_per_subaddr has multiple elements, and if we still have something to pay, // pop front of unused_*_indices_per_subaddr and have unused_*_indices point to the front of unused_*_indices_per_subaddr if ((!dsts.empty() && dsts[0].amount > 0) || adding_fee) { if (unused_transfers_indices->empty() && unused_transfers_indices_per_subaddr.size() > 1) { unused_transfers_indices_per_subaddr.erase(unused_transfers_indices_per_subaddr.begin()); unused_transfers_indices = &unused_transfers_indices_per_subaddr[0].second; } if (unused_dust_indices->empty() && unused_dust_indices_per_subaddr.size() > 1) { unused_dust_indices_per_subaddr.erase(unused_dust_indices_per_subaddr.begin()); unused_dust_indices = &unused_dust_indices_per_subaddr[0].second; } } } if (adding_fee) { LOG_PRINT_L1("We ran out of outputs while trying to gather final fee"); THROW_WALLET_EXCEPTION_IF(1, error::tx_not_possible, unlocked_balance(subaddr_account), needed_money, accumulated_fee + needed_fee); } LOG_PRINT_L1("Done creating " << txes.size() << " transactions, " << print_money(accumulated_fee) << " total fee, " << print_money(accumulated_change) << " total change"); std::vector ptx_vector; for (std::vector::iterator i = txes.begin(); i != txes.end(); ++i) { TX &tx = *i; uint64_t tx_money = 0; for (size_t idx: tx.selected_transfers) tx_money += m_transfers[idx].amount(); LOG_PRINT_L1(" Transaction " << (1+std::distance(txes.begin(), i)) << "/" << txes.size() << ": " << get_size_string(tx.bytes) << ", sending " << print_money(tx_money) << " in " << tx.selected_transfers.size() << " outputs to " << tx.dsts.size() << " destination(s), including " << print_money(tx.ptx.fee) << " fee, " << print_money(tx.ptx.change_dts.amount) << " change"); ptx_vector.push_back(tx.ptx); } // if we made it this far, we're OK to actually send the transactions return ptx_vector; } std::vector wallet2::create_transactions_all(uint64_t below, const cryptonote::account_public_address &address, bool is_subaddress, const size_t fake_outs_count, const uint64_t unlock_time, uint32_t priority, const std::vector& extra, uint32_t subaddr_account, std::set subaddr_indices, bool trusted_daemon) { std::vector unused_transfers_indices; std::vector unused_dust_indices; const bool use_rct = use_fork_rules(4, 0); THROW_WALLET_EXCEPTION_IF(unlocked_balance(subaddr_account) == 0, error::wallet_internal_error, "No unlocked balance in the entire wallet"); std::map balance_per_subaddr = unlocked_balance_per_subaddress(subaddr_account); if (subaddr_indices.empty()) { // in case subaddress index wasn't specified, choose non-empty subaddress randomly (with index=0 being chosen last) if (balance_per_subaddr.count(0) == 1 && balance_per_subaddr.size() > 1) balance_per_subaddr.erase(0); auto i = balance_per_subaddr.begin(); std::advance(i, crypto::rand() % balance_per_subaddr.size()); subaddr_indices.insert(i->first); } for (uint32_t i : subaddr_indices) LOG_PRINT_L2("Spending from subaddress index " << i); // gather all dust and non-dust outputs of specified subaddress for (size_t i = 0; i < m_transfers.size(); ++i) { const transfer_details& td = m_transfers[i]; if (!td.m_spent && !td.m_key_image_partial && (use_rct ? true : !td.is_rct()) && is_transfer_unlocked(td) && td.m_subaddr_index.major == subaddr_account && subaddr_indices.count(td.m_subaddr_index.minor) == 1) { if (below == 0 || td.amount() < below) { if ((td.is_rct()) || is_valid_decomposed_amount(td.amount())) unused_transfers_indices.push_back(i); else unused_dust_indices.push_back(i); } } } THROW_WALLET_EXCEPTION_IF(unused_transfers_indices.empty() && unused_dust_indices.empty(), error::not_enough_money, 0, 0, 0); // not sure if a new error class (something like 'cant_sweep_empty'?) should be introduced return create_transactions_from(address, is_subaddress, unused_transfers_indices, unused_dust_indices, fake_outs_count, unlock_time, priority, extra, trusted_daemon); } std::vector wallet2::create_transactions_single(const crypto::key_image &ki, const cryptonote::account_public_address &address, bool is_subaddress, const size_t fake_outs_count, const uint64_t unlock_time, uint32_t priority, const std::vector& extra, bool trusted_daemon) { std::vector unused_transfers_indices; std::vector unused_dust_indices; const bool use_rct = use_fork_rules(4, 0); // find output with the given key image for (size_t i = 0; i < m_transfers.size(); ++i) { const transfer_details& td = m_transfers[i]; if (td.m_key_image_known && td.m_key_image == ki && !td.m_spent && (use_rct ? true : !td.is_rct()) && is_transfer_unlocked(td)) { if (td.is_rct() || is_valid_decomposed_amount(td.amount())) unused_transfers_indices.push_back(i); else unused_dust_indices.push_back(i); break; } } return create_transactions_from(address, is_subaddress, unused_transfers_indices, unused_dust_indices, fake_outs_count, unlock_time, priority, extra, trusted_daemon); } std::vector wallet2::create_transactions_from(const cryptonote::account_public_address &address, bool is_subaddress, std::vector unused_transfers_indices, std::vector unused_dust_indices, const size_t fake_outs_count, const uint64_t unlock_time, uint32_t priority, const std::vector& extra, bool trusted_daemon) { uint64_t accumulated_fee, accumulated_outputs, accumulated_change; struct TX { std::vector selected_transfers; std::vector dsts; cryptonote::transaction tx; pending_tx ptx; size_t bytes; }; std::vector txes; uint64_t needed_fee, available_for_fee = 0; uint64_t upper_transaction_size_limit = get_upper_transaction_size_limit(); std::vector> outs; const bool use_rct = fake_outs_count > 0 && use_fork_rules(4, 0); const bool bulletproof = use_fork_rules(get_bulletproof_fork(m_testnet), 0); const uint64_t fee_per_kb = get_per_kb_fee(); const uint64_t fee_multiplier = get_fee_multiplier(priority, get_fee_algorithm()); LOG_PRINT_L2("Starting with " << unused_transfers_indices.size() << " non-dust outputs and " << unused_dust_indices.size() << " dust outputs"); if (unused_dust_indices.empty() && unused_transfers_indices.empty()) return std::vector(); // start with an empty tx txes.push_back(TX()); accumulated_fee = 0; accumulated_outputs = 0; accumulated_change = 0; needed_fee = 0; // while we have something to send while (!unused_dust_indices.empty() || !unused_transfers_indices.empty()) { TX &tx = txes.back(); // get a random unspent output and use it to pay next chunk. We try to alternate // dust and non dust to ensure we never get with only dust, from which we might // get a tx that can't pay for itself size_t idx = unused_transfers_indices.empty() ? pop_best_value(unused_dust_indices, tx.selected_transfers) : unused_dust_indices.empty() ? pop_best_value(unused_transfers_indices, tx.selected_transfers) : ((tx.selected_transfers.size() & 1) || accumulated_outputs > fee_per_kb * fee_multiplier * (upper_transaction_size_limit + 1023) / 1024) ? pop_best_value(unused_dust_indices, tx.selected_transfers) : pop_best_value(unused_transfers_indices, tx.selected_transfers); const transfer_details &td = m_transfers[idx]; LOG_PRINT_L2("Picking output " << idx << ", amount " << print_money(td.amount())); // add this output to the list to spend tx.selected_transfers.push_back(idx); uint64_t available_amount = td.amount(); accumulated_outputs += available_amount; // clear any fake outs we'd already gathered, since we'll need a new set outs.clear(); // here, check if we need to sent tx and start a new one LOG_PRINT_L2("Considering whether to create a tx now, " << tx.selected_transfers.size() << " inputs, tx limit " << upper_transaction_size_limit); const size_t estimated_rct_tx_size = estimate_tx_size(use_rct, tx.selected_transfers.size(), fake_outs_count, tx.dsts.size() + 1, extra.size(), bulletproof); bool try_tx = (unused_dust_indices.empty() && unused_transfers_indices.empty()) || ( estimated_rct_tx_size >= TX_SIZE_TARGET(upper_transaction_size_limit)); if (try_tx) { cryptonote::transaction test_tx; pending_tx test_ptx; const size_t estimated_tx_size = estimate_tx_size(use_rct, tx.selected_transfers.size(), fake_outs_count, tx.dsts.size(), extra.size(), bulletproof); needed_fee = calculate_fee(fee_per_kb, estimated_tx_size, fee_multiplier); tx.dsts.push_back(tx_destination_entry(1, address, is_subaddress)); LOG_PRINT_L2("Trying to create a tx now, with " << tx.dsts.size() << " destinations and " << tx.selected_transfers.size() << " outputs"); if (use_rct) transfer_selected_rct(tx.dsts, tx.selected_transfers, fake_outs_count, outs, unlock_time, needed_fee, extra, test_tx, test_ptx, bulletproof); else transfer_selected(tx.dsts, tx.selected_transfers, fake_outs_count, outs, unlock_time, needed_fee, extra, detail::digit_split_strategy, tx_dust_policy(::config::DEFAULT_DUST_THRESHOLD), test_tx, test_ptx); auto txBlob = t_serializable_object_to_blob(test_ptx.tx); needed_fee = calculate_fee(fee_per_kb, txBlob, fee_multiplier); available_for_fee = test_ptx.fee + test_ptx.dests[0].amount + test_ptx.change_dts.amount; LOG_PRINT_L2("Made a " << get_size_string(txBlob) << " tx, with " << print_money(available_for_fee) << " available for fee (" << print_money(needed_fee) << " needed)"); THROW_WALLET_EXCEPTION_IF(needed_fee > available_for_fee, error::wallet_internal_error, "Transaction cannot pay for itself"); while (needed_fee > test_ptx.fee) { LOG_PRINT_L2("We made a tx, adjusting fee and saving it"); tx.dsts[0].amount = available_for_fee - needed_fee; if (use_rct) transfer_selected_rct(tx.dsts, tx.selected_transfers, fake_outs_count, outs, unlock_time, needed_fee, extra, test_tx, test_ptx, bulletproof); else transfer_selected(tx.dsts, tx.selected_transfers, fake_outs_count, outs, unlock_time, needed_fee, extra, detail::digit_split_strategy, tx_dust_policy(::config::DEFAULT_DUST_THRESHOLD), test_tx, test_ptx); txBlob = t_serializable_object_to_blob(test_ptx.tx); needed_fee = calculate_fee(fee_per_kb, txBlob, fee_multiplier); LOG_PRINT_L2("Made an attempt at a final " << get_size_string(txBlob) << " tx, with " << print_money(test_ptx.fee) << " fee and " << print_money(test_ptx.change_dts.amount) << " change"); } LOG_PRINT_L2("Made a final " << get_size_string(txBlob) << " tx, with " << print_money(test_ptx.fee) << " fee and " << print_money(test_ptx.change_dts.amount) << " change"); tx.tx = test_tx; tx.ptx = test_ptx; tx.bytes = txBlob.size(); accumulated_fee += test_ptx.fee; accumulated_change += test_ptx.change_dts.amount; if (!unused_transfers_indices.empty() || !unused_dust_indices.empty()) { LOG_PRINT_L2("We have more to pay, starting another tx"); txes.push_back(TX()); } } } LOG_PRINT_L1("Done creating " << txes.size() << " transactions, " << print_money(accumulated_fee) << " total fee, " << print_money(accumulated_change) << " total change"); std::vector ptx_vector; for (std::vector::iterator i = txes.begin(); i != txes.end(); ++i) { TX &tx = *i; uint64_t tx_money = 0; for (size_t idx: tx.selected_transfers) tx_money += m_transfers[idx].amount(); LOG_PRINT_L1(" Transaction " << (1+std::distance(txes.begin(), i)) << "/" << txes.size() << ": " << get_size_string(tx.bytes) << ", sending " << print_money(tx_money) << " in " << tx.selected_transfers.size() << " outputs to " << tx.dsts.size() << " destination(s), including " << print_money(tx.ptx.fee) << " fee, " << print_money(tx.ptx.change_dts.amount) << " change"); ptx_vector.push_back(tx.ptx); } // if we made it this far, we're OK to actually send the transactions return ptx_vector; } //---------------------------------------------------------------------------------------------------- void wallet2::get_hard_fork_info(uint8_t version, uint64_t &earliest_height) { boost::optional result = m_node_rpc_proxy.get_earliest_height(version, earliest_height); throw_on_rpc_response_error(result, "get_hard_fork_info"); } //---------------------------------------------------------------------------------------------------- bool wallet2::use_fork_rules(uint8_t version, int64_t early_blocks) { // TODO: How to get fork rule info from light wallet node? if(m_light_wallet) return true; uint64_t height, earliest_height; boost::optional result = m_node_rpc_proxy.get_height(height); throw_on_rpc_response_error(result, "get_info"); result = m_node_rpc_proxy.get_earliest_height(version, earliest_height); throw_on_rpc_response_error(result, "get_hard_fork_info"); bool close_enough = height >= earliest_height - early_blocks; // start using the rules that many blocks beforehand if (close_enough) LOG_PRINT_L2("Using v" << (unsigned)version << " rules"); else LOG_PRINT_L2("Not using v" << (unsigned)version << " rules"); return close_enough; } //---------------------------------------------------------------------------------------------------- uint64_t wallet2::get_upper_transaction_size_limit() { if (m_upper_transaction_size_limit > 0) return m_upper_transaction_size_limit; uint64_t full_reward_zone = use_fork_rules(5, 10) ? CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5 : use_fork_rules(2, 10) ? CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V2 : CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V1; return full_reward_zone - CRYPTONOTE_COINBASE_BLOB_RESERVED_SIZE; } //---------------------------------------------------------------------------------------------------- std::vector wallet2::select_available_outputs(const std::function &f) { std::vector outputs; size_t n = 0; for (transfer_container::const_iterator i = m_transfers.begin(); i != m_transfers.end(); ++i, ++n) { if (i->m_spent) continue; if (i->m_key_image_partial) continue; if (!is_transfer_unlocked(*i)) continue; if (f(*i)) outputs.push_back(n); } return outputs; } //---------------------------------------------------------------------------------------------------- std::vector wallet2::get_unspent_amounts_vector() { std::set set; for (const auto &td: m_transfers) { if (!td.m_spent) set.insert(td.is_rct() ? 0 : td.amount()); } std::vector vector; vector.reserve(set.size()); for (const auto &i: set) { vector.push_back(i); } return vector; } //---------------------------------------------------------------------------------------------------- std::vector wallet2::select_available_outputs_from_histogram(uint64_t count, bool atleast, bool unlocked, bool allow_rct, bool trusted_daemon) { epee::json_rpc::request req_t = AUTO_VAL_INIT(req_t); epee::json_rpc::response resp_t = AUTO_VAL_INIT(resp_t); m_daemon_rpc_mutex.lock(); req_t.jsonrpc = "2.0"; req_t.id = epee::serialization::storage_entry(0); req_t.method = "get_output_histogram"; if (trusted_daemon) req_t.params.amounts = get_unspent_amounts_vector(); req_t.params.min_count = count; req_t.params.max_count = 0; req_t.params.unlocked = unlocked; bool r = net_utils::invoke_http_json("/json_rpc", req_t, resp_t, m_http_client, rpc_timeout); m_daemon_rpc_mutex.unlock(); THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "select_available_outputs_from_histogram"); THROW_WALLET_EXCEPTION_IF(resp_t.result.status == CORE_RPC_STATUS_BUSY, error::daemon_busy, "get_output_histogram"); THROW_WALLET_EXCEPTION_IF(resp_t.result.status != CORE_RPC_STATUS_OK, error::get_histogram_error, resp_t.result.status); std::set mixable; for (const auto &i: resp_t.result.histogram) { mixable.insert(i.amount); } return select_available_outputs([mixable, atleast, allow_rct](const transfer_details &td) { if (!allow_rct && td.is_rct()) return false; const uint64_t amount = td.is_rct() ? 0 : td.amount(); if (atleast) { if (mixable.find(amount) != mixable.end()) return true; } else { if (mixable.find(amount) == mixable.end()) return true; } return false; }); } //---------------------------------------------------------------------------------------------------- uint64_t wallet2::get_num_rct_outputs() { epee::json_rpc::request req_t = AUTO_VAL_INIT(req_t); epee::json_rpc::response resp_t = AUTO_VAL_INIT(resp_t); m_daemon_rpc_mutex.lock(); req_t.jsonrpc = "2.0"; req_t.id = epee::serialization::storage_entry(0); req_t.method = "get_output_histogram"; req_t.params.amounts.push_back(0); req_t.params.min_count = 0; req_t.params.max_count = 0; bool r = net_utils::invoke_http_json("/json_rpc", req_t, resp_t, m_http_client, rpc_timeout); m_daemon_rpc_mutex.unlock(); THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "get_num_rct_outputs"); THROW_WALLET_EXCEPTION_IF(resp_t.result.status == CORE_RPC_STATUS_BUSY, error::daemon_busy, "get_output_histogram"); THROW_WALLET_EXCEPTION_IF(resp_t.result.status != CORE_RPC_STATUS_OK, error::get_histogram_error, resp_t.result.status); THROW_WALLET_EXCEPTION_IF(resp_t.result.histogram.size() != 1, error::get_histogram_error, "Expected exactly one response"); THROW_WALLET_EXCEPTION_IF(resp_t.result.histogram[0].amount != 0, error::get_histogram_error, "Expected 0 amount"); return resp_t.result.histogram[0].total_instances; } //---------------------------------------------------------------------------------------------------- const wallet2::transfer_details &wallet2::get_transfer_details(size_t idx) const { THROW_WALLET_EXCEPTION_IF(idx >= m_transfers.size(), error::wallet_internal_error, "Bad transfer index"); return m_transfers[idx]; } //---------------------------------------------------------------------------------------------------- std::vector wallet2::select_available_unmixable_outputs(bool trusted_daemon) { // request all outputs with less than 3 instances const size_t min_mixin = use_fork_rules(6, 10) ? 4 : 2; // v6 increases min mixin from 2 to 4 return select_available_outputs_from_histogram(min_mixin + 1, false, true, false, trusted_daemon); } //---------------------------------------------------------------------------------------------------- std::vector wallet2::select_available_mixable_outputs(bool trusted_daemon) { // request all outputs with at least 3 instances, so we can use mixin 2 with const size_t min_mixin = use_fork_rules(6, 10) ? 4 : 2; // v6 increases min mixin from 2 to 4 return select_available_outputs_from_histogram(min_mixin + 1, true, true, true, trusted_daemon); } //---------------------------------------------------------------------------------------------------- std::vector wallet2::create_unmixable_sweep_transactions(bool trusted_daemon) { // From hard fork 1, we don't consider small amounts to be dust anymore const bool hf1_rules = use_fork_rules(2, 10); // first hard fork has version 2 tx_dust_policy dust_policy(hf1_rules ? 0 : ::config::DEFAULT_DUST_THRESHOLD); const uint64_t fee_per_kb = get_per_kb_fee(); // may throw std::vector unmixable_outputs = select_available_unmixable_outputs(trusted_daemon); size_t num_dust_outputs = unmixable_outputs.size(); if (num_dust_outputs == 0) { return std::vector(); } // split in "dust" and "non dust" to make it easier to select outputs std::vector unmixable_transfer_outputs, unmixable_dust_outputs; for (auto n: unmixable_outputs) { if (m_transfers[n].amount() < fee_per_kb) unmixable_dust_outputs.push_back(n); else unmixable_transfer_outputs.push_back(n); } return create_transactions_from(m_account_public_address, false, unmixable_transfer_outputs, unmixable_dust_outputs, 0 /*fake_outs_count */, 0 /* unlock_time */, 1 /*priority */, std::vector(), trusted_daemon); } bool wallet2::get_tx_key(const crypto::hash &txid, crypto::secret_key &tx_key, std::vector &additional_tx_keys) const { additional_tx_keys.clear(); const std::unordered_map::const_iterator i = m_tx_keys.find(txid); if (i == m_tx_keys.end()) return false; tx_key = i->second; const auto j = m_additional_tx_keys.find(txid); if (j != m_additional_tx_keys.end()) additional_tx_keys = j->second; return true; } //---------------------------------------------------------------------------------------------------- std::string wallet2::get_spend_proof(const crypto::hash &txid, const std::string &message) { THROW_WALLET_EXCEPTION_IF(m_watch_only, error::wallet_internal_error, "get_spend_proof requires spend secret key and is not available for a watch-only wallet"); // fetch tx from daemon COMMAND_RPC_GET_TRANSACTIONS::request req = AUTO_VAL_INIT(req); req.txs_hashes.push_back(epee::string_tools::pod_to_hex(txid)); req.decode_as_json = false; COMMAND_RPC_GET_TRANSACTIONS::response res = AUTO_VAL_INIT(res); bool r; { const boost::lock_guard lock{m_daemon_rpc_mutex}; r = epee::net_utils::invoke_http_json("/gettransactions", req, res, m_http_client, rpc_timeout); } THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "gettransactions"); THROW_WALLET_EXCEPTION_IF(res.status == CORE_RPC_STATUS_BUSY, error::daemon_busy, "gettransactions"); THROW_WALLET_EXCEPTION_IF(res.status != CORE_RPC_STATUS_OK, error::wallet_internal_error, "gettransactions"); THROW_WALLET_EXCEPTION_IF(res.txs.size() != 1, error::wallet_internal_error, "daemon returned wrong response for gettransactions, wrong txs count = " + std::to_string(res.txs.size()) + ", expected 1"); cryptonote::blobdata bd; THROW_WALLET_EXCEPTION_IF(!epee::string_tools::parse_hexstr_to_binbuff(res.txs[0].as_hex, bd), error::wallet_internal_error, "failed to parse tx from hexstr"); cryptonote::transaction tx; crypto::hash tx_hash, tx_prefix_hash; THROW_WALLET_EXCEPTION_IF(!cryptonote::parse_and_validate_tx_from_blob(bd, tx, tx_hash, tx_prefix_hash), error::wallet_internal_error, "failed to parse tx from blob"); THROW_WALLET_EXCEPTION_IF(tx_hash != txid, error::wallet_internal_error, "txid mismatch"); std::vector> signatures; // get signature prefix hash std::string sig_prefix_data((const char*)&txid, sizeof(crypto::hash)); sig_prefix_data += message; crypto::hash sig_prefix_hash; crypto::cn_fast_hash(sig_prefix_data.data(), sig_prefix_data.size(), sig_prefix_hash); for(size_t i = 0; i < tx.vin.size(); ++i) { const txin_to_key* const in_key = boost::get(std::addressof(tx.vin[i])); if (in_key == nullptr) continue; // check if the key image belongs to us const auto found = m_key_images.find(in_key->k_image); if(found == m_key_images.end()) { THROW_WALLET_EXCEPTION_IF(i > 0, error::wallet_internal_error, "subset of key images belong to us, very weird!"); THROW_WALLET_EXCEPTION_IF(true, error::wallet_internal_error, "This tx wasn't generated by this wallet!"); } // derive the real output keypair const transfer_details& in_td = m_transfers[found->second]; const txout_to_key* const in_tx_out_pkey = boost::get(std::addressof(in_td.m_tx.vout[in_td.m_internal_output_index].target)); THROW_WALLET_EXCEPTION_IF(in_tx_out_pkey == nullptr, error::wallet_internal_error, "Output is not txout_to_key"); const crypto::public_key in_tx_pub_key = get_tx_pub_key_from_extra(in_td.m_tx, in_td.m_pk_index); const std::vector in_additionakl_tx_pub_keys = get_additional_tx_pub_keys_from_extra(in_td.m_tx); keypair in_ephemeral; crypto::key_image in_img; THROW_WALLET_EXCEPTION_IF(!generate_key_image_helper(m_account.get_keys(), m_subaddresses, in_tx_out_pkey->key, in_tx_pub_key, in_additionakl_tx_pub_keys, in_td.m_internal_output_index, in_ephemeral, in_img), error::wallet_internal_error, "failed to generate key image"); THROW_WALLET_EXCEPTION_IF(in_key->k_image != in_img, error::wallet_internal_error, "key image mismatch"); // get output pubkeys in the ring const std::vector absolute_offsets = cryptonote::relative_output_offsets_to_absolute(in_key->key_offsets); const size_t ring_size = in_key->key_offsets.size(); THROW_WALLET_EXCEPTION_IF(absolute_offsets.size() != ring_size, error::wallet_internal_error, "absolute offsets size is wrong"); COMMAND_RPC_GET_OUTPUTS_BIN::request req = AUTO_VAL_INIT(req); req.outputs.resize(ring_size); for (size_t j = 0; j < ring_size; ++j) { req.outputs[j].amount = in_key->amount; req.outputs[j].index = absolute_offsets[j]; } COMMAND_RPC_GET_OUTPUTS_BIN::response res = AUTO_VAL_INIT(res); bool r; { const boost::lock_guard lock{m_daemon_rpc_mutex}; r = epee::net_utils::invoke_http_bin("/get_outs.bin", req, res, m_http_client, rpc_timeout); } THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "get_outs.bin"); THROW_WALLET_EXCEPTION_IF(res.status == CORE_RPC_STATUS_BUSY, error::daemon_busy, "get_outs.bin"); THROW_WALLET_EXCEPTION_IF(res.status != CORE_RPC_STATUS_OK, error::wallet_internal_error, "get_outs.bin"); THROW_WALLET_EXCEPTION_IF(res.outs.size() != ring_size, error::wallet_internal_error, "daemon returned wrong response for get_outs.bin, wrong amounts count = " + std::to_string(res.outs.size()) + ", expected " + std::to_string(ring_size)); // copy pubkey pointers std::vector p_output_keys; for (const COMMAND_RPC_GET_OUTPUTS_BIN::outkey &out : res.outs) p_output_keys.push_back(&out.key); // figure out real output index and secret key size_t sec_index = -1; for (size_t j = 0; j < ring_size; ++j) { if (res.outs[j].key == in_ephemeral.pub) { sec_index = j; break; } } THROW_WALLET_EXCEPTION_IF(sec_index >= ring_size, error::wallet_internal_error, "secret index not found"); // generate ring sig for this input signatures.push_back(std::vector()); std::vector& sigs = signatures.back(); sigs.resize(in_key->key_offsets.size()); crypto::generate_ring_signature(sig_prefix_hash, in_key->k_image, p_output_keys, in_ephemeral.sec, sec_index, sigs.data()); } std::string sig_str = "SpendProofV1"; for (const std::vector& ring_sig : signatures) for (const crypto::signature& sig : ring_sig) sig_str += tools::base58::encode(std::string((const char *)&sig, sizeof(crypto::signature))); return sig_str; } //---------------------------------------------------------------------------------------------------- bool wallet2::check_spend_proof(const crypto::hash &txid, const std::string &message, const std::string &sig_str) { const std::string header = "SpendProofV1"; const size_t header_len = header.size(); THROW_WALLET_EXCEPTION_IF(sig_str.size() < header_len || sig_str.substr(0, header_len) != header, error::wallet_internal_error, "Signature header check error"); // fetch tx from daemon COMMAND_RPC_GET_TRANSACTIONS::request req = AUTO_VAL_INIT(req); req.txs_hashes.push_back(epee::string_tools::pod_to_hex(txid)); req.decode_as_json = false; COMMAND_RPC_GET_TRANSACTIONS::response res = AUTO_VAL_INIT(res); bool r; { const boost::lock_guard lock{m_daemon_rpc_mutex}; r = epee::net_utils::invoke_http_json("/gettransactions", req, res, m_http_client, rpc_timeout); } THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "gettransactions"); THROW_WALLET_EXCEPTION_IF(res.status == CORE_RPC_STATUS_BUSY, error::daemon_busy, "gettransactions"); THROW_WALLET_EXCEPTION_IF(res.status != CORE_RPC_STATUS_OK, error::wallet_internal_error, "gettransactions"); THROW_WALLET_EXCEPTION_IF(res.txs.size() != 1, error::wallet_internal_error, "daemon returned wrong response for gettransactions, wrong txs count = " + std::to_string(res.txs.size()) + ", expected 1"); cryptonote::blobdata bd; THROW_WALLET_EXCEPTION_IF(!epee::string_tools::parse_hexstr_to_binbuff(res.txs[0].as_hex, bd), error::wallet_internal_error, "failed to parse tx from hexstr"); cryptonote::transaction tx; crypto::hash tx_hash, tx_prefix_hash; THROW_WALLET_EXCEPTION_IF(!cryptonote::parse_and_validate_tx_from_blob(bd, tx, tx_hash, tx_prefix_hash), error::wallet_internal_error, "failed to parse tx from blob"); THROW_WALLET_EXCEPTION_IF(tx_hash != txid, error::wallet_internal_error, "txid mismatch"); // check signature size size_t num_sigs = 0; for(size_t i = 0; i < tx.vin.size(); ++i) { const txin_to_key* const in_key = boost::get(std::addressof(tx.vin[i])); if (in_key != nullptr) num_sigs += in_key->key_offsets.size(); } std::vector> signatures = { std::vector(1) }; const size_t sig_len = tools::base58::encode(std::string((const char *)&signatures[0][0], sizeof(crypto::signature))).size(); THROW_WALLET_EXCEPTION_IF(sig_str.size() != header_len + num_sigs * sig_len, error::wallet_internal_error, "incorrect signature size"); // decode base58 signatures.clear(); size_t offset = header_len; for(size_t i = 0; i < tx.vin.size(); ++i) { const txin_to_key* const in_key = boost::get(std::addressof(tx.vin[i])); if (in_key == nullptr) continue; signatures.resize(signatures.size() + 1); signatures.back().resize(in_key->key_offsets.size()); for (size_t j = 0; j < in_key->key_offsets.size(); ++j) { std::string sig_decoded; THROW_WALLET_EXCEPTION_IF(!tools::base58::decode(sig_str.substr(offset, sig_len), sig_decoded), error::wallet_internal_error, "Signature decoding error"); THROW_WALLET_EXCEPTION_IF(sizeof(crypto::signature) != sig_decoded.size(), error::wallet_internal_error, "Signature decoding error"); memcpy(&signatures.back()[j], sig_decoded.data(), sizeof(crypto::signature)); offset += sig_len; } } // get signature prefix hash std::string sig_prefix_data((const char*)&txid, sizeof(crypto::hash)); sig_prefix_data += message; crypto::hash sig_prefix_hash; crypto::cn_fast_hash(sig_prefix_data.data(), sig_prefix_data.size(), sig_prefix_hash); std::vector>::const_iterator sig_iter = signatures.cbegin(); for(size_t i = 0; i < tx.vin.size(); ++i) { const txin_to_key* const in_key = boost::get(std::addressof(tx.vin[i])); if (in_key == nullptr) continue; // get output pubkeys in the ring COMMAND_RPC_GET_OUTPUTS_BIN::request req = AUTO_VAL_INIT(req); const std::vector absolute_offsets = cryptonote::relative_output_offsets_to_absolute(in_key->key_offsets); req.outputs.resize(absolute_offsets.size()); for (size_t j = 0; j < absolute_offsets.size(); ++j) { req.outputs[j].amount = in_key->amount; req.outputs[j].index = absolute_offsets[j]; } COMMAND_RPC_GET_OUTPUTS_BIN::response res = AUTO_VAL_INIT(res); bool r; { const boost::lock_guard lock{m_daemon_rpc_mutex}; r = epee::net_utils::invoke_http_bin("/get_outs.bin", req, res, m_http_client, rpc_timeout); } THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "get_outs.bin"); THROW_WALLET_EXCEPTION_IF(res.status == CORE_RPC_STATUS_BUSY, error::daemon_busy, "get_outs.bin"); THROW_WALLET_EXCEPTION_IF(res.status != CORE_RPC_STATUS_OK, error::wallet_internal_error, "get_outs.bin"); THROW_WALLET_EXCEPTION_IF(res.outs.size() != req.outputs.size(), error::wallet_internal_error, "daemon returned wrong response for get_outs.bin, wrong amounts count = " + std::to_string(res.outs.size()) + ", expected " + std::to_string(req.outputs.size())); // copy pointers std::vector p_output_keys; for (const COMMAND_RPC_GET_OUTPUTS_BIN::outkey &out : res.outs) p_output_keys.push_back(&out.key); // check this ring if (!crypto::check_ring_signature(sig_prefix_hash, in_key->k_image, p_output_keys, sig_iter->data())) return false; ++sig_iter; } THROW_WALLET_EXCEPTION_IF(sig_iter != signatures.cend(), error::wallet_internal_error, "Signature iterator didn't reach the end"); return true; } //---------------------------------------------------------------------------------------------------- void wallet2::check_tx_key(const crypto::hash &txid, const crypto::secret_key &tx_key, const std::vector &additional_tx_keys, const cryptonote::account_public_address &address, uint64_t &received, bool &in_pool, uint64_t &confirmations) { crypto::key_derivation derivation; THROW_WALLET_EXCEPTION_IF(!crypto::generate_key_derivation(address.m_view_public_key, tx_key, derivation), error::wallet_internal_error, "Failed to generate key derivation from supplied parameters"); std::vector additional_derivations; additional_derivations.resize(additional_tx_keys.size()); for (size_t i = 0; i < additional_tx_keys.size(); ++i) THROW_WALLET_EXCEPTION_IF(!crypto::generate_key_derivation(address.m_view_public_key, additional_tx_keys[i], additional_derivations[i]), error::wallet_internal_error, "Failed to generate key derivation from supplied parameters"); check_tx_key_helper(txid, derivation, additional_derivations, address, received, in_pool, confirmations); } void wallet2::check_tx_key_helper(const crypto::hash &txid, const crypto::key_derivation &derivation, const std::vector &additional_derivations, const cryptonote::account_public_address &address, uint64_t &received, bool &in_pool, uint64_t &confirmations) { COMMAND_RPC_GET_TRANSACTIONS::request req; COMMAND_RPC_GET_TRANSACTIONS::response res; req.txs_hashes.push_back(epee::string_tools::pod_to_hex(txid)); m_daemon_rpc_mutex.lock(); bool ok = epee::net_utils::invoke_http_json("/gettransactions", req, res, m_http_client); m_daemon_rpc_mutex.unlock(); THROW_WALLET_EXCEPTION_IF(!ok || (res.txs.size() != 1 && res.txs_as_hex.size() != 1), error::wallet_internal_error, "Failed to get transaction from daemon"); cryptonote::blobdata tx_data; if (res.txs.size() == 1) ok = string_tools::parse_hexstr_to_binbuff(res.txs.front().as_hex, tx_data); else ok = string_tools::parse_hexstr_to_binbuff(res.txs_as_hex.front(), tx_data); THROW_WALLET_EXCEPTION_IF(!ok, error::wallet_internal_error, "Failed to parse transaction from daemon"); crypto::hash tx_hash, tx_prefix_hash; cryptonote::transaction tx; THROW_WALLET_EXCEPTION_IF(!cryptonote::parse_and_validate_tx_from_blob(tx_data, tx, tx_hash, tx_prefix_hash), error::wallet_internal_error, "Failed to validate transaction from daemon"); THROW_WALLET_EXCEPTION_IF(tx_hash != txid, error::wallet_internal_error, "Failed to get the right transaction from daemon"); THROW_WALLET_EXCEPTION_IF(!additional_derivations.empty() && additional_derivations.size() != tx.vout.size(), error::wallet_internal_error, "The size of additional derivations is wrong"); received = 0; for (size_t n = 0; n < tx.vout.size(); ++n) { const cryptonote::txout_to_key* const out_key = boost::get(std::addressof(tx.vout[n].target)); if (!out_key) continue; crypto::public_key derived_out_key; bool r = derive_public_key(derivation, n, address.m_spend_public_key, derived_out_key); THROW_WALLET_EXCEPTION_IF(!r, error::wallet_internal_error, "Failed to derive public key"); bool found = out_key->key == derived_out_key; crypto::key_derivation found_derivation = derivation; if (!found && !additional_derivations.empty()) { r = derive_public_key(additional_derivations[n], n, address.m_spend_public_key, derived_out_key); THROW_WALLET_EXCEPTION_IF(!r, error::wallet_internal_error, "Failed to derive public key"); found = out_key->key == derived_out_key; found_derivation = additional_derivations[n]; } if (found) { uint64_t amount; if (tx.version == 1 || tx.rct_signatures.type == rct::RCTTypeNull) { amount = tx.vout[n].amount; } else { crypto::secret_key scalar1; crypto::derivation_to_scalar(found_derivation, n, scalar1); rct::ecdhTuple ecdh_info = tx.rct_signatures.ecdhInfo[n]; rct::ecdhDecode(ecdh_info, rct::sk2rct(scalar1)); const rct::key C = tx.rct_signatures.outPk[n].mask; rct::key Ctmp; rct::addKeys2(Ctmp, ecdh_info.mask, ecdh_info.amount, rct::H); if (rct::equalKeys(C, Ctmp)) amount = rct::h2d(ecdh_info.amount); else amount = 0; } received += amount; } } in_pool = res.txs.front().in_pool; confirmations = (uint64_t)-1; if (!in_pool) { std::string err; uint64_t bc_height = get_daemon_blockchain_height(err); if (err.empty()) confirmations = bc_height - (res.txs.front().block_height + 1); } } std::string wallet2::get_tx_proof(const crypto::hash &txid, const cryptonote::account_public_address &address, bool is_subaddress, const std::string &message) { // determine if the address is found in the subaddress hash table (i.e. whether the proof is outbound or inbound) const bool is_out = m_subaddresses.count(address.m_spend_public_key) == 0; std::string prefix_data((const char*)&txid, sizeof(crypto::hash)); prefix_data += message; crypto::hash prefix_hash; crypto::cn_fast_hash(prefix_data.data(), prefix_data.size(), prefix_hash); std::vector shared_secret; std::vector sig; std::string sig_str; if (is_out) { crypto::secret_key tx_key; std::vector additional_tx_keys; THROW_WALLET_EXCEPTION_IF(!get_tx_key(txid, tx_key, additional_tx_keys), error::wallet_internal_error, "Tx secret key wasn't found in the wallet file."); const size_t num_sigs = 1 + additional_tx_keys.size(); shared_secret.resize(num_sigs); sig.resize(num_sigs); shared_secret[0] = rct::rct2pk(rct::scalarmultKey(rct::pk2rct(address.m_view_public_key), rct::sk2rct(tx_key))); crypto::public_key tx_pub_key; if (is_subaddress) { tx_pub_key = rct2pk(rct::scalarmultKey(rct::pk2rct(address.m_spend_public_key), rct::sk2rct(tx_key))); crypto::generate_tx_proof(prefix_hash, tx_pub_key, address.m_view_public_key, address.m_spend_public_key, shared_secret[0], tx_key, sig[0]); } else { crypto::secret_key_to_public_key(tx_key, tx_pub_key); crypto::generate_tx_proof(prefix_hash, tx_pub_key, address.m_view_public_key, boost::none, shared_secret[0], tx_key, sig[0]); } for (size_t i = 1; i < num_sigs; ++i) { shared_secret[i] = rct::rct2pk(rct::scalarmultKey(rct::pk2rct(address.m_view_public_key), rct::sk2rct(additional_tx_keys[i - 1]))); if (is_subaddress) { tx_pub_key = rct2pk(rct::scalarmultKey(rct::pk2rct(address.m_spend_public_key), rct::sk2rct(additional_tx_keys[i - 1]))); crypto::generate_tx_proof(prefix_hash, tx_pub_key, address.m_view_public_key, address.m_spend_public_key, shared_secret[i], additional_tx_keys[i - 1], sig[i]); } else { crypto::secret_key_to_public_key(additional_tx_keys[i - 1], tx_pub_key); crypto::generate_tx_proof(prefix_hash, tx_pub_key, address.m_view_public_key, boost::none, shared_secret[i], additional_tx_keys[i - 1], sig[i]); } } sig_str = std::string("OutProofV1"); } else { // fetch tx pubkey from the daemon COMMAND_RPC_GET_TRANSACTIONS::request req; COMMAND_RPC_GET_TRANSACTIONS::response res; req.txs_hashes.push_back(epee::string_tools::pod_to_hex(txid)); m_daemon_rpc_mutex.lock(); bool ok = net_utils::invoke_http_json("/gettransactions", req, res, m_http_client); m_daemon_rpc_mutex.unlock(); THROW_WALLET_EXCEPTION_IF(!ok || (res.txs.size() != 1 && res.txs_as_hex.size() != 1), error::wallet_internal_error, "Failed to get transaction from daemon"); cryptonote::blobdata tx_data; if (res.txs.size() == 1) ok = string_tools::parse_hexstr_to_binbuff(res.txs.front().as_hex, tx_data); else ok = string_tools::parse_hexstr_to_binbuff(res.txs_as_hex.front(), tx_data); THROW_WALLET_EXCEPTION_IF(!ok, error::wallet_internal_error, "Failed to parse transaction from daemon"); crypto::hash tx_hash, tx_prefix_hash; cryptonote::transaction tx; THROW_WALLET_EXCEPTION_IF(!cryptonote::parse_and_validate_tx_from_blob(tx_data, tx, tx_hash, tx_prefix_hash), error::wallet_internal_error, "Failed to validate transaction from daemon"); THROW_WALLET_EXCEPTION_IF(tx_hash != txid, error::wallet_internal_error, "Failed to get the right transaction from daemon"); crypto::public_key tx_pub_key = get_tx_pub_key_from_extra(tx); THROW_WALLET_EXCEPTION_IF(tx_pub_key == null_pkey, error::wallet_internal_error, "Tx pubkey was not found"); std::vector additional_tx_pub_keys = get_additional_tx_pub_keys_from_extra(tx); const size_t num_sigs = 1 + additional_tx_pub_keys.size(); shared_secret.resize(num_sigs); sig.resize(num_sigs); const crypto::secret_key& a = m_account.get_keys().m_view_secret_key; shared_secret[0] = rct::rct2pk(rct::scalarmultKey(rct::pk2rct(tx_pub_key), rct::sk2rct(a))); if (is_subaddress) { crypto::generate_tx_proof(prefix_hash, address.m_view_public_key, tx_pub_key, address.m_spend_public_key, shared_secret[0], a, sig[0]); } else { crypto::generate_tx_proof(prefix_hash, address.m_view_public_key, tx_pub_key, boost::none, shared_secret[0], a, sig[0]); } for (size_t i = 1; i < num_sigs; ++i) { shared_secret[i] = rct::rct2pk(rct::scalarmultKey(rct::pk2rct(additional_tx_pub_keys[i - 1]), rct::sk2rct(a))); if (is_subaddress) { crypto::generate_tx_proof(prefix_hash, address.m_view_public_key, additional_tx_pub_keys[i - 1], address.m_spend_public_key, shared_secret[i], a, sig[i]); } else { crypto::generate_tx_proof(prefix_hash, address.m_view_public_key, additional_tx_pub_keys[i - 1], boost::none, shared_secret[i], a, sig[i]); } } sig_str = std::string("InProofV1"); } const size_t num_sigs = shared_secret.size(); // check if this address actually received any funds crypto::key_derivation derivation; THROW_WALLET_EXCEPTION_IF(!crypto::generate_key_derivation(shared_secret[0], rct::rct2sk(rct::I), derivation), error::wallet_internal_error, "Failed to generate key derivation"); std::vector additional_derivations(num_sigs - 1); for (size_t i = 1; i < num_sigs; ++i) THROW_WALLET_EXCEPTION_IF(!crypto::generate_key_derivation(shared_secret[i], rct::rct2sk(rct::I), additional_derivations[i - 1]), error::wallet_internal_error, "Failed to generate key derivation"); uint64_t received; bool in_pool; uint64_t confirmations; check_tx_key_helper(txid, derivation, additional_derivations, address, received, in_pool, confirmations); THROW_WALLET_EXCEPTION_IF(!received, error::wallet_internal_error, tr("No funds received in this tx.")); // concatenate all signature strings for (size_t i = 0; i < num_sigs; ++i) sig_str += tools::base58::encode(std::string((const char *)&shared_secret[i], sizeof(crypto::public_key))) + tools::base58::encode(std::string((const char *)&sig[i], sizeof(crypto::signature))); return sig_str; } bool wallet2::check_tx_proof(const crypto::hash &txid, const cryptonote::account_public_address &address, bool is_subaddress, const std::string &message, const std::string &sig_str, uint64_t &received, bool &in_pool, uint64_t &confirmations) { const bool is_out = sig_str.substr(0, 3) == "Out"; const std::string header = is_out ? "OutProofV1" : "InProofV1"; const size_t header_len = header.size(); THROW_WALLET_EXCEPTION_IF(sig_str.size() < header_len || sig_str.substr(0, header_len) != header, error::wallet_internal_error, "Signature header check error"); // decode base58 std::vector shared_secret(1); std::vector sig(1); const size_t pk_len = tools::base58::encode(std::string((const char *)&shared_secret[0], sizeof(crypto::public_key))).size(); const size_t sig_len = tools::base58::encode(std::string((const char *)&sig[0], sizeof(crypto::signature))).size(); const size_t num_sigs = (sig_str.size() - header_len) / (pk_len + sig_len); THROW_WALLET_EXCEPTION_IF(sig_str.size() != header_len + num_sigs * (pk_len + sig_len), error::wallet_internal_error, "Wrong signature size"); shared_secret.resize(num_sigs); sig.resize(num_sigs); for (size_t i = 0; i < num_sigs; ++i) { std::string pk_decoded; std::string sig_decoded; const size_t offset = header_len + i * (pk_len + sig_len); THROW_WALLET_EXCEPTION_IF(!tools::base58::decode(sig_str.substr(offset, pk_len), pk_decoded), error::wallet_internal_error, "Signature decoding error"); THROW_WALLET_EXCEPTION_IF(!tools::base58::decode(sig_str.substr(offset + pk_len, sig_len), sig_decoded), error::wallet_internal_error, "Signature decoding error"); THROW_WALLET_EXCEPTION_IF(sizeof(crypto::public_key) != pk_decoded.size() || sizeof(crypto::signature) != sig_decoded.size(), error::wallet_internal_error, "Signature decoding error"); memcpy(&shared_secret[i], pk_decoded.data(), sizeof(crypto::public_key)); memcpy(&sig[i], sig_decoded.data(), sizeof(crypto::signature)); } // fetch tx pubkey from the daemon COMMAND_RPC_GET_TRANSACTIONS::request req; COMMAND_RPC_GET_TRANSACTIONS::response res; req.txs_hashes.push_back(epee::string_tools::pod_to_hex(txid)); m_daemon_rpc_mutex.lock(); bool ok = net_utils::invoke_http_json("/gettransactions", req, res, m_http_client); m_daemon_rpc_mutex.unlock(); THROW_WALLET_EXCEPTION_IF(!ok || (res.txs.size() != 1 && res.txs_as_hex.size() != 1), error::wallet_internal_error, "Failed to get transaction from daemon"); cryptonote::blobdata tx_data; if (res.txs.size() == 1) ok = string_tools::parse_hexstr_to_binbuff(res.txs.front().as_hex, tx_data); else ok = string_tools::parse_hexstr_to_binbuff(res.txs_as_hex.front(), tx_data); THROW_WALLET_EXCEPTION_IF(!ok, error::wallet_internal_error, "Failed to parse transaction from daemon"); crypto::hash tx_hash, tx_prefix_hash; cryptonote::transaction tx; THROW_WALLET_EXCEPTION_IF(!cryptonote::parse_and_validate_tx_from_blob(tx_data, tx, tx_hash, tx_prefix_hash), error::wallet_internal_error, "Failed to validate transaction from daemon"); THROW_WALLET_EXCEPTION_IF(tx_hash != txid, error::wallet_internal_error, "Failed to get the right transaction from daemon"); crypto::public_key tx_pub_key = get_tx_pub_key_from_extra(tx); THROW_WALLET_EXCEPTION_IF(tx_pub_key == null_pkey, error::wallet_internal_error, "Tx pubkey was not found"); std::vector additional_tx_pub_keys = get_additional_tx_pub_keys_from_extra(tx); THROW_WALLET_EXCEPTION_IF(additional_tx_pub_keys.size() + 1 != num_sigs, error::wallet_internal_error, "Signature size mismatch with additional tx pubkeys"); std::string prefix_data((const char*)&txid, sizeof(crypto::hash)); prefix_data += message; crypto::hash prefix_hash; crypto::cn_fast_hash(prefix_data.data(), prefix_data.size(), prefix_hash); // check signature std::vector good_signature(num_sigs, 0); if (is_out) { good_signature[0] = is_subaddress ? crypto::check_tx_proof(prefix_hash, tx_pub_key, address.m_view_public_key, address.m_spend_public_key, shared_secret[0], sig[0]) : crypto::check_tx_proof(prefix_hash, tx_pub_key, address.m_view_public_key, boost::none, shared_secret[0], sig[0]); for (size_t i = 0; i < additional_tx_pub_keys.size(); ++i) { good_signature[i + 1] = is_subaddress ? crypto::check_tx_proof(prefix_hash, additional_tx_pub_keys[i], address.m_view_public_key, address.m_spend_public_key, shared_secret[i + 1], sig[i + 1]) : crypto::check_tx_proof(prefix_hash, additional_tx_pub_keys[i], address.m_view_public_key, boost::none, shared_secret[i + 1], sig[i + 1]); } } else { good_signature[0] = is_subaddress ? crypto::check_tx_proof(prefix_hash, address.m_view_public_key, tx_pub_key, address.m_spend_public_key, shared_secret[0], sig[0]) : crypto::check_tx_proof(prefix_hash, address.m_view_public_key, tx_pub_key, boost::none, shared_secret[0], sig[0]); for (size_t i = 0; i < additional_tx_pub_keys.size(); ++i) { good_signature[i + 1] = is_subaddress ? crypto::check_tx_proof(prefix_hash, address.m_view_public_key, additional_tx_pub_keys[i], address.m_spend_public_key, shared_secret[i + 1], sig[i + 1]) : crypto::check_tx_proof(prefix_hash, address.m_view_public_key, additional_tx_pub_keys[i], boost::none, shared_secret[i + 1], sig[i + 1]); } } if (std::any_of(good_signature.begin(), good_signature.end(), [](int i) { return i > 0; })) { // obtain key derivation by multiplying scalar 1 to the shared secret crypto::key_derivation derivation; if (good_signature[0]) THROW_WALLET_EXCEPTION_IF(!crypto::generate_key_derivation(shared_secret[0], rct::rct2sk(rct::I), derivation), error::wallet_internal_error, "Failed to generate key derivation"); std::vector additional_derivations(num_sigs - 1); for (size_t i = 1; i < num_sigs; ++i) if (good_signature[i]) THROW_WALLET_EXCEPTION_IF(!crypto::generate_key_derivation(shared_secret[i], rct::rct2sk(rct::I), additional_derivations[i - 1]), error::wallet_internal_error, "Failed to generate key derivation"); check_tx_key_helper(txid, derivation, additional_derivations, address, received, in_pool, confirmations); return true; } return false; } std::string wallet2::get_wallet_file() const { return m_wallet_file; } std::string wallet2::get_keys_file() const { return m_keys_file; } std::string wallet2::get_daemon_address() const { return m_daemon_address; } uint64_t wallet2::get_daemon_blockchain_height(string &err) { uint64_t height; boost::optional result = m_node_rpc_proxy.get_height(height); if (result) { err = *result; return 0; } err = ""; return height; } uint64_t wallet2::get_daemon_blockchain_target_height(string &err) { epee::json_rpc::request req_t = AUTO_VAL_INIT(req_t); epee::json_rpc::response resp_t = AUTO_VAL_INIT(resp_t); m_daemon_rpc_mutex.lock(); req_t.jsonrpc = "2.0"; req_t.id = epee::serialization::storage_entry(0); req_t.method = "get_info"; bool ok = net_utils::invoke_http_json("/json_rpc", req_t, resp_t, m_http_client); m_daemon_rpc_mutex.unlock(); if (ok) { if (resp_t.result.status == CORE_RPC_STATUS_BUSY) { err = "daemon is busy. Please try again later."; } else if (resp_t.result.status != CORE_RPC_STATUS_OK) { err = resp_t.result.status; } else // success, cleaning up error message { err = ""; } } else { err = "possibly lost connection to daemon"; } return resp_t.result.target_height; } uint64_t wallet2::get_approximate_blockchain_height() const { // time of v2 fork const time_t fork_time = m_testnet ? 1448285909 : 1458748658; // v2 fork block const uint64_t fork_block = m_testnet ? 624634 : 1009827; // avg seconds per block const int seconds_per_block = DIFFICULTY_TARGET_V2; // Calculated blockchain height uint64_t approx_blockchain_height = fork_block + (time(NULL) - fork_time)/seconds_per_block; // testnet got some huge rollbacks, so the estimation is way off if (m_testnet && approx_blockchain_height > 105000) approx_blockchain_height -= 105000; LOG_PRINT_L2("Calculated blockchain height: " << approx_blockchain_height); return approx_blockchain_height; } void wallet2::set_tx_note(const crypto::hash &txid, const std::string ¬e) { m_tx_notes[txid] = note; } std::string wallet2::get_tx_note(const crypto::hash &txid) const { std::unordered_map::const_iterator i = m_tx_notes.find(txid); if (i == m_tx_notes.end()) return std::string(); return i->second; } void wallet2::set_attribute(const std::string &key, const std::string &value) { m_attributes[key] = value; } std::string wallet2::get_attribute(const std::string &key) const { std::unordered_map::const_iterator i = m_attributes.find(key); if (i == m_attributes.end()) return std::string(); return i->second; } void wallet2::set_description(const std::string &description) { set_attribute(ATTRIBUTE_DESCRIPTION, description); } std::string wallet2::get_description() const { return get_attribute(ATTRIBUTE_DESCRIPTION); } const std::pair, std::vector>& wallet2::get_account_tags() { // ensure consistency if (m_account_tags.second.size() != get_num_subaddress_accounts()) m_account_tags.second.resize(get_num_subaddress_accounts(), ""); for (const std::string& tag : m_account_tags.second) { if (!tag.empty() && m_account_tags.first.count(tag) == 0) m_account_tags.first.insert({tag, ""}); } for (auto i = m_account_tags.first.begin(); i != m_account_tags.first.end(); ) { if (std::find(m_account_tags.second.begin(), m_account_tags.second.end(), i->first) == m_account_tags.second.end()) i = m_account_tags.first.erase(i); else ++i; } return m_account_tags; } void wallet2::set_account_tag(const std::set account_indices, const std::string& tag) { for (uint32_t account_index : account_indices) { THROW_WALLET_EXCEPTION_IF(account_index >= get_num_subaddress_accounts(), error::wallet_internal_error, "Account index out of bound"); if (m_account_tags.second[account_index] == tag) MDEBUG("This tag is already assigned to this account"); else m_account_tags.second[account_index] = tag; } get_account_tags(); } void wallet2::set_account_tag_description(const std::string& tag, const std::string& description) { THROW_WALLET_EXCEPTION_IF(tag.empty(), error::wallet_internal_error, "Tag must not be empty"); THROW_WALLET_EXCEPTION_IF(m_account_tags.first.count(tag) == 0, error::wallet_internal_error, "Tag is unregistered"); m_account_tags.first[tag] = description; } std::string wallet2::sign(const std::string &data) const { crypto::hash hash; crypto::cn_fast_hash(data.data(), data.size(), hash); const cryptonote::account_keys &keys = m_account.get_keys(); crypto::signature signature; crypto::generate_signature(hash, keys.m_account_address.m_spend_public_key, keys.m_spend_secret_key, signature); return std::string("SigV1") + tools::base58::encode(std::string((const char *)&signature, sizeof(signature))); } bool wallet2::verify(const std::string &data, const cryptonote::account_public_address &address, const std::string &signature) const { const size_t header_len = strlen("SigV1"); if (signature.size() < header_len || signature.substr(0, header_len) != "SigV1") { LOG_PRINT_L0("Signature header check error"); return false; } crypto::hash hash; crypto::cn_fast_hash(data.data(), data.size(), hash); std::string decoded; if (!tools::base58::decode(signature.substr(header_len), decoded)) { LOG_PRINT_L0("Signature decoding error"); return false; } crypto::signature s; if (sizeof(s) != decoded.size()) { LOG_PRINT_L0("Signature decoding error"); return false; } memcpy(&s, decoded.data(), sizeof(s)); return crypto::check_signature(hash, address.m_spend_public_key, s); } //---------------------------------------------------------------------------------------------------- crypto::public_key wallet2::get_tx_pub_key_from_received_outs(const tools::wallet2::transfer_details &td) const { std::vector tx_extra_fields; if(!parse_tx_extra(td.m_tx.extra, tx_extra_fields)) { // Extra may only be partially parsed, it's OK if tx_extra_fields contains public key } // Due to a previous bug, there might be more than one tx pubkey in extra, one being // the result of a previously discarded signature. // For speed, since scanning for outputs is a slow process, we check whether extra // contains more than one pubkey. If not, the first one is returned. If yes, they're // checked for whether they yield at least one output tx_extra_pub_key pub_key_field; THROW_WALLET_EXCEPTION_IF(!find_tx_extra_field_by_type(tx_extra_fields, pub_key_field, 0), error::wallet_internal_error, "Public key wasn't found in the transaction extra"); const crypto::public_key tx_pub_key = pub_key_field.pub_key; bool two_found = find_tx_extra_field_by_type(tx_extra_fields, pub_key_field, 1); if (!two_found) { // easy case, just one found return tx_pub_key; } // more than one, loop and search const cryptonote::account_keys& keys = m_account.get_keys(); size_t pk_index = 0; const std::vector additional_tx_pub_keys = get_additional_tx_pub_keys_from_extra(td.m_tx); std::vector additional_derivations; for (size_t i = 0; i < additional_tx_pub_keys.size(); ++i) { additional_derivations.push_back({}); bool r = generate_key_derivation(additional_tx_pub_keys[i], keys.m_view_secret_key, additional_derivations.back()); THROW_WALLET_EXCEPTION_IF(!r, error::wallet_internal_error, "Failed to generate key derivation"); } while (find_tx_extra_field_by_type(tx_extra_fields, pub_key_field, pk_index++)) { const crypto::public_key tx_pub_key = pub_key_field.pub_key; crypto::key_derivation derivation; bool r = generate_key_derivation(tx_pub_key, keys.m_view_secret_key, derivation); THROW_WALLET_EXCEPTION_IF(!r, error::wallet_internal_error, "Failed to generate key derivation"); for (size_t i = 0; i < td.m_tx.vout.size(); ++i) { tx_scan_info_t tx_scan_info; check_acc_out_precomp(td.m_tx.vout[i], derivation, additional_derivations, i, tx_scan_info); if (!tx_scan_info.error && tx_scan_info.received) return tx_pub_key; } } // we found no key yielding an output THROW_WALLET_EXCEPTION_IF(true, error::wallet_internal_error, "Public key yielding at least one output wasn't found in the transaction extra"); return crypto::null_pkey; } bool wallet2::export_key_images(const std::string &filename) { std::vector> ski = export_key_images(); std::string magic(KEY_IMAGE_EXPORT_FILE_MAGIC, strlen(KEY_IMAGE_EXPORT_FILE_MAGIC)); const cryptonote::account_public_address &keys = get_account().get_keys().m_account_address; std::string data; data += std::string((const char *)&keys.m_spend_public_key, sizeof(crypto::public_key)); data += std::string((const char *)&keys.m_view_public_key, sizeof(crypto::public_key)); for (const auto &i: ski) { data += std::string((const char *)&i.first, sizeof(crypto::key_image)); data += std::string((const char *)&i.second, sizeof(crypto::signature)); } // encrypt data, keep magic plaintext std::string ciphertext = encrypt_with_view_secret_key(data); return epee::file_io_utils::save_string_to_file(filename, magic + ciphertext); } //---------------------------------------------------------------------------------------------------- std::vector> wallet2::export_key_images() const { std::vector> ski; ski.reserve(m_transfers.size()); for (size_t n = 0; n < m_transfers.size(); ++n) { const transfer_details &td = m_transfers[n]; crypto::hash hash; crypto::cn_fast_hash(&td.m_key_image, sizeof(td.m_key_image), hash); // get ephemeral public key const cryptonote::tx_out &out = td.m_tx.vout[td.m_internal_output_index]; THROW_WALLET_EXCEPTION_IF(out.target.type() != typeid(txout_to_key), error::wallet_internal_error, "Output is not txout_to_key"); const cryptonote::txout_to_key &o = boost::get(out.target); const crypto::public_key pkey = o.key; // get tx pub key std::vector tx_extra_fields; if(!parse_tx_extra(td.m_tx.extra, tx_extra_fields)) { // Extra may only be partially parsed, it's OK if tx_extra_fields contains public key } crypto::public_key tx_pub_key = get_tx_pub_key_from_received_outs(td); const std::vector additional_tx_pub_keys = get_additional_tx_pub_keys_from_extra(td.m_tx); // generate ephemeral secret key crypto::key_image ki; cryptonote::keypair in_ephemeral; bool r = cryptonote::generate_key_image_helper(m_account.get_keys(), m_subaddresses, pkey, tx_pub_key, additional_tx_pub_keys, td.m_internal_output_index, in_ephemeral, ki); THROW_WALLET_EXCEPTION_IF(!r, error::wallet_internal_error, "Failed to generate key image"); THROW_WALLET_EXCEPTION_IF(td.m_key_image_known && !td.m_key_image_partial && ki != td.m_key_image, error::wallet_internal_error, "key_image generated not matched with cached key image"); THROW_WALLET_EXCEPTION_IF(in_ephemeral.pub != pkey, error::wallet_internal_error, "key_image generated ephemeral public key not matched with output_key"); // sign the key image with the output secret key crypto::signature signature; std::vector key_ptrs; key_ptrs.push_back(&pkey); crypto::generate_ring_signature((const crypto::hash&)td.m_key_image, td.m_key_image, key_ptrs, in_ephemeral.sec, 0, &signature); ski.push_back(std::make_pair(td.m_key_image, signature)); } return ski; } uint64_t wallet2::import_key_images(const std::string &filename, uint64_t &spent, uint64_t &unspent) { std::string data; bool r = epee::file_io_utils::load_file_to_string(filename, data); THROW_WALLET_EXCEPTION_IF(!r, error::wallet_internal_error, std::string(tr("failed to read file ")) + filename); const size_t magiclen = strlen(KEY_IMAGE_EXPORT_FILE_MAGIC); if (data.size() < magiclen || memcmp(data.data(), KEY_IMAGE_EXPORT_FILE_MAGIC, magiclen)) { THROW_WALLET_EXCEPTION(error::wallet_internal_error, std::string("Bad key image export file magic in ") + filename); } try { data = decrypt_with_view_secret_key(std::string(data, magiclen)); } catch (const std::exception &e) { THROW_WALLET_EXCEPTION(error::wallet_internal_error, std::string("Failed to decrypt ") + filename + ": " + e.what()); } const size_t headerlen = 2 * sizeof(crypto::public_key); THROW_WALLET_EXCEPTION_IF(data.size() < headerlen, error::wallet_internal_error, std::string("Bad data size from file ") + filename); const crypto::public_key &public_spend_key = *(const crypto::public_key*)&data[0]; const crypto::public_key &public_view_key = *(const crypto::public_key*)&data[sizeof(crypto::public_key)]; const cryptonote::account_public_address &keys = get_account().get_keys().m_account_address; if (public_spend_key != keys.m_spend_public_key || public_view_key != keys.m_view_public_key) { THROW_WALLET_EXCEPTION(error::wallet_internal_error, std::string( "Key images from ") + filename + " are for a different account"); } const size_t record_size = sizeof(crypto::key_image) + sizeof(crypto::signature); THROW_WALLET_EXCEPTION_IF((data.size() - headerlen) % record_size, error::wallet_internal_error, std::string("Bad data size from file ") + filename); size_t nki = (data.size() - headerlen) / record_size; std::vector> ski; ski.reserve(nki); for (size_t n = 0; n < nki; ++n) { crypto::key_image key_image = *reinterpret_cast(&data[headerlen + n * record_size]); crypto::signature signature = *reinterpret_cast(&data[headerlen + n * record_size + sizeof(crypto::key_image)]); ski.push_back(std::make_pair(key_image, signature)); } return import_key_images(ski, spent, unspent); } //---------------------------------------------------------------------------------------------------- uint64_t wallet2::import_key_images(const std::vector> &signed_key_images, uint64_t &spent, uint64_t &unspent, bool check_spent) { COMMAND_RPC_IS_KEY_IMAGE_SPENT::request req = AUTO_VAL_INIT(req); COMMAND_RPC_IS_KEY_IMAGE_SPENT::response daemon_resp = AUTO_VAL_INIT(daemon_resp); THROW_WALLET_EXCEPTION_IF(signed_key_images.size() > m_transfers.size(), error::wallet_internal_error, "The blockchain is out of date compared to the signed key images"); if (signed_key_images.empty()) { spent = 0; unspent = 0; return 0; } for (size_t n = 0; n < signed_key_images.size(); ++n) { const transfer_details &td = m_transfers[n]; const crypto::key_image &key_image = signed_key_images[n].first; const crypto::signature &signature = signed_key_images[n].second; // get ephemeral public key const cryptonote::tx_out &out = td.m_tx.vout[td.m_internal_output_index]; THROW_WALLET_EXCEPTION_IF(out.target.type() != typeid(txout_to_key), error::wallet_internal_error, "Non txout_to_key output found"); const cryptonote::txout_to_key &o = boost::get(out.target); const crypto::public_key pkey = o.key; std::vector pkeys; pkeys.push_back(&pkey); THROW_WALLET_EXCEPTION_IF(!(rct::scalarmultKey(rct::ki2rct(key_image), rct::curveOrder()) == rct::identity()), error::wallet_internal_error, "Key image out of validity domain: input " + boost::lexical_cast(n) + "/" + boost::lexical_cast(signed_key_images.size()) + ", key image " + epee::string_tools::pod_to_hex(key_image)); THROW_WALLET_EXCEPTION_IF(!crypto::check_ring_signature((const crypto::hash&)key_image, key_image, pkeys, &signature), error::wallet_internal_error, "Signature check failed: input " + boost::lexical_cast(n) + "/" + boost::lexical_cast(signed_key_images.size()) + ", key image " + epee::string_tools::pod_to_hex(key_image) + ", signature " + epee::string_tools::pod_to_hex(signature) + ", pubkey " + epee::string_tools::pod_to_hex(*pkeys[0])); req.key_images.push_back(epee::string_tools::pod_to_hex(key_image)); } for (size_t n = 0; n < signed_key_images.size(); ++n) { m_transfers[n].m_key_image = signed_key_images[n].first; m_key_images[m_transfers[n].m_key_image] = n; m_transfers[n].m_key_image_known = true; m_transfers[n].m_key_image_partial = false; } if(check_spent) { m_daemon_rpc_mutex.lock(); bool r = epee::net_utils::invoke_http_json("/is_key_image_spent", req, daemon_resp, m_http_client, rpc_timeout); m_daemon_rpc_mutex.unlock(); THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "is_key_image_spent"); THROW_WALLET_EXCEPTION_IF(daemon_resp.status == CORE_RPC_STATUS_BUSY, error::daemon_busy, "is_key_image_spent"); THROW_WALLET_EXCEPTION_IF(daemon_resp.status != CORE_RPC_STATUS_OK, error::is_key_image_spent_error, daemon_resp.status); THROW_WALLET_EXCEPTION_IF(daemon_resp.spent_status.size() != signed_key_images.size(), error::wallet_internal_error, "daemon returned wrong response for is_key_image_spent, wrong amounts count = " + std::to_string(daemon_resp.spent_status.size()) + ", expected " + std::to_string(signed_key_images.size())); for (size_t n = 0; n < daemon_resp.spent_status.size(); ++n) { transfer_details &td = m_transfers[n]; td.m_spent = daemon_resp.spent_status[n] != COMMAND_RPC_IS_KEY_IMAGE_SPENT::UNSPENT; } } spent = 0; unspent = 0; std::unordered_set spent_txids; // For each spent key image, search for a tx in m_transfers that uses it as input. std::vector swept_transfers; // If such a spending tx wasn't found in m_transfers, this means the spending tx // was created by sweep_all, so we can't know the spent height and other detailed info. for(size_t i = 0; i < m_transfers.size(); ++i) { transfer_details &td = m_transfers[i]; uint64_t amount = td.amount(); if (td.m_spent) spent += amount; else unspent += amount; LOG_PRINT_L2("Transfer " << i << ": " << print_money(amount) << " (" << td.m_global_output_index << "): " << (td.m_spent ? "spent" : "unspent") << " (key image " << req.key_images[i] << ")"); if (i < daemon_resp.spent_status.size() && daemon_resp.spent_status[i] == COMMAND_RPC_IS_KEY_IMAGE_SPENT::SPENT_IN_BLOCKCHAIN) { bool is_spent_tx_found = false; for (auto it = m_transfers.rbegin(); &(*it) != &td; ++it) { bool is_spent_tx = false; for(const cryptonote::txin_v& in : it->m_tx.vin) { if(in.type() == typeid(cryptonote::txin_to_key) && td.m_key_image == boost::get(in).k_image) { is_spent_tx = true; break; } } if (is_spent_tx) { is_spent_tx_found = true; spent_txids.insert(it->m_txid); break; } } if (!is_spent_tx_found) swept_transfers.push_back(i); } } MDEBUG("Total: " << print_money(spent) << " spent, " << print_money(unspent) << " unspent"); if (check_spent) { // query outgoing txes COMMAND_RPC_GET_TRANSACTIONS::request gettxs_req; COMMAND_RPC_GET_TRANSACTIONS::response gettxs_res; gettxs_req.decode_as_json = false; for (const crypto::hash& spent_txid : spent_txids) gettxs_req.txs_hashes.push_back(epee::string_tools::pod_to_hex(spent_txid)); m_daemon_rpc_mutex.lock(); bool r = epee::net_utils::invoke_http_json("/gettransactions", gettxs_req, gettxs_res, m_http_client, rpc_timeout); m_daemon_rpc_mutex.unlock(); THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "gettransactions"); THROW_WALLET_EXCEPTION_IF(gettxs_res.status == CORE_RPC_STATUS_BUSY, error::daemon_busy, "gettransactions"); THROW_WALLET_EXCEPTION_IF(gettxs_res.txs.size() != spent_txids.size(), error::wallet_internal_error, "daemon returned wrong response for gettransactions, wrong count = " + std::to_string(gettxs_res.txs.size()) + ", expected " + std::to_string(spent_txids.size())); // process each outgoing tx auto spent_txid = spent_txids.begin(); for (const COMMAND_RPC_GET_TRANSACTIONS::entry& e : gettxs_res.txs) { THROW_WALLET_EXCEPTION_IF(e.in_pool, error::wallet_internal_error, "spent tx isn't supposed to be in txpool"); // parse tx cryptonote::blobdata bd; THROW_WALLET_EXCEPTION_IF(!epee::string_tools::parse_hexstr_to_binbuff(e.as_hex, bd), error::wallet_internal_error, "parse_hexstr_to_binbuff failed"); cryptonote::transaction spent_tx; crypto::hash spnet_txid_parsed, spent_txid_prefix; THROW_WALLET_EXCEPTION_IF(!cryptonote::parse_and_validate_tx_from_blob(bd, spent_tx, spnet_txid_parsed, spent_txid_prefix), error::wallet_internal_error, "parse_and_validate_tx_from_blob failed"); THROW_WALLET_EXCEPTION_IF(*spent_txid != spnet_txid_parsed, error::wallet_internal_error, "parsed txid mismatch"); // get received (change) amount uint64_t tx_money_got_in_outs = 0; const cryptonote::account_keys& keys = m_account.get_keys(); const crypto::public_key tx_pub_key = get_tx_pub_key_from_extra(spent_tx); crypto::key_derivation derivation; bool r = generate_key_derivation(tx_pub_key, keys.m_view_secret_key, derivation); THROW_WALLET_EXCEPTION_IF(!r, error::wallet_internal_error, "Failed to generate key derivation"); const std::vector additional_tx_pub_keys = get_additional_tx_pub_keys_from_extra(spent_tx); std::vector additional_derivations; for (size_t i = 0; i < additional_tx_pub_keys.size(); ++i) { additional_derivations.push_back({}); r = generate_key_derivation(additional_tx_pub_keys[i], keys.m_view_secret_key, additional_derivations.back()); THROW_WALLET_EXCEPTION_IF(!r, error::wallet_internal_error, "Failed to generate key derivation"); } size_t output_index = 0; for (const cryptonote::tx_out& out : spent_tx.vout) { tx_scan_info_t tx_scan_info; check_acc_out_precomp(out, derivation, additional_derivations, output_index, tx_scan_info); THROW_WALLET_EXCEPTION_IF(tx_scan_info.error, error::wallet_internal_error, "check_acc_out_precomp failed"); if (tx_scan_info.received) { if (tx_scan_info.money_transfered == 0) { rct::key mask; tx_scan_info.money_transfered = tools::decodeRct(spent_tx.rct_signatures, tx_scan_info.received->derivation, output_index, mask); } tx_money_got_in_outs += tx_scan_info.money_transfered; } ++output_index; } // get spent amount uint64_t tx_money_spent_in_ins = 0; uint32_t subaddr_account = (uint32_t)-1; std::set subaddr_indices; for (const cryptonote::txin_v& in : spent_tx.vin) { if (in.type() != typeid(cryptonote::txin_to_key)) continue; auto it = m_key_images.find(boost::get(in).k_image); if (it != m_key_images.end()) { const transfer_details& td = m_transfers[it->second]; uint64_t amount = boost::get(in).amount; if (amount > 0) { THROW_WALLET_EXCEPTION_IF(amount != td.amount(), error::wallet_internal_error, std::string("Inconsistent amount in tx input: got ") + print_money(amount) + std::string(", expected ") + print_money(td.amount())); } amount = td.amount(); tx_money_spent_in_ins += amount; LOG_PRINT_L0("Spent money: " << print_money(amount) << ", with tx: " << *spent_txid); set_spent(it->second, e.block_height); if (m_callback) m_callback->on_money_spent(e.block_height, *spent_txid, spent_tx, amount, spent_tx, td.m_subaddr_index); if (subaddr_account != (uint32_t)-1 && subaddr_account != td.m_subaddr_index.major) LOG_PRINT_L0("WARNING: This tx spends outputs received by different subaddress accounts, which isn't supposed to happen"); subaddr_account = td.m_subaddr_index.major; subaddr_indices.insert(td.m_subaddr_index.minor); } } // create outgoing payment process_outgoing(*spent_txid, spent_tx, e.block_height, e.block_timestamp, tx_money_spent_in_ins, tx_money_got_in_outs, subaddr_account, subaddr_indices); // erase corresponding incoming payment for (auto j = m_payments.begin(); j != m_payments.end(); ++j) { if (j->second.m_tx_hash == *spent_txid) { m_payments.erase(j); break; } } ++spent_txid; } for (size_t n : swept_transfers) { const transfer_details& td = m_transfers[n]; confirmed_transfer_details pd; pd.m_change = (uint64_t)-1; // cahnge is unknown pd.m_amount_in = pd.m_amount_out = td.amount(); // fee is unknown std::string err; pd.m_block_height = get_daemon_blockchain_height(err); // spent block height is unknown, so hypothetically set to the highest crypto::hash spent_txid = crypto::rand(); // spent txid is unknown, so hypothetically set to random m_confirmed_txs.insert(std::make_pair(spent_txid, pd)); } } return m_transfers[signed_key_images.size() - 1].m_block_height; } wallet2::payment_container wallet2::export_payments() const { payment_container payments; for (auto const &p : m_payments) { payments.emplace(p); } return payments; } void wallet2::import_payments(const payment_container &payments) { m_payments.clear(); for (auto const &p : payments) { m_payments.emplace(p); } } void wallet2::import_payments_out(const std::list> &confirmed_payments) { m_confirmed_txs.clear(); for (auto const &p : confirmed_payments) { m_confirmed_txs.emplace(p); } } std::tuple> wallet2::export_blockchain() const { std::tuple> bc; std::get<0>(bc) = m_blockchain.offset(); std::get<1>(bc) = m_blockchain.empty() ? crypto::null_hash: m_blockchain.genesis(); for (size_t n = m_blockchain.offset(); n < m_blockchain.size(); ++n) { std::get<2>(bc).push_back(m_blockchain[n]); } return bc; } void wallet2::import_blockchain(const std::tuple> &bc) { m_blockchain.clear(); if (std::get<0>(bc)) { for (size_t n = std::get<0>(bc); n > 0; ++n) m_blockchain.push_back(std::get<1>(bc)); m_blockchain.trim(std::get<0>(bc)); } for (auto const &b : std::get<2>(bc)) { m_blockchain.push_back(b); } cryptonote::block genesis; generate_genesis(genesis); crypto::hash genesis_hash = get_block_hash(genesis); check_genesis(genesis_hash); m_local_bc_height = m_blockchain.size(); } //---------------------------------------------------------------------------------------------------- std::vector wallet2::export_outputs() const { std::vector outs; outs.reserve(m_transfers.size()); for (size_t n = 0; n < m_transfers.size(); ++n) { const transfer_details &td = m_transfers[n]; outs.push_back(td); } return outs; } //---------------------------------------------------------------------------------------------------- size_t wallet2::import_outputs(const std::vector &outputs) { m_transfers.clear(); m_transfers.reserve(outputs.size()); for (size_t i = 0; i < outputs.size(); ++i) { transfer_details td = outputs[i]; // the hot wallet wouldn't have known about key images (except if we already exported them) cryptonote::keypair in_ephemeral; std::vector tx_extra_fields; THROW_WALLET_EXCEPTION_IF(td.m_tx.vout.empty(), error::wallet_internal_error, "tx with no outputs at index " + boost::lexical_cast(i)); THROW_WALLET_EXCEPTION_IF(!parse_tx_extra(td.m_tx.extra, tx_extra_fields), error::wallet_internal_error, "Transaction extra has unsupported format at index " + boost::lexical_cast(i)); crypto::public_key tx_pub_key = get_tx_pub_key_from_received_outs(td); const std::vector additional_tx_pub_keys = get_additional_tx_pub_keys_from_extra(td.m_tx); const crypto::public_key& out_key = boost::get(td.m_tx.vout[td.m_internal_output_index].target).key; bool r = cryptonote::generate_key_image_helper(m_account.get_keys(), m_subaddresses, out_key, tx_pub_key, additional_tx_pub_keys, td.m_internal_output_index, in_ephemeral, td.m_key_image); THROW_WALLET_EXCEPTION_IF(!r, error::wallet_internal_error, "Failed to generate key image"); expand_subaddresses(td.m_subaddr_index); td.m_key_image_known = true; td.m_key_image_partial = false; THROW_WALLET_EXCEPTION_IF(in_ephemeral.pub != boost::get(td.m_tx.vout[td.m_internal_output_index].target).key, error::wallet_internal_error, "key_image generated ephemeral public key not matched with output_key at index " + boost::lexical_cast(i)); m_key_images[td.m_key_image] = m_transfers.size(); m_pub_keys[td.get_public_key()] = m_transfers.size(); m_transfers.push_back(td); } return m_transfers.size(); } //---------------------------------------------------------------------------------------------------- crypto::public_key wallet2::get_multisig_signer_public_key(const crypto::secret_key &spend_skey) const { crypto::public_key pkey; crypto::secret_key_to_public_key(get_multisig_blinded_secret_key(spend_skey), pkey); return pkey; } //---------------------------------------------------------------------------------------------------- crypto::public_key wallet2::get_multisig_signer_public_key() const { CHECK_AND_ASSERT_THROW_MES(m_multisig, "Wallet is not multisig"); crypto::public_key signer; CHECK_AND_ASSERT_THROW_MES(crypto::secret_key_to_public_key(get_account().get_keys().m_spend_secret_key, signer), "Failed to generate signer public key"); return signer; } //---------------------------------------------------------------------------------------------------- crypto::public_key wallet2::get_multisig_signing_public_key(const crypto::secret_key &msk) const { CHECK_AND_ASSERT_THROW_MES(m_multisig, "Wallet is not multisig"); crypto::public_key pkey; CHECK_AND_ASSERT_THROW_MES(crypto::secret_key_to_public_key(msk, pkey), "Failed to derive public key"); return pkey; } //---------------------------------------------------------------------------------------------------- crypto::public_key wallet2::get_multisig_signing_public_key(size_t idx) const { CHECK_AND_ASSERT_THROW_MES(m_multisig, "Wallet is not multisig"); CHECK_AND_ASSERT_THROW_MES(idx < get_account().get_multisig_keys().size(), "Multisig signing key index out of range"); return get_multisig_signing_public_key(get_account().get_multisig_keys()[idx]); } //---------------------------------------------------------------------------------------------------- rct::key wallet2::get_multisig_k(size_t idx, const std::unordered_set &used_L) const { CHECK_AND_ASSERT_THROW_MES(m_multisig, "Wallet is not multisig"); CHECK_AND_ASSERT_THROW_MES(idx < m_transfers.size(), "idx out of range"); for (const auto &k: m_transfers[idx].m_multisig_k) { rct::key L; rct::scalarmultBase(L, k); if (used_L.find(L) != used_L.end()) return k; } THROW_WALLET_EXCEPTION(tools::error::multisig_export_needed); return rct::zero(); } //---------------------------------------------------------------------------------------------------- rct::multisig_kLRki wallet2::get_multisig_kLRki(size_t n, const rct::key &k) const { CHECK_AND_ASSERT_THROW_MES(n < m_transfers.size(), "Bad m_transfers index"); rct::multisig_kLRki kLRki; kLRki.k = k; cryptonote::generate_multisig_LR(m_transfers[n].get_public_key(), rct::rct2sk(kLRki.k), (crypto::public_key&)kLRki.L, (crypto::public_key&)kLRki.R); kLRki.ki = rct::ki2rct(m_transfers[n].m_key_image); return kLRki; } //---------------------------------------------------------------------------------------------------- rct::multisig_kLRki wallet2::get_multisig_composite_kLRki(size_t n, const crypto::public_key &ignore, std::unordered_set &used_L, std::unordered_set &new_used_L) const { CHECK_AND_ASSERT_THROW_MES(n < m_transfers.size(), "Bad transfer index"); const transfer_details &td = m_transfers[n]; rct::multisig_kLRki kLRki = get_multisig_kLRki(n, rct::skGen()); // pick a L/R pair from every other participant but one size_t n_signers_used = 1; for (const auto &p: m_transfers[n].m_multisig_info) { if (p.m_signer == ignore) continue; for (const auto &lr: p.m_LR) { if (used_L.find(lr.m_L) != used_L.end()) continue; used_L.insert(lr.m_L); new_used_L.insert(lr.m_L); rct::addKeys(kLRki.L, kLRki.L, lr.m_L); rct::addKeys(kLRki.R, kLRki.R, lr.m_R); ++n_signers_used; break; } } CHECK_AND_ASSERT_THROW_MES(n_signers_used >= m_multisig_threshold, "LR not found for enough participants"); return kLRki; } //---------------------------------------------------------------------------------------------------- crypto::key_image wallet2::get_multisig_composite_key_image(size_t n) const { CHECK_AND_ASSERT_THROW_MES(n < m_transfers.size(), "Bad output index"); const transfer_details &td = m_transfers[n]; const crypto::public_key tx_key = get_tx_pub_key_from_received_outs(td); const std::vector additional_tx_keys = cryptonote::get_additional_tx_pub_keys_from_extra(td.m_tx); crypto::key_image ki; std::vector pkis; for (const auto &info: td.m_multisig_info) for (const auto &pki: info.m_partial_key_images) pkis.push_back(pki); bool r = cryptonote::generate_multisig_composite_key_image(get_account().get_keys(), m_subaddresses, td.get_public_key(), tx_key, additional_tx_keys, td.m_internal_output_index, pkis, ki); THROW_WALLET_EXCEPTION_IF(!r, error::wallet_internal_error, "Failed to generate key image"); return ki; } //---------------------------------------------------------------------------------------------------- cryptonote::blobdata wallet2::export_multisig() { std::vector info; const crypto::public_key signer = get_multisig_signer_public_key(); info.resize(m_transfers.size()); for (size_t n = 0; n < m_transfers.size(); ++n) { transfer_details &td = m_transfers[n]; const std::vector additional_tx_pub_keys = get_additional_tx_pub_keys_from_extra(td.m_tx); crypto::key_image ki; td.m_multisig_k.clear(); info[n].m_LR.clear(); info[n].m_partial_key_images.clear(); for (size_t m = 0; m < get_account().get_multisig_keys().size(); ++m) { // we want to export the partial key image, not the full one, so we can't use td.m_key_image bool r = generate_multisig_key_image(get_account().get_keys(), m, td.get_public_key(), ki); CHECK_AND_ASSERT_THROW_MES(r, "Failed to generate key image"); info[n].m_partial_key_images.push_back(ki); } size_t nlr = m_multisig_threshold < m_multisig_signers.size() ? m_multisig_threshold - 1 : 1; for (size_t m = 0; m < nlr; ++m) { td.m_multisig_k.push_back(rct::skGen()); const rct::multisig_kLRki kLRki = get_multisig_kLRki(n, td.m_multisig_k.back()); info[n].m_LR.push_back({kLRki.L, kLRki.R}); } info[n].m_signer = signer; } std::stringstream oss; boost::archive::portable_binary_oarchive ar(oss); ar << info; std::string magic(MULTISIG_EXPORT_FILE_MAGIC, strlen(MULTISIG_EXPORT_FILE_MAGIC)); const cryptonote::account_public_address &keys = get_account().get_keys().m_account_address; std::string header; header += std::string((const char *)&keys.m_spend_public_key, sizeof(crypto::public_key)); header += std::string((const char *)&keys.m_view_public_key, sizeof(crypto::public_key)); header += std::string((const char *)&signer, sizeof(crypto::public_key)); std::string ciphertext = encrypt_with_view_secret_key(header + oss.str()); return MULTISIG_EXPORT_FILE_MAGIC + ciphertext; } //---------------------------------------------------------------------------------------------------- void wallet2::update_multisig_rescan_info(const std::vector> &multisig_k, const std::vector> &info, size_t n) { CHECK_AND_ASSERT_THROW_MES(n < m_transfers.size(), "Bad index in update_multisig_info"); CHECK_AND_ASSERT_THROW_MES(multisig_k.size() >= m_transfers.size(), "Mismatched sizes of multisig_k and info"); MDEBUG("update_multisig_rescan_info: updating index " << n); transfer_details &td = m_transfers[n]; td.m_multisig_info.clear(); for (const auto &pi: info) { CHECK_AND_ASSERT_THROW_MES(n < pi.size(), "Bad pi size"); td.m_multisig_info.push_back(pi[n]); } m_key_images.erase(td.m_key_image); td.m_key_image = get_multisig_composite_key_image(n); td.m_key_image_known = true; td.m_key_image_partial = false; td.m_multisig_k = multisig_k[n]; m_key_images[td.m_key_image] = n; } //---------------------------------------------------------------------------------------------------- size_t wallet2::import_multisig(std::vector blobs) { CHECK_AND_ASSERT_THROW_MES(m_multisig, "Wallet is not multisig"); std::vector> info; std::unordered_set seen; for (cryptonote::blobdata &data: blobs) { const size_t magiclen = strlen(MULTISIG_EXPORT_FILE_MAGIC); THROW_WALLET_EXCEPTION_IF(data.size() < magiclen || memcmp(data.data(), MULTISIG_EXPORT_FILE_MAGIC, magiclen), error::wallet_internal_error, "Bad multisig info file magic in "); data = decrypt_with_view_secret_key(std::string(data, magiclen)); const size_t headerlen = 3 * sizeof(crypto::public_key); THROW_WALLET_EXCEPTION_IF(data.size() < headerlen, error::wallet_internal_error, "Bad data size"); const crypto::public_key &public_spend_key = *(const crypto::public_key*)&data[0]; const crypto::public_key &public_view_key = *(const crypto::public_key*)&data[sizeof(crypto::public_key)]; const crypto::public_key &signer = *(const crypto::public_key*)&data[2*sizeof(crypto::public_key)]; const cryptonote::account_public_address &keys = get_account().get_keys().m_account_address; THROW_WALLET_EXCEPTION_IF(public_spend_key != keys.m_spend_public_key || public_view_key != keys.m_view_public_key, error::wallet_internal_error, "Multisig info is for a different account"); if (get_multisig_signer_public_key() == signer) { MINFO("Multisig info from this wallet ignored"); continue; } if (seen.find(signer) != seen.end()) { MINFO("Duplicate multisig info ignored"); continue; } seen.insert(signer); std::string body(data, headerlen); std::istringstream iss(body); std::vector i; boost::archive::portable_binary_iarchive ar(iss); ar >> i; MINFO(boost::format("%u outputs found") % boost::lexical_cast(i.size())); info.push_back(std::move(i)); } CHECK_AND_ASSERT_THROW_MES(info.size() + 1 <= m_multisig_signers.size() && info.size() + 1 >= m_multisig_threshold, "Wrong number of multisig sources"); std::vector> k; k.reserve(m_transfers.size()); for (const auto &td: m_transfers) k.push_back(td.m_multisig_k); // how many outputs we're going to update size_t n_outputs = m_transfers.size(); for (const auto &pi: info) if (pi.size() < n_outputs) n_outputs = pi.size(); if (n_outputs == 0) return 0; // check signers are consistent for (const auto &pi: info) { CHECK_AND_ASSERT_THROW_MES(std::find(m_multisig_signers.begin(), m_multisig_signers.end(), pi[0].m_signer) != m_multisig_signers.end(), "Signer is not a member of this multisig wallet"); for (size_t n = 1; n < n_outputs; ++n) CHECK_AND_ASSERT_THROW_MES(pi[n].m_signer == pi[0].m_signer, "Mismatched signers in imported multisig info"); } // trim data we don't have info for from all participants for (auto &pi: info) pi.resize(n_outputs); // sort by signer if (!info.empty() && !info.front().empty()) { std::sort(info.begin(), info.end(), [](const std::vector &i0, const std::vector &i1){ return memcmp(&i0[0].m_signer, &i1[0].m_signer, sizeof(i0[0].m_signer)); }); } // first pass to determine where to detach the blockchain for (size_t n = 0; n < n_outputs; ++n) { const transfer_details &td = m_transfers[n]; if (!td.m_key_image_partial) continue; MINFO("Multisig info importing from block height " << td.m_block_height); detach_blockchain(td.m_block_height); break; } for (size_t n = 0; n < n_outputs && n < m_transfers.size(); ++n) { update_multisig_rescan_info(k, info, n); } m_multisig_rescan_k = &k; m_multisig_rescan_info = &info; try { refresh(); } catch (...) {} m_multisig_rescan_info = NULL; m_multisig_rescan_k = NULL; return n_outputs; } //---------------------------------------------------------------------------------------------------- std::string wallet2::encrypt(const std::string &plaintext, const crypto::secret_key &skey, bool authenticated) const { crypto::chacha_key key; crypto::generate_chacha_key(&skey, sizeof(skey), key); std::string ciphertext; crypto::chacha_iv iv = crypto::rand(); ciphertext.resize(plaintext.size() + sizeof(iv) + (authenticated ? sizeof(crypto::signature) : 0)); crypto::chacha20(plaintext.data(), plaintext.size(), key, iv, &ciphertext[sizeof(iv)]); memcpy(&ciphertext[0], &iv, sizeof(iv)); if (authenticated) { crypto::hash hash; crypto::cn_fast_hash(ciphertext.data(), ciphertext.size() - sizeof(signature), hash); crypto::public_key pkey; crypto::secret_key_to_public_key(skey, pkey); crypto::signature &signature = *(crypto::signature*)&ciphertext[ciphertext.size() - sizeof(crypto::signature)]; crypto::generate_signature(hash, pkey, skey, signature); } return ciphertext; } //---------------------------------------------------------------------------------------------------- std::string wallet2::encrypt_with_view_secret_key(const std::string &plaintext, bool authenticated) const { return encrypt(plaintext, get_account().get_keys().m_view_secret_key, authenticated); } //---------------------------------------------------------------------------------------------------- std::string wallet2::decrypt(const std::string &ciphertext, const crypto::secret_key &skey, bool authenticated) const { const size_t prefix_size = sizeof(chacha_iv) + (authenticated ? sizeof(crypto::signature) : 0); THROW_WALLET_EXCEPTION_IF(ciphertext.size() < prefix_size, error::wallet_internal_error, "Unexpected ciphertext size"); crypto::chacha_key key; crypto::generate_chacha_key(&skey, sizeof(skey), key); const crypto::chacha_iv &iv = *(const crypto::chacha_iv*)&ciphertext[0]; std::string plaintext; plaintext.resize(ciphertext.size() - prefix_size); if (authenticated) { crypto::hash hash; crypto::cn_fast_hash(ciphertext.data(), ciphertext.size() - sizeof(signature), hash); crypto::public_key pkey; crypto::secret_key_to_public_key(skey, pkey); const crypto::signature &signature = *(const crypto::signature*)&ciphertext[ciphertext.size() - sizeof(crypto::signature)]; THROW_WALLET_EXCEPTION_IF(!crypto::check_signature(hash, pkey, signature), error::wallet_internal_error, "Failed to authenticate ciphertext"); } crypto::chacha20(ciphertext.data() + sizeof(iv), ciphertext.size() - prefix_size, key, iv, &plaintext[0]); return plaintext; } //---------------------------------------------------------------------------------------------------- std::string wallet2::decrypt_with_view_secret_key(const std::string &ciphertext, bool authenticated) const { return decrypt(ciphertext, get_account().get_keys().m_view_secret_key, authenticated); } //---------------------------------------------------------------------------------------------------- std::string wallet2::make_uri(const std::string &address, const std::string &payment_id, uint64_t amount, const std::string &tx_description, const std::string &recipient_name, std::string &error) { cryptonote::address_parse_info info; if(!get_account_address_from_str(info, testnet(), address)) { error = std::string("wrong address: ") + address; return std::string(); } // we want only one payment id if (info.has_payment_id && !payment_id.empty()) { error = "A single payment id is allowed"; return std::string(); } if (!payment_id.empty()) { crypto::hash pid32; crypto::hash8 pid8; if (!wallet2::parse_long_payment_id(payment_id, pid32) && !wallet2::parse_short_payment_id(payment_id, pid8)) { error = "Invalid payment id"; return std::string(); } } std::string uri = "monero:" + address; unsigned int n_fields = 0; if (!payment_id.empty()) { uri += (n_fields++ ? "&" : "?") + std::string("tx_payment_id=") + payment_id; } if (amount > 0) { // URI encoded amount is in decimal units, not atomic units uri += (n_fields++ ? "&" : "?") + std::string("tx_amount=") + cryptonote::print_money(amount); } if (!recipient_name.empty()) { uri += (n_fields++ ? "&" : "?") + std::string("recipient_name=") + epee::net_utils::conver_to_url_format(recipient_name); } if (!tx_description.empty()) { uri += (n_fields++ ? "&" : "?") + std::string("tx_description=") + epee::net_utils::conver_to_url_format(tx_description); } return uri; } //---------------------------------------------------------------------------------------------------- bool wallet2::parse_uri(const std::string &uri, std::string &address, std::string &payment_id, uint64_t &amount, std::string &tx_description, std::string &recipient_name, std::vector &unknown_parameters, std::string &error) { if (uri.substr(0, 7) != "monero:") { error = std::string("URI has wrong scheme (expected \"monero:\"): ") + uri; return false; } std::string remainder = uri.substr(7); const char *ptr = strchr(remainder.c_str(), '?'); address = ptr ? remainder.substr(0, ptr-remainder.c_str()) : remainder; cryptonote::address_parse_info info; if(!get_account_address_from_str(info, testnet(), address)) { error = std::string("URI has wrong address: ") + address; return false; } if (!strchr(remainder.c_str(), '?')) return true; std::vector arguments; std::string body = remainder.substr(address.size() + 1); if (body.empty()) return true; boost::split(arguments, body, boost::is_any_of("&")); std::set have_arg; for (const auto &arg: arguments) { std::vector kv; boost::split(kv, arg, boost::is_any_of("=")); if (kv.size() != 2) { error = std::string("URI has wrong parameter: ") + arg; return false; } if (have_arg.find(kv[0]) != have_arg.end()) { error = std::string("URI has more than one instance of " + kv[0]); return false; } have_arg.insert(kv[0]); if (kv[0] == "tx_amount") { amount = 0; if (!cryptonote::parse_amount(amount, kv[1])) { error = std::string("URI has invalid amount: ") + kv[1]; return false; } } else if (kv[0] == "tx_payment_id") { if (info.has_payment_id) { error = "Separate payment id given with an integrated address"; return false; } crypto::hash hash; crypto::hash8 hash8; if (!wallet2::parse_long_payment_id(kv[1], hash) && !wallet2::parse_short_payment_id(kv[1], hash8)) { error = "Invalid payment id: " + kv[1]; return false; } payment_id = kv[1]; } else if (kv[0] == "recipient_name") { recipient_name = epee::net_utils::convert_from_url_format(kv[1]); } else if (kv[0] == "tx_description") { tx_description = epee::net_utils::convert_from_url_format(kv[1]); } else { unknown_parameters.push_back(arg); } } return true; } //---------------------------------------------------------------------------------------------------- uint64_t wallet2::get_blockchain_height_by_date(uint16_t year, uint8_t month, uint8_t day) { uint32_t version; if (!check_connection(&version)) { throw std::runtime_error("failed to connect to daemon: " + get_daemon_address()); } if (version < MAKE_CORE_RPC_VERSION(1, 6)) { throw std::runtime_error("this function requires RPC version 1.6 or higher"); } std::tm date = { 0, 0, 0, 0, 0, 0, 0, 0 }; date.tm_year = year - 1900; date.tm_mon = month - 1; date.tm_mday = day; if (date.tm_mon < 0 || 11 < date.tm_mon || date.tm_mday < 1 || 31 < date.tm_mday) { throw std::runtime_error("month or day out of range"); } uint64_t timestamp_target = std::mktime(&date); std::string err; uint64_t height_min = 0; uint64_t height_max = get_daemon_blockchain_height(err) - 1; if (!err.empty()) { throw std::runtime_error("failed to get blockchain height"); } while (true) { COMMAND_RPC_GET_BLOCKS_BY_HEIGHT::request req; COMMAND_RPC_GET_BLOCKS_BY_HEIGHT::response res; uint64_t height_mid = (height_min + height_max) / 2; req.heights = { height_min, height_mid, height_max }; bool r = net_utils::invoke_http_bin("/getblocks_by_height.bin", req, res, m_http_client, rpc_timeout); if (!r || res.status != CORE_RPC_STATUS_OK) { std::ostringstream oss; oss << "failed to get blocks by heights: "; for (auto height : req.heights) oss << height << ' '; oss << endl << "reason: "; if (!r) oss << "possibly lost connection to daemon"; else if (res.status == CORE_RPC_STATUS_BUSY) oss << "daemon is busy"; else oss << res.status; throw std::runtime_error(oss.str()); } cryptonote::block blk_min, blk_mid, blk_max; if (res.blocks.size() < 3) throw std::runtime_error("Not enough blocks returned from daemon"); if (!parse_and_validate_block_from_blob(res.blocks[0].block, blk_min)) throw std::runtime_error("failed to parse blob at height " + std::to_string(height_min)); if (!parse_and_validate_block_from_blob(res.blocks[1].block, blk_mid)) throw std::runtime_error("failed to parse blob at height " + std::to_string(height_mid)); if (!parse_and_validate_block_from_blob(res.blocks[2].block, blk_max)) throw std::runtime_error("failed to parse blob at height " + std::to_string(height_max)); uint64_t timestamp_min = blk_min.timestamp; uint64_t timestamp_mid = blk_mid.timestamp; uint64_t timestamp_max = blk_max.timestamp; if (!(timestamp_min <= timestamp_mid && timestamp_mid <= timestamp_max)) { // the timestamps are not in the chronological order. // assuming they're sufficiently close to each other, simply return the smallest height return std::min({height_min, height_mid, height_max}); } if (timestamp_target > timestamp_max) { throw std::runtime_error("specified date is in the future"); } if (timestamp_target <= timestamp_min + 2 * 24 * 60 * 60) // two days of "buffer" period { return height_min; } if (timestamp_target <= timestamp_mid) height_max = height_mid; else height_min = height_mid; if (height_max - height_min <= 2 * 24 * 30) // don't divide the height range finer than two days { return height_min; } } } //---------------------------------------------------------------------------------------------------- bool wallet2::is_synced() const { uint64_t height; boost::optional result = m_node_rpc_proxy.get_target_height(height); if (result && *result != CORE_RPC_STATUS_OK) return false; return get_blockchain_current_height() >= height; } //---------------------------------------------------------------------------------------------------- std::vector> wallet2::estimate_backlog(uint64_t min_blob_size, uint64_t max_blob_size, const std::vector &fees) { THROW_WALLET_EXCEPTION_IF(min_blob_size == 0, error::wallet_internal_error, "Invalid 0 fee"); THROW_WALLET_EXCEPTION_IF(max_blob_size == 0, error::wallet_internal_error, "Invalid 0 fee"); for (uint64_t fee: fees) { THROW_WALLET_EXCEPTION_IF(fee == 0, error::wallet_internal_error, "Invalid 0 fee"); } // get txpool backlog epee::json_rpc::request req = AUTO_VAL_INIT(req); epee::json_rpc::response res = AUTO_VAL_INIT(res); m_daemon_rpc_mutex.lock(); req.jsonrpc = "2.0"; req.id = epee::serialization::storage_entry(0); req.method = "get_txpool_backlog"; bool r = net_utils::invoke_http_json("/json_rpc", req, res, m_http_client, rpc_timeout); m_daemon_rpc_mutex.unlock(); THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "Failed to connect to daemon"); THROW_WALLET_EXCEPTION_IF(res.result.status == CORE_RPC_STATUS_BUSY, error::daemon_busy, "get_txpool_backlog"); THROW_WALLET_EXCEPTION_IF(res.result.status != CORE_RPC_STATUS_OK, error::get_tx_pool_error); epee::json_rpc::request req_t = AUTO_VAL_INIT(req_t); epee::json_rpc::response resp_t = AUTO_VAL_INIT(resp_t); m_daemon_rpc_mutex.lock(); req_t.jsonrpc = "2.0"; req_t.id = epee::serialization::storage_entry(0); req_t.method = "get_info"; r = net_utils::invoke_http_json("/json_rpc", req_t, resp_t, m_http_client); m_daemon_rpc_mutex.unlock(); THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "get_info"); THROW_WALLET_EXCEPTION_IF(resp_t.result.status == CORE_RPC_STATUS_BUSY, error::daemon_busy, "get_info"); THROW_WALLET_EXCEPTION_IF(resp_t.result.status != CORE_RPC_STATUS_OK, error::get_tx_pool_error); uint64_t full_reward_zone = resp_t.result.block_size_limit / 2; std::vector> blocks; for (uint64_t fee: fees) { double our_fee_byte_min = fee / (double)min_blob_size, our_fee_byte_max = fee / (double)max_blob_size; uint64_t priority_size_min = 0, priority_size_max = 0; for (const auto &i: res.result.backlog) { if (i.blob_size == 0) { MWARNING("Got 0 sized blob from txpool, ignored"); continue; } double this_fee_byte = i.fee / (double)i.blob_size; if (this_fee_byte >= our_fee_byte_min) priority_size_min += i.blob_size; if (this_fee_byte >= our_fee_byte_max) priority_size_max += i.blob_size; } uint64_t nblocks_min = priority_size_min / full_reward_zone; uint64_t nblocks_max = priority_size_max / full_reward_zone; MDEBUG("estimate_backlog: priority_size " << priority_size_min << " - " << priority_size_max << " for " << fee << " (" << our_fee_byte_min << " - " << our_fee_byte_max << " piconero byte fee), " << nblocks_min << " - " << nblocks_max << " blocks at block size " << full_reward_zone); blocks.push_back(std::make_pair(nblocks_min, nblocks_max)); } return blocks; } //---------------------------------------------------------------------------------------------------- void wallet2::generate_genesis(cryptonote::block& b) { if (m_testnet) { cryptonote::generate_genesis_block(b, config::testnet::GENESIS_TX, config::testnet::GENESIS_NONCE); } else { cryptonote::generate_genesis_block(b, config::GENESIS_TX, config::GENESIS_NONCE); } } }