diff options
Diffstat (limited to 'src/wallet/wallet2.cpp')
-rw-r--r-- | src/wallet/wallet2.cpp | 1277 |
1 files changed, 923 insertions, 354 deletions
diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp index 323a3a7fe..d451baa09 100644 --- a/src/wallet/wallet2.cpp +++ b/src/wallet/wallet2.cpp @@ -33,6 +33,7 @@ #include <boost/format.hpp> #include <boost/optional/optional.hpp> #include <boost/utility/value_init.hpp> +#include <boost/algorithm/string/join.hpp> #include "include_base_utils.h" using namespace epee; @@ -45,6 +46,7 @@ using namespace epee; #include "cryptonote_basic/cryptonote_basic_impl.h" #include "common/boost_serialization_helper.h" #include "common/command_line.h" +#include "common/threadpool.h" #include "profile_tools.h" #include "crypto/crypto.h" #include "serialization/binary_utils.h" @@ -89,13 +91,8 @@ using namespace cryptonote; #define SECOND_OUTPUT_RELATEDNESS_THRESHOLD 0.0f -#define KILL_IOSERVICE() \ - do { \ - work.reset(); \ - while (!ioservice.stopped()) ioservice.poll(); \ - threadpool.join_all(); \ - ioservice.stop(); \ - } while(0) +#define SUBADDRESS_LOOKAHEAD_MAJOR 50 +#define SUBADDRESS_LOOKAHEAD_MINOR 200 #define KEY_IMAGE_EXPORT_FILE_MAGIC "Monero key image export\002" @@ -296,6 +293,13 @@ std::unique_ptr<tools::wallet2> generate_from_json(const std::string& json_file, return false; } restore_deterministic_wallet = true; + + GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, seed_passphrase, std::string, String, false, std::string()); + if (field_seed_passphrase_found) + { + if (!field_seed_passphrase.empty()) + recovery_key = cryptonote::decrypt_key(recovery_key, field_seed_passphrase); + } } GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, address, std::string, String, false, std::string()); @@ -316,10 +320,8 @@ std::unique_ptr<tools::wallet2> generate_from_json(const std::string& json_file, // public key if it was not given if (field_address_found) { - cryptonote::account_public_address address; - bool has_payment_id; - crypto::hash8 new_payment_id; - if(!get_account_integrated_address_from_str(address, has_payment_id, new_payment_id, testnet, field_address)) + cryptonote::address_parse_info info; + if(!get_account_address_from_str(info, testnet, field_address)) { tools::fail_msg_writer() << tools::wallet2::tr("invalid address"); return false; @@ -331,7 +333,7 @@ std::unique_ptr<tools::wallet2> generate_from_json(const std::string& json_file, tools::fail_msg_writer() << tools::wallet2::tr("failed to verify view key secret key"); return false; } - if (address.m_view_public_key != pkey) { + if (info.address.m_view_public_key != pkey) { tools::fail_msg_writer() << tools::wallet2::tr("view key does not match standard address"); return false; } @@ -343,7 +345,7 @@ std::unique_ptr<tools::wallet2> generate_from_json(const std::string& json_file, tools::fail_msg_writer() << tools::wallet2::tr("failed to verify spend key secret key"); return false; } - if (address.m_spend_public_key != pkey) { + if (info.address.m_spend_public_key != pkey) { tools::fail_msg_writer() << tools::wallet2::tr("spend key does not match standard address"); return false; } @@ -380,11 +382,13 @@ std::unique_ptr<tools::wallet2> generate_from_json(const std::string& json_file, // from the address if (field_address_found) { - cryptonote::account_public_address address2; - bool has_payment_id; - crypto::hash8 new_payment_id; - get_account_integrated_address_from_str(address2, has_payment_id, new_payment_id, testnet, field_address); - address.m_spend_public_key = address2.m_spend_public_key; + cryptonote::address_parse_info info; + if(!get_account_address_from_str(info, testnet, field_address)) + { + tools::fail_msg_writer() << tools::wallet2::tr("failed to parse address: ") << field_address; + return false; + } + address.m_spend_public_key = info.address.m_spend_public_key; } wallet->generate(field_filename, field_password, address, viewkey); } @@ -426,6 +430,20 @@ static void throw_on_rpc_response_error(const boost::optional<std::string> &stat THROW_WALLET_EXCEPTION_IF(*status != CORE_RPC_STATUS_OK, tools::error::wallet_generic_rpc_error, method, *status); } +std::string strjoin(const std::vector<size_t> &V, const char *sep) +{ + std::stringstream ss; + bool first = true; + for (const auto &v: V) + { + if (!first) + ss << sep; + ss << std::to_string(v); + first = false; + } + return ss.str(); +} + } //namespace namespace tools @@ -509,6 +527,7 @@ std::unique_ptr<wallet2> wallet2::make_dummy(const boost::program_options::varia //---------------------------------------------------------------------------------------------------- bool wallet2::init(std::string daemon_address, boost::optional<epee::net_utils::http::login> daemon_login, uint64_t upper_transaction_size_limit) { + m_checkpoints.init_default_checkpoints(m_testnet); if(m_http_client.is_connected()) m_http_client.disconnect(); m_is_initialized = true; @@ -527,7 +546,7 @@ bool wallet2::is_deterministic() const return keys_deterministic; } //---------------------------------------------------------------------------------------------------- -bool wallet2::get_seed(std::string& electrum_words) const +bool wallet2::get_seed(std::string& electrum_words, const std::string &passphrase) const { bool keys_deterministic = is_deterministic(); if (!keys_deterministic) @@ -541,7 +560,10 @@ bool wallet2::get_seed(std::string& electrum_words) const return false; } - crypto::ElectrumWords::bytes_to_words(get_account().get_keys().m_spend_secret_key, electrum_words, seed_language); + crypto::secret_key key = get_account().get_keys().m_spend_secret_key; + if (!passphrase.empty()) + key = cryptonote::encrypt_key(key, passphrase); + crypto::ElectrumWords::bytes_to_words(key, electrum_words, seed_language); return true; } @@ -560,6 +582,129 @@ void wallet2::set_seed_language(const std::string &language) { seed_language = language; } +//---------------------------------------------------------------------------------------------------- +cryptonote::account_public_address wallet2::get_subaddress(const cryptonote::subaddress_index& index) const +{ + const cryptonote::account_keys& keys = m_account.get_keys(); + if (index.is_zero()) + return keys.m_account_address; + + crypto::public_key D = get_subaddress_spend_public_key(index); + + // C = a*D + crypto::public_key C = rct::rct2pk(rct::scalarmultKey(rct::pk2rct(D), rct::sk2rct(keys.m_view_secret_key))); // could have defined secret_key_mult_public_key() under src/crypto + + // result: (C, D) + cryptonote::account_public_address address; + address.m_view_public_key = C; + address.m_spend_public_key = D; + return address; +} +//---------------------------------------------------------------------------------------------------- +crypto::public_key wallet2::get_subaddress_spend_public_key(const cryptonote::subaddress_index& index) const +{ + const cryptonote::account_keys& keys = m_account.get_keys(); + if (index.is_zero()) + return keys.m_account_address.m_spend_public_key; + + // m = Hs(a || index_major || index_minor) + crypto::secret_key m = cryptonote::get_subaddress_secret_key(keys.m_view_secret_key, index); + + // M = m*G + crypto::public_key M; + crypto::secret_key_to_public_key(m, M); + + // D = B + M + rct::key D_rct; + rct::addKeys(D_rct, rct::pk2rct(keys.m_account_address.m_spend_public_key), rct::pk2rct(M)); // could have defined add_public_key() under src/crypto + crypto::public_key D = rct::rct2pk(D_rct); + + return D; +} +//---------------------------------------------------------------------------------------------------- +std::string wallet2::get_subaddress_as_str(const cryptonote::subaddress_index& index) const +{ + cryptonote::account_public_address address = get_subaddress(index); + return cryptonote::get_account_address_as_str(m_testnet, !index.is_zero(), address); +} +//---------------------------------------------------------------------------------------------------- +std::string wallet2::get_integrated_address_as_str(const crypto::hash8& payment_id) const +{ + return cryptonote::get_account_integrated_address_as_str(m_testnet, get_address(), payment_id); +} +//---------------------------------------------------------------------------------------------------- +void wallet2::add_subaddress_account(const std::string& label) +{ + uint32_t index_major = (uint32_t)get_num_subaddress_accounts(); + expand_subaddresses({index_major, 0}); + m_subaddress_labels[index_major][0] = label; +} +//---------------------------------------------------------------------------------------------------- +void wallet2::add_subaddress(uint32_t index_major, const std::string& label) +{ + if (index_major >= m_subaddress_labels.size()) + throw std::runtime_error("index_major is out of bound"); + uint32_t index_minor = (uint32_t)get_num_subaddresses(index_major); + expand_subaddresses({index_major, index_minor}); + m_subaddress_labels[index_major][index_minor] = label; +} +//---------------------------------------------------------------------------------------------------- +void wallet2::expand_subaddresses(const cryptonote::subaddress_index& index) +{ + if (m_subaddress_labels.size() <= index.major) + { + // add new accounts + cryptonote::subaddress_index index2; + for (index2.major = m_subaddress_labels.size(); index2.major < index.major + SUBADDRESS_LOOKAHEAD_MAJOR; ++index2.major) + { + for (index2.minor = 0; index2.minor < (index2.major == index.major ? index.minor : 0) + SUBADDRESS_LOOKAHEAD_MINOR; ++index2.minor) + { + if (m_subaddresses_inv.count(index2) == 0) + { + crypto::public_key D = get_subaddress_spend_public_key(index2); + m_subaddresses[D] = index2; + m_subaddresses_inv[index2] = D; + } + } + } + m_subaddress_labels.resize(index.major + 1, {"Untitled account"}); + m_subaddress_labels[index.major].resize(index.minor + 1); + } + else if (m_subaddress_labels[index.major].size() <= index.minor) + { + // add new subaddresses + cryptonote::subaddress_index index2 = index; + for (index2.minor = m_subaddress_labels[index.major].size(); index2.minor < index.minor + SUBADDRESS_LOOKAHEAD_MINOR; ++index2.minor) + { + if (m_subaddresses_inv.count(index2) == 0) + { + crypto::public_key D = get_subaddress_spend_public_key(index2); + m_subaddresses[D] = index2; + m_subaddresses_inv[index2] = D; + } + } + m_subaddress_labels[index.major].resize(index.minor + 1); + } +} +//---------------------------------------------------------------------------------------------------- +std::string wallet2::get_subaddress_label(const cryptonote::subaddress_index& index) const +{ + if (index.major >= m_subaddress_labels.size()) + throw std::runtime_error("index.major is out of bound"); + if (index.minor >= m_subaddress_labels[index.major].size()) + throw std::runtime_error("index.minor is out of bound"); + return m_subaddress_labels[index.major][index.minor]; +} +//---------------------------------------------------------------------------------------------------- +void wallet2::set_subaddress_label(const cryptonote::subaddress_index& index, const std::string &label) +{ + if (index.major >= m_subaddress_labels.size()) + throw std::runtime_error("index.major is out of bound"); + if (index.minor >= m_subaddress_labels[index.major].size()) + throw std::runtime_error("index.minor is out of bound"); + m_subaddress_labels[index.major][index.minor] = label; +} +//---------------------------------------------------------------------------------------------------- /*! * \brief Tells if the wallet file is deprecated. */ @@ -584,35 +729,28 @@ void wallet2::set_unspent(size_t idx) td.m_spent_height = 0; } //---------------------------------------------------------------------------------------------------- -void wallet2::check_acc_out_precomp(const crypto::public_key &spend_public_key, const tx_out &o, const crypto::key_derivation &derivation, size_t i, bool &received, uint64_t &money_transfered, bool &error) const +void wallet2::check_acc_out_precomp(const tx_out &o, const crypto::key_derivation &derivation, const std::vector<crypto::key_derivation> &additional_derivations, size_t i, tx_scan_info_t &tx_scan_info) const { if (o.target.type() != typeid(txout_to_key)) { - error = true; + tx_scan_info.error = true; LOG_ERROR("wrong type id in transaction out"); return; } - received = is_out_to_acc_precomp(spend_public_key, boost::get<txout_to_key>(o.target), derivation, i); - if(received) + tx_scan_info.received = is_out_to_acc_precomp(m_subaddresses, boost::get<txout_to_key>(o.target).key, derivation, additional_derivations, i); + if(tx_scan_info.received) { - money_transfered = o.amount; // may be 0 for ringct outputs + tx_scan_info.money_transfered = o.amount; // may be 0 for ringct outputs } else { - money_transfered = 0; + tx_scan_info.money_transfered = 0; } - error = false; + tx_scan_info.error = false; } //---------------------------------------------------------------------------------------------------- -static uint64_t decodeRct(const rct::rctSig & rv, const crypto::public_key &pub, const crypto::secret_key &sec, unsigned int i, rct::key & mask) +static uint64_t decodeRct(const rct::rctSig & rv, const crypto::key_derivation &derivation, 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 @@ -635,11 +773,21 @@ static uint64_t decodeRct(const rct::rctSig & rv, const crypto::public_key &pub, } } //---------------------------------------------------------------------------------------------------- -bool wallet2::wallet_generate_key_image_helper(const cryptonote::account_keys& ack, const crypto::public_key& tx_public_key, size_t real_output_index, cryptonote::keypair& in_ephemeral, crypto::key_image& ki) +void wallet2::scan_output(const cryptonote::account_keys &keys, const cryptonote::transaction &tx, const crypto::public_key &tx_pub_key, size_t i, tx_scan_info_t &tx_scan_info, int &num_vouts_received, std::unordered_map<cryptonote::subaddress_index, uint64_t> &tx_money_got_in_outs, std::vector<size_t> &outs) { - if (!cryptonote::generate_key_image_helper(ack, tx_public_key, real_output_index, in_ephemeral, ki)) - return false; - return true; + bool r = cryptonote::generate_key_image_helper_precomp(keys, boost::get<cryptonote::txout_to_key>(tx.vout[i].target).key, tx_scan_info.received->derivation, i, tx_scan_info.received->index, tx_scan_info.in_ephemeral, tx_scan_info.ki); + THROW_WALLET_EXCEPTION_IF(!r, error::wallet_internal_error, "Failed to generate key image"); + THROW_WALLET_EXCEPTION_IF(tx_scan_info.in_ephemeral.pub != boost::get<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 (tx_scan_info.money_transfered == 0) + { + tx_scan_info.money_transfered = tools::decodeRct(tx.rct_signatures, tx_scan_info.received->derivation, i, tx_scan_info.mask); + } + tx_money_got_in_outs[tx_scan_info.received->index] = tx_scan_info.money_transfered; + tx_scan_info.amount = tx_scan_info.money_transfered; + ++num_vouts_received; } //---------------------------------------------------------------------------------------------------- void wallet2::process_new_transaction(const crypto::hash &txid, const cryptonote::transaction& tx, const std::vector<uint64_t> &o_indices, uint64_t height, uint64_t ts, bool miner_tx, bool pool) @@ -647,10 +795,10 @@ void wallet2::process_new_transaction(const crypto::hash &txid, const cryptonote // In this function, tx (probably) only contains the base information // (that is, the prunable stuff may or may not be included) - if (!miner_tx) + if (!miner_tx && !pool) process_unconfirmed(txid, tx, height); std::vector<size_t> outs; - uint64_t tx_money_got_in_outs = 0; + std::unordered_map<cryptonote::subaddress_index, uint64_t> tx_money_got_in_outs; // per receiving subaddress index crypto::public_key tx_pub_key = null_pkey; std::vector<tx_extra_field> tx_extra_fields; @@ -662,6 +810,7 @@ void wallet2::process_new_transaction(const crypto::hash &txid, const cryptonote // Don't try to extract tx public key if tx has no ouputs size_t pk_index = 0; + std::vector<tx_scan_info_t> tx_scan_info(tx.vout.size()); while (!tx.vout.empty()) { // if tx.vout is not empty, we loop through all tx pubkeys @@ -680,131 +829,84 @@ void wallet2::process_new_transaction(const crypto::hash &txid, const cryptonote 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(); + tools::threadpool& tpool = tools::threadpool::getInstance(); + tools::threadpool::waiter waiter; const cryptonote::account_keys& keys = m_account.get_keys(); crypto::key_derivation derivation; generate_key_derivation(tx_pub_key, keys.m_view_secret_key, derivation); + + // additional tx pubkeys and derivations for multi-destination transfers involving one or more subaddresses + std::vector<crypto::public_key> additional_tx_pub_keys = get_additional_tx_pub_keys_from_extra(tx); + std::vector<crypto::key_derivation> additional_derivations; + for (size_t i = 0; i < additional_tx_pub_keys.size(); ++i) + { + additional_derivations.push_back({}); + generate_key_derivation(additional_tx_pub_keys[i], keys.m_view_secret_key, additional_derivations.back()); + } + 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_precomp(keys.m_account_address.m_spend_public_key, tx.vout[0], derivation, 0, received, money_transfered, error); - if (error) + check_acc_out_precomp(tx.vout[0], derivation, additional_derivations, 0, tx_scan_info[0]); + if (tx_scan_info[0].error) { r = false; } else { // this assumes that the miner tx pays a single address - if (received) + if (tx_scan_info[0].received) { - wallet_generate_key_image_helper(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) - { - 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; + scan_output(keys, tx, tx_pub_key, 0, tx_scan_info[0], num_vouts_received, tx_money_got_in_outs, outs); // 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)); - } - - 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_precomp, this, std::cref(keys.m_account_address.m_spend_public_key), std::cref(tx.vout[i]), std::cref(derivation), i, - std::ref(received[i]), std::ref(money_transfered[i]), std::ref(error[i]))); + tpool.submit(&waiter, boost::bind(&wallet2::check_acc_out_precomp, this, std::cref(tx.vout[i]), std::cref(derivation), std::cref(additional_derivations), i, + std::ref(tx_scan_info[i]))); } - KILL_IOSERVICE(); + waiter.wait(); + for (size_t i = 1; i < tx.vout.size(); ++i) { - if (error[i]) + if (tx_scan_info[i].error) { r = false; break; } - if (received[i]) + if (tx_scan_info[i].received) { - wallet_generate_key_image_helper(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) - { - 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; + scan_output(keys, tx, tx_pub_key, i, tx_scan_info[i], num_vouts_received, tx_money_got_in_outs, outs); } } } } } - else if (tx.vout.size() > 1 && threads > 1) + else if (tx.vout.size() > 1 && tools::threadpool::getInstance().get_max_concurrency() > 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)); - } + tools::threadpool& tpool = tools::threadpool::getInstance(); + tools::threadpool::waiter waiter; - 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_precomp, this, std::cref(keys.m_account_address.m_spend_public_key), std::cref(tx.vout[i]), std::cref(derivation), i, - std::ref(received[i]), std::ref(money_transfered[i]), std::ref(error[i]))); + tpool.submit(&waiter, boost::bind(&wallet2::check_acc_out_precomp, this, std::cref(tx.vout[i]), std::cref(derivation), std::cref(additional_derivations), i, + std::ref(tx_scan_info[i]))); } - KILL_IOSERVICE(); - tx_money_got_in_outs = 0; + waiter.wait(); for (size_t i = 0; i < tx.vout.size(); ++i) { - if (error[i]) + if (tx_scan_info[i].error) { r = false; break; } - if (received[i]) + if (tx_scan_info[i].received) { - wallet_generate_key_image_helper(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) - { - 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; + scan_output(keys, tx, tx_pub_key, i, tx_scan_info[i], num_vouts_received, tx_money_got_in_outs, outs); } } } @@ -812,31 +914,15 @@ void wallet2::process_new_transaction(const crypto::hash &txid, const cryptonote { for (size_t i = 0; i < tx.vout.size(); ++i) { - uint64_t money_transfered = 0; - bool error = false, received = false; - check_acc_out_precomp(keys.m_account_address.m_spend_public_key, tx.vout[i], derivation, i, received, money_transfered, error); - if (error) + check_acc_out_precomp(tx.vout[i], derivation, additional_derivations, i, tx_scan_info[i]); + if (tx_scan_info[i].error) { r = false; break; } - else + if (tx_scan_info[i].received) { - if (received) - { - wallet_generate_key_image_helper(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) - { - 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; - } + scan_output(keys, tx, tx_pub_key, i, tx_scan_info[i], num_vouts_received, tx_money_got_in_outs, outs); } } } @@ -858,7 +944,7 @@ void wallet2::process_new_transaction(const crypto::hash &txid, const cryptonote THROW_WALLET_EXCEPTION_IF(tx.vout.size() <= o, error::wallet_internal_error, "wrong out in transaction: internal index=" + std::to_string(o) + ", total_outs=" + std::to_string(tx.vout.size())); - auto kit = m_pub_keys.find(in_ephemeral[o].pub); + auto kit = m_pub_keys.find(tx_scan_info[o].in_ephemeral.pub); THROW_WALLET_EXCEPTION_IF(kit != m_pub_keys.end() && kit->second >= m_transfers.size(), error::wallet_internal_error, std::string("Unexpected transfer index from public key: ") + "got " + (kit == m_pub_keys.end() ? "<none>" : boost::lexical_cast<std::string>(kit->second)) @@ -874,14 +960,16 @@ void wallet2::process_new_transaction(const crypto::hash &txid, const cryptonote 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_key_image = tx_scan_info[o].ki; td.m_key_image_known = !m_watch_only; td.m_amount = tx.vout[o].amount; td.m_pk_index = pk_index - 1; + td.m_subaddr_index = tx_scan_info[o].received->index; + expand_subaddresses(tx_scan_info[o].received->index); if (td.m_amount == 0) { - td.m_mask = mask[o]; - td.m_amount = amount[o]; + td.m_mask = tx_scan_info[o].mask; + td.m_amount = tx_scan_info[o].amount; td.m_rct = true; } else if (miner_tx && tx.version == 2) @@ -896,10 +984,10 @@ void wallet2::process_new_transaction(const crypto::hash &txid, const cryptonote } set_unspent(m_transfers.size()-1); m_key_images[td.m_key_image] = m_transfers.size()-1; - m_pub_keys[in_ephemeral[o].pub] = m_transfers.size()-1; + m_pub_keys[tx_scan_info[o].in_ephemeral.pub] = m_transfers.size()-1; LOG_PRINT_L0("Received money: " << print_money(td.amount()) << ", with tx: " << txid); if (0 != m_callback) - m_callback->on_money_received(height, txid, tx, td.m_amount); + m_callback->on_money_received(height, txid, tx, td.m_amount, td.m_subaddr_index); } } else if (m_transfers[kit->second].m_spent || m_transfers[kit->second].amount() >= tx.vout[o].amount) @@ -915,7 +1003,7 @@ void wallet2::process_new_transaction(const crypto::hash &txid, const cryptonote << " 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; + tx_money_got_in_outs[tx_scan_info[o].received->index] -= tx.vout[o].amount; if (!pool) { @@ -927,10 +1015,12 @@ void wallet2::process_new_transaction(const crypto::hash &txid, const cryptonote td.m_txid = txid; td.m_amount = tx.vout[o].amount; td.m_pk_index = pk_index - 1; + td.m_subaddr_index = tx_scan_info[o].received->index; + expand_subaddresses(tx_scan_info[o].received->index); if (td.m_amount == 0) { - td.m_mask = mask[o]; - td.m_amount = amount[o]; + td.m_mask = tx_scan_info[o].mask; + td.m_amount = tx_scan_info[o].amount; td.m_rct = true; } else if (miner_tx && tx.version == 2) @@ -943,12 +1033,12 @@ void wallet2::process_new_transaction(const crypto::hash &txid, const cryptonote td.m_mask = rct::identity(); td.m_rct = false; } - THROW_WALLET_EXCEPTION_IF(td.get_public_key() != in_ephemeral[o].pub, error::wallet_internal_error, "Inconsistent public keys"); + THROW_WALLET_EXCEPTION_IF(td.get_public_key() != tx_scan_info[o].in_ephemeral.pub, error::wallet_internal_error, "Inconsistent public keys"); THROW_WALLET_EXCEPTION_IF(td.m_spent, error::wallet_internal_error, "Inconsistent spent status"); LOG_PRINT_L0("Received money: " << print_money(td.amount()) << ", with tx: " << txid); if (0 != m_callback) - m_callback->on_money_received(height, txid, tx, td.m_amount); + m_callback->on_money_received(height, txid, tx, td.m_amount, td.m_subaddr_index); } } } @@ -956,6 +1046,8 @@ void wallet2::process_new_transaction(const crypto::hash &txid, const cryptonote } uint64_t tx_money_spent_in_ins = 0; + boost::optional<uint32_t> subaddr_account; + std::set<uint32_t> subaddr_indices; // check all outputs for spending (compare key images) for(auto& in: tx.vin) { @@ -974,23 +1066,50 @@ void wallet2::process_new_transaction(const crypto::hash &txid, const cryptonote } amount = td.amount(); tx_money_spent_in_ins += amount; + if (subaddr_account && *subaddr_account != td.m_subaddr_index.major) + LOG_ERROR("spent funds are from different subaddress accounts; count of incoming/outgoing payments will be incorrect"); + subaddr_account = td.m_subaddr_index.major; + subaddr_indices.insert(td.m_subaddr_index.minor); if (!pool) { LOG_PRINT_L0("Spent money: " << print_money(amount) << ", with tx: " << txid); set_spent(it->second, height); if (0 != m_callback) - m_callback->on_money_spent(height, txid, tx, amount, tx); + m_callback->on_money_spent(height, txid, tx, amount, tx, td.m_subaddr_index); } } } - if (tx_money_spent_in_ins > 0) + if (tx_money_spent_in_ins > 0 && !pool) + { + uint64_t self_received = std::accumulate<decltype(tx_money_got_in_outs.begin()), uint64_t>(tx_money_got_in_outs.begin(), tx_money_got_in_outs.end(), 0, + [&subaddr_account] (uint64_t acc, const std::pair<cryptonote::subaddress_index, uint64_t>& p) + { + return acc + (p.first.major == *subaddr_account ? p.second : 0); + }); + process_outgoing(txid, tx, height, ts, tx_money_spent_in_ins, self_received, *subaddr_account, subaddr_indices); + // if sending to yourself at the same subaddress account, set the outgoing payment amount to 0 so that it's less confusing + uint64_t fee = tx.version == 1 ? tx_money_spent_in_ins - get_outs_money_amount(tx) : tx.rct_signatures.txnFee; + if (tx_money_spent_in_ins == self_received + fee) + { + auto i = m_confirmed_txs.find(txid); + THROW_WALLET_EXCEPTION_IF(i == m_confirmed_txs.end(), error::wallet_internal_error, + "confirmed tx wasn't found: " + string_tools::pod_to_hex(txid)); + i->second.m_change = self_received; + } + } + + // remove change sent to the spending subaddress account from the list of received funds + for (auto i = tx_money_got_in_outs.begin(); i != tx_money_got_in_outs.end();) { - process_outgoing(txid, tx, height, ts, tx_money_spent_in_ins, tx_money_got_in_outs); + if (subaddr_account && i->first.major == *subaddr_account) + i = tx_money_got_in_outs.erase(i); + else + ++i; } - 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) + // create payment_details for each incoming transfer to a subaddress index + if (tx_money_got_in_outs.size() > 0) { tx_extra_nonce extra_nonce; crypto::hash payment_id = null_hash; @@ -1031,20 +1150,24 @@ void wallet2::process_new_transaction(const crypto::hash &txid, const cryptonote LOG_PRINT_L2("Found unencrypted payment ID: " << payment_id); } - payment_details payment; - payment.m_tx_hash = txid; - 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); - if (0 != m_callback) - m_callback->on_unconfirmed_money_received(height, txid, tx, payment.m_amount); + for (const auto& i : tx_money_got_in_outs) + { + payment_details payment; + payment.m_tx_hash = txid; + payment.m_amount = i.second; + payment.m_block_height = height; + payment.m_unlock_time = tx.unlock_time; + payment.m_timestamp = ts; + payment.m_subaddr_index = i.first; + if (pool) { + m_unconfirmed_payments.emplace(payment_id, payment); + if (0 != m_callback) + m_callback->on_unconfirmed_money_received(height, txid, tx, payment.m_amount, payment.m_subaddr_index); + } + else + m_payments.emplace(payment_id, payment); + LOG_PRINT_L2("Payment found in " << (pool ? "pool" : "block") << ": " << payment_id << " / " << payment.m_tx_hash << " / " << payment.m_amount); } - 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); } } //---------------------------------------------------------------------------------------------------- @@ -1068,7 +1191,7 @@ void wallet2::process_unconfirmed(const crypto::hash &txid, const cryptonote::tr } } //---------------------------------------------------------------------------------------------------- -void wallet2::process_outgoing(const crypto::hash &txid, const cryptonote::transaction &tx, uint64_t height, uint64_t ts, uint64_t spent, uint64_t received) +void wallet2::process_outgoing(const crypto::hash &txid, const cryptonote::transaction &tx, uint64_t height, uint64_t ts, uint64_t spent, uint64_t received, uint32_t subaddr_account, const std::set<uint32_t>& subaddr_indices) { 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 @@ -1094,6 +1217,8 @@ void wallet2::process_outgoing(const crypto::hash &txid, const cryptonote::trans get_payment_id_from_tx_extra_nonce(extra_nonce.nonce, entry.first->second.m_payment_id); } } + entry.first->second.m_subaddr_account = subaddr_account; + entry.first->second.m_subaddr_indices = subaddr_indices; } entry.first->second.m_block_height = height; entry.first->second.m_timestamp = ts; @@ -1145,16 +1270,19 @@ void wallet2::get_short_chain_history(std::list<crypto::hash>& ids) const { size_t i = 0; size_t current_multiplier = 1; - size_t sz = m_blockchain.size(); + size_t sz = m_blockchain.size() - m_blockchain.offset(); if(!sz) + { + ids.push_back(m_blockchain.genesis()); return; + } size_t current_back_offset = 1; - bool genesis_included = false; + bool base_included = false; while(current_back_offset < sz) { - ids.push_back(m_blockchain[sz-current_back_offset]); + ids.push_back(m_blockchain[m_blockchain.offset() + sz-current_back_offset]); if(sz-current_back_offset == 0) - genesis_included = true; + base_included = true; if(i < 10) { ++current_back_offset; @@ -1164,8 +1292,10 @@ void wallet2::get_short_chain_history(std::list<crypto::hash>& ids) const } ++i; } - if(!genesis_included) - ids.push_back(m_blockchain[0]); + if(!base_included) + ids.push_back(m_blockchain[m_blockchain.offset()]); + if(m_blockchain.offset()) + ids.push_back(m_blockchain.genesis()); } //---------------------------------------------------------------------------------------------------- void wallet2::parse_block_round(const cryptonote::blobdata &blob, cryptonote::block &bl, crypto::hash &bl_id, bool &error) const @@ -1250,8 +1380,10 @@ void wallet2::process_blocks(uint64_t start_height, const std::list<cryptonote:: size_t tx_o_indices_idx = 0; THROW_WALLET_EXCEPTION_IF(blocks.size() != o_indices.size(), error::wallet_internal_error, "size mismatch"); + THROW_WALLET_EXCEPTION_IF(!m_blockchain.is_in_bounds(current_index), error::wallet_internal_error, "Index out of bounds of hashchain"); - int threads = tools::get_max_concurrency(); + tools::threadpool& tpool = tools::threadpool::getInstance(); + int threads = tpool.get_max_concurrency(); if (threads > 1) { std::vector<crypto::hash> round_block_hashes(threads); @@ -1262,23 +1394,16 @@ void wallet2::process_blocks(uint64_t start_height, const std::list<cryptonote:: 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)); - } + tools::threadpool::waiter waiter; std::list<block_complete_entry>::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), + tpool.submit(&waiter, boost::bind(&wallet2::parse_block_round, this, std::cref(tmpblocki->block), std::ref(round_blocks[i]), std::ref(round_block_hashes[i]), std::ref(error[i]))); ++tmpblocki; } - KILL_IOSERVICE(); + waiter.wait(); tmpblocki = blocki; for (size_t i = 0; i < round_size; ++i) { @@ -1516,6 +1641,18 @@ void wallet2::update_pool_state(bool refreshed) if (i.first == txid) { found = true; + // if this is a payment to yourself at a different subaddress account, don't skip it + // so that you can see the incoming pool tx with 'show_transfers' on that receiving subaddress account + const unconfirmed_transfer_details& utd = i.second; + for (const auto& dst : utd.m_dests) + { + auto subaddr_index = m_subaddresses.find(dst.addr.m_spend_public_key); + if (subaddr_index != m_subaddresses.end() && subaddr_index->second.major != utd.m_subaddr_account) + { + found = false; + break; + } + } break; } } @@ -1552,23 +1689,22 @@ void wallet2::update_pool_state(bool refreshed) { if (res.txs.size() == txids.size()) { - size_t n = 0; - for (const auto &txid: txids) + for (const auto &tx_entry: res.txs) { - // might have just been put in a block - if (res.txs[n].in_pool) + if (tx_entry.in_pool) { cryptonote::transaction tx; cryptonote::blobdata bd; crypto::hash tx_hash, tx_prefix_hash; - if (epee::string_tools::parse_hexstr_to_binbuff(res.txs[n].as_hex, bd)) + if (epee::string_tools::parse_hexstr_to_binbuff(tx_entry.as_hex, bd)) { if (cryptonote::parse_and_validate_tx_from_blob(bd, tx, tx_hash, tx_prefix_hash)) { - if (tx_hash == txid) + const std::vector<crypto::hash>::const_iterator i = std::find(txids.begin(), txids.end(), tx_hash); + if (i != txids.end()) { - process_new_transaction(txid, tx, std::vector<uint64_t>(), 0, time(NULL), false, true); - m_scanned_pool_txs[0].insert(txid); + process_new_transaction(tx_hash, tx, std::vector<uint64_t>(), 0, time(NULL), false, true); + m_scanned_pool_txs[0].insert(tx_hash); if (m_scanned_pool_txs[0].size() > 5000) { std::swap(m_scanned_pool_txs[0], m_scanned_pool_txs[1]); @@ -1577,7 +1713,7 @@ void wallet2::update_pool_state(bool refreshed) } else { - LOG_PRINT_L0("Mismatched txids when processing unconfimed txes from pool"); + MERROR("Got txid " << tx_hash << " which we did not ask for"); } } else @@ -1587,14 +1723,13 @@ void wallet2::update_pool_state(bool refreshed) } else { - LOG_PRINT_L0("Failed to parse tx " << txid); + LOG_PRINT_L0("Failed to parse transaction from daemon"); } } else { - LOG_PRINT_L1("Tx " << txid << " was in pool, but is no more"); + LOG_PRINT_L1("Transaction from daemon was in pool, but is no more"); } - ++n; } } else @@ -1666,12 +1801,13 @@ void wallet2::fast_refresh(uint64_t stop_height, uint64_t &blocks_start_height, } -bool wallet2::add_address_book_row(const cryptonote::account_public_address &address, const crypto::hash &payment_id, const std::string &description) +bool wallet2::add_address_book_row(const cryptonote::account_public_address &address, const crypto::hash &payment_id, const std::string &description, bool is_subaddress) { wallet2::address_book_row a; a.m_address = address; a.m_payment_id = payment_id; a.m_description = description; + a.m_is_subaddress = is_subaddress; auto old_size = m_address_book.size(); m_address_book.push_back(a); @@ -1698,7 +1834,8 @@ void wallet2::refresh(uint64_t start_height, uint64_t & blocks_fetched, bool& re size_t try_count = 0; 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; + tools::threadpool& tpool = tools::threadpool::getInstance(); + tools::threadpool::waiter waiter; uint64_t blocks_start_height; std::list<cryptonote::block_complete_entry> blocks; std::vector<COMMAND_RPC_GET_BLOCKS_FAST::block_output_indices> o_indices; @@ -1736,11 +1873,11 @@ void wallet2::refresh(uint64_t start_height, uint64_t & blocks_fetched, bool& re 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, next_o_indices, error);}); + tpool.submit(&waiter, [&]{pull_next_blocks(start_height, next_blocks_start_height, short_chain_history, blocks, next_blocks, next_o_indices, error);}); process_blocks(blocks_start_height, blocks, o_indices, added_blocks); blocks_fetched += added_blocks; - pull_thread.join(); + waiter.wait(); if(blocks_start_height == next_blocks_start_height) { m_node_rpc_proxy.set_height(m_blockchain.size()); @@ -1762,8 +1899,7 @@ void wallet2::refresh(uint64_t start_height, uint64_t & blocks_fetched, bool& re catch (const std::exception&) { blocks_fetched += added_blocks; - if (pull_thread.joinable()) - pull_thread.join(); + waiter.wait(); if(try_count < 3) { LOG_PRINT_L1("Another try pull_blocks (try_count=" << try_count << ")..."); @@ -1790,7 +1926,7 @@ void wallet2::refresh(uint64_t start_height, uint64_t & blocks_fetched, bool& re 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())); + LOG_PRINT_L1("Refresh done, blocks received: " << blocks_fetched << ", balance (all accounts): " << print_money(balance_all()) << ", unlocked: " << print_money(unlocked_balance_all())); } //---------------------------------------------------------------------------------------------------- bool wallet2::refresh(uint64_t & blocks_fetched, bool& received_money, bool& ok) @@ -1810,6 +1946,13 @@ bool wallet2::refresh(uint64_t & blocks_fetched, bool& received_money, bool& ok) void wallet2::detach_blockchain(uint64_t height) { LOG_PRINT_L0("Detaching blockchain on height " << height); + + // size 1 2 3 4 5 6 7 8 9 + // block 0 1 2 3 4 5 6 7 8 + // C + THROW_WALLET_EXCEPTION_IF(height <= m_checkpoints.get_max_height() && m_blockchain.size() > m_checkpoints.get_max_height(), + error::wallet_internal_error, "Daemon claims reorg below last checkpoint"); + size_t transfers_detached = 0; for (size_t i = 0; i < m_transfers.size(); ++i) @@ -1840,8 +1983,8 @@ void wallet2::detach_blockchain(uint64_t height) } 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()); + size_t blocks_detached = m_blockchain.size() - height; + m_blockchain.crop(height); m_local_bc_height -= blocks_detached; for (auto it = m_payments.begin(); it != m_payments.end(); ) @@ -1878,12 +2021,16 @@ bool wallet2::clear() m_unconfirmed_txs.clear(); m_payments.clear(); m_tx_keys.clear(); + m_additional_tx_keys.clear(); m_confirmed_txs.clear(); m_unconfirmed_payments.clear(); m_scanned_pool_txs[0].clear(); m_scanned_pool_txs[1].clear(); m_address_book.clear(); m_local_bc_height = 1; + m_subaddresses.clear(); + m_subaddresses_inv.clear(); + m_subaddress_labels.clear(); return true; } @@ -1966,6 +2113,9 @@ bool wallet2::store_keys(const std::string& keys_file_name, const std::string& p value2.SetInt(m_confirm_backlog ? 1 :0); json.AddMember("confirm_backlog", value2, json.GetAllocator()); + value2.SetUint(m_confirm_backlog_threshold); + json.AddMember("confirm_backlog_threshold", value2, json.GetAllocator()); + value2.SetInt(m_testnet ? 1 :0); json.AddMember("testnet", value2, json.GetAllocator()); @@ -2041,6 +2191,7 @@ bool wallet2::load_keys(const std::string& keys_file_name, const std::string& pa m_min_output_value = 0; m_merge_destinations = false; m_confirm_backlog = true; + m_confirm_backlog_threshold = 0; } else { @@ -2113,6 +2264,8 @@ bool wallet2::load_keys(const std::string& keys_file_name, const std::string& pa m_merge_destinations = field_merge_destinations; GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, confirm_backlog, int, Int, false, true); m_confirm_backlog = field_confirm_backlog; + GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, confirm_backlog_threshold, uint32_t, Uint, false, 0); + m_confirm_backlog_threshold = field_confirm_backlog_threshold; GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, testnet, int, Int, false, m_testnet); // Wallet is being opened with testnet flag, but is saved as a mainnet wallet THROW_WALLET_EXCEPTION_IF(m_testnet && !field_testnet, error::wallet_internal_error, "Mainnet wallet can not be opened as testnet wallet"); @@ -2255,7 +2408,7 @@ crypto::secret_key wallet2::generate(const std::string& wallet_, const std::stri // Set blockchain height calculated from current date/time uint64_t approx_blockchain_height = get_approximate_blockchain_height(); if(approx_blockchain_height > 0) { - m_refresh_from_block_height = approx_blockchain_height - blocks_per_month; + m_refresh_from_block_height = approx_blockchain_height >= blocks_per_month ? approx_blockchain_height - blocks_per_month : 0; } } bool r = store_keys(m_keys_file, password, false); @@ -2267,6 +2420,7 @@ crypto::secret_key wallet2::generate(const std::string& wallet_, const std::stri cryptonote::block b; generate_genesis(b); m_blockchain.push_back(get_block_hash(b)); + add_subaddress_account(tr("Primary account")); store(); return retval; @@ -2302,6 +2456,7 @@ void wallet2::generate(const std::string& wallet_, const std::string& password, cryptonote::block b; generate_genesis(b); m_blockchain.push_back(get_block_hash(b)); + add_subaddress_account(tr("Primary account")); store(); } @@ -2337,6 +2492,7 @@ void wallet2::generate(const std::string& wallet_, const std::string& password, cryptonote::block b; generate_genesis(b); m_blockchain.push_back(get_block_hash(b)); + add_subaddress_account(tr("Primary account")); store(); } @@ -2585,13 +2741,52 @@ void wallet2::load(const std::string& wallet_, const std::string& password) check_genesis(genesis_hash); } + trim_hashchain(); + + if (get_num_subaddress_accounts() == 0) + add_subaddress_account(tr("Primary account")); + m_local_bc_height = m_blockchain.size(); } //---------------------------------------------------------------------------------------------------- +void wallet2::trim_hashchain() +{ + uint64_t height = m_checkpoints.get_max_height(); + if (!m_blockchain.empty() && m_blockchain.size() == m_blockchain.offset()) + { + MINFO("Fixing empty hashchain"); + epee::json_rpc::request<cryptonote::COMMAND_RPC_GET_BLOCK_HEADER_BY_HEIGHT::request> req = AUTO_VAL_INIT(req); + epee::json_rpc::response<cryptonote::COMMAND_RPC_GET_BLOCK_HEADER_BY_HEIGHT::response, std::string> res = AUTO_VAL_INIT(res); + m_daemon_rpc_mutex.lock(); + req.jsonrpc = "2.0"; + req.id = epee::serialization::storage_entry(0); + req.method = "getblockheaderbyheight"; + req.params.height = m_blockchain.size() - 1; + bool r = net_utils::invoke_http_json("/json_rpc", req, res, m_http_client, rpc_timeout); + m_daemon_rpc_mutex.unlock(); + if (r && res.result.status == CORE_RPC_STATUS_OK) + { + crypto::hash hash; + epee::string_tools::hex_to_pod(res.result.block_header.hash, hash); + m_blockchain.refill(hash); + } + else + { + MERROR("Failed to request block header from daemon, hash chain may be unable to sync till the wallet is loaded with a usable daemon"); + } + } + if (height > 0 && m_blockchain.size() > height) + { + --height; + MDEBUG("trimming to " << height << ", offset " << m_blockchain.offset()); + m_blockchain.trim(height); + } +} +//---------------------------------------------------------------------------------------------------- void wallet2::check_genesis(const crypto::hash& genesis_hash) const { std::string what("Genesis block mismatch. You probably use wallet without testnet flag with blockchain from test network or vice versa"); - THROW_WALLET_EXCEPTION_IF(genesis_hash != m_blockchain[0], error::wallet_internal_error, what); + THROW_WALLET_EXCEPTION_IF(genesis_hash != m_blockchain.genesis(), error::wallet_internal_error, what); } //---------------------------------------------------------------------------------------------------- std::string wallet2::path() const @@ -2606,6 +2801,8 @@ void wallet2::store() //---------------------------------------------------------------------------------------------------- void wallet2::store_to(const std::string &path, const std::string &password) { + trim_hashchain(); + // if file is the same, we do: // 1. save wallet to the *.new file // 2. remove old wallet file @@ -2668,10 +2865,11 @@ void wallet2::store_to(const std::string &path, const std::string &password) // 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); + bool r = store_keys(m_keys_file, password, false); + THROW_WALLET_EXCEPTION_IF(!r, error::file_save_error, m_keys_file); // save address to the new file const std::string address_file = m_wallet_file + ".address.txt"; - bool r = file_io_utils::save_string_to_file(address_file, m_account.get_public_address_str(m_testnet)); + 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); @@ -2695,52 +2893,109 @@ void wallet2::store_to(const std::string &path, const std::string &password) } } //---------------------------------------------------------------------------------------------------- -uint64_t wallet2::unlocked_balance() const +uint64_t wallet2::balance(uint32_t index_major) const { uint64_t amount = 0; - for(const transfer_details& td: m_transfers) - if(!td.m_spent && is_transfer_unlocked(td)) - amount += td.amount(); - + for (const auto& i : balance_per_subaddress(index_major)) + amount += i.second; return amount; } //---------------------------------------------------------------------------------------------------- -uint64_t wallet2::balance() const +uint64_t wallet2::unlocked_balance(uint32_t index_major) const { uint64_t amount = 0; - for(auto& td: m_transfers) - if(!td.m_spent) - amount += td.amount(); - - - for(auto& utx: m_unconfirmed_txs) - if (utx.second.m_state != wallet2::unconfirmed_transfer_details::failed) - amount+= utx.second.m_change; - + for (const auto& i : unlocked_balance_per_subaddress(index_major)) + amount += i.second; return amount; } //---------------------------------------------------------------------------------------------------- +std::map<uint32_t, uint64_t> wallet2::balance_per_subaddress(uint32_t index_major) const +{ + std::map<uint32_t, uint64_t> amount_per_subaddr; + for (const auto& td: m_transfers) + { + if (td.m_subaddr_index.major == index_major && !td.m_spent) + { + auto found = amount_per_subaddr.find(td.m_subaddr_index.minor); + if (found == amount_per_subaddr.end()) + amount_per_subaddr[td.m_subaddr_index.minor] = td.amount(); + else + found->second += td.amount(); + } + } + for (const auto& utx: m_unconfirmed_txs) + { + if (utx.second.m_subaddr_account == index_major && utx.second.m_state != wallet2::unconfirmed_transfer_details::failed) + { + // all changes go to 0-th subaddress (in the current subaddress account) + auto found = amount_per_subaddr.find(0); + if (found == amount_per_subaddr.end()) + amount_per_subaddr[0] = utx.second.m_change; + else + found->second += utx.second.m_change; + } + } + return amount_per_subaddr; +} +//---------------------------------------------------------------------------------------------------- +std::map<uint32_t, uint64_t> wallet2::unlocked_balance_per_subaddress(uint32_t index_major) const +{ + std::map<uint32_t, uint64_t> amount_per_subaddr; + for(const transfer_details& td: m_transfers) + { + if(td.m_subaddr_index.major == index_major && !td.m_spent && is_transfer_unlocked(td)) + { + auto found = amount_per_subaddr.find(td.m_subaddr_index.minor); + if (found == amount_per_subaddr.end()) + amount_per_subaddr[td.m_subaddr_index.minor] = td.amount(); + else + found->second += td.amount(); + } + } + return amount_per_subaddr; +} +//---------------------------------------------------------------------------------------------------- +uint64_t wallet2::balance_all() const +{ + uint64_t r = 0; + for (uint32_t index_major = 0; index_major < get_num_subaddress_accounts(); ++index_major) + r += balance(index_major); + return r; +} +//---------------------------------------------------------------------------------------------------- +uint64_t wallet2::unlocked_balance_all() const +{ + uint64_t r = 0; + for (uint32_t index_major = 0; index_major < get_num_subaddress_accounts(); ++index_major) + r += unlocked_balance(index_major); + return r; +} +//---------------------------------------------------------------------------------------------------- void wallet2::get_transfers(wallet2::transfer_container& incoming_transfers) const { incoming_transfers = m_transfers; } //---------------------------------------------------------------------------------------------------- -void wallet2::get_payments(const crypto::hash& payment_id, std::list<wallet2::payment_details>& payments, uint64_t min_height) const +void wallet2::get_payments(const crypto::hash& payment_id, std::list<wallet2::payment_details>& payments, uint64_t min_height, const boost::optional<uint32_t>& subaddr_account, const std::set<uint32_t>& subaddr_indices) 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) + std::for_each(range.first, range.second, [&payments, &min_height, &subaddr_account, &subaddr_indices](const payment_container::value_type& x) { + if (min_height < x.second.m_block_height && + (!subaddr_account || *subaddr_account == x.second.m_subaddr_index.major) && + (subaddr_indices.empty() || subaddr_indices.count(x.second.m_subaddr_index.minor) == 1)) { payments.push_back(x.second); } }); } //---------------------------------------------------------------------------------------------------- -void wallet2::get_payments(std::list<std::pair<crypto::hash,wallet2::payment_details>>& payments, uint64_t min_height, uint64_t max_height) const +void wallet2::get_payments(std::list<std::pair<crypto::hash,wallet2::payment_details>>& payments, uint64_t min_height, uint64_t max_height, const boost::optional<uint32_t>& subaddr_account, const std::set<uint32_t>& subaddr_indices) const { auto range = std::make_pair(m_payments.begin(), m_payments.end()); - std::for_each(range.first, range.second, [&payments, &min_height, &max_height](const payment_container::value_type& x) { - if (min_height < x.second.m_block_height && max_height >= x.second.m_block_height) + std::for_each(range.first, range.second, [&payments, &min_height, &max_height, &subaddr_account, &subaddr_indices](const payment_container::value_type& x) { + if (min_height < x.second.m_block_height && max_height >= x.second.m_block_height && + (!subaddr_account || *subaddr_account == x.second.m_subaddr_index.major) && + (subaddr_indices.empty() || subaddr_indices.count(x.second.m_subaddr_index.minor) == 1)) { payments.push_back(x); } @@ -2748,25 +3003,35 @@ void wallet2::get_payments(std::list<std::pair<crypto::hash,wallet2::payment_det } //---------------------------------------------------------------------------------------------------- void wallet2::get_payments_out(std::list<std::pair<crypto::hash,wallet2::confirmed_transfer_details>>& confirmed_payments, - uint64_t min_height, uint64_t max_height) const + uint64_t min_height, uint64_t max_height, const boost::optional<uint32_t>& subaddr_account, const std::set<uint32_t>& subaddr_indices) const { for (auto i = m_confirmed_txs.begin(); i != m_confirmed_txs.end(); ++i) { - if (i->second.m_block_height > min_height && i->second.m_block_height <= max_height) { - confirmed_payments.push_back(*i); - } + if (i->second.m_block_height <= min_height || i->second.m_block_height > max_height) + continue; + if (subaddr_account && *subaddr_account != i->second.m_subaddr_account) + continue; + if (!subaddr_indices.empty() && std::count_if(i->second.m_subaddr_indices.begin(), i->second.m_subaddr_indices.end(), [&subaddr_indices](uint32_t index) { return subaddr_indices.count(index) == 1; }) == 0) + continue; + confirmed_payments.push_back(*i); } } //---------------------------------------------------------------------------------------------------- -void wallet2::get_unconfirmed_payments_out(std::list<std::pair<crypto::hash,wallet2::unconfirmed_transfer_details>>& unconfirmed_payments) const +void wallet2::get_unconfirmed_payments_out(std::list<std::pair<crypto::hash,wallet2::unconfirmed_transfer_details>>& unconfirmed_payments, const boost::optional<uint32_t>& subaddr_account, const std::set<uint32_t>& subaddr_indices) const { for (auto i = m_unconfirmed_txs.begin(); i != m_unconfirmed_txs.end(); ++i) { + if (subaddr_account && *subaddr_account != i->second.m_subaddr_account) + continue; + if (!subaddr_indices.empty() && std::count_if(i->second.m_subaddr_indices.begin(), i->second.m_subaddr_indices.end(), [&subaddr_indices](uint32_t index) { return subaddr_indices.count(index) == 1; }) == 0) + continue; unconfirmed_payments.push_back(*i); } } //---------------------------------------------------------------------------------------------------- -void wallet2::get_unconfirmed_payments(std::list<std::pair<crypto::hash,wallet2::payment_details>>& unconfirmed_payments) const +void wallet2::get_unconfirmed_payments(std::list<std::pair<crypto::hash,wallet2::payment_details>>& unconfirmed_payments, const boost::optional<uint32_t>& subaddr_account, const std::set<uint32_t>& subaddr_indices) const { for (auto i = m_unconfirmed_payments.begin(); i != m_unconfirmed_payments.end(); ++i) { + if ((!subaddr_account || *subaddr_account == i->second.m_subaddr_index.major) && + (subaddr_indices.empty() || subaddr_indices.count(i->second.m_subaddr_index.minor) == 1)) unconfirmed_payments.push_back(*i); } } @@ -2831,6 +3096,7 @@ void wallet2::rescan_blockchain(bool refresh) generate_genesis(genesis); crypto::hash genesis_hash = get_block_hash(genesis); m_blockchain.push_back(genesis_hash); + add_subaddress_account(tr("Primary account")); m_local_bc_height = 1; if (refresh) @@ -3028,7 +3294,7 @@ 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, uint64_t amount_in, 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, uint32_t subaddr_account, const std::set<uint32_t>& subaddr_indices) { unconfirmed_transfer_details& utd = m_unconfirmed_txs[cryptonote::get_transaction_hash(tx)]; utd.m_amount_in = amount_in; @@ -3043,6 +3309,8 @@ void wallet2::add_unconfirmed_tx(const cryptonote::transaction& tx, uint64_t amo utd.m_payment_id = payment_id; utd.m_state = wallet2::unconfirmed_transfer_details::pending; utd.m_timestamp = time(NULL); + utd.m_subaddr_account = subaddr_account; + utd.m_subaddr_indices = subaddr_indices; } //---------------------------------------------------------------------------------------------------- @@ -3113,7 +3381,7 @@ crypto::hash wallet2::get_payment_id(const pending_tx &ptx) const { std::vector<tx_extra_field> tx_extra_fields; if(!parse_tx_extra(ptx.tx.extra, tx_extra_fields)) - return cryptonote::null_hash; + return crypto::null_hash; tx_extra_nonce extra_nonce; crypto::hash payment_id = null_hash; if (find_tx_extra_field_by_type(tx_extra_fields, extra_nonce)) @@ -3128,7 +3396,7 @@ crypto::hash wallet2::get_payment_id(const pending_tx &ptx) const } else if (!get_payment_id_from_tx_extra_nonce(extra_nonce.nonce, payment_id)) { - payment_id = cryptonote::null_hash; + payment_id = crypto::null_hash; } } return payment_id; @@ -3177,7 +3445,7 @@ void wallet2::commit_tx(pending_tx& ptx) } txid = get_transaction_hash(ptx.tx); - crypto::hash payment_id = cryptonote::null_hash; + crypto::hash payment_id = crypto::null_hash; std::vector<cryptonote::tx_destination_entry> dests; uint64_t amount_in = 0; if (store_tx_info()) @@ -3187,10 +3455,11 @@ void wallet2::commit_tx(pending_tx& ptx) for(size_t idx: ptx.selected_transfers) amount_in += m_transfers[idx].amount(); } - add_unconfirmed_tx(ptx.tx, amount_in, dests, payment_id, ptx.change_dts.amount); + add_unconfirmed_tx(ptx.tx, amount_in, dests, payment_id, ptx.change_dts.amount, ptx.construction_data.subaddr_account, ptx.construction_data.subaddr_indices); if (store_tx_info()) { m_tx_keys.insert(std::make_pair(txid, ptx.tx_key)); + m_additional_tx_keys.insert(std::make_pair(txid, ptx.additional_tx_keys)); } LOG_PRINT_L2("transaction " << txid << " generated ok and sent to daemon, key_images: [" << ptx.key_images << "]"); @@ -3203,8 +3472,8 @@ void wallet2::commit_tx(pending_tx& ptx) //fee includes dust if dust policy specified it. LOG_PRINT_L1("Transaction successfully sent. <" << txid << ">" << ENDL << "Commission: " << print_money(ptx.fee) << " (dust sent to dust addr: " << print_money((ptx.dust_added_to_fee ? 0 : ptx.dust)) << ")" << ENDL - << "Balance: " << print_money(balance()) << ENDL - << "Unlocked: " << print_money(unlocked_balance()) << ENDL + << "Balance: " << print_money(balance(ptx.construction_data.subaddr_account)) << ENDL + << "Unlocked: " << print_money(unlocked_balance(ptx.construction_data.subaddr_account)) << ENDL << "Please, wait for confirmation for your balance to be unlocked."); } @@ -3322,12 +3591,13 @@ bool wallet2::sign_tx(unsigned_tx_set &exported_txs, const std::string &signed_f signed_tx_set signed_txes; for (size_t n = 0; n < exported_txs.txes.size(); ++n) { - const tools::wallet2::tx_construction_data &sd = exported_txs.txes[n]; + tools::wallet2::tx_construction_data &sd = exported_txs.txes[n]; LOG_PRINT_L1(" " << (n+1) << ": " << sd.sources.size() << " inputs, ring size " << sd.sources[0].outputs.size()); signed_txes.ptx.push_back(pending_tx()); tools::wallet2::pending_tx &ptx = signed_txes.ptx.back(); crypto::secret_key tx_key; - bool r = cryptonote::construct_tx_and_get_tx_key(m_account.get_keys(), sd.sources, sd.splitted_dsts, sd.extra, ptx.tx, sd.unlock_time, tx_key, sd.use_rct); + std::vector<crypto::secret_key> additional_tx_keys; + bool r = cryptonote::construct_tx_and_get_tx_key(m_account.get_keys(), m_subaddresses, sd.sources, sd.splitted_dsts, sd.change_dts.addr, sd.extra, ptx.tx, sd.unlock_time, tx_key, additional_tx_keys, sd.use_rct); THROW_WALLET_EXCEPTION_IF(!r, error::tx_not_constructed, sd.sources, sd.splitted_dsts, sd.unlock_time, m_testnet); // we don't test tx size, because we don't know the current limit, due to not having a blockchain, // and it's a bit pointless to fail there anyway, since it'd be a (good) guess only. We sign anyway, @@ -3341,6 +3611,7 @@ bool wallet2::sign_tx(unsigned_tx_set &exported_txs, const std::string &signed_f { const crypto::hash txid = get_transaction_hash(ptx.tx); m_tx_keys.insert(std::make_pair(txid, tx_key)); + m_additional_tx_keys.insert(std::make_pair(txid, additional_tx_keys)); } std::string key_images; @@ -3527,7 +3798,7 @@ int wallet2::get_fee_algorithm() // // this function will make multiple calls to wallet2::transfer if multiple // transactions will be required -std::vector<wallet2::pending_tx> wallet2::create_transactions(std::vector<cryptonote::tx_destination_entry> dsts, const size_t fake_outs_count, const uint64_t unlock_time, uint32_t priority, const std::vector<uint8_t> extra, bool trusted_daemon) +std::vector<wallet2::pending_tx> wallet2::create_transactions(std::vector<cryptonote::tx_destination_entry> dsts, const size_t fake_outs_count, const uint64_t unlock_time, uint32_t priority, const std::vector<uint8_t>& extra, bool trusted_daemon) { const std::vector<size_t> unused_transfers_indices = select_available_outputs_from_histogram(fake_outs_count + 1, true, true, true, trusted_daemon); @@ -3867,7 +4138,7 @@ void wallet2::get_outs(std::vector<std::vector<tools::wallet2::get_outs_entry>> } template<typename T> -void wallet2::transfer_selected(const std::vector<cryptonote::tx_destination_entry>& dsts, const std::list<size_t> selected_transfers, size_t fake_outputs_count, +void wallet2::transfer_selected(const std::vector<cryptonote::tx_destination_entry>& dsts, const std::list<size_t>& selected_transfers, size_t fake_outputs_count, std::vector<std::vector<tools::wallet2::get_outs_entry>> &outs, 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) { @@ -3898,6 +4169,10 @@ void wallet2::transfer_selected(const std::vector<cryptonote::tx_destination_ent 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); + uint32_t subaddr_account = m_transfers[*selected_transfers.begin()].m_subaddr_index.major; + for (auto i = ++selected_transfers.begin(); i != selected_transfers.end(); ++i) + THROW_WALLET_EXCEPTION_IF(subaddr_account != m_transfers[*i].m_subaddr_index.major, error::wallet_internal_error, "the tx uses funds from multiple accounts"); + if (outs.empty()) get_outs(outs, selected_transfers, fake_outputs_count); // may throw @@ -3940,6 +4215,7 @@ void wallet2::transfer_selected(const std::vector<cryptonote::tx_destination_ent real_oe.second.mask = rct::commit(td.amount(), td.m_mask); *it_to_replace = real_oe; src.real_out_tx_key = get_tx_pub_key_from_extra(td.m_tx, td.m_pk_index); + src.real_out_additional_tx_keys = get_additional_tx_pub_keys_from_extra(td.m_tx); src.real_output = it_to_replace - src.outputs.begin(); src.real_output_in_tx_index = td.m_internal_output_index; detail::print_source_entry(src); @@ -3950,7 +4226,7 @@ void wallet2::transfer_selected(const std::vector<cryptonote::tx_destination_ent 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.addr = get_subaddress({subaddr_account, 0}); change_dts.amount = found_money - needed_money; } @@ -3963,13 +4239,14 @@ void wallet2::transfer_selected(const std::vector<cryptonote::tx_destination_ent } for(auto& d: dust_dsts) { if (!dust_policy.add_to_fee) - splitted_dsts.push_back(cryptonote::tx_destination_entry(d.amount, dust_policy.addr_for_dust)); + splitted_dsts.push_back(cryptonote::tx_destination_entry(d.amount, dust_policy.addr_for_dust, d.is_subaddress)); dust += d.amount; } crypto::secret_key tx_key; + std::vector<crypto::secret_key> additional_tx_keys; LOG_PRINT_L2("constructing tx"); - bool r = cryptonote::construct_tx_and_get_tx_key(m_account.get_keys(), sources, splitted_dsts, extra, tx, unlock_time, tx_key); + bool r = cryptonote::construct_tx_and_get_tx_key(m_account.get_keys(), m_subaddresses, sources, splitted_dsts, change_dts.addr, extra, tx, unlock_time, tx_key, additional_tx_keys); LOG_PRINT_L2("constructed tx, r="<<r); 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); @@ -3997,6 +4274,7 @@ void wallet2::transfer_selected(const std::vector<cryptonote::tx_destination_ent ptx.change_dts = change_dts; ptx.selected_transfers = selected_transfers; ptx.tx_key = tx_key; + ptx.additional_tx_keys = additional_tx_keys; ptx.dests = dsts; ptx.construction_data.sources = sources; ptx.construction_data.change_dts = change_dts; @@ -4006,10 +4284,15 @@ void wallet2::transfer_selected(const std::vector<cryptonote::tx_destination_ent ptx.construction_data.unlock_time = unlock_time; ptx.construction_data.use_rct = false; ptx.construction_data.dests = dsts; + // record which subaddress indices are being used as inputs + ptx.construction_data.subaddr_account = subaddr_account; + ptx.construction_data.subaddr_indices.clear(); + for (size_t idx: selected_transfers) + ptx.construction_data.subaddr_indices.insert(m_transfers[idx].m_subaddr_index.minor); LOG_PRINT_L2("transfer_selected done"); } -void wallet2::transfer_selected_rct(std::vector<cryptonote::tx_destination_entry> dsts, const std::list<size_t> selected_transfers, size_t fake_outputs_count, +void wallet2::transfer_selected_rct(std::vector<cryptonote::tx_destination_entry> dsts, const std::list<size_t>& selected_transfers, size_t fake_outputs_count, std::vector<std::vector<tools::wallet2::get_outs_entry>> &outs, uint64_t unlock_time, uint64_t fee, const std::vector<uint8_t>& extra, cryptonote::transaction& tx, pending_tx &ptx) { @@ -4043,6 +4326,10 @@ void wallet2::transfer_selected_rct(std::vector<cryptonote::tx_destination_entry 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); + uint32_t subaddr_account = m_transfers[*selected_transfers.begin()].m_subaddr_index.major; + for (auto i = ++selected_transfers.begin(); i != selected_transfers.end(); ++i) + THROW_WALLET_EXCEPTION_IF(subaddr_account != m_transfers[*i].m_subaddr_index.major, error::wallet_internal_error, "the tx uses funds from multiple accounts"); + if (outs.empty()) get_outs(outs, selected_transfers, fake_outputs_count); // may throw @@ -4084,6 +4371,7 @@ void wallet2::transfer_selected_rct(std::vector<cryptonote::tx_destination_entry real_oe.second.mask = rct::commit(td.amount(), td.m_mask); *it_to_replace = real_oe; src.real_out_tx_key = get_tx_pub_key_from_extra(td.m_tx, td.m_pk_index); + src.real_out_additional_tx_keys = get_additional_tx_pub_keys_from_extra(td.m_tx); src.real_output = it_to_replace - src.outputs.begin(); src.real_output_in_tx_index = td.m_internal_output_index; src.mask = td.m_mask; @@ -4110,13 +4398,14 @@ void wallet2::transfer_selected_rct(std::vector<cryptonote::tx_destination_entry } else { - change_dts.addr = m_account.get_keys().m_account_address; + change_dts.addr = get_subaddress({subaddr_account, 0}); } splitted_dsts.push_back(change_dts); crypto::secret_key tx_key; + std::vector<crypto::secret_key> additional_tx_keys; LOG_PRINT_L2("constructing tx"); - bool r = cryptonote::construct_tx_and_get_tx_key(m_account.get_keys(), sources, splitted_dsts, extra, tx, unlock_time, tx_key, true); + bool r = cryptonote::construct_tx_and_get_tx_key(m_account.get_keys(), m_subaddresses, sources, splitted_dsts, change_dts.addr, extra, tx, unlock_time, tx_key, additional_tx_keys, true); LOG_PRINT_L2("constructed tx, r="<<r); 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); @@ -4140,6 +4429,7 @@ void wallet2::transfer_selected_rct(std::vector<cryptonote::tx_destination_entry ptx.change_dts = change_dts; ptx.selected_transfers = selected_transfers; ptx.tx_key = tx_key; + ptx.additional_tx_keys = additional_tx_keys; ptx.dests = dsts; ptx.construction_data.sources = sources; ptx.construction_data.change_dts = change_dts; @@ -4149,6 +4439,11 @@ void wallet2::transfer_selected_rct(std::vector<cryptonote::tx_destination_entry ptx.construction_data.unlock_time = unlock_time; ptx.construction_data.use_rct = true; ptx.construction_data.dests = dsts; + // record which subaddress indices are being used as inputs + ptx.construction_data.subaddr_account = subaddr_account; + ptx.construction_data.subaddr_indices.clear(); + for (size_t idx: selected_transfers) + ptx.construction_data.subaddr_indices.insert(m_transfers[idx].m_subaddr_index.minor); LOG_PRINT_L2("transfer_selected_rct done"); } @@ -4205,7 +4500,7 @@ static size_t estimate_tx_size(bool use_rct, int n_inputs, int mixin, int n_outp return n_inputs * (mixin+1) * APPROXIMATE_INPUT_BYTES; } -std::vector<size_t> wallet2::pick_preferred_rct_inputs(uint64_t needed_money) const +std::vector<size_t> wallet2::pick_preferred_rct_inputs(uint64_t needed_money, uint32_t subaddr_account, const std::set<uint32_t> &subaddr_indices) const { std::vector<size_t> picks; float current_output_relatdness = 1.0f; @@ -4216,7 +4511,7 @@ std::vector<size_t> wallet2::pick_preferred_rct_inputs(uint64_t needed_money) co 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)) + if (!td.m_spent && td.is_rct() && td.amount() >= needed_money && is_transfer_unlocked(td) && td.m_subaddr_index.major == subaddr_account && subaddr_indices.count(td.m_subaddr_index.minor) == 1) { LOG_PRINT_L2("We can use " << i << " alone: " << print_money(td.amount())); picks.push_back(i); @@ -4231,13 +4526,13 @@ std::vector<size_t> wallet2::pick_preferred_rct_inputs(uint64_t needed_money) co 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)) + if (!td.m_spent && td.is_rct() && is_transfer_unlocked(td) && td.m_subaddr_index.major == subaddr_account && subaddr_indices.count(td.m_subaddr_index.minor) == 1) { LOG_PRINT_L2("Considering input " << i << ", " << print_money(td.amount())); for (size_t j = i + 1; j < m_transfers.size(); ++j) { const transfer_details& td2 = m_transfers[j]; - if (!td2.m_spent && td2.is_rct() && td.amount() + td2.amount() >= needed_money && is_transfer_unlocked(td2)) + if (!td2.m_spent && td2.is_rct() && td.amount() + td2.amount() >= needed_money && is_transfer_unlocked(td2) && td2.m_subaddr_index == td.m_subaddr_index) { // update our picks if those outputs are less related than any we // already found. If the same, don't update, and oldest suitable outputs @@ -4334,10 +4629,10 @@ static uint32_t get_count_above(const std::vector<wallet2::transfer_details> &tr // 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::pending_tx> wallet2::create_transactions_2(std::vector<cryptonote::tx_destination_entry> dsts, const size_t fake_outs_count, const uint64_t unlock_time, uint32_t priority, const std::vector<uint8_t> extra, bool trusted_daemon) +std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryptonote::tx_destination_entry> dsts, const size_t fake_outs_count, const uint64_t unlock_time, uint32_t priority, const std::vector<uint8_t>& extra, uint32_t subaddr_account, std::set<uint32_t> subaddr_indices, bool trusted_daemon) { - std::vector<size_t> unused_transfers_indices; - std::vector<size_t> unused_dust_indices; + std::vector<std::pair<uint32_t, std::vector<size_t>>> unused_transfers_indices_per_subaddr; + std::vector<std::pair<uint32_t, std::vector<size_t>>> unused_dust_indices_per_subaddr; uint64_t needed_money; uint64_t accumulated_fee, accumulated_outputs, accumulated_change; struct TX { @@ -4347,23 +4642,24 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp pending_tx ptx; size_t bytes; - void add(const account_public_address &addr, uint64_t amount, unsigned int original_output_index, bool merge_destinations) { + void add(const account_public_address &addr, bool is_subaddress, uint64_t amount, unsigned int original_output_index, bool merge_destinations) { if (merge_destinations) { std::vector<cryptonote::tx_destination_entry>::iterator i; i = std::find_if(dsts.begin(), dsts.end(), [&](const cryptonote::tx_destination_entry &d) { return !memcmp (&d.addr, &addr, sizeof(addr)); }); if (i == dsts.end()) { - dsts.push_back(tx_destination_entry(0,addr)); + dsts.push_back(tx_destination_entry(0,addr,is_subaddress)); i = dsts.end() - 1; } i->amount += amount; } else { - THROW_WALLET_EXCEPTION_IF(original_output_index > dsts.size(), error::wallet_internal_error, "original_output_index too large"); + THROW_WALLET_EXCEPTION_IF(original_output_index > dsts.size(), error::wallet_internal_error, + std::string("original_output_index too large: ") + std::to_string(original_output_index) + " > " + std::to_string(dsts.size())); if (original_output_index == dsts.size()) - dsts.push_back(tx_destination_entry(0,addr)); + dsts.push_back(tx_destination_entry(0,addr,is_subaddress)); THROW_WALLET_EXCEPTION_IF(memcmp(&dsts[original_output_index].addr, &addr, sizeof(addr)), error::wallet_internal_error, "Mismatched destination address"); dsts[original_output_index].amount += amount; } @@ -4395,29 +4691,90 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp // 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 + std::map<uint32_t, uint64_t> balance_per_subaddr = unlocked_balance_per_subaddress(subaddr_account); + + if (subaddr_indices.empty()) // "index=<N1>[,<N2>,...]" wasn't specified -> use all the indices with non-zero unlocked bakance + { + for (const auto& i : balance_per_subaddr) + subaddr_indices.insert(i.first); + } + + // early out if we know we can't make it anyway + // we could also check for being within FEE_PER_KB, but if the fee calculation + // ever changes, this might be missed, so let this go through + uint64_t balance_subtotal = 0; + for (uint32_t index_minor : subaddr_indices) + balance_subtotal += balance_per_subaddr[index_minor]; + THROW_WALLET_EXCEPTION_IF(needed_money > balance_subtotal, error::not_enough_money, + balance_subtotal, needed_money, 0); + + for (uint32_t i : subaddr_indices) + LOG_PRINT_L2("Candidate subaddress index for spending: " << i); + + // gather all dust and non-dust outputs belonging to specified subaddresses + size_t num_nondust_outputs = 0; + size_t num_dust_outputs = 0; for (size_t i = 0; i < m_transfers.size(); ++i) { const transfer_details& td = m_transfers[i]; - if (!td.m_spent && (use_rct ? true : !td.is_rct()) && is_transfer_unlocked(td)) + if (!td.m_spent && (use_rct ? true : !td.is_rct()) && is_transfer_unlocked(td) && td.m_subaddr_index.major == subaddr_account && subaddr_indices.count(td.m_subaddr_index.minor) == 1) { + const uint32_t index_minor = td.m_subaddr_index.minor; + auto find_predicate = [&index_minor](const std::pair<uint32_t, std::vector<size_t>>& x) { return x.first == index_minor; }; if ((td.is_rct()) || is_valid_decomposed_amount(td.amount())) - unused_transfers_indices.push_back(i); + { + auto found = std::find_if(unused_transfers_indices_per_subaddr.begin(), unused_transfers_indices_per_subaddr.end(), find_predicate); + if (found == unused_transfers_indices_per_subaddr.end()) + { + unused_transfers_indices_per_subaddr.push_back({index_minor, {i}}); + } + else + { + found->second.push_back(i); + } + ++num_nondust_outputs; + } else - unused_dust_indices.push_back(i); + { + auto found = std::find_if(unused_dust_indices_per_subaddr.begin(), unused_dust_indices_per_subaddr.end(), find_predicate); + if (found == unused_dust_indices_per_subaddr.end()) + { + unused_dust_indices_per_subaddr.push_back({index_minor, {i}}); + } + else + { + found->second.push_back(i); + } + ++num_dust_outputs; + } } } - LOG_PRINT_L2("Starting with " << unused_transfers_indices.size() << " non-dust outputs and " << unused_dust_indices.size() << " dust outputs"); - // early out if we know we can't make it anyway - // we could also check for being within FEE_PER_KB, but if the fee calculation - // ever changes, this might be missed, so let this go through - THROW_WALLET_EXCEPTION_IF(needed_money > unlocked_balance(), error::not_enough_money, - unlocked_balance(), needed_money, 0); + // shuffle & sort output indices + { + std::random_device rd; + std::mt19937 g(rd()); + std::shuffle(unused_transfers_indices_per_subaddr.begin(), unused_transfers_indices_per_subaddr.end(), g); + std::shuffle(unused_dust_indices_per_subaddr.begin(), unused_dust_indices_per_subaddr.end(), g); + auto sort_predicate = [&balance_per_subaddr] (const std::pair<uint32_t, std::vector<size_t>>& x, const std::pair<uint32_t, std::vector<size_t>>& y) + { + return balance_per_subaddr[x.first] > balance_per_subaddr[y.first]; + }; + std::sort(unused_transfers_indices_per_subaddr.begin(), unused_transfers_indices_per_subaddr.end(), sort_predicate); + std::sort(unused_dust_indices_per_subaddr.begin(), unused_dust_indices_per_subaddr.end(), sort_predicate); + } - if (unused_dust_indices.empty() && unused_transfers_indices.empty()) + LOG_PRINT_L2("Starting with " << num_nondust_outputs << " non-dust outputs and " << num_dust_outputs << " dust outputs"); + + if (unused_dust_indices_per_subaddr.empty() && unused_transfers_indices_per_subaddr.empty()) return std::vector<wallet2::pending_tx>(); + // if empty, put dummy entry so that the front can be referenced later in the loop + if (unused_dust_indices_per_subaddr.empty()) + unused_dust_indices_per_subaddr.push_back({}); + if (unused_transfers_indices_per_subaddr.empty()) + unused_transfers_indices_per_subaddr.push_back({}); + // start with an empty tx txes.push_back(TX()); accumulated_fee = 0; @@ -4441,12 +4798,31 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp // this is used to build a tx that's 1 or 2 inputs, and 2 outputs, which // will get us a known fee. uint64_t estimated_fee = calculate_fee(fee_per_kb, estimate_rct_tx_size(2, fake_outs_count + 1, 2), fee_multiplier); - preferred_inputs = pick_preferred_rct_inputs(needed_money + estimated_fee); + preferred_inputs = pick_preferred_rct_inputs(needed_money + estimated_fee, subaddr_account, subaddr_indices); if (!preferred_inputs.empty()) { string s; for (auto i: preferred_inputs) s += boost::lexical_cast<std::string>(i) + "(" + print_money(m_transfers[i].amount()) + ") "; - LOG_PRINT_L1("Found preferred rct inputs for rct tx: " << s); + LOG_PRINT_L1("Found prefered rct inputs for rct tx: " << s); + + // bring the list of available outputs stored by the same subaddress index to the front of the list + uint32_t index_minor = m_transfers[preferred_inputs[0]].m_subaddr_index.minor; + for (size_t i = 1; i < unused_transfers_indices_per_subaddr.size(); ++i) + { + if (unused_transfers_indices_per_subaddr[i].first == index_minor) + { + std::swap(unused_transfers_indices_per_subaddr[0], unused_transfers_indices_per_subaddr[i]); + break; + } + } + for (size_t i = 1; i < unused_dust_indices_per_subaddr.size(); ++i) + { + if (unused_dust_indices_per_subaddr[i].first == index_minor) + { + std::swap(unused_dust_indices_per_subaddr[0], unused_dust_indices_per_subaddr[i]); + break; + } + } } } LOG_PRINT_L2("done checking preferred"); @@ -4456,23 +4832,22 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp // - or we need to gather more fee // - or we have just one input in that tx, which is rct (to try and make all/most rct txes 2/2) unsigned int original_output_index = 0; - while ((!dsts.empty() && dsts[0].amount > 0) || adding_fee || should_pick_a_second_output(use_rct, txes.back().selected_transfers.size(), unused_transfers_indices, unused_dust_indices)) { + std::vector<size_t>* unused_transfers_indices = &unused_transfers_indices_per_subaddr[0].second; + std::vector<size_t>* unused_dust_indices = &unused_dust_indices_per_subaddr[0].second; + while ((!dsts.empty() && dsts[0].amount > 0) || adding_fee || should_pick_a_second_output(use_rct, txes.back().selected_transfers.size(), *unused_transfers_indices, *unused_dust_indices)) { TX &tx = txes.back(); - LOG_PRINT_L2("Start of loop with " << unused_transfers_indices.size() << " " << unused_dust_indices.size()); - LOG_PRINT_L2("unused_transfers_indices:"); - for (auto t: unused_transfers_indices) - LOG_PRINT_L2(" " << t); - LOG_PRINT_L2("unused_dust_indices:"); - for (auto t: unused_dust_indices) - LOG_PRINT_L2(" " << t); + LOG_PRINT_L2("Start of loop with " << unused_transfers_indices->size() << " " << unused_dust_indices->size()); + LOG_PRINT_L2("Start of loop with " << unused_transfers_indices->size() << " " << unused_dust_indices->size()); + LOG_PRINT_L2("unused_transfers_indices: " << strjoin(*unused_transfers_indices, " ")); + LOG_PRINT_L2("unused_dust_indices:" << strjoin(*unused_dust_indices, " ")); LOG_PRINT_L2("dsts size " << dsts.size() << ", first " << (dsts.empty() ? -1 : dsts[0].amount)); LOG_PRINT_L2("adding_fee " << adding_fee << ", use_rct " << use_rct); // if we need to spend money and don't have any left, we fail - if (unused_dust_indices.empty() && unused_transfers_indices.empty()) { + if (unused_dust_indices->empty() && unused_transfers_indices->empty()) { LOG_PRINT_L2("No more outputs to choose from"); - THROW_WALLET_EXCEPTION_IF(1, error::tx_not_possible, unlocked_balance(), needed_money, accumulated_fee + needed_fee); + THROW_WALLET_EXCEPTION_IF(1, error::tx_not_possible, unlocked_balance(subaddr_account), needed_money, accumulated_fee + needed_fee); } // get a random unspent output and use it to pay part (or all) of the current destination (and maybe next one, etc) @@ -4480,12 +4855,12 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp size_t idx; if ((dsts.empty() || dsts[0].amount == 0) && !adding_fee) { // the "make rct txes 2/2" case - we pick a small value output to "clean up" the wallet too - std::vector<size_t> indices = get_only_rct(unused_dust_indices, unused_transfers_indices); + std::vector<size_t> indices = get_only_rct(*unused_dust_indices, *unused_transfers_indices); idx = pop_best_value(indices, tx.selected_transfers, true); // we might not want to add it if it's a large output and we don't have many left if (m_transfers[idx].amount() >= m_min_output_value) { - if (get_count_above(m_transfers, unused_transfers_indices, m_min_output_value) < m_min_output_count) { + if (get_count_above(m_transfers, *unused_transfers_indices, m_min_output_value) < m_min_output_count) { LOG_PRINT_L2("Second output was not strictly needed, and we're running out of outputs above " << print_money(m_min_output_value) << ", not adding"); break; } @@ -4499,14 +4874,14 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp LOG_PRINT_L2("Second output was not strictly needed, and relatedness " << relatedness << ", not adding"); break; } - pop_if_present(unused_transfers_indices, idx); - pop_if_present(unused_dust_indices, idx); + pop_if_present(*unused_transfers_indices, idx); + pop_if_present(*unused_dust_indices, idx); } else if (!preferred_inputs.empty()) { idx = pop_back(preferred_inputs); - pop_if_present(unused_transfers_indices, idx); - pop_if_present(unused_dust_indices, idx); + pop_if_present(*unused_transfers_indices, idx); + pop_if_present(*unused_dust_indices, idx); } else - idx = pop_best_value(unused_transfers_indices.empty() ? unused_dust_indices : unused_transfers_indices, tx.selected_transfers); + idx = pop_best_value(unused_transfers_indices->empty() ? *unused_dust_indices : *unused_transfers_indices, tx.selected_transfers); const transfer_details &td = m_transfers[idx]; LOG_PRINT_L2("Picking output " << idx << ", amount " << print_money(td.amount()) << ", ki " << td.m_key_image); @@ -4529,9 +4904,9 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp while (!dsts.empty() && dsts[0].amount <= available_amount && estimate_tx_size(use_rct, tx.selected_transfers.size(), fake_outs_count, tx.dsts.size()) < TX_SIZE_TARGET(upper_transaction_size_limit)) { // we can fully pay that destination - LOG_PRINT_L2("We can fully pay " << get_account_address_as_str(m_testnet, dsts[0].addr) << + LOG_PRINT_L2("We can fully pay " << get_account_address_as_str(m_testnet, dsts[0].is_subaddress, dsts[0].addr) << " for " << print_money(dsts[0].amount)); - tx.add(dsts[0].addr, dsts[0].amount, original_output_index, m_merge_destinations); + tx.add(dsts[0].addr, dsts[0].is_subaddress, dsts[0].amount, original_output_index, m_merge_destinations); available_amount -= dsts[0].amount; dsts[0].amount = 0; pop_index(dsts, 0); @@ -4540,9 +4915,9 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp if (available_amount > 0 && !dsts.empty() && estimate_tx_size(use_rct, tx.selected_transfers.size(), fake_outs_count, tx.dsts.size()) < TX_SIZE_TARGET(upper_transaction_size_limit)) { // we can partially fill that destination - LOG_PRINT_L2("We can partially pay " << get_account_address_as_str(m_testnet, dsts[0].addr) << + LOG_PRINT_L2("We can partially pay " << get_account_address_as_str(m_testnet, dsts[0].is_subaddress, dsts[0].addr) << " for " << print_money(available_amount) << "/" << print_money(dsts[0].amount)); - tx.add(dsts[0].addr, available_amount, original_output_index, m_merge_destinations); + tx.add(dsts[0].addr, dsts[0].is_subaddress, available_amount, original_output_index, m_merge_destinations); dsts[0].amount -= available_amount; available_amount = 0; } @@ -4594,7 +4969,7 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp 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 " << + LOG_PRINT_L2("Adjusting amount paid to " << get_account_address_as_str(m_testnet, i->is_subaddress, i->addr) << " from " << print_money(i->amount) << " to " << print_money(new_paid_amount) << " to accommodate " << print_money(needed_fee) << " fee"); dsts[0].amount += i->amount - new_paid_amount; @@ -4639,15 +5014,32 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp { LOG_PRINT_L2("We have more to pay, starting another tx"); txes.push_back(TX()); + original_output_index = 0; } } } + + // if unused_*_indices is empty while unused_*_indices_per_subaddr has multiple elements, and if we still have something to pay, + // pop front of unused_*_indices_per_subaddr and have unused_*_indices point to the front of unused_*_indices_per_subaddr + if ((!dsts.empty() && dsts[0].amount > 0) || adding_fee) + { + if (unused_transfers_indices->empty() && unused_transfers_indices_per_subaddr.size() > 1) + { + unused_transfers_indices_per_subaddr.erase(unused_transfers_indices_per_subaddr.begin()); + unused_transfers_indices = &unused_transfers_indices_per_subaddr[0].second; + } + if (unused_dust_indices->empty() && unused_dust_indices_per_subaddr.size() > 1) + { + unused_dust_indices_per_subaddr.erase(unused_dust_indices_per_subaddr.begin()); + unused_dust_indices = &unused_dust_indices_per_subaddr[0].second; + } + } } if (adding_fee) { LOG_PRINT_L1("We ran out of outputs while trying to gather final fee"); - THROW_WALLET_EXCEPTION_IF(1, error::tx_not_possible, unlocked_balance(), needed_money, accumulated_fee + needed_fee); + THROW_WALLET_EXCEPTION_IF(1, error::tx_not_possible, unlocked_balance(subaddr_account), needed_money, accumulated_fee + needed_fee); } LOG_PRINT_L1("Done creating " << txes.size() << " transactions, " << print_money(accumulated_fee) << @@ -4671,21 +5063,37 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp return ptx_vector; } -std::vector<wallet2::pending_tx> wallet2::create_transactions_all(uint64_t below, const cryptonote::account_public_address &address, const size_t fake_outs_count, const uint64_t unlock_time, uint32_t priority, const std::vector<uint8_t> extra, bool trusted_daemon) +std::vector<wallet2::pending_tx> wallet2::create_transactions_all(uint64_t below, const cryptonote::account_public_address &address, bool is_subaddress, const size_t fake_outs_count, const uint64_t unlock_time, uint32_t priority, const std::vector<uint8_t>& extra, uint32_t subaddr_account, std::set<uint32_t> subaddr_indices, bool trusted_daemon) { std::vector<size_t> unused_transfers_indices; std::vector<size_t> unused_dust_indices; const bool use_rct = use_fork_rules(4, 0); - // gather all our dust and non dust outputs + THROW_WALLET_EXCEPTION_IF(unlocked_balance(subaddr_account) == 0, error::wallet_internal_error, "No unclocked balance in the entire wallet"); + + std::map<uint32_t, uint64_t> balance_per_subaddr = unlocked_balance_per_subaddress(subaddr_account); + + if (subaddr_indices.empty()) + { + // in case subaddress index wasn't specified, choose non-empty subaddress randomly (with index=0 being chosen last) + if (balance_per_subaddr.count(0) == 1 && balance_per_subaddr.size() > 1) + balance_per_subaddr.erase(0); + auto i = balance_per_subaddr.begin(); + std::advance(i, crypto::rand<size_t>() % balance_per_subaddr.size()); + subaddr_indices.insert(i->first); + } + for (uint32_t i : subaddr_indices) + LOG_PRINT_L2("Spending from subaddress index " << i); + + // gather all dust and non-dust outputs of specified subaddress for (size_t i = 0; i < m_transfers.size(); ++i) { const transfer_details& td = m_transfers[i]; - if (!td.m_spent && (use_rct ? true : !td.is_rct()) && is_transfer_unlocked(td)) + if (!td.m_spent && (use_rct ? true : !td.is_rct()) && is_transfer_unlocked(td) && td.m_subaddr_index.major == subaddr_account && subaddr_indices.count(td.m_subaddr_index.minor) == 1) { if (below == 0 || td.amount() < below) { - if (td.is_rct() || is_valid_decomposed_amount(td.amount())) + if ((td.is_rct()) || is_valid_decomposed_amount(td.amount())) unused_transfers_indices.push_back(i); else unused_dust_indices.push_back(i); @@ -4693,10 +5101,12 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_all(uint64_t below } } - return create_transactions_from(address, unused_transfers_indices, unused_dust_indices, fake_outs_count, unlock_time, priority, extra, trusted_daemon); + THROW_WALLET_EXCEPTION_IF(unused_transfers_indices.empty() && unused_dust_indices.empty(), error::not_enough_money, 0, 0, 0); // not sure if a new error class (something like 'cant_sweep_empty'?) should be introduced + + return create_transactions_from(address, is_subaddress, unused_transfers_indices, unused_dust_indices, fake_outs_count, unlock_time, priority, extra, trusted_daemon); } -std::vector<wallet2::pending_tx> wallet2::create_transactions_from(const cryptonote::account_public_address &address, std::vector<size_t> unused_transfers_indices, std::vector<size_t> unused_dust_indices, const size_t fake_outs_count, const uint64_t unlock_time, uint32_t priority, const std::vector<uint8_t> extra, bool trusted_daemon) +std::vector<wallet2::pending_tx> wallet2::create_transactions_from(const cryptonote::account_public_address &address, bool is_subaddress, std::vector<size_t> unused_transfers_indices, std::vector<size_t> unused_dust_indices, const size_t fake_outs_count, const uint64_t unlock_time, uint32_t priority, const std::vector<uint8_t>& extra, bool trusted_daemon) { uint64_t accumulated_fee, accumulated_outputs, accumulated_change; struct TX { @@ -4759,7 +5169,7 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_from(const crypton needed_fee = 0; - tx.dsts.push_back(tx_destination_entry(1, address)); + tx.dsts.push_back(tx_destination_entry(1, address, is_subaddress)); LOG_PRINT_L2("Trying to create a tx now, with " << tx.dsts.size() << " destinations and " << tx.selected_transfers.size() << " outputs"); @@ -4781,7 +5191,7 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_from(const crypton LOG_PRINT_L2("We made a tx, adjusting fee and saving it"); tx.dsts[0].amount = available_for_fee - needed_fee; if (use_rct) - transfer_selected_rct(tx.dsts, tx.selected_transfers, fake_outs_count, outs, unlock_time, needed_fee, extra, + transfer_selected_rct(tx.dsts, tx.selected_transfers, fake_outs_count, outs, unlock_time, needed_fee, extra, test_tx, test_ptx); else transfer_selected(tx.dsts, tx.selected_transfers, fake_outs_count, outs, unlock_time, needed_fee, extra, @@ -4828,21 +5238,6 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_from(const crypton // 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<transfer_container::iterator> 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; -} //---------------------------------------------------------------------------------------------------- void wallet2::get_hard_fork_info(uint8_t version, uint64_t &earliest_height) { @@ -5017,15 +5412,19 @@ std::vector<wallet2::pending_tx> wallet2::create_unmixable_sweep_transactions(bo unmixable_transfer_outputs.push_back(n); } - return create_transactions_from(m_account_public_address, unmixable_transfer_outputs, unmixable_dust_outputs, 0 /*fake_outs_count */, 0 /* unlock_time */, 1 /*priority */, std::vector<uint8_t>(), trusted_daemon); + return create_transactions_from(m_account_public_address, false, unmixable_transfer_outputs, unmixable_dust_outputs, 0 /*fake_outs_count */, 0 /* unlock_time */, 1 /*priority */, std::vector<uint8_t>(), trusted_daemon); } -bool wallet2::get_tx_key(const crypto::hash &txid, crypto::secret_key &tx_key) const +bool wallet2::get_tx_key(const crypto::hash &txid, crypto::secret_key &tx_key, std::vector<crypto::secret_key> &additional_tx_keys) const { + additional_tx_keys.clear(); const std::unordered_map<crypto::hash, crypto::secret_key>::const_iterator i = m_tx_keys.find(txid); if (i == m_tx_keys.end()) return false; tx_key = i->second; + const auto j = m_additional_tx_keys.find(txid); + if (j != m_additional_tx_keys.end()) + additional_tx_keys = j->second; return true; } @@ -5180,6 +5579,15 @@ crypto::public_key wallet2::get_tx_pub_key_from_received_outs(const tools::walle // more than one, loop and search const cryptonote::account_keys& keys = m_account.get_keys(); size_t pk_index = 0; + + const std::vector<crypto::public_key> additional_tx_pub_keys = get_additional_tx_pub_keys_from_extra(td.m_tx); + std::vector<crypto::key_derivation> additional_derivations; + for (size_t i = 0; i < additional_tx_pub_keys.size(); ++i) + { + additional_derivations.push_back({}); + generate_key_derivation(additional_tx_pub_keys[i], keys.m_view_secret_key, additional_derivations.back()); + } + while (find_tx_extra_field_by_type(tx_extra_fields, pub_key_field, pk_index++)) { const crypto::public_key tx_pub_key = pub_key_field.pub_key; crypto::key_derivation derivation; @@ -5187,10 +5595,9 @@ crypto::public_key wallet2::get_tx_pub_key_from_received_outs(const tools::walle for (size_t i = 0; i < td.m_tx.vout.size(); ++i) { - uint64_t money_transfered = 0; - bool error = false, received = false; - check_acc_out_precomp(keys.m_account_address.m_spend_public_key, td.m_tx.vout[i], derivation, i, received, money_transfered, error); - if (!error && received) + tx_scan_info_t tx_scan_info; + check_acc_out_precomp(td.m_tx.vout[i], derivation, additional_derivations, i, tx_scan_info); + if (!tx_scan_info.error && tx_scan_info.received) return tx_pub_key; } } @@ -5198,10 +5605,10 @@ crypto::public_key wallet2::get_tx_pub_key_from_received_outs(const tools::walle // we found no key yielding an output THROW_WALLET_EXCEPTION_IF(true, error::wallet_internal_error, "Public key yielding at least one output wasn't found in the transaction extra"); - return cryptonote::null_pkey; + return crypto::null_pkey; } -bool wallet2::export_key_images(const std::string filename) +bool wallet2::export_key_images(const std::string &filename) { std::vector<std::pair<crypto::key_image, crypto::signature>> ski = export_key_images(); std::string magic(KEY_IMAGE_EXPORT_FILE_MAGIC, strlen(KEY_IMAGE_EXPORT_FILE_MAGIC)); @@ -5249,11 +5656,13 @@ std::vector<std::pair<crypto::key_image, crypto::signature>> wallet2::export_key } crypto::public_key tx_pub_key = get_tx_pub_key_from_received_outs(td); + const std::vector<crypto::public_key> additional_tx_pub_keys = get_additional_tx_pub_keys_from_extra(td.m_tx); // generate ephemeral secret key crypto::key_image ki; cryptonote::keypair in_ephemeral; - cryptonote::generate_key_image_helper(m_account.get_keys(), tx_pub_key, td.m_internal_output_index, in_ephemeral, ki); + bool r = cryptonote::generate_key_image_helper(m_account.get_keys(), m_subaddresses, pkey, tx_pub_key, additional_tx_pub_keys, td.m_internal_output_index, in_ephemeral, ki); + THROW_WALLET_EXCEPTION_IF(!r, error::wallet_internal_error, "Failed to generate key image"); THROW_WALLET_EXCEPTION_IF(td.m_key_image_known && ki != td.m_key_image, error::wallet_internal_error, "key_image generated not matched with cached key image"); @@ -5404,6 +5813,9 @@ uint64_t wallet2::import_key_images(const std::vector<std::pair<crypto::key_imag } spent = 0; unspent = 0; + std::unordered_set<crypto::hash> spent_txids; // For each spent key image, search for a tx in m_transfers that uses it as input. + std::vector<size_t> swept_transfers; // If such a spending tx wasn't found in m_transfers, this means the spending tx + // was created by sweep_all, so we can't know the spent height and other detailed info. for(size_t i = 0; i < m_transfers.size(); ++i) { transfer_details &td = m_transfers[i]; @@ -5414,8 +5826,158 @@ uint64_t wallet2::import_key_images(const std::vector<std::pair<crypto::key_imag unspent += amount; LOG_PRINT_L2("Transfer " << i << ": " << print_money(amount) << " (" << td.m_global_output_index << "): " << (td.m_spent ? "spent" : "unspent") << " (key image " << req.key_images[i] << ")"); + + if (i < daemon_resp.spent_status.size() && daemon_resp.spent_status[i] == COMMAND_RPC_IS_KEY_IMAGE_SPENT::SPENT_IN_BLOCKCHAIN) + { + bool is_spent_tx_found = false; + for (auto it = m_transfers.rbegin(); &(*it) != &td; ++it) + { + bool is_spent_tx = false; + for(const cryptonote::txin_v& in : it->m_tx.vin) + { + if(in.type() == typeid(cryptonote::txin_to_key) && td.m_key_image == boost::get<cryptonote::txin_to_key>(in).k_image) + { + is_spent_tx = true; + break; + } + } + if (is_spent_tx) + { + is_spent_tx_found = true; + spent_txids.insert(it->m_txid); + break; + } + } + + if (!is_spent_tx_found) + swept_transfers.push_back(i); + } } MDEBUG("Total: " << print_money(spent) << " spent, " << print_money(unspent) << " unspent"); + + if (check_spent) + { + // query outgoing txes + COMMAND_RPC_GET_TRANSACTIONS::request gettxs_req; + COMMAND_RPC_GET_TRANSACTIONS::response gettxs_res; + gettxs_req.decode_as_json = false; + for (const crypto::hash& spent_txid : spent_txids) + gettxs_req.txs_hashes.push_back(epee::string_tools::pod_to_hex(spent_txid)); + m_daemon_rpc_mutex.lock(); + bool r = epee::net_utils::invoke_http_json("/gettransactions", gettxs_req, gettxs_res, m_http_client, rpc_timeout); + m_daemon_rpc_mutex.unlock(); + THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "gettransactions"); + THROW_WALLET_EXCEPTION_IF(gettxs_res.status == CORE_RPC_STATUS_BUSY, error::daemon_busy, "gettransactions"); + THROW_WALLET_EXCEPTION_IF(gettxs_res.txs.size() != spent_txids.size(), error::wallet_internal_error, + "daemon returned wrong response for gettransactions, wrong count = " + std::to_string(gettxs_res.txs.size()) + ", expected " + std::to_string(spent_txids.size())); + + // process each outgoing tx + auto spent_txid = spent_txids.begin(); + for (const COMMAND_RPC_GET_TRANSACTIONS::entry& e : gettxs_res.txs) + { + THROW_WALLET_EXCEPTION_IF(e.in_pool, error::wallet_internal_error, "spent tx isn't supposed to be in txpool"); + + // parse tx + cryptonote::blobdata bd; + THROW_WALLET_EXCEPTION_IF(!epee::string_tools::parse_hexstr_to_binbuff(e.as_hex, bd), error::wallet_internal_error, "parse_hexstr_to_binbuff failed"); + cryptonote::transaction spent_tx; + crypto::hash spnet_txid_parsed, spent_txid_prefix; + THROW_WALLET_EXCEPTION_IF(!cryptonote::parse_and_validate_tx_from_blob(bd, spent_tx, spnet_txid_parsed, spent_txid_prefix), error::wallet_internal_error, "parse_and_validate_tx_from_blob failed"); + THROW_WALLET_EXCEPTION_IF(*spent_txid != spnet_txid_parsed, error::wallet_internal_error, "parsed txid mismatch"); + + // get received (change) amount + uint64_t tx_money_got_in_outs = 0; + const cryptonote::account_keys& keys = m_account.get_keys(); + const crypto::public_key tx_pub_key = get_tx_pub_key_from_extra(spent_tx); + crypto::key_derivation derivation; + generate_key_derivation(tx_pub_key, keys.m_view_secret_key, derivation); + const std::vector<crypto::public_key> additional_tx_pub_keys = get_additional_tx_pub_keys_from_extra(spent_tx); + std::vector<crypto::key_derivation> additional_derivations; + for (size_t i = 0; i < additional_tx_pub_keys.size(); ++i) + { + additional_derivations.push_back({}); + generate_key_derivation(additional_tx_pub_keys[i], keys.m_view_secret_key, additional_derivations.back()); + } + size_t output_index = 0; + for (const cryptonote::tx_out& out : spent_tx.vout) + { + tx_scan_info_t tx_scan_info; + check_acc_out_precomp(out, derivation, additional_derivations, output_index, tx_scan_info); + THROW_WALLET_EXCEPTION_IF(tx_scan_info.error, error::wallet_internal_error, "check_acc_out_precomp failed"); + if (tx_scan_info.received) + { + if (tx_scan_info.money_transfered == 0) + { + rct::key mask; + tx_scan_info.money_transfered = tools::decodeRct(spent_tx.rct_signatures, tx_scan_info.received->derivation, output_index, mask); + } + tx_money_got_in_outs += tx_scan_info.money_transfered; + } + ++output_index; + } + + // get spent amount + uint64_t tx_money_spent_in_ins = 0; + uint32_t subaddr_account = (uint32_t)-1; + std::set<uint32_t> subaddr_indices; + for (const cryptonote::txin_v& in : spent_tx.vin) + { + if (in.type() != typeid(cryptonote::txin_to_key)) + continue; + auto it = m_key_images.find(boost::get<cryptonote::txin_to_key>(in).k_image); + if (it != m_key_images.end()) + { + const transfer_details& td = m_transfers[it->second]; + 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(); + tx_money_spent_in_ins += amount; + + LOG_PRINT_L0("Spent money: " << print_money(amount) << ", with tx: " << *spent_txid); + set_spent(it->second, e.block_height); + if (m_callback) + m_callback->on_money_spent(e.block_height, *spent_txid, spent_tx, amount, spent_tx, td.m_subaddr_index); + if (subaddr_account != (uint32_t)-1 && subaddr_account != td.m_subaddr_index.major) + LOG_PRINT_L0("WARNING: This tx spends outputs received by different subaddress accounts, which isn't supposed to happen"); + subaddr_account = td.m_subaddr_index.major; + subaddr_indices.insert(td.m_subaddr_index.minor); + } + } + + // create outgoing payment + process_outgoing(*spent_txid, spent_tx, e.block_height, e.block_timestamp, tx_money_spent_in_ins, tx_money_got_in_outs, subaddr_account, subaddr_indices); + + // erase corresponding incoming payment + for (auto j = m_payments.begin(); j != m_payments.end(); ++j) + { + if (j->second.m_tx_hash == *spent_txid) + { + m_payments.erase(j); + break; + } + } + + ++spent_txid; + } + + for (size_t n : swept_transfers) + { + const transfer_details& td = m_transfers[n]; + confirmed_transfer_details pd; + pd.m_change = (uint64_t)-1; // cahnge is unknown + pd.m_amount_in = pd.m_amount_out = td.amount(); // fee is unknown + std::string err; + pd.m_block_height = get_daemon_blockchain_height(err); // spent block height is unknown, so hypothetically set to the highest + crypto::hash spent_txid = crypto::rand<crypto::hash>(); // spent txid is unknown, so hypothetically set to random + m_confirmed_txs.insert(std::make_pair(spent_txid, pd)); + } + } + return m_transfers[signed_key_images.size() - 1].m_block_height; } wallet2::payment_container wallet2::export_payments() const @@ -5444,20 +6006,28 @@ void wallet2::import_payments_out(const std::list<std::pair<crypto::hash,wallet2 } } -std::vector<crypto::hash> wallet2::export_blockchain() const +std::tuple<size_t,crypto::hash,std::vector<crypto::hash>> wallet2::export_blockchain() const { - std::vector<crypto::hash> bc; - for (auto const &b : m_blockchain) + std::tuple<size_t, crypto::hash, std::vector<crypto::hash>> bc; + std::get<0>(bc) = m_blockchain.offset(); + std::get<1>(bc) = m_blockchain.empty() ? crypto::null_hash: m_blockchain.genesis(); + for (size_t n = m_blockchain.offset(); n < m_blockchain.size(); ++n) { - bc.push_back(b); + std::get<2>(bc).push_back(m_blockchain[n]); } return bc; } -void wallet2::import_blockchain(const std::vector<crypto::hash> &bc) +void wallet2::import_blockchain(const std::tuple<size_t, crypto::hash, std::vector<crypto::hash>> &bc) { m_blockchain.clear(); - for (auto const &b : bc) + if (std::get<0>(bc)) + { + for (size_t n = std::get<0>(bc); n > 0; ++n) + m_blockchain.push_back(std::get<1>(bc)); + m_blockchain.trim(std::get<0>(bc)); + } + for (auto const &b : std::get<2>(bc)) { m_blockchain.push_back(b); } @@ -5494,14 +6064,17 @@ size_t wallet2::import_outputs(const std::vector<tools::wallet2::transfer_detail // the hot wallet wouldn't have known about key images (except if we already exported them) cryptonote::keypair in_ephemeral; std::vector<tx_extra_field> tx_extra_fields; - tx_extra_pub_key pub_key_field; THROW_WALLET_EXCEPTION_IF(td.m_tx.vout.empty(), error::wallet_internal_error, "tx with no outputs at index " + boost::lexical_cast<std::string>(i)); THROW_WALLET_EXCEPTION_IF(!parse_tx_extra(td.m_tx.extra, tx_extra_fields), error::wallet_internal_error, "Transaction extra has unsupported format at index " + boost::lexical_cast<std::string>(i)); crypto::public_key tx_pub_key = get_tx_pub_key_from_received_outs(td); + const std::vector<crypto::public_key> additional_tx_pub_keys = get_additional_tx_pub_keys_from_extra(td.m_tx); - cryptonote::generate_key_image_helper(m_account.get_keys(), tx_pub_key, td.m_internal_output_index, in_ephemeral, td.m_key_image); + const crypto::public_key& out_key = boost::get<cryptonote::txout_to_key>(td.m_tx.vout[td.m_internal_output_index].target).key; + bool r = cryptonote::generate_key_image_helper(m_account.get_keys(), m_subaddresses, out_key, tx_pub_key, additional_tx_pub_keys, td.m_internal_output_index, in_ephemeral, td.m_key_image); + THROW_WALLET_EXCEPTION_IF(!r, error::wallet_internal_error, "Failed to generate key image"); + expand_subaddresses(td.m_subaddr_index); td.m_key_image_known = true; THROW_WALLET_EXCEPTION_IF(in_ephemeral.pub != boost::get<cryptonote::txout_to_key>(td.m_tx.vout[td.m_internal_output_index].target).key, error::wallet_internal_error, "key_image generated ephemeral public key not matched with output_key at index " + boost::lexical_cast<std::string>(i)); @@ -5559,7 +6132,7 @@ std::string wallet2::decrypt(const std::string &ciphertext, const crypto::secret crypto::secret_key_to_public_key(skey, pkey); const crypto::signature &signature = *(const crypto::signature*)&ciphertext[ciphertext.size() - sizeof(crypto::signature)]; THROW_WALLET_EXCEPTION_IF(!crypto::check_signature(hash, pkey, signature), - error::wallet_internal_error, "Failed to authenticate criphertext"); + error::wallet_internal_error, "Failed to authenticate ciphertext"); } crypto::chacha8(ciphertext.data() + sizeof(iv), ciphertext.size() - prefix_size, key, iv, &plaintext[0]); return plaintext; @@ -5572,17 +6145,15 @@ std::string wallet2::decrypt_with_view_secret_key(const std::string &ciphertext, //---------------------------------------------------------------------------------------------------- std::string wallet2::make_uri(const std::string &address, const std::string &payment_id, uint64_t amount, const std::string &tx_description, const std::string &recipient_name, std::string &error) { - cryptonote::account_public_address tmp_address; - bool has_payment_id; - crypto::hash8 new_payment_id; - if(!get_account_integrated_address_from_str(tmp_address, has_payment_id, new_payment_id, testnet(), address)) + cryptonote::address_parse_info info; + if(!get_account_address_from_str(info, testnet(), address)) { error = std::string("wrong address: ") + address; return std::string(); } // we want only one payment id - if (has_payment_id && !payment_id.empty()) + if (info.has_payment_id && !payment_id.empty()) { error = "A single payment id is allowed"; return std::string(); @@ -5638,10 +6209,8 @@ bool wallet2::parse_uri(const std::string &uri, std::string &address, std::strin const char *ptr = strchr(remainder.c_str(), '?'); address = ptr ? remainder.substr(0, ptr-remainder.c_str()) : remainder; - cryptonote::account_public_address addr; - bool has_payment_id; - crypto::hash8 new_payment_id; - if(!get_account_integrated_address_from_str(addr, has_payment_id, new_payment_id, testnet(), address)) + cryptonote::address_parse_info info; + if(!get_account_address_from_str(info, testnet(), address)) { error = std::string("URI has wrong address: ") + address; return false; @@ -5682,7 +6251,7 @@ bool wallet2::parse_uri(const std::string &uri, std::string &address, std::strin } else if (kv[0] == "tx_payment_id") { - if (has_payment_id) + if (info.has_payment_id) { error = "Separate payment id given with an integrated address"; return false; |