diff options
Diffstat (limited to 'src/wallet/wallet2.cpp')
-rw-r--r-- | src/wallet/wallet2.cpp | 314 |
1 files changed, 221 insertions, 93 deletions
diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp index 221dd8e0b..0ad74f52a 100644 --- a/src/wallet/wallet2.cpp +++ b/src/wallet/wallet2.cpp @@ -51,7 +51,6 @@ using namespace epee; #include "cryptonote_protocol/blobdatatype.h" #include "mnemonics/electrum-words.h" #include "common/i18n.h" -#include "common/dns_utils.h" #include "common/util.h" #include "rapidjson/document.h" #include "rapidjson/writer.h" @@ -77,8 +76,8 @@ 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\002" -#define SIGNED_TX_PREFIX "Monero signed tx set\002" +#define UNSIGNED_TX_PREFIX "Monero unsigned tx set\003" +#define SIGNED_TX_PREFIX "Monero signed tx set\003" #define RECENT_OUTPUT_RATIO (0.25) // 25% of outputs are from the recent zone #define RECENT_OUTPUT_ZONE (5 * 86400) // last 5 days are the recent zone @@ -93,6 +92,8 @@ using namespace cryptonote; ioservice.stop(); \ } while(0) +#define KEY_IMAGE_EXPORT_FILE_MAGIC "Monero key image export\002" + namespace { // Create on-demand to prevent static initialization order fiasco issues. @@ -1005,8 +1006,11 @@ void wallet2::process_new_transaction(const cryptonote::transaction& tx, const s payment.m_block_height = height; payment.m_unlock_time = tx.unlock_time; payment.m_timestamp = ts; - if (pool) + if (pool) { m_unconfirmed_payments.emplace(payment_id, payment); + if (0 != m_callback) + m_callback->on_unconfirmed_money_received(height, tx, payment.m_amount); + } else m_payments.emplace(payment_id, payment); LOG_PRINT_L2("Payment found in " << (pool ? "pool" : "block") << ": " << payment_id << " / " << payment.m_tx_hash << " / " << payment.m_amount); @@ -1613,6 +1617,9 @@ void wallet2::refresh(uint64_t start_height, uint64_t & blocks_fetched, bool& re // and then fall through to regular refresh processing } + // If stop() is called during fast refresh we don't need to continue + if(!m_run.load(std::memory_order_relaxed)) + return; pull_blocks(start_height, blocks_start_height, short_chain_history, blocks, o_indices); // always reset start_height to 0 to force short_chain_ history to be used on // subsequent pulls in this refresh. @@ -1632,7 +1639,7 @@ void wallet2::refresh(uint64_t start_height, uint64_t & blocks_fetched, bool& re process_blocks(blocks_start_height, blocks, o_indices, added_blocks); blocks_fetched += added_blocks; pull_thread.join(); - if(!added_blocks) + if(blocks_start_height == next_blocks_start_height) break; // switch to the new blocks from the daemon @@ -1668,7 +1675,9 @@ void wallet2::refresh(uint64_t start_height, uint64_t & blocks_fetched, bool& re try { - update_pool_state(); + // If stop() is called we don't need to check pending transactions + if(m_run.load(std::memory_order_relaxed)) + update_pool_state(); } catch (...) { @@ -1804,6 +1813,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()); @@ -1886,6 +1898,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; @@ -1916,6 +1929,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)); @@ -2139,7 +2154,7 @@ void wallet2::rewrite(const std::string& wallet_name, const std::string& passwor prepare_file_names(wallet_name); boost::system::error_code ignored_ec; THROW_WALLET_EXCEPTION_IF(!boost::filesystem::exists(m_keys_file, ignored_ec), error::file_not_found, m_keys_file); - bool r = store_keys(m_keys_file, password, false); + bool r = store_keys(m_keys_file, password, m_watch_only); THROW_WALLET_EXCEPTION_IF(!r, error::file_save_error, m_keys_file); } /*! @@ -2854,74 +2869,7 @@ std::vector<std::vector<cryptonote::tx_destination_entry>> split_amounts( return retVal; } } // anonymous namespace - -/** - * @brief gets a monero address from the TXT record of a DNS entry - * - * gets the monero address from the TXT record of the DNS entry associated - * with <url>. If this lookup fails, or the TXT record does not contain an - * XMR address in the correct format, returns an empty string. <dnssec_valid> - * will be set true or false according to whether or not the DNS query passes - * DNSSEC validation. - * - * @param url the url to look up - * @param dnssec_valid return-by-reference for DNSSEC status of query - * - * @return a monero address (as a string) or an empty string - */ -std::vector<std::string> wallet2::addresses_from_url(const std::string& url, bool& dnssec_valid) -{ - std::vector<std::string> addresses; - // get txt records - bool dnssec_available, dnssec_isvalid; - std::string oa_addr = tools::DNSResolver::instance().get_dns_format_from_oa_address(url); - auto records = tools::DNSResolver::instance().get_txt_record(oa_addr, dnssec_available, dnssec_isvalid); - - // TODO: update this to allow for conveying that dnssec was not available - if (dnssec_available && dnssec_isvalid) - { - dnssec_valid = true; - } - else dnssec_valid = false; - - // for each txt record, try to find a monero address in it. - for (auto& rec : records) - { - std::string addr = address_from_txt_record(rec); - if (addr.size()) - { - addresses.push_back(addr); - } - } - - return addresses; -} - //---------------------------------------------------------------------------------------------------- -// TODO: parse the string in a less stupid way, probably with regex -std::string wallet2::address_from_txt_record(const std::string& s) -{ - // make sure the txt record has "oa1:xmr" and find it - auto pos = s.find("oa1:xmr"); - - // search from there to find "recipient_address=" - pos = s.find("recipient_address=", pos); - - pos += 18; // move past "recipient_address=" - - // find the next semicolon - auto pos2 = s.find(";", pos); - if (pos2 != std::string::npos) - { - // length of address == 95, we can at least validate that much here - if (pos2 - pos == 95) - { - return s.substr(pos, 95); - } - } - return std::string(); -} - crypto::hash wallet2::get_payment_id(const pending_tx &ptx) const { std::vector<tx_extra_field> tx_extra_fields; @@ -2946,6 +2894,24 @@ crypto::hash wallet2::get_payment_id(const pending_tx &ptx) const } return payment_id; } + +crypto::hash8 wallet2::get_short_payment_id(const pending_tx &ptx) const +{ + crypto::hash8 payment_id8 = null_hash8; + std::vector<tx_extra_field> tx_extra_fields; + if(!parse_tx_extra(ptx.tx.extra, tx_extra_fields)) + return payment_id8; + cryptonote::tx_extra_nonce extra_nonce; + if (find_tx_extra_field_by_type(tx_extra_fields, extra_nonce)) + { + if(get_encrypted_payment_id_from_tx_extra_nonce(extra_nonce.nonce, payment_id8)) + { + decrypt_payment_id(payment_id8, ptx.dests[0].addr.m_view_public_key, ptx.tx_key); + } + } + return payment_id8; +} + //---------------------------------------------------------------------------------------------------- // take a pending tx and actually send it to the daemon void wallet2::commit_tx(pending_tx& ptx) @@ -3016,19 +2982,47 @@ bool wallet2::save_tx(const std::vector<pending_tx>& ptx_vector, const std::stri LOG_PRINT_L0("saving " << ptx_vector.size() << " transactions"); unsigned_tx_set txs; for (auto &tx: ptx_vector) - txs.txes.push_back(tx.construction_data); + { + tx_construction_data construction_data = tx.construction_data; + // Short payment id is encrypted with tx_key. + // Since sign_tx() generates new tx_keys and encrypts the payment id, we need to save the decrypted payment ID + // Get decrypted payment id from pending_tx + crypto::hash8 payment_id = get_short_payment_id(tx); + if (payment_id != null_hash8) + { + // Remove encrypted + remove_field_from_tx_extra(construction_data.extra, typeid(cryptonote::tx_extra_nonce)); + // Add decrypted + std::string extra_nonce; + set_encrypted_payment_id_to_tx_extra_nonce(extra_nonce, payment_id); + if (!add_extra_nonce_to_tx_extra(construction_data.extra, extra_nonce)) + { + LOG_ERROR("Failed to add decrypted payment id to tx extra"); + return false; + } + LOG_PRINT_L1("Decrypted payment ID: " << payment_id); + } + // Save tx construction_data to unsigned_tx_set + txs.txes.push_back(construction_data); + } + txs.transfers = m_transfers; - 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)) + // save as binary + std::ostringstream oss; + boost::archive::portable_binary_oarchive ar(oss); + try + { + ar << txs; + } + catch (...) + { return false; - return epee::file_io_utils::save_string_to_file(filename, std::string(UNSIGNED_TX_PREFIX) + s); + } + LOG_PRINT_L2("Saving unsigned tx data: " << oss.str()); + return epee::file_io_utils::save_string_to_file(filename, std::string(UNSIGNED_TX_PREFIX) + oss.str()); } //---------------------------------------------------------------------------------------------------- -bool wallet2::sign_tx(const std::string &unsigned_filename, const std::string &signed_filename, std::vector<wallet2::pending_tx> &txs, std::function<bool(const unsigned_tx_set&)> accept_func) +bool wallet2::load_unsigned_tx(const std::string &unsigned_filename, unsigned_tx_set &exported_txs) { std::string s; boost::system::error_code errcode; @@ -3049,20 +3043,40 @@ bool wallet2::sign_tx(const std::string &unsigned_filename, const std::string &s 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)) + s = s.substr(magiclen); + try + { + std::istringstream iss(s); + boost::archive::portable_binary_iarchive ar(iss); + ar >> exported_txs; + } + catch (...) { 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"); + return true; +} +//---------------------------------------------------------------------------------------------------- +bool wallet2::sign_tx(const std::string &unsigned_filename, const std::string &signed_filename, std::vector<wallet2::pending_tx> &txs, std::function<bool(const unsigned_tx_set&)> accept_func) +{ + unsigned_tx_set exported_txs; + if(!load_unsigned_tx(unsigned_filename, exported_txs)) + return false; + if (accept_func && !accept_func(exported_txs)) { LOG_PRINT_L1("Transactions rejected by callback"); return false; } + return sign_tx(exported_txs, signed_filename, txs); +} +//---------------------------------------------------------------------------------------------------- +bool wallet2::sign_tx(unsigned_tx_set &exported_txs, const std::string &signed_filename, std::vector<wallet2::pending_tx> &txs) +{ import_outputs(exported_txs.transfers); // sign the transactions @@ -3123,14 +3137,19 @@ bool wallet2::sign_tx(const std::string &unsigned_filename, const std::string &s signed_txes.key_images[i] = m_transfers[i].m_key_image; } - 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)) + // save as binary + std::ostringstream oss; + boost::archive::portable_binary_oarchive ar(oss); + try + { + ar << signed_txes; + } + catch(...) + { return false; - return epee::file_io_utils::save_string_to_file(signed_filename, std::string(SIGNED_TX_PREFIX) + s); + } + LOG_PRINT_L3("Saving signed tx data: " << oss.str()); + return epee::file_io_utils::save_string_to_file(signed_filename, std::string(SIGNED_TX_PREFIX) + oss.str()); } //---------------------------------------------------------------------------------------------------- bool wallet2::load_tx(const std::string &signed_filename, std::vector<tools::wallet2::pending_tx> &ptx, std::function<bool(const signed_tx_set&)> accept_func) @@ -3156,7 +3175,14 @@ bool wallet2::load_tx(const std::string &signed_filename, std::vector<tools::wal 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)) + s = s.substr(magiclen); + try + { + std::istringstream iss(s); + boost::archive::portable_binary_iarchive ar(iss); + ar >> signed_txs; + } + catch (...) { LOG_PRINT_L0("Failed to parse data from " << signed_filename); return false; @@ -3521,6 +3547,23 @@ void wallet2::get_outs(std::vector<std::vector<entry>> &outs, const std::list<si outs.back().reserve(fake_outputs_count + 1); const rct::key mask = td.is_rct() ? rct::commit(td.amount(), td.m_mask) : rct::zeroCommit(td.amount()); + // make sure the real outputs we asked for are really included, along + // with the correct key and mask: this guards against an active attack + // where the node sends dummy data for all outputs, and we then send + // the real one, which the node can then tell from the fake outputs, + // as it has different data than the dummy data it had sent earlier + bool real_out_found = false; + for (size_t n = 0; n < requested_outputs_count; ++n) + { + size_t i = base + n; + 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; + } + THROW_WALLET_EXCEPTION_IF(!real_out_found, error::wallet_internal_error, + "Daemon response did not include the requested real output"); + // pick real out first (it will be sorted when done) outs.back().push_back(std::make_tuple(td.m_global_output_index, boost::get<txout_to_key>(td.m_tx.vout[td.m_internal_output_index].target).key, mask)); @@ -4800,6 +4843,27 @@ crypto::public_key wallet2::get_tx_pub_key_from_received_outs(const tools::walle "Public key yielding at least one output wasn't found in the transaction extra"); return cryptonote::null_pkey; } + +bool wallet2::export_key_images(const std::string filename) +{ + std::vector<std::pair<crypto::key_image, crypto::signature>> ski = export_key_images(); + std::string magic(KEY_IMAGE_EXPORT_FILE_MAGIC, strlen(KEY_IMAGE_EXPORT_FILE_MAGIC)); + const cryptonote::account_public_address &keys = get_account().get_keys().m_account_address; + + std::string data; + data += std::string((const char *)&keys.m_spend_public_key, sizeof(crypto::public_key)); + data += std::string((const char *)&keys.m_view_public_key, sizeof(crypto::public_key)); + for (const auto &i: ski) + { + data += std::string((const char *)&i.first, sizeof(crypto::key_image)); + data += std::string((const char *)&i.second, sizeof(crypto::signature)); + } + + // encrypt data, keep magic plaintext + std::string ciphertext = encrypt_with_view_secret_key(data); + return epee::file_io_utils::save_string_to_file(filename, magic + ciphertext); +} + //---------------------------------------------------------------------------------------------------- std::vector<std::pair<crypto::key_image, crypto::signature>> wallet2::export_key_images() const { @@ -4850,6 +4914,70 @@ std::vector<std::pair<crypto::key_image, crypto::signature>> wallet2::export_key } return ski; } + +uint64_t wallet2::import_key_images(const std::string &filename, uint64_t &spent, uint64_t &unspent) +{ + std::string data; + bool r = epee::file_io_utils::load_file_to_string(filename, data); + + if (!r) + { + fail_msg_writer() << tr("failed to read file ") << filename; + return 0; + } + const size_t magiclen = strlen(KEY_IMAGE_EXPORT_FILE_MAGIC); + if (data.size() < magiclen || memcmp(data.data(), KEY_IMAGE_EXPORT_FILE_MAGIC, magiclen)) + { + fail_msg_writer() << "Bad key image export file magic in " << filename; + return 0; + } + + try + { + data = decrypt_with_view_secret_key(std::string(data, magiclen)); + } + catch (const std::exception &e) + { + fail_msg_writer() << "Failed to decrypt " << filename << ": " << e.what(); + return 0; + } + + const size_t headerlen = 2 * sizeof(crypto::public_key); + if (data.size() < headerlen) + { + fail_msg_writer() << "Bad data size from file " << filename; + return 0; + } + const crypto::public_key &public_spend_key = *(const crypto::public_key*)&data[0]; + const crypto::public_key &public_view_key = *(const crypto::public_key*)&data[sizeof(crypto::public_key)]; + const cryptonote::account_public_address &keys = get_account().get_keys().m_account_address; + if (public_spend_key != keys.m_spend_public_key || public_view_key != keys.m_view_public_key) + { + fail_msg_writer() << "Key images from " << filename << " are for a different account"; + return 0; + } + + const size_t record_size = sizeof(crypto::key_image) + sizeof(crypto::signature); + if ((data.size() - headerlen) % record_size) + { + fail_msg_writer() << "Bad data size from file " << filename; + return 0; + } + size_t nki = (data.size() - headerlen) / record_size; + + std::vector<std::pair<crypto::key_image, crypto::signature>> ski; + ski.reserve(nki); + for (size_t n = 0; n < nki; ++n) + { + crypto::key_image key_image = *reinterpret_cast<const crypto::key_image*>(&data[headerlen + n * record_size]); + crypto::signature signature = *reinterpret_cast<const crypto::signature*>(&data[headerlen + n * record_size + sizeof(crypto::key_image)]); + + ski.push_back(std::make_pair(key_image, signature)); + } + + return import_key_images(ski, spent, unspent); +} + //---------------------------------------------------------------------------------------------------- uint64_t wallet2::import_key_images(const std::vector<std::pair<crypto::key_image, crypto::signature>> &signed_key_images, uint64_t &spent, uint64_t &unspent) { |