diff --git a/src/cryptonote_core/blockchain.cpp b/src/cryptonote_core/blockchain.cpp index 5f9d6937b..e27261c46 100644 --- a/src/cryptonote_core/blockchain.cpp +++ b/src/cryptonote_core/blockchain.cpp @@ -1778,7 +1778,7 @@ bool Blockchain::get_outs(const COMMAND_RPC_GET_OUTPUTS_BIN::request& req, COMMA tx_out_index toi = m_db->get_output_tx_and_index(i.amount, i.index); bool unlocked = is_tx_spendtime_unlocked(m_db->get_tx_unlock_time(toi.first)); - res.outs.push_back({od.pubkey, od.commitment, unlocked}); + res.outs.push_back({od.pubkey, od.commitment, unlocked, od.height, toi.first}); } return true; } diff --git a/src/rpc/core_rpc_server.cpp b/src/rpc/core_rpc_server.cpp index 6ca01ed2c..558031f52 100644 --- a/src/rpc/core_rpc_server.cpp +++ b/src/rpc/core_rpc_server.cpp @@ -300,6 +300,8 @@ namespace cryptonote outkey.key = epee::string_tools::pod_to_hex(i.key); outkey.mask = epee::string_tools::pod_to_hex(i.mask); outkey.unlocked = i.unlocked; + outkey.height = i.height; + outkey.txid = epee::string_tools::pod_to_hex(i.txid); } res.status = CORE_RPC_STATUS_OK; diff --git a/src/rpc/core_rpc_server_commands_defs.h b/src/rpc/core_rpc_server_commands_defs.h index c08e43066..a4edc7538 100644 --- a/src/rpc/core_rpc_server_commands_defs.h +++ b/src/rpc/core_rpc_server_commands_defs.h @@ -49,7 +49,7 @@ namespace cryptonote // advance which version they will stop working with // Don't go over 32767 for any of these #define CORE_RPC_VERSION_MAJOR 1 -#define CORE_RPC_VERSION_MINOR 3 +#define CORE_RPC_VERSION_MINOR 4 #define CORE_RPC_VERSION (((CORE_RPC_VERSION_MAJOR)<<16)|(CORE_RPC_VERSION_MINOR)) struct COMMAND_RPC_GET_HEIGHT @@ -329,11 +329,15 @@ namespace cryptonote crypto::public_key key; rct::key mask; bool unlocked; + uint64_t height; + crypto::hash txid; BEGIN_KV_SERIALIZE_MAP() KV_SERIALIZE_VAL_POD_AS_BLOB(key) KV_SERIALIZE_VAL_POD_AS_BLOB(mask) KV_SERIALIZE(unlocked) + KV_SERIALIZE(height) + KV_SERIALIZE_VAL_POD_AS_BLOB(txid) END_KV_SERIALIZE_MAP() }; @@ -365,11 +369,15 @@ namespace cryptonote std::string key; std::string mask; bool unlocked; + uint64_t height; + std::string txid; BEGIN_KV_SERIALIZE_MAP() KV_SERIALIZE(key) KV_SERIALIZE(mask) KV_SERIALIZE(unlocked) + KV_SERIALIZE(height) + KV_SERIALIZE(txid) END_KV_SERIALIZE_MAP() }; diff --git a/src/simplewallet/simplewallet.cpp b/src/simplewallet/simplewallet.cpp index 088d5e2ac..29d68c055 100644 --- a/src/simplewallet/simplewallet.cpp +++ b/src/simplewallet/simplewallet.cpp @@ -371,6 +371,17 @@ bool simple_wallet::set_always_confirm_transfers(const std::vector return true; } +bool simple_wallet::set_print_ring_members(const std::vector &args/* = std::vector()*/) +{ + const auto pwd_container = get_and_verify_password(); + if (pwd_container) + { + m_wallet->print_ring_members(is_it_true(args[1])); + m_wallet->rewrite(m_wallet_file, pwd_container->password()); + } + return true; +} + bool simple_wallet::set_store_tx_info(const std::vector &args/* = std::vector()*/) { if (m_wallet->watch_only()) @@ -570,7 +581,7 @@ simple_wallet::simple_wallet() m_cmd_binder.set_handler("viewkey", boost::bind(&simple_wallet::viewkey, this, _1), tr("Display private view key")); m_cmd_binder.set_handler("spendkey", boost::bind(&simple_wallet::spendkey, this, _1), tr("Display private spend key")); m_cmd_binder.set_handler("seed", boost::bind(&simple_wallet::seed, this, _1), tr("Display Electrum-style mnemonic seed")); - m_cmd_binder.set_handler("set", boost::bind(&simple_wallet::set_variable, this, _1), tr("Available options: seed language - set wallet seed language; always-confirm-transfers <1|0> - whether to confirm unsplit txes; store-tx-info <1|0> - whether to store outgoing tx info (destination address, payment ID, tx secret key) for future reference; default-mixin - set default mixin (default is 4); auto-refresh <1|0> - whether to automatically sync new blocks from the daemon; refresh-type - set wallet refresh behaviour; priority [1|2|3] - normal/elevated/priority fee; confirm-missing-payment-id <1|0>")); + m_cmd_binder.set_handler("set", boost::bind(&simple_wallet::set_variable, this, _1), tr("Available options: seed language - set wallet seed language; always-confirm-transfers <1|0> - whether to confirm unsplit txes; print-ring-members <1|0> - whether to print detailed information about ring members during confirmation; store-tx-info <1|0> - whether to store outgoing tx info (destination address, payment ID, tx secret key) for future reference; default-mixin - set default mixin (default is 4); auto-refresh <1|0> - whether to automatically sync new blocks from the daemon; refresh-type - set wallet refresh behaviour; priority [1|2|3] - normal/elevated/priority fee; confirm-missing-payment-id <1|0>")); m_cmd_binder.set_handler("rescan_spent", boost::bind(&simple_wallet::rescan_spent, this, _1), tr("Rescan blockchain for spent outputs")); m_cmd_binder.set_handler("get_tx_key", boost::bind(&simple_wallet::get_tx_key, this, _1), tr("Get transaction key (r) for a given ")); m_cmd_binder.set_handler("check_tx_key", boost::bind(&simple_wallet::check_tx_key, this, _1), tr("Check amount going to
in ")); @@ -596,6 +607,7 @@ bool simple_wallet::set_variable(const std::vector &args) { success_msg_writer() << "seed = " << m_wallet->get_seed_language(); success_msg_writer() << "always-confirm-transfers = " << m_wallet->always_confirm_transfers(); + success_msg_writer() << "print-ring-members = " << m_wallet->print_ring_members(); success_msg_writer() << "store-tx-info = " << m_wallet->store_tx_info(); success_msg_writer() << "default-mixin = " << m_wallet->default_mixin(); success_msg_writer() << "auto-refresh = " << m_wallet->auto_refresh(); @@ -632,6 +644,19 @@ bool simple_wallet::set_variable(const std::vector &args) return true; } } + else if (args[0] == "print-ring-members") + { + if (args.size() <= 1) + { + fail_msg_writer() << tr("set print-ring-members: needs an argument (0 or 1)"); + return true; + } + else + { + set_print_ring_members(args); + return true; + } + } else if (args[0] == "store-tx-info") { if (args.size() <= 1) @@ -1119,10 +1144,12 @@ bool simple_wallet::handle_command_line(const boost::program_options::variables_ return true; } //---------------------------------------------------------------------------------------------------- -bool simple_wallet::try_connect_to_daemon(bool silent) +bool simple_wallet::try_connect_to_daemon(bool silent, uint32_t* version) { - uint32_t version = 0; - if (!m_wallet->check_connection(&version)) + uint32_t version_ = 0; + if (!version) + version = &version_; + if (!m_wallet->check_connection(version)) { if (!silent) fail_msg_writer() << tr("wallet failed to connect to daemon: ") << m_wallet->get_daemon_address() << ". " << @@ -1130,10 +1157,10 @@ bool simple_wallet::try_connect_to_daemon(bool silent) "Please make sure daemon is running or restart the wallet with the correct daemon address."); return false; } - if (!m_allow_mismatched_daemon_version && ((version >> 16) != CORE_RPC_VERSION_MAJOR)) + if (!m_allow_mismatched_daemon_version && ((*version >> 16) != CORE_RPC_VERSION_MAJOR)) { if (!silent) - fail_msg_writer() << boost::format(tr("Daemon uses a different RPC major version (%u) than the wallet (%u): %s. Either update one of them, or use --allow-mismatched-daemon-version.")) % (version>>16) % CORE_RPC_VERSION_MAJOR % m_wallet->get_daemon_address(); + fail_msg_writer() << boost::format(tr("Daemon uses a different RPC major version (%u) than the wallet (%u): %s. Either update one of them, or use --allow-mismatched-daemon-version.")) % (*version>>16) % CORE_RPC_VERSION_MAJOR % m_wallet->get_daemon_address(); return false; } return true; @@ -1865,6 +1892,108 @@ bool simple_wallet::rescan_spent(const std::vector &args) return true; } //---------------------------------------------------------------------------------------------------- +bool simple_wallet::print_ring_members(const std::vector& ptx_vector, std::ostream& ostr) +{ + uint32_t version; + if (!try_connect_to_daemon(false, &version)) + { + fail_msg_writer() << tr("failed to connect to the daemon"); + return false; + } + // available for RPC version 1.4 or higher + if (version < 0x10004) + return true; + std::string err; + uint64_t blockchain_height = get_daemon_blockchain_height(err); + if (!err.empty()) + { + fail_msg_writer() << tr("failed to get blockchain height: ") << err; + return false; + } + // for each transaction + for (size_t n = 0; n < ptx_vector.size(); ++n) + { + const cryptonote::transaction& tx = ptx_vector[n].tx; + const tools::wallet2::tx_construction_data& construction_data = ptx_vector[n].construction_data; + ostr << boost::format(tr("\nTransaction %llu/%llu: txid=%s")) % (n + 1) % ptx_vector.size() % cryptonote::get_transaction_hash(tx); + // for each input + std::vector spent_key_height(tx.vin.size()); + std::vector spent_key_txid (tx.vin.size()); + for (size_t i = 0; i < tx.vin.size(); ++i) + { + if (tx.vin[i].type() != typeid(cryptonote::txin_to_key)) + continue; + const cryptonote::txin_to_key& in_key = boost::get(tx.vin[i]); + const cryptonote::tx_source_entry& source = construction_data.sources[i]; + ostr << boost::format(tr("\nInput %llu/%llu: amount=%s")) % (i + 1) % tx.vin.size() % print_money(source.amount); + // convert relative offsets of ring member keys into absolute offsets (indices) associated with the amount + std::vector absolute_offsets = cryptonote::relative_output_offsets_to_absolute(in_key.key_offsets); + // get block heights from which those ring member keys originated + COMMAND_RPC_GET_OUTPUTS_BIN::request req = AUTO_VAL_INIT(req); + req.outputs.resize(absolute_offsets.size()); + for (size_t j = 0; j < absolute_offsets.size(); ++j) + { + req.outputs[j].amount = in_key.amount; + req.outputs[j].index = absolute_offsets[j]; + } + COMMAND_RPC_GET_OUTPUTS_BIN::response res = AUTO_VAL_INIT(res); + bool r = net_utils::invoke_http_bin_remote_command2(m_wallet->get_daemon_address() + "/get_outs.bin", req, res, m_http_client); + err = interpret_rpc_response(r, res.status); + if (!err.empty()) + { + fail_msg_writer() << tr("failed to get output: ") << err; + return false; + } + // make sure that returned block heights are less than blockchain height + for (auto& res_out : res.outs) + { + if (res_out.height >= blockchain_height) + { + fail_msg_writer() << tr("output key's originating block height shouldn't be higher than the blockchain height"); + return false; + } + } + ostr << tr("\nOriginating block heights: "); + for (size_t j = 0; j < absolute_offsets.size(); ++j) + ostr << tr(j == source.real_output ? " *" : " ") << res.outs[j].height; + spent_key_height[i] = res.outs[source.real_output].height; + spent_key_txid [i] = res.outs[source.real_output].txid; + // visualize the distribution, using the code by moneroexamples onion-monero-viewer + const uint64_t resolution = 79; + std::string ring_str(resolution, '_'); + for (size_t j = 0; j < absolute_offsets.size(); ++j) + { + uint64_t pos = (res.outs[j].height * resolution) / blockchain_height; + ring_str[pos] = 'o'; + } + uint64_t pos = (res.outs[source.real_output].height * resolution) / blockchain_height; + ring_str[pos] = '*'; + ostr << tr("\n|") << ring_str << tr("|\n"); + } + // warn if rings contain keys originating from the same tx or temporally very close block heights + bool are_keys_from_same_tx = false; + bool are_keys_from_close_height = false; + for (size_t i = 0; i < tx.vin.size(); ++i) { + for (size_t j = i + 1; j < tx.vin.size(); ++j) + { + if (spent_key_txid[i] == spent_key_txid[j]) + are_keys_from_same_tx = true; + if (std::abs(spent_key_height[i] - spent_key_height[j]) < 5) + are_keys_from_close_height = true; + } + } + if (are_keys_from_same_tx || are_keys_from_close_height) + { + ostr + << tr("\nWarning: Some input keys being spent are from ") + << tr(are_keys_from_same_tx ? "the same transaction" : "blocks that are temporally very close") + << tr(", which can break the anonymity of ring signature. Make sure this is intentional!"); + } + ostr << ENDL; + } + return true; +} +//---------------------------------------------------------------------------------------------------- bool simple_wallet::transfer_main(int transfer_type, const std::vector &args_) { if (!try_connect_to_daemon()) @@ -2075,7 +2204,12 @@ bool simple_wallet::transfer_main(int transfer_type, const std::vectorprint_ring_members()) + { + if (!print_ring_members(ptx_vector, prompt)) + return true; + } + prompt << ENDL << tr("Is this okay? (Y/Yes/N/No): "); std::string accepted = command_line::input_line(prompt.str()); if (std::cin.eof()) @@ -2503,19 +2637,21 @@ bool simple_wallet::sweep_all(const std::vector &args_) total_sent += m_wallet->get_transfer_details(i).amount(); } - std::string prompt_str; + std::ostringstream prompt; + if (!print_ring_members(ptx_vector, prompt)) + return true; if (ptx_vector.size() > 1) { - prompt_str = (boost::format(tr("Sweeping %s in %llu transactions for a total fee of %s. Is this okay? (Y/Yes/N/No): ")) % + prompt << boost::format(tr("Sweeping %s in %llu transactions for a total fee of %s. Is this okay? (Y/Yes/N/No): ")) % print_money(total_sent) % ((unsigned long long)ptx_vector.size()) % - print_money(total_fee)).str(); + print_money(total_fee); } else { - prompt_str = (boost::format(tr("Sweeping %s for a total fee of %s. Is this okay? (Y/Yes/N/No)")) % + prompt << boost::format(tr("Sweeping %s for a total fee of %s. Is this okay? (Y/Yes/N/No)")) % print_money(total_sent) % - print_money(total_fee)).str(); + print_money(total_fee); } - std::string accepted = command_line::input_line(prompt_str); + std::string accepted = command_line::input_line(prompt.str()); if (std::cin.eof()) return true; if (!command_line::is_yes(accepted)) diff --git a/src/simplewallet/simplewallet.h b/src/simplewallet/simplewallet.h index 753bca74d..318d8d7e0 100644 --- a/src/simplewallet/simplewallet.h +++ b/src/simplewallet/simplewallet.h @@ -106,6 +106,7 @@ namespace cryptonote */ bool seed_set_language(const std::vector &args = std::vector()); bool set_always_confirm_transfers(const std::vector &args = std::vector()); + bool set_print_ring_members(const std::vector &args = std::vector()); bool set_store_tx_info(const std::vector &args = std::vector()); bool set_default_mixin(const std::vector &args = std::vector()); bool set_auto_refresh(const std::vector &args = std::vector()); @@ -160,11 +161,12 @@ namespace cryptonote bool show_transfer(const std::vector &args); uint64_t get_daemon_blockchain_height(std::string& err); - bool try_connect_to_daemon(bool silent = false); + bool try_connect_to_daemon(bool silent = false, uint32_t* version = nullptr); bool ask_wallet_create_if_needed(); bool accept_loaded_tx(const std::function get_num_txes, const std::function &get_tx, const std::string &extra_message = std::string()); bool accept_loaded_tx(const tools::wallet2::unsigned_tx_set &txs); bool accept_loaded_tx(const tools::wallet2::signed_tx_set &txs); + bool print_ring_members(const std::vector& ptx_vector, std::ostream& ostr); /*! * \brief Prints the seed with a nice message diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp index 1550787e5..b98d6a97a 100644 --- a/src/wallet/wallet2.cpp +++ b/src/wallet/wallet2.cpp @@ -1808,6 +1808,9 @@ bool wallet2::store_keys(const std::string& keys_file_name, const std::string& p value2.SetInt(m_always_confirm_transfers ? 1 :0); json.AddMember("always_confirm_transfers", value2, json.GetAllocator()); + value2.SetInt(m_print_ring_members ? 1 :0); + json.AddMember("print_ring_members", value2, json.GetAllocator()); + value2.SetInt(m_store_tx_info ? 1 :0); json.AddMember("store_tx_info", value2, json.GetAllocator()); @@ -1890,6 +1893,7 @@ bool wallet2::load_keys(const std::string& keys_file_name, const std::string& pa is_old_file_format = true; m_watch_only = false; m_always_confirm_transfers = false; + m_print_ring_members = false; m_default_mixin = 0; m_default_priority = 0; m_auto_refresh = true; @@ -1920,6 +1924,8 @@ bool wallet2::load_keys(const std::string& keys_file_name, const std::string& pa m_watch_only = field_watch_only; GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, always_confirm_transfers, int, Int, false, true); m_always_confirm_transfers = field_always_confirm_transfers; + GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, print_ring_members, int, Int, false, true); + m_print_ring_members = field_print_ring_members; GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, store_tx_keys, int, Int, false, true); GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, store_tx_info, int, Int, false, true); m_store_tx_info = ((field_store_tx_keys != 0) || (field_store_tx_info != 0)); diff --git a/src/wallet/wallet2.h b/src/wallet/wallet2.h index e1eafbae3..270c94033 100644 --- a/src/wallet/wallet2.h +++ b/src/wallet/wallet2.h @@ -98,7 +98,7 @@ namespace tools }; private: - wallet2(const wallet2&) : m_run(true), m_callback(0), m_testnet(false), m_always_confirm_transfers(true), m_store_tx_info(true), m_default_mixin(0), m_default_priority(0), m_refresh_type(RefreshOptimizeCoinbase), m_auto_refresh(true), m_refresh_from_block_height(0), m_confirm_missing_payment_id(true) {} + wallet2(const wallet2&) : m_run(true), m_callback(0), m_testnet(false), m_always_confirm_transfers(true), m_print_ring_members(false), m_store_tx_info(true), m_default_mixin(0), m_default_priority(0), m_refresh_type(RefreshOptimizeCoinbase), m_auto_refresh(true), m_refresh_from_block_height(0), m_confirm_missing_payment_id(true) {} public: static const char* tr(const char* str);// { return i18n_translate(str, "cryptonote::simple_wallet"); } @@ -119,7 +119,7 @@ namespace tools //! Uses stdin and stdout. Returns a wallet2 and password for wallet with no file if no errors. static std::pair, password_container> make_new(const boost::program_options::variables_map& vm); - wallet2(bool testnet = false, bool restricted = false) : m_run(true), m_callback(0), m_testnet(testnet), m_always_confirm_transfers(true), m_store_tx_info(true), m_default_mixin(0), m_default_priority(0), m_refresh_type(RefreshOptimizeCoinbase), m_auto_refresh(true), m_refresh_from_block_height(0), m_confirm_missing_payment_id(true), m_restricted(restricted), is_old_file_format(false) {} + wallet2(bool testnet = false, bool restricted = false) : m_run(true), m_callback(0), m_testnet(testnet), m_always_confirm_transfers(true), m_print_ring_members(false), m_store_tx_info(true), m_default_mixin(0), m_default_priority(0), m_refresh_type(RefreshOptimizeCoinbase), m_auto_refresh(true), m_refresh_from_block_height(0), m_confirm_missing_payment_id(true), m_restricted(restricted), is_old_file_format(false) {} struct transfer_details { uint64_t m_block_height; @@ -476,6 +476,8 @@ namespace tools bool always_confirm_transfers() const { return m_always_confirm_transfers; } void always_confirm_transfers(bool always) { m_always_confirm_transfers = always; } + bool print_ring_members() const { return m_print_ring_members; } + void print_ring_members(bool value) { m_print_ring_members = value; } bool store_tx_info() const { return m_store_tx_info; } void store_tx_info(bool store) { m_store_tx_info = store; } uint32_t default_mixin() const { return m_default_mixin; } @@ -624,6 +626,7 @@ namespace tools bool is_old_file_format; /*!< Whether the wallet file is of an old file format */ bool m_watch_only; /*!< no spend key */ bool m_always_confirm_transfers; + bool m_print_ring_members; bool m_store_tx_info; /*!< request txkey to be returned in RPC, and store in the wallet cache file */ uint32_t m_default_mixin; uint32_t m_default_priority;