diff options
Diffstat (limited to 'src/wallet/wallet2.cpp')
-rw-r--r-- | src/wallet/wallet2.cpp | 336 |
1 files changed, 238 insertions, 98 deletions
diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp index 6dd7bc0aa..cc0dff999 100644 --- a/src/wallet/wallet2.cpp +++ b/src/wallet/wallet2.cpp @@ -30,12 +30,14 @@ #include <numeric> #include <tuple> +#include <queue> #include <boost/format.hpp> #include <boost/optional/optional.hpp> #include <boost/algorithm/string/classification.hpp> #include <boost/algorithm/string/trim.hpp> #include <boost/algorithm/string/split.hpp> #include <boost/algorithm/string/join.hpp> +#include <boost/algorithm/string/predicate.hpp> #include <boost/asio/ip/address.hpp> #include <boost/range/adaptor/transformed.hpp> #include <boost/preprocessor/stringize.hpp> @@ -142,6 +144,9 @@ using namespace cryptonote; #define IGNORE_LONG_PAYMENT_ID_FROM_BLOCK_VERSION 12 +#define DEFAULT_UNLOCK_TIME (CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE * DIFFICULTY_TARGET_V2) +#define RECENT_SPEND_WINDOW (50 * DIFFICULTY_TARGET_V2) + static const std::string MULTISIG_SIGNATURE_MAGIC = "SigMultisigPkV1"; static const std::string MULTISIG_EXTRA_INFO_MAGIC = "MultisigxV1"; @@ -344,8 +349,6 @@ std::string get_weight_string(const cryptonote::transaction &tx, size_t blob_siz std::unique_ptr<tools::wallet2> make_basic(const boost::program_options::variables_map& vm, bool unattended, const options& opts, const std::function<boost::optional<tools::password_container>(const char *, bool)> &password_prompter) { - namespace ip = boost::asio::ip; - const bool testnet = command_line::get_arg(vm, opts.testnet); const bool stagenet = command_line::get_arg(vm, opts.stagenet); const network_type nettype = testnet ? TESTNET : stagenet ? STAGENET : MAINNET; @@ -367,7 +370,7 @@ std::unique_ptr<tools::wallet2> make_basic(const boost::program_options::variabl // user specified CA file or fingeprints implies enabled SSL by default epee::net_utils::ssl_options_t ssl_options = epee::net_utils::ssl_support_t::e_ssl_support_enabled; - if (command_line::get_arg(vm, opts.daemon_ssl_allow_any_cert)) + if (daemon_ssl_allow_any_cert) ssl_options.verification = epee::net_utils::ssl_verification_t::none; else if (!daemon_ssl_ca_file.empty() || !daemon_ssl_allowed_fingerprints.empty()) { @@ -1004,9 +1007,6 @@ bool get_pruned_tx(const cryptonote::COMMAND_RPC_GET_TRANSACTIONS::entry &entry, namespace tools { -// for now, limit to 30 attempts. TODO: discuss a good number to limit to. -const size_t MAX_SPLIT_ATTEMPTS = 30; - constexpr const std::chrono::seconds wallet2::rpc_timeout; const char* wallet2::tr(const char* str) { return i18n_translate(str, "tools::wallet2"); } @@ -1022,7 +1022,13 @@ gamma_picker::gamma_picker(const std::vector<uint64_t> &rct_offsets, double shap end = rct_offsets.data() + rct_offsets.size() - CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE; num_rct_outputs = *(end - 1); THROW_WALLET_EXCEPTION_IF(num_rct_outputs == 0, error::wallet_internal_error, "No rct outputs"); + THROW_WALLET_EXCEPTION_IF(outputs_to_consider == 0, error::wallet_internal_error, "No rct outputs to consider"); average_output_time = DIFFICULTY_TARGET_V2 * blocks_to_consider / outputs_to_consider; // this assumes constant target over the whole rct range + if (average_output_time == 0) { + // TODO: apply this to all cases; do so alongside a hard fork, where all clients will update at the same time, preventing anonymity puddle formation + average_output_time = DIFFICULTY_TARGET_V2 * blocks_to_consider / static_cast<double>(outputs_to_consider); + } + THROW_WALLET_EXCEPTION_IF(average_output_time == 0, error::wallet_internal_error, "Average seconds per output cannot be 0."); }; gamma_picker::gamma_picker(const std::vector<uint64_t> &rct_offsets): gamma_picker(rct_offsets, GAMMA_SHAPE, GAMMA_SCALE) {} @@ -1031,6 +1037,34 @@ uint64_t gamma_picker::pick() { double x = gamma(engine); x = exp(x); + + if (x > DEFAULT_UNLOCK_TIME) + { + // We are trying to select an output from the chain that appeared 'x' seconds before the + // current chain tip, where 'x' is selected from the gamma distribution recommended in Miller et al. + // (https://arxiv.org/pdf/1704.04299/). + // Our method is to get the average time delta between outputs in the recent past, estimate the number of + // outputs 'n' that would have appeared between 'chain_tip - x' and 'chain_tip', select the real output at + // 'current_num_outputs - n', then randomly select an output from the block where that output appears. + // Source code to paper: https://github.com/maltemoeser/moneropaper + // + // Due to the 'default spendable age' mechanic in Monero, 'current_num_outputs' only contains + // currently *unlocked* outputs, which means the earliest output that can be selected is not at the chain tip! + // Therefore, we must offset 'x' so it matches up with the timing of the outputs being considered. We do + // this by saying if 'x` equals the expected age of the first unlocked output (compared to the current + // chain tip - i.e. DEFAULT_UNLOCK_TIME), then select the first unlocked output. + x -= DEFAULT_UNLOCK_TIME; + } + else + { + // If the spent time suggested by the gamma is less than the unlock time, that means the gamma is suggesting an output + // that is no longer feasible to be spent (possible since the gamma was constructed when consensus rules did not enforce the + // lock time). The assumption made in this code is that an output expected spent quicker than the unlock time would likely + // be spent within RECENT_SPEND_WINDOW after allowed. So it returns an output that falls between 0 and the RECENT_SPEND_WINDOW. + // The RECENT_SPEND_WINDOW was determined with empirical analysis of observed data. + x = crypto::rand_idx(static_cast<uint64_t>(RECENT_SPEND_WINDOW)); + } + uint64_t output_index = x / average_output_time; if (output_index >= num_rct_outputs) return std::numeric_limits<uint64_t>::max(); // bad pick @@ -1216,6 +1250,7 @@ wallet2::wallet2(network_type nettype, uint64_t kdf_rounds, bool unattended, std wallet2::~wallet2() { + deinit(); } bool wallet2::has_testnet_option(const boost::program_options::variables_map& vm) @@ -1323,6 +1358,9 @@ bool wallet2::set_daemon(std::string daemon_address, boost::optional<epee::net_u m_trusted_daemon = trusted_daemon; if (changed) { + if (!m_persistent_rpc_client_id) { + set_rpc_client_secret_key(rct::rct2sk(rct::skGen())); + } m_rpc_payment_state.expected_spent = 0; m_rpc_payment_state.discrepancy = 0; m_node_rpc_proxy.invalidate(); @@ -1602,6 +1640,47 @@ std::string wallet2::get_subaddress_label(const cryptonote::subaddress_index& in return m_subaddress_labels[index.major][index.minor]; } //---------------------------------------------------------------------------------------------------- +void wallet2::scan_tx(const std::vector<crypto::hash> &txids) +{ + // Get the transactions from daemon in batches and add them to a priority queue ordered in chronological order + auto cmp_tx_entry = [](const cryptonote::COMMAND_RPC_GET_TRANSACTIONS::entry& l, const cryptonote::COMMAND_RPC_GET_TRANSACTIONS::entry& r) + { return l.block_height > r.block_height; }; + + std::priority_queue<cryptonote::COMMAND_RPC_GET_TRANSACTIONS::entry, std::vector<COMMAND_RPC_GET_TRANSACTIONS::entry>, decltype(cmp_tx_entry)> txq(cmp_tx_entry); + const size_t SLICE_SIZE = 100; // RESTRICTED_TRANSACTIONS_COUNT as defined in rpc/core_rpc_server.cpp, hardcoded in daemon code + for(size_t slice = 0; slice < txids.size(); slice += SLICE_SIZE) { + cryptonote::COMMAND_RPC_GET_TRANSACTIONS::request req = AUTO_VAL_INIT(req); + cryptonote::COMMAND_RPC_GET_TRANSACTIONS::response res = AUTO_VAL_INIT(res); + req.decode_as_json = false; + req.prune = true; + + size_t ntxes = slice + SLICE_SIZE > txids.size() ? txids.size() - slice : SLICE_SIZE; + for (size_t i = slice; i < slice + ntxes; ++i) + req.txs_hashes.push_back(epee::string_tools::pod_to_hex(txids[i])); + + { + const boost::lock_guard<boost::recursive_mutex> lock{m_daemon_rpc_mutex}; + req.client = get_client_signature(); + bool r = epee::net_utils::invoke_http_json("/gettransactions", req, res, *m_http_client, rpc_timeout); + THROW_WALLET_EXCEPTION_IF(!r, error::wallet_internal_error, "Failed to get transaction from daemon"); + THROW_WALLET_EXCEPTION_IF(res.txs.size() != req.txs_hashes.size(), error::wallet_internal_error, "Failed to get transaction from daemon"); + } + + for (auto& tx_info : res.txs) + txq.push(tx_info); + } + + // Process the transactions in chronologically ascending order + while(!txq.empty()) { + auto& tx_info = txq.top(); + cryptonote::transaction tx; + crypto::hash tx_hash; + THROW_WALLET_EXCEPTION_IF(!get_pruned_tx(tx_info, tx, tx_hash), error::wallet_internal_error, "Failed to get transaction from daemon (2)"); + process_new_transaction(tx_hash, tx, tx_info.output_indices, tx_info.block_height, 0, tx_info.block_timestamp, false, tx_info.in_pool, tx_info.double_spend_seen, {}, {}); + txq.pop(); + } +} +//---------------------------------------------------------------------------------------------------- void wallet2::set_subaddress_label(const cryptonote::subaddress_index& index, const std::string &label) { THROW_WALLET_EXCEPTION_IF(index.major >= m_subaddress_labels.size(), error::account_index_outofbound); @@ -1859,7 +1938,7 @@ void wallet2::cache_tx_data(const cryptonote::transaction& tx, const crypto::has const bool is_miner = tx.vin.size() == 1 && tx.vin[0].type() == typeid(cryptonote::txin_gen); if (!is_miner || m_refresh_type != RefreshType::RefreshNoCoinbase) { - const size_t rec_size = is_miner && m_refresh_type == RefreshType::RefreshOptimizeCoinbase ? 1 : tx.vout.size(); + const size_t rec_size = (is_miner && m_refresh_type == RefreshType::RefreshOptimizeCoinbase && tx.version < 2) ? 1 : tx.vout.size(); if (!tx.vout.empty()) { // if tx.vout is not empty, we loop through all tx pubkeys @@ -2008,7 +2087,7 @@ void wallet2::process_new_transaction(const crypto::hash &txid, const cryptonote { // assume coinbase isn't for us } - else if (miner_tx && m_refresh_type == RefreshOptimizeCoinbase) + else if (miner_tx && m_refresh_type == RefreshOptimizeCoinbase && tx.version < 2) { check_acc_out_precomp_once(tx.vout[0], derivation, additional_derivations, 0, is_out_data_ptr, tx_scan_info[0], output_found[0]); THROW_WALLET_EXCEPTION_IF(tx_scan_info[0].error, error::acc_outs_lookup_error, tx, tx_pub_key, m_account.get_keys()); @@ -2735,9 +2814,8 @@ void wallet2::process_parsed_blocks(uint64_t start_height, const std::vector<cry { if (tx_cache_data[i].empty()) continue; - tpool.submit(&waiter, [&hwdev, &gender, &tx_cache_data, i]() { + tpool.submit(&waiter, [&gender, &tx_cache_data, i]() { auto &slot = tx_cache_data[i]; - boost::unique_lock<hw::device> hwdev_lock(hwdev); for (auto &iod: slot.primary) gender(iod); for (auto &iod: slot.additional) @@ -2780,8 +2858,9 @@ void wallet2::process_parsed_blocks(uint64_t start_height, const std::vector<cry if (m_refresh_type != RefreshType::RefreshNoCoinbase) { THROW_WALLET_EXCEPTION_IF(txidx >= tx_cache_data.size(), error::wallet_internal_error, "txidx out of range"); - const size_t n_vouts = m_refresh_type == RefreshType::RefreshOptimizeCoinbase ? 1 : parsed_blocks[i].block.miner_tx.vout.size(); - tpool.submit(&waiter, [&, i, n_vouts, txidx](){ geniod(parsed_blocks[i].block.miner_tx, n_vouts, txidx); }, true); + const cryptonote::transaction& tx = parsed_blocks[i].block.miner_tx; + const size_t n_vouts = (m_refresh_type == RefreshType::RefreshOptimizeCoinbase && tx.version < 2) ? 1 : tx.vout.size(); + tpool.submit(&waiter, [&, i, n_vouts, txidx](){ geniod(tx, n_vouts, txidx); }, true); } ++txidx; for (size_t j = 0; j < parsed_blocks[i].txes.size(); ++j) @@ -3453,14 +3532,6 @@ void wallet2::refresh(bool trusted_daemon, uint64_t start_height, uint64_t & blo blocks_fetched += added_blocks; } THROW_WALLET_EXCEPTION_IF(!waiter.wait(), error::wallet_internal_error, "Exception in thread pool"); - if(!first && blocks_start_height == next_blocks_start_height) - { - m_node_rpc_proxy.set_height(m_blockchain.size()); - refreshed = true; - break; - } - - first = false; // handle error from async fetching thread if (error) @@ -3471,6 +3542,15 @@ void wallet2::refresh(bool trusted_daemon, uint64_t start_height, uint64_t & blo throw std::runtime_error("proxy exception in refresh thread"); } + if(!first && blocks_start_height == next_blocks_start_height) + { + m_node_rpc_proxy.set_height(m_blockchain.size()); + refreshed = true; + break; + } + + first = false; + if (!next_blocks.empty()) { const uint64_t expected_start_height = std::max(static_cast<uint64_t>(m_blockchain.size()), uint64_t(1)) - 1; @@ -3707,9 +3787,11 @@ void wallet2::detach_blockchain(uint64_t height, std::map<std::pair<uint64_t, ui //---------------------------------------------------------------------------------------------------- bool wallet2::deinit() { - m_is_initialized=false; - unlock_keys_file(); - m_account.deinit(); + if(m_is_initialized) { + m_is_initialized = false; + unlock_keys_file(); + m_account.deinit(); + } return true; } //---------------------------------------------------------------------------------------------------- @@ -4384,7 +4466,7 @@ bool wallet2::load_keys_buf(const std::string& keys_buf, const epee::wipeable_st account_public_address device_account_public_address; THROW_WALLET_EXCEPTION_IF(!hwdev.get_public_address(device_account_public_address), error::wallet_internal_error, "Cannot get a device address"); - THROW_WALLET_EXCEPTION_IF(device_account_public_address != m_account.get_keys().m_account_address, error::wallet_internal_error, "Device wallet does not match wallet address. " + THROW_WALLET_EXCEPTION_IF(device_account_public_address != m_account.get_keys().m_account_address, error::wallet_internal_error, "Device wallet does not match wallet address. If the device uses the passphrase feature, please check whether the passphrase was entered correctly (it may have been misspelled - different passphrases generate different wallets, passphrase is case-sensitive). " "Device address: " + cryptonote::get_account_address_as_str(m_nettype, false, device_account_public_address) + ", wallet address: " + m_account.get_public_address_str(m_nettype)); LOG_PRINT_L0("Device inited..."); @@ -5663,12 +5745,18 @@ void wallet2::load(const std::string& wallet_, const epee::wipeable_string& pass try { - std::stringstream iss; - iss << cache_data; - binary_archive<false> ar(iss); + binary_archive<false> ar{epee::strspan<std::uint8_t>(cache_data)}; if (::serialization::serialize(ar, *this)) if (::serialization::check_stream_state(ar)) loaded = true; + if (!loaded) + { + binary_archive<false> ar{epee::strspan<std::uint8_t>(cache_data)}; + ar.enable_varint_bug_backward_compatibility(); + if (::serialization::serialize(ar, *this)) + if (::serialization::check_stream_state(ar)) + loaded = true; + } } catch(...) { } @@ -5988,7 +6076,7 @@ uint64_t wallet2::balance(uint32_t index_major, bool strict) const { uint64_t amount = 0; if(m_light_wallet) - return m_light_wallet_unlocked_balance; + return m_light_wallet_balance; for (const auto& i : balance_per_subaddress(index_major, strict)) amount += i.second; return amount; @@ -6002,7 +6090,7 @@ uint64_t wallet2::unlocked_balance(uint32_t index_major, bool strict, uint64_t * if (time_to_unlock) *time_to_unlock = 0; if(m_light_wallet) - return m_light_wallet_balance; + return m_light_wallet_unlocked_balance; for (const auto& i : unlocked_balance_per_subaddress(index_major, strict)) { amount += i.second.first; @@ -6042,6 +6130,14 @@ std::map<uint32_t, uint64_t> wallet2::balance_per_subaddress(uint32_t index_majo found->second += utx.second.m_change; } } + + for (const auto& utx: m_unconfirmed_payments) + { + if (utx.second.m_pd.m_subaddr_index.major == index_major) + { + amount_per_subaddr[utx.second.m_pd.m_subaddr_index.minor] += utx.second.m_pd.m_amount; + } + } } return amount_per_subaddr; } @@ -6730,8 +6826,7 @@ bool wallet2::parse_unsigned_tx_from_str(const std::string &unsigned_tx_st, unsi catch(const std::exception &e) { LOG_PRINT_L0("Failed to decrypt unsigned tx: " << e.what()); return false; } try { - std::istringstream iss(s); - binary_archive<false> ar(iss); + binary_archive<false> ar{epee::strspan<std::uint8_t>(s)}; if (!::serialization::serialize(ar, exported_txs)) { LOG_PRINT_L0("Failed to parse data from unsigned tx"); @@ -7045,8 +7140,7 @@ bool wallet2::parse_tx_from_str(const std::string &signed_tx_st, std::vector<too catch (const std::exception &e) { LOG_PRINT_L0("Failed to decrypt signed transaction: " << e.what()); return false; } try { - std::istringstream iss(s); - binary_archive<false> ar(iss); + binary_archive<false> ar{epee::strspan<std::uint8_t>(s)}; if (!::serialization::serialize(ar, signed_txs)) { LOG_PRINT_L0("Failed to deserialize signed transaction"); @@ -7181,8 +7275,7 @@ bool wallet2::parse_multisig_tx_from_str(std::string multisig_tx_st, multisig_tx bool loaded = false; try { - std::istringstream iss(multisig_tx_st); - binary_archive<false> ar(iss); + binary_archive<false> ar{epee::strspan<std::uint8_t>(multisig_tx_st)}; if (::serialization::serialize(ar, exported_txs)) if (::serialization::check_stream_state(ar)) loaded = true; @@ -7984,7 +8077,6 @@ void wallet2::light_wallet_get_outs(std::vector<std::vector<tools::wallet2::get_ // Check if we got enough outputs for each amount for(auto& out: ores.amount_outs) { - const uint64_t out_amount = boost::lexical_cast<uint64_t>(out.amount); THROW_WALLET_EXCEPTION_IF(out.outputs.size() < light_wallet_requested_outputs_count , error::wallet_internal_error, "Not enough outputs for amount: " + boost::lexical_cast<std::string>(out.amount)); MDEBUG(out.outputs.size() << " outputs for amount "+ boost::lexical_cast<std::string>(out.amount) + " received from light wallet node"); } @@ -8539,18 +8631,30 @@ void wallet2::get_outs(std::vector<std::vector<tools::wallet2::get_outs_entry>> } // get the keys for those - req.get_txid = false; - + // the response can get large and end up rejected by the anti DoS limits, so chunk it if needed + size_t offset = 0; + while (offset < req.outputs.size()) { + static const size_t chunk_size = 1000; + COMMAND_RPC_GET_OUTPUTS_BIN::request chunk_req = AUTO_VAL_INIT(chunk_req); + COMMAND_RPC_GET_OUTPUTS_BIN::response chunk_daemon_resp = AUTO_VAL_INIT(chunk_daemon_resp); + chunk_req.get_txid = false; + for (size_t i = 0; i < std::min<size_t>(req.outputs.size() - offset, chunk_size); ++i) + chunk_req.outputs.push_back(req.outputs[offset + i]); + const boost::lock_guard<boost::recursive_mutex> lock{m_daemon_rpc_mutex}; uint64_t pre_call_credits = m_rpc_payment_state.credits; - req.client = get_client_signature(); - bool r = epee::net_utils::invoke_http_bin("/get_outs.bin", req, daemon_resp, *m_http_client, rpc_timeout); - THROW_ON_RPC_RESPONSE_ERROR(r, {}, daemon_resp, "get_outs.bin", error::get_outs_error, get_rpc_status(daemon_resp.status)); - THROW_WALLET_EXCEPTION_IF(daemon_resp.outs.size() != req.outputs.size(), error::wallet_internal_error, + chunk_req.client = get_client_signature(); + bool r = epee::net_utils::invoke_http_bin("/get_outs.bin", chunk_req, chunk_daemon_resp, *m_http_client, rpc_timeout); + THROW_ON_RPC_RESPONSE_ERROR(r, {}, chunk_daemon_resp, "get_outs.bin", error::get_outs_error, get_rpc_status(chunk_daemon_resp.status)); + THROW_WALLET_EXCEPTION_IF(chunk_daemon_resp.outs.size() != chunk_req.outputs.size(), error::wallet_internal_error, "daemon returned wrong response for get_outs.bin, wrong amounts count = " + - std::to_string(daemon_resp.outs.size()) + ", expected " + std::to_string(req.outputs.size())); - check_rpc_cost("/get_outs.bin", daemon_resp.credits, pre_call_credits, daemon_resp.outs.size() * COST_PER_OUT); + std::to_string(chunk_daemon_resp.outs.size()) + ", expected " + std::to_string(chunk_req.outputs.size())); + check_rpc_cost("/get_outs.bin", chunk_daemon_resp.credits, pre_call_credits, chunk_daemon_resp.outs.size() * COST_PER_OUT); + + offset += chunk_size; + for (size_t i = 0; i < chunk_daemon_resp.outs.size(); ++i) + daemon_resp.outs.push_back(std::move(chunk_daemon_resp.outs[i])); } std::unordered_map<uint64_t, uint64_t> scanty_outs; @@ -8593,7 +8697,8 @@ void wallet2::get_outs(std::vector<std::vector<tools::wallet2::get_outs_entry>> if (req.outputs[i].index == td.m_global_output_index) if (daemon_resp.outs[i].key == boost::get<txout_to_key>(td.m_tx.vout[td.m_internal_output_index].target).key) if (daemon_resp.outs[i].mask == mask) - real_out_found = true; + if (daemon_resp.outs[i].unlocked) + real_out_found = true; } THROW_WALLET_EXCEPTION_IF(!real_out_found, error::wallet_internal_error, "Daemon response did not include the requested real output"); @@ -8706,7 +8811,7 @@ void wallet2::transfer_selected(const std::vector<cryptonote::tx_destination_ent // throw if total amount overflows uint64_t for(auto& dt: dsts) { - THROW_WALLET_EXCEPTION_IF(0 == dt.amount, error::zero_destination); + THROW_WALLET_EXCEPTION_IF(0 == dt.amount, error::zero_amount); needed_money += dt.amount; LOG_PRINT_L2("transfer: adding " << print_money(dt.amount) << ", for a total of " << print_money (needed_money)); THROW_WALLET_EXCEPTION_IF(needed_money < dt.amount, error::tx_sum_overflow, dsts, fee, m_nettype); @@ -8865,7 +8970,7 @@ void wallet2::transfer_selected_rct(std::vector<cryptonote::tx_destination_entry // throw if total amount overflows uint64_t for(auto& dt: dsts) { - THROW_WALLET_EXCEPTION_IF(0 == dt.amount, error::zero_destination); + THROW_WALLET_EXCEPTION_IF(0 == dt.amount, error::zero_amount); needed_money += dt.amount; LOG_PRINT_L2("transfer: adding " << print_money(dt.amount) << ", for a total of " << print_money (needed_money)); THROW_WALLET_EXCEPTION_IF(needed_money < dt.amount, error::tx_sum_overflow, dsts, fee, m_nettype); @@ -9778,13 +9883,18 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp TX() : weight(0), needed_fee(0) {} - void add(const cryptonote::tx_destination_entry &de, uint64_t amount, unsigned int original_output_index, bool merge_destinations) { + /* Add an output to the transaction. + * Returns True if the output was added, False if there are no more available output slots. + */ + bool add(const cryptonote::tx_destination_entry &de, uint64_t amount, unsigned int original_output_index, bool merge_destinations, size_t max_dsts) { 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, &de.addr, sizeof(de.addr)); }); if (i == dsts.end()) { + if (dsts.size() >= max_dsts) + return false; dsts.push_back(de); i = dsts.end() - 1; i->amount = 0; @@ -9797,12 +9907,15 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp std::string("original_output_index too large: ") + std::to_string(original_output_index) + " > " + std::to_string(dsts.size())); if (original_output_index == dsts.size()) { + if (dsts.size() >= max_dsts) + return false; dsts.push_back(de); dsts.back().amount = 0; } THROW_WALLET_EXCEPTION_IF(memcmp(&dsts[original_output_index].addr, &de.addr, sizeof(de.addr)), error::wallet_internal_error, "Mismatched destination address"); dsts[original_output_index].amount += amount; } + return true; } }; std::vector<TX> txes; @@ -9830,14 +9943,14 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp needed_money = 0; for(auto& dt: dsts) { - THROW_WALLET_EXCEPTION_IF(0 == dt.amount, error::zero_destination); + THROW_WALLET_EXCEPTION_IF(0 == dt.amount, error::zero_amount); needed_money += dt.amount; LOG_PRINT_L2("transfer: adding " << print_money(dt.amount) << ", for a total of " << print_money (needed_money)); THROW_WALLET_EXCEPTION_IF(needed_money < dt.amount, error::tx_sum_overflow, dsts, 0, m_nettype); } // throw if attempting a transaction with no money - THROW_WALLET_EXCEPTION_IF(needed_money == 0, error::zero_destination); + THROW_WALLET_EXCEPTION_IF(needed_money == 0, error::zero_amount); std::map<uint32_t, std::pair<uint64_t, std::pair<uint64_t, uint64_t>>> unlocked_balance_per_subaddr = unlocked_balance_per_subaddress(subaddr_account, false); std::map<uint32_t, uint64_t> balance_per_subaddr = balance_per_subaddress(subaddr_account, false); @@ -10072,6 +10185,7 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp // clear any fake outs we'd already gathered, since we'll need a new set outs.clear(); + bool out_slots_exhausted = false; if (adding_fee) { LOG_PRINT_L2("We need more fee, adding it to fee"); @@ -10084,20 +10198,32 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp // we can fully pay that destination LOG_PRINT_L2("We can fully pay " << get_account_address_as_str(m_nettype, dsts[0].is_subaddress, dsts[0].addr) << " for " << print_money(dsts[0].amount)); - tx.add(dsts[0], dsts[0].amount, original_output_index, m_merge_destinations); + if (!tx.add(dsts[0], dsts[0].amount, original_output_index, m_merge_destinations, BULLETPROOF_MAX_OUTPUTS-1)) + { + LOG_PRINT_L2("Didn't pay: ran out of output slots"); + out_slots_exhausted = true; + break; + } available_amount -= dsts[0].amount; dsts[0].amount = 0; pop_index(dsts, 0); ++original_output_index; } - if (available_amount > 0 && !dsts.empty() && estimate_tx_weight(use_rct, tx.selected_transfers.size(), fake_outs_count, tx.dsts.size()+1, extra.size(), bulletproof, clsag) < TX_WEIGHT_TARGET(upper_transaction_weight_limit)) { + if (!out_slots_exhausted && available_amount > 0 && !dsts.empty() && estimate_tx_weight(use_rct, tx.selected_transfers.size(), fake_outs_count, tx.dsts.size()+1, extra.size(), bulletproof, clsag) < TX_WEIGHT_TARGET(upper_transaction_weight_limit)) { // we can partially fill that destination LOG_PRINT_L2("We can partially pay " << get_account_address_as_str(m_nettype, dsts[0].is_subaddress, dsts[0].addr) << " for " << print_money(available_amount) << "/" << print_money(dsts[0].amount)); - tx.add(dsts[0], available_amount, original_output_index, m_merge_destinations); - dsts[0].amount -= available_amount; - available_amount = 0; + if (tx.add(dsts[0], available_amount, original_output_index, m_merge_destinations, BULLETPROOF_MAX_OUTPUTS-1)) + { + dsts[0].amount -= available_amount; + available_amount = 0; + } + else + { + LOG_PRINT_L2("Didn't pay: ran out of output slots"); + out_slots_exhausted = true; + } } } @@ -10105,8 +10231,15 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp LOG_PRINT_L2("Considering whether to create a tx now, " << tx.selected_transfers.size() << " inputs, tx limit " << upper_transaction_weight_limit); bool try_tx = false; + + // If the new transaction is full, create it and start a new one + if (out_slots_exhausted) + { + LOG_PRINT_L2("Transaction is full, will create it and start a new tx"); + try_tx = true; + } // if we have preferred picks, but haven't yet used all of them, continue - if (preferred_inputs.empty()) + else if (preferred_inputs.empty()) { if (adding_fee) { @@ -10317,8 +10450,6 @@ bool wallet2::sanity_check(const std::vector<wallet2::pending_tx> &ptx_vector, s { MDEBUG("sanity_check: " << ptx_vector.size() << " txes, " << dsts.size() << " destinations"); - hw::device &hwdev = m_account.get_device(); - THROW_WALLET_EXCEPTION_IF(ptx_vector.empty(), error::wallet_internal_error, "No transactions"); // check every party in there does receive at least the required amount @@ -10355,7 +10486,6 @@ bool wallet2::sanity_check(const std::vector<wallet2::pending_tx> &ptx_vector, s for (const auto &r: required) { const account_public_address &address = r.first; - const crypto::public_key &view_pkey = address.m_view_public_key; uint64_t total_received = 0; for (const auto &ptx: ptx_vector) @@ -11450,6 +11580,9 @@ void wallet2::check_tx_key_helper(const cryptonote::transaction &tx, const crypt void wallet2::check_tx_key_helper(const crypto::hash &txid, const crypto::key_derivation &derivation, const std::vector<crypto::key_derivation> &additional_derivations, const cryptonote::account_public_address &address, uint64_t &received, bool &in_pool, uint64_t &confirmations) { + uint32_t rpc_version; + THROW_WALLET_EXCEPTION_IF(!check_connection(&rpc_version), error::wallet_internal_error, "Failed to connect to daemon: " + get_daemon_address()); + COMMAND_RPC_GET_TRANSACTIONS::request req; COMMAND_RPC_GET_TRANSACTIONS::response res; req.txs_hashes.push_back(epee::string_tools::pod_to_hex(txid)); @@ -11495,10 +11628,17 @@ void wallet2::check_tx_key_helper(const crypto::hash &txid, const crypto::key_de confirmations = 0; if (!in_pool) { - std::string err; - uint64_t bc_height = get_daemon_blockchain_height(err); - if (err.empty()) - confirmations = bc_height - res.txs.front().block_height; + if (rpc_version < MAKE_CORE_RPC_VERSION(3, 7)) + { + std::string err; + uint64_t bc_height = get_daemon_blockchain_height(err); + if (err.empty() && bc_height > res.txs.front().block_height) + confirmations = bc_height - res.txs.front().block_height; + } + else + { + confirmations = res.txs.front().confirmations; + } } } @@ -11961,8 +12101,7 @@ bool wallet2::check_reserve_proof(const cryptonote::account_public_address &addr serializable_unordered_map<crypto::public_key, crypto::signature> subaddr_spendkeys; try { - std::istringstream iss(sig_decoded); - binary_archive<false> ar(iss); + binary_archive<false> ar{epee::strspan<std::uint8_t>(sig_decoded)}; if (::serialization::serialize_noeof(ar, proofs)) if (::serialization::serialize_noeof(ar, subaddr_spendkeys)) if (::serialization::check_stream_state(ar)) @@ -12162,7 +12301,7 @@ uint64_t wallet2::get_approximate_blockchain_height() const // Calculated blockchain height uint64_t approx_blockchain_height = fork_block + (time(NULL) - fork_time)/seconds_per_block; // testnet got some huge rollbacks, so the estimation is way off - static const uint64_t approximate_testnet_rolled_back_blocks = 303967; + static const uint64_t approximate_testnet_rolled_back_blocks = 342100; if (m_nettype == TESTNET && approx_blockchain_height > approximate_testnet_rolled_back_blocks) approx_blockchain_height -= approximate_testnet_rolled_back_blocks; LOG_PRINT_L2("Calculated blockchain height: " << approx_blockchain_height); @@ -12473,7 +12612,7 @@ crypto::public_key wallet2::get_tx_pub_key_from_received_outs(const tools::walle bool wallet2::export_key_images(const std::string &filename, bool all) const { PERF_TIMER(export_key_images); - std::pair<size_t, std::vector<std::pair<crypto::key_image, crypto::signature>>> ski = export_key_images(all); + std::pair<uint64_t, std::vector<std::pair<crypto::key_image, crypto::signature>>> ski = export_key_images(all); std::string magic(KEY_IMAGE_EXPORT_FILE_MAGIC, strlen(KEY_IMAGE_EXPORT_FILE_MAGIC)); const cryptonote::account_public_address &keys = get_account().get_keys().m_account_address; const uint32_t offset = ski.first; @@ -12500,7 +12639,7 @@ bool wallet2::export_key_images(const std::string &filename, bool all) const } //---------------------------------------------------------------------------------------------------- -std::pair<size_t, std::vector<std::pair<crypto::key_image, crypto::signature>>> wallet2::export_key_images(bool all) const +std::pair<uint64_t, std::vector<std::pair<crypto::key_image, crypto::signature>>> wallet2::export_key_images(bool all) const { PERF_TIMER(export_key_images_raw); std::vector<std::pair<crypto::key_image, crypto::signature>> ski; @@ -12997,7 +13136,7 @@ void wallet2::import_blockchain(const std::tuple<size_t, crypto::hash, std::vect m_last_block_reward = cryptonote::get_outs_money_amount(genesis.miner_tx); } //---------------------------------------------------------------------------------------------------- -std::pair<size_t, std::vector<tools::wallet2::transfer_details>> wallet2::export_outputs(bool all) const +std::pair<uint64_t, std::vector<tools::wallet2::transfer_details>> wallet2::export_outputs(bool all) const { PERF_TIMER(export_outputs); std::vector<tools::wallet2::transfer_details> outs; @@ -13037,7 +13176,7 @@ std::string wallet2::export_outputs_to_str(bool all) const return magic + ciphertext; } //---------------------------------------------------------------------------------------------------- -size_t wallet2::import_outputs(const std::pair<size_t, std::vector<tools::wallet2::transfer_details>> &outputs) +size_t wallet2::import_outputs(const std::pair<uint64_t, std::vector<tools::wallet2::transfer_details>> &outputs) { PERF_TIMER(import_outputs); @@ -13143,12 +13282,10 @@ size_t wallet2::import_outputs_from_str(const std::string &outputs_st) try { std::string body(data, headerlen); - std::pair<size_t, std::vector<tools::wallet2::transfer_details>> outputs; + std::pair<uint64_t, std::vector<tools::wallet2::transfer_details>> outputs; try { - std::stringstream iss; - iss << body; - binary_archive<false> ar(iss); + binary_archive<false> ar{epee::strspan<std::uint8_t>(body)}; if (::serialization::serialize(ar, outputs)) if (::serialization::check_stream_state(ar)) loaded = true; @@ -13243,7 +13380,6 @@ rct::multisig_kLRki wallet2::get_multisig_composite_kLRki(size_t n, const std::u { CHECK_AND_ASSERT_THROW_MES(n < m_transfers.size(), "Bad transfer index"); - const transfer_details &td = m_transfers[n]; rct::multisig_kLRki kLRki = get_multisig_kLRki(n, rct::skGen()); // pick a L/R pair from every other participant but one @@ -13401,8 +13537,7 @@ size_t wallet2::import_multisig(std::vector<cryptonote::blobdata> blobs) bool loaded = false; try { - std::istringstream iss(body); - binary_archive<false> ar(iss); + binary_archive<false> ar{epee::strspan<std::uint8_t>(body)}; if (::serialization::serialize(ar, i)) if (::serialization::check_stream_state(ar)) loaded = true; @@ -14124,15 +14259,15 @@ void wallet2::hash_m_transfer(const transfer_details & transfer, crypto::hash &h KECCAK_CTX state; keccak_init(&state); keccak_update(&state, (const uint8_t *) transfer.m_txid.data, sizeof(transfer.m_txid.data)); - keccak_update(&state, (const uint8_t *) transfer.m_internal_output_index, sizeof(transfer.m_internal_output_index)); - keccak_update(&state, (const uint8_t *) transfer.m_global_output_index, sizeof(transfer.m_global_output_index)); - keccak_update(&state, (const uint8_t *) transfer.m_amount, sizeof(transfer.m_amount)); + keccak_update(&state, (const uint8_t *) &transfer.m_internal_output_index, sizeof(transfer.m_internal_output_index)); + keccak_update(&state, (const uint8_t *) &transfer.m_global_output_index, sizeof(transfer.m_global_output_index)); + keccak_update(&state, (const uint8_t *) &transfer.m_amount, sizeof(transfer.m_amount)); keccak_finish(&state, (uint8_t *) hash.data); } //---------------------------------------------------------------------------------------------------- -uint64_t wallet2::hash_m_transfers(int64_t transfer_height, crypto::hash &hash) const +uint64_t wallet2::hash_m_transfers(boost::optional<uint64_t> transfer_height, crypto::hash &hash) const { - CHECK_AND_ASSERT_THROW_MES(transfer_height > (int64_t)m_transfers.size(), "Hash height is greater than number of transfers"); + CHECK_AND_ASSERT_THROW_MES(!transfer_height || *transfer_height <= m_transfers.size(), "Hash height is greater than number of transfers"); KECCAK_CTX state; crypto::hash tmp_hash{}; @@ -14140,12 +14275,12 @@ uint64_t wallet2::hash_m_transfers(int64_t transfer_height, crypto::hash &hash) keccak_init(&state); for(const transfer_details & transfer : m_transfers){ - if (transfer_height >= 0 && current_height >= (uint64_t)transfer_height){ + if (transfer_height && current_height >= *transfer_height){ break; } hash_m_transfer(transfer, tmp_hash); - keccak_update(&state, (const uint8_t *) transfer.m_block_height, sizeof(transfer.m_block_height)); + keccak_update(&state, (const uint8_t *) &transfer.m_block_height, sizeof(transfer.m_block_height)); keccak_update(&state, (const uint8_t *) tmp_hash.data, sizeof(tmp_hash.data)); current_height += 1; } @@ -14157,23 +14292,28 @@ uint64_t wallet2::hash_m_transfers(int64_t transfer_height, crypto::hash &hash) void wallet2::finish_rescan_bc_keep_key_images(uint64_t transfer_height, const crypto::hash &hash) { // Compute hash of m_transfers, if differs there had to be BC reorg. - crypto::hash new_transfers_hash{}; - hash_m_transfers((int64_t) transfer_height, new_transfers_hash); + if (transfer_height <= m_transfers.size()) { + crypto::hash new_transfers_hash{}; + hash_m_transfers(transfer_height, new_transfers_hash); - if (new_transfers_hash != hash) - { - // Soft-Reset to avoid inconsistency in case of BC reorg. - clear_soft(false); // keep_key_images works only with soft reset. - THROW_WALLET_EXCEPTION_IF(true, error::wallet_internal_error, "Transfers changed during rescan, soft or hard rescan is needed"); - } + if (new_transfers_hash == hash) { + // Restore key images in m_transfers from m_key_images + for(auto it = m_key_images.begin(); it != m_key_images.end(); it++) + { + THROW_WALLET_EXCEPTION_IF(it->second >= m_transfers.size(), + error::wallet_internal_error, + "Key images cache contains illegal transfer offset"); + m_transfers[it->second].m_key_image = it->first; + m_transfers[it->second].m_key_image_known = true; + } - // Restore key images in m_transfers from m_key_images - for(auto it = m_key_images.begin(); it != m_key_images.end(); it++) - { - THROW_WALLET_EXCEPTION_IF(it->second >= m_transfers.size(), error::wallet_internal_error, "Key images cache contains illegal transfer offset"); - m_transfers[it->second].m_key_image = it->first; - m_transfers[it->second].m_key_image_known = true; + return; + } } + + // Soft-Reset to avoid inconsistency in case of BC reorg. + clear_soft(false); // keep_key_images works only with soft reset. + THROW_WALLET_EXCEPTION_IF(true, error::wallet_internal_error, "Transfers changed during rescan, soft or hard rescan is needed"); } //---------------------------------------------------------------------------------------------------- uint64_t wallet2::get_bytes_sent() const |