From bba6af9064cb95596039f5a8f7ea460d3dedef0d Mon Sep 17 00:00:00 2001 From: moneromooo-monero Date: Mon, 26 Sep 2016 23:11:10 +0100 Subject: [PATCH] wallet: cold wallet transaction signing This change adds the ability to create a new unsigned transaction from a watch only wallet, and save it to a file. This file can then be moved to another computer/VM where a cold wallet may load it, sign it, and save it. That cold wallet does not need to have a blockchain nor daemon. The signed transaction file can then be moved back to the watch only wallet, which can load it and send it to the daemon. Two new simplewallet commands to use it: sign_transfer (on the cold wallet) submit_transfer (on the watch only wallet) The transfer command used on a watch only wallet now writes an unsigned transaction set in a file called 'unsigned_monero_tx' instead of submitting the tx to the daemon as a normal wallet does. The signed tx file is called 'signed_monero_tx'. --- .../cryptonote_format_utils.cpp | 16 +- src/cryptonote_core/cryptonote_format_utils.h | 15 + src/simplewallet/simplewallet.cpp | 296 ++++++++++++++-- src/simplewallet/simplewallet.h | 3 + src/wallet/wallet2.cpp | 329 +++++++++++++----- src/wallet/wallet2.h | 86 ++++- tests/unit_tests/output_selection.cpp | 8 +- 7 files changed, 627 insertions(+), 126 deletions(-) diff --git a/src/cryptonote_core/cryptonote_format_utils.cpp b/src/cryptonote_core/cryptonote_format_utils.cpp index 7e0c75ccf..64f8eb924 100644 --- a/src/cryptonote_core/cryptonote_format_utils.cpp +++ b/src/cryptonote_core/cryptonote_format_utils.cpp @@ -472,10 +472,7 @@ namespace cryptonote bool construct_tx_and_get_tx_key(const account_keys& sender_account_keys, const std::vector& sources, const std::vector& destinations, std::vector extra, transaction& tx, uint64_t unlock_time, crypto::secret_key &tx_key, bool rct) { std::vector amount_keys; - tx.vin.clear(); - tx.vout.clear(); - tx.signatures.clear(); - tx.rct_signatures.type = rct::RCTTypeNull; + tx.set_null(); amount_keys.clear(); tx.version = rct ? 2 : 1; @@ -615,6 +612,14 @@ namespace cryptonote return false; } + // check for watch only wallet + bool zero_secret_key = true; + for (size_t i = 0; i < sizeof(sender_account_keys.m_spend_secret_key); ++i) + zero_secret_key &= (sender_account_keys.m_spend_secret_key.data[i] == 0); + if (zero_secret_key) + { + LOG_PRINT_L1("Null secret key, skipping signatures"); + } if (tx.version == 1) { @@ -641,7 +646,8 @@ namespace cryptonote tx.signatures.push_back(std::vector()); std::vector& sigs = tx.signatures.back(); sigs.resize(src_entr.outputs.size()); - crypto::generate_ring_signature(tx_prefix_hash, boost::get(tx.vin[i]).k_image, keys_ptrs, in_contexts[i].in_ephemeral.sec, src_entr.real_output, sigs.data()); + if (!zero_secret_key) + crypto::generate_ring_signature(tx_prefix_hash, boost::get(tx.vin[i]).k_image, keys_ptrs, in_contexts[i].in_ephemeral.sec, src_entr.real_output, sigs.data()); ss_ring_s << "signatures:" << ENDL; std::for_each(sigs.begin(), sigs.end(), [&](const crypto::signature& s){ss_ring_s << s << ENDL;}); ss_ring_s << "prefix_hash:" << tx_prefix_hash << ENDL << "in_ephemeral_key: " << in_contexts[i].in_ephemeral.sec << ENDL << "real_output: " << src_entr.real_output; diff --git a/src/cryptonote_core/cryptonote_format_utils.h b/src/cryptonote_core/cryptonote_format_utils.h index a42296994..24db8008e 100644 --- a/src/cryptonote_core/cryptonote_format_utils.h +++ b/src/cryptonote_core/cryptonote_format_utils.h @@ -62,6 +62,16 @@ namespace cryptonote rct::key mask; //ringct amount mask void push_output(uint64_t idx, const crypto::public_key &k, uint64_t amount) { outputs.push_back(std::make_pair(idx, rct::ctkey({rct::pk2rct(k), rct::zeroCommit(amount)}))); } + + BEGIN_SERIALIZE_OBJECT() + FIELD(outputs) + VARINT_FIELD(real_output) + FIELD(real_out_tx_key) + VARINT_FIELD(real_output_in_tx_index) + VARINT_FIELD(amount) + FIELD(rct) + FIELD(mask) + END_SERIALIZE() }; struct tx_destination_entry @@ -71,6 +81,11 @@ namespace cryptonote tx_destination_entry() : amount(0), addr(AUTO_VAL_INIT(addr)) { } tx_destination_entry(uint64_t a, const account_public_address &ad) : amount(a), addr(ad) { } + + BEGIN_SERIALIZE_OBJECT() + VARINT_FIELD(amount) + FIELD(addr) + END_SERIALIZE() }; //--------------------------------------------------------------- diff --git a/src/simplewallet/simplewallet.cpp b/src/simplewallet/simplewallet.cpp index 7260ecf8a..d3938362e 100644 --- a/src/simplewallet/simplewallet.cpp +++ b/src/simplewallet/simplewallet.cpp @@ -389,11 +389,6 @@ bool simple_wallet::seed_set_language(const std::vector &args/* = s bool simple_wallet::set_always_confirm_transfers(const std::vector &args/* = std::vector()*/) { bool success = false; - if (m_wallet->watch_only()) - { - fail_msg_writer() << tr("wallet is watch-only and cannot transfer"); - return true; - } tools::password_container pwd_container(m_wallet_file.empty()); success = pwd_container.read_password(); if (!success) @@ -654,6 +649,8 @@ simple_wallet::simple_wallet() m_cmd_binder.set_handler("transfer", boost::bind(&simple_wallet::transfer_new, this, _1), tr("Same as transfer_original, but using a new transaction building algorithm")); m_cmd_binder.set_handler("sweep_unmixable", boost::bind(&simple_wallet::sweep_unmixable, this, _1), tr("Send all unmixable outputs to yourself with mixin 0")); m_cmd_binder.set_handler("sweep_all", boost::bind(&simple_wallet::sweep_all, this, _1), tr("Send all unlocked balance an address")); + m_cmd_binder.set_handler("sign_transfer", boost::bind(&simple_wallet::sign_transfer, this, _1), tr("Sign a transaction from a file")); + m_cmd_binder.set_handler("submit_transfer", boost::bind(&simple_wallet::submit_transfer, this, _1), tr("Submit a signed transaction from a file")); m_cmd_binder.set_handler("set_log", boost::bind(&simple_wallet::set_log, this, _1), tr("set_log - Change current log detail level, <0-4>")); m_cmd_binder.set_handler("address", boost::bind(&simple_wallet::print_address, this, _1), tr("Show current wallet public address")); m_cmd_binder.set_handler("integrated_address", boost::bind(&simple_wallet::print_integrated_address, this, _1), tr("integrated_address [PID] - Encode a payment ID into an integrated address for the current wallet public address (no argument uses a random payment ID), or decode an integrated address to standard address and payment ID")); @@ -2313,12 +2310,6 @@ bool simple_wallet::transfer_main(int transfer_type, const std::vectorwatch_only()) - { - fail_msg_writer() << tr("this is a watch only wallet"); - return true; - } - std::vector extra; bool payment_id_seen = false; if (1 == local_args.size() % 2) @@ -2455,7 +2446,19 @@ bool simple_wallet::transfer_main(int transfer_type, const std::vectorwatch_only()) + { + bool r = m_wallet->save_tx(ptx_vector, "unsigned_monero_tx"); + if (!r) + { + fail_msg_writer() << tr("Failed to write transaction(s) to file"); + } + else + { + success_msg_writer(true) << tr("Unsigned transaction(s) successfully written to file: ") << "unsigned_monero_tx"; + } + } + else while (!ptx_vector.empty()) { auto & ptx = ptx_vector.back(); m_wallet->commit_tx(ptx); @@ -2562,12 +2565,6 @@ bool simple_wallet::sweep_unmixable(const std::vector &args_) if (!try_connect_to_daemon()) return true; - if(m_wallet->watch_only()) - { - fail_msg_writer() << tr("this is a watch only wallet"); - return true; - } - LOCK_IDLE_SCOPE(); try { @@ -2617,7 +2614,19 @@ bool simple_wallet::sweep_unmixable(const std::vector &args_) } // actually commit the transactions - while (!ptx_vector.empty()) + if (m_wallet->watch_only()) + { + bool r = m_wallet->save_tx(ptx_vector, "unsigned_monero_tx"); + if (!r) + { + fail_msg_writer() << tr("Failed to write transaction(s) to file"); + } + else + { + success_msg_writer(true) << tr("Unsigned transaction(s) successfully written to file: ") << "unsigned_monero_tx"; + } + } + else while (!ptx_vector.empty()) { auto & ptx = ptx_vector.back(); m_wallet->commit_tx(ptx); @@ -2714,12 +2723,6 @@ bool simple_wallet::sweep_all(const std::vector &args_) if (!try_connect_to_daemon()) return true; - if(m_wallet->watch_only()) - { - fail_msg_writer() << tr("this is a watch only wallet"); - return true; - } - std::vector local_args = args_; size_t fake_outs_count; @@ -2849,7 +2852,19 @@ bool simple_wallet::sweep_all(const std::vector &args_) } // actually commit the transactions - while (!ptx_vector.empty()) + if (m_wallet->watch_only()) + { + bool r = m_wallet->save_tx(ptx_vector, "unsigned_monero_tx"); + if (!r) + { + fail_msg_writer() << tr("Failed to write transaction(s) to file"); + } + else + { + success_msg_writer(true) << tr("Unsigned transaction(s) successfully written to file: ") << "unsigned_monero_tx"; + } + } + else while (!ptx_vector.empty()) { auto & ptx = ptx_vector.back(); m_wallet->commit_tx(ptx); @@ -2941,6 +2956,235 @@ bool simple_wallet::sweep_all(const std::vector &args_) return true; } //---------------------------------------------------------------------------------------------------- +bool simple_wallet::accept_loaded_tx(const tools::wallet2::unsigned_tx_set &txs) +{ + // gather info to ask the user + uint64_t amount = 0, amount_to_dests = 0, change = 0; + size_t min_mixin = ~0; + std::unordered_map dests; + const std::string wallet_address = m_wallet->get_account().get_public_address_str(m_wallet->testnet()); + for (size_t n = 0; n < txs.txes.size(); ++n) + { + const tools::wallet2::tx_construction_data &cd = txs.txes[n]; + for (size_t s = 0; s < cd.sources.size(); ++s) + { + amount += cd.sources[s].amount; + size_t mixin = cd.sources[s].outputs.size() - 1; + if (mixin < min_mixin) + min_mixin = mixin; + } + for (size_t d = 0; d < cd.destinations.size(); ++d) + { + const tx_destination_entry &entry = cd.destinations[d]; + std::string address = get_account_address_as_str(m_wallet->testnet(), entry.addr); + std::unordered_map::iterator i = dests.find(address); + if (i == dests.end()) + dests.insert(std::make_pair(address, entry.amount)); + else + i->second += entry.amount; + amount_to_dests += entry.amount; + } + if (cd.change_dts.amount > 0) + { + dests.insert(std::make_pair(get_account_address_as_str(m_wallet->testnet(), cd.change_dts.addr), cd.change_dts.amount)); + amount_to_dests += cd.change_dts.amount; + change += cd.change_dts.amount; + } + } + std::string dest_string; + for (std::unordered_map::const_iterator i = dests.begin(); i != dests.end(); ) + { + dest_string += (boost::format(tr("sending %s to %s")) % print_money(i->second) % i->first).str(); + ++i; + if (i != dests.end()) + dest_string += ", "; + } + if (dest_string.empty()) + dest_string = tr("with no destinations"); + + uint64_t fee = amount - amount_to_dests; + std::string prompt_str = (boost::format(tr("Loaded %lu transactions, for %s, fee %s, change %s, %s, with min mixin %lu (full details in log file). Is this okay? (Y/Yes/N/No)")) % (unsigned long)txs.txes.size() % print_money(amount) % print_money(fee) % print_money(change) % dest_string % (unsigned long)min_mixin).str(); + std::string accepted = command_line::input_line(prompt_str); + return is_it_true(accepted); +} +//---------------------------------------------------------------------------------------------------- +bool simple_wallet::sign_transfer(const std::vector &args_) +{ + if(m_wallet->watch_only()) + { + fail_msg_writer() << tr("This is a watch only wallet"); + return true; + } + + try + { + bool r = m_wallet->sign_tx("unsigned_monero_tx", "signed_monero_tx", [&](const tools::wallet2::unsigned_tx_set &tx){ return accept_loaded_tx(tx); }); + if (!r) + { + fail_msg_writer() << tr("Failed to sign transaction"); + return true; + } + } + catch (const std::exception &e) + { + fail_msg_writer() << tr("Failed to sign transaction: ") << e.what(); + return true; + } + + success_msg_writer(true) << tr("Transaction successfully signed to file: ") << "signed_monero_tx"; + return true; +} +//---------------------------------------------------------------------------------------------------- +bool simple_wallet::submit_transfer(const std::vector &args_) +{ + if (!try_connect_to_daemon()) + return true; + + try + { + std::vector ptx_vector; + bool r = m_wallet->load_tx("signed_monero_tx", ptx_vector); + if (!r) + { + fail_msg_writer() << tr("Failed to load transaction from file"); + return true; + } + + // if more than one tx necessary, prompt user to confirm + if (m_wallet->always_confirm_transfers()) + { + uint64_t total_fee = 0; + uint64_t dust_not_in_fee = 0; + uint64_t dust_in_fee = 0; + for (size_t n = 0; n < ptx_vector.size(); ++n) + { + total_fee += ptx_vector[n].fee; + + if (ptx_vector[n].dust_added_to_fee) + dust_in_fee += ptx_vector[n].dust; + else + dust_not_in_fee += ptx_vector[n].dust; + } + + std::stringstream prompt; + if (ptx_vector.size() > 1) + { + prompt << boost::format(tr("Your transaction needs to be split into %llu transactions. " + "This will result in a transaction fee being applied to each transaction, for a total fee of %s")) % + ((unsigned long long)ptx_vector.size()) % print_money(total_fee); + } + else + { + prompt << boost::format(tr("The transaction fee is %s")) % + print_money(total_fee); + } + if (dust_in_fee != 0) prompt << boost::format(tr(", of which %s is dust from change")) % print_money(dust_in_fee); + if (dust_not_in_fee != 0) prompt << tr(".") << ENDL << boost::format(tr("A total of %s from dust change will be sent to dust address")) + % print_money(dust_not_in_fee); + prompt << tr(".") << ENDL << tr("Is this okay? (Y/Yes/N/No)"); + + std::string accepted = command_line::input_line(prompt.str()); + if (std::cin.eof()) + return true; + if (accepted != "Y" && accepted != "y" && accepted != "Yes" && accepted != "yes") + { + fail_msg_writer() << tr("transaction cancelled."); + + // would like to return false, because no tx made, but everything else returns true + // and I don't know what returning false might adversely affect. *sigh* + return true; + } + } + + // actually commit the transactions + while (!ptx_vector.empty()) + { + auto & ptx = ptx_vector.back(); + m_wallet->commit_tx(ptx); + success_msg_writer(true) << tr("Money successfully sent, transaction: ") << get_transaction_hash(ptx.tx); + + // if no exception, remove element from vector + ptx_vector.pop_back(); + } + } + catch (const tools::error::daemon_busy&) + { + fail_msg_writer() << tr("daemon is busy. Please try later"); + } + catch (const tools::error::no_connection_to_daemon&) + { + fail_msg_writer() << tr("no connection to daemon. Please, make sure daemon is running."); + } + catch (const tools::error::wallet_rpc_error& e) + { + LOG_ERROR("Unknown RPC error: " << e.to_string()); + fail_msg_writer() << tr("RPC error: ") << e.what(); + } + catch (const tools::error::get_random_outs_error&) + { + fail_msg_writer() << tr("failed to get random outputs to mix"); + } + catch (const tools::error::not_enough_money& e) + { + fail_msg_writer() << boost::format(tr("not enough money to transfer, available only %s, transaction amount %s = %s + %s (fee)")) % + print_money(e.available()) % + print_money(e.tx_amount() + e.fee()) % + print_money(e.tx_amount()) % + print_money(e.fee()); + } + catch (const tools::error::not_enough_outs_to_mix& e) + { + auto writer = fail_msg_writer(); + writer << tr("not enough outputs for specified mixin_count") << " = " << e.mixin_count() << ":"; + for (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; + } + } + catch (const tools::error::tx_not_constructed&) + { + fail_msg_writer() << tr("transaction was not constructed"); + } + catch (const tools::error::tx_rejected& e) + { + fail_msg_writer() << (boost::format(tr("transaction %s was rejected by daemon with status: ")) % get_transaction_hash(e.tx())) << e.status(); + } + catch (const tools::error::tx_sum_overflow& e) + { + fail_msg_writer() << e.what(); + } + catch (const tools::error::zero_destination&) + { + fail_msg_writer() << tr("one of destinations is zero"); + } + catch (const tools::error::tx_too_big& e) + { + fail_msg_writer() << tr("Failed to find a suitable way to split transactions"); + } + catch (const tools::error::transfer_error& e) + { + LOG_ERROR("unknown transfer error: " << e.to_string()); + fail_msg_writer() << tr("unknown transfer error: ") << e.what(); + } + catch (const tools::error::wallet_internal_error& e) + { + LOG_ERROR("internal error: " << e.to_string()); + fail_msg_writer() << tr("internal error: ") << e.what(); + } + catch (const std::exception& e) + { + LOG_ERROR("unexpected error: " << e.what()); + fail_msg_writer() << tr("unexpected error: ") << e.what(); + } + catch (...) + { + LOG_ERROR("Unknown error"); + fail_msg_writer() << tr("unknown error"); + } + + return true; +} +//---------------------------------------------------------------------------------------------------- bool simple_wallet::get_tx_key(const std::vector &args_) { std::vector local_args = args_; diff --git a/src/simplewallet/simplewallet.h b/src/simplewallet/simplewallet.h index 6eb18ed9a..61e956ce9 100644 --- a/src/simplewallet/simplewallet.h +++ b/src/simplewallet/simplewallet.h @@ -125,6 +125,8 @@ namespace cryptonote bool transfer_new(const std::vector &args); bool sweep_all(const std::vector &args); bool sweep_unmixable(const std::vector &args); + bool sign_transfer(const std::vector &args); + bool submit_transfer(const std::vector &args); std::vector> split_amounts( std::vector dsts, size_t num_splits ); @@ -152,6 +154,7 @@ namespace cryptonote uint64_t get_daemon_blockchain_height(std::string& err); bool try_connect_to_daemon(bool silent = false); bool ask_wallet_create_if_needed(); + bool accept_loaded_tx(const tools::wallet2::unsigned_tx_set &txs); bool get_address_from_str(const std::string &str, cryptonote::account_public_address &address, bool &has_payment_id, crypto::hash8 &payment_id); /*! diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp index 75b1c36bd..45102d5f2 100644 --- a/src/wallet/wallet2.cpp +++ b/src/wallet/wallet2.cpp @@ -74,6 +74,9 @@ using namespace cryptonote; // arbitrary, used to generate different hashes from the same input #define CHACHA8_KEY_TAIL 0x8c +#define UNSIGNED_TX_PREFIX "Monero unsigned tx set\001" +#define SIGNED_TX_PREFIX "Monero signed tx set\001" + #define KILL_IOSERVICE() \ do { \ work.reset(); \ @@ -173,15 +176,17 @@ bool wallet2::is_deprecated() const return is_old_file_format; } //---------------------------------------------------------------------------------------------------- -void wallet2::set_spent(transfer_details &td, uint64_t height) +void wallet2::set_spent(size_t idx, uint64_t height) { + transfer_details &td = m_transfers[idx]; LOG_PRINT_L2("Setting SPENT at " << height << ": ki " << td.m_key_image << ", amount " << print_money(td.m_amount)); td.m_spent = true; td.m_spent_height = height; } //---------------------------------------------------------------------------------------------------- -void wallet2::set_unspent(transfer_details &td) +void wallet2::set_unspent(size_t idx) { + transfer_details &td = m_transfers[idx]; LOG_PRINT_L2("Setting UNSPENT: ki " << td.m_key_image << ", amount " << print_money(td.m_amount)); td.m_spent = false; td.m_spent_height = 0; @@ -501,7 +506,7 @@ void wallet2::process_new_transaction(const cryptonote::transaction& tx, const s td.m_mask = rct::identity(); td.m_rct = false; } - set_unspent(td); + set_unspent(m_transfers.size()-1); m_key_images[td.m_key_image] = m_transfers.size()-1; LOG_PRINT_L0("Received money: " << print_money(td.amount()) << ", with tx: " << txid()); if (0 != m_callback) @@ -580,7 +585,7 @@ void wallet2::process_new_transaction(const cryptonote::transaction& tx, const s amount = td.amount(); LOG_PRINT_L0("Spent money: " << print_money(amount) << ", with tx: " << txid()); tx_money_spent_in_ins += amount; - set_spent(td, height); + set_spent(it->second, height); if (0 != m_callback) m_callback->on_money_spent(height, tx, amount, tx); } @@ -997,12 +1002,13 @@ void wallet2::update_pool_state() if (pit->second.m_tx.vin[vini].type() == typeid(txin_to_key)) { txin_to_key &tx_in_to_key = boost::get(pit->second.m_tx.vin[vini]); - for (auto &td: m_transfers) + for (size_t i = 0; i < m_transfers.size(); ++i) { + const transfer_details &td = m_transfers[i]; if (td.m_key_image == tx_in_to_key.k_image) { LOG_PRINT_L1("Resetting spent status for output " << vini << ": " << td.m_key_image); - set_unspent(td); + set_unspent(i); break; } } @@ -1301,7 +1307,7 @@ void wallet2::detach_blockchain(uint64_t height) if (td.m_spent && td.m_spent_height >= height) { LOG_PRINT_L1("Resetting spent status for output " << i << ": " << td.m_key_image); - set_unspent(td); + set_unspent(i); } } @@ -1590,7 +1596,8 @@ bool wallet2::verify_password(const std::string& password) const const cryptonote::account_keys& keys = account_data_check.get_keys(); r = r && verify_keys(keys.m_view_secret_key, keys.m_account_address.m_view_public_key); - r = r && verify_keys(keys.m_spend_secret_key, keys.m_account_address.m_spend_public_key); + if(!m_watch_only) + r = r && verify_keys(keys.m_spend_secret_key, keys.m_account_address.m_spend_public_key); return r; } @@ -2128,13 +2135,13 @@ void wallet2::rescan_spent() if (td.m_spent) { LOG_PRINT_L0("Marking output " << i << "(" << td.m_key_image << ") as unspent, it was marked as spent"); - set_unspent(td); + set_unspent(i); td.m_spent_height = 0; } else { LOG_PRINT_L0("Marking output " << i << "(" << td.m_key_image << ") as spent, it was marked as unspent"); - set_spent(td, td.m_spent_height); + set_spent(i, td.m_spent_height); // unknown height, if this gets reorged, it might still be missed } } @@ -2260,7 +2267,7 @@ float wallet2::get_output_relatedness(const transfer_details &td0, const transfe return 0.0f; } //---------------------------------------------------------------------------------------------------- -size_t wallet2::pop_best_value_from(const transfer_container &transfers, std::vector &unused_indices, const std::list& selected_transfers) const +size_t wallet2::pop_best_value_from(const transfer_container &transfers, std::vector &unused_indices, const std::list& selected_transfers) const { std::vector candidates; float best_relatedness = 1.0f; @@ -2268,9 +2275,9 @@ size_t wallet2::pop_best_value_from(const transfer_container &transfers, std::ve { const transfer_details &candidate = transfers[unused_indices[n]]; float relatedness = 0.0f; - for (const auto &i: selected_transfers) + for (size_t i = 0; i < selected_transfers.size(); ++i) { - float r = get_output_relatedness(candidate, *i); + float r = get_output_relatedness(candidate, transfers[i]); if (r > relatedness) { relatedness = r; @@ -2292,7 +2299,7 @@ size_t wallet2::pop_best_value_from(const transfer_container &transfers, std::ve return pop_index (unused_indices, candidates[idx]); } //---------------------------------------------------------------------------------------------------- -size_t wallet2::pop_best_value(std::vector &unused_indices, const std::list& selected_transfers) const +size_t wallet2::pop_best_value(std::vector &unused_indices, const std::list& selected_transfers) const { return pop_best_value_from(m_transfers, unused_indices, selected_transfers); } @@ -2301,7 +2308,7 @@ size_t wallet2::pop_best_value(std::vector &unused_indices, const std::l // returns: // direct return: amount of money found // modified reference: selected_transfers, a list of iterators/indices of input sources -uint64_t wallet2::select_transfers(uint64_t needed_money, std::vector unused_transfers_indices, std::list& selected_transfers, bool trusted_daemon) +uint64_t wallet2::select_transfers(uint64_t needed_money, std::vector unused_transfers_indices, std::list& selected_transfers, bool trusted_daemon) { uint64_t found_money = 0; while (found_money < needed_money && !unused_transfers_indices.empty()) @@ -2309,7 +2316,7 @@ uint64_t wallet2::select_transfers(uint64_t needed_money, std::vector un size_t idx = pop_best_value(unused_transfers_indices, selected_transfers); transfer_container::iterator it = m_transfers.begin() + idx; - selected_transfers.push_back(it); + selected_transfers.push_back(idx); found_money += it->amount(); } @@ -2513,8 +2520,8 @@ void wallet2::commit_tx(pending_tx& ptx) { payment_id = get_payment_id(ptx); dests = ptx.dests; - BOOST_FOREACH(transfer_container::iterator it, ptx.selected_transfers) - amount_in += it->amount(); + BOOST_FOREACH(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); if (store_tx_info()) @@ -2524,9 +2531,9 @@ void wallet2::commit_tx(pending_tx& ptx) LOG_PRINT_L2("transaction " << txid << " generated ok and sent to daemon, key_images: [" << ptx.key_images << "]"); - BOOST_FOREACH(transfer_container::iterator it, ptx.selected_transfers) + BOOST_FOREACH(size_t idx, ptx.selected_transfers) { - set_spent(*it, 0); + set_spent(idx, 0); } //fee includes dust if dust policy specified it. @@ -2544,7 +2551,153 @@ void wallet2::commit_tx(std::vector& ptx_vector) commit_tx(ptx); } } +//---------------------------------------------------------------------------------------------------- +bool wallet2::save_tx(const std::vector& ptx_vector, const std::string &filename) +{ + LOG_PRINT_L0("saving " << ptx_vector.size() << " transactions"); + unsigned_tx_set txs; + for (auto &tx: ptx_vector) + txs.txes.push_back(tx.construction_data); + std::string s = obj_to_json_str(txs); + if (s.empty()) + return false; + LOG_PRINT_L2("Saving unsigned tx data: " << s); + // save as binary as there's no implementation of loading a json_archive + if (!::serialization::dump_binary(txs, s)) + return false; + return epee::file_io_utils::save_string_to_file(filename, std::string(UNSIGNED_TX_PREFIX) + s); +} +//---------------------------------------------------------------------------------------------------- +bool wallet2::sign_tx(const std::string &unsigned_filename, const std::string &signed_filename, std::function accept_func) +{ + std::string s; + boost::system::error_code errcode; + if (!boost::filesystem::exists(unsigned_filename, errcode)) + { + LOG_PRINT_L0("File " << unsigned_filename << " does not exist: " << errcode); + return false; + } + if (!epee::file_io_utils::load_file_to_string(unsigned_filename.c_str(), s)) + { + LOG_PRINT_L0("Failed to load from " << unsigned_filename); + return false; + } + const size_t magiclen = strlen(UNSIGNED_TX_PREFIX); + if (strncmp(s.c_str(), UNSIGNED_TX_PREFIX, magiclen)) + { + LOG_PRINT_L0("Bad magic from " << unsigned_filename); + return false; + } + unsigned_tx_set exported_txs; + if (!::serialization::parse_binary(std::string(s.c_str() + magiclen, s.size() - magiclen), exported_txs)) + { + LOG_PRINT_L0("Failed to parse data from " << unsigned_filename); + return false; + } + LOG_PRINT_L1("Loaded tx unsigned data from binary: " << exported_txs.txes.size() << " transactions"); + + if (accept_func && !accept_func(exported_txs)) + { + LOG_PRINT_L1("Transactions rejected by callback"); + return false; + } + + // sign the transactions + 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]; + LOG_PRINT_L1(" " << (n+1) << ": " << sd.sources.size() << " inputs, mixin " << (sd.sources[0].outputs.size()-1)); + signed_txes.ptx.push_back(pending_tx()); + tools::wallet2::pending_tx &ptx = signed_txes.ptx.back(); + crypto::secret_key tx_key; + std::vector dests = sd.destinations; + if (sd.change_dts.amount > 0) + dests.push_back(sd.change_dts); + bool r = cryptonote::construct_tx_and_get_tx_key(m_account.get_keys(), sd.sources, dests, sd.extra, ptx.tx, sd.unlock_time, tx_key, sd.use_rct); + THROW_WALLET_EXCEPTION_IF(!r, error::tx_not_constructed, sd.sources, sd.destinations, 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, + // and if we really go over limit, the daemon will reject when it gets submitted. Chances are it's + // OK anyway since it was generated in the first place, and rerolling should be within a few bytes. + + // normally, the tx keys are saved in commit_tx, when the tx is actually sent to the daemon. + // we can't do that here since the tx will be sent from the compromised wallet, which we don't want + // to see that info, so we save it here + if (store_tx_info()) + { + const crypto::hash txid = get_transaction_hash(ptx.tx); + m_tx_keys.insert(std::make_pair(txid, tx_key)); + } + + std::string key_images; + bool all_are_txin_to_key = std::all_of(ptx.tx.vin.begin(), ptx.tx.vin.end(), [&](const txin_v& s_e) -> bool + { + CHECKED_GET_SPECIFIC_VARIANT(s_e, const txin_to_key, in, false); + key_images += boost::to_string(in.k_image) + " "; + return true; + }); + THROW_WALLET_EXCEPTION_IF(!all_are_txin_to_key, error::unexpected_txin_type, ptx.tx); + + ptx.key_images = key_images; + ptx.fee = 0; + for (const auto &i: sd.sources) ptx.fee += i.amount; + for (const auto &i: dests) ptx.fee -= i.amount; + ptx.dust = 0; + ptx.dust_added_to_fee = false; + ptx.change_dts = sd.change_dts; +// ptx.selected_transfers = selected_transfers; + ptx.tx_key = rct::rct2sk(rct::identity()); // don't send it back to the untrusted view wallet + ptx.dests = sd.destinations; + ptx.construction_data = sd; + } + + s = obj_to_json_str(signed_txes); + if (s.empty()) + return false; + LOG_PRINT_L2("Saving signed tx data: " << s); + // save as binary as there's no implementation of loading a json_archive + if (!::serialization::dump_binary(signed_txes, s)) + return false; + return epee::file_io_utils::save_string_to_file(signed_filename, std::string(SIGNED_TX_PREFIX) + s); +} +//---------------------------------------------------------------------------------------------------- +bool wallet2::load_tx(const std::string &signed_filename, std::vector &ptx) +{ + std::string s; + boost::system::error_code errcode; + signed_tx_set signed_txs; + + if (!boost::filesystem::exists(signed_filename, errcode)) + { + LOG_PRINT_L0("File " << signed_filename << " does not exist: " << errcode); + return false; + } + + if (!epee::file_io_utils::load_file_to_string(signed_filename.c_str(), s)) + { + LOG_PRINT_L0("Failed to load from " << signed_filename); + return false; + } + const size_t magiclen = strlen(SIGNED_TX_PREFIX); + if (strncmp(s.c_str(), SIGNED_TX_PREFIX, magiclen)) + { + LOG_PRINT_L0("Bad magic from " << signed_filename); + return false; + } + if (!::serialization::parse_binary(std::string(s.c_str() + magiclen, s.size() - magiclen), signed_txs)) + { + LOG_PRINT_L0("Failed to parse data from " << signed_filename); + return false; + } + LOG_PRINT_L1("Loaded signed tx data from binary: " << signed_txs.ptx.size() << " transactions"); + + ptx = signed_txs.ptx; + + return true; +} +//---------------------------------------------------------------------------------------------------- uint64_t wallet2::get_fee_multiplier(uint32_t priority, bool use_new_fee) const { static const uint64_t old_multipliers[3] = {1, 2, 3}; @@ -2563,7 +2716,6 @@ uint64_t wallet2::get_fee_multiplier(uint32_t priority, bool use_new_fee) const THROW_WALLET_EXCEPTION_IF (false, error::invalid_priority); return 1; } - //---------------------------------------------------------------------------------------------------- // separated the call(s) to wallet2::transfer into their own function // @@ -2613,9 +2765,9 @@ std::vector wallet2::create_transactions(std::vector wallet2::create_transactions(std::vector wallet2::create_transactions(std::vector wallet2::create_transactions(std::vector wallet2::create_transactions(std::vector -void wallet2::get_outs(std::vector> &outs, const std::list &selected_transfers, size_t fake_outputs_count) +void wallet2::get_outs(std::vector> &outs, const std::list &selected_transfers, size_t fake_outputs_count) { LOG_PRINT_L2("fake_outputs_count: " << fake_outputs_count); outs.clear(); @@ -2688,8 +2840,8 @@ void wallet2::get_outs(std::vector> &outs, const std::listis_rct() ? 0 : it->amount()); + for(size_t idx: selected_transfers) + req_t.params.amounts.push_back(m_transfers[idx].is_rct() ? 0 : m_transfers[idx].amount()); std::sort(req_t.params.amounts.begin(), req_t.params.amounts.end()); auto end = std::unique(req_t.params.amounts.begin(), req_t.params.amounts.end()); req_t.params.amounts.resize(std::distance(req_t.params.amounts.begin(), end)); @@ -2708,12 +2860,13 @@ void wallet2::get_outs(std::vector> &outs, const std::listis_rct() ? 0 : it->amount(); + const transfer_details &td = m_transfers[idx]; + const uint64_t amount = td.is_rct() ? 0 : td.amount(); std::unordered_set seen_indices; // request more for rct in base recent (locked) coinbases are picked, since they're locked for longer - size_t requested_outputs_count = base_requested_outputs_count + (it->is_rct() ? CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW - CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE : 0); + size_t requested_outputs_count = base_requested_outputs_count + (td.is_rct() ? CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW - CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE : 0); size_t start = req.outputs.size(); // if there are just enough outputs to mix with, use all of them. @@ -2745,8 +2898,8 @@ void wallet2::get_outs(std::vector> &outs, const std::listm_global_output_index); - req.outputs.push_back({amount, it->m_global_output_index}); + seen_indices.emplace(td.m_global_output_index); + req.outputs.push_back({amount, td.m_global_output_index}); // while we still need more mixins while (num_found < requested_outputs_count) @@ -2798,15 +2951,16 @@ void wallet2::get_outs(std::vector> &outs, const std::list scanty_outs; size_t base = 0; outs.reserve(selected_transfers.size()); - for(transfer_container::iterator it: selected_transfers) + for(size_t idx: selected_transfers) { - size_t requested_outputs_count = base_requested_outputs_count + (it->is_rct() ? CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW - CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE : 0); + const transfer_details &td = m_transfers[idx]; + size_t requested_outputs_count = base_requested_outputs_count + (td.is_rct() ? CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW - CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE : 0); outs.push_back(std::vector()); outs.back().reserve(fake_outputs_count + 1); - const rct::key mask = it->is_rct() ? rct::commit(it->amount(), it->m_mask) : rct::zeroCommit(it->amount()); + const rct::key mask = td.is_rct() ? rct::commit(td.amount(), td.m_mask) : rct::zeroCommit(td.amount()); // pick real out first (it will be sorted when done) - outs.back().push_back(std::make_tuple(it->m_global_output_index, boost::get(it->m_tx.vout[it->m_internal_output_index].target).key, mask)); + outs.back().push_back(std::make_tuple(td.m_global_output_index, boost::get(td.m_tx.vout[td.m_internal_output_index].target).key, mask)); // then pick others in random order till we reach the required number // since we use an equiprobable pick here, we don't upset the triangular distribution @@ -2816,12 +2970,12 @@ void wallet2::get_outs(std::vector> &outs, const std::list())); - LOG_PRINT_L2("Looking for " << (fake_outputs_count+1) << " outputs of size " << print_money(it->is_rct() ? 0 : it->amount())); + LOG_PRINT_L2("Looking for " << (fake_outputs_count+1) << " outputs of size " << print_money(td.is_rct() ? 0 : td.amount())); for (size_t o = 0; o < requested_outputs_count && outs.back().size() < fake_outputs_count + 1; ++o) { size_t i = base + order[o]; - LOG_PRINT_L2("Index " << i << "/" << requested_outputs_count << ": idx " << req.outputs[i].index << " (real " << it->m_global_output_index << "), unlocked " << daemon_resp.outs[i].unlocked << ", key " << daemon_resp.outs[i].key); - if (req.outputs[i].index == it->m_global_output_index) // don't re-add real one + LOG_PRINT_L2("Index " << i << "/" << requested_outputs_count << ": idx " << req.outputs[i].index << " (real " << td.m_global_output_index << "), unlocked " << daemon_resp.outs[i].unlocked << ", key " << daemon_resp.outs[i].key); + if (req.outputs[i].index == td.m_global_output_index) // don't re-add real one continue; if (!daemon_resp.outs[i].unlocked) // don't add locked outs continue; @@ -2832,7 +2986,7 @@ void wallet2::get_outs(std::vector> &outs, const std::listis_rct() ? 0 : it->amount()] = outs.back().size(); + scanty_outs[td.is_rct() ? 0 : td.amount()] = outs.back().size(); } else { @@ -2845,18 +2999,19 @@ void wallet2::get_outs(std::vector> &outs, const std::list v; - const rct::key mask = it->is_rct() ? rct::commit(it->amount(), it->m_mask) : rct::zeroCommit(it->amount()); - v.push_back(std::make_tuple(it->m_global_output_index, boost::get(it->m_tx.vout[it->m_internal_output_index].target).key, mask)); + const rct::key mask = td.is_rct() ? rct::commit(td.amount(), td.m_mask) : rct::zeroCommit(td.amount()); + v.push_back(std::make_tuple(td.m_global_output_index, boost::get(td.m_tx.vout[td.m_internal_output_index].target).key, mask)); outs.push_back(v); } } } template -void wallet2::transfer_selected(const std::vector& dsts, const std::list selected_transfers, size_t fake_outputs_count, +void wallet2::transfer_selected(const std::vector& dsts, const std::list selected_transfers, size_t fake_outputs_count, uint64_t unlock_time, uint64_t fee, const std::vector& extra, T destination_split_strategy, const tx_dust_policy& dust_policy, cryptonote::transaction& tx, pending_tx &ptx) { using namespace cryptonote; @@ -2878,9 +3033,9 @@ void wallet2::transfer_selected(const std::vectoramount(); + found_money += m_transfers[idx].amount(); } LOG_PRINT_L2("wanted " << print_money(needed_money) << ", found " << print_money(found_money) << ", fee " << print_money(fee)); @@ -2894,11 +3049,11 @@ void wallet2::transfer_selected(const std::vector sources; - BOOST_FOREACH(transfer_container::iterator it, selected_transfers) + BOOST_FOREACH(size_t idx, selected_transfers) { sources.resize(sources.size()+1); cryptonote::tx_source_entry& src = sources.back(); - transfer_details& td = *it; + const transfer_details& td = m_transfers[idx]; src.amount = td.amount(); src.rct = td.is_rct(); //paste keys (fake and real) @@ -2983,9 +3138,15 @@ void wallet2::transfer_selected(const std::vector dsts, const std::list selected_transfers, size_t fake_outputs_count, +void wallet2::transfer_selected_rct(std::vector dsts, const std::list selected_transfers, size_t fake_outputs_count, uint64_t unlock_time, uint64_t fee, const std::vector& extra, cryptonote::transaction& tx, pending_tx &ptx) { using namespace cryptonote; @@ -3007,9 +3168,9 @@ void wallet2::transfer_selected_rct(std::vectoramount(); + found_money += m_transfers[idx].amount(); } LOG_PRINT_L2("wanted " << print_money(needed_money) << ", found " << print_money(found_money) << ", fee " << print_money(fee)); @@ -3022,11 +3183,11 @@ void wallet2::transfer_selected_rct(std::vector sources; - BOOST_FOREACH(transfer_container::iterator it, selected_transfers) + BOOST_FOREACH(size_t idx, selected_transfers) { sources.resize(sources.size()+1); cryptonote::tx_source_entry& src = sources.back(); - transfer_details& td = *it; + const transfer_details& td = m_transfers[idx]; src.amount = td.amount(); src.rct = td.is_rct(); //paste mixin transaction @@ -3096,6 +3257,12 @@ void wallet2::transfer_selected_rct(std::vector wallet2::create_transactions_2(std::vector selected_transfers; + std::list selected_transfers; std::vector dsts; cryptonote::transaction tx; pending_tx ptx; @@ -3332,7 +3499,7 @@ std::vector wallet2::create_transactions_2(std::vector wallet2::create_transactions_2(std::vector::const_iterator mi = tx.selected_transfers.begin(); mi != tx.selected_transfers.end(); ++mi) - tx_money += (*mi)->amount(); + for (size_t idx: tx.selected_transfers) + tx_money += m_transfers[idx].amount(); LOG_PRINT_L1(" Transaction " << (1+std::distance(txes.begin(), i)) << "/" << txes.size() << ": " << (tx.bytes+1023)/1024 << " kB, sending " << print_money(tx_money) << " in " << tx.selected_transfers.size() << " outputs to " << tx.dsts.size() << " destination(s), including " << @@ -3491,7 +3658,7 @@ std::vector wallet2::create_transactions_all(const cryptono std::vector unused_dust_indices; uint64_t accumulated_fee, accumulated_outputs, accumulated_change; struct TX { - std::list selected_transfers; + std::list selected_transfers; std::vector dsts; cryptonote::transaction tx; pending_tx ptx; @@ -3543,7 +3710,7 @@ std::vector wallet2::create_transactions_all(const cryptono LOG_PRINT_L2("Picking output " << idx << ", amount " << print_money(td.amount())); // add this output to the list to spend - tx.selected_transfers.push_back(m_transfers.begin() + idx); + tx.selected_transfers.push_back(idx); uint64_t available_amount = td.amount(); accumulated_outputs += available_amount; @@ -3620,8 +3787,8 @@ std::vector wallet2::create_transactions_all(const cryptono { TX &tx = *i; uint64_t tx_money = 0; - for (std::list::const_iterator mi = tx.selected_transfers.begin(); mi != tx.selected_transfers.end(); ++mi) - tx_money += (*mi)->amount(); + for (size_t idx: tx.selected_transfers) + tx_money += m_transfers[idx].amount(); LOG_PRINT_L1(" Transaction " << (1+std::distance(txes.begin(), i)) << "/" << txes.size() << ": " << (tx.bytes+1023)/1024 << " kB, sending " << print_money(tx_money) << " in " << tx.selected_transfers.size() << " outputs to " << tx.dsts.size() << " destination(s), including " << @@ -3658,14 +3825,14 @@ void wallet2::transfer_from(const std::vector &outs, size_t num_outputs, // select all dust inputs for transaction // throw if there are none uint64_t money = 0; - std::list selected_transfers; + std::list selected_transfers; #if 1 for (size_t n = 0; n < outs.size(); ++n) { const transfer_details& td = m_transfers[outs[n]]; if (!td.m_spent) { - selected_transfers.push_back (m_transfers.begin() + outs[n]); + selected_transfers.push_back (outs[n]); money += td.amount(); if (selected_transfers.size() >= num_outputs) break; @@ -3693,11 +3860,11 @@ void wallet2::transfer_from(const std::vector &outs, size_t num_outputs, //prepare inputs size_t i = 0; std::vector sources; - BOOST_FOREACH(transfer_container::iterator it, selected_transfers) + BOOST_FOREACH(size_t idx, selected_transfers) { sources.resize(sources.size()+1); cryptonote::tx_source_entry& src = sources.back(); - transfer_details& td = *it; + const transfer_details& td = m_transfers[idx]; src.amount = td.amount(); src.rct = td.is_rct(); @@ -3754,6 +3921,12 @@ void wallet2::transfer_from(const std::vector &outs, size_t num_outputs, ptx.selected_transfers = selected_transfers; ptx.tx_key = tx_key; ptx.dests = dsts; + ptx.construction_data.sources = sources; + ptx.construction_data.destinations = dsts; + ptx.construction_data.change_dts = change_dts; + ptx.construction_data.extra = tx.extra; + ptx.construction_data.unlock_time = unlock_time; + ptx.construction_data.use_rct = false; } //---------------------------------------------------------------------------------------------------- @@ -3969,9 +4142,9 @@ std::vector wallet2::create_unmixable_sweep_transactions(bo ptx_vector.push_back(ptx); // mark transfers to be used as "spent" - BOOST_FOREACH(transfer_container::iterator it, ptx.selected_transfers) + BOOST_FOREACH(size_t idx, ptx.selected_transfers) { - set_spent(*it, 0); + set_spent(idx, 0); } } @@ -3981,9 +4154,9 @@ std::vector wallet2::create_unmixable_sweep_transactions(bo for (auto & ptx : ptx_vector) { // mark transfers to be used as not spent - BOOST_FOREACH(transfer_container::iterator it2, ptx.selected_transfers) + BOOST_FOREACH(size_t idx2, ptx.selected_transfers) { - set_unspent(*it2); + set_unspent(idx2); } } @@ -3999,9 +4172,9 @@ std::vector wallet2::create_unmixable_sweep_transactions(bo for (auto & ptx : ptx_vector) { // mark transfers to be used as not spent - BOOST_FOREACH(transfer_container::iterator it2, ptx.selected_transfers) + BOOST_FOREACH(size_t idx2, ptx.selected_transfers) { - set_unspent(*it2); + set_unspent(idx2); } } @@ -4018,9 +4191,9 @@ std::vector wallet2::create_unmixable_sweep_transactions(bo for (auto & ptx : ptx_vector) { // mark transfers to be used as not spent - BOOST_FOREACH(transfer_container::iterator it2, ptx.selected_transfers) + BOOST_FOREACH(size_t idx2, ptx.selected_transfers) { - set_unspent(*it2); + set_unspent(idx2); } } diff --git a/src/wallet/wallet2.h b/src/wallet/wallet2.h index 89b613d34..d2d5fc7fd 100644 --- a/src/wallet/wallet2.h +++ b/src/wallet/wallet2.h @@ -151,6 +151,25 @@ namespace tools m_amount_in(utd.m_amount_in), m_amount_out(utd.m_amount_out), m_change(utd.m_change), m_block_height(height), m_dests(utd.m_dests), m_payment_id(utd.m_payment_id), m_timestamp(utd.m_timestamp) {} }; + struct tx_construction_data + { + std::vector sources; + std::vector destinations; + cryptonote::tx_destination_entry change_dts; + std::vector extra; + uint64_t unlock_time; + bool use_rct; + + BEGIN_SERIALIZE_OBJECT() + FIELD(sources) + FIELD(destinations) + FIELD(change_dts) + FIELD(extra) + VARINT_FIELD(unlock_time) + FIELD(use_rct) + END_SERIALIZE() + }; + typedef std::vector transfer_container; typedef std::unordered_multimap payment_container; @@ -160,10 +179,41 @@ namespace tools uint64_t dust, fee; bool dust_added_to_fee; cryptonote::tx_destination_entry change_dts; - std::list selected_transfers; + std::list selected_transfers; std::string key_images; crypto::secret_key tx_key; std::vector dests; + + tx_construction_data construction_data; + + BEGIN_SERIALIZE_OBJECT() + FIELD(tx) + VARINT_FIELD(dust) + VARINT_FIELD(fee) + FIELD(dust_added_to_fee) + FIELD(change_dts) + FIELD(selected_transfers) + FIELD(key_images) + FIELD(tx_key) + FIELD(dests) + FIELD(construction_data) + END_SERIALIZE() + }; + + struct unsigned_tx_set + { + std::vector txes; + BEGIN_SERIALIZE_OBJECT() + FIELD(txes) + END_SERIALIZE() + }; + + struct signed_tx_set + { + std::vector ptx; + BEGIN_SERIALIZE_OBJECT() + FIELD(ptx) + END_SERIALIZE() }; struct keys_file_data @@ -298,13 +348,16 @@ namespace tools template void transfer_from(const std::vector &outs, size_t num_outputs, uint64_t unlock_time, uint64_t needed_fee, T destination_split_strategy, const tx_dust_policy& dust_policy, const std::vector& extra, cryptonote::transaction& tx, pending_tx &ptx); template - void transfer_selected(const std::vector& dsts, const std::list selected_transfers, size_t fake_outputs_count, + void transfer_selected(const std::vector& dsts, const std::list selected_transfers, size_t fake_outputs_count, uint64_t unlock_time, uint64_t fee, const std::vector& extra, T destination_split_strategy, const tx_dust_policy& dust_policy, cryptonote::transaction& tx, pending_tx &ptx); - void transfer_selected_rct(std::vector dsts, const std::list selected_transfers, size_t fake_outputs_count, + void transfer_selected_rct(std::vector dsts, const std::list selected_transfers, size_t fake_outputs_count, uint64_t unlock_time, uint64_t fee, const std::vector& extra, cryptonote::transaction& tx, pending_tx &ptx); void commit_tx(pending_tx& ptx_vector); void commit_tx(std::vector& ptx_vector); + bool save_tx(const std::vector& ptx_vector, const std::string &filename); + bool sign_tx(const std::string &unsigned_filename, const std::string &signed_filename, std::function accept_func = NULL); + bool load_tx(const std::string &signed_filename, std::vector &ptx); std::vector create_transactions(std::vector dsts, const size_t fake_outs_count, const uint64_t unlock_time, uint32_t priority, const std::vector extra, bool trusted_daemon); std::vector create_transactions_2(std::vector dsts, const size_t fake_outs_count, const uint64_t unlock_time, uint32_t priority, const std::vector extra, bool trusted_daemon); std::vector create_transactions_all(const cryptonote::account_public_address &address, const size_t fake_outs_count, const uint64_t unlock_time, uint32_t priority, const std::vector extra, bool trusted_daemon); @@ -406,8 +459,8 @@ namespace tools std::vector select_available_unmixable_outputs(bool trusted_daemon); std::vector select_available_mixable_outputs(bool trusted_daemon); - size_t pop_best_value_from(const transfer_container &transfers, std::vector &unused_dust_indices, const std::list& selected_transfers) const; - size_t pop_best_value(std::vector &unused_dust_indices, const std::list& selected_transfers) const; + size_t pop_best_value_from(const transfer_container &transfers, std::vector &unused_dust_indices, const std::list& selected_transfers) const; + size_t pop_best_value(std::vector &unused_dust_indices, const std::list& selected_transfers) const; void set_tx_note(const crypto::hash &txid, const std::string ¬e); std::string get_tx_note(const crypto::hash &txid) const; @@ -445,7 +498,7 @@ namespace tools void fast_refresh(uint64_t stop_height, uint64_t &blocks_start_height, std::list &short_chain_history); void pull_next_blocks(uint64_t start_height, uint64_t &blocks_start_height, std::list &short_chain_history, const std::list &prev_blocks, std::list &blocks, std::vector &o_indices, bool &error); void process_blocks(uint64_t start_height, const std::list &blocks, const std::vector &o_indices, uint64_t& blocks_added); - uint64_t select_transfers(uint64_t needed_money, std::vector unused_transfers_indices, std::list& selected_transfers, bool trusted_daemon); + uint64_t select_transfers(uint64_t needed_money, std::vector unused_transfers_indices, std::list& selected_transfers, bool trusted_daemon); bool prepare_file_names(const std::string& file_path); void process_unconfirmed(const cryptonote::transaction& tx, uint64_t height); void process_outgoing(const cryptonote::transaction& tx, uint64_t height, uint64_t ts, uint64_t spent, uint64_t received); @@ -461,10 +514,10 @@ namespace tools uint64_t get_fee_multiplier(uint32_t priority, bool use_new_fee) const; float get_output_relatedness(const transfer_details &td0, const transfer_details &td1) const; std::vector pick_prefered_rct_inputs(uint64_t needed_money) const; - void set_spent(transfer_details &td, uint64_t height); - void set_unspent(transfer_details &td); + void set_spent(size_t idx, uint64_t height); + void set_unspent(size_t idx); template - void get_outs(std::vector> &outs, const std::list &selected_transfers, size_t fake_outputs_count); + void get_outs(std::vector> &outs, const std::list &selected_transfers, size_t fake_outputs_count); cryptonote::account_base m_account; std::string m_daemon_address; @@ -736,7 +789,7 @@ namespace tools // randomly select inputs for transaction // throw if requested send amount is greater than amount available to send - std::list selected_transfers; + std::list selected_transfers; uint64_t found_money = select_transfers(needed_money, unused_transfers_indices, selected_transfers, trusted_daemon); THROW_WALLET_EXCEPTION_IF(found_money < needed_money, error::not_enough_money, found_money, needed_money - fee, fee); @@ -748,8 +801,9 @@ namespace tools { 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) + BOOST_FOREACH(size_t idx, selected_transfers) { + const transfer_container::const_iterator it = m_transfers.begin() + idx; 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())); @@ -780,11 +834,11 @@ namespace tools //prepare inputs size_t i = 0; std::vector sources; - BOOST_FOREACH(transfer_container::iterator it, selected_transfers) + BOOST_FOREACH(size_t idx, selected_transfers) { sources.resize(sources.size()+1); cryptonote::tx_source_entry& src = sources.back(); - transfer_details& td = *it; + const transfer_details& td = m_transfers[idx]; src.amount = td.amount(); src.rct = false; //paste mixin transaction @@ -871,6 +925,12 @@ namespace tools ptx.selected_transfers = selected_transfers; ptx.tx_key = tx_key; ptx.dests = dsts; + ptx.construction_data.sources = sources; + ptx.construction_data.destinations = dsts; + ptx.construction_data.change_dts = change_dts; + ptx.construction_data.extra = tx.extra; + ptx.construction_data.unlock_time = unlock_time; + ptx.construction_data.use_rct = false; } diff --git a/tests/unit_tests/output_selection.cpp b/tests/unit_tests/output_selection.cpp index 4344d1ffc..d26f5ae1a 100644 --- a/tests/unit_tests/output_selection.cpp +++ b/tests/unit_tests/output_selection.cpp @@ -56,14 +56,14 @@ static tools::wallet2::transfer_container make_transfers_container(size_t N) auto i = std::find(unused_indices.begin(), unused_indices.end(), idx); \ ASSERT_TRUE(i != unused_indices.end()); \ unused_indices.erase(i); \ - selected.push_back(transfers.begin() + idx); \ + selected.push_back(idx); \ } while(0) #define PICK(expected) \ do { \ size_t idx = w.pop_best_value_from(transfers, unused_indices, selected); \ ASSERT_EQ(expected, idx); \ - selected.push_back(transfers.begin() + idx); \ + selected.push_back(idx); \ } while(0) TEST(select_outputs, one_out_of_N) @@ -76,7 +76,7 @@ TEST(select_outputs, one_out_of_N) tools::wallet2::transfer_container transfers = make_transfers_container(10); transfers[6].m_block_height = 700; std::vector unused_indices({0, 1, 2, 3, 4, 5, 6, 7, 8, 9}); - std::list selected; + std::list selected; SELECT(2); PICK(6); } @@ -93,7 +93,7 @@ TEST(select_outputs, order) transfers[3].m_block_height = 716; transfers[4].m_block_height = 701; std::vector unused_indices({0, 1, 2, 3, 4}); - std::list selected; + std::list selected; SELECT(0); PICK(3); // first the one that's far away PICK(2); // then the one that's close