From ef2a1a5cd028f59cd33f78adb96a6b66401a0e79 Mon Sep 17 00:00:00 2001 From: everoddandeven Date: Tue, 1 Oct 2024 15:34:33 +0200 Subject: [PATCH] Maintenance of light wallet methods --- src/rpc/core_rpc_server_commands_defs.h | 8 + src/wallet/wallet2.cpp | 934 +++++++++++++++++++----- src/wallet/wallet2.h | 47 +- src/wallet/wallet_light_rpc.h | 200 ++++- 4 files changed, 1014 insertions(+), 175 deletions(-) diff --git a/src/rpc/core_rpc_server_commands_defs.h b/src/rpc/core_rpc_server_commands_defs.h index 5b328b055..57cf047b9 100644 --- a/src/rpc/core_rpc_server_commands_defs.h +++ b/src/rpc/core_rpc_server_commands_defs.h @@ -353,6 +353,14 @@ namespace cryptonote }; typedef epee::misc_utils::struct_init request; + struct standard_response_t + { + bool status; + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(status) + END_KV_SERIALIZE_MAP() + }; + typedef epee::misc_utils::struct_init standard_response; struct response_t { diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp index 90b573169..8c710b481 100644 --- a/src/wallet/wallet2.cpp +++ b/src/wallet/wallet2.cpp @@ -2173,7 +2173,14 @@ void wallet2::scan_output(const cryptonote::transaction &tx, bool miner_tx, cons CRITICAL_REGION_LOCAL(password_lock); if (!m_encrypt_keys_after_refresh) { - boost::optional pwd = m_callback->on_get_password(pool ? "output found in pool" : "output received"); + boost::optional pwd; + + if (0 != m_callback) { + m_callback->on_get_password(pool ? "output found in pool" : "output received"); + } else { + pwd = boost::none; + } + THROW_WALLET_EXCEPTION_IF(!pwd, error::password_needed, tr("Password is needed to compute key image for incoming monero")); THROW_WALLET_EXCEPTION_IF(!verify_password(*pwd), error::password_needed, tr("Invalid password: password is needed to compute key image for incoming monero")); m_encrypt_keys_after_refresh.reset(new wallet_keys_unlocker(*this, m_ask_password == AskPasswordToDecrypt && !m_unattended && !m_watch_only, *pwd)); @@ -2308,8 +2315,7 @@ void wallet2::process_new_transaction(const crypto::hash &txid, const cryptonote if (pk_index > 1) break; LOG_PRINT_L0("Public key wasn't found in the transaction extra. Skipping transaction " << txid); - if(!ignore_callbacks && 0 != m_callback) - m_callback->on_skip_transaction(height, txid, tx); + if(!ignore_callbacks && 0 != m_callback) m_callback->on_skip_transaction(height, txid, tx); break; } if (!tx_cache_data.primary.empty()) @@ -3920,23 +3926,85 @@ std::shared_ptr, size_t>> wallet2::create return cache; } //---------------------------------------------------------------------------------------------------- +void wallet2::light_wallet_refresh(uint64_t & blocks_fetched, bool& received_money) { + MDEBUG("light_wallet_refresh()"); + + std::string address = get_account().get_public_address_str(m_nettype); + std::string view_key = string_tools::pod_to_hex(unwrap(unwrap(get_account().get_keys().m_view_secret_key))); + + MDEBUG("light_wallet_refresh(): get_unspent_outs"); + + tools::COMMAND_RPC_GET_UNSPENT_OUTS::request uoreq; + tools::COMMAND_RPC_GET_UNSPENT_OUTS::response uores; + + uoreq.amount = "0"; + uoreq.address = address; + uoreq.view_key = view_key; // openMonero specific + uoreq.dust_threshold = boost::lexical_cast(::config::DEFAULT_DUST_THRESHOLD); + // below are required by openMonero api - but are not used. + uoreq.mixin = 0; + uoreq.use_dust = true; + + m_daemon_rpc_mutex.lock(); + bool r = invoke_http_json("/get_unspent_outs", uoreq, uores, rpc_timeout, "POST"); + m_daemon_rpc_mutex.unlock(); + THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "get_unspent_outs"); + THROW_WALLET_EXCEPTION_IF(uores.status == "error", error::wallet_internal_error, uores.reason); + + m_unspent_outputs_response = uores; + m_light_wallet_per_kb_fee = uores.per_kb_fee; + + MDEBUG("light_wallet_refresh(): get_address_txs"); + + tools::COMMAND_RPC_GET_ADDRESS_TXS::request ireq; + tools::COMMAND_RPC_GET_ADDRESS_TXS::response ires; + + ireq.address = address; + ireq.view_key = view_key; + m_daemon_rpc_mutex.lock(); + r = invoke_http_json("/get_address_txs", ireq, ires, rpc_timeout, "POST"); + m_daemon_rpc_mutex.unlock(); + THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "get_address_txs"); + //OpenMonero sends status=success, Mymonero doesn't. + if (m_light_wallet_server_type == OPENMONERO || m_light_wallet_server_type == MYMONERO) THROW_WALLET_EXCEPTION_IF((!ires.status.empty() && ires.status != "success"), error::no_connection_to_daemon, "get_address_txs"); + + m_address_txs_response = ires; + uint64_t last_height = m_light_wallet_blockchain_height < m_refresh_from_block_height ? m_refresh_from_block_height : m_light_wallet_blockchain_height; + blocks_fetched = ires.scanned_height >= last_height ? ires.scanned_height - last_height : 0; + + MDEBUG("light_wallet_refresh(): refreshed wallet"); + + // update m_spent_key_images + + for (auto &unspent_output : uores.outputs) { + for(auto &ski : unspent_output.spend_key_images) { + THROW_WALLET_EXCEPTION_IF(!string_tools::validate_hex(64, ski), error::wallet_internal_error, "Invalid key image"); + crypto::key_image spend_key_image; + string_tools::hex_to_pod(ski, spend_key_image); + m_spent_key_images.emplace(ski); + } + } + +} +//---------------------------------------------------------------------------------------------------- + void wallet2::refresh(bool trusted_daemon, uint64_t start_height, uint64_t & blocks_fetched, bool& received_money, bool check_pool, bool try_incremental, uint64_t max_blocks) { if (m_offline) { blocks_fetched = 0; - received_money = 0; + received_money = false; return; } if(m_light_wallet) { - // MyMonero get_address_info needs to be called occasionally to trigger wallet sync. // This call is not really needed for other purposes and can be removed if mymonero changes their backend. tools::COMMAND_RPC_GET_ADDRESS_INFO::response res; // Get basic info if(light_wallet_get_address_info(res)) { + light_wallet_refresh(blocks_fetched, received_money); // Last stored block height uint64_t prev_height = m_light_wallet_blockchain_height; // Update lw heights @@ -3946,7 +4014,7 @@ void wallet2::refresh(bool trusted_daemon, uint64_t start_height, uint64_t & blo if(m_light_wallet_blockchain_height != prev_height) { MDEBUG("new block since last time!"); - m_callback->on_lw_new_block(m_light_wallet_blockchain_height - 1); + if (0 != m_callback) m_callback->on_lw_new_block(m_light_wallet_blockchain_height - 1); } m_light_wallet_connected = true; MDEBUG("lw scanned block height: " << m_light_wallet_scanned_block_height); @@ -3954,9 +4022,26 @@ void wallet2::refresh(bool trusted_daemon, uint64_t start_height, uint64_t & blo MDEBUG(m_light_wallet_blockchain_height-m_light_wallet_scanned_block_height << " blocks behind"); // TODO: add wallet created block info + std::vector tx_hashes = std::vector(); + + for(auto kv : m_light_wallet_address_txs) { + tx_hashes.push_back(kv.first); + } + light_wallet_get_address_txs(); - } else + + for(auto kv : m_light_wallet_address_txs) { + if(std::find(tx_hashes.begin(), tx_hashes.end(), kv.first) != tx_hashes.end()) { + continue; + } + + if(kv.second.m_incoming) received_money = true; + } + } else { m_light_wallet_connected = false; + blocks_fetched = 0; + received_money = false; + } // Lighwallet refresh done return; @@ -3972,7 +4057,6 @@ void wallet2::refresh(bool trusted_daemon, uint64_t start_height, uint64_t & blo uint64_t blocks_start_height; std::vector blocks; std::vector parsed_blocks; - bool refreshed = false; std::shared_ptr, size_t>> output_tracker_cache; hw::device &hwdev = m_account.get_device(); @@ -6518,8 +6602,6 @@ boost::optional wallet2::get_cache_file_data() uint64_t wallet2::balance(uint32_t index_major, bool strict) const { uint64_t amount = 0; - if(m_light_wallet) - return m_light_wallet_balance; for (const auto& i : balance_per_subaddress(index_major, strict)) amount += i.second; return amount; @@ -6532,8 +6614,6 @@ uint64_t wallet2::unlocked_balance(uint32_t index_major, bool strict, uint64_t * *blocks_to_unlock = 0; if (time_to_unlock) *time_to_unlock = 0; - if(m_light_wallet) - return m_light_wallet_unlocked_balance; for (const auto& i : unlocked_balance_per_subaddress(index_major, strict)) { amount += i.second.first; @@ -6741,24 +6821,49 @@ void wallet2::rescan_spent() { const size_t n_outputs = std::min(chunk_size, m_transfers.size() - start_offset); MDEBUG("Calling is_key_image_spent on " << start_offset << " - " << (start_offset + n_outputs - 1) << ", out of " << m_transfers.size()); - COMMAND_RPC_IS_KEY_IMAGE_SPENT::request req = AUTO_VAL_INIT(req); - COMMAND_RPC_IS_KEY_IMAGE_SPENT::response daemon_resp = AUTO_VAL_INIT(daemon_resp); - for (size_t n = start_offset; n < start_offset + n_outputs; ++n) - req.key_images.push_back(string_tools::pod_to_hex(m_transfers[n].m_key_image)); + if (m_light_wallet) { - const boost::lock_guard lock{m_daemon_rpc_mutex}; - uint64_t pre_call_credits = m_rpc_payment_state.credits; - req.client = get_client_signature(); - bool r = epee::net_utils::invoke_http_json("/is_key_image_spent", req, daemon_resp, *m_http_client, rpc_timeout); - THROW_ON_RPC_RESPONSE_ERROR(r, {}, daemon_resp, "is_key_image_spent", error::is_key_image_spent_error, get_rpc_status(daemon_resp.status)); - THROW_WALLET_EXCEPTION_IF(daemon_resp.spent_status.size() != n_outputs, error::wallet_internal_error, - "daemon returned wrong response for is_key_image_spent, wrong amounts count = " + - std::to_string(daemon_resp.spent_status.size()) + ", expected " + std::to_string(n_outputs)); - check_rpc_cost("/is_key_image_spent", daemon_resp.credits, pre_call_credits, n_outputs * COST_PER_KEY_IMAGE); - } + std::vector key_images; + for (size_t n = start_offset; n < start_offset + n_outputs; ++n) + key_images.push_back(m_transfers[n].m_key_image); + + std::vector result; + { + result = light_wallet_is_key_image_spent(key_images); + } - std::copy(daemon_resp.spent_status.begin(), daemon_resp.spent_status.end(), std::back_inserter(spent_status)); + //std::copy(result.begin(), result.end(), std::back_inserter(spent_status)); + + for(auto res : result) { + if (res) { + spent_status.push_back(1); + } else { + spent_status.push_back(0); + } + } + } + else + { + COMMAND_RPC_IS_KEY_IMAGE_SPENT::request req = AUTO_VAL_INIT(req); + COMMAND_RPC_IS_KEY_IMAGE_SPENT::response daemon_resp = AUTO_VAL_INIT(daemon_resp); + for (size_t n = start_offset; n < start_offset + n_outputs; ++n) + req.key_images.push_back(string_tools::pod_to_hex(m_transfers[n].m_key_image)); + + { + const boost::lock_guard lock{m_daemon_rpc_mutex}; + uint64_t pre_call_credits = m_rpc_payment_state.credits; + req.client = get_client_signature(); + bool r = epee::net_utils::invoke_http_json("/is_key_image_spent", req, daemon_resp, *m_http_client, rpc_timeout); + THROW_ON_RPC_RESPONSE_ERROR(r, {}, daemon_resp, "is_key_image_spent", error::is_key_image_spent_error, get_rpc_status(daemon_resp.status)); + THROW_WALLET_EXCEPTION_IF(daemon_resp.spent_status.size() != n_outputs, error::wallet_internal_error, + "daemon returned wrong response for is_key_image_spent, wrong amounts count = " + + std::to_string(daemon_resp.spent_status.size()) + ", expected " + std::to_string(n_outputs)); + check_rpc_cost("/is_key_image_spent", daemon_resp.credits, pre_call_credits, n_outputs * COST_PER_KEY_IMAGE); + } + + std::copy(daemon_resp.spent_status.begin(), daemon_resp.spent_status.end(), std::back_inserter(spent_status)); + } } // update spent status @@ -7074,17 +7179,26 @@ void wallet2::commit_tx(pending_tx& ptx) if(m_light_wallet) { + THROW_WALLET_EXCEPTION_IF(ptx.fee == 0, error::tx_rejected, ptx.tx, get_rpc_status(false), "Fee to low"); cryptonote::COMMAND_RPC_SUBMIT_RAW_TX::request oreq; - cryptonote::COMMAND_RPC_SUBMIT_RAW_TX::response ores; oreq.address = get_account().get_public_address_str(m_nettype); oreq.view_key = string_tools::pod_to_hex(unwrap(unwrap(get_account().get_keys().m_view_secret_key))); oreq.tx = epee::string_tools::buff_to_hex_nodelimer(tx_to_blob(ptx.tx)); { + bool r = false; const boost::lock_guard lock{m_daemon_rpc_mutex}; - bool r = epee::net_utils::invoke_http_json("/submit_raw_tx", oreq, ores, *m_http_client, rpc_timeout, "POST"); - THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "submit_raw_tx"); - // MyMonero and OpenMonero use different status strings - THROW_WALLET_EXCEPTION_IF(ores.status != "OK" && ores.status != "success" , error::tx_rejected, ptx.tx, get_rpc_status(ores.status), ores.error); + if (m_light_wallet_server_type == STANDARD) { + cryptonote::COMMAND_RPC_SUBMIT_RAW_TX::standard_response ores; + r = epee::net_utils::invoke_http_json("/submit_raw_tx", oreq, ores, *m_http_client, rpc_timeout, "POST"); + // Standard lws uses boolean status + THROW_WALLET_EXCEPTION_IF(!ores.status, error::tx_rejected, ptx.tx, get_rpc_status(ores.status), "error while relaying tx"); + } else { + cryptonote::COMMAND_RPC_SUBMIT_RAW_TX::response ores; + r = epee::net_utils::invoke_http_json("/submit_raw_tx", oreq, ores, *m_http_client, rpc_timeout, "POST"); + THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "submit_raw_tx"); + // MyMonero and OpenMonero use different status strings + THROW_WALLET_EXCEPTION_IF(ores.status != "OK" && ores.status != "success" , error::tx_rejected, ptx.tx, get_rpc_status(ores.status), ores.error); + } } } else @@ -8093,7 +8207,7 @@ uint64_t wallet2::get_base_fee() uint64_t wallet2::get_base_fee(uint32_t priority) { const bool use_2021_scaling = use_fork_rules(HF_VERSION_2021_SCALING, -30 * 1); - if (use_2021_scaling) + if (!m_light_wallet && use_2021_scaling) { // clamp and map to 0..3 indices, mapping 0 (default, but should not end up here) to 0, and 1..4 to 0..3 if (priority == 0) @@ -8116,6 +8230,9 @@ uint64_t wallet2::get_base_fee(uint32_t priority) } return fees[priority]; } + else if(m_light_wallet) { + return FEE_PER_BYTE; + } else { const uint64_t base_fee = get_base_fee(); @@ -8196,6 +8313,7 @@ uint64_t wallet2::adjust_mixin(uint64_t mixin) //---------------------------------------------------------------------------------------------------- uint32_t wallet2::adjust_priority(uint32_t priority) { + if(m_light_wallet) return priority; if (priority == 0 && m_default_priority == 0 && auto_low_priority()) { try @@ -8692,7 +8810,7 @@ void wallet2::light_wallet_get_outs(std::vector> light_wallet_get_outs(outs, selected_transfers, fake_outputs_count); return; } - - if (fake_outputs_count > 0) + else if (!m_light_wallet && fake_outputs_count > 0) { uint64_t segregation_fork_height = get_segregation_fork_height(); // check whether we're shortly after the fork @@ -10112,31 +10229,25 @@ bool wallet2::light_wallet_import_wallet_request(tools::COMMAND_RPC_IMPORT_WALLE return true; } +void wallet2::light_wallet_get_tx_unspent_outs(const std::string tx_hash, std::vector &outputs) const { + if (m_unspent_outputs_response == boost::none) return; + + for(auto &uo : m_unspent_outputs_response.get().outputs) { + if (uo.tx_hash == tx_hash) outputs.push_back(uo); + } +} + void wallet2::light_wallet_get_unspent_outs() { MDEBUG("Getting unspent outs"); - - tools::COMMAND_RPC_GET_UNSPENT_OUTS::request oreq; - tools::COMMAND_RPC_GET_UNSPENT_OUTS::response ores; - - oreq.amount = "0"; - oreq.address = get_account().get_public_address_str(m_nettype); - oreq.view_key = string_tools::pod_to_hex(unwrap(unwrap(get_account().get_keys().m_view_secret_key))); - // openMonero specific - oreq.dust_threshold = boost::lexical_cast(::config::DEFAULT_DUST_THRESHOLD); - // below are required by openMonero api - but are not used. - oreq.mixin = 0; - oreq.use_dust = true; + if (m_unspent_outputs_response == boost::none) { + MWARNING("Could not get unspent outs, wallet not synced"); + return; + } + + tools::COMMAND_RPC_GET_UNSPENT_OUTS::response ores = m_unspent_outputs_response.get(); - m_daemon_rpc_mutex.lock(); - bool r = invoke_http_json("/get_unspent_outs", oreq, ores, rpc_timeout, "POST"); - m_daemon_rpc_mutex.unlock(); - THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "get_unspent_outs"); - THROW_WALLET_EXCEPTION_IF(ores.status == "error", error::wallet_internal_error, ores.reason); - - m_light_wallet_per_kb_fee = ores.per_kb_fee; - std::unordered_map transfers_txs; for(const auto &t: m_transfers) transfers_txs.emplace(t.m_txid,t.m_spent); @@ -10148,28 +10259,43 @@ void wallet2::light_wallet_get_unspent_outs() return; // Clear old outputs - m_transfers.clear(); - + //m_transfers.clear(); + for (const auto &o: ores.outputs) { bool spent = false; bool add_transfer = true; crypto::key_image unspent_key_image; + crypto::key_image out_key_image; crypto::public_key tx_public_key = AUTO_VAL_INIT(tx_public_key); - THROW_WALLET_EXCEPTION_IF(string_tools::validate_hex(64, o.tx_pub_key), error::wallet_internal_error, "Invalid tx_pub_key field"); + crypto::public_key out_public_key; + THROW_WALLET_EXCEPTION_IF(!string_tools::validate_hex(64, o.tx_pub_key), error::wallet_internal_error, "Invalid tx_pub_key field"); + THROW_WALLET_EXCEPTION_IF(!string_tools::validate_hex(64, o.public_key), error::wallet_internal_error, "Invalid public_key field"); string_tools::hex_to_pod(o.tx_pub_key, tx_public_key); - + string_tools::hex_to_pod(o.public_key, out_public_key); + for (const std::string &ski: o.spend_key_images) { spent = false; - // Check if key image is ours - THROW_WALLET_EXCEPTION_IF(string_tools::validate_hex(64, ski), error::wallet_internal_error, "Invalid key image"); + THROW_WALLET_EXCEPTION_IF(!string_tools::validate_hex(64, ski), error::wallet_internal_error, "Invalid key image"); string_tools::hex_to_pod(ski, unspent_key_image); - if(light_wallet_key_image_is_ours(unspent_key_image, tx_public_key, o.index)){ - MTRACE("Output " << o.public_key << " is spent. Key image: " << ski); + + if(light_wallet_key_image_is_ours(unspent_key_image, tx_public_key, o.index, {o.recipient.maj_i,o.recipient.min_i})){ + MTRACE("Spent output found. " << o.public_key); spent = true; break; - } { - MTRACE("Unspent output found. " << o.public_key); + } + } + + uint64_t spent_height = 0; + + if(!spent) + { + MTRACE("Unspent output found. " << o.public_key); + } else if (m_address_txs_response != boost::none) { + for (auto tf : m_address_txs_response.get().transactions) { + if (tf.hash == o.tx_hash) { + spent_height = tf.height; + } } } @@ -10177,24 +10303,27 @@ void wallet2::light_wallet_get_unspent_outs() crypto::hash txid; crypto::public_key tx_pub_key; crypto::public_key public_key; - THROW_WALLET_EXCEPTION_IF(string_tools::validate_hex(64, o.tx_hash), error::wallet_internal_error, "Invalid tx_hash field"); - THROW_WALLET_EXCEPTION_IF(string_tools::validate_hex(64, o.public_key), error::wallet_internal_error, "Invalid public_key field"); - THROW_WALLET_EXCEPTION_IF(string_tools::validate_hex(64, o.tx_pub_key), error::wallet_internal_error, "Invalid tx_pub_key field"); + THROW_WALLET_EXCEPTION_IF(!string_tools::validate_hex(64, o.tx_hash), error::wallet_internal_error, "Invalid tx_hash field"); + THROW_WALLET_EXCEPTION_IF(!string_tools::validate_hex(64, o.public_key), error::wallet_internal_error, "Invalid public_key field"); + THROW_WALLET_EXCEPTION_IF(!string_tools::validate_hex(64, o.tx_pub_key), error::wallet_internal_error, "Invalid tx_pub_key field"); string_tools::hex_to_pod(o.tx_hash, txid); string_tools::hex_to_pod(o.public_key, public_key); string_tools::hex_to_pod(o.tx_pub_key, tx_pub_key); - + uint64_t t_idx = 0; for(auto &t: m_transfers){ if(t.get_public_key() == public_key) { - t.m_spent = spent; + // update spent status + if (!t.m_spent && spent) { + set_spent(t_idx, spent_height); + } add_transfer = false; break; } + t_idx++; } if(!add_transfer) continue; - m_transfers.push_back(transfer_details{}); transfer_details& td = m_transfers.back(); @@ -10204,17 +10333,18 @@ void wallet2::light_wallet_get_unspent_outs() // Add to extra add_tx_pub_key_to_extra(td.m_tx, tx_pub_key); - - td.m_key_image = unspent_key_image; td.m_key_image_known = !m_watch_only && !m_multisig; td.m_key_image_request = false; td.m_key_image_partial = m_multisig; td.m_amount = o.amount; td.m_pk_index = 0; td.m_internal_output_index = o.index; - td.m_spent = spent; td.m_frozen = false; - + td.m_subaddr_index.major = o.recipient.maj_i; + td.m_subaddr_index.minor = o.recipient.min_i; + if (!m_watch_only) generate_output_key_image(out_public_key, tx_public_key, o.index, td.m_subaddr_index, out_key_image); + td.m_key_image = out_key_image; + tx_out txout; txout.target = txout_to_key(public_key); txout.amount = td.m_amount; @@ -10261,6 +10391,9 @@ void wallet2::light_wallet_get_unspent_outs() } if(!spent) set_unspent(m_transfers.size()-1); + else + set_spent(m_transfers.size()-1, spent_height); + m_key_images[td.m_key_image] = m_transfers.size()-1; m_pub_keys[td.get_public_key()] = m_transfers.size()-1; } @@ -10284,20 +10417,14 @@ bool wallet2::light_wallet_get_address_info(tools::COMMAND_RPC_GET_ADDRESS_INFO: void wallet2::light_wallet_get_address_txs() { - MDEBUG("Refreshing light wallet"); + MDEBUG("Getting address txs"); - tools::COMMAND_RPC_GET_ADDRESS_TXS::request ireq; - tools::COMMAND_RPC_GET_ADDRESS_TXS::response ires; - - ireq.address = get_account().get_public_address_str(m_nettype); - ireq.view_key = string_tools::pod_to_hex(unwrap(unwrap(get_account().get_keys().m_view_secret_key))); - m_daemon_rpc_mutex.lock(); - bool r = invoke_http_json("/get_address_txs", ireq, ires, rpc_timeout, "POST"); - m_daemon_rpc_mutex.unlock(); - THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "get_address_txs"); - //OpenMonero sends status=success, Mymonero doesn't. - THROW_WALLET_EXCEPTION_IF((!ires.status.empty() && ires.status != "success"), error::no_connection_to_daemon, "get_address_txs"); + if (m_address_txs_response == boost::none) { + MERROR("Could not get adress txs, wallet not synced"); + return; + } + tools::COMMAND_RPC_GET_ADDRESS_TXS::response ires = m_address_txs_response.get(); // Abort if no transactions if(ires.transactions.empty()) @@ -10317,22 +10444,39 @@ void wallet2::light_wallet_get_address_txs() std::vector pool_txs; for (const auto &t: ires.transactions) { - const uint64_t total_received = t.total_received; + crypto::hash tx_hash; + THROW_WALLET_EXCEPTION_IF(!string_tools::validate_hex(64, t.hash), error::wallet_internal_error, "Invalid hash field"); + string_tools::hex_to_pod(t.hash, tx_hash); + + if (std::find(payments_txs.begin(), payments_txs.end(), tx_hash) != payments_txs.end()) { + MINFO("Skipping already processed transaction " << tx_hash); + continue; + } + + // t can be 'outgoing', 'incoming' or 'both' + uint64_t fake_outputs_count = 0; + std::set subaddr_indices; + uint64_t total_received = t.total_received; uint64_t total_sent = t.total_sent; + crypto::public_key out_public_key; + std::vector spent_outputs; // Check key images - subtract fake outputs from total_sent for(const auto &so: t.spent_outputs) { crypto::public_key tx_public_key; crypto::key_image key_image; - THROW_WALLET_EXCEPTION_IF(string_tools::validate_hex(64, so.tx_pub_key), error::wallet_internal_error, "Invalid tx_pub_key field"); - THROW_WALLET_EXCEPTION_IF(string_tools::validate_hex(64, so.key_image), error::wallet_internal_error, "Invalid key_image field"); + THROW_WALLET_EXCEPTION_IF(!string_tools::validate_hex(64, so.tx_pub_key), error::wallet_internal_error, "Invalid tx_pub_key field"); + THROW_WALLET_EXCEPTION_IF(!string_tools::validate_hex(64, so.key_image), error::wallet_internal_error, "Invalid key_image field"); string_tools::hex_to_pod(so.tx_pub_key, tx_public_key); string_tools::hex_to_pod(so.key_image, key_image); - if(!light_wallet_key_image_is_ours(key_image, tx_public_key, so.out_index)) { + if(!light_wallet_key_image_is_ours(key_image, tx_public_key, so.out_index, {so.sender.maj_i,so.sender.min_i})) { THROW_WALLET_EXCEPTION_IF(so.amount > t.total_sent, error::wallet_internal_error, "Lightwallet: total sent is negative!"); total_sent -= so.amount; + fake_outputs_count++; + } else { + spent_outputs.push_back(so); } } @@ -10341,62 +10485,185 @@ void wallet2::light_wallet_get_address_txs() continue; crypto::hash payment_id = null_hash; - crypto::hash tx_hash; - THROW_WALLET_EXCEPTION_IF(string_tools::validate_hex(64, t.payment_id), error::wallet_internal_error, "Invalid payment_id field"); - THROW_WALLET_EXCEPTION_IF(string_tools::validate_hex(64, t.hash), error::wallet_internal_error, "Invalid hash field"); - string_tools::hex_to_pod(t.payment_id, payment_id); - string_tools::hex_to_pod(t.hash, tx_hash); + //THROW_WALLET_EXCEPTION_IF(!string_tools::validate_hex(16, t.payment_id), error::wallet_internal_error, "Invalid payment_id field"); + if(string_tools::validate_hex(16, t.payment_id)) { + if (!parse_payment_id(t.payment_id, payment_id)) { + payment_id = null_hash; + } + } // lightwallet specific info - bool incoming = (total_received > total_sent); + //bool incoming = (total_received > total_sent); + bool incoming = false; + //bool change = m_watch_only == false ? total_sent - total_received - t.fee == 0 : t.total_sent - t.total_received - t.fee == 0; + bool change = total_sent - total_received - t.fee == 0; + + if(total_received > total_sent || change) { + incoming = true; + } + + bool outgoing = spent_outputs.size() > 0; + address_tx address_tx; address_tx.m_tx_hash = tx_hash; address_tx.m_incoming = incoming; - address_tx.m_amount = incoming ? total_received - total_sent : total_sent - total_received; - address_tx.m_fee = 0; // TODO + address_tx.m_amount = incoming ? total_received - total_sent : total_sent; + address_tx.m_fee = t.fee; // TODO address_tx.m_block_height = t.height; address_tx.m_unlock_time = t.unlock_time; - address_tx.m_timestamp = t.timestamp; + address_tx.m_timestamp = t.timestamp + 3600; // TODO Fix light wallet timestamp address_tx.m_coinbase = t.coinbase; address_tx.m_mempool = t.mempool; - m_light_wallet_address_txs.emplace(tx_hash,address_tx); + + m_light_wallet_address_txs.emplace(tx_hash, address_tx); + + std::vector uouts; + light_wallet_get_tx_unspent_outs(t.hash, uouts); // populate data needed for history (m_payments, m_unconfirmed_payments, m_confirmed_txs) // INCOMING transfers - if(total_received > total_sent) { - payment_details payment; - payment.m_tx_hash = tx_hash; - payment.m_amount = total_received - total_sent; - payment.m_fee = 0; // TODO - payment.m_block_height = t.height; - payment.m_unlock_time = t.unlock_time; - payment.m_timestamp = t.timestamp; - payment.m_coinbase = t.coinbase; - - if (t.mempool) { - if (std::find(unconfirmed_payments_txs.begin(), unconfirmed_payments_txs.end(), tx_hash) == unconfirmed_payments_txs.end()) { - pool_txs.push_back(tx_hash); - // assume false as we don't get that info from the light wallet server - crypto::hash payment_id; - THROW_WALLET_EXCEPTION_IF(!epee::string_tools::hex_to_pod(t.payment_id, payment_id), - error::wallet_internal_error, "Failed to parse payment id"); - emplace_or_replace(m_unconfirmed_payments, payment_id, pool_payment_details{payment, false}); - if (0 != m_callback) { - m_callback->on_lw_unconfirmed_money_received(t.height, payment.m_tx_hash, payment.m_amount); + + if(incoming) { + uint32_t sender_account = 0; + + if (change && !outgoing) + { + uint64_t input_sum = 0; + for (auto uout : uouts) { + if (uout.recipient.maj_i == t.recipient.maj_i) input_sum += uout.amount; + } + + if (input_sum == total_received) { + continue; + } + } + uint64_t received = total_received; + if (total_sent > 0) { + // bisogna trovare il sender della transazione + for (auto &tout : spent_outputs) { + sender_account = tout.sender.maj_i; + break; + } + // e filtrare come segue + for(auto &uout : uouts) { + // se l'output è di un'altro account, allora è uscente per il sender + // se indirizzo stesso account allora è un change + + if (sender_account != uout.recipient.maj_i) { + received -= uout.amount; } } - } else { - if (std::find(payments_txs.begin(), payments_txs.end(), tx_hash) == payments_txs.end()) { - m_payments.emplace(tx_hash, payment); - if (0 != m_callback) { - m_callback->on_lw_money_received(t.height, payment.m_tx_hash, payment.m_amount); + } + + for(auto &uout : uouts) { + if (change && sender_account == uout.recipient.maj_i) { + continue; + } + + THROW_WALLET_EXCEPTION_IF(!string_tools::validate_hex(64, uout.public_key), error::wallet_internal_error, "Invalid output public key field"); + string_tools::hex_to_pod(uout.public_key, out_public_key); + payment_details payment; + payment.m_tx_hash = tx_hash; + payment.m_amount = uout.amount; + payment.m_fee = t.fee; + payment.m_block_height = t.height; + payment.m_unlock_time = t.unlock_time; + payment.m_timestamp = t.timestamp + 3600; // TODO Fix light wallet timestamp + payment.m_coinbase = t.coinbase; + payment.m_subaddr_index = {uout.recipient.maj_i,uout.recipient.min_i}; + if (t.mempool && payment.m_amount != 0) { + if (std::find(unconfirmed_payments_txs.begin(), unconfirmed_payments_txs.end(), tx_hash) == unconfirmed_payments_txs.end()) { + pool_txs.push_back(tx_hash); + // assume false as we don't get that info from the light wallet server + crypto::hash payment_id; + THROW_WALLET_EXCEPTION_IF(!epee::string_tools::hex_to_pod(t.payment_id, payment_id), + error::wallet_internal_error, "Failed to parse payment id"); + emplace_or_replace(m_unconfirmed_payments, payment_id, pool_payment_details{payment, false}); + if (0 != m_callback) { + m_callback->on_lw_unconfirmed_money_received(t.height, payment.m_tx_hash, payment.m_amount); + } + } + } else if (payment.m_amount != 0) { + if (std::find(payments_txs.begin(), payments_txs.end(), payment_id) == payments_txs.end()) { + // se trovo un payment per lo stesso subaddr_index devo incrementare l'amount + m_payments.emplace(payment_id, payment); + payments_txs.push_back(payment_id); + if (0 != m_callback) { + m_callback->on_lw_money_received(t.height, payment.m_tx_hash, payment.m_amount); + } + m_light_wallet_processed_out_public_keys.push_back(out_public_key); + } + else if (std::find(m_light_wallet_processed_out_public_keys.begin(), m_light_wallet_processed_out_public_keys.end(), out_public_key) == m_light_wallet_processed_out_public_keys.end()) { + auto payments_er = m_payments.equal_range(payment_id); + bool payment_found = false; + for(auto payments_it = payments_er.first; payments_it != payments_er.second; ++payments_it) { + if (tx_hash != payments_it->second.m_tx_hash || payments_it->second.m_subaddr_index.major != payment.m_subaddr_index.major || payments_it->second.m_subaddr_index.minor != payment.m_subaddr_index.minor) { + continue; + } + // devo sapere se l'output è stato già processato + // altrimento raddoppio i valori ad ogni chiamata + payments_it->second.m_amount += payment.m_amount; + payment_found = true; + break; + } + + if (!payment_found) { + m_payments.emplace(payment_id, payment); + payments_txs.push_back(payment_id); + if (0 != m_callback) { + m_callback->on_lw_money_received(t.height, payment.m_tx_hash, payment.m_amount); + } + } + + m_light_wallet_processed_out_public_keys.push_back(out_public_key); } } } // Outgoing transfers - } else { - uint64_t amount_sent = total_sent - total_received; + } + if(outgoing) + { + uint32_t sender_account = 0; + + bool has_default = false; + for (auto &tout : spent_outputs) { + if (tout.sender.maj_i == 0) { + has_default = true; + break; + } + } + + if (!has_default) { + for (auto &tout : spent_outputs) { + sender_account = tout.sender.maj_i; + break; + } + } + + std::vector dests; + for(auto &uout : uouts) { + + if (uout.recipient.maj_i == sender_account) { + continue; + } + + cryptonote::subaddress_index index; + index.major = uout.recipient.maj_i; + index.minor = uout.recipient.min_i; + cryptonote::account_public_address address = get_subaddress(index); + bool is_subaddress = uout.recipient.maj_i != 0 || uout.recipient.min_i != 0; + cryptonote::tx_destination_entry dest(uout.amount, address, is_subaddress); + + dests.push_back(dest); + + if (sender_account != uout.recipient.maj_i) { + total_received -= uout.amount; + } + } + uint64_t amount_sent = (total_sent > total_received) ? total_sent - total_received : total_sent; + if (amount_sent == 0) amount_sent += t.fee; + uint64_t amount_out = amount_sent - t.fee; cryptonote::transaction dummy_tx; // not used by light wallet // increase wallet total sent wallet_total_sent += total_sent; @@ -10411,8 +10678,9 @@ void wallet2::light_wallet_get_address_txs() utd.m_amount_out = amount_sent; utd.m_change = 0; utd.m_payment_id = payment_id; - utd.m_timestamp = t.timestamp; + utd.m_timestamp = t.timestamp + 3600; // TO DO Fix light wallet timestamp utd.m_state = wallet2::unconfirmed_transfer_details::pending; + utd.m_dests = dests; m_unconfirmed_txs.emplace(tx_hash,utd); } } @@ -10431,12 +10699,19 @@ void wallet2::light_wallet_get_address_txs() { confirmed_transfer_details ctd; ctd.m_amount_in = amount_sent; - ctd.m_amount_out = amount_sent; + ctd.m_amount_out = amount_out; ctd.m_change = 0; ctd.m_payment_id = payment_id; ctd.m_block_height = t.height; - ctd.m_timestamp = t.timestamp; - m_confirmed_txs.emplace(tx_hash,ctd); + ctd.m_timestamp = t.timestamp + 3600; // TODO Fix light wallet timestamp + ctd.m_subaddr_account = sender_account; + ctd.m_dests = dests; + + for(auto out : spent_outputs) { + ctd.m_subaddr_indices.insert(out.sender.min_i); + } + + m_confirmed_txs.insert(std::make_pair(tx_hash, ctd)); } if (0 != m_callback) { @@ -10447,11 +10722,11 @@ void wallet2::light_wallet_get_address_txs() // when sending a tx to same wallet the receiving amount has to be credited else { - if(confirmed_tx->second.m_amount_in != amount_sent || confirmed_tx->second.m_amount_out != amount_sent) + if(confirmed_tx->second.m_amount_in != amount_sent || confirmed_tx->second.m_amount_out != amount_out) { MDEBUG("Adjusting amount sent/received for tx: <" + t.hash + ">. Is tx sent to own wallet? " << print_money(amount_sent) << " != " << print_money(confirmed_tx->second.m_amount_in)); confirmed_tx->second.m_amount_in = amount_sent; - confirmed_tx->second.m_amount_out = amount_sent; + confirmed_tx->second.m_amount_out = amount_out; confirmed_tx->second.m_change = 0; } } @@ -10479,8 +10754,8 @@ bool wallet2::light_wallet_parse_rct_str(const std::string& rct_string, const cr rct::key encrypted_mask; std::string rct_commit_str = rct_string.substr(0,64); std::string encrypted_mask_str = rct_string.substr(64,64); - THROW_WALLET_EXCEPTION_IF(string_tools::validate_hex(64, rct_commit_str), error::wallet_internal_error, "Invalid rct commit hash: " + rct_commit_str); - THROW_WALLET_EXCEPTION_IF(string_tools::validate_hex(64, encrypted_mask_str), error::wallet_internal_error, "Invalid rct mask: " + encrypted_mask_str); + THROW_WALLET_EXCEPTION_IF(!string_tools::validate_hex(64, rct_commit_str), error::wallet_internal_error, "Invalid rct commit hash: " + rct_commit_str); + THROW_WALLET_EXCEPTION_IF(!string_tools::validate_hex(64, encrypted_mask_str), error::wallet_internal_error, "Invalid rct mask: " + encrypted_mask_str); string_tools::hex_to_pod(rct_commit_str, rct_commit); string_tools::hex_to_pod(encrypted_mask_str, encrypted_mask); if (decrypt) { @@ -10495,8 +10770,81 @@ bool wallet2::light_wallet_parse_rct_str(const std::string& rct_string, const cr return true; } -bool wallet2::light_wallet_key_image_is_ours(const crypto::key_image& key_image, const crypto::public_key& tx_public_key, uint64_t out_index) +// this function already assumes that we checked that the onetime address was addressed to `received_subaddr` +// the `ephem_pubkey` paramter can be a tx main pubkey or an 'additional pubkey' +// `tx_output_index` is the index of the enote in the local output set of the tx +// TODO not good for production, should be optimized +crypto::key_image generate_key_image_for_enote_simplified(const crypto::public_key &ephem_pubkey, + const size_t tx_output_index, + const cryptonote::subaddress_index &received_subaddr, + const cryptonote::account_keys &ack, + hw::device &hwdev) { + // notation: + // - R: ephem_pubkey + // - a: ack.m_view_secret_key [private viewkey] + // - b: ack.m_spend_secret_key [private spendkey] + // - idx: tx_output_index + // - index_major: received_subaddr.major + // - index_minor: received_subaddr.minor + // - Hs() [hash-to-scalar] + // - Hp() [hash-to-point] + + // 1. Diffie-Helman derived secret D = a R + crypto::key_derivation recv_derivation; + CHECK_AND_ASSERT_THROW_MES(hwdev.generate_key_derivation(ephem_pubkey, ack.m_view_secret_key, recv_derivation), + "Failed to perform Diffie-Helman exchange against tx ephem pubkey"); + + // 2. Non-address-extended onetime key secret u = Hs(D || idx) + b + crypto::secret_key onetime_privkey_unextended; + hwdev.derive_secret_key(recv_derivation, tx_output_index, ack.m_spend_secret_key, onetime_privkey_unextended); + + // 3. Subaddress key extension s = Hs(a || index_major || index_minor) if is subaddress, else s = 0 + const crypto::secret_key subaddr_ext{received_subaddr.is_zero() ? + crypto::secret_key{} : hwdev.get_subaddress_secret_key(ack.m_view_secret_key, received_subaddr)}; + + // 4. Onetime address private key x = u + s + crypto::secret_key onetime_privkey; + hwdev.sc_secret_add(onetime_privkey, onetime_privkey_unextended, subaddr_ext); + + // 5. Onetime address K = x G + crypto::public_key onetime_pubkey; + CHECK_AND_ASSERT_THROW_MES(hwdev.secret_key_to_public_key(onetime_privkey, onetime_pubkey), + "Failed to make public key"); + + // 6. Key image I = x Hp(K) + crypto::key_image ki; + hwdev.generate_key_image(onetime_pubkey, onetime_privkey, ki); + + return ki; +} + +bool key_image_is_ours(const crypto::key_image &ki, + const crypto::public_key &ephem_pubkey, + const size_t tx_output_index, + const cryptonote::subaddress_index &received_subaddr, + const cryptonote::account_keys &ack, + hw::device &hwdev) +{ + try + { + return ki == generate_key_image_for_enote_simplified(ephem_pubkey, + tx_output_index, + received_subaddr, + ack, + hwdev); + } + catch (...) { return false; } +} + + +bool wallet2::light_wallet_key_image_is_ours(const crypto::key_image& key_image, const crypto::public_key& tx_public_key, uint64_t out_index, const cryptonote::subaddress_index subaddress_index) +{ + bool result = key_image_is_ours(key_image, tx_public_key, out_index, subaddress_index, get_account().get_keys(), m_account.get_device()); + + return result; + + /* // Lookup key image from cache serializable_map index_keyimage_map; serializable_unordered_map >::const_iterator found_pub_key = m_key_image_cache.find(tx_public_key); @@ -10504,14 +10852,19 @@ bool wallet2::light_wallet_key_image_is_ours(const crypto::key_image& key_image, // pub key found. key image for index cached? index_keyimage_map = found_pub_key->second; std::map::const_iterator index_found = index_keyimage_map.find(out_index); - if(index_found != index_keyimage_map.end()) + if(index_found != index_keyimage_map.end()) { return key_image == index_found->second; + } + } + + if (m_watch_only) { + return false; } // Not in cache - calculate key image crypto::key_image calculated_key_image; cryptonote::keypair in_ephemeral; - + // Subaddresses aren't supported in mymonero/openmonero yet. Roll out the original scheme: // compute D = a*R // compute P = Hs(D || i)*G + B @@ -10522,20 +10875,193 @@ bool wallet2::light_wallet_key_image_is_ours(const crypto::key_image& key_image, bool r = crypto::generate_key_derivation(tx_public_key, ack.m_view_secret_key, derivation); CHECK_AND_ASSERT_MES(r, false, "failed to generate_key_derivation(" << tx_public_key << ", " << crypto::secret_key_explicit_print_ref{ack.m_view_secret_key} << ")"); - r = crypto::derive_public_key(derivation, out_index, ack.m_account_address.m_spend_public_key, in_ephemeral.pub); + r = crypto::derive_public_key(derivation, out_index, get_account().get_keys().m_account_address.m_spend_public_key, in_ephemeral.pub); CHECK_AND_ASSERT_MES(r, false, "failed to derive_public_key (" << derivation << ", " << out_index << ", " << ack.m_account_address.m_spend_public_key << ")"); crypto::derive_secret_key(derivation, out_index, ack.m_spend_secret_key, in_ephemeral.sec); crypto::public_key out_pkey_test; r = crypto::secret_key_to_public_key(in_ephemeral.sec, out_pkey_test); - CHECK_AND_ASSERT_MES(r, false, "failed to secret_key_to_public_key(" << crypto::secret_key_explicit_print_ref{in_ephemeral.sec} << ")"); - CHECK_AND_ASSERT_MES(in_ephemeral.pub == out_pkey_test, false, "derived secret key doesn't match derived public key"); - + CHECK_AND_ASSERT_MES(r, false, "failed to secret_key_to_public_key(" << in_ephemeral.sec << ")"); + CHECK_AND_ASSERT_MES(in_ephemeral.pub == out_pkey_test, false, "derived secret key doesn't match derived public key"); crypto::generate_key_image(in_ephemeral.pub, in_ephemeral.sec, calculated_key_image); index_keyimage_map.emplace(out_index, calculated_key_image); m_key_image_cache.emplace(tx_public_key, index_keyimage_map); return key_image == calculated_key_image; + */ +} + +bool wallet2::generate_output_key_image(const std::string& out_public_key, const std::string& tx_public_key, uint64_t out_index, const cryptonote::subaddress_index subaddr, crypto::key_image& ki) const { + crypto::public_key _out_public_key; + crypto::public_key _tx_public_key; + string_tools::hex_to_pod(out_public_key, _out_public_key); + string_tools::hex_to_pod(tx_public_key, _tx_public_key); + + return generate_output_key_image(_out_public_key, _tx_public_key, out_index, subaddr, ki); +} + +bool wallet2::generate_output_key_image(const crypto::public_key& out_public_key, const crypto::public_key& tx_public_key, uint64_t out_index, const cryptonote::subaddress_index subaddr, crypto::key_image& ki) const { + cryptonote::keypair in_ephemeral; + std::vector additional_tx_pub_keys; + + //if (generate_key_image_helper(m_account.get_keys(), m_subaddresses, out_public_key, tx_public_key, additional_tx_pub_keys, out_index, in_ephemeral, ki, m_account.get_device())) { + // return true; + //} + ki = generate_key_image_for_enote_simplified(tx_public_key, out_index, subaddr, get_account().get_keys(), m_account.get_device()); + + return true; +} + +bool wallet2::light_wallet_is_output_spent(const std::string& out_public_key, const std::string& tx_public_key, uint64_t out_index) { + crypto::public_key _out_public_key; + crypto::public_key _tx_public_key; + string_tools::hex_to_pod(out_public_key, _out_public_key); + string_tools::hex_to_pod(tx_public_key, _tx_public_key); + + return light_wallet_is_output_spent(_out_public_key, _tx_public_key, out_index); +}; + +bool wallet2::light_wallet_is_output_spent(const crypto::public_key& out_public_key, const crypto::public_key& tx_public_key, uint64_t out_index) { + crypto::key_image ki; + cryptonote::keypair in_ephemeral; + std::vector additional_tx_pub_keys; + bool spent = false; + + if (generate_key_image_helper(m_account.get_keys(), m_subaddresses, out_public_key, tx_public_key, additional_tx_pub_keys, out_index, in_ephemeral, ki, m_account.get_device())) { + spent = light_wallet_is_key_image_spent(ki); + } + + return spent; +}; + +std::vector wallet2::light_wallet_is_key_image_spent(const std::vector& key_images) { + std::vector spent_list; + + for(crypto::key_image key_image : key_images) { + bool spent = false; + + for(std::string spent_key_image : m_spent_key_images) { + if (string_tools::pod_to_hex(key_image) == spent_key_image) { + spent = true; + break; + } + } + + spent_list.push_back(spent); + } + + return spent_list; +} + +void wallet2::light_wallet_get_subaddrs() { + MDEBUG("Getting light wallet subaddresses"); + + tools::COMMAND_RPC_GET_SUBADDRS::request oreq; + tools::COMMAND_RPC_GET_SUBADDRS::response ores; + + oreq.address = get_account().get_public_address_str(m_nettype); + oreq.view_key = string_tools::pod_to_hex(unwrap(unwrap(get_account().get_keys().m_view_secret_key))); + + m_daemon_rpc_mutex.lock(); + bool r = invoke_http_json("/get_subaddrs", oreq, ores, rpc_timeout, "POST"); + m_daemon_rpc_mutex.unlock(); + THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "get_subaddrs"); + + m_light_wallet_subaddrs.clear(); + m_light_wallet_accounts.clear(); + + for(auto subaddr : ores.all_subaddrs) { + for(auto index_range : subaddr.value) { + THROW_WALLET_EXCEPTION_IF(index_range.size() != 2, error::wallet_internal_error, "Invalid index range size"); + + uint32_t minor = index_range[0]; + uint32_t major = index_range[1]; + + for(;minor <= major; minor++) { + m_light_wallet_subaddrs.push_back({subaddr.key,minor}); + } + } + + m_light_wallet_accounts.push_back(subaddr.key); + } +} + +bool wallet2::light_wallet_upsert_subaddrs(std::vector subaddrs) { + tools::COMMAND_RPC_UPSERT_SUBADDRS::request oreq; + tools::COMMAND_RPC_UPSERT_SUBADDRS::response ores; + std::vector all_subaddrs; + oreq.address = get_account().get_public_address_str(m_nettype); + oreq.view_key = string_tools::pod_to_hex(unwrap(unwrap(get_account().get_keys().m_view_secret_key))); + oreq.get_all = true; + + for (cryptonote::subaddress_index subaddr : subaddrs) { + oreq.subaddrs.key = subaddr.major; + tools::COMMAND_RPC_UPSERT_SUBADDRS::index_range index_range; + index_range.push_back(0); + index_range.push_back(subaddr.minor); + oreq.subaddrs.value.push_back(index_range); + + m_daemon_rpc_mutex.lock(); + bool r = invoke_http_json("/upsert_subaddrs", oreq, ores, rpc_timeout, "POST"); + m_daemon_rpc_mutex.unlock(); + THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "upsert_subaddrs"); + + all_subaddrs = ores.all_subaddrs; + } + + m_light_wallet_subaddrs.clear(); + m_light_wallet_accounts.clear(); + + for(auto subaddr : ores.all_subaddrs) { + for(auto index_range : subaddr.value) { + THROW_WALLET_EXCEPTION_IF(index_range.size() != 2, error::wallet_internal_error, "Invalid index range size"); + + uint32_t minor = index_range[0]; + uint32_t major = index_range[1]; + + for(;minor <= major; minor++) { + m_light_wallet_subaddrs.push_back({subaddr.key,minor}); + } + } + + m_light_wallet_accounts.push_back(subaddr.key); + } + + return true; +} + +bool wallet2::light_wallet_provision_subaddrs(uint32_t maj_i, uint32_t min_i, uint32_t n_maj, uint32_t n_min) { + tools::COMMAND_RPC_PROVISION_SUBADDRS::request oreq; + tools::COMMAND_RPC_PROVISION_SUBADDRS::response ores; + std::vector all_subaddrs; + oreq.address = get_account().get_public_address_str(m_nettype); + oreq.view_key = string_tools::pod_to_hex(unwrap(unwrap(get_account().get_keys().m_view_secret_key))); + oreq.get_all = true; + + m_daemon_rpc_mutex.lock(); + bool r = invoke_http_json("/provision_subaddrs", oreq, ores, rpc_timeout, "POST"); + m_daemon_rpc_mutex.unlock(); + THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "provision_subaddrs"); + + m_light_wallet_subaddrs.clear(); + m_light_wallet_accounts.clear(); + + for(auto subaddr : ores.all_subaddrs) { + for(auto index_range : subaddr.value) { + THROW_WALLET_EXCEPTION_IF(index_range.size() != 2, error::wallet_internal_error, "Invalid index range size"); + + uint32_t minor = index_range[0]; + uint32_t major = index_range[1]; + + for(;minor <= major; minor++) { + m_light_wallet_subaddrs.push_back({subaddr.key,minor}); + } + } + + m_light_wallet_accounts.push_back(subaddr.key); + } + + return true; } // Another implementation of transaction creation that is hopefully better @@ -11719,7 +12245,7 @@ void wallet2::device_show_address(uint32_t account_index, uint32_t address_index //---------------------------------------------------------------------------------------------------- uint8_t wallet2::get_current_hard_fork() { - if (m_offline) + if (m_offline || m_light_wallet) return 0; cryptonote::COMMAND_RPC_HARD_FORK_INFO::request req_t = AUTO_VAL_INIT(req_t); @@ -13493,7 +14019,6 @@ std::pair> while (offset < m_transfers.size() && !m_transfers[offset].m_key_image_request) ++offset; } - ski.reserve(m_transfers.size() - offset); for (size_t n = offset; n < m_transfers.size(); ++n) { @@ -13529,7 +14054,6 @@ std::pair> key_ptrs.push_back(&pkey); crypto::generate_ring_signature((const crypto::hash&)td.m_key_image, td.m_key_image, key_ptrs, in_ephemeral.sec, 0, &signature); - ski.push_back(std::make_pair(td.m_key_image, signature)); } return std::make_pair(offset, ski); @@ -13593,6 +14117,7 @@ uint64_t wallet2::import_key_images(const std::string &filename, uint64_t &spent uint64_t wallet2::import_key_images(const std::vector> &signed_key_images, size_t offset, uint64_t &spent, uint64_t &unspent, bool check_spent) { PERF_TIMER(import_key_images_lots); + std::vector spent_list; COMMAND_RPC_IS_KEY_IMAGE_SPENT::request req = AUTO_VAL_INIT(req); COMMAND_RPC_IS_KEY_IMAGE_SPENT::response daemon_resp = AUTO_VAL_INIT(daemon_resp); @@ -13607,8 +14132,14 @@ uint64_t wallet2::import_key_images(const std::vector s_key_images; PERF_TIMER_START(import_key_images_A); for (size_t n = 0; n < signed_key_images.size(); ++n) { @@ -13633,6 +14164,7 @@ uint64_t wallet2::import_key_images(const std::vector index_keyimage_map; + serializable_unordered_map >::const_iterator found_pub_key = m_key_image_cache.find(m_transfers[n + offset].get_public_key()); + if (found_pub_key == m_key_image_cache.end()) + { + // pub key not found. + index_keyimage_map.emplace(n + offset, signed_key_images[n].first); + m_key_image_cache.emplace(m_transfers[n + offset].get_public_key(), index_keyimage_map); + } else + { + // pub key found. key image for index cached? + index_keyimage_map = found_pub_key->second; + std::map::iterator index_found = index_keyimage_map.find(n + offset); + if(index_found != index_keyimage_map.end() && signed_key_images[n].first != index_found->second) + { + // update key image + index_found->second = signed_key_images[n].first; + } + else if (index_found == index_keyimage_map.end()) { + index_keyimage_map.emplace(n + offset, signed_key_images[n].first); + m_key_image_cache.emplace(m_transfers[n + offset].get_public_key(), index_keyimage_map); + } + } + } PERF_TIMER_STOP(import_key_images_B); if(check_spent) { - PERF_TIMER(import_key_images_RPC); - { - const boost::lock_guard lock{m_daemon_rpc_mutex}; - uint64_t pre_call_credits = m_rpc_payment_state.credits; - req.client = get_client_signature(); - bool r = epee::net_utils::invoke_http_json("/is_key_image_spent", req, daemon_resp, *m_http_client, rpc_timeout); - THROW_ON_RPC_RESPONSE_ERROR_GENERIC(r, {}, daemon_resp, "is_key_image_spent"); - THROW_WALLET_EXCEPTION_IF(daemon_resp.spent_status.size() != signed_key_images.size(), error::wallet_internal_error, - "daemon returned wrong response for is_key_image_spent, wrong amounts count = " + - std::to_string(daemon_resp.spent_status.size()) + ", expected " + std::to_string(signed_key_images.size())); - check_rpc_cost("/is_key_image_spent", daemon_resp.credits, pre_call_credits, daemon_resp.spent_status.size() * COST_PER_KEY_IMAGE); - } + if (m_light_wallet) { + spent_list = light_wallet_is_key_image_spent(s_key_images); - for (size_t n = 0; n < daemon_resp.spent_status.size(); ++n) - { - transfer_details &td = m_transfers[n + offset]; - td.m_spent = daemon_resp.spent_status[n] != COMMAND_RPC_IS_KEY_IMAGE_SPENT::UNSPENT; + THROW_WALLET_EXCEPTION_IF(spent_list.size() != signed_key_images.size(), error::wallet_internal_error, + "daemon returned wrong response for is_key_image_spent, wrong amounts count = " + + std::to_string(spent_list.size()) + ", expected " + std::to_string(signed_key_images.size())); + + for (size_t n = 0; n < spent_list.size(); ++n) + { + transfer_details &td = m_transfers[n + offset]; + td.m_spent = spent_list[n]; + } + } else { + PERF_TIMER(import_key_images_RPC); + { + const boost::lock_guard lock{m_daemon_rpc_mutex}; + uint64_t pre_call_credits = m_rpc_payment_state.credits; + req.client = get_client_signature(); + bool r = epee::net_utils::invoke_http_json("/is_key_image_spent", req, daemon_resp, *m_http_client, rpc_timeout); + THROW_ON_RPC_RESPONSE_ERROR_GENERIC(r, {}, daemon_resp, "is_key_image_spent"); + THROW_WALLET_EXCEPTION_IF(daemon_resp.spent_status.size() != signed_key_images.size(), error::wallet_internal_error, + "daemon returned wrong response for is_key_image_spent, wrong amounts count = " + + std::to_string(daemon_resp.spent_status.size()) + ", expected " + std::to_string(signed_key_images.size())); + check_rpc_cost("/is_key_image_spent", daemon_resp.credits, pre_call_credits, daemon_resp.spent_status.size() * COST_PER_KEY_IMAGE); + } + + for (size_t n = 0; n < daemon_resp.spent_status.size(); ++n) + { + transfer_details &td = m_transfers[n + offset]; + td.m_spent = daemon_resp.spent_status[n] != COMMAND_RPC_IS_KEY_IMAGE_SPENT::UNSPENT; + } } } spent = 0; @@ -13713,20 +14284,32 @@ uint64_t wallet2::import_key_images(const std::vector::const_iterator skii = spent_key_images.find(td.m_key_image); - if (skii == spent_key_images.end()) - swept_transfers.push_back(i); - else - spent_txids.insert(skii->second); + if (m_light_wallet) { + if (i < spent_list.size() && spent_list[i]) + { + const std::unordered_map::const_iterator skii = spent_key_images.find(td.m_key_image); + if (skii == spent_key_images.end()) + swept_transfers.push_back(i); + else + spent_txids.insert(skii->second); + } + } + else { + if (i < daemon_resp.spent_status.size() && daemon_resp.spent_status[i] == COMMAND_RPC_IS_KEY_IMAGE_SPENT::SPENT_IN_BLOCKCHAIN) + { + const std::unordered_map::const_iterator skii = spent_key_images.find(td.m_key_image); + if (skii == spent_key_images.end()) + swept_transfers.push_back(i); + else + spent_txids.insert(skii->second); + } } } PERF_TIMER_STOP(import_key_images_D); MDEBUG("Total: " << print_money(spent) << " spent, " << print_money(unspent) << " unspent"); - if (check_spent) + if (check_spent && !m_light_wallet) { // query outgoing txes COMMAND_RPC_GET_TRANSACTIONS::request gettxs_req; @@ -15131,6 +15714,13 @@ std::string wallet2::get_rpc_status(const std::string &s) const return ""; } //---------------------------------------------------------------------------------------------------- +std::string wallet2::get_rpc_status(const bool &s) const +{ + if (s) + return CORE_RPC_STATUS_OK; + return ""; +} +//---------------------------------------------------------------------------------------------------- void wallet2::throw_on_rpc_response_error(bool r, const epee::json_rpc::error &error, const std::string &status, const char *method) const { THROW_WALLET_EXCEPTION_IF(error.code, tools::error::wallet_coded_rpc_error, method, error.code, get_rpc_server_error_message(error.code)); diff --git a/src/wallet/wallet2.h b/src/wallet/wallet2.h index 24366f630..cc61d7081 100644 --- a/src/wallet/wallet2.h +++ b/src/wallet/wallet2.h @@ -91,6 +91,14 @@ class wallet_accessor_test; namespace tools { + enum light_wallet_server_type : uint8_t + { + STANDARD = 0, + OPENMONERO, + MYMONERO, + UNDEFINED = 255 + }; + class ringdb; class wallet2; class Notify; @@ -1023,6 +1031,7 @@ private: */ bool light_wallet() const { return m_light_wallet; } void set_light_wallet(bool light_wallet) { m_light_wallet = light_wallet; } + void set_light_wallet_server_type(light_wallet_server_type server_type){ m_light_wallet_server_type = server_type; }; uint64_t get_light_wallet_scanned_block_height() const { return m_light_wallet_scanned_block_height; } uint64_t get_light_wallet_blockchain_height() const { return m_light_wallet_blockchain_height; } @@ -1611,7 +1620,39 @@ private: // Parse rct string bool light_wallet_parse_rct_str(const std::string& rct_string, const crypto::public_key& tx_pub_key, uint64_t internal_output_index, rct::key& decrypted_mask, rct::key& rct_commit, bool decrypt) const; // check if key image is ours - bool light_wallet_key_image_is_ours(const crypto::key_image& key_image, const crypto::public_key& tx_public_key, uint64_t out_index); + bool light_wallet_key_image_is_ours(const crypto::key_image& key_image, const crypto::public_key& tx_public_key, uint64_t out_index, const cryptonote::subaddress_index subaddress_index); + + bool light_wallet_is_output_spent(const std::string& out_public_key, const std::string& tx_public_key, uint64_t out_index); + + bool light_wallet_is_output_spent(const crypto::public_key& out_public_key, const crypto::public_key& tx_public_key, uint64_t out_index); + + bool light_wallet_is_key_image_spent(const crypto::key_image& key_image) { + std::vector key_images; + + key_images.push_back(key_image); + std::vector spent_list = light_wallet_is_key_image_spent(key_images); + + if (spent_list.empty()) return false; + + return spent_list[0]; + } + + bool m_light_wallet_is_logged_in; + boost::optional m_unspent_outputs_response; + boost::optional m_address_txs_response; + void light_wallet_get_tx_unspent_outs(const std::string tx_hash, std::vector &outputs) const; + void light_wallet_refresh(uint64_t & blocks_fectched, bool& received_money); + + std::vector light_wallet_is_key_image_spent(const std::vector& key_images); + std::vector m_light_wallet_subaddrs; + std::vector m_light_wallet_accounts; + std::list m_light_wallet_unspent_outputs; + bool light_wallet_supports_subaddrs(); + void light_wallet_get_subaddrs(); + bool light_wallet_provision_subaddrs(uint32_t maj_i, uint32_t min_i, uint32_t n_maj, uint32_t n_min); + bool light_wallet_upsert_subaddrs(std::vector subaddrs); + bool generate_output_key_image(const std::string& out_public_key, const std::string& tx_public_key, uint64_t out_index, const cryptonote::subaddress_index subaddress, crypto::key_image& ki) const; + bool generate_output_key_image(const crypto::public_key& out_public_key, const crypto::public_key& tx_public_key, uint64_t out_index, const cryptonote::subaddress_index subaddress, crypto::key_image& ki) const; /* * "attributes" are a mechanism to store an arbitrary number of string values @@ -1823,6 +1864,7 @@ private: void on_device_progress(const hw::device_progress& event); std::string get_rpc_status(const std::string &s) const; + std::string get_rpc_status(const bool &s) const; void throw_on_rpc_response_error(bool r, const epee::json_rpc::error &error, const std::string &status, const char *method) const; std::string get_client_signature() const; @@ -1849,6 +1891,7 @@ private: transfer_container m_transfers; payment_container m_payments; serializable_unordered_map m_key_images; + std::set m_spent_key_images; serializable_unordered_map m_pub_keys; cryptonote::account_public_address m_account_public_address; serializable_unordered_map m_subaddresses; @@ -1940,6 +1983,7 @@ private: // Light wallet bool m_light_wallet; /* sends view key to daemon for scanning */ + light_wallet_server_type m_light_wallet_server_type = STANDARD; uint64_t m_light_wallet_scanned_block_height; uint64_t m_light_wallet_blockchain_height; uint64_t m_light_wallet_per_kb_fee = FEE_PER_KB; @@ -1949,6 +1993,7 @@ private: // Light wallet info needed to populate m_payment requires 2 separate api calls (get_address_txs and get_unspent_outs) // We save the info from the first call in m_light_wallet_address_txs for easier lookup. std::unordered_map m_light_wallet_address_txs; + std::vector m_light_wallet_processed_out_public_keys; // store calculated key image for faster lookup serializable_unordered_map > m_key_image_cache; diff --git a/src/wallet/wallet_light_rpc.h b/src/wallet/wallet_light_rpc.h index 743a147f6..60e8b57da 100644 --- a/src/wallet/wallet_light_rpc.h +++ b/src/wallet/wallet_light_rpc.h @@ -49,13 +49,28 @@ namespace tools }; typedef epee::misc_utils::struct_init request; + struct address_meta { + uint32_t maj_i; + uint32_t min_i; + + BEGIN_SERIALIZE_OBJECT() + FIELD(maj_i) + FIELD(min_i) + END_SERIALIZE() + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(maj_i) + KV_SERIALIZE(min_i) + END_KV_SERIALIZE_MAP() + }; + struct spent_output { uint64_t amount; std::string key_image; std::string tx_pub_key; uint64_t out_index; uint32_t mixin; - + address_meta sender; BEGIN_KV_SERIALIZE_MAP() KV_SERIALIZE(amount) @@ -63,6 +78,7 @@ namespace tools KV_SERIALIZE(tx_pub_key) KV_SERIALIZE(out_index) KV_SERIALIZE(mixin) + KV_SERIALIZE(sender) END_KV_SERIALIZE_MAP() }; @@ -75,11 +91,13 @@ namespace tools uint64_t total_sent; uint64_t unlock_time; uint64_t height; + uint64_t fee; std::list spent_outputs; std::string payment_id; bool coinbase; bool mempool; uint32_t mixin; + address_meta recipient; BEGIN_KV_SERIALIZE_MAP() KV_SERIALIZE(id) @@ -89,11 +107,13 @@ namespace tools KV_SERIALIZE(total_sent) KV_SERIALIZE(unlock_time) KV_SERIALIZE(height) + KV_SERIALIZE_OPT(fee, (uint64_t)0) KV_SERIALIZE(spent_outputs) KV_SERIALIZE(payment_id) KV_SERIALIZE(coinbase) KV_SERIALIZE(mempool) KV_SERIALIZE(mixin) + KV_SERIALIZE(recipient) END_KV_SERIALIZE_MAP() }; @@ -136,6 +156,21 @@ namespace tools }; typedef epee::misc_utils::struct_init request; + struct address_meta { + uint32_t maj_i; + uint32_t min_i; + + BEGIN_SERIALIZE_OBJECT() + FIELD(maj_i) + FIELD(min_i) + END_SERIALIZE() + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(maj_i) + KV_SERIALIZE(min_i) + END_KV_SERIALIZE_MAP() + }; + struct spent_output { uint64_t amount; @@ -143,6 +178,7 @@ namespace tools std::string tx_pub_key; uint64_t out_index; uint32_t mixin; + address_meta sender; BEGIN_KV_SERIALIZE_MAP() KV_SERIALIZE(amount) @@ -150,6 +186,7 @@ namespace tools KV_SERIALIZE(tx_pub_key) KV_SERIALIZE(out_index) KV_SERIALIZE(mixin) + KV_SERIALIZE(sender) END_KV_SERIALIZE_MAP() }; @@ -203,6 +240,20 @@ namespace tools }; typedef epee::misc_utils::struct_init request; + struct address_meta { + uint32_t maj_i; + uint32_t min_i; + + BEGIN_SERIALIZE_OBJECT() + FIELD(maj_i) + FIELD(min_i) + END_SERIALIZE() + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(maj_i) + KV_SERIALIZE(min_i) + END_KV_SERIALIZE_MAP() + }; struct output { uint64_t amount; @@ -216,7 +267,7 @@ namespace tools std::vector spend_key_images; uint64_t timestamp; uint64_t height; - + address_meta recipient; BEGIN_KV_SERIALIZE_MAP() KV_SERIALIZE(amount) @@ -230,6 +281,7 @@ namespace tools KV_SERIALIZE(spend_key_images) KV_SERIALIZE(timestamp) KV_SERIALIZE(height) + KV_SERIALIZE(recipient) END_KV_SERIALIZE_MAP() }; @@ -364,4 +416,148 @@ namespace tools typedef epee::misc_utils::struct_init response; }; //----------------------------------------------- + struct COMMAND_RPC_PROVISION_SUBADDRS + { + class index_range : public std::vector { + BEGIN_KV_SERIALIZE_MAP() + + END_KV_SERIALIZE_MAP() + }; + + struct subaddrs_t { + uint32_t key; + std::vector value; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(key) + KV_SERIALIZE(value) + END_KV_SERIALIZE_MAP() + }; + typedef epee::misc_utils::struct_init subaddrs; + + struct request_t + { + std::string address; + std::string view_key; + uint32_t maj_i; + uint32_t min_i; + uint32_t n_maj; + uint32_t n_min; + bool get_all; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(address) + KV_SERIALIZE(view_key) + KV_SERIALIZE(maj_i) + KV_SERIALIZE(min_i) + KV_SERIALIZE(n_maj) + KV_SERIALIZE(n_min) + KV_SERIALIZE(get_all) + END_KV_SERIALIZE_MAP() + }; + typedef epee::misc_utils::struct_init request; + + struct response_t + { + std::vector new_subaddrs; + std::vector all_subaddrs; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(new_subaddrs) + KV_SERIALIZE(all_subaddrs) + END_KV_SERIALIZE_MAP() + }; + typedef epee::misc_utils::struct_init response; + }; + //----------------------------------------------- + struct COMMAND_RPC_UPSERT_SUBADDRS + { + class index_range : public std::vector { + BEGIN_KV_SERIALIZE_MAP() + + END_KV_SERIALIZE_MAP() + }; + + struct subaddrs_obj_t { + uint32_t key; + std::vector value; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(key) + KV_SERIALIZE(value) + END_KV_SERIALIZE_MAP() + }; + typedef epee::misc_utils::struct_init subaddrs_obj; + + struct request_t + { + std::string address; + std::string view_key; + subaddrs_obj subaddrs; + bool get_all; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(address) + KV_SERIALIZE(view_key) + KV_SERIALIZE(subaddrs) + KV_SERIALIZE(get_all) + END_KV_SERIALIZE_MAP() + }; + typedef epee::misc_utils::struct_init request; + + struct response_t + { + std::vector new_subaddrs; + std::vector all_subaddrs; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(new_subaddrs) + KV_SERIALIZE(all_subaddrs) + END_KV_SERIALIZE_MAP() + }; + typedef epee::misc_utils::struct_init response; + }; + //----------------------------------------------- + struct COMMAND_RPC_GET_SUBADDRS + { + struct request_t + { + std::string address; + std::string view_key; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(address) + KV_SERIALIZE(view_key) + END_KV_SERIALIZE_MAP() + }; + typedef epee::misc_utils::struct_init request; + + class index_range : public std::vector { + BEGIN_KV_SERIALIZE_MAP() + + END_KV_SERIALIZE_MAP() + }; + + struct subaddrs_t { + uint32_t key; + std::vector value; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(key) + KV_SERIALIZE(value) + END_KV_SERIALIZE_MAP() + }; + typedef epee::misc_utils::struct_init subaddrs; + + struct response_t + { + std::vector all_subaddrs; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(all_subaddrs) + END_KV_SERIALIZE_MAP() + }; + typedef epee::misc_utils::struct_init response; + }; + //----------------------------------------------- }