diff options
Diffstat (limited to 'src/wallet/wallet2.cpp')
-rw-r--r-- | src/wallet/wallet2.cpp | 1059 |
1 files changed, 856 insertions, 203 deletions
diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp index 8502353ed..99866dbeb 100644 --- a/src/wallet/wallet2.cpp +++ b/src/wallet/wallet2.cpp @@ -29,6 +29,7 @@ // Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers #include <random> +#include <tuple> #include <boost/archive/binary_oarchive.hpp> #include <boost/archive/binary_iarchive.hpp> @@ -55,6 +56,7 @@ using namespace epee; #include "rapidjson/stringbuffer.h" #include "common/json_util.h" #include "common/base58.h" +#include "ringct/rctSigs.h" extern "C" { @@ -96,14 +98,18 @@ void do_prepare_file_names(const std::string& file_path, std::string& keys_file, } } -uint64_t calculate_fee(const cryptonote::blobdata &blob, uint64_t fee_multiplier) +uint64_t calculate_fee(size_t bytes, 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; } +uint64_t calculate_fee(const cryptonote::blobdata &blob, uint64_t fee_multiplier) +{ + return calculate_fee(blob.size(), fee_multiplier); +} + } //namespace namespace tools @@ -168,7 +174,21 @@ 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, uint64_t &money_transfered, bool &error) const +void wallet2::set_spent(transfer_details &td, uint64_t height) +{ + LOG_PRINT_L2("Setting SPENT at " << height << ": ki " << td.m_key_image << ", amount " << print_money(td.m_amount)); + td.m_spent = true; + td.m_spent_height = height; +} +//---------------------------------------------------------------------------------------------------- +void wallet2::set_unspent(transfer_details &td) +{ + LOG_PRINT_L2("Setting UNSPENT: ki " << td.m_key_image << ", amount " << print_money(td.m_amount)); + td.m_spent = false; + td.m_spent_height = 0; +} +//---------------------------------------------------------------------------------------------------- +void wallet2::check_acc_out(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)) { @@ -176,9 +196,10 @@ void wallet2::check_acc_out(const account_keys &acc, const tx_out &o, const cryp LOG_ERROR("wrong type id in transaction out"); return; } - if(is_out_to_acc(acc, boost::get<txout_to_key>(o.target), tx_pub_key, i)) + received = is_out_to_acc(acc, boost::get<txout_to_key>(o.target), tx_pub_key, i); + if(received) { - money_transfered = o.amount; + money_transfered = o.amount; // may be 0 for ringct outputs } else { @@ -187,8 +208,57 @@ void wallet2::check_acc_out(const account_keys &acc, const tx_out &o, const cryp error = false; } //---------------------------------------------------------------------------------------------------- -void wallet2::process_new_transaction(const cryptonote::transaction& tx, uint64_t height, uint64_t ts, bool miner_tx, bool pool) +static uint64_t decodeRct(const rct::rctSig & rv, const crypto::public_key pub, const crypto::secret_key &sec, unsigned int i, rct::key & mask) { + crypto::key_derivation derivation; + bool r = crypto::generate_key_derivation(pub, sec, derivation); + if (!r) + { + LOG_ERROR("Failed to generate key derivation to decode rct output " << i); + return 0; + } + crypto::secret_key scalar1; + crypto::derivation_to_scalar(derivation, i, scalar1); + try + { + switch (rv.type) + { + case rct::RCTTypeSimple: + return rct::decodeRctSimple(rv, rct::sk2rct(scalar1), i, mask); + case rct::RCTTypeFull: + return rct::decodeRct(rv, rct::sk2rct(scalar1), i, mask); + default: + LOG_ERROR("Unsupported rct type: " << rv.type); + return 0; + } + } + catch (const std::exception &e) + { + LOG_ERROR("Failed to decode input " << i); + return 0; + } +} +//---------------------------------------------------------------------------------------------------- +void wallet2::process_new_transaction(const cryptonote::transaction& tx, const std::vector<uint64_t> &o_indices, uint64_t height, uint64_t ts, bool miner_tx, bool pool) +{ + class lazy_txid_getter + { + const cryptonote::transaction &tx; + crypto::hash lazy_txid; + bool computed; + public: + lazy_txid_getter(const transaction &tx): tx(tx), computed(false) {} + const crypto::hash &operator()() + { + if (!computed) + { + lazy_txid = cryptonote::get_transaction_hash(tx); + computed = true; + } + return lazy_txid; + } + } txid(tx); + if (!miner_tx) process_unconfirmed(tx, height); std::vector<size_t> outs; @@ -199,7 +269,7 @@ void wallet2::process_new_transaction(const cryptonote::transaction& tx, uint64_ 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)); + LOG_PRINT_L0("Transaction extra has unsupported format: " << txid()); } // Don't try to extract tx public key if tx has no ouputs @@ -208,14 +278,19 @@ void wallet2::process_new_transaction(const cryptonote::transaction& tx, uint64_ 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)); + LOG_PRINT_L0("Public key wasn't found in the transaction extra. Skipping transaction " << txid()); if(0 != m_callback) m_callback->on_skip_transaction(height, tx); return; } + int num_vouts_received = 0; tx_pub_key = pub_key_field.pub_key; bool r = true; + std::deque<cryptonote::keypair> in_ephemeral(tx.vout.size()); + std::deque<crypto::key_image> ki(tx.vout.size()); + std::deque<uint64_t> amount(tx.vout.size()); + std::deque<rct::key> mask(tx.vout.size()); int threads = tools::get_max_concurrency(); if (miner_tx && m_refresh_type == RefreshNoCoinbase) { @@ -224,8 +299,8 @@ void wallet2::process_new_transaction(const cryptonote::transaction& tx, uint64_ else if (miner_tx && m_refresh_type == RefreshOptimizeCoinbase) { uint64_t money_transfered = 0; - bool error = false; - check_acc_out(m_account.get_keys(), tx.vout[0], tx_pub_key, 0, money_transfered, error); + 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; @@ -233,10 +308,21 @@ void wallet2::process_new_transaction(const cryptonote::transaction& tx, uint64_ else { // this assumes that the miner tx pays a single address - if (money_transfered > 0) + 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<cryptonote::txout_to_key>(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) + { + const cryptonote::account_keys& keys = m_account.get_keys(); + money_transfered = tools::decodeRct(tx.rct_signatures, pub_key_field.pub_key, keys.m_view_secret_key, 0, mask[0]); + } + amount[0] = money_transfered; tx_money_got_in_outs = money_transfered; + ++num_vouts_received; // process the other outs from that tx boost::asio::io_service ioservice; @@ -250,11 +336,12 @@ void wallet2::process_new_transaction(const cryptonote::transaction& tx, uint64_ const account_keys &keys = m_account.get_keys(); std::vector<uint64_t> money_transfered(tx.vout.size()); std::deque<bool> error(tx.vout.size()); + std::deque<bool> 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(money_transfered[i]), std::ref(error[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) @@ -264,10 +351,21 @@ void wallet2::process_new_transaction(const cryptonote::transaction& tx, uint64_ r = false; break; } - if (money_transfered[i]) + 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<cryptonote::txout_to_key>(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) + { + const cryptonote::account_keys& keys = m_account.get_keys(); + money_transfered[i] = tools::decodeRct(tx.rct_signatures, pub_key_field.pub_key, keys.m_view_secret_key, i, mask[i]); + } tx_money_got_in_outs += money_transfered[i]; + amount[i] = money_transfered[i]; + ++num_vouts_received; } } } @@ -286,10 +384,11 @@ void wallet2::process_new_transaction(const cryptonote::transaction& tx, uint64_ const account_keys &keys = m_account.get_keys(); std::vector<uint64_t> money_transfered(tx.vout.size()); std::deque<bool> error(tx.vout.size()); + std::deque<bool> 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(money_transfered[i]), std::ref(error[i]))); + std::ref(received[i]), std::ref(money_transfered[i]), std::ref(error[i]))); } KILL_IOSERVICE(); tx_money_got_in_outs = 0; @@ -300,37 +399,68 @@ void wallet2::process_new_transaction(const cryptonote::transaction& tx, uint64_ r = false; break; } - if (money_transfered[i]) + 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<cryptonote::txout_to_key>(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) + { + const cryptonote::account_keys& keys = m_account.get_keys(); + money_transfered[i] = tools::decodeRct(tx.rct_signatures, pub_key_field.pub_key, keys.m_view_secret_key, i, mask[i]); + } tx_money_got_in_outs += money_transfered[i]; + amount[i] = money_transfered[i]; + ++num_vouts_received; } } } else { - r = lookup_acc_outs(m_account.get_keys(), tx, tx_pub_key, outs, tx_money_got_in_outs); + 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<cryptonote::txout_to_key>(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) + { + const cryptonote::account_keys& keys = m_account.get_keys(); + money_transfered = tools::decodeRct(tx.rct_signatures, pub_key_field.pub_key, keys.m_view_secret_key, i, mask[i]); + } + amount[i] = money_transfered; + tx_money_got_in_outs += money_transfered; + ++num_vouts_received; + } + } + } } 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) + if(!outs.empty() && num_vouts_received > 0) { //good news - got money! take care about it //usually we have only one transfer for user in transaction - 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())); + THROW_WALLET_EXCEPTION_IF(tx.vout.size() != o_indices.size(), error::wallet_internal_error, + "transactions outputs size=" + std::to_string(tx.vout.size()) + + " not match with daemon response size=" + std::to_string(o_indices.size())); } BOOST_FOREACH(size_t o, outs) @@ -338,13 +468,7 @@ void wallet2::process_new_transaction(const cryptonote::transaction& tx, uint64_ 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())); - crypto::key_image ki; - cryptonote::keypair in_ephemeral; - cryptonote::generate_key_image_helper(m_account.get_keys(), tx_pub_key, o, in_ephemeral, ki); - THROW_WALLET_EXCEPTION_IF(in_ephemeral.pub != boost::get<cryptonote::txout_to_key>(tx.vout[o].target).key, - error::wallet_internal_error, "key_image generated ephemeral public key not matched with output_key"); - - auto kit = m_key_images.find(ki); + 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() ? "<none>" : boost::lexical_cast<std::string>(kit->second)) @@ -357,14 +481,32 @@ void wallet2::process_new_transaction(const cryptonote::transaction& tx, uint64_ 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; - td.m_spent = false; + td.m_global_output_index = o_indices[o]; + td.m_tx = (const cryptonote::transaction_prefix&)tx; + td.m_txid = txid(); + 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]; + td.m_rct = true; + } + else if (miner_tx && tx.version == 2) + { + td.m_mask = rct::identity(); + td.m_rct = true; + } + else + { + td.m_mask = rct::identity(); + td.m_rct = false; + } + set_unspent(td); 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)); + LOG_PRINT_L0("Received money: " << print_money(td.amount()) << ", with tx: " << txid()); if (0 != m_callback) - m_callback->on_money_received(height, td.m_tx, td.m_internal_output_index); + m_callback->on_money_received(height, tx, td.m_amount); } } else if (m_transfers[kit->second].m_spent || m_transfers[kit->second].amount() >= tx.vout[o].amount) @@ -387,14 +529,32 @@ void wallet2::process_new_transaction(const cryptonote::transaction& tx, uint64_ 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; - THROW_WALLET_EXCEPTION_IF(td.m_key_image != ki, error::wallet_internal_error, "Inconsistent key images"); + td.m_global_output_index = o_indices[o]; + td.m_tx = (const cryptonote::transaction_prefix&)tx; + td.m_txid = txid(); + td.m_amount = tx.vout[o].amount; + if (td.m_amount == 0) + { + td.m_mask = mask[o]; + td.m_amount = amount[o]; + td.m_rct = true; + } + else if (miner_tx && tx.version == 2) + { + td.m_mask = rct::identity(); + td.m_rct = true; + } + else + { + td.m_mask = rct::identity(); + td.m_rct = false; + } + 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)); + LOG_PRINT_L0("Received money: " << print_money(td.amount()) << ", with tx: " << txid()); if (0 != m_callback) - m_callback->on_money_received(height, td.m_tx, td.m_internal_output_index); + m_callback->on_money_received(height, tx, td.m_amount); } } } @@ -410,12 +570,20 @@ void wallet2::process_new_transaction(const cryptonote::transaction& tx, uint64_ auto it = m_key_images.find(boost::get<cryptonote::txin_to_key>(in).k_image); if(it != m_key_images.end()) { - LOG_PRINT_L0("Spent money: " << print_money(boost::get<cryptonote::txin_to_key>(in).amount) << ", with tx: " << get_transaction_hash(tx)); - tx_money_spent_in_ins += boost::get<cryptonote::txin_to_key>(in).amount; transfer_details& td = m_transfers[it->second]; - td.m_spent = true; + uint64_t amount = boost::get<cryptonote::txin_to_key>(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: " << txid()); + tx_money_spent_in_ins += amount; + set_spent(td, height); if (0 != m_callback) - m_callback->on_money_spent(height, td.m_tx, td.m_internal_output_index, tx); + m_callback->on_money_spent(height, tx, amount, tx); } } @@ -467,7 +635,7 @@ void wallet2::process_new_transaction(const cryptonote::transaction& tx, uint64_ } payment_details payment; - payment.m_tx_hash = cryptonote::get_transaction_hash(tx); + payment.m_tx_hash = txid(); payment.m_amount = received; payment.m_block_height = height; payment.m_unlock_time = tx.unlock_time; @@ -482,6 +650,9 @@ void wallet2::process_new_transaction(const cryptonote::transaction& tx, uint64_ //---------------------------------------------------------------------------------------------------- void wallet2::process_unconfirmed(const cryptonote::transaction& tx, uint64_t height) { + if (m_unconfirmed_txs.empty()) + return; + crypto::hash txid = get_transaction_hash(tx); auto unconf_it = m_unconfirmed_txs.find(txid); if(unconf_it != m_unconfirmed_txs.end()) { @@ -501,25 +672,38 @@ void wallet2::process_unconfirmed(const cryptonote::transaction& tx, uint64_t he 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); - confirmed_transfer_details &ctd = m_confirmed_txs[txid]; - // operator[] creates if not found + std::pair<std::unordered_map<crypto::hash, confirmed_transfer_details>::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 - ctd.m_amount_in = spent; - ctd.m_amount_out = get_outs_money_amount(tx); - ctd.m_change = received; - ctd.m_block_height = height; - ctd.m_timestamp = ts; + 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) +void wallet2::process_new_blockchain_entry(const cryptonote::block& b, const cryptonote::block_complete_entry& bche, const crypto::hash& bl_id, uint64_t height, const cryptonote::COMMAND_RPC_GET_BLOCKS_FAST::block_output_indices &o_indices) { + size_t txidx = 0; + THROW_WALLET_EXCEPTION_IF(bche.txs.size() + 1 != o_indices.indices.size(), error::wallet_internal_error, + "block transactions=" + std::to_string(bche.txs.size()) + + " not match with daemon response size=" + std::to_string(o_indices.indices.size())); + //handle transactions from new block //optimization: seeking only for blocks that are not older then the wallet creation time plus 1 day. 1 day is for possible user incorrect time setup if(b.timestamp + 60*60*24 > m_account.get_createtime() && height >= m_refresh_from_block_height) { TIME_MEASURE_START(miner_tx_handle_time); - process_new_transaction(b.miner_tx, height, b.timestamp, true, false); + process_new_transaction(b.miner_tx, o_indices.indices[txidx++].indices, height, b.timestamp, true, false); TIME_MEASURE_FINISH(miner_tx_handle_time); TIME_MEASURE_START(txs_handle_time); @@ -528,7 +712,7 @@ void wallet2::process_new_blockchain_entry(const cryptonote::block& b, const cry 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); + process_new_transaction(tx, o_indices.indices[txidx++].indices, 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"); @@ -578,7 +762,7 @@ void wallet2::parse_block_round(const cryptonote::blobdata &blob, cryptonote::bl bl_id = get_block_hash(bl); } //---------------------------------------------------------------------------------------------------- -void wallet2::pull_blocks(uint64_t start_height, uint64_t &blocks_start_height, const std::list<crypto::hash> &short_chain_history, std::list<cryptonote::block_complete_entry> &blocks) +void wallet2::pull_blocks(uint64_t start_height, uint64_t &blocks_start_height, const std::list<crypto::hash> &short_chain_history, std::list<cryptonote::block_complete_entry> &blocks, std::vector<cryptonote::COMMAND_RPC_GET_BLOCKS_FAST::block_output_indices> &o_indices) { cryptonote::COMMAND_RPC_GET_BLOCKS_FAST::request req = AUTO_VAL_INIT(req); cryptonote::COMMAND_RPC_GET_BLOCKS_FAST::response res = AUTO_VAL_INIT(res); @@ -591,9 +775,13 @@ void wallet2::pull_blocks(uint64_t start_height, uint64_t &blocks_start_height, THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "getblocks.bin"); THROW_WALLET_EXCEPTION_IF(res.status == CORE_RPC_STATUS_BUSY, error::daemon_busy, "getblocks.bin"); THROW_WALLET_EXCEPTION_IF(res.status != CORE_RPC_STATUS_OK, error::get_blocks_error, res.status); + THROW_WALLET_EXCEPTION_IF(res.blocks.size() != res.output_indices.size(), error::wallet_internal_error, + "mismatched blocks (" + boost::lexical_cast<std::string>(res.blocks.size()) + ") and output_indices (" + + boost::lexical_cast<std::string>(res.output_indices.size()) + ") sizes from daemon"); blocks_start_height = res.start_height; blocks = res.blocks; + o_indices = res.output_indices; } //---------------------------------------------------------------------------------------------------- void wallet2::pull_hashes(uint64_t start_height, uint64_t &blocks_start_height, const std::list<crypto::hash> &short_chain_history, std::list<crypto::hash> &hashes) @@ -614,10 +802,13 @@ void wallet2::pull_hashes(uint64_t start_height, uint64_t &blocks_start_height, hashes = res.m_block_ids; } //---------------------------------------------------------------------------------------------------- -void wallet2::process_blocks(uint64_t start_height, const std::list<cryptonote::block_complete_entry> &blocks, uint64_t& blocks_added) +void wallet2::process_blocks(uint64_t start_height, const std::list<cryptonote::block_complete_entry> &blocks, const std::vector<cryptonote::COMMAND_RPC_GET_BLOCKS_FAST::block_output_indices> &o_indices, uint64_t& blocks_added) { size_t current_index = start_height; blocks_added = 0; + size_t tx_o_indices_idx = 0; + + THROW_WALLET_EXCEPTION_IF(blocks.size() != o_indices.size(), error::wallet_internal_error, "size mismatch"); int threads = tools::get_max_concurrency(); if (threads > 1) @@ -660,7 +851,7 @@ void wallet2::process_blocks(uint64_t start_height, const std::list<cryptonote:: if(current_index >= m_blockchain.size()) { - process_new_blockchain_entry(bl, *blocki, bl_id, current_index); + process_new_blockchain_entry(bl, *blocki, bl_id, current_index, o_indices[b+i]); ++blocks_added; } else if(bl_id != m_blockchain[current_index]) @@ -672,7 +863,7 @@ void wallet2::process_blocks(uint64_t start_height, const std::list<cryptonote:: string_tools::pod_to_hex(m_blockchain[current_index])); detach_blockchain(current_index); - process_new_blockchain_entry(bl, *blocki, bl_id, current_index); + process_new_blockchain_entry(bl, *blocki, bl_id, current_index, o_indices[b+i]); } else { @@ -694,7 +885,7 @@ void wallet2::process_blocks(uint64_t start_height, const std::list<cryptonote:: 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); + process_new_blockchain_entry(bl, bl_entry, bl_id, current_index, o_indices[tx_o_indices_idx]); ++blocks_added; } else if(bl_id != m_blockchain[current_index]) @@ -706,7 +897,7 @@ void wallet2::process_blocks(uint64_t start_height, const std::list<cryptonote:: string_tools::pod_to_hex(m_blockchain[current_index])); detach_blockchain(current_index); - process_new_blockchain_entry(bl, bl_entry, bl_id, current_index); + process_new_blockchain_entry(bl, bl_entry, bl_id, current_index, o_indices[tx_o_indices_idx]); } else { @@ -714,6 +905,7 @@ void wallet2::process_blocks(uint64_t start_height, const std::list<cryptonote:: } ++current_index; + ++tx_o_indices_idx; } } } @@ -730,7 +922,7 @@ void wallet2::refresh(uint64_t start_height, uint64_t & blocks_fetched) refresh(start_height, blocks_fetched, received_money); } //---------------------------------------------------------------------------------------------------- -void wallet2::pull_next_blocks(uint64_t start_height, uint64_t &blocks_start_height, std::list<crypto::hash> &short_chain_history, const std::list<cryptonote::block_complete_entry> &prev_blocks, std::list<cryptonote::block_complete_entry> &blocks, bool &error) +void wallet2::pull_next_blocks(uint64_t start_height, uint64_t &blocks_start_height, std::list<crypto::hash> &short_chain_history, const std::list<cryptonote::block_complete_entry> &prev_blocks, std::list<cryptonote::block_complete_entry> &blocks, std::vector<cryptonote::COMMAND_RPC_GET_BLOCKS_FAST::block_output_indices> &o_indices, bool &error) { error = false; @@ -748,7 +940,7 @@ void wallet2::pull_next_blocks(uint64_t start_height, uint64_t &blocks_start_hei } // pull the new blocks - pull_blocks(start_height, blocks_start_height, short_chain_history, blocks); + pull_blocks(start_height, blocks_start_height, short_chain_history, blocks, o_indices); } catch(...) { @@ -772,7 +964,7 @@ void wallet2::update_pool_state() std::unordered_map<crypto::hash, wallet2::unconfirmed_transfer_details>::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)); + const std::string txid = epee::string_tools::pod_to_hex(it->first); bool found = false; for (auto it2: res.transactions) { @@ -799,6 +991,24 @@ void wallet2::update_pool_state() { 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<txin_to_key>(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); + set_unspent(td); + break; + } + } + } + } } } } @@ -869,7 +1079,7 @@ void wallet2::update_pool_state() { if (tx_hash == txid) { - process_new_transaction(tx, 0, time(NULL), false, true); + process_new_transaction(tx, std::vector<uint64_t>(), 0, time(NULL), false, true); } else { @@ -980,11 +1190,12 @@ void wallet2::refresh(uint64_t start_height, uint64_t & blocks_fetched, bool& re 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; + crypto::hash last_tx_hash_id = m_transfers.size() ? m_transfers.back().m_txid : null_hash; std::list<crypto::hash> short_chain_history; boost::thread pull_thread; uint64_t blocks_start_height; std::list<cryptonote::block_complete_entry> blocks; + std::vector<COMMAND_RPC_GET_BLOCKS_FAST::block_output_indices> o_indices; // pull the first set of blocks get_short_chain_history(short_chain_history); @@ -1001,7 +1212,7 @@ void wallet2::refresh(uint64_t start_height, uint64_t & blocks_fetched, bool& re // and then fall through to regular refresh processing } - pull_blocks(start_height, blocks_start_height, short_chain_history, blocks); + pull_blocks(start_height, blocks_start_height, short_chain_history, blocks, o_indices); // always reset start_height to 0 to force short_chain_ history to be used on // subsequent pulls in this refresh. start_height = 0; @@ -1013,10 +1224,11 @@ void wallet2::refresh(uint64_t start_height, uint64_t & blocks_fetched, bool& re // pull the next set of blocks while we're processing the current one uint64_t next_blocks_start_height; std::list<cryptonote::block_complete_entry> next_blocks; + std::vector<cryptonote::COMMAND_RPC_GET_BLOCKS_FAST::block_output_indices> next_o_indices; bool error = false; - pull_thread = boost::thread([&]{pull_next_blocks(start_height, next_blocks_start_height, short_chain_history, blocks, next_blocks, error);}); + pull_thread = boost::thread([&]{pull_next_blocks(start_height, next_blocks_start_height, short_chain_history, blocks, next_blocks, next_o_indices, error);}); - process_blocks(blocks_start_height, blocks, added_blocks); + process_blocks(blocks_start_height, blocks, o_indices, added_blocks); blocks_fetched += added_blocks; pull_thread.join(); if(!added_blocks) @@ -1025,6 +1237,7 @@ void wallet2::refresh(uint64_t start_height, uint64_t & blocks_fetched, bool& re // switch to the new blocks from the daemon blocks_start_height = next_blocks_start_height; blocks = next_blocks; + o_indices = next_o_indices; // handle error from async fetching thread if (error) @@ -1049,7 +1262,7 @@ void wallet2::refresh(uint64_t start_height, uint64_t & blocks_fetched, bool& re } } } - if(last_tx_hash_id != (m_transfers.size() ? get_transaction_hash(m_transfers.back().m_tx) : null_hash)) + if(last_tx_hash_id != (m_transfers.size() ? m_transfers.back().m_txid : null_hash)) received_money = true; try @@ -1083,6 +1296,16 @@ 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); + set_unspent(td); + } + } + 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(); @@ -1272,26 +1495,25 @@ bool wallet2::load_keys(const std::string& keys_file_name, const std::string& pa 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); + GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, seed_language, std::string, String, false, std::string()); if (field_seed_language_found) { set_seed_language(field_seed_language); } - GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, watch_only, int, Int, false); - m_watch_only = field_watch_only_found && field_watch_only; - GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, always_confirm_transfers, int, Int, false); + GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, watch_only, int, Int, false, false); + m_watch_only = field_watch_only; + GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, always_confirm_transfers, int, Int, false, 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); + GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, store_tx_keys, int, Int, false, true); + GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, store_tx_info, int, Int, false, true); + m_store_tx_info = ((field_store_tx_keys != 0) || (field_store_tx_info != 0)); + GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, default_mixin, unsigned int, Uint, false, 0); + m_default_mixin = field_default_mixin; + GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, default_fee_multiplier, unsigned int, Uint, false, 0); + m_default_fee_multiplier = field_default_fee_multiplier; + GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, auto_refresh, int, Int, false, true); + m_auto_refresh = field_auto_refresh; + GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, refresh_type, int, Int, false, RefreshType::RefreshDefault); m_refresh_type = RefreshType::RefreshDefault; if (field_refresh_type_found) { @@ -1300,7 +1522,7 @@ bool wallet2::load_keys(const std::string& keys_file_name, const std::string& pa 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); + GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, refresh_height, uint64_t, Uint64, false, 0); if (field_refresh_height_found) m_refresh_from_block_height = field_refresh_height; } @@ -1896,12 +2118,15 @@ void wallet2::rescan_spent() if (td.m_spent) { LOG_PRINT_L0("Marking output " << i << "(" << td.m_key_image << ") as unspent, it was marked as spent"); + set_unspent(td); + td.m_spent_height = 0; } else { LOG_PRINT_L0("Marking output " << i << "(" << td.m_key_image << ") as spent, it was marked as unspent"); + set_spent(td, td.m_spent_height); + // 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; } } } @@ -1962,6 +2187,7 @@ namespace T pop_index(std::vector<T>& vec, size_t idx) { CHECK_AND_ASSERT_MES(!vec.empty(), T(), "Vector must be non-empty"); + CHECK_AND_ASSERT_MES(idx < vec.size(), T(), "idx out of bounds"); T res = vec[idx]; if (idx + 1 != vec.size()) @@ -1981,6 +2207,84 @@ namespace size_t idx = crypto::rand<size_t>() % vec.size(); return pop_index (vec, idx); } + + template<typename T> + T pop_back(std::vector<T>& vec) + { + CHECK_AND_ASSERT_MES(!vec.empty(), T(), "Vector must be non-empty"); + + T res = vec.back(); + vec.pop_back(); + return res; + } +} +//---------------------------------------------------------------------------------------------------- +// This returns a handwavy estimation of how much two outputs are related +// If they're from the same tx, then they're fully related. From close block +// heights, they're kinda related. The actual values don't matter, just +// their ordering, but it could become more murky if we add scores later. +float wallet2::get_output_relatedness(const transfer_details &td0, const transfer_details &td1) const +{ + int dh; + + // expensive test, and same tx will fall onto the same block height below + if (td0.m_txid == td1.m_txid) + return 1.0f; + + // same block height -> possibly tx burst, or same tx (since above is disabled) + dh = td0.m_block_height > td1.m_block_height ? td0.m_block_height - td1.m_block_height : td1.m_block_height - td0.m_block_height; + if (dh == 0) + return 0.9f; + + // adjacent blocks -> possibly tx burst + if (dh == 1) + return 0.8f; + + // could extract the payment id, and compare them, but this is a bit expensive too + + // similar block heights + if (dh < 10) + return 0.2f; + + // don't think these are particularly related + return 0.0f; +} +//---------------------------------------------------------------------------------------------------- +size_t wallet2::pop_best_value_from(const transfer_container &transfers, std::vector<size_t> &unused_indices, const std::list<transfer_container::iterator>& selected_transfers) const +{ + std::vector<size_t> candidates; + float best_relatedness = 1.0f; + for (size_t n = 0; n < unused_indices.size(); ++n) + { + const transfer_details &candidate = transfers[unused_indices[n]]; + float relatedness = 0.0f; + for (const auto &i: selected_transfers) + { + float r = get_output_relatedness(candidate, *i); + if (r > relatedness) + { + relatedness = r; + if (relatedness == 1.0f) + break; + } + } + + if (relatedness < best_relatedness) + { + best_relatedness = relatedness; + candidates.clear(); + } + + if (relatedness == best_relatedness) + candidates.push_back(n); + } + size_t idx = crypto::rand<size_t>() % candidates.size(); + return pop_index (unused_indices, candidates[idx]); +} +//---------------------------------------------------------------------------------------------------- +size_t wallet2::pop_best_value(std::vector<size_t> &unused_indices, const std::list<transfer_container::iterator>& selected_transfers) const +{ + return pop_best_value_from(m_transfers, unused_indices, selected_transfers); } //---------------------------------------------------------------------------------------------------- // Select random input sources for transaction. @@ -1992,7 +2296,7 @@ uint64_t wallet2::select_transfers(uint64_t needed_money, std::vector<size_t> un uint64_t found_money = 0; while (found_money < needed_money && !unused_transfers_indices.empty()) { - size_t idx = pop_random_value(unused_transfers_indices); + size_t idx = pop_best_value(unused_transfers_indices, selected_transfers); transfer_container::iterator it = m_transfers.begin() + idx; selected_transfers.push_back(it); @@ -2002,12 +2306,16 @@ uint64_t wallet2::select_transfers(uint64_t needed_money, std::vector<size_t> un return found_money; } //---------------------------------------------------------------------------------------------------- -void wallet2::add_unconfirmed_tx(const cryptonote::transaction& tx, const std::vector<cryptonote::tx_destination_entry> &dests, const crypto::hash &payment_id, uint64_t change_amount) +void wallet2::add_unconfirmed_tx(const cryptonote::transaction& tx, uint64_t amount_in, const std::vector<cryptonote::tx_destination_entry> &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_tx = (const cryptonote::transaction_prefix&)tx; utd.m_dests = dests; utd.m_payment_id = payment_id; utd.m_state = wallet2::unconfirmed_transfer_details::pending; @@ -2190,19 +2498,26 @@ void wallet2::commit_tx(pending_tx& ptx) txid = get_transaction_hash(ptx.tx); crypto::hash payment_id = cryptonote::null_hash; std::vector<cryptonote::tx_destination_entry> 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, dests, payment_id, ptx.change_dts.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; + { + set_spent(*it, 0); + } //fee includes dust if dust policy specified it. LOG_PRINT_L0("Transaction successfully sent. <" << txid << ">" << ENDL @@ -2280,7 +2595,9 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions(std::vector<crypto // mark transfers to be used as "spent" BOOST_FOREACH(transfer_container::iterator it, ptx.selected_transfers) - it->m_spent = true; + { + set_spent(*it, 0); + } } // if we made it this far, we've selected our transactions. committing them will mark them spent, @@ -2290,7 +2607,9 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions(std::vector<crypto { // mark transfers to be used as not spent BOOST_FOREACH(transfer_container::iterator it2, ptx.selected_transfers) - it2->m_spent = false; + { + set_unspent(*it2); + } } @@ -2307,8 +2626,9 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions(std::vector<crypto { // mark transfers to be used as not spent BOOST_FOREACH(transfer_container::iterator it2, ptx.selected_transfers) - it2->m_spent = false; - + { + set_unspent(*it2); + } } if (attempt_count >= MAX_SPLIT_ATTEMPTS) @@ -2325,8 +2645,9 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions(std::vector<crypto { // mark transfers to be used as not spent BOOST_FOREACH(transfer_container::iterator it2, ptx.selected_transfers) - it2->m_spent = false; - + { + set_unspent(*it2); + } } throw; @@ -2334,40 +2655,12 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions(std::vector<crypto } } -template<typename T> -void wallet2::transfer_selected(const std::vector<cryptonote::tx_destination_entry>& dsts, const std::list<transfer_container::iterator> selected_transfers, size_t fake_outputs_count, - uint64_t unlock_time, uint64_t fee, const std::vector<uint8_t>& extra, T destination_split_strategy, const tx_dust_policy& dust_policy, cryptonote::transaction& tx, pending_tx &ptx) +template<typename entry> +void wallet2::get_outs(std::vector<std::vector<entry>> &outs, const std::list<transfer_container::iterator> &selected_transfers, size_t fake_outputs_count) { - 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<uint64_t, crypto::public_key> entry; - std::vector<std::vector<entry>> outs; - if (fake_outputs_count) + LOG_PRINT_L2("fake_outputs_count: " << fake_outputs_count); + outs.clear(); + if (fake_outputs_count > 0) { // get histogram for the amounts we need epee::json_rpc::request<cryptonote::COMMAND_RPC_GET_OUTPUT_HISTOGRAM::request> req_t = AUTO_VAL_INIT(req_t); @@ -2377,7 +2670,10 @@ void wallet2::transfer_selected(const std::vector<cryptonote::tx_destination_ent 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.amounts.push_back(it->is_rct() ? 0 : it->amount()); + std::sort(req_t.params.amounts.begin(), req_t.params.amounts.end()); + auto end = std::unique(req_t.params.amounts.begin(), req_t.params.amounts.end()); + req_t.params.amounts.resize(std::distance(req_t.params.amounts.begin(), end)); req_t.params.unlocked = true; 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(); @@ -2387,12 +2683,15 @@ void wallet2::transfer_selected(const std::vector<cryptonote::tx_destination_ent // 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); + LOG_PRINT_L2("requested_outputs_count: " << requested_outputs_count); // 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) { + const uint64_t amount = it->is_rct() ? 0 : it->amount(); std::unordered_set<uint64_t> seen_indices; size_t start = req.outputs.size(); @@ -2401,28 +2700,30 @@ void wallet2::transfer_selected(const std::vector<cryptonote::tx_destination_ent uint64_t num_outs = 0; for (auto he: resp_t.result.histogram) { - if (he.amount == it->amount()) + if (he.amount == amount) { num_outs = he.instances; break; } } + LOG_PRINT_L1("" << num_outs << " outputs of size " << print_money(amount)); + if (num_outs <= requested_outputs_count) { for (uint64_t i = 0; i < num_outs; i++) - req.outputs.push_back({it->amount(), i}); + req.outputs.push_back({amount, i}); // duplicate to make up shortfall: this will be caught after the RPC call, // so we can also output the amounts for which we can't reach the required // mixin after checking the actual unlockedness for (uint64_t i = num_outs; i < requested_outputs_count; ++i) - req.outputs.push_back({it->amount(), num_outs - 1}); + req.outputs.push_back({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}); + req.outputs.push_back({amount, it->m_global_output_index}); // while we still need more mixins while (num_found < requested_outputs_count) @@ -2447,7 +2748,7 @@ void wallet2::transfer_selected(const std::vector<cryptonote::tx_destination_ent continue; seen_indices.emplace(i); - req.outputs.push_back({it->amount(), i}); + req.outputs.push_back({amount, i}); ++num_found; } } @@ -2457,6 +2758,9 @@ void wallet2::transfer_selected(const std::vector<cryptonote::tx_destination_ent [](const COMMAND_RPC_GET_OUTPUTS::out &a, const COMMAND_RPC_GET_OUTPUTS::out &b) { return a.index < b.index; }); } + for (auto i: req.outputs) + LOG_PRINT_L1("asking for output " << i.index << " for " << print_money(i.amount)); + // get the keys for those m_daemon_rpc_mutex.lock(); r = epee::net_utils::invoke_http_bin_remote_command2(m_daemon_address + "/get_outs.bin", req, daemon_resp, m_http_client, 200000); @@ -2475,9 +2779,10 @@ void wallet2::transfer_selected(const std::vector<cryptonote::tx_destination_ent { outs.push_back(std::vector<entry>()); outs.back().reserve(fake_outputs_count + 1); + const rct::key mask = it->is_rct() ? rct::commit(it->amount(), it->m_mask) : rct::zeroCommit(it->amount()); // pick real out first (it will be sorted when done) - outs.back().push_back({it->m_global_output_index, boost::get<txout_to_key>(it->m_tx.vout[it->m_internal_output_index].target).key}); + outs.back().push_back(std::make_tuple(it->m_global_output_index, boost::get<txout_to_key>(it->m_tx.vout[it->m_internal_output_index].target).key, mask)); // then pick others in random order till we reach the required number // since we use an equiprobable pick here, we don't upset the triangular distribution @@ -2496,7 +2801,7 @@ void wallet2::transfer_selected(const std::vector<cryptonote::tx_destination_ent 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}); + outs.back().push_back(std::make_tuple(req.outputs[i].index, daemon_resp.outs[i].key, daemon_resp.outs[i].mask)); } if (outs.back().size() < fake_outputs_count + 1) { @@ -2505,14 +2810,14 @@ void wallet2::transfer_selected(const std::vector<cryptonote::tx_destination_ent 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; }); + std::sort(outs.back().begin(), outs.back().end(), [](const entry &a, const entry &b) { return std::get<0>(a) < std::get<0>(b); }); // 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, + THROW_WALLET_EXCEPTION_IF(std::get<0>(outs.back()[n]) == std::get<0>(outs.back()[n-1]), 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, + THROW_WALLET_EXCEPTION_IF(std::get<1>(outs.back()[n]) == std::get<1>(outs.back()[n-1]), error::wallet_internal_error, "Duplicate keys after we have weeded them out"); } } @@ -2525,10 +2830,47 @@ void wallet2::transfer_selected(const std::vector<cryptonote::tx_destination_ent for (transfer_container::iterator it: selected_transfers) { std::vector<entry> v; - v.push_back({it->m_global_output_index, boost::get<txout_to_key>(it->m_tx.vout[it->m_internal_output_index].target).key}); + const rct::key mask = it->is_rct() ? rct::commit(it->amount(), it->m_mask) : rct::zeroCommit(it->amount()); + v.push_back(std::make_tuple(it->m_global_output_index, boost::get<txout_to_key>(it->m_tx.vout[it->m_internal_output_index].target).key, mask)); outs.push_back(v); } } +} + +template<typename T> +void wallet2::transfer_selected(const std::vector<cryptonote::tx_destination_entry>& dsts, const std::list<transfer_container::iterator> selected_transfers, size_t fake_outputs_count, + uint64_t unlock_time, uint64_t fee, const std::vector<uint8_t>& 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::tuple<uint64_t, crypto::public_key, rct::key> entry; + std::vector<std::vector<entry>> outs; + get_outs(outs, selected_transfers, fake_outputs_count); // may throw //prepare inputs typedef cryptonote::tx_source_entry::output_entry tx_output_entry; @@ -2540,13 +2882,16 @@ void wallet2::transfer_selected(const std::vector<cryptonote::tx_destination_ent cryptonote::tx_source_entry& src = sources.back(); transfer_details& td = *it; src.amount = td.amount(); + src.rct = td.is_rct(); //paste keys (fake and real) for (size_t n = 0; n < fake_outputs_count + 1; ++n) { tx_output_entry oe; - oe.first = outs[out_index][n].first; - oe.second = outs[out_index][n].second; + oe.first = std::get<0>(outs[out_index][n]); + oe.second.dest = rct::pk2rct(std::get<1>(outs[out_index][n])); + oe.second.mask = std::get<2>(outs[out_index][n]); + src.outputs.push_back(oe); ++i; } @@ -2561,7 +2906,8 @@ void wallet2::transfer_selected(const std::vector<cryptonote::tx_destination_ent tx_output_entry real_oe; real_oe.first = td.m_global_output_index; - real_oe.second = boost::get<txout_to_key>(td.m_tx.vout[td.m_internal_output_index].target).key; + real_oe.second.dest = rct::pk2rct(boost::get<txout_to_key>(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(); @@ -2621,6 +2967,225 @@ void wallet2::transfer_selected(const std::vector<cryptonote::tx_destination_ent ptx.dests = dsts; } +void wallet2::transfer_selected_rct(std::vector<cryptonote::tx_destination_entry> dsts, const std::list<transfer_container::iterator> selected_transfers, size_t fake_outputs_count, + uint64_t unlock_time, uint64_t fee, const std::vector<uint8_t>& 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 std::tuple<uint64_t, crypto::public_key, rct::key> entry; + std::vector<std::vector<entry>> outs; + get_outs(outs, selected_transfers, fake_outputs_count); // may throw + + //prepare inputs + size_t i = 0, out_index = 0; + std::vector<cryptonote::tx_source_entry> 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(); + src.rct = td.is_rct(); + //paste mixin transaction + + typedef cryptonote::tx_source_entry::output_entry tx_output_entry; + for (size_t n = 0; n < fake_outputs_count + 1; ++n) + { + tx_output_entry oe; + oe.first = std::get<0>(outs[out_index][n]); + oe.second.dest = rct::pk2rct(std::get<1>(outs[out_index][n])); + oe.second.mask = std::get<2>(outs[out_index][n]); + src.outputs.push_back(oe); + } + ++i; + + //paste real transaction to the random index + auto it_to_replace = std::find_if(src.outputs.begin(), src.outputs.end(), [&](const tx_output_entry& a) + { + return a.first == td.m_global_output_index; + }); + THROW_WALLET_EXCEPTION_IF(it_to_replace == src.outputs.end(), error::wallet_internal_error, + "real output not found"); + + tx_output_entry real_oe; + real_oe.first = td.m_global_output_index; + real_oe.second.dest = rct::pk2rct(boost::get<txout_to_key>(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; + src.mask = td.m_mask; + 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; + 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 + + // simple + size += 1; + + // message + size += 32; + + // rangeSigs + size += (2*64*32+32+64*32) * n_outputs; + + // MGs - only the last slot of II is saved, the rest can be reconstructed + size += n_inputs * (32 * (mixin+1) * n_inputs + 32 + 32 * (/*n_inputs+*/1)); + + // mixRing - not serialized, can be reconstructed + /* size += 2 * 32 * (mixin+1) * n_inputs; */ + + // pseudoOuts + size += 32 * n_outputs; + // ecdhInfo + size += 3 * 32 * n_outputs; + // outPk - only commitment is saved + size += 1 * 32 * n_outputs; + // txnFee + size += 4; + + LOG_PRINT_L2("estimated rct tx size for " << n_inputs << " at mixin " << mixin << " and " << n_outputs << ": " << size << " (" << ((32 * n_inputs/*+1*/) + 2 * 32 * (mixin+1) * n_inputs + 32 * n_outputs) << " saved)"); + return size; +} + +std::vector<size_t> wallet2::pick_prefered_rct_inputs(uint64_t needed_money) const +{ + std::vector<size_t> picks; + float current_output_relatdness = 1.0f; + + LOG_PRINT_L2("pick_prefered_rct_inputs: needed_money " << print_money(needed_money)); + + // try to find a rct input of enough size + for (size_t i = 0; i < m_transfers.size(); ++i) + { + const transfer_details& td = m_transfers[i]; + if (!td.m_spent && td.is_rct() && td.amount() >= needed_money && is_transfer_unlocked(td)) + { + LOG_PRINT_L2("We can use " << i << " alone: " << print_money(td.amount())); + picks.push_back(i); + return picks; + } + } + + // then try to find two outputs + // this could be made better by picking one of the outputs to be a small one, since those + // are less useful since often below the needed money, so if one can be used in a pair, + // it gets rid of it for the future + for (size_t i = 0; i < m_transfers.size(); ++i) + { + const transfer_details& td = m_transfers[i]; + if (!td.m_spent && td.is_rct() && is_transfer_unlocked(td)) + { + LOG_PRINT_L2("Considering input " << i << ", " << print_money(td.amount())); + for (size_t j = i + 1; j < m_transfers.size(); ++j) + { + const transfer_details& td2 = m_transfers[j]; + if (!td2.m_spent && td2.is_rct() && td.amount() + td2.amount() >= needed_money && is_transfer_unlocked(td2)) + { + // update our picks if those outputs are less related than any we + // already found. If the same, don't update, and oldest suitable outputs + // will be used in preference. + float relatedness = get_output_relatedness(td, td2); + LOG_PRINT_L2(" with input " << j << ", " << print_money(td2.amount()) << ", relatedness " << relatedness); + if (relatedness < current_output_relatdness) + { + // reset the current picks with those, and return them directly + // if they're unrelated. If they are related, we'll end up returning + // them if we find nothing better + picks.clear(); + picks.push_back(i); + picks.push_back(j); + LOG_PRINT_L0("we could use " << i << " and " << j); + if (relatedness == 0.0f) + return picks; + current_output_relatdness = relatedness; + } + } + } + } + } + + return picks; +} + // 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 @@ -2662,12 +3227,13 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp 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(4, 0); + + fee_multiplier = sanitize_fee_multiplier(fee_multiplier); // throw if attempting a transaction with no destinations THROW_WALLET_EXCEPTION_IF(dsts.empty(), error::zero_destination); - fee_multiplier = sanitize_fee_multiplier (fee_multiplier); - // calculate total amount being sent to all destinations // throw if total amount overflows uint64_t needed_money = 0; @@ -2686,9 +3252,9 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp for (size_t i = 0; i < m_transfers.size(); ++i) { const transfer_details& td = m_transfers[i]; - if (!td.m_spent && is_transfer_unlocked(td)) + if (!td.m_spent && (use_rct ? true : !td.is_rct()) && is_transfer_unlocked(td)) { - if (is_valid_decomposed_amount(td.amount())) + if ((td.is_rct()) || is_valid_decomposed_amount(td.amount())) unused_transfers_indices.push_back(i); else unused_dust_indices.push_back(i); @@ -2704,6 +3270,28 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp adding_fee = false; needed_fee = 0; + // for rct, since we don't see the amounts, we will try to make all transactions + // look the same, with 1 or 2 inputs, and 2 outputs. One input is preferable, as + // this prevents linking to another by provenance analysis, but two is ok if we + // try to pick outputs not from the same block. We will get two outputs, one for + // the destination, and one for change. + std::vector<size_t> prefered_inputs; + uint64_t rct_outs_needed = 2 * (fake_outs_count + 1); + rct_outs_needed += 100; // some fudge factor since we don't know how many are locked + if (use_rct && get_num_rct_outputs() >= rct_outs_needed) + { + // this is used to build a tx that's 1 or 2 inputs, and 2 outputs, which + // will get us a known fee. + uint64_t estimated_fee = calculate_fee(estimate_rct_tx_size(2, fake_outs_count + 1, 2), fee_multiplier); + prefered_inputs = pick_prefered_rct_inputs(needed_money + estimated_fee); + if (!prefered_inputs.empty()) + { + string s; + for (auto i: prefered_inputs) s += print_money(m_transfers[i].amount()) + " "; + LOG_PRINT_L1("Found prefered rct inputs for rct tx: " << s); + } + } + // while we have something to send while ((!dsts.empty() && dsts[0].amount > 0) || adding_fee) { TX &tx = txes.back(); @@ -2716,7 +3304,7 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp // 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); + size_t idx = !prefered_inputs.empty() ? pop_back(prefered_inputs) : !unused_transfers_indices.empty() ? pop_best_value(unused_transfers_indices, tx.selected_transfers) : pop_best_value(unused_dust_indices, tx.selected_transfers); const transfer_details &td = m_transfers[idx]; LOG_PRINT_L2("Picking output " << idx << ", amount " << print_money(td.amount())); @@ -2765,7 +3353,12 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp } else { - try_tx = dsts.empty() || (tx.selected_transfers.size() * (fake_outs_count+1) * APPROXIMATE_INPUT_BYTES >= TX_SIZE_TARGET(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; + try_tx = dsts.empty() || (estimated_rct_tx_size >= TX_SIZE_TARGET(upper_transaction_size_limit)); } if (try_tx) { @@ -2776,8 +3369,12 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp LOG_PRINT_L2("Trying to create a tx now, with " << tx.dsts.size() << " destinations and " << tx.selected_transfers.size() << " outputs"); - 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); + 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); @@ -2814,11 +3411,15 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp else { LOG_PRINT_L2("We made a tx, adjusting fee and saving it"); - 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); + 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"); + 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; @@ -2862,7 +3463,7 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp return ptx_vector; } -std::vector<wallet2::pending_tx> wallet2::create_transactions_all(const cryptonote::account_public_address &address, const size_t fake_outs_count, const uint64_t unlock_time, uint64_t fee_multiplier, const std::vector<uint8_t> extra, bool trusted_daemon) +std::vector<wallet2::pending_tx> 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<uint8_t> extra, bool trusted_daemon) { std::vector<size_t> unused_transfers_indices; std::vector<size_t> unused_dust_indices; @@ -2877,16 +3478,15 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_all(const cryptono std::vector<TX> txes; uint64_t needed_fee, available_for_fee = 0; uint64_t upper_transaction_size_limit = get_upper_tranaction_size_limit(); - - fee_multiplier = sanitize_fee_multiplier(fee_multiplier); + const bool use_rct = use_fork_rules(4, 0); // 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 && is_transfer_unlocked(td)) + if (!td.m_spent && (use_rct ? true : !td.is_rct()) && is_transfer_unlocked(td)) { - if (is_valid_decomposed_amount(td.amount())) + if (td.is_rct() || is_valid_decomposed_amount(td.amount())) unused_transfers_indices.push_back(i); else unused_dust_indices.push_back(i); @@ -2908,7 +3508,7 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_all(const cryptono // 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); + size_t idx = unused_transfers_indices.empty() ? pop_best_value(unused_dust_indices, tx.selected_transfers) : unused_dust_indices.empty() ? pop_best_value(unused_transfers_indices, tx.selected_transfers) : ((tx.selected_transfers.size() & 1) || accumulated_outputs > FEE_PER_KB * fee_multiplier * (upper_transaction_size_limit + 1023) / 1024) ? pop_best_value(unused_dust_indices, tx.selected_transfers) : pop_best_value(unused_transfers_indices, tx.selected_transfers); const transfer_details &td = m_transfers[idx]; LOG_PRINT_L2("Picking output " << idx << ", amount " << print_money(td.amount())); @@ -2921,7 +3521,12 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_all(const cryptono // 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 = (unused_dust_indices.empty() && unused_transfers_indices.empty()) || (tx.selected_transfers.size() * (fake_outs_count+1) * APPROXIMATE_INPUT_BYTES >= TX_SIZE_TARGET(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; @@ -2933,8 +3538,12 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_all(const cryptono LOG_PRINT_L2("Trying to create a tx now, with " << tx.dsts.size() << " destinations and " << tx.selected_transfers.size() << " outputs"); - 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); + 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; @@ -2946,8 +3555,12 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_all(const cryptono do { LOG_PRINT_L2("We made a tx, adjusting fee and saving it"); tx.dsts[0].amount = available_for_fee - needed_fee; - 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); + 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) << @@ -3057,6 +3670,7 @@ void wallet2::transfer_from(const std::vector<size_t> &outs, size_t num_outputs, cryptonote::tx_source_entry& src = sources.back(); transfer_details& td = *it; src.amount = td.amount(); + src.rct = td.is_rct(); //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) @@ -3065,7 +3679,8 @@ void wallet2::transfer_from(const std::vector<size_t> &outs, size_t num_outputs, }); tx_output_entry real_oe; real_oe.first = td.m_global_output_index; - real_oe.second = boost::get<txout_to_key>(td.m_tx.vout[td.m_internal_output_index].target).key; + real_oe.second.dest = rct::pk2rct(boost::get<txout_to_key>(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(); @@ -3113,36 +3728,45 @@ void wallet2::transfer_from(const std::vector<size_t> &outs, size_t num_outputs, } //---------------------------------------------------------------------------------------------------- -bool wallet2::use_fork_rules(uint8_t version) +void wallet2::get_hard_fork_info(uint8_t version, uint64_t &earliest_height) { - 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<cryptonote::COMMAND_RPC_HARD_FORK_INFO::request> req_t = AUTO_VAL_INIT(req_t); epee::json_rpc::response<cryptonote::COMMAND_RPC_HARD_FORK_INFO::response, std::string> 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); + 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(); + CHECK_AND_ASSERT_THROW_MES(r, "Failed to connect to daemon"); + CHECK_AND_ASSERT_THROW_MES(resp_t.result.status != CORE_RPC_STATUS_BUSY, "Failed to connect to daemon"); + CHECK_AND_ASSERT_THROW_MES(resp_t.result.status == CORE_RPC_STATUS_OK, "Failed to get hard fork status"); + + earliest_height = resp_t.result.earliest_height; +} +//---------------------------------------------------------------------------------------------------- +bool wallet2::use_fork_rules(uint8_t version, uint64_t early_blocks) +{ + cryptonote::COMMAND_RPC_GET_HEIGHT::request req = AUTO_VAL_INIT(req); + cryptonote::COMMAND_RPC_GET_HEIGHT::response res = AUTO_VAL_INIT(res); + + 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(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"); + 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"); + + uint64_t earliest_height; + get_hard_fork_info(version, earliest_height); // can throw - bool close_enough = res.height >= resp_t.result.earliest_height - 10; // start using the rules a bit beforehand + bool close_enough = res.height >= earliest_height - early_blocks; // start using the rules that many blocks beforehand if (close_enough) - LOG_PRINT_L2("Using HF1 rules"); + LOG_PRINT_L2("Using v" << (unsigned)version << " rules"); else - LOG_PRINT_L2("Not using HF1 rules"); + LOG_PRINT_L2("Not using v" << (unsigned)version << " rules"); return close_enough; } //---------------------------------------------------------------------------------------------------- @@ -3150,7 +3774,7 @@ 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; + uint64_t full_reward_zone = use_fork_rules(2, 10) ? 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; } //---------------------------------------------------------------------------------------------------- @@ -3213,6 +3837,8 @@ std::vector<size_t> wallet2::select_available_outputs_from_histogram(uint64_t co } 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()) @@ -3226,6 +3852,28 @@ std::vector<size_t> wallet2::select_available_outputs_from_histogram(uint64_t co }); } //---------------------------------------------------------------------------------------------------- +uint64_t wallet2::get_num_rct_outputs() +{ + epee::json_rpc::request<cryptonote::COMMAND_RPC_GET_OUTPUT_HISTOGRAM::request> req_t = AUTO_VAL_INIT(req_t); + epee::json_rpc::response<cryptonote::COMMAND_RPC_GET_OUTPUT_HISTOGRAM::response, std::string> resp_t = AUTO_VAL_INIT(resp_t); + m_daemon_rpc_mutex.lock(); + req_t.jsonrpc = "2.0"; + req_t.id = epee::serialization::storage_entry(0); + req_t.method = "get_output_histogram"; + req_t.params.amounts.push_back(0); + req_t.params.min_count = 0; + req_t.params.max_count = 0; + bool r = net_utils::invoke_http_json_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, "get_num_rct_outputs"); + THROW_WALLET_EXCEPTION_IF(resp_t.result.status == CORE_RPC_STATUS_BUSY, error::daemon_busy, "get_output_histogram"); + THROW_WALLET_EXCEPTION_IF(resp_t.result.status != CORE_RPC_STATUS_OK, error::get_histogram_error, resp_t.result.status); + THROW_WALLET_EXCEPTION_IF(resp_t.result.histogram.size() != 1, error::get_histogram_error, "Expected exactly one response"); + THROW_WALLET_EXCEPTION_IF(resp_t.result.histogram[0].amount != 0, error::get_histogram_error, "Expected 0 amount"); + + return resp_t.result.histogram[0].instances; +} +//---------------------------------------------------------------------------------------------------- std::vector<size_t> wallet2::select_available_unmixable_outputs(bool trusted_daemon) { // request all outputs with less than 3 instances @@ -3241,7 +3889,7 @@ std::vector<size_t> wallet2::select_available_mixable_outputs(bool trusted_daemo std::vector<wallet2::pending_tx> 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 + const bool hf1_rules = use_fork_rules(2, 10); // first hard fork has version 2 tx_dust_policy dust_policy(hf1_rules ? 0 : ::config::DEFAULT_DUST_THRESHOLD); // may throw @@ -3290,7 +3938,9 @@ std::vector<wallet2::pending_tx> wallet2::create_unmixable_sweep_transactions(bo // mark transfers to be used as "spent" BOOST_FOREACH(transfer_container::iterator it, ptx.selected_transfers) - it->m_spent = true; + { + set_spent(*it, 0); + } } // if we made it this far, we've selected our transactions. committing them will mark them spent, @@ -3300,8 +3950,9 @@ std::vector<wallet2::pending_tx> wallet2::create_unmixable_sweep_transactions(bo { // mark transfers to be used as not spent BOOST_FOREACH(transfer_container::iterator it2, ptx.selected_transfers) - it2->m_spent = false; - + { + set_unspent(*it2); + } } // if we made it this far, we're OK to actually send the transactions @@ -3317,8 +3968,9 @@ std::vector<wallet2::pending_tx> wallet2::create_unmixable_sweep_transactions(bo { // mark transfers to be used as not spent BOOST_FOREACH(transfer_container::iterator it2, ptx.selected_transfers) - it2->m_spent = false; - + { + set_unspent(*it2); + } } if (attempt_count >= MAX_SPLIT_ATTEMPTS) @@ -3335,8 +3987,9 @@ std::vector<wallet2::pending_tx> wallet2::create_unmixable_sweep_transactions(bo { // mark transfers to be used as not spent BOOST_FOREACH(transfer_container::iterator it2, ptx.selected_transfers) - it2->m_spent = false; - + { + set_unspent(*it2); + } } throw; @@ -3521,7 +4174,7 @@ uint64_t wallet2::import_key_images(const std::vector<std::pair<crypto::key_imag for (size_t n = 0; n < daemon_resp.spent_status.size(); ++n) { transfer_details &td = m_transfers[n]; - uint64_t amount = td.m_tx.vout[td.m_internal_output_index].amount; + 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; |