From 1593553e03aef8d44621aaf79a33ba25f69a2bd7 Mon Sep 17 00:00:00 2001 From: moneromooo-monero Date: Mon, 1 Aug 2016 22:16:00 +0100 Subject: new unlocked parameter to output_histogram This constrains the number of instances of any amount to the unlocked ones (as defined by the default unlock time setting: outputs with non default unlock time are not considered, so may be counted as unlocked even if they are not actually unlocked). --- src/wallet/wallet2.cpp | 9 +++++---- src/wallet/wallet2.h | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) (limited to 'src/wallet') diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp index 43dedaf82..502d64c9c 100644 --- a/src/wallet/wallet2.cpp +++ b/src/wallet/wallet2.cpp @@ -2238,7 +2238,7 @@ uint64_t wallet2::sanitize_fee_multiplier(uint64_t fee_multiplier) const // transactions will be required std::vector wallet2::create_transactions(std::vector dsts, const size_t fake_outs_count, const uint64_t unlock_time, uint64_t fee_multiplier, const std::vector extra, bool trusted_daemon) { - const std::vector unused_transfers_indices = select_available_outputs_from_histogram(fake_outs_count + 1, true, trusted_daemon); + const std::vector unused_transfers_indices = select_available_outputs_from_histogram(fake_outs_count + 1, true, true, trusted_daemon); fee_multiplier = sanitize_fee_multiplier(fee_multiplier); @@ -3061,7 +3061,7 @@ std::vector wallet2::get_unspent_amounts_vector() return vector; } //---------------------------------------------------------------------------------------------------- -std::vector wallet2::select_available_outputs_from_histogram(uint64_t count, bool atleast, bool trusted_daemon) +std::vector wallet2::select_available_outputs_from_histogram(uint64_t count, bool atleast, bool unlocked, bool trusted_daemon) { epee::json_rpc::request req_t = AUTO_VAL_INIT(req_t); epee::json_rpc::response resp_t = AUTO_VAL_INIT(resp_t); @@ -3073,6 +3073,7 @@ std::vector wallet2::select_available_outputs_from_histogram(uint64_t co req_t.params.amounts = get_unspent_amounts_vector(); req_t.params.min_count = count; req_t.params.max_count = 0; + req_t.params.unlocked = unlocked; bool r = net_utils::invoke_http_json_remote_command2(m_daemon_address + "/json_rpc", req_t, resp_t, m_http_client); m_daemon_rpc_mutex.unlock(); THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "select_available_unmixable_outputs"); @@ -3102,13 +3103,13 @@ std::vector wallet2::select_available_outputs_from_histogram(uint64_t co std::vector wallet2::select_available_unmixable_outputs(bool trusted_daemon) { // request all outputs with less than 3 instances - return select_available_outputs_from_histogram(3, false, trusted_daemon); + return select_available_outputs_from_histogram(3, false, true, trusted_daemon); } //---------------------------------------------------------------------------------------------------- std::vector wallet2::select_available_mixable_outputs(bool trusted_daemon) { // request all outputs with at least 3 instances, so we can use mixin 2 with - return select_available_outputs_from_histogram(3, true, trusted_daemon); + return select_available_outputs_from_histogram(3, true, true, trusted_daemon); } //---------------------------------------------------------------------------------------------------- std::vector wallet2::create_unmixable_sweep_transactions(bool trusted_daemon) diff --git a/src/wallet/wallet2.h b/src/wallet/wallet2.h index 62a3c5031..9ff6c4e21 100644 --- a/src/wallet/wallet2.h +++ b/src/wallet/wallet2.h @@ -383,7 +383,7 @@ namespace tools std::string get_keys_file() const; std::string get_daemon_address() const; - std::vector select_available_outputs_from_histogram(uint64_t count, bool atleast, bool trusted_daemon); + std::vector select_available_outputs_from_histogram(uint64_t count, bool atleast, bool unlocked, bool trusted_daemon); std::vector select_available_outputs(const std::function &f); std::vector select_available_unmixable_outputs(bool trusted_daemon); std::vector select_available_mixable_outputs(bool trusted_daemon); -- cgit v1.2.3 From 11dc091464a6cef41434a0bb9f8604f6151c8dc5 Mon Sep 17 00:00:00 2001 From: moneromooo-monero Date: Tue, 2 Aug 2016 21:48:09 +0100 Subject: Fake outs set is now decided by the wallet This plugs a privacy leak from the wallet to the daemon, as the daemon could previously see what input is included as a transaction input, which the daemon hadn't previously supplied. Now, the wallet requests a particular set of outputs, including the real one. This can result in transactions that can't be accepted if the wallet happens to select too many outputs with non standard unlock times. The daemon could know this and select another output, but the wallet is blind to it. It's currently very unlikely since I don't think anything uses non default unlock times. The wallet requests more outputs than necessary so it can use spares if any of the returns outputs are still locked. If there are not enough spares to reach the desired mixin, the transaction will fail. --- src/wallet/api/wallet.cpp | 4 +- src/wallet/wallet2.cpp | 215 +++++++++++++++++++++++++++++++++++---------- src/wallet/wallet2.h | 4 +- src/wallet/wallet_errors.h | 6 +- 4 files changed, 177 insertions(+), 52 deletions(-) (limited to 'src/wallet') diff --git a/src/wallet/api/wallet.cpp b/src/wallet/api/wallet.cpp index 67d2ebecb..4b97e2f1d 100644 --- a/src/wallet/api/wallet.cpp +++ b/src/wallet/api/wallet.cpp @@ -527,8 +527,8 @@ PendingTransaction *WalletImpl::createTransaction(const string &dst_addr, const } catch (const tools::error::not_enough_outs_to_mix& e) { std::ostringstream writer; writer << tr("not enough outputs for specified mixin_count") << " = " << e.mixin_count() << ":"; - for (const cryptonote::COMMAND_RPC_GET_RANDOM_OUTPUTS_FOR_AMOUNTS::outs_for_amount& outs_for_amount : e.scanty_outs()) { - writer << "\n" << tr("output amount") << " = " << print_money(outs_for_amount.amount) << ", " << tr("found outputs to mix") << " = " << outs_for_amount.outs.size(); + for (const std::pair outs_for_amount : e.scanty_outs()) { + writer << "\n" << tr("output amount") << " = " << print_money(outs_for_amount.first) << ", " << tr("found outputs to mix") << " = " << outs_for_amount.second; } m_errorString = writer.str(); m_status = Status_Error; diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp index 502d64c9c..b1544321c 100644 --- a/src/wallet/wallet2.cpp +++ b/src/wallet/wallet2.cpp @@ -2364,45 +2364,174 @@ void wallet2::transfer_selected(const std::vector entry; + std::vector> outs; + if (fake_outputs_count) { - COMMAND_RPC_GET_RANDOM_OUTPUTS_FOR_AMOUNTS::request req = AUTO_VAL_INIT(req); - req.outs_count = fake_outputs_count + 1;// add one to make possible (if need) to skip real output key - BOOST_FOREACH(transfer_container::iterator it, selected_transfers) + // get histogram for the amounts we need + epee::json_rpc::request req_t = AUTO_VAL_INIT(req_t); + epee::json_rpc::response resp_t = AUTO_VAL_INIT(resp_t); + m_daemon_rpc_mutex.lock(); + req_t.jsonrpc = "2.0"; + req_t.id = epee::serialization::storage_entry(0); + req_t.method = "get_output_histogram"; + for(auto it: selected_transfers) + req_t.params.amounts.push_back(it->amount()); + req_t.params.unlocked = true; + bool r = net_utils::invoke_http_json_remote_command2(m_daemon_address + "/json_rpc", req_t, resp_t, m_http_client); + m_daemon_rpc_mutex.unlock(); + THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "transfer_selected"); + THROW_WALLET_EXCEPTION_IF(resp_t.result.status == CORE_RPC_STATUS_BUSY, error::daemon_busy, "get_output_histogram"); + THROW_WALLET_EXCEPTION_IF(resp_t.result.status != CORE_RPC_STATUS_OK, error::get_histogram_error, resp_t.result.status); + + // we ask for more, to have spares if some outputs are still locked + size_t requested_outputs_count = (size_t)((fake_outputs_count + 1) * 1.5 + 1); + + // generate output indices to request + COMMAND_RPC_GET_OUTPUTS::request req = AUTO_VAL_INIT(req); + COMMAND_RPC_GET_OUTPUTS::response daemon_resp = AUTO_VAL_INIT(daemon_resp); + for(transfer_container::iterator it: selected_transfers) { - THROW_WALLET_EXCEPTION_IF(it->m_tx.vout.size() <= it->m_internal_output_index, error::wallet_internal_error, - "m_internal_output_index = " + std::to_string(it->m_internal_output_index) + - " is greater or equal to outputs count = " + std::to_string(it->m_tx.vout.size())); - req.amounts.push_back(it->amount()); + std::unordered_set seen_indices; + size_t start = req.outputs.size(); + + // if there are just enough outputs to mix with, use all of them. + // Eventually this should become impossible. + uint64_t num_outs = 0; + for (auto he: resp_t.result.histogram) + { + if (he.amount == it->amount()) + { + num_outs = he.instances; + break; + } + } + if (num_outs <= requested_outputs_count) + { + for (uint64_t i = 0; i < num_outs; i++) + req.outputs.push_back({it->amount(), i}); + // duplicate to make up shortfall: this will be caught after the RPC call, + // so we can also output the amounts for which we can't reach the required + // mixin after checking the actual unlockedness + for (uint64_t i = num_outs; i < requested_outputs_count; ++i) + req.outputs.push_back({it->amount(), num_outs - 1}); + } + else + { + // start with real one + uint64_t num_found = 1; + seen_indices.emplace(it->m_global_output_index); + req.outputs.push_back({it->amount(), it->m_global_output_index}); + + // while we still need more mixins + while (num_found < requested_outputs_count) + { + // if we've gone through every possible output, we've gotten all we can + if (seen_indices.size() == num_outs) + break; + + // get a random output index from the DB. If we've already seen it, + // return to the top of the loop and try again, otherwise add it to the + // list of output indices we've seen. + + // triangular distribution over [a,b) with a=0, mode c=b=up_index_limit + uint64_t r = crypto::rand() % ((uint64_t)1 << 53); + double frac = std::sqrt((double)r / ((uint64_t)1 << 53)); + uint64_t i = (uint64_t)(frac*num_outs); + // just in case rounding up to 1 occurs after sqrt + if (i == num_outs) + --i; + + if (seen_indices.count(i)) + continue; + seen_indices.emplace(i); + + req.outputs.push_back({it->amount(), i}); + ++num_found; + } + } + + // sort the subsection, to ensure the daemon doesn't know wich output is ours + std::sort(req.outputs.begin() + start, req.outputs.end(), + [](const COMMAND_RPC_GET_OUTPUTS::out &a, const COMMAND_RPC_GET_OUTPUTS::out &b) { return a.index < b.index; }); } + // get the keys for those m_daemon_rpc_mutex.lock(); - bool r = epee::net_utils::invoke_http_bin_remote_command2(m_daemon_address + "/getrandom_outs.bin", req, daemon_resp, m_http_client, 200000); + r = epee::net_utils::invoke_http_bin_remote_command2(m_daemon_address + "/get_outs.bin", req, daemon_resp, m_http_client, 200000); m_daemon_rpc_mutex.unlock(); - THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "getrandom_outs.bin"); - THROW_WALLET_EXCEPTION_IF(daemon_resp.status == CORE_RPC_STATUS_BUSY, error::daemon_busy, "getrandom_outs.bin"); + THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "get_outs.bin"); + THROW_WALLET_EXCEPTION_IF(daemon_resp.status == CORE_RPC_STATUS_BUSY, error::daemon_busy, "get_outs.bin"); THROW_WALLET_EXCEPTION_IF(daemon_resp.status != CORE_RPC_STATUS_OK, error::get_random_outs_error, daemon_resp.status); - THROW_WALLET_EXCEPTION_IF(daemon_resp.outs.size() != selected_transfers.size(), error::wallet_internal_error, - "daemon returned wrong response for getrandom_outs.bin, wrong amounts count = " + - std::to_string(daemon_resp.outs.size()) + ", expected " + std::to_string(selected_transfers.size())); - - std::vector scanty_outs; - BOOST_FOREACH(COMMAND_RPC_GET_RANDOM_OUTPUTS_FOR_AMOUNTS::outs_for_amount& amount_outs, daemon_resp.outs) - { - if (amount_outs.outs.size() < fake_outputs_count) + THROW_WALLET_EXCEPTION_IF(daemon_resp.outs.size() != req.outputs.size(), error::wallet_internal_error, + "daemon returned wrong response for get_outs.bin, wrong amounts count = " + + std::to_string(daemon_resp.outs.size()) + ", expected " + std::to_string(req.outputs.size())); + + std::unordered_map scanty_outs; + size_t base = 0; + outs.reserve(selected_transfers.size()); + for(transfer_container::iterator it: selected_transfers) + { + outs.push_back(std::vector()); + outs.back().reserve(fake_outputs_count + 1); + + // pick real out first (it will be sorted when done) + outs.back().push_back({it->m_global_output_index, boost::get(it->m_tx.vout[it->m_internal_output_index].target).key}); + + // then pick others in random order till we reach the required number + // since we use an equiprobable pick here, we don't upset the triangular distribution + std::vector order; + order.resize(requested_outputs_count); + for (size_t n = 0; n < order.size(); ++n) + order[n] = n; + std::shuffle(order.begin(), order.end(), std::default_random_engine(crypto::rand())); + + for (size_t o = 0; o < requested_outputs_count && outs.back().size() < fake_outputs_count + 1; ++o) { - scanty_outs.push_back(amount_outs); + size_t i = base + order[o]; + if (req.outputs[i].index == it->m_global_output_index) // don't re-add real one + continue; + if (!daemon_resp.outs[i].unlocked) // don't add locked outs + continue; + if (o > 0 && daemon_resp.outs[i].key == daemon_resp.outs[i - 1].key) // don't add duplicates + continue; + outs.back().push_back({req.outputs[i].index, daemon_resp.outs[i].key}); + } + if (outs.back().size() < fake_outputs_count + 1) + { + scanty_outs[it->amount()] = outs.back().size(); } + else + { + // sort the subsection, so any spares are reset in order + std::sort(outs.back().begin(), outs.back().end(), [](const entry &a, const entry &b) { return a.first < b.first; }); + + // sanity check + for (size_t n = 1; n < outs.back().size(); ++n) + { + THROW_WALLET_EXCEPTION_IF(outs.back()[n].first == outs.back()[n-1].first, error::wallet_internal_error, + "Duplicate indices though we did not ask for any"); + THROW_WALLET_EXCEPTION_IF(outs.back()[n].second == outs.back()[n-1].second, error::wallet_internal_error, + "Duplicate keys after we have weeded them out"); + } + } + base += requested_outputs_count; } THROW_WALLET_EXCEPTION_IF(!scanty_outs.empty(), error::not_enough_outs_to_mix, scanty_outs, fake_outputs_count); } + else + { + for (transfer_container::iterator it: selected_transfers) + { + std::vector v; + v.push_back({it->m_global_output_index, boost::get(it->m_tx.vout[it->m_internal_output_index].target).key}); + outs.push_back(v); + } + } //prepare inputs - size_t i = 0; + typedef cryptonote::tx_source_entry::output_entry tx_output_entry; + size_t i = 0, out_index = 0; std::vector sources; BOOST_FOREACH(transfer_container::iterator it, selected_transfers) { @@ -2410,38 +2539,34 @@ void wallet2::transfer_selected(const std::vector= fake_outputs_count) - break; - } + tx_output_entry oe; + oe.first = outs[out_index][n].first; + oe.second = outs[out_index][n].second; + src.outputs.push_back(oe); + ++i; } //paste real transaction to the random index - auto it_to_insert = std::find_if(src.outputs.begin(), src.outputs.end(), [&](const tx_output_entry& a) + auto it_to_replace = std::find_if(src.outputs.begin(), src.outputs.end(), [&](const tx_output_entry& a) { - return a.first >= td.m_global_output_index; + return a.first == td.m_global_output_index; }); - //size_t real_index = src.outputs.size() ? (rand() % src.outputs.size() ):0; + THROW_WALLET_EXCEPTION_IF(it_to_replace == src.outputs.end(), error::wallet_internal_error, + "real output not found"); + tx_output_entry real_oe; real_oe.first = td.m_global_output_index; real_oe.second = boost::get(td.m_tx.vout[td.m_internal_output_index].target).key; - auto interted_it = src.outputs.insert(it_to_insert, real_oe); + *it_to_replace = real_oe; src.real_out_tx_key = get_tx_pub_key_from_extra(td.m_tx); - src.real_output = interted_it - src.outputs.begin(); + src.real_output = it_to_replace - src.outputs.begin(); src.real_output_in_tx_index = td.m_internal_output_index; detail::print_source_entry(src); - ++i; + ++out_index; } cryptonote::tx_destination_entry change_dts = AUTO_VAL_INIT(change_dts); diff --git a/src/wallet/wallet2.h b/src/wallet/wallet2.h index 9ff6c4e21..d0c514a6d 100644 --- a/src/wallet/wallet2.h +++ b/src/wallet/wallet2.h @@ -668,12 +668,12 @@ namespace tools "daemon returned wrong response for getrandom_outs.bin, wrong amounts count = " + std::to_string(daemon_resp.outs.size()) + ", expected " + std::to_string(selected_transfers.size())); - std::vector scanty_outs; + std::unordered_map scanty_outs; BOOST_FOREACH(COMMAND_RPC_GET_RANDOM_OUTPUTS_FOR_AMOUNTS::outs_for_amount& amount_outs, daemon_resp.outs) { if (amount_outs.outs.size() < fake_outputs_count) { - scanty_outs.push_back(amount_outs); + scanty_outs[amount_outs.amount] = amount_outs.outs.size(); } } THROW_WALLET_EXCEPTION_IF(!scanty_outs.empty(), error::not_enough_outs_to_mix, scanty_outs, fake_outputs_count); diff --git a/src/wallet/wallet_errors.h b/src/wallet/wallet_errors.h index ea0b6a1f5..8ea061152 100644 --- a/src/wallet/wallet_errors.h +++ b/src/wallet/wallet_errors.h @@ -377,7 +377,7 @@ namespace tools //---------------------------------------------------------------------------------------------------- struct not_enough_outs_to_mix : public transfer_error { - typedef std::vector scanty_outs_t; + typedef std::unordered_map scanty_outs_t; explicit not_enough_outs_to_mix(std::string&& loc, const scanty_outs_t& scanty_outs, size_t mixin_count) : transfer_error(std::move(loc), "not enough outputs to mix") @@ -393,9 +393,9 @@ namespace tools { std::ostringstream ss; ss << transfer_error::to_string() << ", mixin_count = " << m_mixin_count << ", scanty_outs:"; - for (const auto& outs_for_amount : m_scanty_outs) + for (const auto& out: m_scanty_outs) { - ss << '\n' << cryptonote::print_money(outs_for_amount.amount) << " - " << outs_for_amount.outs.size(); + ss << '\n' << cryptonote::print_money(out.first) << " - " << out.second; } return ss.str(); } -- cgit v1.2.3