// Copyright (c) 2014-2016, 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_base_utils.h" using namespace epee; #include "cryptonote_config.h" #include "wallet2.h" #include "cryptonote_core/cryptonote_format_utils.h" #include "rpc/core_rpc_server_commands_defs.h" #include "misc_language.h" #include "cryptonote_core/cryptonote_basic_impl.h" #include "common/boost_serialization_helper.h" #include "profile_tools.h" #include "crypto/crypto.h" #include "serialization/binary_utils.h" #include "cryptonote_protocol/blobdatatype.h" #include "mnemonics/electrum-words.h" #include "common/dns_utils.h" #include "common/util.h" #include "rapidjson/document.h" #include "rapidjson/writer.h" #include "rapidjson/stringbuffer.h" #include "common/json_util.h" #include "common/base58.h" #include "ringct/rctSigs.h" extern "C" { #include "crypto/keccak.h" #include "crypto/crypto-ops.h" } using namespace cryptonote; // 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 KILL_IOSERVICE() \ do { \ work.reset(); \ while (!ioservice.stopped()) ioservice.poll(); \ threadpool.join_all(); \ ioservice.stop(); \ } while(0) namespace { 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(const cryptonote::blobdata &blob, uint64_t fee_multiplier) { THROW_WALLET_EXCEPTION_IF(fee_multiplier <= 0 || fee_multiplier > 3, tools::error::invalid_fee_multiplier); uint64_t bytes = blob.size(); uint64_t kB = (bytes + 1023) / 1024; return kB * FEE_PER_KB * fee_multiplier; } } //namespace namespace tools { // for now, limit to 30 attempts. TODO: discuss a good number to limit to. const size_t MAX_SPLIT_ATTEMPTS = 30; //---------------------------------------------------------------------------------------------------- void wallet2::init(const std::string& daemon_address, uint64_t upper_transaction_size_limit) { m_upper_transaction_size_limit = upper_transaction_size_limit; m_daemon_address = daemon_address; } //---------------------------------------------------------------------------------------------------- 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 { 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::ElectrumWords::bytes_to_words(get_account().get_keys().m_spend_secret_key, electrum_words, seed_language); 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; } /*! * \brief Tells if the wallet file is deprecated. */ bool wallet2::is_deprecated() const { return is_old_file_format; } //---------------------------------------------------------------------------------------------------- void wallet2::check_acc_out(const account_keys &acc, const tx_out &o, const crypto::public_key &tx_pub_key, size_t i, bool &received, uint64_t &money_transfered, bool &error) const { if (o.target.type() != typeid(txout_to_key)) { error = true; LOG_ERROR("wrong type id in transaction out"); return; } received = is_out_to_acc(acc, boost::get(o.target), tx_pub_key, i); if(received) { money_transfered = o.amount; // may be 0 for ringct outputs } else { money_transfered = 0; } error = false; } //---------------------------------------------------------------------------------------------------- void wallet2::process_new_transaction(const cryptonote::transaction& tx, uint64_t height, uint64_t ts, bool miner_tx, bool pool) { if (!miner_tx) process_unconfirmed(tx, height); std::vector outs; uint64_t tx_money_got_in_outs = 0; 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: " << get_transaction_hash(tx)); } // Don't try to extract tx public key if tx has no ouputs if (!tx.vout.empty()) { tx_extra_pub_key pub_key_field; if(!find_tx_extra_field_by_type(tx_extra_fields, pub_key_field)) { LOG_PRINT_L0("Public key wasn't found in the transaction extra. Skipping transaction " << get_transaction_hash(tx)); if(0 != m_callback) m_callback->on_skip_transaction(height, tx); return; } tx_pub_key = pub_key_field.pub_key; bool r = true; std::deque in_ephemeral(tx.vout.size()); std::deque ki(tx.vout.size()); std::deque amount(tx.vout.size()); std::deque mask(tx.vout.size()); int threads = tools::get_max_concurrency(); if (miner_tx && m_refresh_type == RefreshNoCoinbase) { // assume coinbase isn't for us } else if (miner_tx && m_refresh_type == RefreshOptimizeCoinbase) { uint64_t money_transfered = 0; bool error = false, received = false; check_acc_out(m_account.get_keys(), tx.vout[0], tx_pub_key, 0, received, money_transfered, error); if (error) { r = false; } else { // this assumes that the miner tx pays a single address if (received) { cryptonote::generate_key_image_helper(m_account.get_keys(), tx_pub_key, 0, in_ephemeral[0], ki[0]); THROW_WALLET_EXCEPTION_IF(in_ephemeral[0].pub != boost::get(tx.vout[0].target).key, error::wallet_internal_error, "key_image generated ephemeral public key not matched with output_key"); outs.push_back(0); if (money_transfered == 0) money_transfered = rct::decodeRct(tx.rct_signatures, rct::sk2rct(in_ephemeral[0].sec), 0, mask[0]); amount[0] = money_transfered; tx_money_got_in_outs = money_transfered; // process the other outs from that tx boost::asio::io_service ioservice; boost::thread_group threadpool; std::unique_ptr < boost::asio::io_service::work > work(new boost::asio::io_service::work(ioservice)); for (int i = 0; i < threads; i++) { threadpool.create_thread(boost::bind(&boost::asio::io_service::run, &ioservice)); } const account_keys &keys = m_account.get_keys(); std::vector money_transfered(tx.vout.size()); std::deque error(tx.vout.size()); std::deque received(tx.vout.size()); // the first one was already checked for (size_t i = 1; i < tx.vout.size(); ++i) { ioservice.dispatch(boost::bind(&wallet2::check_acc_out, this, std::cref(keys), std::cref(tx.vout[i]), std::cref(tx_pub_key), i, std::ref(received[i]), std::ref(money_transfered[i]), std::ref(error[i]))); } KILL_IOSERVICE(); for (size_t i = 1; i < tx.vout.size(); ++i) { if (error[i]) { r = false; break; } if (received[i]) { cryptonote::generate_key_image_helper(m_account.get_keys(), tx_pub_key, i, in_ephemeral[i], ki[i]); THROW_WALLET_EXCEPTION_IF(in_ephemeral[i].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 (money_transfered[i] == 0) money_transfered[i] = rct::decodeRct(tx.rct_signatures, rct::sk2rct(in_ephemeral[i].sec), i, mask[i]); tx_money_got_in_outs += money_transfered[i]; amount[i] = money_transfered[i]; } } } } } else if (tx.vout.size() > 1 && threads > 1) { boost::asio::io_service ioservice; boost::thread_group threadpool; std::unique_ptr < boost::asio::io_service::work > work(new boost::asio::io_service::work(ioservice)); for (int i = 0; i < threads; i++) { threadpool.create_thread(boost::bind(&boost::asio::io_service::run, &ioservice)); } const account_keys &keys = m_account.get_keys(); std::vector money_transfered(tx.vout.size()); std::deque error(tx.vout.size()); std::deque received(tx.vout.size()); for (size_t i = 0; i < tx.vout.size(); ++i) { ioservice.dispatch(boost::bind(&wallet2::check_acc_out, this, std::cref(keys), std::cref(tx.vout[i]), std::cref(tx_pub_key), i, std::ref(received[i]), std::ref(money_transfered[i]), std::ref(error[i]))); } KILL_IOSERVICE(); tx_money_got_in_outs = 0; for (size_t i = 0; i < tx.vout.size(); ++i) { if (error[i]) { r = false; break; } if (received[i]) { cryptonote::generate_key_image_helper(m_account.get_keys(), tx_pub_key, i, in_ephemeral[i], ki[i]); THROW_WALLET_EXCEPTION_IF(in_ephemeral[i].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 (money_transfered[i] == 0) money_transfered[i] = rct::decodeRct(tx.rct_signatures, rct::sk2rct(in_ephemeral[i].sec), i, mask[i]); tx_money_got_in_outs += money_transfered[i]; amount[i] = money_transfered[i]; } } } else { for (size_t i = 0; i < tx.vout.size(); ++i) { uint64_t money_transfered = 0; bool error = false, received = false; check_acc_out(m_account.get_keys(), tx.vout[i], tx_pub_key, i, received, money_transfered, error); if (error) { r = false; break; } else { if (received) { cryptonote::generate_key_image_helper(m_account.get_keys(), tx_pub_key, i, in_ephemeral[i], ki[i]); THROW_WALLET_EXCEPTION_IF(in_ephemeral[i].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 (money_transfered == 0) money_transfered = rct::decodeRct(tx.rct_signatures, rct::sk2rct(in_ephemeral[i].sec), i, mask[i]); amount[i] = money_transfered; tx_money_got_in_outs += money_transfered; } } } } THROW_WALLET_EXCEPTION_IF(!r, error::acc_outs_lookup_error, tx, tx_pub_key, m_account.get_keys()); if(!outs.empty() && tx_money_got_in_outs) { //good news - got money! take care about it //usually we have only one transfer for user in transaction cryptonote::COMMAND_RPC_GET_TX_GLOBAL_OUTPUTS_INDEXES::request req = AUTO_VAL_INIT(req); cryptonote::COMMAND_RPC_GET_TX_GLOBAL_OUTPUTS_INDEXES::response res = AUTO_VAL_INIT(res); if (!pool) { req.txid = get_transaction_hash(tx); m_daemon_rpc_mutex.lock(); bool r = net_utils::invoke_http_bin_remote_command2(m_daemon_address + "/get_o_indexes.bin", req, res, m_http_client, WALLET_RCP_CONNECTION_TIMEOUT); m_daemon_rpc_mutex.unlock(); THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "get_o_indexes.bin"); THROW_WALLET_EXCEPTION_IF(res.status == CORE_RPC_STATUS_BUSY, error::daemon_busy, "get_o_indexes.bin"); THROW_WALLET_EXCEPTION_IF(res.status != CORE_RPC_STATUS_OK, error::get_out_indices_error, res.status); THROW_WALLET_EXCEPTION_IF(res.o_indexes.size() != tx.vout.size(), error::wallet_internal_error, "transactions outputs size=" + std::to_string(tx.vout.size()) + " not match with COMMAND_RPC_GET_TX_GLOBAL_OUTPUTS_INDEXES response size=" + std::to_string(res.o_indexes.size())); } BOOST_FOREACH(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_key_images.find(ki[o]); THROW_WALLET_EXCEPTION_IF(kit != m_key_images.end() && kit->second >= m_transfers.size(), error::wallet_internal_error, std::string("Unexpected transfer index from key image: ") + "got " + (kit == m_key_images.end() ? "" : boost::lexical_cast(kit->second)) + ", m_transfers.size() is " + boost::lexical_cast(m_transfers.size())); if (kit == m_key_images.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 = res.o_indexes[o]; td.m_tx = tx; td.m_key_image = ki[o]; td.m_amount = tx.vout[o].amount; if (td.m_amount == 0) { td.m_mask = mask[o]; td.m_amount = amount[o]; } else { td.m_mask = rct::identity(); } td.m_spent = false; m_key_images[td.m_key_image] = m_transfers.size()-1; LOG_PRINT_L0("Received money: " << print_money(td.amount()) << ", with tx: " << get_transaction_hash(tx)); if (0 != m_callback) m_callback->on_money_received(height, td.m_tx, td.m_amount); } } else if (m_transfers[kit->second].m_spent || m_transfers[kit->second].amount() >= tx.vout[o].amount) { LOG_ERROR("key image " << epee::string_tools::pod_to_hex(ki) << " 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("key image " << epee::string_tools::pod_to_hex(ki) << " 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.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 = res.o_indexes[o]; td.m_tx = tx; td.m_amount = tx.vout[o].amount; if (td.m_amount == 0) { td.m_mask = mask[o]; td.m_amount = amount[o]; } else { td.m_mask = rct::identity(); } THROW_WALLET_EXCEPTION_IF(td.m_key_image != ki[o], error::wallet_internal_error, "Inconsistent key images"); 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: " << get_transaction_hash(tx)); if (0 != m_callback) m_callback->on_money_received(height, td.m_tx, td.m_amount); } } } } } uint64_t tx_money_spent_in_ins = 0; // check all outputs for spending (compare key images) BOOST_FOREACH(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(); LOG_PRINT_L0("Spent money: " << print_money(amount) << ", with tx: " << get_transaction_hash(tx)); tx_money_spent_in_ins += amount; td.m_spent = true; td.m_spent_height = height; if (0 != m_callback) m_callback->on_money_spent(height, td.m_tx, amount, tx); } } if (tx_money_spent_in_ins > 0) { process_outgoing(tx, height, ts, tx_money_spent_in_ins, tx_money_got_in_outs); } uint64_t received = (tx_money_spent_in_ins < tx_money_got_in_outs) ? tx_money_got_in_outs - tx_money_spent_in_ins : 0; if (0 < received) { 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); } payment_details payment; payment.m_tx_hash = cryptonote::get_transaction_hash(tx); payment.m_amount = received; payment.m_block_height = height; payment.m_unlock_time = tx.unlock_time; payment.m_timestamp = ts; if (pool) m_unconfirmed_payments.emplace(payment_id, payment); 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 cryptonote::transaction& tx, uint64_t height) { crypto::hash txid = get_transaction_hash(tx); 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 cryptonote::transaction &tx, uint64_t height, uint64_t ts, uint64_t spent, uint64_t received) { crypto::hash txid = get_transaction_hash(tx); 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; } entry.first->second.m_block_height = height; entry.first->second.m_timestamp = ts; } //---------------------------------------------------------------------------------------------------- void wallet2::process_new_blockchain_entry(const cryptonote::block& b, const cryptonote::block_complete_entry& bche, const crypto::hash& bl_id, uint64_t height) { //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(b.miner_tx, height, b.timestamp, true, false); TIME_MEASURE_FINISH(miner_tx_handle_time); TIME_MEASURE_START(txs_handle_time); BOOST_FOREACH(auto& txblob, bche.txs) { cryptonote::transaction tx; bool r = parse_and_validate_tx_from_blob(txblob, tx); THROW_WALLET_EXCEPTION_IF(!r, error::tx_parse_error, txblob); process_new_transaction(tx, height, b.timestamp, false, false); } 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(); if(!sz) return; size_t current_back_offset = 1; bool genesis_included = false; while(current_back_offset < sz) { ids.push_back(m_blockchain[sz-current_back_offset]); if(sz-current_back_offset == 0) genesis_included = true; if(i < 10) { ++current_back_offset; }else { current_back_offset += current_multiplier *= 2; } ++i; } if(!genesis_included) ids.push_back(m_blockchain[0]); } //---------------------------------------------------------------------------------------------------- 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) { 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; req.start_height = start_height; m_daemon_rpc_mutex.lock(); bool r = net_utils::invoke_http_bin_remote_command2(m_daemon_address + "/getblocks.bin", req, res, m_http_client, WALLET_RCP_CONNECTION_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); blocks_start_height = res.start_height; blocks = res.blocks; } //---------------------------------------------------------------------------------------------------- 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_remote_command2(m_daemon_address + "/gethashes.bin", req, res, m_http_client, WALLET_RCP_CONNECTION_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, uint64_t& blocks_added) { size_t current_index = start_height; blocks_added = 0; int threads = tools::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); boost::asio::io_service ioservice; boost::thread_group threadpool; std::unique_ptr < boost::asio::io_service::work > work(new boost::asio::io_service::work(ioservice)); for (size_t i = 0; i < round_size; i++) { threadpool.create_thread(boost::bind(&boost::asio::io_service::run, &ioservice)); } std::list::const_iterator tmpblocki = blocki; for (size_t i = 0; i < round_size; ++i) { ioservice.dispatch(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; } KILL_IOSERVICE(); 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); ++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); } else { LOG_PRINT_L2("Block is already in blockchain: " << string_tools::pod_to_hex(bl_id)); } ++current_index; ++blocki; } } } else { BOOST_FOREACH(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); ++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); } else { LOG_PRINT_L2("Block is already in blockchain: " << string_tools::pod_to_hex(bl_id)); } ++current_index; } } } //---------------------------------------------------------------------------------------------------- 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, bool &error) { error = false; try { // 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); } catch(...) { error = true; } } //---------------------------------------------------------------------------------------------------- void wallet2::update_pool_state() { // get the pool state cryptonote::COMMAND_RPC_GET_TRANSACTION_POOL::request req; cryptonote::COMMAND_RPC_GET_TRANSACTION_POOL::response res; m_daemon_rpc_mutex.lock(); bool r = epee::net_utils::invoke_http_json_remote_command2(m_daemon_address + "/get_transaction_pool", req, res, m_http_client, 200000); m_daemon_rpc_mutex.unlock(); THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "get_transaction_pool"); THROW_WALLET_EXCEPTION_IF(res.status == CORE_RPC_STATUS_BUSY, error::daemon_busy, "get_transaction_pool"); THROW_WALLET_EXCEPTION_IF(res.status != CORE_RPC_STATUS_OK, error::get_tx_pool_error); // 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 std::string txid = epee::string_tools::pod_to_hex(cryptonote::get_transaction_hash(it->second.m_tx)); bool found = false; for (auto it2: res.transactions) { if (it2.id_hash == 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 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) { 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 (auto &td: m_transfers) { if (td.m_key_image == tx_in_to_key.k_image) { LOG_PRINT_L1("Resetting spent status for output " << vini << ": " << td.m_key_image); td.m_spent = 0; td.m_spent_height = 0; break; } } } } } } } // remove pool txes to us that aren't in the pool anymore std::unordered_map::iterator uit = m_unconfirmed_payments.begin(); while (uit != m_unconfirmed_payments.end()) { const std::string txid = string_tools::pod_to_hex(uit->first); bool found = false; for (auto it2: res.transactions) { if (it2.id_hash == txid) { found = true; break; } } auto pit = uit++; if (!found) { m_unconfirmed_payments.erase(pit); } } // add new pool txes to us for (auto it: res.transactions) { cryptonote::blobdata txid_data; if(epee::string_tools::parse_hexstr_to_binbuff(it.id_hash, txid_data)) { const crypto::hash txid = *reinterpret_cast(txid_data.data()); if (m_unconfirmed_payments.find(txid) == m_unconfirmed_payments.end()) { LOG_PRINT_L1("Found new pool tx: " << txid); bool found = false; for (const auto &i: m_unconfirmed_txs) { if (i.first == txid) { found = true; break; } } if (!found) { // not one of those we sent ourselves cryptonote::COMMAND_RPC_GET_TRANSACTIONS::request req; cryptonote::COMMAND_RPC_GET_TRANSACTIONS::response res; req.txs_hashes.push_back(it.id_hash); req.decode_as_json = false; m_daemon_rpc_mutex.lock(); bool r = epee::net_utils::invoke_http_json_remote_command2(m_daemon_address + "/gettransactions", req, res, m_http_client, 200000); m_daemon_rpc_mutex.unlock(); if (r && res.status == CORE_RPC_STATUS_OK) { if (res.txs.size() == 1) { // might have just been put in a block if (res.txs[0].in_pool) { cryptonote::transaction tx; cryptonote::blobdata bd; crypto::hash tx_hash, tx_prefix_hash; if (epee::string_tools::parse_hexstr_to_binbuff(res.txs[0].as_hex, bd)) { if (cryptonote::parse_and_validate_tx_from_blob(bd, tx, tx_hash, tx_prefix_hash)) { if (tx_hash == txid) { process_new_transaction(tx, 0, time(NULL), false, true); } else { LOG_PRINT_L0("Mismatched txids when processing unconfimed txes from pool"); } } else { LOG_PRINT_L0("failed to validate transaction from daemon"); } } else { LOG_PRINT_L0("Failed to parse tx " << txid); } } else { LOG_PRINT_L1("Tx " << txid << " was in pool, but is no more"); } } else { LOG_PRINT_L0("Expected 1 tx, got " << res.txs.size()); } } else { LOG_PRINT_L0("Error calling gettransactions daemon RPC: r " << r << ", status " << res.status); } } else { LOG_PRINT_L1("We sent that one"); } } else { LOG_PRINT_L1("Already saw that one"); } } else { LOG_PRINT_L0("Failed to parse txid"); } } } //---------------------------------------------------------------------------------------------------- void wallet2::fast_refresh(uint64_t stop_height, uint64_t &blocks_start_height, std::list &short_chain_history) { std::list hashes; 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) { std::list::iterator right; // drop early 3 off, skipping the genesis block if (short_chain_history.size() > 3) { right = short_chain_history.end(); std::advance(right,-1); std::list::iterator left = right; std::advance(left, -3); short_chain_history.erase(left, right); } 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; BOOST_FOREACH(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; } } } //---------------------------------------------------------------------------------------------------- void wallet2::refresh(uint64_t start_height, uint64_t & blocks_fetched, bool& received_money) { 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() ? get_transaction_hash(m_transfers.back().m_tx) : null_hash; std::list short_chain_history; boost::thread pull_thread; uint64_t blocks_start_height; std::list blocks; // 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 } pull_blocks(start_height, blocks_start_height, short_chain_history, blocks); // 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; bool error = false; pull_thread = boost::thread([&]{pull_next_blocks(start_height, next_blocks_start_height, short_chain_history, blocks, next_blocks, error);}); process_blocks(blocks_start_height, blocks, added_blocks); blocks_fetched += added_blocks; pull_thread.join(); if(!added_blocks) break; // switch to the new blocks from the daemon blocks_start_height = next_blocks_start_height; blocks = next_blocks; // 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; if (pull_thread.joinable()) pull_thread.join(); 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() ? get_transaction_hash(m_transfers.back().m_tx) : null_hash)) received_money = true; try { update_pool_state(); } catch (...) { LOG_PRINT_L1("Failed to check pending transactions"); } LOG_PRINT_L1("Refresh done, blocks received: " << blocks_fetched << ", balance: " << print_money(balance()) << ", unlocked: " << print_money(unlocked_balance())); } //---------------------------------------------------------------------------------------------------- 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_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); td.m_spent = false; td.m_spent_height = 0; } } 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++) { 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"); m_key_images.erase(it_ki); ++transfers_detached; } m_transfers.erase(it, m_transfers.end()); size_t blocks_detached = m_blockchain.end() - (m_blockchain.begin()+height); m_blockchain.erase(m_blockchain.begin()+height, m_blockchain.end()); 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() { return true; } //---------------------------------------------------------------------------------------------------- bool wallet2::clear() { m_blockchain.clear(); m_transfers.clear(); m_key_images.clear(); m_unconfirmed_txs.clear(); m_payments.clear(); m_tx_keys.clear(); m_confirmed_txs.clear(); m_local_bc_height = 1; 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 std::string& password, bool watch_only) { std::string account_data; 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_always_confirm_transfers ? 1 :0); json.AddMember("always_confirm_transfers", 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_fee_multiplier); json.AddMember("default_fee_multiplier", 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()); // Serialize the JSON object rapidjson::StringBuffer buffer; rapidjson::Writer writer(buffer); json.Accept(writer); account_data = buffer.GetString(); // Encrypt the entire JSON object. crypto::chacha8_key key; crypto::generate_chacha8_key(password, key); std::string cipher; cipher.resize(account_data.size()); keys_file_data.iv = crypto::rand(); crypto::chacha8(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 std::string& password) { 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::chacha8_key key; crypto::generate_chacha8_key(password, key); std::string account_data; account_data.resize(keys_file_data.account_data.size()); 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. rapidjson::Document json; if (json.Parse(account_data.c_str()).HasParseError()) { is_old_file_format = true; m_watch_only = false; m_always_confirm_transfers = false; m_default_mixin = 0; m_default_fee_multiplier = 0; m_auto_refresh = true; m_refresh_type = RefreshType::RefreshDefault; } else { 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); if (field_seed_language_found) { set_seed_language(field_seed_language); } GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, watch_only, int, Int, false); m_watch_only = field_watch_only_found && field_watch_only; GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, always_confirm_transfers, int, Int, false); m_always_confirm_transfers = field_always_confirm_transfers_found && field_always_confirm_transfers; GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, store_tx_keys, int, Int, false); GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, store_tx_info, int, Int, false); m_store_tx_info = (field_store_tx_keys_found && (field_store_tx_keys != 0)) || (field_store_tx_info_found && (field_store_tx_info != 0)); GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, default_mixin, unsigned int, Uint, false); m_default_mixin = field_default_mixin_found ? field_default_mixin : 0; GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, default_fee_multiplier, unsigned int, Uint, false); m_default_fee_multiplier = field_default_fee_multiplier_found ? field_default_fee_multiplier : 0; GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, auto_refresh, int, Int, false); m_auto_refresh = !field_auto_refresh_found || (field_auto_refresh != 0); GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, refresh_type, int, Int, false); 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); if (field_refresh_height_found) m_refresh_from_block_height = field_refresh_height; } 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) 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 * * 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& password) const { const std::string keys_file_name = m_keys_file; 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::chacha8_key key; crypto::generate_chacha8_key(password, key); std::string account_data; account_data.resize(keys_file_data.account_data.size()); 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. rapidjson::Document json; 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); 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 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 std::string& password, const crypto::secret_key& recovery_param, bool recover, bool two_random) { clear(); prepare_file_names(wallet_); 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; 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) LOG_PRINT_RED_L0("String with address text not saved"); cryptonote::block b; generate_genesis(b); m_blockchain.push_back(get_block_hash(b)); store(); return retval; } /*! * \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 std::string& password, const cryptonote::account_public_address &account_public_address, const crypto::secret_key& viewkey) { clear(); prepare_file_names(wallet_); 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; 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) LOG_PRINT_RED_L0("String with address text not saved"); cryptonote::block b; generate_genesis(b); m_blockchain.push_back(get_block_hash(b)); 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 std::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_); 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; 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) LOG_PRINT_RED_L0("String with address text not saved"); cryptonote::block b; generate_genesis(b); m_blockchain.push_back(get_block_hash(b)); store(); } /*! * \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 std::string& password) { 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, false); 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 std::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; } //---------------------------------------------------------------------------------------------------- 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(bool *same_version) { boost::lock_guard lock(m_daemon_rpc_mutex); if(!m_http_client.is_connected()) { net_utils::http::url_content u; net_utils::parse_url(m_daemon_address, u); if(!u.port) { u.port = m_testnet ? config::testnet::RPC_DEFAULT_PORT : config::RPC_DEFAULT_PORT; } if (!m_http_client.connect(u.host, std::to_string(u.port), WALLET_RCP_CONNECTION_TIMEOUT)) return false; } if (same_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_remote_command2(m_daemon_address + "/json_rpc", req_t, resp_t, m_http_client); if (!r || resp_t.result.status != CORE_RPC_STATUS_OK) *same_version = false; else *same_version = resp_t.result.version == CORE_RPC_VERSION; } return true; } //---------------------------------------------------------------------------------------------------- bool wallet2::generate_chacha8_key_from_secret_keys(crypto::chacha8_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; char data[sizeof(view_key) + sizeof(spend_key) + 1]; memcpy(data, &view_key, sizeof(view_key)); memcpy(data + sizeof(view_key), &spend_key, sizeof(spend_key)); data[sizeof(data) - 1] = CHACHA8_KEY_TAIL; crypto::generate_chacha8_key(data, sizeof(data), key); memset(data, 0, sizeof(data)); return true; } //---------------------------------------------------------------------------------------------------- void wallet2::load(const std::string& wallet_, const std::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::chacha8_key key; generate_chacha8_key_from_secret_keys(key); std::string cache_data; cache_data.resize(cache_file_data.cache_data.size()); crypto::chacha8(cache_file_data.cache_data.data(), cache_file_data.cache_data.size(), key, cache_file_data.iv, &cache_data[0]); std::stringstream iss; iss << cache_data; boost::archive::binary_iarchive ar(iss); ar >> *this; } catch (...) { LOG_PRINT_L1("Failed to load encrypted cache, trying unencrypted"); std::stringstream iss; 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); } m_local_bc_height = m_blockchain.size(); } //---------------------------------------------------------------------------------------------------- void wallet2::check_genesis(const crypto::hash& genesis_hash) const { std::string what("Genesis block missmatch. You probably use wallet without testnet flag with blockchain from test network or vice versa"); THROW_WALLET_EXCEPTION_IF(genesis_hash != m_blockchain[0], error::wallet_internal_error, what); } //---------------------------------------------------------------------------------------------------- void wallet2::store() { store_to("", ""); } //---------------------------------------------------------------------------------------------------- void wallet2::store_to(const std::string &path, const std::string &password) { // 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::binary_oarchive ar(oss); ar << *this; wallet2::cache_file_data cache_file_data = boost::value_initialized(); cache_file_data.cache_data = oss.str(); crypto::chacha8_key key; generate_chacha8_key_from_secret_keys(key); std::string cipher; cipher.resize(cache_file_data.cache_data.size()); cache_file_data.iv = crypto::rand(); crypto::chacha8(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 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); // 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); store_keys(m_keys_file, password, false); // save address to the new file const std::string address_file = m_wallet_file + ".address.txt"; bool 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 { // 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::unlocked_balance() const { uint64_t amount = 0; BOOST_FOREACH(const transfer_details& td, m_transfers) if(!td.m_spent && is_transfer_unlocked(td)) amount += td.amount(); return amount; } //---------------------------------------------------------------------------------------------------- uint64_t wallet2::balance() const { uint64_t amount = 0; BOOST_FOREACH(auto& td, m_transfers) if(!td.m_spent) amount += td.amount(); BOOST_FOREACH(auto& utx, m_unconfirmed_txs) if (utx.second.m_state != wallet2::unconfirmed_transfer_details::failed) amount+= utx.second.m_change; return amount; } //---------------------------------------------------------------------------------------------------- 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 { auto range = m_payments.equal_range(payment_id); std::for_each(range.first, range.second, [&payments, &min_height](const payment_container::value_type& x) { if (min_height < x.second.m_block_height) { payments.push_back(x.second); } }); } //---------------------------------------------------------------------------------------------------- void wallet2::get_payments(std::list>& payments, uint64_t min_height, uint64_t max_height) const { auto range = std::make_pair(m_payments.begin(), m_payments.end()); std::for_each(range.first, range.second, [&payments, &min_height, &max_height](const payment_container::value_type& x) { if (min_height < x.second.m_block_height && max_height >= x.second.m_block_height) { payments.push_back(x); } }); } //---------------------------------------------------------------------------------------------------- void wallet2::get_payments_out(std::list>& confirmed_payments, uint64_t min_height, uint64_t max_height) 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) { confirmed_payments.push_back(*i); } } } //---------------------------------------------------------------------------------------------------- void wallet2::get_unconfirmed_payments_out(std::list>& unconfirmed_payments) const { for (auto i = m_unconfirmed_txs.begin(); i != m_unconfirmed_txs.end(); ++i) { unconfirmed_payments.push_back(*i); } } //---------------------------------------------------------------------------------------------------- void wallet2::get_unconfirmed_payments(std::list>& unconfirmed_payments) const { for (auto i = m_unconfirmed_payments.begin(); i != m_unconfirmed_payments.end(); ++i) { unconfirmed_payments.push_back(*i); } } //---------------------------------------------------------------------------------------------------- void wallet2::rescan_spent() { std::vector key_images; // make a list of key images for all our outputs for (size_t i = 0; i < m_transfers.size(); ++i) { const transfer_details& td = m_transfers[i]; key_images.push_back(string_tools::pod_to_hex(td.m_key_image)); } 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); req.key_images = key_images; m_daemon_rpc_mutex.lock(); bool r = epee::net_utils::invoke_http_json_remote_command2(m_daemon_address + "/is_key_image_spent", req, daemon_resp, m_http_client, 200000); 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() != 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(key_images.size())); // update spent status for (size_t i = 0; i < m_transfers.size(); ++i) { transfer_details& td = m_transfers[i]; if (td.m_spent != (daemon_resp.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"); td.m_spent_height = 0; } else { LOG_PRINT_L0("Marking output " << i << "(" << td.m_key_image << ") as spent, it was marked as unspent"); // unknown height, if this gets reorged, it might still be missed } td.m_spent = daemon_resp.spent_status[i] != COMMAND_RPC_IS_KEY_IMAGE_SPENT::UNSPENT; } } } //---------------------------------------------------------------------------------------------------- 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); m_local_bc_height = 1; if (refresh) this->refresh(); } //---------------------------------------------------------------------------------------------------- bool wallet2::is_transfer_unlocked(const transfer_details& td) const { if(!is_tx_spendtime_unlocked(td.m_tx.unlock_time, td.m_block_height)) return false; if(td.m_block_height + CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE > m_blockchain.size()) 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_blockchain.size()-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"); 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); } } //---------------------------------------------------------------------------------------------------- // 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::list& selected_transfers, bool trusted_daemon) { uint64_t found_money = 0; while (found_money < needed_money && !unused_transfers_indices.empty()) { size_t idx = pop_random_value(unused_transfers_indices); transfer_container::iterator it = m_transfers.begin() + idx; selected_transfers.push_back(it); 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) { 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_change = change_amount; utd.m_sent_time = time(NULL); utd.m_tx = tx; utd.m_dests = dests; utd.m_payment_id = payment_id; utd.m_state = wallet2::unconfirmed_transfer_details::pending; utd.m_timestamp = time(NULL); } //---------------------------------------------------------------------------------------------------- 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 /** * @brief gets a monero address from the TXT record of a DNS entry * * gets the monero address from the TXT record of the DNS entry associated * with . If this lookup fails, or the TXT record does not contain an * XMR address in the correct format, returns an empty string. * will be set true or false according to whether or not the DNS query passes * DNSSEC validation. * * @param url the url to look up * @param dnssec_valid return-by-reference for DNSSEC status of query * * @return a monero address (as a string) or an empty string */ std::vector wallet2::addresses_from_url(const std::string& url, bool& dnssec_valid) { std::vector addresses; // get txt records bool dnssec_available, dnssec_isvalid; std::string oa_addr = tools::DNSResolver::instance().get_dns_format_from_oa_address(url); auto records = tools::DNSResolver::instance().get_txt_record(oa_addr, dnssec_available, dnssec_isvalid); // TODO: update this to allow for conveying that dnssec was not available if (dnssec_available && dnssec_isvalid) { dnssec_valid = true; } else dnssec_valid = false; // for each txt record, try to find a monero address in it. for (auto& rec : records) { std::string addr = address_from_txt_record(rec); if (addr.size()) { addresses.push_back(addr); } } return addresses; } //---------------------------------------------------------------------------------------------------- // TODO: parse the string in a less stupid way, probably with regex std::string wallet2::address_from_txt_record(const std::string& s) { // make sure the txt record has "oa1:xmr" and find it auto pos = s.find("oa1:xmr"); // search from there to find "recipient_address=" pos = s.find("recipient_address=", pos); pos += 18; // move past "recipient_address=" // find the next semicolon auto pos2 = s.find(";", pos); if (pos2 != std::string::npos) { // length of address == 95, we can at least validate that much here if (pos2 - pos == 95) { return s.substr(pos, 95); } } return std::string(); } 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 cryptonote::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 (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 = cryptonote::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; crypto::hash txid; 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_remote_command2(m_daemon_address + "/sendrawtransaction", req, daemon_send_resp, m_http_client, 200000); 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); txid = get_transaction_hash(ptx.tx); crypto::hash payment_id = cryptonote::null_hash; std::vector dests; uint64_t amount_in = 0; if (store_tx_info()) { payment_id = get_payment_id(ptx); dests = ptx.dests; BOOST_FOREACH(transfer_container::iterator it, ptx.selected_transfers) amount_in += it->amount(); } add_unconfirmed_tx(ptx.tx, amount_in, dests, payment_id, ptx.change_dts.amount); if (store_tx_info()) m_tx_keys.insert(std::make_pair(txid, ptx.tx_key)); LOG_PRINT_L2("transaction " << txid << " generated ok and sent to daemon, key_images: [" << ptx.key_images << "]"); BOOST_FOREACH(transfer_container::iterator it, ptx.selected_transfers) { it->m_spent = true; it->m_spent_height = 0; } //fee includes dust if dust policy specified it. LOG_PRINT_L0("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()) << ENDL << "Unlocked: " << print_money(unlocked_balance()) << 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); } } uint64_t wallet2::sanitize_fee_multiplier(uint64_t fee_multiplier) const { // 0, default value used for previous fee argument, defaults to normal fee if (fee_multiplier == 0) return m_default_fee_multiplier > 0 ? m_default_fee_multiplier : 1; // 1 to 3 are allowed as multipliers if (fee_multiplier >= 1 && fee_multiplier <= 3) return fee_multiplier; THROW_WALLET_EXCEPTION_IF (false, error::invalid_fee_multiplier); return 1; } //---------------------------------------------------------------------------------------------------- // 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, uint64_t fee_multiplier, const std::vector extra, bool trusted_daemon) { const std::vector unused_transfers_indices = select_available_outputs_from_histogram(fake_outs_count + 1, true, true, trusted_daemon); fee_multiplier = sanitize_fee_multiplier(fee_multiplier); // 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. uint64_t needed_fee = 0; 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(txBlob, fee_multiplier); } while (ptx.fee < needed_fee); ptx_vector.push_back(ptx); // mark transfers to be used as "spent" BOOST_FOREACH(transfer_container::iterator it, ptx.selected_transfers) { it->m_spent = true; it->m_spent_height = 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 BOOST_FOREACH(transfer_container::iterator it2, ptx.selected_transfers) { it2->m_spent = false; it2->m_spent_height = 0; } } // 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 BOOST_FOREACH(transfer_container::iterator it2, ptx.selected_transfers) { it2->m_spent = false; it2->m_spent_height = 0; } } 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 BOOST_FOREACH(transfer_container::iterator it2, ptx.selected_transfers) { it2->m_spent = false; it2->m_spent_height = 0; } } throw; } } } template void wallet2::transfer_selected(const std::vector& dsts, const std::list selected_transfers, size_t fake_outputs_count, 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); uint64_t upper_transaction_size_limit = get_upper_tranaction_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 BOOST_FOREACH(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; BOOST_FOREACH(auto it, selected_transfers) { found_money += it->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_money, found_money, needed_money - fee, fee); typedef std::pair entry; std::vector> outs; if (fake_outputs_count) { // 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(auto it: selected_transfers) req_t.params.amounts.push_back(it->amount()); req_t.params.unlocked = true; bool r = net_utils::invoke_http_json_remote_command2(m_daemon_address + "/json_rpc", req_t, resp_t, m_http_client); 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 requested_outputs_count = (size_t)((fake_outputs_count + 1) * 1.5 + 1); // generate output indices to request COMMAND_RPC_GET_OUTPUTS::request req = AUTO_VAL_INIT(req); COMMAND_RPC_GET_OUTPUTS::response daemon_resp = AUTO_VAL_INIT(daemon_resp); for(transfer_container::iterator it: selected_transfers) { std::unordered_set seen_indices; 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; for (auto he: resp_t.result.histogram) { if (he.amount == it->amount()) { num_outs = he.instances; break; } } if (num_outs <= requested_outputs_count) { for (uint64_t i = 0; i < num_outs; i++) req.outputs.push_back({it->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({it->amount(), num_outs - 1}); } else { // start with real one uint64_t num_found = 1; seen_indices.emplace(it->m_global_output_index); req.outputs.push_back({it->amount(), it->m_global_output_index}); // 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. // 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)); uint64_t i = (uint64_t)(frac*num_outs); // just in case rounding up to 1 occurs after sqrt if (i == num_outs) --i; if (seen_indices.count(i)) continue; seen_indices.emplace(i); req.outputs.push_back({it->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 COMMAND_RPC_GET_OUTPUTS::out &a, const COMMAND_RPC_GET_OUTPUTS::out &b) { return a.index < b.index; }); } // get the keys for those m_daemon_rpc_mutex.lock(); r = epee::net_utils::invoke_http_bin_remote_command2(m_daemon_address + "/get_outs.bin", req, daemon_resp, m_http_client, 200000); 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(selected_transfers.size()); for(transfer_container::iterator it: selected_transfers) { outs.push_back(std::vector()); outs.back().reserve(fake_outputs_count + 1); // pick real out first (it will be sorted when done) outs.back().push_back({it->m_global_output_index, boost::get(it->m_tx.vout[it->m_internal_output_index].target).key}); // 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())); for (size_t o = 0; o < requested_outputs_count && outs.back().size() < fake_outputs_count + 1; ++o) { size_t i = base + order[o]; if (req.outputs[i].index == it->m_global_output_index) // don't re-add real one continue; if (!daemon_resp.outs[i].unlocked) // don't add locked outs continue; if (o > 0 && daemon_resp.outs[i].key == daemon_resp.outs[i - 1].key) // don't add duplicates continue; outs.back().push_back({req.outputs[i].index, daemon_resp.outs[i].key}); } if (outs.back().size() < fake_outputs_count + 1) { scanty_outs[it->amount()] = outs.back().size(); } else { // sort the subsection, so any spares are reset in order std::sort(outs.back().begin(), outs.back().end(), [](const entry &a, const entry &b) { return a.first < b.first; }); // sanity check for (size_t n = 1; n < outs.back().size(); ++n) { THROW_WALLET_EXCEPTION_IF(outs.back()[n].first == outs.back()[n-1].first, error::wallet_internal_error, "Duplicate indices though we did not ask for any"); THROW_WALLET_EXCEPTION_IF(outs.back()[n].second == outs.back()[n-1].second, error::wallet_internal_error, "Duplicate keys after we have weeded them out"); } } base += requested_outputs_count; } THROW_WALLET_EXCEPTION_IF(!scanty_outs.empty(), error::not_enough_outs_to_mix, scanty_outs, fake_outputs_count); } else { for (transfer_container::iterator it: selected_transfers) { std::vector v; v.push_back({it->m_global_output_index, boost::get(it->m_tx.vout[it->m_internal_output_index].target).key}); outs.push_back(v); } } //prepare inputs typedef cryptonote::tx_source_entry::output_entry tx_output_entry; size_t i = 0, out_index = 0; std::vector sources; BOOST_FOREACH(transfer_container::iterator it, selected_transfers) { sources.resize(sources.size()+1); cryptonote::tx_source_entry& src = sources.back(); transfer_details& td = *it; src.amount = td.amount(); //paste keys (fake and real) for (size_t n = 0; n < fake_outputs_count + 1; ++n) { tx_output_entry oe; oe.first = outs[out_index][n].first; oe.second.dest = rct::pk2rct(outs[out_index][n].second); oe.second.mask = rct::zeroCommit(td.amount()); 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); src.real_output = it_to_replace - src.outputs.begin(); src.real_output_in_tx_index = td.m_internal_output_index; detail::print_source_entry(src); ++out_index; } cryptonote::tx_destination_entry change_dts = AUTO_VAL_INIT(change_dts); if (needed_money < found_money) { change_dts.addr = m_account.get_keys().m_account_address; 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); BOOST_FOREACH(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)); } BOOST_FOREACH(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)); dust += d.amount; } crypto::secret_key tx_key; bool r = cryptonote::construct_tx_and_get_tx_key(m_account.get_keys(), sources, splitted_dsts, extra, tx, unlock_time, tx_key); THROW_WALLET_EXCEPTION_IF(!r, error::tx_not_constructed, sources, splitted_dsts, unlock_time, m_testnet); THROW_WALLET_EXCEPTION_IF(upper_transaction_size_limit <= get_object_blobsize(tx), error::tx_too_big, tx, upper_transaction_size_limit); std::string key_images; bool all_are_txin_to_key = std::all_of(tx.vin.begin(), 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, 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.dests = dsts; } void wallet2::transfer_selected_rct(std::vector dsts, const std::list selected_transfers, size_t fake_outputs_count, uint64_t unlock_time, uint64_t fee, const std::vector& extra, 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); uint64_t upper_transaction_size_limit = get_upper_tranaction_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 BOOST_FOREACH(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; BOOST_FOREACH(auto it, selected_transfers) { found_money += it->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_money, found_money, needed_money - fee, fee); typedef cryptonote::tx_source_entry::output_entry tx_output_entry; std::vector selected_transfer_to_daemon_resp; COMMAND_RPC_GET_RANDOM_RCT_OUTPUTS::request rct_req = AUTO_VAL_INIT(rct_req); COMMAND_RPC_GET_RANDOM_RCT_OUTPUTS::response rct_daemon_resp = AUTO_VAL_INIT(rct_daemon_resp); COMMAND_RPC_GET_RANDOM_OUTPUTS_FOR_AMOUNTS::request daemon_req = AUTO_VAL_INIT(daemon_req); COMMAND_RPC_GET_RANDOM_OUTPUTS_FOR_AMOUNTS::response daemon_resp = AUTO_VAL_INIT(daemon_resp); if(fake_outputs_count) { rct_req.outs_count = 0; size_t n_rct_inputs = 0; BOOST_FOREACH(transfer_container::iterator it, selected_transfers) { THROW_WALLET_EXCEPTION_IF(it->m_tx.vout.size() <= it->m_internal_output_index, error::wallet_internal_error, "m_internal_output_index = " + std::to_string(it->m_internal_output_index) + " is greater or equal to outputs count = " + std::to_string(it->m_tx.vout.size())); if (it->is_rct()) ++n_rct_inputs; else daemon_req.amounts.push_back(it->amount()); selected_transfer_to_daemon_resp.push_back(daemon_req.amounts.size() - 1); } size_t n_amounts_requested = daemon_req.amounts.size(); rct_req.outs_count = n_rct_inputs * (fake_outputs_count + 1); if (rct_req.outs_count > 0) { LOG_PRINT_L2("We need RCT fake outs for " << n_rct_inputs << " inputs"); rct_req.outs_count = n_rct_inputs * (fake_outputs_count + 1); // add one to make possible (if need) to skip real output key m_daemon_rpc_mutex.lock(); bool r = epee::net_utils::invoke_http_bin_remote_command2(m_daemon_address + "/getrandom_rctouts.bin", rct_req, rct_daemon_resp, m_http_client, 200000); m_daemon_rpc_mutex.unlock(); THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "getrandom_rctouts.bin"); THROW_WALLET_EXCEPTION_IF(rct_daemon_resp.status == CORE_RPC_STATUS_BUSY, error::daemon_busy, "getrandom_rctouts.bin"); THROW_WALLET_EXCEPTION_IF(rct_daemon_resp.status != CORE_RPC_STATUS_OK, error::get_random_outs_error, rct_daemon_resp.status); THROW_WALLET_EXCEPTION_IF(rct_daemon_resp.outs.size() != n_rct_inputs * (fake_outputs_count + 1), error::wallet_internal_error, "daemon returned wrong response for getrandom_rctouts.bin, wrong amounts count = " + std::to_string(daemon_resp.outs.size()) + ", expected " + std::to_string(n_rct_inputs * (fake_outputs_count + 1))); } if (!daemon_req.amounts.empty()) { LOG_PRINT_L2("We need non-RCT fake outs for " << daemon_req.amounts.size() << " inputs"); daemon_req.outs_count = fake_outputs_count + 1; // add one to make possible (if need) to skip real output key m_daemon_rpc_mutex.lock(); bool r = epee::net_utils::invoke_http_bin_remote_command2(m_daemon_address + "/getrandom_outs.bin", daemon_req, daemon_resp, m_http_client, 200000); m_daemon_rpc_mutex.unlock(); THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "getrandom_outs.bin"); THROW_WALLET_EXCEPTION_IF(daemon_resp.status == CORE_RPC_STATUS_BUSY, error::daemon_busy, "getrandom_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() != n_amounts_requested, error::wallet_internal_error, "daemon returned wrong response for getrandom_outs.bin, wrong amounts count = " + std::to_string(daemon_resp.outs.size()) + ", expected " + std::to_string(n_amounts_requested)); std::unordered_map scanty_outs; BOOST_FOREACH(COMMAND_RPC_GET_RANDOM_OUTPUTS_FOR_AMOUNTS::outs_for_amount& amount_outs, daemon_resp.outs) { if (amount_outs.outs.size() < fake_outputs_count) { scanty_outs[amount_outs.amount] = amount_outs.outs.size(); } } THROW_WALLET_EXCEPTION_IF(!scanty_outs.empty(), error::not_enough_outs_to_mix, scanty_outs, fake_outputs_count); } } //prepare inputs size_t i = 0; std::vector sources; rct_daemon_resp.outs.sort([](const COMMAND_RPC_GET_RANDOM_RCT_OUTPUTS::out_entry& a, const COMMAND_RPC_GET_RANDOM_RCT_OUTPUTS::out_entry& b){return a.global_amount_index < b.global_amount_index;}); std::list::const_iterator rctit = rct_daemon_resp.outs.begin(); BOOST_FOREACH(transfer_container::iterator it, selected_transfers) { sources.resize(sources.size()+1); cryptonote::tx_source_entry& src = sources.back(); transfer_details& td = *it; src.amount = td.amount(); //paste mixin transaction if(it->is_rct()) { while (src.outputs.size() < fake_outputs_count) { THROW_WALLET_EXCEPTION_IF(rctit == rct_daemon_resp.outs.end(), error::wallet_internal_error, "Out of rct inputs"); // check if we have the daemon supplied output in our real ones bool found = false; BOOST_FOREACH(transfer_container::iterator checkit, selected_transfers) { if (checkit->m_global_output_index == rctit->global_amount_index && checkit->m_tx.vout[checkit->m_internal_output_index].amount == rctit->amount) { found = true; break; } } if(found) { ++rctit; continue; } tx_output_entry oe; oe.first = rctit->global_amount_index; oe.second.dest = rct::pk2rct(rctit->out_key); oe.second.mask = rctit->commitment; ++rctit; src.outputs.push_back(oe); } } else { typedef COMMAND_RPC_GET_RANDOM_OUTPUTS_FOR_AMOUNTS::out_entry out_entry; typedef cryptonote::tx_source_entry::output_entry tx_output_entry; size_t idx = selected_transfer_to_daemon_resp[i]; THROW_WALLET_EXCEPTION_IF(daemon_req.amounts.size() != daemon_resp.outs.size(), error::wallet_internal_error, "Bad amounts/out size"); THROW_WALLET_EXCEPTION_IF(idx >= daemon_resp.outs.size(), error::wallet_internal_error, "Bad mapping to daemon_resp.outs"); THROW_WALLET_EXCEPTION_IF(td.amount() != daemon_req.amounts[idx], error::wallet_internal_error, "Bad mapping to daemon_resp.outs/amounts"); daemon_resp.outs[idx].outs.sort([](const out_entry& a, const out_entry& b){return a.global_amount_index < b.global_amount_index;}); BOOST_FOREACH(out_entry& daemon_oe, daemon_resp.outs[idx].outs) { if(td.m_global_output_index == daemon_oe.global_amount_index) continue; tx_output_entry oe; oe.first = daemon_oe.global_amount_index; oe.second.dest = rct::pk2rct(daemon_oe.out_key); oe.second.mask = rct::zeroCommit(td.amount()); src.outputs.push_back(oe); if(src.outputs.size() >= fake_outputs_count) break; } } //paste real transaction to the random index auto it_to_insert = std::find_if(src.outputs.begin(), src.outputs.end(), [&](const tx_output_entry& a) { return a.first >= td.m_global_output_index; }); //size_t real_index = src.outputs.size() ? (rand() % src.outputs.size() ):0; // TODO: same index (for now) 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); auto interted_it = src.outputs.insert(it_to_insert, real_oe); src.real_out_tx_key = get_tx_pub_key_from_extra(td.m_tx); src.real_output = interted_it - src.outputs.begin(); src.real_output_in_tx_index = td.m_internal_output_index; src.mask = td.m_mask; detail::print_source_entry(src); ++i; } cryptonote::tx_destination_entry change_dts = AUTO_VAL_INIT(change_dts); if (needed_money < found_money) { change_dts.addr = m_account.get_keys().m_account_address; change_dts.amount = found_money - needed_money; } dsts.push_back(change_dts); crypto::secret_key tx_key; bool r = cryptonote::construct_tx_and_get_tx_key(m_account.get_keys(), sources, dsts, extra, tx, unlock_time, tx_key, true); THROW_WALLET_EXCEPTION_IF(!r, error::tx_not_constructed, sources, dsts, unlock_time, m_testnet); THROW_WALLET_EXCEPTION_IF(upper_transaction_size_limit <= get_object_blobsize(tx), error::tx_too_big, tx, upper_transaction_size_limit); std::string key_images; bool all_are_txin_to_key = std::all_of(tx.vin.begin(), 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, tx); 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; ptx.tx_key = tx_key; ptx.dests = dsts; } static size_t estimate_rct_tx_size(int n_inputs, int mixin, int n_outputs) { 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 += 40; // rct signatures // rangeSigs size += (2*64*32+32+64*32) * n_outputs; // MG - only the last slot of II is saved, the rest can be reconstructed size += 32 * (mixin+1) * n_inputs + 32 + 32 * (/*n_inputs+*/1) ; // mixRing - not serialized, can be reconstructed /* size += 2 * 32 * (mixin+1) * n_inputs; */ // ecdhInfo size += 3 * 32 * n_outputs; // outPk size += 2 * 32 * n_outputs; LOG_PRINT_L2("estimated rct tx size for " << n_inputs << " at mixin " << mixin << " and " << n_outputs << ": " << size << " (" << (32 * n_inputs + 2 * 32 * (mixin+1) * n_inputs) << " saved)"); return size; } // 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, uint64_t fee_multiplier, const std::vector extra, bool trusted_daemon) { std::vector unused_transfers_indices; std::vector unused_dust_indices; uint64_t needed_money; uint64_t accumulated_fee, accumulated_outputs, accumulated_change; struct TX { std::list selected_transfers; std::vector dsts; cryptonote::transaction tx; pending_tx ptx; size_t bytes; void add(const account_public_address &addr, uint64_t amount) { 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(amount,addr)); else i->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_tranaction_size_limit(); const bool use_rct = use_fork_rules(3); fee_multiplier = sanitize_fee_multiplier(fee_multiplier); // 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; BOOST_FOREACH(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); // gather all our dust and non dust outputs for (size_t i = 0; i < m_transfers.size(); ++i) { const transfer_details& td = m_transfers[i]; if (!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); } } LOG_PRINT_L2("Starting with " << unused_transfers_indices.size() << " non-dust outputs and " << unused_dust_indices.size() << " dust outputs"); // start with an empty tx txes.push_back(TX()); accumulated_fee = 0; accumulated_outputs = 0; accumulated_change = 0; adding_fee = false; needed_fee = 0; // while we have something to send while ((!dsts.empty() && dsts[0].amount > 0) || adding_fee) { TX &tx = txes.back(); // 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::not_enough_money, unlocked_balance(), 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 = !unused_transfers_indices.empty() ? pop_random_value(unused_transfers_indices) : pop_random_value(unused_dust_indices); 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(m_transfers.begin() + idx); uint64_t available_amount = td.amount(); accumulated_outputs += available_amount; 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) { // we can fully pay that destination LOG_PRINT_L2("We can fully pay " << get_account_address_as_str(m_testnet, dsts[0].addr) << " for " << print_money(dsts[0].amount)); tx.add(dsts[0].addr, dsts[0].amount); available_amount -= dsts[0].amount; dsts[0].amount = 0; pop_index(dsts, 0); } if (available_amount > 0 && !dsts.empty()) { // we can partially fill that destination LOG_PRINT_L2("We can partially pay " << get_account_address_as_str(m_testnet, dsts[0].addr) << " for " << print_money(available_amount) << "/" << print_money(dsts[0].amount)); tx.add(dsts[0].addr, available_amount); 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; 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 { size_t estimated_rct_tx_size; if (use_rct) estimated_rct_tx_size = estimate_rct_tx_size(tx.selected_transfers.size(), fake_outs_count, tx.dsts.size() + 1); else estimated_rct_tx_size = tx.selected_transfers.size() * (fake_outs_count+1) * APPROXIMATE_INPUT_BYTES; 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; needed_fee = 0; 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, unlock_time, needed_fee, extra, test_tx, test_ptx); else transfer_selected(tx.dsts, tx.selected_transfers, fake_outs_count, 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(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 " << txBlob.size() << " kB tx, with " << print_money(available_for_fee) << " available for fee (" << print_money(needed_fee) << " needed)"); if (needed_fee > available_for_fee && 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->addr) << " from " << print_money(i->amount) << " to " << print_money(new_paid_amount) << " to accomodate " << 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"); if (use_rct) transfer_selected_rct(tx.dsts, tx.selected_transfers, fake_outs_count, unlock_time, needed_fee, extra, test_tx, test_ptx); else transfer_selected(tx.dsts, tx.selected_transfers, fake_outs_count, 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); LOG_PRINT_L2("Made a final " << ((txBlob.size() + 1023)/1024) << " kB 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()); } } } } if (adding_fee) { LOG_PRINT_L1("We ran out of outputs while trying to gather final fee"); THROW_WALLET_EXCEPTION_IF(1, error::not_enough_money, unlocked_balance(), 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 (std::list::const_iterator mi = tx.selected_transfers.begin(); mi != tx.selected_transfers.end(); ++mi) tx_money += (*mi)->amount(); LOG_PRINT_L1(" Transaction " << (1+std::distance(txes.begin(), i)) << "/" << txes.size() << ": " << (tx.bytes+1023)/1024 << " kB, 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(const cryptonote::account_public_address &address, const size_t fake_outs_count, const uint64_t unlock_time, const uint64_t fee_multiplier, const std::vector extra, bool trusted_daemon) { std::vector unused_transfers_indices; std::vector unused_dust_indices; uint64_t accumulated_fee, accumulated_outputs, accumulated_change; struct TX { std::list 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_tranaction_size_limit(); const bool use_rct = use_fork_rules(3); // gather all our dust and non dust outputs for (size_t i = 0; i < m_transfers.size(); ++i) { const transfer_details& td = m_transfers[i]; if (!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); } } LOG_PRINT_L2("Starting with " << unused_transfers_indices.size() << " non-dust outputs and " << unused_dust_indices.size() << " dust outputs"); // 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_random_value(unused_dust_indices) : unused_dust_indices.empty() ? pop_random_value(unused_transfers_indices) : ((tx.selected_transfers.size() & 1) || accumulated_outputs > FEE_PER_KB * fee_multiplier * (upper_transaction_size_limit + 1023) / 1024) ? pop_random_value(unused_dust_indices) : pop_random_value(unused_transfers_indices); 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(m_transfers.begin() + idx); uint64_t available_amount = td.amount(); accumulated_outputs += available_amount; // 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); size_t estimated_rct_tx_size; if (use_rct) estimated_rct_tx_size = estimate_rct_tx_size(tx.selected_transfers.size(), fake_outs_count, tx.dsts.size() + 1); else estimated_rct_tx_size = tx.selected_transfers.size() * (fake_outs_count+1) * APPROXIMATE_INPUT_BYTES; 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; needed_fee = 0; tx.dsts.push_back(tx_destination_entry(1, address)); 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, unlock_time, needed_fee, extra, test_tx, test_ptx); else transfer_selected(tx.dsts, tx.selected_transfers, fake_outs_count, 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(txBlob, fee_multiplier); available_for_fee = test_ptx.fee + test_ptx.dests[0].amount + test_ptx.change_dts.amount; LOG_PRINT_L2("Made a " << txBlob.size() << " kB 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"); do { 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, unlock_time, needed_fee, extra, test_tx, test_ptx); else transfer_selected(tx.dsts, tx.selected_transfers, fake_outs_count, 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(txBlob, fee_multiplier); LOG_PRINT_L2("Made an attempt at a final " << ((txBlob.size() + 1023)/1024) << " kB tx, with " << print_money(test_ptx.fee) << " fee and " << print_money(test_ptx.change_dts.amount) << " change"); } while (needed_fee > test_ptx.fee); LOG_PRINT_L2("Made a final " << ((txBlob.size() + 1023)/1024) << " kB 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 (std::list::const_iterator mi = tx.selected_transfers.begin(); mi != tx.selected_transfers.end(); ++mi) tx_money += (*mi)->amount(); LOG_PRINT_L1(" Transaction " << (1+std::distance(txes.begin(), i)) << "/" << txes.size() << ": " << (tx.bytes+1023)/1024 << " kB, 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; } uint64_t wallet2::unlocked_dust_balance(const tx_dust_policy &dust_policy) const { uint64_t money = 0; std::list selected_transfers; for (transfer_container::const_iterator i = m_transfers.begin(); i != m_transfers.end(); ++i) { const transfer_details& td = *i; if (!td.m_spent && td.amount() < dust_policy.dust_threshold && is_transfer_unlocked(td)) { money += td.amount(); } } return money; } template void wallet2::transfer_from(const std::vector &outs, size_t num_outputs, uint64_t unlock_time, uint64_t needed_fee, T destination_split_strategy, const tx_dust_policy& dust_policy, const std::vector &extra, cryptonote::transaction& tx, pending_tx &ptx) { using namespace cryptonote; uint64_t upper_transaction_size_limit = get_upper_tranaction_size_limit(); // select all dust inputs for transaction // throw if there are none uint64_t money = 0; std::list selected_transfers; #if 1 for (size_t n = 0; n < outs.size(); ++n) { const transfer_details& td = m_transfers[outs[n]]; if (!td.m_spent) { selected_transfers.push_back (m_transfers.begin() + outs[n]); money += td.amount(); if (selected_transfers.size() >= num_outputs) break; } } #else for (transfer_container::iterator i = m_transfers.begin(); i != m_transfers.end(); ++i) { const transfer_details& td = *i; if (!td.m_spent && (td.amount() < dust_policy.dust_threshold || !is_valid_decomposed_amount(td.amount())) && is_transfer_unlocked(td)) { selected_transfers.push_back (i); money += td.amount(); if (selected_transfers.size() >= num_outputs) break; } } #endif // we don't allow no output to self, easier, but one may want to burn the dust if = fee THROW_WALLET_EXCEPTION_IF(money <= needed_fee, error::not_enough_money, money, needed_fee, needed_fee); typedef cryptonote::tx_source_entry::output_entry tx_output_entry; //prepare inputs size_t i = 0; std::vector sources; BOOST_FOREACH(transfer_container::iterator it, selected_transfers) { sources.resize(sources.size()+1); cryptonote::tx_source_entry& src = sources.back(); transfer_details& td = *it; src.amount = td.amount(); //paste real transaction to the random index auto it_to_insert = std::find_if(src.outputs.begin(), src.outputs.end(), [&](const tx_output_entry& a) { return a.first >= td.m_global_output_index; }); 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); auto interted_it = src.outputs.insert(it_to_insert, real_oe); src.real_out_tx_key = get_tx_pub_key_from_extra(td.m_tx); src.real_output = interted_it - src.outputs.begin(); src.real_output_in_tx_index = td.m_internal_output_index; detail::print_source_entry(src); ++i; } cryptonote::tx_destination_entry change_dts = AUTO_VAL_INIT(change_dts); std::vector dsts; uint64_t money_back = money - needed_fee; if (dust_policy.dust_threshold > 0) money_back = money_back - money_back % dust_policy.dust_threshold; dsts.push_back(cryptonote::tx_destination_entry(money_back, m_account_public_address)); std::vector splitted_dsts, dust; destination_split_strategy(dsts, change_dts, dust_policy.dust_threshold, splitted_dsts, dust); BOOST_FOREACH(auto& d, dust) { 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)); } crypto::secret_key tx_key; bool r = cryptonote::construct_tx_and_get_tx_key(m_account.get_keys(), sources, splitted_dsts, extra, tx, unlock_time, tx_key); THROW_WALLET_EXCEPTION_IF(!r, error::tx_not_constructed, sources, splitted_dsts, unlock_time, m_testnet); THROW_WALLET_EXCEPTION_IF(upper_transaction_size_limit <= get_object_blobsize(tx), error::tx_too_big, tx, upper_transaction_size_limit); std::string key_images; bool all_are_txin_to_key = std::all_of(tx.vin.begin(), 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, tx); ptx.key_images = key_images; ptx.fee = money - money_back; ptx.dust = 0; ptx.tx = tx; ptx.change_dts = change_dts; ptx.selected_transfers = selected_transfers; ptx.tx_key = tx_key; ptx.dests = dsts; } //---------------------------------------------------------------------------------------------------- bool wallet2::use_fork_rules(uint8_t version) { cryptonote::COMMAND_RPC_GET_HEIGHT::request req = AUTO_VAL_INIT(req); cryptonote::COMMAND_RPC_GET_HEIGHT::response res = AUTO_VAL_INIT(res); 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(); bool r = net_utils::invoke_http_json_remote_command2(m_daemon_address + "/getheight", req, res, m_http_client); m_daemon_rpc_mutex.unlock(); CHECK_AND_ASSERT_MES(r, false, "Failed to connect to daemon"); CHECK_AND_ASSERT_MES(res.status != CORE_RPC_STATUS_BUSY, false, "Failed to connect to daemon"); CHECK_AND_ASSERT_MES(res.status == CORE_RPC_STATUS_OK, false, "Failed to get current blockchain height"); m_daemon_rpc_mutex.lock(); req_t.jsonrpc = "2.0"; req_t.id = epee::serialization::storage_entry(0); req_t.method = "hard_fork_info"; req_t.params.version = version; r = net_utils::invoke_http_json_remote_command2(m_daemon_address + "/json_rpc", req_t, resp_t, m_http_client); m_daemon_rpc_mutex.unlock(); CHECK_AND_ASSERT_MES(r, false, "Failed to connect to daemon"); CHECK_AND_ASSERT_MES(resp_t.result.status != CORE_RPC_STATUS_BUSY, false, "Failed to connect to daemon"); CHECK_AND_ASSERT_MES(resp_t.result.status == CORE_RPC_STATUS_OK, false, "Failed to get hard fork status"); bool close_enough = res.height >= resp_t.result.earliest_height - 10; // start using the rules a bit 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_tranaction_size_limit() { if (m_upper_transaction_size_limit > 0) return m_upper_transaction_size_limit; uint64_t full_reward_zone = use_fork_rules(2) ? CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V2 : CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V1; return ((full_reward_zone * 125) / 100) - 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 (!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.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 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_remote_command2(m_daemon_address + "/json_rpc", req_t, resp_t, m_http_client); m_daemon_rpc_mutex.unlock(); THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "select_available_unmixable_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); std::set mixable; for (const auto &i: resp_t.result.histogram) { mixable.insert(i.amount); } return select_available_outputs([mixable, atleast](const transfer_details &td) { if (td.is_rct()) return false; const uint64_t amount = td.amount(); if (atleast) { if (mixable.find(amount) != mixable.end()) return true; } else { if (mixable.find(amount) == mixable.end()) return true; } return false; }); } //---------------------------------------------------------------------------------------------------- std::vector wallet2::select_available_unmixable_outputs(bool trusted_daemon) { // request all outputs with less than 3 instances return select_available_outputs_from_histogram(3, false, true, 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 return select_available_outputs_from_histogram(3, 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); // first hard fork has version 2 tx_dust_policy dust_policy(hf1_rules ? 0 : ::config::DEFAULT_DUST_THRESHOLD); // 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(); } // 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); size_t num_outputs_per_tx = (num_dust_outputs + num_tx - 1) / num_tx; std::vector ptx_vector; try { // for each new tx for (size_t i=0; i extra; // loop until fee is met without increasing tx size to next KB boundary. uint64_t needed_fee = 0; do { transfer_from(unmixable_outputs, num_outputs_per_tx, (uint64_t)0 /* unlock_time */, 0, detail::digit_split_strategy, dust_policy, extra, tx, ptx); auto txBlob = t_serializable_object_to_blob(ptx.tx); needed_fee = calculate_fee(txBlob, 1); // reroll the tx with the actual amount minus the fee // if there's not enough for the fee, it'll throw transfer_from(unmixable_outputs, num_outputs_per_tx, (uint64_t)0 /* unlock_time */, needed_fee, detail::digit_split_strategy, dust_policy, extra, tx, ptx); txBlob = t_serializable_object_to_blob(ptx.tx); needed_fee = calculate_fee(txBlob, 1); } while (ptx.fee < needed_fee); ptx_vector.push_back(ptx); // mark transfers to be used as "spent" BOOST_FOREACH(transfer_container::iterator it, ptx.selected_transfers) { it->m_spent = true; it->m_spent_height = 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 BOOST_FOREACH(transfer_container::iterator it2, ptx.selected_transfers) { it2->m_spent = false; it2->m_spent_height = 0; } } // 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 BOOST_FOREACH(transfer_container::iterator it2, ptx.selected_transfers) { it2->m_spent = false; it2->m_spent_height = 0; } } 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 BOOST_FOREACH(transfer_container::iterator it2, ptx.selected_transfers) { it2->m_spent = false; it2->m_spent_height = 0; } } throw; } } } bool wallet2::get_tx_key(const crypto::hash &txid, crypto::secret_key &tx_key) const { const std::unordered_map::const_iterator i = m_tx_keys.find(txid); if (i == m_tx_keys.end()) return false; tx_key = i->second; return true; } 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; } 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; } 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); } //---------------------------------------------------------------------------------------------------- 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 } tx_extra_pub_key pub_key_field; THROW_WALLET_EXCEPTION_IF(!find_tx_extra_field_by_type(tx_extra_fields, pub_key_field), error::wallet_internal_error, "Public key wasn't found in the transaction extra"); crypto::public_key tx_pub_key = pub_key_field.pub_key; // generate ephemeral secret key crypto::key_image ki; cryptonote::keypair in_ephemeral; cryptonote::generate_key_image_helper(m_account.get_keys(), tx_pub_key, td.m_internal_output_index, in_ephemeral, ki); THROW_WALLET_EXCEPTION_IF(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::vector> &signed_key_images, uint64_t &spent, uint64_t &unspent) { 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(!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_daemon_rpc_mutex.lock(); bool r = epee::net_utils::invoke_http_json_remote_command2(m_daemon_address + "/is_key_image_spent", req, daemon_resp, m_http_client, 200000); 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())); spent = 0; unspent = 0; for (size_t n = 0; n < daemon_resp.spent_status.size(); ++n) { transfer_details &td = m_transfers[n]; uint64_t amount = td.amount(); td.m_spent = daemon_resp.spent_status[n] != COMMAND_RPC_IS_KEY_IMAGE_SPENT::UNSPENT; if (td.m_spent) spent += amount; else unspent += amount; LOG_PRINT_L2("Transfer " << n << ": " << print_money(amount) << " (" << td.m_global_output_index << "): " << (td.m_spent ? "spent" : "unspent") << " (key image " << req.key_images[n] << ")"); } LOG_PRINT_L1("Total: " << print_money(spent) << " spent, " << print_money(unspent) << " unspent"); return m_transfers[signed_key_images.size() - 1].m_block_height; } //---------------------------------------------------------------------------------------------------- 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); } } }